Verified by running the app against a live node and watching a real rescan. Three
issues that only surfaced at runtime:
- Wrong RPC name: this daemon (hush/komodo) exposes the runtime rescan as
"rescan <height>", not bitcoin's "rescanblockchain". runtimeRescan() and
RPCClient::rescanBlockchain() used the bitcoin name and failed with "Method not
found" on every node. Corrected to "rescan".
- Witness/rescan progress never surfaced during a rescan: the daemon-output parser
that drives it was gated behind rpcConnected, but a heavy rescan holds cs_main so
getinfo times out and the RPC reads disconnected — silencing the parser exactly
when it's needed. The parser reads the daemon's stdout pipe (no RPC), so it now
runs whenever the daemon process is alive. It also now parses INLINE on the main
thread instead of via fast_worker_, so it can't be starved when the worker is
blocked on a getrescaninfo call (which waits on cs_main during a witness rebuild).
- Witness rebuild has TWO sub-phases with different scales — the initial-witness
pass ("Setting Initial Sapling Witness for tx <hash>, <i> of <N>") and the cache
walk ("Building Witnesses for block <h> <frac> complete, <n> remaining"). Tracking
them with one monotonic value pinned the bar at the initial pass's ~100% through
the whole cache walk. They're now tracked as distinct phases (witness_phase) with
their own monotonic progress and labels ("Setting witnesses" vs "Rebuilding
witnesses"), so neither resets/bounces and the long phase shows real movement.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The witness-rebuild bar reset repeatedly because the daemon's "Building Witnesses
for block <h> <frac> complete" line reports per-call progress: BuildWitnessCache is
re-invoked for each connected block and each call walks from its own start height to
the tip, so the fraction restarts every time. The earlier "Setting Initial Sapling
Witness for tx <i> of <m>" counter resets per call too, so neither is a usable
overall metric.
Derive a stable, monotonic percentage from the "<n> remaining" count instead: track
the largest "remaining" seen during the phase as the full span and show how far
remaining has fallen below it. The longest pass defines 0→100%; the short per-block
follow-up passes only nudge the bar near the end rather than resetting it. The
"Setting Initial" line now only marks the phase active. Per-phase tracking resets at
phase start and every rescan-completion site.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The daemon's post-rescan witness rebuild ("Building Witnesses for block ...")
is a distinct, often-long phase that previously showed only as an indeterminate
"Rescanning..." with no progress. Parse the daemon's witness-build log lines and
surface a dedicated progress indicator.
- Parse "Building Witnesses for block <h> <frac> complete, <n> remaining" (and the
earlier "Setting Initial Sapling Witness for tx ..., <i> of <m>") from daemon
output, extracting a 0..1 fraction and remaining-block count.
- New SyncInfo fields building_witnesses / witness_progress / witness_remaining,
cleared at every rescan-completion site (warmup-end, getrescaninfo poll, runtime
rescan callback, daemon-log "finished").
- Status bar shows "Rebuilding witnesses NN%" (priority over the generic rescan
text); the loading overlay (shown during -rescan warmup) gets a labelled witness
progress bar with the remaining-block count.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
A wallet bootstrapped from a snapshot keeps its wallet.dat but never rescans, so
its spent-state is stale and the first send tries to spend already-spent notes and
is rejected. The startup -rescan flag can't fix it either: the snapshot lacks the
pre-snapshot block history -rescan needs, so it errors. The working fix is a runtime
rescanblockchain RPC from a height the snapshot actually has.
- Add App::runtimeRescan(startHeight): runs rescanblockchain via the worker, drives
the rescanning UI state, and owns completion via the RPC callback (getrescaninfo
is unavailable on this daemon). Suppresses the per-second mining/rescan pollers
and the Core/balance/tx refreshes while the daemon holds cs_main for the scan.
- Add App::detectLowestAvailableBlockHeight(): async binary search via getblock for
the lowest height whose block data is on disk → the snapshot base, and whether the
node still has full history.
- Auto-reconcile after bootstrap: both completion sites (wizard + Settings download
dialog) mark a pending rescan; once the daemon is back up and the tip is known,
detect the base and runtimeRescan() from it (or -rescan restart on a full node).
- Settings "Rescan Blockchain" now probes first: full-history nodes get the existing
-rescan restart; bootstrapped/pruned nodes get a prompt pre-filled with the
detected base height that runs the runtime rescan.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The Daemon binary panel (info + the all-actions toolbar) lived inside the 60%-wide
NODE column, so the six-button toolbar clipped. Pull it out into a dedicated full-width
row rendered after the NODE + SECURITY columns reconcile, so it spans the whole card:
Installed | Bundled info side by side, status line, and the Install bundled | Refresh |
Test connection | Rescan | Delete blockchain | Repair wallet toolbar now have the full
container width and no longer clip. The NODE column keeps only the node/RPC info.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Move Test Connection / Rescan Blockchain / Delete Blockchain / Repair Wallet onto the
same line as and right after the Daemon binary buttons (Install bundled | Refresh), so
all node actions form a single toolbar row beneath the Daemon binary panel. Buttons are
auto-sized to pack onto one line; each disabled-state group (connection vs embedded
daemon vs bundle present) keeps its own guard. Removes the former two stacked button rows.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Delete Blockchain and Repair Wallet now sit on one line, paired with the same
uniform button width as Test Connection / Rescan Blockchain (a clean 2-column grid;
all maintenance buttons share one rowBtnW sized across the four labels).
- The Daemon binary panel lays Installed and Bundled side by side across the node
column (version + size·date | version + size) instead of stacked narrow lines, with
the status line spanning underneath and Install bundled / Refresh paired below.
- Shorten the install button label to "Install bundled" so it fits the shared width;
the tooltip still explains the full action. Date shown as YYYY-MM-DD.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Previously the wallet re-extracted the bundled dragonxd on startup whenever the
installed binary's size differed from the bundle ("stale" overwrite), which could
replace a node a user had deliberately placed in dragonx/.
Now dragonx binaries (dragonxd/cli/tx) are auto-placed ONLY when missing — never
auto-overwritten on a size mismatch (needsParamsExtraction + extractEmbeddedResources).
Params/asmap keep their size-based refresh; a daemon dropped next to the wallet exe
still takes priority and is never touched.
Replacing the daemon is now an explicit action: Settings → "Daemon binary" reports the
installed binary's version (scanned from the file), size and modified date, compares it
to the version bundled in this build, and offers an "Install bundled daemon" button.
That stops the node, overwrites dragonxd/cli/tx with the bundled copies (waiting for the
process to release the file lock), and restarts — wallet/keys/chain data untouched.
Adds resources::{getInstalledDaemonInfo,getBundledDaemonInfo,reextractBundledDaemon}
(+ a version-string scanner) and App::reinstallBundledDaemon().
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
dragonxd's z_sendmany picks notes to cover the recipient total (nTotalOut) but not
the miner fee, then rejects the build unless the selected notes cover amount+fee
(rpcwallet.cpp:5312 vs asyncrpcoperation_sendmany.cpp:278). So a shielded send whose
largest notes sum exactly to the amount fails with "Insufficient shielded funds,
have H, need H+fee" despite ample balance — e.g. sending exactly 2.0 from an address
whose biggest note is 2.0.
Since the failure is async (reported via the opid poll), detect it there: when a
shielded send fails with that message and the selected total H >= the requested
amount (selection covered the amount but stopped one note short of the fee — vs a
genuine shortfall where H < amount), re-issue the send once with a tiny self-output
(= fee) back to the from-address. That lifts the daemon's selection target past the
boundary so it grabs another note and can cover the fee; the recipient still receives
the exact amount. Retries are tracked so a second failure surfaces normally (no loop).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
renderLoadingOverlay() rendered the daemon's recent output twice during startup:
a bare 4-line centered tail (section 2c, init/warmup only) and the styled
terminal-style box (section 4, always shown when the embedded daemon exists).
The bare tail was a strict subset of the box, so the same dragonxd output showed
stacked twice. Remove the redundant bare tail; keep the terminal box (which also
matches the shutdown screen's panel).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
When a note's stored record is corrupt or its tx isn't in the canonical chain,
z_sendmany fails to build a valid sapling spend proof even after a full -rescan,
because a plain rescan replays witnesses but keeps the existing tx/note records.
The zcashd repair for this is -zapwallettxes=2, which deletes all wallet tx/note
records and rebuilds them from the chain (keys/addresses preserved).
Adds a RepairWallet lifecycle operation that mirrors the existing -rescan plumbing
(one-shot zapOnNextStart flag on the embedded daemon; -zapwallettxes=2 implies and
supersedes -rescan), an App::repairWallet() that reuses the rescan status UI (so the
status bar + warmup-end completion detection apply), and a confirmed "Repair Wallet"
button + dialog in Settings → node maintenance (embedded daemon only).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Two related fixes for the post-bootstrap "send fails / rescan stuck at 99%" trap:
1) Rescan completion now keys off warmup-end. A -rescan runs entirely inside daemon
warmup (every RPC returns -28 until it finishes), so warmup completing IS the rescan
completing. The old detectors relied on getrescaninfo (which some daemons answer with
"Method not found") or a "Done rescanning"/bench log line the daemon may never print,
leaving the status bar stuck at 99% — so users killed the rescan before it finished.
When warmup ends and a rescan was confirmed active, clear the rescan state, flip to
100%, refresh history/balance, and toast completion.
2) z_sendmany failures that mean stale shielded note data (shielded-requirements-not-met,
missing sapling anchor, invalid sapling spend proof, bad-txns-sapling-*) now append a
plain-language hint telling the user to run a full rescan, instead of surfacing only the
raw daemon string.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The lite status bar showed "blocks: 0" once fully synced. The backend's
`syncstatus` only includes synced_blocks/total_blocks WHILE actively scanning;
at rest it returns just {"syncing":"false"}, so the parsed syncedBlocks was 0
and became state.sync.blocks. On the synced refresh path, additionally query the
backend `height` command (wallet last-scanned height, a fast local read) and use
it as the synced height/tip, so the block count is correct at rest.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
On first run the lite welcome screen's "Restore from seed" button only showed a
hint toast and bounced the user to Settings, dismissing the welcome with no
wallet open — it never prompted for a seed. Add a real restore step to the
welcome wizard: a seed-phrase field + optional birthday height, which calls
beginRestoreWalletAsync() (same server failover as create/open), shows
"Restoring…" progress, then completes (wallet syncs) or surfaces the error to
retry. The seed buffer is wiped on success/Back and in finish().
(The Settings -> Lite -> Restore path already prompted for a seed; this fixes
the first-run welcome path.)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The lite SilentDragonXLite backend stores its wallet in its own directory
(dirs::data_dir()/silentdragonxlite — %APPDATA%\silentdragonxlite on Windows,
~/.silentdragonxlite on Linux, ~/Library/Application Support/silentdragonxlite on
macOS), NOT the full-node getDragonXDataDir() (…/Hush/DRAGONX). The newly added
lite "Open data folder" button opened the wrong (full-node) directory.
Add Platform::getLiteWalletDataDir() mirroring the backend's get_zcash_data_path
for the "main" chain, and point the lite button at it. The full-node button is
unchanged.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Lite send/shield, unlock, and key-import failures were shown only as transient
toasts — impossible to copy. Route them through liteLog() so they also appear in
the lite Console (which has a Copy button), alongside the lifecycle/open/sync
errors the controller already logs:
- send/shield broadcast failures (App broadcast-result delivery)
- wallet unlock failure
- key import failure (controller; logs the error text only, never the key)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Add an explicit button in Settings that opens the wallet/blockchain data
directory (getDragonXDataDir()) in the OS file manager via the existing
Platform::openFolder(). Placed in the full-node connection section (next to the
data-dir path, which was only a subtle clickable link) and in the lite section
(always available). i18n strings added.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Add a maintenance option for the lite wallet to re-download and re-scan every
block from the lite server — useful when balances or history look wrong.
- LiteWalletController::startRescan() runs the backend `rescan` command (which
clears the wallet's synced block cache and re-syncs from its birthday) on a
detached thread, reusing the existing sync progress/refresh machinery: it
resets syncDone_ so refreshModel() shows progress again and refreshes data on
completion. No-op if no wallet is open or a scan is already running.
- scanInProgress() exposes the initial-sync-or-rescan state.
- Settings (lite, open wallet) gains a "Redownload blocks" button behind a
confirmation modal, disabled while a scan is running. i18n strings added.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The header and coin logos load disk-first (for dev builds / theme drop-ins) and
fall back to the copies embedded in the exe. The portable single-file build has
no res/img/ folder beside it, so the disk read always failed and logged
"LoadTextureFromFile: failed to read ..." before the (successful) embedded
fallback. Guard each disk load with std::filesystem::exists() so the missing
file is skipped silently and we go straight to the embedded logo — no error
line, logos unchanged.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The console mixed RPC traces, daemon output, and the wallet's own "[app] ..."
log lines with no way to hide the latter. Add an "App" checkbox alongside the
existing Daemon/Errors/RPC toggles. Since [app] lines share COLOR_INFO with
other info text, the filter matches them by their "[app] " prefix rather than by
color. Default on; unit test + i18n added.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
On a fresh open the status-bar peer count stayed 0 until the Network tab was
opened. refreshData() — the one-shot refresh run on connect / warmup-complete /
unlock — only refreshed peers when the active tab was Peers, so on any other tab
nothing populated the count until a tab visit forced it. Refresh peers
unconditionally there so the count appears right after connecting; the periodic
20s Peers timer (all tabs) keeps it current after that.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Add a fuzzy mode to the explorer search: a non-numeric, non-full-hash query now
filters the list to cached blocks whose hash (or height text) contains the query
substring, live as you type. Backed by a new ExplorerBlockCache::searchBlocks()
(SQLite LIKE with escaped wildcards), memoized per query so it doesn't hit the DB
every frame. Exact queries still navigate precisely: a block height re-anchors
the list, and a full 64-char hash is resolved via RPC. Row clicks still open the
detail modal. Empty results show "No matching cached blocks".
Note: fuzzy matching covers cached (browsed/prefetched) blocks only — the daemon
has no partial-hash index — while exact height/hash lookups reach any block.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Typing in the explorer search ran an exact lookup that popped the block-detail
modal. It now updates the recent-blocks LIST as you type: a block height
re-anchors the list to that height (offline-friendly, no RPC), and a complete
64-char hash is resolved to its height and the list jumps there (a txid still
shows the inline tx view) — all without a modal. Clearing the box returns to the
recent (tip) blocks. Row clicks still open the detail modal for an explicit
full view. Removed the now-unused fetchBlockDetailByHash.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The explorer search only ran on Enter or the Search button. Now it also fires
automatically ~350ms after the user stops typing, once the query is resolvable
— a block height (all digits) or a complete 64-char hash/txid. Partial hex is
ignored so it won't flash "invalid query" mid-type, per-keystroke RPC spam is
avoided via the debounce, and the same query isn't re-run. Enter/button still
work for an immediate search.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The Rescan blockchain and Restart daemon buttons fired immediately on click —
both are disruptive (long offline rescan / connection drop) and easy to hit by
accident. Route them through confirmation modals, matching the existing
delete-blockchain / clear-ztx confirmations: the button now sets a confirm flag
and an overlay dialog performs the action only on explicit confirm. New i18n
strings added with English defaults.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The peer count in the status bar is state_.peers.size(), refreshed only by
getpeerinfo — and the peers refresh interval was 0 (disabled) on every tab
except Peers. So the count never changed until you opened the Peers/Network
tab. Give peers a slow 20s cadence on all tabs (30s on Console); the Peers tab
keeps its fast 5s for the live list. During sync this is still overridden by
kSyncProfile (peers 0) so it can't contend with block download. Test updated to
the new intervals.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
When a newer wallet build embeds a newer daemon, extractEmbeddedResources()
detects the size change and tries to overwrite dragonxd.exe in the daemon dir —
but the write is a plain truncating ofstream, which fails silently if the file
is locked. A running (or just-killed, handle-not-yet-released) daemon locks the
.exe on Windows (and Linux returns ETXTBSY), so the stale binary was kept and
the wallet kept launching the old daemon version.
If the direct write fails, move the stale binary aside to "<name>.old" (renaming
a running/locked executable is permitted on both Windows and Linux — the running
process keeps the moved copy) and write the fresh one at the original path. A
best-effort pass removes leftover .old files once the old process has exited.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
build.sh bundles whatever daemon binary already sits in prebuilt-binaries/, and
setup.sh only rebuilds a platform's daemon when its flag (--win/--mac) is passed
— so a daemon left over from an older source revision silently shipped in the
wallet (the Network tab showed dragonxd v1.0.1 while the source was v1.0.2).
Add a stale-daemon guard: compare the vX.Y.Z baked into each prebuilt daemon
against CLIENT_VERSION_* in the checked-out dragonx source. On the present/skip
and --check paths it now prints either "matches dragonx source" or a STALE
warning naming both versions and the rebuild command, plus a summary reminder at
the end of the daemon section. Version is read with grep -a (no binutils/strings
dependency); no-ops cleanly when the source or a binary is absent.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The History badge counts transactions with confirmations==0, iterating the raw
transaction list. Autoshield transactions have two legs sharing one txid, and
the send leg parsed from z_viewtransaction carries confirmations=0 even when the
transaction is long confirmed (the receive leg holds the real count). So the
badge counted those stale legs and stuck at a non-zero number (e.g. 7) with no
pending transactions.
Treat a txid with ANY confirmed leg as confirmed, and count UNIQUE unconfirmed
txids rather than legs — so confirmed multi-leg transactions don't inflate the
badge and genuinely pending ones still count once each.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
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>
Marking/unmarking a mining address triggered a long history reload: it called
invalidateShieldedHistoryScanProgress() + forced a transaction refresh, which
re-scans every z-address over many RPC cycles. But "mined" vs "receive" is a
pure function of the LOCAL mining-address set — the daemon knows nothing about
it — so a chain re-scan is pointless.
Relabel the affected rows in the in-memory history directly and persist just
those to the encrypted SQLite history cache. The History tab updates instantly
(its display cache rebuilds on the type change), with no daemon round-trip and
no reload. Only re-save when something actually changed.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Unmarking (or marking) a mining address didn't change the history. The refresh
re-scanned the affected transaction as "receive", but appendMissingPreviousTransactions
carries over not-yet-rescanned prior transactions and dedupes by txid+TYPE — so
the stale "mined" copy was carried over right alongside the fresh "receive", and
the change never appeared.
Re-label state_.transactions in setMiningAddress() the moment the flag changes
(mined vs receive is just whether the receiving address is mining-flagged). The
History tab updates instantly, and the next refresh's carry-over now matches the
fresh scan instead of duplicating the old label. The reclassified list is also
persisted via the existing cache save.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
History streams in over many refresh cycles (the incremental shielded scan
walks every z-address), so the first batch appears long before the list is
complete — with no indication more is still coming. The existing loading banner
deliberately goes quiet once any rows are on screen.
Track whether the first full shielded scan has finished
(initial_history_scan_complete_) and, until it has, surface a progress percentage
(fraction of z-addresses scanned) in transactionRefreshProgressText() — which the
History tab already renders as its pulsing loading indicator. Goes quiet once the
first scan completes; routine per-block re-scans don't re-trigger it. Reset on a
full history invalidation (rescan / session reset) so it shows again on reload.
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>
A rescan ran to completion but the status bar stayed at "Rescanning 99%"
forever. The daemon-output parser only treated "Done rescanning"/"Rescan
complete" as finished, but this daemon prints neither — it logs the rescan
benchmark timing line exactly when the scan ends:
2026-... rescan 16760577ms
then resumes normal block processing. So the parser saw the last
"Still rescanning ... Progress=0.99" and never the finish, leaving it stuck.
- Recognise the " rescan <N>ms" bench line as completion (it ends in "ms",
which the "Still rescanning"/"Rescanning..." progress lines never do).
- When the parser reads "Still rescanning" straight from the daemon log, mark
rescan_confirmed_active_ — hard proof the scan is running that doesn't depend
on catching a getrescaninfo warmup error, so the RPC completion path can also
fire after the daemon leaves warmup. Clear it on finish.
The parser reads the daemon's debug.log via the controller (not RPC), so this
completes the rescan UI even if the RPC connection hasn't re-established yet.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
While the daemon processes -rescan it sits in RPC warmup and rejects every call
with -28 ("Rescanning..."). The balance/tx/address refreshes already skip warmup
(state_.warming_up), but the 1-second mining poll didn't — so getmininginfo fired
the whole rescan and flooded the log with "getMiningInfo error: Rescanning..."
(~680 entries in one capture).
Gate refreshMiningInfo() on !state_.warming_up like the other refreshes. The
getrescaninfo progress poll still runs (it's how the warmup/rescan is tracked).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Clicking Settings → Rescan restarted the daemon with -rescan correctly, but the
progress poll fired "Blockchain rescan complete" the instant it was clicked,
then showed nothing for the entire (multi-hour) rescan — so it looked broken.
Cause: the very first getrescaninfo poll runs before the daemon has restarted
and hits the still-running pre-restart daemon, which answers rescanning=false.
The completion branch took that as "done", cleared the rescanning flag, and the
real rescan then ran invisibly. (Confirmed from a Windows debug-log capture: an
instant OK{"rescanning":false}, then ~6400 warmup errors over ~5h, all swallowed.)
Fixes:
- Gate completion on a new rescan_confirmed_active_ flag that's only set once we
actually observe the rescan running, so a pre-restart rescanning=false can't be
misread as completion.
- While the daemon is in -rescan RPC warmup it rejects every call with the live
phase as the message ("Loading block index..." -> "Rescanning..."). Treat that
as proof-of-progress: surface it as rescan_status and mark confirmed-active,
instead of silently swallowing it. The status bar keeps its animated
"Rescanning..." for the whole run, then reports complete when warmup ends.
- Read rescan_progress whether the daemon returns it as a string or a number
(the get<std::string>() would have thrown on a numeric field).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
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>