Skip to content

feat!: change wallet to use http calls for utxo syncing #7151

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed

Conversation

stringhandler
Copy link
Collaborator

@stringhandler stringhandler commented Jun 2, 2025

Co-Authored-By: @ksrichard

Summary by CodeRabbit

  • New Features

    • Added HTTP endpoints for querying UTXOs by block and transaction queries.
    • Integrated OpenAPI documentation and Swagger UI for API exploration.
    • Enabled paginated UTXO synchronization via HTTP streaming.
    • Introduced HTTP client URL configuration in wallet and FFI interfaces.
  • Improvements

    • Standardized JSON error responses across HTTP endpoints.
    • Refined wallet UTXO scanning to use a single HTTP client and asynchronous key recovery.
    • Enhanced HTTP client and UTXO scanning logging.
    • Improved API models with detailed OpenAPI schemas and validation.
  • Refactor

    • Simplified wallet recovery and event reporting by removing peer-based connectivity.
    • Updated transaction key recovery methods to accept commitment and encrypted data separately.
    • Removed deprecated error types and streamlined error handling in client and wallet code.
  • Chores

    • Updated dependencies including utoipa, anyhow, and url.
    • Added cargo aliases for CI and expanded safe-to-deploy crate exemptions.
    • Enhanced logging configuration with new appenders and loggers for HTTP and UTXO scanning.

BREAKING CHANGE: FFI interface has changed. New config setting needed.

ksrichard added 30 commits May 13, 2025 14:59
…-as-http' into feature/base-node-wallet-service-as-http
…-as-http' into feature/base-node-wallet-service-as-http
Copy link
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: 5

🔭 Outside diff range comments (1)
base_layer/wallet/src/utxo_scanner_service/utxo_scanner_task.rs (1)

197-578: 🛠️ Refactor suggestion

Refactor the overly complex attempt_sync method.

The method is marked with #[allow(clippy::too_many_lines)] indicating it's too complex. Consider breaking it down into smaller, more focused methods for better maintainability.

Consider extracting the following into separate methods:

  • Header fetching logic
  • Block scanning logic
  • Progress reporting logic
  • Error handling and retry logic
♻️ Duplicate comments (2)
base_layer/core/src/base_node/rpc/models/get_utxos_by_block.rs (1)

29-132: Consider deriving the schema automatically to improve maintainability.

The manual schema definition is error-prone and will require updates whenever TransactionOutput changes. This could lead to documentation drift.

Consider implementing ToSchema derive macro on TransactionOutput directly, or using a schema generation utility that can introspect the struct. This would ensure the OpenAPI documentation stays in sync with the actual implementation.

If manual schema is necessary due to serialization customizations, consider adding tests to verify the schema matches the actual struct serialization.

clients/rust/base_node_wallet_client/src/client/http.rs (1)

78-136: ⚠️ Potential issue

Address previously flagged error handling and configurability issues.

The implementation still has the error handling issues mentioned in previous reviews:

  1. The page size is hardcoded (now to 10 instead of 5)
  2. Error handling continues the loop indefinitely on failure
  3. No proper cancellation mechanism beyond the shutdown signal

Consider implementing the improvements suggested in the previous review to:

  • Make the page size configurable
  • Break the loop on errors instead of continuing indefinitely
  • Add proper error handling with retry limits
🧹 Nitpick comments (6)
base_layer/core/src/base_node/rpc/models/sync_utxos_by_block.rs (1)

38-38: Fix missing space in struct name.

Apply this diff to fix the formatting:

