Skip to content

test: backend E2E test harness with SPV wallet scenario#673

Merged
lklimek merged 92 commits into
v1.0-devfrom
test/backend-e2e-spv-wallet
Mar 18, 2026
Merged

test: backend E2E test harness with SPV wallet scenario#673
lklimek merged 92 commits into
v1.0-devfrom
test/backend-e2e-spv-wallet

Conversation

@lklimek
Copy link
Copy Markdown
Contributor

@lklimek lklimek commented Mar 2, 2026

Issue being fixed or feature implemented

Refs #672 (partial — establishes the backend E2E test harness; CLI interface is a separate effort)

User Story

Imagine you are a developer working on Dash Evo Tool. You want to verify that
wallet operations, identity management, and DPNS registration actually work
against Dash testnet — not just in unit tests, but end-to-end through the same
AppContext and BackendTask pipeline the real app uses. You run a single
cargo test command and get confidence that the core backend works correctly
before shipping.

Summary

  • Backend E2E test framework with shared singleton BackendTestContext (via tokio::sync::OnceCell) that initializes SPV, connects to testnet peers, and restores a pre-funded framework wallet
  • Test wallet lifecycle: create_funded_test_wallet() generates fresh wallets, funds them from the framework wallet, and waits for spendable balance
  • Reliable spendable balance checking: Wallet::spv_confirmed_balance() returns None when SPV hasn't synced yet (no max_balance() fallback), and wait_for_spendable_balance() uses it to avoid false positives
  • Explicit data_dir threading: AppContext::new() takes data_dir: PathBuf instead of relying on unsafe { set_var("DASH_EVO_DATA_DIR") } — eliminates UB and enables parallel test execution
  • Type-safe error handling: register_wallet() returns Result<_, TaskError> with dedicated WalletAlreadyImported variant; Wallet::new_from_seed() returns TaskError with WalletKeyDerivationFailed
  • Production code improvements: IdentityKeys fields are pub(crate), build_identity_registration() and get_receive_address() moved to production code, Secret type used for wallet passwords
  • 6 test scenarios: SPV wallet sync, send/receive funds, fetch contracts, identity creation, DPNS registration, identity withdrawal
  • Cleanup subsystem: sweeps orphaned test wallets from previous runs back to the framework wallet

