5 Commits
Author SHA1 Message Date
DanS cc2d383166 Merge dev: lite6 + lite7 default servers 2026-09-02 04:20:45 -05:00
DanS 290d3c31a2 Merge dev: i18n back-fill (313 strings × 8 languages) 2026-09-02 03:30:16 -05:00
DanSandClaude Opus 4.8 0e78fe02e4 Merge dev: macOS Berkeley DB build fix (build.sh)
Derive the macOS BDB depends triple from the build arch + pass the BDB paths
to CMake, so the full-node mac release builds the wallet-rebuild helper on
Intel (x86_64) as well as Apple Silicon.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-09-02 02:29:37 -05:00
DanSandClaude Opus 4.8 06d34dff5e Merge dev: ObsidianDragon 2.0.1 / ObsidianDragonLite 1.1.0
Brings the full 2.0.x line to master: security-audit remediations, diagnostics/
logging, mining overhaul, HiDPI/UI audit, wallet recovery + migrate-to-seed,
daemon-startup hardening, seed-phrase backup, in-app FAQ, i18n + 8-language
translations, chat delete/block, per-frame render perf, and the Lite variant
(1.1.0) with its variant-aware FAQ. Windows app exe now stripped.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-09-02 01:14:27 -05:00
DanSandClaude Fable 5 c252e60f7f docs: add v2.0.0 and lite-v1.0.0 release notes
Release notes for ObsidianDragon v2.0.0 (full-node) and ObsidianDragonLite
v1.0.0, with verified SHA-256s for the Linux/Windows artifacts and the
osxcross-built macOS artifacts. Both tags point at this commit's parent
(fffee9f); tables cover AppImage/zip/exe/dmg/app.zip per variant.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-23 22:33:58 -05:00
20 changed files with 181 additions and 730 deletions
+2 -2
View File
@@ -15,7 +15,7 @@ if(APPLE)
endif()
project(ObsidianDragon
VERSION 2.0.2
VERSION 2.0.1
LANGUAGES C CXX
DESCRIPTION "DragonX Cryptocurrency Wallet"
)
@@ -26,7 +26,7 @@ set(DRAGONX_VERSION_SUFFIX "")
# ObsidianDragonLite is versioned INDEPENDENTLY of the full-node app above. The active variant's
# version flows to the generated header, the Windows .rc/manifest, and build.sh's release names via
# DRAGONX_APP_VERSION* (resolved in the lite/full block below).
set(DRAGONX_LITE_VERSION "1.1.1")
set(DRAGONX_LITE_VERSION "1.1.0")
set(DRAGONX_LITE_VERSION_SUFFIX "")
# C++17 standard
+61
View File
@@ -0,0 +1,61 @@
**DragonX (DRGX) Lite Desktop Wallet — v1.0.0**
ObsidianDragonLite is a lightweight companion to the ObsidianDragon full-node wallet. It skips the embedded full node entirely — **no multi-gigabyte blockchain download and near-instant startup** — by connecting to a DragonX lite-wallet (lightwalletd-style) server, while keeping the same native ImGui interface and shielded-first workflow.
This is the **first release** of the Lite variant.
---
## Why Lite?
- **No blockchain to download.** Sync in seconds instead of hours; a few MB of state instead of many GB.
- **Same wallet, lighter footprint.** The familiar ObsidianDragon UI, shielded and transparent addresses, and address book — without running a node or miner.
- **Portable.** A single self-contained binary that stores its data in its own `ObsidianDragonLite` folder, so it coexists cleanly alongside the full-node wallet.
---
## Features
- **Fast, node-free operation** — connect to a DragonX lite-wallet server and sync in seconds; minimal disk and RAM.
- **Wallet lifecycle** — create a new wallet, restore from a seed phrase, or open an existing one; wallets auto-open on startup, with a first-run welcome prompt and **guided seed backup** on creation.
- **Shielded + transparent** — send, shield, and receive DRGX; per-address balances computed from unspent notes and UTXOs.
- **Keys & seed** — export your seed phrase and keys, and import keys, from Settings.
- **Passphrase encryption** — encrypt, unlock, lock, and decrypt the wallet; you're prompted to unlock at send time and on startup when the wallet is locked.
- **Encrypted messaging (HushChat)** — built-in **Contacts** and **Chat** with a seed-derived, **SilentDragonXLite-compatible** identity and a seed-encrypted local message store, so you can message other DragonX users end-to-end.
- **Server management** — a built-in **server browser with automatic failover**; switch servers with live reconnect/recovery, and a **"Redownload blocks"** action to rescan from the server.
- **Status & diagnostics** — a **Network tab** showing connection and sync status, plus an **interactive Console** for running backend commands and copyable error output.
- **Multi-language UI** — full internationalization covering 8 languages: German, Spanish, French, Japanese, Korean, Portuguese, Russian, and Chinese.
- **Cross-platform** — native builds for Linux (AppImage + zip), Windows (portable exe + zip), and macOS (DMG + .app), x86-64.
- QR code generation for receiving addresses
- Extensive theme and appearance options
---
## How it differs from the full-node wallet
- **No embedded `dragonxd`, no mining, no local blockchain explorer.** Chain data comes from the lite-wallet server rather than being verified locally by a full node — a lighter footprint in exchange for trusting the server for chain state.
- Everything key-related stays **on your device**: seed, keys, and (when enabled) the encryption passphrase never leave the machine.
---
## Downloads
| File | SHA-256 |
|------|---------|
| ObsidianDragonLite-1.0.0.AppImage | `a1459d6081124a6b8df47aa898b60c0237875e8cbb132e4fd8ae637673055ed4` |
| ObsidianDragonLite-1.0.0-Linux-x64.zip | `573af502a6372334572e4dc88bd26e0cf36ed543b9df1e835fd8e4abd295108d` |
| ObsidianDragonLite-1.0.0.exe | `28659efb770fa573bd1b1d0fcab1c1a3f9c7e46574185c3916cc9f669684852c` |
| ObsidianDragonLite-1.0.0-Windows-x64.zip | `f6434a931e873e5936ebe249b044d6c7a413694767c667374c91ee438259b50c` |
| ObsidianDragonLite-1.0.0-macOS-x86_64.dmg | `10f976f1453e6b1978cb7a85026fd033c85a10c1e66f436475aa628379a298c7` |
| ObsidianDragonLite-1.0.0-macOS-x86_64.app.zip | `3e7259c363f7be2e9027c5282c94b742f4be280c998203752dc0a0551f6ab68b` |
## System Requirements
- **Linux:** x86-64, glibc 2.31+ (Ubuntu 20.04+, Fedora 33+, etc.)
- **Windows:** x86-64, Windows 10 or later
- **macOS:** x86-64 (Intel; runs on Apple Silicon via Rosetta 2), macOS 10.15 Catalina or later
- Network access to a DragonX lite-wallet server.
## License
Released under the **GPLv3**. See [LICENSE](https://git.dragonx.is/DragonX/ObsidianDragon/src/branch/master/LICENSE) for details.
-43
View File
@@ -1,43 +0,0 @@
**DragonX (DRGX) Lite Desktop Wallet — v1.1.1**
ObsidianDragonLite is the lightweight, node-free companion to ObsidianDragon — it connects to a DragonX lite-wallet server for near-instant startup, with the same native ImGui interface and shielded-first workflow. **v1.1.1 is a maintenance release** centered on an important wallet-encryption fix.
---
## ⚠️ Security fix — please read
**Creating or restoring a Lite wallet with a passphrase now actually encrypts it.**
In prior versions, the create / restore / open flow used by the Settings UI **silently discarded the passphrase you entered**:
- A **newly created or restored** wallet was saved to disk **unencrypted** — the seed and spending keys were stored in the clear even though you set a passphrase.
- **Opening** an already-encrypted wallet did not unlock it with the supplied passphrase (it opened locked and re-prompted).
v1.1.1 fixes this: create/restore now **encrypt** the wallet with your passphrase (the backend encrypts and locks it), and open now **unlocks** it.
**What you should do:** if you created or restored a Lite wallet **with a passphrase** on an earlier version, treat that wallet's on-disk data as unencrypted. Open it in v1.1.1 and re-apply encryption from **Settings → encrypt** (and, if you're concerned the plaintext wallet file may have been exposed, move the funds to a freshly created encrypted wallet). Wallets you never set a passphrase on are unaffected.
---
## Also fixed
- **Chinese, Japanese, and Korean FAQ text renders correctly.** Several Help/FAQ titles showed `??` instead of characters (e.g. 什么, 门) after the previous translation back-fill; the bundled CJK font subset now includes those glyphs.
---
## Downloads
macOS builds are x86-64 (Intel; run on Apple Silicon via Rosetta 2).
| File | SHA-256 |
|------|---------|
| ObsidianDragonLite-1.1.1.AppImage | `323ff5f8658a5550aa12678ce4be37e32a2bfd3f3b0543074cc86a4267895dcf` |
| ObsidianDragonLite-1.1.1-Linux-x64.zip | `e8604e479aed7e020946df45bf0d9373401354907e10565011694efa9978e631` |
| ObsidianDragonLite-1.1.1.exe | `4cdcf291fe43c4fed04258aa55bb4e9b591f52780f9db5fb8f8bef522c43a351` |
| ObsidianDragonLite-1.1.1-Windows-x64.zip | `c387bfcda508f32195c41fabec9f23d8b094d693a5dba41fbf8ec4abab6f2f6e` |
| ObsidianDragonLite-1.1.1-macOS-x86_64.dmg | `bb0c491cb8f90ae55d162ec7c04c8f30988fa2f79c7911471220cddab44f99a5` |
| ObsidianDragonLite-1.1.1-macOS-x86_64.app.zip | `a2a75fe40bf1f4db3abed8206dcc40d9fc74d8db2df7704afc9c834106b1c247` |
---
For the full feature set (node-free operation, wallet lifecycle, shielded + transparent send/shield/receive, HushChat, server browser with failover, 8-language UI), see the **lite-v1.0.0** release notes — v1.1.1 changes only the items listed above.
+86
View File
@@ -0,0 +1,86 @@
**DragonX (DRGX) Full-Node Desktop Wallet — v2.0.0**
This is ObsidianDragon, a native ImGui-based wallet for the DragonX network. It ships with an embedded full-node daemon (`dragonxd`) and an integrated CPU miner (DRG-XMRig), giving users a complete, self-contained experience on Linux, Windows, and macOS.
---
## What's New in v2.0.0
v2.0.0 is a major release. It adds **end-to-end encrypted HushChat messaging** and **BIP39 seed-phrase wallets** (with migrate-to-seed for legacy wallets), lets the wallet **update its own node and miner** with each install cryptographically verified, and ships a **security-hardening pass** across the updater, RPC, and secret-storage surfaces — plus full support for the **new multi-threaded `dragonxd`** and a large stability and UX overhaul.
### New Features
- **Encrypted messaging (HushChat)** — new **Contacts** and **Chat** tabs bring DRGX-native, end-to-end encrypted messaging. Your chat identity is derived from your wallet seed, messages are encrypted with libsodium (XChaCha20-Poly1305 / secretstream) and stored in a **seed-encrypted local database**, and the wire format is interoperable with SilentDragonX / SilentDragonXLite.
- **BIP39 seed-phrase wallets & migrate-to-seed** — new wallets are backed by a mnemonic **seed phrase** you can back up from Settings. Existing **legacy wallets can be migrated into a seed wallet** — the wallet mints a new mnemonic wallet, sweeps your funds to it, and adopts it only once the sweep is mined, keeping a timestamped `wallet.dat` backup throughout. (The bundled `dragonxd` supports the required mnemonic RPCs; older nodes degrade gracefully.)
- **In-app daemon updater** — Settings → **Node & Security → Daemon binary → "Check for updates…"** downloads the latest `dragonxd` from the project Gitea, verifies its **SHA-256 and a detached ed25519 signature** before installing, and replaces the binary **atomically while the node keeps running** (the new build takes effect on the next daemon start). A two-pane version picker lets you pin, downgrade, or install any published/pre-release build.
- **In-app miner updater** — an **"Update miner…"** action in the Mining tab fetches, verifies (SHA-256 + ed25519), and installs the latest DRG-XMRig, with the same version picker and a display of current vs. latest.
- **Daemon binary management** — the wallet no longer auto-overwrites a node you've dropped in; a dedicated Settings panel shows the managed binary and gathers all node actions (restart, rescan, repair) onto one toolbar.
- **Repair Wallet** — a one-click `-zapwallettxes=2` recovery in Settings, plus automatic wallet reconciliation after a bootstrap and runtime rescan support for pruned nodes.
- **Explorer search** — fuzzy, live (debounced) filtering of the block list by partial hash or height.
- **History sorting & fast load** — sort transactions by date or amount, a "Loading older history (N%)" indicator during the initial bulk load, and persisted history that surfaces pending sends immediately.
- **Smarter mining** — the thread benchmark now measures **sustained (thermally-throttled) hashrate** instead of an inflated initial burst, with GPU-aware idle mining and corrected idle thread scaling.
### Security & Integrity
- **Mandatory signature verification** — both the daemon and miner updaters **require** a valid ed25519 signature (checked against a key pinned in the wallet) in addition to the SHA-256; an install is refused if the signature is missing or invalid.
- **Hardened secret handling** — RPC credentials are wiped from memory after use, RPC responses are size-capped and scrubbed of secrets in logs, generated node config is written **owner-only (0600)**, and secrets copied to the clipboard **auto-clear**.
- **Safe on-disk writes** — settings, address book, and secret files are written **atomically with owner-only permissions**; the PIN vault fsyncs its secure-delete overwrite; SQLite cache growth is bounded.
- **Recipient validation** — sends validate recipient address checksums (Base58Check + Bech32) before building a transaction.
- **Path-traversal protection** — archive extraction (chain bootstrap and the updaters) rejects any entry that would escape the target directory (zip-slip), and the bootstrap fails closed on a missing checksum rather than trusting an unverified archive.
- **Robustness** — malformed RPC error JSON is guarded and sends are single-flight, preventing duplicate/ill-formed submissions.
### Improvements
- **New multi-threaded daemon support** — live Sapling note-witness rebuild progress, accurate sync-speed display, reliable rescan-completion detection, and RPC polling throttled during sync so block download isn't slowed.
- **Networking** — DragonX DNS seed nodes, `-maxconnections` passed to the daemon, and a peer count that stays current on every tab.
- **Node resilience** — non-blocking warmup so you can connect while the daemon is still initializing, a live daemon-console tail on the startup overlay, fast-failing connect probes, and mid-session disconnect detection that keeps the UI responsive (in-flight RPC calls abort cleanly on disconnect/shutdown).
- **Address list** — modernized with drag-to-transfer, labels, and view-only handling.
- **UI / DPI** — overlay dialogs scale correctly with the font/DPI setting, improved CJK font rendering, native language names in the language picker, and format-incompatible translations are rejected.
- **Settings** — an "Open data folder" button and confirmation modals for rescan and restart-daemon.
### Bug Fixes
Extensive fixes across history (shielded-tx ordering, stuck "refreshing history" banners, unconfirmed-badge stickiness), sends (correct fee passed to `z_sendmany`, fast-lane worker restart on reconnect, note-selection fee-gap workaround), rescan (accurate completion detection, no false "complete", no per-second error flood), RPC (mid-session disconnect handling, stale-refresh invalidation), and UI layout/i18n. See the commit history for the full list.
---
## Features
- **Full-node wallet** — send, receive, and verify DRGX transactions with a bundled `dragonxd` daemon; no external setup required.
- **Self-updating** — verify-and-install the latest node and miner from within the app (SHA-256 + ed25519 signature enforced).
- **Encrypted messaging** — built-in HushChat with a seed-derived identity, end-to-end encryption, and a seed-encrypted local message store, interoperable with SilentDragonX / SilentDragonXLite.
- **Seed-phrase wallets** — BIP39 mnemonic backup, and migrate-to-seed to upgrade a legacy wallet into a seed wallet.
- **Built-in CPU mining** — start/stop DRG-XMRig from the Mining tab with real-time hashrate and pool statistics, mine-when-idle support with configurable delay, and sustained-hashrate benchmarking.
- **Multi-language UI** — full internationalization covering 8 languages: German, Spanish, French, Japanese, Korean, Portuguese, Russian, and Chinese.
- **Cross-platform** — native builds for Linux (AppImage + zip), Windows (portable exe + zip), and macOS (**universal** DMG + .app); Linux/Windows x86-64, macOS Intel + Apple Silicon.
- Shielded (z-address) and transparent (t-address) send/receive
- QR code generation for receiving addresses
- Transaction history with memo support, date/amount sorting, and pending-send tracking
- Blockchain explorer with live block/transaction search
- Blockchain rescan and Sapling witness-rebuild with status-bar progress
- Repair Wallet and daemon-binary management in Settings
- Built-in console
- Extensive theme and appearance options
---
## Downloads
| File | SHA-256 |
|------|---------|
| ObsidianDragon-2.0.0.AppImage | `34c9ea57ec27bf415e59d2890d995ed9069bfce04d979d7d8d58aa18f4570aa1` |
| ObsidianDragon-2.0.0-Linux-x64.zip | `563004b4e45650dbfa9411d62fa1c59a3ca2199bd43a647b1614f2683bbdb5fa` |
| ObsidianDragon-2.0.0.exe | `56100ae59dc3c67445d015cac5412ca40465bf459ba182c5ad2477a3b95ff548` |
| ObsidianDragon-2.0.0-Windows-x64.zip | `ce9d067f36c5296288a473d3c2cceca7fcf807672a93a2084afc53c3b6e780d9` |
| ObsidianDragon-2.0.0-macOS-universal.dmg | `8a01b6c0c2b5bebd64a222ba4a5ad04a1b8851138f28458b5c7f767fbb65db66` |
| ObsidianDragon-2.0.0-macOS-universal.app.zip | `1bce1e5be15d9a2f3c6f42c95b23a0e89df142a73d65895252e4d8f50d19f541` |
## System Requirements
- **Linux:** x86-64, glibc 2.31+ (Ubuntu 20.04+, Fedora 33+, etc.)
- **Windows:** x86-64, Windows 10 or later
- **macOS:** Intel & Apple Silicon (universal binary), macOS 10.15 Catalina or later
## License
Released under the **GPLv3**. See [LICENSE](https://git.dragonx.is/DragonX/ObsidianDragon/src/branch/master/LICENSE) for details.
-41
View File
@@ -1,41 +0,0 @@
**DragonX (DRGX) Full-Node Desktop Wallet — v2.0.2**
ObsidianDragon is a native ImGui-based full-node wallet for the DragonX network, with an embedded `dragonxd` daemon and the integrated DRG-XMRig CPU miner. **v2.0.2 is a maintenance release** — a focused set of transaction-history, sync-safety, and correctness fixes on top of v2.0.x. No configuration changes are required; just replace the binary.
---
## What's Fixed in v2.0.2
### Transaction history
- **Outgoing shielded sends now appear in History.** Sends *from* a shielded (z-)address — both **z→z** (shielded-to-shielded) and **z→t** (deshield to a transparent address) — were missing from the History tab, because the daemon's `listtransactions` doesn't report them and `z_viewtransaction` doesn't expose the transparent leg. The wallet now enumerates them via `z_listsentbyaddress`, so **your History matches your balance.** (Your funds were never affected — only the display.)
- **More accurate send rows** — a single transaction with two equal-value outputs to different recipients is no longer collapsed into one row, and a transparent self-send / change output is no longer double-counted as both sent and received.
### Sync & fund safety
- **A node that loses all its peers is no longer reported as "synced."** Previously, a wallet whose node dropped to **0 peers** could keep showing a stale balance as if it were current — and allow a Send against it. The wallet now treats a node with no peer-derived chain tip as *not synced*, and a new **"Connected, but no network peers — the node can't sync"** banner explains the stall instead of silently looking healthy.
- **Migrate-to-seed hardening.** If a migrate-to-seed was interrupted (app or daemon restart) *after* the sweep transaction had broadcast but *before* it confirmed, the wallet could mistake the temporarily-zero balance for an empty wallet and adopt the new seed wallet — swapping `wallet.dat` — before the sweep was mined. It now records that a sweep is in flight and **will not swap your wallet until the sweep confirms**, so funds can't be stranded in the timestamped backup.
- **Fixed a rare crash** caused by a data race on the daemon-status text (the daemon-monitor thread writing it while the UI thread read it).
### Display / internationalization
- **Chinese, Japanese, and Korean FAQ text renders correctly.** Several Help/FAQ titles showed `??` instead of characters (e.g. 什么, 门) after the previous translation back-fill; the bundled CJK font subset now includes those glyphs.
---
## Downloads
macOS builds are x86-64 (Intel; run on Apple Silicon via Rosetta 2).
| File | SHA-256 |
|------|---------|
| ObsidianDragon-2.0.2.AppImage | `9bf7328965c6348113ba7a30b0284f15787c635af17efae3dfa8a87e56e10c9b` |
| ObsidianDragon-2.0.2-Linux-x64.zip | `1371880c4187a94d4cb56d535471142c5f1a8053ef73fffd99d59e7b78c75c42` |
| ObsidianDragon-2.0.2.exe | `673347d2696fb6f5bd09dc25e415d5ae8e532abde75b8d48c3cd26545461de50` |
| ObsidianDragon-2.0.2-Windows-x64.zip | `f5ea7c45e66c6c1b90a8127f96dee04235691acb913ddc86327f2060798cd28c` |
| ObsidianDragon-2.0.2-macOS-x86_64.dmg | `7038580807a8e93e5443bdc04f2aede3ae1c30e5bc9f8776a207edb2e87771d1` |
| ObsidianDragon-2.0.2-macOS-x86_64.app.zip | `78159512ca684b95528231fd46f2d07b5bcc6f2da4d815fc1ecb2ff52127f41c` |
---
For the full feature set (HushChat messaging, seed-phrase wallets & migrate-to-seed, in-app node/miner updaters, built-in mining, 8-language UI), see the **v2.0.0** release notes — v2.0.2 changes only the fixes listed above.
Binary file not shown.
+16 -60
View File
@@ -11,7 +11,6 @@
#include "rpc/rpc_worker.h"
#include "rpc/connection.h"
#include "config/settings.h"
#include "data/seed_migration_resume.h"
#include "wallet/lite_wallet_controller.h"
#include "wallet/lite_wallet_server_selection_adapter.h"
#include "wallet/lite_rollout_policy.h"
@@ -2300,8 +2299,6 @@ void App::renderNodeStatusBanner()
in.connection_status = connection_status_;
in.daemon_last_error = daemon_controller_ ? daemon_controller_->lastError() : std::string();
in.lite_open_error = lite_open_error_;
in.tip_known = state_.sync.tip_known;
in.blocks = state_.sync.blocks;
const ui::NodeBannerState banner = ui::evaluateNodeStatusBanner(in);
if (!banner.show) return;
@@ -2334,8 +2331,6 @@ void App::renderNodeStatusBanner()
title = TR("node_banner_crashed_title"); icon = ICON_MD_ERROR; break;
case ui::NodeBannerReason::LiteOpenFailed:
title = TR("node_banner_lite_open_failed"); icon = ICON_MD_ERROR; break;
case ui::NodeBannerReason::NoPeers:
title = TR("node_banner_no_peers_title"); icon = ICON_MD_CLOUD_OFF; break;
case ui::NodeBannerReason::FullNodeOffline:
default:
title = TR("node_banner_offline_title"); icon = ICON_MD_CLOUD_OFF; break;
@@ -2822,8 +2817,7 @@ void App::renderStatusBar()
ImGui::TextDisabled("%s", statusShown.c_str());
}
occupiedX = statusX;
} else if (const std::string ds = getDaemonStatus();
!ds.empty() && ds.find("Error") != std::string::npos) {
} else if (!daemon_status_.empty() && daemon_status_.find("Error") != std::string::npos) {
const char* errText = TR("sb_daemon_not_found");
float statusW = ImGui::CalcTextSize(errText).x;
float statusX = versionX - statusW - gap;
@@ -4320,33 +4314,8 @@ void App::renderSeedMigrationDialog()
break;
}
if (seed_migration_balance_ <= DRAGONX_DEFAULT_FEE) {
const bool sweepSubmitted = settings_ && settings_->getSeedMigrationSweepSubmitted();
if (!dragonx::sweepGateMayAdoptEmptyWallet(sweepSubmitted)) {
// A sweep was already broadcast but its txid was lost (its opid didn't survive a
// daemon/app restart). A ~0 balance is AMBIGUOUS — the sweep may still be unconfirmed
// in the mempool and could revert on a drop/reorg — so we must NOT adopt (swap
// wallet.dat) here. Try to locate the sweep tx to resume the confirmation gate (which
// requires >=1 conf); until then, wait/refresh. If the sweep was dropped the balance
// simply returns and the normal Sweep UI reappears — the user is never trapped.
ImGui::TextWrapped("%s", TR("mig_sweep_inflight"));
ImGui::Spacing();
ImGui::TextColored(kMedium, "%s%s", TR("mig_checking_sweep"), ui::material::LoadingDots());
ImGui::Spacing();
if (ui::material::TactileButton(TR("mig_check_sweep"), ImVec2(190 * dp, 0)))
locateSweepTxid();
ImGui::SameLine();
if (ui::material::TactileButton(TR("refresh"), ImVec2(110 * dp, 0))) {
refreshSeedMigrationBalance();
locateSweepTxid();
}
ImGui::SameLine();
if (ui::material::TactileButton(TR("mig_later"), ImVec2(100 * dp, 0)))
close();
break;
}
// Nothing to sweep and no sweep was ever submitted (a genuinely empty wallet) — adopt the
// fresh seed wallet directly. Its wallet.dat replaces the current (empty) one; adopt still
// moves the legacy wallet aside to a timestamped backup.
// Nothing to sweep — adopt the fresh seed wallet directly. Its wallet.dat replaces the
// current (empty) one; adopt still moves the legacy wallet aside to a timestamped backup.
ImGui::TextWrapped("%s", TR("mig_no_funds"));
ImGui::Spacing();
ImGui::TextWrapped("%s%s", TR("mig_to"), seed_migration_dest_.c_str());
@@ -5197,18 +5166,6 @@ void App::setCurrentTab(int tab) {
setCurrentPage(kTabMap[tab]);
}
std::string App::getDaemonStatus() const
{
std::lock_guard<std::mutex> lk(daemon_status_mutex_);
return daemon_status_; // a copy — safe to use after the lock is released
}
void App::setDaemonStatus(std::string s)
{
std::lock_guard<std::mutex> lk(daemon_status_mutex_);
daemon_status_ = std::move(s);
}
bool App::startEmbeddedDaemon()
{
if (!supportsEmbeddedDaemon()) {
@@ -5228,16 +5185,16 @@ bool App::startEmbeddedDaemon()
// Try to extract embedded resources if available
if (resources::hasEmbeddedResources()) {
DEBUG_LOGF("Extracting embedded Sapling params...\n");
setDaemonStatus(TR("sb_extracting_sapling"));
daemon_status_ = TR("sb_extracting_sapling");
if (!resources::extractEmbeddedResources()) {
setDaemonStatus(TR("sb_daemon_extract_failed"));
daemon_status_ = TR("sb_daemon_extract_failed");
DEBUG_LOGF("[ERROR] extractEmbeddedResources() failed — disk full or permission denied?\n");
return false;
}
// Check again after extraction
if (!rpc::Connection::verifySaplingParams()) {
setDaemonStatus(TR("sb_sapling_failed"));
daemon_status_ = TR("sb_sapling_failed");
DEBUG_LOGF("Sapling params still not found after extraction!\n");
DEBUG_LOGF("Expected location: %s\n", rpc::Connection::getSaplingParamsDir().c_str());
return false;
@@ -5254,7 +5211,7 @@ bool App::startEmbeddedDaemon()
if (!exe_dir.empty()) {
std::string dirErr;
if (!util::Platform::ensureDirectory(daemon_dir, &dirErr)) {
setDaemonStatus(dirErr);
daemon_status_ = dirErr;
DEBUG_LOGF("[ERROR] %s\n", dirErr.c_str());
return false;
}
@@ -5288,7 +5245,7 @@ bool App::startEmbeddedDaemon()
if (copied && rpc::Connection::verifySaplingParams()) {
DEBUG_LOGF("Sapling params copied from exe directory successfully\n");
} else {
setDaemonStatus(TR("sb_sapling_not_found"));
daemon_status_ = TR("sb_sapling_not_found");
DEBUG_LOGF("Sapling params not found and no embedded resources available!\n");
DEBUG_LOGF("Expected location: %s\n", rpc::Connection::getSaplingParamsDir().c_str());
return false;
@@ -5305,7 +5262,7 @@ bool App::startEmbeddedDaemon()
if (!exe_dir.empty()) {
std::string dirErr;
if (!util::Platform::ensureDirectory(daemon_dir, &dirErr)) {
setDaemonStatus(dirErr);
daemon_status_ = dirErr;
DEBUG_LOGF("[ERROR] %s\n", dirErr.c_str());
return false;
}
@@ -5341,7 +5298,7 @@ bool App::startEmbeddedDaemon()
if (copyFailed) {
char buf[512];
snprintf(buf, sizeof(buf), TR("sb_daemon_files_failed"), daemon_dir.c_str());
setDaemonStatus(buf);
daemon_status_ = buf;
DEBUG_LOGF("[ERROR] One or more daemon files failed to copy to %s\n", daemon_dir.c_str());
return false;
}
@@ -5354,22 +5311,21 @@ bool App::startEmbeddedDaemon()
// Set up state callback
daemon_controller_->setStateCallback([this](daemon::EmbeddedDaemon::State state, const std::string& msg) {
// NB: this callback runs on EmbeddedDaemon's monitor thread — write via the locked setter.
switch (state) {
case daemon::EmbeddedDaemon::State::Starting:
setDaemonStatus(TR("sb_starting_daemon"));
daemon_status_ = TR("sb_starting_daemon");
break;
case daemon::EmbeddedDaemon::State::Running:
setDaemonStatus(TR("sb_dragonxd_running"));
daemon_status_ = TR("sb_dragonxd_running");
break;
case daemon::EmbeddedDaemon::State::Stopping:
setDaemonStatus(TR("sb_dragonxd_stopping"));
daemon_status_ = TR("sb_dragonxd_stopping");
break;
case daemon::EmbeddedDaemon::State::Stopped:
setDaemonStatus(TR("sb_dragonxd_stopped"));
daemon_status_ = TR("sb_dragonxd_stopped");
break;
case daemon::EmbeddedDaemon::State::Error:
setDaemonStatus("Error: " + msg);
daemon_status_ = "Error: " + msg;
break;
}
});
@@ -7002,7 +6958,7 @@ std::string App::buildDiagnosticsReport()
<< (state_.warming_up ? " (warming up)" : "") << "\n";
#if !DRAGONX_LITE_BUILD
os << "daemon status: " << getDaemonStatus() << "\n";
os << "daemon status: " << daemon_status_ << "\n";
if (daemon_controller_) {
os << "daemon running: " << (daemon_controller_->isRunning() ? "yes" : "no")
<< ", crashes: " << daemon_controller_->crashCount() << "\n";
+2 -9
View File
@@ -267,7 +267,7 @@ public:
bool isConnected() const { return state_.connected; }
int getBlockHeight() const { return state_.sync.blocks; }
const std::string& getConnectionStatus() const { return connection_status_; }
std::string getDaemonStatus() const; // thread-safe copy (daemon_status_ is written off-thread)
const std::string& getDaemonStatus() const { return daemon_status_; }
// Balance info (convenience wrappers)
double getShieldedBalance() const { return state_.shielded_balance; }
@@ -884,7 +884,6 @@ private:
std::function<void(bool, const std::string&)> makeSweepCompletionCallback(bool resumed);
void beginSweepToSeedWallet(); // z_mergetoaddress ["ANY_TADDR","ANY_ZADDR"] -> dest
void pollSweepStatus(); // Confirming step: poll sweep confirmations + legacy balance
void locateSweepTxid(); // recover a lost sweep txid (send to dest) -> Confirming gate
void beginAdoptSeedWallet(); // stop daemon -> swap wallet.dat -> restart with -rescan
void provisionChatIdentityFromSecret(std::string secret);
std::string chatReplyZaddr(); // a stable (persisted) wallet z-addr — chat IDENTITY (reply-to)
@@ -1124,13 +1123,7 @@ private:
// Embedded daemon state
bool use_embedded_daemon_ = wallet::supportsEmbeddedDaemon(wallet::currentWalletCapabilities());
// Written by the DaemonController state callback (which fires on EmbeddedDaemon's monitor thread)
// AND read on the UI thread — so all access goes through set/getDaemonStatus() under this mutex.
// getDaemonStatus() returns a COPY: a returned reference could dangle if the monitor thread
// reassigned the string mid-read (data race / use-after-free).
mutable std::mutex daemon_status_mutex_;
std::string daemon_status_; // guarded by daemon_status_mutex_ — use set/getDaemonStatus()
void setDaemonStatus(std::string s);
std::string daemon_status_;
mutable std::string daemon_mem_diag_; // diagnostic info for daemon memory detection
size_t daemon_output_offset_ = 0; // for incremental output parsing (rescan detection)
-76
View File
@@ -4701,9 +4701,6 @@ void App::showSeedMigrationDialog()
seed_migration_balance_loaded_ = false;
seed_migration_nofunds_confirmed_ = false;
refreshSeedMigrationBalance();
// If a sweep was already broadcast (the marker survives a lost opid), try to locate its tx so
// we resume at the confirmation gate instead of risking a premature "no funds -> adopt".
if (settings_ && settings_->getSeedMigrationSweepSubmitted()) locateSweepTxid();
break;
case MigrationResume::Intro:
default:
@@ -4771,12 +4768,6 @@ void App::beginSweepToSeedWallet()
}
seed_migration_step_ = SeedMigrationStep::Sweeping;
seed_migration_status_ = "Building the sweep transaction…";
// Mark the sweep as submitted BEFORE broadcasting (persisted synchronously). If the app or daemon
// then dies after the tx reaches the mempool but before its opid/txid is persisted, a resumed
// migration still knows a sweep is in flight and won't mistake the resulting ~0 legacy balance for
// an empty wallet and adopt (swap wallet.dat) prematurely — funds in an unconfirmed sweep can still
// revert on a drop/reorg. Cleared below only if the submit definitively fails (nothing broadcast).
if (settings_) { settings_->setSeedMigrationSweepSubmitted(true); settings_->save(); }
const std::string dest = seed_migration_dest_;
worker_->post([this, dest]() -> rpc::RPCWorker::MainCb {
std::string opid, err;
@@ -4789,8 +4780,6 @@ void App::beginSweepToSeedWallet()
} catch (const std::exception& e) { err = e.what(); }
return [this, opid, err]() {
if (!err.empty() || opid.empty()) {
// Submit failed — nothing was broadcast, so drop the "sweep in flight" marker.
if (settings_) { settings_->setSeedMigrationSweepSubmitted(false); settings_->save(); }
seed_migration_status_ = err.empty() ? "The sweep failed to start." : err;
seed_migration_step_ = SeedMigrationStep::Error;
return;
@@ -4893,69 +4882,6 @@ void App::pollSweepStatus()
});
}
// Best-effort recovery when a sweep was broadcast but its opid was lost (e.g. a daemon restart wiped
// the in-memory op queue) so we have no txid to confirm against. The sweep is an outgoing send to
// seed_migration_dest_; find it among the wallet's own sends (z_listsentbyaddress) and, if present,
// adopt its txid and resume at the confirmation gate (pollSweepStatus / Confirming). Read-only, and
// backstopped by that gate (confs >= 1 && legacy balance ~0), so a wrong match cannot lose funds —
// it only avoids the premature "no funds -> adopt" short-circuit while a sweep is unconfirmed.
void App::locateSweepTxid()
{
if (capture_mode_) return;
if (!rpc_ || !worker_ || !state_.connected) return;
if (seed_migration_confirm_in_flight_ || !seed_migration_sweep_txid_.empty()) return;
if (seed_migration_dest_.empty()) return;
seed_migration_confirm_in_flight_ = true;
const std::string dest = seed_migration_dest_;
worker_->post([this, dest]() -> rpc::RPCWorker::MainCb {
std::string found;
rpc::RPCClient::TraceScope trace("Migrate / locate sweep");
try {
// z_listsentbyaddress ignores its address argument and returns ALL wallet sends
// (oldest -> newest); the sweep is the latest send whose outputs include the destination.
auto sent = rpc_->call("z_listsentbyaddress", {dest, 0});
if (sent.is_array()) {
for (const auto& e : sent) {
if (!e.is_object() || !e.contains("sends") || !e["sends"].is_object()) continue;
const auto& s = e["sends"];
bool toDest = false;
for (const char* leg : {"saplingSends", "transparentSends"}) {
auto it = s.find(leg);
if (it == s.end() || !it->is_array()) continue;
for (const auto& o : *it) {
if (o.is_object() && o.value("address", std::string()) == dest) {
toDest = true;
break;
}
}
if (toDest) break;
}
if (toDest) {
auto t = e.value("txid", std::string());
if (!t.empty()) found = t; // keep scanning; entries are oldest->newest
}
}
}
} catch (...) {}
return [this, found]() {
seed_migration_confirm_in_flight_ = false;
if (found.empty() || !seed_migration_sweep_txid_.empty()) return;
// Adopt the recovered txid and hand off to the confirmation gate (which alone permits adopt).
seed_migration_sweep_txid_ = found;
if (settings_) {
settings_->setSeedMigrationSweepTxid(found);
settings_->setSeedMigrationSweepOpid("");
settings_->save();
}
seed_migration_sweep_confs_ = 0;
seed_migration_legacy_remaining_ = -1.0;
seed_migration_poll_timer_ = 0.0f;
seed_migration_status_.clear();
seed_migration_step_ = SeedMigrationStep::Confirming;
};
});
}
// Phase 2 step 2: adopt the new seed wallet as the primary wallet. Stop the daemon, move the
// legacy wallet.dat aside to a timestamped backup (NEVER delete it), copy the new wallet in, and
// restart with -rescan so the new keys pick up the swept funds. Runs on a background thread.
@@ -5382,7 +5308,6 @@ void App::pumpSeedMigration()
settings_->setSeedMigrationTempDir("");
settings_->setSeedMigrationSweepTxid("");
settings_->setSeedMigrationSweepOpid(""); // W3-3
settings_->setSeedMigrationSweepSubmitted(false);
settings_->save();
}
seed_migration_status_ = err; // a non-empty warning here (e.g. restart hiccup) is shown on Done
@@ -5430,7 +5355,6 @@ void App::pumpSeedMigration()
// txid/opid (the resume block reads these whenever the migration is pending).
settings_->setSeedMigrationSweepTxid("");
settings_->setSeedMigrationSweepOpid("");
settings_->setSeedMigrationSweepSubmitted(false);
settings_->save();
}
} else {
-2
View File
@@ -267,7 +267,6 @@ bool Settings::load(const std::string& path)
loadScalar(j, "seed_migration_temp_dir", seed_migration_temp_dir_);
loadScalar(j, "seed_migration_sweep_txid", seed_migration_sweep_txid_);
loadScalar(j, "seed_migration_sweep_opid", seed_migration_sweep_opid_);
loadScalar(j, "seed_migration_sweep_submitted", seed_migration_sweep_submitted_);
loadScalar(j, "auto_lock_timeout", auto_lock_timeout_);
loadScalar(j, "unlock_duration", unlock_duration_);
loadScalar(j, "pin_enabled", pin_enabled_);
@@ -560,7 +559,6 @@ bool Settings::save(const std::string& path)
j["seed_migration_temp_dir"] = seed_migration_temp_dir_;
j["seed_migration_sweep_txid"] = seed_migration_sweep_txid_;
j["seed_migration_sweep_opid"] = seed_migration_sweep_opid_;
j["seed_migration_sweep_submitted"] = seed_migration_sweep_submitted_;
j["auto_lock_timeout"] = auto_lock_timeout_;
j["unlock_duration"] = unlock_duration_;
j["pin_enabled"] = pin_enabled_;
-7
View File
@@ -396,12 +396,6 @@ public:
// in the same write that persists the txid, so the txid always outranks it (see [[decideSeedMigrationResume]]).
std::string getSeedMigrationSweepOpid() const { return seed_migration_sweep_opid_; }
void setSeedMigrationSweepOpid(const std::string& v) { seed_migration_sweep_opid_ = v; }
// Set TRUE synchronously when a sweep is broadcast; survives the stale-opid resume fallback so the
// Sweep step knows a ~0 legacy balance may be an UNCONFIRMED in-flight sweep (funds not yet safe)
// and must NOT short-circuit to adopt/swap wallet.dat. Cleared on confirmed-adopt / reset / a
// failed submit. See [[decideSeedMigrationResume]] and App::renderSeedMigrationDialog (fund-safety).
bool getSeedMigrationSweepSubmitted() const { return seed_migration_sweep_submitted_; }
void setSeedMigrationSweepSubmitted(bool v) { seed_migration_sweep_submitted_ = v; }
// Security — auto-lock timeout (seconds; 0 = disabled)
int getAutoLockTimeout() const { return auto_lock_timeout_; }
@@ -646,7 +640,6 @@ private:
std::string seed_migration_temp_dir_;
std::string seed_migration_sweep_txid_;
std::string seed_migration_sweep_opid_;
bool seed_migration_sweep_submitted_ = false;
int auto_lock_timeout_ = 900; // 15 minutes
int unlock_duration_ = 600; // 10 minutes
bool pin_enabled_ = false;
-8
View File
@@ -34,12 +34,4 @@ inline MigrationResume decideSeedMigrationResume(bool pending,
return MigrationResume::SweepGate;
}
// At the Sweep step with a ~0 legacy balance, may we offer the direct "no funds -> adopt" short-cut
// (swap wallet.dat immediately)? Only when NO sweep has been submitted — a genuinely empty wallet.
// If a sweep WAS submitted, a ~0 balance is ambiguous: the sweep tx may be unconfirmed in the mempool
// and could revert on a drop/reorg, so adopting now can strand the funds in the moved-aside backup.
// In that case adopt must go through the confirmation-gated path (>=1 conf) instead — never swap
// wallet.dat on an unconfirmed sweep. (fund-safety: audit)
inline bool sweepGateMayAdoptEmptyWallet(bool sweepSubmitted) { return !sweepSubmitted; }
} // namespace dragonx
+1 -7
View File
@@ -149,13 +149,7 @@ struct SyncInfo {
float witness_progress = 0.0f; // 0.0 - 1.0, within the current sub-phase
int witness_remaining = 0; // blocks left in the cache walk (0 if unknown / phase 1)
// True only when the daemon reports a positive peer-derived tip (longestchain > 0). A node with
// no peers reports longestchain == 0, so `blocks` is then only the LOCAL best height and cannot be
// trusted as "at the network tip". isSynced() gates on this so a peerless / cold-started node is
// never reported synced (which would enable Send against a stale balance). (audit: peerless-synced)
bool tip_known = false;
bool isSynced() const { return tip_known && !syncing && blocks > 0 && blocks >= headers - 2; }
bool isSynced() const { return !syncing && blocks > 0 && blocks >= headers - 2; }
};
/**
+1 -96
View File
@@ -853,11 +853,8 @@ void NetworkRefreshService::appendViewTransactionOutputs(std::vector<Transaction
for (const auto& out : entry.outgoing_outputs) {
bool alreadyTracked = false;
for (const auto& existing : transactions) {
// Key on address as well as amount: two outgoing outputs of EQUAL value to DIFFERENT
// recipients in one tx are distinct rows — an amount-only match would drop the second.
if (existing.txid == txid && existing.type == "send" &&
std::abs(existing.amount + out.value) < 0.00000001 &&
existing.address == out.address) {
std::abs(existing.amount + out.value) < 0.00000001) {
alreadyTracked = true;
break;
}
@@ -888,55 +885,6 @@ void NetworkRefreshService::appendViewTransactionOutputs(std::vector<Transaction
}
}
void NetworkRefreshService::appendSentTransparentOutputs(std::vector<TransactionInfo>& transactions,
const std::string& txid,
const nlohmann::json& sentEntry)
{
if (!sentEntry.is_object() || !sentEntry.contains("sends") || !sentEntry["sends"].is_object())
return;
const auto& sends = sentEntry["sends"];
auto it = sends.find("transparentSends");
if (it == sends.end() || !it->is_array()) return;
// Per-tx metadata for the synthesized rows.
int confirmations = 0;
std::int64_t timestamp = 0;
if (auto v = readOptional<int>(sentEntry, "confirmations")) confirmations = *v;
if (auto v = readOptional<std::int64_t>(sentEntry, "blocktime")) timestamp = *v;
if (timestamp == 0) {
if (auto v = readOptional<std::int64_t>(sentEntry, "time")) timestamp = *v;
}
for (const auto& out : *it) {
auto amount = readOptional<double>(out, "amount");
if (!amount) continue;
const double debit = -std::abs(*amount); // the daemon reports -nValue; a send is a debit
std::string address;
if (auto a = readOptional<std::string>(out, "address")) address = *a;
// Dedup against rows already present — notably listtransactions-derived t->t sends, which
// this whole-wallet payload ALSO reports. Same txid+address+amount == the same output.
bool exists = false;
for (const auto& e : transactions) {
if (e.txid == txid && e.type == "send" && e.address == address &&
std::abs(e.amount - debit) < 0.00000001) {
exists = true;
break;
}
}
if (exists) continue;
TransactionInfo info;
info.txid = txid;
info.type = "send";
info.address = address;
info.amount = debit;
info.confirmations = confirmations;
info.timestamp = timestamp;
transactions.push_back(std::move(info));
}
}
void NetworkRefreshService::sortTransactionsNewestFirst(std::vector<TransactionInfo>& transactions)
{
std::sort(transactions.begin(), transactions.end(),
@@ -1037,45 +985,6 @@ NetworkRefreshService::TransactionRefreshResult NetworkRefreshService::collectTr
result.shieldedScanComplete = !pendingShieldedIndex.has_value();
result.nextShieldedScanStartIndex = pendingShieldedIndex.value_or(0);
// Discover outgoing shielded (z->z) sends. A pure z->z send moves no value in or out of the
// transparent pool (valueBalance == 0), so the daemon's `listtransactions` never reports it,
// and — being a send rather than a receive — it never shows up in `z_listreceivedbyaddress`
// either. Its txid therefore never reaches `knownTxids`, so the z_viewtransaction enrichment
// below is never run for it and the send is invisible in History (the reported bug).
//
// `z_listsentbyaddress` scans the whole wallet for sends — the address argument is NOT used to
// filter (the daemon iterates every wallet tx against "*"), so a single call yields all send
// txids. Two legs need handling:
// * SHIELDED (z->z) outgoing legs are surfaced by z_viewtransaction, so we fold the txid into
// `knownTxids` and let the z_viewtransaction / appendViewTransactionOutputs path build them.
// * TRANSPARENT outgoing legs (z->t deshields) are NOT surfaced by z_viewtransaction (it
// enumerates only Sapling outputs), and for a z-originated send listtransactions also omits
// the external t-vout (GetAmounts' isFromMyTaddr is false when inputs are shielded). So we
// build those rows directly from the payload's `sends.transparentSends[]` here. They are
// deduped (by txid+address+amount) against listtransactions-derived t->t sends above, so a
// purely-transparent (t->t) send — which the payload ALSO reports — is not double-counted.
//
// Gated on shieldedScanComplete so it adds no cost during the multi-cycle initial received
// scan: it is one more O(mapWallet) pass holding cs_main (comparable to the listtransactions
// pass), and the caller's adaptive throttle (txRefreshDue) already accounts for it via scanMs.
if (result.shieldedScanComplete && !snapshot.shieldedAddresses.empty()) {
try {
json sent = rpc.call("z_listsentbyaddress",
json::array({snapshot.shieldedAddresses.front(), 0}));
if (sent.is_array()) {
for (const auto& sentTx : sent) {
auto txid = readOptional<std::string>(sentTx, "txid");
if (!txid || txid->empty()) continue;
appendSentTransparentOutputs(result.transactions, *txid, sentTx);
knownTxids.insert(*txid); // shielded (z->z) legs enriched via z_viewtransaction
}
}
} catch (const std::exception& e) {
transactionRpcError = true;
DEBUG_LOGF("z_listsentbyaddress error: %s\n", e.what());
}
}
for (const auto& txid : snapshot.sendTxids) {
knownTxids.insert(txid);
}
@@ -1396,10 +1305,6 @@ void NetworkRefreshService::applyCoreRefreshResult(WalletState& state,
if (result.headers) state.sync.headers = *result.headers;
if (result.bestBlockHash) state.sync.best_blockhash = *result.bestBlockHash;
if (result.verificationProgress) state.sync.verification_progress = *result.verificationProgress;
// A peerless daemon reports longestchain == 0 (no peer heights). Track that as "tip unknown" from
// THIS refresh's raw value (not the retained state, which keeps the last positive value) so a
// peerless node isn't reported synced against its own stale local height. (audit: peerless-synced)
state.sync.tip_known = (result.longestChain.has_value() && *result.longestChain > 0);
if (result.longestChain && *result.longestChain > 0) state.longestchain = *result.longestChain;
if (state.longestchain > 0 && state.sync.blocks > state.longestchain) state.longestchain = state.sync.blocks;
if (state.longestchain > 0)
-6
View File
@@ -310,12 +310,6 @@ public:
static void appendViewTransactionOutputs(std::vector<TransactionInfo>& transactions,
const std::string& txid,
const TransactionViewCacheEntry& entry);
// Build "send" rows from a z_listsentbyaddress entry's sends.transparentSends[] — the transparent
// (z->t deshield) outgoing outputs that z_viewtransaction does not surface. Deduped by
// txid+address+amount against rows already present (e.g. listtransactions-derived t->t sends).
static void appendSentTransparentOutputs(std::vector<TransactionInfo>& transactions,
const std::string& txid,
const nlohmann::json& sentEntry);
static void sortTransactionsNewestFirst(std::vector<TransactionInfo>& transactions);
static TransactionRefreshResult collectTransactionRefreshResult(RefreshRpcGateway& rpc,
const TransactionRefreshSnapshot& snapshot,
+4 -25
View File
@@ -31,7 +31,6 @@ enum class NodeBannerReason {
FullNodeOffline, // a reachable node was lost, or never came up; reconnect offered
DaemonCrashed, // the embedded daemon crashed repeatedly and auto-restart stopped
LiteOpenFailed, // lite build: the wallet failed to open
NoPeers, // connected to the daemon but no peer-derived tip (0 peers) — stalled, stale
};
struct NodeBannerState {
@@ -51,13 +50,6 @@ struct NodeBannerInputs {
bool daemon_initializing = false; // daemon launching / block index loading
bool connection_in_progress = false; // a connect attempt is actively running
// Peer connectivity: a full node connected to the daemon but with NO peer-derived tip
// (longestchain == 0 -> tip_known false) has no peers, so it silently stalls at a stale height
// while balance/sync look "done". `blocks` gates the check so we don't flag the brief window
// before the first chain-info refresh. (audit: no-peer-banner)
bool tip_known = true; // sync.tip_known — a peer-derived network tip is known
int blocks = 0; // sync.blocks — >0 once chain info has been fetched
// Full-node embedded-daemon crash signal.
bool using_embedded_daemon = false;
bool has_daemon_controller = false;
@@ -89,26 +81,13 @@ inline NodeBannerState evaluateNodeStatusBanner(const NodeBannerInputs& in) {
return s;
}
// Full node. In an expected startup phase → the loading/warmup overlay owns the screen, so no
// banner. An active connect attempt likewise shows progress, not an error — don't flicker a
// banner over it. (These return even if `connected` has flipped early, matching prior behavior.)
// Full node. Connected, or in an expected startup phase → the loading/warmup overlay owns
// the screen, so no banner. An active connect attempt likewise shows progress, not an
// error — don't flicker a banner over it.
if (in.connected) return s;
if (in.warming_up || in.daemon_initializing) return s;
if (in.connection_in_progress) return s;
// Connected to the daemon (RPC works) but with no peer-derived tip → the node has no peers and
// cannot learn about new blocks; it silently stalls at a stale height while everything looks
// synced. Surface it as a recoverable warning (reconnect won't help, so no action button).
if (in.connected) {
if (!in.tip_known && in.blocks > 0) {
s.show = true;
s.severity = NodeBannerSeverity::Warning;
s.reason = NodeBannerReason::NoPeers;
s.action = NodeBannerAction::None;
s.detail = in.connection_status;
}
return s;
}
// Genuinely offline. Distinguish "the embedded daemon crashed and we stopped retrying" (a
// hard fault needing a manual restart) from an ordinary lost/failed connection (retryable).
if (in.using_embedded_daemon && in.has_daemon_controller && !in.daemon_running &&
-4
View File
@@ -425,9 +425,6 @@ void I18n::loadBuiltinEnglish()
// Sweep step: balance still loading / no funds to move
strings_["mig_checking_balance"] = "Checking your balance";
strings_["mig_no_funds"] = "Your wallet has no funds to migrate. You can adopt the new seed wallet directly — it replaces your current (empty) wallet with the seed-backed one. Your old wallet is still moved aside to a timestamped backup, just in case.";
strings_["mig_sweep_inflight"] = "A sweep was already started and hasn't confirmed yet. Your balance reads zero because the funds are in the pending sweep transaction — they are not lost. Wait for it to confirm before finishing; the wallet will not be swapped until the sweep is mined.";
strings_["mig_checking_sweep"] = "Looking for the sweep transaction";
strings_["mig_check_sweep"] = "Check the sweep";
strings_["mig_adopt_now"] = "Adopt seed wallet";
strings_["mig_nofunds_confirm"] = "I understand this replaces my current wallet with the new seed wallet";
// Adopting step note (shown under the spinner while the node restarts + rescans)
@@ -1462,7 +1459,6 @@ void I18n::loadBuiltinEnglish()
strings_["sb_wallet_needs_recovery"] = "Wallet repair available";
// Persistent node-status banner (App::renderNodeStatusBanner).
strings_["node_banner_offline_title"] = "Not connected to the DragonX node";
strings_["node_banner_no_peers_title"] = "Connected, but no network peers — the node can't sync";
strings_["node_banner_crashed_title"] = "The node stopped unexpectedly";
strings_["node_banner_lite_open_failed"] = "Couldn't open your wallet";
strings_["node_banner_reconnect"] = "Reconnect";
+3 -60
View File
@@ -450,47 +450,31 @@ void LiteWalletController::pumpAsyncOpen()
bool LiteWalletController::beginCreateWalletAsync(LiteWalletCreateRequest request)
{
auto req = std::make_shared<LiteWalletCreateRequest>(std::move(request));
// Encrypt the brand-new wallet with the user's passphrase once it opens. The async worker
// can't (walletOpen_ isn't set until pumpLifecycleResult runs), so stash it and apply on the
// main-thread finalize — otherwise the passphrase is discarded and the seed stored in
// plaintext (audit C1). Mirrors the synchronous createWallet() wrapper.
stashPendingLifecycleSecurity(req->passphrase, PendingSecurityOp::Encrypt);
const bool started = beginLifecycleRequestAsync(
return beginLifecycleRequestAsync(
"Create",
[req](LiteWalletLifecycleService& svc, const std::string& url) {
req->serverUrl = url;
return svc.createWallet(*req);
},
[req]() { secureWipeLiteSecret(req->passphrase); });
if (!started) clearPendingLifecycleSecurity();
return started;
}
bool LiteWalletController::beginOpenWalletAsync(LiteWalletOpenRequest request)
{
auto req = std::make_shared<LiteWalletOpenRequest>(std::move(request));
// An existing wallet may be encrypted+locked — unlock it with the supplied passphrase on the
// main-thread finalize (the async worker can't; walletOpen_ isn't set yet). Without this an
// encrypted wallet opens but stays locked and the passphrase is never verified (audit M1).
stashPendingLifecycleSecurity(req->passphrase, PendingSecurityOp::Unlock);
const bool started = beginLifecycleRequestAsync(
return beginLifecycleRequestAsync(
"Open",
[req](LiteWalletLifecycleService& svc, const std::string& url) {
req->serverUrl = url;
return svc.openWallet(*req);
},
[req]() { secureWipeLiteSecret(req->passphrase); });
if (!started) clearPendingLifecycleSecurity();
return started;
}
bool LiteWalletController::beginRestoreWalletAsync(LiteWalletRestoreRequest request)
{
auto req = std::make_shared<LiteWalletRestoreRequest>(std::move(request));
// Encrypt the restored wallet with the user's passphrase once it opens (same rationale and
// main-thread finalize as create — audit C1).
stashPendingLifecycleSecurity(req->passphrase, PendingSecurityOp::Encrypt);
const bool started = beginLifecycleRequestAsync(
return beginLifecycleRequestAsync(
"Restore",
[req](LiteWalletLifecycleService& svc, const std::string& url) {
req->serverUrl = url;
@@ -500,8 +484,6 @@ bool LiteWalletController::beginRestoreWalletAsync(LiteWalletRestoreRequest requ
secureWipeLiteSecret(req->seedPhrase);
secureWipeLiteSecret(req->passphrase);
});
if (!started) clearPendingLifecycleSecurity();
return started;
}
bool LiteWalletController::beginLifecycleRequestAsync(
@@ -603,54 +585,15 @@ void LiteWalletController::pumpLifecycleResult()
// log the failure otherwise (shared with the synchronous lifecycle path).
onLifecycleResult(out);
if (out.walletReady) {
// Wallet is now open (walletOpen_ set by onLifecycleResult) — apply the stashed passphrase:
// encrypt a freshly created/restored wallet, or unlock an existing encrypted one (C1/M1).
applyPendingLifecycleSecurity();
lastOpenError_.clear();
lastOpenWarming_ = false;
} else {
clearPendingLifecycleSecurity(); // wallet didn't open — drop the stashed passphrase
lastOpenError_ = out.error.empty() ? out.status.message : out.error;
lastOpenWarming_ = liteOpenErrorIsWarmup(lastOpenError_);
}
lastLifecycleResult_ = std::move(out);
}
void LiteWalletController::stashPendingLifecycleSecurity(const std::string& passphrase,
PendingSecurityOp op)
{
clearPendingLifecycleSecurity(); // wipe any stale prior stash first
pendingLifecyclePassphrase_ = passphrase; // independent copy; wiped in apply/clear
pendingLifecycleSecurityOp_ = op;
}
void LiteWalletController::applyPendingLifecycleSecurity()
{
const PendingSecurityOp op = pendingLifecycleSecurityOp_;
if (op == PendingSecurityOp::None || pendingLifecyclePassphrase_.empty() ||
!walletOpen_.load() || !bridge_) {
clearPendingLifecycleSecurity();
return;
}
if (op == PendingSecurityOp::Encrypt) {
// encryptWallet() takes its own copy and wipes it; the backend encrypts + locks + saves.
const auto enc = encryptWallet(pendingLifecyclePassphrase_);
if (!enc.ok)
liteLog("wallet created/restored but ENCRYPTION FAILED — wallet is NOT encrypted: " +
enc.error);
} else if (op == PendingSecurityOp::Unlock) {
if (!unlockWallet(pendingLifecyclePassphrase_))
liteLog("wallet opened but unlock failed (wrong passphrase?)");
}
clearPendingLifecycleSecurity();
}
void LiteWalletController::clearPendingLifecycleSecurity()
{
secureWipeLiteSecret(pendingLifecyclePassphrase_); // no-op if already empty
pendingLifecycleSecurityOp_ = PendingSecurityOp::None;
}
void LiteWalletController::startSync()
{
if (syncLaunched_.exchange(true)) return;
-14
View File
@@ -388,20 +388,6 @@ private:
std::make_shared<std::optional<LiteWalletLifecycleResult>>();
LiteWalletLifecycleResult lastLifecycleResult_; // main-thread only; last finalized request
// Passphrase to apply on the main thread once an async create/restore/open finalizes. The
// backend wallet is not controller-"open" (walletOpen_) until pumpLifecycleResult() runs
// onLifecycleResult(), and encryptWallet()/unlockWallet() gate on walletOpen_ — so the async
// worker thread cannot do it. The passphrase is stashed here (main-thread only) and applied in
// pumpLifecycleResult(), mirroring the synchronous createWallet()/restoreWallet()/openWallet()
// wrappers. Without this the UI's async path silently discarded the passphrase, storing the
// seed UNENCRYPTED (create/restore) or never unlocking an encrypted wallet (open) — audit C1/M1.
enum class PendingSecurityOp { None, Encrypt, Unlock };
std::string pendingLifecyclePassphrase_; // main-thread only; wiped after use
PendingSecurityOp pendingLifecycleSecurityOp_ = PendingSecurityOp::None;
void stashPendingLifecycleSecurity(const std::string& passphrase, PendingSecurityOp op);
void applyPendingLifecycleSecurity(); // encrypt (create/restore) or unlock (open), then wipe
void clearPendingLifecycleSecurity(); // wipe + reset (rejected request / failed open)
// Joinable background refresh worker (fast iterations: syncstatus, plus data once synced).
std::thread worker_;
std::atomic<bool> running_{false};
+5 -270
View File
@@ -1628,7 +1628,6 @@ void testNetworkRefreshRpcCollectors()
{"time", 50}, {"memoStr", "shielded memo"}}
}));
transactionRpc.addResponse("z_listreceivedbyaddress", json::array());
transactionRpc.addResponse("z_listsentbyaddress", json::array());
transactionRpc.addResponse("z_viewtransaction", json{
{"spends", json::array({json{{"address", "zs-from"}}})},
{"outputs", json::array({
@@ -1640,13 +1639,12 @@ void testNetworkRefreshRpcCollectors()
auto transactionResult = Refresh::collectTransactionRefreshResult(transactionRpc, snapshot, 321, 4);
EXPECT_TRUE(transactionRpc.methodNames() == std::vector<std::string>({
"listtransactions", "z_listreceivedbyaddress", "z_listreceivedbyaddress",
"z_listsentbyaddress", "z_viewtransaction", "gettransaction"
"z_viewtransaction", "gettransaction"
}));
EXPECT_EQ(transactionRpc.calls[1].params, json::array({"zs-one", 0}));
EXPECT_EQ(transactionRpc.calls[2].params, json::array({"zs-two", 0}));
EXPECT_EQ(transactionRpc.calls[3].params, json::array({"zs-one", 0}));
EXPECT_EQ(transactionRpc.calls[3].params, json::array({"pending-send"}));
EXPECT_EQ(transactionRpc.calls[4].params, json::array({"pending-send"}));
EXPECT_EQ(transactionRpc.calls[5].params, json::array({"pending-send"}));
EXPECT_EQ(transactionResult.blockHeight, 321);
EXPECT_EQ(transactionResult.newViewTxEntries.size(), static_cast<size_t>(1));
EXPECT_EQ(transactionResult.newViewTxEntries.count("pending-send"), static_cast<size_t>(1));
@@ -1659,129 +1657,6 @@ void testNetworkRefreshRpcCollectors()
EXPECT_EQ(transactionResult.transactions.front().timestamp, static_cast<int64_t>(500));
EXPECT_EQ(transactionResult.transactions[1].txid, std::string("transparent-a"));
// Outgoing shielded (z->z) send discovery. A pure z->z send has valueBalance == 0, so it is
// absent from listtransactions AND (being a send, not a receive) from z_listreceivedbyaddress —
// it is surfaced only via the whole-wallet z_listsentbyaddress sweep, whose txids are folded
// into the z_viewtransaction enrichment to build the send row. Regression test for the
// "outgoing transactions missing from History" bug.
{
Refresh::TransactionRefreshSnapshot zzSnapshot;
zzSnapshot.shieldedAddresses = {"zs-mine"};
MockRefreshRpc zzRpc;
zzRpc.addResponse("listtransactions", json::array()); // no transparent movement
zzRpc.addResponse("z_listreceivedbyaddress", json::array()); // it's a send, not a receive
zzRpc.addResponse("z_listsentbyaddress", json::array({
json{{"txid", "zz-send"}} // discovered only here
}));
zzRpc.addResponse("z_viewtransaction", json{
{"spends", json::array({json{{"address", "zs-mine"}}})},
{"outputs", json::array({
json{{"outgoing", true}, {"address", "zs-recipient"}, {"value", 1.25},
{"memoStr", "z2z memo"}}
})}
});
zzRpc.addResponse("gettransaction", json{{"time", 800}, {"confirmations", 3}});
auto zzResult = Refresh::collectTransactionRefreshResult(zzRpc, zzSnapshot, 400, 4);
EXPECT_TRUE(zzRpc.methodNames() == std::vector<std::string>({
"listtransactions", "z_listreceivedbyaddress", "z_listsentbyaddress",
"z_viewtransaction", "gettransaction"
}));
EXPECT_EQ(zzResult.transactions.size(), static_cast<size_t>(1));
EXPECT_EQ(zzResult.transactions[0].txid, std::string("zz-send"));
EXPECT_EQ(zzResult.transactions[0].type, std::string("send"));
EXPECT_NEAR(zzResult.transactions[0].amount, -1.25, 0.00000001);
EXPECT_EQ(zzResult.transactions[0].address, std::string("zs-recipient"));
EXPECT_EQ(zzResult.transactions[0].memo, std::string("z2z memo"));
EXPECT_EQ(zzResult.transactions[0].timestamp, static_cast<int64_t>(800));
EXPECT_EQ(zzResult.newViewTxEntries.count("zz-send"), static_cast<size_t>(1));
// Guard: with no shielded addresses the sweep is skipped entirely (transparent sends are
// already covered by listtransactions), so no z_listsentbyaddress call is made.
Refresh::TransactionRefreshSnapshot noZSnapshot;
MockRefreshRpc noZRpc;
noZRpc.addResponse("listtransactions", json::array());
auto noZResult = Refresh::collectTransactionRefreshResult(noZRpc, noZSnapshot, 401, 4);
EXPECT_TRUE(noZRpc.methodNames() == std::vector<std::string>({"listtransactions"}));
EXPECT_EQ(noZResult.transactions.size(), static_cast<size_t>(0));
}
// z->t deshield: the external transparent recipient is reported ONLY in z_listsentbyaddress's
// sends.transparentSends (z_viewtransaction sees only the shielded change note, isOutgoing=false;
// listtransactions omits the t-vout because inputs are shielded). The row must be synthesized
// directly from that payload — a direct sibling of the z->z gap.
{
Refresh::TransactionRefreshSnapshot ztSnapshot;
ztSnapshot.shieldedAddresses = {"zs-mine"};
MockRefreshRpc ztRpc;
ztRpc.addResponse("listtransactions", json::array()); // z-originated: no t-input row
ztRpc.addResponse("z_listreceivedbyaddress", json::array());
ztRpc.addResponse("z_listsentbyaddress", json::array({
json{{"txid", "zt-send"}, {"confirmations", 5}, {"blocktime", 900},
{"sends", json{
{"transparentSends", json::array({
json{{"address", "R-recipient"}, {"amount", -2.5}, {"vout", 0}}
})},
{"saplingSends", json::array()}
}}}
}));
// z->t viewtransaction: only a change note back to self -> no outgoing outputs.
ztRpc.addResponse("z_viewtransaction", json{{"spends", json::array()}, {"outputs", json::array()}});
auto ztResult = Refresh::collectTransactionRefreshResult(ztRpc, ztSnapshot, 500, 4);
EXPECT_EQ(ztResult.transactions.size(), static_cast<size_t>(1));
EXPECT_EQ(ztResult.transactions[0].txid, std::string("zt-send"));
EXPECT_EQ(ztResult.transactions[0].type, std::string("send"));
EXPECT_NEAR(ztResult.transactions[0].amount, -2.5, 0.00000001);
EXPECT_EQ(ztResult.transactions[0].address, std::string("R-recipient"));
EXPECT_EQ(ztResult.transactions[0].confirmations, 5);
}
// t->t send is reported by BOTH listtransactions AND z_listsentbyaddress's transparentSends —
// it must appear ONCE (deduped by txid+address+amount), not doubled.
{
Refresh::TransactionRefreshSnapshot ttSnapshot;
ttSnapshot.shieldedAddresses = {"zs-mine"};
MockRefreshRpc ttRpc;
ttRpc.addResponse("listtransactions", json::array({
json{{"txid", "tt-send"}, {"category", "send"}, {"amount", -1.0},
{"time", 800}, {"confirmations", 3}, {"address", "R-dest"}}
}));
ttRpc.addResponse("z_listreceivedbyaddress", json::array());
ttRpc.addResponse("z_listsentbyaddress", json::array({
json{{"txid", "tt-send"}, {"confirmations", 3}, {"blocktime", 800},
{"sends", json{{"transparentSends", json::array({
json{{"address", "R-dest"}, {"amount", -1.0}, {"vout", 0}}
})}}}}
}));
ttRpc.addResponse("z_viewtransaction", json{{"spends", json::array()}, {"outputs", json::array()}});
auto ttResult = Refresh::collectTransactionRefreshResult(ttRpc, ttSnapshot, 500, 4);
int ttSends = 0;
for (const auto& t : ttResult.transactions)
if (t.txid == std::string("tt-send") && t.type == std::string("send")) ++ttSends;
EXPECT_EQ(ttSends, 1); // not double-counted
}
// Multi-output send: two outgoing outputs of EQUAL value to DIFFERENT recipients in one tx must
// both render (address-keyed dedup) — an amount-only dedup dropped the second.
{
Refresh::TransactionRefreshSnapshot multiSnapshot;
multiSnapshot.sendTxids = {"multi-send"};
MockRefreshRpc multiRpc;
multiRpc.addResponse("listtransactions", json::array());
multiRpc.addResponse("z_viewtransaction", json{
{"spends", json::array({json{{"address", "zs-from"}}})},
{"outputs", json::array({
json{{"outgoing", true}, {"address", "zs-a"}, {"value", 0.5}, {"memoStr", "a"}},
json{{"outgoing", true}, {"address", "zs-b"}, {"value", 0.5}, {"memoStr", "b"}}
})}
});
multiRpc.addResponse("gettransaction", json{{"time", 700}, {"confirmations", 2}});
auto multiResult = Refresh::collectTransactionRefreshResult(multiRpc, multiSnapshot, 500, 4);
int multiSends = 0;
for (const auto& t : multiResult.transactions)
if (t.txid == std::string("multi-send") && t.type == std::string("send")) ++multiSends;
EXPECT_EQ(multiSends, 2); // both equal-value outputs kept
}
Refresh::TransactionRefreshSnapshot cachedOnlySnapshot;
auto cachedOnlyEntry = cachedEntry;
cachedOnlyEntry.timestamp = 450;
@@ -1927,7 +1802,6 @@ void testNetworkRefreshRpcCollectors()
strictRpc.addResponse("listtransactions", json::array());
strictRpc.addResponse("z_listreceivedbyaddress", json::array());
strictRpc.addResponse("z_listreceivedbyaddress", json::array());
strictRpc.addResponse("z_listsentbyaddress", json::array());
auto strictResult = Refresh::collectTransactionRefreshResult(strictRpc, strictSnapshot, 102, 4);
EXPECT_EQ(strictResult.shieldedAddressesScanned, static_cast<size_t>(2)); // both re-scanned
@@ -1939,12 +1813,10 @@ void testNetworkRefreshRpcCollectors()
MockRefreshRpc tolerantRpc;
tolerantRpc.addResponse("listtransactions", json::array());
// No z_listreceivedbyaddress responses: the addresses must be skipped (else call() throws).
// The shielded scan still completes, so the whole-wallet z_listsentbyaddress sweep runs once.
tolerantRpc.addResponse("z_listsentbyaddress", json::array());
auto tolerantResult = Refresh::collectTransactionRefreshResult(tolerantRpc, tolerantSnapshot, 102, 4);
EXPECT_EQ(tolerantResult.shieldedAddressesScanned, static_cast<size_t>(0)); // both skipped
EXPECT_TRUE(tolerantResult.shieldedScanComplete);
EXPECT_TRUE(tolerantRpc.methodNames() == std::vector<std::string>({"listtransactions", "z_listsentbyaddress"}));
EXPECT_TRUE(tolerantRpc.methodNames() == std::vector<std::string>({"listtransactions"}));
}
Refresh::TransactionRefreshSnapshot recentSnapshot;
@@ -2030,11 +1902,10 @@ void testNetworkRefreshRpcCollectors()
MockRefreshRpc finalShieldedRpc;
finalShieldedRpc.addResponse("listtransactions", json::array());
finalShieldedRpc.addResponse("z_listreceivedbyaddress", json::array());
finalShieldedRpc.addResponse("z_listsentbyaddress", json::array());
auto finalShielded = Refresh::collectTransactionRefreshResult(
finalShieldedRpc, partialShieldedSnapshot, 400, 0);
EXPECT_TRUE(finalShieldedRpc.methodNames() == std::vector<std::string>({
"listtransactions", "z_listreceivedbyaddress", "z_listsentbyaddress"
"listtransactions", "z_listreceivedbyaddress"
}));
EXPECT_EQ(finalShieldedRpc.calls[1].params, json::array({"zs-two", 0}));
EXPECT_TRUE(finalShielded.shieldedScanComplete);
@@ -2051,10 +1922,9 @@ void testNetworkRefreshRpcCollectors()
});
MockRefreshRpc cachedShieldedRpc;
cachedShieldedRpc.addResponse("listtransactions", json::array());
cachedShieldedRpc.addResponse("z_listsentbyaddress", json::array());
auto cachedShielded = Refresh::collectTransactionRefreshResult(
cachedShieldedRpc, cachedShieldedSnapshot, 500, 0);
EXPECT_TRUE(cachedShieldedRpc.methodNames() == std::vector<std::string>({"listtransactions", "z_listsentbyaddress"}));
EXPECT_TRUE(cachedShieldedRpc.methodNames() == std::vector<std::string>({"listtransactions"}));
EXPECT_TRUE(cachedShielded.shieldedScanComplete);
EXPECT_EQ(cachedShielded.shieldedAddressesScanned, static_cast<size_t>(0));
EXPECT_EQ(cachedShielded.transactions.size(), static_cast<size_t>(1));
@@ -2100,7 +1970,6 @@ void testNetworkRefreshRpcCollectors()
json{{"txid", "shielded-mined"}, {"amount", 4.0}, {"confirmations", 102},
{"time", 210}, {"memoStr", "pool"}}
}));
miningTxRpc.addResponse("z_listsentbyaddress", json::array());
auto miningTxResult = Refresh::collectTransactionRefreshResult(miningTxRpc, miningSnapshot, 328, 0);
EXPECT_EQ(miningTxResult.transactions.size(), static_cast<size_t>(2));
EXPECT_EQ(miningTxResult.transactions[0].type, std::string("mined"));
@@ -2136,25 +2005,6 @@ void testNetworkRefreshResultModels()
EXPECT_EQ(state.longestchain, 110);
EXPECT_EQ(state.notarized, 90);
EXPECT_EQ(state.last_balance_update, static_cast<int64_t>(1234));
EXPECT_TRUE(state.sync.tip_known); // longestchain>0 -> peer-derived tip is known
EXPECT_FALSE(state.sync.isSynced()); // still catching up (blocks 100 < tip 110)
// Peerless node: the daemon reports longestchain == 0. Even at the local best height (blocks ==
// headers) it must NOT be reported synced (the tip is unknown) — otherwise Send would be offered
// against a stale balance on a node that has silently lost all peers. (audit: peerless-synced)
{
dragonx::WalletState peerless;
auto pc = Refresh::parseCoreRefreshResult(
json{{"private", "5.00000000"}, {"transparent", "0.00000000"}, {"total", "5.00000000"}},
json{{"private", "5.00000000"}, {"transparent", "0.00000000"}, {"total", "5.00000000"}},
true,
json{{"blocks", 200}, {"headers", 200}, {"bestblockhash", "peerless-200"},
{"verificationprogress", 1.0}, {"longestchain", 0}, {"notarized", 0}},
true);
Refresh::applyCoreRefreshResult(peerless, pc, 1235);
EXPECT_FALSE(peerless.sync.tip_known);
EXPECT_FALSE(peerless.sync.isSynced()); // peerless -> not synced despite blocks == headers
}
auto connectionInfo = Refresh::parseConnectionInfoResult(
json{{"version", 120000}, {"protocolversion", 170002}, {"p2pport", 8233},
@@ -2788,28 +2638,6 @@ void testNodeStatusBanner()
NodeBannerInputs in; in.connected = true;
EXPECT_TRUE(!evaluateNodeStatusBanner(in).show);
}
// Connected but no peer-derived tip (0 peers) once chain info is in → amber "no peers" banner.
{
NodeBannerInputs in;
in.connected = true; in.tip_known = false; in.blocks = 3262059;
in.connection_status = "Peers: 0";
NodeBannerState s = evaluateNodeStatusBanner(in);
EXPECT_TRUE(s.show);
EXPECT_TRUE(s.severity == NodeBannerSeverity::Warning);
EXPECT_TRUE(s.reason == NodeBannerReason::NoPeers);
EXPECT_TRUE(s.action == NodeBannerAction::None);
EXPECT_EQ(s.detail, std::string("Peers: 0"));
}
// Connected, tip unknown, but no chain info yet (blocks==0) → no banner (avoid startup flicker).
{
NodeBannerInputs in; in.connected = true; in.tip_known = false; in.blocks = 0;
EXPECT_TRUE(!evaluateNodeStatusBanner(in).show);
}
// Connected with a known tip → no banner.
{
NodeBannerInputs in; in.connected = true; in.tip_known = true; in.blocks = 100;
EXPECT_TRUE(!evaluateNodeStatusBanner(in).show);
}
// Expected startup phases own the screen (loading/warmup overlay) → no banner.
{
NodeBannerInputs in; in.warming_up = true;
@@ -2964,12 +2792,6 @@ void testSeedMigrationResume()
// No txid and no opid → the Sweep gate (whether or not connected).
EXPECT_TRUE(decideSeedMigrationResume(true, true, "", "", true) == MigrationResume::SweepGate);
EXPECT_TRUE(decideSeedMigrationResume(true, true, "", "", false) == MigrationResume::SweepGate);
// fund-safety: the Sweep step may only short-circuit a ~0 balance straight to adopt when NO sweep
// was ever submitted (a genuinely empty wallet). Once a sweep has been broadcast, a ~0 balance is
// ambiguous (the tx may be unconfirmed in the mempool), so direct adopt must be blocked.
EXPECT_TRUE(dragonx::sweepGateMayAdoptEmptyWallet(/*sweepSubmitted=*/false)); // empty wallet -> ok
EXPECT_FALSE(dragonx::sweepGateMayAdoptEmptyWallet(/*sweepSubmitted=*/true)); // in-flight -> blocked
}
void testLoggerFileSink()
@@ -5479,92 +5301,6 @@ void testLiteWalletControllerAsyncLifecycleFailover()
dragonx::test::g_liteFakeWarmupServerSubstr.clear();
}
// C1/M1: the ASYNC lifecycle path (the one the Settings UI actually calls) must apply the
// passphrase — encrypt a freshly created/restored wallet, unlock an existing encrypted one.
// Previously the async path discarded the passphrase (encrypt/unlock lived only in the sync
// wrappers), so a UI-created wallet stored its seed in PLAINTEXT and an encrypted wallet opened
// but stayed locked.
void testLiteWalletControllerAsyncAppliesPassphrase()
{
using namespace dragonx::wallet;
const auto liteCaps = makeWalletCapabilities(WalletBuildKind::Lite, false, true);
LiteConnectionSettings conn;
conn.chainName = "main";
conn.servers = { LiteServerEndpoint{"https://good.example", "Good", true} };
conn.selectionMode = LiteServerSelectionMode::Sticky;
conn.stickyServerUrl = "https://good.example";
const auto drain = [](LiteWalletController& c) {
for (int i = 0; i < 400 && c.lifecycleRequestInProgress(); ++i)
std::this_thread::sleep_for(std::chrono::milliseconds(5));
c.pumpLifecycleResult();
};
// C1: async create with a passphrase encrypts (and locks) the new wallet; same passphrase unlocks.
{
dragonx::test::resetLiteFakeCounters();
dragonx::test::g_liteFakeWalletExists = false;
dragonx::test::g_liteFakeEncrypted = false;
dragonx::test::g_liteFakeLocked = false;
LiteWalletController controller(liteCaps, conn,
LiteClientBridge::fromApi(dragonx::test::makeFakeLiteApi()));
LiteWalletCreateRequest req;
req.passphrase = "hunter2";
EXPECT_TRUE(controller.beginCreateWalletAsync(req));
drain(controller);
EXPECT_TRUE(controller.walletOpen());
const auto s = controller.encryptionStatus();
EXPECT_TRUE(s.encrypted); // async create ENCRYPTED the wallet (C1 fixed)
EXPECT_TRUE(s.locked); // encrypt locks immediately
EXPECT_TRUE(controller.unlockWallet("hunter2"));
EXPECT_FALSE(controller.encryptionStatus().locked);
}
// C1: async restore with a passphrase likewise encrypts the restored wallet.
{
dragonx::test::resetLiteFakeCounters();
dragonx::test::g_liteFakeWalletExists = false;
dragonx::test::g_liteFakeEncrypted = false;
dragonx::test::g_liteFakeLocked = false;
LiteWalletController controller(liteCaps, conn,
LiteClientBridge::fromApi(dragonx::test::makeFakeLiteApi()));
LiteWalletRestoreRequest req;
req.seedPhrase = "abandon abandon abandon abandon abandon abandon abandon abandon "
"abandon abandon abandon abandon abandon abandon abandon abandon "
"abandon abandon abandon abandon abandon abandon abandon art";
req.birthday = 0;
req.passphrase = "hunter2";
EXPECT_TRUE(controller.beginRestoreWalletAsync(req));
drain(controller);
EXPECT_TRUE(controller.walletOpen());
EXPECT_TRUE(controller.encryptionStatus().encrypted); // C1 fixed for restore
}
// M1: async open of an already-encrypted+locked wallet unlocks it with the supplied passphrase.
{
dragonx::test::resetLiteFakeCounters();
dragonx::test::g_liteFakeWalletExists = true;
dragonx::test::g_liteFakeEncrypted = true; // existing wallet is encrypted...
dragonx::test::g_liteFakeLocked = true; // ...and locked at open time
LiteWalletController controller(liteCaps, conn,
LiteClientBridge::fromApi(dragonx::test::makeFakeLiteApi()));
LiteWalletOpenRequest req;
req.passphrase = "hunter2";
EXPECT_TRUE(controller.beginOpenWalletAsync(req));
drain(controller);
EXPECT_TRUE(controller.walletOpen());
const auto s = controller.encryptionStatus();
EXPECT_TRUE(s.encrypted);
EXPECT_FALSE(s.locked); // async open UNLOCKED it (M1 fixed)
}
dragonx::test::g_liteFakeWalletExists = false;
dragonx::test::g_liteFakeEncrypted = false;
dragonx::test::g_liteFakeLocked = false;
dragonx::test::g_liteFakeDeadServerSubstr.clear();
dragonx::test::g_liteFakeWarmupServerSubstr.clear();
}
// M2: a parsed lite refresh bundle maps through to the app's WalletState (the last hop
// the Balance/Receive/Transactions tabs read), with zatoshi->DRGX conversion, z/t address
// split, transaction typing, confirmations, and sync progress.
@@ -7764,7 +7500,6 @@ int main()
testLiteWalletControllerM5Persistence();
testLiteWalletControllerEncryption();
testLiteWalletControllerCreateEncryptsWithPassphrase();
testLiteWalletControllerAsyncAppliesPassphrase();
testLiteChainNameMigration();
testLiteRefreshModelAppliesToWalletState();
testLiteSendShowsRecipientFromOutgoing();