Skip to content

feat: improve wallet balance checks from external clients#7207

Merged
SWvheerden merged 11 commits intotari-project:developmentfrom
hansieodendaal:ho_verify_balance
Jun 12, 2025
Merged

feat: improve wallet balance checks from external clients#7207
SWvheerden merged 11 commits intotari-project:developmentfrom
hansieodendaal:ho_verify_balance

Conversation

@hansieodendaal
Copy link
Copy Markdown
Contributor

@hansieodendaal hansieodendaal commented Jun 12, 2025

Description

  • Fixed wallet grpc wallet debouncer event service not starting with wallet recovery and view-only wallet creation.
  • Improved UTXO scanning when a wallet is recovered, as recovery and normal mode scanning are active simultaneously, which caused some of the issues.
  • Added a new cucumber test in the wallet FFI section to verify that a wallet with coinbases and transactional funds can be:
    • successfully recovered into another wallet;
    • into a view-only wallet;
    • into an FFI wallet
      and that the balances will correspond.
  • Fixed a number of cucumber FFI tests that were not working, although the CI reports green ticks for them. The latter CI issue will be fixed in a follow-up PR.

Motivation and Context

Various reports have been received regarding discrepancies in wallet balances between the console wallet, TU wallet (view-only wallet), and the mobile wallet (FFI wallet).

How Has This Been Tested?

New cucumber test: Scenario: As a client I want to be able to check my balance from restored wallets

cargo +nightly-2025-01-01 test --all-features --release --test cucumber -- -c 1 --name "Scenario: As a client I want to be able to check my balance from restored wallets"

What process can a PR reviewer use to test or verify this change?

Code review
Run the cucumber test

Breaking Changes

  • None
  • Requires data directory on base node to be deleted
  • Requires hard fork
  • Other - Please specify

Summary by CodeRabbit

  • New Features

    • Improved wallet event monitoring to prevent duplicate monitors and added support for new transaction event types.
    • Enhanced UTXO scanner service to better manage scanning based on base node connectivity and block updates.
    • Added new step in integration tests to validate all wallet transactions.
    • Improved logging for wallet recovery and scanning services for better traceability.
  • Bug Fixes

    • Prevented multiple concurrent event monitors from being started in the wallet.
  • Refactor

    • Unified wallet address retrieval in integration tests for consistency.
    • Reworked async handling and logging in UTXO scanner and wallet recovery services.
  • Chores

    • Updated integration test port allocation to avoid port conflicts.
    • Updated documentation for integration test toolchain recommendations.
  • Tests

    • Enhanced and reordered wallet integration test scenarios for improved reliability and clarity.

Hansie Odendaal and others added 10 commits June 5, 2025 09:08
Added cucumber test to verify wallet balance with recovery into FFI wallet.
- Fixed grpc balance and state methods not being updated due to
  event monitor not being started.
- Fixed simualtaneous execution of recovery and scanning tasks
  during recovery.
…lance

# Conflicts:
#	integration_tests/tests/features/WalletFFI.feature
#	integration_tests/tests/steps/wallet_steps.rs
@hansieodendaal hansieodendaal requested a review from a team as a code owner June 12, 2025 10:00
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Jun 12, 2025

Walkthrough

This update introduces several improvements and refactors across the wallet, base node, and integration testing components. Key changes include enhanced state management and event monitoring in wallet services, conditional compilation for base node-specific features, asynchronous refactoring for recovery and scanning logic, improved logging, and a more robust port assignment mechanism in integration tests. Test steps and scenarios are updated for consistency and reliability.

Changes

File(s) Change Summary
applications/minotari_console_wallet/src/grpc/wallet_debouncer.rs,
applications/minotari_console_wallet/src/grpc/wallet_grpc_server.rs,
applications/minotari_console_wallet/src/wallet_modes.rs
Event monitor logic refactored to prevent multiple spawns; new public methods for starting event monitoring; gRPC server updated to start event monitor before serving.
applications/minotari_console_wallet/src/recovery.rs Logging for UTXO scanner events improved for consistency and detail.
base_layer/core/src/common/waiting_requests.rs,
base_layer/core/src/transactions/transaction_components/transaction_output.rs
Methods related to waiting requests and range proof verification are now conditionally compiled with base_node feature.
base_layer/wallet/src/utxo_scanner_service/service.rs,
base_layer/wallet/src/utxo_scanner_service/utxo_scanner_task.rs
UTXO scanner service and task refactored for improved scanning control, asynchronous recovery checks, and enhanced logging.
base_layer/wallet/src/utxo_scanner_service/uxto_scanner_service_builder.rs Manual Debug implementation for UtxoScannerMode replaces derived version.
integration_tests/src/lib.rs,
integration_tests/src/world.rs,
integration_tests/src/base_node_process.rs,
integration_tests/src/merge_mining_proxy.rs,
integration_tests/src/wallet_process.rs
Port assignment logic refactored to avoid reuse by tracking assigned ports in TariWorld.
integration_tests/src/wallet_ffi.rs WalletFFI struct extended with log_path; spawn/restart signatures updated; port assignment now uses TariWorld.
integration_tests/tests/steps/wallet_ffi_steps.rs Test step for emoji ID validation improved; restart method updated to accept port; new step attribute for stopping wallet.
integration_tests/tests/steps/wallet_steps.rs Wallet address retrieval unified; new step for validating all wallets' transactions; improved naming and additional step expressions.
integration_tests/tests/features/WalletFFI.feature Test scenarios reordered and simplified for wallet recovery and transaction checks.
integration_tests/README.md Nightly toolchain version in linker issue note updated.