Test plan

  • cargo clippy --all-features --all-targets -- -D warnings passes
  • cargo +nightly fmt --all passes
  • cargo test --all-features --workspace passes (unit + doc tests)
  • cargo test --test backend-e2e --all-features -- --ignored --nocapture passes against live testnet
  • Verify wait_for_spendable_balance correctly waits (doesn't return early due to max_balance() fallback)

🤖 Co-authored by Claudius the Magnificent AI Agent

lklimek and others added 30 commits February 17, 2026 16:32
Add UX specification, technical architecture, and HTML mockup for the
MessageBanner component that will replace the ~50 ad-hoc error/message
display implementations across screens with a single reusable component.

Key design decisions:
- Per-screen MessageBanner with show()/set_message() API
- All colors via DashColors (zero hardcoded Color32 values)
- 4 severity levels: Error, Warning, Success, Info
- Auto-dismiss for Success/Info (5s), persistent for Error/Warning
- Follows Component Design Pattern conventions (private fields, builder, show)
- No changes to BackendTask/TaskResult/AppState architecture

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* refactor(context): replace RwLock<Sdk> with ArcSwap<Sdk>

Sdk is internally thread-safe (Arc, ArcSwapOption, atomics) and all
methods take &self. The RwLock was adding unnecessary contention across
backend tasks.

Using ArcSwap instead of plain Sdk because reinit_core_client_and_sdk()
needs to atomically swap the entire Sdk instance when config changes.
ArcSwap provides lock-free reads with atomic swap for the rare write.

Suggested-by: lklimek

* fix: address CodeRabbit review findings for ArcSwap migration

- Fix import ordering: move arc_swap::ArcSwap before crossbeam_channel
- Remove redundant SDK loads in load_identity_from_wallet, register_dpns_name,
  and load_identity — use the sdk parameter already passed to these functions
- Fix stale TODO referencing removed sdk.read().unwrap() API
- Rename sdk_guard → sdk in transfer, withdraw_from_identity, and
  refresh_loaded_identities_dpns_names (no longer lock guards)
- Pass &sdk to run_platform_info_task from dispatch site instead of
  reloading internally
- Fix leftover sdk.write() call in context_provider.rs (RwLock remnant)
- Add missing Color32 import in wallets dialogs

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* refactor: address remaining CodeRabbit review feedback on ArcSwap migration

- Move SDK load outside for loop in refresh_loaded_identities_dpns_names.rs
  so it's loaded once for the batch instead of on each iteration
- Update stale TODO comment in default_platform_version() to reflect that
  this is a free function with no sdk access

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* refactor: consolidate double read-lock on spv_context_provider

Clone the SPV provider in a single lock acquisition, then bind app
context on the clone instead of locking twice.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

---------

Co-authored-by: PastaClaw <thepastaclaw@users.noreply.github.com>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
* refactor: remove unused Insight API references

The `insight_api_url` field in `NetworkConfig` and its associated
`insight_api_uri()` method were never used in production code (both
marked `#[allow(dead_code)]`). Remove the field, method, config
entries, env example lines, and related tests.

https://claude.ai/code/session_01HWPmCJHT8KTZGP9bFiksjn

* refactor: remove unused `show_in_ui` field from NetworkConfig

The `show_in_ui` field was defined on `NetworkConfig` and serialized in
`save()`, but never read by any production code to control network
visibility. Remove the field, its serialization, env example lines,
and test references.

https://claude.ai/code/session_01HWPmCJHT8KTZGP9bFiksjn

* fix: add missing `Color32` import in wallet dialogs

https://claude.ai/code/session_01HWPmCJHT8KTZGP9bFiksjn

---------

Co-authored-by: Claude <noreply@anthropic.com>
* build: remove snap version

* build: add Flatpak packaging and CI workflow

Add Flatpak build manifest, desktop entry, AppStream metadata, and
GitHub Actions workflow for building and distributing Flatpak bundles.
Uses freedesktop 25.08 runtime with rust-stable and llvm21 extensions.
No application source code changes required - works in SPV mode by default.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* build: address review findings for Flatpak packaging

- Pin GitHub Actions to commit SHAs for supply chain security
- Upgrade softprops/action-gh-release from v1 to v2.2.2
- Remove redundant --socket=x11 (fallback-x11 suffices)
- Remove duplicate tag trigger preventing double builds on release
- Remove duplicate env vars inherited from top-level build-options
- Add Flatpak build artifacts to .gitignore
- Add bugtracker URL to AppStream metainfo
- Remove deprecated <categories> from metainfo (use .desktop instead)
- Add Terminal=false and Keywords to desktop entry
- Add disk space check after SDK install in CI
- Rename artifact to include architecture suffix

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* build: simplify CI workflows for Linux-only releases

- Remove "Free disk space" step from flatpak and release workflows
- Remove Windows and macOS builds from release workflow
- Use "ubuntu" runner image instead of pinned versions
- Clean up unused matrix.ext references

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* build: attach to existing releases instead of creating new ones

- Replace release-creating job with attach-to-release (only on release event)
- Add 14-day retention for build artifacts
- On tag push or workflow_dispatch, only upload artifacts (no release)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* revert: restore release.yml to original v1.0-dev version

The release workflow changes were out of scope for the Flatpak PR.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: address CodeRabbit review comments

- Fix CRLF line endings in Flatpak manifest (convert to LF)
- Set app_id on ViewportBuilder to match desktop StartupWMClass
- Use --locked flag for reproducible cargo builds in Flatpak
- Rename --repo=repo to --repo=flatpak-repo to match .gitignore
- Add architecture note for protoc x86_64 binary

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* docs: add Flatpak install instructions to README

Add a dedicated section for installing via Flatpak on Linux,
clarify that prerequisites are only needed for building from source,
and rename "Installation" to "Build from Source" for clarity.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: match StartupWMClass to Flatpak app_id

Use reverse-DNS format org.dash.DashEvoTool to match the
Wayland app_id set via ViewportBuilder::with_app_id().

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: use ** glob for branch trigger to match feat/flatpak

Single * doesn't match path separators in GitHub Actions branch filters.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* feat: add aarch64 Flatpak build and caching to CI

- Add matrix strategy for parallel x86_64 and aarch64 builds
- Patch protoc URL/sha256 per architecture at build time
- Cache .flatpak-builder directory keyed on arch + manifest + lockfile
- Pin actions/cache to SHA

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: convert desktop and metainfo files to LF line endings

Flatpak builder validates desktop files and rejects CRLF line endings.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* build: cancel in-progress Flatpak builds on new push

Add concurrency group keyed on git ref so a new push cancels
any running build for the same branch or release.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: address PR review findings for Flatpak packaging

- Remove unnecessary --filesystem=xdg-config/dash-evo-tool:create
  (Flatpak already redirects XDG_CONFIG_HOME to sandbox)
- Add categories and keywords to AppStream metainfo for discoverability
- Update README with both x86_64/aarch64 install commands, uninstall
  instructions, and Flatpak data path note
- Clarify aarch64 comment in manifest to reference CI sed patching

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* chore: workflow timeout and perms

* fix: move permissions to job level in Flatpak workflow

Step-level permissions are not valid in GitHub Actions. Move
contents: write to the job level where it is needed for release
attachment.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* build: cache Cargo registry and target in Flatpak CI

Bind-mount host-side cargo-cache and cargo-target directories into the
Flatpak build sandbox so CARGO_HOME and target/ persist across builds.
Uses split restore/save with cleanup of incremental and registry/src
(similar to Swatinem/rust-cache).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: scope cargo cache bind-mount sed to build-args only

The previous sed matched --share=network in both finish-args and
build-args, corrupting finish-args. Use a sed range to only target
the build-args section.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Apply suggestions from code review

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Aligns elapsed display with the countdown timer which already adds 1
to avoid showing "0s" immediately.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Resolves conflict in mint_tokens_screen.rs by combining both Warning
handling from the base branch and refresh_banner clearing from HEAD.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Merge v1.0-dev (d45b3ee) into the unified message display branch,
resolving 45 conflicted files across the codebase.

Resolution strategy:
- Keep the unified MessageBanner approach (design branch) for all
  screen-level message display, replacing v1.0-dev's inline colored
  labels and per-screen message fields
- Keep simplified status enums without timestamp parameters, since
  MessageBanner handles elapsed-time display via BannerHandle
- Merge v1.0-dev's MessageBanner enhancements: details/suggestion
  fields, collapsible details section, tracing-based logging,
  BannerState::new()/reset_to() constructors, distinct error icon
- Merge v1.0-dev's batch refresh counting in IdentitiesScreen
  (pending_refresh_count, total_refresh_count, pluralized messages)
- Merge v1.0-dev's ContactDetailsScreen database loading and
  Platform persistence via backend tasks
- Merge v1.0-dev's improved UX text in ContactRequests (informative
  label instead of non-functional Cancel button)
