Now that the release publishes a valid .sig per archive (verified against the pinned key for
linux/win/macOS), enable enforcement and fix two bugs that the newer multi-platform release
(v6.25.3, which added a macOS build) exposed:
- kXmrigRequireSignature = true: refuse any install whose release doesn't publish a valid
ed25519 signature over the archive. Verified live end-to-end against the signed v6.25.3
(archive SHA-256 + signature -> install).
- Drop the redundant inner-binary SHA-256 check. It keyed on the inner filename, but both the
linux and macOS archives contain a binary literally named "xmrig", so the two "xmrig (…)"
checksum lines collided in the map and the linux install compared against the macOS hash ->
spurious "could not verify" failure. The whole archive is already verified (SHA-256 +
signature), so every extracted member is authentic by transitivity — the per-member check
added nothing but ambiguity.
- Fix the macOS platform token: the asset is named "...-macos-x86_64.zip", not "...-macos-x64",
so selectXmrigAsset never matched it. currentXmrigPlatformToken() now returns "macos-x86_64"
on Intel macs (arm64 has no build -> Unavailable). Added a matcher test for the macOS naming.
Both variants build; suite stable (0 failures / multiple runs); live require-mode install verified.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Pin the ed25519 public key in xmrig_updater.h, activating signature verification in soft mode
(kXmrigRequireSignature=false): a release's ".sig" asset is verified when present, but an
unsigned release still installs on TLS + SHA-256. Verified live against the current release
(v6.25.2, which ships no .sig yet) — still installs.
- gitignore *.ed25519.key / *.ed25519.pub.b64 so a signing secret key can never be committed.
- Add a unit test that the pinned key decodes to a valid 32-byte ed25519 key (a malformed paste
fails the build, not silently disabling verification).
Bug fix (found via a flaky test): verifyXmrigSignature trimmed trailing whitespace BEFORE the
raw-64-byte check, so a raw signature whose last byte equals '\n'/'\r'/space/tab (~1.6% of
signatures) was corrupted and rejected. Now base64 is tried first (safe to trim) and the raw
path uses the exact untrimmed bytes. Added a deterministic regression test that forces a
whitespace-terminated raw signature. Suite is stable (0 failures in 10 runs; was ~3/8).
Also de-brittled the live integration test: it no longer pins a release-specific binary hash
(reaching Done already means the worker verified the binary against the release's own checksum).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Closes the supply-chain gap the review flagged: today the archive and its SHA-256 share one
trust root (the release body), so a compromised/edited release can ship an arbitrary binary
that still "verifies". This adds authenticity via a detached ed25519 signature checked against
a public key PINNED IN THE BINARY (not fetched), using libsodium's crypto_sign_verify_detached.
Opt-in / soft rollout:
- kXmrigSignaturePublicKeyBase64 in xmrig_updater.h is EMPTY by default -> signatures are not
checked and behavior is unchanged (TLS + SHA-256 only). Paste the base64 public key to enable.
- Once a key is pinned, an install verifies a "<archive>.sig" asset (base64/raw 64-byte ed25519
signature over the archive bytes) when present; kXmrigRequireSignature=true additionally
refuses installs that publish no signature.
- The check runs after the SHA-256 check, over the same already-read archive bytes; refuses on
a missing key-but-required, unreachable .sig, or invalid signature.
- verifyXmrigSignature + selectXmrigSignatureAsset are pure (libsodium only) and unit-tested:
valid base64 + raw-64-byte signatures verify; tampered data, wrong key, and malformed/empty
inputs all fail closed. Cross-tool interop verified (Python stdlib base64 == sodium base64).
- scripts/sign-xmrig-release.sh: keygen / sign / pubkey helper (PyNaCl = same libsodium ed25519)
to produce the .sig assets and the public key to pin.
No behavior change until a key is pinned. Both variants build; suite passes; live worker
re-verified (signatures off by default).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Adds util/XmrigUpdater: a background-thread service (mirrors util/Bootstrap) that pulls
the latest DRG-XMRig release from the project's Gitea, verifies it, and installs the miner
binary into the daemon directory. Service layer only; the mining-tab UI hook comes next.
Flow: GET /api/v1/repos/DragonX/drg-xmrig/releases/latest -> pick the asset matching this
platform (…-linux-x64.zip / …-win-x64.zip; no macOS build -> graceful "unavailable") ->
download (libcurl, TLS verified) -> verify the archive SHA-256 -> extract with miniz,
flattening the versioned subdir the archive nests the binary in -> verify the extracted
binary's SHA-256 in memory before writing it -> atomic install (+chmod +x on POSIX). On
Windows also extracts WinRing0x64.sys; config.json/README.md are skipped.
Security (download-and-execute): TLS is verified, and BOTH the archive and the inner binary
are checked against the SHA-256 checksums published in the release body (parsed as
"<hex> <name>" lines) — install is refused on a missing or mismatched checksum.
Split into a pure core (xmrig_updater_core.cpp: release parse, asset/platform match, checksum
parse, SHA-256) and the curl/miniz worker (xmrig_updater.cpp). The core is unit-tested against
a real captured release fixture (tests/fixtures/xmrig/release_latest.json); an env-gated
(DRAGONX_TEST_NETWORK=1) integration test exercises the worker live and was verified end-to-end
on linux-x64 (inner binary SHA-256 matches the published value). Both variants build; suite passes.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Adds a fail-open, local-only gate that decides whether the lite wallet may run,
so a post-release issue can disable it and rollout can be staged — without any
phone-home (privacy posture: no runtime network fetch; the per-install rollout
bucket is a hashed, never-transmitted local id).
- wallet/lite_rollout_policy.{h,cpp}: a pure decision core. Order — emergency env
kill-switch (absolute) -> local override -> manifest gates (global enable /
version floor-ceiling / blocklist / staged-rollout permille) -> fail-open allow.
Plus a JSON manifest loader (missing/invalid -> fail-open) and FNV-1a bucketing.
- Threads the decision through LiteWalletController -> LiteWalletLifecycleService:
new availability() reason RolloutDisabled blocks create/open/restore and surfaces
the gate's user-facing message via the lifecycle status.
- App::rebuildLiteWallet() resolves it from: DRAGONX_LITE_KILL_SWITCH (env), the
lite_rollout setting (auto/force_on/force_off), and a locally-cached manifest at
<config-dir>/lite_rollout.json. install id generated once via libsodium.
- Settings: persist lite_rollout override + the install id.
A signed remote fetcher can populate the manifest cache later without touching the
policy. Unit-tested (version compare, bucketing, override/env precedence, manifest
gates, staged rollout, loader fail-open, controller integration) and runtime-verified
on Linux (env kill-switch, manifest disable, control sync). Both variants build;
full suite passes; hygiene clean.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
lite_backend_artifact_{contract,resolver}.{cpp,h} (~1,960 lines) were
app-linked but never invoked: all 14 public entry points
(evaluateLiteBackendArtifactContract/Resolver, evaluateLiteBackendActivation-
Readiness, the resolve*/...Name helpers) had zero callers in the app, the
lite_smoke tool, build scripts, or surviving tests. The real backend load
path (LiteClientBridge::linkedSdxl) uses direct litelib_* externs, and the
DRAGONX_ENABLE_LITE_BACKEND symbol check is done in CMake against the symbols
inventory (FATAL_ERROR on a missing symbol) — not via these C++ files. The
files were saturated with churn markers (disabled / dry-dispatch / scaffold).
- Delete the four artifact files and their 8 CMakeLists references.
- Drop the orphaned test cruft in test_phase4.cpp: the contract include,
5 type aliases, and 3 never-called helpers (heapConstructPlanResult,
makeReadyLiteBackendArtifactProvenance, liteBackendArtifactContractHasIssue)
left over from the already-removed bridge-runtime tests.
- Correct the CLAUDE.md lite-wallet description (it credited these files with
backend validation that CMake actually performs) and drop the stale
lite_bridge_runtime mention.
Both variants build; full test suite passes; source-hygiene check clean.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
lite_bridge_runtime.{cpp,h} was ~25k lines of dry-dispatch / dynamic-loader
scaffolding that the shipping wallet never used: 0 of its 122 public types
reached the app binary. The only live code on the bridge path was the
owned-string memory-safety helper — LiteClientBridge::linkedSdxl() already
loads the backend via direct litelib_* externs in lite_client_bridge.cpp.
- Extract LiteBridgeOwnedString + liteBridgeRuntimeTakeOwnedString into
src/wallet/lite_owned_string.{h,cpp} (the copy-before-free / free-once /
wipe / "Error:"-classify boundary), with the runtime-friend coupling removed.
- Point lite_client_bridge.cpp at the new header.
- Delete lite_bridge_runtime.{cpp,h} and the 16 runtime-only tests +
their fixtures/aliases in test_phase4.cpp; keep the 5 owned-string tests
(retargeted) and restore testGeneratedResourceBehavior, which had been
caught in the runtime-test line range.
- Swap the CMake source/header references.
Both variants build; full test suite passes; source-hygiene check clean.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Move 8 dated-snapshot / dormant-feature docs to docs/_archive/ (git-ignored,
kept locally), leaving only repo essentials + the active lite plan tracked:
- docs/codebase-audit-2026-04-27.md, docs/codebase-overview.md — "current as of
2026-04-27" snapshots, superseded by CLAUDE.md and the v2 plan.
- docs/ui-static-state.md — Phase-9-era UI static-state review snapshot.
- docs/chat-port-feasibility-2026-05-06.md, docs/chat-protocol-spec-2026-05-06.md
— superseded/old-"Batch"-framing docs for the dormant, gated-OFF chat module.
- tests/fixtures/hushchat/{README,CAPTURE_MANIFEST,IMPORT_CHECKLIST}.md -> docs/
_archive/hushchat/ — human docs (not tool input) for the dormant chat fixtures;
the .json fixtures the HushChatFixtureCheck tool globs remain tracked.
These docs only cross-referenced each other (no code/CMake/script refs); no
dangling tracked links remain. Tracked .md (non-libs): 14 -> 6.
Co-Authored-By: Claude Opus 4.8 <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>
Wire the backend passphrase-encryption commands into LiteWalletController:
- encryptWallet / decryptWallet (take passphrase by value, securely wipe it,
save after), unlockWallet / lockWallet (bring spending keys into/out of
memory), and encryptionStatus() -> {encrypted, locked}. All return
failure-safe results; errors arrive as {"error":..} or "Error:" (handled).
- Fold encryptionstatus into refreshModel() (polled every cycle, available even
mid-sync since it reads local wallet state) and apply it in
applyLiteRefreshModelToWalletState, so WalletState.isEncrypted()/isLocked()
track the backend — which gates the existing locked/auto-lock UI.
Backend contracts verified against the SDXL source: encrypt/unlock/decrypt take
the passphrase as the single arg; lock takes none; encryptionstatus returns
{"encrypted","locked"}; ops return {"result":"success"} / {"error":..}.
Tests: testLiteWalletControllerEncryption drives encrypt -> lock -> unlock ->
decrypt via encryptionStatus(), checks empty-passphrase + closed-wallet rejection,
and that the status folds into WalletState. Fake models the state machine.
GUI wiring (encrypt in Settings, unlock prompt / lock action) is the follow-up;
the backend create flow remains unencrypted by default until encrypt is run.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Verified against the SDXL Rust source that the backend auto-saves only on
new-address / import / rescan; it does NOT save after sync, send, or shield, and
litelib_shutdown merely sets a flag. So without intervention a first sync
(~30 min) and any sent transaction are lost on restart.
The controller now triggers the backend `save` at exactly the right points:
- after the detached `sync` completes — and BEFORE syncDone_ is set, so a
syncComplete() observer always sees a fully persisted wallet;
- after a successful send / shield (the doSend/doShield cores; skipped on
failure so a failed broadcast doesn't write);
- a guarded best-effort flush in the destructor, only when syncDone_ and no
broadcast is in flight, so shutdown never blocks on the wallet lock held by an
uninterruptible scan or in-progress proving;
- plus a public saveWallet() for explicit/periodic saves.
Wallet-file crash recovery (.dat / .dat.bak rotation) is already handled inside
the backend.
Tests: testLiteWalletControllerM5Persistence proves saves fire after
sync/send/shield and explicit saveWallet(), and do NOT fire on a failed send or
with no wallet open (fake gains a save counter). Plan doc updated; M5b
(packaging/CI/signing/rollout) remains.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Add the spend & backup surface to LiteWalletController, with the real SDXL
backend contracts verified against the Rust source:
- send / shield: ASYNC (detached broadcast thread + takeBroadcastResult() slot,
mirroring the sync thread's shared-lifetime pattern, since sapling proving can
take seconds), plus synchronous *Blocking cores for tests. send uses the
JSON-array form ([{address,amount,memo}]) because litelib_execute passes the
whole args string as ONE argument (no whitespace split) — the space-separated
CLI form would never parse. send/shield report failure via {"error":..} in the
body (NOT an "Error:" prefix), so the result is derived from the parsed JSON.
- importKey: auto-detects transparent WIF (U/5/K/L -> timport) vs shielded key
(-> import); takes the key by value and securely wipes it before returning.
- exportPrivateKeys / exportSeed: synchronous local reads returning SECRET
material (flagged: no logging; caller wipes after the user saves the backup).
- broadcast thread is detached in the dtor (captures shared bridge + flag + slot,
never `this`), so it is safe to outlive the controller.
Tests: testLiteWalletControllerM4 drives send (success / no-recipients /
{"error":..} / async-slot delivery / pre-open rejection), shield, export, seed,
and import (shielded + WIF + pre-open). Fake backend returns the real command
shapes + a g_liteFakeSendFails error toggle.
GUI wiring (send_tab button, backup/import UI) is deferred like the M3 UI hop
(GUI-unverifiable here). Plan doc updated.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Re-audited this session's lite-wallet changes (originally written at medium
effort) and fixed the genuine issues found:
- walletReady (open path): litelib_initialize_existing returns the bare string
"OK", which is NOT valid JSON, so the previous `json::accept(value)` check
marked a *successful* open as not-ready. Key off a non-empty success response
instead (the bridge already maps "Error:"/null to failure). Drops the now
unused nlohmann include.
- sync progress: while the detached sync thread is still running, syncDone_ is
authoritative — don't surface the backend's transient idle syncstatus
({"syncing":"false"} -> parser progress=1.0/complete=true) as a misleading
100%/done. Force complete=false and zero the bogus 1.0 in the progress model.
- per-address balance: also exclude `pending` outputs (notes/utxos from an
unconfirmed received tx) so per-address figures match confirmed/available.
- secret wiping: the settings page left the page-local request copies
(input.request.*Request.{passphrase,seedPhrase}) unwiped, and the
validation-only fallback path wiped nothing. Replace the single-path memzero
with an RAII scrubber that wipes both the UI char buffers and the request
string copies on every return path.
- concurrency: document that concurrent bridge->execute() is intentionally
unguarded — litelib serializes wallet access internally via
Arc<RwLock<LightWallet>>, so a C++ mutex is unnecessary and would defeat the
sync/syncstatus concurrency the design relies on. syncLaunched_ -> atomic.
Tests: fake backend now returns the real init shapes (seed object for
create/restore, bare "OK" for open) and a new open-path case guards the
walletReady regression. Removed an unreliable alloc==freed leak assert from the
thread-bearing controller test (kept in the thread-free bridge test). Also fixed
a stray CMake indent and removed ~220MB of untracked build/debug scratch.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- LiteWalletController::newAddress(shielded) runs the backend "new" command ("zs"/"R" ->
do_new_address), parses the ["addr"] response, and returns the new address; the next
refresh lists it. Fast (local derivation), safe on the UI thread.
- fake_lite_backend returns ["zs1fakenew"]/["R1fakenew"] for "new" by args.
- testLiteWalletControllerNewAddress covers shielded/transparent + no-wallet error.
Also confirmed (no code needed): the sync-progress indicator already works for lite —
balance_tab reads state.sync.* which M2b-3 populates. Per-address balances landed in M2.
Remaining M3 is pure UI wiring (receive_tab button -> newAddress, loading/empty states),
which isn't verifiable without a GUI session.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
applyLiteRefreshModelToWalletState now derives each address's balance by summing its unspent
notes/utxos (excluding spent and unconfirmed-spent outputs) instead of the aggregate-only
zeros, so the Receive/Balance UI shows per-address amounts. The notes parser shape is
confirmed against do_list_notes in the backend source.
testLitePerAddressBalances covers the summing + spent-exclusion. Completes M2.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
LiteWalletGateway::refresh() aborted the entire refresh on the first command whose bridge
call or parse failed — which turned a single real-backend shape mismatch (e.g. syncstatus)
into a total, empty-everything refresh. Since the balance/addresses/list real shapes are
still unverified and we've already hit shape drift twice, make refresh resilient:
- Run every planned command; assembleLiteWalletRefreshBundle already skips failed results.
- result.ok = any usable data came back (bundle.complete still reflects all-succeeded).
- One command's failure now degrades gracefully — the other sections still populate.
testLiteWalletGatewayRefreshSkipsFailedCommand (fake balance returns invalid JSON) asserts
the refresh still succeeds with addresses/transactions/info populated and balance skipped.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The backend `sync` command is a blocking, uninterruptible full chain scan (do_sync(true);
does not honor the shutdown flag), and balance/list block until synced. Previously
startSync() ran on the main thread (would freeze wallet creation) and the worker could
block, making the destructor join() hang at shutdown.
Redesign:
- bridge is now std::shared_ptr<LiteClientBridge>, shared with a detached sync thread so
detaching is safe and litelib_shutdown isn't called while a running sync still holds the
bridge; the controller's own ref prevents premature shutdown during normal operation.
- startSync() launches the blocking `sync` on a detached thread (non-blocking; never joined).
- refreshModel() gates on syncDone_: while syncing it publishes syncstatus progress only;
once synced it does the full balance/addresses/list refresh (now fast).
- destructor joins only the fast poll worker and detaches the sync thread -> no hang.
- syncComplete() accessor added.
Tests (deterministic, via a blocking-sync fake; counters made atomic for the detached
thread): testLiteWalletControllerShutdownDoesNotHangDuringSync (destructor returns <1.5s
with sync blocked); refresh/worker tests wait for syncComplete()/a balance-bearing model.
Stable across repeated runs; lite+backend and full-node apps build clean.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The real backend returns syncstatus as idle {"syncing":"false"} (string) or in-progress
{"syncing":"true","synced_blocks":N,"total_blocks":M} (commands.rs:83-87), but
parseLiteSyncStatusResponse hard-required the block fields and failed whenever the wallet
wasn't actively syncing — so sync/progress never updated in the real app.
- Read "syncing" as a string; require synced_blocks/total_blocks only when syncing=true;
idle => complete, synced/total 0.
- fake_lite_backend syncstatus now uses the real "syncing":"true" shape.
- testLiteSyncStatusParserRealShapes covers idle, in-progress, and missing-counts-while-syncing.
- Verified against the live backend via lite_smoke --refresh (syncstatus parse_ok=1).
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>
Shared-bridge refactor (litelib is a global singleton; every LiteClientBridge calls
litelib_shutdown() on destruction, so services must not each own one):
- LiteWalletLifecycleService, LiteWalletGateway, LiteSyncService now take a non-owning
LiteClientBridge*; LiteWalletController owns the single bridge and passes &bridge_.
Sync + controller refresh:
- LiteSyncService::startSync executes the real "sync" command (was a stub).
- LiteWalletController: startSync() (auto-fires when a wallet becomes ready) and
refreshWalletState(WalletState&) — polls syncstatus, runs gateway.refresh(), maps the
bundle, applies balances/addresses/transactions/sync into WalletState.
Tests:
- fake_lite_backend.h returns command-shaped JSON (per tests/fixtures/lite/result_parsers.json).
- testLiteWalletControllerRefreshPopulatesState drives the full path against the fake.
- Surfaced + worked around a real integration issue: parseLiteInfoResponse requires
latest_block_height and the gateway aborts the whole refresh on the first command's
parse failure (fragile vs partial backend responses; hardening tracked for M2b-3).
Verified: ctest green; lite+backend, full-node, lite-no-backend apps + lite_smoke build clean.
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>
Add an encrypted SQLite transaction history cache with cached tip metadata and
per-address shielded scan progress so startup and full refreshes avoid
re-scanning every z-address while still invalidating on wallet/address/rescan
changes.
Improve wallet history loading by paging transparent transactions, preserving
cached shielded and sent rows, keeping recent/unconfirmed activity visible, and
classifying mining-address receives. Show z_sendmany opid sends immediately in
History and Overview, pin pending rows through refreshes, and apply optimistic
address/balance debits until opids resolve.
Add timestamped RPC console tracing by source/method without logging params or
results, reduce redundant refresh/RPC calls, and cache Explorer recent block
summaries in SQLite.
Expand focused tests for transaction cache encryption, scan-progress
persistence/invalidation, history preservation, operation-status parsing,
pending send visibility, and Explorer/RPC refresh behavior.
Replay cached outgoing viewtransaction entries during transaction refresh so shielded sends created from the wallet remain in the History tab after send tracking is cleared.
Keep incomplete tracked sends retryable, preserve cached send timestamp/confirmation metadata, and emit a send placeholder from gettransaction metadata when viewtransaction enrichment is not yet available.
Add regression coverage for cached sends, retryable empty entries, placeholder sends, and send txid cleanup behavior.