Concurrency audit: I18n::translate() returns a const char* pointing INTO strings_, and loadLanguage()
clears/rebuilds strings_ on the main thread at a language switch. The EmbeddedDaemon state/monitor callback
builds its status strings via TR() off the main thread, so a concurrent language switch could free the
map node out from under that pointer — a use-after-free data race.
- Add a shared_mutex guarding strings_: loadLanguage() takes it exclusively for the whole clear+rebuild
(its helpers loadBuiltinEnglish/overlayTranslations are called only from there and don't lock, so no
recursion); a new translateCopy()/trCopy() takes a shared lock and returns an owned std::string.
- The lock-free translate()/TR() stays for main-thread UI (same thread as the writer, so never races).
- The four daemon-state setDaemonStatus(TR(...)) calls in the monitor callback now use util::trCopy(...).
Synchronization only; no fund-path call touched. Threading fix — clean build + ctest; the race is runtime-only.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Concurrency audit: SecureVault::scope_ (std::string) is written by setWalletScope() on the main thread
(wallet switch) while getVaultPath() — reached from retrieve()/hasVault()/changePin()/store() — runs on
RPC worker threads during a send-time or startup unlock. A concurrent wallet switch and a worker vault op
is a data race on the string (and could resolve the wrong vault file).
Guard scope_ with a small mutex: setWalletScope() assigns under it; getVaultPath() copies it out under the
lock and uses the copy. The lock is held only for the string copy — never across Argon2id or file I/O —
and getVaultPath() never calls setWalletScope(), so no deadlock. Argon2id params, key derivation, AEAD,
the vault file format and retrieve/store/changePin are byte-identical. SecureVault is held via unique_ptr
and used by pointer, so the added (non-movable) mutex is fine.
Synchronization only. Threading fix — clean build + ctest; the race is runtime-only.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Concurrency audit: last_error_ was written unlocked at the five startProcess() error sites (while the
monitor thread could be in setState() writing it under error_mutex_) and read unlocked in start() — a
data race on a std::string. process_pid_ / stdout_fd_ (POSIX) and process_handle_ (Windows) are plain
scalars written by startProcess, the stop paths and the monitor (reap) and read by accessors.
- Route every startProcess() last_error_ write through a new locked setLastError(); read the start()
detail via the already-locked getLastError().
- Make process_pid_ / stdout_fd_ / process_handle_ std::atomic (trivially atomic scalars/handles), so
individual loads/stores no longer race. Process start/stop ordering and the shutdown hardening are
unchanged — this only removes the data races, not the (already-serialized) sequencing.
Synchronization only; no fund-path call touched. Threading fix — verified by a clean build + ctest; the
race itself is runtime-only. (The Windows process_handle_ atomic is reasoning/compile-only on this Linux box.)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Resilience audit, vendored SDXL core (post-hoc wallet bookkeeping only — the send/build/broadcast path,
note/UTXO selection, fees, recipients, memo and the post-broadcast pending-marking are byte-identical):
- cleanup_mempool never cleared unconfirmed_spent, so a send that was dropped / never mined / reorged out
locked its notes and UTXOs until the app was reloaded ("insufficient funds" on a funded wallet). It now
releases the lock once the send is provably dead. A send is built with Builder::new(tip) (expiry = tip+20,
mineable through tip+20), so the lock is held until the chain reaches tip+21 (MEMPOOL_KEEP = DELTA+1) —
one block past the old mempool-drop point, which would have released the notes while the tx could still be
mined. A txid already in `txs` was mined (scan settled its inputs) and is skipped. mempool_txs is released
before `txs` is taken, so the two locks are never nested.
- invalidate_block reset nd.spent on reorged-out spenders but never walked wtx.utxos, so a transparent UTXO
whose spender was reorged out kept spent=Some(removed txid) and dropped out of the balance until a rescan.
It now resets utxo.spent the same way (unconfirmed_spent left as-is, as for notes — the send may be re-mined
on the new branch).
- Removed the stale "TODO remove unconfirmed_spent on expiry" in data.rs (now done).
Verified with the scratch-mirror rtest.sh (cargo 1.63, scratch HOME): cargo test --release --lib green,
129 passed / 0 failed / 12 ignored, including the oracle differential tests (updated in lockstep to clear
the sim's pending flag at the same expiry point) and test_z_mempool_expiry. The C-ABI wrapper and the
artifact rebuild / lite_smoke (needs a live server) were not run; they don't touch these functions.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Lite-parity audit, FIX C (gating/validation/presentation only — no encrypt/unlock/send backend call changed):
- Lite unlock went straight to the backend with no lockout. New App::unlockLiteWallet() runs the SAME
escalating, persisted attempt-lockout as the full node (restoreLockoutState/applyUnlockFailure), wired into
the Settings unlock, the send-time prompt and the lock screen. A wrong-passphrase failure counts; an
already-unlocked/not-encrypted result does not.
- state_.locked is now cleared immediately on a successful unlock (the ~2s status poll lagged, so the UI
stayed "locked" after a correct passphrase).
- PIN quick-unlock controls are hidden in lite Settings — lite has no PIN vault, so they were dead.
- A create/restore whose passphrase encryption FAILS now raises a one-shot visible error
(takeEncryptionFailureNotice -> lite_encrypt_failed_unencrypted) instead of only a log line; the wallet is
left open but unencrypted. Fixed the flag on BOTH the lifecycle and the direct create/restore encrypt paths.
- A background sync save is skipped while an encrypted wallet is unlocked (the backend save re-locks it),
resuming once locked.
Compile- and unit-verified (testLiteWalletControllerLockUnlockParity covers the save-deferral + encrypt-failure
notice, run twice for the flaky guard); the lock-screen lockout/PIN-hiding UX itself is runtime/GUI-only and
not exercised here — worth a manual lite GUI pass.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Resilience audit, migrate-to-seed (the sweep RPC is byte-identical — see below):
- Adopt was enabled at >= 1 confirmation and never re-read state before stopping the daemon, so a 1-block
reorg after the swap could un-mine the sweep and strand the funds in the .bak. Adopt now needs a
reorg-safe depth (>= 10, matching the History "confirmed" threshold), and the button runs
verifyThenAdoptSeedWallet(): a read-only gettransaction(confs) + z_gettotalbalance minconf 0 (includes
unconfirmed, fixing the old minconf-1 "no funds" gate) immediately before the daemon stop, refusing while
syncing or if any read fails. The remainder re-sweep still shows once mined (>=1).
- The wallet.dat swap was rename(legacy->bak) then non-atomic copy(new->wallet.dat): a hard kill left
wallet.dat missing or half-written. New order: probe the new file is a BDB wallet; stage a size-verified
copy beside wallet.dat; COPY (never overwrite) the legacy wallet to the timestamped .bak and verify it;
then ONE atomic fs::rename of the staged file over wallet.dat. At every instant wallet.dat is the whole
old or whole new wallet, and a verified .bak exists before the replace. On any failure only our scratch
files are removed; the user's wallet is untouched.
Byte-identical: z_mergetoaddress (beginSweepToSeedWallet, ["ANY_TADDR","ANY_ZADDR"], amounts, fee, recipient,
memo) and beginAdoptSeedWallet's disconnect/vault-removal/stop/rescan/restart logic are unchanged; only the
adopt gating, two new read-only RPCs, presentation, and the file-move safety change. Pure tests:
sweepConfsAreReorgSafe + adoptReverifyPasses.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Lite-parity audit, sync-progress semantics (state/presentation only — no send/sync backend call changed):
- A scan failure set scan->failed + lite_error, but no UI read lite_error, so the user saw "Syncing 99.9%"
forever. The status bar now shows the error text on a lite scan/server failure.
- After the initial catch-up, a server loss on the cheap probe path (ProbeFailed) left the last "Synced"
model on screen. The worker now re-publishes that model demoted (not complete, reason "server unreachable").
- refreshModelImpl's height-read "synced" override no longer fires while syncstatus says the backend is
still scanning or is indeterminate, and a server-bound read failure in a full cycle demotes to not-synced.
- The syncstatus parser no longer treats a missing/unrecognised "syncing" field as fully synced: it reports
an explicit "indeterminate" (not complete). Explicit "false" stays caught-up; the fake backend's
syncing:true 1000/1000 behaviour is unchanged.
Tests: parser cases for missing/odd/idle "syncing" (testLiteSyncStatus*).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Lite-parity audit: per-address spendableBalance was set equal to the gross balance, ignoring the backend's
per-note 'spendable' flag (spending key present AND note at/below the anchor height), and has_spending_key
was effectively always true (mapAddress never set spendabilityKnown). So the Send auto-picker could select
an address that can't fund the send, and an imported view-only (zxviews) address looked spendable.
- Parser: carry the backend per-note 'spendable' flag when present (Sapling notes; transparent utxos keep
the confirmed/unspent rule). It can only narrow spendability, never widen it.
- Mapper: derive per-address spending-key evidence from positive signals only — a spendable note proves a
key (spendable), a confirmed long-mature (>=100 blocks) still-non-spendable note proves view-only
(!spendable); fresh/immature/no notes stay unknown so nothing is mislabelled.
- Controller: info.balance stays the gross sum; info.spendableBalance now counts only backend-spendable
outputs; a tolerated partial refresh keeps the last-known spendable balance and view-only verdict.
Display/selection only — gating can only get stricter for view-only/unanchored funds; no send/sweep/encrypt
call changes. Test: testLitePerAddressSpendableAndViewOnly.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Concurrency audit (lite): LiteClientBridge::shutdownCalled_ was a plain bool, set by retire()/destruction
on one thread and read nowhere by the detached sync/console/broadcast threads. A retired controller's
in-flight execute() (send/save/sync) therefore still ran litelib_* against the PROCESS-GLOBAL client,
which may already belong to a NEW controller after a server switch.
- shutdownCalled_ is now std::atomic<bool>; shutdown() uses exchange() so exactly one caller tears the
client down (idempotent) and the flag is published for the detached threads.
- execute() and initializeNew/NewFromPhrase/Existing refuse once the bridge is shut down (shutDownResult),
so a stale command can't reach the global client.
Gating only — doSend/doShield, the JSON send payload, amounts/memo/recipients, persistAfterBroadcast,
encrypt/unlock/export and api_.execute argument passing are byte-identical when the bridge is live.
Test: testLiteClientBridgeRefusesAfterShutdown.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Performance audit: tx_revision bumps on every new block because each tx's confirmation count rises, so
the History tab's groupByTxid + merge + sort + deep-copy display cache fully rebuilt once per ~36s block
on the UI thread, even when nothing but the counts changed.
Add WalletState::tx_structure_revision, bumped alongside tx_revision EXCEPT when setTransactionsIfChanged
sees a pure confirmation-count delta (new sameTransactionLayout: every row equal ignoring the confirmation
COUNT, but the pending/confirmations==0 state — which drives the pending-first sort and shield merge — must
still match). Every other writer bumps both (conservative default). The History tab's groups cache and
display cache now key on tx_structure_revision; on a confirmation-only delta it patches dtx.confirmations
in place (rows, order and indices into state.transactions are unchanged) instead of rebuilding, and re-stamps
the filter snapshot so "export current filter" stays valid. Displayed confirmations still update every block.
No fund path touched; display/caching only. Test: testTxStructureRevision.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Concurrency audit: RPCWorker::stop() discards any MainCb the fast worker pushed after the last drain,
and lock_unlock_in_progress_ / pin_in_progress_ / encrypt_in_progress_ are cleared only inside those
callbacks. So a disconnect (3-failure threshold, daemon restart) mid-unlock destroyed the callback that
would have cleared the flag, leaving the lock screen stuck on "Unlocking..." until the app was restarted.
Clear the three flags in onDisconnected() right after the fast worker is torn down (gated on it having
existed, since only fast-lane-posted callbacks can be lost this way). Chosen over draining the worker,
which would run stale callbacks after state_/rpc_session_ were already reset. The user can retry after
reconnect and the real encryption state is re-read on connect. No fund-path call is touched.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Lite-parity audit, two History-correctness findings (display/parse only — no send/broadcast call touched):
- A lite send carried a POSITIVE tx.amount, so History, the Details dialog and CSV export rendered an
outgoing payment as a green "+" receipt (the full node uses a negative debit). The mapper already
computes signedAmountZatoshis (negative for sends, positive for receives); applyLiteRefreshModelToWalletState
now uses it for tx.amount, so the shared UI shows a red "-" and CSV a negative value. The summary cards
use std::abs and are unaffected.
- A send this wallet did not author (seed restore / another device) has empty outgoing_metadata, so the
parser summed it to 0 and showed amount 0 with a blank address, understating Sent totals. parseTransactionRecord
now falls back to |record.amount| (the net debit, incl. fee) when the metadata sum is 0, tolerant of a
missing/odd field; explicit outgoing_metadata still wins.
Tests: testLiteSendAmountIsNegative (send maps to <0, receive stays >0) and
testLiteForeignSendFallsBackToRecordAmount.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Resilience audit, highest-value finding: a z_sendmany (full node) or lite broadcast whose REPLY is
lost — a transport timeout / connection cut AFTER the request already left us — was reported as an
ordinary "Transaction failed". The user then retries and can broadcast the same payment twice, because
the daemon may already hold the operation (z_sendmany returns an opid and runs async).
Classify the ambiguous case and surface it distinctly, without touching the send itself:
- New pure helpers in rpc/rpc_failure.h: sendReplyMayBeLost() (an RpcTransportError that was requestSent
with no HTTP status and a non-OK curl code — reply-timeout / got-nothing / our-own-abort; definitive
rejections and any HTTP status are excluded), liteSendErrorMayBeInFlight() (a transport-style gRPC text
heuristic, vetoing connect-phase and server-rejection text), and a readable-English marker tag.
- app_network.cpp: the z_sendmany catch records the flag; the failure branch (non-background only) tags
the callback text and forces a wallet re-read so a committed send shows in History. onDisconnected()
tags already-pending send callbacks the same way (they exist only after an opid was returned).
- Lite UserSend (app.cpp): a transport-style broadcast error is tagged identically.
- Send tab: a tagged outcome shows a non-success warning pointing at History and clears the compose form
so any resend must be retyped deliberately; it is NOT presented as a retryable failure.
The actual rpc_->call("z_sendmany", {from, recipients, 1, fee}), the lite send/broadcast call, and all
amounts/fees/recipients/memo bytes are byte-identical; only result-handling and presentation change.
Pure classifier/marker regression tests added (testSendMaybeInFlight).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Fund-correctness audit, sweep + lite paths:
- Key-import sweep: balAt() swallowed every RPC error as 0.0, so a transient
timeout/warmup after the import misclassified a funded key as "no funds". Let
the failure propagate to the outer handler so the user sees a real error and
can retry, instead of a false __NOFUNDS__.
- Lite send: the double→zatoshi conversion was an unchecked llround+cast — a
negative/NaN amount would wrap to a huge uint64. Route it through
util::coinsToZatoshis and reject invalid amounts before broadcast.
- Lite balance: the spend gate used the gross shielded total (including
unverified/unconfirmed notes). Use the backend's spendable_zbalance (a required
field) for spendablePrivateBalance/Total, clamped to the shielded total.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Fund-correctness audit, send path:
- Flagship wrong-send: s_send_max was only cleared on a successful send, so after
Max → typing a smaller amount → changing the fee tier in the confirm dialog,
the amount was silently overwritten back to the maximum and Confirm sent far
more than the user typed. Clear s_send_max on any manual amount edit (DRGX and
USD inputs), so a later fee bump never rewrites a hand-entered amount.
- A valid exact-maximum send was blocked by 1 ulp because amount+fee<=available
was computed in IEEE doubles. New util::toSats/canAfford compare in int64
satoshis; wired into the Review gate, the confirm-time re-check, the Send
tooltip and the amount-error banner. This only unblocks a correct send; it
changes no sent amount.
- Added util::coinsToZatoshis (reject non-finite / <=0 / rounds-to-0 / over-supply)
and regression tests for formatAmountFixed, toSats/canAfford and coinsToZatoshis
(incl. the exact failing inputs the audit found).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Defense-in-depth against a local attacker / core dump reading un-zeroed heap
copies of secrets (threat model is local, not remote). Best-effort by nature —
std::string SSO/reallocation and ImGui buffer growth can still strand copies —
so these shrink the exposure window rather than guarantee absence:
- rpc_client performCall: a scope guard now sodium_memzeroes the request body +
payload tree (walletpassphrase / *_importkey args) after the call and detaches
CURLOPT_POSTFIELDS. Request content sent to curl is unchanged.
- New ui::scrubImGuiInputTextState() zeroes ImGui's retained InputText buffers
(live/revert/callback/deactivated) — the app memzeroed its own char[]s but
ImGui kept copies. Called from wipeSecrets()/wipeDisplayedSecrets(). PINNED to
ImGui 1.92.6 via a #error guard so a vendored bump forces a re-audit (the stb
undo buffer is an incomplete type in the header, so it's left as a noted gap).
- Lite exportSeed/exportPrivateKeys wipe result.value + the parsed JSON tree.
- Seed-word grid (SplitSeedWords/RenderSeedWordGrid) zeroes its per-frame word
and cell copies; the wizard word/chip vectors are zeroed before clearing.
- ChatStore::clear() + ResetChatTab() zero decrypted message bodies on
lock/switch/teardown.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Six bounded defensive fixes:
- Payment-URI amount is now strictly validated (reject NaN/inf/sign/garbage,
<=8 decimals, 0..21,000,000) at parse time, so a crafted QR/URI can't inject a
bogus amount into the Send form. The z_sendmany call the user confirms is
unchanged; this is input validation only.
- export-all-keys filename is sanitized (basename-only, no traversal) via a
shared util::sanitizeExportFilename (factored out of the CSV export dialog).
- Chat append()/upsert() wrap the strict serialize in try/catch so a malformed
(invalid-UTF-8, attacker-influenceable) body is skipped, not crashed on —
matching appendBatch's existing drop behavior.
- http_download (used by the updaters + pool stats) restricts transfers AND
redirects to HTTPS and bounds the download path's redirects, so a redirect
can't downgrade to http:// or another scheme.
- The default DRGX.conf (rpcuser/rpcpassword) is written 0600 from the start via
writeFileAtomically, closing the brief world-readable ofstream+chmod window.
- Auto-lock is suppressed only for SOLO mining (needs the wallet), not pool
mining (pays an external address), which was holding the wallet open forever.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The console redacted secret command ARGS but showed the OUTPUT of key-revealing
commands (dumpprivkey / z_exportkey / z_exportviewingkey / z_exportmnemonic /
dumpwallet / z_exportwallet, and the lite seed/export verbs) in full — visible
in the scrollback, Copy All, Ctrl+C and the filter. Now, when the submitted
command is key-revealing, its result is held ONLY in a member buffer (never in
the scrollback model, so Copy-All etc. can't leak it) and a masked placeholder
is shown. An explicit "Copy secret" button routes through the auto-clearing
clipboard; the buffer auto-wipes after 60s, on Clear, and on wallet lock
(via wipeDisplayedSecrets). The command string passed to the executor is
byte-identical. New pure helper ConsoleCommandRevealsSecret is unit-testable.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- writeFileAtomically honored restrictPermissions only on POSIX (0600); on
Windows it was a no-op, so exported keys / seed backups / the vault / chat
exports inherited the parent dir's ACL. It now builds a PROTECTED owner-only
DACL (current-user SID, SE_DACL_PROTECTED) on the temp file before the atomic
rename; best-effort, falling back to default ACLs on failure. (Verified it
compiles under mingw-w64; the POSIX path is unchanged.)
- The key-export dialog's Copy wrote the private key to the plain, permanent
clipboard; it now goes through copySecretToClipboard() (45s armed auto-wipe +
its own "copied, auto-clears" notice), like the seed backup copy.
- Bootstrap archive verification dropped the collision-prone MD5 fallback: it now
requires a published SHA-256 and fails closed otherwise. (Authenticity vs a
malicious server still needs a detached ed25519 signature like the updaters —
a separate release-coordinated change.)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- 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>
- lite_owned_string: the destructor and move-assignment no longer copy the raw
value before freeing (nothing reads the copy there, and a std::string copy can
throw bad_alloc — from the noexcept destructor that meant std::terminate, and
it could also double-attempt the copy after a throw). They now free only; the
copy happens solely in intoResult(), which also moves (not copies) the value
into its result so no extra plaintext lingers.
- lite_result_parsers: jsonToUnsigned rejected nothing on the string path, so
std::stoull accepted "-1" and wrapped it to a huge balance; now requires a
leading digit. jsonToSigned rejects leading whitespace/'+'. The outgoing-
metadata sum rejects negative values and guards against signed overflow (UB),
so a hostile/buggy backend can't forge or overflow a transaction amount.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Finishes the feedback-conventions hygiene thread: ~30 hardcoded ImVec4 green /
red / amber literals used for success / error / warning text now use
material::Success/Error/WarningVec4(), so semantic status colors follow the
active skin and stay legible in every theme (several hardcoded greens/reds read
poorly on the light/pastel skins). Covers validate-address, transaction-details,
key-export, export-all-keys, about, the Settings confirmations/lock state, and
the encryption/decryption dialogs. Brand/logo colors and muted white/grey
(disabled) text are left as-is; dynamic-alpha animated indicators are untouched.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- The import-key sweep and migrate-to-seed flows set several user-visible
status/error strings (shown via sweep_status_ / seed_migration_status_ /
rescan_status) as bare English; routed them through TR() with additive keys.
- Extended scripts/check-source-hygiene.sh with two low-false-positive content
guards so this debt can't regrow: a bare-English migrate/sweep status
assignment, and a Tactile button sized with a raw (unscaled) pixel literal.
Passes clean on the tree (362 files); the dev sweep harness is allowlisted.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Feedback-convention + HiDPI cleanup (from the audit's living-hygiene threads):
- Copy actions that were silent now toast a confirmation: transaction
address/txid (context menu, click-to-copy, detail buttons), peer and banned-
peer addresses, the Settings data-dir copy, the chat message "Copy", and the
key-export result copy.
- The first-run wizard's Skip/Back buttons used raw ImVec2(120/80,0) widths that
rendered tiny at HiDPI; they now multiply by ui::Layout::dpiScale().
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Finishes the transactions-export partial. The dialog now offers three scopes:
- All transactions (unchanged default; exported with no copy).
- Current History filter — the Transactions tab publishes a TxFilterSnapshot of
the visible legs (a merged autoshield row contributes both its send+receive
legs, so the CSV stays a per-leg record like an unfiltered export); the radio
is disabled with an explanation when no filter is active, and a snapshot that
predates the current tx list (revision mismatch) is refused rather than
silently exporting everything.
- Date range (YYYY-MM-DD, inclusive end-of-day; pending/dateless rows excluded
with a note; invalid/reversed dates blocked inline).
The live count reflects what will actually be exported. formatTransactionsCsv
and the atomic write are unchanged.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The transactions CSV export now sanitizes the typed filename (basename only,
illegal chars → '_', no leading dots/trailing dots-or-spaces, length-capped,
always .csv) so it can't be pointed outside the config dir, and the success
screen gets an "Open folder" button. (All-vs-filter / date-range export scope is
left for a follow-up — it needs tx-tab filter-snapshot plumbing.)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Chat: a dismissible first-run intro card (how chat works — encrypted, sent as
shielded txs with a small fee, tied to the seed) with a "Copy my chat address"
button, shown in the empty thread pane; dismissal persisted. The copy logic is
shared with the existing sidebar button (moved to a helper, unchanged).
- Toasts: clicking a toast now opens the existing notification-history panel, and
a "+N more" badge appears when several live toasts are queued (render only ever
showed the newest). Badge opacity is a ui.toml token.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Explorer: a hash lookup now tells a transport/RPC failure apart from a genuine
not-found (RPC -5/-8), showing "Lookup failed: <friendly error>" instead of
"no block or transaction found"; block-detail errors routed through
friendlyRpcError. getblock/getblockhash/getrawtransaction calls unchanged.
- Key-export: when the wallet is locked, the dialog now says "Unlock your wallet
first" with an Unlock button (lite → requestLiteUnlock; full-node → close to
reveal the lock screen) instead of a raw error, and the fetching state animates
with LoadingDots. The export RPC/args are unchanged.
(i18n.cpp carries the additive keys for these and the chat/CSV commits below.)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Two gaps in the destructive-command confirm:
- Regression: a command confirmed via the command-reference dialog's "Run" was
replayed through submitConsoleCommand, which re-armed the destructive gate and
silently did nothing until the user retyped it. The dialog now pre-arms the
exact-line confirm so the replay executes.
- z_exportmnemonic (full-node seed-phrase reveal) wasn't in the reference
catalog, so a pasted z_exportmnemonic ran instantly. Added a small
secret-revealing fallback set (z_exportmnemonic, z_exportviewingkey) to the
destructive check. Command execution is otherwise unchanged.
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>
The header key-verify lock was an unexplained hover-only icon. It's now
clickable, opening a dialog that shows the contact's identity-key fingerprint
and explains comparing it out-of-band, with a Copy-key button and a clearer
tooltip. The crypto/verification logic is unchanged (the wording is careful that
the lock means "encrypted to this key", since there is no trusted/verified flag).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Peer ban actions only toasted on failure; success was silent. Added success
toasts for ban/unban/clear-all, and the bulk "Clear All Bans" now takes a
confirming second click (per-row unban stays one-click). The setban/clearbanned
RPC calls are unchanged.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- First-run handoff: one info toast ("node starting, will keep syncing in the
background") at the completion point, instead of dropping the user onto a busy
blank window with no explanation.
- Daemon updater: a newer installed binary now sets a persistent "restart
pending" reminder (surfaced in Settings → Daemon Binary with a Restart button)
and the updater's Done screen leads with a warning-level "restart required"
headline — easy to miss before.
- Settings: the debug-categories "restart required" note is raised from ~35%
alpha to a readable color with the restart prompt moved directly beneath it;
the DEBUG OPTIONS header shows a lock + explanatory caption so it's clearly a
gated diagnostics section (the auth gate itself is unchanged); a couple of
hardcoded color/size literals now use material tokens / dpiScale.
- Removed the legacy duplicate settings_window.cpp/.h (only reachable via a dev
flag, never in production) so edits can't silently miss one of two settings UIs.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- The failed-unlock error message now stays on screen until the next attempt /
mode switch / success, instead of fading after ~3s (the reason was often gone
before it could be read).
- The escalating failed-attempt lockout counter + expiry are now persisted
(keyed to the active wallet file) and restored once per lock-screen session,
so killing and relaunching the app can no longer reset the lockout curve — a
brute-force window. A forward clock change is capped; the PIN/passphrase is
never stored. The verification (walletpassphrase/decrypt) path is unchanged.
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>
The chat header's export button wrote a decrypted plain-text file immediately on
click, warning only via a hover tooltip. It now opens a confirm dialog that
states the file is unencrypted and shows the destination path before writing.
The export format, path and write call are unchanged.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- The pool payout-address rule is no longer hover-only: an invalid payout
address now outlines the field in red and shows a short reason below it (a row
is reserved so it doesn't overlap the card beneath). The longer tooltip stays.
- The solo "Mining Addr" row gains a hover explainer and a "None yet" empty-state
(with a tooltip pointing to Receive → New mining address) instead of a blank.
- Replaced hardcoded English in the earnings/stats cards ("(%d txn)", uptime
"N/A"/"%lldm %llds"/"%lldh %lldm") with TR() keys.
Also carries the chat-export confirm strings used by the next commit.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Request-Payment no longer dead-ends with no shielded address: it shows a hint
and a "Create a shielded address" button (auto-selecting the new address), and
labels address entries with their contact/wallet names.
- The balance "Recent transactions" empty area uses a proper empty-state and now
distinguishes "still loading / syncing" from "no transactions yet" instead of a
dim one-liner that also showed mid-sync.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Send-tab UX pass (presentation/validation only — the z_sendmany submit path, its
amounts, fee, recipients and memo bytes are unchanged, and the Review button plus
the confirm-time live re-check still block any send until the wallet is synced):
- The form is now editable while syncing (so the recipient/amount/memo can be
pre-filled); only Review/Send stays gated, with an "available once synced" note.
- The recipient's red "Invalid address" no longer flashes from the first
keystroke — it waits until the field loses focus.
- The memo field stays visible (text preserved) with a "requires a shielded
recipient" explainer for transparent recipients, instead of vanishing + silently
dropping what was typed.
- "Cancel" (which only cleared the form) is relabeled "Clear form", disabled when
empty, and drops its redundant confirm (the timed Undo already covers it).
- The source picker shows address labels, and warns when the chosen source is
transparent (non-private on this chain).
Also adds the string keys used here and by the next commit.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- The Shielded balance card gains the explanatory hover tooltip the mining card
already has (suppressed while the pending-chip tooltip is showing).
- Toasts get a severity-tinted background fill and a slightly thicker accent
border, so Success/Warning/Error/Info differ by more than hue alone (the
per-severity leading icon already existed). Tint opacity is a ui.toml token.
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>
The conversation list now marks a pending stranger request with a small
"Request" pill (bottom-right of the row), so it's distinguishable before you
open it; the preview text is clipped to the pill's left so they don't overlap.
Completes the request-affordance pair with the in-thread Accept/Block card.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Extends the labeled "Pending +X" chip (with its awaiting-confirmations tooltip)
to three more layouts via a shared DrawPendingChip() helper. It draws on the
ticker line (the USD value sits on the line below in these layouts, so there's no
overlap) and is skipped if it would overflow the card. The remaining draw-list
layouts with right-aligned content (Consolidated, Minimal, the grid variants)
are left for a follow-up.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
An unsolicited first message from a stranger (an incoming ContactRequest, with
nothing sent by us and the peer not in contacts) now shows an Accept / Block
card above the thread instead of looking like a normal conversation. Accept adds
the peer to the address book (which clears the request state); Block deletes and
blocks the conversation (reversible from the Blocked manager). ConvSummary gains
a derived pendingRequest flag. (The list-row "Request" pill and a header Block
button were left out to avoid the fragile hand-drawn header-toolbar geometry.)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Shield-coinbase replaced its free-text "*"-wildcard address box with a dropdown
of the wallet's mining (transparent) addresses and their balances ("All mining
addresses" default), and the confirm step now previews "Shield up to X DRGX from
N mining address(es)… to <label>? Network fee: Y" instead of only relabeling the
button. Also blocks submitting from an empty source or one with no funds. The
z_shieldcoinbase call (args/fee/limit/destination) is unchanged.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The unconfirmed balance was shown in only one layout as an unlabeled "+X" that
read as a mystery. It's now a labeled "Pending +X" chip with an "awaiting
confirmations" tooltip — the Classic card's badge is relabeled, and the shared
compact hero (Dashboard + Two-row layouts) now draws it too. (The remaining
hand-drawn layouts can reuse the same pattern as a follow-up.)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Bootstrap: Retry now reuses the source the user actually started (was always
the default), and a "Try mirror" button offers the alternate server on
failure (s_lastUrl remembered on start, cleared on a fresh Confirm).
- Validate Address: classification + checksum are local, so the dialog no
longer requires a node connection — offline/lite now validate and show the
type, with ownership reported as "Unknown — node offline / not checkable in
the lite wallet" instead of a dead "not connected" screen.
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>
The shield/merge status line was amber only while in-flight and uncoloured for
every terminal state (success, failure, uncertain). It now tracks an OpOutcome
(display-only) and colours the terminal line Success/Error/Warning accordingly,
shows a LoadingDots spinner while the 120s scan runs, and maps the raw daemon
error through friendlyRpcError. The actual shield/merge submit logic is
unchanged (no "Try again" that could double-submit a fund move).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>