Sequence Diagram(s)

sequenceDiagram
    participant Client
    participant WalletGrpcServer
    participant WalletDebouncer

    Client->>WalletGrpcServer: start_balance_debouncer_event_monitor()
    WalletGrpcServer->>WalletDebouncer: start_event_monitor_if_needed()
    alt Event monitor not started
        WalletDebouncer->>WalletDebouncer: Spawn event monitor task
        WalletDebouncer->>WalletDebouncer: Set event_monitor_started = true
    else Event monitor already started
        WalletDebouncer-->>WalletGrpcServer: Do nothing
    end
    WalletGrpcServer-->>Client: Monitor started or already running
Loading
sequenceDiagram
    participant UtxoScannerService
    participant BaseNode
    participant Database

    loop Service Running
        UtxoScannerService->>BaseNode: Wait for connection
        alt Connected
            BaseNode-->>UtxoScannerService: StateChanged or NewBlockDetected
            UtxoScannerService->>Database: check_recovery_mode()
            alt Not in recovery
                UtxoScannerService->>UtxoScannerService: should_scan(new_block_hash)
                alt Should scan
                    UtxoScannerService->>UtxoScannerService: Update block_tip_to_scan_to
                    UtxoScannerService->>UtxoScannerService: Trigger scanning task
                end
            end
        else Disconnected
            UtxoScannerService->>UtxoScannerService: Suspend scanning
        end
    end
Loading

Poem

🐇
A hop, a skip, a monitor anew,
Ports now tracked, no more déjà vu!
Wallets scan with mindful care,
Logging tales for all to share.
Async checks and features gated,
Integration tests coordinated—
The warren’s code is celebrated!

✨ Finishing Touches
  • 📝 Generate Docstrings

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Explain this complex logic.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query. Examples:
    • @coderabbitai explain this code block.
    • @coderabbitai modularize this function.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read src/utils.ts and explain its main purpose.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.
    • @coderabbitai help me debug CodeRabbit configuration file.

Support

Need help? Create a ticket on our support page for assistance with any issues or questions.

Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

CodeRabbit Commands (Invoked using PR comments)

  • @coderabbitai pause to pause the reviews on a PR.
  • @coderabbitai resume to resume the paused reviews.
  • @coderabbitai review to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
  • @coderabbitai full review to do a full review from scratch and review all the files again.
  • @coderabbitai summary to regenerate the summary of the PR.
  • @coderabbitai generate docstrings to generate docstrings for this PR.
  • @coderabbitai generate sequence diagram to generate a sequence diagram of the changes in this PR.
  • @coderabbitai resolve resolve all the CodeRabbit review comments.
  • @coderabbitai configuration to show the current CodeRabbit configuration for the repository.
  • @coderabbitai help to get help.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Documentation and Community

  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

@github-actions
Copy link
Copy Markdown

Test Results (CI)

    3 files    126 suites   1h 11m 0s ⏱️
1 332 tests 1 331 ✅ 0 💤 1 ❌
3 994 runs  3 991 ✅ 0 💤 3 ❌

For more details on these failures, see this check.

Results for commit f3550cc.

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: 8

🔭 Outside diff range comments (3)
base_layer/core/src/common/waiting_requests.rs (1)

52-65: 💡 Verification agent

🧩 Analysis chain

insert / remove vanish when base_node is disabled – downstream code may break

Both public async helpers are now hidden behind #[cfg(feature = "base_node")].
Any crate ‑ for example the wallet-only binaries – that still depend on WaitingRequests::insert() or remove() will no longer compile when built without the feature flag.

