116a997a3a461aa519aa8bc2387f42d7f0f46265
102
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
81733f5603 |
fix(rpc): run rescans and key imports without the 30 s timeout
A rescan or key import holds the node's cs_main for the whole wallet scan. On the shared clients it hit the 30 s curl timeout, reported a failure that wasn't one, abandoned a daemon RPC slot and flipped the app into "node busy". - One long wallet RPC at a time (rescan, importprivkey, importaddress, importwallet, z_importkey, z_importviewingkey, z_importwallet) runs on its own ungated client with no client timeout (rpc/long_call_lane.h). It can be aborted from any thread, and an abort is never lost, even one that lands before the client opens. - The GUI rescan, key import and sweep use it. The sweep's fund-moving body is unchanged; only the client it calls differs. runtimeRescan() returns bool, and a refused post-bootstrap reconcile stays armed. - The console routes rescan and the import commands through the lane; its input stays usable meanwhile, and a refusal is printed like any other result. RPCClient gains callRaw(.., timeoutSec). - Restart, wallet switch, delete-chain, bootstrap (now also from Settings) and the other node-stopping flows are refused while it runs, with a message saying a key import or rescan is still running; quit asks for confirmation. The lane is aborted at quit. - A second import or rescan is refused before the key is copied anywhere. An import cut off after the request reached the node (a lost reply, or a proxy's 502/504 while the node still imports) says the key may already be in the wallet and to run Settings -> Rescan. Only the node's own HTTP-layer refusals (401, 503, work queue full, unhandled request) say that the key was not imported. A pure classifyLongCallCutOff() makes that choice. - New strings in all 8 languages; console results stay English. Tests: the long-method list and the console's routing; one lane at a time; an abort lands whether it comes before the client opens or during the call; a call with no client timeout outlasts a 3 s scan, where a 1 s gated client times out and marks the node busy; classifyLongCallCutOff() for every failure kind with and without a sent request, and from raw HTTP replies (502, 504, a plain 500 -> cut off; 401, 503, queue full, unhandled -> refused). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
78e38570a8 |
fix(bootstrap): check archive and free space before deleting chain data
The bootstrap removed blocks/, chainstate/ and notarizations/ first and only then opened the archive. An unsafe entry, a full disk or a corrupt member therefore left no chain, or a half-written one for dragonxd to start on. - Preflight before anything is deleted: every archive entry must be safe (no absolute path, no "..") and readable, and the disk must hold the extracted size plus max(256 MiB, 5%), counting what removing the old chain frees (a symlinked chain dir counts 0). Free space counts as unknown only when the probe errors, the capacity is 0 or the available size is -1; a known disk with 0 bytes free is a refusal. The old chain is kept on every preflight failure. - Protected members are skipped, compared case-insensitively: any path component starting with "wallet", any "database" component, a .cookie or *.conf file, and top-level *.dat files other than fee_estimates, peers and banlist. The number of non-critical member failures is logged. - The removal of the old chain is verified (a locked file fails the run instead of mixing old and new chain files). If the extraction fails or is aborted, the partial chain is removed again before the failure is published; isDone() turns true only after the worker's last filesystem change. - Cancel is disabled while the old chain data is being replaced, and quit aborts a running bootstrap. - A complete zip from an earlier attempt is reused on Retry; its checksum is re-verified. A download that cannot fit fails early, and a disk-full write error says so. Tests: the space rule (boundaries, unknown vs. full disk, saturation); planArchive sizes, zip-slip, garbage and truncated zips; the protected-name list; reclaimableBytes with symlinks; each preflight refusal keeps the old chain and wallet.dat byte-identical; a corrupt critical member removes the partial chain before the failure is published. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
3ac46abe1a |
fix(security): compile the UI sweep only into dev builds
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> |
||
|
|
1f162ec830 |
build(i18n): embed language JSON only on Windows, add lookup fallbacks
CMake turned every res/lang/*.json into an xxd header that i18n.cpp compiled in as a fallback, on every platform. Only the Windows single-file exe needs it: the Linux zip and AppImage and the macOS .app always ship res/lang, and loadLanguage() reads the loose file first. On Linux the embedded copy added about 1.5 MB to the binary, and every translation edit recompiled i18n.cpp. A new option, DRAGONX_EMBED_LANG, defaults to ON for WIN32 and OFF elsewhere: - ON without xxd is now a configure error (it used to warn, then fail to compile i18n.cpp on the missing headers); - i18n.cpp compiles the embedded includes and the fallback only when ON; - outside Windows, loadLanguage() also tries <exe>/../share/<app>/res/lang (the cmake --install layout and the full-node AppImage) and <exe>/../Resources/res/lang (the macOS .app binary run directly), so those layouts find the loose files whatever the working directory; before, only the embedded copy covered that. Measured: - full-node Linux binary: .data 1,699,696 -> 168,592 bytes; stripped 19,983,136 -> 18,456,128 bytes (-1.53 MB); - with the ImGui demo stubs, the Linux zips and AppImages are 0.50-0.55 MB smaller, with identical file lists including all 8 language files; - a Lite build started from an unrelated working directory loads the ja.json next to the binary (2422 strings), and the packaged Lite zip and AppImage load ja.json with no new not-found or failed lines; - Windows keeps the embedded copy: the 8 language headers are generated and the ja.json bytes are in the exe. A new test loads "de" from the loose files and checks a translated string; with bin/res/lang moved away exactly its two assertions fail. Behaviour changes on Linux and macOS: a missing or corrupt res/lang now falls back to English instead of the embedded copy, and a translation edit needs only the copy_langs step instead of a recompile. Not verifiable here: the macOS .app in a non-English language, and the Windows single-file exe at runtime. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
70eacd90ad |
build(imgui): compile imgui_demo.cpp to its empty stubs
The ImGui demo window cannot be opened in the wallet (its show flag is never set), but imgui_demo.cpp was compiled and linked in full. It is now compiled with IMGUI_DISABLE_DEMO_WINDOWS, which reduces it to empty stubs. ShowDemoWindow, ShowStyleEditor, ShowAboutWindow, ShowUserGuide and ShowStyleSelector stay defined, and imgui.h declares them unconditionally, so a per-file define is ODR-safe. IMGUI_DISABLE_DEBUG_TOOLS is deliberately not set: it changes imgui.h declarations and would drop ImGui's duplicate-ID highlight. Measured: - imgui_demo.cpp.o: 755,392 -> 1,704 bytes; - stripped full-node Linux binary: 20,267,680 -> 19,983,136 bytes (-284,544); - full-node Windows release exe: 282,112 bytes smaller; - the "Dear ImGui Demo" string is gone and the stubs are still defined. With Makefile generators the per-file define lands in the target's flags.make, so each existing build tree recompiles the whole app once (153 files, about 71 s at -j8 here). No runtime or visual change. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
8b094595b1 |
build(cmake): exclude HushChatFixtureCheck from the default build
HushChatFixtureCheck is a dev-only CLI for the HushChat fixture files.
Nothing runs it (no ctest entry, not linked into the app or the tests),
yet every dev and test build compiled and linked it.
It is now EXCLUDE_FROM_ALL, with a usage note next to the target:
cmake --build build/linux --target HushChatFixtureCheck
A dry run of `all` no longer mentions it (4 lines before) while
ObsidianDragonTests stays in `all`, and building the target explicitly
still produces a working binary (`--help` exits 0). The fixture template
and the chat sources are unchanged.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
||
|
|
e65be1d24b |
build(cmake): add a dragonx_release target that builds only what ships
Release builds ran a plain `cmake --build .`, which builds `all`: on top of the app and the wallet-rebuild helper, every --*-release run also compiled HushChatFixtureCheck, ObsidianDragonTests where BUILD_TESTING is on (Linux, macOS) and, in Lite backend trees, lite_smoke and lite_send_smoke. None of them ship. A new `dragonx_release` custom target depends on ObsidianDragon plus dragonx-wallet-rebuild, copy_langs and copy_themes where those targets exist, and the three release paths in build.sh build only that target. The dev build (./build.sh) still builds `all`. BUILD_TESTING is deliberately left alone: release and dev builds share build/<platform>, and writing BUILD_TESTING=OFF into that cache would leave ctest running a stale test binary. Measured: - a dry run of dragonx_release schedules none of the test, smoke-tool or fixture-CLI builds that `all` schedules, in a full-node, a Lite+backend and a mingw tree; the wallet-rebuild helper is still built; - all four release builds (Linux and Windows, full node and Lite) log the dragonx_release target and no ObsidianDragonTests build, and the Linux zip and AppImage file lists are unchanged. Consequences: a release run no longer refreshes the test binary or the lite smoke tools in build/<platform>/bin, so run ./build.sh or `cmake --build` before ctest, and build the Windows smoke tools with `--target lite_smoke lite_send_smoke`. If a release step starts copying another CMake-built file out of bin/, its target must be added to dragonx_release. Not verifiable here: macOS --mac-release (the .app should still contain the app, dragonx-wallet-rebuild and Contents/Resources/res). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
8c0a22b7bc |
fix(ui): cut ellipsis truncation on UTF-8 code-point boundaries
TruncateToWidth, the middle "front...back" ellipsis for addresses, pool URLs, paths, labels and the status bar, cut at byte offsets: a multi-byte character at either cut was split into invalid UTF-8 and drawn as replacement characters (e.g. a translated Cyrillic or CJK status). It also tried front lengths from n/2 down until one fit, building and measuring a new string for each: up to O(n^2) per truncated label per frame. util::textfit::middleEllipsis keeps the same contract and is byte-identical for ASCII, but snaps the front cut down and the back cut up to code-point boundaries, and finds the largest fitting front length by bisection into one reused buffer: O(log n) measures for single-line text. Text containing a newline keeps the linear scan, because its measured width (the widest line) does not grow steadily with the cut. text_fit.h also gains largestFittingPrefix, a bisection over code-point boundaries; its first caller is the node-status banner in a following change. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
4a95d9ecb7 |
perf(chat): lay out and draw only the visible chat thread rows
Every frame the open chat thread handled all N messages: grouping flags from two day keys each, a measuring layout of every body, then the draw, which laid every body out a second time and ran the bubble, hover and popup code, however few rows were on screen. The thread now keeps a layout cache: grouping flags, each body's wrapped size (keyed by message, so only new or re-wrapped bodies are measured) and each row's height, with prefix offsets. A frame binary- searches the visible range and submits those rows plus any failed send (Retry stays reachable with Tab), the row being drag-selected and the row whose context menu is open; one spacer stands in for each skipped run, and scrolling to the newest message works as before. - Rows are rebuilt when the chat store changes or when the wrap width, fonts, font size, DPI scale, rasterizer density or compact mode change. Fonts are keyed by FontId, so a font reload at the same size (the colour-emoji toggle) re-measures. A rebuild is O(N), about 0.5-1 ms at 3000 messages. - Grouping flags are recomputed on a data change and once a minute, together with util::refreshTimezone(), so a system timezone change reaches chat timestamps within a minute. Rows above the visible range are now one spacer of their summed heights, so the visible rows no longer move by a pixel each time a row above them crosses the top of the app window. Debug builds log once if a cached wrap width or row height ever disagrees with the live layout. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
a69380817e |
perf(chat): use reentrant local time for chat timestamps
The open chat thread called std::localtime at least twice per message per frame (two day keys for grouping), and more for day separators and time labels. On glibc every call re-checks the timezone with a stat of /etc/localtime. The chat time helpers now use util::localTime() from the new util/local_time.h: localtime_r (localtime_s on Windows), with no per-call timezone check and no shared static result buffer. glibc's localtime_r reads the timezone only once, so local_time.h also adds util::refreshTimezone() (tzset) for caches to call when they rebuild. Nothing calls it yet; its first caller is the chat thread's grouping-flags cache in the next commit. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
c6fc1ac168 |
fix(images): cap and stream image decoding in a GL-free decoder
Image decoding lived in the GL texture file and had no limits: a file of any size was read whole, stb and libwebp allocated whatever the header claimed (a WebP alone up to 1 GB), and a GIF was decoded at full size, every frame, before the 300-frame cap. A 720p 150-frame GIF avatar cost 0.2-1.5 s and 140-370 MB on the UI thread. stb's GIF loader also read before the start of its buffer on disposal-3 frames, and when a long GIF ran out of memory it freed the frame delays that the caller then freed again (an abort). Decoding, and the stb implementation, now live in the GL-free util/image_decode_core, safe on any thread: - the header is probed first; stills above 256 MP, any GIF or animated WebP canvas above 32 Mi px, and files above 256 MiB are rejected before anything is allocated (real photos, 200 MP phone shots and wide panoramas still load); - a GIF decodes one composited frame at a time, downscaled as it goes, and stops at the frame cap: peak memory is stb's working buffers, two canvases and the small frames; a truncated GIF keeps the frames decoded so far; - a header stb cannot read means "unsupported", never a blind decode; - decodes above 16 MP run one at a time; a decode can be cancelled between frames. util::StartImageDecode runs one decode on a detached worker that owns its job and its lane's in-flight counter, so nothing waits at exit; the next commits move avatars and the image picker onto it. The limits apply now to avatars and the picker's hover previews; thumbnails and avatar imports get them when they move to workers. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
b2a57a61a0 |
feat(util): add SerialJobLane
A FIFO of blocking jobs, run one at a time on a background thread that a post starts when none is running and that exits once the queue is empty. Each job returns a completion, and completions run only in pump(), on the owner's thread. Unlike RPCWorker and AsyncTaskManager nothing ever joins: abandon() (also run by the destructor) drops queued jobs and undelivered completions and refuses later posts, and a job stuck in a blocking call finishes on its own thread. It is meant for backend calls that can wait out a long scan and may have to be given up. post() reports a dropped job (abandoned lane, or no thread could be started), and waitIdle() is bounded. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
296c04386c |
perf(mining): sample daemon memory off the UI thread
The daemon's RSS was read on the UI thread: a scan of every /proc entry on Linux (1-7 ms), a forked ps on macOS (up to ~30 ms). The mining poll probed every 1-5 s on every tab, and the Mining tab's memory bar probed again every 1.5 s. util::BackgroundSampler now runs the probe on one lazily started thread and caches the result. getDaemonMemoryUsageMB() returns the last sample and asks for a new one at most every 3 s, and only while the Mining tab memory bar or the Peers tab Memory cell is drawn: nothing on other tabs or while minimized. EmbeddedDaemon::memoryProbe() hands the sampler a pid snapshot (on Windows the value read from the owned process handle), so nothing off-thread touches daemon state. The mining poll does no I/O any more; it carries the last sample. Both shutdown paths stop the sampler. Fixed along the way: an unreadable /proc made the directory iterator throw on the UI thread; the scan now just stops. User-visible: - the Peers tab Memory cell keeps updating during sync and shows an external daemon's memory (it showed the mining poll's value, which pauses during sync and covers only the embedded daemon); - the Mining tab memory bar refreshes every ~3 s (was 1.5 s). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
2618744e59 |
fix(rescan): probe the node's block range off the UI thread
Finding the lowest block a node still has (Settings > Rescan, and the automatic reconcile after a bootstrap) ran a binary search of about 42 synchronous getblockhash/getblock calls on the UI thread, each waiting for the RPC connection behind any wallet scan (10-20 s on a big wallet) and each with a 30 s timeout. The window froze for that long. It now runs as one task on the wallet RPC worker, bounded to 90 s, and the answer comes back on the UI thread; meanwhile the dialog shows "Checking which blocks..." with Rescan disabled. An answer from an earlier connection counts as "could not tell", and one for an earlier opening of the dialog is dropped. The search probes block 1 first, so a full-history node answers in one probe, and it answers "could not tell" at once while a runtime rescan holds the lane. Fixed along the way: any probe error (timeout, busy node) read as "block missing", so a busy node could get a bogus "bootstrapped, rescan from the tip"; and a non-string getblockhash reply never called back, leaving the dialog on "Checking..." for good. Only the daemon's "can't read block" error now counts as missing; anything else means "could not tell". User-visible: - after a bootstrap, a probe that cannot tell is tried up to 6 times, 30 s apart and only while the node is not busy, then a toast points to Settings > Rescan, instead of falling back to the -rescan restart, which fails on a bootstrapped node; - the Settings Rescan dialog warns when the probe could not tell. Two new strings, translated into all eight languages. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
93b6871511 |
perf(history): write the history snapshot on change, off the UI thread
Every history refresh, address refresh, connect and lock-state check rewrote the encrypted tx-history snapshot on the UI thread: a JSON document of every row, its encryption, then INSERT OR REPLACE and a prune, an estimated 40-120 ms on a 5k-transaction wallet, even when nothing had changed. Now a signature of what would be stored (about 1 ms on 5k rows) decides whether to write. Confirmations count only by the thresholds the UI reads (conflicted, pending, 1-9, 10-99, 100+); per-address scan heights, exact confirmations and the tip drift every block and form a separate part. - Routine refreshes write a content change at most every 20 s (a deferred one is released from the update loop) and the per-block drift at most every 10 min. Nothing changed means no write. - A mining-address relabel, a passphrase change, a rescan, a disconnect, a wallet lock and exit write whatever is not on disk yet, at once. - The UI thread only copies the rows and the key into a job; a single background writer (newest job wins) builds, encrypts and stores it. A failed write is retried. - Exit queues the final write so it overlaps the daemon stop, then waits up to 3 s for the writer; a disk that hangs past that keeps the previous snapshot. - A loaded snapshot counts as stored, so the first identical refresh doesn't rewrite it. An address refresh re-keys the snapshot only when the address set changed. After a hard kill the snapshot can now be up to ~20 s behind for rows and 10 min for scan heights, exact confirmations and the tip (it was one refresh); the first refresh replaces it either way. Lite builds, which never read this cache, no longer write it on Refresh or a mining relabel, and the screenshot capture mode's demo rows never reach it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
6c02e67c6d |
perf(mining): one save, at most one restart per thread-count gesture
Every thread-count change on the Mining tab saved settings.json and, with a miner running, stopped and restarted it: xmrig re-inits its RandomX dataset and reconnects to the pool, seconds of lost hashing each time. A drag across the grid did this once per tile crossed (up to ~24), with a "Starting pool miner" toast each; every -/+ click or Enter did it again. The tab now hands each pick to the App, which puts the newest in settings at once and applies it when the input settles: on drag release, or 0.5 s after the last click, even if the Mining tab was left in the meantime. The header, RAM estimate and tile fill still follow the drag live. - Pool: the restart is decided on the mining-control thread, after every earlier start or stop has run. It never starts a miner the user stopped, is a no-op when xmrig already runs at that count, and is no longer silently skipped when the pick lands in another restart's stop window (which left xmrig on the old count). - Solo: one setgenerate per gesture. A pick made while one is in flight now waits for it instead of being dropped (another stale count). - Clicking the tile that is already selected does nothing; it used to save settings and restart the miner. - With idle thread scaling on, the grid no longer snaps back to the old count while a pick is on its way to the miner. A pool auto-balance restart keeps a count still queued. - Idle thread scaling changes the pool count through the same retarget. Its correction after a start compares with the count last requested, not the xmrig API's lagging report, so it comes about 1 s in instead of after the first API report (3-5 s). - A retarget whose pool config no longer validates shows the same error and stops the miner, as the old stop-then-start did. - While a benchmark runs a pick is saved, but the miner change is dropped: the benchmark owns the count until it ends. A crash within 0.5 s of a pick, or mid-drag, loses that pick; every clean exit saves it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
3f1194a84b |
feat(util): add CoalescingWriter for background file/snapshot writes
One background thread for blocking write jobs, for the settings.json / wallets.json saves and the tx-history snapshot that later commits move off the UI thread. Jobs are keyed (a file path, "tx-history"): - a submit for a key that is still queued replaces that job and keeps its place in line (latest wins); - keys run first in, first out, one job at a time, so two writes of one file never overlap and an older snapshot never lands after a newer one; - submitAndWait() blocks until that job (or a newer one for the key) has run and returns its result, for writes the caller needs on disk; - flush() waits with a bound; stop() runs everything queued, then joins; - the thread starts on first use and sleeps when idle; with no thread (none could be created, or after stop()) a job runs inline. persistFileAtomically() routes Platform::writeFileAtomically through a writer, or writes synchronously when there is none. Nothing uses it yet. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
b29a09eaa0 |
perf(chat): sends re-read by origin, chat scans under the scan budget
A z_sendmany submit set off a burst of wallet scans before anything had changed. Ops now re-read the wallet when they finish, by who started them: the user's sends and dialog ops get a re-read plus one forced balance once none of their ops is pending; client auto-shield re-reads unforced; chat messages and splits refresh only the chat note view. A pending send no longer forces scans, and chat txids no longer enter the tracked-send set. Chat ingest skips stored messages before decrypting and caches undecryptable ones per identity. The inbox scan runs when getwalletinfo's txcount changed (a cheap probe, at most 2% duty) or a cost-scaled safety net is due; the note-buffer scan runs on need. Both are admitted by the scan budget, on a steady clock. The chat payer comes from a per-address view of verified notes, with debits re-applied across in-flight scans; splits are sized to one payer and only when they can refill the buffer. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
f86cc4d6b7 |
perf(net): hold wallet scans at connect, in the background and on tab entry
- 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> |
||
|
|
323143e111 |
perf(net): one scan budget for all wallet scans, gated on what changed
The balance, address scan, history and probe were throttled one by one at 10% each, which added up to 30-40% of dragonxd's cs_main with all of them due. services::WalletScanGovernor runs one wallet scan at a time across both RPC lanes and keeps one shared quiet clock at 15% of wall-clock (cut when the node is behind). Waiters are served first in, first out, with a head start for what the page shows and a starvation floor of max(30 s visible / 900 s hidden, 15x the stream's cost). Explicit work (a dirty set, the user's send, a first load) is never refused but is charged. Routine scans run only when the tip, the mempool size (getmempoolinfo: no cs_main) or a wallet-mutation epoch changed since their last good scan, or a safety sweep is due. Pages that don't show the balance poll it at most every max(10 min, 50x its cost) (2 min with client-side auto-shield); coming back into view with an old value fetches it at once. The staleness badge counts from the last poll that confirmed nothing changed, and hides while the budget holds the balance within its guarantee. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
879de37d18 |
perf(history): scan shielded history in bounded passes, not every block
Every block re-ran z_listreceivedbyaddress for every z-address (O(wallet) each, under cs_main) and a whole-wallet z_listsentbyaddress. On a 40-address wallet that was ~80% of wall-clock. The history now advances in passes: - an address is rescanned when it is stale by a number of blocks scaled to its measured cost (8 to 500), never-scanned and hinted addresses first (a hint: z_listunspent shows a note the history doesn't); - z_listsentbyaddress covers only the blocks since the last good sweep (+100 reorg margin); the whole-wallet sweep runs once per connect, with a 10 min / 1 h / 6 h backoff after real failures; - listtransactions pages are sized from last time, and an unchanged tip reuses the transparent rows; - found-but-unenriched txids carry over as a backlog; only -5 is a definitive "not ours" for z_viewtransaction; - between blocks, a cheap recent poll plus a one-address probe. Duty model for a 40-address wallet at 10 s per scan: ~9% instead of ~80%. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
7ceff1eaaa |
fix(market): back off failed market fetches instead of retrying every frame
The Market tab calls the exchange-list, price-history and candle fetches every frame, so a failed fetch (offline, rate-limited, a venue's API down) was retried on the very next frame, ~20 requests a second. - util::FetchBackoff: 30 s doubling to a 10 min cap, +/-20% jitter. The exchange list, the price history (a partial result counts as a failure) and the per-pair candles each back off on failure. - Candle backoff is per pair. Switching to another pair still loads at once, a failed refresh keeps the on-screen pair's last candles, and switching back to a pair that is backing off shows the aggregate chart rather than the previous pair's candles. - The Market tab's refresh button retries at once (at most every 5 s). A price fetch (60 s timer, no backoff) that succeeds after failing means the network is back, so it lifts all three backoffs. - A partial price history no longer marks the chart loaded: with no fetch timestamp, the 30-minute reload gate measured system uptime and blocked retries for the first 30 minutes after boot. - The exchange list goes through httpGetString (TLS verified, size capped) with short timeouts (5 s connect, 12 s total, as it runs on the shared RPC worker) instead of its own curl code. A failed fetch is retried; it used to be tried only once per session. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
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> |
||
|
|
71ed92b9f1 |
feat(wallet): consolidate notes per address, keeping each address's funds in place
Users who keep funds apart under different labelled addresses can now shed notes (smaller wallet, faster scans/witness rebuilds) without the funds being merged into a single address. - Merge/Consolidate dialog: "Where should the funds go?" - Into one address (unchanged) or Keep in each address. The latter lists shielded addresses with 2+ spendable notes (label, note count, amount; too-small ones disabled) and merges each address's notes back into that address with z_mergetoaddress [addr] addr. Addresses with more notes than the batch size get several batches (the daemon locks a batch's notes before returning, so calls take disjoint notes); the dialog suggests a second run after those confirm. Per-address progress; ops keep running if the dialog is closed. The Consolidate (wallet-size) preset opens in this mode; Merge keeps "Into one". Shielded only: a transparent address can't take a merge's output. - services/note_consolidation: groupSpendableNotes, worthConsolidating, estimateBatches, consolidateInPlace (pure; driven by a merge-call function), with unit tests against a stand-in following dragonxd's rules. - fake_dragonxd: optional note wallet (FAKE_WALLET_NOTES, FAKE_MERGE_SECS) with z_mergetoaddress and its operations. - i18n: 17 new keys in all 8 languages; CJK subset rebuilt. - UI sweep: modal-consolidate target. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
9339bb0663 |
test(windows): build and run the test suite for Windows from WSL
scripts/test-windows.sh cross-compiles ObsidianDragonTests.exe (and the fake_dragonxd stub) with mingw into build/windows-tests, reusing the toolchain and dependency sources from build/windows, and runs it from WSL through Windows interop (or wine). All tests pass on Windows. CMake: - Windows forced BUILD_TESTING OFF in the cache to keep curl's tests out, which also removed ObsidianDragonTests from every Windows build. Hide it from curl with a scoped variable instead, and restore our cache value afterwards (curl's cmake_dependent_option FORCE-writes it). Windows still defaults to OFF, so release builds are unchanged. - ObsidianDragonTests now depends on copy_langs: i18n.cpp includes the generated embedded/lang_*.h, so a tree that builds only the tests failed (latent on Linux too). - Link the Windows system libraries the tests need. Tests: - testTmpPath/fixtureDir/removeTree helpers: no hard-coded /tmp, a runtime fixture-dir override (the compiled-in path is a Linux one), and best-effort cleanup that can't abort the suite. - Close the explorer cache before deleting its directory (Windows can't delete an open file). - Compile out the address-book and wallet-index round-trips on Windows: getConfigDir() uses SHGetFolderPath there, which no environment variable redirects, so they would overwrite the real %APPDATA%\ObsidianDragon files. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
7b6e8a8a8e |
test(daemon): add fake_dragonxd, a slow dragonxd stand-in for stop/kill testing
A scriptable fake dragonxd that reproduces, in minutes, the incident where the GUI hard-killed a node busy with an hour-long witness rebuild: - dragonxd's HTTP-RPC queue (8 workers + 16 queued, -rpcthreads / -rpcworkqueue honoured): a full queue answers an immediate plain-text 500 "Work queue depth exceeded", and a request whose client gave up keeps its slot until a worker reaches it - a global cs_main held by a "witness rebuild" that logs the real markers; every method except stop/help takes it - warmup -28 with the daemon's allow-list, an optional startup replay - dragonxd's shutdown order: stop (or SIGTERM on POSIX) unbinds RPC, 503s open connections, waits for the rebuild, then logs "Shutdown: In progress..." / "Shutdown: done" and exits 0 Everything is appended to <datadir>/fake_dragonxd.events; a missing "clean-exit" line means the process was killed. One portable C++17 file (POSIX + Winsock), OFF by default (-DDRAGONX_BUILD_FAKE_DAEMON=ON), built into <build>/fake_dragonxd/ so it can't shadow the real bin/dragonxd. run-gui-with-fake.sh runs the GUI against it in a throwaway HOME. Against master's GUI on Linux it reproduces the bug: closing during a rebuild queues stop behind cs_main-blocked workers, then the GUI sends SIGTERM and SIGKILL mid-rebuild, with no clean exit. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
fae8b20fb7 |
fix: FAQ CJK glyphs, outgoing-tx history, lite encryption, migrate & sync safety
Bug-report fixes plus adversarial-audit follow-ups. Bumps full 2.0.1->2.0.2 and lite 1.1.0->1.1.1. i18n / fonts: - Rebuild the NotoSansCJK subset with the glyphs the FAQ back-fill introduced (什/么/门/做); Chinese FAQ titles rendered as "??" on zh (and ja/ko). Transaction history (enumeration gaps): - Surface outgoing shielded z->z sends via z_listsentbyaddress (listtransactions and z_listreceivedbyaddress never report them). - Surface z->t deshields by parsing z_listsentbyaddress transparentSends (which z_viewtransaction does not expose); dedupe t->t against listtransactions. - Key send-row dedup on address so equal-value multi-output sends aren't dropped. Lite wallet (key safety): - The async create/restore/open path the UI uses now applies the passphrase: encrypt a new/restored wallet, unlock an existing one. It previously discarded the passphrase, storing the seed/keys in PLAINTEXT (create/restore) or leaving an encrypted wallet locked (open). Migrate-to-seed (fund safety): - Persist a sweep-submitted marker at broadcast so a resumed migration can't treat a ~0 balance (an unconfirmed in-flight sweep) as an empty wallet and adopt (swap wallet.dat) before the sweep confirms; best-effort locate routes to the confirm gate. Sync / threading: - Gate isSynced() on a peer-derived tip (tip_known) so a peerless node isn't reported synced (Send against a stale balance); add a "no peers" status banner. - Guard daemon_status_ with a mutex (monitor-thread write vs UI-thread read). Tests: extends tests/test_phase4.cpp across all of the above; suite green. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BwycPDQSfEKtrTS2uC4JeT |
||
|
|
c7e48c16f8 |
feat(lite): version 1.1.0 + variant-aware FAQ
Bump DRAGONX_LITE_VERSION 1.0.0 -> 1.1.0: since lite-1.0.0 the Lite variant
gained the whole 2.0.x shared UI/UX + diagnostics + i18n + perf work and this
session's chat delete/block — a minor bump (new features, no breaking change).
Make the FAQ variant-aware (walletFaq(fullNode)) so the Lite build reads
correctly: "What is ObsidianDragonLite?", encryption in the Wallet tab (not
Node & Security), migrate-to-seed hidden, node-specific answers reworded
neutrally ("the wallet syncs"), a new "Lite Wallet" subcategory (server model +
privacy tradeoff) standing in for the hidden Daemon group, and the redundant
single "Wallet" group tab dropped when there's no Daemon group to switch to.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
||
|
|
d27f387d6d |
feat(ui): in-app FAQ, DPI-scaling audit fixes, mining/stratum polish + localize strings
Bundles the session's UI work (the touched files carry several of these changes together, so they are committed as one coherent UI batch): - FAQ: new RenderFaqDialog + data-driven faq_content, opened from a status-bar "?" (and the Windows title bar), styled like the Wallets modal with search, Wallet/Daemon tabs, and smooth scroll. - DPI/font-scale audit: multiply hand-drawn absolute geometry by Layout::dpiScale() across ~30 files so nothing renders native-size at HiDPI / font_scale 1.5 (verified with a full sweep at 1.5x). - Mining: chart now fills the horizontal space; thread stepper +/- buttons match the input-box height; move the stratum-host toggle into Node & Security (v1.3.0+). - Settings: fix the auto-shield status text overlapping the grid. - Sidebar: drop the peer-count badge on the Network button. - i18n: wrap 193 hardcoded literals with TR() (keys/translations added in the preceding i18n commit), so the security/PIN/lock flow, seed-backup wizard, and witness-rebuild dialog localize. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
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> |
||
|
|
8ffcd9cc8c |
build(node): bundle dragonx-wallet-rebuild in Linux + Windows releases
Wires the wallet-rebuild recovery helper into the release pipeline so a shipped
build actually carries it (the app locates it next to dragonxd).
- CMakeLists: link the vendored STATIC Berkeley DB for the helper — add
Threads::Threads + dl (Linux) / ws2_32 (Windows) that the static libdb-6.2 needs
(the system shared lib pulled those in transitively; the static one doesn't).
- build.sh (Linux + Windows): pass BDB_INCLUDE_DIR/BDB_LIBRARY explicitly at
configure, pointing at external/dragonx/depends/<triple>/{include,lib/libdb-6.2.a}
— the same libdb the daemon links, so the helper's output is a v6.2 btree the
bundled dragonxd reads. Explicit paths bypass find_library (and the mingw
toolchain's sysroot-only find restriction). Guarded: no depends → helper simply
not built/bundled. Strip + copy the helper next to dragonxd(.exe) in both bundles.
Verified: Linux links the vendored libdb-6.2.a statically (no dynamic libdb) and
rebuilds the real broken wallet correctly; the helper cross-compiles cleanly with
mingw against the vendored Windows libdb-6.2.a to a PE32+ x64 exe. Full app +
helper build, suite green (1/1), build.sh syntax OK.
Remaining: macOS Berkeley DB (no in-tree depends artifact) + resource-embedding as
an alternative to side-by-side bundling.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
||
|
|
bc183257b7 |
feat(node): in-app "Rebuild wallet database" recovery for a BDB-inconsistent wallet
Automates the manual recovery that fixed a wallet.dat with stale Berkeley DB extent metadata (the "main" subdb metapage records a low last_pgno while its live data spans thousands of pages beyond it). A tolerant page-walk reads every record, but the daemon's BDB verify rejects the file and auto-salvages it — finding nothing and shrinking the wallet to empty on each restart (the salvage cascade that looks like fund loss). Plain "Restore original" can't fix it (hands the same broken file back → re-salvage); a rebuild must produce a fresh, consistent DB. Pieces (Approach A from the design workflow — out-of-process helper keeps AGPL Berkeley DB out of the GPLv3 GUI): - util/wallet_file_probe.h: extractWalletBtreeRecords() — sibling to parseWalletBtree that collects raw (key,value) bytes (same bounds-checked, subdb-aware walk). Records copied verbatim → encrypted key material passes through as opaque ciphertext (no passphrase). Overflow-page values (only large tx history) are skipped + counted; a rescan rebuilds history — funds unaffected. - tools/wallet_rebuild/main.cpp: dragonx-wallet-rebuild CLI — reads via the tolerant reader, writes the records into a fresh BDB "main" btree via libdb (DB_EXCL, never overwrites), prints a JSON summary. New BDB-guarded CMake target. - App::rebuildWalletDatabase(): picks the largest readable wallet/.bak as source, stops the daemon, runs the helper, VERIFIES the output (readable BDB with keys) before swapping, moves the current wallet aside (kept, timestamped), installs the rebuilt one, clears the stale BDB env, sets -rescan, restarts. Copy/rename only — never deletes. Result surfaced via the existing pumpWalletRestore channel. - Wired as the preferred action on the existing wallet-auto-recovery dialog (shown only when the helper is present). Full-node only; lite-safe. Verified end-to-end against the real broken wallet: helper reads 3,808 t-keys + 1 z-key + HD seed and the daemon LOADS the rebuilt output with no salvage. Adds extractWalletBtreeRecords coverage. Build clean, suite green (1/1). Remaining (follow-up): release packaging — build.sh bundling the helper built against the vendored per-platform static libdb (DRAGONX_BDB_ROOT), and a macOS Berkeley DB port (no in-tree artifact). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
a1d3964e34 |
fix(lite): require exactly 24 words on first-run restore (crash on valid seed)
The lite first-run restore wizard enabled Restore for {12,15,18,21,24}-word
phrases, but the SDXL backend only accepts 24-word / 32-byte-entropy seeds:
LightWallet::new does copy_from_slice(&phrase.entropy()) into a [u8;32]
(lightwallet.rs:231), which panics on 16/20/24/28-byte entropy. Mnemonic::
from_phrase accepts the shorter valid phrase, and the restore FFI
litelib_initialize_new_from_phrase (lib.rs:127) has no catch_unwind (unlike
litelib_execute), so the panic unwinds across extern "C" -> process abort
(UB on the pinned rustc 1.63). A user restoring a legitimate 12-word seed
from another wallet crashed the app.
The Settings restore gate was already tightened to == 24 (
|
||
|
|
16244d84a0 |
chore(release): bump version to 2.0.1
Full-node ObsidianDragon 2.0.0 -> 2.0.1 (single source of truth: the project() VERSION in CMakeLists.txt). Verified the generated header renders "2.0.1 (ObsidianDragon)". The Lite variant is versioned independently (DRAGONX_LITE_VERSION, unchanged at 1.0.0). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> |
||
|
|
d188a08db7 |
test(daemon): add F1/F2 process-lifecycle integration tests; fix clobbered start error
Links the real EmbeddedDaemon into the ObsidianDragonTests target (its deps were already present) and adds two POSIX integration tests that exercise the actual fork/exec/waitpid fixes headlessly: - testExecFailureReported (F2): start() against a non-executable file must fail with a precise "not executable or wrong architecture" reason. - testDaemonCrashDetected (F1): a short-lived child that exits abnormally is still detected (crash_count_ increments) while isRunning() is hammered from the test thread — a regression test for the reap race. Writing the F2 test surfaced a real bug: start()'s failure branch called setState(State::Error, "Failed to start dragonxd process"), and setState stores the Error message into last_error_ — clobbering the precise message startProcess() had just set, so getLastError()/the UI only ever saw the generic string. Fixed to pass the preserved detail to setState, so the precise reason survives and now also reaches the state callback (crash panel / status). ctest 1/1, green including the two new integration tests. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> |
||
|
|
b561406c23 |
fix(build): make macOS release builds work natively
The --mac-release path (full-node and --lite-backend) had never run on a real Mac and broke on several Linux-only assumptions: - build.sh version parser used GNU sed \+ (BSD sed reads it literally), so it aborted before compiling — switched to POSIX [[:space:]][[:space:]]*. - build.sh libsodium universal check used GNU grep \| — switched to grep -E. - libwebp's cpu.cmake applies -mno-sse2 to its reference DSP files; under a universal build (-arch arm64;x86_64) that lands on the x86_64 slice, where it disables _Float16 and breaks the SDK 26 <math.h>. Added an idempotent FetchContent PATCH_COMMAND (cmake/patch-libwebp-simd.cmake) to neutralize it. - The SDXL lite backend static lib needs Security + CoreFoundation frameworks on macOS; added them to the imported dragonx_lite_backend target for APPLE. - Added a DRAGONX_MAC_ARCHS override plus auto-detect: with --lite-backend the app is built for the arch(es) the backend .a actually provides (its pinned ring 0.16.11 is x86_64-only), instead of failing to link a universal app. - build-lite-backend-artifact.sh uses bash 4+ (mapfile); added a re-exec guard for macOS's stock bash 3.2 and added bash to setup.sh's macOS core deps. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
bcee4bfe72 |
fix(security): apply audit remediations to dev (ported + dev-only)
Dev-branch audit (docs/_archive/security-audit-dev-2026-07-22.md) re-found the
master issues (absent on dev) plus new ones in dev-only code. Applied here;
full-node build + test suite green; the subtle fixes were adversarially re-verified.
Ported from the master remediation (adapted to dev's code):
- bootstrap: reject zip-slip / path-traversal archive members (isSafeArchivePath)
before writing (S2-1). (dev already fail-closes on a missing checksum.)
- xmrig updater: fail closed when a signature is required but no key is pinned (F1-1).
- http_download.httpGetString: 16 MiB hard cap + MAXFILESIZE on the shared
metadata/price fetch (F1-2 / caps the updater + exchange paths at one site).
- rpc_client: explicit SSL_VERIFYPEER/VERIFYHOST (F4-1) and a 256 MiB response
cap in WriteCallback (F4-3).
- lite_connection_service: reject remote http:// lite servers, loopback only (L1-1).
Loopback is matched by a strict dotted-decimal 127.0.0.0/8 check (not a
startsWith("127.") prefix, which would wrongly accept 127.0.0.1.evil.com), with
userinfo/fragment stripping.
- lite controller: propagate encrypt/decrypt save() failure instead of reporting
success (F7-1).
- xmrig_manager: chmod(0600) the pool config before writing secrets (F5-2).
- app: clear the copied secret from the OS clipboard on shutdown (F3b-1).
- export_transactions: neutralize CSV/spreadsheet formula injection (F13-1).
- build pipeline: build-from-source lite backend + remove the self-attested
CMake signature gate (F15-1); pinned+verified appimagetool (F15-3/4);
verified Sapling params in setup.sh (F15-6); build.sh exits 0 on success.
(F14-1 empty-quoted-arg and F8-2 NUL-termination were already fixed on dev.)
Dev-only findings:
- rpc_client.callRaw: scrub the raw buffer + parsed tree (templated scrubJsonSecrets
for ordered_json) so console dumpprivkey/z_exportkey keys don't linger in freed
heap (N1-1).
- seed_wallet_creator: wipe the exported mnemonic on the failure path so a discarded
failed result never carries a live seed (W1-2).
- export_all_keys: write the plaintext key dump 0600 + atomically via
writeFileAtomically (U1-2).
- chat_database: restrict chat_messages.sqlite and its WAL/SHM sidecars to owner-only (C3-1).
Not done (need a decision, documented in the report):
- Chat header metadata (cid/z/p) rides outside the AEAD (C1/C2) — binding it is a
wire-protocol change requiring SilentDragonXLite interop review.
- Bootstrap lacks an offline-rooted signature (S2-2 residual) — needs signing infra.
- Console scrollback retains console-typed key-export output in plaintext (N1-1
residual) — inherent to an echoing console; would need output redaction.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
||
|
|
17525003c5 |
feat(ui): themed DragonX logo — runtime-rasterized SVG recolored per skin
Replace the shared app logo (Overview header, first-run wizard, lock screen) with the DragonX mark rendered from res/img/logos/logo_dragonx.svg, recolored to each theme: the dragon body takes the theme accent (Primary()), the inner detail stays light. Vendor nanosvg (memononen, zlib/public-domain) in libs/nanosvg/ and add util::LoadTextureFromSvg (parse a copy, recolor shapes by luminance, rasterize to RGBA, upload via the existing CreateRawTexture — GL + DX11). The SVG is embedded as a string (src/embedded/logo_dragonx_svg.h) so it's available in every build. The load moves into App::ensureLogoTexture(), called at the top of render() BEFORE the wizard/lock early-returns (so those screens get the logo too, which they never did before), and re-rasterizes when the accent or the dark/light variant changes. The per-skin PNG remains a fallback if rasterization ever fails; logo-texture lifetime is now freed on replace + on theme switch (was leaked). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
a78b44246e |
build(freetype): color-emoji rendering via a cross-platform FreeType backend
The chat "Emoji style: Color" option renders the merged emoji in color (COLR/CPAL Twemoji) instead of the monochrome NotoEmoji subset. This needs FreeType, which the default stb_truetype rasterizer can't do for color glyphs. - Vendor imgui_freetype (matches the bundled 1.92 ImFontLoader API) and embed a 1.4 MB COLRv0 Twemoji font (no libpng/harfbuzz needed). - CMake gains an optional FreeType path: native Linux/macOS use the system FreeType via find_package; the mingw-w64 cross-compile has none, so build.sh --win-release now cross-builds a minimal static FreeType (scripts/build-freetype-mingw.sh) and passes it in. Absent FreeType => graceful monochrome fallback, so no build breaks. - Typography selects the FreeType loader + the color font (LoadColor) when color emoji is on, else the stb loader + mono subset; toggling reloads the atlas. Both the DX11 and OpenGL backends already support the 1.92 RGBA dynamic atlas, so color glyphs render. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
923d086092 |
feat(images): WebP decode via libwebp + more stb formats
Broaden avatar/image support: - Add libwebp (FetchContent, static, decode-only) so WebP loads. Built from source for Linux / mingw-Windows / macOS-osxcross identically — the cross sysroots have no webp, so vendoring from source is the one portable path; encode/tool builds are disabled to avoid pulling in libpng/zlib. Linked as webp + webpdemux (the latter for animated WebP, wired next). - texture_loader routes all decode through DecodeImageRGBA: sniffs the RIFF/ WEBP header and uses libwebp (WebPDecodeRGBAInto into a free()-able buffer), else stb — so every existing caller (avatars, QR, thumbnails) gains WebP for free with no allocator mismatch. - Enable stb's TGA / PSD / PNM / PIC decoders and add .webp/.tga/.psd/.pnm/ .ppm/.pgm/.pic to the image-picker + avatar-library extension lists. Verified: libwebp builds static and a real .webp decodes to correct RGBA. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
72bf59149d |
feat(wallet): per-wallet data isolation foundation (P1)
First phase of multi-wallet support: keep each wallet's data separate so loading
a different wallet no longer shows the previous one's data, and lay the metadata
groundwork for the wallet-files list.
- Address book scoped per wallet: AddressBookEntry gains a "scope" ("global" or a
wallet-identity hash); the Contacts tab shows global + current-wallet contacts
(App::activeWalletIdentityHash) with a "show in every wallet" toggle + globe
badge. Legacy entries migrate to "global". Scope-aware de-dup allows the same
address across different wallets.
- Wallet metadata index (data/wallet_index -> wallets.json): file-keyed cache of
balance / address count / identity / size / last-opened / synced-here, since
those can't be read off a wallet.dat without loading it. Populated on connect +
address refresh (change-detecting upsert). Plus an active_wallet_file setting
for the -wallet=<name> switch coming in P2.
Unit-tested (testAddressBookScope, testWalletIndex): migration, visibility filter,
scope-aware de-dup, upsert change-detection, save/reload round-trip.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
||
|
|
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> |
||
|
|
718cb82c13 |
feat(wallet): migrate-to-seed, Phase 1 — isolated seed-wallet creation
First, fund-safe step of "migrate a legacy wallet to a seed wallet": mint a brand-new BIP39 mnemonic wallet in an ISOLATED throwaway datadir so the user can later sweep funds into it. No funds move and the real wallet.dat / main daemon are never touched — the isolated node runs concurrently on its own port. - EmbeddedDaemon: one-shot isolated-datadir override (setNextStartOverride) consumed on the next start(); a skip-default-port-check for an isolated instance beside the main daemon; and a tcpPortInUse() probe. The datadir's basename must be the assetchain name (DRAGONX) and the daemon reads <datadir>/DRAGONX.conf itself, so no -conf is passed (both learned from live testing against the hd-transparent-keys daemon). - SeedWalletCreator (src/daemon): picks a free port, writes a throwaway conf, starts an isolated dragonxd with -usemnemonic=1 -connect=0, waits for RPC, calls z_exportmnemonic + z_getnewaddress (the sweep target), stops it, and keeps/scrubs the temp datadir. Off-thread; the seed is wiped after use. - App: beginCreateSeedWallet (background) -> pumpSeedMigration (main-thread handoff) -> renderSeedMigrationDialog (Intro -> Working -> Show seed -> Error), reusing the seed_display grid + clipboard auto-clear. - Settings: "Migrate to seed…" button (full-node gated) + persisted pending migration state (dest address + temp datadir) for Phase 2 to adopt. Validated end-to-end against the hd-transparent-keys daemon: the isolated node comes up in ~4s and returns a 24-word mnemonic + a shielded z-address. Requires that daemon (the bundled Jun-28 build lacks z_exportmnemonic). The migration modal uses English literals for now (as renderBackupDialog does); the whole flow gets translated once Phase 2 finalizes it. Phase 2 (sweep + adopt) is next. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
6c08a08127 |
feat(chat): enable HushChat by default + legacy (non-mnemonic) wallet support
Flip DRAGONX_ENABLE_CHAT to default ON (a fresh configure now builds chat in), and make full-node chat work for every wallet + any daemon. Legacy compat: chat identity is derived from the wallet's mnemonic (z_exportmnemonic) for a portable, SDXLite-compatible identity — but a legacy random-seed wallet has no mnemonic, and released daemons don't have that RPC at all. Provisioning now falls back to a stable z-address's spending key (z_exportkey) in those cases: a functional, wallet-local identity that works on any daemon. Since a chat identity is local (peers learn your public key from your message headers, not your derivation), interop is unaffected; only cross-client portability needs the mnemonic. The spending key is an in-memory KDF input over a key the wallet already holds, wiped after use — no new exposure. Stability: the chosen chat z-address (the reply-to in headers AND the legacy identity source) is now persisted in settings (chat_reply_zaddr), so the identity + reply address don't shift when new addresses are generated. chatReplyZaddr() picks the smallest spendable z-addr once and reuses it. CLAUDE.md updated to reflect the default flip. Verified: Linux + Windows build with chat ON, fresh-configure default confirmed ON, ctest 100%, hygiene clean. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
4db609fb52 |
feat(chat): composer + contact requests — outgoing construction (Phase 4)
Add the outgoing side of HushChat: compose messages and start conversations.
The wire construction is the byte-exact inverse of the receive parser and is
proven by a self-consistent round-trip (build → parse → decrypt).
- src/chat/chat_outgoing.{h,cpp} (pure): buildOutgoingMessage encrypts via
encryptOutgoing and buildOutgoingContactRequest carries plaintext; both emit
the header memo JSON ({h,v,z,cid,t,e,p}, which nlohmann serializes
alphabetically to match SilentDragonXLite) + the payload memo, validating the
512-byte memo limit, the 64-hex peer key, and the "no leading '{'" rule for
request text. My public key goes in header "p"; the peer's key is the
encryption recipient.
- ChatService: composeMessage/composeContactRequest (encrypt with the held
identity) + recordOutgoing (echo an Outgoing ChatMessage into the store + DB —
we never harvest our own sends, so this is the only local record).
- App: sendChatMessage(cid,text) sources the peer z-addr + public key from the
conversation, composes, and records a random-id echo; startChatConversation
(zaddr,text) mints a random cid + composes a plaintext contact request;
chatReplyZaddr() picks a spendable z-addr. broadcastChatMemos() is the Phase-5
transport seam (network delivery + real-SDXL interop verification land there).
- Chat tab: a message composer (shown once the peer's key is known, else a
"waiting for reply" hint) + a "New conversation" modal that sends a contact
request to a z-address.
- Tests: outgoing round-trip through the receive parser + decrypt, contact-request
passthrough, validation guards, and ChatService compose + recordOutgoing echo.
Adversarially reviewed (crypto/interop, secret hygiene, logic, UI) — 0 findings.
Gated by DRAGONX_ENABLE_CHAT (default OFF). Verified: Linux + Windows(mingw)
build with chat ON, ctest 100%, hygiene clean; caches restored to the OFF default.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
||
|
|
980a100edd |
feat(chat): read-only Chat sidebar tab (Phase 3)
Add the first user-visible HushChat surface: a Chat tab in the sidebar with a
two-pane, read-only conversation view — a conversation list on the left and the
selected thread on the right — reading from the App-owned ChatService store.
- New src/ui/windows/chat_tab.{h,cpp} (RenderChatTab(App*), mirroring the
Contacts tab). Conversations are sorted by most-recent activity; peer
z-addresses resolve to contact names via the address book when known; each
message shows sender + timestamp + wrapped body, with a contact-request tag
and a read-only footer (composing arrives in a later phase). Empty states
cover "wallet locked / identity not ready" and "no conversations yet".
- Sidebar wiring: NavPage::Chat + registry entry (static_assert stays balanced),
NavPageSurface + GetNavIconMD (ICON_MD_CHAT), dispatch in app.cpp, trace/sweep
page names, and App::chatService() accessor. The tab is gated on the new
WalletUiSurface::Chat, which returns DRAGONX_ENABLE_CHAT != 0 — so it only
appears in chat-enabled builds and is hidden (and unreachable) by default.
- i18n: English defaults for the chat nav label + hint strings.
Verified: Linux + Windows(mingw) build with chat ON, ctest 100%, hygiene clean;
caches restored to the OFF default. The screenshot sweep now includes the Chat
tab (sweepPageName "chat") for per-theme visual review.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
||
|
|
46eec37013 |
feat(chat): persistent, seed-encrypted message store (Phase 2)
Swap the in-memory-only chat store for durable sqlite persistence with real
per-transaction timestamps, encrypted at rest under a key derived from the
wallet's own seed (no passphrase, works on encrypted and unencrypted wallets).
- ChatDatabase (src/chat/chat_database.{h,cpp}): sqlite store mirroring
data::TransactionHistoryCache. unlockWithSecret(seed) derives a 32-byte AEAD
storage key and a wallet-partition tag via domain-separated keyed BLAKE2b
(generichash) contexts. Every record — bodies, peer z-addrs, threading,
timestamps — is crypto_aead_xchacha20poly1305_ietf-encrypted with a random
nonce and the wallet tag as associated data; even the dedup key is a keyed
hash of txid+position, so nothing about your conversations is in cleartext on
disk. Rows are partitioned per-wallet; a different seed sees nothing. Messages
are decrypted once at ingest then re-encrypted under the storage key, so
load() needs only the storage key, not the chat identity.
- ChatService: ingest() now stamps each message with its own transaction time
(txid->time map + fallback) and writes new (store-deduped) messages through to
the database; loadFromDatabase() rehydrates the in-memory read model on unlock.
- App: unlock the chat DB with the same seed in provisionChatIdentityFromSecret
and load prior history; lock the DB + clear the decrypted in-memory store on
relock and on lite-controller rebuild.
Adversarial review (4 confirmed findings, all fixed): don't provision if the
wallet locks mid-fetch (re-check isLocked at completion); wipe the serialized
plaintext temporary in append(); trim the seed into a separate fully-wiped
buffer (no residue past a shrunk size()); scrub the mnemonic copy in the RPC
json result.
Tests: ChatDatabase round-trip (persist/reload, field + order fidelity), dedup,
per-wallet isolation, lock inertness, and ChatService write-through + reload
without an identity. Gated by DRAGONX_ENABLE_CHAT (default OFF). Verified:
Linux + Windows(mingw) build with chat ON, ctest 100%, hygiene clean; caches
restored to the OFF default.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
||
|
|
ba03de938e |
feat(chat): message model, in-memory store, and receive service
Phase 1 Steps 4-6 (the receive pipeline, minus the App/sync wiring which lands next). - chat_message: the in-memory ChatMessage model (direction, kind, txid, cid, peer zaddr/pubkey, body, timestamp, payload_position). No libsodium. - chat_store: threads messages by conversation_id and dedups by (txid, payload_position) so re-scanning the chain never double-inserts. - chat_service: owns the long-lived chat identity keypair (move-disabled, wiped on destruction/clear) and the store. ingest() decrypts each Message (drops undecryptable ones silently — no plaintext/memo logging), passes a ContactRequest's plaintext through, and threads the result. No-op without an identity. Tests: a metadata batch decrypts into a threaded conversation; re-ingest dedups; a contact request carries through; a wrong identity decrypts nothing; no-identity ingest is a no-op. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
2502487e44 |
feat(chat): seed-derived identity + secretstream crypto core
Phase 1 crypto foundation (gated by DRAGONX_ENABLE_CHAT; the pure primitives are always compiled + unit-tested, the feature gate lives at the future service layer). - chat_identity: derive the DragonX-native X25519 (crypto_kx) identity from a stable per-wallet secret via a domain-separated keyed BLAKE2b KDF (crypto_generichash, context "DragonX-HushChat-Identity-v1") into a clean 32-byte crypto_kx seed. Deterministic; skips SDXL's UTF-8-hex-seed quirk (that's the Phase-4 import path). Per §5.6. - chat_crypto: encryptOutgoing (server_tx) / decryptIncoming (client_rx) via crypto_secretstream_xchacha20poly1305, byte-exact per Appendix A.3/A.4 so DragonX interoperates with SilentDragonXLite. Single chunk, TAG_FINAL; decrypt enforces both the Poly1305 tag and TAG_FINAL (stricter than SDXL). Every session key / seed / stream state / plaintext scratch is sodium_memzero'd on all paths; no secret is ever logged. - Tests: encrypt->decrypt round-trip (incl. empty + UTF-8), identity determinism, feature-gate + empty-secret handling, and malformed/tampered/ wrong-key inputs all fail safely. Adversarially security-reviewed (3 lenses: secret hygiene, crypto correctness, SDXL wire-interop) — confirmed byte-compatible with the SDXL reference in both directions. Fixes from review: check the secretstream_push return before reporting Ok; allow the empty-plaintext ciphertext (== ABYTES) so encrypt/decrypt round-trip symmetrically; document the caller-owns-and-wipes secret contract. Interop caveat: round-trip proves self-consistency; a real captured SDXL message is still needed to prove wire-compat end to end. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
ddd53dc006 |
feat(send): add a shared contact picker to the recipient field
Phase 0c. The Send recipient field only offered Paste and a tx-history suggestion list — no way to pick a saved contact. Add a compact contacts icon button next to Paste that opens a picker popup over the App-owned address book; selecting a contact fills the recipient (re-validated next frame). - New header-only src/ui/windows/contact_picker.h: ContactPickerPopup(id, book, outBuf, outSz) — a reusable popup listing contacts ([Z]/[T] tag + label + short address) that copies the chosen address into the caller's buffer. Kept out of the material layer so the design-system headers stay free of data/ deps; reserved for Chat "new conversation" later. - send_tab: shrink the recipient input to make room for the icon button (ICON_MD_CONTACTS), wire the popup to app->addressBook(). Coexists with the existing Paste + suggestion affordances. - i18n: send_contacts_button tooltip. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |