Commit Graph
30 Commits
Author SHA1 Message Date
DanSandClaude Opus 5.5 575abd4bcc perf(ui): render frames on demand instead of continuously
While theme effects were on (the default) the main loop drew every
frame it could. Where vsync is off (offscreen, some drivers, remote
sessions) an idle wallet spun at ~3,000 fps, 88% of a core; with vsync
it still drew every refresh. Frames are now paced by what needs them:

- full rate while there is input, a held key, a window appearing or
  settling, a smooth scroll or the UI sweep;
- ambient (30 fps focused, 10 fps unfocused) for theme effects, the
  acrylic backdrop, avatar animations and notifications;
- progress (10 fps focused, 4 fps unfocused) for UI that changes
  without input. The updater and bootstrap downloads, console
  commands, lite-wallet operations, seed migration, the loading
  overlay and the lock-screen countdowns ask for it with
  material::RequestProgressFrame();
- otherwise idle, as before: wait for events, with a housekeeping wake
  every 2 s (5 s unfocused).

A software frame cap (0.9x the display refresh) bounds the loop where
vsync is off, and hidden, occluded or minimized windows get no ambient
frames. Reduce Motion freezes the theme-effect clock
(ThemeEffects::setMotionFrozen), so it needs no ambient frames either.

The displayed-balance roll goes through one clamped StepDisplayLerp
(balance_lerp.h). An idle-length frame pushed dt*speed past 1, so the
roll overshot, and under Reduce Motion's speed of 999 it diverged to
inf/NaN. The classic layout had its own copy of the old code.

Idle CPU, Lite build, offscreen (no vsync): 88% -> 5% of a core with
effects on, and 91% -> 3% with effects saved off (with the previous
commit).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 19:52:09 -05:00
DanSandClaude Opus 5.5 6b14098bb2 fix(daemon): make the "keep the node running?" prompt actually appear
Quitting during a witness rebuild is supposed to ask before stopping the
node, because a stop throws the rebuild away and a killed node replays
and redoes it. The guard almost never fired:
- it returned false for any daemon >= v1.3.0, on the false premise that
  v1.3.0 checkpoints a rebuild (it does not; nothing survives a kill)
- it read a 16 KB debug.log tail and compared the last progress line with
  the newest line, so any unrelated line during the rebuild's silent
  second phase turned it off; and on POSIX that file is stale
- daemon_version, which the bypass needed, is zeroed when a busy node
  drops the connection

Now a DaemonBusyTracker (src/daemon/daemon_busy_detect.h, pure) follows
dragonxd's own markers in order: rebuilding (Phase-1 / legacy markers,
ended by the completion line, an abort line or the next UpdateTip) and
replaying (Activating best chain ... Done Loading), reset by a new banner
or "Shutdown: done". It is fed from the owned output on the UI thread,
including the backlog skipped after a restore. The quit guard prompts
when the quit would stop the node and it is rebuilding, or has been
alive but only timing out / refusing as busy for >= 30 s. A replay alone
never prompts: stopping it is safe. The stop loop uses the same signal
to never offer Kill during a rebuild.

Also:
- daemon output offsets are absolute, so a reader that falls behind a
  trim of the 1 MB buffer no longer skips new output (which could hide a
  start or end marker)
- a quit deferred while minimized restores and raises the window, and
  the wizard renders the prompt, so it is never waiting unseen

Verified live with the fake daemon: quitting mid-rebuild now shows the
prompt for a v1.3.0 node and leaves the node alone; a silent busy node
prompts after 30 s.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 00:02:27 -05:00
DanSandClaude Opus 5.5 5993488176 feat(daemon): deliver stop reliably and never kill the node unasked
Closing the wallet sent "stop" at most twice, gated on a getinfo probe
that needs the daemon's cs_main, then killed the node after 20 s (Windows)
or 30 s (POSIX) whether or not stop had arrived. A node busy with an
hour-long witness rebuild therefore never got stop, was killed mid-
rebuild, and replayed everything since its last flush on every start.

Delivery (src/daemon/stop_delivery.h, RPCClient::open/trySendStop):
- stop goes over a dedicated client with no probe, so it never waits on
  rpc_'s lock or a cs_main-bound getinfo
