While a blur overlay (a modal dialog or the lock screen) covers the
window, the theme backdrop effects kept requesting animation frames
even though they are not visible, so the app never went idle and
re-rendered continuously.
The ambient animation predicate now drops the backdrop-effects term
when a blur overlay was active last frame. Reduce Motion and the
unfocused/hidden throttle were already handled, and the overlay's own
moving elements (the lock screen's unlock dots, a spinner) still request
frames through the progress term, so nothing on screen freezes.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Alt + mouse wheel adjusts the font scale, but it was only handled in the
poll loop that runs after the app wakes. When the app was idle it blocks
in a single timed wait, and that first wheel event went straight to
ImGui, scrolling the window instead of changing the scale until a later
event arrived.
- The Alt+wheel handling is pulled into a shared helper used by both the
idle wait path and the poll loop, so the first event while idle is
consumed and triggers a redraw.
- Behaviour is otherwise unchanged: the visual scale updates smoothly and
the font atlas is rebuilt once scrolling stops.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The window always asked DWM for a full-window blur region and, on
Windows 11, an acrylic backdrop. When the user runs with an opaque
window (low-spec mode or full opacity) that desktop blur is never seen
but DWM still composites it every frame.
- An early read of the saved settings decides whether the window starts
opaque before the backdrop is configured.
- When it does, the full-window blur region is left as SDL's zero-area
default and the backdrop type is set to none instead of acrylic.
Windows-only.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The custom minimize / maximize / restore buttons are drawn from inside
app.render(), while an ImGui frame is open. Calling SDL to change the
window state there pushes resize events synchronously, which re-enters
the resize watcher and can nest a new frame inside the open one.
- A click now records the requested op instead of acting on it, and the
window state change is applied once the frame has closed, after the
pacing block so the follow-up redraw is not dropped.
Windows-only; it rides the borderless caption controls.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The live-resize event watcher redrew for every resize event, and each
resize tore down and recreated the DX11 render target even when the
pixel size had not actually changed. A single drag fires both a RESIZED
and a PIXEL_SIZE_CHANGED event reporting the same final size, so the
work ran twice.
- The watcher keys off the real pixel size and returns early when it has
not moved since the last pass, collapsing the duplicate event pair into
one redraw. It also bails out when called from inside an open ImGui
frame, which the caption buttons can trigger.
- The DX11 resize now checks the swap chain's current dimensions up
front and does nothing when the size already matches and the target is
live, skipping the destroy/recreate churn.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
On Windows stdout and stderr were reopened onto the same log path with
two separate freopen() calls. Each stream then had its own file handle
and its own write offset into the one file, so the two writers
overwrote each other and interleaved into a garbled log.
- stdout is reopened (truncating the file per run), stderr is pointed at
the same underlying handle via _dup2 so both share one offset and
append in order.
- stderr is first given a valid descriptor, since a GUI-subsystem
process may start with no fd 2, and is left unbuffered so crash
diagnostics are not lost.
Also corrects a stale comment that named the old log file.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Startup initialised SDL's gamepad subsystem and turned on ImGui gamepad
navigation, but the wallet is never driven by a gamepad. Initialising
the subsystem enumerates attached controllers and loads the mapping
database for nothing.
- SDL now starts with the video subsystem only.
- ImGui keeps keyboard navigation; the gamepad navigation flag is
removed from both the DX11 and the OpenGL setup paths.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
On Windows SDL3 raises the global timer resolution with
timeBeginPeriod(1) and holds it for the whole process lifetime, which
keeps the system on a 1 ms tick and costs power for no benefit here.
- A startup hint now asks SDL to leave the system timer alone. Nothing
in the app needs sub-15 ms waits: vsync paces drawing, the idle wait
is 200 ms, and background sleeps are 50 ms or longer.
The hint is Windows-only in effect and is ignored elsewhere; an
environment override that turns it off is harmless.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The acrylic effect blits its backdrop from the default framebuffer using
the logical viewport size as the source rectangle. On a HiDPI display
the framebuffer is larger than the logical size, so the blur sampled
only the lower-left corner of the screen and the glass showed the wrong
region stretched across each panel.
- The capture blit source now uses the real framebuffer pixel size
(logical size times ImGui's DisplayFramebufferScale, truncated the
same way the GL backend does), while the capture and blur targets stay
at logical size. The framebuffer size is tracked separately from the
viewport size so quality or buffer-lifetime resets cannot lose it, and
moving the window between a Retina and a non-Retina display redoes the
capture. DX11 is unaffected; its scale is one.
- The debug screenshot sweep captures at framebuffer size too, so HiDPI
captures are no longer cropped.
Tests: the framebuffer-extent helper matches the GL backend's
truncation and falls back to the logical size when the scale is unknown.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The auto-lock idle clock was reset whenever any ImGui item was active,
so a text field left focused (a half-filled send form, say) counted as
continuous activity and kept the wallet unlocked indefinitely, well past
the configured idle timeout.
- The idle clock is now fed by real input events (key, text, mouse,
touch, pen, gamepad button) as they are ingested on the main thread,
plus a still-held mouse button so a drag or scrollbar grab that emits
no new event still counts. A focused but untouched widget no longer
does.
- A new header-only IdleTracker owns the clock and never moves backwards,
so an event stamped before the previous one cannot shorten the idle
time.
Behaviour change: a form left focused and idle past the timeout now
auto-locks. The form's buffers are app state and survive the lock and
unlock, so nothing typed is lost.
Tests: a new IdleTracker case covers idle time growing with no events
(enough to trip a 900s lock), activity resetting it, the clock never
moving backwards, and a query before the last activity reading zero
rather than a negative value.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The screenshot sweep and the full UI sweep are dev tools. The full sweep
injects demo data into the live wallet state and the real data and
config dirs, and the tab sweep screenshots real balances and addresses.
Both were compiled into every build: the Settings buttons were
reachable in full-node releases, and the DRAGONX_FULL_SWEEP env hook
worked in every build, Lite releases included.
A default-OFF option alone would fail open: dev and release builds
share build/linux (and build/mac), so a release configured without
--clean would inherit a cached ON from the last dev configure.
- CMake: option(DRAGONX_ENABLE_UI_SWEEP ... OFF), passed to
ObsidianDragon and ObsidianDragonTests as a compile definition.
- build.sh passes it explicitly: ON in the dev build, OFF in every
release configure (Linux, Windows, macOS osxcross and native).
- Packaging guard: while the cache says ON, dragonx_release depends on
dragonx_release_sweep_guard, which fails, so a hand-configured ON
tree cannot package a release.
- startSweepImpl is a no-op unless the option is ON (an undefined macro
counts as 0, so it fails closed). The enabled branch logs the
sentinel "[Sweep] DRAGONX_UI_SWEEP_COMPILED_IN", so a release can be
checked with `strings <bin> | grep -c DRAGONX_UI_SWEEP_COMPILED_IN`
(must be 0).
- main.cpp: the DRAGONX_FULL_SWEEP hook is compiled out (autoSweep is
false). It must be: with the sweep a no-op, the hook would quit the
app once its warm-up frames ran out.
- Settings > Node > Debug options: both sweep buttons, open-dir, the
seed-demo-chat button and the current-theme checkbox are compiled only
into dev builds. The daemon debug categories are unchanged.
No i18n change: the existing strings stay in the tables.
Verified with an isolated release (DRAGONX_BUILD_ROOT/DRAGONX_RELEASE_ROOT
in a scratch dir) whose tree was first configured by hand with ON:
build.sh --linux-release without --clean leaves the cache OFF, and the
sentinel count is 0 in the built binary, the zip and the AppImage.
With the cache set back to ON by hand, --target dragonx_release fails
at the guard before compiling anything. Dev trees configured ON keep
the sweep (sentinel present; a one-surface DRAGONX_FULL_SWEEP run
against the fake daemon captured and exited cleanly).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Turning blur off (blur 0), switching to low-spec mode or enabling
reduced transparency after the acrylic targets existed left the capture
FBO and both blur ping-pong targets allocated for the rest of the
session, although nothing draws with them while the effect is skipped.
They are now released at a frame boundary and rebuilt on demand:
- ImGuiAcrylic::ReleaseIdleBuffers() runs right after app.preFrame(),
before NewFrame(), when the previous frame's draw data has been
rendered, and once in the resize-watcher frame (GL and DX11). While
the effect is skipped it frees the targets; when the effect is wanted
again it reallocates them once, with no per-frame retry if that fails;
- resize() while skipped only records that a reallocation is needed;
- the release checks the GL names (and each DX11 texture) directly, so a
half-allocated ping-pong pair is freed too; DX11 also resets the
ping-pong index;
- the capture paths and applyBlur() return early without a capture
buffer, which would otherwise blit the default framebuffer onto
itself;
- each release logs "released targets (effect skipped)".
With blur on, the path and the output are unchanged (screenshot sweeps
within noise). After re-enabling, the first frame draws the tinted
fallback until a fresh capture exists.
Measured with software GL, toggling every 30 frames for 1200 frames: the
targets were released exactly one frame after each switch-off (22 of 22)
and rebuilt on each switch-on (21 of 21), with no GL or framebuffer
errors. While off at 1280x720, with a fixed malloc mmap threshold, the
process holds 4.62 MB less (the 1280x720 RGBA8 capture plus two 320x180
RGBA16F blur targets) and RssAnon is about 3.1-3.5 MB lower. With
glibc's default threshold the freed memory stays in the heap and RssAnon
does not visibly drop.
Known issue: every off/on pair now frees and reallocates those targets.
Under software GL (llvmpipe) with the default glibc allocator the freed
texture memory is not reused: toggling every 2 frames grew RssAnon from
49 to 906 MB over 240 pairs, every 8 frames from 56 to 179 MB, and every
30 frames it levels off near 120 MB. It is not a GL object leak (with a
fixed mmap threshold the mapped bytes stay flat), and on a hardware
driver (WSLg d3d12) RssAnon stayed between 67.5 and 74.9 MB under the
2-frame churn (94.3 MB without the release). Users reach fast toggling
by holding Ctrl+Shift+Down (key repeat, about 20 Hz) or by dragging the
blur slider back and forth across 0. A short delay before releasing
would avoid it if software-rendered users matter.
Not verifiable here: the DX11 release and reallocation at runtime, and
macOS.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The GL window asked for a 24-bit depth and 8-bit stencil buffer, and
every acrylic Framebuffer attached a DEPTH24_STENCIL8 renderbuffer the
size of its color texture. Nothing uses them: no code enables depth or
stencil tests (the ImGui GL3 backend disables both), every blit and
readback is color-only, and only Framebuffer::clear() also cleared the
depth buffer, which nothing read. The window and every FBO, including
the full-size capture FBO, carried that memory, and the FBO
renderbuffers were reallocated on every resize.
Now:
- the window requests depth 0 and stencil 0 explicitly (SDL3 requests a
depth buffer by default when the attributes are simply removed);
alpha 8 stays for the transparent window;
- Framebuffer has no renderbuffer, and clear() clears color only;
- a permanent startup log line,
"GL default framebuffer: depth=N stencil=N", reports what the driver
provided; "(no attachment)" marks a failed query, which is how some
drivers report a buffer that does not exist.
The log shows depth=0 stencil=0 both offscreen and on WSLg X11, where
"Window transparency enabled by compositor" is still logged, with no
framebuffer-incomplete errors. Screenshot sweeps at 1280x720 and
2560x1440, and a glass run with ui_opacity 0.6 and blur on (so panels go
through the FBO blur), match the previous build within noise. The
driver-side memory saving itself was not measured. DX11 is unaffected:
its swap chain has no depth buffer.
Not verifiable here: macOS (an NSOpenGL 3.2 core context with a
transparent window).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
main() built the font atlas before App::init() loaded ui.toml, so the
first build used the hard-coded fallback sizes (H1 24 px, Body2 12 px).
App::init() then rebuilt for a saved font scale above 1.0, still before
the schema load and so still at the fallback sizes. Only the color-emoji
rebuild requested for the first frame reached the ui.toml sizes, so
color-emoji users built the atlas twice at font scale 1.0 and three
times at 1.5.
Users with chat_emoji_color=false never got that last rebuild. They
built the atlas once or twice and ran the whole session at the fallback
sizes, e.g. Body2 at 12 px instead of 15 and H1 at 24 px instead of 20.
Now:
- App::loadBaseUiSchema(), the ui.toml load moved out of App::init()
unchanged, runs in main() before both InitImGui paths (DX11 and GL),
together with the saved font scale and emoji style from the early
settings read. It is idempotent, so the call left in App::init() is a
no-op;
- the two rebuild triggers in App::init() are gone;
- Typography records what the atlas was built from (the 15 schema sizes,
the schema's font scale, the effective emoji style and the schema
generation), and preFrame() rebuilds once when they differ. While the
schema generation and the emoji style are unchanged this costs two
compares per frame.
Measured on the Lite build (color and mono emoji, font scale 1.0 and
1.5, English and Japanese): every run builds the atlas exactly once, at
the ui.toml sizes times the scale (H1 20 px and Body2 15 px at 1.0,
30 px and 22 px at 1.5), and ui.toml is loaded before the window and GL
context are created. The packaged Lite zip and AppImage log one atlas
build instead of two. Color-emoji screenshot sweeps match the previous
build within noise.
Visible changes:
- users with chat_emoji_color=false had been rendering the whole UI at
the fallback font sizes; they now get the designed ui.toml sizes, so
every mono-emoji screen differs from before and matches the
color-emoji layout;
- a user skin whose [fonts] differ from ui.toml now rebuilds the atlas
once when it is activated or left; on a runtime switch the atlas used
to keep the old sizes until a restart. With a test skin that sets
Body2 to 18 px, startup builds twice and ends at 18 px; switching to a
bundled skin rebuilds once, switching among the 9 bundled skins
rebuilds nothing (they carry an identical copy of [fonts]), and
switching back rebuilds once;
- editing ui.toml while running still gives exactly one rebuild.
Not verifiable here: the Windows DX11 path with the schema loaded before
its first FreeType build, macOS, and the interactive font-scale slider,
Alt+scroll and emoji toggle, which were checked by reading the code.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Settings::save() and WalletIndex::save() ran the atomic write (temp
file, fsync, rename, directory fsync) on the UI thread: on every
preference change, and for wallets.json after every address-scan pass
whose wallet metadata changed, i.e. repeatedly during sync, when the
disk is busiest. Each took milliseconds to tens of milliseconds, and
seconds under heavy disk load.
The JSON is still built on the caller; the write is queued on an
App-owned CoalescingWriter, latest wins per file, one write at a time.
- After a fund- or security-critical setter (seed-migration stage,
encryption pending, PIN enabled, active wallet file, wizard completed)
the next save still waits until the file is on disk, so e.g. the
sweep-submitted marker is durable before the sweep is broadcast. A
failed durable save keeps the next one durable.
- Saves before the writer is attached (the upgrade re-save at startup),
and in tests and tools, stay synchronous.
- main() flushes the queue (at most 3 s) before and after
App::shutdown(), before the single-instance lock is released, so a
relaunch never reads a stale settings.json.
A kill or power loss right after an ordinary save can now lose that
last preference change; the previous file stays intact. WalletIndex
saves report "queued" rather than "written" (no caller reads it). On a
wedged disk, exit can wait up to 2 x 3 s longer, inside the 15 s
watchdog.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
hasPendingRPCResults() becomes hasPendingBackgroundResults(): the one
check the idle loop makes, every <= 200 ms, for work that finished on a
background thread and still has to be applied by update(). Lanes added
later put their term there. It still covers only the two RPC worker
lanes; no behaviour change.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Connect gate: after the node starts answering, wallet scans wait until the
lock state is known and the node knows its tip (or the wallet has fewer
than 500 transactions), at most 20 s. A node already behind latches the
sync profile from the connect's getinfo. Meanwhile a list-only pass keeps
the address list and the cached history up, and the session's first
balance and per-address balances are read once even while catching up,
so the wallet shows its last-synced values instead of 0.
- Background: minimized, hidden, or covered without focus (3 s debounce,
any input cancels) polls only the chain state every 15 s. Wallet scans,
the mining poll, price, nudges and auto-lock interaction tracking pause;
auto-lock now fires while minimized. Leaving a held profile marks every
timer due again.
- Tab entry marks the timers due instead of forcing scans; a stale list or
a never-loaded history still loads on entry.
- The scan entry points own the hard gates, failed enumerations keep the
previous lists behind a retry hold, and failed balance calls never
overwrite known balances with 0.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Explorer: the tip check runs every frame the Explorer tab is open,
and it read both tip rows and rewrote them each time (two autocommit
WAL writes per frame). A steady tip is now kept in memory and skips
SQLite. A new tip writes both rows in one savepoint, so a crash
between them can't pair a new height with the old hash. A test
checks that 100 steady checks write nothing.
- Console: while the daemon was in the Error state the Console tab
added its error line on every poll, i.e. every frame, re-laying out
the whole buffer each time. It prints once per error now: on entering
Error, when the message changes, or after another crash
(EmbeddedDaemon::getUnexpectedExitCount(), never reset). setState
writes the message before the state, so a reader that sees Error sees
that error's message. The stray " ===" suffix is gone.
- perf.log: the profiler appended a block every 300 frames on every
run and never rotated the file (tens of MB a day for a wallet left
open). It is now off unless the app starts with DRAGONX_PERFLOG=1;
otherwise its zone hooks are near no-ops.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Restoring or un-maximizing the window called refreshNow(), which wipes
the history-scan progress and re-scans every z-address (a
z_listreceivedbyaddress per address, each holding cs_main). The Peers
tab's refresh button did the same, and holding F5 auto-repeated it.
- Window restore no longer forces a refresh; the regular timers catch
up (skipping the daemon-output backlog stays).
- The Peers refresh button refreshes peers only.
- F5 ignores key repeat. An explicit F5 or History refresh still does
the full re-scan.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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.
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)
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.
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
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)