Turns the "Daemon Error + raw log dump" moment into one calm, honest recovery
dialog plus a recovery-aware rescan screen. Presentation + orchestration only —
the file-safety logic in rebuildWalletDatabase()/restoreOriginalWallet() (source
selection, verify-before-swap, copy/rename-never-delete, .bak) is unchanged.
- One authoritative dialog with a phase machine Offer -> Working -> Done/Failed.
The duplicate in-overlay recovery card, the untranslated red "Daemon Error"
heading, and the raw daemon-log dump are gone for the recovery case (they stay
for genuine, unrelated crashes).
- Offer is a choice-cards layout: "Repair automatically" (recommended, accent-
tinted) vs "Restore original", side by side; the rare actions ("Show me the
files", "Decide later") and a plain-language "What happens to my files?" sit in
a quiet footer. When the rebuild helper is missing, it collapses to a single
Restore card — never a dead end.
- Post-repair rescan shows a calm "Finishing your wallet repair" screen with
elapsed time + the growing wallet size, instead of "RPC timeout / taking longer
than expected / restart daemon"; the daemon-crash toast is suppressed and the
detection toast is downgraded from red to info.
- Fixes a confirmed dead-end: if a repair succeeds but the restarted daemon then
crashes for a *different* reason (block index, disk, OOM), the recovery flags
now clear (in tryConnect + onConnected) so it surfaces as a normal daemon
failure instead of freezing forever on a reassuring "don't restart" screen.
- Clickable "Wallet repair available" status-bar chip for re-entry.
The same app.cpp changes HiDPI-harden the surfaces the recovery flow lives on:
the status-bar and loading-overlay hand-drawn geometry are multiplied by dpiScale
(they rendered native-size and clipped at HiDPI / font_scale>1), the loading-
overlay status text wraps instead of running off both edges, and the node-status
banner floors its height to its DPI-baked font so the title can't clip off the top.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Findings from a UI-cutoff audit — each is a spot where an in-tree helper
(truncateMiddle / TruncateToWidth / measured button width / the *dpiScale/*hs
factors) was bypassed:
- notifications: the toast-pill height/padding/icon-gap were raw logical px while
the icon/text drawn inside are DPI-baked, so they clipped the pill at HiDPI.
Scale the geometry by dpiScale (not the already-scaled glyph metrics).
- settings: in the two-column NODE & SECURITY layout the data-directory path could
overrun into the Daemon-binary column (shared draw list, no clip rect between
them). Middle-ellipsize it to the column width; the full path stays in the
tooltip + click-to-open + copy.
- send: the "Confirm & Send" button width came straight from the schema and was
never measured against the label, clipping the Russian translation on the
pre-broadcast dialog. Size to max(schema width, measured label + padding).
- balance: the recent-tx address-column offset missed the `* hs` DPI factor its
sibling (amount-right-margin) uses, overlapping the type label at HiDPI.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The dragonx-wallet-rebuild helper is the only thing that repairs a genuinely
BDB-inconsistent wallet.dat — plain "Restore" just re-triggers the daemon's
salvage cascade — yet it was silently dropped from every packaged build:
- Linux zip/AppImage copied a hand-picked file list that omitted it.
- Windows bundled it only behind a soft `[[ -f ]]` guard (silent skip).
- macOS never wired Berkeley DB, never built it, never bundled it.
build.sh now HARD-REQUIRES the helper for full-node releases (fails the build if
the vendored Berkeley DB depends are missing, rather than shipping recovery-less),
and ships it in the Linux zip + AppImage, the Windows zip, and the macOS .app.
It also compiles the helper standalone for Windows and INCBINs it, and
embedded_resources gains ensureWalletRebuildHelperExtracted() to extract it on
demand — so a self-contained ObsidianDragon.exe carries recovery exactly like the
embedded daemon, even on a machine where first-run param extraction already ran.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Reported with a screenshot: on a salvage-then-abort, the status correctly read
"Wallet needs recovery — see the prompt" but no prompt appeared — the separate
BeginOverlayDialog is occluded by the full-frame loading/daemon-error overlay
that's drawn every frame while the node is down.
Render the recovery actions directly IN the daemon-error overlay when a salvage is
detected: a concise message + prominent one-click "Rebuild wallet database" /
"Restore original" / "Open data folder" buttons (same handlers as the dialog),
placed right after the title and skipping the verbose daemon-output dump so they
stay on-screen. The verbose diagnostics + crash-count hint still show for
non-recovery errors. Build clean, suite green.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Reported: loading a BDB-inconsistent wallet silently renamed it and created a new
one — no recovery dialog. Two causes, both fixed:
1) Detection ran only in onConnected(). The salvage happens at STARTUP, and the
node may never connect (block-index abort, long sync, crash) — or a long sync
trims the salvage line out of the rolling output buffer before connect. Extract
detectWalletAutoRecovery() and run it every tryConnect() tick (every ~5s during
startup), so the salvage is caught the instant it appears, regardless of whether
the node connects. Also hold the crash-restart loop while a salvage is pending,
so the wallet can't be re-salvaged/shrunk while the Rebuild/Restore dialog is up.
2) walletAutoRecovered() only matched the SUCCESSFUL-salvage strings. A
BDB-inconsistent file makes aggressive salvage FAIL ("found no records"), which
prints different lines. Broaden the detector to the signals that fire in every
case: "CDBEnv::Salvage", the "Renamed <wallet> to wallet.<ts>.bak" rename, and
"found no records in wallet" — while still not matching normal startup or a
block-DB abort.
Adds the exact failed-salvage sequence to the detector test. Build clean, suite
green (1/1).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Wires the wallet-rebuild recovery helper into the release pipeline so a shipped
build actually carries it (the app locates it next to dragonxd).
- CMakeLists: link the vendored STATIC Berkeley DB for the helper — add
Threads::Threads + dl (Linux) / ws2_32 (Windows) that the static libdb-6.2 needs
(the system shared lib pulled those in transitively; the static one doesn't).
- build.sh (Linux + Windows): pass BDB_INCLUDE_DIR/BDB_LIBRARY explicitly at
configure, pointing at external/dragonx/depends/<triple>/{include,lib/libdb-6.2.a}
— the same libdb the daemon links, so the helper's output is a v6.2 btree the
bundled dragonxd reads. Explicit paths bypass find_library (and the mingw
toolchain's sysroot-only find restriction). Guarded: no depends → helper simply
not built/bundled. Strip + copy the helper next to dragonxd(.exe) in both bundles.
Verified: Linux links the vendored libdb-6.2.a statically (no dynamic libdb) and
rebuilds the real broken wallet correctly; the helper cross-compiles cleanly with
mingw against the vendored Windows libdb-6.2.a to a PE32+ x64 exe. Full app +
helper build, suite green (1/1), build.sh syntax OK.
Remaining: macOS Berkeley DB (no in-tree depends artifact) + resource-embedding as
an alternative to side-by-side bundling.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Automates the manual recovery that fixed a wallet.dat with stale Berkeley DB
extent metadata (the "main" subdb metapage records a low last_pgno while its live
data spans thousands of pages beyond it). A tolerant page-walk reads every record,
but the daemon's BDB verify rejects the file and auto-salvages it — finding nothing
and shrinking the wallet to empty on each restart (the salvage cascade that looks
like fund loss). Plain "Restore original" can't fix it (hands the same broken file
back → re-salvage); a rebuild must produce a fresh, consistent DB.
Pieces (Approach A from the design workflow — out-of-process helper keeps AGPL
Berkeley DB out of the GPLv3 GUI):
- util/wallet_file_probe.h: extractWalletBtreeRecords() — sibling to parseWalletBtree
that collects raw (key,value) bytes (same bounds-checked, subdb-aware walk).
Records copied verbatim → encrypted key material passes through as opaque
ciphertext (no passphrase). Overflow-page values (only large tx history) are
skipped + counted; a rescan rebuilds history — funds unaffected.
- tools/wallet_rebuild/main.cpp: dragonx-wallet-rebuild CLI — reads via the tolerant
reader, writes the records into a fresh BDB "main" btree via libdb (DB_EXCL, never
overwrites), prints a JSON summary. New BDB-guarded CMake target.
- App::rebuildWalletDatabase(): picks the largest readable wallet/.bak as source,
stops the daemon, runs the helper, VERIFIES the output (readable BDB with keys)
before swapping, moves the current wallet aside (kept, timestamped), installs the
rebuilt one, clears the stale BDB env, sets -rescan, restarts. Copy/rename only —
never deletes. Result surfaced via the existing pumpWalletRestore channel.
- Wired as the preferred action on the existing wallet-auto-recovery dialog
(shown only when the helper is present). Full-node only; lite-safe.
Verified end-to-end against the real broken wallet: helper reads 3,808 t-keys + 1
z-key + HD seed and the daemon LOADS the rebuilt output with no salvage. Adds
extractWalletBtreeRecords coverage. Build clean, suite green (1/1).
Remaining (follow-up): release packaging — build.sh bundling the helper built
against the vendored per-platform static libdb (DRAGONX_BDB_ROOT), and a macOS
Berkeley DB port (no in-tree artifact).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
A wallet.dat placed in the repo root for recovery was untracked but NOT ignored,
so a stray 'git add .' could commit private keys. *.bak already covered the
salvage backups; add wallet.dat / wallet-*.dat / wallet.dat.* explicitly.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The "Restore original wallet" action picked the newest wallet.<ts>.bak — but the
daemon auto-salvages on every failed BDB verify, and each round SHRINKS the wallet
(salvage keeps only readable records + drops the dead-page bloat). In a cascade the
newest .bak is the most-degraded (seen in the wild as "Salvage found no records")
while the original is the oldest and by far the largest.
Pick by file SIZE instead: add largestWalletSalvageBak((name,size) pairs) — the
largest wallet.<digits>.bak is the least-salvaged, i.e. the pristine original (an
emptied salvage is tiny; a real wallet is large); ties break to the newest ts.
Factor the shared parse into parseWalletSalvageBakTs(). restoreOriginalWallet()
now gathers file sizes and uses it (still verifies the pick is a valid BDB before
swapping). newestWalletSalvageBak kept for reference.
Adds a cascade regression test (a 40KB emptied newest .bak must NOT win over the
194MB original). Suite green (1/1).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Adds the restore action to the wallet-auto-recovery warning: undo the daemon's
salvage by swapping the untouched original (wallet.<ts>.bak) back over the
salvaged copy and clearing the stale BDB env that triggered the false recovery,
then restarting. Modeled on beginAdoptSeedWallet (stop daemon → file ops →
restart on a worker; result pumped to the main thread for notifications).
Safety (fund-adjacent file ops on a real wallet — copy/rename only, never delete
user data):
- picks the newest wallet.<unixtime>.bak via the pure, unit-tested
newestWalletSalvageBak(); aborts if none.
- verifies the .bak is a real Berkeley DB (probeWalletFile) before touching
anything — won't overwrite a working wallet with a bad backup.
- stops the daemon first (stopDaemonForWalletSwitch) so wallet.dat is released.
- moves the salvaged copy aside to wallet.dat.salvaged-<ts>.dat (kept), COPIES
the .bak into place (the .bak stays), moves database/ aside to
database.pre-restore-<ts>.bak (kept), and drops only the transient __db.*
BDB region files. Rolls back the move if the copy fails.
- relaunches the node even on failure so it's never left down.
The warning dialog now offers Restore original wallet / Open data folder /
Keep salvaged copy. Full-node only; lite-safe. Build clean, suite green (1/1).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
dragonxd auto-recovers a wallet.dat that fails BDB verification on startup — no
flag needed (CWallet::Verify → CDBEnv::Verify(walletFile, CWalletDB::Recover)):
it moves the original to wallet.<timestamp>.bak, salvages readable keys into a
fresh wallet.dat, and keeps running. The salvage can be incomplete (or the whole
thing a FALSE POSITIVE from stale/cross-platform BDB env state — __db.* / the
database/ dir carried between machines), so the node silently comes up on a
possibly-empty wallet. To the user that reads as fund loss, with no warning.
Detect it and warn loudly instead:
- daemon/daemon_startup_diagnosis.h: pure walletAutoRecovered() (the salvage /
"Original wallet.dat saved as wallet.<ts>.bak" markers) + newestWalletSalvageBak()
(picks the wallet.<unixtime>.bak the recovery just made).
- onConnected() scans the node's captured output once per session; on a match it
shows a warning dialog + notification: the ORIGINAL is safe in wallet.<ts>.bak,
the shown balance may be incomplete, and here are the exact steps to restore it
(rename the .bak back + delete the stale database/ + __db.* env). One-click
"Open data folder" jumps straight there. Full-node only; lite-safe.
Deliberately does NOT auto-swap the wallet files (untested per-platform file
manipulation on a real wallet is not worth the risk) — it informs + guides.
Adds walletAutoRecovered / newestWalletSalvageBak coverage to
testBlockDbOutputDiagnosis. Suite green (1/1).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
When a daemon update changes the block-index on-disk format (or the index is
corrupt), dragonxd aborts at startup — "non-canonical optional discriminant" →
"Error loading block database. Aborted." — and the wallet silently shows a zero
balance. Previously the connect loop just crash-restarted into the same abort up
to 3x and then reported a bare "Daemon crashed N times", with no path forward.
Now:
- daemon/daemon_startup_diagnosis.h: pure blockDbOutputLooksBroken() classifies
the crashed node's captured console output (the fatal block-DB markers).
- The connect loop detects it on the FIRST abort, STOPS crash-restarting into the
same failure (each retry reloads the whole index — wasteful), and offers a fix.
- A one-shot -reindex flag (EmbeddedDaemon::setReindexOnNextStart → DaemonController
forwarder → args) rebuilds the block index + chainstate from the intact raw
blocks; App::reindexBlockDatabase() arms it and un-gates the loop to restart.
- An auto-shown dialog (renderBlockDbReindexDialog) + a notification explain the
situation ("your coins are safe; the node just can't load the chain") and offer
a one-click "Rebuild block database". Full-node only (gated), lite-safe.
This is the exact trap behind a real "big wallet shows no funds" report: a
post-format-change daemon over pre-change chaindata. Reindex also fixes a plain
corrupt index.
Adds testBlockDbOutputDiagnosis (the abort sequence + individual markers trip it;
normal startup / wallet-corruption / asmap errors do not). Suite green (1/1).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The wallets list badged the active/linked row from the runtime seed status
(activeWalletSeedBadge → wallet_seed_status_, reset only on disconnect) in
preference to the offline on-disk probe. Two issues let a genuinely legacy
wallet render as "seed phrase":
- The offline probe's budget-fallback branch dropped the fMnemonicSeed flag:
res.mnemonic was set only in the (parsed && complete) branch. The probe shares
a 768 MB budget across all wallet files, so a large wallet (e.g. a 194 MB one)
probed after the budget is spent falls into the fallback, loses its seed/legacy
classification (mnemonic → 0), and the row defers to the runtime badge.
- With mnemonic == 0, the code used the runtime badge, which can still carry a
HasMnemonic from a previously-active mnemonic wallet — mislabelling the legacy
wallet.
Fix:
- Carry the definitive positives (fMnemonicSeed/hdSeed/mkey) from a cap-truncated
btree walk — a found marker is authoritative even when the scan didn't finish.
- Make the on-disk fMnemonicSeed read take precedence: it's the SAME flag the
daemon's IsMnemonicSeed()/z_exportmnemonic consult, so a definitive read wins;
the runtime badge is used only when the probe genuinely couldn't decide, and
never overrides a definitive on-disk classification.
Verified the wallet in question is truly legacy (fMnemonicSeed=false on disk,
matching the daemon's CHDChain serialization + IsMnemonicSeed). The flag reader
(hdChainMnemonicFlag) is already unit-tested; suite green.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Audit of the full-node RPC response parsers and updater release-body parsers
(find -> adversarially-verify workflow) surfaced three worth fixing; three
others guard formats the project doesn't emit and are backstopped by signature
verification, so they're documented rather than churned.
- Price (Medium): parseCoinGeckoPriceResponse used .value(key, 0.0), which
throws type_error on a PRESENT null. CoinGecko emits null for usd_24h_change/
usd_24h_vol on illiquid tokens (DRGX is one) while still returning a valid
spot price; the outer catch turned that into no price update at all. Read
null-tolerantly so the valid usd/btc survives.
- Daemon updater (Medium): parseDaemonChecksums blanked '|'/backtick but not
markdown emphasis, so a bolded **archive.zip** checksum row was dropped and a
valid, correctly-signed release would be refused. Also blank '*'/'_' (cannot
cause a wrong-asset match; the 64-hex + .zip-suffix tests are unchanged).
- Opid poll (Low, severe failure mode): parseOperationStatusPoll read id/status
via .value() (throws on a present non-string) and the call site parsed OUTSIDE
its try/catch, so a throw left opid_poll_in_progress_ stuck true and wedged all
z-operation polling for the session. Type-check the reads and parse inside the
guard. (dragonxd can't emit non-string id/status; this is defense-in-depth.)
Regression tests: CoinGecko null field; opid non-string id/status; **bold**
checksum row. Suite green (1/1).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The lite first-run restore wizard enabled Restore for {12,15,18,21,24}-word
phrases, but the SDXL backend only accepts 24-word / 32-byte-entropy seeds:
LightWallet::new does copy_from_slice(&phrase.entropy()) into a [u8;32]
(lightwallet.rs:231), which panics on 16/20/24/28-byte entropy. Mnemonic::
from_phrase accepts the shorter valid phrase, and the restore FFI
litelib_initialize_new_from_phrase (lib.rs:127) has no catch_unwind (unlike
litelib_execute), so the panic unwinds across extern "C" -> process abort
(UB on the pinned rustc 1.63). A user restoring a legitimate 12-word seed
from another wallet crashed the app.
The Settings restore gate was already tightened to == 24 (6ff1fda) but the
first-run wizard gate (df14533) was never updated — same restore path, two
verdicts, crash only via the more-common first-run path.
Add shared util/seed_phrase.{h,cpp} as the single source of truth:
- normalizeSeedPhrase: fold NBSP/en/em/ideographic/narrow spaces to ASCII,
strip zero-width marks, collapse+trim (word bytes untouched)
- seedPhraseWordCount
- isCompleteRecoveryPhrase(int) == 24 (the sole SDXL contract)
Both restore gates now count via the normalizer and gate via
isCompleteRecoveryPhrase, and both submit the normalized phrase. This closes
the crash, reconciles the two gates so they can't drift again, and — because
tiny-bip39 splits on literal ASCII space with no NFKD — makes an NBSP-pasted
24-word seed (common from PDFs/note apps) restore correctly instead of being
undercounted and rejected.
Adds testSeedPhraseHelpers. Suite green (1/1).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Audit of the lite import path found no false-rejection defect (no client-side
gate; the two-command fallback in importKey makes the U/5/K/L prefix guess
non-binding; lite send reuses the now-P2SH-fixed send_tab helpers). But the
"zxview" viewing-key comment — which was WRONG in the full node (fixed earlier)
— is genuinely CORRECT here: SDXL's import takes an extended full viewing key
(zxviews…, hrp_sapling_viewing_key), whereas the full node's z_importviewingkey
takes an incoming viewing key (zivks…). The two are not interchangeable.
Add a note so nobody "harmonizes" the two gates and reintroduces the full-node
bug. Comment-only; no behavior change.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Same defect class as the import-key fix: a hardcoded prefix/length pre-filter
layered over the checksum validators rejected valid addresses before the daemon
saw them. The send-screen recipient gate required a[0]=='R', and the payment-URI
parser accepted only 'R'/'t' with rigid length bands — so every valid P2SH /
multisig address (DragonX SCRIPT_ADDRESS=85 → 'b…') was silently refused, leaving
the Send button disabled with no usable recipient.
Centralize recipient recognition in util/address_validation:
- isTransparentAddress: Base58Check with a 21-byte version+hash160 payload —
covers P2PKH ('R…', v60) AND P2SH ('b…', v85) on every network, rejects WIF
keys / typos by real checksum.
- isShieldedAddress: Bech32 + a Sapling payment-address HRP (zs / ztestsapling /
zregtestsapling), distinguishing a payment address from a viewing key.
- isValidRecipientAddress: either of the above.
send_tab's two validity helpers (the single choke point for all 5 call sites) and
the payment-URI format check now route through these. The URI parser now
checksum-validates the recipient (fail-fast on transcription errors) rather than
being prefix/length-only.
Tests use real checksummed vectors (P2PKH/P2SH/shielded, WIF- and typo-rejection);
testPaymentUri updated off its old fake fixed-char addresses. Suite green (1/1).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The client-side pre-check rejected legitimate keys before the daemon ever
saw them, surfacing "Unrecognized key format" / a cryptic daemon "Invalid"
error. Two concrete defects plus the brittle heuristic behind them:
- Viewing keys: isViewingKey looked for Zcash's "zxview" extended-FVK
prefix, but DragonX's z_exportviewingkey emits a Sapling *incoming*
viewing key (HRP "zivks"), which z_importviewingkey is the only form the
daemon decodes. Every real DragonX viewing key was refused. (F1)
- Uncompressed transparent WIF: the length+first-char heuristic accepted
{5,K,L,U} only, but a version-188 uncompressed key starts with '7'. (F2)
Replace the heuristic with structural validation using the existing
checksum validators (F3): add util::decodeBase58Check (checksum-stripped
payload) and util::bech32Hrp (HRP of a valid Bech32 string). Transparent
keys are now accepted by decoding Base58Check and checking the payload is a
33/34-byte secret key with a DragonX SECRET_KEY version byte (188 main/
regtest, 128 testnet) — covering compressed and uncompressed, rejecting
addresses/typos by real checksum. Viewing keys are matched by the real
incoming-VK HRPs (zivks / zivktestsapling / zivkregtestsapling).
The Sweep gate and the dialog's live type indicator run off the same
predicates, so they are fixed too (F4). Messaging now names the likely
cause and appends a wrong-coin/network hint to the daemon's raw "Invalid"
error (F5).
Adds testPrivateKeyImportRecognition plus decodeBase58Check/bech32Hrp
coverage; suite green (1/1).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The alert-history popup content sat flush against the popup's top and bottom edges. Add a
padY spacer above the header and below the content (on both the empty and populated paths).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Full-node ObsidianDragon 2.0.0 -> 2.0.1 (single source of truth: the project() VERSION in
CMakeLists.txt). Verified the generated header renders "2.0.1 (ObsidianDragon)". The Lite
variant is versioned independently (DRAGONX_LITE_VERSION, unchanged at 1.0.0).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Migrate-to-seed submits z_mergetoaddress -> an async opid, then only persists the resolved
txid once the op completes. An app-close during Sweeping (opid submitted, txid not yet
resolved) dropped the opid and resumed at the re-sweep gate, silently losing the tx.
Now the opid is persisted and re-tracked on resume. If the daemon forgot it (restart —
its op queue is in-memory only), the existing poller flags it stale and the callback falls
back to the dismissable Sweep gate; it can never hang (a thrown RPC aborts the poll, so a
stale classification only comes from a *successful* poll that omits the opid).
- New seed_migration_sweep_opid setting; adopted atomically with clearing any prior txid in
the SAME settings.save(), and only once the submit succeeds — so a failed "Sweep remaining"
re-sweep keeps the already-mined first sweep's Confirming context, and txid/opid are never
both authoritative (resume checks txid first; torn-write safe).
- Resume routing extracted to a pure, unit-tested helper
(data/seed_migration_resume.h::decideSeedMigrationResume): txid -> Confirming; opid AND
connected -> re-track (Sweeping); else -> the dismissable Sweep gate. The connectivity gate
keeps a disconnected resume out of the buttonless Sweeping spinner.
- Shared makeSweepCompletionCallback(resumed): success -> Confirming; resumed-stale -> Sweep
gate (re-fetch balance + "may have already completed" copy); fresh-fail -> Error.
Fund safety unchanged: adopt still gated on legacy balance ~0 AND sweep tx mined; legacy
wallet.dat only ever moved to a never-deleted timestamped .bak.
Reviewed in two adversarial rounds (design + implementation) per the migration-code mandate;
both safety facts (no fund loss, no hang) held, and the resume-UX traps they surfaced are
fixed. Build-clean; ctest 1/1 (adds testSeedMigrationResume). See docs/wallet-hardening.md.
*** Still requires a live mainnet interrupted-sweep run before release (human gate). ***
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Follow-up to the node-banner / staleness-badge / alert-history features — a 5-dimension
finder->verify review surfaced 4 real issues (the ImGui-stack-balance finder found none):
- Alert popup grew off the right edge: pivot (0,1) pinned the panel's LEFT edge at the
bell, which sits near the window's right edge, so a 320px panel overflowed rightward
(an explicit SetNextWindowPos pivot skips ImGui's on-screen clamp). Anchor the
bottom-RIGHT corner at the bell instead (pivot (1,1) at bellMax.x) so it grows left.
- Staleness badge could flash red on reconnect: WalletState::clear() reset everything
except the four last_*_update stamps, so the pre-outage timestamp survived and the
badge briefly showed "Updated Nm ago" the same frame the node banner cleared. Zero the
stamps in clear() (all readers treat 0 as "never"; app_network.cpp:1473 guards != 0).
- Banner min-height floor wasn't DPI-scaled: std::max(minH, baseH*vScale()) now uses
minH * dpiScale() so both operands are in scaled px.
- New i18n keys weren't in res/lang/: back-filled all 16 diagnostics/QoL keys into the 8
language files, additively (128 insertions, 0 deletions). zh/ja/ko reworded around 2
glyphs missing from the CJK subset and hard-asserted tofu-free against the subset font.
Build-clean both variants; ctest 1/1.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Toasts fade in 1-4s, so anything that scrolled past was gone. Notifications now retains
every pushed alert in a capped (100) ring buffer with a wall-clock epoch (AlertRecord) —
separate from the 5-item live-toast deque — plus a monotonic total_pushed_ counter.
A bell in the status-bar right cluster opens an upward popup listing recent alerts
newest-first: severity icon + colour (reusing the toast palette), the message, and a
relative age (formatTimeAgoShort), with a Clear-all action. An unread dot on the bell,
coloured by the most-severe unseen alert, marks alerts that arrived since the panel was
last opened — driven by totalPushed() deltas so it survives capping/clearing.
Thread note: every push is on the UI thread (RPC results run as main-thread MainCb
callbacks), matching this class's existing lock-free model; documented as a
no-raw-worker-thread invariant.
New i18n keys (alerts_*). Build-clean; ctest 1/1 (adds testNotificationHistory: retention,
order, cap, monotonic counter, clear). Closes the QoL bundle and the Foundation tier.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
When the wallet is connected but the balance has quietly stopped refreshing — a busy
daemon can fail z_gettotalbalance without dropping the whole connection (only *both*
core RPCs failing 3x triggers a disconnect) — the old number sits on screen while the
node-status banner stays hidden. The Total Balance card now shows a small pill on its
status line ("Updated 2m ago", amber, escalating to red past 3 min) so the stale value
isn't silently trusted; hovering explains it and points at the node connection.
No refresh-path changes: WalletState::last_balance_update is already stamped only on a
successful fetch (network_refresh_service.cpp), so the badge reads it and computes age
against the same std::time clock via util::formatTimeAgoShort. The decision is a pure,
unit-tested helper (ui/staleness_badge.h::evaluateStalenessBadge, 45s/180s thresholds)
gated on connected so it never contradicts the banner.
Closes P2 (5/5). Build-clean; ctest 1/1 (adds testStalenessBadge).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
A persistent horizontal strip now appears at the top of the content column whenever the
wallet can't reach its node — unlike the transient toasts it stays up for as long as the
fault persists, so an offline wallet is never silently mistaken for a working one.
The show/severity/action decision is a pure, unit-tested function
(ui/node_status_banner.h::evaluateNodeStatusBanner) fed a state snapshot by the new
App::renderNodeStatusBanner(). Three cases:
- full-node offline -> amber, "Reconnect" (App::tryConnect)
- embedded daemon crashed
& auto-restart gave up -> red, "Restart node" (App::restartDaemon)
- lite wallet open failed -> red, message-only
Suppressed during the wizard / wallet-switch / daemon-restart / screenshot-sweep / shutdown,
and while an expected startup phase (warmup / init / connect-in-progress) already owns the
screen. Banner height lives in res/themes/ui.toml (banners.node-status); colours come from the
material semantic palette; the detail text is ellipsis-clipped so it can't push the action
button off-screen. Drawn before the content edge-fade vertex capture so it stays fully opaque.
New i18n keys (node_banner_*). Build-clean both variants; ctest 1/1 (adds testNodeStatusBanner).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Settings (logging section) gains two support-friendly actions, now that the logging
foundation actually produces logs (W7-2):
- Open log folder: opens the config dir (Platform::openFolder) so users can find
dragonx-debug.log / dragonx-crash.log.
- Copy diagnostics: copies a plaintext support snapshot to the clipboard via the new
App::buildDiagnosticsReport() — version, build variant, platform, connection status,
active wallet path + existence + size, encryption/lock state, sync heights, and (full-
node) daemon status/running/crash-count/lastError, plus the log paths. No secrets.
Build-clean; ctest 1/1. Remaining QoL: persistent alert history, a daemon/RPC error
banner, and the W6-2 refresh-staleness badge. See docs/wallet-hardening.md.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The Foundation tier — answers the original "make it easier to diagnose problems" ask.
- W7-2 (keystone): the app-level Logger file sink was never initialized, so LOG/LOGF/
VERBOSE_LOGF went nowhere and dragonx-debug.log didn't exist on Linux/macOS at all.
main() now calls Logger::init(<config>/dragonx-debug.log) on every platform. Fixed a
latent deadlock this exposed: init() wrote its banner via write(), which re-locks the
non-recursive mutex_ it already holds — now written directly. On Windows the raw
stdout/stderr freopen moved to a separate dragonx-stdout.log so the two writers don't
contend on one file. Added testLoggerFileSink (also a deadlock guard — it would hang if
the fix regressed).
- W7-3: no crash handler existed on Linux/macOS. Added an async-signal-safe sigaction
handler (SIGSEGV/ABRT/BUS/FPE/ILL) that writes the signal id + a backtrace_symbols_fd
backtrace to dragonx-crash.log, then re-raises the default disposition for a core dump —
the POSIX counterpart of the Windows SEH filter.
- W7-4: Logger::init now rotates the log to a single .1 backup past 10 MB, so a long or
verbose session can't grow it unbounded.
Build-clean; ctest 1/1. Remaining Foundation: the QoL bundle (mostly UI). See
docs/wallet-hardening.md.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
P2 robustness batch (localized):
- W5-1 (Med, lite): persistAfterBroadcast returned false on a persistent post-send/shield
save failure, but both callers discarded it and it never logged — completely silent. It
now liteLogs the failure (the spent note re-derives on the next sync, so it's a
robustness gap, not fund loss).
- W5-2 (Med, lite): the post-sync and post-rescan save results (in the detached scan
threads) were ignored; both now liteLog on failure. LiteDiagnostics::log is mutex-guarded,
so it's safe from those threads.
- W6-1 (Med): WalletState::clear() didn't reset mining/pool_mining, so a wallet switch could
briefly show the previous wallet's hashrate/blocks. Now reset in clear() (the daemon
restarts on switch, so mining genuinely stops).
- W6-3 (Low): AddressBook::load() cleared entries_ then threw on the first non-object array
element — discarding EVERY contact. It now guards is_object() + per-entry try/catch,
skipping and counting malformed entries.
Build-clean; ctest 1/1. Remaining P2: W6-2 (refresh-staleness badge — needs UI, overlaps
the diagnostics Foundation bundle). See docs/wallet-hardening.md.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- W1-3 (Med): updateWalletIndexForActiveWallet stamped syncedHere in the markOpened block
at bare connect (idHash still empty), letting a freshly-restored wallet skip its needed
rescan. syncedHere is now stamped only once the wallet's identity is verified (idHash
non-empty), so it takes effect at the post-address-refresh index update; lastOpenedEpoch
still records at open.
- Startup guard (the W1-1 launch counterpart): App::init now exists()-checks the recorded
active wallet before the daemon is configured. A non-default active wallet moved/deleted
between sessions falls back to the default wallet.dat with a warning, instead of the
daemon silently auto-creating an empty wallet under the missing name. Runs before the PIN
vault init so the vault is scoped to the wallet actually opened.
Completes P1-B. Remaining P1: W3-3 (sweep opid persistence) deferred for careful
adversarially-reviewed work — re-tracking a stale opid could hang the migration if the op
poller doesn't time out; the existing balance/mined gates already prevent fund loss. See
docs/wallet-hardening.md.
Build-clean; ctest 1/1.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- W1-1 (High): switchToWallet never verified the target wallet file exists before
switching. dragonxd auto-creates a fresh empty wallet for a missing -wallet=<name>, so a
moved/deleted wallet file silently "opened" as a brand-new empty wallet with a zero
balance — looking exactly like fund loss. It now std::filesystem::exists-checks
datadir/<walletFile> before switching (ahead of the daemon-stop prompt) and blocks with a
"not found (moved or deleted?)" warning. Because the check runs regardless of how
switchToWallet is invoked, it also closes W1-4 (the stale switcher-row TOCTOU).
- W1-2 (Med): walletOutputLooksCorrupt matched the generic "Error loading wallet" string,
which dragonxd also prints for DB_TOO_NEW (a newer-version wallet) — so a version mismatch
was offered a -salvagewallet repair that cannot fix it. The generic match is now excluded
when the output also contains "newer version".
Build-clean; ctest 1/1. Remaining P1-B: W1-3 (syncedHere timing) + the startup-path
existence check. See docs/wallet-hardening.md.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Migrate-to-seed (legacy -> mnemonic wallet) moves real funds; three correctness fixes:
- W3-1 (High): beginAdoptSeedWallet swapped a hardcoded datadir/wallet.dat instead of the
ACTIVE wallet file. With a non-default active wallet (e.g. wallet-2.dat) it installed the
swept seed wallet into an unloaded wallet.dat and left the daemon reloading the emptied
legacy wallet — swept funds only recoverable via the seed phrase. Now swaps
datadir + "/" + getActiveWalletFile(), captured on the main thread (switching is blocked
during migration, so no race).
- W3-2 (High): SeedWalletCreator::create() ran remove_all(<config>/seed-migrate)
unconditionally at the start, so a prior migration that swept funds into the temp wallet
but was abandoned/crashed before adopting would have that fund-bearing wallet destroyed.
It now refuses (with a clear message) when DRAGONX/wallet.dat already exists — a completed
migration removes the dir on adopt, so a leftover means an unfinished one.
- W3-4 (Med): switchToWallet blocked switching only while the migration dialog was open;
closing it via "Later" mid-migration dropped the guard. Now also blocks while
getSeedMigrationPending().
Build-clean; ctest 1/1. Remaining P1-A: W3-3 (persist the sweep opid). See
docs/wallet-hardening.md.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
P0-B encryption-integrity cluster.
W2-2: the first-run wizard's "encrypt" stored the passphrase only in memory and let the
user into the app immediately, so a quit/crash or a failed daemon connect before the
deferred encryption applied left the wallet unencrypted with NO record encryption was
ever requested — the user believing it was encrypted. A persisted encryption_pending
settings flag is now set the moment encryption is requested (never the passphrase, only
the fact). refreshWalletEncryptionState() reconciles it on every connect: wallet observed
encrypted -> clear the flag; wallet NOT encrypted while the flag is set and no deferred
encryption is pending/in-flight -> a once-per-session "your wallet is NOT encrypted — open
Settings to finish" warning (the flag stays set, so it recurs each launch until resolved).
The passphrase is deliberately never persisted to auto-complete — surfacing it is the
secure choice.
W2-4: lockWallet()'s continuation only handled success — a failed walletlock RPC silently
left the wallet UNLOCKED (an unfulfilled auto-lock). It now logs and warns once (reset on
the next successful lock) so a failing auto-lock is visible instead of leaving the wallet
exposed.
Touches settings.{h,cpp}, app_wizard.cpp, app_security.cpp, app.h. Not unit-testable at
this layer (RPC/connect-driven state). Build-clean; ctest 1/1. See docs/wallet-hardening.md.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The lite create/open/restore requests carried a passphrase field that the UI collected
(a labeled, masked "passphrase" input) but the backend initialize* calls never used —
so a user could believe their lite wallet was passphrase-protected at creation when it
did nothing. It now has a real meaning, wired in LiteWalletController:
- create / restore -> encryptWallet(passphrase): the backend encrypts + locks + saves
the brand-new wallet.
- open -> unlockWallet(passphrase), but only when encryptionStatus() reports the existing
wallet is actually encrypted + locked (no spurious unlock on an unencrypted wallet).
encryptWallet/unlockWallet take their own copy of the passphrase and wipe it; the
request copy is still wiped as before. A post-create encrypt failure is liteLog'd (the
wallet still exists, so the create is not failed).
Six existing lite-controller tests carried an incidental "hunter2" create passphrase from
when the field was dead; removed (they exercise non-encryption flows and want an
unencrypted wallet), and added testLiteWalletControllerCreateEncryptsWithPassphrase to
prove the new behavior. Completes the wallet-hardening P0-A cluster (7/7). ctest 1/1.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The seed-phrase "Save" already wrote the file 0600 and zeroed the in-memory buffer, but
the success message was a bare "Saved to <path>" — no hint that it's a permanent
UNENCRYPTED copy of the seed at a predictable location. The message now reads
"Saved an UNENCRYPTED seed file — move it to secure offline storage and delete this
copy: <path>". English source updated; the res/lang back-fill of this changed key is
deferred to the batch i18n pass.
Also documents W5-3 (lite create-time passphrase) as a product decision rather than a
speculative change: the field is already wiped on every path (minimal security risk),
but the labeled masked "passphrase" input at lite create/open/restore is never consumed
by the backend — so either remove the dead UI or wire it into the lite encrypt flow.
Finishes the actionable part of the wallet-hardening P0-A cluster (docs/wallet-hardening.md).
Build-clean; ctest 1/1.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The wallet-hardening memzero cluster. Uses the file's established sodium_memzero
pattern (matching the existing lambda-capture scrub at app_network.cpp:2885 and the JSON
scrub at :4025) rather than a new type, since importPrivateKey/sweepPrivateKey are
fund-moving code.
- W4-1 importPrivateKey / sweepPrivateKey: the spending/viewing key was copied ≥3×
(calling frame -> worker-lambda capture -> JSON params) and never scrubbed. Now zeroed
on all paths: the calling-frame copy after the worker post, the lambda's captured copy
(lambda made mutable, zeroed once the request is sent), and the request params copy.
- W4-3 exportAllKeys / backupWallet: the concatenated all-keys buffer is now zeroed after
the consumer uses it, and the backup is written via
Platform::writeFileAtomically(..., restrictPermissions=true) — atomic and owner-only
(0600) — instead of a umask-default std::ofstream that left it world-readable.
- W2-3 decrypt-wallet passphrase: std::move-captured into the worker lambda (no plaintext
copy left in the calling frame) and sodium_memzero'd right after unlockWallet, its only
use.
Not unit-testable (no observable RPC effect — the key value sent to the daemon is
unchanged; only post-use memory zeroing is added). Build-clean; ctest 1/1. See
docs/wallet-hardening.md.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Removing wallet encryption z_exportwallet'd every private key in cleartext to
<datadir>/obsidiandecryptexport<ts>, re-imported it, and never deleted it — leaving a
full plaintext dump of every key on disk permanently. The decrypt flow now scrubs
(best-effort in-place zero-overwrite) and removes that file as soon as the
z_importwallet attempt resolves, on both the success and failure paths. Recovery, if
ever needed, remains the encrypted backup (wallet.dat.encrypted.bak), never this file.
Second fix in the wallet-hardening P0-A cluster (docs/wallet-hardening.md). Not
unit-testable (fs I/O in a deep worker lambda); build-clean, ctest 1/1.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The RPC console echoed and stored typed commands verbatim, so `walletpassphrase
<secret>`, `z_importkey <key>`, `encryptwallet <pass>` etc. left the secret in the
visible log AND the 100-entry recall history (copyable). Adds a pure, unit-testable
RedactConsoleCommand()/ConsoleCommandCarriesSecret() (allowlist of 13 secret-bearing
first-tokens) in console_tab_helpers; submitConsoleCommand() now echoes and stores
`> walletpassphrase ****` while still executing the real command unredacted. Bare
secret commands and non-secret commands pass through unchanged.
Output-secret commands (dumpprivkey / z_exportkey / z_exportmnemonic) — whose secret is
in the RESULT — are a separate redaction concern, tracked as a follow-up.
First fix in the wallet-hardening P0-A cluster (see docs/wallet-hardening.md). New
testConsoleSecretRedaction (11 assertions); ctest 1/1.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Links the real EmbeddedDaemon into the ObsidianDragonTests target (its deps were
already present) and adds two POSIX integration tests that exercise the actual
fork/exec/waitpid fixes headlessly:
- testExecFailureReported (F2): start() against a non-executable file must fail with a
precise "not executable or wrong architecture" reason.
- testDaemonCrashDetected (F1): a short-lived child that exits abnormally is still
detected (crash_count_ increments) while isRunning() is hammered from the test
thread — a regression test for the reap race.
Writing the F2 test surfaced a real bug: start()'s failure branch called
setState(State::Error, "Failed to start dragonxd process"), and setState stores the
Error message into last_error_ — clobbering the precise message startProcess() had
just set, so getLastError()/the UI only ever saw the generic string. Fixed to pass the
preserved detail to setState, so the precise reason survives and now also reaches the
state callback (crash panel / status).
ctest 1/1, green including the two new integration tests.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Introduces CHANGELOG.md (Keep a Changelog style, Unreleased section) covering this
batch, with F8's breaking change — remote plaintext RPC now refused by default —
called out front and center along with the rpctls=1 / rpcallowplaintext=1 recovery
steps. Also records the Security / Fixed / Added entries for F1-F7. Updates the
tracking doc's status to reflect the completed release-notes + i18n back-fill and the
remaining pre-release items (F1/F2 manual repros, CJK subset-font rebuild).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Additively adds translations for the 6 new keys from this batch
(sb_daemon_extract_failed, sb_daemon_files_failed, loading_stall_{title,body,hint},
sb_plaintext_remote_blocked) across res/lang/*.json. es/de/fr/pt/ru get all 6; for
zh/ja/ko a string is only added when every glyph is already in the current
NotoSansCJK-Subset.ttf, since the subset can't be rebuilt here — 6 zh/ja/ko entries
whose glyphs aren't yet subsetted are left on the English fallback rather than render
as tofu. Written sorted, indent=4, ensure_ascii=False (matching add_missing_translations.py);
purely additive (42 insertions, 0 removals). The remaining 6 need a
scripts/build_cjk_subset.py font rebuild.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
F8 (security). Two related fixes to how the wallet decides whether an RPC target is
safe to send Basic-auth credentials to:
- isLocalHost() was matching any host that merely *starts* "127." via
rfind("127.",0)==0, so "127.evil.com" (and "127.0.0.1.attacker", "127.300.0.1",
"1270.0.0.1") were misclassified as loopback and treated as local. It now uses a
strict isExactIPv4Loopback() parser: exactly four 0-255 dot-separated octets with
the first == 127. localhost / ::1 / [::1] handling is unchanged.
- A remote rpchost over plain HTTP (no rpctls=1) previously only produced a
dismissible warning and then sent rpcuser:rpcpassword in cleartext, where a
local-network MITM could capture them. tryConnect() now REFUSES that connection
(clear status line + one-time notification, no creds sent) unless the user opts in
explicitly with rpcallowplaintext=1 in DRAGONX.conf (new
ConnectionConfig::allow_plaintext_remote, parsed in parseConfFile; policy in the
new allowsPlaintextRemote()). Local/embedded daemons and rpctls=1 remotes are
unaffected.
BREAKING: a wallet configured for remote plaintext RPC will stop connecting until
rpcallowplaintext=1 (or rpctls=1) is added to DRAGONX.conf. Must be called out in the
release notes. The Settings-toggle UI is deferred (the conf-key opt-in is the recovery
path; see docs/daemon-startup-hardening.md).
Adds testIsLocalHost and testAllowsPlaintextRemote to test_phase4.cpp; one i18n key
(English) added to i18n.cpp.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
F3: the daemon connect loop retried forever with only an animated spinner when the
daemon was reachable-but-never-ready (stuck in RPC warmup / -28, or an external daemon
that never finishes init) -- no error, no guidance, no escape. It now stamps
connect_stall_since_ the moment the daemon first goes "reachable but not ready" (the
warmup branch + applyDaemonInitStatus) and clears it on connect / disconnect /
warmup-complete. A pure, unit-testable util::connectHasStalled() helper (new
util/connect_stall.h, 45s default from ui.toml [screens.loading].stall-timeout-sec)
drives a "Taking longer than expected" notice in renderLoadingOverlay(): a title, a
reassuring body with elapsed seconds, and a full-node hint to Settings > Restart Daemon
or the Console. The background retry keeps running underneath, so the notice self-clears
the instant it connects. Guarded off while the daemon is in State::Error (that case is
owned by the existing crash-count hint).
The overlay is a pure draw-list layer with no interactive widgets, so this follows the
existing crash-hint idiom (guidance text, not injected buttons); the stalled state is
computed locally in the overlay, so the only new App member is connect_stall_since_.
Adds testConnectHasStalled to test_phase4.cpp and three i18n keys to i18n.cpp (English
source of truth; the res/lang/*.json back-fill is deferred to a single
add_missing_translations.py run at the end of the batch).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Three verified daemon-startup edge-case fixes centered on the config/params
filesystem path:
- F7: new non-throwing Platform::ensureDirectory(dir, outError) with one
consistent "Cannot create <dir>: <reason>. Check permissions / free space."
message. Replaces the unchecked/throwing create_directories sites at main.cpp
(pre-init: log + Windows MessageBox + return 1), connection.cpp's
autoDetectConfig (was the *throwing* overload -- could raise an uncaught
filesystem_error through its callers; now sets the new
ConnectionConfig::dir_error), and both app.cpp daemon-dir sites (surface via
daemon_status_ + return false). The primary connect path (app_network.cpp)
checks dir_error and shows it instead of mislabelling it "waiting for config".
embedded_resources.cpp already checked its error_code, so it is left as-is.
- F6: verifySaplingParams() now hash-verifies each param against its pinned
canonical SHA-256 (source of truth: scripts/build-lite-backend-artifact.sh)
instead of only checking existence, so a truncated / corrupt-but-present param
is rejected up front rather than failing later on a shielded operation. A
<params_dir>/.sapling_verified marker keyed on size:mtime avoids re-hashing
~48MB on every startup. Logic extracted to the injectable, unit-testable
verifySaplingParamsIn(dir, digests); reuses util::sha256Hex (no new hash impl).
- F5: startEmbeddedDaemon() now checks extractEmbeddedResources()'s return and
the previously-dropped copy_file error_code in the daemon-binary fallback loop,
aborting with a clear status (sb_daemon_extract_failed / sb_daemon_files_failed)
instead of failing opaquely at spawn. An absent source file stays non-fatal.
Adds testPlatformEnsureDirectory and testVerifySaplingParams to test_phase4.cpp.
i18n keys added to i18n.cpp (English source of truth); the res/lang/*.json
back-fill via add_missing_translations.py is deferred to a single run at the end
of the batch. Progress tracked in docs/daemon-startup-hardening.md.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Three verified daemon-startup edge-case fixes in the embedded-daemon process
lifecycle (all in embedded_daemon.{cpp,h}):
- F1: EmbeddedDaemon::isRunning() (POSIX) now reads the atomic state_ instead of
calling waitpid(WNOHANG) from the UI thread, which raced monitorProcess()'s own
reap. waitpid is one-shot: whichever thread won consumed the exit status; if
isRunning() won, the monitor never saw the crash, so crash_count_/State::Error
and the 3-strike restart cap were silently lost. monitorProcess() is now the sole
reaper (predicate Running || Stopping keeps stop()'s wait loops correct). Mirrors
the existing XmrigManager::isRunning() fix.
- F2: startProcess() (POSIX) adds a close-on-exec self-pipe exec handshake. On a
non-executable / wrong-arch / corrupt binary, execv fails in the child and the
parent now learns synchronously (reads errno vs EOF), reaps the zombie, sets a
precise last_error_ ("not executable or wrong architecture"), and returns false
-- instead of reporting State::Running for a daemon that never started. Uses
pipe()+FD_CLOEXEC (not pipe2) so the branch stays shared with macOS. Parent-side
setpgid is now best-effort + logged.
- F4: start() gates on a lingering datadir lock after the port check. A graceful
shutdown releases the RPC port ~90s before the datadir .lock, so a rapid
stop->start spawned a daemon that died on the lock and, three times in ~12s,
tripped the 3-strike crash cap before the lock cleared. start() now polls
isDaemonProcessRunning() with a bounded ~300ms wait and bails with a distinct
non-crash Error (no crash_count_ bump) that the connect loop retries once the
lock clears. Isolated migrate-to-seed starts (skip_port_check_ / -datadir
override) are exempt.
Adds the testDatadirLockGate unit test (pure evaluateDatadirLockGate matrix) to
test_phase4.cpp. Plan and progress tracked in docs/daemon-startup-hardening.md.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Several related mining-tab pool improvements:
- Report the default pool fee correctly: pool.dragonx.is is 1%, not 0%.
The registry constant was hardcoded to 0. It now also fetches the live
poolFee from the pool's /api/stats alongside hashrate (no extra
request), so the displayed fee self-corrects and falls back to the
compile-time value only when the fetch hasn't landed.
- Show fractional fees: new FormatFeePercent trims trailing zeros so
whole fees read "1%" and fractional ones keep their decimals ("1.5%").
- Surface saved + custom pools in the pool list card: the list is now
the union of the official pools, the user's saved favorites, and the
currently-mined pool (effectivePools), each a selectable, endpoint-
deduped row. Previously the card only showed the hardcoded knownPools().
- Fix the xmrig "user" field: the "Payout Address" field now drives the
pool login rewards are credited to (resolveMiningUserAddress), instead
of being written only to "pass" while "user" was auto-derived from the
wallet's own first z-address -- which silently ignored a configured
payout address and could route rewards to the wrong address.
Unit tests cover parsePoolFee, FormatFeePercent, effectivePools, and
resolveMiningUserAddress; full app + ObsidianDragonTests build clean.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The pinned librustzcash / transitive crates (notably traitobject 0.1.0) rely on
pre-1.70 trait coherence and fail to compile on newer rustc (E0119), so the backend
must build with 1.63. Add a rust-toolchain.toml in the vendored backend so rustup
auto-selects 1.63 when cargo runs there — no more manual RUSTUP_TOOLCHAIN=1.63.0.
The pin is scoped to the backend tree (repo-root cargo keeps the default toolchain).
Also symlink the pin into the prepared build root so --silentdragonxlitelib-dir
builds honor it too.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- The macOS DMG filename used a separate hardcoded DMG_BASENAME ("DragonX_Wallet"),
so the export didn't match the .app/zip (ObsidianDragon). Derive it from
APP_BASENAME: full-node -> ObsidianDragon-*.dmg, lite -> ObsidianDragonLite-*.dmg.
The mounted volume + CFBundleName keep the "DragonX Wallet" display branding.
- build_release_mac did `rm -rf "$out"`, wiping all of release/mac on every build, so
building one variant destroyed the other's artifacts. Scope the cleanup to the
current variant's files (as the Linux/Windows release paths already do) so the
full-node and lite releases coexist.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The --mac-release path (full-node and --lite-backend) had never run on a real
Mac and broke on several Linux-only assumptions:
- build.sh version parser used GNU sed \+ (BSD sed reads it literally), so it
aborted before compiling — switched to POSIX [[:space:]][[:space:]]*.
- build.sh libsodium universal check used GNU grep \| — switched to grep -E.
- libwebp's cpu.cmake applies -mno-sse2 to its reference DSP files; under a
universal build (-arch arm64;x86_64) that lands on the x86_64 slice, where it
disables _Float16 and breaks the SDK 26 <math.h>. Added an idempotent
FetchContent PATCH_COMMAND (cmake/patch-libwebp-simd.cmake) to neutralize it.
- The SDXL lite backend static lib needs Security + CoreFoundation frameworks on
macOS; added them to the imported dragonx_lite_backend target for APPLE.
- Added a DRAGONX_MAC_ARCHS override plus auto-detect: with --lite-backend the
app is built for the arch(es) the backend .a actually provides (its pinned
ring 0.16.11 is x86_64-only), instead of failing to link a universal app.
- build-lite-backend-artifact.sh uses bash 4+ (mapfile); added a re-exec guard
for macOS's stock bash 3.2 and added bash to setup.sh's macOS core deps.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
setup.sh --win ran `sudo apt-get install` (and update-alternatives)
unconditionally, forcing the whole run under sudo even when mingw-w64 was
already present. Running setup as root makes the daemon cross-compile run as
root, leaving root-owned artifacts under external/dragonx that break `make
clean` on a later non-sudo build (stale objects relink -> mingw link failure
recurs). Gate the apt/update-alternatives block behind a presence check so
`./setup.sh --win` runs sudo-free (and the daemon build as the invoking user)
when the toolchain is already installed.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Adds `sign-daemon-release.sh release <secret.key> <version>`: zips each staged
prebuilt-binaries/dragonxd-{linux,mac,win}/ into dragonx-<version>-{linux-amd64,
macos,win64}.zip, signs each (detached ed25519), and prints the SHA-256 checksum
table for the release body. Platforms with no staged daemon are skipped; warns
if the signing key doesn't match the pubkey pinned in daemon_updater.h. Keeps the
existing keygen/pubkey/sign subcommands.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Point the miner build at DragonX/drg-xmrig and rename the prebuilt staging
dir xmrig-hac -> drg-xmrig across setup.sh, build.sh, the legacy Windows
script, .gitignore and README.
setup.sh now clones/builds the miner from git.dragonx.is/DragonX/drg-xmrig
(was dragonx/xmrig-hac), and the prebuilt staging dir is renamed
prebuilt-binaries/xmrig-hac -> prebuilt-binaries/drg-xmrig across build.sh, the
legacy Windows build script, .gitignore and README.
Also aligns setup.sh's section-8 directory list with section 7 (it previously
used a bare "xmrig" token, creating a stray empty prebuilt-binaries/xmrig/),
and updates the xmrig_manager.cpp header comment.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
In table view the table is inset into its glass panel by tVpad and its
outer_size is listH-2*tVpad, so the post-table cursor ended tVpad (~10px) above
where the cards/list views leave it, pulling the "N address saved" footer up.
Land the cursor at the glass-panel bottom (tpMin.y + listH) so the footer lines
up across all three view styles.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- balance: inset the address-row favorite (star) button by the card's inner
padding so it mirrors the left margin instead of hugging the card edge.
- mining: remove pool.dragonx.cc from the built-in default pools (pool.dragonx.is
is now the sole default); update the pool-registry test accordingly.
- mining: middle-truncate saved pool-URL and payout-address dropdown rows (new
shared material::TruncateToWidth helper) so a full z-address no longer runs
under the trailing delete (X) button.
- mining: fix the thread-grid cells overflowing the card at >100% display
scaling — the reserved Mine-button width used a raw clamp that didn't scale;
scale it by dp so cols is estimated correctly (no-op at 100%).
Full-node build + test suite green.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Dev-branch audit (docs/_archive/security-audit-dev-2026-07-22.md) re-found the
master issues (absent on dev) plus new ones in dev-only code. Applied here;
full-node build + test suite green; the subtle fixes were adversarially re-verified.
Ported from the master remediation (adapted to dev's code):
- bootstrap: reject zip-slip / path-traversal archive members (isSafeArchivePath)
before writing (S2-1). (dev already fail-closes on a missing checksum.)
- xmrig updater: fail closed when a signature is required but no key is pinned (F1-1).
- http_download.httpGetString: 16 MiB hard cap + MAXFILESIZE on the shared
metadata/price fetch (F1-2 / caps the updater + exchange paths at one site).
- rpc_client: explicit SSL_VERIFYPEER/VERIFYHOST (F4-1) and a 256 MiB response
cap in WriteCallback (F4-3).
- lite_connection_service: reject remote http:// lite servers, loopback only (L1-1).
Loopback is matched by a strict dotted-decimal 127.0.0.0/8 check (not a
startsWith("127.") prefix, which would wrongly accept 127.0.0.1.evil.com), with
userinfo/fragment stripping.
- lite controller: propagate encrypt/decrypt save() failure instead of reporting
success (F7-1).
- xmrig_manager: chmod(0600) the pool config before writing secrets (F5-2).
- app: clear the copied secret from the OS clipboard on shutdown (F3b-1).
- export_transactions: neutralize CSV/spreadsheet formula injection (F13-1).
- build pipeline: build-from-source lite backend + remove the self-attested
CMake signature gate (F15-1); pinned+verified appimagetool (F15-3/4);
verified Sapling params in setup.sh (F15-6); build.sh exits 0 on success.
(F14-1 empty-quoted-arg and F8-2 NUL-termination were already fixed on dev.)
Dev-only findings:
- rpc_client.callRaw: scrub the raw buffer + parsed tree (templated scrubJsonSecrets
for ordered_json) so console dumpprivkey/z_exportkey keys don't linger in freed
heap (N1-1).
- seed_wallet_creator: wipe the exported mnemonic on the failure path so a discarded
failed result never carries a live seed (W1-2).
- export_all_keys: write the plaintext key dump 0600 + atomically via
writeFileAtomically (U1-2).
- chat_database: restrict chat_messages.sqlite and its WAL/SHM sidecars to owner-only (C3-1).
Not done (need a decision, documented in the report):
- Chat header metadata (cid/z/p) rides outside the AEAD (C1/C2) — binding it is a
wire-protocol change requiring SilentDragonXLite interop review.
- Bootstrap lacks an offline-rooted signature (S2-2 residual) — needs signing infra.
- Console scrollback retains console-typed key-export output in plaintext (N1-1
residual) — inherent to an echoing console; would need output redaction.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Add de/es/fr/pt/ru/zh/ja/ko translations for the tt_* tooltip keys added for
the Chat & Contacts, lite Node & Security, and Debug Options controls. Written
additively (indent=4, sort_keys, ensure_ascii=False) — +37 keys per file, no
other churn. Product/technical tokens (dragonxd, RPC, HD, CPU, 0-conf,
lite-seed-backup.txt, Shift+Enter, numeric units) kept verbatim. Rebuilt the
CJK subset font for the 30 new zh/ja/ko glyphs (all now covered).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
A per-card audit found 38 interactive controls with no tooltip. Add them, with
new tt_* i18n keys (English source; per-language JSONs fall back to English
until translated):
- Chat & Contacts (8): emoji/bubble style, accent, density, text size, poll
rate, timestamps, enter-sends — the segmented/beginRow helpers added none.
- Node & Security (26): the whole lite wallet lifecycle / backup-keys /
encryption panel, plus the full-node RPC-details toggle and daemon Refresh.
- Debug Options (4): the screenshot-sweep / full-UI-sweep / open-folder /
seed-demo-chat buttons.
Theme, Wallet, Explorer, About were already fully covered.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Break up the tall, left-heavy Chat & Contacts card internally: Appearance on
the left, Messaging on the right when the card is wide (fills the width, ~halves
the height). Same Indent technique as the Node & Security card; the row helpers
retarget their leftX/rowW per column so controls right-align within each. The
narrow chat-settings modal stays single-column.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Page-level two-column masonry made the cards narrow and fought their internal
layouts. Revert the whole-page band (full-width stacked cards again) and keep
the internal NODE & SECURITY two-column — the better lever is breaking up the
large cards internally, not columning the page. The Explorer row-start-X fix is
kept (correct at full width too).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Per feedback, keep Theme & Language and About/Debug full width (they read
better wide), and put only Wallet / Node & Security (left) and Explorer /
Chat & Contacts (right) in the two-column band. The band captures its top
anchor below the Theme card and restores full width before About.
Also fix an Explorer bug the two-column band exposed: row 1 placed the Address
column with an absolute SameLine(pad + halfW + …) that ignores the right-column
Indent, so "Address URL" leaked into the left column. Anchor it to the row's
indent-inclusive start X instead (identical result at full width).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The 1000-logical-px threshold never engaged at 125% OS scaling: the panel's
real-px width (~1190) was below 1000×dpiScale (~1250), so the page stayed
single-column even though the internal NODE & SECURITY two-column (760×dpiScale)
did engage. Drop to 780 logical px — still requires usable (~390 logical) columns
but engages at the widths real windows actually have under display scaling.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Render the section cards in two columns when the page is wide (>1000px):
left = Theme, Wallet, Node & Security; right = Explorer, Chat & Contacts,
About; Debug spans full width below. Driven by shadowing `availWidth` to the
column width for the whole card region and floating the right column with
Indent() (a one-shot cursor set can't hold a column across line-advances);
each card's own responsive gates collapse their internal columns at the
narrower width. Collapses to one column below the breakpoint.
Fix the half-width overflows a parallel audit surfaced (cards previously only
ever rendered full-width):
- WALLET Tools & Actions: merge-to-address was a 3rd button on a 2-button row
(unconditional SameLine) — flip the separators so 2-per-row pairs cleanly.
- EXPLORER: wrap the Block Explorer button to its own row when the checkboxes
+ button won't fit (measured, locale-safe).
- NODE & SECURITY: wrap "Remove Encryption" below when the encrypted row's
three buttons + status don't fit the column.
- ABOUT: 2x2 button grid instead of 1x4 so labels don't clip.
- THEME: reserve the real gaps + custom-skin "*" marker in the theme combo
width so Refresh doesn't spill past the edge.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The encrypt controls + auto-lock combo + PIN hint are one SameLine chain that
fits at full width but overflowed the half-width NODE & SECURITY column,
spilling the "Encrypt wallet first to enable PIN" hint over the Daemon
column's buttons. When the section is narrow, break the auto-lock/PIN group
onto its own row at the section's left edge instead of continuing the line.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The two-column NODE & SECURITY card collapsed both columns onto the left,
overlapping. A one-time SetCursorScreenPos() can't hold a column: ImGui
resets the cursor X to the window's left indent on every line-advance, so
the first widget in the right column (a Dummy spacer) bounced everything
after it back to the left margin.
Shift the whole right column with ImGui::Indent(colW+gap) instead, so each
line-start lands at the column X; set the first widget's X explicitly to
match, and Unindent before continuing the page. Left column already renders
at the shadowed half-width, so it stays in its lane.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The NODE & SECURITY card stacked Node / RPC / Security / Daemon-binary in one
full-width column, packing everything on the left. Split it into two columns when
the card is wide enough: Node + RPC + Security on the left, Daemon binary on the
right — which fills the previously-empty right half and roughly halves the card's
height. Falls back to a single column on narrow windows (and when there's no
daemon section). Implemented by shadowing contentW + sectionOrigin per column
(renderSecuritySection already takes an explicit width/origin), so every
sub-section lays out within its column; the card auto-sizes to the taller column.
Full-node only for now; the lite Node section stays single-column.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Step 5. The Wallet privacy/daemon checkbox row and the Advanced-Effects checkbox
row shrank their text (SetWindowFontScale) to cram onto one line — they now wrap
to new rows at full size instead, so nothing clips on narrow windows (the lite
"Animate avatars" was being cut off). Also drop two now-unused locals left over
from the Node/Daemon rebuild. (The Explorer row, the RPC collapsible header, and
the secondary/compact effects layout are left for a later polish pass.)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Step 4. Bring the Lite settings section in line with the full-node one: its
subsection headers (Backup & keys, Security, Maintenance) switch from the Body2
type style to the accent Overline used by the full-node Node/Daemon/Security
subsections, and the standalone action buttons (Open data folder, Show seed,
Show private keys, Redownload blocks) become icon ActionButtons matching the
full-node button styling. The intricate secret-reveal / encryption flow and its
label-column input layout are left untouched.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Step 3. Node "Data Dir" is now a clean key/value block: the directory on its own
row as a click-to-open accent link with a copy button, the wallet size below, and
the folder buttons on their own row (icon ActionButtons) — no more path
font-scaling or right-align overflow math. Daemon binary shows Installed / Bundled
as key/value rows plus a Success/Warning status chip (replacing the fragile
atom-by-atom wrapped status line); "Check for updates…" is now the accent Primary
action, and the maintenance actions are a separate row with Delete Blockchain
styled Destructive (Test/Rescan/Repair Secondary). All handlers, disabled-state
predicates and tooltips are preserved; the buttons wrap instead of overflowing.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Step 1: promote the polished chat-settings building blocks into reusable material
components (src/ui/material/settings_controls.h) — SettingsSubheader (accent
small-caps), SettingsRow (label left / control right), SegmentedControl (iOS
track+pill), and a tiered ActionButton (Primary=accent fill, Secondary=glass,
Tertiary=ghost, Destructive=error) with an optional leading Material icon, plus a
ButtonFlow that wraps rows of buttons.
Step 2: rebuild the Wallet BACKUP & DATA button wall on them. The 9–11 identical
buttons that were font-scaled onto one row are now tier-ordered, icon-labelled,
and wrap to new rows: the emphasized actions the user flagged — Import key, Seed
phrase, Wallets…, Download Bootstrap — cluster on top as accent Primary buttons;
common actions (viewing-key import, Backup, Migrate, Setup Wizard) are Secondary;
exports are low-emphasis Tertiary. All click handlers/tooltips and the migrate
"legacy wallet" glow are preserved. Removes the SetWindowFontScale legibility hack.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The custom DragonX chat emoji now recolors to the theme like the logo — body =
accent, detail = white on dark skins / on-surface (dark) on light skins — and
re-rasterizes on a theme/dark-light change (moved out of the one-time fixed-color
load into ensureLogoTexture's re-render block). The detail highlights keep it
legible even on accent-tinted outgoing bubbles.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The SVG's white highlight (detail) washed out against a light card/background on
light skins — the dragon's wings/detail nearly vanished, leaving only the accent
body. Make the detail colour theme-aware: white on dark skins (unchanged), the
theme's on-surface colour (dark) on light skins, so the full mark reads on both.
Applies to the header logo and the balance coin icon; re-rasterizes on the
dark↔light flip (already tracked by the logo guard). The fixed-brand ":drgx:"
emoji is unaffected (its crimson body carries white detail fine on any bubble).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The prominent DragonX mark on the Overview (next to the balance) is the coin /
currency icon (coin_logo_tex_), a separate texture from the header logo — and it
was still loaded from the fixed logo_dragonx_128.png, so it never recolored when
switching themes. Route it through the same themed SVG: ensureLogoTexture() now
rasterizes BOTH the header logo and the coin icon from logo_dragonx.svg recolored
to the theme accent, re-rasterizing both together on a skin / dark-light change
(coin done first, since the header branch early-returns on success). The old
one-shot PNG coin-logo load in render() is removed (superseded), the PNG remains
a fallback, and the coin texture is now freed on theme switch (was leaked).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add a DragonX custom emoji to chat. The emoji picker gains a DragonX tile (first
cell) that inserts the text ":drgx:"; message bodies containing it render the mark
inline via layoutChatBodyRich, which flows text words + the emoji image with word
wrap (plain bodies keep the tighter layoutChatBody path, so normal messages are
unaffected). Other clients simply show the literal ":drgx:" text — a portable
encoding with graceful degradation.
The emoji is the DragonX SVG rasterized once at fixed brand colors (crimson body,
white detail) via LoadTextureFromSvg — theme-independent so it looks identical for
sender and receiver — exposed as App::getDrgxEmojiTexture(). Emoji insertion is
factored into one insertToken() helper (space-prepend + on-chain byte cap), shared
by the DragonX tile and the Unicode emojis. Drag-to-select text is skipped on
":drgx:" messages (their inline layout doesn't match the plain-text hit-test
geometry); right-click "copy" still copies the whole message.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Replace the shared app logo (Overview header, first-run wizard, lock screen) with
the DragonX mark rendered from res/img/logos/logo_dragonx.svg, recolored to each
theme: the dragon body takes the theme accent (Primary()), the inner detail stays
light. Vendor nanosvg (memononen, zlib/public-domain) in libs/nanosvg/ and add
util::LoadTextureFromSvg (parse a copy, recolor shapes by luminance, rasterize to
RGBA, upload via the existing CreateRawTexture — GL + DX11). The SVG is embedded as
a string (src/embedded/logo_dragonx_svg.h) so it's available in every build.
The load moves into App::ensureLogoTexture(), called at the top of render() BEFORE
the wizard/lock early-returns (so those screens get the logo too, which they never
did before), and re-rasterizes when the accent or the dark/light variant changes.
The per-skin PNG remains a fallback if rasterization ever fails; logo-texture
lifetime is now freed on replace + on theme switch (was leaked).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Adds 🐉🐲🐍🦎🐊🐾🥚🪺🏰 ⚔ 🛡 🗡 to the emoji picker, with DRGX-flavoured
keywords (the dragons are searchable by "dragon"/"drgx"). All are single-codepoint
emoji verified present in both the bundled monochrome NotoEmoji subset and the
color Twemoji font, so no font rebuild is needed and they render on other clients
(standard Unicode, UTF-8 in the memo, within the 236-byte cap).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Transactions that carried a chat message (sent or received, messages + contact
requests) now show a "Message" badge in the History tab, and a new "Chat" option
in the type-filter combo shows only those transactions.
Detection reuses what the chat store already tracks: ChatStore gains an inline
chatTxids() returning the set of on-chain txids that carried a chat message; the
tab builds it once per frame (cheap — O(chat messages)) and tests txid membership
for the badge and the filter. Empty when chat is disabled or the wallet has no
chat identity. Both variants populate the chat store before this tab renders, so
the same code serves full-node and lite with no separate handling.
The "Message" badge reuses the stacked top-pill slot (chat txs are send/receive,
not the autoshield "shield" type, so it and the "Shielded" badge are mutually
exclusive; chat takes precedence) in a distinct Secondary() colour. Guards the
summary-card accent idx_map so the new filter value (4) can't index it OOB.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The pill was drawn on the parent window's draw list after EndChild, but ImGui
renders a child window ON TOP of its parent — so the message text covered it.
Draw it instead on the thread child's own draw list, after the message loop
(later draw commands render on top), with a PushClipRect so the child's content
padding / scrollbar doesn't clip it. Clickability is unchanged (hand-drawn +
IsMouseHoveringRect / IsMouseClicked test the cursor directly), and it stays
pinned to the visible bottom-right corner via absolute screen coordinates.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Five message-thread improvements:
- Auto-scroll like the Console tab: new messages pin to the bottom only while
the reader is already there; a wheel-up detaches (re-armed on return to the
bottom, after a cooldown). A direct mouse-in-rect test (not IsWindowHovered,
which is false while the composer holds focus) matches ApplySmoothScroll so
scrolling up to read history while typing still detaches.
- The "Latest" jump pill is now hand-drawn (rect + IsMouseHoveringRect /
IsMouseClicked) instead of ImGui::Button: it overlaps the thread child which
owns mouse-hover there, so a real widget on the parent never got the click.
Clicking it re-arms auto-scroll; its geometry is shared with the deselect
guard so a pill click no longer drops an active selection.
- Sending a message closes the emoji picker (it takes over the conversation
list) so the list reappears, and clears its search filter.
- Inserting an emoji prepends a space when the draft doesn't already end in
whitespace (still respecting the on-chain byte cap).
- Message text is selectable by click-drag (chatBodyLines / chatBodyHitTest
mirror layoutChatBody's wrap so highlight + hit-test align with the drawn
text); while a range is active a copy button shows at the bubble's top on the
opposite side and copies the selected substring.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The status-bar chat-buffer indicator was hardcoded English. Add semantic keys
(chat_buffer_sending[_one]/preparing/loading/ready) to the English source and
route chatBufferStatusText() through TR()+snprintf like the other counted
strings — a singular/plural pair for the send count, %d/%d for the buffer
fill. Translate all five into de/es/fr/ja/ko/pt/ru/zh (additive JSON edits,
format specifiers preserved so the runtime overlay accepts them), and rebuild
the CJK subset font for the two new glyphs (버퍼's 퍼 U+D37C, 缓冲's 冲 U+51B2).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Each chat message is a shielded tx that spends a verified note, so a burst of
messages hit "insufficient verified funds" once the single note is spent. Add a
per-frame note-buffer coordinator (both variants) that keeps ~10 verified
spendable notes: it serializes sends through the one broadcast channel, counts
verified notes by block depth (lite) or a rate-limited z_listunspent scan
(full node), self-splits in the background to refill toward the target, and
queues overflow to drain as change matures — with honest Sent/Failed status
instead of the prior optimistic "Sent". Guardrails: single split in flight with
a watchdog, cooldown, and session-generation guards so a wallet switch can't
drain another wallet's queue. Surface a "Chat buffer: N/10 ready" indicator in
the status bar while the Chat tab is active, and route chat diagnostics through
the console on both variants (the full-node console now drains the shared ring).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Carry the sender's compose time as an optional "ts" (Unix seconds) in the
plaintext header JSON that rides outside the AEAD, and prefer it as the
displayed message time so both ends show the same send time regardless of when
the tx confirms. Parse "ts" leniently. On ingest, clamp: reject a "ts"
implausibly in the future vs the receive/block time (1h skew tolerated) so a
wrong/ahead peer clock can't pin messages to the bottom of a thread; a past
compose time is fine (the note buffer may broadcast a queued message later, and
a confirmed tx's block time is always >= compose time). Tests cover the
round-trip and the future-clock clamp.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Replace the plain "N/236" byte counter with a radial wheel drawn inside the
input, right-aligned, filling green→amber→red as the message approaches the
236-byte memo cap. Vertically center the text with left padding (FramePadding),
hard-cap typing at the cap (no silent overflow), add Shift+Enter for a newline
(via an always-callback, since ImGui 1.92's Enter shortcut needs exact mods)
while plain Enter still sends, animate the input growing taller as lines are
added, and word-wrap the display instead of overflowing left. Give a hard
newline a touch more space above than a soft wrap in the message bubble.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Jade's edge-trace hero hand-walked the panel perimeter as straight line
segments, so rounded corners chamfered into flat polygons. Retire it in
favour of the gradient-border effect, which draws via AddRect and hugs
the real corner arcs.
- theme_effects: drawGradientBorderShift gains phaseOffset + alphaMul;
drawPanelEffects now draws it on every glass panel (position-phased so
panels drift like veins at different depths, 0.6x alpha vs the active
nav button), gated behind a new opt-in gradient-border-panels flag so
button-only themes (Obsidian) are unaffected.
- jade.toml: edge-trace off; gradient-border-panels on, speed 0.10,
jade->gold; jade motes + viewport wash/vignette kept.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The 1024x1024 jade marble texture referenced by res/themes/jade.toml,
committed on request so the Jade skin ships with its background instead
of falling back to the programmatic gradient. Sits alongside the other
tracked theme backgrounds in res/img/backgrounds/texture/.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The chat and contacts panes drew flat ImGui fills (ChildBg / default
WindowBg / NoBackground), so they never sampled the acrylic blur and
showed the raw backdrop texture. Route them through DrawGlassPanel --
the pattern the peers_tab sibling and every other tab already use:
- chat: frost the conversation-list + thread panes and the composer
input box, and add inner padding so content doesn't hug the glass edges
- contacts: frost the card/list container AND the table view, padding both
Padding needed ImGuiChildFlags_AlwaysUseWindowPadding -- a borderless
child silently ignores WindowPadding otherwise (documented gotcha in
daemon_download_dialog.h). BeginTable can't take that flag, so the table
view is inset within its glass panel instead.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
A new bundled skin driven by the existing jade_bg.png background: deep
green surfaces, jade-green primary accents, and a soft gold secondary
echoing the stone's veins, plus a jade specular-glare + jade-to-gold
sidebar-border effect. SkinManager auto-discovers it (no C++/CMake
changes) and it appears in Settings -> Appearance as "Jade".
The background asset (res/img/backgrounds/texture/jade_bg.png) is a
binary handled separately; until it lands in the repo the skin falls
back to the programmatic gradient on other machines.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
testXmrigAssetSelection / testDaemonAssetSelection index rel.assets[i]
right after EXPECT_TRUE(i >= 0), but select*Asset returns -1 on no match
and this harness's EXPECT doesn't abort -- so a fixture/parser regression
would read rel.assets[-1] and SIGSEGV the whole suite instead of
reporting the failed EXPECT. Gate each index read behind `if (i >= 0)`
(same pattern as the AddressBook config-dir fix). No behavior change on
the valid checked-in fixtures; ctest passes 100%.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
testAddressBookScope hardcoded ".config/ObsidianDragon" for its
pre-scoping addressbook.json, but AddressBook::load() reads the
per-variant config dir (Lite -> ObsidianDragonLite/). In a --lite build
the write and read diverged, so load() found nothing, size()==0, and the
following entries()[0] read out of bounds -> SIGSEGV, taking the whole
suite down.
Write to dragonx::util::Platform::getConfigDir() (the same path load()
uses, resolved under the test's temp HOME) and guard the entries()[0]
access so a non-aborting EXPECT can never SIGSEGV the suite. Verified:
ctest now passes 100% in BOTH the full-node and --lite builds.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Give the lite console the parity analog of the full-node RPC command
reference: the same searchable two-pane modal (browse/search, detail
pane, examples, Insert / Insert & run, destructive confirm), driven by
the lite backend's own command set instead of daemon RPC.
- ConsoleCommandExecutor::commandReference() returns the category table
(full node = consoleCommandCategories(); lite = new
liteConsoleCommandCategories() -- 25 backend verbs in 5 categories).
The shared renderCommandsPopup reads the table from the executor.
- The Commands button now shows for both variants (gated on
commandReference()!=nullptr); title/tooltip/arg-quoting branch on
hasRpcReference() (clarified to mean "speaks JSON-RPC / full node").
- Lite args are bare tokens (the backend takes one unsplit arg string
and does not strip quotes), so the param-builder's JSON string
auto-quoting is disabled for lite -- Insert & run emits runnable bare
commands. send uses the JSON-array form (its positional form is
unreachable via the single-arg transport); new uses zs/R; import
takes just the key (the backend hardcodes birthday=0).
Adds 7 i18n keys across all 8 languages (additive). Full-node behavior
is unchanged. Adversarially reviewed (data vs the backend registry,
plumbing/regression, i18n) with all findings fixed.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The lite console tab lagged the full-node one on i18n: its toolbar status,
status lines, help, and error/warning strings were hard-coded English or
worded for a full node (a "daemon" the lite wallet doesn't have).
- toolbarStatus()/statusLines(): route through TR() (reusing the existing
lite_net_* keys where they already map)
- printHelp(): enumerate the 25 pass-through backend verbs for discoverability,
since the C++ tab intercepts `help` before the backend's own HelpCommand runs
- gate the destructive 'stop' confirmation to the full node (hasRpcReference);
lite has no node, so it lets 'stop' fall through to the backend instead of
showing a phantom "shut down the node" warning behind a dead gate
- word the not-connected error per variant (daemon vs "no wallet open")
- TR() the "(no output)" command-result fallback
Adds 7 i18n keys across all 8 languages (additive); rebuilds the CJK subset
for the one new glyph (U+C5D4 for the Korean "backend"). Build + ctest +
source-hygiene green; changes adversarially verified (no logic/i18n defects).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The status indicator's left inset (+2) and dot-to-text reservation (+6) were raw pixels while the dot radius is DPI-scaled, so the dot shifted a couple native px at HiDPI. Multiply both by Layout::dpiScale() per the project's hand-drawn-geometry rule.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
std::localtime shares a process-wide static tm; both sites are reachable from background threads (the RPC price-parse on the worker thread, and Logger::write from worker/monitor threads), so a concurrent localtime call could clobber the struct between the call and the read. Copy into a local std::tm via localtime_r / localtime_s, matching the codebase's existing convention (console_tab rpcTraceTimestamp, app_network, etc.). From the console-tab audit; benign (garbled/stale timestamp only), no crash or state impact.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The accent + text-colour toggle buttons pushed/popped ImGuiCol_Text guarded by a flag the TactileButton flips in between, so a click left the colour stack unbalanced for that frame — and ImGui's error recovery drew a red rectangle around the console window (imgui.cpp:11727). Capture the flag into a local before the button and guard both the push and the pop with it.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Render the portfolio trend sparkline in every row style, each with a layout that features it:
- Table: a dedicated header-labelled TREND column (48dp rows), aligned across rows.
- Cards: a left-aligned info stack (icon + label / value+delta chip / DRGX) with a full-height sparkline filling the rest of the width (content-driven, so short groups give the chart more room).
- Spotlight: a big-value-over-a-full-width-chart-band hero tile (92dp).
Default sparkline interval is now MONTH (a real curve from the daily series, not the young minute buffer). Removed the per-row container accent line (redundant with the icon/sparkline colour) and the per-group shielded/transparent bar (private-by-default makes it near-always 100%, and dropped its now-dead SumPortfolioSplit helper). DPI-scale the sparkline stroke. Adds the "Trend" column string across 9 languages.
Relocated the Market controls out of a removed settings modal + gear: the chart line/candle picker now sits top-right of the chart (left of the trade button, candle-capable ranges only), and the Table/Cards/Spotlight picker sits left of Manage. Dropped the market fetch-prices toggle (still in the general Settings page).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Two coupled Market-tab changes (same file):
- Sparkline in every style: render the per-entry trend sparkline in Table (a reserved trend column, aligned under the column header, reserved only when at least one group opts in so the dense grid isn't padded otherwise) and Cards (a filled strip on the right, above the full-width Z/T bar), not just Spotlight — so the per-entry 'show sparkline' setting is honored in all three views. DPI-scale the sparkline stroke (1.2f/1.4f * dp), from the sparkline audit's L1.
- Fix the candlestick toggle intermittently missing: the Market tab never triggered the per-exchange OHLC fetch on its own (only a pair-chip / refresh click did), so candlesticks were unavailable on a fresh tab open. Call refreshExchangeChart() every frame (self-throttled + in-flight-guarded — its own doc comment already says 'safe to call every frame'), which also fixes the new pair's candles never loading if you switch pairs mid-fetch.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Market-tab audit follow-up (all 9 confirmed findings):
- Scale the unscaled corner radii on the Cards/Spotlight row rects, the address-preview glass panel, the chart Y-axis labels, and both chart hover tooltips by dpiScale() — they mismatched the coincident already-scaled rects on HiDPI (the CLAUDE.md hand-drawn-geometry rule).
- Share the portfolio row-height/gap formula (pfRowHeight/pfRowGapFor) and the TABLE column widths (kPfValColW/kPfDrgxColW) between the draw loop and the scroll-region height budget so they can't drift and clip.
- Derive the portfolio total from the shielded/transparent split (one address scan instead of two in Cards).
- Build the address-picker selected-set once (unordered_set) instead of a linear PortfolioEntryContains per sort-compare and per row.
No correctness/security/threading defects were found in the audit.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Widen the chat settings modal and split it into a live message preview (canned bubbles rendered with the real style/accent/density/font/emoji/timestamp code) beside the controls; size the Done button to its text.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>