- every attempt is classified, and StopDeliveryPolicy decides the next:
  Accepted / 503 = delivered; Queued (reply timeout) is resent at most
  every 30 s; QueueFull / not listening are retried every 1.5 s; a
  refusal after we were heard means the node is shutting down; a 401 on
  our own node tries the next credential source
- POSIX: a group SIGTERM goes out as soon as an attempt fails, which
  bypasses the HTTP queue entirely

Waiting (DaemonController::executeDaemonStop, pure, virtual-clock
tested):
- never kills on its own. Quitting waits as long as the node needs; the
  shutdown screen offers "Close wallet — leave node running" after 3 s,
  and "Kill node (unsafe)" (confirmed) only once stop was delivered and
  the node showed no CPU/I-O progress for ~10 min outside a rebuild
- other callers (restart, rescan, reinstall, switch, ...) wait up to
  120 s, let go once the app starts quitting, and keep watching a node
  that is still running; wallet switch refuses to go ahead then
- EmbeddedDaemon gains stop-session primitives (beginStop,
  pollOwnedProcess, signalGracefulStop, forceKillOwnedProcess,
  ownedResourceUsage over the Linux process group / Windows handle,
  endStop) and only one stop session runs at a time
- the Force Quit button and its stall heuristic are gone; "Leave
  running" aborts a pending stop attempt and closes at once

Verified against the fake daemon with the real GUI: closing mid-rebuild
now delivers stop (or SIGTERM 2 ms after a queue-full refusal) and the
node exits cleanly after its rebuild, where it was SIGKILLed before. The
shutdown screen, its "Leave running" button and the idle Kill offer
(after ~11 min) were checked by screenshot. New strings are English-
only for now.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-01 22:50:11 -05:00
DanSandClaude Opus 4.8 0343d48c13 feat(ui): keep the wallet syncing while minimized
Previously the main loop just did SDL_Delay+continue when minimized, skipping app.update() —
so the wallet stopped draining RPC results, ticking the refresh scheduler, and reconnecting
until it was restored (and a large daemon-output backlog piled up, which is what produced the
spurious "rescan complete" toast on restore).

Now app.update() runs while minimized (it only reads GetIO/GetTime/IsAnyItemActive, all valid
outside a NewFrame) with a real-clock DeltaTime, skipping only the ImGui frame + GPU present,
throttled to ~5 Hz so CPU stays near-idle. Also clamp io.DeltaTime at the top of App::update()
so a long minimize (or machine sleep) can't report a huge delta and fire every refresh/animation
timer at once on the next update. No backlog now builds, so skipDaemonOutputBacklog becomes a
harmless no-op.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-09-01 01:09:50 -05:00
DanSandClaude Opus 4.8 a3892c0fd3 fix(ui): stop spurious "Blockchain rescan complete" toast after restoring from minimize
While minimized the main loop skips the frame body (SDL_Delay+continue), so App::update() —
which drains the daemon output via outputSince(daemon_output_offset_) — never runs and the
offset isn't advanced. On restore the whole accumulated backlog is parsed in one batch: a
background witness rebuild's progress lines (parsed as a rescan → state_.sync.rescanning=true)
AND its "rebuilt … in Xms" completion (parsed as finished) arrive together and fire
"Blockchain rescan complete" for a scan the user never initiated.

On WINDOW_RESTORED, discard the daemon-output backlog (advance the offset to the current end,
via App::skipDaemonOutputBacklog) before the resumed update parses it — so only new output is
parsed. Genuine user-initiated rescans still surface completion via the getrescaninfo monitor.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-09-01 00:49:06 -05:00
DanSandClaude Opus 4.8 6ee81a5abe fix: security-audit remediation (15 findings), empty-wallet warning, and send/chat/console/shutdown UX
Security audit remediation (15 confirmed findings from the codebase audit):
- H-02: scrub+delete the decrypt-flow plaintext key export on ALL exit paths
  (RAII guard) and purge stale obsidiandecryptexport* files at startup.