-pub struct MinimalUtxoInfo{
+pub struct MinimalUtxoInfo {
base_layer/wallet/src/utxo_scanner_service/uxto_scanner_service_builder.rs (2)

23-23: Remove unused import.

The std::any import is not used anywhere in this file.

-use std::any;
-

162-162: Clean up extra whitespace and formatting.

There are unnecessary blank lines and inconsistent formatting.

-          let http_client_url = match &self.node_url {
+        let http_client_url = match &self.node_url {
             Some(url) => url.clone(),
             None => return Err(anyhow::anyhow!("Node URL must be set before building the UTXO scanner service.")),
         };
         let resources = UtxoScannerResources {
             // ... other fields ...
             http_client_url
         };
-        
 
-       Ok( UtxoScannerService::new(
+        Ok(UtxoScannerService::new(

Also applies to: 180-182

base_layer/core/src/base_node/rpc/query_service.rs (2)

148-148: Remove unnecessary blank lines.

Multiple extra blank lines that should be removed for consistency.

Also applies to: 160-160, 169-170


235-235: Document or make configurable the chunk size for outputs.

The code chunks outputs into groups of 2000 without explanation. This magic number should be documented or made configurable.

+        // Chunk outputs to avoid excessive memory usage and message size
+        const OUTPUT_CHUNK_SIZE: usize = 2000;
-        for output_chunk in outputs.chunks(2000) {
+        for output_chunk in outputs.chunks(OUTPUT_CHUNK_SIZE) {
base_layer/wallet/src/utxo_scanner_service/utxo_scanner_task.rs (1)

474-476: Fix awkward line breaking in while loop condition.

The while loop condition has poor formatting that reduces readability.

-        while let Some(response) = 
-            utxo_stream.recv().await
-         {
+        while let Some(response) = utxo_stream.recv().await {
📜 Review details

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

📥 Commits

Reviewing files that changed from the base of the PR and between cf7b4e5 and 39a084a.

⛔ Files ignored due to path filters (1)
  • Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (27)
  • applications/minotari_console_wallet/log4rs_sample.yml (6 hunks)
  • applications/minotari_console_wallet/src/recovery.rs (3 hunks)
  • applications/minotari_node/src/http/handler/get_utxos_by_block.rs (1 hunks)
  • applications/minotari_node/src/http/handler/mod.rs (1 hunks)
  • applications/minotari_node/src/http/server.rs (3 hunks)
  • base_layer/core/src/base_node/rpc/mod.rs (2 hunks)
  • base_layer/core/src/base_node/rpc/models/get_utxos_by_block.rs (1 hunks)
  • base_layer/core/src/base_node/rpc/models/mod.rs (1 hunks)
  • base_layer/core/src/base_node/rpc/models/sync_utxos_by_block.rs (1 hunks)
  • base_layer/core/src/base_node/rpc/query_service.rs (4 hunks)
  • base_layer/core/src/transactions/transaction_components/encrypted_data.rs (2 hunks)
  • base_layer/core/src/transactions/transaction_key_manager/inner.rs (2 hunks)
  • base_layer/core/src/transactions/transaction_key_manager/interface.rs (1 hunks)
  • base_layer/core/src/transactions/transaction_key_manager/wrapper.rs (1 hunks)
  • base_layer/wallet/src/lib.rs (1 hunks)
  • base_layer/wallet/src/output_manager_service/recovery/mod.rs (1 hunks)
  • base_layer/wallet/src/output_manager_service/recovery/output_recovery_info.rs (1 hunks)
  • base_layer/wallet/src/output_manager_service/recovery/standard_outputs_recoverer.rs (1 hunks)
  • base_layer/wallet/src/utxo_scanner_service/initializer.rs (6 hunks)
  • base_layer/wallet/src/utxo_scanner_service/service.rs (6 hunks)
  • base_layer/wallet/src/utxo_scanner_service/utxo_scanner_task.rs (16 hunks)
  • base_layer/wallet/src/utxo_scanner_service/uxto_scanner_service_builder.rs (9 hunks)
  • base_layer/wallet_ffi/src/lib.rs (5 hunks)
  • base_layer/wallet_ffi/wallet.h (1 hunks)
  • clients/rust/base_node_wallet_client/Cargo.toml (1 hunks)
  • clients/rust/base_node_wallet_client/src/client/http.rs (2 hunks)
  • clients/rust/base_node_wallet_client/src/client/mod.rs (2 hunks)
✅ Files skipped from review due to trivial changes (6)
  • base_layer/wallet/src/output_manager_service/recovery/mod.rs
  • base_layer/wallet/src/output_manager_service/recovery/output_recovery_info.rs
  • base_layer/wallet/src/lib.rs
  • base_layer/core/src/transactions/transaction_key_manager/inner.rs
  • applications/minotari_console_wallet/log4rs_sample.yml
  • base_layer/wallet_ffi/wallet.h
🚧 Files skipped from review as they are similar to previous changes (9)
  • clients/rust/base_node_wallet_client/Cargo.toml
  • base_layer/core/src/base_node/rpc/models/mod.rs
  • applications/minotari_console_wallet/src/recovery.rs
  • base_layer/wallet/src/utxo_scanner_service/initializer.rs
  • applications/minotari_node/src/http/server.rs
  • base_layer/wallet/src/utxo_scanner_service/service.rs
  • applications/minotari_node/src/http/handler/mod.rs
  • base_layer/wallet_ffi/src/lib.rs
  • base_layer/core/src/base_node/rpc/mod.rs
🧰 Additional context used
🧠 Learnings (1)
base_layer/core/src/base_node/rpc/query_service.rs (1)
Learnt from: ksrichard
PR: tari-project/tari#7129
File: applications/minotari_node/src/http/handler/sync_utxos_by_block.rs:21-29
Timestamp: 2025-05-29T20:59:32.020Z
Learning: In the Tari codebase, request validation for RPC services is handled at the service layer using the `serde_valid` crate rather than at the HTTP handler level. For example, `SyncUtxosByBlockRequest` validates that limit is between 1-5 and page is minimum 0. HTTP handlers should delegate validation to the service layer rather than implementing their own validation.
⏰ Context from checks skipped due to timeout of 90000ms (6)
  • GitHub Check: test (testnet, esmeralda)
  • GitHub Check: test (mainnet, stagenet)
  • GitHub Check: test (nextnet, nextnet)
  • GitHub Check: Cucumber tests / FFI
  • GitHub Check: cargo check with stable
  • GitHub Check: Cucumber tests / Base Layer
🔇 Additional comments (9)
base_layer/wallet/src/output_manager_service/recovery/standard_outputs_recoverer.rs (1)

297-297: LGTM! Correctly updated method call to match new interface.

The change aligns with the updated try_output_key_recovery method signature that now accepts separate commitment and encrypted_data parameters instead of the full TransactionOutput. This improves modularity by only passing the specific data needed for key recovery.

base_layer/core/src/transactions/transaction_components/encrypted_data.rs (1)

619-624: Code organization improvement.

The EncryptedData struct was repositioned within the file with no functional changes. This improves code organization by moving the struct definition closer to its implementation.

base_layer/core/src/transactions/transaction_key_manager/interface.rs (1)

390-391: Good API improvement that enhances modularity.

The method signature change from accepting a full TransactionOutput to separate commitment and encrypted_data parameters improves modularity by requiring only the specific data needed for key recovery. This aligns with the broader refactoring to support HTTP-based UTXO synchronization with minimal data structures.

Note: This is a breaking API change, but appears to be consistently updated across implementations.

clients/rust/base_node_wallet_client/src/client/mod.rs (2)

5-10: New imports support HTTP-based UTXO syncing functionality.

The imports are well-organized and necessary for the new UTXO syncing methods:

  • RPC models for request/response types
  • ShutdownSignal for graceful termination
  • mpsc for streaming responses

23-33:

✅ Verification successful

Well-designed API for HTTP-based UTXO synchronization.

The new trait methods provide a clean interface for UTXO syncing:

  • get_utxos_by_block for single block queries
  • sync_utxos_by_block for streaming across block ranges with shutdown support

The API design is sound with appropriate error handling and streaming capabilities for large datasets.

Verify that implementations of these trait methods exist and handle errors appropriately:


🏁 Script executed:

#!/bin/bash
# Description: Check for implementations of the new trait methods
# Expected: Find implementations in http.rs or other implementation files

# Search for implementations of the new methods
rg -A 10 "async fn get_utxos_by_block"
rg -A 10 "async fn sync_utxos_by_block"

Length of output: 9953


Implementations Verified and Errors Properly Propagated

  • The methods are implemented in
    clients/rust/base_node_wallet_client/src/client/http.rs:
    • get_utxos_by_block constructs the URL, sends the HTTP request, and deserializes the response, using ? to propagate any URL‐parsing, network, or JSON errors as ClientError.
    • sync_utxos_by_block spawns a background task that iterates over the block range, streams responses into an mpsc::Receiver, and forwards any errors back through the ClientError channel.

All requested trait implementations exist and properly bubble up errors. Approving these changes.

applications/minotari_node/src/http/handler/get_utxos_by_block.rs (1)

49-62: LGTM! Well-structured handler implementation.

The handler implementation follows best practices with proper error handling, logging, and async/await usage. The parameter conversion and service call are correctly implemented.

base_layer/core/src/transactions/transaction_key_manager/wrapper.rs (1)

411-423: LGTM! Improved modularity through parameter separation.

The refactor to accept separate CompressedCommitment and EncryptedData parameters instead of a full TransactionOutput improves modularity and aligns well with the new minimal UTXO representation introduced in the RPC models. This change supports more efficient output key recovery.

base_layer/core/src/base_node/rpc/models/sync_utxos_by_block.rs (1)

16-18: LGTM! Limit increase addresses previous feedback.

The maximum limit has been appropriately increased from 5 to 2000, which addresses the previous review comment about enabling more efficient synchronization by reducing the number of HTTP requests required.

base_layer/core/src/base_node/rpc/models/get_utxos_by_block.rs (1)

14-17: LGTM! Simple and clean request model.

The request structure is appropriately minimal and well-defined for single block UTXO queries.

Comment on lines 38 to 48
#[utoipa::path(
get,
operation_id = "sync_utxos_by_block",
params(GetUtxosByBlockQueryParams),
path = "/sync_utxos_by_block",
responses(
(status = 200, description = "UTXOs returned successfully in the given headers' hash range", body = SyncUtxosByBlockResponse),
(status = NOT_FOUND, description = "Header not found", body = ErrorResponse, example = json!({"error": "Header not found at height: 10"})),
(status = INTERNAL_SERVER_ERROR, description = "Start/end header hash not found or header height mismatch", body = ErrorResponse),
),
)]
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

Fix inconsistent OpenAPI operation metadata.

The operation metadata contains inconsistencies that could confuse API consumers:

  1. operation_id = "sync_utxos_by_block" doesn't match the actual functionality of getting UTXOs for a single block
  2. The response description mentions "given headers' hash range" but this endpoint handles a single block hash

Apply this diff to fix the inconsistencies:

 #[utoipa::path(
     get,
-    operation_id = "sync_utxos_by_block",
+    operation_id = "get_utxos_by_block",
     params(GetUtxosByBlockQueryParams),
-    path = "/sync_utxos_by_block",
+    path = "/get_utxos_by_block",
     responses(
-        (status = 200, description = "UTXOs returned successfully in the given headers' hash range", body = SyncUtxosByBlockResponse),
+        (status = 200, description = "UTXOs returned successfully for the given block", body = GetUtxosByBlockResponse),
         (status = NOT_FOUND, description = "Header not found", body = ErrorResponse, example = json!({"error": "Header not found at height: 10"})),
         (status = INTERNAL_SERVER_ERROR, description = "Start/end header hash not found or header height mismatch", body = ErrorResponse),
     ),
 )]
🤖 Prompt for AI Agents
In applications/minotari_node/src/http/handler/get_utxos_by_block.rs around
lines 38 to 48, the OpenAPI operation metadata has inconsistencies: the
operation_id "sync_utxos_by_block" does not reflect that the endpoint fetches
UTXOs for a single block, and the response description incorrectly mentions a
range of headers instead of a single block hash. Update the operation_id to
something like "get_utxos_by_block" and revise the response description to
accurately state that UTXOs are returned for the specified block hash.

Copy link
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: 3

♻️ Duplicate comments (3)
base_layer/wallet/src/utxo_scanner_service/utxo_scanner_task.rs (3)

242-242: ⚠️ Potential issue

Remove all debug print statements.

Multiple debug log statements with "here" should be removed before merging.

Remove all instances of:

-info!(target: LOG_TARGET, "here");

Also applies to: 286-286, 445-445, 455-455, 468-468, 473-473, 485-485, 501-501, 506-506, 544-544


607-661: ⚠️ Potential issue

Implement the TODO for importing UTXOs to transaction service.

The import_utxos_to_transaction_service method has a TODO comment and the entire implementation is commented out. This appears to be critical functionality that's missing.

Would you like me to help implement the UTXO import logic or create an issue to track this missing functionality?


613-659: 🛠️ Refactor suggestion

Remove large block of commented out code.

The commented implementation should be removed. If this functionality is needed, it should be properly implemented or tracked in an issue.

Remove lines 614-659 which contain the commented out implementation.

🧹 Nitpick comments (1)
base_layer/core/src/base_node/rpc/models/sync_utxos_by_block.rs (1)

41-41: Remove the commented out code.

If the script field is not needed, remove the commented line to keep the code clean.

-    // pub script: Vec<u8>,
📜 Review details

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

📥 Commits

Reviewing files that changed from the base of the PR and between 39a084a and 9e8877a.

📒 Files selected for processing (5)
  • applications/minotari_node/src/http/server.rs (3 hunks)
  • base_layer/core/src/base_node/rpc/models/sync_utxos_by_block.rs (1 hunks)
  • base_layer/core/src/base_node/rpc/query_service.rs (4 hunks)
  • base_layer/wallet/src/lib.rs (2 hunks)
  • base_layer/wallet/src/utxo_scanner_service/utxo_scanner_task.rs (13 hunks)
🚧 Files skipped from review as they are similar to previous changes (3)
  • base_layer/wallet/src/lib.rs
  • applications/minotari_node/src/http/server.rs
  • base_layer/core/src/base_node/rpc/query_service.rs
⏰ Context from checks skipped due to timeout of 90000ms (6)
  • GitHub Check: Cucumber tests / FFI
  • GitHub Check: Cucumber tests / Base Layer
  • GitHub Check: test (mainnet, stagenet)
  • GitHub Check: test (testnet, esmeralda)
  • GitHub Check: test (nextnet, nextnet)
  • GitHub Check: cargo check with stable
🔇 Additional comments (4)
base_layer/core/src/base_node/rpc/models/sync_utxos_by_block.rs (1)

12-21: Good improvement on the limit value.

The maximum limit has been increased from 5 to 2000, which allows for more efficient synchronization as suggested in previous reviews.

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

481-499: Verify the incomplete scanning logic.

The code checks for found outputs but the actual import logic seems incomplete. After finding outputs, it fetches the block but then calls import_utxos_to_transaction_service which only has a TODO.

This scanning logic appears to be incomplete. The flow is:

  1. Search for owned outputs
  2. If found, fetch the full block
  3. Call import_utxos_to_transaction_service (which is not implemented)

Please verify that this partial implementation is intentional or if it needs to be completed before merging.


509-534: Clarify the commented out recovery tracking logic.

Lines 511-512 and 531-532 contain commented out code for tracking recovery counts and amounts. This seems like important functionality for the scanner.

-                        // count += scanned_block.num_outputs.unwrap_or(0);
-                        // amount += scanned_block.amount.unwrap_or_else(|| 0.into())

and

-                        // num_recovered = num_recovered.saturating_add(count);
-                        // total_amount += amount;

Are these recovery metrics no longer needed, or should they be properly implemented?


557-605: Good refactoring of the output search logic.

The method has been properly renamed from scan_for_outputs to search_for_owned_outputs and updated to work with MinimalUtxoSyncInfo. The async key recovery integration looks correct.

Copy link
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: 0

♻️ Duplicate comments (3)
base_layer/wallet/src/utxo_scanner_service/utxo_scanner_task.rs (3)

265-265: Remove debug print statements.

Multiple instances of debug print statements that should be removed before merging.

Remove all instances of:

-info!(target: LOG_TARGET, "here");

Also applies to: 311-311, 470-470, 481-481, 494-494, 498-498, 526-526, 532-532, 536-536, 576-576


206-211: Remove commented out code.

This commented code block should be removed to keep the codebase clean.

-        // let address = rpc_client.get_wallet_query_http_service_address().await?;
-        // if address.http_address.is_empty() {
-            // Err(UtxoScannerError::BaseNodeWalletServiceUrlEmpty)
-        // } else {
             Ok(http::Client::new(self.resources.http_client_url.clone()))
-        // }

813-817: Remove commented out method.

The get_next_peer method is no longer needed with the HTTP client approach and should be removed.

-    // fn get_next_peer(&mut self) -> Option<NodeId> {
-    //     let peer = self.peer_seeds.get(self.peer_index).map(NodeId::from_public_key);
-    //     self.peer_index += 1;
-    //     peer
-    // }
🧹 Nitpick comments (2)
base_layer/wallet/src/utxo_scanner_service/uxto_scanner_service_builder.rs (1)

23-23: Remove unused imports.

The imports std::any and crate::schema::client_key_values::key are not used anywhere in this file and should be removed to keep the code clean.

-use std::any;
-    schema::client_key_values::key,

Also applies to: 39-39

base_layer/core/src/base_node/rpc/query_service.rs (1)

194-194: Use consistent database access pattern.

Line 194 uses self.db directly while the rest of the method uses self.db(). For consistency, use the helper method throughout.

-        let end_header = self
-            .db
+        let end_header = self
+            .db()
📜 Review details

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

📥 Commits

Reviewing files that changed from the base of the PR and between 9e8877a and 260636f.

📒 Files selected for processing (24)
  • applications/minotari_console_wallet/src/automation/commands.rs (4 hunks)
  • applications/minotari_console_wallet/src/init/mod.rs (2 hunks)
  • applications/minotari_console_wallet/src/recovery.rs (3 hunks)
  • applications/minotari_console_wallet/src/wallet_modes.rs (1 hunks)
  • applications/minotari_node/src/http/handler/mod.rs (1 hunks)
  • base_layer/core/src/base_node/rpc/models/get_utxos_by_block.rs (1 hunks)
  • base_layer/core/src/base_node/rpc/models/mod.rs (1 hunks)
  • base_layer/core/src/base_node/rpc/models/sync_utxos_by_block.rs (1 hunks)
  • base_layer/core/src/base_node/rpc/query_service.rs (4 hunks)
  • base_layer/core/src/transactions/transaction_components/encrypted_data.rs (1 hunks)
  • base_layer/core/src/transactions/transaction_key_manager/inner.rs (1 hunks)
  • base_layer/core/src/transactions/transaction_key_manager/wrapper.rs (1 hunks)
  • base_layer/wallet/src/lib.rs (2 hunks)
  • base_layer/wallet/src/output_manager_service/recovery/mod.rs (1 hunks)
  • base_layer/wallet/src/output_manager_service/recovery/output_recovery_info.rs (1 hunks)
  • base_layer/wallet/src/output_manager_service/recovery/standard_outputs_recoverer.rs (1 hunks)
  • base_layer/wallet/src/utxo_scanner_service/initializer.rs (6 hunks)
  • base_layer/wallet/src/utxo_scanner_service/service.rs (6 hunks)
  • base_layer/wallet/src/utxo_scanner_service/utxo_scanner_task.rs (14 hunks)
  • base_layer/wallet/src/utxo_scanner_service/uxto_scanner_service_builder.rs (10 hunks)
  • base_layer/wallet/src/wallet.rs (2 hunks)
  • base_layer/wallet_ffi/src/lib.rs (5 hunks)
  • clients/rust/base_node_wallet_client/src/client/http.rs (2 hunks)
  • clients/rust/base_node_wallet_client/src/client/mod.rs (2 hunks)
✅ Files skipped from review due to trivial changes (1)
  • base_layer/wallet/src/output_manager_service/recovery/output_recovery_info.rs
🚧 Files skipped from review as they are similar to previous changes (20)
  • base_layer/wallet/src/output_manager_service/recovery/mod.rs
  • base_layer/core/src/base_node/rpc/models/mod.rs
  • base_layer/wallet/src/wallet.rs
  • base_layer/wallet/src/output_manager_service/recovery/standard_outputs_recoverer.rs
  • applications/minotari_console_wallet/src/recovery.rs
  • base_layer/core/src/transactions/transaction_components/encrypted_data.rs
  • base_layer/wallet/src/lib.rs
  • base_layer/wallet/src/utxo_scanner_service/initializer.rs
  • applications/minotari_console_wallet/src/init/mod.rs
  • applications/minotari_console_wallet/src/automation/commands.rs
  • clients/rust/base_node_wallet_client/src/client/mod.rs
  • base_layer/wallet_ffi/src/lib.rs
  • base_layer/core/src/base_node/rpc/models/get_utxos_by_block.rs
  • base_layer/core/src/base_node/rpc/models/sync_utxos_by_block.rs
  • base_layer/core/src/transactions/transaction_key_manager/inner.rs
  • clients/rust/base_node_wallet_client/src/client/http.rs
  • applications/minotari_node/src/http/handler/mod.rs
  • applications/minotari_console_wallet/src/wallet_modes.rs
  • base_layer/core/src/transactions/transaction_key_manager/wrapper.rs
  • base_layer/wallet/src/utxo_scanner_service/service.rs
🧰 Additional context used
🧠 Learnings (1)
base_layer/core/src/base_node/rpc/query_service.rs (1)
Learnt from: ksrichard
PR: tari-project/tari#7129
File: applications/minotari_node/src/http/handler/sync_utxos_by_block.rs:21-29
Timestamp: 2025-05-29T20:59:32.020Z
Learning: In the Tari codebase, request validation for RPC services is handled at the service layer using the `serde_valid` crate rather than at the HTTP handler level. For example, `SyncUtxosByBlockRequest` validates that limit is between 1-5 and page is minimum 0. HTTP handlers should delegate validation to the service layer rather than implementing their own validation.
🔇 Additional comments (3)
base_layer/wallet/src/utxo_scanner_service/uxto_scanner_service_builder.rs (1)

104-107: HTTP URL validation and error handling looks good.

The builder correctly validates that the HTTP node URL is set before building the service and provides clear error messages. The transition from peer-based to HTTP-based architecture is well implemented.

Also applies to: 116-123, 175-182

base_layer/core/src/base_node/rpc/query_service.rs (1)

88-147: New query methods are well implemented.

The fetch_kernel, fetch_utxos_by_block, and fetch_utxos methods provide comprehensive blockchain and mempool querying capabilities with proper validation, error handling, and pagination logic. The mempool integration for transaction status queries is particularly well designed.

Also applies to: 149-177, 179-289

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

205-212: HTTP client transition is well implemented.

The refactor from peer-based RPC to HTTP client is properly executed. The new methods for HTTP client creation, block scanning logic, streaming API usage, and output scanning with key manager integration are all correctly implemented.

Also applies to: 214-255, 471-477, 589-639

Copy link
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: 1

♻️ Duplicate comments (4)
clients/rust/base_node_wallet_client/src/client/http.rs (2)

107-107: Remove JSON body from GET request.

Sending a JSON body with a GET request is non-standard. The header_hash is already included in the query string, making the JSON body redundant.


122-181: Address hardcoded limit and error handling issues.

The implementation still has issues previously identified:

  1. The limit is hardcoded to 10 (line 135)
  2. Error handling doesn't break the loop - it will continue indefinitely on failure

Consider making the page size configurable and breaking the loop on errors:

+const DEFAULT_PAGE_SIZE: usize = 10;

 async fn sync_utxos_by_block(
     &self,
     start_header_hash: Vec<u8>,
     end_header_hash: Vec<u8>,
     shutdown: ShutdownSignal,
 ) -> Result<mpsc::Receiver<Result<SyncUtxosByBlockResponse, anyhow::Error>>, anyhow::Error> {
     debug!(target: LOG_TARGET, "Starting UTXO sync from {} to {}", start_header_hash.to_hex(), end_header_hash.to_hex());
     let mut target_url = self.api_address.join("/sync_utxos_by_block")?;
     let (resp_tx, resp_rx) = mpsc::channel(1000);
     let start_header_hash_hex = start_header_hash.to_hex();
     let end_header_hash_hex = end_header_hash.to_hex();
     let client = self.http_client.clone();

-    let limit = 10;
+    let limit = DEFAULT_PAGE_SIZE;
     tokio::spawn(async move {
         let mut page = 0;
         let mut has_next_page = true;
         while has_next_page {
             if shutdown.is_triggered() {
                 info!(target: LOG_TARGET, "UTXO sync task shutdown triggered, exiting loop");
                 break;
             }
             // ... existing code ...
             match client.get(target_url.clone()).send().await {
                 Ok(response) => match response.json::<SyncUtxosByBlockResponse>().await {
                     Ok(response) => {
                         has_next_page = response.has_next_page;
                         debug!(target: LOG_TARGET, "Received UTXOs for page {}", page);
                         if let Err(send_error) = resp_tx.send(Ok(response)).await {
                             error!(target: LOG_TARGET, "Error sending utxo response: {:?}", send_error);
+                            break; // Channel closed
                         }
                     },
                     Err(error) => {
                         if let Err(send_error) = resp_tx.send(Err(error.into())).await {
                             error!(target: LOG_TARGET, "Error sending error result: {:?}", send_error);
                         }
+                        break; // Stop on error
                     },
                 },
                 Err(error) => {
                     if let Err(send_error) = resp_tx.send(Err(error.into())).await {
                         error!(target: LOG_TARGET, "Error sending error result: {:?}", send_error);
                     }
+                    break; // Stop on error
                 },
             }
base_layer/wallet/src/utxo_scanner_service/utxo_scanner_task.rs (2)

274-592: Remove all debug print statements.

Multiple instances of uninformative debug print statements need to be removed throughout the file.

Remove all occurrences of:

-info!(target: LOG_TARGET, "here");

This applies to lines: 274, 276, 278, 282, 299, 305, 323, 471, 486, 497, 510, 514, 532, 542, 548, 553, 592


824-828: Remove commented out method.

The get_next_peer method is obsolete with the HTTP client approach and should be removed.

-    // fn get_next_peer(&mut self) -> Option<NodeId> {
-    //     let peer = self.peer_seeds.get(self.peer_index).map(NodeId::from_public_key);
-    //     self.peer_index += 1;
-    //     peer
-    // }
🧹 Nitpick comments (1)
applications/minotari_console_wallet/log4rs_sample.yml (1)

96-96: Remove trailing spaces.

-    encoder:
-      pattern: "{d(%Y-%m-%d %H:%M:%S.%f)} [{t}] [Thread:{I}] {l:5} {m} // {f}:{L}{n}"    
+    encoder:
+      pattern: "{d(%Y-%m-%d %H:%M:%S.%f)} [{t}] [Thread:{I}] {l:5} {m} // {f}:{L}{n}"
🧰 Tools
🪛 YAMLlint (1.37.1)

[error] 96-96: trailing spaces

(trailing-spaces)

📜 Review details

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

📥 Commits

Reviewing files that changed from the base of the PR and between 260636f and b68905a.

⛔ Files ignored due to path filters (1)
  • Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (12)
  • Cargo.toml (2 hunks)
  • applications/minotari_console_wallet/log4rs_sample.yml (6 hunks)
  • base_layer/core/src/base_node/rpc/models/sync_utxos_by_block.rs (1 hunks)
  • base_layer/wallet/src/error.rs (2 hunks)
  • base_layer/wallet/src/utxo_scanner_service/error.rs (0 hunks)
  • base_layer/wallet/src/utxo_scanner_service/mod.rs (0 hunks)
  • base_layer/wallet/src/utxo_scanner_service/utxo_scanner_task.rs (20 hunks)
  • clients/rust/base_node_wallet_client/Cargo.toml (1 hunks)
  • clients/rust/base_node_wallet_client/src/client/http.rs (2 hunks)
  • clients/rust/base_node_wallet_client/src/client/mod.rs (1 hunks)
  • clients/rust/base_node_wallet_client/src/error.rs (0 hunks)
  • clients/rust/base_node_wallet_client/src/lib.rs (0 hunks)
💤 Files with no reviewable changes (4)
  • base_layer/wallet/src/utxo_scanner_service/mod.rs
  • clients/rust/base_node_wallet_client/src/lib.rs
  • clients/rust/base_node_wallet_client/src/error.rs
  • base_layer/wallet/src/utxo_scanner_service/error.rs
✅ Files skipped from review due to trivial changes (1)
  • Cargo.toml
🚧 Files skipped from review as they are similar to previous changes (4)
  • clients/rust/base_node_wallet_client/Cargo.toml
  • base_layer/wallet/src/error.rs
  • base_layer/core/src/base_node/rpc/models/sync_utxos_by_block.rs
  • clients/rust/base_node_wallet_client/src/client/mod.rs
🧰 Additional context used
🪛 YAMLlint (1.37.1)
applications/minotari_console_wallet/log4rs_sample.yml

[error] 96-96: trailing spaces

(trailing-spaces)

🔇 Additional comments (3)
applications/minotari_console_wallet/log4rs_sample.yml (2)

66-96: LGTM!

The new utxo_scanner and http appenders are well-configured and follow the established pattern of other appenders in the file.

🧰 Tools
🪛 YAMLlint (1.37.1)

[error] 96-96: trailing spaces

(trailing-spaces)


146-157: LGTM!

The new logger configurations for wallet::utxo_scanning and tari::wallet::client::http are properly set up with appropriate appenders and non-additive settings.

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

605-650: LGTM!

The search_for_owned_outputs implementation correctly handles output decryption using both view key and recovery key approaches, with appropriate error handling.

Copy link
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: 1

♻️ Duplicate comments (6)
clients/rust/base_node_wallet_client/src/client/http.rs (2)

112-133: ⚠️ Potential issue

Remove JSON body from GET request.

Sending a JSON body with a GET request (line 119) is non-standard. The header hash is already included in the query parameters, so the JSON body should be removed.

Apply this diff to fix the issue:

 let res = self
     .http_client
     .get(target_url)
-    .json(&models::GetUtxosByBlockRequest { header_hash })
     .send()
     .await?;

135-194: 🛠️ Refactor suggestion

Make page size configurable and improve error handling.

The implementation has several areas for improvement:

  1. The page size is hardcoded to 10 (line 148)
  2. Error handling continues the loop indefinitely on failures

Consider these improvements:

+const DEFAULT_PAGE_SIZE: usize = 10;
+
 async fn sync_utxos_by_block(
     &self,
     start_header_hash: Vec<u8>,
     end_header_hash: Vec<u8>,
     shutdown: ShutdownSignal,
 ) -> Result<mpsc::Receiver<Result<SyncUtxosByBlockResponse, anyhow::Error>>, anyhow::Error> {
     debug!(target: LOG_TARGET, "Starting UTXO sync from {} to {}", start_header_hash.to_hex(), end_header_hash.to_hex());
     let mut target_url = self.api_address.join("/sync_utxos_by_block")?;
     let (resp_tx, resp_rx) = mpsc::channel(1000);
     let start_header_hash_hex = start_header_hash.to_hex();
     let end_header_hash_hex = end_header_hash.to_hex();
     let client = self.http_client.clone();

-    let limit = 10;
+    let limit = DEFAULT_PAGE_SIZE;
     tokio::spawn(async move {
         let mut page = 0;
         let mut has_next_page = true;
+        let mut consecutive_errors = 0;
+        const MAX_CONSECUTIVE_ERRORS: usize = 3;
         while has_next_page {
             if shutdown.is_triggered() {
                 info!(target: LOG_TARGET, "UTXO sync task shutdown triggered, exiting loop");
                 break;
             }
             // ... rest of the code ...
             match client.get(target_url.clone()).send().await {
                 Ok(response) => match response.json::<SyncUtxosByBlockResponse>().await {
                     Ok(response) => {
+                        consecutive_errors = 0;  // Reset error counter on success
                         has_next_page = response.has_next_page;
                         // ... rest of the code ...
                     },
                     Err(error) => {
+                        consecutive_errors += 1;
+                        if consecutive_errors >= MAX_CONSECUTIVE_ERRORS {
+                            error!(target: LOG_TARGET, "Too many consecutive errors, stopping sync");
+                            break;
+                        }
                         if let Err(send_error) = resp_tx.send(Err(error.into())).await {
                             error!(target: LOG_TARGET, "Error sending error result: {:?}", send_error);
                         }
                     },
                 },
                 Err(error) => {
+                    consecutive_errors += 1;
+                    if consecutive_errors >= MAX_CONSECUTIVE_ERRORS {
+                        error!(target: LOG_TARGET, "Too many consecutive errors, stopping sync");
+                        break;
+                    }
                     if let Err(send_error) = resp_tx.send(Err(error.into())).await {
                         error!(target: LOG_TARGET, "Error sending error result: {:?}", send_error);
                     }
                 },
             }
base_layer/wallet/src/utxo_scanner_service/utxo_scanner_task.rs (4)

207-214: Remove commented out code.

This commented code block should be removed to keep the codebase clean.

-    // let address = rpc_client.get_wallet_query_http_service_address().await?;
-    // if address.http_address.is_empty() {
-    // Err(anyhow::Error::BaseNodeWalletServiceUrlEmpty)
-    // } else {
     Ok(http::Client::new(self.resources.http_client_url.clone()))
-    // }

823-827: Remove commented out method.

The get_next_peer method is no longer needed with the HTTP client approach and should be removed.

-    // fn get_next_peer(&mut self) -> Option<NodeId> {
-    //     let peer = self.peer_seeds.get(self.peer_index).map(NodeId::from_public_key);
-    //     self.peer_index += 1;
-    //     peer
-    // }

274-275: ⚠️ Potential issue

Remove all debug print statements.

Multiple instances of debug print statements need to be removed before merging.

Remove all instances of:

-info!(target: LOG_TARGET, "here");

Also applies to lines: 483, 494, 507, 511, 520, 539, 545, 549, 567, 589

Also applies to: 276-277, 279-280, 282-283, 299-300, 305-306, 315-316, 323-324


542-543: ⚠️ Potential issue

Remove or implement incomplete TODO markers.

There are incomplete todo!() comments that suggest unfinished implementation.

Either implement the intended functionality or remove these todo!() calls if the code is not required:

-// todo!();

Also applies to: 547-548

🧹 Nitpick comments (1)
base_layer/wallet/src/utxo_scanner_service/utxo_scanner_task.rs (1)

845-851: Remove commented out code block.

This commented code should be removed to maintain code cleanliness.

-        // let block_height_birthday = client
-        //     .get_height_at_time(epoch_time_birthday)
-        //     .await
-        //     .unwrap_or_else(|e| {
-        //         warn!(target: LOG_TARGET, "Problem requesting `height_at_time` from Base Node: {}", e);
-        //         0
-        //     });
📜 Review details

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

📥 Commits

Reviewing files that changed from the base of the PR and between b68905a and c197003.

📒 Files selected for processing (3)
  • base_layer/wallet/src/utxo_scanner_service/utxo_scanner_task.rs (20 hunks)
  • clients/rust/base_node_wallet_client/src/client/http.rs (2 hunks)
  • clients/rust/base_node_wallet_client/src/client/mod.rs (1 hunks)
🔇 Additional comments (1)
clients/rust/base_node_wallet_client/src/client/mod.rs (1)

5-30: LGTM! Good refactoring to standardize error handling.

The changes to use anyhow::Error instead of a custom error type simplifies error handling across the codebase. The modification to return Option<BlockHeader> for get_header_by_height is a good design choice that explicitly handles the case where a header might not exist at a given height.

Comment on lines 225 to 234
loop {
next_header = wallet_service_client
.get_header_by_height(last_scanned_block.height + 1)
.await?;
if !next_header.is_none() {
break;
}
height = height.saturating_sub(1);
}
let next_header = next_header.unwrap();
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

Fix potential infinite loop in header search.

The loop searching for a known header could run indefinitely if no header is found. Add a lower bound to prevent this.

 loop {
     next_header = wallet_service_client
         .get_header_by_height(last_scanned_block.height + 1)
         .await?;
     if !next_header.is_none() {
         break;
     }
+    if height == 0 {
+        return Err(anyhow!("No known headers found in base node"));
+    }
     height = height.saturating_sub(1);
 }
📝 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
loop {
next_header = wallet_service_client
.get_header_by_height(last_scanned_block.height + 1)
.await?;
if !next_header.is_none() {
break;
}
height = height.saturating_sub(1);
}
let next_header = next_header.unwrap();
loop {
next_header = wallet_service_client
.get_header_by_height(last_scanned_block.height + 1)
.await?;
if !next_header.is_none() {
break;
}
if height == 0 {
return Err(anyhow!("No known headers found in base node"));
}
height = height.saturating_sub(1);
}
let next_header = next_header.unwrap();
🤖 Prompt for AI Agents
In base_layer/wallet/src/utxo_scanner_service/utxo_scanner_task.rs around lines
225 to 234, the loop that searches for the next header can run indefinitely if
no header is found. To fix this, add a lower bound check on the height variable
inside the loop and break or return an error if the height reaches zero or
another minimum value, preventing an infinite loop.

Copy link
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: 1

♻️ Duplicate comments (3)
base_layer/wallet/src/utxo_scanner_service/utxo_scanner_task.rs (3)

203-210: Remove commented-out code.

The commented-out code should be removed to keep the codebase clean.

-    /// Try to instantiate a Base Node Wallet Service client.
-    fn base_node_wallet_service_client(&self) -> Result<http::Client, anyhow::Error> {
-        // let address = rpc_client.get_wallet_query_http_service_address().await?;
-        // if address.http_address.is_empty() {
-        // Err(anyhow::Error::BaseNodeWalletServiceUrlEmpty)
-        // } else {
-        Ok(http::Client::new(self.resources.http_client_url.clone()))
-        // }
-    }
+    /// Try to instantiate a Base Node Wallet Service client.
+    fn base_node_wallet_service_client(&self) -> Result<http::Client, anyhow::Error> {
+        Ok(http::Client::new(self.resources.http_client_url.clone()))
+    }

212-256: Fix potential infinite loop in header search.

The loop searching for a known header could run indefinitely if no header is found. This is the same issue identified in past reviews.

 loop {
     next_header = wallet_service_client
         .get_header_by_height(last_scanned_block.height + 1)
         .await?;
     if !next_header.is_none() {
         break;
     }
+    if height == 0 {
+        return Err(anyhow!("No known headers found in base node"));
+    }
     height = height.saturating_sub(1);
 }

765-769: Remove commented-out method.

The get_next_peer method is no longer needed with the HTTP client approach and should be removed as previously noted.

-    // fn get_next_peer(&mut self) -> Option<NodeId> {
-    //     let peer = self.peer_seeds.get(self.peer_index).map(NodeId::from_public_key);
-    //     self.peer_index += 1;
-    //     peer
-    // }
🧹 Nitpick comments (1)
base_layer/wallet/src/utxo_scanner_service/utxo_scanner_task.rs (1)

787-815: Remove large commented-out code block.

The commented-out code for getting height at time should be removed to clean up the codebase.

-        // let block_height_birthday = client
-        //     .get_height_at_time(epoch_time_birthday)
-        //     .await
-        //     .unwrap_or_else(|e| {
-        //         warn!(target: LOG_TARGET, "Problem requesting `height_at_time` from Base Node: {}", e);
-        //         0
-        //     });
📜 Review details

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

📥 Commits

Reviewing files that changed from the base of the PR and between f8fd2b8 and 8530e7d.

📒 Files selected for processing (7)
  • applications/minotari_console_wallet/src/automation/commands.rs (5 hunks)
  • applications/minotari_console_wallet/src/recovery.rs (4 hunks)
  • base_layer/wallet/src/storage/sqlite_db/scanned_blocks.rs (2 hunks)
  • base_layer/wallet/src/utxo_scanner_service/handle.rs (1 hunks)
  • base_layer/wallet/src/utxo_scanner_service/service.rs (6 hunks)
  • base_layer/wallet/src/utxo_scanner_service/utxo_scanner_task.rs (15 hunks)
  • base_layer/wallet_ffi/src/tasks.rs (2 hunks)
🚧 Files skipped from review as they are similar to previous changes (5)
  • applications/minotari_console_wallet/src/automation/commands.rs
  • base_layer/wallet/src/utxo_scanner_service/handle.rs
  • base_layer/wallet_ffi/src/tasks.rs
  • applications/minotari_console_wallet/src/recovery.rs
  • base_layer/wallet/src/utxo_scanner_service/service.rs
⏰ Context from checks skipped due to timeout of 90000ms (6)
  • GitHub Check: test (nextnet, nextnet)
  • GitHub Check: test (mainnet, stagenet)
  • GitHub Check: test (testnet, esmeralda)
  • GitHub Check: cargo check with stable
  • GitHub Check: Cucumber tests / Base Layer
  • GitHub Check: Cucumber tests / FFI
🔇 Additional comments (6)
base_layer/wallet/src/storage/sqlite_db/scanned_blocks.rs (2)

54-62: LGTM: Constructor correctly nullifies removed fields.

The constructor properly sets both amount and num_outputs to None, which aligns with the broader refactoring to remove these fields from the data model.


103-113: LGTM: Conversion trait properly handles field removal.

The From<ScannedBlock> implementation correctly sets both amount and num_outputs to None, maintaining consistency with the simplified ScannedBlock data model that no longer tracks these values.

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

23-72: LGTM: Import updates support the HTTP client refactoring.

The new imports correctly support the architectural shift to HTTP-based communication, including the new HTTP client, error handling with anyhow, and updated RPC models for UTXO synchronization.


90-100: LGTM: Generic type parameter addition enhances flexibility.

The addition of the TKeyManager generic type parameter allows for better abstraction and testability of the key management functionality.


544-589: LGTM: Key recovery logic is well-implemented.

The search_for_owned_outputs method correctly implements both view key-based detection for change outputs and DH-based detection for received outputs. The error handling and key derivation logic appear sound.


470-501: LGTM: Output processing logic is well-structured.

The two-phase approach of first identifying owned outputs with minimal data, then fetching full block data for confirmed matches is an efficient strategy that minimizes bandwidth usage while ensuring complete recovery.

Comment on lines +447 to +453
let mut utxo_stream = client
.sync_utxos_by_block(
start_header_hash.to_vec(),
end_header_hash.to_vec(),
self.shutdown_signal.clone(),
)
.await?;
Copy link
Contributor

Choose a reason for hiding this comment

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

💡 Verification agent

🧩 Analysis chain

Verify shutdown signal handling in streaming.

Ensure that the shutdown signal is properly handled during UTXO streaming to prevent resource leaks or hanging connections.

The shutdown signal is passed to the streaming method, but verify that the HTTP client properly terminates the stream when the signal is triggered:


🏁 Script executed:

#!/bin/bash
# Description: Check how shutdown signals are handled in the HTTP client streaming implementation

# Search for shutdown signal handling in HTTP client
ast-grep --pattern 'fn sync_utxos_by_block($$$) {
  $$$
}'

# Look for shutdown signal usage in HTTP client
rg -A 10 "shutdown.*signal" --type rust

Length of output: 70876


🏁 Script executed:

#!/bin/bash
set -e
# Locate the implementation of `sync_utxos_by_block`
rg -n "sync_utxos_by_block" -n

Length of output: 4085


🏁 Script executed:

#!/bin/bash
set -e
# Display the HTTP client implementation for `sync_utxos_by_block`
rg -n "async fn sync_utxos_by_block" -A50 clients/rust/base_node_wallet_client/src/client/http.rs

Length of output: 3036


🏁 Script executed:

#!/bin/bash
set -e
# Show full async block in sync_utxos_by_block to verify shutdown handling
sed -n '135,260p' clients/rust/base_node_wallet_client/src/client/http.rs

Length of output: 2987


🏁 Script executed:

#!/bin/bash
set -e
# Show utxo_stream and shutdown handling in utxo_scanner_task
rg -n "utxo_stream" -n base_layer/wallet/src/utxo_scanner_service/utxo_scanner_task.rs -A10 -B10
rg -n "shutdown_signal" -n base_layer/wallet/src/utxo_scanner_service/utxo_scanner_task.rs -A5 -B5

Length of output: 3486


Add cancellation for in-flight UTXO streaming on shutdown

It looks like both the HTTP client’s sync_utxos_by_block and the scanner loop only check shutdown.is_triggered() between pages/messages, so a long-running HTTP request or a stalled recv().await could hang until it completes. To ensure timely shutdown and avoid leaked connections/tasks, introduce tokio::select! around the request and receive futures:

• In clients/rust/base_node_wallet_client/src/client/http.rs (around lines 152–186):
Replace

while has_next_page {
    if shutdown.is_triggered() { break; }
    let response = client.get(target_url.clone()).send().await?;}

with something like:

loop {
    tokio::select! {
        _ = shutdown.wait() => break,
        resp = client.get(target_url.clone()).send() => match resp {
            Ok(response) => { /* parse and send */ }
            Err(err) => break,
        }
    }
    if !has_next_page { break; }
    page += 1;
}

• In base_layer/wallet/src/utxo_scanner_service/utxo_scanner_task.rs (around lines 447–461):
Wrap the receive in select! so you can abort waiting for the next page:

loop {
    tokio::select! {
        _ = self.shutdown_signal.wait() => return Ok(total_scanned),
        msg = utxo_stream.recv() => match msg {
            Some(response) => {}
            None => break,
        }
    }}

This change guarantees that both pending HTTP calls and channel receives are cancelled immediately when shutdown is triggered.

🤖 Prompt for AI Agents
In base_layer/wallet/src/utxo_scanner_service/utxo_scanner_task.rs around lines
447 to 453, the current UTXO streaming loop waits on utxo_stream.recv() without
concurrently checking the shutdown signal, which can cause hangs on shutdown. To
fix this, wrap the receive call in a tokio::select! block that concurrently
awaits either the shutdown_signal.wait() or utxo_stream.recv(), returning early
if shutdown is triggered. Similarly, in
clients/rust/base_node_wallet_client/src/client/http.rs around lines 152 to 186,
refactor the HTTP request loop to use tokio::select! to concurrently await the
shutdown signal or the HTTP request future, breaking the loop immediately if
shutdown occurs. This ensures timely cancellation of in-flight requests and
prevents resource leaks.

Copy link
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: 1

♻️ Duplicate comments (4)
base_layer/wallet/src/utxo_scanner_service/utxo_scanner_task.rs (4)

168-175: Remove commented out code and verify error handling

The HTTP client creation logic is correct, but the commented out code should be removed for cleanliness. Also consider adding validation for the URL configuration.

-        // let address = rpc_client.get_wallet_query_http_service_address().await?;
-        // if address.http_address.is_empty() {
-        // Err(anyhow::Error::BaseNodeWalletServiceUrlEmpty)
-        // } else {
         Ok(http::Client::new(self.resources.http_client_url.clone()))
-        // }

186-194: Add bounds check to prevent infinite loop

The loop searching for a known header could run indefinitely if no header is found. Add a lower bound to prevent this:

 loop {
     next_header = wallet_service_client
         .get_header_by_height(last_scanned_block.height + 1)
         .await?;
     if !next_header.is_none() {
         break;
     }
+    if height == 0 {
+        return Err(anyhow!("No known headers found in base node"));
+    }
     height = height.saturating_sub(1);
 }

413-416: Improve shutdown signal handling during streaming

The current implementation only checks the shutdown signal between messages, which could cause delays during long-running HTTP requests. Consider wrapping the receive call in tokio::select! to enable immediate cancellation:

-        while let Some(response) = utxo_stream.recv().await {
-            if self.shutdown_signal.is_triggered() {
-                return Ok((total_scanned, total_num_recovered, total_value_recovered));
-            }
+        loop {
+            tokio::select! {
+                _ = self.shutdown_signal.wait() => return Ok((total_scanned, total_num_recovered, total_value_recovered)),
+                msg = utxo_stream.recv() => match msg {
+                    Some(response) => {
+                        let response = response?;
+                        // ... process response
+                    }
+                    None => break,
+                }
+            }
+        }

726-730: Remove commented out method

The get_next_peer method is no longer needed with the HTTP client approach and should be removed:

-    // fn get_next_peer(&mut self) -> Option<NodeId> {
-    //     let peer = self.peer_seeds.get(self.peer_index).map(NodeId::from_public_key);
-    //     self.peer_index += 1;
-    //     peer
-    // }
🧹 Nitpick comments (3)
base_layer/wallet/tests/transaction_service_tests/service.rs (1)

240-240: Consider making the test URL configurable or use a constant.

The hardcoded localhost URL works for tests, but consider extracting it to a constant at the module level for better maintainability and consistency across tests.

+const TEST_HTTP_NODE_URL: &str = "http://127.0.0.1:5434";
+
 // ... later in the function
-    let http_node_url = Url::parse("http://127.0.0.1:5434").unwrap();
+    let http_node_url = Url::parse(TEST_HTTP_NODE_URL).unwrap();
base_layer/wallet/tests/utxo_scanner/mod.rs (1)

727-727: Remove commented out code

The commented out line // let num_outputs = if h % 2 == 1 { Some(1) } else { None }; should be removed to keep the codebase clean, as it appears to reference fields that no longer exist in the ScannedBlock struct.

-        // let num_outputs = if h % 2 == 1 { Some(1) } else { None };
base_layer/wallet/src/utxo_scanner_service/utxo_scanner_task.rs (1)

748-754: Consider removing commented out code

The commented out code block for get_height_at_time request should be removed if it's no longer needed, or properly implemented if it's required functionality.

-        // let block_height_birthday = client
-        //     .get_height_at_time(epoch_time_birthday)
-        //     .await
-        //     .unwrap_or_else(|e| {
-        //         warn!(target: LOG_TARGET, "Problem requesting `height_at_time` from Base Node: {}", e);
-        //         0
-        //     });
📜 Review details

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

📥 Commits

Reviewing files that changed from the base of the PR and between 7568a05 and 5470588.

📒 Files selected for processing (14)
  • .cargo/config.toml (1 hunks)
  • applications/minotari_console_wallet/src/automation/commands.rs (4 hunks)
  • applications/minotari_console_wallet/src/recovery.rs (4 hunks)
  • base_layer/core/src/transactions/transaction_components/test.rs (2 hunks)
  • base_layer/core/src/transactions/transaction_components/transaction_output.rs (1 hunks)
  • base_layer/core/src/transactions/transaction_components/wallet_output_builder.rs (1 hunks)
  • base_layer/core/src/transactions/transaction_protocol/recipient.rs (1 hunks)
  • base_layer/core/src/transactions/transaction_protocol/sender.rs (1 hunks)
  • base_layer/wallet/src/utxo_scanner_service/handle.rs (1 hunks)
  • base_layer/wallet/src/utxo_scanner_service/utxo_scanner_task.rs (15 hunks)
  • base_layer/wallet/tests/transaction_service_tests/service.rs (4 hunks)
  • base_layer/wallet/tests/utxo_scanner/mod.rs (8 hunks)
  • base_layer/wallet_ffi/src/tasks.rs (2 hunks)
  • base_layer/wallet_ffi/wallet.h (1 hunks)
✅ Files skipped from review due to trivial changes (1)
  • .cargo/config.toml
🚧 Files skipped from review as they are similar to previous changes (5)
  • base_layer/wallet/src/utxo_scanner_service/handle.rs
  • base_layer/wallet_ffi/src/tasks.rs
  • applications/minotari_console_wallet/src/recovery.rs
  • applications/minotari_console_wallet/src/automation/commands.rs
  • base_layer/wallet_ffi/wallet.h
🧰 Additional context used
🧬 Code Graph Analysis (1)
base_layer/wallet/tests/utxo_scanner/mod.rs (3)
base_layer/wallet/src/utxo_scanner_service/initializer.rs (1)
  • UtxoScannerService (124-128)
comms/core/src/test_utils/mocks/connectivity_manager.rs (1)
  • create_connectivity_mock (43-50)
base_layer/wallet/src/utxo_scanner_service/service.rs (1)
  • builder (107-109)
⏰ Context from checks skipped due to timeout of 90000ms (6)
  • GitHub Check: test (mainnet, stagenet)
  • GitHub Check: test (testnet, esmeralda)
  • GitHub Check: test (nextnet, nextnet)
  • GitHub Check: cargo check with stable
  • GitHub Check: Cucumber tests / Base Layer
  • GitHub Check: Cucumber tests / FFI
🔇 Additional comments (18)
base_layer/core/src/transactions/transaction_components/transaction_output.rs (1)

632-632: LGTM! Interface change correctly implemented.

The update to pass separate commitment() and encrypted_data() parameters instead of the entire TransactionOutput aligns with the broader interface refactor described in the AI summary. The accessor methods are used correctly and the functionality remains equivalent.

base_layer/core/src/transactions/transaction_protocol/sender.rs (1)

1725-1725: LGTM! Consistent interface adaptation.

The update correctly adapts to the new try_output_key_recovery method signature by passing separate commitment() and encrypted_data() parameters. This change is consistent with the broader interface refactor across the codebase.

base_layer/core/src/transactions/transaction_components/wallet_output_builder.rs (1)

396-396: Interface change correctly implemented in test.

The update to try_output_key_recovery correctly passes commitment() and encrypted_data() as separate parameters instead of the full output object, maintaining the same test logic while conforming to the updated interface.

base_layer/core/src/transactions/transaction_protocol/recipient.rs (1)

250-250: Consistent interface update in recipient protocol test.

The change correctly updates the try_output_key_recovery call to use separate commitment and encrypted data parameters, maintaining test functionality while conforming to the new interface.

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

104-104: Interface update correctly applied in key manager test.

The change properly updates the try_output_key_recovery call to pass commitment and encrypted data separately, maintaining the test's validation logic.


572-572: Consistent interface change in output recovery test.

The update correctly implements the new method signature while preserving the test's functionality to validate output key recovery.

base_layer/wallet/tests/transaction_service_tests/service.rs (3)

174-174: LGTM: Import addition for URL handling.

The import of url::Url is correctly added to support the new HTTP-based UTXO syncing functionality.


287-287: LGTM: Correct parameter passing for HTTP-based UTXO scanning.

The HTTP node URL is properly passed to the UtxoScannerServiceInitializer, which aligns with the refactoring from peer-based RPC to HTTP calls.


1563-1563: LGTM: Method signature correctly updated.

The call to try_output_key_recovery has been properly updated to pass the output's commitment and encrypted data separately instead of the entire output object, matching the new interface described in the AI summary.

base_layer/wallet/tests/utxo_scanner/mod.rs (3)

89-89: LGTM: Generic type parameter correctly updated

The change from WalletConnectivityMock to MemoryDbKeyManager properly reflects the architectural shift from peer-based connectivity to key manager-based operations.


177-211: LGTM: Service builder correctly updated for new architecture

The builder configuration properly uses the new MemoryDbKeyManager generic parameter and passes the key manager to build_with_resources. The simplified configuration without connectivity mocks aligns with the HTTP client approach.


374-374: LGTM: Event pattern matching correctly updated

The explicit ignoring of the time_taken field with time_taken: _, properly handles the updated UtxoScannerEvent::Completed structure while maintaining test functionality.

Also applies to: 522-524, 706-708

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

67-83: LGTM: Struct updates properly support new architecture

The new SyncResult struct provides good encapsulation of scanning results, and the addition of the TKeyManager generic parameter to UtxoScannerTask correctly supports the key manager-based approach.


89-135: LGTM: Run method correctly updated for new architecture

The updated run() method properly uses the new SyncResult struct and maintains appropriate error handling and retry logic for the HTTP client approach.


224-289: LGTM: Sync method correctly implements HTTP client approach

The attempt_sync() method is properly refactored to use the HTTP client, correctly accumulates scanning results, and maintains appropriate error handling and shutdown signal checking.


503-550: LGTM: Output recovery logic correctly implemented

The search_for_owned_outputs() method properly implements key-based output recovery using the view key for change outputs and sender offset recovery for received outputs. Error handling for invalid commitments and keys is appropriate.


615-615: LGTM: Performance improvement with slice reference

Changing the parameter from Vec<(WalletOutput, ImportStatus, TxId, TransactionOutput)> to a slice reference avoids unnecessary cloning and improves performance.


667-685: LGTM: Error handling improved with anyhow::Error

The updated return types using anyhow::Error provide better flexibility for error handling while maintaining the same logic for recovery mode management.

@@ -101,7 +101,7 @@ async fn key_manager_input() {
.expect("Should be able to create transaction output");

assert_eq!(*input.features().unwrap(), OutputFeatures::default());
let (_, value, _) = key_manager.try_output_key_recovery(&output, None).await.unwrap();
let (_, value, _) = key_manager.try_output_key_recovery(output.commitment(), output.encrypted_data(), None).await.unwrap();
Copy link
Contributor

Choose a reason for hiding this comment

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

💡 Verification agent

🧩 Analysis chain

Verify all usages of try_output_key_recovery have been updated.

Ensure that all calls to try_output_key_recovery throughout the codebase have been updated to use the new signature with separate commitment and encrypted_data parameters.

Also applies to: 572-572


🏁 Script executed:

#!/bin/bash
# Description: Search for any remaining old-style calls to try_output_key_recovery
# Expected: All calls should use the new signature with separate parameters

echo "Searching for try_output_key_recovery method calls..."
rg -A 2 -B 1 "try_output_key_recovery"

echo -e "\n=== Checking for potential old-style calls ==="
# Look for calls that might still pass a full output object
rg "try_output_key_recovery.*output[^.]" --type rust

echo -e "\n=== Checking method definition ==="
# Find the actual method definition to understand the new signature
ast-grep --pattern $'try_output_key_recovery($_, $_, $_) {
  $$$
}'

Length of output: 6461


Update remaining outdated call in documentation

One legacy usage of try_output_key_recovery remains in the docs. Please update it to the new signature with separate commitment and encrypted_data parameters:

• docs/guide/tari_transaction_creation_overview.md

- let (key, _value, _) = key_manager_alice.try_output_key_recovery(output, None).await.unwrap();
+ let (key, _value, _) = key_manager_alice
+     .try_output_key_recovery(output.commitment(), output.encrypted_data(), None)
+     .await
+     .unwrap();
🤖 Prompt for AI Agents
In base_layer/core/src/transactions/transaction_components/test.rs at line 104,
ensure the call to try_output_key_recovery uses the new signature with separate
commitment and encrypted_data parameters, not a single output object. Update the
call to pass output.commitment() and output.encrypted_data() as separate
arguments, and verify all other calls in the codebase follow this pattern. Also,
update the legacy usage in docs/guide/tari_transaction_creation_overview.md
accordingly.

@stringhandler stringhandler changed the title feat: change wallet to use http calls for utxo syncing feat!: change wallet to use http calls for utxo syncing Jun 10, 2025
Copy link
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: 1

♻️ Duplicate comments (1)
base_layer/wallet_ffi/src/lib.rs (1)

10021-10042: URL validation logic is comprehensive and handles edge cases well.

The validation covers null pointers, string conversion failures, and URL parsing errors with appropriate error codes for each case.

Note: Previous review comments about formatting issues in this section have been addressed.

🧹 Nitpick comments (1)
base_layer/wallet_ffi/src/lib.rs (1)

6746-6746: Document breaking change in FFI interface.

The addition of the http_base_node parameter is a breaking change to the FFI interface. Ensure this is properly documented in release notes and migration guides.

📜 Review details

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

📥 Commits

Reviewing files that changed from the base of the PR and between 0c5c545 and 57384d0.

📒 Files selected for processing (2)
  • base_layer/wallet_ffi/src/lib.rs (37 hunks)
  • base_layer/wallet_ffi/wallet.h (2 hunks)
✅ Files skipped from review due to trivial changes (1)
  • base_layer/wallet_ffi/wallet.h
⏰ Context from checks skipped due to timeout of 90000ms (5)
  • GitHub Check: test (nextnet, nextnet)
  • GitHub Check: ci
  • GitHub Check: test (mainnet, stagenet)
  • GitHub Check: test (testnet, esmeralda)
  • GitHub Check: cargo check with stable
🔇 Additional comments (6)
base_layer/wallet_ffi/src/lib.rs (6)

84-84: Imports look correct for the new HTTP-based functionality.

The addition of WalletConnectivityInterface and WalletKeyManager imports aligns with the architectural changes described in the PR objectives.

Also applies to: 112-112


193-193: Good addition of URL parsing capability.

The url::Url import is necessary for the new HTTP URL validation logic.


9985-9985: Consistent FFI interface change for recovery function.

The addition of http_base_node parameter to wallet_start_recovery maintains consistency with the wallet_create function changes.


10008-10008: Architectural change from connectivity handle to key manager.

The change in generic parameters from WalletConnectivityHandle to WalletKeyManager reflects the shift from peer-based to HTTP-based connectivity as described in the PR objectives.


10045-10045: Consistent method change for HTTP-based connectivity.

The change from .with_peers(peer_public_keys) to .with_http_node_url(http_url) aligns with the overall architectural shift to HTTP-based UTXO syncing.


11611-11612: Test updates are consistent and use appropriate test URL.

All test cases have been updated to provide the required http_base_node parameter using a consistent localhost URL for testing.

Also applies to: 11665-11666, 11790-11791, 12027-12028, 12100-12101, 12186-12187, 12407-12408, 12553-12554, 12706-12707, 12979-12980, 13261-13262, 13531-13532, 13919-13920, 13990-13991

Comment on lines +6895 to +6903
let http_base_node = if http_base_node.is_null() {
None
} else {
let base_node = CStr::from_ptr(http_base_node)
.to_str()
.expect("A non-null base node should be able to be converted to string")
.to_owned();
Some(base_node)
};
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

Replace panic-prone .expect() with proper error handling.

Using .expect() in FFI code can cause crashes in the calling application if string conversion fails. This should be handled gracefully with proper error reporting.

-    let http_base_node = if http_base_node.is_null() {
-        None
-    } else {
-        let base_node = CStr::from_ptr(http_base_node)
-            .to_str()
-            .expect("A non-null base node should be able to be converted to string")
-            .to_owned();
-        Some(base_node)
-    };
+    let http_base_node = if http_base_node.is_null() {
+        None
+    } else {
+        match CStr::from_ptr(http_base_node).to_str() {
+            Ok(s) => Some(s.to_owned()),
+            Err(_) => {
+                *error_out = LibWalletError::from(InterfaceError::PointerError("http_base_node contains invalid UTF-8".to_string())).code;
+                return ptr::null_mut();
+            }
+        }
+    };
📝 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
let http_base_node = if http_base_node.is_null() {
None
} else {
let base_node = CStr::from_ptr(http_base_node)
.to_str()
.expect("A non-null base node should be able to be converted to string")
.to_owned();
Some(base_node)
};
let http_base_node = if http_base_node.is_null() {
None
} else {
match CStr::from_ptr(http_base_node).to_str() {
Ok(s) => Some(s.to_owned()),
Err(_) => {
*error_out = LibWalletError::from(
InterfaceError::PointerError(
"http_base_node contains invalid UTF-8".to_string()
)
).code;
return ptr::null_mut();
}
}
};
🤖 Prompt for AI Agents
In base_layer/wallet_ffi/src/lib.rs around lines 6895 to 6903, replace the
panic-prone .expect() call on string conversion with proper error handling.
Instead of unwrapping the result of to_str(), handle the Result explicitly by
checking for an error and returning an appropriate error code or message to the
caller, ensuring the FFI boundary does not cause the application to crash on
invalid input.

Copy link
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: 5

♻️ Duplicate comments (3)
base_layer/wallet/src/utxo_scanner_service/initializer.rs (1)

124-143: Panic-on-error (expect) remains – replace with proper error propagation

Identical feedback was already given earlier; the expect("Failed to build UTXO scanner service") will still crash the
wallet in production. Convert the enclosing async closure to Result and bubble the error up instead of aborting.

base_layer/wallet_ffi/src/lib.rs (1)

6898-6906: ⚠️ Potential issue

Still using .expect() – FFI may abort the host process

The panic-prone .expect() call survived the refactor although it was pointed out in an earlier review. Any invalid UTF-8 in http_base_node will unwind past the FFI boundary and crash the embedding application.

-        let base_node = CStr::from_ptr(http_base_node)
-            .to_str()
-            .expect("A non-null base node should be able to be converted to string")
-            .to_owned();
-        Some(base_node)
+        match CStr::from_ptr(http_base_node).to_str() {
+            Ok(s) => Some(s.to_owned()),
+            Err(_) => {
+                *error_out = LibWalletError::from(
+                    InterfaceError::PointerError("http_base_node contains invalid UTF-8".into()),
+                )
+                .code;
+                return ptr::null_mut();
+            }
+        }
base_layer/wallet/src/utxo_scanner_service/utxo_scanner_task.rs (1)

415-419: Shutdown still blocks on recv().await
Previous feedback about wrapping the recv() in a tokio::select! with the
shutdown signal hasn’t been addressed. The task will hang indefinitely if the
stream stalls after shutdown is triggered.
(See earlier review thread.)

🧹 Nitpick comments (9)
base_layer/wallet/src/utxo_scanner_service/initializer.rs (1)

49-67: Validate http_node_url during construction

Url::parse will accept many “valid‐ish” inputs (e.g. missing scheme defaults to file:).
Before storing the value, consider validating that

  1. the scheme is http or https, and
  2. the host part is present.
-    pub fn new(backend: WalletDatabase<T>, network: Network, birthday_offset: u16, http_node_url: Url) -> Self {
+    pub fn new(
+        backend: WalletDatabase<T>,
+        network: Network,
+        birthday_offset: u16,
+        http_node_url: Url,
+    ) -> Self {
+        assert!(
+            matches!(http_node_url.scheme(), "http" | "https") && http_node_url.has_host(),
+            "UtxoScannerServiceInitializer: invalid base-node URL: {}",
+            http_node_url
+        );

This fail-fast check prevents accidentally running against file:///tmp or similar.

base_layer/wallet/tests/utxo_scanner/mod.rs (2)

90-95: Introduce a type alias to tame the 3-parameter generic

UtxoScannerService<WalletSqliteDatabase, MemoryDbKeyManager, MockHttpClientFactory> is verbose and appears dozens of
times. A simple alias improves readability across the test module.

type TestScannerService = UtxoScannerService<
    WalletSqliteDatabase,
    MemoryDbKeyManager,
    MockHttpClientFactory,
>;

Usage then becomes Option<TestScannerService>.


822-830: 1 s watchdog is too tight → intermittent CI flakiness

Heavy CI boxes or the Tokio scheduler under load can legitimately take >1 s to spawn the scanner.
Bump the timeout or make it configurable to avoid spurious failures.

-let delay = time::sleep(Duration::from_secs(1));
+let delay = time::sleep(Duration::from_secs(10));
base_layer/wallet/src/utxo_scanner_service/service.rs (2)

204-208: HttpClientFactory should be Send + Sync

UtxoScannerService and the task spawn the factory across threads, but the bound is missing on the trait itself.
Adding it prevents accidental non-thread-safe factory implementations.

-pub trait HttpClientFactory: Clone {
+pub trait HttpClientFactory: Clone + Send + Sync + 'static {

Note: existing impls already satisfy these bounds, so this is a zero-cost compile-time safety net.


221-227: Consider caching the HTTP client

DefaultHttpClientFactory::create_http_client instantiates a new HTTP client per call.
If the client maintains a connection pool or TLS session, reusing a single instance is cheaper:

 #[derive(Clone)]
 pub struct DefaultHttpClientFactory {
-    node_url: Url,
+    client: minotari_node_wallet_client::http::Client,
 }
 
 impl DefaultHttpClientFactory {
     pub fn new(node_url: Url) -> Self {
-        Self { node_url }
+        Self {
+            client: minotari_node_wallet_client::http::Client::new(node_url),
+        }
     }
 }
 
 impl HttpClientFactory for DefaultHttpClientFactory {
     type Client = minotari_node_wallet_client::http::Client;
 
     fn create_http_client(&self) -> Self::Client {
-        minotari_node_wallet_client::http::Client::new(self.node_url.clone())
+        self.client.clone()
     }
 }

(Assumes the upstream client is Clone.)

base_layer/wallet_ffi/src/lib.rs (2)

10030-10037: Minor: unnecessary to_string() and inconsistent capitalization in error message

format!("Url is not valid: {}", e.to_string()) creates an extra allocation; the Display impl is already enough. Also prefer “URL” to keep messages consistent.

-                    "Url is not valid: {}",
-                    e.to_string()
+                    "URL is not valid: {}",
+                    e

7010-7015: Shadowing makes intent opaque

The local http_base_node (Option) shadows the raw pointer argument of the same name. While legal, it hampers readability and increases the chance of accidental misuse. Consider renaming to http_base_node_opt or similar.

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

528-530: Typo: offfset_pub_key

Minor readability nit – triple-f in the variable name.

-let offfset_pub_key = RistrettoPublicKey::from_canonical_bytes(&output.sender_offset_public_key)
+let offset_pub_key = RistrettoPublicKey::from_canonical_bytes(&output.sender_offset_public_key)

542-546: Timing metrics are misleading

one_sided_time is measured immediately after resetting start, so it is
effectively always ≈ 0 ms. Either remove the second measurement or place it
around an actual workload.

📜 Review details

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

📥 Commits

Reviewing files that changed from the base of the PR and between 57384d0 and 1306138.

📒 Files selected for processing (11)
  • applications/minotari_console_wallet/src/recovery.rs (4 hunks)
  • base_layer/core/src/base_node/rpc/models/get_tip_info.rs (1 hunks)
  • base_layer/core/src/base_node/rpc/models/sync_utxos_by_block.rs (1 hunks)
  • base_layer/wallet/src/utxo_scanner_service/initializer.rs (4 hunks)
  • base_layer/wallet/src/utxo_scanner_service/service.rs (3 hunks)
  • base_layer/wallet/src/utxo_scanner_service/utxo_scanner_task.rs (14 hunks)
  • base_layer/wallet/src/utxo_scanner_service/uxto_scanner_service_builder.rs (6 hunks)
  • base_layer/wallet/tests/support/base_node_http_service_mock.rs (1 hunks)
  • base_layer/wallet/tests/support/mod.rs (1 hunks)
  • base_layer/wallet/tests/utxo_scanner/mod.rs (13 hunks)
  • base_layer/wallet_ffi/src/lib.rs (37 hunks)
✅ Files skipped from review due to trivial changes (1)
  • base_layer/wallet/tests/support/mod.rs
🚧 Files skipped from review as they are similar to previous changes (4)
  • base_layer/core/src/base_node/rpc/models/sync_utxos_by_block.rs
  • base_layer/core/src/base_node/rpc/models/get_tip_info.rs
  • applications/minotari_console_wallet/src/recovery.rs
  • base_layer/wallet/src/utxo_scanner_service/uxto_scanner_service_builder.rs
⏰ Context from checks skipped due to timeout of 90000ms (1)
  • GitHub Check: ci
🔇 Additional comments (2)
base_layer/wallet/tests/support/base_node_http_service_mock.rs (1)

108-129: ⚠️ Potential issue

Return an explicit error when header hash is unknown

get_utxos_by_block silently succeeds with an empty vector if neither the block nor the header exists.
Tests depending on a failure signal will pass unintentionally.

-        let header = state.blocks.values().find(|h| h.hash().to_vec() == header_hash);
-        let res = if let Some(header) = header {
+        let header = state.blocks.values().find(|h| h.hash().to_vec() == header_hash);
+        let res = if let Some(header) = header {
             ...
         } else {
-            return Err(Error::msg("Header not found for the given hash"));
+            return Err(Error::msg("Unknown header hash"));
         };

Likely an incorrect or invalid review comment.

base_layer/wallet_ffi/src/lib.rs (1)

6748-6751: Update C-header / documentation for the new parameter

wallet_create now requires an http_base_node parameter. Ensure the exported C header (wallet_ffi.h) and accompanying docs/examples reflect this breaking change so downstream integrators don’t segfault from mismatched call-sites.

Comment on lines +131 to +199
async fn sync_utxos_by_block(
&self,
start_header_hash: Vec<u8>,
end_header_hash: Vec<u8>,
shutdown: ShutdownSignal,
) -> Result<mpsc::Receiver<Result<SyncUtxosByBlockResponse, Error>>, Error> {
let (tx, rx) = mpsc::channel(100);
let state2 = self.state.read().await;

let start_height = state2
.blocks
.values()
.find(|b| b.hash().to_vec() == start_header_hash)
.map_or(0, |b| b.height);

let end_height = state2
.blocks
.values()
.find(|b| b.hash().to_vec() == end_header_hash)
.map_or(0, |b| b.height);
let state = self.state.clone();
tokio::spawn(async move {
let state = state.read().await;
let mut current_height = 0;
let mut blocks = vec![];
let page_size = 5;

for height in start_height..=end_height {
if shutdown.is_triggered() {
break;
}
if let Some(ub) = state.utxos_by_block.get(&height) {
let block_header = state.blocks.get(&height).cloned();
if let Some(header) = block_header {
blocks.push(BlockUtxoInfo {
header_hash: header.hash().to_vec(),
height: header.height,
outputs: ub
.utxos
.iter()
.map(|o| models::MinimalUtxoSyncInfo {
output_hash: o.hash().to_vec(),
commitment: o.commitment.as_bytes().to_vec(),
encrypted_data: o.encrypted_data.to_byte_vec(),
sender_offset_public_key: o.sender_offset_public_key.to_vec(),
})
.collect(),
mined_timestamp: header.timestamp.as_u64(),
});
current_height = height;
}
}
if blocks.len() >= page_size || height == end_height {
let has_next_page = height < end_height;
let response = SyncUtxosByBlockResponse {
blocks: blocks.clone(),
has_next_page,
};
blocks.clear();

if tx.send(Ok(response)).await.is_err() {
break; // Channel closed
}
}
}
});

Ok(rx)
}
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

sync_utxos_by_block silently falls back to height 0

When the supplied start/end hashes aren’t found, start_height/end_height default to 0, which can mask test setup
errors. Prefer early validation:

-let start_height = ...map_or(0, |b| b.height);
-let end_height   = ...map_or(0, |b| b.height);
+let start_height = ...ok_or_else(|| Error::msg("start_header_hash not found"))?.height;
+let end_height   = ...ok_or_else(|| Error::msg("end_header_hash not found"))?.height;
📝 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
async fn sync_utxos_by_block(
&self,
start_header_hash: Vec<u8>,
end_header_hash: Vec<u8>,
shutdown: ShutdownSignal,
) -> Result<mpsc::Receiver<Result<SyncUtxosByBlockResponse, Error>>, Error> {
let (tx, rx) = mpsc::channel(100);
let state2 = self.state.read().await;
let start_height = state2
.blocks
.values()
.find(|b| b.hash().to_vec() == start_header_hash)
.map_or(0, |b| b.height);
let end_height = state2
.blocks
.values()
.find(|b| b.hash().to_vec() == end_header_hash)
.map_or(0, |b| b.height);
let state = self.state.clone();
tokio::spawn(async move {
let state = state.read().await;
let mut current_height = 0;
let mut blocks = vec![];
let page_size = 5;
for height in start_height..=end_height {
if shutdown.is_triggered() {
break;
}
if let Some(ub) = state.utxos_by_block.get(&height) {
let block_header = state.blocks.get(&height).cloned();
if let Some(header) = block_header {
blocks.push(BlockUtxoInfo {
header_hash: header.hash().to_vec(),
height: header.height,
outputs: ub
.utxos
.iter()
.map(|o| models::MinimalUtxoSyncInfo {
output_hash: o.hash().to_vec(),
commitment: o.commitment.as_bytes().to_vec(),
encrypted_data: o.encrypted_data.to_byte_vec(),
sender_offset_public_key: o.sender_offset_public_key.to_vec(),
})
.collect(),
mined_timestamp: header.timestamp.as_u64(),
});
current_height = height;
}
}
if blocks.len() >= page_size || height == end_height {
let has_next_page = height < end_height;
let response = SyncUtxosByBlockResponse {
blocks: blocks.clone(),
has_next_page,
};
blocks.clear();
if tx.send(Ok(response)).await.is_err() {
break; // Channel closed
}
}
}
});
Ok(rx)
}
async fn sync_utxos_by_block(
&self,
start_header_hash: Vec<u8>,
end_header_hash: Vec<u8>,
shutdown: ShutdownSignal,
) -> Result<mpsc::Receiver<Result<SyncUtxosByBlockResponse, Error>>, Error> {
let (tx, rx) = mpsc::channel(100);
let state2 = self.state.read().await;
let start_height = state2
.blocks
.values()
.find(|b| b.hash().to_vec() == start_header_hash)
- .map_or(0, |b| b.height);
+ .ok_or_else(|| Error::msg("start_header_hash not found"))?
+ .height;
let end_height = state2
.blocks
.values()
.find(|b| b.hash().to_vec() == end_header_hash)
- .map_or(0, |b| b.height);
+ .ok_or_else(|| Error::msg("end_header_hash not found"))?
+ .height;
let state = self.state.clone();
tokio::spawn(async move {
let state = state.read().await;
let mut current_height = 0;
let mut blocks = vec![];
let page_size = 5;
for height in start_height..=end_height {
if shutdown.is_triggered() {
break;
}
if let Some(ub) = state.utxos_by_block.get(&height) {
let block_header = state.blocks.get(&height).cloned();
if let Some(header) = block_header {
blocks.push(BlockUtxoInfo {
header_hash: header.hash().to_vec(),
height: header.height,
outputs: ub
.utxos
.iter()
.map(|o| models::MinimalUtxoSyncInfo {
output_hash: o.hash().to_vec(),
commitment: o.commitment.as_bytes().to_vec(),
encrypted_data: o.encrypted_data.to_byte_vec(),
sender_offset_public_key: o.sender_offset_public_key.to_vec(),
})
.collect(),
mined_timestamp: header.timestamp.as_u64(),
});
current_height = height;
}
}
if blocks.len() >= page_size || height == end_height {
let has_next_page = height < end_height;
let response = SyncUtxosByBlockResponse {
blocks: blocks.clone(),
has_next_page,
};
blocks.clear();
if tx.send(Ok(response)).await.is_err() {
break; // Channel closed
}
}
}
});
Ok(rx)
}
🤖 Prompt for AI Agents
In base_layer/wallet/tests/support/base_node_http_service_mock.rs around lines
131 to 199, the function sync_utxos_by_block defaults start_height and
end_height to 0 when the provided start or end header hashes are not found,
which can hide test setup errors. Modify the code to validate that both
start_height and end_height are found before proceeding; if either is missing,
return an error early instead of defaulting to 0. This ensures that invalid
input is caught promptly and does not cause silent fallback behavior.

Comment on lines +13994 to +13995
let http_base_node_address: *const c_char =
CString::new("http://127.0.1:2222").unwrap().into_raw() as *const c_char;
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

Invalid test URL – missing octet

"http://127.0.1:2222" is not a valid dotted-quad IPv4 address and will fail Url::parse, contradicting the positive-path test intent.

-                CString::new("http://127.0.1:2222").unwrap().into_raw() as *const c_char;
+                CString::new("http://127.0.0.1:2222").unwrap().into_raw() as *const c_char;
📝 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
let http_base_node_address: *const c_char =
CString::new("http://127.0.1:2222").unwrap().into_raw() as *const c_char;
let http_base_node_address: *const c_char =
CString::new("http://127.0.0.1:2222").unwrap().into_raw() as *const c_char;
🤖 Prompt for AI Agents
In base_layer/wallet_ffi/src/lib.rs around lines 13994 to 13995, the test URL
"http://127.0.1:2222" is invalid due to a missing octet in the IPv4 address.
Replace the URL with a valid IPv4 address such as "http://127.0.0.1:2222" to
ensure the URL parses correctly and the positive-path test behaves as intended.

use log::*;
use minotari_node_wallet_client::{http, BaseNodeWalletClient};
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

Remove unused http import to avoid compilation warnings/errors

http is imported but never used in this file. rustc will emit a warning that may be escalated to an error by #![deny(unused_imports)].

-use minotari_node_wallet_client::{http, BaseNodeWalletClient};
+use minotari_node_wallet_client::BaseNodeWalletClient;
📝 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
use minotari_node_wallet_client::{http, BaseNodeWalletClient};
-use minotari_node_wallet_client::{http, BaseNodeWalletClient};
+use minotari_node_wallet_client::BaseNodeWalletClient;
🤖 Prompt for AI Agents
In base_layer/wallet/src/utxo_scanner_service/utxo_scanner_task.rs at line 31,
remove the unused import `http` from the `use
minotari_node_wallet_client::{http, BaseNodeWalletClient};` statement to prevent
compilation warnings or errors caused by unused imports.

Comment on lines +424 to +427
let current_header_hash = response.header_hash;
let mined_timestamp = DateTime::<Utc>::from_timestamp(response.mined_timestamp as i64, 0)
.unwrap_or(DateTime::<Utc>::MIN_UTC);
let outputs = response.outputs;
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

Compilation failure: misuse of chrono API

DateTime::<Utc>::from_timestamp() returns an Option<DateTime<Utc>>
beginning with chrono 0.4.23. Calling .unwrap_or(...) on the result of
from_timestamp therefore requires an unwrap_or on the Option, but here
it is applied to the DateTime value, which doesn’t compile.

-let mined_timestamp = DateTime::<Utc>::from_timestamp(response.mined_timestamp as i64, 0)
-    .unwrap_or(DateTime::<Utc>::MIN_UTC);
+let mined_timestamp = Utc
+    .timestamp_opt(response.mined_timestamp as i64, 0)
+    .single()
+    .unwrap_or(DateTime::<Utc>::MIN_UTC);
📝 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
let current_header_hash = response.header_hash;
let mined_timestamp = DateTime::<Utc>::from_timestamp(response.mined_timestamp as i64, 0)
.unwrap_or(DateTime::<Utc>::MIN_UTC);
let outputs = response.outputs;
let current_header_hash = response.header_hash;
- let mined_timestamp = DateTime::<Utc>::from_timestamp(response.mined_timestamp as i64, 0)
- .unwrap_or(DateTime::<Utc>::MIN_UTC);
+ let mined_timestamp = Utc
+ .timestamp_opt(response.mined_timestamp as i64, 0)
+ .single()
+ .unwrap_or(DateTime::<Utc>::MIN_UTC);
let outputs = response.outputs;
🤖 Prompt for AI Agents
In base_layer/wallet/src/utxo_scanner_service/utxo_scanner_task.rs around lines
424 to 427, the code incorrectly calls unwrap_or on the result of
DateTime::<Utc>::from_timestamp(), which now returns an Option<DateTime<Utc>>.
To fix this, apply unwrap_or on the Option returned by from_timestamp, providing
DateTime::<Utc>::MIN_UTC as the default value inside unwrap_or to handle the
None case properly.

Comment on lines +183 to +195
let mut height = last_scanned_block.height;
let mut next_header;
// Keep going backwards until we find a header that is known to the base node
loop {
next_header = wallet_service_client
.get_header_by_height(last_scanned_block.height + 1)
.await?;
if !next_header.is_none() {
break;
}
height = height.saturating_sub(1);
}
let next_header = next_header.unwrap();
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

Infinite-loop risk: height variable is ignored

The intention is to walk backwards until a header is found, but the call
still uses last_scanned_block.height + 1 on every iteration while height
is mutated but never read. If the next header is unknown the loop never
terminates.

-            let mut height = last_scanned_block.height;
+            let mut height = last_scanned_block.height;

             // Keep going backwards until we find a header that is known to the base node
             loop {
-                next_header = wallet_service_client
-                    .get_header_by_height(last_scanned_block.height + 1)
+                next_header = wallet_service_client
+                    .get_header_by_height(height + 1)
                     .await?;
                 if !next_header.is_none() {
                     break;
                 }
-                height = height.saturating_sub(1);
+                if height == 0 {
+                    return Err(anyhow!(
+                        "Could not find a known header—reached genesis without success"
+                    ));
+                }
+                height = height.saturating_sub(1);
             }
📝 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
let mut height = last_scanned_block.height;
let mut next_header;
// Keep going backwards until we find a header that is known to the base node
loop {
next_header = wallet_service_client
.get_header_by_height(last_scanned_block.height + 1)
.await?;
if !next_header.is_none() {
break;
}
height = height.saturating_sub(1);
}
let next_header = next_header.unwrap();
let mut height = last_scanned_block.height;
let mut next_header;
// Keep going backwards until we find a header that is known to the base node
loop {
next_header = wallet_service_client
.get_header_by_height(height + 1)
.await?;
if !next_header.is_none() {
break;
}
if height == 0 {
return Err(anyhow!(
"Could not find a known header—reached genesis without success"
));
}
height = height.saturating_sub(1);
}
let next_header = next_header.unwrap();
🤖 Prompt for AI Agents
In base_layer/wallet/src/utxo_scanner_service/utxo_scanner_task.rs around lines
183 to 195, the loop intended to find a known header walks backwards by
decrementing the height variable, but the call to get_header_by_height always
uses last_scanned_block.height + 1 instead of the updated height. To fix this,
replace last_scanned_block.height + 1 with the mutable height variable in the
get_header_by_height call so the loop correctly queries decreasing heights and
terminates when a known header is found.

Copy link
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: 0

♻️ Duplicate comments (2)
base_layer/wallet_ffi/src/lib.rs (2)

14231-14233: Invalid IPv4 address in test fixture (repeat of earlier feedback)

"http://127.0.1:2222" is still present; the missing octet makes it an invalid dotted-quad and Url::parse will fail on the “happy-path” test.

This was flagged previously – please update to 127.0.0.1 (or another valid host).


6912-6920: ⚠️ Potential issue

expect will abort the host application across the FFI boundary

Calling .expect() on the UTF-8 conversion is unsafe here; invalid data from C will trigger an unwind/panic that crosses the FFI boundary and aborts the process.

-        let base_node = CStr::from_ptr(http_base_node)
-            .to_str()
-            .expect("A non-null base node should be able to be converted to string")
-            .to_owned();
-        Some(base_node)
+        match CStr::from_ptr(http_base_node).to_str() {
+            Ok(s) => Some(s.to_owned()),
+            Err(_) => {
+                *error_out = LibWalletError::from(
+                    InterfaceError::PointerError("http_base_node contains invalid UTF-8".to_string())
+                ).code;
+                return ptr::null_mut();
+            }
+        }
🧹 Nitpick comments (1)
base_layer/wallet_ffi/src/lib.rs (1)

10186-10198: Minor: fix error-string casing

Nit, but the error string says “Url is not valid”. Elsewhere in the file you changed similar messages to “URL”. Keeping it consistent helps grep-ability.

-                        "Url is not valid: {}",
+                        "URL is not valid: {}",
📜 Review details

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

📥 Commits

Reviewing files that changed from the base of the PR and between fc6ef6c and a8b8ad4.

⛔ Files ignored due to path filters (1)
  • Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (14)
  • Cargo.toml (2 hunks)
  • applications/minotari_console_wallet/src/automation/commands.rs (4 hunks)
  • applications/minotari_console_wallet/src/init/mod.rs (2 hunks)
  • applications/minotari_console_wallet/src/wallet_modes.rs (1 hunks)
  • base_layer/common_types/Cargo.toml (1 hunks)
  • base_layer/core/Cargo.toml (1 hunks)
  • base_layer/core/src/transactions/transaction_protocol/sender.rs (1 hunks)
  • base_layer/wallet/Cargo.toml (2 hunks)
  • base_layer/wallet/src/output_manager_service/service.rs (1 hunks)
  • base_layer/wallet/src/transaction_service/service.rs (1 hunks)
  • base_layer/wallet/tests/transaction_service_tests/service.rs (4 hunks)
  • base_layer/wallet_ffi/src/lib.rs (37 hunks)
  • base_layer/wallet_ffi/wallet.h (2 hunks)
  • supply-chain/config.toml (7 hunks)
🚧 Files skipped from review as they are similar to previous changes (13)
  • base_layer/core/Cargo.toml
  • base_layer/wallet/Cargo.toml
  • base_layer/common_types/Cargo.toml
  • base_layer/core/src/transactions/transaction_protocol/sender.rs
  • base_layer/wallet/src/transaction_service/service.rs
  • Cargo.toml
  • base_layer/wallet/src/output_manager_service/service.rs
  • applications/minotari_console_wallet/src/wallet_modes.rs
  • applications/minotari_console_wallet/src/init/mod.rs
  • supply-chain/config.toml
  • applications/minotari_console_wallet/src/automation/commands.rs
  • base_layer/wallet/tests/transaction_service_tests/service.rs
  • base_layer/wallet_ffi/wallet.h
⏰ Context from checks skipped due to timeout of 90000ms (1)
  • GitHub Check: ci
🔇 Additional comments (1)
base_layer/wallet_ffi/src/lib.rs (1)

82-87: Possible unused import – WalletConnectivityInterface

connectivity_service::WalletConnectivityInterface was added but I can’t find any usage in the added hunks.
If it’s indeed unused the crate will fail to compile with unused import warnings (denied by #![deny(warnings)] in this crate).

Please run cargo check --all-features to confirm whether the import is required and remove it if not.

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