The chart fetch and the Market tab both resolved the saved exchange and
pair names into the registry, with their own near-identical scans and
slightly different fallbacks. Keeping two copies risked the chart and
the chips disagreeing about which pair was selected.
- A single resolver maps the saved names to a registry index, matching
the exchange name (any exchange when none is saved) then the pair
display name, and falling back to the registry's first pair, the UI
default. The chart fetch and the Market tab both call it.
- The Market tab now derives its selection indices from the saved names
against the current registry every frame, so the indices stay correct
when the live exchange list replaces the fallback; a steady-state fast
path skips the scan once the indices already name the saved selection.
Tests: the resolver returns the right index for a matched exchange/pair,
an empty-exchange match, and the first-pair fallback on misses.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
When the daemon disconnects, teardown stops the fast RPC worker without
draining its queued main-thread callbacks. The console busy gate was
released inside that callback, so a command whose callback was discarded
left the input disabled for the rest of the session.
- The busy gate now drops from an RAII guard at the end of the worker
job itself, so it is released however the job ends: success, error, or
a worker torn down undrained.
- The result is published to the console before the gate drops, so a
freed-up console always has the output queued.
- An unknown, non-std::exception failure is now caught and reported
instead of escaping the job.
User-visible: the Console input can no longer stay stuck disabled after
a reconnect.
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>
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>
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>
The History tab copies every chat txid into a fresh set each frame to
badge and filter chat transactions: M string allocations for M chat
messages. ChatStore gains hasChatTxid(), an O(1) lookup in a txid index
that is rebuilt only after the store changed (append, delivery update,
erase, clear) and never built for an empty store. clear() also drops
the index, so no chat txids linger after a lock. The History tab moves
to it in the next commit.
The chat note-buffer pump, which runs every frame while chat is idle,
built the whole conversation-id list just to see whether the store was
empty; it now asks store().empty(), O(1) instead of O(M).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
WalletState had no change signal for its transaction list, so a UI
cache had to fingerprint all N transactions every frame to notice an
update. It now carries tx_revision, a counter that only increases;
clear() on disconnect bumps it instead of resetting it, so a cache
built before a reconnect or wallet switch can never match again.
- Every writer of the list bumps it: the full-node refresh, loading the
cached history, adding, completing and removing a pending send, a
mining-address relabel, the lite refresh and the screenshot demo
rows.
- The full-node refresh and the lite refresh (every ~2 s) swap in the
new list only when it differs field by field, the amount compared
bit-exactly because -0.0 and +0.0 print differently. An unchanged
refresh keeps the list and the revision; the compare costs O(N) per
refresh, not per frame.
Nothing reads the counter yet.
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>
"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>
Lite opens an encrypted wallet locked, but the model saying so arrives
a moment later. The first seed export therefore failed with "locked"
and disabled chat for good, so encrypted Lite wallets never got chat.
- While the wallet is locked, a failure recorded before the lock was
seen is cleared, so provisioning runs again after the unlock (like
lockLiteWallet()). Nothing is retried while locked: that branch
returns before any RPC or backend call.
- Lite waits for the first model's lock state before exporting the
seed.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The Market tab calls the exchange-list, price-history and candle
fetches every frame, so a failed fetch (offline, rate-limited, a
venue's API down) was retried on the very next frame, ~20 requests a
second.
- util::FetchBackoff: 30 s doubling to a 10 min cap, +/-20% jitter.
The exchange list, the price history (a partial result counts as a
failure) and the per-pair candles each back off on failure.
- Candle backoff is per pair. Switching to another pair still loads at
once, a failed refresh keeps the on-screen pair's last candles, and
switching back to a pair that is backing off shows the aggregate
chart rather than the previous pair's candles.
- The Market tab's refresh button retries at once (at most every 5 s).
A price fetch (60 s timer, no backoff) that succeeds after failing
means the network is back, so it lifts all three backoffs.
- A partial price history no longer marks the chart loaded: with no
fetch timestamp, the 30-minute reload gate measured system uptime
and blocked retries for the first 30 minutes after boot.
- The exchange list goes through httpGetString (TLS verified, size
capped) with short timeouts (5 s connect, 12 s total, as it runs on
the shared RPC worker) instead of its own curl code. A failed fetch
is retried; it used to be tried only once per session.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A send stays tracked until z_viewtransaction shows an outgoing output.
Shields, merges/consolidations and chat note splits pay only the wallet
itself, so they never showed one. They stayed tracked for the whole
session and forced a full history refresh past the scan throttle on
every tab.
- A tracked send whose view has no outgoing output is retired once it
is mined (1+ confirmations) or conflicted.
- Tracked sends are enriched first, and reaching the per-cycle view cap
no longer ends the loop, so the cap can't starve them.
- Backstop: a tracked send still unsettled 20 blocks after it was added
(expired, dropped from the mempool) stops being tracked.
- Tracked sends are cleared on disconnect: they belong to that
connection's wallet.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Connected-but-locked fell through to tryConnect() on every Core tick
while the lock screen sat idle: new sockets, getinfo and getwalletinfo,
a forced address scan, a CoinGecko fetch and a wallet-index rewrite.
Its wallet scans also held cs_main ahead of the user's
walletpassphrase.
- While locked, poll chain state only (refreshCoreData without the
balance). That keeps the sync height current and the 3-strike
lost-connection check armed, so a node that exits while locked is
still restarted. Pool mining keeps the lock screen down, so while it
runs the throttled balance, the peers and the lock state (for an
unlock done in the Console tab) keep refreshing too.
- Warmup polls count refused/reset connections toward the
lost-connection check, so a node that died during warmup is
restarted instead of leaving the loading or lock screen over it.
- The wallet salvage/degraded detectors run at connect and again when
warmup ends: for encrypted wallets they relied on the repeated
tryConnect.
- restartDaemonAfterEncryption records the wallet as encrypted and
locked itself (the wizard's deferred path never did), and marks the
main client disconnected so the reconnect path picks up the restarted
node, aborting any in-flight call first.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
DragonX is a private chain (-ac_private=1): consensus rejects any
non-coinbase output to a transparent address, so a t-address can only
ever receive mining rewards, and its coins can only move to a shielded
address. The wallet treated them like ordinary recipients.
Block paying one:
- Send: a transparent recipient shows "Mining address - can't be paid"
and disables Review Send (tooltip explains; ask for a zs... address).
Typing, paste, contacts and payment URIs all go through this field.
- Overview drag-to-transfer: a mining-address row is never a transfer
target (it only reorders).
- Pool payout must be shielded (pools pay with ordinary transactions):
field tooltip, disabled Start button and the start check say why.
- isValidRecipientAddress() is now shielded-only; new isDragonXAddress()
and classifyRecipient() (Shielded / MiningOnly / Invalid).
Call them mining addresses:
- Overview: "Mining rewards" card (amber "Not yet shielded" when there
is a balance; hover explains; click opens the Shield dialog), "New
mining address" button with a tooltip, rows typed "Mining".
- QR / label dialogs, portfolio filter, contact badge ("Mining only"),
Receive notifications.
i18n: 17 new keys in all 8 languages, 12 dead keys removed, CJK subset
rebuilt. Tests: recipient classification, translation completeness.
UI sweep: new "send-mining-recipient" target.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
S12 - our node across sessions:
- The node the wallet launches is recorded in <datadir>/obsidiandragon-
dragonxd.pid (pid + process start time + exe). A later session that
finds it still running (after "Leave running" / keep_daemon_running)
stops it gracefully on quit and waits for that exact process (no time
limit, "Leave running" offered); never signals or kills it. A reused
pid or a reboot never matches; stale records are deleted.
- stop_external_daemon is now honoured when this session only connected
(no controller); it was ignored.
S13 - the node's own log on every platform:
- No -printtoconsole on Linux/macOS: the node writes its timestamped
debug.log (which outlives the GUI) and we tail it next to the stderr
pipe. Tail path from the datadir or the isolated override (Windows had
%APPDATA% hard-coded, wrong for migrate-to-seed starts).
- After the node shrinks debug.log at startup, resume at this run's
banner instead of re-reading the old run's tail.
- A node we didn't launch (left running, or found running) feeds the
busy tracker from its debug.log, tied to the live process (our record,
or POSIX hushd.pid) and only from lines it wrote. A GUI attaching to a
node mid-rebuild now says "Node busy: rebuilding note witnesses" and
asks before stopping it.
- A start that only finds a running node no longer resets the tracker;
a found busy node shows the busy overlay at once.
S14: launch our node with -rpcthreads=16 unless DRAGONX.conf sets it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Every operation that stops the node (restart, rescan, repair, delete
chain data, reinstall, bootstrap, wallet switch/adopt/restore/rebuild,
wizard restart, encrypt, decrypt) now:
- refuses up front while the node is rebuilding witnesses or has not
answered for 30s, with a "try again when it's done" warning, and sends
no stop;
- goes ahead only once the node's PROCESS has exited, not its RPC port.
A stop that doesn't finish within 120s cancels the operation (warning;
a cancelled rescan/repair disarms its flags); nothing is started over
the old node, chain data isn't deleted, wallet.dat isn't swapped.
Specifics:
- stopDaemonForWalletSwitch() no longer reports "stopped" when the app
is quitting (adopt could swap wallet.dat under a live node).
- Decrypt waited a fixed 3s after "stop" before renaming wallet.dat; it
now waits for the process to exit, and scrubs the key export if not.
- Bootstrap waits for the process before cleanChainData and stops the
node through the stop routine instead of the shared RPC client.
- StopScope::Connected lets switch/reinstall/decrypt stop an adopted or
external node through the same never-kill routine.
- A bounded stop that can't reach the node (a foreign node refusing our
credentials) gives up after 5s instead of 120s.
- macOS: isDaemonProcessRunning() checks processes (libproc), not the
RPC port.
- rpcBusyWhileAlive() also reads the busy gate: since the gate, the
first timeout usually lands on another lane and the 30s clock never
started (also weakened the quit prompt for a silent rebuild).
- Notifications render during the first-run wizard.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A node rebuilding note witnesses for an hour was shown as "Starting
DragonX node... Launching dragonxd", which invited closing it.
- Overlay, a new NodeBusy banner and the status bar now say what the node
is doing (rebuilding witnesses / catching up on blocks / up but not
answering), with the elapsed time and Phase-1 block progress. A rebuild
during startup warm-up replaces the stale "-28 Activating best chain"
text, and the overlay re-applies its text once a second instead of only
on a connect attempt (30s apart while the node is busy).
- Quit prompt: elapsed time, "Keep node running & quit" is the default,
"Stop anyway & quit" becomes "Stop node & wait" (it no longer kills).
- A reinstall skipped because the node is still running now notifies.
- Status bar: don't repeat the warm-up status on the right, and don't
print "Connected" there in non-English locales.
- i18n: 31 new keys in all 8 languages; 10 dead force-quit/old-modal keys
removed; CJK subset rebuilt for the new glyphs.
- Tests: chooseDaemonStatus, NodeBusy banner priority, and every new key
present in all 8 JSONs with matching printf conversions.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
dragonxd serves RPC with 8 workers and a 16-deep queue. While a witness
rebuild holds cs_main, every request needing it waits, and a request the
client gave up on keeps its slot. The GUI kept polling and re-probing
until all 24 slots were its own abandoned requests (even stop was then
refused), treated the busy node as lost (tearing down the wallet state),
and retried connect at frame rate on some pages.
Busy gate (src/rpc/rpc_busy_gate.h), enforced in RPCClient::performCall
for the shared rpc_ / fast_rpc_:
- the first reply timeout or queue-full refusal marks the node busy;
from then on nothing needing cs_main is sent (calls fail at once with
the new RpcFailureKind::Deferred). stop, help and getrescaninfo are
lock-free in dragonxd and always go through
- one canary getinfo with no client timeout waits on its own client and
thread; its answer clears the gate and triggers an immediate refresh
(queue-full: retried after 2..30 s; no answer: back to normal handling)
- the timer dispatch skips Core/Tx/Addresses/Peers/chat/mining/opid and
connect attempts while busy
- the 3-strike teardown counts only failures that put the connection in
doubt, never busy ones; no balance scans follow a busy chain-info call
Reconnect storm: retries were armed with setTimer(Core, kCoreDefault - 1),
i.e. "4 s elapsed", which against Overview's 2 s interval meant every
frame. RefreshScheduler::deferFor() is an absolute hold, and
ConnectRetryBackoff picks the delay by failure kind (not listening
1/2/5 s, queue full 2..30 s, busy 30 s, else 4 s).
Measured against the fake daemon: abandoned requests during a rebuild
went from ~24 (every slot) to at most 2, with no teardown and recovery
when the rebuild ends; a saturated queue sees about one canary per
back-off step instead of ~190 requests/s.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The one-time "back up your seed phrase" nudge only stopped probing on a
success or the exact "not derived from a mnemonic" error. Any other
answer re-armed it for the next frame, so a daemon without the RPC
("Method not found", e.g. the older bundled binary) was sent
z_exportmnemonic at frame rate: ~2,300 requests/s against a test node.
Chat identity setup re-posted its fetch on the next frame after a
transient failure in the same way.
An inconclusive nudge probe now waits 5 minutes before the next; a
transient identity-fetch failure waits 10 seconds.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Quitting during a witness rebuild is supposed to ask before stopping the
node, because a stop throws the rebuild away and a killed node replays
and redoes it. The guard almost never fired:
- it returned false for any daemon >= v1.3.0, on the false premise that
v1.3.0 checkpoints a rebuild (it does not; nothing survives a kill)
- it read a 16 KB debug.log tail and compared the last progress line with
the newest line, so any unrelated line during the rebuild's silent
second phase turned it off; and on POSIX that file is stale
- daemon_version, which the bypass needed, is zeroed when a busy node
drops the connection
Now a DaemonBusyTracker (src/daemon/daemon_busy_detect.h, pure) follows
dragonxd's own markers in order: rebuilding (Phase-1 / legacy markers,
ended by the completion line, an abort line or the next UpdateTip) and
replaying (Activating best chain ... Done Loading), reset by a new banner
or "Shutdown: done". It is fed from the owned output on the UI thread,
including the backlog skipped after a restore. The quit guard prompts
when the quit would stop the node and it is rebuilding, or has been
alive but only timing out / refusing as busy for >= 30 s. A replay alone
never prompts: stopping it is safe. The stop loop uses the same signal
to never offer Kill during a rebuild.
Also:
- daemon output offsets are absolute, so a reader that falls behind a
trim of the 1 MB buffer no longer skips new output (which could hide a
start or end marker)
- a quit deferred while minimized restores and raises the window, and
the wizard renders the prompt, so it is never waiting unseen
Verified live with the fake daemon: quitting mid-rebuild now shows the
prompt for a v1.3.0 node and leaves the node alone; a silent busy node
prompts after 30 s.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Every caller guessed what went wrong from what(): "Timeout" meant busy,
"401" meant auth, and warmup was code -28 or one of six substrings. A
non-JSON HTTP 500 lost its body, so "Work queue depth exceeded" read as a
bare "HTTP 500", and libcurl reports connect- and reply-phase timeouts
with the same code.
New header-only src/rpc/rpc_failure.h:
- RpcFailureKind (NotListening, ConnectTimeout, ReplyTimeout, QueueFull,
ShuttingDown, Unhandled, Auth, Warmup, AppError, Aborted, Other) and a
pure classifyRpcFailure(curl code, request-sent, HTTP status, JSON-RPC
code, plain-text body)
- formatHttpError keeps a short, printable plain-text body
- RpcError (moved here) stays "the daemon answered with a JSON-RPC
error". The new RpcTransportError means "no JSON-RPC answer": curl
failures and HTTP-layer refusals (401, 503, queue full, unhandled).
Keeping them separate matters: callers catch RpcError to mean "the
daemon said no" (chat identity's no-mnemonic fallback, seed export),
so a timeout must not be one. They now treat a queue-full 500 as
transient rather than as "no".
RPCClient:
- performCall tells the two timeouts apart with CURLINFO_REQUEST_SIZE
(0 = never reached the daemon; reset on every transfer, checked on
libcurl 7.81), and callRaw now shares performCall and the HTTP-failure
path.
- connect() decides warmup by the -28 code only, so a plain-text body
saying "Loading" can't pass for warmup, and records
getLastConnectFailureKind().
- "Connection failed" names the kind and is logged once, then every
100th repeat (one incident logged it 60k times).
- secret-bearing calls never echo a response body.
tryConnect's "401"/"Timeout" checks now use the kind, and
CoreRefreshResult carries blockchainErrKind/balanceErrKind for the busy
handling to come.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Bug-report fixes plus adversarial-audit follow-ups. Bumps full 2.0.1->2.0.2 and
lite 1.1.0->1.1.1.
i18n / fonts:
- Rebuild the NotoSansCJK subset with the glyphs the FAQ back-fill introduced
(什/么/门/做); Chinese FAQ titles rendered as "??" on zh (and ja/ko).
Transaction history (enumeration gaps):
- Surface outgoing shielded z->z sends via z_listsentbyaddress (listtransactions
and z_listreceivedbyaddress never report them).
- Surface z->t deshields by parsing z_listsentbyaddress transparentSends (which
z_viewtransaction does not expose); dedupe t->t against listtransactions.
- Key send-row dedup on address so equal-value multi-output sends aren't dropped.
Lite wallet (key safety):
- The async create/restore/open path the UI uses now applies the passphrase:
encrypt a new/restored wallet, unlock an existing one. It previously discarded
the passphrase, storing the seed/keys in PLAINTEXT (create/restore) or leaving
an encrypted wallet locked (open).
Migrate-to-seed (fund safety):
- Persist a sweep-submitted marker at broadcast so a resumed migration can't treat
a ~0 balance (an unconfirmed in-flight sweep) as an empty wallet and adopt (swap
wallet.dat) before the sweep confirms; best-effort locate routes to the confirm gate.
Sync / threading:
- Gate isSynced() on a peer-derived tip (tip_known) so a peerless node isn't reported
synced (Send against a stale balance); add a "no peers" status banner.
- Guard daemon_status_ with a mutex (monitor-thread write vs UI-thread read).
Tests: extends tests/test_phase4.cpp across all of the above; suite green.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BwycPDQSfEKtrTS2uC4JeT
Adds a per-conversation "delete" with two modes, and removes the per-frame
rescans of the chat history that shared these files.
Delete conversation (header trash icon → confirm dialog):
- Delete (revive-on-new-message): clears local history and tombstones the
messages (new chat_deleted table, keyed dedup hashes) so the every-few-
seconds memo re-scan can't re-import them; a genuinely NEW message (new
txid) revives the thread.
- Delete & block: removes history WITHOUT a tombstone and records the cid as
blocked (settings); ChatService::ingest drops that conversation's messages
— old and future — until unblocked from the "Blocked" manager, which then
re-imports the conversation from chain.
- Local-only (messages remain on-chain; the peer keeps their copy).
deleteConversation() deletes the DB rows FIRST and only then mutates the
store, so a failed write can't leave the two diverged.
Unit-tested (revive / tombstone-survives-reload / block / unblock) and
adversarially reviewed (store/DB divergence, half-open DB, revive-unread).
Performance (chat + badge hot paths, from the perf audit):
- ChatStore gains revision(); the Chat unread badge is now a single O(N)
no-alloc pass cached on it (was O(conversations x messages) copy+sort every
frame). The conversation list and open thread are memoized on revision()
(+ show-hidden and AddressBook::revision() for peer-name resolution).
- AddressBook gains revision() so an in-place contact rename invalidates the
chat memo (an edit keeps entries().size() constant).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The node kept falling behind the network near the tip because sync
DETECTION was starved: getblockchaininfo was queued behind the
O(mapWallet) wallet RPCs (z_gettotalbalance / z_listunspent), so
longestchain went stale, the wallet decided it was "synced", and it
resumed hammering cs_main — a feedback loop.
- Issue getblockchaininfo FIRST each cycle and skip the balance/address/
tx refresh entirely while behind, so sync state (and kSyncProfile)
updates before any heavy wallet scan runs.
- Gate the two chat note scans (refreshChatNoteBudgetNode /
fastScanChatMemos) on effectivelySyncing() and the active page, so chat
memo scanning no longer competes with block connection during sync.
- Windows debug.log tailer: reset the read offset when dragonxd truncates
the log on startup (it was stranding at Block:0 with no witness/rescan
progress).
- Tests cover the getblockchaininfo-first ordering and behind-skip.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The daemon-output parser treats autonomous background witness rebuilds as a rescan (they set
state_.sync.rescanning via the foundWitness branch), so one completing fired "Blockchain rescan
complete" even though the user never started a rescan — most visibly after a minimize, where a
whole rebuild's start+finish arrives in one batch.
Add user_initiated_rescan_ (atomic — some triggers run on worker threads), set it at the wallet's
real rescan triggers (the Rescan button, a -rescan/salvage/zap/reindex restart, key import, seed
migration), and gate the three "rescan complete" toasts on it, clearing it when shown. The
rescan/witness progress state machine is untouched — only the toast is gated — so background
rebuilds no longer announce a completed rescan while genuine user rescans still do (and can't
double-toast).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Rec3 — v1.3.0+ opens a wallet that lost its hdchain in DEGRADED mode (existing funds spendable,
but no new HD-key derivation) instead of aborting. Add a daemon-log classifier
(walletOpenedDegraded) + detectWalletDegraded(), warned once per session. Pre-1.3.0 daemons
never emit that line, so it's a no-op there.
O1 — probe z_autoshieldstatus once per connection (now decoupled from our own toggle/balance) and,
in Settings, show whether the node handles auto-shield itself (+ its destination, or the daemon's
disabled_reason). The checkbox now governs only the wallet's fallback shielder, which defers to the
node. Nothing renders on pre-1.3.0 daemons (no such RPC), so behaviour is unchanged.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
v1.3.0 daemons auto-shield coinbase by default (when the HD seed is recoverable — which
every ObsidianDragon-created wallet is, via -usemnemonic=1). The wallet also ran its own
client-side auto-shield every refresh tick, so both raced for the same coinbase UTXOs and
split funds across different z-addresses (the wallet picks the first z_listaddresses entry;
the daemon uses a seed-hardened derivation).
Probe z_autoshieldstatus once per connection (while synced, so the daemon is past warmup)
and skip the wallet's client-side shield when the daemon reports it active. Fail-closed: a
pre-1.3.0 daemon has no such RPC, so the probe returns active=false and the wallet keeps
shielding — no regression on the currently-bundled v1.0.3. Re-probes on reconnect (handles
a live daemon upgrade/swap).
Follow-up (not done): drive the Settings "Auto-shield" toggle + disabled_reason from
z_autoshieldstatus (O1) — needs runtime verification of the RPC fields on a live v1.3.0 node.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
_popen/_popen-style shell-outs flash a cmd.exe console window on Windows. Add
Platform::runHiddenCapture() — CreateProcess + CREATE_NO_WINDOW capturing stdout on
Windows, popen on POSIX — and route the remaining shell-outs through it:
- GPU-aware idle detection (getGpuUtilization: "where nvidia-smi" / "nvidia-smi --query-gpu")
- xmrig discovery + version (findXmrigBinary "where xmrig.exe"; "<bin> --version", stderr merged)
- wallet-rebuild helper (app_network) — keeps its exit-code check via the new exitCode out-param
None of these are on the launch path (that was the daemon spawn, fixed in a2f84be); each
would flash a console only when it ran (idle-GPU mining, mining tab, wallet recovery).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
On a fully-shielded (ac_private=1) chain, z_gettotalbalance is O(mapWallet) and
holds the daemon's cs_main for its whole duration — ~20s on a ~5k-tx wallet. The
Overview refresh polled it every ~2s (twice: minconf 0 and 1), so cs_main was
held almost continuously, starving the single block-connection thread: the node
connected blocks only in the gaps between polls and could fall further behind
the tip than it caught up (observed live: gap growing 58→100 blocks while the
GUI was open, one core pegged on GetFilteredNotes, 22 idle, ~17 B/s download).
Two hardening changes on top of the existing "skip balance while syncing" guard:
- Hysteresis: keep the low-impact sync profile (and balance suppression) for a
short settle window after catching up, so a large-wallet scan can't
immediately re-starve connection and bounce the node back into syncing. Armed
only on the syncing→caught-up edge, so a wallet synced from the start is never
throttled at connect (effectivelySyncing()).
- Adaptive balance cadence: time each z_gettotalbalance scan and require the
next poll to wait at least (cost / 10%), so balance scanning never occupies
more than ~10% of wall-clock. Cheap wallets are unaffected (the tab's Core
timer stays the cadence); a ~20s scan backs off to ~200s. Wallet mutations
(send/shield) force the next poll through so the user's own action updates the
balance immediately (balanceRefreshDue()).
getblockchaininfo keeps its normal cadence throughout, so sync progress stays
live. Build + test_phase4 green.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Make Merge to Address actually serve wallet-bloat consolidation and be far less
opaque. New ShieldDialog::showConsolidate() preset (used by the large-wallet
Settings banner + alert action) frames it as "Consolidate funds" and targets
shielded notes — the bloat the nudge warns about.
- Source selector: consolidate shielded notes (ANY_SAPLING), transparent
(ANY_TADDR), or both (*) — previously hardcoded to ANY_TADDR, which never
reduced the shielded-witness bloat. Batch limit now applies to the right side.
- Scope: on open, count spendable UTXOs + notes (listunspent / z_listunspent)
and show "N transparent + M shielded · ~X DRGX"; warn "repeat to finish" when
the set exceeds one batch.
- Destination auto-selects the best spendable z-address (button enabled by
default); empty wallets get an inline "Create shielded address" (z_getnewaddress).
- Advanced disclosure hides Fee + "Max inputs per batch" (renamed from the "UTXO
Limit" jargon) with sane defaults.
- Inline confirm step before the fund-moving call (amount + input count + dest).
- Live progress: self-polls z_getoperationstatus to show Consolidating… →
Done/Failed, replacing the raw opid + manual "Check status" button.
All three merge entry points now use the typed showMerge()/showConsolidate()
(no stale-static leaks from direct show(MergeToAddress)). Shield-coinbase mode
keeps working. New i18n keys fall back to English.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Extend the wallet-bloat warning beyond the Settings banner: when wallet.dat
first crosses 500 MB (full-node, synced), fire a one-time warning toast plus a
clickable "Consolidate notes…" entry in the bell/alerts panel that opens Merge
to Address. The persisted large_wallet_warned flag keeps it once-only and
re-arms if the file later shrinks back under the threshold.
- AlertRecord gains an optional onClick + actionHint; Notifications::action()
pushes a toast and a clickable history entry. renderAlertHistoryPanel() now
renders the accent action link (under the message) and measures true content
height so wrapped messages + the link aren't clipped.
- App::maybeWarnLargeWallet() (mirrors maybeRemindSeedBackup) runs once per
launch from update(); reuses the existing wallet_size_warn/consolidate strings.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Fixes all 22 confirmed findings from the mining-tab audit (10 Medium, 12 Low; 0 Critical/High),
adversarially reviewed (6 follow-ups found + fixed, incl. the review-caught idle-auto-start bypass
and a wrong benchmark-restore condition).
Crash-safety & lifecycle:
- M-04: join a stale/finished monitor thread in XmrigManager::start() and ~XmrigManager so an xmrig
crash-then-restart (or quit) no longer std::terminate()s the wallet.
- L-03/L-10: surface an unexpected miner exit once and clear the stale running flag.
UI never blocks (M-03/L-06/L-08/L-09/L-13): pool start/stop now run on a dedicated serialized FIFO
mining-control thread (joined before teardown), so the ~13 call sites don't block the render thread on
stop()'s SIGTERM->SIGKILL->join; the spawn result marshals back to the UI.
Miner-process / pool trust boundary:
- M-01: validate the payout address (util::isValidRecipientAddress) at EVERY start path — the UI gate
AND App::startPoolMining() (idle auto-start / thread scaling) — so a stale/wrong-chain address can't
silently lose rewards.
- M-09: SSRF guard skips the background pool-stats GET for loopback/private/link-local/single-label hosts.
- M-02/L-02: cap the pool-stats + xmrig-API HTTP response bodies.
- L-01: write the xmrig config 0600 at creation (POSIX open with mode) — no world/group-readable window.
- M-10: reject shell-metacharacter binary paths before the version popen (excluding '()' so Program Files
(x86) still works).
Solo mining: M-06/M-08 clamp thread count to [1, cores] at the setgenerate/xmrig boundary; M-07 notify +
don't lie on stop failure.
Correctness: L-05 block-time constant 75->150s (chainparams); M-05 discloses pool-mode "Est. Daily" as a
rough solo-equivalent; L-04/L-11/L-12 benchmark lifecycle (cancel on nav-away / mode-switch with restore,
skip rebalance mid-benchmark); L-07 honor cancel mid-extract in both the xmrig and daemon updaters.
Two new i18n keys back-filled across all 8 languages; CJK subset font rebuilt for the new glyphs.
Verified across full-node, lite, and Windows builds; tests pass.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Security audit remediation (15 confirmed findings from the codebase audit):
- H-02: scrub+delete the decrypt-flow plaintext key export on ALL exit paths
(RAII guard) and purge stale obsidiandecryptexport* files at startup.
- M-01/L-03/L-04/L-05/L-07: sodium_memzero the Set-PIN and encrypt-PIN worker
passphrase/PIN copies, the RPC Basic-auth string (auth_), the exported/imported
key buffers (App::wipeSecrets, called from ~App and before main's _Exit), and
the first-run wizard "Skip" buffers.
- M-03/M-04/M-05/L-06: return locked COPIES from XmrigManager/EmbeddedDaemon
getters (dedicated error_mutex_; DaemonController::lastError now by value),
route xmrig last_error_ writes through a locked setter, and wrap
shutdown_status_/wizard_stop_status_ in a locking GuardedStatus
(wizard_stopping_external_ -> std::atomic).
- M-02: persist after a console send/shield/import in the lite backend.
- L-01: require the confirm click for z_shieldcoinbase/z_mergetoaddress.
- L-02: quote/escape each Windows daemon argv per the MSDN CommandLineToArgvW rules.
- L-08: pin json/tomlplusplus/libwebp FetchContent to immutable commit SHAs.
- I-01: extract updater archives from the already-verified in-memory buffer
(no disk re-read TOCTOU).
Feature: warn once (full-node) when the active wallet loads empty while a sibling
wallet file in the datadir holds keys. A funded salvage wallet.<ts>.bak routes to
the recovery/Restore flow; a funded sibling .dat routes to the wallet manager.
Per-wallet-file dismissal; gated on synced + address-list-loaded to avoid false
positives on warm reconnect / spent-down wallets.
UX fixes:
- send: show the TOTAL balance (with a spendable "available" note) in the source
dropdown and keep pending-change addresses visible.
- chat: insert emoji at the cursor position; restrict new-chat recipients to
shielded (z) addresses.
- console: optional auto-focus of the command input on tab open (off by default).
- shutdown: when "stop external daemon" is on, keep the shutdown screen up until
the external node actually exits, showing live status.
Adversarially reviewed; verified across full-node, lite, and Windows builds; tests pass.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Sending a small amount from an address holding a large balance made the displayed
balance collapse to ~0 until the tx confirmed. A shielded spend consumes the whole
source note; the change returns as a 0-confirmation note, and every balance query
used the default minconf=1 — so the spent note dropped out and the change wasn't
counted yet.
Split every balance into two views:
- DISPLAY (balance / privateBalance / transparentBalance / totalBalance) — now
queried at minconf=0, so it INCLUDES the user's own pending change and no longer
craters. This is what the Overview, balance tab, market portfolio and receive
tab show. unconfirmedBalance is now populated (= total - spendable).
- SPENDABLE (new spendableBalance / spendable*Balance) — confirmed (minconf>=1),
what z_sendmany (run at minconf=1) can actually spend. The Send form's available/
Max/validation, the from-address selection, the drag-to-transfer dialog cap, the
chat pay-from and the auto-shield gate all size off these, so they never offer
0-conf change the daemon would reject.
Implementation: a single z_listunspent(0)/listunspent(0), partitioned per-note by
"confirmations">=1; z_gettotalbalance called at minconf 0 (display) and 1
(spendable); the z_getbalance fallback queries both. applyPendingSendDelta (the
optimistic post-send debit) now touches ONLY the spendable fields — debiting the
display too would re-crater it on top of the honest minconf=0 RPC. Lite mirrors
spendableBalance = balance (its per-address balance is already confirmed) so lite
sends aren't zeroed. The confirmed-only gates (seed-migration/sweep z_gettotalbalance,
sweep z_getbalance(addr,1), z_sendmany's minconf arg) are untouched.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
A shielded send with a memo failed: "Invalid parameter, expected memo data in
hexadecimal format or to use 'utf8:' prefix." The Send-tab path (and its fee-gap
retry) put the user's plain-text memo straight into the z_sendmany recipient,
which the daemon now rejects — it wants the memo hex-encoded or with a "utf8:"
prefix. The chat path already prefixes with "utf8:"; do the same for user memos.
Only the RPC recipient["memo"] is prefixed; the raw memo is still what's stored
for the transaction-history display, and the daemon returns the decoded memoStr
to receivers, so it round-trips as plain text.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Turns the "Daemon Error + raw log dump" moment into one calm, honest recovery
dialog plus a recovery-aware rescan screen. Presentation + orchestration only —
the file-safety logic in rebuildWalletDatabase()/restoreOriginalWallet() (source
selection, verify-before-swap, copy/rename-never-delete, .bak) is unchanged.
- One authoritative dialog with a phase machine Offer -> Working -> Done/Failed.
The duplicate in-overlay recovery card, the untranslated red "Daemon Error"
heading, and the raw daemon-log dump are gone for the recovery case (they stay
for genuine, unrelated crashes).
- Offer is a choice-cards layout: "Repair automatically" (recommended, accent-
tinted) vs "Restore original", side by side; the rare actions ("Show me the
files", "Decide later") and a plain-language "What happens to my files?" sit in
a quiet footer. When the rebuild helper is missing, it collapses to a single
Restore card — never a dead end.
- Post-repair rescan shows a calm "Finishing your wallet repair" screen with
elapsed time + the growing wallet size, instead of "RPC timeout / taking longer
than expected / restart daemon"; the daemon-crash toast is suppressed and the
detection toast is downgraded from red to info.
- Fixes a confirmed dead-end: if a repair succeeds but the restarted daemon then
crashes for a *different* reason (block index, disk, OOM), the recovery flags
now clear (in tryConnect + onConnected) so it surfaces as a normal daemon
failure instead of freezing forever on a reassuring "don't restart" screen.
- Clickable "Wallet repair available" status-bar chip for re-entry.
The same app.cpp changes HiDPI-harden the surfaces the recovery flow lives on:
the status-bar and loading-overlay hand-drawn geometry are multiplied by dpiScale
(they rendered native-size and clipped at HiDPI / font_scale>1), the loading-
overlay status text wraps instead of running off both edges, and the node-status
banner floors its height to its DPI-baked font so the title can't clip off the top.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>