diff --git a/Cargo.toml b/Cargo.toml index 4fb5dc19d..33c2aaac8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -7,8 +7,6 @@ version = "0.40.0" [patch.crates-io] dashcore_hashes = { path = "hashes" } -# Use fixed version of elliptic-curve-tools with DefaultIsZeroes trait bound -elliptic-curve-tools = { git = "https://github.com/mikelodder7/elliptic-curve-tools", branch = "main" } [profile.release] # Default to unwinding for most crates diff --git a/dash-spv-ffi/FFI_API.md b/dash-spv-ffi/FFI_API.md index b3e2902bb..7f9121b23 100644 --- a/dash-spv-ffi/FFI_API.md +++ b/dash-spv-ffi/FFI_API.md @@ -4,7 +4,7 @@ This document provides a comprehensive reference for all FFI (Foreign Function I **Auto-generated**: This documentation is automatically generated from the source code. Do not edit manually. -**Total Functions**: 70 +**Total Functions**: 71 ## Table of Contents @@ -138,7 +138,7 @@ Functions: 2 ### Utility Functions -Functions: 18 +Functions: 19 | Function | Description | Module | |----------|-------------|--------| @@ -151,7 +151,7 @@ Functions: 18 | `dash_spv_ffi_client_get_stats` | Get current runtime statistics for the SPV client | client | | `dash_spv_ffi_client_get_tip_hash` | Get the current chain tip hash (32 bytes) if available | client | | `dash_spv_ffi_client_get_tip_height` | Get the current chain tip height (absolute) | client | -| `dash_spv_ffi_client_get_wallet_manager` | Get the wallet manager from the SPV client Returns an opaque pointer to FFIW... | client | +| `dash_spv_ffi_client_get_wallet_manager` | Get the wallet manager from the SPV client Returns a pointer to an `FFIWalle... | client | | `dash_spv_ffi_client_record_send` | Record that we attempted to send a transaction by its txid | client | | `dash_spv_ffi_client_rescan_blockchain` | Request a rescan of the blockchain from a given height (not yet implemented) | client | | `dash_spv_ffi_enable_test_mode` | No description | utils | @@ -160,6 +160,7 @@ Functions: 18 | `dash_spv_ffi_string_array_destroy` | Destroy an array of FFIString pointers (Vec<*mut FFIString>) and their contents | types | | `dash_spv_ffi_string_destroy` | No description | types | | `dash_spv_ffi_version` | No description | utils | +| `dash_spv_ffi_wallet_manager_free` | Release a wallet manager obtained from `dash_spv_ffi_client_get_wallet_manager` | client | ## Detailed Function Documentation @@ -1117,14 +1118,14 @@ Get the current chain tip height (absolute). # Safety - `client` must be a vali #### `dash_spv_ffi_client_get_wallet_manager` ```c -dash_spv_ffi_client_get_wallet_manager(client: *mut FFIDashSpvClient,) -> *mut c_void +dash_spv_ffi_client_get_wallet_manager(client: *mut FFIDashSpvClient,) -> *mut FFIWalletManager ``` **Description:** -Get the wallet manager from the SPV client Returns an opaque pointer to FFIWalletManager that contains a cloned Arc reference to the wallet manager. This allows direct interaction with the wallet manager without going through the client. # Safety The caller must ensure that: - The client pointer is valid - The returned pointer is freed using `wallet_manager_free` from key-wallet-ffi # Returns An opaque pointer (void*) to the wallet manager, or NULL if the client is not initialized. Swift should treat this as an OpaquePointer. Get a handle to the wallet manager owned by this client. # Safety - `client` must be a valid, non-null pointer. +Get the wallet manager from the SPV client Returns a pointer to an `FFIWalletManager` wrapper that clones the underlying `Arc>`. This allows direct interaction with the wallet manager without going back through the client for each call. # Safety The caller must ensure that: - The client pointer is valid - The returned pointer is released exactly once using `dash_spv_ffi_wallet_manager_free` # Returns A pointer to the wallet manager wrapper, or NULL if the client is not initialized. **Safety:** -The caller must ensure that: - The client pointer is valid - The returned pointer is freed using `wallet_manager_free` from key-wallet-ffi +The caller must ensure that: - The client pointer is valid - The returned pointer is released exactly once using `dash_spv_ffi_wallet_manager_free` **Module:** `client` @@ -1237,6 +1238,22 @@ dash_spv_ffi_version() -> *const c_char --- +#### `dash_spv_ffi_wallet_manager_free` + +```c +dash_spv_ffi_wallet_manager_free(manager: *mut FFIWalletManager) -> () +``` + +**Description:** +Release a wallet manager obtained from `dash_spv_ffi_client_get_wallet_manager`. This simply forwards to `wallet_manager_free` in key-wallet-ffi so that lifetime management is consistent between direct key-wallet usage and the SPV client pathway. # Safety - `manager` must either be null or a pointer previously returned by `dash_spv_ffi_client_get_wallet_manager`. + +**Safety:** +- `manager` must either be null or a pointer previously returned by `dash_spv_ffi_client_get_wallet_manager`. + +**Module:** `client` + +--- + ## Type Definitions ### Core Types @@ -1266,7 +1283,7 @@ dash_spv_ffi_version() -> *const c_char 2. **Cleanup Required**: All returned pointers must be freed using the appropriate `_destroy` function 3. **Thread Safety**: The SPV client is thread-safe 4. **Error Handling**: Check return codes and use `dash_spv_ffi_get_last_error()` for details -5. **Opaque Pointers**: `dash_spv_ffi_client_get_wallet_manager()` returns `void*` for Swift compatibility +5. **Shared Ownership**: `dash_spv_ffi_client_get_wallet_manager()` returns `FFIWalletManager*` that must be released with `dash_spv_ffi_wallet_manager_free()` ## Usage Examples @@ -1289,8 +1306,8 @@ if (result != 0) { // Sync to chain tip dash_spv_ffi_client_sync_to_tip(client, NULL, NULL); -// Get wallet manager (returns void* for Swift) -void* wallet_manager = dash_spv_ffi_client_get_wallet_manager(client); +// Get wallet manager (shares ownership with the client) +FFIWalletManager* wallet_manager = dash_spv_ffi_client_get_wallet_manager(client); // Clean up dash_spv_ffi_client_destroy(client); diff --git a/dash-spv-ffi/dash_spv_ffi.h b/dash-spv-ffi/dash_spv_ffi.h index b8169ea5c..bdbde3fbc 100644 --- a/dash-spv-ffi/dash_spv_ffi.h +++ b/dash-spv-ffi/dash_spv_ffi.h @@ -531,27 +531,35 @@ int32_t dash_spv_ffi_client_enable_mempool_tracking(struct FFIDashSpvClient *cli /** * Get the wallet manager from the SPV client * - * Returns an opaque pointer to FFIWalletManager that contains a cloned Arc reference to the wallet manager. - * This allows direct interaction with the wallet manager without going through the client. + * Returns a pointer to an `FFIWalletManager` wrapper that clones the underlying + * `Arc>`. This allows direct interaction with the wallet + * manager without going back through the client for each call. * * # Safety * * The caller must ensure that: * - The client pointer is valid - * - The returned pointer is freed using `wallet_manager_free` from key-wallet-ffi + * - The returned pointer is released exactly once using + * `dash_spv_ffi_wallet_manager_free` * * # Returns * - * An opaque pointer (void*) to the wallet manager, or NULL if the client is not initialized. - * Swift should treat this as an OpaquePointer. - * Get a handle to the wallet manager owned by this client. + * A pointer to the wallet manager wrapper, or NULL if the client is not initialized. + */ + FFIWalletManager *dash_spv_ffi_client_get_wallet_manager(struct FFIDashSpvClient *client) ; + +/** + * Release a wallet manager obtained from `dash_spv_ffi_client_get_wallet_manager`. + * + * This simply forwards to `wallet_manager_free` in key-wallet-ffi so that + * lifetime management is consistent between direct key-wallet usage and the + * SPV client pathway. * * # Safety - * - `client` must be a valid, non-null pointer. + * - `manager` must either be null or a pointer previously returned by + * `dash_spv_ffi_client_get_wallet_manager`. */ - -void *dash_spv_ffi_client_get_wallet_manager(struct FFIDashSpvClient *client) -; + void dash_spv_ffi_wallet_manager_free(FFIWalletManager *manager) ; struct FFIClientConfig *dash_spv_ffi_config_new(FFINetwork network) ; diff --git a/dash-spv-ffi/examples/wallet_manager_usage.rs b/dash-spv-ffi/examples/wallet_manager_usage.rs index baffeb79d..639396cf1 100644 --- a/dash-spv-ffi/examples/wallet_manager_usage.rs +++ b/dash-spv-ffi/examples/wallet_manager_usage.rs @@ -5,7 +5,7 @@ /// 2. No longer requires going through the client for each operation /// 3. Cleaner and more efficient access to wallet functionality use dash_spv_ffi::*; -use key_wallet_ffi::{wallet_manager_free, wallet_manager_wallet_count, FFIError}; +use key_wallet_ffi::{wallet_manager_wallet_count, FFIError}; fn main() { unsafe { @@ -21,22 +21,22 @@ fn main() { panic!("Failed to create client"); } - // Get the wallet manager - now returns void* for Swift compatibility - // This contains a cloned Arc to the wallet manager, allowing - // direct interaction without going through the client - let wallet_manager_ptr = dash_spv_ffi_client_get_wallet_manager(client); - if wallet_manager_ptr.is_null() { + // Get the wallet manager - this returns a strongly typed pointer that + // shares the Arc with the SPV client, allowing direct interaction + let wallet_manager = dash_spv_ffi_client_get_wallet_manager(client); + if wallet_manager.is_null() { panic!("Failed to get wallet manager"); } - // Cast back to FFIWalletManager for use - let wallet_manager = wallet_manager_ptr as *mut key_wallet_ffi::FFIWalletManager; // Now we can use the wallet manager directly // No need to go through client -> inner -> spv_client -> wallet() // Get the number of wallets (should be 0 initially) let mut error = std::mem::zeroed::(); - let wallet_count = wallet_manager_wallet_count(wallet_manager, &mut error); + let wallet_count = wallet_manager_wallet_count( + wallet_manager as *const key_wallet_ffi::FFIWalletManager, + &mut error, + ); println!("Number of wallets: {}", wallet_count); // Note: To get total balance, you would need to iterate through wallets @@ -66,7 +66,7 @@ fn main() { // Clean up // The wallet manager can now be independently destroyed // It maintains its own Arc reference to the underlying wallet - wallet_manager_free(wallet_manager); + dash_spv_ffi_wallet_manager_free(wallet_manager); dash_spv_ffi_client_destroy(client); dash_spv_ffi_config_destroy(config); diff --git a/dash-spv-ffi/include/dash_spv_ffi.h b/dash-spv-ffi/include/dash_spv_ffi.h index b8169ea5c..dae86e8a6 100644 --- a/dash-spv-ffi/include/dash_spv_ffi.h +++ b/dash-spv-ffi/include/dash_spv_ffi.h @@ -161,6 +161,17 @@ typedef struct FFIEventCallbacks { void *user_data; } FFIEventCallbacks; +/** + * Opaque handle to the wallet manager owned by the SPV client. + * + * This is intentionally zero-sized so it can be used purely as an FFI handle + * while still allowing Rust to cast to the underlying key-wallet manager + * implementation when necessary. + */ +typedef struct FFIWalletManager { + uint8_t _private[0]; +} FFIWalletManager; + /** * Handle for Core SDK that can be passed to Platform SDK */ @@ -531,27 +542,35 @@ int32_t dash_spv_ffi_client_enable_mempool_tracking(struct FFIDashSpvClient *cli /** * Get the wallet manager from the SPV client * - * Returns an opaque pointer to FFIWalletManager that contains a cloned Arc reference to the wallet manager. - * This allows direct interaction with the wallet manager without going through the client. + * Returns a pointer to an `FFIWalletManager` wrapper that clones the underlying + * `Arc>`. This allows direct interaction with the wallet + * manager without going back through the client for each call. * * # Safety * * The caller must ensure that: * - The client pointer is valid - * - The returned pointer is freed using `wallet_manager_free` from key-wallet-ffi + * - The returned pointer is released exactly once using + * `dash_spv_ffi_wallet_manager_free` * * # Returns * - * An opaque pointer (void*) to the wallet manager, or NULL if the client is not initialized. - * Swift should treat this as an OpaquePointer. - * Get a handle to the wallet manager owned by this client. + * A pointer to the wallet manager wrapper, or NULL if the client is not initialized. + */ + struct FFIWalletManager *dash_spv_ffi_client_get_wallet_manager(struct FFIDashSpvClient *client) ; + +/** + * Release a wallet manager obtained from `dash_spv_ffi_client_get_wallet_manager`. + * + * This simply forwards to `wallet_manager_free` in key-wallet-ffi so that + * lifetime management is consistent between direct key-wallet usage and the + * SPV client pathway. * * # Safety - * - `client` must be a valid, non-null pointer. + * - `manager` must either be null or a pointer previously returned by + * `dash_spv_ffi_client_get_wallet_manager`. */ - -void *dash_spv_ffi_client_get_wallet_manager(struct FFIDashSpvClient *client) -; + void dash_spv_ffi_wallet_manager_free(struct FFIWalletManager *manager) ; struct FFIClientConfig *dash_spv_ffi_config_new(FFINetwork network) ; diff --git a/dash-spv-ffi/scripts/generate_ffi_docs.py b/dash-spv-ffi/scripts/generate_ffi_docs.py index 1c5d2b719..517e52b47 100644 --- a/dash-spv-ffi/scripts/generate_ffi_docs.py +++ b/dash-spv-ffi/scripts/generate_ffi_docs.py @@ -287,7 +287,10 @@ def generate_markdown(functions: List[FFIFunction]) -> str: md.append("2. **Cleanup Required**: All returned pointers must be freed using the appropriate `_destroy` function") md.append("3. **Thread Safety**: The SPV client is thread-safe") md.append("4. **Error Handling**: Check return codes and use `dash_spv_ffi_get_last_error()` for details") - md.append("5. **Opaque Pointers**: `dash_spv_ffi_client_get_wallet_manager()` returns `void*` for Swift compatibility") + md.append( + "5. **Shared Ownership**: `dash_spv_ffi_client_get_wallet_manager()` returns `FFIWalletManager*` " + "that must be released with `dash_spv_ffi_wallet_manager_free()`" + ) md.append("") # Usage Examples @@ -312,8 +315,8 @@ def generate_markdown(functions: List[FFIFunction]) -> str: md.append("// Sync to chain tip") md.append("dash_spv_ffi_client_sync_to_tip(client, NULL, NULL);") md.append("") - md.append("// Get wallet manager (returns void* for Swift)") - md.append("void* wallet_manager = dash_spv_ffi_client_get_wallet_manager(client);") + md.append("// Get wallet manager (shares ownership with the client)") + md.append("FFIWalletManager* wallet_manager = dash_spv_ffi_client_get_wallet_manager(client);") md.append("") md.append("// Clean up") md.append("dash_spv_ffi_client_destroy(client);") diff --git a/dash-spv-ffi/src/client.rs b/dash-spv-ffi/src/client.rs index 84b8a3979..a6858970a 100644 --- a/dash-spv-ffi/src/client.rs +++ b/dash-spv-ffi/src/client.rs @@ -1,9 +1,9 @@ use crate::{ null_check, set_last_error, FFIClientConfig, FFIDetailedSyncProgress, FFIErrorCode, - FFIEventCallbacks, FFIMempoolStrategy, FFISpvStats, FFISyncProgress, + FFIEventCallbacks, FFIMempoolStrategy, FFISpvStats, FFISyncProgress, FFIWalletManager, }; // Import wallet types from key-wallet-ffi -use key_wallet_ffi::FFIWalletManager; +use key_wallet_ffi::FFIWalletManager as KeyWalletFFIWalletManager; use dash_spv::storage::DiskStorageManager; use dash_spv::types::SyncStage; @@ -1485,27 +1485,24 @@ pub unsafe extern "C" fn dash_spv_ffi_client_record_send( /// Get the wallet manager from the SPV client /// -/// Returns an opaque pointer to FFIWalletManager that contains a cloned Arc reference to the wallet manager. -/// This allows direct interaction with the wallet manager without going through the client. +/// Returns a pointer to an `FFIWalletManager` wrapper that clones the underlying +/// `Arc>`. This allows direct interaction with the wallet +/// manager without going back through the client for each call. /// /// # Safety /// /// The caller must ensure that: /// - The client pointer is valid -/// - The returned pointer is freed using `wallet_manager_free` from key-wallet-ffi +/// - The returned pointer is released exactly once using +/// `dash_spv_ffi_wallet_manager_free` /// /// # Returns /// -/// An opaque pointer (void*) to the wallet manager, or NULL if the client is not initialized. -/// Swift should treat this as an OpaquePointer. -/// Get a handle to the wallet manager owned by this client. -/// -/// # Safety -/// - `client` must be a valid, non-null pointer. +/// A pointer to the wallet manager wrapper, or NULL if the client is not initialized. #[no_mangle] pub unsafe extern "C" fn dash_spv_ffi_client_get_wallet_manager( client: *mut FFIDashSpvClient, -) -> *mut c_void { +) -> *mut FFIWalletManager { null_check!(client, std::ptr::null_mut()); let client = &*client; @@ -1517,11 +1514,29 @@ pub unsafe extern "C" fn dash_spv_ffi_client_get_wallet_manager( let runtime = client.runtime.clone(); // Create the FFIWalletManager with the cloned Arc - let manager = FFIWalletManager::from_arc(wallet_arc, runtime); + let manager = KeyWalletFFIWalletManager::from_arc(wallet_arc, runtime); - Box::into_raw(Box::new(manager)) as *mut c_void + Box::into_raw(Box::new(manager)) as *mut FFIWalletManager } else { set_last_error("Client not initialized"); std::ptr::null_mut() } } + +/// Release a wallet manager obtained from `dash_spv_ffi_client_get_wallet_manager`. +/// +/// This simply forwards to `wallet_manager_free` in key-wallet-ffi so that +/// lifetime management is consistent between direct key-wallet usage and the +/// SPV client pathway. +/// +/// # Safety +/// - `manager` must either be null or a pointer previously returned by +/// `dash_spv_ffi_client_get_wallet_manager`. +#[no_mangle] +pub unsafe extern "C" fn dash_spv_ffi_wallet_manager_free(manager: *mut FFIWalletManager) { + if manager.is_null() { + return; + } + + key_wallet_ffi::wallet_manager::wallet_manager_free(manager as *mut KeyWalletFFIWalletManager); +} diff --git a/dash-spv-ffi/src/types.rs b/dash-spv-ffi/src/types.rs index d703e376a..a66a35211 100644 --- a/dash-spv-ffi/src/types.rs +++ b/dash-spv-ffi/src/types.rs @@ -4,6 +4,16 @@ use dash_spv::{ChainState, PeerInfo, SpvStats, SyncProgress}; use std::ffi::{CStr, CString}; use std::os::raw::{c_char, c_void}; +/// Opaque handle to the wallet manager owned by the SPV client. +/// +/// This is intentionally zero-sized so it can be used purely as an FFI handle +/// while still allowing Rust to cast to the underlying key-wallet manager +/// implementation when necessary. +#[repr(C)] +pub struct FFIWalletManager { + _private: [u8; 0], +} + #[repr(C)] pub struct FFIString { pub ptr: *mut c_char, diff --git a/dash-spv-ffi/tests/test_wallet_manager.rs b/dash-spv-ffi/tests/test_wallet_manager.rs index 570ffefce..531ae182f 100644 --- a/dash-spv-ffi/tests/test_wallet_manager.rs +++ b/dash-spv-ffi/tests/test_wallet_manager.rs @@ -1,10 +1,18 @@ #[cfg(test)] mod tests { use dash_spv_ffi::*; + use dashcore::Network; + use key_wallet::wallet::initialization::WalletAccountCreationOptions; + use key_wallet::wallet::managed_wallet_info::ManagedWalletInfo; use key_wallet_ffi::{ - wallet_manager::{wallet_manager_free, wallet_manager_wallet_count}, - FFIError, FFIWalletManager, + wallet_manager::{ + wallet_manager_free_wallet_ids, wallet_manager_get_wallet_ids, + wallet_manager_import_wallet_from_bytes, wallet_manager_wallet_count, + }, + FFIError, FFINetwork, FFIWalletManager, }; + use key_wallet_manager::wallet_manager::WalletManager; + use std::ffi::CStr; #[test] fn test_get_wallet_manager() { @@ -30,7 +38,86 @@ mod tests { assert_eq!(count, 0); // Clean up - wallet_manager_free(wallet_manager as *mut FFIWalletManager); + dash_spv_ffi_wallet_manager_free(wallet_manager); + dash_spv_ffi_client_destroy(client); + dash_spv_ffi_config_destroy(config); + } + } + + #[test] + fn test_wallet_manager_shared_via_client_imports_wallet() { + unsafe { + let config = dash_spv_ffi_config_testnet(); + assert!(!config.is_null()); + + let client = dash_spv_ffi_client_new(config); + assert!(!client.is_null()); + + let wallet_manager = dash_spv_ffi_client_get_wallet_manager(client); + assert!(!wallet_manager.is_null()); + let wallet_manager_ptr = wallet_manager as *mut key_wallet_ffi::FFIWalletManager; + + // Prepare a serialized wallet using the native manager so we can import it + let mut native_manager = WalletManager::::new(); + let (serialized_wallet, expected_wallet_id) = native_manager + .create_wallet_from_mnemonic_return_serialized_bytes( + "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about", + "", + &[Network::Dash], + None, + WalletAccountCreationOptions::Default, + false, + false, + ) + .expect("wallet serialization should succeed"); + + // Import the serialized wallet through the FFI pointer we retrieved from the client + let mut error = FFIError::success(); + let mut imported_wallet_id = [0u8; 32]; + let import_ok = wallet_manager_import_wallet_from_bytes( + wallet_manager_ptr, + serialized_wallet.as_ptr(), + serialized_wallet.len(), + imported_wallet_id.as_mut_ptr(), + &mut error as *mut FFIError, + ); + assert!(import_ok, "import should succeed: {:?}", error); + assert_eq!(imported_wallet_id, expected_wallet_id); + + // Fetch wallet IDs through FFI to confirm the manager sees the new wallet + let mut ids_ptr: *mut u8 = std::ptr::null_mut(); + let mut id_count: usize = 0; + let ids_ok = wallet_manager_get_wallet_ids( + wallet_manager_ptr as *const FFIWalletManager, + &mut ids_ptr, + &mut id_count, + &mut error as *mut FFIError, + ); + assert!(ids_ok, "get_wallet_ids should succeed: {:?}", error); + assert_eq!(id_count, 1); + assert!(!ids_ptr.is_null()); + + let ids_slice = std::slice::from_raw_parts(ids_ptr, id_count * 32); + assert_eq!(&ids_slice[..32], &expected_wallet_id); + wallet_manager_free_wallet_ids(ids_ptr, id_count); + + // Call the describe helper through FFI to ensure the shared instance reports correctly + let mut description_error = FFIError::success(); + let description_ptr = key_wallet_ffi::wallet_manager_describe( + wallet_manager_ptr as *const FFIWalletManager, + FFINetwork::Dash, + &mut description_error as *mut FFIError, + ); + assert!(!description_ptr.is_null(), "describe should succeed: {:?}", description_error); + let description = CStr::from_ptr(description_ptr).to_string_lossy().into_owned(); + key_wallet_ffi::wallet_manager_free_string(description_ptr); + assert!( + description.contains("WalletManager: 1 wallet"), + "description should mention the imported wallet, got: {}", + description + ); + + dash_spv_ffi_wallet_manager_free(wallet_manager); dash_spv_ffi_client_destroy(client); dash_spv_ffi_config_destroy(config); } diff --git a/dash-spv/Cargo.toml b/dash-spv/Cargo.toml index 493dee200..38797e1d7 100644 --- a/dash-spv/Cargo.toml +++ b/dash-spv/Cargo.toml @@ -16,7 +16,7 @@ key-wallet = { path = "../key-wallet" } key-wallet-manager = { path = "../key-wallet-manager" } # BLS signatures -blsful = { git = "https://github.com/dashpay/agora-blsful", rev = "be108b2cf6ac64eedbe04f91c63731533c8956bc" } +blsful = { git = "https://github.com/dashpay/agora-blsful", rev = "0c34a7a488a0bd1c9a9a2196e793b303ad35c900" } # CLI clap = { version = "4.0", features = ["derive"] } diff --git a/dash-spv/src/client/block_processor.rs b/dash-spv/src/client/block_processor.rs index fe7129905..56b00d488 100644 --- a/dash-spv/src/client/block_processor.rs +++ b/dash-spv/src/client/block_processor.rs @@ -179,17 +179,21 @@ impl String { + "MockWallet (test implementation)".to_string() + } } fn create_test_block(network: Network) -> Block { @@ -236,6 +240,10 @@ mod tests { // Always return false - filter doesn't match false } + + async fn describe(&self, _network: Network) -> String { + "NonMatchingWallet (test implementation)".to_string() + } } let (task_tx, task_rx) = mpsc::unbounded_channel(); diff --git a/dash-spv/src/client/mod.rs b/dash-spv/src/client/mod.rs index ada0831d0..47b46e261 100644 --- a/dash-spv/src/client/mod.rs +++ b/dash-spv/src/client/mod.rs @@ -119,6 +119,42 @@ impl< storage.clear_sync_state().await.map_err(SpvError::Storage) } + /// Clear all stored filter headers and compact filters while keeping other data intact. + pub async fn clear_filters(&mut self) -> Result<()> { + { + let mut storage = self.storage.lock().await; + storage.clear_filters().await.map_err(SpvError::Storage)?; + } + + // Reset in-memory chain state for filters + { + let mut state = self.state.write().await; + state.filter_headers.clear(); + state.current_filter_tip = None; + } + + // Reset filter sync manager tracking + self.sync_manager.filter_sync_mut().clear_filter_state().await; + + // Reset filter-related statistics + let received_heights = { + let stats = self.stats.read().await; + stats.received_filter_heights.clone() + }; + + { + let mut stats = self.stats.write().await; + stats.filter_headers_downloaded = 0; + stats.filter_height = 0; + stats.filters_downloaded = 0; + stats.filters_received = 0; + } + + received_heights.lock().await.clear(); + + Ok(()) + } + /// Take the progress receiver for external consumption. pub fn take_progress_receiver( &mut self, @@ -2510,8 +2546,15 @@ mod message_handler_test; #[cfg(test)] mod tests { + use super::{ClientConfig, DashSpvClient}; + use crate::network::mock::MockNetworkManager; + use crate::storage::MemoryStorageManager; use crate::types::{MempoolState, UnconfirmedTransaction}; - use dashcore::{Amount, Transaction, TxOut}; + use dashcore::{Amount, Network, Transaction, TxOut}; + use key_wallet::wallet::initialization::WalletAccountCreationOptions; + use key_wallet::wallet::managed_wallet_info::ManagedWalletInfo; + use key_wallet_manager::wallet_interface::WalletInterface; + use key_wallet_manager::wallet_manager::WalletManager; use std::sync::Arc; use tokio::sync::RwLock; @@ -2521,6 +2564,63 @@ mod tests { // 2. Validation of transaction effects on addresses // 3. Edge cases like zero amounts and conflicting signs + #[tokio::test] + async fn client_exposes_shared_wallet_manager() { + let config = ClientConfig { + network: Network::Dash, + enable_filters: false, + enable_masternodes: false, + enable_mempool_tracking: false, + ..Default::default() + }; + + let network_manager = MockNetworkManager::new(); + let storage = MemoryStorageManager::new().await.expect("memory storage should initialize"); + let wallet = Arc::new(RwLock::new(WalletManager::::new())); + + let client = DashSpvClient::new(config, network_manager, storage, wallet) + .await + .expect("client construction must succeed"); + + let shared_wallet = client.wallet().clone(); + + { + let guard = shared_wallet.read().await; + assert_eq!(guard.wallet_count(), 0, "new managers start empty"); + } + + let mut temp_manager = WalletManager::::new(); + let (serialized_wallet, _wallet_id) = temp_manager + .create_wallet_from_mnemonic_return_serialized_bytes( + "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about", + "", + &[Network::Dash], + None, + WalletAccountCreationOptions::Default, + false, + false, + ) + .expect("wallet serialization should succeed"); + + { + let mut guard = shared_wallet.write().await; + guard + .import_wallet_from_bytes(&serialized_wallet) + .expect("importing serialized wallet should succeed"); + } + + let description = { + let guard = shared_wallet.read().await; + guard.describe(Network::Dash).await + }; + + assert!( + description.contains("WalletManager: 1 wallet"), + "description should capture imported wallet, got: {}", + description + ); + } + #[tokio::test] async fn test_get_mempool_balance_logic() { // Create a simple test scenario to validate the balance calculation logic diff --git a/dash-spv/src/storage/disk.rs b/dash-spv/src/storage/disk.rs index 9551bbca1..ac3fff3a5 100644 --- a/dash-spv/src/storage/disk.rs +++ b/dash-spv/src/storage/disk.rs @@ -1631,6 +1631,27 @@ impl StorageManager for DiskStorageManager { Ok(()) } + async fn clear_filters(&mut self) -> StorageResult<()> { + // Stop worker to prevent concurrent writes to filter directories + self.stop_worker().await; + + // Clear in-memory filter state + self.active_filter_segments.write().await.clear(); + *self.cached_filter_tip_height.write().await = None; + + // Remove filter headers and compact filter files + let filters_dir = self.base_path.join("filters"); + if filters_dir.exists() { + tokio::fs::remove_dir_all(&filters_dir).await?; + } + tokio::fs::create_dir_all(&filters_dir).await?; + + // Restart background worker for future operations + self.start_worker().await; + + Ok(()) + } + async fn stats(&self) -> StorageResult { let mut component_sizes = HashMap::new(); let mut total_size = 0u64; diff --git a/dash-spv/src/storage/memory.rs b/dash-spv/src/storage/memory.rs index bf3767034..dd0c8b946 100644 --- a/dash-spv/src/storage/memory.rs +++ b/dash-spv/src/storage/memory.rs @@ -307,6 +307,12 @@ impl StorageManager for MemoryStorageManager { Ok(()) } + async fn clear_filters(&mut self) -> StorageResult<()> { + self.filter_headers.clear(); + self.filters.clear(); + Ok(()) + } + async fn stats(&self) -> StorageResult { let mut component_sizes = HashMap::new(); diff --git a/dash-spv/src/storage/mod.rs b/dash-spv/src/storage/mod.rs index e4f144312..17a6ac7a1 100644 --- a/dash-spv/src/storage/mod.rs +++ b/dash-spv/src/storage/mod.rs @@ -154,6 +154,9 @@ pub trait StorageManager: Send + Sync { /// Clear all data. async fn clear(&mut self) -> StorageResult<()>; + /// Clear all filter headers and compact filters. + async fn clear_filters(&mut self) -> StorageResult<()>; + /// Get storage statistics. async fn stats(&self) -> StorageResult; diff --git a/dash-spv/src/sync/filters.rs b/dash-spv/src/sync/filters.rs index c8509028f..99f2ca876 100644 --- a/dash-spv/src/sync/filters.rs +++ b/dash-spv/src/sync/filters.rs @@ -3466,4 +3466,12 @@ impl StorageResult<()> { + if self.fail_on_write { + return Err(StorageError::WriteFailed("Mock write failure".to_string())); + } + Ok(()) + } + async fn stats(&self) -> StorageResult { Ok(dash_spv::storage::StorageStats { header_count: 0, diff --git a/dash-spv/tests/error_recovery_integration_test.rs b/dash-spv/tests/error_recovery_integration_test.rs index f7cd0dc98..218969ac3 100644 --- a/dash-spv/tests/error_recovery_integration_test.rs +++ b/dash-spv/tests/error_recovery_integration_test.rs @@ -655,6 +655,10 @@ impl StorageManager for MockStorageManager { Ok(()) } + async fn clear_filters(&mut self) -> dash_spv::error::StorageResult<()> { + Ok(()) + } + async fn stats(&self) -> dash_spv::error::StorageResult { Ok(dash_spv::storage::StorageStats { header_count: 0, diff --git a/dash/Cargo.toml b/dash/Cargo.toml index 30f357511..6dc4edc1b 100644 --- a/dash/Cargo.toml +++ b/dash/Cargo.toml @@ -63,7 +63,7 @@ anyhow = { version= "1.0" } hex = { version= "0.4" } bincode = { version= "=2.0.0-rc.3", optional = true } bincode_derive = { version= "=2.0.0-rc.3", optional = true } -blsful = { git = "https://github.com/dashpay/agora-blsful", rev = "be108b2cf6ac64eedbe04f91c63731533c8956bc", optional = true } +blsful = { git = "https://github.com/dashpay/agora-blsful", rev = "0c34a7a488a0bd1c9a9a2196e793b303ad35c900", optional = true } ed25519-dalek = { version = "2.1", features = ["rand_core"], optional = true } blake3 = "1.8.1" thiserror = "2" diff --git a/key-wallet-ffi/FFI_API.md b/key-wallet-ffi/FFI_API.md index 8a4f9aba6..b613851a6 100644 --- a/key-wallet-ffi/FFI_API.md +++ b/key-wallet-ffi/FFI_API.md @@ -4,7 +4,7 @@ This document provides a comprehensive reference for all FFI (Foreign Function I **Auto-generated**: This documentation is automatically generated from the source code. Do not edit manually. -**Total Functions**: 232 +**Total Functions**: 234 ## Table of Contents @@ -42,7 +42,7 @@ Functions: 3 ### Wallet Manager -Functions: 17 +Functions: 19 | Function | Description | Module | |----------|-------------|--------| @@ -51,8 +51,10 @@ Functions: 17 | `wallet_manager_add_wallet_from_mnemonic_with_options` | Add a wallet from mnemonic to the manager with options # Safety - `manager`... | wallet_manager | | `wallet_manager_create` | Create a new wallet manager | wallet_manager | | `wallet_manager_current_height` | Get current height for a network # Safety - `manager` must be a valid point... | wallet_manager | +| `wallet_manager_describe` | Describe the wallet manager for a given network and return a newly allocated ... | wallet_manager | | `wallet_manager_free` | Free wallet manager # Safety - `manager` must be a valid pointer to an FFIW... | wallet_manager | | `wallet_manager_free_addresses` | Free address array # Safety - `addresses` must be a valid pointer to an arr... | wallet_manager | +| `wallet_manager_free_string` | Free a string previously returned by wallet manager APIs | wallet_manager | | `wallet_manager_free_wallet_bytes` | No description | wallet_manager | | `wallet_manager_free_wallet_ids` | Free wallet IDs buffer # Safety - `wallet_ids` must be a valid pointer to a... | wallet_manager | | `wallet_manager_get_managed_wallet_info` | Get managed wallet info from the manager Returns a reference to the managed ... | wallet_manager | @@ -476,6 +478,22 @@ Get current height for a network # Safety - `manager` must be a valid pointer --- +#### `wallet_manager_describe` + +```c +wallet_manager_describe(manager: *const FFIWalletManager, network: crate::FFINetwork, error: *mut FFIError,) -> *mut c_char +``` + +**Description:** +Describe the wallet manager for a given network and return a newly allocated C string. # Safety - `manager` must be a valid pointer to an `FFIWalletManager` - Callers must free the returned string with `wallet_manager_free_string` + +**Safety:** +- `manager` must be a valid pointer to an `FFIWalletManager` - Callers must free the returned string with `wallet_manager_free_string` + +**Module:** `wallet_manager` + +--- + #### `wallet_manager_free` ```c @@ -508,6 +526,22 @@ Free address array # Safety - `addresses` must be a valid pointer to an array --- +#### `wallet_manager_free_string` + +```c +wallet_manager_free_string(value: *mut c_char) -> () +``` + +**Description:** +Free a string previously returned by wallet manager APIs. # Safety - `value` must be either null or a pointer obtained from `wallet_manager_describe` (or other wallet manager FFI helpers that specify this free function). - The pointer must not be used after this call returns. + +**Safety:** +- `value` must be either null or a pointer obtained from `wallet_manager_describe` (or other wallet manager FFI helpers that specify this free function). - The pointer must not be used after this call returns. + +**Module:** `wallet_manager` + +--- + #### `wallet_manager_free_wallet_bytes` ```c diff --git a/key-wallet-ffi/include/key_wallet_ffi.h b/key-wallet-ffi/include/key_wallet_ffi.h index ae0749d60..344fa14f8 100644 --- a/key-wallet-ffi/include/key_wallet_ffi.h +++ b/key-wallet-ffi/include/key_wallet_ffi.h @@ -3727,6 +3727,31 @@ FFIAccountResult wallet_add_account_with_string_xpub(FFIWallet *wallet, const char *xpub_string) ; +/* + Describe the wallet manager for a given network and return a newly + allocated C string. + + # Safety + - `manager` must be a valid pointer to an `FFIWalletManager` + - Callers must free the returned string with `wallet_manager_free_string` + */ + +char *wallet_manager_describe(const FFIWalletManager *manager, + FFINetwork network, + FFIError *error) +; + +/* + Free a string previously returned by wallet manager APIs. + + # Safety + - `value` must be either null or a pointer obtained from + `wallet_manager_describe` (or other wallet manager FFI helpers that + specify this free function). + - The pointer must not be used after this call returns. + */ + void wallet_manager_free_string(char *value) ; + /* Create a new wallet manager */ diff --git a/key-wallet-ffi/src/lib.rs b/key-wallet-ffi/src/lib.rs index 7fbb41b46..4488fda2c 100644 --- a/key-wallet-ffi/src/lib.rs +++ b/key-wallet-ffi/src/lib.rs @@ -34,9 +34,10 @@ pub use error::{FFIError, FFIErrorCode}; pub use types::{FFIBalance, FFINetwork, FFINetworks, FFIWallet}; pub use utxo::FFIUTXO; pub use wallet_manager::{ - wallet_manager_create, wallet_manager_free, wallet_manager_free_wallet_ids, - wallet_manager_get_wallet, wallet_manager_get_wallet_balance, wallet_manager_get_wallet_ids, - wallet_manager_wallet_count, FFIWalletManager, + wallet_manager_create, wallet_manager_describe, wallet_manager_free, + wallet_manager_free_string, wallet_manager_free_wallet_ids, wallet_manager_get_wallet, + wallet_manager_get_wallet_balance, wallet_manager_get_wallet_ids, wallet_manager_wallet_count, + FFIWalletManager, }; // ============================================================================ diff --git a/key-wallet-ffi/src/wallet_manager.rs b/key-wallet-ffi/src/wallet_manager.rs index cefeced72..4522083b8 100644 --- a/key-wallet-ffi/src/wallet_manager.rs +++ b/key-wallet-ffi/src/wallet_manager.rs @@ -19,6 +19,7 @@ use crate::types::FFINetworks; use crate::FFINetwork; use key_wallet::wallet::managed_wallet_info::ManagedWalletInfo; use key_wallet::Network; +use key_wallet_manager::wallet_interface::WalletInterface; use key_wallet_manager::wallet_manager::WalletManager; /// FFI wrapper for WalletManager @@ -44,6 +45,64 @@ impl FFIWalletManager { } } +/// Describe the wallet manager for a given network and return a newly +/// allocated C string. +/// +/// # Safety +/// - `manager` must be a valid pointer to an `FFIWalletManager` +/// - Callers must free the returned string with `wallet_manager_free_string` +#[no_mangle] +pub unsafe extern "C" fn wallet_manager_describe( + manager: *const FFIWalletManager, + network: crate::FFINetwork, + error: *mut FFIError, +) -> *mut c_char { + if manager.is_null() { + FFIError::set_error(error, FFIErrorCode::InvalidInput, "Null pointer provided".to_string()); + return ptr::null_mut(); + } + + let manager_ref = &*manager; + let runtime = manager_ref.runtime.clone(); + let manager_arc = manager_ref.manager.clone(); + + let description = runtime.block_on(async { + let guard = manager_arc.read().await; + guard.describe(network.into()).await + }); + + match CString::new(description) { + Ok(c_string) => { + FFIError::set_success(error); + c_string.into_raw() + } + Err(e) => { + FFIError::set_error( + error, + FFIErrorCode::InvalidState, + format!("Failed to create description string: {}", e), + ); + ptr::null_mut() + } + } +} + +/// Free a string previously returned by wallet manager APIs. +/// +/// # Safety +/// - `value` must be either null or a pointer obtained from +/// `wallet_manager_describe` (or other wallet manager FFI helpers that +/// specify this free function). +/// - The pointer must not be used after this call returns. +#[no_mangle] +pub unsafe extern "C" fn wallet_manager_free_string(value: *mut c_char) { + if value.is_null() { + return; + } + + drop(CString::from_raw(value)); +} + /// Create a new wallet manager #[no_mangle] pub extern "C" fn wallet_manager_create(error: *mut FFIError) -> *mut FFIWalletManager { diff --git a/key-wallet-manager/src/wallet_interface.rs b/key-wallet-manager/src/wallet_interface.rs index 43094eaa3..9f68c038e 100644 --- a/key-wallet-manager/src/wallet_interface.rs +++ b/key-wallet-manager/src/wallet_interface.rs @@ -2,6 +2,7 @@ //! //! This module defines the trait that SPV clients use to interact with wallets. +use alloc::string::String; use async_trait::async_trait; use dashcore::bip158::BlockFilter; use dashcore::prelude::CoreBlockHeight; @@ -49,4 +50,12 @@ pub trait WalletInterface: Send + Sync { async fn earliest_required_height(&self, _network: Network) -> Option { None } + + /// Provide a human-readable description of the wallet implementation. + /// + /// Implementations are encouraged to include high-level state such as the + /// number of managed wallets, networks, or tracked scripts. + async fn describe(&self, _network: Network) -> String { + "Wallet interface description unavailable".to_string() + } } diff --git a/key-wallet-manager/src/wallet_manager/process_block.rs b/key-wallet-manager/src/wallet_manager/process_block.rs index 0f18892d4..402efb562 100644 --- a/key-wallet-manager/src/wallet_manager/process_block.rs +++ b/key-wallet-manager/src/wallet_manager/process_block.rs @@ -1,6 +1,9 @@ use crate::wallet_interface::WalletInterface; use crate::{Network, WalletManager}; +use alloc::string::String; +use alloc::vec::Vec; use async_trait::async_trait; +use core::fmt::Write as _; use dashcore::bip158::BlockFilter; use dashcore::prelude::CoreBlockHeight; use dashcore::{Block, BlockHash, Transaction, Txid}; @@ -136,4 +139,28 @@ impl WalletInterface for WalletM // Return None if no wallets with known birth heights were found for this network earliest } + + async fn describe(&self, network: Network) -> String { + let wallet_count = self.wallet_infos.len(); + if wallet_count == 0 { + return format!("WalletManager: 0 wallets (network {})", network); + } + + let mut details = Vec::with_capacity(wallet_count); + for (wallet_id, info) in &self.wallet_infos { + let name = info.name().unwrap_or("unnamed"); + + let mut wallet_id_hex = String::with_capacity(wallet_id.len() * 2); + for byte in wallet_id { + let _ = write!(&mut wallet_id_hex, "{:02x}", byte); + } + + let script_count = info.monitored_addresses(network).len(); + let summary = format!("{} scripts", script_count); + + details.push(format!("{} ({}): {}", name, wallet_id_hex, summary)); + } + + format!("WalletManager: {} wallet(s) on {}\n{}", wallet_count, network, details.join("\n")) + } } diff --git a/swift-dash-core-sdk/Sources/DashSPVFFI/include/dash_spv_ffi.h b/swift-dash-core-sdk/Sources/DashSPVFFI/include/dash_spv_ffi.h index b8169ea5c..dae86e8a6 100644 --- a/swift-dash-core-sdk/Sources/DashSPVFFI/include/dash_spv_ffi.h +++ b/swift-dash-core-sdk/Sources/DashSPVFFI/include/dash_spv_ffi.h @@ -161,6 +161,17 @@ typedef struct FFIEventCallbacks { void *user_data; } FFIEventCallbacks; +/** + * Opaque handle to the wallet manager owned by the SPV client. + * + * This is intentionally zero-sized so it can be used purely as an FFI handle + * while still allowing Rust to cast to the underlying key-wallet manager + * implementation when necessary. + */ +typedef struct FFIWalletManager { + uint8_t _private[0]; +} FFIWalletManager; + /** * Handle for Core SDK that can be passed to Platform SDK */ @@ -531,27 +542,35 @@ int32_t dash_spv_ffi_client_enable_mempool_tracking(struct FFIDashSpvClient *cli /** * Get the wallet manager from the SPV client * - * Returns an opaque pointer to FFIWalletManager that contains a cloned Arc reference to the wallet manager. - * This allows direct interaction with the wallet manager without going through the client. + * Returns a pointer to an `FFIWalletManager` wrapper that clones the underlying + * `Arc>`. This allows direct interaction with the wallet + * manager without going back through the client for each call. * * # Safety * * The caller must ensure that: * - The client pointer is valid - * - The returned pointer is freed using `wallet_manager_free` from key-wallet-ffi + * - The returned pointer is released exactly once using + * `dash_spv_ffi_wallet_manager_free` * * # Returns * - * An opaque pointer (void*) to the wallet manager, or NULL if the client is not initialized. - * Swift should treat this as an OpaquePointer. - * Get a handle to the wallet manager owned by this client. + * A pointer to the wallet manager wrapper, or NULL if the client is not initialized. + */ + struct FFIWalletManager *dash_spv_ffi_client_get_wallet_manager(struct FFIDashSpvClient *client) ; + +/** + * Release a wallet manager obtained from `dash_spv_ffi_client_get_wallet_manager`. + * + * This simply forwards to `wallet_manager_free` in key-wallet-ffi so that + * lifetime management is consistent between direct key-wallet usage and the + * SPV client pathway. * * # Safety - * - `client` must be a valid, non-null pointer. + * - `manager` must either be null or a pointer previously returned by + * `dash_spv_ffi_client_get_wallet_manager`. */ - -void *dash_spv_ffi_client_get_wallet_manager(struct FFIDashSpvClient *client) -; + void dash_spv_ffi_wallet_manager_free(struct FFIWalletManager *manager) ; struct FFIClientConfig *dash_spv_ffi_config_new(FFINetwork network) ; diff --git a/swift-dash-core-sdk/Sources/KeyWalletFFI/include/key_wallet_ffi.h b/swift-dash-core-sdk/Sources/KeyWalletFFI/include/key_wallet_ffi.h index ae0749d60..344fa14f8 100644 --- a/swift-dash-core-sdk/Sources/KeyWalletFFI/include/key_wallet_ffi.h +++ b/swift-dash-core-sdk/Sources/KeyWalletFFI/include/key_wallet_ffi.h @@ -3727,6 +3727,31 @@ FFIAccountResult wallet_add_account_with_string_xpub(FFIWallet *wallet, const char *xpub_string) ; +/* + Describe the wallet manager for a given network and return a newly + allocated C string. + + # Safety + - `manager` must be a valid pointer to an `FFIWalletManager` + - Callers must free the returned string with `wallet_manager_free_string` + */ + +char *wallet_manager_describe(const FFIWalletManager *manager, + FFINetwork network, + FFIError *error) +; + +/* + Free a string previously returned by wallet manager APIs. + + # Safety + - `value` must be either null or a pointer obtained from + `wallet_manager_describe` (or other wallet manager FFI helpers that + specify this free function). + - The pointer must not be used after this call returns. + */ + void wallet_manager_free_string(char *value) ; + /* Create a new wallet manager */