fake_dragonxd now answers rescan, importprivkey, z_importkey and
z_importviewingkey, so the GUI's long wallet RPCs can be reproduced
without a real node.
- Each one holds cs_main for FAKE_RESCAN_SECS (default 0). The lock-free
getrescaninfo reports the scan while it runs, and, like v1.3.0, the
scan stops early when a shutdown is requested.
- Argument checks, error codes and return values follow dragonxd
(z_importkey returns null, importprivkey the t-address, the default
"whenkeyisnew" skips a known key, z_importviewingkey needs its zaddr).
Keys are only prefix-checked and never stored or logged; an imported
spending key gets a made-up address that the address lists then show.
- FAKE_ENCRYPTED=1 / FAKE_WALLET_PASSPHRASE: an encrypted wallet that
starts locked, with walletpassphrase (-14 on a wrong passphrase),
walletpassphrasechange, walletlock and getwalletinfo unlocked_until;
the imports answer -13 while it is locked. As in dragonxd, the right
passphrase with a non-integer timeout unlocks first and only then
fails (-1): the wallet stays unlocked with no relock timer of its
own. A timer pending from an earlier unlock keeps running; otherwise
only walletlock locks it again. Without FAKE_ENCRYPTED, those three
methods answer -15 like dragonxd.
- FAKE_METHODS_ALL call events show keys and passphrases as <redacted>.
A plain run is unchanged: the same 63 GUI calls before and after, and
identical replies to every one of them.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
"Keep in each address" posted one task that called z_mergetoaddress for
every batch of every address back to back; each call is a whole-wallet
scan under cs_main, and the daemon runs the ops one at a time anyway.
It is now an App-owned services::ConsolidationRun:
- one RPC at a time; the next merge only after the previous batch left the
queue and a gap as long as the last call took (2-120 s);
- pauses while the node syncs, the wallet is locked, a send of ours is in
progress, or another wallet scan is running;
- failures classified: nothing that may have queued is ever resent; a
breaker stops after 3 dead addresses or 3 failed ops in a row;
- each merge is charged to the scan budget; one re-read and one forced
balance at the end;
- it keeps running when the dialog closes: a status-bar chip, a run view
with progress, the wait reason and a Stop button.
The "Into one" merge can no longer be confirmed twice while its call is
in flight. fake_dragonxd gains FAKE_SCAN_SECS, FAKE_MERGE_SERIAL and
FAKE_MERGE_FAIL.
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>
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>
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>
- getrescaninfo takes no lock in dragonxd (it only reads wallet fields),
so the stub no longer holds it behind cs_main. Holding it there made
the GUI's rescan-progress poll look like an abandoned request every
30 s during a simulated rebuild.
- a `methods` event lists the 10 most-requested JSON-RPC methods with
counts (alongside `stats`, and at exit): what the GUI is polling.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Holds cs_main like a witness rebuild but logs no rebuild markers: a node
that is busy while its output says nothing (an external node, or a
rebuild path that prints no progress), for exercising the GUI's
"busy but alive" handling.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Scripted runs nobody needs to watch can run the GUI without a window:
OpenGL still loads, and connecting and shutting down behave as usual.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A Lite-configured tree builds ObsidianDragonLite and has no embedded
daemon, so any bin/ObsidianDragon left in it is a stale full-node binary
that the harness would otherwise run silently. Point --build-dir at a
full-node tree instead.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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>
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>
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>
Two dead-code removals surfaced by the audit; no runtime behavior change.
1. Delete the lite-lifecycle "readiness" scaffold (~1,586 lines). The pure
dry-run evaluateLiteWalletServerLifecycleReadiness (whose own status text
says "wallet lifecycle execution is still disabled in this scaffold") had
zero callers; executeLiteWalletServerSelectionUi had zero callers; and
executeLiteWalletLifecycleUiRequest's only caller was a fallback in
settings_page reached only when app->liteWallet() is null. Deleted
lite_wallet_server_lifecycle_readiness.{h,cpp} and the readiness /
UI-execution machinery + enums + translation tables from both adapters,
keeping the genuinely-live helpers (liteConnectionSettingsFromAppSettings,
applyLiteConnectionSettingsToAppSettings, redactLiteServerSelectionValue,
and the LiteWalletLifecycleUiExecutionInput DTO the live create/open/
restore path still uses). The null-backend fallback now sets a plain
"Lite wallet backend unavailable" status. This is exactly the
readiness/scaffold pattern CLAUDE.md forbids regrowing.
2. Split the HushChat fixture/capture-manifest/seed-projection tooling
(~2,050 lines, incl. the libsodium seed-projection) out of
chat_protocol.cpp (1854 -> 284) and chat_protocol.h (586 -> 105) into a
new chat_fixture_tooling.{h,cpp} compiled ONLY into the HushChatFixtureCheck
dev tool — never into the app, lite, or test binaries (verified via nm).
The app keeps only the runtime slice (memo parsing + tx metadata
extraction) that network_refresh_service actually calls. Also moved the
decrypt-preflight/hex helpers, which likewise had no runtime caller.
Note: the HushChat split is on gated-off experimental code and should be
coordinated with the pending dormant-HushChat-content handling (commit
af06b8b) before the lite PR — done here as a pure mechanical split, no redesign.
Full-node + Lite + HushChatFixtureCheck build clean; ctest 1/1; hygiene clean.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
tools/lite_send_smoke drives the real SDXL backend's exact GUI send path
(newaddr/status/list/tree/send/rescan) for diagnosing shielded sends.
scripts/gen-lite-checkpoints.sh generates verified mainnet checkpoints from a
synced dragonxd (getblockhash + getblockmerkletree), self-checking against a
known checkpoint before emitting.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Startup lock screen (soft): once the first refresh reveals the auto-opened wallet
is encrypted+locked, show the unlock modal on launch (reusing renderLiteUnlockPrompt,
one-shot per session). Soft by design — balances stay viewable via viewing keys
while locked, so the user may dismiss and browse read-only; only spending needs
the passphrase.
Real-backend verification: add `lite_smoke --encrypt` (create -> encryptionstatus
-> encrypt -> lock -> unlock, checking flags; passphrase never printed). Running it
against the real SDXL backend showed encrypt LOCKS immediately
(after encrypt: encrypted=1, locked=1) — the backend removes spending keys right
after encrypting. The controller already relays encryptionstatus faithfully (UI is
state-driven, so unaffected), but the fake modeled encrypt->unlocked; corrected the
fake (encrypt -> encrypted+locked) and the test sequence (encrypt -> unlock -> lock
-> decrypt) to match real behavior.
Builds clean, tests pass, hygiene clean.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Add `lite_smoke --keys`: create a fresh wallet and exercise the M4/M5
spend/backup commands (new-address, export, seed, save) against the real linked
SDXL backend, verifying each response's JSON shape with nlohmann. SECRET-SAFE:
seed and private-key VALUES are never printed — only field presence/shape and
counts (no send/shield, which would broadcast).
Verified live (isolated HOME, throwaway wallet shredded after):
new z shape_ok=1 new t shape_ok=1
seed has_seed=1 has_birthday=1 (REDACTED)
export is_array=1 count=4 has_private_key=1 (REDACTED)
save result_success=1
Confirms the controller's newAddress / exportSeed / exportPrivateKeys / save
parsing matches real backend output.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
lite_smoke: add --restore-recent (restore a throwaway wallet at birthday≈tip) and factor
the data-shape checks (non-blocking commands first). Finding: the backend downloads from a
fixed checkpoint regardless of birthday, so first sync is ~30 min and balance/list block
until synced — a full live data run is impractical.
Verified all refresh parsers against the real backend without a full sync:
- live run: info/addresses/syncstatus parse_ok=1 (addresses z=1/t=6 on a restored wallet).
- via the authoritative Rust source (commands.rs / lightclient.rs):
- balance do_balance fields match parseLiteBalanceResponse.
- list do_list_transactions: sends use outgoing_metadata (no top-level address), receives
use address+amount; parseTransactionRecord already branches correctly.
- syncstatus was the only mismatch (fixed in the prior commit).
No parser changes needed beyond syncstatus. M2 refresh path verified end-to-end at the
shape level.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- LiteWalletController owns a background std::thread worker that, once a wallet is ready,
refreshes every ~4s and publishes a copyable LiteWalletAppRefreshModel under a mutex.
Worker auto-starts on lifecycle-ready and is stopped+joined in the destructor. status_
is written only on the main thread; walletOpen_/syncStarted_ are atomic.
- App::update() calls takeRefreshedModel() and applies it into state_ on the main thread
(WalletState is non-copyable, so the model crosses the thread boundary, not the state),
so the existing Balance/Receive/Transactions tabs populate from lite data.
- refreshWalletState() refactored onto refreshModel() (pure, worker-safe).
- testLiteWalletControllerWorkerProducesModel verifies the worker publishes a populated
model (stable across repeated runs). Builds clean in all configs.
Real-backend smoke (lite_smoke --refresh now runs real output through the parsers) found
two integration bugs, documented in the plan for follow-up:
- syncstatus parser requires synced_blocks/total_blocks but the real idle response is
{"syncing":"false"} (string), so it fails to parse when not actively syncing.
- the first data query (balance/list) blocks on a full chain sync, which would hang the
worker's shutdown join — needs a cancel/timeout path.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Preserve the previously-uncommitted lite wallet implementation and related dev WIP
under version control:
- src/wallet/ lite services: client bridge, bridge runtime, connection, lifecycle,
sync, gateway, result parsers, state mapper, artifact contract/resolver, refresh
services, UI adapters, wallet_backend/capabilities. (Includes two small M1 fixes:
lifecycle walletReady now parses the response; default chain name -> "main".)
- src/chat/ chat protocol; tests/fixtures/ (lite + hushchat); tools/hushchat_fixture_check.cpp;
scripts/build-lite-backend-artifact.sh.
- Pre-existing modified app_network/security/wizard, network_refresh_service, sidebar,
mining_tab, bootstrap dialog, and version headers captured as-is.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>