- M-01/L-03/L-04/L-05/L-07: sodium_memzero the Set-PIN and encrypt-PIN worker
  passphrase/PIN copies, the RPC Basic-auth string (auth_), the exported/imported
  key buffers (App::wipeSecrets, called from ~App and before main's _Exit), and
  the first-run wizard "Skip" buffers.
- M-03/M-04/M-05/L-06: return locked COPIES from XmrigManager/EmbeddedDaemon
  getters (dedicated error_mutex_; DaemonController::lastError now by value),
  route xmrig last_error_ writes through a locked setter, and wrap
  shutdown_status_/wizard_stop_status_ in a locking GuardedStatus
  (wizard_stopping_external_ -> std::atomic).
- M-02: persist after a console send/shield/import in the lite backend.
- L-01: require the confirm click for z_shieldcoinbase/z_mergetoaddress.
- L-02: quote/escape each Windows daemon argv per the MSDN CommandLineToArgvW rules.
- L-08: pin json/tomlplusplus/libwebp FetchContent to immutable commit SHAs.
- I-01: extract updater archives from the already-verified in-memory buffer
  (no disk re-read TOCTOU).

Feature: warn once (full-node) when the active wallet loads empty while a sibling
wallet file in the datadir holds keys. A funded salvage wallet.<ts>.bak routes to
the recovery/Restore flow; a funded sibling .dat routes to the wallet manager.
Per-wallet-file dismissal; gated on synced + address-list-loaded to avoid false
positives on warm reconnect / spent-down wallets.

UX fixes:
- send: show the TOTAL balance (with a spendable "available" note) in the source
  dropdown and keep pending-change addresses visible.
- chat: insert emoji at the cursor position; restrict new-chat recipients to
  shielded (z) addresses.
- console: optional auto-focus of the command input on tab open (off by default).
- shutdown: when "stop external daemon" is on, keep the shutdown screen up until
  the external node actually exits, showing live status.

Adversarially reviewed; verified across full-node, lite, and Windows builds; tests pass.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-08-18 13:18:49 -05:00
DanSandClaude Opus 4.8 207f9074db feat(diagnostics): make the logging + crash infrastructure actually work (W7-2, W7-3, W7-4)
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>
2026-08-02 15:31:23 -05:00
DanSandClaude Opus 4.8 2675b8ab93 fix(startup): surface filesystem failures and verify Sapling param integrity
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>
2026-08-02 11:07:32 -05:00
DanSandClaude Opus 4.8 c291e8a587 fix(app): use #if not #ifdef for the per-variant instance lock
DRAGONX_LITE_BUILD is ALWAYS defined (0 for the full node, 1 for Lite) via
$<BOOL:...>, so the previous #ifdef was true for BOTH variants — the full node
took the Lite branch and grabbed the "obsidiandragonlite" lock, so launching Lite
next still collided ("ObsidianDragonLite already running"). Switch to
#if DRAGONX_LITE_BUILD (value check), matching how the rest of the codebase
guards this macro.

Verified: full-node binary now contains only "obsidiandragon"; preprocessor check
confirms LITE=1 selects "obsidiandragonlite". (wallet_capabilities.h's #ifndef is
a separate, correct default-definition idiom — not affected.)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-16 19:08:41 -05:00
DanSandClaude Opus 4.8 06e55d6394 feat(app): per-variant single-instance lock so full node + Lite can run together
The single-instance lock hardcoded the name "obsidiandragon" for both variants,
so launching ObsidianDragonLite while ObsidianDragon was running (or vice-versa)
was refused with "Another instance is already running". Nothing else actually
required them to be exclusive — DRAGONX_APP_NAME already gives each variant its
own config dir (settings / wallets index / address book / chat db), the full
node's daemon lives under ~/.hush/DRAGONX with no Lite counterpart, and the lock
guards no cross-instance IPC (the payment URI is handled locally).

Key the lock per variant (obsidiandragon / obsidiandragonlite) — the Windows
named mutex already derives from the same name, so it's fixed on both platforms.
Each variant still enforces a single instance of itself. Also make the
already-running message report the actual variant (DRAGONX_APP_NAME) and use
MessageBoxA so it can. mingw-verified.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-16 18:42:10 -05:00
DanSandClaude Opus 4.8 88091bd77c feat(wallet): live progress modal for wallet switching
The "Stop the running node?" confirm modal now stays open through the entire
switch — stop → wait-for-exit → start → reconnect — showing a live phase, and
auto-closes the moment the new node connects. This turns the up-to-a-minute
graceful-shutdown wait from an apparent freeze into visible progress.

- WalletSwitchPhase (Stopping/Starting/Reconnecting/Failed) + atomic phase and
  dialog-open flags; the worker advances the phase, onConnected closes the modal,
  and a failed switch shows the accurate reason with a Close button.
- Owned switches (no confirm) also show the progress modal directly.
- "Continue in background" escape hatch so a long rescan / a hung startup never
  traps the user (the switch keeps running; a toast reports the result).
- isWalletSwitchInProgress() keeps the frame loop redrawing so the phase text and
  spinner animate while otherwise idle.
- i18n (EN + 8 languages, additive) + a modal-switch-progress sweep surface.

State machine adversarially verified 6/6 (thread-safety, no stuck modal,
confirm→progress transition, owned/unowned, no phase leak, redraw scoping).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-15 20:56:53 -05:00
DanSandClaude Opus 4.8 4b8d80b2fc feat(contacts): animated avatars (GIF/WebP) with a Settings toggle
Animate contact avatars end to end:

- texture_loader gains LoadAnimatedRGBA: decodes an image into a downscaled
  RGBA frame sequence + per-frame durations — animated GIF via stb
  (stbi_load_gif_from_memory) and animated WebP via libwebp's WebPAnimDecoder;
  stills (and APNG, which stb reads as one image) return a single frame. Frames
  are box-downscaled (smaller cap for animations) to bound VRAM, capped at 300.
- The contacts avatar cache now holds a frame sequence; currentAvatarFrame()
  advances animated avatars by the ImGui clock and is used everywhere avatars
  draw (list, cards, table, grid, preview). When a live animated frame is drawn
  it flags the render loop (ConsumeContactsAvatarAnimation, clear-on-read) so
  main.cpp keeps producing frames while animation plays and idles when it stops
  or the contacts view is hidden.
- New animate_avatars setting (default on) + a Settings appearance toggle
  ("Animate avatars"); off shows the first frame only. currentAvatarFrame
  honors it. +i18n (8 langs, CJK subset rebuilt for 帧/播/첫).

Verified: a 3-frame GIF and a 3-frame animated WebP both decode to 3 frames
with correct 120ms delays through the exact libwebp/stb calls used here.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-15 11:11:06 -05:00
DanSandClaude Opus 4.8 a80feb708e fix(window): resize the borderless HWND directly so DPI-move sizes are exact
The hardware [WINLOG] trace showed the DPI-transition target computed correctly
(min height 720) but the window stuck ~5% larger (759), with every deferred retry
failing to shrink it. Root cause: the DX11 window is a borderless native HWND
(WS_CAPTION/WS_THICKFRAME, for native move/resize) wrapped by SDL. SDL_SetWindowSize
converts the requested CLIENT size to a window rect via AdjustWindowRectEx (adding
the caption+border frame), but our WM_NCCALCSIZE handler folds that whole frame
back into the client area — so every SDL resize inflates the window by ~one frame
(~39px height, ~16px width) and the 720 minimum becomes an unreachable 759. That,
not the earlier min-ordering/settle logic, is why the height wouldn't stay at min.

resizeWindowExact() resizes the native HWND directly via SetWindowPos on the DX11
path (window rect == client for a borderless window → exact requested size); other
platforms keep SDL_SetWindowSize. Used for the DPI-transition resize and the
deferred retry, so the window now lands on the true minimum in both directions.

Keeps the [WINLOG] native-rect + min-track tracing so the next capture confirms the
fix (or surfaces any remaining gap).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-10 01:08:58 -05:00
DanSandClaude Opus 4.8 010465f835 chore(window): extend [WINLOG] with native-rect + min-track tracing
The first hardware capture showed handleDisplayScaleChange computing the correct
target (e.g. 1353x720, height = min) but the window never reaching it across all
30 deferred retries — stuck ~5% above target in BOTH DPI directions (759 vs 720
at 100%, 1136 vs 1080 at 150%). That rules out the earlier min-ordering / settle
logic and points at either the borderless WM_GETMINMAXINFO min-track
(720 * g_borderlessDpi) or a native-HWND non-client frame discrepancy on the
externally-created, SDL-wrapped DirectComposition window.

Add the data to disambiguate:
- winlogState now also logs the native GetWindowRect vs GetClientRect (+ pos),
  exposing any window-vs-client size mismatch that SDL_GetWindowSize hides.
- WM_GETMINMAXINFO logs g_borderlessDpi and the resulting min-track (once per
  change) so we can see the actual OS-enforced minimum during the transition.
- Forward-declared winlog so the WndProc can use it.

Windows-only, no behavior change. Grep the log for "[WINLOG]".

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-10 01:05:49 -05:00
DanSandClaude Opus 4.8 eb08b81706 chore(window): add [WINLOG] window-dimension tracing for the multi-monitor bug
Always-on stderr tracing (visible in release builds via the Windows
dragonx-debug.log redirect, unlike DRAGONX_DEBUG-gated DEBUG_LOGF) to capture
exactly how the window size / display-scale / minimum change when dragging
between monitors of different DPI. Logs:
- every WINDOW_RESIZED: size, actual vs stored scale, dpiPending/settle/retries,
  and the DPI-vs-user classification (IGNORED/RECORDED);
- every DISPLAY_SCALE_CHANGED + the full handleDisplayScaleChange trace
  (saved-under-old-scale, restore/proportional, min-clamp, display-clamp, the
  SetWindowMinimumSize+SetWindowSize, and the resulting window state);
- each deferred-resize retry;
- monitor crossings (WINDOW_MOVED, throttled to display changes);
- startup and shutdown size.

Low volume — only fires on window events. Temporary diagnostic; remove once the
multi-monitor sizing bug is fixed. Grep the log for "[WINLOG]".

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-10 00:49:59 -05:00
DanSandClaude Opus 4.8 8cbd0cab9d fix(window): preserve the exact minimum height across monitor DPI moves
Moving the window between monitors of different scaling (150% -> 100% -> 150%)
left the height off the minimum in BOTH directions, forcing a manual resize each
time. A deep trace against the vendored SDL3 source found two distinct causes:

- Shrink leg (higher -> lower scale): handleDisplayScaleChange called
  SDL_SetWindowSize BEFORE SDL_SetWindowMinimumSize. SDL clamps a size request up
  to the current minimum (SDL_video.c:3184-3191), so the smaller target was
  clamped back to the stale larger min; SetWindowMinimumSize only grows a window,
  never shrinks it, so lowering the min afterwards didn't pull it back. The
  deferred retry masked it only on rendered frames, so it leaked when the app was
  idle. Fix: set the new minimum BEFORE resizing.

- Return leg (lower -> higher scale): the single-shot dpiResizePending flag let a
  later settling WINDOW_RESIZED in the same burst be misclassified as a user
  resize and recorded, corrupting savedSizeForScale for that scale so the
  remembered min was lost. Fix: a frame-counted settle window (dpiSettleFrames)
  suppresses resize-recording for the whole transition burst regardless of event
  ordering; also clamp any restored/scaled size up to the new scale's minimum.

Only user resizes outside the ~40-frame settle burst are recorded, so normal
resizing is unaffected. SDL clamp semantics verified in-repo; build clean. The
transition needs real multi-monitor hardware to exercise end-to-end.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-09 23:58:05 -05:00
DanSandClaude Opus 4.8 a9f0d544cc fix(window): clamp the restored window size to the display on every startup
The only startup clamp to the display's usable bounds lived inside the
`currentDpiScale > 1.01f` HiDPI branch, so on a standard-DPI (100%) cold start it
never ran. A window size saved on a larger or since-disconnected monitor then
opened oversized / partly off-screen (title bar under the taskbar), since the
window is also re-centered at the saved size.

Hoist the clamp-to-usable-bounds + re-center out of the HiDPI branch so it runs
on every startup; the DPI multiply and scaled minimum-size stay HiDPI-only. It
resizes only when the clamp actually changes the size, so a window that fits its
display is left untouched — only genuinely-oversized ones clamp. Also log when a
persisted size is rejected on load/save (below the 1024x720 floor) so "my window
keeps resetting" reports become traceable.

Root-caused via a multi-agent audit of the window-size/DPI logic, which also
confirmed the save/restore DPI round-trip is otherwise correct (the restore-side
auto-scale multiply was mistaken by the investigators for a unit bug). Verified:
a 9999x9999 saved size now clamps to the display at scale 1.00 (previously the
clamp was skipped on a same-DPI cold start).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-09 23:23:39 -05:00
DanSandClaude Opus 4.8 a86cb8f0f3 feat(debug): full UI screenshot sweep — captures modals/dialogs/flows/states
The existing sweep only captures tabs. This adds a "Full UI sweep" (DEBUG
OPTIONS) that also drives every normally-hidden surface into view and captures
it under every skin, so all the UI a normal sweep misses can be reviewed at
once.

- The capture unit becomes a "surface" (SweepTarget: a base tab, optionally
  with a setup()/teardown() that forces a modal / multi-step flow / state
  overlay on top). Tabs are surfaces with a null setup. The existing tab sweep
  is folded into the same state machine (one code path). All sweep logic +
  the catalog + demo data move into a new src/app_sweep.cpp.
- Runs OFFLINE against injected demo data (installDemoWalletData snapshots then
  restores the real state_ — WalletState isn't copy-assignable, so a targeted
  field snapshot is used) and fires NO live op: capture_mode_ guards the
  seed-backup RPC, auto-lock, and the migration pumps; the async-firing dialogs
  are neutralized by pre-setting their state (fetch_started_ + demo phrase;
  seed_migration_step_ set directly). Verified: 0 RPC/secret/send calls.
- Catalog (v1): tabs + the bool-flag modals (import/export key, backup,
  seed-backup, about, settings) + the 6 migrate-to-seed steps + 4 wizard steps
  + lock/warmup/not-ready overlays + the send-confirm popup (via a new
  ui::SweepShowSendConfirm debug hook, since its state is send_tab-static).
- Overlay/blur surfaces settle 8 frames (blur backdrop freeze); setup re-runs
  each frame (OpenPopup popups must re-fire). updateScreenshotSweep now runs
  before the first-run-wizard early-return in render() so the sweep advances
  while a wizard step is shown. main.cpp drops vsync during a sweep so it isn't
  throttled to the compositor's unfocused rate. Output:
  <config>/screenshots-full/<surface>/<skin>.png + index.md.
- DRAGONX_FULL_SWEEP=1 runs the sweep headlessly then quits (CI / verification).

Verified end-to-end under WSLg: 288 PNGs (9 skins x 32 surfaces), all 1200x720,
[Sweep] done, clean quit, no daemon. Byte-identity across runs holds only for
static skins on data-free surfaces — the rest vary by animated theme effects /
live price / relative timestamps (expected for a review tool, not a bug).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-07 16:57:54 -05:00
DanSandClaude Opus 4.8 7ee20bb497 fix(wallet): connect the isolated seed node over plaintext RPC + add smoke
The seed-wallet creator forced TLS on its RPC client, but the DRAGONX RPC is
plaintext HTTP on localhost (-tls=only applies to P2P, not the RPC; the main
GUI's conf has no rpctls key and connects over http too). Forcing https made
every connect fail with "SSL connect error", so the isolated create never
completed. Connect without TLS, matching the main daemon path.

Also add an env-gated (DRAGONX_SEED_CREATE_SMOKE=1) headless smoke in main()
that runs SeedWalletCreator::create() before GUI init and prints
ok/word-count/address (never the seed words) — used to validate the full C++
creator path without the GUI. Harmless in normal runs.

Validated end-to-end against the hd-transparent-keys (v1.0.3-dc45e7d9)
daemon: ok=1, words=24, a shielded z-address returned, temp datadir cleaned up.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-07 14:17:00 -05:00
DanSandClaude Opus 4.8 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
DanSandClaude Opus 4.8 af2db4366c refactor(audit): de-duplicate the main.cpp backdrop draw + DPI transition
Two render-loop de-duplications, zero behavior change (verified byte-equivalent
on both the GL and DX11 backends via the mingw Windows cross-build):

- drawWindowBackdrop(app, bgDL, p0, p1, backdrop_active, lowSpec): the ~80-line
  gradient/texture/acrylic-capture/WindowBg-alpha block was copy-pasted in the
  main loop (with low-spec guards) and the resize-watcher lambda (without them).
  Extracted once; the main loop calls it with lowSpec=isLowSpecMode() and the
  resize watcher with lowSpec=false — each reproducing its CURRENT behavior
  exactly (pure dedup, no change). The intentional differences around it
  (resize's immediate present(0) vs the main loop's vsync, the main loop's
  try/catch, the BeginFrame low-spec guard) are left untouched.

- handleDisplayScaleChange(window, newScale, savedSizeForScale, lastKnownW/H,
  dpiTargetW/H, dpiResizeRetries, dpiResizePending): the SDL display-scale-change
  handler was duplicated ~verbatim in the idle-wait and poll event paths
  (differing only in debug-log text). Moved the body into one helper called from
  both; the #ifdef DRAGONX_USE_DX11 g_borderlessDpi and #ifdef __APPLE__ scale
  normalization are preserved.

Deliberately did NOT unify the full frame/present sequence — the resize path's
immediate present and lack of try/catch are intentional.

Full-node (GL) + Lite build clean; Windows/DX11 cross-build compiles main.cpp
clean; ctest 1/1; hygiene clean. NEEDS RUNTIME VERIFICATION: a live window
drag-resize (backdrop draws, no flicker) and a monitor DPI-scale change (window
resizes + fonts reload).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-02 21:00:40 -05:00
DanS 975743f754 feat(wallet): persist history and surface pending sends
Add an encrypted SQLite transaction history cache with cached tip metadata and
per-address shielded scan progress so startup and full refreshes avoid
re-scanning every z-address while still invalidating on wallet/address/rescan
changes.

Improve wallet history loading by paging transparent transactions, preserving
cached shielded and sent rows, keeping recent/unconfirmed activity visible, and
classifying mining-address receives. Show z_sendmany opid sends immediately in
History and Overview, pin pending rows through refreshes, and apply optimistic
address/balance debits until opids resolve.

Add timestamped RPC console tracing by source/method without logging params or
results, reduce redundant refresh/RPC calls, and cache Explorer recent block
summaries in SQLite.

Expand focused tests for transaction cache encryption, scan-progress
persistence/invalidation, history preservation, operation-status parsing,
pending send visibility, and Explorer/RPC refresh behavior.
2026-05-05 03:22:14 -05:00
DanS 2b3277529e macOS port: build, rendering, daemon, and mining fixes
Build & setup:
- Fix setup.sh and build.sh for macOS (bundle daemon, xmrig, sapling params, asmap.dat into .app)
- Fix CMakeLists.txt libsodium linking for macOS
- Fix incbin.h to use __DATA,__const section on macOS
- Remove vendored libsodium-1.0.18 source tree (use fetch script instead)
- Remove prebuilt-binaries/xmrig (replaced by xmrig-hac)
- Add .DS_Store to .gitignore

Rendering & UI:
- Use GLSL #version 150 and OpenGL 3.2 Core Profile on macOS
- Force dpiScale=1.0 on macOS to fix Retina double-scaling
- Set default window/UI opacity to 100% on Mac/Linux
- Add scroll fade shader guard for macOS GL compatibility
- Add ImGui error recovery around render loop and mining tab

Daemon & bootstrap:
- Fix getDragonXDataDir() to return ~/Library/Application Support/Hush/DRAGONX/ on macOS
- Fix isPortInUse() with connect() fallback (no /proc/net/tcp on macOS)
- Increase daemon watchdog timeout from 3s to 15s
- Add daemon status indicator (colored dot + label) in wizard bootstrap phases

Mining tab:
- Fix EmbeddedDaemon::getMemoryUsageMB() crash on macOS (was using Linux /proc)
- Fix XmrigManager::getMemoryUsageMB() to use ps on macOS instead of /proc
- Restructure RenderMiningTab with wrapper pattern for exception safety
- Fix default pool URL to include port (pool.dragonx.is:3433)
2026-03-19 14:26:04 -05:00
DanS 40dd6d45b2 daemon version check, idle mining control, bootstrap mirror, import key paste, and cleanup
- Add startup binary version checking for dragonxd/xmrig
- Display daemon version in UI
- Add idle mining thread count adjustment
- Add bootstrap mirror option (bootstrap2.dragonx.is) in setup wizard
- Add paste button to import private key dialog with address validation
- Add z-address generation UI feedback (loading indicator)
- Add option to delete blockchain data while preserving wallet.dat
- Add font scale slider hotkey tooltip (Ctrl+Plus/Ctrl+Minus)
- Fix Windows RPC auth: trim \r from config values, add .cookie fallback
- Fix connection status message during block index loading
- Improve application shutdown to prevent lingering background process
2026-03-17 14:57:12 -05:00
DanS d7bc5c638a fix: remove D3D11 debug layer flag that prevented startup on user machines
DRAGONX_DEBUG was defined unconditionally, causing D3D11CreateDevice() to
request the debug layer via D3D11_CREATE_DEVICE_DEBUG. This layer is only
available on machines with the Windows SDK or Graphics Tools installed,
so the call fails with DXGI_ERROR_SDK_COMPONENT_MISSING on regular user
machines — causing the app to silently exit.
2026-03-12 00:13:27 -05:00
DanS b3a0ce29ed fix: Windows identity, async address creation, mining UI, and chart artifacts
Windows identity:
- Add VERSIONINFO resource (.rc) with ObsidianDragon file description
- Embed application manifest for DPI awareness and shell identity
- Patch libwinpthread/libpthread to remove competing VERSIONINFO
- Set AppUserModelID and HWND property store to override Task Manager cache
- Link patched pthread libs to eliminate "POSIX WinThreads" description

Address creation (+New button):
- Move z_getnewaddress/getnewaddress off UI thread to async worker
- Inject new address into state immediately for instant UI selection
- Trigger background refresh for balance updates

Mining tab:
- Add pool mining dropdown with saved URLs/workers and bookmarks
- Add solo mining log panel from daemon output with chart/log toggle
- Fix toggle button cursor (render after InputTextMultiline)
- Auto-restart miner on pool config change
- Migrate default pool URL to include stratum port

Transactions:
- Sort pending (0-conf) transactions to top of history
- Fall back to timereceived when timestamp is missing

Shutdown:
- Replace blocking sleep_for calls with 100ms polling loops
- Check shutting_down_ flag throughout daemon restart/bootstrap flows
- Reduce daemon stop timeout from 30s to 10s

Other:
- Fix market chart fill artifact (single concave polygon vs per-segment quads)
- Add bootstrap checksum verification state display
- Rename daemon client identifier to ObsidianDragon
2026-03-05 22:43:27 -06:00
DanS eda19ef6d5 improved font scaling text and window adjustment, added ctrl + scroll hotkey for font scaling 2026-03-05 01:22:20 -06:00
DanS f5378a55ed UX improvements 2026-02-28 00:58:43 -06:00
DanS eebfd5947e console and mining tab visual improvements 2026-02-27 13:30:06 -06:00
DanS 3aee55b49c ObsidianDragon - DragonX ImGui Wallet
Full-node GUI wallet for DragonX cryptocurrency.
Built with Dear ImGui, SDL3, and OpenGL3/DX11.

Features:
- Send/receive shielded and transparent transactions
- Autoshield with merged transaction display
- Built-in CPU mining (xmrig)
- Peer management and network monitoring
- Wallet encryption with PIN lock
- QR code generation for receive addresses
- Transaction history with pagination
- Console for direct RPC commands
- Cross-platform (Linux, Windows)
2026-02-27 00:26:01 -06:00