A minimal fallback (even a #[cfg(not(feature = "base_node"))] panic!() stub) would keep the API surface consistent and avoid a feature-gate ripple effect.

+#cfg(not(feature = "base_node")]
+#[allow(dead_code)]
+impl<T> WaitingRequests<T> {
+    pub async fn insert(&self, _key: RequestKey, _reply_tx: OneshotSender<T>) {
+        unreachable!("WaitingRequests::insert is only available with the `base_node` feature enabled")
+    }
+
+    pub async fn remove(&self, _key: RequestKey) -> Option<(OneshotSender<T>, Instant)> {
+        unreachable!("WaitingRequests::remove is only available with the `base_node` feature enabled")
+    }
+}

Please double-check that all non-base_node targets are green after this change.
If none of them actually call these methods, consider making the whole type #[cfg(feature = "base_node")] instead.


🏁 Script executed:

#!/bin/bash
# Search for WaitingRequests usage in the codebase
rg -n 'WaitingRequests' .

Length of output: 1389


🏁 Script executed:

#!/bin/bash
# Check for calls to WaitingRequests::insert or ::remove outside the base_node feature
rg -n 'waiting_requests\.insert' .
rg -n 'waiting_requests\.remove' .

Length of output: 472


🏁 Script executed:

#!/bin/bash
# Inspect the top of service.rs to see if it's behind a cfg(feature = "base_node") guard
sed -n '1,50p' base_layer/core/src/base_node/service/service.rs

# Check where base_node module is declared in the core crate and if it's feature-gated
rg -n 'mod base_node' -n 'cfg.*base_node' base_layer/core/src/lib.rs

Length of output: 2563


Add non-base_node stubs for insert/remove to avoid breakage

The calls to waiting_requests.insert(…) and .remove(…) in
base_layer/core/src/base_node/service/service.rs (lines 529, 601, 676) are unconditionally compiled, but the methods themselves are only present when the base_node feature is enabled. This will cause compilation errors in any build with default-features = false.

You should provide fallback implementations when base_node is disabled, for example:

--- a/base_layer/core/src/common/waiting_requests.rs
+++ b/base_layer/core/src/common/waiting_requests.rs
@@
     #[cfg(feature = "base_node")]
     pub async fn insert(&self, key: RequestKey, reply_tx: OneshotSender<T>) {
         self.requests
             .write()
             .await
             .insert(key, Some((reply_tx, Instant::now())));
     }
+
+    #[cfg(not(feature = "base_node"))]
+    #[allow(dead_code)]
+    pub async fn insert(&self, _key: RequestKey, _reply_tx: OneshotSender<T>) {
+        unreachable!("`WaitingRequests::insert` requires the `base_node` feature");
+    }
@@
     #[cfg(feature = "base_node")]
     pub async fn remove(&self, key: RequestKey) -> Option<(OneshotSender<T>, Instant)> {
         self.requests.write().await.remove(&key).unwrap_or(None)
     }
+
+    #[cfg(not(feature = "base_node"))]
+    #[allow(dead_code)]
+    pub async fn remove(&self, _key: RequestKey) -> Option<(OneshotSender<T>, Instant)> {
+        unreachable!("`WaitingRequests::remove` requires the `base_node` feature");
+    }

Alternatively, if nothing outside base_node ever uses WaitingRequests, you could gate the entire type and its callers behind #[cfg(feature = "base_node")] instead.

base_layer/core/src/transactions/transaction_components/transaction_output.rs (2)

243-302: 🛠️ Refactor suggestion

⚠️ Potential issue

verify_range_proof() now hidden, but test and external callers still use it

The whole verification path is guarded by #[cfg(feature = "base_node")].
However:

  1. The integration tests in this same file (#[tokio::test]) call tx_output.verify_range_proof unconditionally.
  2. Other crates (wallet, validator, etc.) may rely on the method when compiled without the base_node feature.

This leads to compilation failures for cargo test --no-default-features or any binary that disables the flag.

Action items:
• Either gate the test module and all external uses with the same feature flag,
• or leave the function available for all builds and internally cfg only the heavy base-node specific branches.


564-607: ⚠️ Potential issue

batch_verify_range_proofs hidden behind feature flag but re-exported in super

super::batch_verify_range_proofs is imported in the test module without a matching cfg. Same compilation issue as above.

If the function truly is base-node-only, gate the test module:

-#[cfg(test)]
+#[cfg(all(test, feature = "base_node"))]
mod test { ... }

or expose a stub for non-base_node builds that simply returns Ok(()).

♻️ Duplicate comments (1)
integration_tests/src/base_node_process.rs (1)

116-118: Same unwrap() panic risk as in wallet spawning

Replicate the defensive‐coding improvement suggested for wallet_process.rs
to avoid random CI panics when no port is free.

🧹 Nitpick comments (16)
base_layer/core/src/transactions/transaction_components/transaction_output.rs (1)

46-57: Duplicate / inconsistent imports – minor cleanup

extended_range_proof::ExtendedRangeProofService is imported unconditionally, while extended_range_proof::Statement is gated behind base_node.
If ExtendedRangeProofService is also only used inside the newly-gated code, the unconditional import will become “unused” for non-base_node builds and trigger warnings.

Either gate both, or keep both unconditional.

- use tari_crypto::{
-     commitment::HomomorphicCommitmentFactory,
-     errors::RangeProofError,
-     extended_range_proof::ExtendedRangeProofService,
-     tari_utilities::hex::Hex,
- };
+#[cfg(feature = "base_node")]
+use tari_crypto::extended_range_proof::ExtendedRangeProofService;
+
+use tari_crypto::{
+    commitment::HomomorphicCommitmentFactory,
+    errors::RangeProofError,
+    tari_utilities::hex::Hex,
+};
integration_tests/src/world.rs (1)

113-154: assigned_ports should be a HashSet<u64> – keys & values are identical

IndexMap<u64, u64> stores the port number twice, wastes memory and
communicates no ordering requirements. A simple HashSet<u64> expresses the
intent (“membership”) and reduces copy-paste errors.

-    pub assigned_ports: IndexMap<u64, u64>,
+    pub assigned_ports: HashSet<u64>,

Adjust get_port accordingly:

-if !world.assigned_ports.contains_key(&u64::from(port)) {
-    break port;
-}
+if !world.assigned_ports.contains(&u64::from(port)) {
+    break port;
+}
...
-world.assigned_ports.insert(u64::from(port), u64::from(port));
+world.assigned_ports.insert(u64::from(port));
base_layer/wallet/src/utxo_scanner_service/uxto_scanner_service_builder.rs (2)

23-24: Redundant std::fmt::Debug import

The trait is only referenced fully-qualified (std::fmt::Formatter) in the custom impl and by derives, so this use is unused and will raise a warning with rustc -D warnings.
Please remove it.


57-64: Custom Debug impl duplicates information – could derive and wrap

If the only aim is to rename the variants for log readability, consider composing rather than fully re-implementing:

-impl Debug for UtxoScannerMode {
-    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
-        match self {
-            UtxoScannerMode::Recovery => write!(f, "UtxoRecoveryMode"),
-            UtxoScannerMode::Scanning => write!(f, "UtxoScanningMode"),
-        }
-    }
-}
+impl Debug for UtxoScannerMode {
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        let renamed = match self {
+            UtxoScannerMode::Recovery => "UtxoRecoveryMode",
+            UtxoScannerMode::Scanning => "UtxoScanningMode",
+        };
+        f.write_str(renamed)
+    }
+}

