Add a sort selector next to the type filter with four modes: Newest first
(default), Oldest first, Largest amount, Smallest amount. The mode folds into
the merged-list memoization cache key (so the list re-sorts only when the mode
changes) and the comparator branches on it, keeping txid as a deterministic
tiebreak. Changing the sort resets to page 1.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The Sent summary card showed 0 while selecting the Sent filter listed
transactions. The card counted only plain "send" rows and deliberately excluded
both legs of an autoshield pair as an "internal move", but the list shows the
merged "shield" row under the Sent filter. With only shielding transactions and
no plain sends, the card read 0 against a non-empty Sent list.
Count each shield pair toward the Sent card (with the shielded receive-leg
amount, which is what the merged row displays), so the card and the filter agree.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
History looked unsorted because every merged "shield" row carried
confirmations=0, and the list sorts 0-conf (pending) transactions to the very
top. So long-confirmed shielding transactions floated above newer ones — and
when the type filter was switched off "All" they vanished (shield rows only
match the "Sent" filter), which read as "transactions disappear when sorting".
Root cause: the autoshield merge set the row's confirmations to
min(send, recv). Both legs are the SAME transaction (one real confirmation
count), but the send leg (parsed from z_viewtransaction) routinely arrives with
confirmations=0, so min() picked 0. Use max() to take the populated value.
Also give the sort a txid tiebreak so same-block transactions keep a stable
order instead of reshuffling every time a new block bumps confirmations.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Three layout fixes in the export-key modal, all symptoms of widths/heights
authored as raw pixels while text scales with the user's font setting:
- "Copy to Clipboard" no longer clips — the Show/Hide · Copy · QR buttons are
auto-width (size 0) so they always fit their label;
- those buttons now share one font, so Show/Hide matches Copy (was a smaller
toggle-button font);
- the read-only address and key fields are sized to the wrapped text instead
of a fixed 60/80px, removing the empty space below their value.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The overlay dialog's content child is AutoResizeY, but the glass card behind
it was drawn to a fixed viewport ratio — leaving a tall band of empty glass
below short dialogs (e.g. the key-export modal had a gap under its Close
button). Measure the rendered card height each frame and reuse it next frame
to draw the glass to the content; fall back to (and stay capped at) the ratio
so tall dialogs are unchanged and can't run off-screen.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
From a screenshot at a non-default font scale: the red WARNING box clipped its text
("...balance, but" cut off) because it used a fixed 80px child height while the text inside
scales with the font. Make the warning box auto-size to its content (ImGuiChildFlags_AutoResizeY)
so it never clips at any scale, and scale the address / key read-only field heights by
Layout::dpiScale() for the same reason. Complements the card-width scaling fix.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Every BeginOverlayDialog is passed a raw pixel card width (550, 620, …), but the fonts and
spacing inside scale with Layout::dpiScale() — which includes the user's font-size setting. At
any non-default scale the content outgrew the fixed card, so text overflowed the card edge and
elements misaligned. Scale the card width by dpiScale() (no-op at the default 1.0 scale) and clamp
it to the viewport so a large scale can't push it off-screen. Fixes all overlay dialogs at once.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Auto-clear: the Copy button now routes through App::copySecretToClipboard, so a copied
private/viewing key is wiped from the clipboard after ~45s (same protection as the seed) with
a "auto-clears" notice — instead of the raw SetClipboardText that left it indefinitely.
- QR: once the key is revealed, a Show/Hide QR toggle renders the key's QR inline (via the same
GenerateQRTexture/RenderQRCode widget the Receive tab uses) for scanning into another wallet.
The QR texture is cached, regenerated on key change, and freed on hide/close/dismiss; hiding the
key also hides its QR.
- Actions row tightened to Show/Hide · Copy · QR, and the key + QR texture are now cleared on any
dismissal (Close button, scrim click, Esc), not just the Close button.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The address list supported two drag gestures that collided: dragging a row onto another
transferred funds, dragging into a gap reordered. Since rows are contiguous, a reorder-drag was
almost always over another row, so it triggered a fund transfer instead of reordering.
Disambiguate by WHERE on the target row the drag is released (user's suggestion): the top/bottom
~30% edge bands = reorder (an insertion line is shown), the centre = transfer (the row highlights).
A zero-balance row or an off-row drop always reorders. Tooltip and i18n hint updated to match.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
First slice of decomposing balance_tab.cpp (3449 lines). The five rendering helpers used by every
balance layout — UpdateBalanceLerp, RenderCompactHero, RenderSharedAddressList (599 lines, the
drag-reorderable address list), RenderSharedRecentTx, RenderSyncBar — are moved verbatim into
balance_components.cpp. balance_tab.cpp is now 2680 lines.
Clean extraction: the helpers' interactive statics (drag/copy/hide/show) are function-local and
move WITH them; the only file-scope state they share is the balance-lerp animation values
(s_dispTotal/Shielded/Transparent/Unconfirmed) and s_generating_z_address, now non-static and
declared `extern` in balance_components.h (defined once in balance_tab.cpp, so both TUs share the
same objects). RenderCompactHero's default arg moved to the header declaration. The layouts (still
in balance_tab.cpp) call the helpers via the new header.
Verified: full-node + Windows + lite build (links cleanly -> extern state resolves), tests,
hygiene. This touches every layout's address list / recent-tx / hero / sync bar, so needs a
hands-on pass across the balance layouts before the next slice.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Final slice of decomposing mining_tab.cpp. The ~529-line "Mode toggle" section (SOLO | POOL
segmented control + pool URL/worker inputs) is moved verbatim into RenderMiningModeToggle().
mining_tab.cpp is now 311 lines (was 2628) — just the tab dispatch, thread-sync glue, benchmark
advance, section-budget setup, and four card calls.
State the toggle mutates is passed BY REFERENCE so behaviour is identical: the pool-mode flag,
the settings-dirty flag, and the pool URL / worker char[256] buffers (the text inputs write into
them) — passed as char(&)[256] references and named with their original identifiers so the body
stays byte-identical.
Verified: full-node + Windows + lite build, tests, hygiene. Audit #10 complete: the 2628-line
monolith is now five focused files (earnings, stats, controls, mode-toggle + the 311-line shell).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
BeginOverlayDialog dismisses on a click outside the card via IsMouseClicked (mouse-down). When
the dialog is opened by a button that fires on the same frame (e.g. the mining tab's
"Update miner…" button), that opening click is still registered as an outside-click, so the
dialog opens and instantly closes — it just "flashes". Skip the outside-click dismissal on the
frame the scrim window first appears (ImGui::IsWindowAppearing()); normal outside-click closing
is unaffected on every subsequent frame. Fixes all overlay dialogs, not just the xmrig updater.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Third and largest slice of decomposing mining_tab.cpp. The ~843-line "Controls" card (CPU-core
grid + drag-to-select, mining start/stop button, benchmark + miner-update controls) is moved
verbatim into RenderMiningControls(). mining_tab.cpp is now 839 lines (was 2628 originally).
The most coupled section, so mutated state is passed BY REFERENCE — the benchmark
(ThreadBenchmark&), selected thread count (int&), and drag state (bool&/int&) — with local
reference aliases so the body stays byte-identical and interactions (drag, benchmark, start/stop)
behave exactly as before. Read-only context is passed by value/const; the compiler verified
const-correctness. Local statics inside the block moved with it.
Verified: full-node + Windows + lite build, tests, hygiene, no startup crash.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Second slice of decomposing mining_tab.cpp. The ~313-line "Hashrate + Stats" card (stat values +
hashrate chart / live-log view) is moved verbatim into RenderMiningStats(); mining_tab.cpp is now
1680 lines (was 1992 after slice 1, 2628 originally). Body byte-identical apart from a s_pool_mode
alias; the chart/log toggle statics (s_show_pool_log/s_show_solo_log) moved with the card, and the
log buffer was already a function-local static. No App dependency in this section.
Verified: full-node + Windows + lite build, tests, hygiene, clean smoke start. Pending hands-on
visual check before the next slice.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
First incremental slice of decomposing the 2628-line mining_tab.cpp monolith (one giant
RenderMiningTabContent function). The ~636-line "Earnings" card section is moved verbatim into
RenderMiningEarnings(); mining_tab.cpp is now 1992 lines and calls it with the immediate-mode
layout context as parameters (draw list, fonts, scale/spacing, glass spec, pool-mode flag).
Behavior-preserving by construction: the body is byte-identical (the only additions are a
`const bool s_pool_mode = poolMode` alias and a local scratch `buf` so the moved code keeps its
original identifiers). The earnings-filter static moved with the card it belongs to. The
compiler surfaced every enclosing dependency, which became explicit parameters.
Verified: full-node + Windows + lite build, tests, hygiene, clean smoke start. Pending hands-on
visual check of the Earnings card before extracting the next section.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- rpc_client: wipe the plaintext "user:password" temporary with sodium_memzero after
base64-encoding it into the auth header (std::string doesn't zero its buffer on
destruction).
- connection: the auto-generated DRAGONX.conf holds rpcuser/rpcpassword in plaintext but
was written with the default umask (often world-readable 0644). Restrict it to owner
read/write after creation so another local user can't read the credentials.
- app: copying a seed phrase / private key to the clipboard now arms an auto-clear —
App::copySecretToClipboard() copies the secret and, after 45s, wipes the clipboard IF it
still holds that secret (compared via a stored hash, never the plaintext). Wired into the
lite first-run wizard's seed Copy and the Settings export-secret Copy, with a
"clipboard auto-clears in 45s" notice. pumpSecretClipboardClear() runs each frame.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Per-frame hot paths in the immediate-mode UI were allocating needlessly:
- Address filtering in the balance tab rebuilt a std::string filter per address AND
containsIgnoreCase() lower-cased two fresh copies per call — ~6×N allocations/frame
on large wallets. New util::containsIgnoreCase(string_view, string_view) is
allocation-free, and the filter is now built once outside the loop.
- Four duplicated "time ago" implementations (balance_tab_helpers, balance_recent_tx,
send_tab, transactions_tab) are consolidated into util::formatTimeAgo (localized long
form) + util::formatTimeAgoShort (compact "5s ago"), preserving each call site's exact
display style. Both use snprintf, no per-row string concatenation.
- The send-tab address-suggestion scan (a walk over the whole tx list) is memoized on the
typed text + tx count, so it no longer recomputes every frame while the user pauses.
New src/util/text_format.{h,cpp}; the two existing containsIgnoreCase/timeAgo definitions
now delegate to it. Added to both the app and test targets (test target also gains i18n.cpp,
which text_format's localized path needs).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The History tab rebuilt its entire display list on every render frame: indexing all
transactions by txid, merging autoshield send+receive pairs into "shield" rows, and
std::sort-ing the result — O(N log N) plus several heap allocations at ~60fps, only to
show one 50-row page. The data is already sorted newest-first by the refresh service,
so the per-frame sort was redundant on top.
Memoize the merged+sorted list, rebuilding only when the underlying transactions
actually change. The cache key is a cheap, allocation-free FNV-1a fingerprint over the
display-relevant fields (count, last update time, and each tx's confirmations /
timestamp / type+address first char) — a new block bumps every confirmation so the key
changes and we rebuild; otherwise (the common read/scroll case) the cache is reused.
Filtering, search, and pagination still run per-frame over the cached list (cheap linear
scans that depend on interactive state).
Also document that App::shouldRefreshTransactions() is block-height/dirty driven (not
interval-gated) — the Transactions timer only paces the check; the recent-poll handles
between-block mempool/unconfirmed deltas.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The lite backend's litelib_execute() is the same command interface as
silentdragonxlite-cli (balance, info, height, list, notes, addresses, sync,
syncstatus, new, send, shield, encrypt, …), so the lite Console can be a real
interactive console — like the full-node RPC console — instead of a read-only
diagnostics log.
Controller: add an async arbitrary-command runner mirroring the broadcast
pattern — runConsoleCommand() splits "<command> [args]" (the first token is the
command, the remainder is passed as the single arg string litelib_execute
expects, since it does NOT whitespace-split), runs the bridge call on a detached
thread that captures the shared bridge (never `this`), and delivers the result
to a main-thread slot drained by takeConsoleResult(). Results are NEVER routed
through LiteDiagnostics (seed/export can return secrets).
Console tab: a command input (Enter to run, Up/Down history via the shared
console_input_model helpers) over a unified scroll buffer that interleaves the
automatic diagnostics events with user command I/O, colour-coded, with the live
status header preserved. The input is disabled while a command runs.
Two backend footguns are intercepted at the UI layer before forwarding:
`clear` (the backend command WIPES wallet tx history — re-bound to clearing the
view, what the user expects) and `quit`/`exit` (would only save; the embedded
backend must stay running with the app).
Test: runConsoleCommand drives the fake backend (info -> raw response; "new zs"
-> exercises the command/arg split; blank line rejected).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The Settings page drove the controller's synchronous createWallet/openWallet/
restoreWallet, which blocks the UI thread on the (often flaky) lightwalletd and
gives up after the first server. Add a generic async lifecycle path that mirrors
the async-open failover but carries the full request (passphrase, restore seed/
birthday/account/overwrite):
- beginCreateWalletAsync / beginOpenWalletAsync / beginRestoreWalletAsync run
on a detached thread that builds its OWN local LiteWalletLifecycleService
from captured value copies + the shared bridge (never `this`, so it can
safely outlive the controller). Each request type's serverUrl override field
feeds the failover: try the preferred server, then every other usable
default; stop on the first ready wallet or a structural block; keep the
preferred server's error on total failure. The request's secrets are wiped
once the attempt finishes.
- pumpLifecycleResult() finalizes on the main thread (flip walletOpen, persist,
start sync) and caches the result for the UI; wired into App::update next to
pumpAsyncOpen(). beginAsyncLifecycle() now also yields to an in-flight
lifecycle request so the auto-open loop can't race it on the same bridge.
- settings_page kicks off the async op, disables the button while in flight,
and polls the cached result each frame for the status/summary.
Tests: testLiteWalletControllerAsyncLifecycleFailover covers async create (with
passphrase) and restore failing over preferred->fallback, plus all-servers-down.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Restore: live "N / 24 words" count, a one-line birthday explanation, and a guard
that rejects a restore unless all 24 words are entered (the secret scrubber still
wipes the input on the early return).
- Backup: "Show seed" now also shows the birthday (needed to restore quickly) with a
"back this up too" note, a stronger "only way to restore" warning, and a "Save to
file" button that writes the seed + birthday to an owner-only (0600) file in the
config dir via the atomic-write helper.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
A lite build compiled without the SDXL backend (DRAGONX_ENABLE_LITE_BACKEND off,
i.e. built with --lite instead of --lite-backend) leaves the controller null, so
the wallet never opens and the UI shows a silent "disconnected" state. The Console
status now states the cause and the fix directly ("Lite backend not linked in this
build (rebuild with --lite-backend)") instead of a vague "unavailable".
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The Console could look empty if the wallet produced few events. Make it useful
in every state and remove a cross-platform footgun:
- Add a live status header read straight from the controller (connected /
connecting / disconnected, sync %, and the last open error) — independent of the
diagnostics event log, so the Console always shows the current connection +
wallet-open state even when the log is sparse.
- Move LiteDiagnostics::instance() into a single .cpp so there is exactly one
instance across the binary, rather than relying on the linker folding an
inline-function static across translation units (a known fragility, especially
on mingw/Windows — the most likely cause of a stuck-empty event log there).
Verified the writer and reader share one instance on Linux; builds clean for
full-node, lite, and Windows cross-compile; tests pass.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The lite variant had no visibility into why a wallet failed to open — just a
"disconnected" spinner. Add a lite-only Console tab (full-node keeps its RPC
console) that shows a live diagnostic log.
- LiteDiagnostics: a small thread-safe, bounded ring buffer (header-only). The
controller writes to it from its background threads: each failover server
attempt and result, wallet open/create/restore outcomes, sync start, and
blocked-open reasons. The App logs controller (re)builds with the preferred
server.
- lite_console_tab: a terminal-styled, read-only view of the log (newest at the
bottom, error/success lines coloured) with Clear / Copy / Auto-scroll. Reachable
even when the wallet is locked (it's diagnostics, no secrets). Registered as
NavPage::LiteConsole, gated lite-only via WalletUiSurface::LiteConsole.
A unit test drives an open-with-failover and asserts the log records the
connection attempt and the successful open. Built clean for full-node, lite, and
Windows cross-compile.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The startup auto-open of an existing lite wallet discarded openWallet()'s result,
so when initialize_existing failed (e.g. the lightwalletd server is unreachable)
the UI just showed a "disconnected" spinner with no reason — and DEBUG_LOGF is
compiled out of release builds, so there was no way to see why. Capture the
failure: store the reason, show it in the Network tab status line (in place of
"no wallet open"), and raise a notification. Cleared once a wallet opens.
This doesn't change open behaviour — it makes a stuck open diagnosable.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The send screen labelled any prefix+length match as a "Valid" address, so a
mistyped address that still matched the pattern passed the gate. Add pure,
offline checksum validation — Base58Check (transparent R-addresses) and Bech32
(Sapling zs-addresses) — and require it in the validity check. Both verifiers are
version-byte/HRP agnostic (the HRP is taken from the string, the Base58 checksum
is chain-independent), so a correct implementation never rejects a genuine
address while catching transcription errors. Works for both build variants
(no daemon round-trip), unit-tested against standard BIP173 / Base58Check vectors.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- importKey routed transparent vs. shielded purely by the first character, which
can mis-route (e.g. testnet/regtest WIFs). On failure, try the other import
command before reporting an error (each validates the encoding, so a wrong
command rejects rather than mis-imports). The key copy is wiped after both tries.
- Clamp the shield dialog's fee input to [0, 1] DRGX, mirroring the UTXO-limit
clamp, so a negative or fat-fingered huge fee can't be submitted.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Balance card hashrate now uses the shared FormatHashrate() (TH/GH/MH/KH/H)
instead of a bespoke two-tier KH/s formatter.
- Recent-tx rows show the full untruncated address on hover — two z-addresses can
truncate to the same first/last window — and the truncate helpers guard maxLen<=3.
- Remove the unused viewTop/viewBot "viewport culling" locals in the tx list
(pagination already bounds per-frame work).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The transaction-status overlay decided error vs. success styling by searching the
status string for "Error"/"Failed" — so under a non-English locale a failed send
rendered as a green success. Drive it from the existing s_status_success flag
instead. Also show the USD-mode DRGX preview at 8 dp so it matches the confirm
panel and the amount actually sent (was 4 dp).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
GetAvailableBalance() read state.addresses[s_selected_from_idx], but the index
desyncs from s_from_address (the value actually debited) after an address-list
refresh, and is left at -1 when the source is chosen from another tab's "Send
from this address" — which made the sufficiency check see a 0 balance and wrongly
block a valid send. Look the balance up by matching the source address string.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Market chart now plots the real accumulated price_history instead of a
rand()-generated curve; the hover tooltip no longer claims a specific "Xh ago"
price and the x-axis only labels the truthful "Now" point. Falls back to the
existing empty state until there are >=2 real samples.
- Transactions summary cards exclude autoshield legs (same txid send + receive-to-z)
so a shield isn't double-counted into both Sent and Received, matching the list.
- Send/Receive sync banners use verification_progress like every other surface,
instead of the blocks/headers ratio that over-reports during early sync.
- Fix printf format/type mismatches: %.0f<-int (market % shielded), %d<-size_t
(peer counts), %ld<-int64_t (peer byte counters, wrong on Windows).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The connection state machine never tore down on a lost connection: refresh-loop
RPC errors were swallowed, rpc_->isConnected() stayed true after a daemon
crash/restart/socket drop, and the UI showed stale balances with no reconnect.
Several operations also ran synchronous curl straight from ImGui handlers.
- Add handleLostConnection(): after N consecutive cycles where BOTH core RPCs
fail (warmup excluded, so no reconnect loop), disconnect so update()'s
reconnect branch re-enters tryConnect().
- Move banPeer/unbanPeer/clearBans and key export/import onto the worker thread
(import requests a rescan that could freeze the UI for the curl timeout).
- Run the block-info dialog's two chained RPCs on the worker thread (+ guard the
getblockhash result type).
- Detect daemon warmup via the JSON-RPC -28 code (new RpcError carrying the code;
message text preserved so 401/warmup string-matching is unaffected), and widen
CONNECTTIMEOUT to 10s for remote/TLS hosts (2s localhost).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
z_sendmany returns an opid immediately; the tx is built/signed/broadcast
asynchronously afterward. The send path showed "Transaction sent successfully!"
and cleared the form on opid receipt, so a later async failure contradicted it.
Shield/merge stored the opid only in a dialog-local static (never polled), and
auto-shield ran a blocking z_shieldcoinbase on the UI thread and discarded its
opid — async failures of all three were silently lost.
- Add App::trackOperation(opid) so shield/merge/auto-shield register with the
shared opid poller (failures surface, balances refresh on completion).
- Defer the full-node send's success/failure to the poller via per-opid callbacks
(parseOperationStatusPoll now exposes failureByOpid); the "Sending..." spinner
covers the finalizing window, and the form is kept until terminal status.
- Dispatch auto-shield through the worker thread and use the configured fee.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Add a status panel at the top of the Network tab driven by the live WalletState:
- Connection: a colored dot + Connected / Syncing / Not connected, with the in-use server host
(or "Random server") and its latency on the right.
- Sync: "<pct>% · <walletHeight> / <chainHeight>" while syncing (with a thin progress bar),
"Synced · block N" when complete, or "No wallet open" when disconnected.
Reads app->state().sync (populated by the lite refresh: progress / wallet+chain height / complete)
and state().connected (= walletOpen). Advances with a Dummy so the bounds grow correctly.
Both variants build; suite passes; hygiene clean; lite GUI smoke OK.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Each server card advanced to the next via a bare SetCursorScreenPos, which ImGui won't use to
extend the scroll region's content height ("Code uses SetCursorPos() to extend window boundaries
... submit an item e.g. Dummy() afterwards"). Beyond the warning, this meant cards past the fold
wouldn't scroll. Advance with an ImGui::Dummy(cardW, gap) below each card so the content height
grows correctly.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
A lite-wallet-only "Network" tab (full-node keeps the Peers tab; exactly one shows per variant)
to manage lightwalletd servers, replacing the basic selector that was in Settings.
- Card list of servers with per-server latency + status dot, DNS host + resolved IP, and an
Official/Custom pill. Official DragonX servers get a glowing outline.
- Pick a server (Sticky) by clicking its card, or toggle "use a random server" (Random mode);
selection applies immediately (App::rebuildLiteWallet(force=true) tears down + rebuilds the
controller against the new server and resyncs — its dtor detaches the uninterruptible sync
thread, so this doesn't block).
- Add custom servers; hide/unhide servers (persisted set, revealed by a "Show hidden" toggle).
- Latency/IP come from a new background probe (util/LiteServerProbe): libcurl CONNECT_ONLY does
the TCP+TLS handshake (works for gRPC lightwalletd, no HTTP response needed), recording
APPCONNECT_TIME as latency and CURLINFO_PRIMARY_IP. Auto-runs on tab open + a Refresh button.
Wiring: WalletUiSurface::LiteNetwork (gated !fullNodePagesAvailable) + NavPage::LiteNetwork in
the sidebar + app.cpp dispatch; settings gains a hidden-servers set; isOfficialLiteServer() added
to lite_connection_service. The Settings page lite-server selector + its plumbing are removed
(single source of truth = the tab).
Reuses the existing server model (LiteServerPreference, Sticky/Random, selectLiteServer) and UI
primitives (DrawGlassPanel, ThemeEffects glow, peers-tab ping-dot idiom). Unit-tested
(liteServerHost, isOfficialLiteServer) + an env-gated live probe (verified vs lite.dragonx.is:
online, latency, IP). Both variants + lite-backend build; suite passes; hygiene clean; GUI
smoke-launched without crash.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Relocate the miner-update control from a standalone full-width button into the mining-control
header row, immediately left of the benchmark button:
- The button now shows the latest available version ("Update <tag>"), with the current installed
version as text to its left ("Current: <tag>" / "none").
- A one-shot background version check (util::XmrigUpdater::startCheck) runs the first time the pool
section is shown, so the latest tag can be displayed; until it arrives the button reads
"Update miner…". Clicking opens the existing dialog; disabled (greyed, with tooltip) while the
miner is running.
- New i18n keys: xmrig_update_short, xmrig_current, xmrig_none.
Both variants build; suite passes; GUI smoke-launched without crash.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Replace the English string literals in the miner-update dialog + the "Update miner…" mining-tab
button/tooltip with TR() keys, and register their English text in i18n.cpp's loadBuiltinEnglish()
(the in-code English fallback that non-English locales overlay). Reuses the existing cancel/close/
retry keys. Labeled values use a "%s %s" literal format with a TR'd label (no -Wformat-security
risk). Non-English locales fall back to English for the new xmrig_* keys until translations are
added to res/lang/*.json.
Both variants build; suite passes; hygiene clean.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Addresses confirmed findings from the multi-lens review of the updater:
- Cancelable + live progress (was: download uncancelable, progress stuck at 0%, closing
the dialog mid-download blocked the UI thread on the worker join). Wire a libcurl
CURLOPT_XFERINFOFUNCTION that publishes byte counts and returns abort when cancel() is
requested; add a Cancel button. The dialog's destructor now aborts the transfer promptly,
so closing mid-download no longer freezes the UI.
- Graceful "unavailable" instead of a red error on platforms with no published build
(macOS / ARM): new terminal State::Unavailable rendered neutrally, not as a failure.
- Install-time running guard (TOCTOU): App::isPoolMinerRunning() re-checked in the dialog
before each install, so a dialog opened before mining started can't replace a live binary.
- Size caps: CURLOPT_MAXFILESIZE on the download and a per-archive-member ceiling before
decomphressing into memory, to bound an attacker-controlled archive.
- Distinguish a local read failure of the downloaded archive from a checksum mismatch
(was reported misleadingly as "possible tampering").
- Reword the dialog's verification note to "checked against the release's published SHA-256
checksum" (integrity, not authenticity — see the signing note below).
Not fixed here (needs your input): WinRing0x64.sys has no per-file hash published, but it is
covered by the verified archive checksum (it is inside the verified zip); and the release is
not cryptographically signed — checksums and binary share one trust root. Adding a pinned-key
ed25519/minisign signature is the real supply-chain hardening and needs an offline signing key
+ a release-process change.
Both variants build; suite passes; live worker re-verified end-to-end on linux-x64.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Wires util::XmrigUpdater into the GUI:
- ui/windows/xmrig_download_dialog.h: a modal (mirrors BootstrapDownloadDialog) that drives
the updater — Checking -> Up-to-date/Update-available -> Downloading/Verifying/Extracting ->
Done/Failed, with a progress bar and a "verified against its published checksum" note. On
success it persists the installed release tag to settings. Rendered each frame from App::render.
- mining_tab: an "Update miner…" button in the pool section, disabled (with a tooltip) while
xmrig is running so a live binary is never replaced.
- settings: persist the installed DRG-XMRig tag (xmrig_version) for update detection.
Both variants build; suite passes; GUI smoke-launched without crashing.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
~9,988 lines of header-only UI code that no compiled translation unit reached,
verified by transitive include-reachability from every .cpp plus a symbol sweep
(all 28 component classes — Snackbar, Ripple, NavDrawerSpec, TabBarSpec,
TransitionManager, … — had zero references in live code):
- src/ui/material/ component library: the material.h umbrella, components/*
(app_bar, cards, chips, dialogs, inputs, lists, nav_drawer, progress, slider,
snackbar, tabs, text_fields), and the animation system (elevation, motion,
ripple, transitions, app_layout) — 19 headers. Kept the live helpers the app
actually uses directly: color_theme, colors, type/typography, draw_helpers,
layout, project_icons, and components/buttons (included by mining_tab).
- src/ui/screens/ layer: main_layout, home_screen, send_screen, etc. — the
original screen stack and the only consumer of the dead component library.
The live UI runs through ui/windows/ (34 .cpp) + ui/pages/.
- src/embedded/resources.h: a superseded dragonx::embedded::Resources duplicate;
the app uses src/resources/embedded_resources.h.
None were in CMakeLists or included by live code, so the build is unaffected.
Both variants build; full test suite passes; source-hygiene check clean.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Both are header-only, in no CMake target, #included nowhere, and their only
symbols (ScrollFadeRT, DrawScrollFadeMask) are referenced nowhere:
- src/ui/effects/scroll_fade_fbo.h — superseded by the shader-based
scroll_fade_shader.h (the implementation actually used by settings_page).
- src/ui/material/gpu_mask.h — a GPU blend-mask helper never integrated.
App + test build clean after removal; tests pass; hygiene clean.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
When the user confirms a send on a locked encrypted lite wallet, show an unlock
modal (passphrase -> unlockWallet) instead of letting the backend reject it with
"Wallet is locked". After unlocking, the user re-confirms the send (the form is
preserved). Balances remain viewable while locked; only spending needs unlock.
- send_tab: the Confirm-and-send button routes to App::requestLiteUnlock() when
getWalletState().isLocked(), else sends as before.
- App::renderLiteUnlockPrompt(): centered modal, passphrase (Enter submits),
Unlock/Cancel; the passphrase buffer is sodium-zeroed after every path.
Full-node unaffected (gated on liteWallet()/isLocked()). Builds clean, launches
clean, tests pass.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Add a "Security" subsection to Settings → Backup & keys (open wallet only) that
wires the encryption controller methods to the UI:
- Unencrypted wallet: passphrase field + "Encrypt wallet".
- Encrypted + locked: "Unlock" (passphrase) ; Encrypted + unlocked: "Lock now".
- Encrypted: passphrase + "Remove encryption" (decrypt).
- Status line reflects the result; state shown from WalletState.isEncrypted()/
isLocked() (kept current by the controller's encryptionstatus refresh poll).
Secret hygiene: the passphrase inputs (lite_enc_pass / lite_dec_pass) are
sodium-zeroed immediately after each action and when the wallet closes while the
section was open.
Runtime-checked: app auto-opens a wallet and the new encryptionstatus worker poll
runs clean (no errors); tests pass; hygiene clean.
Follow-ups (not yet): a send-time unlock prompt and a startup lock-screen overlay
for an encrypted+locked wallet (today: unlock via Settings; balances remain
viewable while locked).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
In lite builds there is no daemon, and isConnected() now tracks the lite wallet,
so the full-node "not connected / waiting for daemon" wording was misleading when
no wallet is open. Add two strings (lite_no_wallet, lite_no_wallet_short; English
built-ins, so other languages fall back until translated) and use them in lite:
- receive/send address preview + receive empty-state overlay + send "can't send"
tooltip + transactions empty state -> "No wallet open [— create or open one in
Settings]" instead of daemon wording.
- Status bar: the red indicator shows "No wallet open" (not "Disconnected") in
lite; the P2P peer count is skipped (lite has no peers); and the redundant
full-node connection-detail line is suppressed (connection_status_ set to
"Connected"/"" from the lite wallet state).
Full-node wording unchanged (all gated on isLiteBuild()). Build + run clean
(no RPC noise), tests pass.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Add a "Backup & keys" section to the lite Settings page, shown only for an open
wallet, wiring the M4 controller backup/import surface into the GUI:
- "Show seed" / "Show private keys" -> exportSeed() / exportPrivateKeys();
the revealed secret is displayed read-only (TextWrapped, no extra copies) with
Copy and "Hide & wipe" controls.
- "Import key" (password input) -> importKey() (auto-detects WIF vs shielded);
do_import_sk just records the key + saves (no synchronous rescan), so this is
safe on the UI thread — history appears after the next sync.
Secret hygiene: the revealed-backup buffer is sodium-wiped via
secureWipeLiteSecret on hide, on a new export (overwrite), and if the wallet
closes while revealed; each export also wipes the controller's result copy; the
import input buffer is zeroed immediately after submission.
Lite app + full-node variant build/link clean; controller methods already
covered by testLiteWalletControllerM4; hygiene clean. GUI behavior itself isn't
auto-verifiable here.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The LiteWalletController was constructed once at App::init() with the lite
connection settings known at startup; changing the lite server in Settings
persisted to disk but never reached the live controller, so the new server had
no effect until the next launch.
Factor the construction into App::rebuildLiteWallet() and call it after a
successful server-selection save. The rebuild deliberately preserves a live
session: if a wallet is already open (and possibly mid-sync), it no-ops and the
new selection applies on the next controller build, rather than discarding the
open wallet and its uninterruptible in-flight sync.
Closes the last remaining HIGH from the session audit.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Re-audited this session's lite-wallet changes (originally written at medium
effort) and fixed the genuine issues found:
- walletReady (open path): litelib_initialize_existing returns the bare string
"OK", which is NOT valid JSON, so the previous `json::accept(value)` check
marked a *successful* open as not-ready. Key off a non-empty success response
instead (the bridge already maps "Error:"/null to failure). Drops the now
unused nlohmann include.
- sync progress: while the detached sync thread is still running, syncDone_ is
authoritative — don't surface the backend's transient idle syncstatus
({"syncing":"false"} -> parser progress=1.0/complete=true) as a misleading
100%/done. Force complete=false and zero the bogus 1.0 in the progress model.
- per-address balance: also exclude `pending` outputs (notes/utxos from an
unconfirmed received tx) so per-address figures match confirmed/available.
- secret wiping: the settings page left the page-local request copies
(input.request.*Request.{passphrase,seedPhrase}) unwiped, and the
validation-only fallback path wiped nothing. Replace the single-path memzero
with an RAII scrubber that wipes both the UI char buffers and the request
string copies on every return path.
- concurrency: document that concurrent bridge->execute() is intentionally
unguarded — litelib serializes wallet access internally via
Arc<RwLock<LightWallet>>, so a C++ mutex is unnecessary and would defeat the
sync/syncstatus concurrency the design relies on. syncLaunched_ -> atomic.
Tests: fake backend now returns the real init shapes (seed object for
create/restore, bare "OK" for open) and a new open-path case guards the
walletReady regression. Removed an unreliable alloc==freed leak assert from the
thread-bearing controller test (kept in the thread-free bridge test). Also fixed
a stray CMake indent and removed ~220MB of untracked build/debug scratch.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Preserve the previously-uncommitted lite wallet implementation and related dev WIP
under version control:
- src/wallet/ lite services: client bridge, bridge runtime, connection, lifecycle,
sync, gateway, result parsers, state mapper, artifact contract/resolver, refresh
services, UI adapters, wallet_backend/capabilities. (Includes two small M1 fixes:
lifecycle walletReady now parses the response; default chain name -> "main".)
- src/chat/ chat protocol; tests/fixtures/ (lite + hushchat); tools/hushchat_fixture_check.cpp;
scripts/build-lite-backend-artifact.sh.
- Pre-existing modified app_network/security/wizard, network_refresh_service, sidebar,
mining_tab, bootstrap dialog, and version headers captured as-is.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>