- Locking the wallet (manual / auto / lite) now calls wipeDisplayedSecrets(),
which scrubs + closes any revealed seed phrase, migration seed, or exported
key — previously a locked wallet could leave the phrase/key on screen and
resident. Runs synchronously, before the async lock round-trip.
- The console secret-arg redaction list only had full-node RPC names, so lite
verbs (import / timport / encrypt / decrypt / unlock) echoed keys and
passphrases into the scrollback and up-arrow history; added them.
- The offline wallet-rebuild helper was invoked via a shell string
(popen "sh -c") built from paths including the wallet filename, so a crafted
name could inject a command; it now refuses any path with a shell
metacharacter (rc stays -1 and the existing verify-before-swap fails safe).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The code changes intended with the settings_window removal in a90aa29 (a bad
git-add pathspec aborted staging them there, leaving that commit with only the
file deletions and CMakeLists still referencing them):
- First-run handoff info toast at the completion point.
- Daemon-updater "restart pending" reminder (Settings → Daemon Binary) + the
Done screen's warning-level "restart required" headline.
- Debug restart-note raised to a readable color with the restart prompt beneath
it; DEBUG OPTIONS header shows a lock + explanatory caption (auth gate
unchanged); a few hardcoded literals → material tokens / dpiScale.
- Drop the legacy settings_window from the build (CMakeLists + the dev-only
references) to match the file deletions.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Migrate-to-seed is irreversible and fund-moving, so its gates are strengthened
(UI only — the sweep/adopt/wipe logic, z_mergetoaddress args, amounts and
destination are byte-identical):
- Continue now requires a second, consequence-stating acknowledgement checkbox
(funds move and are recoverable only via the seed phrase), not just "I wrote
it down".
- Discard, Sweep-all and Sweep-remaining each take a second confirming click
(stating the amount being moved) instead of firing on one click; the confirm
body runs verbatim only after confirmation, and arming never survives a
close/reopen.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
First-run bootstrap polish:
- Failed card wraps the (often long) error instead of clipping it, and offers a
"Try mirror" button; Retry now keeps whichever source (main/mirror) was used.
- Bootstrap offer shows an honest size/time hint and relabels "Mirror" →
"Use mirror" with a clearer tooltip.
- Daemon status dot/label is keyed off the EmbeddedDaemon::State enum instead of
matching the English substring "Stopping" (which broke in every other
language), and the faint ~55%-alpha label is raised to ~86%.
- Bootstrap Done screen: for the embedded daemon it now offers "Start Daemon"
(and says the node will start) instead of telling the user to start it by hand.
Also carries the new string-table keys for these (and the shielded-card tooltip
used by the next commit).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Sync ETA: a new pure util::SyncRateEstimator (App-owned) replaces the
function-local statics that went stale after a rescan/restart (producing the
"412h" ETA). It self-resets on a height drop and on connect/disconnect/rescan/
restart (via an atomic flag, since onConnected can run off-thread), formats
with days, and uses the Warning() token.
- Settings: the Explorer-tab URL fields + custom-fees/fetch-prices checkboxes
now persist on edit — they were the only controls still relying on the
About-only Save button that a user on this tab never sees.
- Export-all-keys: hidden in the lite build (which has its own key export),
instead of dead-ending on "Not connected to the daemon".
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- A mining start/stop/pool failure now sets a persistent App::mining_last_error_
(cleared on the next attempt), rendered as a red caption with "Press Mine to
try again" below the Mine button (plus a Dismiss, and a Windows-Defender
"How to fix" on an AV block), so a crashed/blocked/failed miner is no longer
just a button that "did nothing" once its toast faded. Two more hardcoded
English strings (pool-payout-invalid, AV-blocked) moved to i18n.
- Pool connection status is now a persistent "Status" row in the pool stats
card (PoolMiningState.running_since / last_error): it flips from "Connecting…"
to "Can't reach pool — check the URL" after a 30s timeout instead of sitting
on red forever, and no longer vanishes once logs arrive. The old transient
banner in mining_tab.cpp was removed.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
When the node rejects the wallet's RPC login (bad rpcuser/rpcpassword), the
status bar showed developer jargon ("Auth failed — check rpcuser/rpcpassword")
and the only action was Reconnect, which just repeats the failure. Now a
dedicated AuthFailed banner reason surfaces "Wallet can't sign in to the node"
with a plain-language detail and a real fix: Restart node for the embedded
daemon (regenerates its cookie creds) or Open config folder for an external one
(to fix DRAGONX.conf). Driven by a new rpc_auth_failed_ flag (set on the auth
path, cleared on each connect attempt), with evaluator unit tests.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The conversation header only copies a PEER's address, so a user had no way to
hand out their own chat address for others to message them. Add a "Copy my chat
address" link in the left pane (public App::chatMyAddress() forwards the stable
seed-derived reply z-address; copied on click only, with an "address copied"
toast and a warning if none exists yet).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Seed-backup dialog: a failed "Save to file" was drawn in the success (green)
colour; it's now red on failure, green on success (flag reset on close).
- Encrypt-wallet dialog: the disabled Encrypt button now says why once the user
starts typing ("Use at least 8 characters." / "The passphrases don't match."),
and a "Show passphrase" checkbox reveals both fields.
- Key-export dialog: the raw RPC exception (e.g. "...walletpassphrase first")
is routed through util::friendlyRpcError(), so a locked wallet reads
"Unlock your wallet first, then try again." instead of daemon jargon.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
renderNodeStatusBanner suppresses itself during a UI sweep (the forced demo
state would otherwise paint a banner over every tab). Add a narrow opt-in
(sweep_show_node_banner_) so ONLY the overview-no-peers target shows it; every
other capture is unchanged. The target also clears connection_in_progress_ so
the evaluator classifies the state as no-peers rather than "connecting".
Verified in a full-node + DRAGONX_ENABLE_UI_SWEEP build: the banner renders as
"Connected, but no network peers — the node can't sync · Check your internet
connection and firewall, then restart the node to find peers." (Restart button
only appears with an embedded daemon, which the sweep has none of.)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Capture the states the earlier verification couldn't reach:
- overview-syncing: the balance "catching up — funds are safe" strip (P4).
- overview-no-peers: the node-status banner's no-peers detail (P2; full-node
only — renders as a plain Overview in the Lite sweep binary).
- chat-identity-preparing / -unavailable: the chat empty states that used to
all say "unlock your wallet" (P3). Setup clears the fabricated demo identity
(clearIdentity wipes only the key; the demo message store survives);
sweepEnsureDemoChatIdentity() re-fabricates on teardown so later targets keep
the populated chat UI.
Also make the demo baseline a genuinely synced node (tip_known=true) so the
no-peers banner only appears on its own target, and dismiss the lite first-run
welcome prompt during the demo so it stops occluding every lite screenshot
(snapshotted + restored). Verified: all four new captures render correctly.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Seed backup: the one-time 12s toast that marked itself "reminded" the instant
it fired (so a funded, unbacked wallet was never nudged again) is replaced by
an actionable alert with a "Back up now" link that opens the backup dialog and
persists in the bell panel. "Reminded" now means the user actually viewed the
phrase (set from the backup dialog), so the nudge re-fires each session until
they do, and escalates to a warning once a balance exists.
- Console: fund-moving / key-revealing commands (dumpprivkey, z_exportkey,
z_sendmany, z_mergetoaddress, …) now require a confirming re-entry when typed
or PASTED, honoring the command reference's `destructive` flag that was
previously enforced only by the reference dialog's Run button. (Output
masking of key dumps in the scrollback is still a follow-up.)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Chat tab no longer shows "Unlock your wallet" for every missing-identity
case. A new App::chatIdentityStatus() distinguishes Locked (real),
Preparing (unlocked, deriving the identity) and Unavailable (view-only or a
legacy wallet with no exportable secret — correctly latched). Each gets its
own icon + message, and the Unavailable prompt points at Back up / Migrate
instead of telling the user to unlock an already-unlocked wallet.
- A chat message that sits queued ("Sending") because the wallet is offline or
has no shielded funds for the fee now surfaces the reason (throttled, after a
short dwell), wiring the ready-made chat_toast_not_connected /
chat_toast_need_funds strings that were never shown. The send stays queued
and goes out when the condition clears.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Persisting the sent-sweep watermark means a relaunch runs a windowed sweep,
which can't see a change made outside the GUI (a Console z_importviewingkey,
an external same-seed restore) that adds an old send below the window. This
adds a periodic full re-sweep so that case is still caught within ~7 days.
- sentSweepSafetyNetDue(now, lastFull) = lastFull > 0 && now - lastFull > 7d
(a wallet that has never full-swept doesn't trigger one; the seeding sweep
records the time). lastFullTime / lastFullHeight ride in the sent_sweep
snapshot; any successful full sweep (seed, verify-mismatch or safety-net)
resets the clock. On load, a valid watermark without a clock starts it now,
so even watermarks seeded before this field eventually re-sweep.
- When due, the snapshot forces a full sweep for that cycle. Gated on: a
watermark exists, a routine (non-syncing) cycle, not the Mining page, and
not in full-sweep backoff -- so a full sweep that can't finish on a slow
daemon backs off instead of retrying every cycle.
Degrades gracefully: ~0.4 s on a note-cache daemon; callLong(120 s) with
backoff on an old one (the deferred banding would keep each cs_main hold
short so it fits there too). Helper + persistence covered by tests.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
z_listsentbyaddress re-walks the whole wallet when there's no usable
watermark, which the daemon does under cs_main for every send -- hundreds
of seconds, 400+ s and ~19 GB RSS on a large wallet. The watermark lived
only in memory, so every relaunch, reconnect or daemon restart paid for a
full sweep. This persists it so those cases run a bounded windowed sweep
([z, 0, 2, tip - W + margin]) instead (SENT-HISTORY-SWEEP Part 1).
- The watermark (W = last swept tip), its anchor (A = W - margin) and anchor
hash (H_A = getblockhash(A)), plus the enrichment backlog and the full-
sweep backoff, ride in the encrypted history snapshot as an optional
`sent_sweep` object (schema stays v1; a missing/inconsistent one reads as
"no watermark" = today's behaviour, so older builds and payloads are safe).
- Before the first windowed sweep after a load, the anchor is verified
(getblockhash(A) == H_A). A match means any fork is inside the window; a
mismatch (reorg deeper than the margin, different chain/datadir) or an
inability to check drops the watermark and sweeps the whole wallet.
- The first sweep after a load uses a bigger margin (absorbs notifier lag),
and a watermark older than 2400 blocks uses the long timeout (callLong).
- The backlog is persisted WITH W: a pure z->z send's txid lives only in a
sweep result and would be lost if the window moved past an un-enriched one.
- Watermark changes reach disk (folded into the store signature), and every
hard case still forces a full sweep: reorg (anchor verify), GUI rescan /
zap / import / address change, restoreOriginalWallet, and disconnect.
Deferred (follow-ups, each only turns a correct-but-slow case faster, never a
correctness risk): the banded full sweep for seeding on a pre-note-cache
daemon, the external-import tripwire, and the weekly safety-net re-sweep.
Part 2 (the daemon's decrypt cost) is already on the daemon's
perf/sapling-note-cache branch.
Pure helpers + cache round-trip + anchor-verify/large-window covered by
tests; the getblockhash-after-sweep updates the pinned RPC sequences.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The sidebar unconfirmed-tx badge was recomputed only when the tx list
changed, keyed on last_tx_update plus the list size. That missed the
one case that matters most: a transaction confirming without the list
count changing. last_tx_update did not move for a same-second content
change, and lite never stamped it at all, so the badge stuck on a
stale 0-conf count until some unrelated refresh happened to shift the
list size.
- The badge now keys on tx_revision, which the state bumps on every
content change - including a confirmation flip at an unchanged tx
count - and which lite stamps too, so the count tracks confirmations
on both the full node and the lite wallet.
- The sentinel starts at 0, which tx_revision never takes (it starts
at 1), so the first frame always computes once.
- The recompute still runs only when the revision actually moved, so
the per-frame work the previous guard avoided is still avoided.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
At quit the UI thread drained and joined the mining-control thread and
then called the miner's blocking stop with a 3 s budget. Both ran on the
UI thread, so the window could stall for up to that long before the
shutdown screen rendered.
- Quit now only signals the mining-control thread (no join) on the UI
thread. The blocking teardown — join the control thread, then stop the
miner — moves to a background thread, so xmrig still has a single
driver but the UI thread never blocks on it.
- On the daemon-stop path that teardown runs first on the shutdown
thread, so the miner is fully stopped before the daemon stop begins.
On the disconnect-only quit paths it runs on its own thread, which
the final shutdown joins before tearing xmrig down, so there is never
a second concurrent join of the control thread.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
When the daemon was restarted, the output-parse offset was reset to 0.
The daemon's output buffer is retained across runs, so the next poll
re-read the whole retained tail of the previous run and fed it to the
busy tracker as if it were fresh. A witness rebuild or rescan line left
over from the old run could then be mistaken for activity in the new
one.
- On restart the offset is now reset to the absolute end of the output
at that moment, captured on the lifecycle thread after the stop and
before the start, so the UI resumes parsing only the new run's output
rather than replaying the old tail.
- The capture is handed to the UI thread through a pending flag and
applied there before the next read, both on the normal poll and when
skipping the backlog, so the offset is only ever moved on the thread
that owns it.
Tests cover reading from the captured watermark (only the new run is
seen) versus the old reset-to-0 behaviour (the previous run replays).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
While the seed migration sits on the Confirming gate it polled the
sweep's confirmations and a full wallet balance together on every pass.
That balance call is the expensive one, and it only matters once the
sweep is mined and we do not yet have a post-mine figure, yet it ran
every poll even mid-sync when it cannot move the gate.
- The confirmation count is still read on every poll.
- The balance scan now runs only when it can actually advance the adopt
gate: the sweep is mined, no post-mine balance has been recorded yet,
and no sync is in progress. The manual Refresh button forces it
regardless.
- The decision lives in a small pure helper, and the poller snapshots
the main-thread state it needs rather than reading members off the
worker thread.
- The Confirming screen now claims a leftover remainder (and offers the
re-sweep button) only once the balance has actually been measured; while
it is still unknown it keeps waiting instead of prompting on a sentinel.
Tests cover the gate helper across the forced, syncing, and mined cases.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
A failed wallet unlock was always treated as a wrong guess: the lockout
counter was bumped and the back-off curve armed no matter why the RPC
failed. A busy or warming-up node, a transport timeout, or a wallet that
is not actually encrypted would all eat an attempt and could push a user
into a lockout without a single mistyped passphrase.
The failed call is now classified before it is acted on:
- Only the daemon's "passphrase incorrect" error counts as a wrong
guess and advances the lockout.
- A warming-up or unreachable node, a reply timeout, or a full queue is
reported as a transient busy-node condition with no attempt charged.
- A "wallet is not encrypted" reply refreshes the local encryption state
instead of penalising the user.
- Any unknown error surfaces its message without touching the counter.
The PIN path got the same treatment: when the vault decrypts correctly
but the unlock RPC then fails for a non-credential reason, the attempt
is no longer counted.
Tests cover the classification of each failure kind.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
A secret copied to the clipboard auto-clears after a timeout, but the
deadline was stored as an ImGui::GetTime() value. That clock only
advances while frames are being drawn, so while the app idled (no
redraws) it effectively stood still and the pending clear could be put
off well past the intended window.
- The clear deadline is now a steady_clock time_point, which keeps
counting whether or not the UI is rendering.
- The pump uses the armed hash as its "something is pending" flag rather
than a sentinel deadline value, and compares the real wall time.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
When following the debug.log of a node this session did not launch, every
poll re-read the last 2 MB of the file and re-scanned it for the process
banner. If that node logs to its console instead, the banner is never
there, so the full re-scan repeated on every poll for the life of the
process.
- A miss is now remembered by pid and file size: if nothing has changed
since the last miss, or the file is growing but the backoff has not
elapsed, the scan is skipped.
- The retry schedule re-scans quickly during a fresh-node grace window
(the banner flushes a beat after the pid appears) and then backs off
from 4 s, doubling to a 60 s cap. The memo is cleared on a successful
tie and when the followed process goes away.
Tests: the retry delay is fast within the grace window and then steps
4, 8, 16 s up to the 60 s cap.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Hovering the Paste button in the import-key dialog read the OS clipboard
every frame to draw a ghost preview of the key, and the trimmed
plaintext was rebuilt each frame and never wiped. SDL's clipboard read
is a synchronous round-trip, and the pasted value is a secret private or
viewing key.
- The clipboard is now read only on hover entry and then at most twice a
second, and the classified preview is cached rather than recomputed
every frame.
- The cached preview is a dedicated secret buffer: it is wiped the moment
it stops being shown, wiped on secret wipe, and the transient trimmed
copy is zeroed right after it is classified.
- Trimming moves to a shared clipboard helper shared with the paste path.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Market-tab portfolio groups were tied to a wallet by a hash of its
address set. That hash changed whenever an address was added, so a
group built for a wallet could silently drop out of view once the
wallet gained a new address.
- Groups are now scoped by the wallet's stable id, which is persisted
and never derived from the address set, so adding an address can no
longer orphan a group.
- An older build's address-hash scope fails open (the group stays
visible) and is migrated once per wallet per session to the stable id.
A legacy group is claimed when its old hash matches, when one of its
own addresses belongs to this wallet, or when only one wallet is
known; anything ambiguous is left legacy and stays visible.
- Scope filtering waits until the wallet's addresses are loaded, so a
group never flashes an empty 0.0000 balance and Add stays disabled
until the wallet is resolved. This works on Lite, which never stamps
an address-update time.
Behaviour: an older build opened after this change hides groups tagged
with a stable "w:" scope (they are not deleted and return on the next
upgrade) - worth a release note.
Tests: group visibility across global, stable and legacy scopes with the
wallet resolved and not; the migration rules (hash match, address
overlap, single-wallet claim, ambiguous left alone, idempotent); and a
save/load round trip where an address-set change cannot orphan the scope.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The Mining tab shows earnings derived from transaction history, but it
was not treated as a wallet-data page, so newly mined or paid-out
earnings did not appear until the user left the tab and came back. A
coinbase row also changes category in place (immature becomes generate
at maturity, or orphan on a reorg), and the Recent merge kept both the
old and the new row, double-counting the earnings.
- The Mining tab now gets the cheap between-blocks Recent poll: one
listtransactions per new block, bounded by the 15 s transaction-age
timer, once history has loaded. It never runs a full routine scan and
never pre-empts an owed dirty cycle; tracked sends still take their
normal cycle on any page.
- The shielded-payout probe runs only while the pool miner is running,
since pool payouts are shielded receives while a solo miner's earnings
are transparent coinbase rows.
- The Recent merge drops the previous row of the other coinbase category
when a coinbase transitions, so maturing or reorged rewards are no
longer counted twice. A non-coinbase row sharing the txid is left
alone.
Behaviour change: mining earnings update on the Mining tab per block
instead of only on tab re-entry, and maturing or reorged coinbase rows
no longer double-count.
Tests: the per-block poll gate (one poll per block, due only after
history loads, held while dirty or addresses are dirty), the Mining-page
cycle shaping, and the coinbase transition merge for immature to
generate, generate to orphan, and a shared-txid non-coinbase row.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Setting a PIN always unlocked the wallet with a 5 s probe and then
re-locked it, even when the wallet was already unlocked, so a PIN check
silently re-locked an unlocked wallet. And once the daemon's absolute
unlock window ran out the GUI still showed "unlocked", so the next send
failed with nothing on screen explaining it.
- The Set-PIN check now decides on the main thread: an unlocked wallet
is verified with the time it has left (capped at seven days) and is
not re-locked, so the GUI and the daemon keep agreeing; a locked
wallet still uses the 5 s probe and re-locks. Either way the daemon's
lock state is re-read afterwards, which self-heals any drift.
- When the daemon's unlock window has expired but the GUI still says
"unlocked", a bounded reconcile (once per expiry, 30 s back-off)
re-reads getwalletinfo so the lock screen and the transaction-cache
lock follow the daemon. Lite wallets, which have no daemon timer, are
left alone.
Behaviour change: with auto-lock disabled the daemon still re-locks at
its window, and the GUI now lands on the lock screen instead of
appearing unlocked until a send fails.
Tests: the Set-PIN verify plan (unlocked keeps its remaining time and
skips the re-lock, locked or near-expiry falls back to the 5 s probe and
re-lock, long windows are capped) and the unlock-expiry predicate across
encrypted, locked, no-deadline and not-yet-expired cases.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The wizard persists an "encryption requested" flag so a quit or crash
before the wallet is actually encrypted is not silent. That flag was
only reconciled on the slow-path getwalletinfo refresh, so it could be
left stale, and the clear-and-warn logic lived inline in two places.
- A single reconcileEncryptionPending now runs on every connect and on
every encryption-state refresh, keyed on the parsed getwalletinfo
reply. It clears the flag whenever the wallet is encrypted, however it
got there (deferred request, Settings, another client), and warns once
per session when encryption was requested but the wallet is not
encrypted and nothing is pending or in flight.
- A failed or warm-up (-28) RPC decides nothing, so a 15 s timeout that
forces encrypted=false can no longer mis-clear or mis-warn.
- The flag is also cleared at encryptwallet success and when the user
removes encryption, so neither path leaves a stale flag that would
warn on the next connect. No settings format change.
Tests: the pure decision helper over the flag, RPC-ok, encrypted,
deferred-pending, in-flight and already-warned combinations (clear when
encrypted, warn once when lost, nothing on a failed RPC or no flag).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The auto-lock idle clock was reset whenever any ImGui item was active,
so a text field left focused (a half-filled send form, say) counted as
continuous activity and kept the wallet unlocked indefinitely, well past
the configured idle timeout.
- The idle clock is now fed by real input events (key, text, mouse,
touch, pen, gamepad button) as they are ingested on the main thread,
plus a still-held mouse button so a drag or scrollbar grab that emits
no new event still counts. A focused but untouched widget no longer
does.
- A new header-only IdleTracker owns the clock and never moves backwards,
so an event stamped before the previous one cannot shorten the idle
time.
Behaviour change: a form left focused and idle past the timeout now
auto-locks. The form's buffers are app state and survive the lock and
unlock, so nothing typed is lost.
Tests: a new IdleTracker case covers idle time growing with no events
(enough to trip a 900s lock), activity resetting it, the clock never
moving backwards, and a query before the last activity reading zero
rather than a negative value.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The bootstrap preflight's protected names could be bypassed by names
that resolve onto them, and its cancel and quit paths had two timing
gaps.
- Entry names are judged as the file system resolves them: '\' is a
separator, "." components are dropped and each component loses its
trailing dots and spaces, as Win32 does ("./wallet.dat" and
"DRAGONX.conf." name protected files). The whole archive is refused
for an absolute or drive-rooted name, a ".." component, any ':'
(drive letter or NTFS alternate data stream, "DRAGONX.conf::$DATA"),
an 8.3 short name ("DRAGON~1.CON"), a component of only dots or
spaces (other than "."), and any byte >= 0x80 or ASCII control
character: macOS folds or ignores some Unicode (U+017F, U+212A,
U+200C) onto a protected name, and the published bootstrap archive
uses plain ASCII names. A refused name is echoed
with \xNN escapes. A member is written at the path it was checked
under, and the protected-name check runs on the resolved name.
- The user's Cancel (Settings dialog and first-run wizard) goes through
requestCancel(). It is refused once the old chain data starts being
replaced, and the start of that window re-checks a cancel under the
same lock, so a click lands before the old chain is touched or not at
all.
- Quit cancels the wizard's and the Settings bootstrap when the
shutdown starts; the HEAD and checksum requests now end at once on
cancel, and a cancelled HEAD keeps the partial download. A run
cancelled before the window leaves the old chain as it was. A run
inside it stops extracting and removes its partial chain while the
shutdown screen shows "Cleaning up...", so it ends with no chain data
(wallet.dat untouched; Retry reuses the zip, or the node resyncs).
That wait is bounded: at most 2 minutes from the start of the
shutdown, then 3 s in shutdown(), after which a worker still running
is left to the process exit. Only a cleanup that overruns that cap
can leave a partial chain, which dragonxd then answers with its
reindex offer.
Tests: refusal of the Windows-resolved, non-ASCII and control-character
names with the escaped echo; the protected-name list incl. the resolved
forms and a planted -wallet file, conf and database/ that stay
byte-identical; the user-cancel window; the bounded quit wait and
safeToAbandon(); a cancel ends a HEAD request that never answers.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
ensureLogoTexture() still freed the chat emoji, header logo and coin logo
textures it replaced at once, while every other theme-driven replacement
already goes through retireTexture() and is freed at the next preFrame().
It runs at the top of render(), so nothing referenced them yet; this is
hygiene, so that the same rule covers every replaced texture.
- ensureLogoTexture(): the three direct DestroyTexture() calls become
retireTexture(). Cost: the old textures live one extra frame on a theme
or accent change.
- setGradientTexture() (unused today) retires the texture it replaces
instead of dropping it.
- No drain at shutdown: the process exits and the live logo and gradient
textures are not freed at exit either.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A rescan or key import holds the node's cs_main for the whole wallet
scan. On the shared clients it hit the 30 s curl timeout, reported a
failure that wasn't one, abandoned a daemon RPC slot and flipped the app
into "node busy".
- One long wallet RPC at a time (rescan, importprivkey, importaddress,
importwallet, z_importkey, z_importviewingkey, z_importwallet) runs on
its own ungated client with no client timeout (rpc/long_call_lane.h).
It can be aborted from any thread, and an abort is never lost, even
one that lands before the client opens.
- The GUI rescan, key import and sweep use it. The sweep's fund-moving
body is unchanged; only the client it calls differs. runtimeRescan()
returns bool, and a refused post-bootstrap reconcile stays armed.
- The console routes rescan and the import commands through the lane;
its input stays usable meanwhile, and a refusal is printed like any
other result. RPCClient gains callRaw(.., timeoutSec).
- Restart, wallet switch, delete-chain, bootstrap (now also from
Settings) and the other node-stopping flows are refused while it runs,
with a message saying a key import or rescan is still running; quit
asks for confirmation. The lane is aborted at quit.
- A second import or rescan is refused before the key is copied anywhere.
An import cut off after the request reached the node (a lost reply,
or a proxy's 502/504 while the node still imports) says the key may
already be in the wallet and to run Settings -> Rescan. Only the
node's own HTTP-layer refusals (401, 503, work queue full, unhandled
request) say that the key was not imported. A pure
classifyLongCallCutOff() makes that choice.
- New strings in all 8 languages; console results stay English.
Tests: the long-method list and the console's routing; one lane at a
time; an abort lands whether it comes before the client opens or during
the call; a call with no client timeout outlasts a 3 s scan, where a 1 s
gated client times out and marks the node busy; classifyLongCallCutOff()
for every failure kind with and without a sent request, and from raw
HTTP replies (502, 504, a plain 500 -> cut off; 401, 503, queue full,
unhandled -> refused).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A passphrase change left the PIN vault sealing the old passphrase. The
next PIN unlock opened the vault, walletpassphrase answered RPC -14, and
the generic catch ran the lockout curve on a correct PIN, while Settings
kept showing the PIN as active.
- Passphrase change: when the active wallet's vault exists at completion
(same vault path as at dispatch, passphrase actually changed), the
vault is marked out of date for the session and kept on disk: PIN entry
is no longer offered, a warning toast says so, and Settings shows
"PIN out of date" with a tooltip on how to fix it. The Change
Passphrase dialog notes beforehand that a PIN will go out of date.
- PIN unlock: RPC -14 after the vault opened is treated as "PIN out of
date": passphrase entry, a message, no attempt counted, no lockout, and
the vault is not deleted (-14 is only the daemon's answer; the vault may
hold the only copy of a passphrase that opens an older backup). After a
restart Settings shows the PIN as active until the first PIN attempt.
- A malformed vault (zero-filled by a removeVault() whose unlink failed,
truncated, unknown version) is treated as missing: passphrase entry,
"PIN file unreadable", no penalty. New SecureVault::isWellFormed().
- Race guards: Set/Change/Remove PIN are disabled while a passphrase
change runs, and Change Passphrase while a PIN operation runs.
- Setting or removing the PIN, or switching wallets, clears the mark;
setting a PIN offers PIN entry again. Change PIN keeps the mark (the
vault still seals the same passphrase).
- Pure decisions in data/pin_vault_policy.h, unit-tested. New strings in
all 8 languages.
No vault, wallet or settings format change.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The full UI sweep (dev builds only) ran its demo surfaces against the
real profile. Its teardown deleted wallet-{cold,trading,mining,donations,
2023,payroll}.dat from the real datadir, it created zero-filled wallet
files and picker-demo/extra-wallets folders, its demo wallet-index upsert
re-minted the active wallet's scopeId (hiding "w:"-scoped contacts), the
Contacts tab could re-attach legacy-scoped contacts to such a throwaway
id in addressbook.json, and it persisted the demo active wallet file and
Mining pool worker.
- Preflight: a full sweep is refused (toast + log line) while a real
workflow is in flight: a pending seed_migration_* setting or open
migrate dialog, a wallet switch, an encryption/import workflow, an
encrypt/PIN/lock operation, the seed-backup dialog, or a bootstrap.
The DRAGONX_FULL_SWEEP hook then exits as for an empty catalog.
- Persistence latches: AddressBook (snapshot on the first sweepBegin,
save/load latched while a demo-contacts surface is shown), WalletIndex
and Settings (active wallet file, contacts view mode, pool URL/worker)
for the whole sweep. Settings::sweepEnd() is the last statement of
clearDemoWalletData and flushes once only if a save was suppressed; a
quit mid-sweep writes nothing to the profile (the 0700 sandbox, with
zero-filled demo files only, may stay in the temp dir).
- Scope ids: during a full sweep activeWalletScopeId() returns the
persisted id or "" and never mints one, so the Contacts tab cannot
move legacy-scoped contacts to a throwaway id. The only address-book
write a sweep can still cause is the one-time legacy re-attach to the
wallet's persisted id, which opening Contacts does anyway.
- Sandbox: demo wallet files and folders go to a fresh random 0700 dir
under the temp dir (exclusive create, a pre-existing path or symlink
is refused), removed at sweep end. The teardown that deleted those six
wallet files is gone.
- Cleanup: the migrate-to-seed state, wizard phase, demo chat
conversations and a fabricated demo chat identity are reset at sweep
end; the seed-backup surface always shows the demo phrase and a real
z_exportmnemonic result is dropped while the sweep runs.
- i18n: one new key (screenshot_sweep_refused) in i18n.cpp and the 8
language files.
Tests: the address book snapshots only on the first sweepBegin, keeps
addressbook.json byte-identical while latched and restores the real
book; the wallet index keeps wallets.json and the real scope id, also
with the background writer attached; Settings restores the latched
values and flushes once only when a save was suppressed; the sandbox
helper refuses a pre-existing path or symlink and removes only a
prefixed dir, never a symlink's target.
Release note: v2.0.x UI sweeps could delete wallet-{cold,trading,mining,
donations,2023,payroll}.dat from the data directory and re-scope
contacts; this cannot be undone automatically.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
reloadThemeImages() replaced gradient_tex_ in each of its three branches
(image loaded, load failed, no background) without freeing the old
texture, so every skin activation, including the one at startup, leaked
one texture.
The logo and coin textures were freed immediately instead, although the
current frame's draw lists can still reference them: the window backdrop
records its images at the start of the frame and skin switches happen
mid-frame (Settings, Ctrl+arrow keys), and the screenshot sweep switches
skins after RenderDrawData() but before the secondary viewports are
rendered. That hands the renderer a freed texture.
Textures replaced mid-frame are now retired to a queue (retireTexture()
also zeroes the member) that the next preFrame() frees first, after the
previous frame's draw data has been rendered. The old gradient is
retired at the top of reloadThemeImages(), and the logo and coin where
they used to be freed. ensureLogoTexture() still frees directly, since
it runs at the top of render(), before this frame draws a logo.
Shutdown does not drain the queue.
Measured with a temporary live-texture counter over a screenshot sweep
of all 10 skins: before, 6, 7, ... 15 live textures (one more per skin);
now a constant 5. The first screenshot after each skin switch is
pixel-identical to before on all 10 skins, and the full sweep matches
within noise.
Not verifiable here: DX11 at runtime.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
main() built the font atlas before App::init() loaded ui.toml, so the
first build used the hard-coded fallback sizes (H1 24 px, Body2 12 px).
App::init() then rebuilt for a saved font scale above 1.0, still before
the schema load and so still at the fallback sizes. Only the color-emoji
rebuild requested for the first frame reached the ui.toml sizes, so
color-emoji users built the atlas twice at font scale 1.0 and three
times at 1.5.
Users with chat_emoji_color=false never got that last rebuild. They
built the atlas once or twice and ran the whole session at the fallback
sizes, e.g. Body2 at 12 px instead of 15 and H1 at 24 px instead of 20.
Now:
- App::loadBaseUiSchema(), the ui.toml load moved out of App::init()
unchanged, runs in main() before both InitImGui paths (DX11 and GL),
together with the saved font scale and emoji style from the early
settings read. It is idempotent, so the call left in App::init() is a
no-op;
- the two rebuild triggers in App::init() are gone;
- Typography records what the atlas was built from (the 15 schema sizes,
the schema's font scale, the effective emoji style and the schema
generation), and preFrame() rebuilds once when they differ. While the
schema generation and the emoji style are unchanged this costs two
compares per frame.
Measured on the Lite build (color and mono emoji, font scale 1.0 and
1.5, English and Japanese): every run builds the atlas exactly once, at
the ui.toml sizes times the scale (H1 20 px and Body2 15 px at 1.0,
30 px and 22 px at 1.5), and ui.toml is loaded before the window and GL
context are created. The packaged Lite zip and AppImage log one atlas
build instead of two. Color-emoji screenshot sweeps match the previous
build within noise.
Visible changes:
- users with chat_emoji_color=false had been rendering the whole UI at
the fallback font sizes; they now get the designed ui.toml sizes, so
every mono-emoji screen differs from before and matches the
color-emoji layout;
- a user skin whose [fonts] differ from ui.toml now rebuilds the atlas
once when it is activated or left; on a runtime switch the atlas used
to keep the old sizes until a restart. With a test skin that sets
Body2 to 18 px, startup builds twice and ends at 18 px; switching to a
bundled skin rebuilds once, switching among the 9 bundled skins
rebuilds nothing (they carry an identical copy of [fonts]), and
switching back rebuilds once;
- editing ui.toml while running still gives exactly one rebuild.
Not verifiable here: the Windows DX11 path with the schema loaded before
its first FreeType build, macOS, and the interactive font-scale slider,
Alt+scroll and emoji toggle, which were checked by reading the code.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
On a first run or after an upgrade, Windows release builds wrote the
bundled Sapling params, asmap, node and miner (about 88 MB) in
App::init, before the first frame: 0.5-3 s of frozen blank window, 5-30 s
on a slow disk or under antivirus. Zip, AppImage and .app builds copied
the bundled files into the daemon dir and hashed the 51.5 MB of params on
the UI thread at the first node start, a 0.3-1.5 s freeze in the wizard
or the connect loop.
A stat-only check at startup decides whether anything is missing, stale
or unverified; a normal launch starts no thread and shows nothing.
Otherwise one background job extracts (Windows) or copies (Linux, macOS)
the files, then verifies the params once:
- The first frame paints at once. If the job takes longer than 0.4 s, a
chip at the bottom shows "Preparing wallet files... N%", then
"Verifying wallet files...".
- A node start on the UI thread while the job runs is deferred and
replayed when it ends (never into a bootstrap download, a restart or a
live connection); worker threads wait, and give up on quit. After an
upgrade that ships a new miner, the node starts after the miner is
written instead of before. Pool-miner starts and the miner version
check wait too, so the old miner is never run or reported while it is
being replaced.
- Each file goes to "<name>.partial", is flushed and renamed into place
under one lock shared with the daemon start, the bundled-node
reinstall and the miner extraction, and the job stops between 4 MB
chunks at shutdown. An interrupted write used to leave a truncated
binary that was trusted forever; a leftover .partial is now removed.
Other behaviour changes:
- Linux and macOS full-node builds copy the bundled node files (about
77 MB) into the daemon dir at first launch, even if the wallet ends up
on an external node; before, this happened at the first embedded start.
- Lite no longer writes the 53 MB of params and asmap it never reads.
- A disk-full or permission failure that leaves the node files unusable
shows a warning; a miner blocked by antivirus alone does not.
- A corrupt or truncated param in a zip, AppImage or .app install is
replaced from the bundle and verified again instead of trusted forever.
- Reinstalling the bundled node holds off the connect loop and a deferred
start for its whole run (either could launch the old binary while the
new one was written), and is refused while another restart is running.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
New address, Import key, Encrypt and Remove encryption called the
backend on the UI thread. Each ends in the backend's save, which waits on
the lock a scan holds for its whole duration and every refresh read holds
briefly, so during a scan the window froze until the scan ended. A create
or restore with a passphrase encrypted and saved just after its first
scan had started: for a restore, a freeze for the whole initial scan. A
server switch destroyed the old controller on the UI thread and joined
its refresh worker, which can sit for minutes in a read against a dead
server (each balance, notes or list read retries for ~62 s).
- The four ops run one at a time on a background lane and their results
are applied on the main thread; Settings shows "Working…" meanwhile.
Key and passphrase copies are wiped after the call, or when a queued
op is dropped.
- A create or restore encrypts and saves (an open unlocks) before the
first scan starts, so the save takes milliseconds and the scan runs on
the encrypted file.
- Import, Encrypt, Remove encryption and Receive's New address are
disabled while a scan runs ("Wait for blockchain to sync"); Redownload
blocks is disabled while an op is queued.
- A switch retires the old controller without blocking (queued ops
dropped, pending ones reported failed, its client shut down) and
destroys it on a background thread. It no longer saves on the way out:
the new client re-scans the blocks since the last save, as after a
restart.
- A switch waits for an op in flight, at most 15 s, so the op can't
finish and save on the old client while the new one owns the wallet
file. The Network tab already shows the new selection meanwhile.
- The refresh worker stops between backend commands, skips the remaining
server reads once one fails, and then retries after 4, 8, then 15 s
instead of every 2 s.
- Quit waits up to 2.5 s for an op in flight, e.g. Encrypt's save.
Encrypt and Remove encryption now report a failed save: the backend
returns it as a JSON error that the old check never saw, so the failure
was reported as success. The trimmed copy of a key typed into Import is
now wiped too, and stopping the refresh worker can no longer miss its
wakeup.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The daemon's RSS was read on the UI thread: a scan of every /proc entry
on Linux (1-7 ms), a forked ps on macOS (up to ~30 ms). The mining poll
probed every 1-5 s on every tab, and the Mining tab's memory bar probed
again every 1.5 s.
util::BackgroundSampler now runs the probe on one lazily started thread
and caches the result. getDaemonMemoryUsageMB() returns the last sample
and asks for a new one at most every 3 s, and only while the Mining tab
memory bar or the Peers tab Memory cell is drawn: nothing on other tabs
or while minimized. EmbeddedDaemon::memoryProbe() hands the sampler a
pid snapshot (on Windows the value read from the owned process handle),
so nothing off-thread touches daemon state. The mining poll does no I/O
any more; it carries the last sample. Both shutdown paths stop the
sampler.
Fixed along the way: an unreadable /proc made the directory iterator
throw on the UI thread; the scan now just stops.
User-visible:
- the Peers tab Memory cell keeps updating during sync and shows an
external daemon's memory (it showed the mining poll's value, which
pauses during sync and covers only the embedded daemon);
- the Mining tab memory bar refreshes every ~3 s (was 1.5 s).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Finding the lowest block a node still has (Settings > Rescan, and the
automatic reconcile after a bootstrap) ran a binary search of about 42
synchronous getblockhash/getblock calls on the UI thread, each waiting
for the RPC connection behind any wallet scan (10-20 s on a big wallet)
and each with a 30 s timeout. The window froze for that long.
It now runs as one task on the wallet RPC worker, bounded to 90 s, and
the answer comes back on the UI thread; meanwhile the dialog shows
"Checking which blocks..." with Rescan disabled. An answer from an
earlier connection counts as "could not tell", and one for an earlier
opening of the dialog is dropped. The search probes block 1 first, so a
full-history node answers in one probe, and it answers "could not tell"
at once while a runtime rescan holds the lane.
Fixed along the way: any probe error (timeout, busy node) read as "block
missing", so a busy node could get a bogus "bootstrapped, rescan from
the tip"; and a non-string getblockhash reply never called back, leaving
the dialog on "Checking..." for good. Only the daemon's "can't read
block" error now counts as missing; anything else means "could not
tell".
User-visible:
- after a bootstrap, a probe that cannot tell is tried up to 6 times,
30 s apart and only while the node is not busy, then a toast points
to Settings > Rescan, instead of falling back to the -rescan
restart, which fails on a bootstrapped node;
- the Settings Rescan dialog warns when the probe could not tell.
Two new strings, translated into all eight languages.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The CoinGecko price, tickers and market chart and the exchange candle
GETs ran on the wallet's RPC worker, the same FIFO lane as balance,
history and sends. A slow or blocked endpoint held that lane for up to
10-12 s per price or ticker fetch and 2 x 30 s per chart or candle job,
and a wallet scan in turn delayed the price. At quit a hung GET held
shutdown until the 15 s watchdog cut teardown short.
They now run on their own RPCWorker thread; results are applied on the
main thread and wake the idle loop. A new util::httpGet takes a cancel
flag (checked by libcurl's progress callback), sets CURLOPT_NOSIGNAL and
returns the HTTP status and error; httpGetString wraps it. Shutdown sets
the flag, so queued GETs return at once and one in flight aborts within
about a second. The price uses a plain in-flight flag instead of the
connection-scoped refresh job, so a disconnect no longer throws away a
good in-flight price and leaves it loading until the next tick.
User-visible:
- chart and candle GETs use a 5 s connect / 20 s total timeout (were
15 s / 30 s);
- the price request goes through the shared GET: User-Agent
ObsidianDragon/1.0 (was DragonX-Wallet/1.0) and a 16 MiB reply cap;
- market fetches on connect start alongside the wallet refresh instead
of queuing behind it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Every history refresh, address refresh, connect and lock-state check
rewrote the encrypted tx-history snapshot on the UI thread: a JSON
document of every row, its encryption, then INSERT OR REPLACE and a
prune, an estimated 40-120 ms on a 5k-transaction wallet, even when
nothing had changed.
Now a signature of what would be stored (about 1 ms on 5k rows)
decides whether to write. Confirmations count only by the thresholds
the UI reads (conflicted, pending, 1-9, 10-99, 100+); per-address scan
heights, exact confirmations and the tip drift every block and form a
separate part.
- Routine refreshes write a content change at most every 20 s (a
deferred one is released from the update loop) and the per-block
drift at most every 10 min. Nothing changed means no write.
- A mining-address relabel, a passphrase change, a rescan, a
disconnect, a wallet lock and exit write whatever is not on disk yet,
at once.
- The UI thread only copies the rows and the key into a job; a single
background writer (newest job wins) builds, encrypts and stores it. A
failed write is retried.
- Exit queues the final write so it overlaps the daemon stop, then
waits up to 3 s for the writer; a disk that hangs past that keeps the
previous snapshot.
- A loaded snapshot counts as stored, so the first identical refresh
doesn't rewrite it. An address refresh re-keys the snapshot only when
the address set changed.
After a hard kill the snapshot can now be up to ~20 s behind for rows
and 10 min for scan heights, exact confirmations and the tip (it was
one refresh); the first refresh replaces it either way. Lite builds,
which never read this cache, no longer write it on Refresh or a mining
relabel, and the screenshot capture mode's demo rows never reach it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Every thread-count change on the Mining tab saved settings.json and,
with a miner running, stopped and restarted it: xmrig re-inits its
RandomX dataset and reconnects to the pool, seconds of lost hashing
each time. A drag across the grid did this once per tile crossed (up to
~24), with a "Starting pool miner" toast each; every -/+ click or Enter
did it again.
The tab now hands each pick to the App, which puts the newest in
settings at once and applies it when the input settles: on drag
release, or 0.5 s after the last click, even if the Mining tab was left
in the meantime. The header, RAM estimate and tile fill still follow
the drag live.
- Pool: the restart is decided on the mining-control thread, after every
earlier start or stop has run. It never starts a miner the user
stopped, is a no-op when xmrig already runs at that count, and is no
longer silently skipped when the pick lands in another restart's stop
window (which left xmrig on the old count).
- Solo: one setgenerate per gesture. A pick made while one is in flight
now waits for it instead of being dropped (another stale count).
- Clicking the tile that is already selected does nothing; it used to
save settings and restart the miner.
- With idle thread scaling on, the grid no longer snaps back to the old
count while a pick is on its way to the miner. A pool auto-balance
restart keeps a count still queued.
- Idle thread scaling changes the pool count through the same retarget.
Its correction after a start compares with the count last requested,
not the xmrig API's lagging report, so it comes about 1 s in instead
of after the first API report (3-5 s).
- A retarget whose pool config no longer validates shows the same error
and stops the miner, as the old stop-then-start did.
- While a benchmark runs a pick is saved, but the miner change is
dropped: the benchmark owns the count until it ends.
A crash within 0.5 s of a pick, or mid-drag, loses that pick; every
clean exit saves it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Settings::save() and WalletIndex::save() ran the atomic write (temp
file, fsync, rename, directory fsync) on the UI thread: on every
preference change, and for wallets.json after every address-scan pass
whose wallet metadata changed, i.e. repeatedly during sync, when the
disk is busiest. Each took milliseconds to tens of milliseconds, and
seconds under heavy disk load.
The JSON is still built on the caller; the write is queued on an
App-owned CoalescingWriter, latest wins per file, one write at a time.
- After a fund- or security-critical setter (seed-migration stage,
encryption pending, PIN enabled, active wallet file, wizard completed)
the next save still waits until the file is on disk, so e.g. the
sweep-submitted marker is durable before the sweep is broadcast. A
failed durable save keeps the next one durable.
- Saves before the writer is attached (the upgrade re-save at startup),
and in tests and tools, stay synchronous.
- main() flushes the queue (at most 3 s) before and after
App::shutdown(), before the single-instance lock is released, so a
relaunch never reads a stale settings.json.
A kill or power loss right after an ordinary save can now lose that
last preference change; the previous file stays intact. WalletIndex
saves report "queued" rather than "written" (no caller reads it). On a
wedged disk, exit can wait up to 2 x 3 s longer, inside the 15 s
watchdog.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
hasPendingRPCResults() becomes hasPendingBackgroundResults(): the one
check the idle loop makes, every <= 200 ms, for work that finished on a
background thread and still has to be applied by update(). Lanes added
later put their term there. It still covers only the two RPC worker
lanes; no behaviour change.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
"Keep in each address" posted one task that called z_mergetoaddress for
every batch of every address back to back; each call is a whole-wallet
scan under cs_main, and the daemon runs the ops one at a time anyway.
It is now an App-owned services::ConsolidationRun:
- one RPC at a time; the next merge only after the previous batch left the
queue and a gap as long as the last call took (2-120 s);
- pauses while the node syncs, the wallet is locked, a send of ours is in
progress, or another wallet scan is running;
- failures classified: nothing that may have queued is ever resent; a
breaker stops after 3 dead addresses or 3 failed ops in a row;
- each merge is charged to the scan budget; one re-read and one forced
balance at the end;
- it keeps running when the dialog closes: a status-bar chip, a run view
with progress, the wait reason and a Stop button.
The "Into one" merge can no longer be confirmed twice while its call is
in flight. fake_dragonxd gains FAKE_SCAN_SECS, FAKE_MERGE_SERIAL and
FAKE_MERGE_FAIL.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A z_sendmany submit set off a burst of wallet scans before anything had
changed. Ops now re-read the wallet when they finish, by who started them:
the user's sends and dialog ops get a re-read plus one forced balance once
none of their ops is pending; client auto-shield re-reads unforced; chat
messages and splits refresh only the chat note view. A pending send no
longer forces scans, and chat txids no longer enter the tracked-send set.
Chat ingest skips stored messages before decrypting and caches
undecryptable ones per identity. The inbox scan runs when getwalletinfo's
txcount changed (a cheap probe, at most 2% duty) or a cost-scaled safety
net is due; the note-buffer scan runs on need. Both are admitted by the
scan budget, on a steady clock. The chat payer comes from a per-address
view of verified notes, with debits re-applied across in-flight scans;
splits are sized to one payer and only when they can refill the buffer.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Connect gate: after the node starts answering, wallet scans wait until the
lock state is known and the node knows its tip (or the wallet has fewer
than 500 transactions), at most 20 s. A node already behind latches the
sync profile from the connect's getinfo. Meanwhile a list-only pass keeps
the address list and the cached history up, and the session's first
balance and per-address balances are read once even while catching up,
so the wallet shows its last-synced values instead of 0.
- Background: minimized, hidden, or covered without focus (3 s debounce,
any input cancels) polls only the chain state every 15 s. Wallet scans,
the mining poll, price, nudges and auto-lock interaction tracking pause;
auto-lock now fires while minimized. Leaving a held profile marks every
timer due again.
- Tab entry marks the timers due instead of forcing scans; a stale list or
a never-loaded history still loads on entry.
- The scan entry points own the hard gates, failed enumerations keep the
previous lists behind a retry hold, and failed balance calls never
overwrite known balances with 0.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The balance, address scan, history and probe were throttled one by one at
10% each, which added up to 30-40% of dragonxd's cs_main with all of them
due. services::WalletScanGovernor runs one wallet scan at a time across
both RPC lanes and keeps one shared quiet clock at 15% of wall-clock (cut
when the node is behind). Waiters are served first in, first out, with a
head start for what the page shows and a starvation floor of max(30 s
visible / 900 s hidden, 15x the stream's cost). Explicit work (a dirty
set, the user's send, a first load) is never refused but is charged.
Routine scans run only when the tip, the mempool size (getmempoolinfo:
no cs_main) or a wallet-mutation epoch changed since their last good scan,
or a safety sweep is due. Pages that don't show the balance poll it at
most every max(10 min, 50x its cost) (2 min with client-side auto-shield);
coming back into view with an old value fetches it at once. The staleness
badge counts from the last poll that confirmed nothing changed, and hides
while the budget holds the balance within its guarantee.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Every block re-ran z_listreceivedbyaddress for every z-address (O(wallet)
each, under cs_main) and a whole-wallet z_listsentbyaddress. On a 40-address
wallet that was ~80% of wall-clock. The history now advances in passes:
- an address is rescanned when it is stale by a number of blocks scaled to
its measured cost (8 to 500), never-scanned and hinted addresses first
(a hint: z_listunspent shows a note the history doesn't);
- z_listsentbyaddress covers only the blocks since the last good sweep
(+100 reorg margin); the whole-wallet sweep runs once per connect, with
a 10 min / 1 h / 6 h backoff after real failures;
- listtransactions pages are sized from last time, and an unchanged tip
reuses the transparent rows;
- found-but-unenriched txids carry over as a backlog; only -5 is a
definitive "not ours" for z_viewtransaction;
- between blocks, a cheap recent poll plus a one-address probe.
Duty model for a 40-address wallet at 10 s per scan: ~9% instead of ~80%.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The Core poll ran z_gettotalbalance twice (minconf 0 and 1), each a whole-
wallet scan under dragonxd's cs_main. The spendable (minconf=1) call now
runs only after the display call succeeded, and only when something is
unconfirmed or pending or the exact total moved: about 95% of polls make
one scan instead of two. A forced post-send balance can no longer be lost
to a refused enqueue, and pending-send debits aren't re-applied each tick.
z_getoperationstatus is filtered to our opids with its real argument form
([[id,...]]; the old ["id"] was rejected), falling back to the unfiltered
call if a daemon refuses the filter. The Shield/Merge dialog's own polls
are filtered and session-guarded too, and finally reach Done; the dead
RPCClient wrappers are deleted.
The Merge/Consolidate scope scan becomes a tested state machine: one scan
at a time, failures latch behind a Retry button, only "node not ready"
retries by itself (10/30/60 s, at most 3), Lite never scans. Its 120 s
calls are maintenance calls: every main-thread disconnect aborts one in
flight first, so a lifecycle action no longer freezes the UI behind it.
An aborted merge that was already sent says it may still go through.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>