Commit Graph

111 Commits

Author SHA1 Message Date
403e145b30 feat(wallet): give new full-node wallets a BIP39 seed phrase + backup UI
New full-node wallets now get a real 24-word recovery phrase, and users can
reveal/back it up from Settings.

- Pass -usemnemonic=1 to dragonxd. The daemon reads it only inside
  GenerateNewSeed() when a wallet has no seed yet, so it is inert on existing
  wallets (safe to pass unconditionally) and makes every freshly-created
  wallet mnemonic-backed — its phrase is then exportable via z_exportmnemonic
  and portable to SDXLite/ObsidianDragonLite. (Existing/legacy wallets are
  unaffected and keep using the chat identity's z_exportkey fallback.)
- App::exportSeedPhrase() wraps z_exportmnemonic on the RPC worker with
  sodium_memzero wiping on every path; it flags the daemon's "not derived
  from a mnemonic" error as a legacy wallet rather than a failure.
- New "Back up seed phrase" modal (Settings -> Backup & Data, full-node
  gated): reveals the phrase in a numbered word grid, copy (45s clipboard
  auto-clear) + save-to-file (0600), wipes the secret on every close path
  incl. dismiss-mid-fetch. Shared ui/windows/seed_display.h grid helper.
- One-time nudge (settings flag seed_backup_reminded) toasts mnemonic-wallet
  users to back up their phrase; legacy wallets are not nagged.
- 15 new strings translated into all 8 languages (additive, 957->972 keys);
  CJK subset font rebuilt for the new glyphs.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-07 10:49:03 -05:00
008cc82ee5 feat(chat): harvest chat memos on the lite variant (lite receive)
The full-node receive harvest works off z_viewtransaction; the lite wallet's
transactions come from the backend instead, so lite chat receive was unwired.

App::ingestLiteChatMemos runs after each lite refresh: the backend lists one
entry per received (non-change) note — same txid, distinct position + decoded
UTF-8 memo — so a chat tx yields two entries (header + payload). Group the
Receive-kind notes by txid, feed them through the SAME extractHushChatTransaction
Metadata parser (now order-tolerant, so the daemon's output shuffle is a non-issue),
and thread the result into ChatService. The store dedups (txid+position), so
re-listing every refresh is harmless.

This closes the lite variant's chat loop end-to-end: identity (exportSeed), DB
unlock/load, receive (here), UI, and send (broadcastChatMemosLite). Gated by
DRAGONX_ENABLE_CHAT (default OFF). Verified: Linux + Windows build with chat ON,
ctest 100%, hygiene clean; caches restored to OFF.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-06 17:05:20 -05:00
980a100edd feat(chat): read-only Chat sidebar tab (Phase 3)
Add the first user-visible HushChat surface: a Chat tab in the sidebar with a
two-pane, read-only conversation view — a conversation list on the left and the
selected thread on the right — reading from the App-owned ChatService store.

- New src/ui/windows/chat_tab.{h,cpp} (RenderChatTab(App*), mirroring the
  Contacts tab). Conversations are sorted by most-recent activity; peer
  z-addresses resolve to contact names via the address book when known; each
  message shows sender + timestamp + wrapped body, with a contact-request tag
  and a read-only footer (composing arrives in a later phase). Empty states
  cover "wallet locked / identity not ready" and "no conversations yet".
- Sidebar wiring: NavPage::Chat + registry entry (static_assert stays balanced),
  NavPageSurface + GetNavIconMD (ICON_MD_CHAT), dispatch in app.cpp, trace/sweep
  page names, and App::chatService() accessor. The tab is gated on the new
  WalletUiSurface::Chat, which returns DRAGONX_ENABLE_CHAT != 0 — so it only
  appears in chat-enabled builds and is hidden (and unreachable) by default.
- i18n: English defaults for the chat nav label + hint strings.

Verified: Linux + Windows(mingw) build with chat ON, ctest 100%, hygiene clean;
caches restored to the OFF default. The screenshot sweep now includes the Chat
tab (sweepPageName "chat") for per-theme visual review.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-06 15:05:40 -05:00
46eec37013 feat(chat): persistent, seed-encrypted message store (Phase 2)
Swap the in-memory-only chat store for durable sqlite persistence with real
per-transaction timestamps, encrypted at rest under a key derived from the
wallet's own seed (no passphrase, works on encrypted and unencrypted wallets).

- ChatDatabase (src/chat/chat_database.{h,cpp}): sqlite store mirroring
  data::TransactionHistoryCache. unlockWithSecret(seed) derives a 32-byte AEAD
  storage key and a wallet-partition tag via domain-separated keyed BLAKE2b
  (generichash) contexts. Every record — bodies, peer z-addrs, threading,
  timestamps — is crypto_aead_xchacha20poly1305_ietf-encrypted with a random
  nonce and the wallet tag as associated data; even the dedup key is a keyed
  hash of txid+position, so nothing about your conversations is in cleartext on
  disk. Rows are partitioned per-wallet; a different seed sees nothing. Messages
  are decrypted once at ingest then re-encrypted under the storage key, so
  load() needs only the storage key, not the chat identity.
- ChatService: ingest() now stamps each message with its own transaction time
  (txid->time map + fallback) and writes new (store-deduped) messages through to
  the database; loadFromDatabase() rehydrates the in-memory read model on unlock.
- App: unlock the chat DB with the same seed in provisionChatIdentityFromSecret
  and load prior history; lock the DB + clear the decrypted in-memory store on
  relock and on lite-controller rebuild.

Adversarial review (4 confirmed findings, all fixed): don't provision if the
wallet locks mid-fetch (re-check isLocked at completion); wipe the serialized
plaintext temporary in append(); trim the seed into a separate fully-wiped
buffer (no residue past a shrunk size()); scrub the mnemonic copy in the RPC
json result.

Tests: ChatDatabase round-trip (persist/reload, field + order fidelity), dedup,
per-wallet isolation, lock inertness, and ChatService write-through + reload
without an identity. Gated by DRAGONX_ENABLE_CHAT (default OFF). Verified:
Linux + Windows(mingw) build with chat ON, ctest 100%, hygiene clean; caches
restored to the OFF default.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-06 14:49:33 -05:00
1738468f8c feat(chat): provision seed-derived identity + wire the receive sink
Complete the Phase-1 App integration for HushChat, now that the dragonx
`hd-transparent-keys` daemon exposes a BIP39 mnemonic (z_exportmnemonic)
that is byte-compatible with SilentDragonXLite. Both variants derive the
chat identity from the wallet's OWN seed phrase, so it is portable across
full-node and lite (same words -> same KDF input -> same identity) and
recoverable from the single wallet backup.

- App owns a dragonx::chat::ChatService; maybeProvisionChatIdentity() runs
  each update() tick and, once the wallet seed is reachable and unlocked,
  derives the identity via deriveChatIdentityFromSecret and sets it on the
  service. Full-node fetches z_exportmnemonic off the UI thread via the RPC
  worker; lite reads it synchronously through LiteWalletController::exportSeed.
  Secrets are wiped on every path.
- Wire the previously-dead TransactionRefreshResult.hushChatMetadata sink:
  both the full and recent transaction-refresh completions now ChatService
  ::ingest the harvested memos (before the result is moved) so incoming
  messages are decrypted and threaded. The store dedups across both paths.
- Robust provisioning gates: skip + re-arm on relock (isLocked, mirrored for
  both variants), don't fetch before the encryption state is known, and treat
  a non-mnemonic wallet (RpcError) as identity-unavailable rather than
  retrying every tick. rebuildLiteWallet re-arms so a server switch / re-open
  re-derives from the newly opened wallet.

Gated by DRAGONX_ENABLE_CHAT (default OFF) via the constexpr
hushChatFeatureEnabledAtBuild() gate, so the wiring folds away in shipping
builds. Verified: Linux + Windows(mingw) build with chat ON, ctest 100%,
hygiene clean; caches restored to the OFF default.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-06 03:33:00 -05:00
ee59a9e2cd feat(ui): promote the address book to a Contacts sidebar tab
Phase 0b. The address book was buried in Settings behind a modal dialog.
Make it a first-class "Contacts" tab (which will also become the chat
roster), rendered inline in the main content area.

- New NavPage::Contacts (after History; ICON_MD_CONTACTS) +
  WalletUiSurface::Contacts. isUiSurfaceAvailable's `default: return true`
  shows it in BOTH variants; uiSurfaceNeedsWalletData's default keeps it
  usable before wallet data loads. All the touchpoints wired: NavPageSurface,
  GetNavIconMD, the app.cpp dispatch case, the app_network.cpp tracePageName
  case, and the `contacts` i18n label.
- New src/ui/windows/contacts_tab.{h,cpp}: RenderContactsTab lifts the
  toolbar + table + add/edit modal out of address_book_dialog, rendered in a
  BeginChild scroll region (peers_tab pattern) instead of an overlay; the
  add/edit form stays a modal layered over the tab. Reuses the existing
  address_book_* i18n keys and the dialogs.address-book schema.
- Delete address_book_dialog.{h,cpp}; remove its app.cpp render pump and the
  dead App::show_address_book_. The Settings "Address Book…" button now
  navigates to the tab (setCurrentPage) instead of opening the modal, so the
  Tools & Actions grid layout is untouched.
- CMake: swap the dialog sources for contacts_tab.

Behavior-preserving move; search/sort/keyboard/contrast land in 0d. Visual
check pending the per-theme screenshot sweep.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-05 16:59:34 -05:00
62998179ec refactor(data): make AddressBook an App-owned shared instance
Phase 0a of the Contacts/Chat plan. The address book was a file-scoped
singleton (`s_address_book` + `getAddressBook()`) trapped inside
address_book_dialog.cpp, reachable only from that TU. Promote it to an
App-owned member so the upcoming Contacts tab, the Send contact picker,
and a future Chat roster all read one source of truth.

- app.h: add `data::AddressBook address_book_;` + `App::addressBook()`
  accessors, next to the other owned data models.
- app.cpp: load it once in App::init() (idempotent; missing file is fine;
  purely local, no daemon dependency).
- address_book_dialog.cpp: delete the singleton + getAddressBook(); read
  through the App* the dialog already carries (dropping the dead
  `(void)app;`). show() is static so it can't reach the App — drop its
  per-open reload (the book is authoritative for the app lifetime and
  self-saves on mutation). Drop the now-unused <memory> include.

No behavior change; groundwork for the Contacts tab.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-05 16:48:04 -05:00
658c0f355b fix(ui): validate and clamp dialog input
Tighten input handling across several dialogs so bad/edge input can't
produce a doomed request or a confusing display:

- app_security: passphrase strength meter now factors character-class
  diversity — an all-one-class string downgrades one tier so "aaaaaaaa"
  no longer scores as high as a mixed one.
- block_info: clamp the height field to the chain tip and hide/deny
  "Next" at the tip (was only yielding a raw RPC error).
- address_label: trim surrounding whitespace before saving; a
  whitespace-only label clears it (mirrors clearing the icon).
- send_tab: re-clamp the amount when a Max send has its fee bumped in the
  confirm popup (kept the total within budget) and NUL-terminate the
  strncpy'd address/memo when a payment URI overwrites the form.
- settings_page: reject an empty/whitespace-only lite wallet path before
  dispatching an unusable open/restore request.
- peers_tab: guard ExtractIP against an empty address.
- transaction_details: disable "View in explorer" when the configured
  explorer URL is empty/whitespace so we never open a garbage link.
- app: seed-backup "Skip" now requires a second, deliberate click with a
  fund-loss warning before it dismisses.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-05 15:08:59 -05:00
b0cc6bcef4 fix: Tier-2 remaining mediums — maintenance/mining feedback + force-quit hang detection
The last four (more involved) robustness items from the audit:

- deleteBlockchainData: post the deleted-item count to the main thread (via an
  atomic, since Notifications isn't thread-safe) and show a completion toast
  ("Blockchain data deleted (N items). Daemon restarting…") — previously the
  result was only logged.
- Pool mining: pool start has a connect delay with no feedback; announce
  "Starting pool miner — connecting…" on a successful start and "Pool miner
  connected and hashing." once the poll confirms it (contained pool_starting_
  flag; the shared mining-toggle state machine is untouched).
- Force Quit (shutdown screen): gate it on the status text having STALLED (a
  genuine hang) rather than a bare 10s timer — with a 20s hard-ceiling backstop —
  and show a state-aware caution naming the stuck step (force-quitting mid daemon
  flush risks the chainstate).
- Benchmark: require a confirming second click that first builds candidates to
  estimate the duration ("Benchmark takes ~Ns and interrupts mining"), instead
  of interrupting mining immediately.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-05 14:14:56 -05:00
129a8e6449 fix: Tier-2 UX robustness — action guards, in-progress guards, input trimming
The app-wide "import-key pattern" fixes (missing guards / untrimmed input / no
re-entrancy protection), from the robustness audit:

- Import-key dialog: unify the type indicator and the RPC dispatch on one
  classifier (classifyPrivateKey is now prefix-aware — an "SK..." shielded key no
  longer misroutes to the transparent RPC), trim manual input (not just Paste),
  and gate both the indicator and the Import button on a shared
  isRecognizedPrivateKey() so unrecognized/empty input can't be submitted.
- Shield: guard the submit button on connected + !syncing (like Send), with a
  disabled tooltip, instead of firing into a raw daemon error.
- Mining: disable the pool Mine button when the payout address is empty
  ("enter a payout address first"), and trim the pool URL/worker on persist.
- Console: track in-flight RPC commands (atomic counter + busy() override) so the
  input is disabled while a command runs instead of piling up on the worker.
- Maintenance (rescan/repair/delete-chain/reinstall-daemon): re-entrancy guard —
  bail with "already in progress" instead of launching a duplicate destructive op.
- Bootstrap dialog: re-attach to an in-flight download on reopen instead of
  resetting state and orphaning the running worker.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-05 13:09:23 -05:00
df14533ad3 fix: Tier-1 UX robustness — backup/export integrity, verified installs, input validation
From the app-wide robustness audit (calibrated to the import-key dialog). These
are the safety-critical fixes; several could cost users funds:

- Backup/export false success (FUND LOSS): exportAllKeys pre-seeded a header, so
  a keyless result (the usual case when the wallet is encrypted+locked) still
  looked non-empty and backupWallet wrote a private-key-less file and reported
  "Backup saved". Now exportAllKeys returns an exported count; backupWallet and
  the export-all dialog refuse to write / report success on 0 keys, disclose the
  count, flag partial results as INCOMPLETE, and the backup dialog confirms
  before overwriting an existing file (+ trims the path).
- Bootstrap: fail CLOSED — refuse to install an unverified multi-GB archive when
  no checksum is published (was `return true`), mirroring the xmrig/daemon updaters.
- Restore-from-seed: require a valid BIP39 word count (12/15/18/21/24) with a live
  "should be 24 words — you have N" hint instead of accepting any non-empty text.
- Encryption passphrase: detect leading/trailing whitespace and BLOCK with a
  warning (not a silent trim, which would change the passphrase and lock the user
  out).
- Receive payment QR: emit the canonical `drgx:` scheme with a URL-encoded memo
  via a new shared util::buildPaymentUri (the request-payment dialog now routes
  through it too, so they can't diverge); the old "dragonx:" + raw memo was
  unparseable by the wallet's own scanner.

Also clamps the receive "Recent Received" list to the 4 most recent (companion to
the recent-lists commit).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-05 12:54:05 -05:00
da56bb73a0 refactor(ui): consolidate raw ImGui buttons onto material::TactileButton
UI-standardization audit: ~66 call sites still used raw ImGui::Button /
ImGui::SmallButton instead of the app's canonical TactileButton, so those
buttons missed the standard glass fill + rim + tactile overlay (and the
light-theme flat treatment). Convert 59 plain action-button sites across the
wizard, security dialogs, settings, market/console/explorer tabs, and the
address dialogs to material::TactileButton / TactileSmallButton (drop-in:
same signature/return, args preserved verbatim). Danger buttons keep their
PushStyleColor wrappers — Tactile renders through them then overlays.

Deliberately left raw (not plain buttons): InvisibleButton hit-targets, the
frameless transparent-bg collapsible-header toggles (RPC/Debug), the icon-only
pagination chevrons, and the receive All/Z/T segmented filter — a glass rim
would clash with those intentionally borderless/segmented looks.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-04 19:00:14 -05:00
dddf1b6ec7 feat(debug): theme x tab screenshot sweep + Debug options dialog
Add a debug tool that cycles every skin across every build-enabled tab and saves a
PNG of each to a timestamped folder under the config dir (screenshots/sweep_<ts>/)
— for gathering visual context on theme-specific UI work.

- App state machine (app_network.cpp): startScreenshotSweep() builds the
  skin+enabled-page lists, saves the current skin/page, and steps through them;
  updateScreenshotSweep() (top of App::render) pins the page + settles ~4 frames
  after each skin/page change (skin switches reload TOML + reset the acrylic
  capture, so they need to settle); wantsScreenshotThisFrame()/screenshotSweepPath()
  /onScreenshotCaptured() coordinate with main.cpp. Restores the original skin/page
  when done; nothing persisted.
- Framebuffer capture (main.cpp): read the finished frame and encode PNG via the
  bundled miniz (tdefl_write_image_to_png_file_in_memory_ex). GL reads FBO 0 (RGBA,
  bottom-up -> flip); DX11 copies the backbuffer to a staging texture + maps it
  (BGRA, top-down -> channel-swap). Alpha forced opaque.
- Settings UI: move the Verbose logging checkbox off the wallet row into a new
  "Debug options" button that opens a dialog housing the verbose toggle + a "Run
  screenshot sweep" button. New i18n keys.

Both platforms build; no-crash smoke verified.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-04 17:41:49 -05:00
34ded6b441 feat(ui): shutdown-screen consistency polish (Phase 3)
The two Phase 3 "bespoke overlays" aren't content modals: the send-error is an
in-tab glass banner (already consistent — left as-is), and the shutdown screen is
a teardown screen where a blur backdrop would be semantically wrong (should read
as "closing") + risky on the teardown path. Per that finding, apply minor
consistency polish to the shutdown screen only:
- Share the overlay-scrim tone via a new material::OverlayScrimColor(opacity),
  used by both the dialog scrim and the shutdown scrim (was a slightly different
  hardcoded 0.06/0.06/0.08 → now the 0.04/0.04/0.06 dialog base).
- "Shutting Down" title → theme Warning() amber (was hardcoded gold).
- Force-Quit confirm destructive button → theme Error() (alpha-varied) instead of
  hardcoded reds.
No behavior change; the shutdown scrim stays opaque (no blur).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-04 14:07:36 -05:00
ccbb0783b5 chore(ui): remove dead dialog duplicates (Phase 2 cleanup)
Grep-verified dead, zero live callers (the live paths all go through
BeginOverlayDialog and already got the blur backdrop):
- App::renderAboutDialog (app.cpp/app.h) — dispatch uses ui::RenderAboutDialog.
- src/ui/windows/import_key_dialog.{h,cpp} (ImportKeyDialog class) — live path is
  App::renderImportKeyDialog.
- src/ui/windows/backup_wallet_dialog.{h,cpp} (BackupWalletDialog class) — live
  path is App::renderBackupDialog.
Drop the two dead files from CMakeLists. No behavior change.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-04 12:29:38 -05:00
ebf01bd858 feat(market): migrate portfolio modal onto the shared blur-overlay framework (Phase 1)
Dogfood the Phase 0 framework: RenderPortfolioEditor now opens via
material::BeginOverlayDialog(BlurFloat) + EndOverlayDialog instead of its own
inline overlay scaffold, and uses the promoted material:: SegmentedControl /
RightAlignX / BeginFadeScrollChild helpers (overlay_scroll.h). Delete the
portfolio's inline backdrop/capture-once state machine + its PfEditState capture
statics + the four local helper copies. App::render's ModalRenderGuard is now
driven by the modal-agnostic AnyBlurOverlayActiveLastFrame() (timing-equivalent),
and LatchBlurOverlayActive() at frame end handles the acrylic re-capture on close.

Framework tweak: floating cards pop the card WindowPadding after BeginChild (so
nested children don't inherit it) and center button labels, keeping the net
style-var count at 2 so EndOverlayDialog is unchanged and GlassCard stays
byte-identical. RenderPortfolioEditor: 357 -> 282 lines. Needs GUI verification
that the portfolio modal is visually/behaviourally unchanged.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-04 02:33:15 -05:00
fe18628216 feat(market): bump the portfolio backdrop blur radius (64) via sole-consumer isolation
Raising the backdrop radius alone would thrash the shared radius-keyed blur cache (the
glass panels blur at ~30). Instead, while a full-window blur overlay is active, glass
panels use their opaque fallback (IsFullWindowBlurOverlayActive) — they're covered by the
backdrop anyway — so the backdrop is the SOLE applyBlur caller and can use a strong custom
radius (64) with the cache staying coherent and frozen. The flag is toggled alongside the
theme-effect suppression by a RAII guard in App::render().

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-03 21:15:12 -05:00
5afb339ba7 fix(market): portfolio modal — hover assert, cut-off footer, plain heading, FX bleed, bigger delete icon
- Fix the ImGui error on hovering a group row: the per-row delete was an
  overlapping InvisibleButton + SetCursorScreenPos over the row Selectable. Replace
  it with a manual IsMouseHoveringRect / IsMouseClicked hit-test (no overlapping
  ImGui item, no cursor manipulation).
- Delete icon is larger (iconMed) and inset from the row edge (spacingMd).
- Footer no longer clipped: reserve 56px for the 40px buttons + separator.
- Manage-portfolio heading is now plain text (h6) with no title-bar background or
  divider — content floats on the backdrop.
- Sidebar theme-effect borders no longer bleed through the overlay: suppress panel
  theme-effects frame-wide (RAII guard in App::render via ui::PortfolioEditorActive)
  while the modal is open, instead of only during the market render.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-03 20:47:33 -05:00
c1d5f79502 feat(market): real historical sparklines via CoinGecko market_chart
Portfolio-group sparklines previously resampled the ~24-minute in-session price
buffer, so the hour/day/week/month intervals could never fill. Fetch real
historical USD series from CoinGecko /market_chart and back each interval with
appropriately-grained data.

- MarketInfo gains two timestamped series: price_chart_intraday (days=1, 5-min)
  and price_chart_daily (days=365, daily), plus a fetch timestamp.
- App::refreshMarketChart() fetches both on the RPC worker via the TLS-verifying
  util::httpGetString helper, self-throttled to ~30 min (historical data moves
  slowly). Gated by getFetchPrices(); triggered on connect and each price tick.
- Pure NetworkRefreshService::parseCoinGeckoMarketChart() parses {"prices":
  [[ms,price],...]} into (unix-seconds, price); malformed rows skipped. Unit-tested.
- market_tab pfSparklineSeries() maps interval -> series+bucket: minute = live
  buffer; hour = intraday bucketed to 1h; day/week/month = daily bucketed to
  1d/7d/30d. Falls back to the in-session buffer until the fetch populates.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-03 14:50:17 -05:00
de11850c74 refactor(audit): batch 4 — UI design-system helpers + i18n
Mechanical, behavior-preserving consolidations from the audit. Rendering
extractions were kept byte-equivalent; sites that couldn't be made identical
were left as-is (noted below).

Shared helpers:
- util::truncateMiddle(s,maxLen) / (s,front,back) in text_format.h — replaces
  6 file-local middle-ellipsis truncators + several inline substr sites
  (send/receive/transactions/balance_recent_tx/explorer + app.cpp + 3 dialogs).
  Carries the maxLen<=3 guard, fixing the latent unsigned-underflow copies.
- material::LoadingDots() in draw_helpers.h — one animated-ellipsis source for
  8 copy-pasted spinner sites (identical GetTime()*3 phase preserved).
- Reuse the existing FormatHashrate() in explorer_tab + peers_tab (dropped two
  inline hashrate ladders).
- material::DrawButtonGlassOverlay() — the glass-fill/rim/tactile overlay block
  shared by TactileButton / TactileSmallButton / schema TactileButton.
- material::CollapsibleHeader() — the invisible-button + label + chevron idiom
  (3 of 5 settings_page sites; RPC/Debug headers skipped — non-identical).
- material::GlassCardScope (RAII) — the ChannelsSplit/Indent/DrawGlassPanel/
  ChannelsMerge card scaffold (5 settings_page cards; About/send/receive skipped
  — different padding / logo interleaving).
- material::DialogWarningHeader()/DialogConfirmFooter() — warning header +
  50/50 Cancel/danger footer across the confirm dialogs; button height moved
  from a hardcoded 40px into ui.toml (components.overlay-dialog.confirm-btn-height).
- File-local enterLowSpec()/exitLowSpec() collapse the 3 low-spec snapshot copies.

i18n: wrapped hardcoded English in the shared balance render paths and the
whole Lite lifecycle/security section in TR(), with English defaults added to
loadBuiltinEnglish() (res/lang/*.json left for the translation tooling — TR
falls back to English, so output is unchanged).

Full-node + Lite build clean; ctest 1/1; hygiene clean. These are rendering
changes — the Settings page (cards/headers/confirm dialogs), all tactile
buttons, and the Lite settings section warrant a screenshot check.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-02 17:29:12 -05:00
280a71e973 refactor(audit): batch 3 — decompose App::update() + collapse acrylic fork
Three P1 structural refactors from the audit; no behavior change.

1. Extract the ~180-line daemon-stdout rescan/witness parser out of the
   ~790-line App::update() into a pure, static, unit-testable
   NetworkRefreshService::parseDaemonRescanOutput() returning a
   DaemonRescanScan result struct (sits next to the existing static parse*
   siblings). The scanning half moved verbatim; App::update() keeps the
   state-application half, now reading scan.* fields. Adds
   testParseDaemonRescanOutput() covering all six daemon signals + edge
   cases against fixture log snippets — previously untestable without a
   live daemon.

2. Extract the ~95-line global keyboard-shortcut block (Ctrl+, / theme
   cycle / F5 / low-spec / effects / gradient / wizard) out of
   App::update() into App::handleGlobalShortcuts().

3. Collapse the ImGuiAcrylic frontend fork: the GLAD and DX11 namespaces
   were ~375 lines of semantically-identical code (the frontend makes zero
   direct GL/DX calls — all backend work delegates to AcrylicMaterial, which
   has a backend per API). Extended the single frontend's guard to
   #if defined(DRAGONX_HAS_GLAD) || defined(DRAGONX_USE_DX11) and deleted the
   DX11 duplicate, leaving the no-backend stub. Kills the Windows/DX11 drift
   hazard (acrylic frontend changes now made once).

Verified: full-node + Lite build clean; ctest 1/1 (incl. the new parser
tests); source-hygiene clean; and the collapsed acrylic path was
compile-verified under DX11 via the mingw-w64 Windows cross-build
(ObsidianDragon.exe links).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-02 16:40:40 -05:00
f8150434d4 fix(audit): batch 1 — latent unlock/path/rescan correctness bugs
Four correctness fixes surfaced by the codebase audit:

1. Lite address-book path bug: AddressBook::getDefaultPath() hardcoded
   "ObsidianDragon" while Settings::getDefaultPath() uses DRAGONX_APP_NAME,
   so the Lite build wrote addressbook.json into the full-node app's config
   dir instead of ObsidianDragonLite/. Now uses DRAGONX_APP_NAME on all
   platforms (no macOS path change — the getConfigDir consolidation, which
   would move the macOS dir, is deferred).

2. PIN-unlock lockout bypass: the PIN path duplicated unlockWallet's
   success/lockout logic and its RPC-error branch bumped the attempt counter
   but skipped the escalating-lockout math entirely — a PIN user hitting an
   RPC error escaped the lockout curve. Extracted App::applyUnlockSuccess()
   and App::applyUnlockFailure() and routed all unlock paths (passphrase,
   PIN vault-fail, PIN RPC-fail) through them, so the lockout curve now
   applies uniformly. (noVault stays a mode-switch, not a failed attempt.)

3. Witness/rescan reset drift: the ~11-line rescan+witness completion reset
   was copy-pasted at four sites (app.cpp x2, app_network.cpp x2); adding a
   witness field and missing a copy would leave stale progress. Folded into
   App::resetWitnessRescanProgress(). Left the distinct phase-transition
   reset in App::update() untouched (it sets witness_phase, not 0).

4. Truncation underflow: send_tab/receive_tab's size_t TruncateAddress
   copies computed (maxLen - 3) without guarding maxLen <= 3, which wraps
   and throws std::out_of_range on short inputs. Added the guard.

Full-node + Lite build clean; ctest 1/1; source-hygiene clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-02 15:54:36 -05:00
2552234610 feat(mining): fetch pool hashrates + miner version at startup
Warm the Mining tab's data during App::init() (both off the UI thread) instead of
only when the user opens the tab or clicks a mine / auto-balance button:
- kick the idempotent one-shot `xmrig --version` detection, and
- kick a background pool-stats refresh over the known pools.

Gated on supportsPoolMining(). The pool-stats snapshot lands on a later frame and
the auto-balance driver's self-throttling still applies, so this is just an
earlier first fetch — hashrates + version are visible as soon as the tab opens.

Full-node + lite build clean; ctest green; source hygiene clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-01 21:01:35 -05:00
33735b1d52 refactor(console): phase 2 — channel-as-semantic key; decouple executor from UI colors
Make ConsoleChannel the canonical semantic classification of every console
line. Producers (command executor, app log forwarders, result formatter) tag
each line with a channel; the UI derives both the text color and the left
accent-bar color from it at draw time, and the output filter keys off it.

This replaces the prior scheme of storing an ImU32 color per line and then
reverse-engineering the source from color-equality plus a "[daemon] "/"[xmrig] "
/"[app] "/"[rpc] " text prefix. Consequences:

- ConsoleLine now stores {text, ConsoleChannel} — no per-line color.
- addLine()/addRpcTraceLine() take a channel; the redundant text prefixes are
  gone (the accent bar carries the origin).
- The executor's ConsoleAddLineFn hands a channel, not an ImU32, so the backend
  no longer depends on ConsoleTab::COLOR_* — FullNode/Lite executors emit clean
  text + channel.
- The theme-remap loop that rewrote stored colors on light/dark flip is deleted;
  colors are resolved per-channel each frame (refreshColors on flip only).
- ConsoleOutputFilter drops its color fields; consoleLinePassesFilter() keys off
  the channel. Test updated to the channel-based API.
- Added a Warning channel (amber severity peer of Error/Success) so the
  notification + logger warning forwarders keep their color.

Full-node + lite build clean; ctest green; source hygiene clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-01 18:22:35 -05:00
b5001ad4db refactor(console): unify lite + full-node console behind a pluggable executor
The two consoles were separate implementations (the rich full-node ConsoleTab and a
minimal RenderLiteConsoleTab). Introduce ConsoleCommandExecutor to abstract the only
real differences — command execution + log source — so both variants share the one rich
terminal (the lite console now gains selection, zoom, copy, and JSON-colored output).

- console_command_executor.{h,cpp}: interface + FullNodeConsoleExecutor (dragonxd log
  ingestion + RPC command execution, result queue) and LiteConsoleExecutor (lite
  diagnostics ring + backend runConsoleCommand/takeConsoleResult, connection/sync status).
- ConsoleTab::render() now takes a ConsoleCommandExecutor&; log ingestion, command
  submission, and command results flow through it. The RPC command-reference popup and
  the daemon/errors/rpc-trace/app filter toggles are gated on the executor's capabilities;
  the toolbar status dot + a status header come from the executor.
- Safety preserved: `clear`/`cls` is intercepted as a view-only clear in the shared UI so
  it is NEVER forwarded (the lite backend's `clear` wipes tx history); command output is
  not persisted to diagnostics.
- App owns the executor (created per variant) and routes both NavPages to console_tab_;
  lite_console_tab.{h,cpp} deleted.

Needs runtime verification of BOTH consoles (full-node RPC + lite: clear, seed/export
secrecy, sync status, command results).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-01 15:59:17 -05:00
57f52b7a6c feat(ui): semibold window title + "Lite" suffix on the lite build
- Draw the brand word "ObsidianDragon" in Ubuntu Medium (the closest weight the
  project ships — no true 600 face) as a stand-in for semibold; append "Lite" in the
  Regular font under DRAGONX_LITE_BUILD so the lite window reads "ObsidianDragonLite".
- Fix a misleading comment: pool mining (and thus auto-balance) is available in both
  builds; only solo mining is full-node-only.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-01 15:13:40 -05:00
5c490c8d53 feat(mining): auto-balance driver, per-pool algo, live miner version
- app: periodic weighted-random auto-balance of the pool while pool-mining
  (~30-min cadence; restarts the miner only when the pick actually changes),
  plus snapshot + refresh accessors for the UI.
- app_network: resolve the xmrig algo per pool at start (pool.dragonx.cc =
  rx/dragonx, pool.dragonx.is = rx/hush; custom hosts keep the setting).
- xmrig_manager: schema-aware pool-side hashrate readout (fixes a silent 0 for
  Miningcore pools); expose the running miner's version from its HTTP API and a
  cached `xmrig --version` detection so the UI can show it before mining.
- wallet_state: carry the running miner's version.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-01 14:35:22 -05:00
4473e7e00a feat(updater): in-app dragonxd updater + browse-all-releases
Add a full-node daemon updater (util/DaemonUpdater + daemon_download_dialog)
reachable from Settings -> NODE & SECURITY: downloads/verifies (SHA-256 +
enforced ed25519 signature) and atomically installs the latest dragonxd from
the project Gitea, with a "Restart daemon now" step. Add a shared "Browse all
releases..." picker (release_list_view) to both the miner and daemon updaters
so users can pin older/pre-release builds. Pure no-I/O cores
(daemon_updater_core / xmrig_updater_core) are unit-tested; sign-daemon-release.sh
signs release archives offline.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-27 21:27:13 -05:00
69a6fb3e64 fix(fullnode): smooth witness-cache progress from the new daemon's done/total
Use the new daemon's "Reading blocks for witness rebuild: <done> / <total>" as an
exact fraction: the reported total is the denominator directly, so the bar sweeps
0..1 smoothly instead of being held near the top by the peak-anchored remaining
heuristic (kept only as a fallback for older daemons that log bare "<n> remaining").
Also snap to 100% on the parallel rebuild's completion line ("rebuilt <n> note
witness cache(s) … using <t> thread(s)"), which otherwise logs no progress, so the
bar visibly finishes before the rescan-complete signal clears it.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-22 15:48:33 -05:00
ed6120eef9 feat(fullnode): parse the new multi-threaded daemon's witness-rebuild progress
The updated dragonxd (parallel witness rebuild) replaced the per-block
"Building Witnesses for block <h> <frac> complete, <n> remaining" log line with a
clean serial read counter "Reading blocks for witness rebuild: <done> / <total>".
Parse the new line and map it onto the existing phase-2 path (remaining = total -
done), so the witness progress bar shows done/total against this daemon. The old
"Building Witnesses" matcher is kept for backward compatibility with older daemons;
"Setting Initial Sapling Witness for tx … i of N" (phase 1) is unchanged.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-22 13:41:36 -05:00
a5bd2dadd7 fix(fullnode): make witness sub-phase upgrade-only to stop progress thrash
The initial-witness pass ("Setting Initial Sapling Witness") and the cache walk
("Building Witnesses for block … remaining") interleave during a rescan — the daemon
does both per block. The phase selector picked phase 1 whenever a batch had only
initial-pass lines, so once the cache walk started an interleaved initial line would
flip the phase back to 1 and reset the bar to 0 every batch. Make the phase
upgrade-only (once the cache walk is seen it never drops back), so the reset happens
at most twice (→1, →2) and the cache-walk percentage advances monotonically.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-22 13:09:29 -05:00
25ee1496b4 fix(fullnode): make witness/rescan progress work on the real daemon
Verified by running the app against a live node and watching a real rescan. Three
issues that only surfaced at runtime:

- Wrong RPC name: this daemon (hush/komodo) exposes the runtime rescan as
  "rescan <height>", not bitcoin's "rescanblockchain". runtimeRescan() and
  RPCClient::rescanBlockchain() used the bitcoin name and failed with "Method not
  found" on every node. Corrected to "rescan".

- Witness/rescan progress never surfaced during a rescan: the daemon-output parser
  that drives it was gated behind rpcConnected, but a heavy rescan holds cs_main so
  getinfo times out and the RPC reads disconnected — silencing the parser exactly
  when it's needed. The parser reads the daemon's stdout pipe (no RPC), so it now
  runs whenever the daemon process is alive. It also now parses INLINE on the main
  thread instead of via fast_worker_, so it can't be starved when the worker is
  blocked on a getrescaninfo call (which waits on cs_main during a witness rebuild).

- Witness rebuild has TWO sub-phases with different scales — the initial-witness
  pass ("Setting Initial Sapling Witness for tx <hash>, <i> of <N>") and the cache
  walk ("Building Witnesses for block <h> <frac> complete, <n> remaining"). Tracking
  them with one monotonic value pinned the bar at the initial pass's ~100% through
  the whole cache walk. They're now tracked as distinct phases (witness_phase) with
  their own monotonic progress and labels ("Setting witnesses" vs "Rebuilding
  witnesses"), so neither resets/bounces and the long phase shows real movement.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-22 13:00:36 -05:00
e2bc3623b6 fix(fullnode): stable overall progress for Sapling witness rebuild
The witness-rebuild bar reset repeatedly because the daemon's "Building Witnesses
for block <h> <frac> complete" line reports per-call progress: BuildWitnessCache is
re-invoked for each connected block and each call walks from its own start height to
the tip, so the fraction restarts every time. The earlier "Setting Initial Sapling
Witness for tx <i> of <m>" counter resets per call too, so neither is a usable
overall metric.

Derive a stable, monotonic percentage from the "<n> remaining" count instead: track
the largest "remaining" seen during the phase as the full span and show how far
remaining has fallen below it. The longest pass defines 0→100%; the short per-block
follow-up passes only nudge the bar near the end rather than resetting it. The
"Setting Initial" line now only marks the phase active. Per-phase tracking resets at
phase start and every rescan-completion site.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-21 12:44:01 -05:00
b32fe07cb1 feat(fullnode): show Sapling note witness-rebuild progress
The daemon's post-rescan witness rebuild ("Building Witnesses for block ...")
is a distinct, often-long phase that previously showed only as an indeterminate
"Rescanning..." with no progress. Parse the daemon's witness-build log lines and
surface a dedicated progress indicator.

- Parse "Building Witnesses for block <h> <frac> complete, <n> remaining" (and the
  earlier "Setting Initial Sapling Witness for tx ..., <i> of <m>") from daemon
  output, extracting a 0..1 fraction and remaining-block count.
- New SyncInfo fields building_witnesses / witness_progress / witness_remaining,
  cleared at every rescan-completion site (warmup-end, getrescaninfo poll, runtime
  rescan callback, daemon-log "finished").
- Status bar shows "Rebuilding witnesses NN%" (priority over the generic rescan
  text); the loading overlay (shown during -rescan warmup) gets a labelled witness
  progress bar with the remaining-block count.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-21 12:23:52 -05:00
a0532275dd feat(fullnode): auto-reconcile wallet after bootstrap; runtime rescan for pruned nodes
A wallet bootstrapped from a snapshot keeps its wallet.dat but never rescans, so
its spent-state is stale and the first send tries to spend already-spent notes and
is rejected. The startup -rescan flag can't fix it either: the snapshot lacks the
pre-snapshot block history -rescan needs, so it errors. The working fix is a runtime
rescanblockchain RPC from a height the snapshot actually has.

- Add App::runtimeRescan(startHeight): runs rescanblockchain via the worker, drives
  the rescanning UI state, and owns completion via the RPC callback (getrescaninfo
  is unavailable on this daemon). Suppresses the per-second mining/rescan pollers
  and the Core/balance/tx refreshes while the daemon holds cs_main for the scan.
- Add App::detectLowestAvailableBlockHeight(): async binary search via getblock for
  the lowest height whose block data is on disk → the snapshot base, and whether the
  node still has full history.
- Auto-reconcile after bootstrap: both completion sites (wizard + Settings download
  dialog) mark a pending rescan; once the daemon is back up and the tip is known,
  detect the base and runtimeRescan() from it (or -rescan restart on a full node).
- Settings "Rescan Blockchain" now probes first: full-history nodes get the existing
  -rescan restart; bootstrapped/pruned nodes get a prompt pre-filled with the
  detected base height that runs the runtime rescan.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-21 11:48:30 -05:00
b2e104358d feat(fullnode): manage the daemon binary in Settings; stop auto-overwriting it
Previously the wallet re-extracted the bundled dragonxd on startup whenever the
installed binary's size differed from the bundle ("stale" overwrite), which could
replace a node a user had deliberately placed in dragonx/.

Now dragonx binaries (dragonxd/cli/tx) are auto-placed ONLY when missing — never
auto-overwritten on a size mismatch (needsParamsExtraction + extractEmbeddedResources).
Params/asmap keep their size-based refresh; a daemon dropped next to the wallet exe
still takes priority and is never touched.

Replacing the daemon is now an explicit action: Settings → "Daemon binary" reports the
installed binary's version (scanned from the file), size and modified date, compares it
to the version bundled in this build, and offers an "Install bundled daemon" button.
That stops the node, overwrites dragonxd/cli/tx with the bundled copies (waiting for the
process to release the file lock), and restarts — wallet/keys/chain data untouched.

Adds resources::{getInstalledDaemonInfo,getBundledDaemonInfo,reextractBundledDaemon}
(+ a version-string scanner) and App::reinstallBundledDaemon().

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-18 21:06:59 -05:00
de70e68472 fix(fullnode): work around daemon note-selection fee-gap on shielded sends
dragonxd's z_sendmany picks notes to cover the recipient total (nTotalOut) but not
the miner fee, then rejects the build unless the selected notes cover amount+fee
(rpcwallet.cpp:5312 vs asyncrpcoperation_sendmany.cpp:278). So a shielded send whose
largest notes sum exactly to the amount fails with "Insufficient shielded funds,
have H, need H+fee" despite ample balance — e.g. sending exactly 2.0 from an address
whose biggest note is 2.0.

Since the failure is async (reported via the opid poll), detect it there: when a
shielded send fails with that message and the selected total H >= the requested
amount (selection covered the amount but stopped one note short of the fee — vs a
genuine shortfall where H < amount), re-issue the send once with a tiny self-output
(= fee) back to the from-address. That lifts the daemon's selection target past the
boundary so it grabs another note and can cover the fee; the recipient still receives
the exact amount. Retries are tracked so a second failure surfaces normally (no loop).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-17 14:27:52 -05:00
0fe12d65df fix(ui): drop duplicate daemon log on the startup overlay
renderLoadingOverlay() rendered the daemon's recent output twice during startup:
a bare 4-line centered tail (section 2c, init/warmup only) and the styled
terminal-style box (section 4, always shown when the embedded daemon exists).
The bare tail was a strict subset of the box, so the same dragonxd output showed
stacked twice. Remove the redundant bare tail; keep the terminal box (which also
matches the shutdown screen's panel).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-17 07:48:53 -05:00
37c8287a12 feat(fullnode): add "Repair Wallet" (-zapwallettxes=2) to Settings
When a note's stored record is corrupt or its tx isn't in the canonical chain,
z_sendmany fails to build a valid sapling spend proof even after a full -rescan,
because a plain rescan replays witnesses but keeps the existing tx/note records.
The zcashd repair for this is -zapwallettxes=2, which deletes all wallet tx/note
records and rebuilds them from the chain (keys/addresses preserved).

Adds a RepairWallet lifecycle operation that mirrors the existing -rescan plumbing
(one-shot zapOnNextStart flag on the embedded daemon; -zapwallettxes=2 implies and
supersedes -rescan), an App::repairWallet() that reuses the rescan status UI (so the
status bar + warmup-end completion detection apply), and a confirmed "Repair Wallet"
button + dialog in Settings → node maintenance (embedded daemon only).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-17 07:42:10 -05:00
6ff80354df fix(fullnode): reliable rescan completion + self-explaining shielded send errors
Two related fixes for the post-bootstrap "send fails / rescan stuck at 99%" trap:

1) Rescan completion now keys off warmup-end. A -rescan runs entirely inside daemon
   warmup (every RPC returns -28 until it finishes), so warmup completing IS the rescan
   completing. The old detectors relied on getrescaninfo (which some daemons answer with
   "Method not found") or a "Done rescanning"/bench log line the daemon may never print,
   leaving the status bar stuck at 99% — so users killed the rescan before it finished.
   When warmup ends and a rescan was confirmed active, clear the rescan state, flip to
   100%, refresh history/balance, and toast completion.

2) z_sendmany failures that mean stale shielded note data (shielded-requirements-not-met,
   missing sapling anchor, invalid sapling spend proof, bad-txns-sapling-*) now append a
   plain-language hint telling the user to run a full rescan, instead of surfacing only the
   raw daemon string.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-16 22:43:24 -05:00
0e2c786ebf fix(lite): welcome "Restore from seed" now prompts for the seed inline
On first run the lite welcome screen's "Restore from seed" button only showed a
hint toast and bounced the user to Settings, dismissing the welcome with no
wallet open — it never prompted for a seed. Add a real restore step to the
welcome wizard: a seed-phrase field + optional birthday height, which calls
beginRestoreWalletAsync() (same server failover as create/open), shows
"Restoring…" progress, then completes (wallet syncs) or surfaces the error to
retry. The seed buffer is wiped on success/Back and in finish().

(The Settings -> Lite -> Restore path already prompted for a seed; this fixes
the first-run welcome path.)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-16 02:11:19 -05:00
b3251e9244 feat(lite): mirror errors into the lite Console (copyable), not just toasts
Lite send/shield, unlock, and key-import failures were shown only as transient
toasts — impossible to copy. Route them through liteLog() so they also appear in
the lite Console (which has a Copy button), alongside the lifecycle/open/sync
errors the controller already logs:
- send/shield broadcast failures (App broadcast-result delivery)
- wallet unlock failure
- key import failure (controller; logs the error text only, never the key)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-15 22:59:34 -05:00
f0867084f3 fix(ui): stop spurious "failed to read" logo errors in the portable build
The header and coin logos load disk-first (for dev builds / theme drop-ins) and
fall back to the copies embedded in the exe. The portable single-file build has
no res/img/ folder beside it, so the disk read always failed and logged
"LoadTextureFromFile: failed to read ..." before the (successful) embedded
fallback. Guard each disk load with std::filesystem::exists() so the missing
file is skipped silently and we go straight to the embedded logo — no error
line, logos unchanged.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-13 12:58:19 -05:00
88851f5eea fix(history): unstick the unconfirmed-tx badge on confirmed shields
The History badge counts transactions with confirmations==0, iterating the raw
transaction list. Autoshield transactions have two legs sharing one txid, and
the send leg parsed from z_viewtransaction carries confirmations=0 even when the
transaction is long confirmed (the receive leg holds the real count). So the
badge counted those stale legs and stuck at a non-zero number (e.g. 7) with no
pending transactions.

Treat a txid with ANY confirmed leg as confirmed, and count UNIQUE unconfirmed
txids rather than legs — so confirmed multi-leg transactions don't inflate the
badge and genuinely pending ones still count once each.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-13 10:06:46 -05:00
2b70ee5cd8 feat(history): show "Loading older history (N%)" during the initial bulk load
History streams in over many refresh cycles (the incremental shielded scan
walks every z-address), so the first batch appears long before the list is
complete — with no indication more is still coming. The existing loading banner
deliberately goes quiet once any rows are on screen.

Track whether the first full shielded scan has finished
(initial_history_scan_complete_) and, until it has, surface a progress percentage
(fraction of z-addresses scanned) in transactionRefreshProgressText() — which the
History tab already renders as its pulsing loading indicator. Goes quiet once the
first scan completes; routine per-block re-scans don't re-trigger it. Reset on a
full history invalidation (rescan / session reset) so it shows again on reload.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-13 09:05:37 -05:00
3a597482da fix(rescan): detect this daemon's completion ("rescan <N>ms"), unstick 99%
A rescan ran to completion but the status bar stayed at "Rescanning 99%"
forever. The daemon-output parser only treated "Done rescanning"/"Rescan
complete" as finished, but this daemon prints neither — it logs the rescan
benchmark timing line exactly when the scan ends:

    2026-... rescan             16760577ms

then resumes normal block processing. So the parser saw the last
"Still rescanning ... Progress=0.99" and never the finish, leaving it stuck.

- Recognise the " rescan <N>ms" bench line as completion (it ends in "ms",
  which the "Still rescanning"/"Rescanning..." progress lines never do).
- When the parser reads "Still rescanning" straight from the daemon log, mark
  rescan_confirmed_active_ — hard proof the scan is running that doesn't depend
  on catching a getrescaninfo warmup error, so the RPC completion path can also
  fire after the daemon leaves warmup. Clear it on finish.

The parser reads the daemon's debug.log via the controller (not RPC), so this
completes the rescan UI even if the RPC connection hasn't re-established yet.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-13 08:16:25 -05:00
b9b2d469d4 fix(rescan): stop the per-second getmininginfo error flood during rescan
While the daemon processes -rescan it sits in RPC warmup and rejects every call
with -28 ("Rescanning..."). The balance/tx/address refreshes already skip warmup
(state_.warming_up), but the 1-second mining poll didn't — so getmininginfo fired
the whole rescan and flooded the log with "getMiningInfo error: Rescanning..."
(~680 entries in one capture).

Gate refreshMiningInfo() on !state_.warming_up like the other refreshes. The
getrescaninfo progress poll still runs (it's how the warmup/rescan is tracked).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-12 18:47:01 -05:00
25fef8ff4d fix(rescan): stop the instant false "rescan complete"; show live status
Clicking Settings → Rescan restarted the daemon with -rescan correctly, but the
progress poll fired "Blockchain rescan complete" the instant it was clicked,
then showed nothing for the entire (multi-hour) rescan — so it looked broken.

Cause: the very first getrescaninfo poll runs before the daemon has restarted
and hits the still-running pre-restart daemon, which answers rescanning=false.
The completion branch took that as "done", cleared the rescanning flag, and the
real rescan then ran invisibly. (Confirmed from a Windows debug-log capture: an
instant OK{"rescanning":false}, then ~6400 warmup errors over ~5h, all swallowed.)

Fixes:
- Gate completion on a new rescan_confirmed_active_ flag that's only set once we
  actually observe the rescan running, so a pre-restart rescanning=false can't be
  misread as completion.
- While the daemon is in -rescan RPC warmup it rejects every call with the live
  phase as the message ("Loading block index..." -> "Rescanning..."). Treat that
  as proof-of-progress: surface it as rescan_status and mark confirmed-active,
  instead of silently swallowing it. The status bar keeps its animated
  "Rescanning..." for the whole run, then reports complete when warmup ends.
- Read rescan_progress whether the daemon returns it as a string or a number
  (the get<std::string>() would have thrown on a numeric field).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-12 17:34:44 -05:00
bc788d008e fix(send): poll z_getoperationstatus without the per-opid filter
The opid poll called z_getoperationstatus(["opid"]) to check a specific
operation, but this daemon rejects the filtered form with "JSON value is not a
number/array as expected" (a UniValue error returned as an RPC error). The
poll's catch swallowed it, so every completed send stayed stuck on "Waiting
for operation" forever — confirmed via a Windows debug-log capture showing the
throw on every 2s cycle. The no-arg form works (verified in the console).

Call z_getoperationstatus with no arguments (returns ALL operations) and filter
to the opids we're tracking in parseOperationStatusPoll(). The parser now skips
any operation whose id isn't in the requested set, so unrelated/old operations
can't fire a spurious error toast or pollute send state. The stale-opid logic
is unchanged (the no-arg form still reports in-progress ops, so a genuinely
pending opid is never misread as stale).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-12 11:31:50 -05:00
9ee8f9a43b fix(send): restart the fast-lane worker on reconnect so the opid poll runs
A completed send could spin forever on "Waiting for operation (N)". Root
cause: onDisconnected() stopped fast_worker_ but kept the unique_ptr, so
onConnected()'s `if (!fast_worker_)` guard never restarted it — after the
first reconnect (daemon warmup, restart, any RPC blip) the fast lane stayed
dead for the whole session.

The opid poll was the only fast_worker_ user that posted to it directly with
no fallback, so it alone broke: its post() landed on a stopped thread, the
result MainCb never ran, opid_poll_in_progress_ stuck true, and the poll never
fired again — leaving the operation (already "success" on the daemon, with a
txid) untracked.

Two fixes:
- onDisconnected() now reset()s fast_worker_ after stop(), so onConnected
  recreates and starts a fresh one (restores the fast lane for all its users,
  not just the poll).
- the opid poll now falls back to worker_ when the fast lane isn't running,
  matching every other fast_worker_ call site — defense in depth.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-12 01:53:30 -05:00