- Fix post-merge issues: remove references to deleted `message` field
  in ContactDetailsScreen, fix infinite recursion in display_message

https://claude.ai/code/session_015EEVFee5cXpgaGASfZcAN3
Resolve 3 conflicting files (add_existing_identity_screen, add_new_identity_screen,
wallets_screen) by preferring v1.0-dev changes and adapting them to the unified
MessageBanner pattern where needed.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
set_global() no longer resets timestamps, auto-dismiss timer, or
logged flag when a banner with identical text already exists. This
makes it safe to call every frame without log spam or timer restarts.

Cherry-picked from origin/fix/spv-peer-timeout (08e3b3b).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- R01: Replace expect("No key selected") with graceful match + error
  banner in 11 token screens to prevent panics on missing signing key
- R03: Remove dead backend_message field from AddExistingIdentityScreen
- R06: Replace is_some() + unwrap() with idiomatic if-let-Some pattern
  in 10 token screens; use is_some_and() in structs.rs
- R09: Add use imports for MessageBanner in 5 dashpay screens, replacing
  22 fully-qualified crate::ui::components::MessageBanner:: calls
- R10: Replace custom_dash_qt_error_message inline rendering with
  MessageBanner::set_global in network_chooser_screen

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…C-07

- SEC-08: Restore safe if-let-Some pattern in WithdrawalScreen::refresh()
  to prevent double unwrap() panic on DB error or deleted identity
- SEC-09: Restore original DB lookup in SendPaymentScreen::load_contact_info()
  replacing hardcoded "alice.dash" mock data
- RUST-015: Revert unimplemented!() back to ui.label() in
  update_token_config MarketplaceTradeMode arm
- SEC-05: Add success banners for contact request accept/reject in
  ContactRequests::display_task_result
- SEC-07: Add MessageBanner::clear_all_global() and call it from
  AppState::change_network() to prevent stale banners leaking across
  network switches

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add fallible constructor rule (Result<Self, ...> when they can fail),
rename section to "General rules", and document MessageBanner
idempotency (no guard needed for set_global).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Replace all expect() calls in token screen constructors and confirmation
handlers with MessageBanner error display. Constructors handle errors
internally and return Self with degraded state, keeping create_screen()
clean. refresh() methods now show errors via MessageBanner instead of
tracing-only logging.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Change MessageBanner public methods to accept `impl Display` for
message text and `impl Debug` for details, instead of `&str`. Remove
needless `&format!(...)` borrows across 27 call sites.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Display persistent MessageBanner notifications based on network
connection state transitions. Mode-aware messages guide users toward
the right recovery action (RPC vs SPV).

- Disconnected (RPC): "Disconnected — check that Dash Core is running"
- Disconnected (SPV): "Disconnected — check your internet connection"
- Syncing (RPC): "Syncing with Dash Core…"
- Syncing (SPV): "SPV sync in progress…"
- Synced: banner cleared

Uses Option<OverallConnectionState> for change detection, with None
as initial/post-network-switch sentinel to force first evaluation.

Closes #667 (partial — action links deferred to follow-up)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Copy link
Copy Markdown

@claude claude Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Claudius the Magnificent's Follow-up Review — PR #673

The Grand Admiral returns for round two. The previous 26 review threads remain unaddressed — I shall not repeat myself. Instead, three NEW findings that slipped past the first wave.

New Findings (MEDIUM)

ID Title Location
RUST-002 derive_first_address silently returns empty maps wallet/mod.rs:470-495
RUST-001 UTXO spendability workaround duplicated core/mod.rs:745-808
PROJ-009 Fragile string matching for wallet-exists check harness.rs:146

Previous Review Status

26 existing threads, 0 addressed. The HIGH finding (missing pending_wallet_selection) and both MEDIUM findings from the first review remain open. The coderabbitai threads are similarly untouched.

Verdict

Still requesting changes. The 3 new findings above are in addition to the 26 unresolved threads from prior reviews.


Review by Claudius the Magnificent — Grand Admiral of Code, Lord of All Compilers

Comment thread src/model/wallet/mod.rs Outdated
Comment thread src/backend_task/core/mod.rs Outdated
Comment thread tests/backend-e2e/harness.rs Outdated
Production code:
- Extract patch_utxo_spendability_flags() helper to deduplicate
  workaround in estimate_fallback_amount and build_unsigned_payment_tx
- Add IdentityKeys::new() constructor
- Make wallet+address persistence atomic via store_wallet_with_addresses()
- Add pending_wallet_selection after wallet creation

Test code:
- Add DPNS registration retry for identity propagation delay
- Use u64 hex for DPNS name uniqueness (CMT-023)
- Calibrate test funding amounts per reviewer feedback
- Add fragility note on string-match wallet detection (CMT-006)
- Add TODO for identity fund withdrawal in cleanup (CMT-032)
- Add INTENTIONAL comment for bounded channel design (CMT-017)
- Add balance assertion for return leg in send_funds test
- Log reconcile_spv_wallets errors in wait helpers
- Use is_ok_and for clarity in spv_wallet test

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 7

♻️ Duplicate comments (4)
src/backend_task/core/mod.rs (1)

924-935: ⚠️ Potential issue | 🟠 Major

Don't infer InstantSend from height == 0.

