First incremental slice of decomposing the 2628-line mining_tab.cpp monolith (one giant
RenderMiningTabContent function). The ~636-line "Earnings" card section is moved verbatim into
RenderMiningEarnings(); mining_tab.cpp is now 1992 lines and calls it with the immediate-mode
layout context as parameters (draw list, fonts, scale/spacing, glass spec, pool-mode flag).
Behavior-preserving by construction: the body is byte-identical (the only additions are a
`const bool s_pool_mode = poolMode` alias and a local scratch `buf` so the moved code keeps its
original identifiers). The earnings-filter static moved with the card it belongs to. The
compiler surfaced every enclosing dependency, which became explicit parameters.
Verified: full-node + Windows + lite build, tests, hygiene, clean smoke start. Pending hands-on
visual check of the Earnings card before extracting the next section.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The sync throttle (kSyncProfile) covers the core/transactions/addresses/peers timers, but
getmininginfo runs off the separate 1s Fast timer and so still polled ~every 5s during
sync — another cs_main contender slowing block connection. Skip it while syncing unless the
user is on the Mining tab or actively mining (where live stats are wanted). Completes the
"no RPC contention during sync beyond the 10s progress poll" goal.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The full-node wallet polled the daemon at the per-tab cadence regardless of sync state.
On the Peers/Network tab that meant getpeerinfo every 5s + core every 5s + a full
transaction scan on every new block — and blocks arrive fast during sync. Each of those
calls takes the daemon's cs_main lock, the same lock block connection needs, so the node
synced noticeably slower than on the lightweight Console tab (core 10s, no peer polling).
Make the refresh cadence sync-aware:
- RefreshScheduler::kSyncProfile {core 10s, transactions/addresses/peers disabled} is applied
to ALL tabs while state_.sync.syncing, and reverts to the per-tab profile when sync ends.
applyRefreshPolicy() picks the profile; update() re-applies it on the syncing<->synced
transition. This suppresses getpeerinfo and the per-block tx scan during sync (that data is
incomplete mid-sync anyway) — every tab now syncs as fast as Console.
- collectCoreRefreshResult(rpc, includeBalance): skip z_gettotalbalance (wallet lock + cs_main)
while syncing; only getblockchaininfo runs, which is also what drives sync-progress detection.
applyCoreRefreshResult already leaves the balance untouched when balanceOk is false.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- rpc_client::callRaw: a daemon error object is no longer assumed to carry a string
"message" — a malformed error now yields a clean "RPC error: <dump>" instead of throwing
a json type-exception from .get<std::string>().
- sendTransaction (full-node): add a single-flight guard so a rapid double-click can't issue
two z_sendmany before the first returns its opid. The lite path already guarded this; the
send form guards it in the UI, but the controller entry point now does too.
(#9 from the audit was mostly false positives on verification — all popen sites already
null-check and the xmrig download FILE* path has no throwing calls. The payment-URI
checksum idea was dropped: the send flow already checksum-validates the recipient before
broadcasting, and tightening the parser would reject the placeholder addresses the existing
test relies on; added a comment noting this is format-only by design.)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- rpc_client: wipe the plaintext "user:password" temporary with sodium_memzero after
base64-encoding it into the auth header (std::string doesn't zero its buffer on
destruction).
- connection: the auto-generated DRAGONX.conf holds rpcuser/rpcpassword in plaintext but
was written with the default umask (often world-readable 0644). Restrict it to owner
read/write after creation so another local user can't read the credentials.
- app: copying a seed phrase / private key to the clipboard now arms an auto-clear —
App::copySecretToClipboard() copies the secret and, after 45s, wipes the clipboard IF it
still holds that secret (compared via a stored hash, never the plaintext). Wired into the
lite first-run wizard's seed Copy and the Settings export-secret Copy, with a
"clipboard auto-clears in 45s" notice. pumpSecretClipboardClear() runs each frame.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Per-frame hot paths in the immediate-mode UI were allocating needlessly:
- Address filtering in the balance tab rebuilt a std::string filter per address AND
containsIgnoreCase() lower-cased two fresh copies per call — ~6×N allocations/frame
on large wallets. New util::containsIgnoreCase(string_view, string_view) is
allocation-free, and the filter is now built once outside the loop.
- Four duplicated "time ago" implementations (balance_tab_helpers, balance_recent_tx,
send_tab, transactions_tab) are consolidated into util::formatTimeAgo (localized long
form) + util::formatTimeAgoShort (compact "5s ago"), preserving each call site's exact
display style. Both use snprintf, no per-row string concatenation.
- The send-tab address-suggestion scan (a walk over the whole tx list) is memoized on the
typed text + tx count, so it no longer recomputes every frame while the user pauses.
New src/util/text_format.{h,cpp}; the two existing containsIgnoreCase/timeAgo definitions
now delegate to it. Added to both the app and test targets (test target also gains i18n.cpp,
which text_format's localized path needs).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Two issues shared one root cause: the shielded-receive scan marked each z-address "scanned
at the EXACT current tip," but a new block (~36s on DRGX) advances the tip and invalidates
every prior per-address scan. A wallet with more z-addresses than one refresh cycle can
scan therefore never reached "all scanned at tip" — so shieldedScanComplete stayed false
and transactions_dirty_ stayed true forever, which (a) kept the history-refresh banner lit
and the full rescan churning every cycle, and (b) blocked maybeFinishTransactionSendProgress
(it waited on transactions_dirty_), leaving the send-progress indicator stuck on.
Fix 1 — completion tolerance. Add TransactionRefreshSnapshot::shieldedScanTipTolerance: an
address counts as fresh if its last scan is within N blocks of the tip (0 = old strict
behavior, so existing tests are unchanged). The app scales N with the z-address count
(2 + count/96, capped at 50), so a multi-block pass can COMPLETE before its earliest scan
goes stale. This also throttles full rescans to ~N blocks instead of every block —
transactions_dirty_ clears, the banner stops, and CPU/RPC churn drops. Already-fresh
addresses are skipped, so the per-block cost falls back to just the (cheap) transparent
listtransactions.
Fix 2 — send-progress gate. maybeFinishTransactionSendProgress() no longer waits on the
transaction history scan (transactions_dirty_ / Transactions job): the sent tx is already
shown via the optimistic pending insert, and the spend is reflected once the balance
refresh lands, so it now finishes on the address/balance signal alone.
Test: a tolerant snapshot skips recently-scanned addresses (shieldedAddressesScanned == 0,
shieldedScanComplete) while a strict one re-scans them.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The banner was driven by transactions_dirty_, which refreshTransactionData() sets to
!shieldedScanComplete. The shielded-receive scan marks each z-address "scanned at tip,"
but every new block (~36s on DRGX) advances the tip and invalidates all prior per-address
scans, so for a wallet with more z-addresses than the per-cycle budget (8 on History) the
scan can never catch the tip — shieldedScanComplete stays false, transactions_dirty_ stays
true, and the banner stayed lit indefinitely.
Decouple the user-facing banner from that perpetual background scan:
- A just-sent transaction being enriched still surfaces (the user is waiting on it).
- Once any history is displayed, stay quiet for routine background refreshes — new receives
still appear live as they're scanned.
- The loading banner now only shows during the genuine INITIAL load (nothing displayed yet)
and send enrichment.
This is a UI-visibility fix; the underlying per-block full shielded rescan (and the related
send-progress flag that maybeFinishTransactionSendProgress gates on transactions_dirty_) are
separate follow-ups.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The History tab rebuilt its entire display list on every render frame: indexing all
transactions by txid, merging autoshield send+receive pairs into "shield" rows, and
std::sort-ing the result — O(N log N) plus several heap allocations at ~60fps, only to
show one 50-row page. The data is already sorted newest-first by the refresh service,
so the per-frame sort was redundant on top.
Memoize the merged+sorted list, rebuilding only when the underlying transactions
actually change. The cache key is a cheap, allocation-free FNV-1a fingerprint over the
display-relevant fields (count, last update time, and each tx's confirmations /
timestamp / type+address first char) — a new block bumps every confirmation so the key
changes and we rebuild; otherwise (the common read/scroll case) the cache is reused.
Filtering, search, and pagination still run per-frame over the cached list (cheap linear
scans that depend on interactive state).
Also document that App::shouldRefreshTransactions() is block-height/dirty driven (not
interval-gated) — the Transactions timer only paces the check; the recent-poll handles
between-block mempool/unconfirmed deltas.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The 1.3.0 lite-capability work gated isUsingEmbeddedDaemon() on the compile flag
DRAGONX_ENABLE_EMBEDDED_DAEMON (in 1.2.0 it was hardcoded true, so the daemon
always launched). The lite branch in CMakeLists set that flag OFF with
`CACHE BOOL ... FORCE`, which POISONS the build dir's cache: a later full-node
reconfigure of the same dir keeps the forced-OFF value (the full-node branch
never re-asserts it), so embeddedDaemonAvailable=false and the wallet extracts
dragonxd but never starts it — exactly the reported "unpacks dragonxd.exe but
does not start the daemon, manual start works."
Note the two gates are independent: the binary is EMBEDDED/extracted via build.sh
(HAS_EMBEDDED_DAEMON), while LAUNCHING is gated by DRAGONX_ENABLE_EMBEDDED_DAEMON
— so they diverged (extract yes, launch no).
The forced cache write was also pointless: makeWalletCapabilities() already
forces the embedded-daemon capability off for any lite build via
`fullNodeBuild && embeddedDaemonCompiled`, so lite never launches a daemon
regardless of the flag.
Fix:
- CMakeLists: remove the FORCE cache poisoning (the root cause).
- build.sh: set DRAGONX_ENABLE_EMBEDDED_DAEMON explicitly per variant (ON for
full-node, OFF for lite), mirroring the existing DRAGONX_BUILD_LITE handling,
so an already-poisoned build dir is HEALED on the next build rather than
silently keeping the stale OFF.
Verified: a poisoned Windows cache (=0) flips to =1 on reconfigure; full-node
builds define =1, lite =0; tests + hygiene green.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The connection probe (getinfo) used a 30s request timeout, so when something on
the local RPC port accepts TCP but never answers — a daemon still loading the
block index, or a wedged/foreign occupant — every attempt blocked the full 30s
before the wallet could retry or update its status. That is the "stuck, timing
out every 30s" behaviour users hit.
A healthy local daemon answers getinfo in milliseconds, and a warming one
returns -28 just as fast, so a long hang on localhost only ever means trouble.
Probe localhost with an 8s timeout (remote/TLS keeps the 30s budget). The
per-call override restores the persistent 30s afterwards, so normal RPC calls
that legitimately take longer are unaffected — only the probe fails faster, so
the wallet retries promptly and reflects "initializing" / recovery within
seconds of the daemon becoming ready.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The daemon can spawn successfully (CreateProcess OK) and then exit immediately —
a missing runtime DLL, wrong architecture, corrupt binary, datadir lock, etc.
EmbeddedDaemon's crash monitor already builds a detailed reason for this
(translated Windows exit code, e.g. "STATUS_DLL_NOT_FOUND — required DLL not
found", plus the launch command and a debug.log tail) and stores it in
lastError(), but it runs on a background thread and was never shown. The result
was the exact symptom users reported: the wallet unpacks dragonxd.exe, looks
"stuck connecting", and the node silently dies-and-respawns until it gives up —
with no visible reason (manually starting dragonxd works, so the wallet then
connects to it).
tryConnect now watches the daemon's crash count (on the main thread, where it
already logs daemon state) and surfaces each NEW crash's lastError() once, as a
sticky error notification, with a concise "Couldn't start dragonxd" status. The
counter resets on a successful connect (alongside the daemon's own crash-count
reset), so a later crash re-notifies.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Two failure modes left the wallet stuck on a silent "connecting / Starting
dragonxd…" spinner with no path forward:
1. Stale external-daemon latch. EmbeddedDaemon::start() sets
external_daemon_detected_ whenever the RPC port was busy at a prior attempt
and never re-checks it, so tryConnect's no-config branch trusted that latch
and waited forever for a config the phantom would never write — even after a
stale/half-dead process freed the port. Now the port is re-evaluated LIVE
(EmbeddedDaemon::isRpcPortInUse()) each attempt: if it's genuinely busy we
keep waiting (and, after a bounded ~20s with no config, warn that whatever
owns the port isn't a usable DragonX node and how to fix it); if it's free we
fall through and start our own daemon.
2. Silent start failure. When startEmbeddedDaemon() failed (binary not found,
Sapling params missing, spawn failure) the status stayed on "Starting
dragonxd…" with the real reason only in a VERBOSE log. Now the reason
(daemon_controller_->lastError()) is surfaced once as a sticky error
notification, with a short "Couldn't start dragonxd" status.
Both counters reset on a successful connect so the messages re-arm for the next
disconnect. Lite is unaffected (tryConnect returns early for lite builds).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The lite backend's litelib_execute() is the same command interface as
silentdragonxlite-cli (balance, info, height, list, notes, addresses, sync,
syncstatus, new, send, shield, encrypt, …), so the lite Console can be a real
interactive console — like the full-node RPC console — instead of a read-only
diagnostics log.
Controller: add an async arbitrary-command runner mirroring the broadcast
pattern — runConsoleCommand() splits "<command> [args]" (the first token is the
command, the remainder is passed as the single arg string litelib_execute
expects, since it does NOT whitespace-split), runs the bridge call on a detached
thread that captures the shared bridge (never `this`), and delivers the result
to a main-thread slot drained by takeConsoleResult(). Results are NEVER routed
through LiteDiagnostics (seed/export can return secrets).
Console tab: a command input (Enter to run, Up/Down history via the shared
console_input_model helpers) over a unified scroll buffer that interleaves the
automatic diagnostics events with user command I/O, colour-coded, with the live
status header preserved. The input is disabled while a command runs.
Two backend footguns are intercepted at the UI layer before forwarding:
`clear` (the backend command WIPES wallet tx history — re-bound to clearing the
view, what the user expects) and `quit`/`exit` (would only save; the embedded
backend must stay running with the app).
Test: runConsoleCommand drives the fake backend (info -> raw response; "new zs"
-> exercises the command/arg split; blank line rejected).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The full-node Console tab already streams the daemon's output, but during
startup the user is held on the loading overlay (wallet-data tabs are blocked),
so they can't watch progress without navigating away. Surface the last few
console lines the node printed (UpdateTip height=…, "Verifying blocks…", etc.)
directly under the status/description on the overlay while initializing or
warming up, so progress is visible where the user is already looking.
Full-node only (guarded on daemon_controller_); each line is trimmed and
ellipsis-truncated to one row. Reuses DaemonController::recentLines().
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
When the full-node connect probe (getinfo) times out, the daemon is reachable
at the TCP level but busy initializing (loading the block index, verifying,
activating best chain, …) and won't answer RPC. The wallet only recognized the
JSON-RPC -28 warmup reply, so a raw socket timeout fell through to a bare,
alarming "Connection failed" retry with no indication of what the user was
waiting on.
Add a daemon-initializing UI state that drives the existing loading overlay:
- WalletState::daemon_initializing — daemon up/launching but not serving yet
(distinct from warming_up, which needs a -28 reply).
- App::applyDaemonInitStatus() infers the current phase from the daemon's own
console output (scanning recent lines for Loading/Verifying/Activating/
Rescanning/Rewinding/Pruning) and the latest block height, producing a
friendly title + description, e.g. "Processing blocks… (Block 123456)".
- The connect loop calls it from the daemon-starting and external-detected
branches: a timeout -> "reachable but initializing", a connect refusal ->
"launching, waiting to come online". Cleared on a real connect.
- The loading overlay now shows the description for daemon_initializing too,
and the status-bar amber indicator covers it (so Peers/Console tabs without
the overlay still explain the wait).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The Settings page drove the controller's synchronous createWallet/openWallet/
restoreWallet, which blocks the UI thread on the (often flaky) lightwalletd and
gives up after the first server. Add a generic async lifecycle path that mirrors
the async-open failover but carries the full request (passphrase, restore seed/
birthday/account/overwrite):
- beginCreateWalletAsync / beginOpenWalletAsync / beginRestoreWalletAsync run
on a detached thread that builds its OWN local LiteWalletLifecycleService
from captured value copies + the shared bridge (never `this`, so it can
safely outlive the controller). Each request type's serverUrl override field
feeds the failover: try the preferred server, then every other usable
default; stop on the first ready wallet or a structural block; keep the
preferred server's error on total failure. The request's secrets are wiped
once the attempt finishes.
- pumpLifecycleResult() finalizes on the main thread (flip walletOpen, persist,
start sync) and caches the result for the UI; wired into App::update next to
pumpAsyncOpen(). beginAsyncLifecycle() now also yields to an in-flight
lifecycle request so the auto-open loop can't race it on the same bridge.
- settings_page kicks off the async op, disables the button while in flight,
and polls the cached result each frame for the status/summary.
Tests: testLiteWalletControllerAsyncLifecycleFailover covers async create (with
passphrase) and restore failing over preferred->fallback, plus all-servers-down.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Mirror the async-open path for wallet creation. beginOpenExisting() and
beginCreateWallet() now both delegate to beginAsyncLifecycle(bool create),
which runs the backend init on a detached thread and walks the failover
server list (preferred server first, then all usable defaults), reporting
the preferred server's error on total failure. The first-run wizard's
Create button drives this through a non-blocking "creating" poll state so
the UI no longer freezes while the backend contacts a (possibly flaky)
lightwalletd. The created seed response is securely wiped immediately and
read back via exportSeed for the reveal/verify steps.
Safe because litelib_initialize_new contacts the server before writing any
wallet file and LightClient::new errors if a wallet already exists, so a
failed candidate leaves no partial state.
Tests: fake backend's initialize_new now honors the dead/warmup server
substrings; testLiteWalletControllerOpenFailover gains a create-failover
case (preferred dead, fallback good -> walletOpen).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Restore: live "N / 24 words" count, a one-line birthday explanation, and a guard
that rejects a restore unless all 24 words are entered (the secret scrubber still
wipes the input on the early return).
- Backup: "Show seed" now also shows the birthday (needed to restore quickly) with a
"back this up too" note, a stronger "only way to restore" warning, and a "Save to
file" button that writes the seed + birthday to an owner-only (0600) file in the
config dir via the atomic-write helper.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Creating a wallet was one-click and silent — it never showed the seed, relying on
the user to later find Settings -> Show seed, which is an easy-to-miss fund-loss
risk. Replace the first-run prompt with a 3-step guided flow mirroring the upstream
SilentDragonXLite wizard:
1. Welcome (Create / Restore / Later) — unchanged entry.
2. Reveal: after createWallet, read the seed back via exportSeed and show all 24
words (numbered grid) + the birthday, with a strong "only way to restore"
warning, plus Copy. ("Skip" leaves the wallet created, seed backable later.)
3. Verify: tap the words in order (shuffled chips) to confirm the backup before
finishing; out-of-order taps are rejected with a hint.
The seed is held only for the wizard and securely wiped (sodium_memzero) on finish.
Builds clean for full-node, lite, and Windows cross-compile.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The failover overwrote outcome.error on every attempt, so a total open failure
reported whichever (often broken) fallback was tried last — e.g. lite5's
"CertNotValidForName" — instead of what the user's preferred server actually did.
Keep the first (preferred) server's error as the summary so "Open failed: …" names
the actionable reason; the per-server attempts are still in the Console log, and
the warmup flag is still set if any server was warming up.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
When the preferred lightwalletd server is reachable but warming up (JSON-RPC -28
/ "Activating best chain"), the failover treated it like a dead server and fell
through to the others, so the wallet didn't open until the next 20s retry — even
though the healthy server was ready within seconds.
Detect the warmup error during failover, flag it on the open outcome
(lastOpenWasWarmup()), and have the App retry on a short ~4s interval in that case
instead of 20s, so the wallet opens promptly once warmup clears. A unit test
covers a warming-preferred + dead-fallback open setting the flag.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
A lite build compiled without the SDXL backend (DRAGONX_ENABLE_LITE_BACKEND off,
i.e. built with --lite instead of --lite-backend) leaves the controller null, so
the wallet never opens and the UI shows a silent "disconnected" state. The Console
status now states the cause and the fix directly ("Lite backend not linked in this
build (rebuild with --lite-backend)") instead of a vague "unavailable".
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The Console could look empty if the wallet produced few events. Make it useful
in every state and remove a cross-platform footgun:
- Add a live status header read straight from the controller (connected /
connecting / disconnected, sync %, and the last open error) — independent of the
diagnostics event log, so the Console always shows the current connection +
wallet-open state even when the log is sparse.
- Move LiteDiagnostics::instance() into a single .cpp so there is exactly one
instance across the binary, rather than relying on the linker folding an
inline-function static across translation units (a known fragility, especially
on mingw/Windows — the most likely cause of a stuck-empty event log there).
Verified the writer and reader share one instance on Linux; builds clean for
full-node, lite, and Windows cross-compile; tests pass.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The lite variant had no visibility into why a wallet failed to open — just a
"disconnected" spinner. Add a lite-only Console tab (full-node keeps its RPC
console) that shows a live diagnostic log.
- LiteDiagnostics: a small thread-safe, bounded ring buffer (header-only). The
controller writes to it from its background threads: each failover server
attempt and result, wallet open/create/restore outcomes, sync start, and
blocked-open reasons. The App logs controller (re)builds with the preferred
server.
- lite_console_tab: a terminal-styled, read-only view of the log (newest at the
bottom, error/success lines coloured) with Clear / Copy / Auto-scroll. Reachable
even when the wallet is locked (it's diagnostics, no secrets). Registered as
NavPage::LiteConsole, gated lite-only via WalletUiSurface::LiteConsole.
A unit test drives an open-with-failover and asserts the log records the
connection attempt and the successful open. Built clean for full-node, lite, and
Windows cross-compile.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Opening an existing lite wallet ran synchronously on the UI thread and used a
single server, so a dead/unreachable lightwalletd server froze startup for the
connect timeout and then stranded the wallet ("disconnected" spinner) — and the
DragonX lite servers are flaky (often several down at once).
Add LiteWalletController::beginOpenExisting() / pumpAsyncOpen(): the open runs on
a background thread (mirroring the sync/broadcast shared-lifetime pattern — it
captures only shared_ptrs + value copies, never `this`), trying the preferred
server first and then every other usable default until one succeeds. The main
thread finalizes the result (flips walletOpen, starts sync) or records the reason.
The rollout gate is still checked up-front on the main thread.
App: auto-open now calls beginOpenExisting() and pumps it each tick, retrying on
a 20s interval so a transient outage self-heals once a server returns; a failed
open surfaces its reason (notification + Network tab) instead of a silent spinner.
Tested: a fake bridge that fails specific servers exercises both
preferred-dead -> fallback-opens and all-dead -> fails-with-reason. Built clean
for full-node, lite, and Windows cross-compile.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The wallet auto-open is a one-shot (lite_autoopen_done_), but rebuildLiteWallet()
creates a fresh, closed controller — so switching the lite server from the Network
tab (rebuildLiteWallet force=true), or any later rebuild, left the wallet
permanently closed ("disconnected" spinner) because auto-open never fired again.
Re-arm the one-shot (and clear the surfaced open-error) in rebuildLiteWallet so
the next update() tick reopens the existing wallet against the new server. This is
the recovery path when the configured lightwalletd server is unreachable: the
Network tab surfaces the failure reason, the user picks a reachable server, and
the wallet reopens. Also makes the Network tab's apply-immediately server switch
actually take effect.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Both variants hardcoded "ObsidianDragon" as the per-user config folder
(settings.json, themes, the lite_rollout cache), so the lite app and the
full-node app shared one settings.json. That cross-variant pollution can leave
the lite server selection in a bad state — and since openWallet() contacts the
selected lightwalletd server, a wrong/empty server URL there makes an existing
wallet fail to open (a silent "disconnected" spinner).
Use DRAGONX_APP_NAME (already "ObsidianDragon" / "ObsidianDragonLite" per variant)
for the config-dir name in Settings::getDefaultPath, Platform::getConfigDir and
getObsidianDragonDir (and the theme-setup exe-name probe). Full-node is unchanged;
lite now reads/writes %APPDATA%\ObsidianDragonLite (and ~/.config/ObsidianDragonLite),
so it starts from a clean, isolated config and uses default servers.
Note: the lite wallet file itself lives in the litelib backend's own data dir
(unaffected); this isolates the GUI config only.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The startup auto-open of an existing lite wallet discarded openWallet()'s result,
so when initialize_existing failed (e.g. the lightwalletd server is unreachable)
the UI just showed a "disconnected" spinner with no reason — and DEBUG_LOGF is
compiled out of release builds, so there was no way to see why. Capture the
failure: store the reason, show it in the Network tab status line (in place of
"no wallet open"), and raise a notification. Cleared once a wallet opens.
This doesn't change open behaviour — it makes a stuck open diagnosable.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
stop()-ing a worker that is mid curl_easy_perform joined on the UI thread, so a
slow/hung transfer froze the UI until the request timeout. Add RPCClient::
requestAbort() (a thread-safe atomic read by a curl progress callback that aborts
the transfer), and call it before stopping the workers on disconnect
(onDisconnected) and shutdown (beginShutdown + the synchronous fallback). The
flag is cleared on each connect() so a fresh connection never starts aborted.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The send screen labelled any prefix+length match as a "Valid" address, so a
mistyped address that still matched the pattern passed the gate. Add pure,
offline checksum validation — Base58Check (transparent R-addresses) and Bech32
(Sapling zs-addresses) — and require it in the validity check. Both verifiers are
version-byte/HRP agnostic (the HRP is taken from the string, the Base58 checksum
is chain-independent), so a correct implementation never rejects a genuine
address while catching transcription errors. Works for both build variants
(no daemon round-trip), unit-tested against standard BIP173 / Base58Check vectors.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
removeVault() overwrote vault.dat with zeros then unlinked it, but never flushed
to stable storage, so the zeros could stay in the OS cache and never reach disk.
flush + fsync before unlink on POSIX (still best-effort on CoW/SSD, but now does
what it claims).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
resetJobs() cleared the in-progress flags but left generations_ untouched, so a
refresh WorkFn still executing on the worker when a disconnect cleared state_
could pass completeDispatch's generation check and apply last-connection data
onto the new session. Bump every job's generation in resetJobs() so any
pre-reset ticket is treated as stale.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- importKey routed transparent vs. shielded purely by the first character, which
can mis-route (e.g. testnet/regtest WIFs). On failure, try the other import
command before reporting an error (each validates the encoding, so a wrong
command rejects rather than mis-imports). The key copy is wiped after both tries.
- Clamp the shield dialog's fee input to [0, 1] DRGX, mirroring the UTXO-limit
clamp, so a negative or fat-fingered huge fee can't be submitted.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Balance card hashrate now uses the shared FormatHashrate() (TH/GH/MH/KH/H)
instead of a bespoke two-tier KH/s formatter.
- Recent-tx rows show the full untruncated address on hover — two z-addresses can
truncate to the same first/last window — and the truncate helpers guard maxLen<=3.
- Remove the unused viewTop/viewBot "viewport culling" locals in the tx list
(pagination already bounds per-frame work).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The transaction-status overlay decided error vs. success styling by searching the
status string for "Error"/"Failed" — so under a non-English locale a failed send
rendered as a green success. Drive it from the existing s_status_success flag
instead. Also show the USD-mode DRGX preview at 8 dp so it matches the confirm
panel and the amount actually sent (was 4 dp).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- testAtomicFileWrite: Platform::writeFileAtomically creates dirs, overwrites,
leaves no .tmp, and honors owner-only perms.
- failureByOpid assertion in the operation-status poll parser test.
- testTransactionHistoryCachePrunesOldWallets: a save under a new identity prunes
the prior identity's snapshot.
- testLiteSendShowsRecipientFromOutgoing / testLitePartialRefreshKeepsPriorAddressBalances.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Many strings are used directly as printf/ImGui format strings, and translations
are loaded from user/installer-modifiable JSON with no validation. A translated
value that drops or changes a conversion specifier would be passed to printf with
mismatched varargs (undefined behavior) on a wallet screen.
overlayTranslations() now compares each translated value's argument signature
against the English source and keeps English on mismatch. Also adds the
send_status_unconfirmed string used by the deferred-send-result path.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
GetAvailableBalance() read state.addresses[s_selected_from_idx], but the index
desyncs from s_from_address (the value actually debited) after an address-list
refresh, and is left at -1 when the source is chosen from another tab's "Send
from this address" — which made the sufficiency check see a 0 balance and wrongly
block a valid send. Look the balance up by matching the source address string.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- A Send record carries its recipient in outgoing_metadata, not the top-level
address/memo, so sent txs showed a blank destination + memo. Surface the first
recipient (single-recipient case) into the transaction list.
- A tolerated partial refresh where the notes/utxo command failed (addresses
present, spendable outputs absent) zeroed every per-address balance, which looks
like fund loss. Preserve the last-known per-address balances in that case.
- Retry the post-send/shield save once on transient failure instead of ignoring
the result (the backend does not auto-save after send/shield).
- An unparseable broadcast response now uses cautious wording ("status could not
be confirmed — check Transactions before retrying") rather than implying a hard
failure, avoiding a blind double-spend retry.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Write vault.dat atomically and 0600 (it holds the PIN-encrypted passphrase, so
a world-readable copy enables an offline brute-force of the short PIN), and
chmod the tx-history SQLite + its WAL/SHM sidecars to 0600 on open.
- The tx-history snapshot and key-salt rows are keyed on a hash of the full
address set, which changes whenever a new address is generated — orphaning the
prior hash's full-history blob and salt forever. pruneOtherWallets() now drops
rows for every non-live wallet hash on each save, bounding the database.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Market chart now plots the real accumulated price_history instead of a
rand()-generated curve; the hover tooltip no longer claims a specific "Xh ago"
price and the x-axis only labels the truthful "Now" point. Falls back to the
existing empty state until there are >=2 real samples.
- Transactions summary cards exclude autoshield legs (same txid send + receive-to-z)
so a shield isn't double-counted into both Sent and Received, matching the list.
- Send/Receive sync banners use verification_progress like every other surface,
instead of the blocks/headers ratio that over-reports during early sync.
- Fix printf format/type mismatches: %.0f<-int (market % shielded), %d<-size_t
(peer counts), %ld<-int64_t (peer byte counters, wrong on Windows).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The connection state machine never tore down on a lost connection: refresh-loop
RPC errors were swallowed, rpc_->isConnected() stayed true after a daemon
crash/restart/socket drop, and the UI showed stale balances with no reconnect.
Several operations also ran synchronous curl straight from ImGui handlers.
- Add handleLostConnection(): after N consecutive cycles where BOTH core RPCs
fail (warmup excluded, so no reconnect loop), disconnect so update()'s
reconnect branch re-enters tryConnect().
- Move banPeer/unbanPeer/clearBans and key export/import onto the worker thread
(import requests a rescan that could freeze the UI for the curl timeout).
- Run the block-info dialog's two chained RPCs on the worker thread (+ guard the
getblockhash result type).
- Detect daemon warmup via the JSON-RPC -28 code (new RpcError carrying the code;
message text preserved so 401/warmup string-matching is unaffected), and widen
CONNECTTIMEOUT to 10s for remote/TLS hosts (2s localhost).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
z_sendmany returns an opid immediately; the tx is built/signed/broadcast
asynchronously afterward. The send path showed "Transaction sent successfully!"
and cleared the form on opid receipt, so a later async failure contradicted it.
Shield/merge stored the opid only in a dialog-local static (never polled), and
auto-shield ran a blocking z_shieldcoinbase on the UI thread and discarded its
opid — async failures of all three were silently lost.
- Add App::trackOperation(opid) so shield/merge/auto-shield register with the
shared opid poller (failures surface, balances refresh on completion).
- Defer the full-node send's success/failure to the poller via per-opid callbacks
(parseOperationStatusPoll now exposes failureByOpid); the "Sending..." spinner
covers the finalizing window, and the form is kept until terminal status.
- Dispatch auto-shield through the worker thread and use the configured fee.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
settings.json and addressbook.json were written in place with a bare ofstream —
a crash or power loss mid-write truncated the file, and on the next launch the
parse failure silently reset every preference (hidden/favorite addresses, labels,
pool workers, language, theme, lite-server list) because the next save overwrote
the corrupt file with defaults.
Add Platform::writeFileAtomically() (temp file -> fsync -> atomic rename; dir
fsync on POSIX, MoveFileEx on Windows; optional owner-only 0600) and route both
saves through it. On a parse failure, quarantine the unreadable settings file to
settings.json.corrupt-<ts> instead of clobbering it.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The full-node send built the recipients array and called z_sendmany with only
(fromaddress, amounts) — dropping the minconf and fee positional args. The whole
fee-tier UI (Low/Normal/High, send-max math, the confirmation fee) was collected
and shown but never sent, so the daemon silently applied its own default fee and
the Low/High tiers were cosmetic.
Pass {from, recipients, 1, fee}, with the fee formatted fixed-decimal so the
daemon's ParseFixedPoint accepts it (a small double like 0.00005 would otherwise
serialize to "5e-05" and be rejected).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Add a status panel at the top of the Network tab driven by the live WalletState:
- Connection: a colored dot + Connected / Syncing / Not connected, with the in-use server host
(or "Random server") and its latency on the right.
- Sync: "<pct>% · <walletHeight> / <chainHeight>" while syncing (with a thin progress bar),
"Synced · block N" when complete, or "No wallet open" when disconnected.
Reads app->state().sync (populated by the lite refresh: progress / wallet+chain height / complete)
and state().connected (= walletOpen). Advances with a Dummy so the bounds grow correctly.
Both variants build; suite passes; hygiene clean; lite GUI smoke OK.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Each server card advanced to the next via a bare SetCursorScreenPos, which ImGui won't use to
extend the scroll region's content height ("Code uses SetCursorPos() to extend window boundaries
... submit an item e.g. Dummy() afterwards"). Beyond the warning, this meant cards past the fold
wouldn't scroll. Advance with an ImGui::Dummy(cardW, gap) below each card so the content height
grows correctly.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
A lite-wallet-only "Network" tab (full-node keeps the Peers tab; exactly one shows per variant)
to manage lightwalletd servers, replacing the basic selector that was in Settings.
- Card list of servers with per-server latency + status dot, DNS host + resolved IP, and an
Official/Custom pill. Official DragonX servers get a glowing outline.
- Pick a server (Sticky) by clicking its card, or toggle "use a random server" (Random mode);
selection applies immediately (App::rebuildLiteWallet(force=true) tears down + rebuilds the
controller against the new server and resyncs — its dtor detaches the uninterruptible sync
thread, so this doesn't block).
- Add custom servers; hide/unhide servers (persisted set, revealed by a "Show hidden" toggle).
- Latency/IP come from a new background probe (util/LiteServerProbe): libcurl CONNECT_ONLY does
the TCP+TLS handshake (works for gRPC lightwalletd, no HTTP response needed), recording
APPCONNECT_TIME as latency and CURLINFO_PRIMARY_IP. Auto-runs on tab open + a Refresh button.
Wiring: WalletUiSurface::LiteNetwork (gated !fullNodePagesAvailable) + NavPage::LiteNetwork in
the sidebar + app.cpp dispatch; settings gains a hidden-servers set; isOfficialLiteServer() added
to lite_connection_service. The Settings page lite-server selector + its plumbing are removed
(single source of truth = the tab).
Reuses the existing server model (LiteServerPreference, Sticky/Random, selectLiteServer) and UI
primitives (DrawGlassPanel, ThemeEffects glow, peers-tab ping-dot idiom). Unit-tested
(liteServerHost, isOfficialLiteServer) + an env-gated live probe (verified vs lite.dragonx.is:
online, latency, IP). Both variants + lite-backend build; suite passes; hygiene clean; GUI
smoke-launched without crash.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The linux/windows release packaging did `rm -rf "$out"` on the whole output dir, so building
ObsidianDragonLite into release/<os>/ wiped the ObsidianDragon artifacts already there (both
variants share release/linux and release/windows). Remove only the CURRENT variant's prior
artifacts (by APP_BASENAME, which can't cross-match — "ObsidianDragon-*" excludes
"ObsidianDragonLite-*"), so full-node and lite releases coexist.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>