Slightly shorter and avoids repeating write! boilerplate.

applications/minotari_console_wallet/src/wallet_modes.rs (1)

28-34: Several newly added imports are unused

TryFutureExt, Wallet, GetBalanceRequest, and Request are not referenced anywhere in this file after the change, which will trigger compiler warnings. Remove or gate them behind #[cfg(feature = "grpc")] if they are conditionally used elsewhere.

Also applies to: 47-48

integration_tests/src/lib.rs (1)

43-55: Potential infinite loop if every port is occupied

get_port loops forever if no free port exists in the given range. In CI environments with port exhaustion this can hang tests indefinitely. Consider capping attempts and returning None after N tries, letting the caller decide how to proceed.

integration_tests/tests/steps/wallet_ffi_steps.rs (1)

88-90: Log message is misleading
The line writes “Adding wallet” while the function is actually stopping / destroying it.

-    cucumber_steps_log(format!("Adding wallet {}", wallet));
+    cucumber_steps_log(format!("Stopping wallet {}", wallet));
integration_tests/src/wallet_ffi.rs (1)

64-78: Clean separation of log & data paths – LGTM

Introducing log_path and keeping it in the struct simplifies restarts and avoids duplicate path construction. ⚠️ Minor: the range 18000..18499 is upper-exclusive, so 18499 is never used—document this or switch to ..=.

applications/minotari_console_wallet/src/grpc/wallet_debouncer.rs (1)

87-97: Idempotent monitor start – nice, but could still race

start_event_monitor_if_needed works because callers hold &mut self, yet setting event_monitor_started after spawning leaves a tiny window for double-spawning if a future refactor allows concurrent callers. Atomically swapping the flag before tokio::spawn (e.g., if let Ok(mut flag) = self.event_monitor_started.try_lock() { … }) would close the gap.

integration_tests/tests/steps/wallet_steps.rs (2)

851-858: Typo in function name (dest_wallt) will bite compile-time search tools

send_one_sided_transaction_from_source_wallet_to_dest_wallt contains a misspelling (wallt). While the compiler doesn’t care, grep-based tooling and readers do.

