Zero-behavior-change cleanup ahead of the console refactor:
- Delete dead API/members: addCommandResult (unused), handleSelection (declared, no
def), isAutoScrollEnabled (no callers), and scroll_to_bottom_ (written 8× but never
read) with all its writes.
- Drop the unused screenToTextPos `line_height` parameter (+ 4 call sites) and the unused
`segEnd` local — clears the two -Wunused warnings.
- Remove the toolbar filter checkboxes' dead `static bool s_prev_*` change-detectors,
whose only effect was setting the dead scroll_to_bottom_.
- Fix the console_new_lines format/arg mismatch (format takes one %d; the call passed a
spurious plural-suffix arg).
- Fix the channel/color incoherence: a "[daemon] error:" line kept red text but got a
blue daemon bar — the prefix no longer downgrades an error channel.
- Delete 11 orphaned lite_console_* i18n keys left over from the console merge (keep
lite_console_help_passthrough).
Full-node + lite build clean, ctest green.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Right-clicking the output now offers "Copy <value>" when a long alphanumeric token
(txid/blockhash/address, >=16 chars) is under the cursor — resolved via screenToTextPos.
Uses right-click so it never conflicts with the left-drag text selection.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
renderOutput already draws a clickable "N new lines" jump-to-bottom indicator
(console_tab.cpp ~996), so the pill added earlier duplicated it — and reused the
"console_new_lines" format string incorrectly. Remove the pill and the stray i18n entry;
the existing indicator stands.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Highlight the matched substring (case-insensitive) in every visible line with a
translucent yellow behind the text, drawn per wrap-segment like the selection highlight.
- Show a live match count next to the filter input ("N matches", red when zero).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Surface new_lines_since_scroll_ (already tracked) as a floating pill at the output
panel's bottom-right whenever the user is scrolled up with unseen output; clicking it
re-enables auto-scroll and snaps to the bottom.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The two consoles were separate implementations (the rich full-node ConsoleTab and a
minimal RenderLiteConsoleTab). Introduce ConsoleCommandExecutor to abstract the only
real differences — command execution + log source — so both variants share the one rich
terminal (the lite console now gains selection, zoom, copy, and JSON-colored output).
- console_command_executor.{h,cpp}: interface + FullNodeConsoleExecutor (dragonxd log
ingestion + RPC command execution, result queue) and LiteConsoleExecutor (lite
diagnostics ring + backend runConsoleCommand/takeConsoleResult, connection/sync status).
- ConsoleTab::render() now takes a ConsoleCommandExecutor&; log ingestion, command
submission, and command results flow through it. The RPC command-reference popup and
the daemon/errors/rpc-trace/app filter toggles are gated on the executor's capabilities;
the toolbar status dot + a status header come from the executor.
- Safety preserved: `clear`/`cls` is intercepted as a view-only clear in the shared UI so
it is NEVER forwarded (the lite backend's `clear` wipes tx history); command output is
not persisted to diagnostics.
- App owns the executor (created per variant) and routes both NavPages to console_tab_;
lite_console_tab.{h,cpp} deleted.
Needs runtime verification of BOTH consoles (full-node RPC + lite: clear, seed/export
secrecy, sync status, command results).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Fetch /coins/dragonx-2/tickers once per session (App::refreshExchanges, reusing the
price-fetch worker path) and build the exchange registry at runtime into
state.market.exchanges; parseCoinGeckoTickers groups tickers by venue. The Market tab
sources from this live list and falls back to the compiled-in registry (now including
OurBit + Nonkyc.io) when offline. Fixes the missing OurBit pair and auto-tracks future
listings.
- Fix stale selection defaults (TradeOgre / DRGX-BTC -> Nonkyc.io / DRGX/USDT) that never
matched the registry, and make the attribution generic ("Price data from CoinGecko").
- Reframe the single-asset portfolio card as a DRGX holdings dashboard: relabel to
"MY DRGX" and show the 24h change on the holdings value (colored by direction).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Data + service layer for supporting multiple mining pools:
- pool_registry: KnownPool table (pool.dragonx.is + pool.dragonx.cc) carrying
each pool's stratum host, xmrig algo, and stats-API schema; pure helpers
findKnownPoolByUrl / resolvePoolAlgo / parsePoolHashrate / chooseWeightedPool
(weighted-random by hashrate with incumbent stickiness).
- pool_stats_service: background, schema-aware hashrate fetcher (libcurl, TLS,
browser UA for Cloudflare) modelled on XmrigUpdater.
- settings: pool_select_mode (manual | auto_balance), string-serialized.
- i18n strings + unit tests (both stats schemas, algo resolution, weighted pick).
Note: pool.dragonx.cc's stratum host is us.dragonx.cc:3333 — the .cc domain is
only the Cloudflare-proxied web/API host and does not accept stratum on :3333.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add a full-node daemon updater (util/DaemonUpdater + daemon_download_dialog)
reachable from Settings -> NODE & SECURITY: downloads/verifies (SHA-256 +
enforced ed25519 signature) and atomically installs the latest dragonxd from
the project Gitea, with a "Restart daemon now" step. Add a shared "Browse all
releases..." picker (release_list_view) to both the miner and daemon updaters
so users can pin older/pre-release builds. Pure no-I/O cores
(daemon_updater_core / xmrig_updater_core) are unit-tested; sign-daemon-release.sh
signs release archives offline.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Verified by running the app against a live node and watching a real rescan. Three
issues that only surfaced at runtime:
- Wrong RPC name: this daemon (hush/komodo) exposes the runtime rescan as
"rescan <height>", not bitcoin's "rescanblockchain". runtimeRescan() and
RPCClient::rescanBlockchain() used the bitcoin name and failed with "Method not
found" on every node. Corrected to "rescan".
- Witness/rescan progress never surfaced during a rescan: the daemon-output parser
that drives it was gated behind rpcConnected, but a heavy rescan holds cs_main so
getinfo times out and the RPC reads disconnected — silencing the parser exactly
when it's needed. The parser reads the daemon's stdout pipe (no RPC), so it now
runs whenever the daemon process is alive. It also now parses INLINE on the main
thread instead of via fast_worker_, so it can't be starved when the worker is
blocked on a getrescaninfo call (which waits on cs_main during a witness rebuild).
- Witness rebuild has TWO sub-phases with different scales — the initial-witness
pass ("Setting Initial Sapling Witness for tx <hash>, <i> of <N>") and the cache
walk ("Building Witnesses for block <h> <frac> complete, <n> remaining"). Tracking
them with one monotonic value pinned the bar at the initial pass's ~100% through
the whole cache walk. They're now tracked as distinct phases (witness_phase) with
their own monotonic progress and labels ("Setting witnesses" vs "Rebuilding
witnesses"), so neither resets/bounces and the long phase shows real movement.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The daemon's post-rescan witness rebuild ("Building Witnesses for block ...")
is a distinct, often-long phase that previously showed only as an indeterminate
"Rescanning..." with no progress. Parse the daemon's witness-build log lines and
surface a dedicated progress indicator.
- Parse "Building Witnesses for block <h> <frac> complete, <n> remaining" (and the
earlier "Setting Initial Sapling Witness for tx ..., <i> of <m>") from daemon
output, extracting a 0..1 fraction and remaining-block count.
- New SyncInfo fields building_witnesses / witness_progress / witness_remaining,
cleared at every rescan-completion site (warmup-end, getrescaninfo poll, runtime
rescan callback, daemon-log "finished").
- Status bar shows "Rebuilding witnesses NN%" (priority over the generic rescan
text); the loading overlay (shown during -rescan warmup) gets a labelled witness
progress bar with the remaining-block count.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
A wallet bootstrapped from a snapshot keeps its wallet.dat but never rescans, so
its spent-state is stale and the first send tries to spend already-spent notes and
is rejected. The startup -rescan flag can't fix it either: the snapshot lacks the
pre-snapshot block history -rescan needs, so it errors. The working fix is a runtime
rescanblockchain RPC from a height the snapshot actually has.
- Add App::runtimeRescan(startHeight): runs rescanblockchain via the worker, drives
the rescanning UI state, and owns completion via the RPC callback (getrescaninfo
is unavailable on this daemon). Suppresses the per-second mining/rescan pollers
and the Core/balance/tx refreshes while the daemon holds cs_main for the scan.
- Add App::detectLowestAvailableBlockHeight(): async binary search via getblock for
the lowest height whose block data is on disk → the snapshot base, and whether the
node still has full history.
- Auto-reconcile after bootstrap: both completion sites (wizard + Settings download
dialog) mark a pending rescan; once the daemon is back up and the tip is known,
detect the base and runtimeRescan() from it (or -rescan restart on a full node).
- Settings "Rescan Blockchain" now probes first: full-history nodes get the existing
-rescan restart; bootstrapped/pruned nodes get a prompt pre-filled with the
detected base height that runs the runtime rescan.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Delete Blockchain and Repair Wallet now sit on one line, paired with the same
uniform button width as Test Connection / Rescan Blockchain (a clean 2-column grid;
all maintenance buttons share one rowBtnW sized across the four labels).
- The Daemon binary panel lays Installed and Bundled side by side across the node
column (version + size·date | version + size) instead of stacked narrow lines, with
the status line spanning underneath and Install bundled / Refresh paired below.
- Shorten the install button label to "Install bundled" so it fits the shared width;
the tooltip still explains the full action. Date shown as YYYY-MM-DD.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Previously the wallet re-extracted the bundled dragonxd on startup whenever the
installed binary's size differed from the bundle ("stale" overwrite), which could
replace a node a user had deliberately placed in dragonx/.
Now dragonx binaries (dragonxd/cli/tx) are auto-placed ONLY when missing — never
auto-overwritten on a size mismatch (needsParamsExtraction + extractEmbeddedResources).
Params/asmap keep their size-based refresh; a daemon dropped next to the wallet exe
still takes priority and is never touched.
Replacing the daemon is now an explicit action: Settings → "Daemon binary" reports the
installed binary's version (scanned from the file), size and modified date, compares it
to the version bundled in this build, and offers an "Install bundled daemon" button.
That stops the node, overwrites dragonxd/cli/tx with the bundled copies (waiting for the
process to release the file lock), and restarts — wallet/keys/chain data untouched.
Adds resources::{getInstalledDaemonInfo,getBundledDaemonInfo,reextractBundledDaemon}
(+ a version-string scanner) and App::reinstallBundledDaemon().
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
When a note's stored record is corrupt or its tx isn't in the canonical chain,
z_sendmany fails to build a valid sapling spend proof even after a full -rescan,
because a plain rescan replays witnesses but keeps the existing tx/note records.
The zcashd repair for this is -zapwallettxes=2, which deletes all wallet tx/note
records and rebuilds them from the chain (keys/addresses preserved).
Adds a RepairWallet lifecycle operation that mirrors the existing -rescan plumbing
(one-shot zapOnNextStart flag on the embedded daemon; -zapwallettxes=2 implies and
supersedes -rescan), an App::repairWallet() that reuses the rescan status UI (so the
status bar + warmup-end completion detection apply), and a confirmed "Repair Wallet"
button + dialog in Settings → node maintenance (embedded daemon only).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Two related fixes for the post-bootstrap "send fails / rescan stuck at 99%" trap:
1) Rescan completion now keys off warmup-end. A -rescan runs entirely inside daemon
warmup (every RPC returns -28 until it finishes), so warmup completing IS the rescan
completing. The old detectors relied on getrescaninfo (which some daemons answer with
"Method not found") or a "Done rescanning"/bench log line the daemon may never print,
leaving the status bar stuck at 99% — so users killed the rescan before it finished.
When warmup ends and a rescan was confirmed active, clear the rescan state, flip to
100%, refresh history/balance, and toast completion.
2) z_sendmany failures that mean stale shielded note data (shielded-requirements-not-met,
missing sapling anchor, invalid sapling spend proof, bad-txns-sapling-*) now append a
plain-language hint telling the user to run a full rescan, instead of surfacing only the
raw daemon string.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
On first run the lite welcome screen's "Restore from seed" button only showed a
hint toast and bounced the user to Settings, dismissing the welcome with no
wallet open — it never prompted for a seed. Add a real restore step to the
welcome wizard: a seed-phrase field + optional birthday height, which calls
beginRestoreWalletAsync() (same server failover as create/open), shows
"Restoring…" progress, then completes (wallet syncs) or surfaces the error to
retry. The seed buffer is wiped on success/Back and in finish().
(The Settings -> Lite -> Restore path already prompted for a seed; this fixes
the first-run welcome path.)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The lite SilentDragonXLite backend stores its wallet in its own directory
(dirs::data_dir()/silentdragonxlite — %APPDATA%\silentdragonxlite on Windows,
~/.silentdragonxlite on Linux, ~/Library/Application Support/silentdragonxlite on
macOS), NOT the full-node getDragonXDataDir() (…/Hush/DRAGONX). The newly added
lite "Open data folder" button opened the wrong (full-node) directory.
Add Platform::getLiteWalletDataDir() mirroring the backend's get_zcash_data_path
for the "main" chain, and point the lite button at it. The full-node button is
unchanged.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Add an explicit button in Settings that opens the wallet/blockchain data
directory (getDragonXDataDir()) in the OS file manager via the existing
Platform::openFolder(). Placed in the full-node connection section (next to the
data-dir path, which was only a subtle clickable link) and in the lite section
(always available). i18n strings added.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Add a maintenance option for the lite wallet to re-download and re-scan every
block from the lite server — useful when balances or history look wrong.
- LiteWalletController::startRescan() runs the backend `rescan` command (which
clears the wallet's synced block cache and re-syncs from its birthday) on a
detached thread, reusing the existing sync progress/refresh machinery: it
resets syncDone_ so refreshModel() shows progress again and refreshes data on
completion. No-op if no wallet is open or a scan is already running.
- scanInProgress() exposes the initial-sync-or-rescan state.
- Settings (lite, open wallet) gains a "Redownload blocks" button behind a
confirmation modal, disabled while a scan is running. i18n strings added.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The console mixed RPC traces, daemon output, and the wallet's own "[app] ..."
log lines with no way to hide the latter. Add an "App" checkbox alongside the
existing Daemon/Errors/RPC toggles. Since [app] lines share COLOR_INFO with
other info text, the filter matches them by their "[app] " prefix rather than by
color. Default on; unit test + i18n added.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Add a fuzzy mode to the explorer search: a non-numeric, non-full-hash query now
filters the list to cached blocks whose hash (or height text) contains the query
substring, live as you type. Backed by a new ExplorerBlockCache::searchBlocks()
(SQLite LIKE with escaped wildcards), memoized per query so it doesn't hit the DB
every frame. Exact queries still navigate precisely: a block height re-anchors
the list, and a full 64-char hash is resolved via RPC. Row clicks still open the
detail modal. Empty results show "No matching cached blocks".
Note: fuzzy matching covers cached (browsed/prefetched) blocks only — the daemon
has no partial-hash index — while exact height/hash lookups reach any block.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The Rescan blockchain and Restart daemon buttons fired immediately on click —
both are disruptive (long offline rescan / connection drop) and easy to hit by
accident. Route them through confirmation modals, matching the existing
delete-blockchain / clear-ztx confirmations: the button now sets a confirm flag
and an overlay dialog performs the action only on explicit confirm. New i18n
strings added with English defaults.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Add a sort selector next to the type filter with four modes: Newest first
(default), Oldest first, Largest amount, Smallest amount. The mode folds into
the merged-list memoization cache key (so the list re-sorts only when the mode
changes) and the comparator branches on it, keeping txid as a deterministic
tiebreak. Changing the sort resets to page 1.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
History streams in over many refresh cycles (the incremental shielded scan
walks every z-address), so the first batch appears long before the list is
complete — with no indication more is still coming. The existing loading banner
deliberately goes quiet once any rows are on screen.
Track whether the first full shielded scan has finished
(initial_history_scan_complete_) and, until it has, surface a progress percentage
(fraction of z-addresses scanned) in transactionRefreshProgressText() — which the
History tab already renders as its pulsing loading indicator. Goes quiet once the
first scan completes; routine per-block re-scans don't re-trigger it. Reset on a
full history invalidation (rescan / session reset) so it shows again on reload.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Auto-clear: the Copy button now routes through App::copySecretToClipboard, so a copied
private/viewing key is wiped from the clipboard after ~45s (same protection as the seed) with
a "auto-clears" notice — instead of the raw SetClipboardText that left it indefinitely.
- QR: once the key is revealed, a Show/Hide QR toggle renders the key's QR inline (via the same
GenerateQRTexture/RenderQRCode widget the Receive tab uses) for scanning into another wallet.
The QR texture is cached, regenerated on key change, and freed on hide/close/dismiss; hiding the
key also hides its QR.
- Actions row tightened to Show/Hide · Copy · QR, and the key + QR texture are now cleared on any
dismissal (Close button, scrim click, Esc), not just the Close button.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The address list supported two drag gestures that collided: dragging a row onto another
transferred funds, dragging into a gap reordered. Since rows are contiguous, a reorder-drag was
almost always over another row, so it triggered a fund transfer instead of reordering.
Disambiguate by WHERE on the target row the drag is released (user's suggestion): the top/bottom
~30% edge bands = reorder (an insertion line is shown), the centre = transfer (the row highlights).
A zero-balance row or an off-row drop always reorders. Tooltip and i18n hint updated to match.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- 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>
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 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 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>
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 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>
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>
- 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>
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>
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>
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>
Relocate the miner-update control from a standalone full-width button into the mining-control
header row, immediately left of the benchmark button:
- The button now shows the latest available version ("Update <tag>"), with the current installed
version as text to its left ("Current: <tag>" / "none").
- A one-shot background version check (util::XmrigUpdater::startCheck) runs the first time the pool
section is shown, so the latest tag can be displayed; until it arrives the button reads
"Update miner…". Clicking opens the existing dialog; disabled (greyed, with tooltip) while the
miner is running.
- New i18n keys: xmrig_update_short, xmrig_current, xmrig_none.
Both variants build; suite passes; GUI smoke-launched without crash.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Replace the English string literals in the miner-update dialog + the "Update miner…" mining-tab
button/tooltip with TR() keys, and register their English text in i18n.cpp's loadBuiltinEnglish()
(the in-code English fallback that non-English locales overlay). Reuses the existing cancel/close/
retry keys. Labeled values use a "%s %s" literal format with a TR'd label (no -Wformat-security
risk). Non-English locales fall back to English for the new xmrig_* keys until translations are
added to res/lang/*.json.
Both variants build; suite passes; hygiene clean.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Now that the release publishes a valid .sig per archive (verified against the pinned key for
linux/win/macOS), enable enforcement and fix two bugs that the newer multi-platform release
(v6.25.3, which added a macOS build) exposed:
- kXmrigRequireSignature = true: refuse any install whose release doesn't publish a valid
ed25519 signature over the archive. Verified live end-to-end against the signed v6.25.3
(archive SHA-256 + signature -> install).
- Drop the redundant inner-binary SHA-256 check. It keyed on the inner filename, but both the
linux and macOS archives contain a binary literally named "xmrig", so the two "xmrig (…)"
checksum lines collided in the map and the linux install compared against the macOS hash ->
spurious "could not verify" failure. The whole archive is already verified (SHA-256 +
signature), so every extracted member is authentic by transitivity — the per-member check
added nothing but ambiguity.
- Fix the macOS platform token: the asset is named "...-macos-x86_64.zip", not "...-macos-x64",
so selectXmrigAsset never matched it. currentXmrigPlatformToken() now returns "macos-x86_64"
on Intel macs (arm64 has no build -> Unavailable). Added a matcher test for the macOS naming.
Both variants build; suite stable (0 failures / multiple runs); live require-mode install verified.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Pin the ed25519 public key in xmrig_updater.h, activating signature verification in soft mode
(kXmrigRequireSignature=false): a release's ".sig" asset is verified when present, but an
unsigned release still installs on TLS + SHA-256. Verified live against the current release
(v6.25.2, which ships no .sig yet) — still installs.
- gitignore *.ed25519.key / *.ed25519.pub.b64 so a signing secret key can never be committed.
- Add a unit test that the pinned key decodes to a valid 32-byte ed25519 key (a malformed paste
fails the build, not silently disabling verification).
Bug fix (found via a flaky test): verifyXmrigSignature trimmed trailing whitespace BEFORE the
raw-64-byte check, so a raw signature whose last byte equals '\n'/'\r'/space/tab (~1.6% of
signatures) was corrupted and rejected. Now base64 is tried first (safe to trim) and the raw
path uses the exact untrimmed bytes. Added a deterministic regression test that forces a
whitespace-terminated raw signature. Suite is stable (0 failures in 10 runs; was ~3/8).
Also de-brittled the live integration test: it no longer pins a release-specific binary hash
(reaching Done already means the worker verified the binary against the release's own checksum).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Closes the supply-chain gap the review flagged: today the archive and its SHA-256 share one
trust root (the release body), so a compromised/edited release can ship an arbitrary binary
that still "verifies". This adds authenticity via a detached ed25519 signature checked against
a public key PINNED IN THE BINARY (not fetched), using libsodium's crypto_sign_verify_detached.
Opt-in / soft rollout:
- kXmrigSignaturePublicKeyBase64 in xmrig_updater.h is EMPTY by default -> signatures are not
checked and behavior is unchanged (TLS + SHA-256 only). Paste the base64 public key to enable.
- Once a key is pinned, an install verifies a "<archive>.sig" asset (base64/raw 64-byte ed25519
signature over the archive bytes) when present; kXmrigRequireSignature=true additionally
refuses installs that publish no signature.
- The check runs after the SHA-256 check, over the same already-read archive bytes; refuses on
a missing key-but-required, unreachable .sig, or invalid signature.
- verifyXmrigSignature + selectXmrigSignatureAsset are pure (libsodium only) and unit-tested:
valid base64 + raw-64-byte signatures verify; tampered data, wrong key, and malformed/empty
inputs all fail closed. Cross-tool interop verified (Python stdlib base64 == sodium base64).
- scripts/sign-xmrig-release.sh: keygen / sign / pubkey helper (PyNaCl = same libsodium ed25519)
to produce the .sig assets and the public key to pin.
No behavior change until a key is pinned. Both variants build; suite passes; live worker
re-verified (signatures off by default).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Addresses confirmed findings from the multi-lens review of the updater:
- Cancelable + live progress (was: download uncancelable, progress stuck at 0%, closing
the dialog mid-download blocked the UI thread on the worker join). Wire a libcurl
CURLOPT_XFERINFOFUNCTION that publishes byte counts and returns abort when cancel() is
requested; add a Cancel button. The dialog's destructor now aborts the transfer promptly,
so closing mid-download no longer freezes the UI.
- Graceful "unavailable" instead of a red error on platforms with no published build
(macOS / ARM): new terminal State::Unavailable rendered neutrally, not as a failure.
- Install-time running guard (TOCTOU): App::isPoolMinerRunning() re-checked in the dialog
before each install, so a dialog opened before mining started can't replace a live binary.
- Size caps: CURLOPT_MAXFILESIZE on the download and a per-archive-member ceiling before
decomphressing into memory, to bound an attacker-controlled archive.
- Distinguish a local read failure of the downloaded archive from a checksum mismatch
(was reported misleadingly as "possible tampering").
- Reword the dialog's verification note to "checked against the release's published SHA-256
checksum" (integrity, not authenticity — see the signing note below).
Not fixed here (needs your input): WinRing0x64.sys has no per-file hash published, but it is
covered by the verified archive checksum (it is inside the verified zip); and the release is
not cryptographically signed — checksums and binary share one trust root. Adding a pinned-key
ed25519/minisign signature is the real supply-chain hardening and needs an offline signing key
+ a release-process change.
Both variants build; suite passes; live worker re-verified end-to-end on linux-x64.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Adds util/XmrigUpdater: a background-thread service (mirrors util/Bootstrap) that pulls
the latest DRG-XMRig release from the project's Gitea, verifies it, and installs the miner
binary into the daemon directory. Service layer only; the mining-tab UI hook comes next.
Flow: GET /api/v1/repos/DragonX/drg-xmrig/releases/latest -> pick the asset matching this
platform (…-linux-x64.zip / …-win-x64.zip; no macOS build -> graceful "unavailable") ->
download (libcurl, TLS verified) -> verify the archive SHA-256 -> extract with miniz,
flattening the versioned subdir the archive nests the binary in -> verify the extracted
binary's SHA-256 in memory before writing it -> atomic install (+chmod +x on POSIX). On
Windows also extracts WinRing0x64.sys; config.json/README.md are skipped.
Security (download-and-execute): TLS is verified, and BOTH the archive and the inner binary
are checked against the SHA-256 checksums published in the release body (parsed as
"<hex> <name>" lines) — install is refused on a missing or mismatched checksum.
Split into a pure core (xmrig_updater_core.cpp: release parse, asset/platform match, checksum
parse, SHA-256) and the curl/miniz worker (xmrig_updater.cpp). The core is unit-tested against
a real captured release fixture (tests/fixtures/xmrig/release_latest.json); an env-gated
(DRAGONX_TEST_NETWORK=1) integration test exercises the worker live and was verified end-to-end
on linux-x64 (inner binary SHA-256 matches the published value). Both variants build; suite passes.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
When the user confirms a send on a locked encrypted lite wallet, show an unlock
modal (passphrase -> unlockWallet) instead of letting the backend reject it with
"Wallet is locked". After unlocking, the user re-confirms the send (the form is
preserved). Balances remain viewable while locked; only spending needs unlock.
- send_tab: the Confirm-and-send button routes to App::requestLiteUnlock() when
getWalletState().isLocked(), else sends as before.
- App::renderLiteUnlockPrompt(): centered modal, passphrase (Enter submits),
Unlock/Cancel; the passphrase buffer is sodium-zeroed after every path.
Full-node unaffected (gated on liteWallet()/isLocked()). Builds clean, launches
clean, tests pass.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Replace the bare "land on main UI with a No-wallet overlay" first-run with a
lite welcome modal, shown when no wallet file exists yet (lite_wallet_ present,
not open, walletExists() false):
- "Create new wallet" — one-click createWallet({}); on success, notifies the user
to back up their recovery phrase and navigates to Settings (Backup & keys),
where the seed can be revealed/copied via the existing backup UI.
- "Restore from seed" — navigates to Settings (Lite wallet request → Restore).
- "Later" — dismiss for the session.
Routes to the already-built + verified create/restore/backup flows rather than
re-implementing seed display in the modal (no new secret-handling surface).
Dismissed once an action is chosen; never shown again once a wallet exists.
Full-node is unaffected (renderLiteFirstRunPrompt() returns early when
lite_wallet_ is null). English i18n built-ins added.
Verified: fresh-HOME lite launch shows the prompt, clean run + shutdown, no
crash/RPC noise; tests pass.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>