The opid poll called z_getoperationstatus(["opid"]) to check a specific
operation, but this daemon rejects the filtered form with "JSON value is not a
number/array as expected" (a UniValue error returned as an RPC error). The
poll's catch swallowed it, so every completed send stayed stuck on "Waiting
for operation" forever — confirmed via a Windows debug-log capture showing the
throw on every 2s cycle. The no-arg form works (verified in the console).
Call z_getoperationstatus with no arguments (returns ALL operations) and filter
to the opids we're tracking in parseOperationStatusPoll(). The parser now skips
any operation whose id isn't in the requested set, so unrelated/old operations
can't fire a spurious error toast or pollute send state. The stale-opid logic
is unchanged (the no-arg form still reports in-progress ops, so a genuinely
pending opid is never misread as stale).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
A completed send could spin forever on "Waiting for operation (N)". Root
cause: onDisconnected() stopped fast_worker_ but kept the unique_ptr, so
onConnected()'s `if (!fast_worker_)` guard never restarted it — after the
first reconnect (daemon warmup, restart, any RPC blip) the fast lane stayed
dead for the whole session.
The opid poll was the only fast_worker_ user that posted to it directly with
no fallback, so it alone broke: its post() landed on a stopped thread, the
result MainCb never ran, opid_poll_in_progress_ stuck true, and the poll never
fired again — leaving the operation (already "success" on the daemon, with a
txid) untracked.
Two fixes:
- onDisconnected() now reset()s fast_worker_ after stop(), so onConnected
recreates and starts a fresh one (restores the fast lane for all its users,
not just the poll).
- the opid poll now falls back to worker_ when the fast lane isn't running,
matching every other fast_worker_ call site — defense in depth.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
A prior change passed the user-selected fee to z_sendmany as a fixed-decimal
string (mirroring the recipient amount). But the daemon reads the fee param
with UniValue::get_real(), which rejects a string with "JSON value is not a
number as expected" — breaking every z_sendmany send (surfaced via the
address-to-address transfer feature).
Pass the raw double instead. get_real() parses it directly and accepts any
number notation (including the "5e-05" form of a small fee), so this is
correct for all fee values. The recipient "amount" stays a fixed-decimal
string on purpose — that field is parsed with ParseFixedPoint, which a
scientific-notation double would break.
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>
The wallet is meant to ship xmrig embedded (HAS_EMBEDDED_XMRIG -> getXmrigPath() extracts it
at runtime), but build.sh only embedded a flat prebuilt-binaries/xmrig-hac/xmrig.exe — while
the published DRG-XMRig archive ships the binary inside a versioned subdir
(drg-xmrig-6.25.3-win-x64/xmrig.exe). So xmrig.exe was never present, HAS_EMBEDDED_XMRIG stayed
undefined, and the Windows wallet ran with no miner: "xmrig binary not found", pool mining and
the thread benchmark both fail.
build.sh now extracts xmrig.exe (flattened) from the matching win-x64 zip when a raw binary
isn't already staged, so the existing embed step fires. (Checks the extracted file rather than
unzip's exit code, which is non-zero when a glob matches nothing.) Verified: --win-release now
logs "Extracted xmrig.exe", stages it (6.7M), and defines HAS_EMBEDDED_XMRIG=1.
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>
The sync throttle (kSyncProfile) covers the core/transactions/addresses/peers timers, but
getmininginfo runs off the separate 1s Fast timer and so still polled ~every 5s during
sync — another cs_main contender slowing block connection. Skip it while syncing unless the
user is on the Mining tab or actively mining (where live stats are wanted). Completes the
"no RPC contention during sync beyond the 10s progress poll" goal.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The full-node wallet polled the daemon at the per-tab cadence regardless of sync state.
On the Peers/Network tab that meant getpeerinfo every 5s + core every 5s + a full
transaction scan on every new block — and blocks arrive fast during sync. Each of those
calls takes the daemon's cs_main lock, the same lock block connection needs, so the node
synced noticeably slower than on the lightweight Console tab (core 10s, no peer polling).
Make the refresh cadence sync-aware:
- RefreshScheduler::kSyncProfile {core 10s, transactions/addresses/peers disabled} is applied
to ALL tabs while state_.sync.syncing, and reverts to the per-tab profile when sync ends.
applyRefreshPolicy() picks the profile; update() re-applies it on the syncing<->synced
transition. This suppresses getpeerinfo and the per-block tx scan during sync (that data is
incomplete mid-sync anyway) — every tab now syncs as fast as Console.
- collectCoreRefreshResult(rpc, includeBalance): skip z_gettotalbalance (wallet lock + cs_main)
while syncing; only getblockchaininfo runs, which is also what drives sync-progress detection.
applyCoreRefreshResult already leaves the balance untouched when balanceOk is false.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- rpc_client::callRaw: a daemon error object is no longer assumed to carry a string
"message" — a malformed error now yields a clean "RPC error: <dump>" instead of throwing
a json type-exception from .get<std::string>().
- sendTransaction (full-node): add a single-flight guard so a rapid double-click can't issue
two z_sendmany before the first returns its opid. The lite path already guarded this; the
send form guards it in the UI, but the controller entry point now does too.
(#9 from the audit was mostly false positives on verification — all popen sites already
null-check and the xmrig download FILE* path has no throwing calls. The payment-URI
checksum idea was dropped: the send flow already checksum-validates the recipient before
broadcasting, and tightening the parser would reject the placeholder addresses the existing
test relies on; added a comment noting this is format-only by design.)
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>
Two issues shared one root cause: the shielded-receive scan marked each z-address "scanned
at the EXACT current tip," but a new block (~36s on DRGX) advances the tip and invalidates
every prior per-address scan. A wallet with more z-addresses than one refresh cycle can
scan therefore never reached "all scanned at tip" — so shieldedScanComplete stayed false
and transactions_dirty_ stayed true forever, which (a) kept the history-refresh banner lit
and the full rescan churning every cycle, and (b) blocked maybeFinishTransactionSendProgress
(it waited on transactions_dirty_), leaving the send-progress indicator stuck on.
Fix 1 — completion tolerance. Add TransactionRefreshSnapshot::shieldedScanTipTolerance: an
address counts as fresh if its last scan is within N blocks of the tip (0 = old strict
behavior, so existing tests are unchanged). The app scales N with the z-address count
(2 + count/96, capped at 50), so a multi-block pass can COMPLETE before its earliest scan
goes stale. This also throttles full rescans to ~N blocks instead of every block —
transactions_dirty_ clears, the banner stops, and CPU/RPC churn drops. Already-fresh
addresses are skipped, so the per-block cost falls back to just the (cheap) transparent
listtransactions.
Fix 2 — send-progress gate. maybeFinishTransactionSendProgress() no longer waits on the
transaction history scan (transactions_dirty_ / Transactions job): the sent tx is already
shown via the optimistic pending insert, and the spend is reflected once the balance
refresh lands, so it now finishes on the address/balance signal alone.
Test: a tolerant snapshot skips recently-scanned addresses (shieldedAddressesScanned == 0,
shieldedScanComplete) while a strict one re-scans them.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The banner was driven by transactions_dirty_, which refreshTransactionData() sets to
!shieldedScanComplete. The shielded-receive scan marks each z-address "scanned at tip,"
but every new block (~36s on DRGX) advances the tip and invalidates all prior per-address
scans, so for a wallet with more z-addresses than the per-cycle budget (8 on History) the
scan can never catch the tip — shieldedScanComplete stays false, transactions_dirty_ stays
true, and the banner stayed lit indefinitely.
Decouple the user-facing banner from that perpetual background scan:
- A just-sent transaction being enriched still surfaces (the user is waiting on it).
- Once any history is displayed, stay quiet for routine background refreshes — new receives
still appear live as they're scanned.
- The loading banner now only shows during the genuine INITIAL load (nothing displayed yet)
and send enrichment.
This is a UI-visibility fix; the underlying per-block full shielded rescan (and the related
send-progress flag that maybeFinishTransactionSendProgress gates on transactions_dirty_) are
separate follow-ups.
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 1.3.0 lite-capability work gated isUsingEmbeddedDaemon() on the compile flag
DRAGONX_ENABLE_EMBEDDED_DAEMON (in 1.2.0 it was hardcoded true, so the daemon
always launched). The lite branch in CMakeLists set that flag OFF with
`CACHE BOOL ... FORCE`, which POISONS the build dir's cache: a later full-node
reconfigure of the same dir keeps the forced-OFF value (the full-node branch
never re-asserts it), so embeddedDaemonAvailable=false and the wallet extracts
dragonxd but never starts it — exactly the reported "unpacks dragonxd.exe but
does not start the daemon, manual start works."
Note the two gates are independent: the binary is EMBEDDED/extracted via build.sh
(HAS_EMBEDDED_DAEMON), while LAUNCHING is gated by DRAGONX_ENABLE_EMBEDDED_DAEMON
— so they diverged (extract yes, launch no).
The forced cache write was also pointless: makeWalletCapabilities() already
forces the embedded-daemon capability off for any lite build via
`fullNodeBuild && embeddedDaemonCompiled`, so lite never launches a daemon
regardless of the flag.
Fix:
- CMakeLists: remove the FORCE cache poisoning (the root cause).
- build.sh: set DRAGONX_ENABLE_EMBEDDED_DAEMON explicitly per variant (ON for
full-node, OFF for lite), mirroring the existing DRAGONX_BUILD_LITE handling,
so an already-poisoned build dir is HEALED on the next build rather than
silently keeping the stale OFF.
Verified: a poisoned Windows cache (=0) flips to =1 on reconfigure; full-node
builds define =1, lite =0; tests + hygiene green.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The connection probe (getinfo) used a 30s request timeout, so when something on
the local RPC port accepts TCP but never answers — a daemon still loading the
block index, or a wedged/foreign occupant — every attempt blocked the full 30s
before the wallet could retry or update its status. That is the "stuck, timing
out every 30s" behaviour users hit.
A healthy local daemon answers getinfo in milliseconds, and a warming one
returns -28 just as fast, so a long hang on localhost only ever means trouble.
Probe localhost with an 8s timeout (remote/TLS keeps the 30s budget). The
per-call override restores the persistent 30s afterwards, so normal RPC calls
that legitimately take longer are unaffected — only the probe fails faster, so
the wallet retries promptly and reflects "initializing" / recovery within
seconds of the daemon becoming ready.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The daemon can spawn successfully (CreateProcess OK) and then exit immediately —
a missing runtime DLL, wrong architecture, corrupt binary, datadir lock, etc.
EmbeddedDaemon's crash monitor already builds a detailed reason for this
(translated Windows exit code, e.g. "STATUS_DLL_NOT_FOUND — required DLL not
found", plus the launch command and a debug.log tail) and stores it in
lastError(), but it runs on a background thread and was never shown. The result
was the exact symptom users reported: the wallet unpacks dragonxd.exe, looks
"stuck connecting", and the node silently dies-and-respawns until it gives up —
with no visible reason (manually starting dragonxd works, so the wallet then
connects to it).
tryConnect now watches the daemon's crash count (on the main thread, where it
already logs daemon state) and surfaces each NEW crash's lastError() once, as a
sticky error notification, with a concise "Couldn't start dragonxd" status. The
counter resets on a successful connect (alongside the daemon's own crash-count
reset), so a later crash re-notifies.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Two failure modes left the wallet stuck on a silent "connecting / Starting
dragonxd…" spinner with no path forward:
1. Stale external-daemon latch. EmbeddedDaemon::start() sets
external_daemon_detected_ whenever the RPC port was busy at a prior attempt
and never re-checks it, so tryConnect's no-config branch trusted that latch
and waited forever for a config the phantom would never write — even after a
stale/half-dead process freed the port. Now the port is re-evaluated LIVE
(EmbeddedDaemon::isRpcPortInUse()) each attempt: if it's genuinely busy we
keep waiting (and, after a bounded ~20s with no config, warn that whatever
owns the port isn't a usable DragonX node and how to fix it); if it's free we
fall through and start our own daemon.
2. Silent start failure. When startEmbeddedDaemon() failed (binary not found,
Sapling params missing, spawn failure) the status stayed on "Starting
dragonxd…" with the real reason only in a VERBOSE log. Now the reason
(daemon_controller_->lastError()) is surfaced once as a sticky error
notification, with a short "Couldn't start dragonxd" status.
Both counters reset on a successful connect so the messages re-arm for the next
disconnect. Lite is unaffected (tryConnect returns early for lite builds).
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 full-node Console tab already streams the daemon's output, but during
startup the user is held on the loading overlay (wallet-data tabs are blocked),
so they can't watch progress without navigating away. Surface the last few
console lines the node printed (UpdateTip height=…, "Verifying blocks…", etc.)
directly under the status/description on the overlay while initializing or
warming up, so progress is visible where the user is already looking.
Full-node only (guarded on daemon_controller_); each line is trimmed and
ellipsis-truncated to one row. Reuses DaemonController::recentLines().
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
When the full-node connect probe (getinfo) times out, the daemon is reachable
at the TCP level but busy initializing (loading the block index, verifying,
activating best chain, …) and won't answer RPC. The wallet only recognized the
JSON-RPC -28 warmup reply, so a raw socket timeout fell through to a bare,
alarming "Connection failed" retry with no indication of what the user was
waiting on.
Add a daemon-initializing UI state that drives the existing loading overlay:
- WalletState::daemon_initializing — daemon up/launching but not serving yet
(distinct from warming_up, which needs a -28 reply).
- App::applyDaemonInitStatus() infers the current phase from the daemon's own
console output (scanning recent lines for Loading/Verifying/Activating/
Rescanning/Rewinding/Pruning) and the latest block height, producing a
friendly title + description, e.g. "Processing blocks… (Block 123456)".
- The connect loop calls it from the daemon-starting and external-detected
branches: a timeout -> "reachable but initializing", a connect refusal ->
"launching, waiting to come online". Cleared on a real connect.
- The loading overlay now shows the description for daemon_initializing too,
and the status-bar amber indicator covers it (so Peers/Console tabs without
the overlay still explain the wait).
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>
Mirror the async-open path for wallet creation. beginOpenExisting() and
beginCreateWallet() now both delegate to beginAsyncLifecycle(bool create),
which runs the backend init on a detached thread and walks the failover
server list (preferred server first, then all usable defaults), reporting
the preferred server's error on total failure. The first-run wizard's
Create button drives this through a non-blocking "creating" poll state so
the UI no longer freezes while the backend contacts a (possibly flaky)
lightwalletd. The created seed response is securely wiped immediately and
read back via exportSeed for the reveal/verify steps.
Safe because litelib_initialize_new contacts the server before writing any
wallet file and LightClient::new errors if a wallet already exists, so a
failed candidate leaves no partial state.
Tests: fake backend's initialize_new now honors the dead/warmup server
substrings; testLiteWalletControllerOpenFailover gains a create-failover
case (preferred dead, fallback good -> walletOpen).
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>
Creating a wallet was one-click and silent — it never showed the seed, relying on
the user to later find Settings -> Show seed, which is an easy-to-miss fund-loss
risk. Replace the first-run prompt with a 3-step guided flow mirroring the upstream
SilentDragonXLite wizard:
1. Welcome (Create / Restore / Later) — unchanged entry.
2. Reveal: after createWallet, read the seed back via exportSeed and show all 24
words (numbered grid) + the birthday, with a strong "only way to restore"
warning, plus Copy. ("Skip" leaves the wallet created, seed backable later.)
3. Verify: tap the words in order (shuffled chips) to confirm the backup before
finishing; out-of-order taps are rejected with a hint.
The seed is held only for the wizard and securely wiped (sodium_memzero) on finish.
Builds clean for full-node, lite, and Windows cross-compile.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The failover overwrote outcome.error on every attempt, so a total open failure
reported whichever (often broken) fallback was tried last — e.g. lite5's
"CertNotValidForName" — instead of what the user's preferred server actually did.
Keep the first (preferred) server's error as the summary so "Open failed: …" names
the actionable reason; the per-server attempts are still in the Console log, and
the warmup flag is still set if any server was warming up.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
When the preferred lightwalletd server is reachable but warming up (JSON-RPC -28
/ "Activating best chain"), the failover treated it like a dead server and fell
through to the others, so the wallet didn't open until the next 20s retry — even
though the healthy server was ready within seconds.
Detect the warmup error during failover, flag it on the open outcome
(lastOpenWasWarmup()), and have the App retry on a short ~4s interval in that case
instead of 20s, so the wallet opens promptly once warmup clears. A unit test
covers a warming-preferred + dead-fallback open setting the flag.
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>
Opening an existing lite wallet ran synchronously on the UI thread and used a
single server, so a dead/unreachable lightwalletd server froze startup for the
connect timeout and then stranded the wallet ("disconnected" spinner) — and the
DragonX lite servers are flaky (often several down at once).
Add LiteWalletController::beginOpenExisting() / pumpAsyncOpen(): the open runs on
a background thread (mirroring the sync/broadcast shared-lifetime pattern — it
captures only shared_ptrs + value copies, never `this`), trying the preferred
server first and then every other usable default until one succeeds. The main
thread finalizes the result (flips walletOpen, starts sync) or records the reason.
The rollout gate is still checked up-front on the main thread.
App: auto-open now calls beginOpenExisting() and pumps it each tick, retrying on
a 20s interval so a transient outage self-heals once a server returns; a failed
open surfaces its reason (notification + Network tab) instead of a silent spinner.
Tested: a fake bridge that fails specific servers exercises both
preferred-dead -> fallback-opens and all-dead -> fails-with-reason. Built clean
for full-node, lite, and Windows cross-compile.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The wallet auto-open is a one-shot (lite_autoopen_done_), but rebuildLiteWallet()
creates a fresh, closed controller — so switching the lite server from the Network
tab (rebuildLiteWallet force=true), or any later rebuild, left the wallet
permanently closed ("disconnected" spinner) because auto-open never fired again.
Re-arm the one-shot (and clear the surfaced open-error) in rebuildLiteWallet so
the next update() tick reopens the existing wallet against the new server. This is
the recovery path when the configured lightwalletd server is unreachable: the
Network tab surfaces the failure reason, the user picks a reachable server, and
the wallet reopens. Also makes the Network tab's apply-immediately server switch
actually take effect.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Both variants hardcoded "ObsidianDragon" as the per-user config folder
(settings.json, themes, the lite_rollout cache), so the lite app and the
full-node app shared one settings.json. That cross-variant pollution can leave
the lite server selection in a bad state — and since openWallet() contacts the
selected lightwalletd server, a wrong/empty server URL there makes an existing
wallet fail to open (a silent "disconnected" spinner).
Use DRAGONX_APP_NAME (already "ObsidianDragon" / "ObsidianDragonLite" per variant)
for the config-dir name in Settings::getDefaultPath, Platform::getConfigDir and
getObsidianDragonDir (and the theme-setup exe-name probe). Full-node is unchanged;
lite now reads/writes %APPDATA%\ObsidianDragonLite (and ~/.config/ObsidianDragonLite),
so it starts from a clean, isolated config and uses default servers.
Note: the lite wallet file itself lives in the litelib backend's own data dir
(unaffected); this isolates the GUI config only.
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>
stop()-ing a worker that is mid curl_easy_perform joined on the UI thread, so a
slow/hung transfer froze the UI until the request timeout. Add RPCClient::
requestAbort() (a thread-safe atomic read by a curl progress callback that aborts
the transfer), and call it before stopping the workers on disconnect
(onDisconnected) and shutdown (beginShutdown + the synchronous fallback). The
flag is cleared on each connect() so a fresh connection never starts aborted.
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>
removeVault() overwrote vault.dat with zeros then unlinked it, but never flushed
to stable storage, so the zeros could stay in the OS cache and never reach disk.
flush + fsync before unlink on POSIX (still best-effort on CoW/SSD, but now does
what it claims).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
resetJobs() cleared the in-progress flags but left generations_ untouched, so a
refresh WorkFn still executing on the worker when a disconnect cleared state_
could pass completeDispatch's generation check and apply last-connection data
onto the new session. Bump every job's generation in resetJobs() so any
pre-reset ticket is treated as stale.
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>