-async fn send_one_sided_transaction_from_source_wallet_to_dest_wallt(
+async fn send_one_sided_transaction_from_source_wallet_to_dest_wallet(

Refactor before the typo spreads to step definitions and docs.


308-310: Repeated unwrap() on get_wallet_address obscures context on failure

Every call to world.get_wallet_address(..).await.unwrap() will panic with a cryptic “called Option::unwrap() on a None value” which doesn’t indicate which wallet was missing.

Replace with expect (or a small helper) to surface the wallet name:

-let wallet_address = world.get_wallet_address(&wallet_name).await.unwrap();
+let wallet_address = world
+    .get_wallet_address(&wallet_name)
+    .await
+    .expect(&format!("Address for wallet '{}' not found", wallet_name));

Same pattern occurs in the highlighted lines – worth a quick rg 'unwrap()' sweep.

Also applies to: 383-385, 430-432, 470-472, 860-864, 2316-2319, 2850-2853, 3011-3014

base_layer/wallet/src/utxo_scanner_service/utxo_scanner_task.rs (3)

173-177: Logging placeholders look suspicious

debug! uses three {}-style placeholders but only two unique values (final_height twice).
This will compile, yet the message is confusing – it prints the same number in both “Progress(..)” and “Completed(..)”.

- "{:?}: Published events 'UtxoScannerEvent::Progress(..{})' and 'UtxoScannerEvent::Completed(..{})'"
+ "{:?}: Published events 'UtxoScannerEvent::Progress(..{:#})' and 'UtxoScannerEvent::Completed(..{:#})'"

Consider either removing the duplicated placeholder or inserting the correct values.


291-299: Add parentheses for clarity around mixed || / && expression

The precedence rules make the current logic correct, but the intent is easy to mis-read:

if self.shutdown_signal.is_triggered()
    || (self.check_recovery_mode().await? && self.mode != UtxoScannerMode::Recovery)

Explicit grouping prevents accidental regressions when this line is edited later.


703-721: Async wrappers without awaits introduce needless overhead

set_recovery_mode, check_recovery_mode, and clear_recovery_mode are now async, yet they perform only synchronous DB calls and contain no .await.
This:

  1. Allocates a Future each call for no benefit.
  2. Hides the fact that the operations are instantaneous, encouraging careless .await-spamming in call-sites.

Unless these functions will become truly asynchronous, revert them to sync variants and drop the .await at call-sites.

base_layer/wallet/src/utxo_scanner_service/service.rs (2)

285-307: should_scan logic is hard to reason about

The nested Option comparisons coupled with manual flags make the intent opaque and error-prone.

A clearer expression:

matches!(
    (self.last_block_tip_scanned, self.block_tip_to_scan_to),
    (Some(last), Some(target)) if last != new_hash && target != new_hash
) || self.last_block_tip_scanned.is_none() && self.block_tip_to_scan_to != Some(new_hash)

At minimum, add unit tests documenting the expected truth table.


285-292: Duplicate recovery-mode DB probe

Both this service and UtxoScannerTask implement their own check_recovery_mode, duplicating code and increasing drift risk.
Extract a single helper (e.g. resources.is_recovery_active()) and call it from both sites.

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 66bec7f and f3550cc.

📒 Files selected for processing (19)
  • applications/minotari_console_wallet/src/grpc/wallet_debouncer.rs (5 hunks)
  • applications/minotari_console_wallet/src/grpc/wallet_grpc_server.rs (1 hunks)
  • applications/minotari_console_wallet/src/recovery.rs (2 hunks)
  • applications/minotari_console_wallet/src/wallet_modes.rs (3 hunks)
  • base_layer/core/src/common/waiting_requests.rs (2 hunks)
  • base_layer/core/src/transactions/transaction_components/transaction_output.rs (4 hunks)
  • base_layer/wallet/src/utxo_scanner_service/service.rs (8 hunks)
  • base_layer/wallet/src/utxo_scanner_service/utxo_scanner_task.rs (21 hunks)
  • base_layer/wallet/src/utxo_scanner_service/uxto_scanner_service_builder.rs (2 hunks)
  • integration_tests/README.md (1 hunks)
  • integration_tests/src/base_node_process.rs (1 hunks)
  • integration_tests/src/lib.rs (1 hunks)
  • integration_tests/src/merge_mining_proxy.rs (1 hunks)
  • integration_tests/src/wallet_ffi.rs (3 hunks)
  • integration_tests/src/wallet_process.rs (1 hunks)
  • integration_tests/src/world.rs (2 hunks)
  • integration_tests/tests/features/WalletFFI.feature (2 hunks)
  • integration_tests/tests/steps/wallet_ffi_steps.rs (2 hunks)
  • integration_tests/tests/steps/wallet_steps.rs (17 hunks)
🧰 Additional context used
🧬 Code Graph Analysis (4)
integration_tests/src/base_node_process.rs (1)
integration_tests/src/lib.rs (1)
  • get_port (39-56)
integration_tests/src/merge_mining_proxy.rs (1)
integration_tests/src/lib.rs (1)
  • get_port (39-56)
integration_tests/tests/steps/wallet_ffi_steps.rs (2)
base_layer/common_types/src/tari_address/mod.rs (1)
  • from_emoji_string (273-277)
integration_tests/src/ffi/wallet_address.rs (1)
  • emoji_id (85-96)
integration_tests/tests/steps/wallet_steps.rs (3)
integration_tests/src/merge_mining_proxy.rs (1)
  • create_wallet_client (220-227)
integration_tests/tests/steps/mod.rs (1)
  • cucumber_steps_log (64-69)
base_layer/core/src/transactions/transaction_components/encrypted_data.rs (1)
  • open_from_string (517-522)
🔇 Additional comments (7)
integration_tests/README.md (1)

7-7: LGTM – doc tweak only

Nightly toolchain date updated; no further action required.

integration_tests/src/merge_mining_proxy.rs (1)

55-61: unwrap() will panic on the extremely rare None path – prefer graceful error handling

get_port returns an Option<u64>. Although the current implementation loops until it succeeds, defensive code in integration infrastructure should avoid panicking outright:

-        port: get_port(world, 18000..18499).unwrap(),
+        port: get_port(world, 18000..18499)
+            .expect("Failed to acquire an available port in range 18000-18499"),

Using expect with a descriptive message keeps the intent clear and prevents an opaque panic.

applications/minotari_console_wallet/src/recovery.rs (2)

140-150: Good logging enhancement

Replacing bare println! calls with formatted msg logged at debug! level improves observability without altering functionality.


210-213: 👍 Added debug on stream close

The additional log on RecvError::Closed will help diagnose premature channel shutdowns during recovery.

integration_tests/tests/features/WalletFFI.feature (1)

46-56: Solid step ordering to avoid double-spend race conditions

Explicitly stopping OTHER_WALLET (l.47) and MY_WALLET (l.51) before beginning seed-word-based restores makes the scenario deterministic and prevents two live instances from touching the same data directory. Nice catch.

integration_tests/tests/steps/wallet_ffi_steps.rs (1)

78-81: Semantic check beats length check

Replacing the fixed-length assertion with TariAddress::from_emoji_string(&emoji_id) guarantees the string is a valid address rather than merely 132 chars long. Good improvement.

applications/minotari_console_wallet/src/grpc/wallet_debouncer.rs (1)

183-186: Expanded event coverage

Adding TransactionImported and TransactionMinedUnconfirmed to the refresh trigger list ensures balance accuracy for previously overlooked paths—good catch.

Comment on lines +187 to +189
pub async fn start_balance_debouncer_event_monitor(&self) {
self.debouncer.lock().await.start_event_monitor_if_needed().await
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

⚠️ Potential issue

Avoid holding the tokio::sync::MutexGuard across an .await – risk of starvation & dead-lock

lock().await returns a MutexGuard that is not Send.
By immediately calling an async method (.await) on the guarded value you keep the
lock held for the entire duration of that future, blocking every other task that
needs the same mutex and potentially dead-locking if the future itself needs to
re-acquire the lock internally.

Refactor so the lock is released before the await point and propagate errors:

-pub async fn start_balance_debouncer_event_monitor(&self) {
-    self.debouncer.lock().await.start_event_monitor_if_needed().await
-}
+pub async fn start_balance_debouncer_event_monitor(&self) -> Result<(), WalletDebouncerError> {
+    // 1) grab the guard
+    let mut debouncer = self.debouncer.lock().await;
+    // 2) create the future **without** awaiting
+    let fut = debouncer.start_event_monitor_if_needed();
+    drop(debouncer);            // 3) release the lock
+    fut.await                    // 4) await outside the critical section
+}

This keeps the critical section minimal and surfaces any failure to the caller.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
pub async fn start_balance_debouncer_event_monitor(&self) {
self.debouncer.lock().await.start_event_monitor_if_needed().await
}
pub async fn start_balance_debouncer_event_monitor(&self) -> Result<(), WalletDebouncerError> {
// 1) grab the guard
let mut debouncer = self.debouncer.lock().await;
// 2) create the future **without** awaiting
let fut = debouncer.start_event_monitor_if_needed();
drop(debouncer); // 3) release the lock
fut.await // 4) await outside the critical section
}
🤖 Prompt for AI Agents
In applications/minotari_console_wallet/src/grpc/wallet_grpc_server.rs around
lines 187 to 189, the code holds the tokio::sync::MutexGuard across an await,
which can cause deadlocks and starvation. To fix this, acquire the lock and
extract the necessary data or state without awaiting, then release the lock
before calling any async functions that require awaiting. Also, modify the
function to propagate any errors from start_event_monitor_if_needed to the
caller instead of ignoring them.

Comment on lines +88 to 90
port = get_port(world, 18000..18499).unwrap();
grpc_port = get_port(world, 18500..18999).unwrap();

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

unwrap() on get_port will panic when the pool is exhausted

get_port returns an Option<u64>, but the call site force-unwraps.
When many wallets / nodes are spawned in parallel the port range can be
exhausted, causing a hard panic and a red CI run.

Consider propagating the error or using expect(...) with a helpful message so
the failure is explicit:

-        port = get_port(world, 18000..18499).unwrap();
-        grpc_port = get_port(world, 18500..18999).unwrap();
+        port = get_port(world, 18_000..18_499)
+            .expect("No free P2P port available in 18000-18499");
+        grpc_port = get_port(world, 18_500..18_999)
+            .expect("No free gRPC port available in 18500-18999");
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
port = get_port(world, 18000..18499).unwrap();
grpc_port = get_port(world, 18500..18999).unwrap();
port = get_port(world, 18_000..18_499)
.expect("No free P2P port available in 18000-18499");
grpc_port = get_port(world, 18_500..18_999)
.expect("No free gRPC port available in 18500-18999");
🤖 Prompt for AI Agents
In integration_tests/src/wallet_process.rs around lines 88 to 90, the calls to
get_port use unwrap(), which will cause a panic if no ports are available in the
specified range. Replace unwrap() with expect() and provide a clear error
message indicating that the port pool is exhausted, so failures are explicit and
easier to diagnose during parallel test runs.

Comment on lines +486 to +487
handle.block_on(async { grpc.start_balance_debouncer_event_monitor().await });

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

Simplify .block_on call and propagate potential errors

start_balance_debouncer_event_monitor() already returns a future; wrapping it in an extra async block is unnecessary, and any error it returns is silently ignored:

-            handle.block_on(async { grpc.start_balance_debouncer_event_monitor().await });
+            handle
+                .block_on(grpc.start_balance_debouncer_event_monitor())
+                .map_err(|e| ExitError::new(ExitCode::GrpcError, e.to_string()))?;

This keeps the code concise and ensures the wallet exits cleanly if the monitor fails to start.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
handle.block_on(async { grpc.start_balance_debouncer_event_monitor().await });
handle
.block_on(grpc.start_balance_debouncer_event_monitor())
.map_err(|e| ExitError::new(ExitCode::GrpcError, e.to_string()))?;
🤖 Prompt for AI Agents
In applications/minotari_console_wallet/src/wallet_modes.rs at lines 486 to 487,
remove the unnecessary async block wrapping the call to
grpc.start_balance_debouncer_event_monitor() inside handle.block_on. Instead,
directly pass the future returned by start_balance_debouncer_event_monitor() to
block_on and handle the Result it returns by propagating any errors to ensure
failures are not ignored and the wallet can exit cleanly if the monitor fails to
start.

Comment on lines +39 to 55
pub fn get_port(world: &mut TariWorld, range: Range<u16>) -> Option<u64> {
let min = range.clone().min().expect("A minimum possible port number");
let max = range.max().expect("A maximum possible port number");

loop {
let port = rand::thread_rng().gen_range(min..max);
let port = loop {
let port = rand::thread_rng().gen_range(min..max);
if !world.assigned_ports.contains_key(&u64::from(port)) {
break port;
}
};

if TcpListener::bind(("127.0.0.1", port)).is_ok() {
world.assigned_ports.insert(u64::from(port), u64::from(port));
return Some(u64::from(port));
}
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

min() / max() iterate the entire range each call – use range.start/range.end instead

Range<u16> already exposes its bounds; invoking iterator adapters is unnecessary and marginally wasteful:

-    let min = range.clone().min().expect("A minimum possible port number");
-    let max = range.max().expect("A maximum possible port number");
+    let min = range.start;
+    let max = range.end;

Clearer, faster, and avoids the .expect() noise.

🤖 Prompt for AI Agents
In integration_tests/src/lib.rs around lines 39 to 55, the code uses range.min()
and range.max() which unnecessarily iterate over the entire range and require
expect() calls. Replace these with direct access to range.start and range.end to
get the minimum and maximum bounds of the range. This change will simplify the
code, improve performance, and remove the need for expect() calls.

Comment on lines +617 to +620
{
let ffi_wallet = world.get_mut_ffi_wallet(&wallet).unwrap();
ffi_wallet.restart(ffi_wallet.port);
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Re-binding to the same TCP port can fail on some OSes

restart(ffi_wallet.port) re-uses the previous port immediately after destroy(). If the old listener is still in TIME_WAIT, the bind may fail sporadically.

Consider picking a fresh port:

-        let ffi_wallet = world.get_mut_ffi_wallet(&wallet).unwrap();
-        ffi_wallet.restart(ffi_wallet.port);
+        let ffi_wallet = world.get_mut_ffi_wallet(&wallet).unwrap();
+        let new_port = get_port(world, 18000..18499).expect("No free port");
+        ffi_wallet.restart(new_port);
🤖 Prompt for AI Agents
In integration_tests/tests/steps/wallet_ffi_steps.rs around lines 617 to 620,
the code calls restart with the same TCP port which can fail if the old socket
is in TIME_WAIT state. To fix this, modify the restart call to use a fresh,
dynamically allocated port instead of reusing ffi_wallet.port, ensuring the new
port is free and avoiding bind errors.

Comment on lines +180 to 190
pub fn restart(&mut self, port: u64) {
self.wallet.lock().unwrap().destroy();
let port = get_port(18000..18499).unwrap();
let transport_config =
ffi::TransportConfig::create_tcp(CString::new(format!("/ip4/127.0.0.1/tcp/{}", port)).unwrap().into_raw());
let now: DateTime<Utc> = SystemTime::now().into();
let base_dir = format!("./log/ffi_wallets/{}", now.format("%Y%m%d-%H%M%S"));
let comms_config = ffi::CommsConfig::create(port, transport_config, base_dir.clone());
let log_path = format!("{}/log/ffi_wallet.log", base_dir);
self.wallet = ffi::Wallet::create(comms_config, log_path, null());
let comms_config = ffi::CommsConfig::create(
port,
transport_config,
self.base_dir.as_os_str().to_str().unwrap().into(),
);
self.wallet = ffi::Wallet::create(comms_config, self.log_path.clone(), null());
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Port reuse risk mirrors the test-step concern

restart(&mut self, port) keeps the same port by design. For robustness, grab a fresh unused port (see comment in step file) or at least handle bind errors and retry.

🤖 Prompt for AI Agents
In integration_tests/src/wallet_ffi.rs around lines 180 to 190, the restart
method reuses the same port which risks bind errors if the port is still in use.
Modify the method to either acquire a fresh unused port dynamically before
creating the transport config or add error handling around the wallet creation
to catch bind failures and retry with a different port. This ensures robustness
by avoiding port conflicts during restart.

Comment on lines +724 to +738
#[then(expr = "all wallets validate their transactions")]
#[when(expr = "all wallets validate their transactions")]
async fn all_wallets_validate_their_transactions(world: &mut TariWorld) {
let wallets = world.wallets.keys().cloned().collect::<Vec<_>>();
for wallet in &wallets {
let mut client = create_wallet_client(world, wallet.clone()).await.unwrap();
let result = client.validate_all_transactions(ValidateRequest {}).await;
if let Err(e) = result {
cucumber_steps_log(format!(
"Error! Wallet {} failed to validate transactions, error: {:?}",
wallet, e
));
}
}
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

Step handler never fails when validation fails – errors are silently ignored

validate_all_transactions is the whole point of this step. By only logging the Err and continuing, the test suite will happily pass even when every single wallet fails validation.

-        let result = client.validate_all_transactions(ValidateRequest {}).await;
-        if let Err(e) = result {
-            cucumber_steps_log(format!(
-                "Error! Wallet {} failed to validate transactions, error: {:?}",
-                wallet, e
-            ));
-        }
+        client
+            .validate_all_transactions(ValidateRequest {})
+            .await
+            .unwrap_or_else(|e| panic!("Wallet {} failed to validate transactions: {:?}", wallet, e));

Fail fast so that CI actually reports regressions.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
#[then(expr = "all wallets validate their transactions")]
#[when(expr = "all wallets validate their transactions")]
async fn all_wallets_validate_their_transactions(world: &mut TariWorld) {
let wallets = world.wallets.keys().cloned().collect::<Vec<_>>();
for wallet in &wallets {
let mut client = create_wallet_client(world, wallet.clone()).await.unwrap();
let result = client.validate_all_transactions(ValidateRequest {}).await;
if let Err(e) = result {
cucumber_steps_log(format!(
"Error! Wallet {} failed to validate transactions, error: {:?}",
wallet, e
));
}
}
}
#[then(expr = "all wallets validate their transactions")]
#[when(expr = "all wallets validate their transactions")]
async fn all_wallets_validate_their_transactions(world: &mut TariWorld) {
let wallets = world.wallets.keys().cloned().collect::<Vec<_>>();
for wallet in &wallets {
let mut client = create_wallet_client(world, wallet.clone()).await.unwrap();
client
.validate_all_transactions(ValidateRequest {})
.await
.unwrap_or_else(|e| panic!("Wallet {} failed to validate transactions: {:?}", wallet, e));
}
}
🤖 Prompt for AI Agents
In integration_tests/tests/steps/wallet_steps.rs around lines 724 to 738, the
step handler currently logs errors from validate_all_transactions but does not
fail the test on error, causing silent test passes. Modify the code to
immediately return or panic on the first validation error to fail fast and
ensure the test suite reports regressions properly.

Comment on lines 181 to 200
let task = self.create_task(local_shutdown.to_signal());
let mode = self.mode.clone();
let mut task_join_handle = task::spawn(async move {
trace!(target: LOG_TARGET, "{:?}: Spawning new UTXO scanning task", mode);
if let Err(err) = task.run().await {
error!(target: LOG_TARGET, "Error scanning UTXOs: {}", err);
error!(target: LOG_TARGET, "{:?}: Error scanning UTXOs: {}", mode, err);
}
})
.fuse();

// These events will change the scanning behaviour:
// - Base node state changes will trigger a new scanning round if the block tip has changed and not yet
// scanned.
// - A successfully completed scanning round will update the last scanned block tip.
// - A newly established base node connection will reset the last and current scanned block tip states to
// force a new scanning round when the next base node state change is received.
// - One-sided payment message changes will update the message used in one-sided payments.
// - Recovery message changes will update the message used in recovery.
// - Shutdown signal will stop the task if it is running, and let that thread exit gracefully.
loop {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Potential overlap of multiple scanning tasks

local_shutdown.trigger() is fired to stop the running task, then the loop breaks and immediately starts a new task on the next iteration.
Because the previous JoinHandle is discarded without an await or abort, a slow-to-shutdown task can keep running in the background, leading to:

  • two scanners touching the DB concurrently,
  • duplicated progress events,
  • unnecessary load.

Before spawning the next task, wait for the previous one to finish (e.g. store the handle outside the inner loop and await it after triggering shutdown) or call handle.abort() to guarantee termination.

🤖 Prompt for AI Agents
In base_layer/wallet/src/utxo_scanner_service/service.rs around lines 181 to
200, the code spawns a new scanning task each loop iteration without properly
awaiting or aborting the previous task's JoinHandle, causing overlapping tasks
and potential concurrency issues. To fix this, store the JoinHandle outside the
loop and before starting a new task, trigger shutdown on the current task and
either await its completion or call abort() on its handle to ensure the previous
task has fully stopped before spawning the next one.

@SWvheerden SWvheerden merged commit 58c3e41 into tari-project:development Jun 12, 2025
10 of 15 checks passed
@hansieodendaal hansieodendaal deleted the ho_verify_balance branch June 13, 2025 13:18
sdbondi added a commit to sdbondi/tari that referenced this pull request Jun 18, 2025
* development: (607 commits)
  Wallet GRPC port comment fix from 18142 to 18143 (tari-project#7221)
  feat: integrated address support for Ledger (tari-project#7198)
  chore: new release v4.1.1-pre.0 (tari-project#7211)
  fix: migration can now correctly resume after stopping (tari-project#7210)
  fix: only revalidated rejected transactions on startup (tari-project#7209)
  fix: add filtering flag back (tari-project#7208)
  feat: improve wallet balance checks from external clients (tari-project#7207)
  feat!: update grpc supply query (tari-project#7137)
  docs: Updated API GRPC and Exchange Guide (tari-project#7205)
  chore: new release v4.4.0-pre.0 (tari-project#7202)
  feat: update base node proto to search bytes (tari-project#7201)
  feat: full PayRef implementation (tari-project#7154)
  test: add ffi cucumber wallet balance test (tari-project#7189)
  chore: fix tests (tari-project#7196)
  fix(network-discovery): add back idle event handling (tari-project#7194)
  Update SECURITY.md (tari-project#7193)
  fix: transaction manager service unmined lookup (tari-project#7192)
  fix: wallet ffi database name mismatch for mobile wallet (tari-project#7191)
  fix: payment_id deserialize (tari-project#7187)
  fix: remove code for deleting stale peers (tari-project#7184)
  ...
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.

2 participants