height == 0 only means "unconfirmed". Marking every mempool UTXO as is_instantlocked makes fallback estimation and coin selection treat non-locked outputs as spendable, which can build follow-up spends the network rejects. Leave is_instantlocked unchanged unless you have a real lock signal.

Suggested fix
 fn patch_utxo_spendability_flags(utxo: &mut Utxo) {
     if utxo.height > 0 {
         utxo.is_confirmed = true;
-    } else {
-        utxo.is_instantlocked = true;
     }
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/backend_task/core/mod.rs` around lines 924 - 935, In
patch_utxo_spendability_flags, stop inferring InstantSend from utxo.height == 0;
only set utxo.is_confirmed = true when utxo.height > 0 and otherwise leave
utxo.is_instantlocked untouched so mempool UTXOs are not incorrectly marked
instant-locked. Update the function (patch_utxo_spendability_flags) to remove or
guard the assignment to utxo.is_instantlocked and only modify is_instantlocked
when a real InstantSend lock signal is present elsewhere.
tests/backend-e2e/task_runner.rs (1)

12-22: ⚠️ Potential issue | 🟠 Major

Drain this channel instead of leaving it unread.

This helper is generic over all backend tasks. An unread tokio::mpsc::channel(32) will stall the 33rd TaskResult, so a chatty task can hang the test harness.

Suggested fix
-    let (tx, _rx) = tokio::sync::mpsc::channel::<TaskResult>(32);
+    let (tx, mut rx) = tokio::sync::mpsc::channel::<TaskResult>(32);
+    tokio::spawn(async move {
+        while rx.recv().await.is_some() {}
+    });
     let sender = SenderAsync::new(tx, egui::Context::default());
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/backend-e2e/task_runner.rs` around lines 12 - 22, The test helper
run_task creates a tokio::mpsc channel and drops the receiver, which can block
the sender if >32 TaskResult messages are sent; fix this by spawning a
background task to continuously drain the receiver instead of leaving it unread:
after creating (tx, rx) call tokio::spawn with an async move loop that awaits
rx.recv() and discards each TaskResult until None, then proceed to construct
SenderAsync::new(tx, egui::Context::default()) and call
app_context.run_backend_task(task, sender). Ensure you reference the local
variables tx and rx, the function run_task, SenderAsync::new, and
app_context.run_backend_task when making the change.
tests/backend-e2e/send_funds.rs (1)

148-167: ⚠️ Potential issue | 🟠 Major

This return-leg check can succeed before wallet A receives anything back.

hash_a already has well over send_amount after the first A→B payment, so wait_for_balance(..., send_amount, ...) can pass immediately and the test still won't prove the second transfer arrived. Capture A's balance just before Line 148 and wait for it to increase; with subtract_fee_from_amount = true, send_amount is not the correct threshold anyway.

Suggested change
+    let a_balance_before_return = {
+        let w = wallet_a.read().expect("lock");
+        w.total_balance_duffs()
+    };
+
-    let _result = send_with_retry(
+    let _result = send_with_retry(
         app_context,
         &wallet_b,
         hash_b,
         a_address,
         send_amount,
         true,
         "E2E test B->A return",
     )
     .await;
 
     // Verify A received the return payment
     let a_balance_after_return = wait_for_balance(
         app_context,
         hash_a,
-        send_amount, // A should have at least send_amount back (minus fee from B)
+        a_balance_before_return + 1,
         Duration::from_secs(120),
     )
     .await
     .expect("A should receive return funds from B");
 
+    assert!(
+        a_balance_after_return > a_balance_before_return,
+        "A should receive the return payment"
+    );
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/backend-e2e/send_funds.rs` around lines 148 - 167, Before sending the
return from B, read and store wallet A's current balance (using the same
mechanism as wait_for_balance or the balance query that returns a numeric
value), then after calling send_with_retry(...) wait for A's balance to increase
by the expected net amount (consider the fee because subtract_fee_from_amount =
true) rather than waiting for an absolute threshold of send_amount; replace the
current wait_for_balance(app_context, hash_a, send_amount, ...) check with a
wait that compares new_balance >= old_balance + expected_net_received (or uses
wait_for_balance with expected_net_received and the stored old_balance as a
baseline). Ensure you reference hash_a, a_address, send_amount, and
send_with_retry when locating the code to change.
tests/backend-e2e/wait.rs (1)

19-36: ⚠️ Potential issue | 🟠 Major

Fail fast on reconcile/setup errors instead of turning them into balance timeouts.

Both polling loops just log reconcile_spv_wallets() failures and keep spinning when wallet_hash is absent. That burns the full timeout and reports the wrong problem when SPV startup or wallet registration is already broken.

Also applies to: 61-78

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/backend-e2e/wait.rs` around lines 19 - 36, The polling loop swallows
failures from reconcile_spv_wallets() and missing wallet entries, causing long
timeouts with misleading balance errors; change the loop to fail fast by
returning or bubbling the error when reconcile_spv_wallets().await returns
Err(e) (e.g., return Err(anyhow::Error::new(e)) or propagate with ?), and also
return an explicit error if app_context.wallets().read() can't find wallet_hash
instead of silently continuing; update both occurrences (the
reconcile_spv_wallets() check and the wallet lookup logic that calls
wallets.get(&wallet_hash) / wallet.total_balance_duffs()) to propagate failures
immediately.
🧹 Nitpick comments (2)
src/backend_task/identity/mod.rs (1)

94-102: Extract key-data encoding into one helper.

to_key_storage() now repeats the same HASH160-vs-raw encoding branch that to_public_keys_map() also maintains. Centralizing that logic will keep persisted key data and runtime key generation from drifting again.

Also applies to: 141-149

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/backend_task/identity/mod.rs` around lines 94 - 102, The duplicate branch
that encodes public key bytes based on master_private_key_type
(KeyType::ECDSA_HASH160 vs raw) should be pulled into a single helper function
(e.g., encode_public_key_data(master_private_key, master_private_key_type,
&secp) or similar) and used by both to_key_storage() and to_public_keys_map();
move the HASH160 path
(public_key(&secp).pubkey_hash().to_byte_array().to_vec().into()) and the raw
path (public_key(&secp).to_bytes().into()) into that helper and replace the
duplicated logic at the shown location and the other occurrence around lines
141-149 with calls to the new helper.
tests/backend-e2e/identity_withdraw.rs (1)

92-97: Verify the credited Core funds as well.

This test currently passes on the return variant alone. Add a post-withdraw assertion against the wallet or DB state for withdraw_address so the E2E catches wrong-address or wrong-amount regressions.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/backend-e2e/identity_withdraw.rs` around lines 92 - 97, After matching
BackendTaskSuccessResult::WithdrewFromIdentity, add an assertion that checks the
Core balance credited to withdraw_address (or the corresponding DB row) to
ensure the correct amount was received; e.g., after the existing match arm for
WithdrewFromIdentity(fee_result) call the test helper or client to fetch the
wallet/DB balance for withdraw_address (or use your existing
wallet_client.get_balance(withdraw_address) /
query_db_balance(withdraw_address)), compute the expected credited amount from
fee_result or test fixtures, and assert equality (panic/assert_eq!) so the test
fails if the wrong address or amount was used.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/context/wallet_lifecycle.rs`:
- Around line 107-115: The UNIQUE constraint error mapping in the call to
store_wallet_with_addresses currently returns a network-specific message but the
uniqueness is on wallet.seed_hash, so change the mapped message to be
network-agnostic (e.g., "This wallet has already been imported.") in the closure
passed to store_wallet_with_addresses; keep the existing detection of UNIQUE
constraint via e.to_string().contains("UNIQUE constraint failed") and only
replace the returned string, referencing the symbols wallet.seed_hash,
self.network, and store_wallet_with_addresses to locate the code to modify.

In `@src/model/wallet/mod.rs`:
- Around line 407-408: The constructor currently pre-populates known_addresses
and watched_addresses by calling Self::derive_first_address with
master_bip44_ecdsa_extended_public_key, which prevents
AppContext::register_wallet from invoking bootstrap_wallet_addresses (it only
runs when known_addresses.is_empty()), causing imported mnemonics to skip full
BIP44/BIP32/identity/SPV bootstrap. Remove or avoid populating known_addresses
and watched_addresses in the constructor (and the analogous call around
derive_first_address at the other location) so they remain empty on import;
allow register_wallet to call bootstrap_wallet_addresses to perform full address
discovery and SPV watch setup.

In `@src/ui/wallets/add_new_wallet_screen.rs`:
- Around line 162-180: The code currently propagates raw error strings from
Wallet::new_from_seed and app_context.register_wallet directly into self.error;
instead, catch those errors and convert them into a user-friendly MessageBanner
using BannerHandle::with_details(...) to attach the technical error text.
Specifically, wrap calls to Wallet::new_from_seed and
self.app_context.register_wallet in match/if let Err handling, set self.error =
Some(MessageBanner::error("Unable to add wallet").with_details(err.to_string()))
(or equivalent API), and only show the friendly message to the UI while
preserving the original error text in the BannerHandle details; keep the
existing logic that sets receive_address_string/receive_address and
core_wallet_name inside the success branch when Wallet::new_from_seed and
register_wallet succeed.

In `@tests/backend-e2e/harness.rs`:
- Around line 385-389: The call to wait::wait_for_spendable_balance currently
waits for only 1 duff, causing a race with the rest of the funded amount; change
the call to wait for the full funded amount (not 1) so callers can immediately
build transactions. Replace the hardcoded 1 with the actual expected funded
amount variable (e.g. funded_amount or the constant used when funding the test
wallet) in the call to wait::wait_for_spendable_balance(app_context, seed_hash,
<funded_amount>, Duration::from_secs(120)) so the helper blocks until the entire
funded balance is spendable.

In `@tests/backend-e2e/send_funds.rs`:
- Around line 63-66: The retry condition in the Err(e) match in
tests/backend-e2e/send_funds.rs only checks for "Insufficient" or "No UTXOs", so
errors like "No spendable funds available" (occuring when
subtract_fee_from_amount is true) do not trigger the retry/backoff; update the
conditional in that Err(e) arm to also check e.to_string().contains("No
spendable funds available") (or a shorter unique substring like "No spendable")
so the B→A leg will retry when attempt < MAX_RETRIES, keeping the existing
attempt and MAX_RETRIES logic unchanged.

In `@tests/backend-e2e/wait.rs`:
- Around line 66-76: The test is calling Wallet::confirmed_balance_duffs() which
falls back to max_balance() and can report non-spendable local UTXOs as
confirmed; change the wait to read the raw SPV-backed confirmed balance (or add
and call a no-fallback accessor on Wallet such as
confirmed_spv_balance_duffs()/spv_confirmed_balance()) instead of
confirmed_balance_duffs(), so the check uses only the SPV-confirmed fields;
update the code in wait.rs where wallets.get(...).map(|wallet_arc| { let wallet
= wallet_arc.read().expect("wallet lock"); wallet.confirmed_balance_duffs() })
to call the new/no-fallback accessor or directly read the SPV fields and compare
those to min_balance.

---

Duplicate comments:
In `@src/backend_task/core/mod.rs`:
- Around line 924-935: In patch_utxo_spendability_flags, stop inferring
InstantSend from utxo.height == 0; only set utxo.is_confirmed = true when
utxo.height > 0 and otherwise leave utxo.is_instantlocked untouched so mempool
UTXOs are not incorrectly marked instant-locked. Update the function
(patch_utxo_spendability_flags) to remove or guard the assignment to
utxo.is_instantlocked and only modify is_instantlocked when a real InstantSend
lock signal is present elsewhere.

In `@tests/backend-e2e/send_funds.rs`:
- Around line 148-167: Before sending the return from B, read and store wallet
A's current balance (using the same mechanism as wait_for_balance or the balance
query that returns a numeric value), then after calling send_with_retry(...)
wait for A's balance to increase by the expected net amount (consider the fee
because subtract_fee_from_amount = true) rather than waiting for an absolute
threshold of send_amount; replace the current wait_for_balance(app_context,
hash_a, send_amount, ...) check with a wait that compares new_balance >=
old_balance + expected_net_received (or uses wait_for_balance with
expected_net_received and the stored old_balance as a baseline). Ensure you
reference hash_a, a_address, send_amount, and send_with_retry when locating the
code to change.

In `@tests/backend-e2e/task_runner.rs`:
- Around line 12-22: The test helper run_task creates a tokio::mpsc channel and
drops the receiver, which can block the sender if >32 TaskResult messages are
sent; fix this by spawning a background task to continuously drain the receiver
instead of leaving it unread: after creating (tx, rx) call tokio::spawn with an
async move loop that awaits rx.recv() and discards each TaskResult until None,
then proceed to construct SenderAsync::new(tx, egui::Context::default()) and
call app_context.run_backend_task(task, sender). Ensure you reference the local
variables tx and rx, the function run_task, SenderAsync::new, and
app_context.run_backend_task when making the change.

In `@tests/backend-e2e/wait.rs`:
- Around line 19-36: The polling loop swallows failures from
reconcile_spv_wallets() and missing wallet entries, causing long timeouts with
misleading balance errors; change the loop to fail fast by returning or bubbling
the error when reconcile_spv_wallets().await returns Err(e) (e.g., return
Err(anyhow::Error::new(e)) or propagate with ?), and also return an explicit
error if app_context.wallets().read() can't find wallet_hash instead of silently
continuing; update both occurrences (the reconcile_spv_wallets() check and the
wallet lookup logic that calls wallets.get(&wallet_hash) /
wallet.total_balance_duffs()) to propagate failures immediately.

---

Nitpick comments:
In `@src/backend_task/identity/mod.rs`:
- Around line 94-102: The duplicate branch that encodes public key bytes based
on master_private_key_type (KeyType::ECDSA_HASH160 vs raw) should be pulled into
a single helper function (e.g., encode_public_key_data(master_private_key,
master_private_key_type, &secp) or similar) and used by both to_key_storage()
and to_public_keys_map(); move the HASH160 path
(public_key(&secp).pubkey_hash().to_byte_array().to_vec().into()) and the raw
path (public_key(&secp).to_bytes().into()) into that helper and replace the
duplicated logic at the shown location and the other occurrence around lines
141-149 with calls to the new helper.

In `@tests/backend-e2e/identity_withdraw.rs`:
- Around line 92-97: After matching
BackendTaskSuccessResult::WithdrewFromIdentity, add an assertion that checks the
Core balance credited to withdraw_address (or the corresponding DB row) to
ensure the correct amount was received; e.g., after the existing match arm for
WithdrewFromIdentity(fee_result) call the test helper or client to fetch the
wallet/DB balance for withdraw_address (or use your existing
wallet_client.get_balance(withdraw_address) /
query_db_balance(withdraw_address)), compute the expected credited amount from
fee_result or test fixtures, and assert equality (panic/assert_eq!) so the test
fails if the wrong address or amount was used.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 3009ac48-02db-4262-a825-ec37623e694f

📥 Commits

Reviewing files that changed from the base of the PR and between c124149 and e054d4c.

📒 Files selected for processing (16)
  • src/backend_task/core/mod.rs
  • src/backend_task/identity/mod.rs
  • src/context/wallet_lifecycle.rs
  • src/database/wallet.rs
  • src/model/wallet/mod.rs
  • src/ui/wallets/add_new_wallet_screen.rs
  • tests/backend-e2e/cleanup.rs
  • tests/backend-e2e/harness.rs
  • tests/backend-e2e/identity_create.rs
  • tests/backend-e2e/identity_helpers.rs
  • tests/backend-e2e/identity_withdraw.rs
  • tests/backend-e2e/register_dpns.rs
  • tests/backend-e2e/send_funds.rs
  • tests/backend-e2e/spv_wallet.rs
  • tests/backend-e2e/task_runner.rs
  • tests/backend-e2e/wait.rs
🚧 Files skipped from review as they are similar to previous changes (1)
  • tests/backend-e2e/cleanup.rs

Comment thread src/context/wallet_lifecycle.rs
Comment thread src/model/wallet/mod.rs Outdated
Comment thread src/ui/wallets/add_new_wallet_screen.rs
Comment thread src/ui/wallets/add_new_wallet_screen.rs Outdated
Comment thread tests/backend-e2e/framework/harness.rs Outdated
Comment thread tests/backend-e2e/send_funds.rs Outdated
Comment thread tests/backend-e2e/framework/wait.rs
lklimek and others added 2 commits March 11, 2026 13:52
Remove `patch_utxo_spendability_flags()` that faked IS-locked status on
mempool UTXOs. Wait for upstream fix (dashpay/rust-dashcore#514) to
properly set is_confirmed/is_instantlocked flags on UTXOs.

Also:
- Update dashpay/platform rev to aa86b74f7e2
- Adapt to upstream API: FeeLevel→FeeRate, remove NetworkExt import
- Fix retry to catch "No UTXOs" errors in test harness

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Move the single canonical copy to backend_task::identity::mod and have
the UI screen import it, eliminating ~240 lines of duplicated function
and tests.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@lklimek lklimek self-assigned this Mar 11, 2026
@lklimek lklimek added the blocked Blocked by something external to this issue label Mar 11, 2026
@lklimek
Copy link
Copy Markdown
Contributor Author

lklimek commented Mar 11, 2026

Waiting for mempool support in dash-spv

@lklimek lklimek removed their assignment Mar 11, 2026
lklimek and others added 2 commits March 18, 2026 12:31
…spv-wallet

# Conflicts:
#	src/ui/wallets/import_mnemonic_screen.rs
The backend E2E tests need updates after the TaskError migration
(#739) changed AppContext field visibility. Commenting out the
CI steps until the tests are adapted.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Comment thread src/backend_task/identity/mod.rs Outdated
Comment thread src/context/wallet_lifecycle.rs Outdated
Comment thread src/model/wallet/mod.rs Outdated
Comment thread src/model/wallet/mod.rs Outdated
Comment thread tests/backend-e2e/framework/cleanup.rs
Comment thread tests/backend-e2e/README.md Outdated
Comment thread tests/backend-e2e/README.md Outdated
Comment thread tests/backend-e2e/register_dpns.rs Outdated
Comment thread tests/backend-e2e/register_dpns.rs
Comment thread tests/backend-e2e/send_funds.rs Outdated
lklimek and others added 4 commits March 18, 2026 14:35
- Restrict IdentityKeys fields to pub(crate) to prevent private key
  exposure outside the crate
- Change register_wallet() to return TaskError instead of String, using
  proper rusqlite error matching via is_unique_constraint_violation()
  and a new WalletAlreadyImported variant
- Change Wallet::new_from_seed() to accept Option<&Secret> for password
  instead of Option<&str>, keeping sensitive data in the Secret wrapper
- Change Wallet::new_from_seed() to return TaskError instead of String,
  with a new WalletKeyDerivationFailed variant for derivation errors
- Move build_identity_registration() and get_receive_address() from
  test helpers to production code in src/backend_task/identity/mod.rs
- Extract is_unique_constraint_violation() to src/database/mod.rs as a
  shared pub(crate) utility, removing the duplicate in import_mnemonic_screen
- Update all callers in add_new_wallet_screen and import_mnemonic_screen

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Move framework modules (harness, task_runner, wait, funding, cleanup,
  identity_helpers) into tests/backend-e2e/framework/ subdirectory
- Make E2E_WALLET_MNEMONIC required (panic with instructions if unset)
- Remove auto-faucet from initialization flow (keep as helper)
- Remove retry loops in identity_create and identity_withdraw tests
- Remove unnecessary wait_for_spendable_balance calls (already done
  by create_funded_test_wallet)
- Replace all println!/eprintln! with tracing macros
- Initialize tracing subscriber in harness init
- Add "No spendable funds" and "spendable" to send retry conditions
- Remove stale "other agent" NOTE comments from identity_helpers
- Consolidate funding logic (harness delegates to funding module)
- Update README for required mnemonic and new directory structure

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Adapt test framework to production API changes:
- Use IdentityKeys::new() constructor (fields now pub(crate))
- Match TaskError::WalletAlreadyImported variant instead of string
- Allow dead_code on faucet helpers (kept but not auto-called)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…dependency

Add `data_dir: PathBuf` field to `AppContext` and thread it through
`Config::load_from()`, `Config::save()`, `SpvManager::new()`,
`start_dash_qt()`, and `create_dash_core_config_if_not_exists()`.

This enables E2E tests to specify their data directory without mutating
process-wide environment variables, making parallel test execution safe.

The `DASH_EVO_DATA_DIR` env var is still checked in production via
`app_user_data_dir_path()`, but the resolved path is threaded through
as a value rather than re-read from env on every call.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds a headless backend E2E test harness (SPV + funded wallets + Platform flows) and refactors wallet creation so UI and tests can construct/register wallets through a shared, side-effect-controlled API.

Changes:

  • Introduce tests/backend-e2e/ harness + ignored network-dependent tests for SPV wallet ops, Core sends, identity flows, DPNS, and contract fetch.
  • Refactor wallet creation into Wallet::new_from_seed() (pure) + AppContext::register_wallet() (DB + in-memory + SPV activation).
  • Make filesystem/config/SPV paths depend on an explicit data_dir carried by AppContext (incl. DASH_EVO_DATA_DIR override), and adapt callers accordingly.

Reviewed changes

Copilot reviewed 39 out of 40 changed files in this pull request and generated 5 comments.

Show a summary per file
File Description
tests/backend-e2e/main.rs Defines the backend E2E test binary and modules.
tests/backend-e2e/README.md Documents how to run and extend the backend E2E suite.
tests/backend-e2e/spv_wallet.rs Verifies SPV startup + wallet registration + DB persistence.
tests/backend-e2e/send_funds.rs E2E Core send/receive flow between two wallets.
tests/backend-e2e/register_dpns.rs Identity registration + DPNS name registration and lookup.
tests/backend-e2e/identity_create.rs Identity creation smoke test.
tests/backend-e2e/identity_withdraw.rs Withdraw credits from Platform identity to Core address.
tests/backend-e2e/fetch_contract.rs Fetch contracts from Platform (existing + non-existent).
tests/backend-e2e/cleanup_only.rs Noop test to trigger orphan wallet sweep as a CI step.
tests/backend-e2e/framework/mod.rs Exposes harness helper modules.
tests/backend-e2e/framework/harness.rs Singleton context: temp DB/workdir, SPV startup, framework wallet restore/funding.
tests/backend-e2e/framework/task_runner.rs Test wrapper around run_backend_task.
tests/backend-e2e/framework/wait.rs Polling helpers (balance/spendable/SPV peer/wallet presence).
tests/backend-e2e/framework/funding.rs Framework wallet balance checks + optional faucet helper.
tests/backend-e2e/framework/cleanup.rs Best-effort sweep of orphaned test wallet funds.
tests/backend-e2e/framework/identity_helpers.rs Test helpers for identity key derivation + receive address.
src/model/wallet/mod.rs Adds Wallet::new_from_seed() and centralizes BIP44 constants/first address derivation.
src/context/wallet_lifecycle.rs Adds AppContext::register_wallet() as the single entrypoint for persistence + SPV loading.
src/database/wallet.rs Adds atomic store_wallet_with_addresses() transaction helper.
src/database/mod.rs Adds shared is_unique_constraint_violation() helper.
src/backend_task/error.rs Adds typed wallet creation/import error variants.
src/backend_task/identity/mod.rs Adds IdentityKeys::new(), canonical default_identity_key_specs(), and key-data encoding fix for HASH160.
src/ui/identities/add_new_identity_screen/mod.rs Switches to canonical default_identity_key_specs() source.
src/ui/wallets/add_new_wallet_screen.rs Simplifies wallet creation to use Wallet::new_from_seed() + register_wallet().
src/ui/wallets/import_mnemonic_screen.rs Same wallet creation refactor for mnemonic import flow.
src/spv/manager.rs Makes SPV state directory relative to provided app data dir.
src/spv/tests.rs Updates SPV tests for new SpvManager::new(&data_dir, ...) API.
src/config.rs Adds Config::load_from(data_dir) / save(data_dir); keeps load() for default dir.
src/app_dir.rs Adds DASH_EVO_DATA_DIR override + data_file_path() + directory/env helpers.
src/context/mod.rs Adds AppContext.data_dir and uses Config::load_from(&data_dir) + SpvManager::new(&data_dir, ...).
src/app.rs Threads data_dir through AppState creation and contexts; DB path now relative to data_dir.
src/backend_task/core/start_dash_qt.rs Writes logs/configs relative to AppContext.data_dir.
src/backend_task/core/mod.rs Uses Config::load_from(&self.data_dir); adapts fee API change.
src/ui/network_chooser_screen.rs Loads/saves config relative to mainnet context data_dir.
src/ui/tools/masternode_list_diff_screen.rs Removes obsolete NetworkExt import.
src/ui/tokens/tokens_screen/mod.rs Updates tests for new AppContext::new(data_dir, ...) signature.
.github/workflows/tests.yml Adds backend-e2e paths to triggers; backend E2E CI steps currently commented out.
CLAUDE.md Documents backend-e2e test location and purpose.
Cargo.toml Bumps dash-sdk rev; adds tokio-shared-rt dev dependency.
Cargo.lock Updates dependency graph for new dash-sdk rev + tokio-shared-rt.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread tests/backend-e2e/framework/task_runner.rs
Comment thread src/database/mod.rs Outdated
Comment thread tests/backend-e2e/framework/harness.rs
Comment thread src/spv/tests.rs Outdated
Comment thread .github/workflows/tests.yml
… check reliable

- Remove retry loop from create_funded_test_wallet; wait for full
  amount_duffs instead of 1 duff in spendable balance check
- Add Wallet::spv_confirmed_balance() that returns None when SPV
  hasn't synced yet (no max_balance fallback)
- Use spv_confirmed_balance() in wait_for_spendable_balance so
  the wait never gets false positives from the fallback
- Remove --test-threads=1 requirement from README (unsafe set_var
  was the only reason; data_dir is now threaded explicitly)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@lklimek lklimek removed blocked Blocked by something external to this issue labels Mar 18, 2026
@lklimek
Copy link
Copy Markdown
Contributor Author

lklimek commented Mar 18, 2026

Tests in this PR are commented / ignored. We need fully working SPV to enable them by default.

…SPV test dirs

- Remove send_with_retry() from send_funds.rs; use
  wait_for_spendable_balance before each send instead
- Add SQLITE_CONSTRAINT_PRIMARYKEY (1555) to uniqueness check
  alongside SQLITE_CONSTRAINT_UNIQUE (2067)
- Use tempfile::TempDir in SPV tests instead of fixed /tmp/spv-test
  path to prevent state leaks and support concurrent test runs

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@lklimek lklimek enabled auto-merge (squash) March 18, 2026 17:22
@lklimek lklimek disabled auto-merge March 18, 2026 17:45
@lklimek lklimek merged commit fffc649 into v1.0-dev Mar 18, 2026
5 checks passed
@lklimek lklimek deleted the test/backend-e2e-spv-wallet branch March 18, 2026 17:45
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants