diff --git a/dash-network/src/lib.rs b/dash-network/src/lib.rs index df03b0c85..23cab92b7 100644 --- a/dash-network/src/lib.rs +++ b/dash-network/src/lib.rs @@ -9,6 +9,7 @@ use std::fmt; #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] #[cfg_attr(feature = "serde", serde(rename_all = "lowercase"))] #[non_exhaustive] +#[repr(u8)] #[cfg_attr(feature = "bincode", derive(Encode, Decode))] pub enum Network { /// Classic Dash Core Payment Chain diff --git a/dash-spv-ffi/include/dash_spv_ffi.h b/dash-spv-ffi/include/dash_spv_ffi.h index 15c3a4442..decd0efb9 100644 --- a/dash-spv-ffi/include/dash_spv_ffi.h +++ b/dash-spv-ffi/include/dash_spv_ffi.h @@ -32,13 +32,13 @@ typedef enum FFIValidationMode { Full = 2, } FFIValidationMode; -typedef struct FFIClientConfig FFIClientConfig; - /** * FFIDashSpvClient structure */ typedef struct FFIDashSpvClient FFIDashSpvClient; +typedef ClientConfig FFIClientConfig; + typedef struct FFIString { char *ptr; uintptr_t length; @@ -100,6 +100,8 @@ typedef struct FFIArray { void *data; uintptr_t len; uintptr_t capacity; + uintptr_t elem_size; + uintptr_t elem_align; } FFIArray; typedef void (*BlockCallback)(uint32_t height, const uint8_t (*hash)[32], void *user_data); @@ -209,7 +211,7 @@ typedef struct FFIUnconfirmedTransaction { uintptr_t addresses_len; } FFIUnconfirmedTransaction; -struct FFIDashSpvClient *dash_spv_ffi_client_new(const struct FFIClientConfig *config); +struct FFIDashSpvClient *dash_spv_ffi_client_new(const FFIClientConfig *config); int32_t dash_spv_ffi_client_start(struct FFIDashSpvClient *client); @@ -315,10 +317,10 @@ bool dash_spv_ffi_client_is_filter_sync_available(struct FFIDashSpvClient *clien FFIBalance *dash_spv_ffi_client_get_address_balance(struct FFIDashSpvClient *client, const char *address); -struct FFIArray dash_spv_ffi_client_get_utxos(struct FFIDashSpvClient *client); +struct FFIArray *dash_spv_ffi_client_get_utxos(struct FFIDashSpvClient *client); -struct FFIArray dash_spv_ffi_client_get_utxos_for_address(struct FFIDashSpvClient *client, - const char *address); +struct FFIArray *dash_spv_ffi_client_get_utxos_for_address(struct FFIDashSpvClient *client, + const char *address); int32_t dash_spv_ffi_client_set_event_callbacks(struct FFIDashSpvClient *client, struct FFIEventCallbacks callbacks); @@ -337,8 +339,8 @@ int32_t dash_spv_ffi_client_watch_script(struct FFIDashSpvClient *client, const int32_t dash_spv_ffi_client_unwatch_script(struct FFIDashSpvClient *client, const char *script_hex); -struct FFIArray dash_spv_ffi_client_get_address_history(struct FFIDashSpvClient *client, - const char *address); +struct FFIArray *dash_spv_ffi_client_get_address_history(struct FFIDashSpvClient *client, + const char *address); struct FFITransaction *dash_spv_ffi_client_get_transaction(struct FFIDashSpvClient *client, const char *txid); @@ -346,9 +348,9 @@ struct FFITransaction *dash_spv_ffi_client_get_transaction(struct FFIDashSpvClie int32_t dash_spv_ffi_client_broadcast_transaction(struct FFIDashSpvClient *client, const char *tx_hex); -struct FFIArray dash_spv_ffi_client_get_watched_addresses(struct FFIDashSpvClient *client); +struct FFIArray *dash_spv_ffi_client_get_watched_addresses(struct FFIDashSpvClient *client); -struct FFIArray dash_spv_ffi_client_get_watched_scripts(struct FFIDashSpvClient *client); +struct FFIArray *dash_spv_ffi_client_get_watched_scripts(struct FFIDashSpvClient *client); FFIBalance *dash_spv_ffi_client_get_total_balance(struct FFIDashSpvClient *client); @@ -363,8 +365,8 @@ int32_t dash_spv_ffi_client_is_transaction_confirmed(struct FFIDashSpvClient *cl void dash_spv_ffi_transaction_destroy(struct FFITransaction *tx); -struct FFIArray dash_spv_ffi_client_get_address_utxos(struct FFIDashSpvClient *client, - const char *address); +struct FFIArray *dash_spv_ffi_client_get_address_utxos(struct FFIDashSpvClient *client, + const char *address); int32_t dash_spv_ffi_client_enable_mempool_tracking(struct FFIDashSpvClient *client, enum FFIMempoolStrategy strategy); @@ -378,57 +380,54 @@ int32_t dash_spv_ffi_client_record_send(struct FFIDashSpvClient *client, const c FFIBalance *dash_spv_ffi_client_get_mempool_balance(struct FFIDashSpvClient *client, const char *address); -struct FFIClientConfig *dash_spv_ffi_config_new(enum FFINetwork network); +FFIClientConfig *dash_spv_ffi_config_new(enum FFINetwork network); -struct FFIClientConfig *dash_spv_ffi_config_mainnet(void); +FFIClientConfig *dash_spv_ffi_config_mainnet(void); -struct FFIClientConfig *dash_spv_ffi_config_testnet(void); +FFIClientConfig *dash_spv_ffi_config_testnet(void); -int32_t dash_spv_ffi_config_set_data_dir(struct FFIClientConfig *config, const char *path); +int32_t dash_spv_ffi_config_set_data_dir(FFIClientConfig *config, const char *path); -int32_t dash_spv_ffi_config_set_validation_mode(struct FFIClientConfig *config, +int32_t dash_spv_ffi_config_set_validation_mode(FFIClientConfig *config, enum FFIValidationMode mode); -int32_t dash_spv_ffi_config_set_max_peers(struct FFIClientConfig *config, uint32_t max_peers); +int32_t dash_spv_ffi_config_set_max_peers(FFIClientConfig *config, uint32_t max_peers); -int32_t dash_spv_ffi_config_add_peer(struct FFIClientConfig *config, const char *addr); +int32_t dash_spv_ffi_config_add_peer(FFIClientConfig *config, const char *addr); -int32_t dash_spv_ffi_config_set_user_agent(struct FFIClientConfig *config, const char *user_agent); +int32_t dash_spv_ffi_config_set_user_agent(FFIClientConfig *config, const char *user_agent); -int32_t dash_spv_ffi_config_set_relay_transactions(struct FFIClientConfig *config, bool _relay); +int32_t dash_spv_ffi_config_set_relay_transactions(FFIClientConfig *config, bool _relay); -int32_t dash_spv_ffi_config_set_filter_load(struct FFIClientConfig *config, bool load_filters); +int32_t dash_spv_ffi_config_set_filter_load(FFIClientConfig *config, bool load_filters); -enum FFINetwork dash_spv_ffi_config_get_network(const struct FFIClientConfig *config); +enum FFINetwork dash_spv_ffi_config_get_network(const FFIClientConfig *config); -struct FFIString dash_spv_ffi_config_get_data_dir(const struct FFIClientConfig *config); +struct FFIString dash_spv_ffi_config_get_data_dir(const FFIClientConfig *config); -void dash_spv_ffi_config_destroy(struct FFIClientConfig *config); +void dash_spv_ffi_config_destroy(FFIClientConfig *config); -int32_t dash_spv_ffi_config_set_mempool_tracking(struct FFIClientConfig *config, bool enable); +int32_t dash_spv_ffi_config_set_mempool_tracking(FFIClientConfig *config, bool enable); -int32_t dash_spv_ffi_config_set_mempool_strategy(struct FFIClientConfig *config, +int32_t dash_spv_ffi_config_set_mempool_strategy(FFIClientConfig *config, enum FFIMempoolStrategy strategy); -int32_t dash_spv_ffi_config_set_max_mempool_transactions(struct FFIClientConfig *config, +int32_t dash_spv_ffi_config_set_max_mempool_transactions(FFIClientConfig *config, uint32_t max_transactions); -int32_t dash_spv_ffi_config_set_mempool_timeout(struct FFIClientConfig *config, - uint64_t timeout_secs); +int32_t dash_spv_ffi_config_set_mempool_timeout(FFIClientConfig *config, uint64_t timeout_secs); -int32_t dash_spv_ffi_config_set_fetch_mempool_transactions(struct FFIClientConfig *config, - bool fetch); +int32_t dash_spv_ffi_config_set_fetch_mempool_transactions(FFIClientConfig *config, bool fetch); -int32_t dash_spv_ffi_config_set_persist_mempool(struct FFIClientConfig *config, bool persist); +int32_t dash_spv_ffi_config_set_persist_mempool(FFIClientConfig *config, bool persist); -bool dash_spv_ffi_config_get_mempool_tracking(const struct FFIClientConfig *config); +bool dash_spv_ffi_config_get_mempool_tracking(const FFIClientConfig *config); -enum FFIMempoolStrategy dash_spv_ffi_config_get_mempool_strategy(const struct FFIClientConfig *config); +enum FFIMempoolStrategy dash_spv_ffi_config_get_mempool_strategy(const FFIClientConfig *config); -int32_t dash_spv_ffi_config_set_start_from_height(struct FFIClientConfig *config, uint32_t height); +int32_t dash_spv_ffi_config_set_start_from_height(FFIClientConfig *config, uint32_t height); -int32_t dash_spv_ffi_config_set_wallet_creation_time(struct FFIClientConfig *config, - uint32_t timestamp); +int32_t dash_spv_ffi_config_set_wallet_creation_time(FFIClientConfig *config, uint32_t timestamp); const char *dash_spv_ffi_get_last_error(void); @@ -490,6 +489,16 @@ void dash_spv_ffi_string_destroy(struct FFIString s); void dash_spv_ffi_array_destroy(struct FFIArray *arr); +/** + * Destroy an array of FFIString pointers (Vec<*mut FFIString>) and their contents. + * + * This function: + * - Iterates the array elements as pointers to FFIString and destroys each via dash_spv_ffi_string_destroy + * - Frees the underlying vector buffer stored in FFIArray + * - Does not free the FFIArray struct itself (safe for both stack- and heap-allocated structs) + */ +void dash_spv_ffi_string_array_destroy(struct FFIArray *arr); + /** * Destroys the raw transaction bytes allocated for an FFIUnconfirmedTransaction * diff --git a/dash-spv-ffi/peer_reputation.json b/dash-spv-ffi/peer_reputation.json deleted file mode 100644 index dfd7cb05e..000000000 --- a/dash-spv-ffi/peer_reputation.json +++ /dev/null @@ -1,79 +0,0 @@ -[ - [ - "34.214.48.68:19999", - { - "score": 0, - "ban_count": 0, - "positive_actions": 0, - "negative_actions": 0, - "connection_attempts": 1, - "successful_connections": 0 - } - ], - [ - "34.222.21.14:19999", - { - "score": 0, - "ban_count": 0, - "positive_actions": 0, - "negative_actions": 0, - "connection_attempts": 1, - "successful_connections": 0 - } - ], - [ - "34.220.134.30:19999", - { - "score": 0, - "ban_count": 0, - "positive_actions": 0, - "negative_actions": 0, - "connection_attempts": 1, - "successful_connections": 0 - } - ], - [ - "18.237.170.32:19999", - { - "score": 0, - "ban_count": 0, - "positive_actions": 0, - "negative_actions": 0, - "connection_attempts": 2, - "successful_connections": 0 - } - ], - [ - "34.210.84.163:19999", - { - "score": 0, - "ban_count": 0, - "positive_actions": 0, - "negative_actions": 0, - "connection_attempts": 1, - "successful_connections": 0 - } - ], - [ - "34.217.58.158:19999", - { - "score": 0, - "ban_count": 0, - "positive_actions": 0, - "negative_actions": 0, - "connection_attempts": 2, - "successful_connections": 0 - } - ], - [ - "34.210.26.195:19999", - { - "score": 0, - "ban_count": 0, - "positive_actions": 0, - "negative_actions": 0, - "connection_attempts": 1, - "successful_connections": 0 - } - ] -] \ No newline at end of file diff --git a/dash-spv-ffi/src/client.rs b/dash-spv-ffi/src/client.rs index cf7799e41..cdccffb91 100644 --- a/dash-spv-ffi/src/client.rs +++ b/dash-spv-ffi/src/client.rs @@ -1,6 +1,6 @@ use crate::{ null_check, set_last_error, FFIArray, FFIClientConfig, FFIDetailedSyncProgress, FFIErrorCode, - FFIEventCallbacks, FFIMempoolStrategy, FFISpvStats, FFIString, FFISyncProgress, FFITransaction, + FFIEventCallbacks, FFIMempoolStrategy, FFISpvStats, FFISyncProgress, FFITransaction, }; // Import wallet types from key-wallet-ffi use key_wallet_ffi::{FFIBalance, FFIUTXO as FFIUtxo}; @@ -613,23 +613,23 @@ pub unsafe extern "C" fn dash_spv_ffi_client_test_sync(client: *mut FFIDashSpvCl let result = client.runtime.block_on(async { let mut guard = client.inner.lock().unwrap(); if let Some(ref mut spv_client) = *guard { - println!("Starting test sync..."); + tracing::info!("Starting test sync..."); // Get initial height let start_height = match spv_client.sync_progress().await { Ok(progress) => progress.header_height, Err(e) => { - eprintln!("Failed to get initial height: {}", e); + tracing::error!("Failed to get initial height: {}", e); return Err(e); } }; - println!("Initial height: {}", start_height); + tracing::info!("Initial height: {}", start_height); // Start sync match spv_client.sync_to_tip().await { - Ok(_) => println!("Sync started successfully"), + Ok(_) => tracing::info!("Sync started successfully"), Err(e) => { - eprintln!("Failed to start sync: {}", e); + tracing::error!("Failed to start sync: {}", e); return Err(e); } } @@ -641,19 +641,19 @@ pub unsafe extern "C" fn dash_spv_ffi_client_test_sync(client: *mut FFIDashSpvCl let end_height = match spv_client.sync_progress().await { Ok(progress) => progress.header_height, Err(e) => { - eprintln!("Failed to get final height: {}", e); + tracing::error!("Failed to get final height: {}", e); return Err(e); } }; - println!("Final height: {}", end_height); + tracing::info!("Final height: {}", end_height); if end_height > start_height { - println!("✅ Sync working! Downloaded {} headers", end_height - start_height); + tracing::info!("✅ Sync working! Downloaded {} headers", end_height - start_height); Ok(()) } else { let msg = "No headers downloaded".to_string(); - eprintln!("❌ {}", msg); - Err(dash_spv::SpvError::Sync(dash_spv::SyncError::SyncFailed(msg))) + tracing::error!("❌ {}", msg); + Err(dash_spv::SpvError::Sync(dash_spv::SyncError::Network(msg))) } } else { Err(dash_spv::SpvError::Config("Client not initialized".to_string())) @@ -775,7 +775,6 @@ pub unsafe extern "C" fn dash_spv_ffi_client_sync_to_tip_with_progress( // Spawn sync task in a separate thread with safe callback access let runtime_handle = runtime.handle().clone(); let sync_callbacks_clone = sync_callbacks.clone(); - let shutdown_signal_clone = client.shutdown_signal.clone(); let sync_handle = std::thread::spawn(move || { // Run monitoring loop let monitor_result = runtime_handle.block_on(async move { @@ -1029,14 +1028,18 @@ pub unsafe extern "C" fn dash_spv_ffi_client_get_address_balance( } #[no_mangle] -pub unsafe extern "C" fn dash_spv_ffi_client_get_utxos(client: *mut FFIDashSpvClient) -> FFIArray { +pub unsafe extern "C" fn dash_spv_ffi_client_get_utxos( + client: *mut FFIDashSpvClient, +) -> *mut FFIArray { null_check!( client, - FFIArray { + Box::into_raw(Box::new(FFIArray { data: std::ptr::null_mut(), len: 0, - capacity: 0 - } + capacity: 0, + elem_size: 0, + elem_align: 1 + })) ); let client = &(*client); @@ -1059,14 +1062,16 @@ pub unsafe extern "C" fn dash_spv_ffi_client_get_utxos(client: *mut FFIDashSpvCl }); match result { - Ok(arr) => arr, + Ok(arr) => Box::into_raw(Box::new(arr)), Err(e) => { set_last_error(&e.to_string()); - FFIArray { + Box::into_raw(Box::new(FFIArray { data: std::ptr::null_mut(), len: 0, capacity: 0, - } + elem_size: 0, + elem_align: 1, + })) } } } @@ -1075,33 +1080,39 @@ pub unsafe extern "C" fn dash_spv_ffi_client_get_utxos(client: *mut FFIDashSpvCl pub unsafe extern "C" fn dash_spv_ffi_client_get_utxos_for_address( client: *mut FFIDashSpvClient, address: *const c_char, -) -> FFIArray { +) -> *mut FFIArray { null_check!( client, - FFIArray { + Box::into_raw(Box::new(FFIArray { data: std::ptr::null_mut(), len: 0, - capacity: 0 - } + capacity: 0, + elem_size: 0, + elem_align: 1 + })) ); null_check!( address, - FFIArray { + Box::into_raw(Box::new(FFIArray { data: std::ptr::null_mut(), len: 0, - capacity: 0 - } + capacity: 0, + elem_size: 0, + elem_align: 1 + })) ); let addr_str = match CStr::from_ptr(address).to_str() { Ok(s) => s, Err(e) => { set_last_error(&format!("Invalid UTF-8 in address: {}", e)); - return FFIArray { + return Box::into_raw(Box::new(FFIArray { data: std::ptr::null_mut(), len: 0, capacity: 0, - }; + elem_size: 0, + elem_align: 1, + })); } }; @@ -1109,11 +1120,13 @@ pub unsafe extern "C" fn dash_spv_ffi_client_get_utxos_for_address( Ok(a) => a.assume_checked(), Err(e) => { set_last_error(&format!("Invalid address: {}", e)); - return FFIArray { + return Box::into_raw(Box::new(FFIArray { data: std::ptr::null_mut(), len: 0, capacity: 0, - }; + elem_size: 0, + elem_align: 1, + })); } }; @@ -1142,14 +1155,16 @@ pub unsafe extern "C" fn dash_spv_ffi_client_get_utxos_for_address( }); match result { - Ok(arr) => arr, + Ok(arr) => Box::into_raw(Box::new(arr)), Err(e) => { set_last_error(&e.to_string()); - FFIArray { + Box::into_raw(Box::new(FFIArray { data: std::ptr::null_mut(), len: 0, capacity: 0, - } + elem_size: 0, + elem_align: 1, + })) } } } @@ -1254,13 +1269,15 @@ pub unsafe extern "C" fn dash_spv_ffi_client_watch_address( } }; - let _addr = match dashcore::Address::::from_str(addr_str) { - Ok(a) => a.assume_checked(), + match dashcore::Address::::from_str(addr_str) { + Ok(a) => { + let _ = a.assume_checked(); + } Err(e) => { set_last_error(&format!("Invalid address: {}", e)); return FFIErrorCode::InvalidArgument as i32; } - }; + } let client = &(*client); let inner = client.inner.clone(); @@ -1302,13 +1319,15 @@ pub unsafe extern "C" fn dash_spv_ffi_client_unwatch_address( } }; - let _addr = match dashcore::Address::::from_str(addr_str) { - Ok(a) => a.assume_checked(), + match dashcore::Address::::from_str(addr_str) { + Ok(a) => { + let _ = a.assume_checked(); + } Err(e) => { set_last_error(&format!("Invalid address: {}", e)); return FFIErrorCode::InvalidArgument as i32; } - }; + } let client = &(*client); let inner = client.inner.clone(); @@ -1342,10 +1361,9 @@ pub unsafe extern "C" fn dash_spv_ffi_client_watch_script( null_check!(client); null_check!(script_hex); - let _script = match validate_script_hex(script_hex) { - Ok(script) => script, - Err(error_code) => return error_code, - }; + if let Err(error_code) = validate_script_hex(script_hex) { + return error_code; + } let client = &(*client); let inner = client.inner.clone(); @@ -1379,10 +1397,9 @@ pub unsafe extern "C" fn dash_spv_ffi_client_unwatch_script( null_check!(client); null_check!(script_hex); - let _script = match validate_script_hex(script_hex) { - Ok(script) => script, - Err(error_code) => return error_code, - }; + if let Err(error_code) = validate_script_hex(script_hex) { + return error_code; + } let client = &(*client); let inner = client.inner.clone(); @@ -1412,33 +1429,22 @@ pub unsafe extern "C" fn dash_spv_ffi_client_unwatch_script( pub unsafe extern "C" fn dash_spv_ffi_client_get_address_history( client: *mut FFIDashSpvClient, address: *const c_char, -) -> FFIArray { - null_check!( - client, - FFIArray { - data: std::ptr::null_mut(), - len: 0, - capacity: 0 - } - ); - null_check!( - address, - FFIArray { - data: std::ptr::null_mut(), - len: 0, - capacity: 0 - } - ); +) -> *mut FFIArray { + if client.is_null() || address.is_null() { + return std::ptr::null_mut(); + } let addr_str = match CStr::from_ptr(address).to_str() { Ok(s) => s, Err(e) => { set_last_error(&format!("Invalid UTF-8 in address: {}", e)); - return FFIArray { + return Box::into_raw(Box::new(FFIArray { data: std::ptr::null_mut(), len: 0, capacity: 0, - }; + elem_size: 0, + elem_align: 1, + })); } }; @@ -1446,20 +1452,24 @@ pub unsafe extern "C" fn dash_spv_ffi_client_get_address_history( Ok(a) => a.assume_checked(), Err(e) => { set_last_error(&format!("Invalid address: {}", e)); - return FFIArray { + return Box::into_raw(Box::new(FFIArray { data: std::ptr::null_mut(), len: 0, capacity: 0, - }; + elem_size: 0, + elem_align: 1, + })); } }; // Not implemented in dash-spv yet - FFIArray { + Box::into_raw(Box::new(FFIArray { data: std::ptr::null_mut(), len: 0, capacity: 0, - } + elem_size: 0, + elem_align: 1, + })) } #[no_mangle] @@ -1549,43 +1559,37 @@ pub unsafe extern "C" fn dash_spv_ffi_client_broadcast_transaction( #[no_mangle] pub unsafe extern "C" fn dash_spv_ffi_client_get_watched_addresses( client: *mut FFIDashSpvClient, -) -> FFIArray { - null_check!( - client, - FFIArray { - data: std::ptr::null_mut(), - len: 0, - capacity: 0 - } - ); +) -> *mut FFIArray { + if client.is_null() { + return std::ptr::null_mut(); + } // Not implemented in dash-spv yet - FFIArray { + Box::into_raw(Box::new(FFIArray { data: std::ptr::null_mut(), len: 0, capacity: 0, - } + elem_size: 0, + elem_align: 1, + })) } #[no_mangle] pub unsafe extern "C" fn dash_spv_ffi_client_get_watched_scripts( client: *mut FFIDashSpvClient, -) -> FFIArray { - null_check!( - client, - FFIArray { - data: std::ptr::null_mut(), - len: 0, - capacity: 0 - } - ); +) -> *mut FFIArray { + if client.is_null() { + return std::ptr::null_mut(); + } // Not implemented in dash-spv yet - FFIArray { + Box::into_raw(Box::new(FFIArray { data: std::ptr::null_mut(), len: 0, capacity: 0, - } + elem_size: 0, + elem_align: 1, + })) } #[no_mangle] @@ -1692,7 +1696,10 @@ pub unsafe extern "C" fn dash_spv_ffi_transaction_destroy(tx: *mut FFITransactio pub unsafe extern "C" fn dash_spv_ffi_client_get_address_utxos( client: *mut FFIDashSpvClient, address: *const c_char, -) -> FFIArray { +) -> *mut FFIArray { + if client.is_null() || address.is_null() { + return std::ptr::null_mut(); + } crate::client::dash_spv_ffi_client_get_utxos_for_address(client, address) } @@ -1734,9 +1741,6 @@ pub unsafe extern "C" fn dash_spv_ffi_client_get_balance_with_mempool( ) -> *mut FFIBalance { null_check!(client, std::ptr::null_mut()); - let client = &(*client); - let inner = client.inner.clone(); - set_last_error("Wallet-wide mempool balance not available in current dash-spv version"); std::ptr::null_mut() } diff --git a/dash-spv-ffi/src/config.rs b/dash-spv-ffi/src/config.rs index 1266d139f..913b19a92 100644 --- a/dash-spv-ffi/src/config.rs +++ b/dash-spv-ffi/src/config.rs @@ -20,6 +20,7 @@ impl From for ValidationMode { } } +#[repr(transparent)] pub struct FFIClientConfig { inner: ClientConfig, } diff --git a/dash-spv-ffi/src/types.rs b/dash-spv-ffi/src/types.rs index baaa116e8..043b765dd 100644 --- a/dash-spv-ffi/src/types.rs +++ b/dash-spv-ffi/src/types.rs @@ -14,7 +14,8 @@ pub struct FFIString { impl FFIString { pub fn new(s: &str) -> Self { let c_string = CString::new(s).unwrap_or_else(|_| CString::new("").unwrap()); - let length = s.len(); + // Compute length from the finalized CString to avoid mismatches when input contains NULs + let length = c_string.as_bytes().len(); FFIString { ptr: c_string.into_raw(), length, @@ -288,6 +289,8 @@ pub struct FFIArray { pub data: *mut c_void, pub len: usize, pub capacity: usize, + pub elem_size: usize, + pub elem_align: usize, } impl FFIArray { @@ -309,6 +312,8 @@ impl FFIArray { data, len, capacity, + elem_size: std::mem::size_of::(), + elem_align: std::mem::align_of::(), } } @@ -331,13 +336,49 @@ pub unsafe extern "C" fn dash_spv_ffi_string_destroy(s: FFIString) { #[no_mangle] pub unsafe extern "C" fn dash_spv_ffi_array_destroy(arr: *mut FFIArray) { if !arr.is_null() { - let arr = Box::from_raw(arr); - if !arr.data.is_null() && arr.capacity > 0 { - Vec::from_raw_parts(arr.data as *mut u8, arr.len, arr.capacity); + // Only deallocate the vector buffer recorded in the struct; do not free the struct itself. + // This makes it safe to pass pointers to stack-allocated FFIArray values returned by-value. + if !(*arr).data.is_null() && (*arr).capacity > 0 { + // Deallocate the vector buffer using the original layout + use std::alloc::{dealloc, Layout}; + let size = (*arr).elem_size.saturating_mul((*arr).capacity); + if size > 0 && (*arr).elem_align.is_power_of_two() && (*arr).elem_align > 0 { + // Safety: elem_size/elem_align were recorded from the original Vec + let layout = Layout::from_size_align_unchecked(size, (*arr).elem_align); + unsafe { dealloc((*arr).data as *mut u8, layout) }; + } } } } +/// Destroy an array of FFIString pointers (Vec<*mut FFIString>) and their contents. +/// +/// This function: +/// - Iterates the array elements as pointers to FFIString and destroys each via dash_spv_ffi_string_destroy +/// - Frees the underlying vector buffer stored in FFIArray +/// - Does not free the FFIArray struct itself (safe for both stack- and heap-allocated structs) +#[no_mangle] +pub unsafe extern "C" fn dash_spv_ffi_string_array_destroy(arr: *mut FFIArray) { + if arr.is_null() { + return; + } + + // Destroy each FFIString pointed to by the array elements + if !(*arr).data.is_null() && (*arr).len > 0 { + let slice = std::slice::from_raw_parts((*arr).data as *const *mut FFIString, (*arr).len); + for &ffi_string_ptr in slice.iter() { + if !ffi_string_ptr.is_null() { + // Take ownership and destroy + let boxed = Box::from_raw(ffi_string_ptr); + dash_spv_ffi_string_destroy(*boxed); + } + } + } + + // Free the vector buffer itself + dash_spv_ffi_array_destroy(arr); +} + #[repr(C)] pub struct FFITransaction { pub txid: FFIString, diff --git a/dash-spv-ffi/src/wallet.rs b/dash-spv-ffi/src/wallet.rs new file mode 100644 index 000000000..10aa3da8c --- /dev/null +++ b/dash-spv-ffi/src/wallet.rs @@ -0,0 +1,1717 @@ +use crate::client::FFIDashSpvClient; +use crate::types::FFINetwork; +use crate::{null_check, FFIArray}; +use crate::{set_last_error, FFIString}; +use dash_spv::FilterMatch; +use dashcore::{consensus, OutPoint, ScriptBuf, Txid}; +use key_wallet::account::StandardAccountType; +use key_wallet::wallet::initialization::WalletAccountCreationOptions; +use key_wallet::wallet::managed_wallet_info::transaction_building::AccountTypePreference; +use key_wallet::AccountType; +use key_wallet::Utxo as KWUtxo; +use key_wallet::WalletBalance; +use key_wallet_manager::wallet_interface::WalletInterface; +use key_wallet_manager::wallet_manager::{AccountTypeUsed, WalletId}; +use std::ffi::CStr; +use std::os::raw::c_char; +use std::str::FromStr; + +#[repr(C)] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum FFIWatchItemType { + Address = 0, + Script = 1, + Outpoint = 2, +} + +#[repr(C)] +pub struct FFIWatchItem { + pub item_type: FFIWatchItemType, + pub data: FFIString, +} + +impl FFIWatchItem { + pub unsafe fn to_watch_item(&self) -> Result<(), String> { + // Note: This method uses NetworkUnchecked for backward compatibility. + // Consider using to_watch_item_with_network for proper network validation. + let data_str = FFIString::from_ptr(self.data.ptr)?; + + match self.item_type { + FFIWatchItemType::Address => { + let _addr = + dashcore::Address::::from_str(&data_str) + .map_err(|e| format!("Invalid address: {}", e))? + .assume_checked(); + Ok(()) + } + FFIWatchItemType::Script => { + let script_bytes = + hex::decode(&data_str).map_err(|e| format!("Invalid script hex: {}", e))?; + let _script = ScriptBuf::from(script_bytes); + Ok(()) + } + FFIWatchItemType::Outpoint => { + let parts: Vec<&str> = data_str.split(':').collect(); + if parts.len() != 2 { + return Err("Invalid outpoint format (expected txid:vout)".to_string()); + } + let txid: Txid = parts[0].parse().map_err(|e| format!("Invalid txid: {}", e))?; + let vout: u32 = parts[1].parse().map_err(|e| format!("Invalid vout: {}", e))?; + let _ = OutPoint::new(txid, vout); + Ok(()) + } + } + } + + /// Convert FFIWatchItem to WatchItem with network validation + pub unsafe fn to_watch_item_with_network( + &self, + network: dashcore::Network, + ) -> Result<(), String> { + let data_str = FFIString::from_ptr(self.data.ptr)?; + + match self.item_type { + FFIWatchItemType::Address => { + let addr = + dashcore::Address::::from_str(&data_str) + .map_err(|e| format!("Invalid address: {}", e))?; + + // Validate that the address belongs to the expected network + let _checked_addr = addr.require_network(network).map_err(|_| { + format!("Address {} is not valid for network {:?}", data_str, network) + })?; + Ok(()) + } + FFIWatchItemType::Script => { + let script_bytes = + hex::decode(&data_str).map_err(|e| format!("Invalid script hex: {}", e))?; + let _script = ScriptBuf::from(script_bytes); + Ok(()) + } + FFIWatchItemType::Outpoint => { + let _outpoint = OutPoint::from_str(&data_str) + .map_err(|e| format!("Invalid outpoint: {}", e))?; + Ok(()) + } + } + } +} + +#[repr(C)] +#[derive(Debug, Clone, Copy)] +pub struct FFIBalance { + pub confirmed: u64, + pub pending: u64, + pub instantlocked: u64, + pub mempool: u64, + pub mempool_instant: u64, + pub total: u64, +} + +// Balance struct removed from dash-spv public API; use AddressBalance conversion below + +impl From for FFIBalance { + fn from(balance: dash_spv::types::AddressBalance) -> Self { + FFIBalance { + confirmed: balance.confirmed.to_sat(), + pending: balance.unconfirmed.to_sat(), + instantlocked: 0, // AddressBalance doesn't have instantlocked + mempool: balance.pending.to_sat(), + mempool_instant: balance.pending_instant.to_sat(), + total: balance.total().to_sat(), + } + } +} + +#[repr(C)] +pub struct FFIUtxo { + pub txid: FFIString, + pub vout: u32, + pub amount: u64, + pub script_pubkey: FFIString, + pub address: FFIString, + pub height: u32, + pub is_coinbase: bool, + pub is_confirmed: bool, + pub is_instantlocked: bool, +} + +impl From for FFIUtxo { + fn from(utxo: KWUtxo) -> Self { + FFIUtxo { + txid: FFIString::new(&utxo.outpoint.txid.to_string()), + vout: utxo.outpoint.vout, + amount: utxo.txout.value, + script_pubkey: FFIString::new(&hex::encode(utxo.txout.script_pubkey.to_bytes())), + address: FFIString::new(&utxo.address.to_string()), + height: utxo.height, + is_coinbase: utxo.is_coinbase, + is_confirmed: utxo.is_confirmed, + is_instantlocked: utxo.is_instantlocked, + } + } +} + +#[repr(C)] +pub struct FFITransactionResult { + pub txid: FFIString, + pub version: i32, + pub locktime: u32, + pub size: u32, + pub weight: u32, + pub fee: u64, + pub confirmation_time: u64, + pub confirmation_height: u32, +} + +/// FFI-safe representation of a TransactionRecord with owned data +/// +/// This structure contains owned data that must be properly cleaned up +/// using dash_spv_ffi_transaction_record_destroy +#[repr(C)] +pub struct FFITransactionRecord { + /// Transaction as hex string + pub transaction_hex: FFIString, + /// Transaction ID as hex string + pub txid: FFIString, + /// Block height (0 if unconfirmed) + pub height: u32, + /// Whether this transaction is confirmed + pub is_confirmed: bool, + /// Block hash as hex string (empty if unconfirmed) + pub block_hash: FFIString, + /// Timestamp + pub timestamp: u64, + /// Net amount for this account (positive = incoming, negative = outgoing) + pub net_amount: i64, + /// Fee paid (0 if not available) + pub fee: u64, + /// Transaction label (empty if no label) + pub label: FFIString, + /// Whether this is our transaction + pub is_ours: bool, +} + +// TransactionResult no longer available from dash-spv; conversion removed + +#[repr(C)] +pub struct FFIBlockResult { + pub hash: FFIString, + pub height: u32, + pub time: u32, + pub tx_count: u32, +} + +// BlockResult no longer available from dash-spv; conversion removed + +#[repr(C)] +pub struct FFIFilterMatch { + pub block_hash: FFIString, + pub height: u32, + pub block_requested: bool, +} + +impl From for FFIFilterMatch { + fn from(filter_match: FilterMatch) -> Self { + FFIFilterMatch { + block_hash: FFIString::new(&filter_match.block_hash.to_string()), + height: filter_match.height, + block_requested: filter_match.block_requested, + } + } +} + +#[repr(C)] +pub struct FFIAddressStats { + pub address: FFIString, + pub utxo_count: u32, + pub total_value: u64, + pub confirmed_value: u64, + pub pending_value: u64, + pub spendable_count: u32, + pub coinbase_count: u32, +} + +// AddressStats no longer available from dash-spv; conversion removed + +impl From for FFIBalance { + fn from(bal: WalletBalance) -> Self { + // Map confirmed/unconfirmed/locked; mempool fields are not tracked here + let confirmed = bal.confirmed; + let unconfirmed = bal.unconfirmed; + // "locked" is not exposed in FFIBalance directly; keep in total implicitly + FFIBalance { + confirmed, + pending: unconfirmed, + instantlocked: 0, + mempool: 0, + mempool_instant: 0, + total: bal.total, + } + } +} + +#[no_mangle] +pub unsafe extern "C" fn dash_spv_ffi_watch_item_address( + address: *const c_char, +) -> *mut FFIWatchItem { + if address.is_null() { + set_last_error("Null address pointer"); + return std::ptr::null_mut(); + } + + match CStr::from_ptr(address).to_str() { + Ok(addr_str) => { + let item = FFIWatchItem { + item_type: FFIWatchItemType::Address, + data: FFIString::new(addr_str), + }; + Box::into_raw(Box::new(item)) + } + Err(e) => { + set_last_error(&format!("Invalid UTF-8 in address: {}", e)); + std::ptr::null_mut() + } + } +} + +#[no_mangle] +pub unsafe extern "C" fn dash_spv_ffi_watch_item_script( + script_hex: *const c_char, +) -> *mut FFIWatchItem { + if script_hex.is_null() { + set_last_error("Null script pointer"); + return std::ptr::null_mut(); + } + + match CStr::from_ptr(script_hex).to_str() { + Ok(script_str) => { + let item = FFIWatchItem { + item_type: FFIWatchItemType::Script, + data: FFIString::new(script_str), + }; + Box::into_raw(Box::new(item)) + } + Err(e) => { + set_last_error(&format!("Invalid UTF-8 in script: {}", e)); + std::ptr::null_mut() + } + } +} + +#[no_mangle] +pub unsafe extern "C" fn dash_spv_ffi_watch_item_outpoint( + txid: *const c_char, + vout: u32, +) -> *mut FFIWatchItem { + if txid.is_null() { + set_last_error("Null txid pointer"); + return std::ptr::null_mut(); + } + + match CStr::from_ptr(txid).to_str() { + Ok(txid_str) => { + let outpoint_str = format!("{}:{}", txid_str, vout); + let item = FFIWatchItem { + item_type: FFIWatchItemType::Outpoint, + data: FFIString::new(&outpoint_str), + }; + Box::into_raw(Box::new(item)) + } + Err(e) => { + set_last_error(&format!("Invalid UTF-8 in txid: {}", e)); + std::ptr::null_mut() + } + } +} + +#[no_mangle] +pub unsafe extern "C" fn dash_spv_ffi_watch_item_destroy(item: *mut FFIWatchItem) { + if !item.is_null() { + let item = Box::from_raw(item); + dash_spv_ffi_string_destroy(item.data); + } +} + +#[no_mangle] +pub unsafe extern "C" fn dash_spv_ffi_balance_destroy(balance: *mut FFIBalance) { + if !balance.is_null() { + let _ = Box::from_raw(balance); + } +} + +#[no_mangle] +pub unsafe extern "C" fn dash_spv_ffi_utxo_destroy(utxo: *mut FFIUtxo) { + if !utxo.is_null() { + let utxo = Box::from_raw(utxo); + dash_spv_ffi_string_destroy(utxo.txid); + dash_spv_ffi_string_destroy(utxo.script_pubkey); + dash_spv_ffi_string_destroy(utxo.address); + } +} + +#[no_mangle] +pub unsafe extern "C" fn dash_spv_ffi_transaction_result_destroy(tx: *mut FFITransactionResult) { + if !tx.is_null() { + let tx = Box::from_raw(tx); + dash_spv_ffi_string_destroy(tx.txid); + } +} + +/// Destroy a single FFITransactionRecord and all its owned resources +/// +/// # Safety +/// * `record` must be a valid pointer to an FFITransactionRecord +/// * The pointer must not be used after this function is called +/// * This function should only be called once per FFITransactionRecord +#[no_mangle] +pub unsafe extern "C" fn dash_spv_ffi_transaction_record_destroy( + record: *mut FFITransactionRecord, +) { + if !record.is_null() { + let record = Box::from_raw(record); + dash_spv_ffi_string_destroy(record.transaction_hex); + dash_spv_ffi_string_destroy(record.txid); + dash_spv_ffi_string_destroy(record.block_hash); + dash_spv_ffi_string_destroy(record.label); + } +} + +/// Destroy an array of FFITransactionRecord pointers and their contents. +/// +/// This function: +/// - Iterates the array elements as pointers to FFITransactionRecord and destroys each via dash_spv_ffi_transaction_record_destroy +/// - Frees the underlying vector buffer stored in FFIArray +/// - Does not free the FFIArray struct itself (safe for both stack- and heap-allocated structs) +/// +/// # Safety +/// * `arr` must be a valid pointer to an FFIArray containing FFITransactionRecord pointers +/// * Each element in the array must be a valid FFITransactionRecord pointer +/// * The array elements will be destroyed and should not be used after this call +#[no_mangle] +pub unsafe extern "C" fn dash_spv_ffi_transaction_record_array_destroy(arr: *mut FFIArray) { + if arr.is_null() { + return; + } + + // Destroy each FFITransactionRecord pointed to by the array elements + if !(*arr).data.is_null() && (*arr).len > 0 { + let slice = + std::slice::from_raw_parts((*arr).data as *const *mut FFITransactionRecord, (*arr).len); + for &record_ptr in slice.iter() { + if !record_ptr.is_null() { + // Take ownership and destroy + dash_spv_ffi_transaction_record_destroy(record_ptr); + } + } + } + + // Free the vector buffer itself + crate::types::dash_spv_ffi_array_destroy(arr); +} + +#[no_mangle] +pub unsafe extern "C" fn dash_spv_ffi_block_result_destroy(block: *mut FFIBlockResult) { + if !block.is_null() { + let block = Box::from_raw(block); + dash_spv_ffi_string_destroy(block.hash); + } +} + +#[no_mangle] +pub unsafe extern "C" fn dash_spv_ffi_filter_match_destroy(filter_match: *mut FFIFilterMatch) { + if !filter_match.is_null() { + let filter_match = Box::from_raw(filter_match); + dash_spv_ffi_string_destroy(filter_match.block_hash); + } +} + +#[no_mangle] +pub unsafe extern "C" fn dash_spv_ffi_address_stats_destroy(stats: *mut FFIAddressStats) { + if !stats.is_null() { + let stats = Box::from_raw(stats); + dash_spv_ffi_string_destroy(stats.address); + } +} + +use crate::types::dash_spv_ffi_string_destroy; + +#[no_mangle] +pub unsafe extern "C" fn dash_spv_ffi_validate_address( + address: *const c_char, + network: FFINetwork, +) -> i32 { + if address.is_null() { + return 0; + } + + let addr_str = match CStr::from_ptr(address).to_str() { + Ok(s) => s, + Err(_) => return 0, + }; + + // Convert FFI network to dashcore network + let net: dashcore::Network = network.into(); + + // Try to parse the address as unchecked first + match dashcore::Address::::from_str(addr_str) { + Ok(addr_unchecked) => { + // Check if the address is valid for the given network + match addr_unchecked.require_network(net) { + Ok(_) => 1, // Address is valid for the specified network + Err(_) => 0, // Address is for a different network + } + } + Err(_) => 0, + } +} + +#[no_mangle] +pub unsafe extern "C" fn dash_spv_ffi_wallet_get_monitored_addresses( + client: *mut FFIDashSpvClient, + network: FFINetwork, +) -> *mut crate::FFIArray { + null_check!(client, std::ptr::null_mut()); + + let client = &(*client); + let inner = client.inner.clone(); + + let result = client.run_async(|| async { + let guard = inner.lock().unwrap(); + if let Some(ref spv_client) = *guard { + let wallet_manager = &spv_client.wallet().read().await.base; + let addresses = wallet_manager.monitored_addresses(network.into()); + + let ffi_strings: Vec<*mut FFIString> = addresses + .into_iter() + .map(|addr| Box::into_raw(Box::new(FFIString::new(&addr.to_string())))) + .collect(); + + Ok(crate::FFIArray::new(ffi_strings)) + } else { + Err("Client not initialized".to_string()) + } + }); + + match result { + Ok(array) => Box::into_raw(Box::new(array)), + Err(e) => { + set_last_error(&e); + std::ptr::null_mut() + } + } +} + +#[no_mangle] +pub unsafe extern "C" fn dash_spv_ffi_wallet_get_balance( + client: *mut FFIDashSpvClient, + wallet_id_ptr: *const c_char, +) -> *mut crate::FFIBalance { + null_check!(client, std::ptr::null_mut()); + null_check!(wallet_id_ptr, std::ptr::null_mut()); + + // Parse wallet id as 64-char hex string + let wallet_id_hex = match CStr::from_ptr(wallet_id_ptr).to_str() { + Ok(s) => s, + Err(e) => { + set_last_error(&format!("Invalid UTF-8 in wallet id: {}", e)); + return std::ptr::null_mut(); + } + }; + + let mut id: [u8; 32] = [0u8; 32]; + let bytes = hex::decode(wallet_id_hex).unwrap_or_default(); + if bytes.len() != 32 { + set_last_error("Wallet ID must be 32 bytes hex"); + return std::ptr::null_mut(); + } + id.copy_from_slice(&bytes); + + let client = &(*client); + let inner = client.inner.clone(); + + let result: Result = client.run_async(|| async { + let guard = inner.lock().unwrap(); + if let Some(ref spv_client) = *guard { + let wallet = spv_client.wallet().clone(); + let wallet = wallet.read().await; + match wallet.base.get_wallet_balance(&id) { + Ok(b) => Ok(crate::FFIBalance::from(b)), + Err(e) => Err(e.to_string()), + } + } else { + Err("Client not initialized".to_string()) + } + }); + + match result { + Ok(bal) => Box::into_raw(Box::new(bal)), + Err(e) => { + set_last_error(&e); + std::ptr::null_mut() + } + } +} + +#[no_mangle] +pub unsafe extern "C" fn dash_spv_ffi_wallet_get_utxos( + client: *mut FFIDashSpvClient, + wallet_id_ptr: *const c_char, +) -> *mut FFIArray { + null_check!(client, std::ptr::null_mut()); + null_check!(wallet_id_ptr, std::ptr::null_mut()); + + let wallet_id_hex = match CStr::from_ptr(wallet_id_ptr).to_str() { + Ok(s) => s, + Err(e) => { + set_last_error(&format!("Invalid UTF-8 in wallet id: {}", e)); + return std::ptr::null_mut(); + } + }; + + let mut id: [u8; 32] = [0u8; 32]; + let bytes = hex::decode(wallet_id_hex).unwrap_or_default(); + if bytes.len() != 32 { + set_last_error("Wallet ID must be 32 bytes hex"); + return std::ptr::null_mut(); + } + id.copy_from_slice(&bytes); + + let client = &(*client); + let inner = client.inner.clone(); + + let result: Result = client.run_async(|| async { + let guard = inner.lock().unwrap(); + if let Some(ref spv_client) = *guard { + let wallet = spv_client.wallet().clone(); + let wallet = wallet.read().await; + match wallet.base.wallet_utxos(&id) { + Ok(set) => { + let ffi: Vec = + set.into_iter().cloned().map(crate::FFIUtxo::from).collect(); + Ok(FFIArray::new(ffi)) + } + Err(e) => Err(e.to_string()), + } + } else { + Err("Client not initialized".to_string()) + } + }); + + match result { + Ok(arr) => Box::into_raw(Box::new(arr)), + Err(e) => { + set_last_error(&e); + std::ptr::null_mut() + } + } +} + +#[repr(C)] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum FFIWalletAccountCreationOptions { + /// Default account creation: Creates account 0 for BIP44, account 0 for CoinJoin, + /// and all special purpose accounts (Identity Registration, Identity Invitation, + /// Provider keys, etc.) + Default = 0, + /// Create only BIP44 accounts (no CoinJoin or special accounts) + BIP44AccountsOnly = 1, + /// Create no accounts at all - useful for tests that want to manually control account creation + None = 2, +} + +impl From for WalletAccountCreationOptions { + fn from(options: FFIWalletAccountCreationOptions) -> Self { + match options { + FFIWalletAccountCreationOptions::Default => WalletAccountCreationOptions::Default, + FFIWalletAccountCreationOptions::BIP44AccountsOnly => { + WalletAccountCreationOptions::BIP44AccountsOnly(Default::default()) + } + FFIWalletAccountCreationOptions::None => WalletAccountCreationOptions::None, + } + } +} + +#[repr(C)] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum FFIAccountType { + /// Standard BIP44 account for regular transactions + BIP44 = 0, + /// Standard BIP32 account for regular transactions + BIP32 = 1, + /// CoinJoin account for private transactions + CoinJoin = 2, + /// Identity registration funding + IdentityRegistration = 3, + /// Identity top-up funding + IdentityTopUp = 4, + /// Identity invitation funding + IdentityInvitation = 5, + /// Provider voting keys (DIP-3) + ProviderVotingKeys = 6, + /// Provider owner keys (DIP-3) + ProviderOwnerKeys = 7, + /// Provider operator keys (DIP-3) + ProviderOperatorKeys = 8, + /// Provider platform P2P keys (DIP-3, ED25519) + ProviderPlatformKeys = 9, +} + +impl FFIAccountType { + /// Convert FFI account type to internal AccountType + /// + /// # Arguments + /// * `account_index` - Required for BIP44, BIP32, and CoinJoin account types + /// * `registration_index` - Required for IdentityTopUp account type + pub fn to_account_type( + self, + account_index: Option, + registration_index: Option, + ) -> Result { + use key_wallet::AccountType::*; + + match self { + FFIAccountType::BIP44 => { + let index = account_index.ok_or("Account index required for BIP44 accounts")?; + Ok(Standard { + index, + standard_account_type: StandardAccountType::BIP44Account, + }) + } + FFIAccountType::BIP32 => { + let index = account_index.ok_or("Account index required for BIP32 accounts")?; + Ok(Standard { + index, + standard_account_type: StandardAccountType::BIP32Account, + }) + } + FFIAccountType::CoinJoin => { + let index = account_index.ok_or("Account index required for CoinJoin accounts")?; + Ok(CoinJoin { + index, + }) + } + FFIAccountType::IdentityRegistration => Ok(IdentityRegistration), + FFIAccountType::IdentityTopUp => { + let registration_index = registration_index + .ok_or("Registration index required for IdentityTopUp accounts")?; + Ok(IdentityTopUp { + registration_index, + }) + } + FFIAccountType::IdentityInvitation => Ok(IdentityInvitation), + FFIAccountType::ProviderVotingKeys => Ok(ProviderVotingKeys), + FFIAccountType::ProviderOwnerKeys => Ok(ProviderOwnerKeys), + FFIAccountType::ProviderOperatorKeys => Ok(ProviderOperatorKeys), + FFIAccountType::ProviderPlatformKeys => Ok(ProviderPlatformKeys), + } + } +} + +/// Create a new wallet from mnemonic phrase +/// +/// # Arguments +/// * `client` - Pointer to FFIDashSpvClient +/// * `mnemonic` - The mnemonic phrase as null-terminated C string +/// * `passphrase` - Optional BIP39 passphrase (can be null/empty) +/// * `network` - The network to use +/// * `account_options` - Account creation options +/// * `name` - Wallet name as null-terminated C string +/// * `birth_height` - Optional birth height (can be 0 for none) +/// +/// # Returns +/// * Pointer to FFIString containing hex-encoded WalletId (32 bytes as 64-char hex) +/// * Returns null on error (check last_error) +#[no_mangle] +pub unsafe extern "C" fn dash_spv_ffi_wallet_create_from_mnemonic( + client: *mut FFIDashSpvClient, + mnemonic: *const c_char, + passphrase: *const c_char, + network: FFINetwork, + account_options: FFIWalletAccountCreationOptions, + name: *const c_char, + birth_height: u32, +) -> *mut FFIString { + null_check!(client, std::ptr::null_mut()); + null_check!(mnemonic, std::ptr::null_mut()); + null_check!(name, std::ptr::null_mut()); + + let client = &(*client); + let inner = client.inner.clone(); + + let mnemonic_str = match CStr::from_ptr(mnemonic).to_str() { + Ok(s) => s, + Err(e) => { + set_last_error(&format!("Invalid UTF-8 in mnemonic: {}", e)); + return std::ptr::null_mut(); + } + }; + + let passphrase_str = if passphrase.is_null() { + "" + } else { + match CStr::from_ptr(passphrase).to_str() { + Ok(s) => s, + Err(e) => { + set_last_error(&format!("Invalid UTF-8 in passphrase: {}", e)); + return std::ptr::null_mut(); + } + } + }; + + let name_str = match CStr::from_ptr(name).to_str() { + Ok(s) => s, + Err(e) => { + set_last_error(&format!("Invalid UTF-8 in name: {}", e)); + return std::ptr::null_mut(); + } + }; + + let result = client.run_async(|| async { + let mut guard = inner.lock().unwrap(); + if let Some(ref mut spv_client) = *guard { + let wallet_manager = &mut spv_client.wallet().write().await.base; + + // Generate a random WalletId + let wallet_id = WalletId::from(rand::random::<[u8; 32]>()); + + let network = network.into(); + let account_creation_options: WalletAccountCreationOptions = account_options.into(); + let birth_height_opt = if birth_height == 0 { + None + } else { + Some(birth_height) + }; + + match wallet_manager.create_wallet_from_mnemonic( + wallet_id, + name_str.to_string(), + mnemonic_str, + passphrase_str, + Some(network), + birth_height_opt, + account_creation_options, + ) { + Ok(_) => { + // Convert WalletId to hex string + Ok(hex::encode(wallet_id)) + } + Err(e) => Err(e.to_string()), + } + } else { + Err("Client not initialized".to_string()) + } + }); + + match result { + Ok(wallet_id_hex) => Box::into_raw(Box::new(FFIString::new(&wallet_id_hex))), + Err(e) => { + set_last_error(&e); + std::ptr::null_mut() + } + } +} + +/// Create a new empty wallet (test wallet with fixed mnemonic) +/// +/// # Arguments +/// * `client` - Pointer to FFIDashSpvClient +/// * `network` - The network to use +/// * `account_options` - Account creation options +/// * `name` - Wallet name as null-terminated C string +/// +/// # Returns +/// * Pointer to FFIString containing hex-encoded WalletId (32 bytes as 64-char hex) +/// * Returns null on error (check last_error) +#[no_mangle] +pub unsafe extern "C" fn dash_spv_ffi_wallet_create( + client: *mut FFIDashSpvClient, + network: FFINetwork, + account_options: FFIWalletAccountCreationOptions, + name: *const c_char, +) -> *mut FFIString { + null_check!(client, std::ptr::null_mut()); + null_check!(name, std::ptr::null_mut()); + + let client = &(*client); + let inner = client.inner.clone(); + + let name_str = match CStr::from_ptr(name).to_str() { + Ok(s) => s, + Err(e) => { + set_last_error(&format!("Invalid UTF-8 in name: {}", e)); + return std::ptr::null_mut(); + } + }; + + let result = client.run_async(|| async { + let mut guard = inner.lock().unwrap(); + if let Some(ref mut spv_client) = *guard { + let wallet_manager = &mut spv_client.wallet().write().await.base; + + // Generate a random WalletId + let wallet_id = WalletId::from(rand::random::<[u8; 32]>()); + + let network = network.into(); + let account_creation_options: WalletAccountCreationOptions = account_options.into(); + + match wallet_manager.create_wallet( + wallet_id, + name_str.to_string(), + account_creation_options, + network, + ) { + Ok(_) => { + // Convert WalletId to hex string + Ok(hex::encode(wallet_id)) + } + Err(e) => Err(e.to_string()), + } + } else { + Err("Client not initialized".to_string()) + } + }); + + match result { + Ok(wallet_id_hex) => Box::into_raw(Box::new(FFIString::new(&wallet_id_hex))), + Err(e) => { + set_last_error(&e); + std::ptr::null_mut() + } + } +} + +/// Get a list of all wallet IDs +/// +/// # Arguments +/// * `client` - Pointer to FFIDashSpvClient +/// +/// # Returns +/// * Pointer to FFIArray of FFIString objects containing hex-encoded WalletIds +#[no_mangle] +pub unsafe extern "C" fn dash_spv_ffi_wallet_list(client: *mut FFIDashSpvClient) -> *mut FFIArray { + null_check!(client, std::ptr::null_mut()); + + let client = &(*client); + let inner = client.inner.clone(); + + let result: Result = client.run_async(|| async { + let guard = inner.lock().unwrap(); + if let Some(ref spv_client) = *guard { + let wallet_manager = &spv_client.wallet().read().await.base; + // Return an array of pointers to FFIString to match FFI expectations + let wallet_ids: Vec<*mut FFIString> = wallet_manager + .list_wallets() + .iter() + .map(|id| Box::into_raw(Box::new(FFIString::new(&hex::encode(id))))) + .collect(); + + Ok(FFIArray::new(wallet_ids)) + } else { + Err("Client not initialized".to_string()) + } + }); + + match result { + Ok(arr) => Box::into_raw(Box::new(arr)), + Err(e) => { + set_last_error(&e); + std::ptr::null_mut() + } + } +} + +/// Import a wallet from an extended private key +/// +/// # Arguments +/// * `client` - Pointer to FFIDashSpvClient +/// * `xprv` - The extended private key string (base58check encoded) +/// * `network` - The network to use +/// * `account_options` - Account creation options +/// * `name` - Wallet name as null-terminated C string +/// +/// # Returns +/// * Pointer to FFIString containing hex-encoded WalletId (32 bytes as 64-char hex) +/// * Returns null on error (check last_error) +#[no_mangle] +pub unsafe extern "C" fn dash_spv_ffi_wallet_import_from_xprv( + client: *mut FFIDashSpvClient, + xprv: *const c_char, + network: FFINetwork, + account_options: FFIWalletAccountCreationOptions, + name: *const c_char, +) -> *mut FFIString { + null_check!(client, std::ptr::null_mut()); + null_check!(xprv, std::ptr::null_mut()); + null_check!(name, std::ptr::null_mut()); + + let client = &(*client); + let inner = client.inner.clone(); + + let xprv_str = match CStr::from_ptr(xprv).to_str() { + Ok(s) => s, + Err(e) => { + set_last_error(&format!("Invalid UTF-8 in xprv: {}", e)); + return std::ptr::null_mut(); + } + }; + + let name_str = match CStr::from_ptr(name).to_str() { + Ok(s) => s, + Err(e) => { + set_last_error(&format!("Invalid UTF-8 in name: {}", e)); + return std::ptr::null_mut(); + } + }; + + let result = client.run_async(|| async { + let mut guard = inner.lock().unwrap(); + if let Some(ref mut spv_client) = *guard { + let wallet_manager = &mut spv_client.wallet().write().await.base; + + // Generate a random WalletId + let wallet_id = WalletId::from(rand::random::<[u8; 32]>()); + + let network = network.into(); + let account_creation_options: WalletAccountCreationOptions = account_options.into(); + + match wallet_manager.import_wallet_from_extended_priv_key( + wallet_id, + name_str.to_string(), + xprv_str, + network, + account_creation_options, + ) { + Ok(_) => { + // Convert WalletId to hex string + Ok(hex::encode(wallet_id)) + } + Err(e) => Err(e.to_string()), + } + } else { + Err("Client not initialized".to_string()) + } + }); + + match result { + Ok(wallet_id_hex) => Box::into_raw(Box::new(FFIString::new(&wallet_id_hex))), + Err(e) => { + set_last_error(&e); + std::ptr::null_mut() + } + } +} + +/// Import a watch-only wallet from an extended public key +/// +/// # Arguments +/// * `client` - Pointer to FFIDashSpvClient +/// * `xpub` - The extended public key string (base58check encoded) +/// * `network` - The network to use +/// * `name` - Wallet name as null-terminated C string +/// +/// # Returns +/// * Pointer to FFIString containing hex-encoded WalletId (32 bytes as 64-char hex) +/// * Returns null on error (check last_error) +#[no_mangle] +pub unsafe extern "C" fn dash_spv_ffi_wallet_import_from_xpub( + client: *mut FFIDashSpvClient, + xpub: *const c_char, + network: FFINetwork, + name: *const c_char, +) -> *mut FFIString { + null_check!(client, std::ptr::null_mut()); + null_check!(xpub, std::ptr::null_mut()); + null_check!(name, std::ptr::null_mut()); + + let client = &(*client); + let inner = client.inner.clone(); + + let xpub_str = match CStr::from_ptr(xpub).to_str() { + Ok(s) => s, + Err(e) => { + set_last_error(&format!("Invalid UTF-8 in xpub: {}", e)); + return std::ptr::null_mut(); + } + }; + + let name_str = match CStr::from_ptr(name).to_str() { + Ok(s) => s, + Err(e) => { + set_last_error(&format!("Invalid UTF-8 in name: {}", e)); + return std::ptr::null_mut(); + } + }; + + let result = client.run_async(|| async { + let mut guard = inner.lock().unwrap(); + if let Some(ref mut spv_client) = *guard { + let wallet_manager = &mut spv_client.wallet().write().await.base; + + // Generate a random WalletId + let wallet_id = WalletId::from(rand::random::<[u8; 32]>()); + + let network = network.into(); + + match wallet_manager.import_wallet_from_xpub( + wallet_id, + name_str.to_string(), + xpub_str, + network, + ) { + Ok(_) => { + // Convert WalletId to hex string + Ok(hex::encode(wallet_id)) + } + Err(e) => Err(e.to_string()), + } + } else { + Err("Client not initialized".to_string()) + } + }); + + match result { + Ok(wallet_id_hex) => Box::into_raw(Box::new(FFIString::new(&wallet_id_hex))), + Err(e) => { + set_last_error(&e); + std::ptr::null_mut() + } + } +} + +#[repr(C)] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum FFIAccountTypePreference { + /// Use BIP44 account only + BIP44 = 0, + /// Use BIP32 account only + BIP32 = 1, + /// Prefer BIP44, fallback to BIP32 + PreferBIP44 = 2, + /// Prefer BIP32, fallback to BIP44 + PreferBIP32 = 3, +} + +impl From for AccountTypePreference { + fn from(pref: FFIAccountTypePreference) -> Self { + match pref { + FFIAccountTypePreference::BIP44 => AccountTypePreference::BIP44, + FFIAccountTypePreference::BIP32 => AccountTypePreference::BIP32, + FFIAccountTypePreference::PreferBIP44 => AccountTypePreference::PreferBIP44, + FFIAccountTypePreference::PreferBIP32 => AccountTypePreference::PreferBIP32, + } + } +} + +#[repr(C)] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum FFIAccountTypeUsed { + /// BIP44 account was used + BIP44 = 0, + /// BIP32 account was used + BIP32 = 1, +} + +#[repr(C)] +pub struct FFIAddressGenerationResult { + pub address: *mut FFIString, + pub account_type_used: FFIAccountTypeUsed, +} + +/// Add a new account to an existing wallet from an extended public key +/// +/// This creates a watch-only account that can monitor addresses and transactions +/// but cannot sign them. +/// +/// # Arguments +/// * `client` - Pointer to FFIDashSpvClient +/// * `wallet_id_hex` - Hex-encoded wallet ID (64 characters) +/// * `xpub` - The extended public key string (base58check encoded) +/// * `account_type` - The type of account to create +/// * `network` - The network for the account +/// * `account_index` - Account index (required for BIP44, BIP32, CoinJoin) +/// * `registration_index` - Registration index (required for IdentityTopUp) +/// +/// # Returns +/// * FFIErrorCode::Success on success +/// * FFIErrorCode::InvalidArgument on error (check last_error) +#[no_mangle] +pub unsafe extern "C" fn dash_spv_ffi_wallet_add_account_from_xpub( + client: *mut FFIDashSpvClient, + wallet_id_hex: *const c_char, + xpub: *const c_char, + account_type: FFIAccountType, + network: FFINetwork, + account_index: u32, + registration_index: u32, +) -> i32 { + null_check!(client, crate::FFIErrorCode::InvalidArgument as i32); + null_check!(wallet_id_hex, crate::FFIErrorCode::InvalidArgument as i32); + null_check!(xpub, crate::FFIErrorCode::InvalidArgument as i32); + + let client = &(*client); + let inner = client.inner.clone(); + + let wallet_id_hex_str = match CStr::from_ptr(wallet_id_hex).to_str() { + Ok(s) => s, + Err(e) => { + set_last_error(&format!("Invalid UTF-8 in wallet ID: {}", e)); + return crate::FFIErrorCode::InvalidArgument as i32; + } + }; + + let xpub_str = match CStr::from_ptr(xpub).to_str() { + Ok(s) => s, + Err(e) => { + set_last_error(&format!("Invalid UTF-8 in xpub: {}", e)); + return crate::FFIErrorCode::InvalidArgument as i32; + } + }; + + // Parse wallet ID + let mut wallet_id: [u8; 32] = [0u8; 32]; + let bytes = match hex::decode(wallet_id_hex_str) { + Ok(b) => b, + Err(e) => { + set_last_error(&format!("Invalid hex wallet ID: {}", e)); + return crate::FFIErrorCode::InvalidArgument as i32; + } + }; + if bytes.len() != 32 { + set_last_error("Wallet ID must be 32 bytes hex"); + return crate::FFIErrorCode::InvalidArgument as i32; + } + wallet_id.copy_from_slice(&bytes); + + // Convert account type with parameters + let account_index_opt = if matches!( + account_type, + FFIAccountType::BIP44 | FFIAccountType::BIP32 | FFIAccountType::CoinJoin + ) { + Some(account_index) + } else { + None + }; + + let registration_index_opt = if matches!(account_type, FFIAccountType::IdentityTopUp) { + Some(registration_index) + } else { + None + }; + + let account_type_internal = + match account_type.to_account_type(account_index_opt, registration_index_opt) { + Ok(at) => at, + Err(e) => { + set_last_error(&e); + return crate::FFIErrorCode::InvalidArgument as i32; + } + }; + + let result: Result<(), String> = client.run_async(|| async { + let mut guard = inner.lock().unwrap(); + if let Some(ref mut spv_client) = *guard { + let wallet_manager = &mut spv_client.wallet().write().await.base; + + // Parse the extended public key + let extended_pub_key = key_wallet::ExtendedPubKey::from_str(xpub_str) + .map_err(|e| format!("Invalid xpub: {}", e))?; + + match wallet_manager.create_account( + &wallet_id, + account_type_internal, + network.into(), + Some(extended_pub_key), + ) { + Ok(()) => Ok(()), + Err(e) => Err(e.to_string()), + } + } else { + Err("Client not initialized".to_string()) + } + }); + + match result { + Ok(()) => crate::FFIErrorCode::Success as i32, + Err(e) => { + set_last_error(&e); + crate::FFIErrorCode::InvalidArgument as i32 + } + } +} + +/// Get wallet-wide mempool balance +/// +/// This returns the total unconfirmed balance (mempool transactions) across all +/// accounts in the specified wallet. This represents the balance from transactions +/// that have been broadcast but not yet confirmed in a block. +/// +/// # Arguments +/// * `client` - Pointer to FFIDashSpvClient +/// * `wallet_id_hex` - Hex-encoded wallet ID (64 characters), or null for all wallets +/// +/// # Returns +/// * Total mempool balance in satoshis +/// * Returns 0 if wallet not found or client not initialized (check last_error) +#[no_mangle] +pub unsafe extern "C" fn dash_spv_ffi_wallet_get_mempool_balance( + client: *mut FFIDashSpvClient, + wallet_id_hex: *const c_char, +) -> u64 { + null_check!(client, 0); + + let client = &(*client); + let inner = client.inner.clone(); + + // Parse wallet ID if provided + let wallet_id_opt = if wallet_id_hex.is_null() { + None + } else { + let wallet_id_hex_str = match CStr::from_ptr(wallet_id_hex).to_str() { + Ok(s) => s, + Err(e) => { + set_last_error(&format!("Invalid UTF-8 in wallet ID: {}", e)); + return 0; + } + }; + + let mut wallet_id: [u8; 32] = [0u8; 32]; + let bytes = match hex::decode(wallet_id_hex_str) { + Ok(b) => b, + Err(e) => { + set_last_error(&format!("Invalid hex wallet ID: {}", e)); + return 0; + } + }; + if bytes.len() != 32 { + set_last_error("Wallet ID must be 32 bytes hex"); + return 0; + } + wallet_id.copy_from_slice(&bytes); + Some(wallet_id) + }; + + let result = client.run_async(|| async { + let guard = inner.lock().unwrap(); + if let Some(ref spv_client) = *guard { + let wallet_manager = &spv_client.wallet().read().await.base; + + if let Some(wallet_id) = wallet_id_opt { + // Get mempool balance for specific wallet + match wallet_manager.get_wallet_balance(&wallet_id) { + Ok(balance) => Ok(balance.unconfirmed), + Err(e) => Err(e.to_string()), + } + } else { + // Get total mempool balance across all wallets + let mut total_mempool_balance = 0u64; + for wallet_id in wallet_manager.list_wallets() { + if let Ok(balance) = wallet_manager.get_wallet_balance(wallet_id) { + total_mempool_balance = + total_mempool_balance.saturating_add(balance.unconfirmed); + } + } + Ok(total_mempool_balance) + } + } else { + Err("Client not initialized".to_string()) + } + }); + + match result { + Ok(balance) => balance, + Err(e) => { + set_last_error(&e); + 0 + } + } +} + +/// Get wallet-wide mempool transaction count +/// +/// This returns the total number of unconfirmed transactions (in mempool) across all +/// accounts in the specified wallet. +/// +/// # Arguments +/// * `client` - Pointer to FFIDashSpvClient +/// * `wallet_id_hex` - Hex-encoded wallet ID (64 characters), or null for all wallets +/// +/// # Returns +/// * Total mempool transaction count +/// * Returns 0 if wallet not found or client not initialized (check last_error) +#[no_mangle] +pub unsafe extern "C" fn dash_spv_ffi_wallet_get_mempool_transaction_count( + client: *mut FFIDashSpvClient, + wallet_id_hex: *const c_char, +) -> u32 { + null_check!(client, 0); + + let client = &(*client); + let inner = client.inner.clone(); + + // Parse wallet ID if provided + let wallet_id_opt = if wallet_id_hex.is_null() { + None + } else { + let wallet_id_hex_str = match CStr::from_ptr(wallet_id_hex).to_str() { + Ok(s) => s, + Err(e) => { + set_last_error(&format!("Invalid UTF-8 in wallet ID: {}", e)); + return 0; + } + }; + + let mut wallet_id: [u8; 32] = [0u8; 32]; + let bytes = match hex::decode(wallet_id_hex_str) { + Ok(b) => b, + Err(e) => { + set_last_error(&format!("Invalid hex wallet ID: {}", e)); + return 0; + } + }; + if bytes.len() != 32 { + set_last_error("Wallet ID must be 32 bytes hex"); + return 0; + } + wallet_id.copy_from_slice(&bytes); + Some(wallet_id) + }; + + let result = client.run_async(|| async { + let guard = inner.lock().unwrap(); + if let Some(ref spv_client) = *guard { + let wallet_manager = &spv_client.wallet().read().await.base; + + if let Some(wallet_id) = wallet_id_opt { + // Get mempool transaction count for specific wallet + match wallet_manager.wallet_transaction_history(&wallet_id) { + Ok(txs) => { + let mempool_count = + txs.iter().filter(|tx_record| tx_record.height.is_none()).count(); + Ok(mempool_count as u32) + } + Err(e) => Err(e.to_string()), + } + } else { + // Get total mempool transaction count across all wallets + let mut total_mempool_count = 0u32; + for wallet_id in wallet_manager.list_wallets() { + if let Ok(txs) = wallet_manager.wallet_transaction_history(wallet_id) { + let wallet_mempool_count = + txs.iter().filter(|tx_record| tx_record.height.is_none()).count() + as u32; + total_mempool_count = + total_mempool_count.saturating_add(wallet_mempool_count); + } + } + Ok(total_mempool_count) + } + } else { + Err("Client not initialized".to_string()) + } + }); + + match result { + Ok(count) => count, + Err(e) => { + set_last_error(&e); + 0 + } + } +} + +/// Record a sent transaction in the wallet +/// +/// This records a transaction that was sent/broadcast by the client, updating the +/// wallet state to reflect the outgoing transaction. The transaction will be tracked +/// in mempool until it's confirmed in a block. +/// +/// # Arguments +/// * `client` - Pointer to FFIDashSpvClient +/// * `tx_hex` - Hex-encoded transaction data +/// * `network` - The network for the transaction +/// +/// # Returns +/// * FFIErrorCode::Success on success +/// * FFIErrorCode::InvalidArgument on error (check last_error) +#[no_mangle] +pub unsafe extern "C" fn dash_spv_ffi_wallet_record_sent_transaction( + client: *mut FFIDashSpvClient, + tx_hex: *const c_char, + network: FFINetwork, +) -> i32 { + null_check!(client, crate::FFIErrorCode::InvalidArgument as i32); + null_check!(tx_hex, crate::FFIErrorCode::InvalidArgument as i32); + + let client = &(*client); + let inner = client.inner.clone(); + + let tx_hex_str = match CStr::from_ptr(tx_hex).to_str() { + Ok(s) => s, + Err(e) => { + set_last_error(&format!("Invalid UTF-8 in transaction hex: {}", e)); + return crate::FFIErrorCode::InvalidArgument as i32; + } + }; + + // Parse transaction from hex + let tx_bytes = match hex::decode(tx_hex_str) { + Ok(bytes) => bytes, + Err(e) => { + set_last_error(&format!("Invalid hex transaction: {}", e)); + return crate::FFIErrorCode::InvalidArgument as i32; + } + }; + + let transaction: dashcore::Transaction = match consensus::deserialize(&tx_bytes) { + Ok(tx) => tx, + Err(e) => { + set_last_error(&format!("Invalid transaction format: {}", e)); + return crate::FFIErrorCode::InvalidArgument as i32; + } + }; + + let result = client.run_async(|| async { + let mut guard = inner.lock().unwrap(); + if let Some(ref mut spv_client) = *guard { + // Record the sent transaction by processing it as a mempool transaction + // This will update the wallet state to reflect the outgoing transaction + spv_client + .wallet() + .write() + .await + .process_mempool_transaction(&transaction, network.into()) + .await; + Ok(()) + } else { + Err("Client not initialized".to_string()) + } + }); + + match result { + Ok(()) => crate::FFIErrorCode::Success as i32, + Err(e) => { + set_last_error(&e); + crate::FFIErrorCode::InvalidArgument as i32 + } + } +} + +/// Get a receive address from a specific wallet and account +/// +/// This generates a new unused receive address (external chain) for the specified +/// wallet and account. The address will be marked as used if mark_as_used is true. +/// +/// # Arguments +/// * `client` - Pointer to FFIDashSpvClient +/// * `wallet_id_hex` - Hex-encoded wallet ID (64 characters) +/// * `network` - The network for the address +/// * `account_index` - Account index (0 for first account) +/// * `account_type_pref` - Account type preference (BIP44, BIP32, or preference) +/// * `mark_as_used` - Whether to mark the address as used after generation +/// +/// # Returns +/// * Pointer to FFIAddressGenerationResult containing the address and account type used +/// * Returns null if address generation fails (check last_error) +#[no_mangle] +pub unsafe extern "C" fn dash_spv_ffi_wallet_get_receive_address( + client: *mut FFIDashSpvClient, + wallet_id_hex: *const c_char, + network: FFINetwork, + account_index: u32, + account_type_pref: FFIAccountTypePreference, + mark_as_used: bool, +) -> *mut FFIAddressGenerationResult { + null_check!(client, std::ptr::null_mut()); + null_check!(wallet_id_hex, std::ptr::null_mut()); + + let client = &(*client); + let inner = client.inner.clone(); + + let wallet_id_hex_str = match CStr::from_ptr(wallet_id_hex).to_str() { + Ok(s) => s, + Err(e) => { + set_last_error(&format!("Invalid UTF-8 in wallet ID: {}", e)); + return std::ptr::null_mut(); + } + }; + + // Parse wallet ID + let mut wallet_id: [u8; 32] = [0u8; 32]; + let bytes = match hex::decode(wallet_id_hex_str) { + Ok(b) => b, + Err(e) => { + set_last_error(&format!("Invalid hex wallet ID: {}", e)); + return std::ptr::null_mut(); + } + }; + if bytes.len() != 32 { + set_last_error("Wallet ID must be 32 bytes hex"); + return std::ptr::null_mut(); + } + wallet_id.copy_from_slice(&bytes); + + let result: Result = client.run_async(|| async { + let mut guard = inner.lock().unwrap(); + if let Some(ref mut spv_client) = *guard { + let wallet_manager = &mut spv_client.wallet().write().await.base; + + match wallet_manager.get_receive_address( + &wallet_id, + network.into(), + account_index, + account_type_pref.into(), + mark_as_used, + ) { + Ok(addr_result) => { + if let (Some(address), Some(account_type_used)) = + (addr_result.address, addr_result.account_type_used) + { + let ffi_account_type = match account_type_used { + AccountTypeUsed::BIP44 => FFIAccountTypeUsed::BIP44, + AccountTypeUsed::BIP32 => FFIAccountTypeUsed::BIP32, + }; + + Ok(FFIAddressGenerationResult { + address: Box::into_raw(Box::new(FFIString::new(&address.to_string()))), + account_type_used: ffi_account_type, + }) + } else { + Err("No address could be generated".to_string()) + } + } + Err(e) => Err(e.to_string()), + } + } else { + Err("Client not initialized".to_string()) + } + }); + + match result { + Ok(result) => Box::into_raw(Box::new(result)), + Err(e) => { + set_last_error(&e); + std::ptr::null_mut() + } + } +} + +/// Get a change address from a specific wallet and account +/// +/// This generates a new unused change address (internal chain) for the specified +/// wallet and account. The address will be marked as used if mark_as_used is true. +/// +/// # Arguments +/// * `client` - Pointer to FFIDashSpvClient +/// * `wallet_id_hex` - Hex-encoded wallet ID (64 characters) +/// * `network` - The network for the address +/// * `account_index` - Account index (0 for first account) +/// * `account_type_pref` - Account type preference (BIP44, BIP32, or preference) +/// * `mark_as_used` - Whether to mark the address as used after generation +/// +/// # Returns +/// * Pointer to FFIAddressGenerationResult containing the address and account type used +/// * Returns null if address generation fails (check last_error) +#[no_mangle] +pub unsafe extern "C" fn dash_spv_ffi_wallet_get_change_address( + client: *mut FFIDashSpvClient, + wallet_id_hex: *const c_char, + network: FFINetwork, + account_index: u32, + account_type_pref: FFIAccountTypePreference, + mark_as_used: bool, +) -> *mut FFIAddressGenerationResult { + null_check!(client, std::ptr::null_mut()); + null_check!(wallet_id_hex, std::ptr::null_mut()); + + let client = &(*client); + let inner = client.inner.clone(); + + let wallet_id_hex_str = match CStr::from_ptr(wallet_id_hex).to_str() { + Ok(s) => s, + Err(e) => { + set_last_error(&format!("Invalid UTF-8 in wallet ID: {}", e)); + return std::ptr::null_mut(); + } + }; + + // Parse wallet ID + let mut wallet_id: [u8; 32] = [0u8; 32]; + let bytes = match hex::decode(wallet_id_hex_str) { + Ok(b) => b, + Err(e) => { + set_last_error(&format!("Invalid hex wallet ID: {}", e)); + return std::ptr::null_mut(); + } + }; + if bytes.len() != 32 { + set_last_error("Wallet ID must be 32 bytes hex"); + return std::ptr::null_mut(); + } + wallet_id.copy_from_slice(&bytes); + + let result: Result = client.run_async(|| async { + let mut guard = inner.lock().unwrap(); + if let Some(ref mut spv_client) = *guard { + let wallet_manager = &mut spv_client.wallet().write().await.base; + + match wallet_manager.get_change_address( + &wallet_id, + network.into(), + account_index, + account_type_pref.into(), + mark_as_used, + ) { + Ok(addr_result) => { + if let (Some(address), Some(account_type_used)) = + (addr_result.address, addr_result.account_type_used) + { + let ffi_account_type = match account_type_used { + AccountTypeUsed::BIP44 => FFIAccountTypeUsed::BIP44, + AccountTypeUsed::BIP32 => FFIAccountTypeUsed::BIP32, + }; + + Ok(FFIAddressGenerationResult { + address: Box::into_raw(Box::new(FFIString::new(&address.to_string()))), + account_type_used: ffi_account_type, + }) + } else { + Err("No address could be generated".to_string()) + } + } + Err(e) => Err(e.to_string()), + } + } else { + Err("Client not initialized".to_string()) + } + }); + + match result { + Ok(result) => Box::into_raw(Box::new(result)), + Err(e) => { + set_last_error(&e); + std::ptr::null_mut() + } + } +} + +/// Free an FFIAddressGenerationResult and its associated resources +/// +/// # Safety +/// * `result` must be a valid pointer to an FFIAddressGenerationResult +/// * The pointer must not be used after this function is called +/// * This function should only be called once per FFIAddressGenerationResult +#[no_mangle] +pub unsafe extern "C" fn dash_spv_ffi_address_generation_result_destroy( + result: *mut FFIAddressGenerationResult, +) { + if !result.is_null() { + let result = Box::from_raw(result); + if !result.address.is_null() { + let addr_ptr = result.address; + // Read the FFIString from the raw pointer and destroy it + let addr_string = unsafe { *Box::from_raw(addr_ptr) }; + dash_spv_ffi_string_destroy(addr_string); + } + } +} diff --git a/dash-spv-ffi/tests/c_tests/test_advanced.c b/dash-spv-ffi/tests/c_tests/test_advanced.c index a82a41d5c..970fc5752 100644 --- a/dash-spv-ffi/tests/c_tests/test_advanced.c +++ b/dash-spv-ffi/tests/c_tests/test_advanced.c @@ -49,11 +49,11 @@ void test_wallet_operations() { } // Test getting UTXOs - FFIArray utxos = dash_spv_ffi_client_get_address_utxos(client, test_addresses[0]); - if (utxos.data != NULL) { + FFIArray* utxos = dash_spv_ffi_client_get_address_utxos(client, test_addresses[0]); + if (utxos != NULL) { // New wallet should have no UTXOs - TEST_ASSERT(utxos.len == 0); - dash_spv_ffi_array_destroy(&utxos); + TEST_ASSERT(utxos->len == 0); + dash_spv_ffi_array_destroy(utxos); } // Test unwatching address diff --git a/dash-spv-ffi/tests/integration/test_full_workflow.rs b/dash-spv-ffi/tests/integration/test_full_workflow.rs index be31df734..a4c95fcf1 100644 --- a/dash-spv-ffi/tests/integration/test_full_workflow.rs +++ b/dash-spv-ffi/tests/integration/test_full_workflow.rs @@ -350,7 +350,7 @@ mod tests { for _ in 0..10 { let addresses = dash_spv_ffi_client_get_watched_addresses(client); if !addresses.is_null() { - dash_spv_ffi_array_destroy(*addresses); + dash_spv_ffi_array_destroy(addresses); } thread::sleep(Duration::from_millis(100)); } diff --git a/dash-spv-ffi/tests/test_client.rs b/dash-spv-ffi/tests/test_client.rs index 5afd30815..72dc98b23 100644 --- a/dash-spv-ffi/tests/test_client.rs +++ b/dash-spv-ffi/tests/test_client.rs @@ -173,11 +173,10 @@ mod tests { let client = dash_spv_ffi_client_new(config); let utxos = dash_spv_ffi_client_get_utxos(client); - assert!(utxos.len == 0 || !utxos.data.is_null()); - - if utxos.len > 0 { - let utxos_ptr = Box::into_raw(Box::new(utxos)); - dash_spv_ffi_array_destroy(utxos_ptr); + // adapt to pointer return + if !utxos.is_null() { + assert_eq!((*utxos).len, 0); + dash_spv_ffi_array_destroy(utxos); } dash_spv_ffi_client_destroy(client); diff --git a/dash-spv-ffi/tests/test_error.rs b/dash-spv-ffi/tests/test_error.rs index 13eacc77c..dd407b868 100644 --- a/dash-spv-ffi/tests/test_error.rs +++ b/dash-spv-ffi/tests/test_error.rs @@ -1,9 +1,11 @@ #[cfg(test)] mod tests { use dash_spv_ffi::*; + use serial_test::serial; use std::ffi::CStr; #[test] + #[serial] fn test_error_handling() { clear_last_error(); @@ -26,6 +28,7 @@ mod tests { } #[test] + #[serial] fn test_error_codes() { assert_eq!(FFIErrorCode::Success as i32, 0); assert_eq!(FFIErrorCode::NullPointer as i32, 1); @@ -41,6 +44,7 @@ mod tests { } #[test] + #[serial] fn test_handle_error() { let ok_result: Result = Ok(42); let handled = handle_error(ok_result); diff --git a/dash-spv-ffi/tests/test_event_callbacks.rs b/dash-spv-ffi/tests/test_event_callbacks.rs index 8ab81eb99..d2ed2067d 100644 --- a/dash-spv-ffi/tests/test_event_callbacks.rs +++ b/dash-spv-ffi/tests/test_event_callbacks.rs @@ -1,4 +1,3 @@ -use dash_spv_ffi::callbacks::{BlockCallback, TransactionCallback}; use dash_spv_ffi::*; use serial_test::serial; use std::ffi::{c_char, c_void, CStr, CString}; @@ -86,7 +85,7 @@ extern "C" fn test_compact_filter_matched_callback( }; // Convert wallet ID to string - let wallet_id_str = if wallet_id.is_null() { + let _wallet_id_str = if wallet_id.is_null() { String::new() } else { unsafe { CStr::from_ptr(wallet_id).to_string_lossy().into_owned() } @@ -103,8 +102,8 @@ extern "C" fn test_wallet_transaction_callback( txid: *const [u8; 32], confirmed: bool, amount: i64, - addresses: *const c_char, - block_height: u32, + _addresses: *const c_char, + _block_height: u32, is_ours: bool, user_data: *mut c_void, ) { diff --git a/dash-spv-ffi/tests/test_mempool_tracking.rs b/dash-spv-ffi/tests/test_mempool_tracking.rs index 7d1061584..e3395bfcd 100644 --- a/dash-spv-ffi/tests/test_mempool_tracking.rs +++ b/dash-spv-ffi/tests/test_mempool_tracking.rs @@ -1,12 +1,7 @@ -use dash_spv_ffi::callbacks::{ - MempoolConfirmedCallback, MempoolRemovedCallback, MempoolTransactionCallback, -}; use dash_spv_ffi::*; -use std::ffi::{CStr, CString}; +use std::ffi::CString; use std::os::raw::{c_char, c_void}; use std::sync::{Arc, Mutex}; -use std::thread; -use std::time::Duration; #[derive(Default)] struct TestCallbacks { diff --git a/dash-spv-ffi/tests/test_platform_integration.rs b/dash-spv-ffi/tests/test_platform_integration.rs index 337e41fb8..a0780a7ab 100644 --- a/dash-spv-ffi/tests/test_platform_integration.rs +++ b/dash-spv-ffi/tests/test_platform_integration.rs @@ -6,11 +6,11 @@ mod test_platform_integration { #[test] fn test_quorum_public_key_buffer_size_validation() { // Test that buffer size validation works correctly - let client: *mut FFIDashSpvClient = ptr::null_mut(); + let _client: *mut FFIDashSpvClient = ptr::null_mut(); let quorum_hash = [0u8; 32]; - let mut small_buffer = [0u8; 47]; // Too small - should fail + let _small_buffer = [0u8; 47]; // Too small - should fail let mut correct_buffer = [0u8; 48]; // Correct size - should succeed (if implemented) - let mut large_buffer = [0u8; 100]; // Larger than needed - should succeed (if implemented) + let _large_buffer = [0u8; 100]; // Larger than needed - should succeed (if implemented) unsafe { // Test with null client - should fail with NullPointer diff --git a/dash-spv-ffi/tests/unit/test_async_operations.rs b/dash-spv-ffi/tests/unit/test_async_operations.rs index 927f81c2f..30373b9e0 100644 --- a/dash-spv-ffi/tests/unit/test_async_operations.rs +++ b/dash-spv-ffi/tests/unit/test_async_operations.rs @@ -102,18 +102,6 @@ mod tests { let (client, config, _temp_dir) = create_test_client(); assert!(!client.is_null()); - extern "C" fn null_data_completion( - _success: bool, - _error: *const c_char, - user_data: *mut c_void, - ) { - // Don't assert here - just verify user_data is what we expect - // The callback might not be called if sync fails early - if !user_data.is_null() { - panic!("Expected null user_data, got non-null pointer"); - } - } - // Don't call sync_to_tip on unstarted client as it will hang // Test null user_data handling in a different way println!("Testing null user_data safety without starting client"); @@ -365,7 +353,6 @@ mod tests { race_conditions: Arc, concurrent_callbacks: Arc, max_concurrent: Arc, - barrier: Arc, shared_state: Arc>>, } @@ -374,7 +361,6 @@ mod tests { race_conditions: race_conditions.clone(), concurrent_callbacks: concurrent_callbacks.clone(), max_concurrent: max_concurrent.clone(), - barrier: barrier.clone(), shared_state: Arc::new(Mutex::new(Vec::new())), }; diff --git a/dash-spv-ffi/tests/unit/test_error_handling.rs b/dash-spv-ffi/tests/unit/test_error_handling.rs index 690ed9db0..c32b92b9c 100644 --- a/dash-spv-ffi/tests/unit/test_error_handling.rs +++ b/dash-spv-ffi/tests/unit/test_error_handling.rs @@ -156,6 +156,7 @@ mod tests { } #[test] + #[serial] fn test_null_pointer_error_handling() { // Test null_check! macro behavior unsafe { @@ -173,25 +174,17 @@ mod tests { #[test] fn test_invalid_enum_handling() { - // Test with invalid network value - // Since we can't safely create an invalid enum in Rust, we'll test the C API - // by calling it with a raw value that doesn't correspond to any valid variant + // Use a valid enum value to avoid UB in Rust tests. If invalid raw inputs + // need to be tested, do so from a C test or add a raw-int FFI entrypoint. unsafe { - // dash_spv_ffi_config_new expects FFINetwork but we'll cast an invalid i32 - // This simulates what could happen from C code - let config = { - extern "C" { - fn dash_spv_ffi_config_new(network: i32) -> *mut FFIClientConfig; - } - dash_spv_ffi_config_new(999) - }; - // Should still create a config (defaults to Dash) + let config = dash_spv_ffi_config_new(FFINetwork::Dash); assert!(!config.is_null()); dash_spv_ffi_config_destroy(config); } } #[test] + #[serial] fn test_handle_error_helper() { // Test Ok case let ok_result: Result = Ok(42); diff --git a/dash-spv-ffi/tests/unit/test_memory_management.rs b/dash-spv-ffi/tests/unit/test_memory_management.rs index e1141860a..326965f50 100644 --- a/dash-spv-ffi/tests/unit/test_memory_management.rs +++ b/dash-spv-ffi/tests/unit/test_memory_management.rs @@ -199,6 +199,8 @@ mod tests { data: std::ptr::null_mut(), len: 0, capacity: 0, + elem_size: 0, + elem_align: 1, }; dash_spv_ffi_array_destroy(Box::into_raw(Box::new(null_array))); } diff --git a/dash-spv/Cargo.toml b/dash-spv/Cargo.toml index 76d373a9e..fffe0e2bf 100644 --- a/dash-spv/Cargo.toml +++ b/dash-spv/Cargo.toml @@ -64,4 +64,8 @@ path = "src/main.rs" [lib] name = "dash_spv" -path = "src/lib.rs" \ No newline at end of file +path = "src/lib.rs" + +[features] +# Gate incomplete mock-dependent tests to avoid "unexpected cfg" warnings. +skip_mock_implementation_incomplete = [] \ No newline at end of file diff --git a/dash-spv/examples/filter_sync.rs b/dash-spv/examples/filter_sync.rs index 317609049..306724b84 100644 --- a/dash-spv/examples/filter_sync.rs +++ b/dash-spv/examples/filter_sync.rs @@ -3,7 +3,7 @@ use dash_spv::network::MultiPeerNetworkManager; use dash_spv::storage::MemoryStorageManager; use dash_spv::{init_logging, ClientConfig, DashSpvClient}; -use dashcore::{Address, Network}; +use dashcore::Address; use key_wallet::wallet::managed_wallet_info::ManagedWalletInfo; use key_wallet_manager::spv_wallet_manager::SPVWalletManager; use key_wallet_manager::wallet_manager::WalletManager; diff --git a/dash-spv/examples/reorg_demo.rs b/dash-spv/examples/reorg_demo.rs deleted file mode 100644 index 36e7eae3c..000000000 --- a/dash-spv/examples/reorg_demo.rs +++ /dev/null @@ -1,127 +0,0 @@ -// TODO: This example needs to be updated as the reorganize() method was removed -// The reorganization logic is now handled internally by the SPV client -// and wallet state is managed through the WalletInterface - -#![allow(dead_code)] - -//! Demo showing that chain reorganization now works without borrow conflicts - -// Temporarily disable this example -fn main() { - println!("This example is temporarily disabled pending updates to use the new architecture"); -} - -#[cfg(skip_example)] -mod disabled_example { - use dash_spv::chain::{ChainWork, Fork, ReorgManager}; - use dash_spv::storage::{MemoryStorageManager, StorageManager}; - use dash_spv::types::ChainState; - use dashcore::{blockdata::constants::genesis_block, Header as BlockHeader, Network}; - use dashcore_hashes::Hash; - use key_wallet::wallet::managed_wallet_info::ManagedWalletInfo; - use key_wallet_manager::spv_wallet_manager::SPVWalletManager; - use key_wallet_manager::wallet_manager::WalletManager; - use std::sync::Arc; - use tokio::sync::RwLock; - - fn create_test_header(prev: &BlockHeader, nonce: u32) -> BlockHeader { - let mut header = prev.clone(); - header.prev_blockhash = prev.block_hash(); - header.nonce = nonce; - header.time = prev.time + 600; // 10 minutes later - header - } - - #[tokio::main] - async fn main() -> Result<(), Box> { - println!("🔧 Chain Reorganization Demo - Testing Borrow Conflict Fix\n"); - - // Create test components - let network = Network::Dash; - let genesis = genesis_block(network).header; - let mut chain_state = ChainState::new_for_network(network); - let wallet_manager = Arc::new(RwLock::new(SPVWalletManager::with_base(WalletManager::< - ManagedWalletInfo, - >::new()))); - let mut storage = MemoryStorageManager::new().await?; - - println!("📦 Building main chain: genesis -> block1 -> block2"); - - // Build main chain: genesis -> block1 -> block2 - let block1 = create_test_header(&genesis, 1); - let block2 = create_test_header(&block1, 2); - - // Store main chain - storage.store_headers(&[genesis]).await?; - storage.store_headers(&[block1]).await?; - storage.store_headers(&[block2]).await?; - - // Update chain state - chain_state.add_header(genesis); - chain_state.add_header(block1); - chain_state.add_header(block2); - - println!("✅ Main chain height: {}", chain_state.get_height()); - - println!("\n📦 Building fork: genesis -> block1' -> block2' -> block3'"); - - // Build fork chain: genesis -> block1' -> block2' -> block3' - let block1_fork = create_test_header(&genesis, 100); // Different nonce - let block2_fork = create_test_header(&block1_fork, 101); - let block3_fork = create_test_header(&block2_fork, 102); - - // Create fork with more work - let fork = Fork { - fork_point: genesis.block_hash(), - fork_height: 0, // Fork from genesis - tip_hash: block3_fork.block_hash(), - tip_height: 3, - headers: vec![block1_fork, block2_fork, block3_fork], - chain_work: ChainWork::from_bytes([255u8; 32]), // Maximum work - }; - - println!("✅ Fork chain height: {}", fork.tip_height); - println!("✅ Fork has more work than main chain"); - - println!("\n🔄 Attempting reorganization..."); - println!(" This previously failed with borrow conflict!"); - - // Create reorg manager - let reorg_manager = ReorgManager::new(100, false); - - // This should now work without borrow conflicts! - // Note: reorganize now takes wallet as an Arc> where W: WalletInterface - match reorg_manager - .reorganize(&mut chain_state, wallet_manager.clone(), &fork, &mut storage) - .await - { - Ok(event) => { - println!("\n✅ Reorganization SUCCEEDED!"); - println!( - " - Common ancestor: {} at height {}", - event.common_ancestor, event.common_height - ); - println!(" - Disconnected {} headers", event.disconnected_headers.len()); - println!(" - Connected {} headers", event.connected_headers.len()); - println!(" - New chain height: {}", chain_state.get_height()); - - // Verify new headers were stored - let header_at_3 = storage.get_header(3).await?; - if header_at_3.is_some() { - println!("\n✅ New chain tip verified in storage!"); - } - - println!("\n🎉 Borrow conflict has been resolved!"); - println!(" The reorganization now uses a phased approach:"); - println!(" 1. Read phase: Collect all necessary data"); - println!(" 2. Write phase: Apply changes using only StorageManager"); - } - Err(e) => { - println!("\n❌ Reorganization failed: {}", e); - println!(" This suggests the borrow conflict still exists."); - } - } - - Ok(()) - } -} // end of disabled_example module diff --git a/dash-spv/peer_reputation.json b/dash-spv/peer_reputation.json deleted file mode 100644 index 680fb2b19..000000000 --- a/dash-spv/peer_reputation.json +++ /dev/null @@ -1,57 +0,0 @@ -[ - [ - "34.210.26.195:19999", - { - "score": 0, - "ban_count": 0, - "positive_actions": 0, - "negative_actions": 0, - "connection_attempts": 1, - "successful_connections": 0 - } - ], - [ - "34.214.48.68:19999", - { - "score": 0, - "ban_count": 0, - "positive_actions": 0, - "negative_actions": 0, - "connection_attempts": 2, - "successful_connections": 0 - } - ], - [ - "34.210.84.163:19999", - { - "score": 0, - "ban_count": 0, - "positive_actions": 0, - "negative_actions": 0, - "connection_attempts": 3, - "successful_connections": 0 - } - ], - [ - "34.217.58.158:19999", - { - "score": 0, - "ban_count": 0, - "positive_actions": 0, - "negative_actions": 0, - "connection_attempts": 2, - "successful_connections": 0 - } - ], - [ - "18.237.170.32:19999", - { - "score": 0, - "ban_count": 0, - "positive_actions": 0, - "negative_actions": 0, - "connection_attempts": 1, - "successful_connections": 0 - } - ] -] \ No newline at end of file diff --git a/dash-spv/src/chain/orphan_pool.rs b/dash-spv/src/chain/orphan_pool.rs index 3ffa4c578..94de7086c 100644 --- a/dash-spv/src/chain/orphan_pool.rs +++ b/dash-spv/src/chain/orphan_pool.rs @@ -337,7 +337,6 @@ mod tests { fn test_process_attempts() { let mut pool = OrphanPool::new(); let header = create_test_header(BlockHash::from([0u8; 32]), 1); - let block_hash = header.block_hash(); pool.add_orphan(header); diff --git a/dash-spv/src/chain/reorg.rs b/dash-spv/src/chain/reorg.rs index 6abb6676c..157f0241c 100644 --- a/dash-spv/src/chain/reorg.rs +++ b/dash-spv/src/chain/reorg.rs @@ -543,14 +543,6 @@ mod tests { use dashcore::Network; use dashcore_hashes::Hash; - fn create_test_header(prev: &BlockHeader, nonce: u32) -> BlockHeader { - let mut header = prev.clone(); - header.prev_blockhash = prev.block_hash(); - header.nonce = nonce; - header.time = prev.time + 600; // 10 minutes later - header - } - #[test] fn test_reorg_validation() { let reorg_mgr = ReorgManager::new(100, false); diff --git a/dash-spv/src/client/block_processor.rs b/dash-spv/src/client/block_processor.rs index c7bc12065..1970ceb89 100644 --- a/dash-spv/src/client/block_processor.rs +++ b/dash-spv/src/client/block_processor.rs @@ -1,12 +1,12 @@ //! Block processing functionality for the Dash SPV client. -use std::collections::{HashMap, HashSet}; +use std::collections::HashSet; use std::sync::Arc; use tokio::sync::{mpsc, oneshot, Mutex, RwLock}; use crate::error::{Result, SpvError}; use crate::storage::StorageManager; -use crate::types::{AddressBalance, SpvEvent, SpvStats}; +use crate::types::{SpvEvent, SpvStats}; use key_wallet_manager::wallet_interface::WalletInterface; /// Task for the block processing worker. @@ -571,19 +571,8 @@ impl Result { - // WalletInterface doesn't expose per-address balance - // Return empty balance for now - Ok(AddressBalance { - confirmed: dashcore::Amount::from_sat(0), - unconfirmed: dashcore::Amount::from_sat(0), - pending: dashcore::Amount::from_sat(0), - pending_instant: dashcore::Amount::from_sat(0), - }) - } + */ /// Update chain state with information from the processed block. async fn update_chain_state_with_block(&mut self, block: &dashcore::Block) -> Result<()> { diff --git a/dash-spv/src/client/block_processor_test.rs b/dash-spv/src/client/block_processor_test.rs index a5507b116..59a64295f 100644 --- a/dash-spv/src/client/block_processor_test.rs +++ b/dash-spv/src/client/block_processor_test.rs @@ -8,22 +8,19 @@ mod tests { use crate::storage::StorageManager; use crate::types::{SpvEvent, SpvStats}; use dashcore::{blockdata::constants::genesis_block, Block, Network, Transaction}; - use std::collections::HashSet; use std::sync::Arc; use tokio::sync::{mpsc, oneshot, Mutex, RwLock}; // Mock WalletInterface implementation for testing struct MockWallet { - network: Network, processed_blocks: Arc>>, processed_transactions: Arc>>, } impl MockWallet { - fn new(network: Network) -> Self { + fn new() -> Self { Self { - network, processed_blocks: Arc::new(Mutex::new(Vec::new())), processed_transactions: Arc::new(Mutex::new(Vec::new())), } @@ -83,7 +80,7 @@ mod tests { let (task_tx, task_rx) = mpsc::unbounded_channel(); let (event_tx, event_rx) = mpsc::unbounded_channel(); let stats = Arc::new(RwLock::new(SpvStats::default())); - let wallet = Arc::new(RwLock::new(MockWallet::new(Network::Dash))); + let wallet = Arc::new(RwLock::new(MockWallet::new())); let storage = Arc::new(Mutex::new(MemoryStorageManager::new().await.unwrap())); let processor = BlockProcessor::new( task_rx, @@ -215,9 +212,7 @@ mod tests { #[tokio::test] async fn test_process_compact_filter_no_match() { // Create a custom mock wallet that returns false for filter checks - struct NonMatchingWallet { - network: Network, - } + struct NonMatchingWallet {} #[async_trait::async_trait] impl key_wallet_manager::wallet_interface::WalletInterface for NonMatchingWallet { @@ -258,9 +253,7 @@ mod tests { let (task_tx, task_rx) = mpsc::unbounded_channel(); let (event_tx, mut event_rx) = mpsc::unbounded_channel(); let stats = Arc::new(RwLock::new(SpvStats::default())); - let wallet = Arc::new(RwLock::new(NonMatchingWallet { - network: Network::Dash, - })); + let wallet = Arc::new(RwLock::new(NonMatchingWallet {})); let storage = Arc::new(Mutex::new(MemoryStorageManager::new().await.unwrap())); let processor = @@ -305,7 +298,7 @@ mod tests { #[tokio::test] async fn test_process_mempool_transaction() { - let (processor, task_tx, event_rx, wallet, _storage) = setup_processor().await; + let (processor, task_tx, _event_rx, wallet, _storage) = setup_processor().await; // Create a test transaction let block = create_test_block(Network::Dash); diff --git a/dash-spv/src/client/config.rs b/dash-spv/src/client/config.rs index 30ca26e4a..69736603e 100644 --- a/dash-spv/src/client/config.rs +++ b/dash-spv/src/client/config.rs @@ -4,7 +4,7 @@ use std::net::SocketAddr; use std::path::PathBuf; use std::time::Duration; -use dashcore::{Address, Network, ScriptBuf}; +use dashcore::Network; // Serialization removed due to complex Address types use crate::types::ValidationMode; @@ -22,6 +22,7 @@ pub enum MempoolStrategy { /// Configuration for the Dash SPV client. #[derive(Debug, Clone)] +#[repr(C)] pub struct ClientConfig { /// Network to connect to. pub network: Network, diff --git a/dash-spv/src/client/config_test.rs b/dash-spv/src/client/config_test.rs index 7fbf80940..800cdb2c5 100644 --- a/dash-spv/src/client/config_test.rs +++ b/dash-spv/src/client/config_test.rs @@ -4,7 +4,7 @@ mod tests { use crate::client::config::{ClientConfig, MempoolStrategy}; use crate::types::ValidationMode; - use dashcore::{Address, Network}; + use dashcore::Network; use std::net::SocketAddr; use std::path::PathBuf; @@ -61,7 +61,6 @@ mod tests { #[test] fn test_builder_pattern() { let path = PathBuf::from("/test/storage"); - let addr: SocketAddr = "1.2.3.4:9999".parse().unwrap(); let config = ClientConfig::mainnet() .with_storage_path(path.clone()) diff --git a/dash-spv/src/client/mod.rs b/dash-spv/src/client/mod.rs index 8fb6cae61..4cabe073d 100644 --- a/dash-spv/src/client/mod.rs +++ b/dash-spv/src/client/mod.rs @@ -135,24 +135,6 @@ impl< ) } - /// Helper to process balance changes with error handling. - async fn process_address_balance( - &self, - address: &dashcore::Address, - success_handler: F, - ) -> Option - where - F: FnOnce(AddressBalance) -> T, - { - match self.get_address_balance(address).await { - Ok(balance) => Some(success_handler(balance)), - Err(e) => { - tracing::error!("Failed to get balance for address {}: {}", address, e); - None - } - } - } - // UTXO mismatch checking removed - handled by external wallet // Address mismatch checking removed - handled by external wallet @@ -208,7 +190,7 @@ impl< // Create sync manager let received_filter_heights = stats.read().await.received_filter_heights.clone(); tracing::info!("Creating sequential sync manager"); - let mut sync_manager = + let sync_manager = SequentialSyncManager::new(&config, received_filter_heights, wallet.clone()) .map_err(SpvError::Sync)?; @@ -554,6 +536,7 @@ impl< } /// Update mempool filter with wallet's monitored addresses. + #[allow(dead_code)] async fn update_mempool_filter(&mut self) { // TODO: Get monitored addresses from wallet // For now, create empty filter until wallet integration is complete diff --git a/dash-spv/src/error.rs b/dash-spv/src/error.rs index 5574e1ac5..e5d8c6710 100644 --- a/dash-spv/src/error.rs +++ b/dash-spv/src/error.rs @@ -112,6 +112,21 @@ pub enum StorageError { LockPoisoned(String), } +impl Clone for StorageError { + fn clone(&self) -> Self { + match self { + StorageError::Corruption(s) => StorageError::Corruption(s.clone()), + StorageError::NotFound(s) => StorageError::NotFound(s.clone()), + StorageError::WriteFailed(s) => StorageError::WriteFailed(s.clone()), + StorageError::ReadFailed(s) => StorageError::ReadFailed(s.clone()), + StorageError::Io(err) => StorageError::Io(io::Error::new(err.kind(), err.to_string())), + StorageError::Serialization(s) => StorageError::Serialization(s.clone()), + StorageError::InconsistentState(s) => StorageError::InconsistentState(s.clone()), + StorageError::LockPoisoned(s) => StorageError::LockPoisoned(s.clone()), + } + } +} + /// Validation-related errors. #[derive(Debug, Error)] pub enum ValidationError { diff --git a/dash-spv/src/mempool_filter.rs b/dash-spv/src/mempool_filter.rs index 1371316aa..986cc7070 100644 --- a/dash-spv/src/mempool_filter.rs +++ b/dash-spv/src/mempool_filter.rs @@ -195,68 +195,45 @@ impl MempoolFilter { } } -// Tests temporarily disabled during WatchItem removal -// TODO: Rewrite tests to work with wallet integration -#[cfg(test_disabled)] +// Tests for mempool filter functionality with wallet integration +#[cfg(test)] mod tests { use super::*; use dashcore::{Network, OutPoint, ScriptBuf, TxIn, TxOut, Witness}; use std::str::FromStr; - // Helper to create a test address - fn test_address(network: Network) -> Address { - Address::from_str("XjbaGWaGnvEtuQAUoBgDxJWe8ZNv45upG2") - .unwrap() - .require_network(network) - .unwrap() + // Stub types for ignored tests + #[derive(Clone)] + enum WatchItem { + Address(Address), + Script(ScriptBuf), + Outpoint(OutPoint), } - // Helper to create another test address - fn test_address2(network: Network) -> Address { - Address::from_str("Xan9iCVe1q5jYRDZ4VSMCtBjq2VyQA3Dge") - .unwrap() - .require_network(network) - .unwrap() - } + impl WatchItem { + fn address(addr: Address) -> Self { + WatchItem::Address(addr) + } - // Helper to create a test transaction - fn create_test_transaction(outputs: Vec<(Address, u64)>, inputs: Vec) -> Transaction { - let mut tx_outputs = vec![]; - for (addr, amount) in outputs { - tx_outputs.push(TxOut { - value: amount, - script_pubkey: addr.script_pubkey(), - }); + fn address_from_height(addr: Address, _height: u32) -> Self { + WatchItem::Address(addr) } - let mut tx_inputs = vec![]; - for outpoint in inputs { - tx_inputs.push(TxIn { - previous_output: outpoint, - script_sig: ScriptBuf::new(), - sequence: 0xffffffff, - witness: Witness::new(), - }); + fn script(script: ScriptBuf) -> Self { + WatchItem::Script(script) } - Transaction { - version: 1, - lock_time: 0, - input: tx_inputs, - output: tx_outputs, - special_transaction_payload: None, + fn outpoint(outpoint: OutPoint) -> Self { + WatchItem::Outpoint(outpoint) } } - // MockWallet for test purposes only - #[cfg(test)] struct MockWallet { network: Network, watched_addresses: HashSet
, utxos: HashSet, } - #[cfg(test)] impl MockWallet { fn new(network: Network) -> Self { Self { @@ -270,63 +247,135 @@ mod tests { self.watched_addresses.insert(address); } - fn add_utxo(&mut self, outpoint: OutPoint) { - self.utxos.insert(outpoint); + fn network(&self) -> &Network { + &self.network } - fn network(&self) -> Network { - self.network + fn watched_addresses(&self) -> &HashSet
{ + &self.watched_addresses } - fn has_utxo(&self, outpoint: &OutPoint) -> bool { - self.utxos.contains(outpoint) + fn utxos(&self) -> &HashSet { + &self.utxos } + } - fn is_transaction_relevant(&self, tx: &Transaction) -> bool { - // Check if any input spends our UTXOs - for input in &tx.input { - if self.utxos.contains(&input.previous_output) { - return true; - } - } + // Helper to create deterministically generated test addresses + fn create_test_addresses(network: Network, count: usize) -> Vec
{ + use dashcore::hashes::{sha256, Hash}; + use dashcore::{secp256k1, PrivateKey, PublicKey}; + + let mut addresses = Vec::new(); + let secp = secp256k1::Secp256k1::new(); - // Check if any output is to our watched addresses - for output in &tx.output { - if let Ok(address) = Address::from_script(&output.script_pubkey, self.network) { - if self.watched_addresses.contains(&address) { - return true; - } + // Fixed seed for deterministic generation + let seed = b"dash-spv-test-seed"; + + let mut index: u64 = 0; + while addresses.len() < count { + // Create deterministic secret: SHA256(fixed_seed + index) + let mut data = seed.to_vec(); + data.extend_from_slice(&index.to_le_bytes()); + + let secret_bytes = sha256::Hash::hash(&data).to_byte_array(); + + // Try to create secp256k1 SecretKey from the hash + match secp256k1::SecretKey::from_byte_array(&secret_bytes) { + Ok(secret_key) => { + // Create PrivateKey from SecretKey + let private_key = PrivateKey::new(secret_key, network); + + // Create PublicKey from PrivateKey + let public_key = PublicKey::from_private_key(&secp, &private_key); + + // Create P2PKH address from PublicKey + let address = Address::p2pkh(&public_key, network); + + addresses.push(address); + } + Err(_) => { + // Skip this index if key derivation fails + tracing::trace!("Skipping index {}: invalid secret key", index); } } - false + index += 1; } - fn calculate_net_amount(&self, tx: &Transaction) -> i64 { - let mut net_amount: i64 = 0; + addresses + } - // Subtract spent amounts - for input in &tx.input { - if self.has_utxo(&input.previous_output) { - // In real implementation, we'd look up the actual value - // For testing, assume 10000 sats per UTXO - net_amount -= 10000; - } - } + // Test function to verify deterministic address generation + #[cfg(test)] + fn test_deterministic_addresses() { + use dashcore::{AddressType, Network}; + + // Generate addresses twice with the same parameters + let addresses1 = create_test_addresses(Network::Dash, 3); + let addresses2 = create_test_addresses(Network::Dash, 3); + + // Verify they are identical + assert_eq!(addresses1.len(), 3); + assert_eq!(addresses2.len(), 3); + assert_eq!(addresses1, addresses2); + + // Verify they are valid P2PKH addresses + for addr in &addresses1 { + assert_eq!(addr.address_type(), Some(AddressType::P2pkh)); + assert_eq!(*addr.network(), Network::Dash); + } - // Add received amounts - for output in &tx.output { - if let Ok(address) = Address::from_script(&output.script_pubkey, self.network) { - if self.watched_addresses.contains(&address) { - net_amount += output.value as i64; - } - } - } + // Test with different networks + let testnet_addresses = create_test_addresses(Network::Testnet, 2); + assert_eq!(testnet_addresses.len(), 2); + for addr in &testnet_addresses { + assert_eq!(addr.address_type(), Some(AddressType::P2pkh)); + assert_eq!(*addr.network(), Network::Testnet); + } + + println!("Deterministic address generation test passed!"); + println!("Dash addresses: {:?}", addresses1); + println!("Testnet addresses: {:?}", testnet_addresses); + } + + // Helper to create a test transaction + fn create_test_transaction(outputs: Vec<(Address, u64)>, inputs: Vec) -> Transaction { + let mut tx_outputs = vec![]; + for (addr, amount) in outputs { + tx_outputs.push(TxOut { + value: amount, + script_pubkey: addr.script_pubkey(), + }); + } + + let mut tx_inputs = vec![]; + for outpoint in inputs { + tx_inputs.push(TxIn { + previous_output: outpoint, + script_sig: ScriptBuf::new(), + sequence: 0xffffffff, + witness: Witness::new(), + }); + } - net_amount + Transaction { + version: 1, + lock_time: 0, + input: tx_inputs, + output: tx_outputs, + special_transaction_payload: None, } } + // Stub implementations for ignored tests + fn test_address(network: Network) -> Address { + create_test_addresses(network, 1)[0].clone() + } + + fn test_address2(network: Network) -> Address { + create_test_addresses(network, 2)[1].clone() + } + #[tokio::test] async fn test_selective_strategy() { let mempool_state = Arc::new(RwLock::new(MempoolState::default())); @@ -386,7 +435,7 @@ mod tests { dashcore::Amount::from_sat(0), false, false, - HashSet::new(), + Vec::new(), 0, )); state.add_transaction(UnconfirmedTransaction::new( @@ -400,7 +449,7 @@ mod tests { dashcore::Amount::from_sat(0), false, false, - HashSet::new(), + Vec::new(), 0, )); drop(state); @@ -415,18 +464,21 @@ mod tests { #[tokio::test] async fn test_is_transaction_relevant_with_address() { let network = Network::Dash; - let addr1 = test_address(network); - let addr2 = test_address2(network); + + // Create a wallet and get addresses from it + let addresses = create_test_addresses(network, 2); + let addr1 = &addresses[0]; + let addr2 = &addresses[1]; let mempool_state = Arc::new(RwLock::new(MempoolState::default())); - let watch_items = vec![WatchItem::address(addr1.clone())]; + let watched_addresses = vec![addr1.clone()].into_iter().collect(); let filter = MempoolFilter::new( MempoolStrategy::Selective, Duration::from_secs(300), 1000, mempool_state, - watch_items, + watched_addresses, network, ); @@ -435,192 +487,88 @@ mod tests { assert!(filter.is_transaction_relevant(&tx1)); // Transaction sending to unwatched address should not be relevant - let tx2 = create_test_transaction(vec![(addr2, 50000)], vec![]); + let tx2 = create_test_transaction(vec![(addr2.clone(), 50000)], vec![]); assert!(!filter.is_transaction_relevant(&tx2)); } #[tokio::test] async fn test_is_transaction_relevant_with_script() { let network = Network::Dash; - let addr = test_address(network); - let script = addr.script_pubkey(); + + // Create a wallet and get addresses from it + let addresses = create_test_addresses(network, 2); + let addr = &addresses[0]; + let addr2 = &addresses[1]; let mempool_state = Arc::new(RwLock::new(MempoolState::default())); - let watch_items = vec![WatchItem::Script(script.clone())]; + let watched_addresses = vec![addr.clone()].into_iter().collect(); let filter = MempoolFilter::new( MempoolStrategy::Selective, Duration::from_secs(300), 1000, mempool_state, - watch_items, + watched_addresses, network, ); // Transaction with watched script should be relevant - let tx = create_test_transaction(vec![(addr, 50000)], vec![]); + let tx = create_test_transaction(vec![(addr.clone(), 50000)], vec![]); assert!(filter.is_transaction_relevant(&tx)); // Transaction without watched script should not be relevant - let addr2 = test_address2(network); - let tx2 = create_test_transaction(vec![(addr2, 50000)], vec![]); + let tx2 = create_test_transaction(vec![(addr2.clone(), 50000)], vec![]); assert!(!filter.is_transaction_relevant(&tx2)); } #[tokio::test] async fn test_is_transaction_relevant_with_outpoint() { let network = Network::Dash; - let addr = test_address(network); - // Create a specific outpoint to watch - let watched_outpoint = OutPoint { - txid: Txid::from_str( - "2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a", - ) - .unwrap(), - vout: 0, - }; + // Create a wallet and get an address from it + let addresses = create_test_addresses(network, 1); + let addr = &addresses[0]; let mempool_state = Arc::new(RwLock::new(MempoolState::default())); - let watch_items = vec![WatchItem::Outpoint(watched_outpoint)]; + let watched_addresses = vec![addr.clone()].into_iter().collect(); let filter = MempoolFilter::new( MempoolStrategy::Selective, Duration::from_secs(300), 1000, mempool_state, - watch_items, + watched_addresses, network, ); - // Transaction spending watched outpoint should be relevant - let tx = create_test_transaction(vec![(addr.clone(), 50000)], vec![watched_outpoint]); + // Transaction receiving to watched address should be relevant + let tx = create_test_transaction(vec![(addr.clone(), 50000)], vec![]); assert!(filter.is_transaction_relevant(&tx)); - // Transaction not spending watched outpoint should not be relevant - let other_outpoint = OutPoint { - txid: Txid::from_str( - "6363636363636363636363636363636363636363636363636363636363636363", - ) - .unwrap(), - vout: 1, + // Transaction not involving watched address should not be relevant + // Create a completely different address not in our watched list + let other_addr = { + // Create from script to ensure it's different from watched addresses + use dashcore::script::ScriptBuf; + let script = + ScriptBuf::from_hex("76a914123456789012345678901234567890123456789088ac").unwrap(); + Address::from_script(&script, network).unwrap() }; - let tx2 = create_test_transaction(vec![(addr, 50000)], vec![other_outpoint]); + let tx2 = create_test_transaction(vec![(other_addr, 50000)], vec![]); assert!(!filter.is_transaction_relevant(&tx2)); } - #[tokio::test] - #[ignore = "requires real Wallet implementation"] - async fn test_process_transaction_outgoing() { - let network = Network::Dash; - let addr = test_address(network); - - let mempool_state = Arc::new(RwLock::new(MempoolState::default())); - let watch_items = vec![WatchItem::address(addr.clone())]; - - let filter = MempoolFilter::new( - MempoolStrategy::Selective, - Duration::from_secs(300), - 1000, - mempool_state, - watch_items, - network, - ); - - let mut wallet = MockWallet::new(network); - wallet.add_watched_address(addr.clone()); - - // Add a UTXO that we own - let our_outpoint = OutPoint { - txid: Txid::from_str( - "0101010101010101010101010101010101010101010101010101010101010101", - ) - .unwrap(), - vout: 0, - }; - wallet.add_utxo(our_outpoint); - - // Create transaction spending our UTXO - let tx = create_test_transaction(vec![(addr.clone(), 5000)], vec![our_outpoint]); - - // let result = filter.process_transaction(tx.clone(), &wallet).await; - // assert!(result.is_some()); - // - // let unconfirmed_tx = result.unwrap(); - // assert_eq!(unconfirmed_tx.transaction.txid(), tx.txid()); - // assert!(unconfirmed_tx.is_outgoing); - // assert_eq!(unconfirmed_tx.addresses.len(), 1); - // assert_eq!(unconfirmed_tx.addresses[0], addr); - // assert_eq!(unconfirmed_tx.net_amount, -5000); // Lost 10000, received 5000 - } - - #[tokio::test] - #[ignore = "requires real Wallet implementation"] - async fn test_process_transaction_incoming() { - let network = Network::Dash; - let addr = test_address(network); - - let mempool_state = Arc::new(RwLock::new(MempoolState::default())); - let watch_items = vec![WatchItem::address(addr.clone())]; - - let filter = MempoolFilter::new( - MempoolStrategy::Selective, - Duration::from_secs(300), - 1000, - mempool_state, - watch_items, - network, - ); - - let mut wallet = MockWallet::new(network); - wallet.add_watched_address(addr.clone()); - - // Create transaction sending to our address (not spending our UTXOs) - let tx = create_test_transaction(vec![(addr.clone(), 25000)], vec![]); - - // let result = filter.process_transaction(tx.clone(), &wallet).await; - // assert!(result.is_some()); - // - // let unconfirmed_tx = result.unwrap(); - // assert_eq!(unconfirmed_tx.transaction.txid(), tx.txid()); - // assert!(!unconfirmed_tx.is_outgoing); - // assert_eq!(unconfirmed_tx.addresses.len(), 1); - // assert_eq!(unconfirmed_tx.addresses[0], addr); - // assert_eq!(unconfirmed_tx.net_amount, 25000); - } - - #[tokio::test] - #[ignore = "requires real Wallet implementation"] - async fn test_process_transaction_fetch_all_strategy() { - let network = Network::Dash; - let watched_addr = test_address(network); - let unwatched_addr = test_address2(network); - - let mempool_state = Arc::new(RwLock::new(MempoolState::default())); - let watch_items = vec![WatchItem::address(watched_addr.clone())]; - - let filter = MempoolFilter::new( - MempoolStrategy::FetchAll, - Duration::from_secs(300), - 1000, - mempool_state, - watch_items, - network, - ); + // TODO: Implement test for processing outgoing transactions + // This test should verify that when we spend our own UTXOs, the transaction + // is properly processed and marked as outgoing with correct net_amount calculation - let mut wallet = MockWallet::new(network); - wallet.add_watched_address(watched_addr.clone()); + // TODO: Implement test for processing incoming transactions + // This test should verify that when we receive payments to our addresses, + // the transaction is properly processed and marked as incoming with positive net_amount - // Transaction to watched address should be processed - let tx1 = create_test_transaction(vec![(watched_addr.clone(), 10000)], vec![]); - // let result1 = filter.process_transaction(tx1, &wallet).await; - // assert!(result1.is_some()); - - // Transaction to unwatched address should NOT be processed (even with FetchAll) - let tx2 = create_test_transaction(vec![(unwatched_addr, 10000)], vec![]); - // let result2 = filter.process_transaction(tx2, &wallet).await; - // assert!(result2.is_none()); - } + // TODO: Implement test for FetchAll strategy behavior + // This test should verify that with FetchAll strategy, transactions to watched addresses + // are processed while transactions to unwatched addresses are not processed (filtered out) #[tokio::test] async fn test_capacity_limits() { @@ -652,7 +600,7 @@ mod tests { dashcore::Amount::from_sat(0), false, false, - HashSet::new(), + Vec::new(), 0, )); } @@ -695,17 +643,18 @@ mod tests { dashcore::Amount::from_sat(0), false, false, - HashSet::new(), + Vec::new(), 0, ); let old_txid = old_tx.txid(); state.transactions.insert(old_txid, old_tx); // Manually set the first_seen time to be old - if let Some(tx) = state.transactions.get_mut(&old_txid) { - // This is a hack since we can't modify Instant directly - // In real tests, we'd use a time abstraction - } + // TODO: Implement time manipulation for testing + // if let Some(tx) = state.transactions.get_mut(&old_txid) { + // // This is a hack since we can't modify Instant directly + // // In real tests, we'd use a time abstraction + // } // Add a recent transaction let recent_tx = UnconfirmedTransaction::new( @@ -719,7 +668,7 @@ mod tests { dashcore::Amount::from_sat(0), false, false, - HashSet::new(), + Vec::new(), 0, ); let recent_txid = recent_tx.txid(); @@ -735,6 +684,11 @@ mod tests { assert!(pruned.is_empty() || !pruned.is_empty()); // Tautology, but shows the test ran } + #[test] + fn test_deterministic_address_generation() { + test_deterministic_addresses(); + } + #[tokio::test] async fn test_bloom_filter_strategy() { let mempool_state = Arc::new(RwLock::new(MempoolState::default())); @@ -755,19 +709,27 @@ mod tests { } #[tokio::test] + #[ignore = "requires MockWallet implementation"] async fn test_address_with_earliest_height() { let network = Network::Dash; let addr = test_address(network); let mempool_state = Arc::new(RwLock::new(MempoolState::default())); let watch_items = vec![WatchItem::address_from_height(addr.clone(), 100000)]; + let watched_addresses: HashSet
= watch_items + .into_iter() + .filter_map(|item| match item { + WatchItem::Address(addr) => Some(addr), + _ => None, + }) + .collect(); let filter = MempoolFilter::new( MempoolStrategy::Selective, Duration::from_secs(300), 1000, mempool_state, - watch_items, + watched_addresses, network, ); @@ -775,37 +737,45 @@ mod tests { wallet.add_watched_address(addr.clone()); // Transaction to watched address should still be relevant - let tx = create_test_transaction(vec![(addr, 50000)], vec![]); + let tx = create_test_transaction(vec![(addr.clone(), 50000)], vec![]); assert!(filter.is_transaction_relevant(&tx)); } #[tokio::test] + #[ignore = "requires MockWallet implementation"] async fn test_multiple_watch_items() { let network = Network::Dash; let addr1 = test_address(network); let addr2 = test_address2(network); - let script = addr1.script_pubkey(); - let outpoint = OutPoint { + + let mempool_state = Arc::new(RwLock::new(MempoolState::default())); + let dummy_script = ScriptBuf::new(); + let dummy_outpoint = OutPoint { txid: Txid::from_str( - "4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", ) .unwrap(), - vout: 2, + vout: 0, }; - - let mempool_state = Arc::new(RwLock::new(MempoolState::default())); let watch_items = vec![ WatchItem::address(addr1.clone()), - WatchItem::Script(script), - WatchItem::Outpoint(outpoint), + WatchItem::script(dummy_script), + WatchItem::outpoint(dummy_outpoint), ]; + let watched_addresses: HashSet
= watch_items + .into_iter() + .filter_map(|item| match item { + WatchItem::Address(addr) => Some(addr), + _ => None, + }) + .collect(); let filter = MempoolFilter::new( MempoolStrategy::Selective, Duration::from_secs(300), 1000, mempool_state, - watch_items, + watched_addresses, network, ); @@ -818,9 +788,9 @@ mod tests { let tx1 = create_test_transaction(vec![(addr1.clone(), 1000)], vec![]); assert!(filter.is_transaction_relevant(&tx1)); - // Match by outpoint - let tx2 = create_test_transaction(vec![(addr2.clone(), 2000)], vec![outpoint]); - assert!(filter.is_transaction_relevant(&tx2)); + // TODO: Match by outpoint - requires OutPoint to be stored in WatchItem::Outpoint variant + // let tx2 = create_test_transaction(vec![(addr2.clone(), 2000)], vec![outpoint]); + // assert!(filter.is_transaction_relevant(&tx2)); // No match let other_outpoint = OutPoint { diff --git a/dash-spv/src/sync/filters.rs b/dash-spv/src/sync/filters.rs index 844afe49c..cfe43ecf0 100644 --- a/dash-spv/src/sync/filters.rs +++ b/dash-spv/src/sync/filters.rs @@ -1682,7 +1682,7 @@ impl SyncResult> { @@ -1893,6 +1893,7 @@ impl, - processing_thread_requests: std::sync::Arc< + _network_message_sender: mpsc::Sender, + _processing_thread_requests: std::sync::Arc< std::sync::Mutex>, >, stats: std::sync::Arc>, diff --git a/dash-spv/src/sync/sequential/mod.rs b/dash-spv/src/sync/sequential/mod.rs index b6a49ee57..096c3e1c9 100644 --- a/dash-spv/src/sync/sequential/mod.rs +++ b/dash-spv/src/sync/sequential/mod.rs @@ -1892,7 +1892,7 @@ impl< async fn handle_post_sync_cfilter( &mut self, cfilter: dashcore::network::message_filter::CFilter, - network: &mut N, + _network: &mut N, storage: &mut S, ) -> SyncResult<()> { tracing::info!("📥 Processing filter for new block after sync"); diff --git a/dash-spv/src/sync/sequential/recovery.rs b/dash-spv/src/sync/sequential/recovery.rs index fa5104080..2acfc44aa 100644 --- a/dash-spv/src/sync/sequential/recovery.rs +++ b/dash-spv/src/sync/sequential/recovery.rs @@ -524,7 +524,7 @@ mod tests { ); // Determine recovery strategy - let strategy = recovery_manager.determine_strategy(&phase, &error); + let _strategy = recovery_manager.determine_strategy(&phase, &error); // Create mock network and storage (would need proper mocks in real tests) // For this test, we're mainly interested in the error being preserved diff --git a/dash-spv/src/sync/validation_test.rs b/dash-spv/src/sync/validation_test.rs index 5bf3e1310..e23ce9b98 100644 --- a/dash-spv/src/sync/validation_test.rs +++ b/dash-spv/src/sync/validation_test.rs @@ -210,33 +210,13 @@ mod tests { mod perf_tests { use crate::sync::chainlock_validation::{ChainLockValidationConfig, ChainLockValidator}; - use crate::sync::validation::{ValidationConfig, ValidationEngine}; use dashcore::BlockHash; use std::time::Instant; - #[tokio::test] - #[ignore] // Run with --ignored flag for performance tests - async fn test_validation_performance() { - let config = ValidationConfig::default(); - let engine = ValidationEngine::new(config); - - let start = Instant::now(); - - // Create large QRInfo for performance testing - let mut qr_info = super::tests::create_mock_qr_info(); - - // Add 1000 diffs - for i in 0..1000 { - qr_info.mn_list_diff_list.push(super::tests::create_mock_mn_list_diff(i)); - } - - let duration = start.elapsed(); - println!("Created test data in {:?}", duration); - - // Note: Actual validation would require proper engine setup - // This test demonstrates the performance testing framework - } + // TODO: Implement performance test for validation + // This test should measure performance of validation with large QRInfo datasets + // and verify that validation completes within acceptable time limits #[tokio::test] #[ignore] @@ -250,7 +230,7 @@ mod perf_tests { // Simulate many cache operations for i in 0..10000 { - let hash = BlockHash::from([i as u8; 32]); + let _hash = BlockHash::from([i as u8; 32]); // Cache operations would happen during validation } diff --git a/dash-spv/src/validation/headers_test.rs b/dash-spv/src/validation/headers_test.rs index 5e22e8484..7c53bf636 100644 --- a/dash-spv/src/validation/headers_test.rs +++ b/dash-spv/src/validation/headers_test.rs @@ -29,16 +29,6 @@ mod tests { } } - /// Create a valid test header that connects to previous - fn create_valid_header(prev_header: &BlockHeader, time_offset: u32) -> BlockHeader { - create_test_header( - prev_header.block_hash(), - 12345, - 0x1e0fffff, // Easy difficulty for testing - prev_header.time + time_offset, - ) - } - #[test] fn test_validation_mode_none_always_passes() { let validator = HeaderValidator::new(ValidationMode::None); diff --git a/dash-spv/tests/block_download_test.rs b/dash-spv/tests/block_download_test.rs index 81306fe39..523e86c06 100644 --- a/dash-spv/tests/block_download_test.rs +++ b/dash-spv/tests/block_download_test.rs @@ -3,7 +3,7 @@ //! NOTE: This test file is currently disabled due to incomplete mock NetworkManager implementation. //! TODO: Re-enable once NetworkManager trait methods are fully implemented. -#![cfg(skip_mock_implementation_incomplete)] +#![cfg(feature = "skip_mock_implementation_incomplete")] //! Tests for block downloading on filter match functionality. @@ -24,8 +24,8 @@ use dash_spv::{ client::ClientConfig, network::NetworkManager, storage::MemoryStorageManager, - sync::{FilterSyncManager, SyncManager}, - types::{FilterMatch, WatchItem}, + sync::{sequential::SequentialSyncManager, FilterSyncManager}, + types::FilterMatch, }; /// Mock network manager for testing @@ -198,21 +198,25 @@ fn create_test_filter_match(block_hash: BlockHash, height: u32) -> FilterMatch { } } +#[ignore = "mock implementation incomplete"] #[tokio::test] async fn test_filter_sync_manager_creation() { let config = create_test_config(); let received_heights = Arc::new(Mutex::new(HashSet::new())); - let filter_sync = FilterSyncManager::new(&config, received_heights); + let filter_sync: FilterSyncManager = + FilterSyncManager::new(&config, received_heights); assert!(!filter_sync.has_pending_downloads()); assert_eq!(filter_sync.pending_download_count(), 0); } +#[ignore = "mock implementation incomplete"] #[tokio::test] async fn test_request_block_download() { let config = create_test_config(); let received_heights = Arc::new(Mutex::new(HashSet::new())); - let mut filter_sync = FilterSyncManager::new(&config, received_heights); + let mut filter_sync: FilterSyncManager = + FilterSyncManager::new(&config, received_heights); let mut network = MockNetworkManager::new(); let block_hash = BlockHash::from_slice(&[1u8; 32]).unwrap(); @@ -244,11 +248,13 @@ async fn test_request_block_download() { assert_eq!(filter_sync.pending_download_count(), 1); } +#[ignore = "mock implementation incomplete"] #[tokio::test] async fn test_duplicate_block_request_prevention() { let config = create_test_config(); let received_heights = Arc::new(Mutex::new(HashSet::new())); - let mut filter_sync = FilterSyncManager::new(&config, received_heights); + let mut filter_sync: FilterSyncManager = + FilterSyncManager::new(&config, received_heights); let mut network = MockNetworkManager::new(); let block_hash = BlockHash::from_slice(&[1u8; 32]).unwrap(); @@ -266,11 +272,13 @@ async fn test_duplicate_block_request_prevention() { assert_eq!(filter_sync.pending_download_count(), 1); } +#[ignore = "mock implementation incomplete"] #[tokio::test] async fn test_handle_downloaded_block() { let config = create_test_config(); let received_heights = Arc::new(Mutex::new(HashSet::new())); - let mut filter_sync = FilterSyncManager::new(&config, received_heights); + let mut filter_sync: FilterSyncManager = + FilterSyncManager::new(&config, received_heights); let mut network = MockNetworkManager::new(); let block = create_test_block(); @@ -295,11 +303,13 @@ async fn test_handle_downloaded_block() { assert_eq!(filter_sync.pending_download_count(), 0); } +#[ignore = "mock implementation incomplete"] #[tokio::test] async fn test_handle_unexpected_block() { let config = create_test_config(); let received_heights = Arc::new(Mutex::new(HashSet::new())); - let mut filter_sync = FilterSyncManager::new(&config, received_heights); + let mut filter_sync: FilterSyncManager = + FilterSyncManager::new(&config, received_heights); let block = create_test_block(); @@ -310,11 +320,13 @@ async fn test_handle_unexpected_block() { assert!(result.is_none()); } +#[ignore = "mock implementation incomplete"] #[tokio::test] async fn test_process_multiple_filter_matches() { let config = create_test_config(); let received_heights = Arc::new(Mutex::new(HashSet::new())); - let mut filter_sync = FilterSyncManager::new(&config, received_heights); + let mut filter_sync: FilterSyncManager = + FilterSyncManager::new(&config, received_heights); let mut network = MockNetworkManager::new(); // Create multiple filter matches @@ -359,37 +371,47 @@ async fn test_process_multiple_filter_matches() { assert_eq!(filter_sync.pending_download_count(), 3); } +#[ignore = "mock implementation incomplete"] #[tokio::test] async fn test_sync_manager_integration() { let config = create_test_config(); let received_heights = Arc::new(Mutex::new(HashSet::new())); - let mut sync_manager = SyncManager::new(&config, received_heights) - .expect("Failed to create SyncManager for integration test"); + let wallet = + Arc::new(RwLock::new(key_wallet_manager::spv_wallet_manager::SPVWalletManager::with_base( + key_wallet_manager::wallet_manager::WalletManager::< + key_wallet::wallet::managed_wallet_info::ManagedWalletInfo, + >::new(), + ))); + let mut sync_manager: SequentialSyncManager = + SequentialSyncManager::new(&config, received_heights, wallet) + .expect("Failed to create SequentialSyncManager for integration test"); let mut network = MockNetworkManager::new(); let block_hash = BlockHash::from_slice(&[1u8; 32]).unwrap(); let filter_matches = vec![create_test_filter_match(block_hash, 100)]; // Request block downloads through sync manager - let result = sync_manager.request_block_downloads(filter_matches, &mut network).await; - assert!(result.is_ok()); + // Note: request_block_downloads method doesn't exist in the current SequentialSyncManager API + // let result = sync_manager.request_block_downloads(filter_matches, &mut network).await; + // assert!(result.is_ok()); // Check state through sync manager // Note: Methods for checking pending downloads and handling blocks // may not exist in current API. This test may need significant refactoring. } +#[ignore = "mock implementation incomplete"] #[tokio::test] async fn test_filter_match_and_download_workflow() { let config = create_test_config(); let _storage = MemoryStorageManager::new().await.unwrap(); let received_heights = Arc::new(Mutex::new(HashSet::new())); - let mut filter_sync = FilterSyncManager::new(&config, received_heights); + let mut filter_sync: FilterSyncManager = + FilterSyncManager::new(&config, received_heights); let mut network = MockNetworkManager::new(); - // Create test address and watch item + // Create test address (WatchItem replaced with wallet-based tracking) let address = create_test_address(); - let _watch_items = vec![WatchItem::address(address)]; // This is a simplified test - in real usage, we'd need to: // 1. Store filter headers and filters @@ -409,11 +431,13 @@ async fn test_filter_match_and_download_workflow() { assert!(filter_sync.has_pending_downloads()); } +#[ignore = "mock implementation incomplete"] #[tokio::test] async fn test_reset_clears_download_state() { let config = create_test_config(); let received_heights = Arc::new(Mutex::new(HashSet::new())); - let mut filter_sync = FilterSyncManager::new(&config, received_heights); + let mut filter_sync: FilterSyncManager = + FilterSyncManager::new(&config, received_heights); let mut network = MockNetworkManager::new(); let block_hash = BlockHash::from_slice(&[1u8; 32]).unwrap(); diff --git a/dash-spv/tests/cfheader_gap_test.rs b/dash-spv/tests/cfheader_gap_test.rs index ede8f9009..bed8ee0c9 100644 --- a/dash-spv/tests/cfheader_gap_test.rs +++ b/dash-spv/tests/cfheader_gap_test.rs @@ -1,10 +1,8 @@ //! Tests for CFHeader gap detection and auto-restart functionality. //! -//! NOTE: This test file is currently disabled due to incomplete mock NetworkManager implementation. +//! NOTE: This test file is currently ignored due to incomplete mock NetworkManager implementation. //! TODO: Re-enable once NetworkManager trait methods are fully implemented. -#![cfg(skip_mock_implementation_incomplete)] - //! Tests for CFHeader gap detection and auto-restart functionality. use std::collections::HashSet; @@ -13,7 +11,7 @@ use std::sync::{Arc, Mutex}; use dash_spv::{ client::ClientConfig, error::{NetworkError, NetworkResult}, - network::NetworkManager, + network::{MultiPeerNetworkManager, NetworkManager}, storage::{MemoryStorageManager, StorageManager}, sync::filters::FilterSyncManager, }; @@ -41,10 +39,15 @@ fn create_mock_filter_header() -> FilterHeader { } #[tokio::test] +#[ignore = "mock NetworkManager implementation incomplete"] async fn test_cfheader_gap_detection_no_gap() { let config = ClientConfig::new(Network::Dash); let received_heights = Arc::new(Mutex::new(HashSet::new())); - let filter_sync = FilterSyncManager::new(&config, received_heights); + let filter_sync: FilterSyncManager = + FilterSyncManager::::new( + &config, + received_heights, + ); let mut storage = MemoryStorageManager::new().await.unwrap(); @@ -71,10 +74,15 @@ async fn test_cfheader_gap_detection_no_gap() { } #[tokio::test] +#[ignore = "mock NetworkManager implementation incomplete"] async fn test_cfheader_gap_detection_with_gap() { let config = ClientConfig::new(Network::Dash); let received_heights = Arc::new(Mutex::new(HashSet::new())); - let filter_sync = FilterSyncManager::new(&config, received_heights); + let filter_sync: FilterSyncManager = + FilterSyncManager::::new( + &config, + received_heights, + ); let mut storage = MemoryStorageManager::new().await.unwrap(); @@ -104,10 +112,15 @@ async fn test_cfheader_gap_detection_with_gap() { } #[tokio::test] +#[ignore = "mock NetworkManager implementation incomplete"] async fn test_cfheader_gap_detection_filter_ahead() { let config = ClientConfig::new(Network::Dash); let received_heights = Arc::new(Mutex::new(HashSet::new())); - let filter_sync = FilterSyncManager::new(&config, received_heights); + let filter_sync: FilterSyncManager = + FilterSyncManager::::new( + &config, + received_heights, + ); let mut storage = MemoryStorageManager::new().await.unwrap(); @@ -137,12 +150,14 @@ async fn test_cfheader_gap_detection_filter_ahead() { } #[tokio::test] +#[ignore = "mock NetworkManager implementation incomplete"] async fn test_cfheader_restart_cooldown() { let mut config = ClientConfig::new(Network::Dash); config.cfheader_gap_restart_cooldown_secs = 1; // 1 second cooldown for testing let received_heights = Arc::new(Mutex::new(HashSet::new())); - let mut filter_sync = FilterSyncManager::new(&config, received_heights); + let mut filter_sync: FilterSyncManager = + FilterSyncManager::new(&config, received_heights); let mut storage = MemoryStorageManager::new().await.unwrap(); @@ -250,24 +265,26 @@ async fn test_cfheader_restart_cooldown() { let mut network = MockNetworkManager; + // Note: The following tests are skipped because MockNetworkManager doesn't implement + // the full MultiPeerNetworkManager interface required by maybe_restart_cfheader_sync_for_gap // First attempt should try to restart (and fail) - let result1 = filter_sync.maybe_restart_cfheader_sync_for_gap(&mut network, &mut storage).await; - assert!(result1.is_err(), "First restart attempt should fail with mock network"); + // let result1 = filter_sync.maybe_restart_cfheader_sync_for_gap(&mut network, &mut storage).await; + // assert!(result1.is_err(), "First restart attempt should fail with mock network"); // Second attempt immediately should be blocked by cooldown - let result2 = filter_sync.maybe_restart_cfheader_sync_for_gap(&mut network, &mut storage).await; - assert!(result2.is_ok(), "Second attempt should not error"); - assert!(!result2.unwrap(), "Second attempt should return false due to cooldown"); + // let result2 = filter_sync.maybe_restart_cfheader_sync_for_gap(&mut network, &mut storage).await; + // assert!(result2.is_ok(), "Second attempt should not error"); + // assert!(!result2.unwrap(), "Second attempt should return false due to cooldown"); // Wait for cooldown to expire tokio::time::sleep(std::time::Duration::from_secs(2)).await; // Third attempt should try again (and fail) - let result3 = filter_sync.maybe_restart_cfheader_sync_for_gap(&mut network, &mut storage).await; + // let result3 = filter_sync.maybe_restart_cfheader_sync_for_gap(&mut network, &mut storage).await; // The third attempt should either fail (if trying to restart) or return Ok(false) if max attempts reached - let should_fail_or_be_disabled = result3.is_err() || (result3.is_ok() && !result3.unwrap()); - assert!( - should_fail_or_be_disabled, - "Third restart attempt should fail or be disabled after cooldown" - ); + // let should_fail_or_be_disabled = result3.is_err() || (result3.is_ok() && !result3.unwrap()); + // assert!( + // should_fail_or_be_disabled, + // "Third restart attempt should fail or be disabled after cooldown" + // ); } diff --git a/dash-spv/tests/chainlock_simple_test.rs b/dash-spv/tests/chainlock_simple_test.rs index f11994764..7efccda69 100644 --- a/dash-spv/tests/chainlock_simple_test.rs +++ b/dash-spv/tests/chainlock_simple_test.rs @@ -57,7 +57,7 @@ async fn test_chainlock_validation_flow() { >::new()))); // Create the SPV client - let mut client = + let client = DashSpvClient::new(config, network_manager, storage_manager, wallet).await.unwrap(); // Test that update_chainlock_validation works diff --git a/dash-spv/tests/chainlock_validation_test.rs b/dash-spv/tests/chainlock_validation_test.rs index e77b64388..5001b66d5 100644 --- a/dash-spv/tests/chainlock_validation_test.rs +++ b/dash-spv/tests/chainlock_validation_test.rs @@ -3,7 +3,7 @@ //! NOTE: This test file is currently disabled due to incomplete mock NetworkManager implementation. //! TODO: Re-enable once NetworkManager trait methods are fully implemented. -#![cfg(skip_mock_implementation_incomplete)] +#![cfg(feature = "skip_mock_implementation_incomplete")] //! Integration tests for ChainLock validation flow with masternode engine @@ -17,6 +17,9 @@ use dashcore::blockdata::constants::genesis_block; use dashcore::sml::masternode_list_engine::MasternodeListEngine; use dashcore::Network; use dashcore::{BlockHash, ChainLock}; +use key_wallet::wallet::managed_wallet_info::ManagedWalletInfo; +use key_wallet_manager::spv_wallet_manager::SPVWalletManager; +use key_wallet_manager::wallet_manager::WalletManager; use std::sync::Arc; use std::time::Duration; use tempfile::TempDir; @@ -172,6 +175,7 @@ fn create_test_chainlock(height: u32, block_hash: BlockHash) -> ChainLock { } } +#[ignore = "mock implementation incomplete"] #[tokio::test] async fn test_chainlock_validation_without_masternode_engine() { init_logging(); @@ -181,8 +185,13 @@ async fn test_chainlock_validation_without_masternode_engine() { let storage_path = temp_dir.path().to_path_buf(); // Create storage and network managers - let storage = Box::new(DiskStorageManager::new(storage_path).await.unwrap()); - let network = Box::new(MockNetworkManager::new()); + let mut storage = DiskStorageManager::new(storage_path).await.unwrap(); + let network = MockNetworkManager::new(); + + // Create wallet manager + let wallet = Arc::new(RwLock::new(SPVWalletManager::with_base(WalletManager::< + ManagedWalletInfo, + >::new()))); // Create client config let config = ClientConfig { @@ -194,7 +203,7 @@ async fn test_chainlock_validation_without_masternode_engine() { }; // Create the SPV client - let mut client = DashSpvClient::new(config).await.unwrap(); + let mut client = DashSpvClient::new(config, network, storage, wallet).await.unwrap(); // Add a test header to storage let genesis = genesis_block(Network::Dash).header; @@ -208,11 +217,11 @@ async fn test_chainlock_validation_without_masternode_engine() { // Process the ChainLock (should queue it since no masternode engine) let chainlock_manager = client.chainlock_manager(); let chain_state = ChainState::new(); - let result = - chainlock_manager.process_chain_lock(chain_lock.clone(), &chain_state, &mut *storage).await; + // Note: In the current API, we need to access storage differently + // For now, skip this test as it needs to be rewritten for the new client API - // Should succeed but queue for later validation - assert!(result.is_ok()); + // Skip the rest of this test for now + return; // Verify it was queued // Note: pending_chainlocks is private, can't access directly @@ -221,6 +230,7 @@ async fn test_chainlock_validation_without_masternode_engine() { // assert_eq!(pending[0].block_height, 0); } +#[ignore = "mock implementation incomplete"] #[tokio::test] async fn test_chainlock_validation_with_masternode_engine() { init_logging(); @@ -230,14 +240,19 @@ async fn test_chainlock_validation_with_masternode_engine() { let storage_path = temp_dir.path().to_path_buf(); // Create storage and network managers - let storage = Box::new(DiskStorageManager::new(storage_path).await.unwrap()); - let mut network = Box::new(MockNetworkManager::new()); + let mut storage = DiskStorageManager::new(storage_path).await.unwrap(); + let mut network = MockNetworkManager::new(); // Add a test ChainLock to be received let genesis = genesis_block(Network::Dash).header; let chain_lock = create_test_chainlock(0, genesis.block_hash()); network.add_chain_lock(chain_lock.clone()); + // Create wallet manager + let wallet = Arc::new(RwLock::new(SPVWalletManager::with_base(WalletManager::< + ManagedWalletInfo, + >::new()))); + // Create client config with masternodes enabled let config = ClientConfig { network: Network::Dash, @@ -248,7 +263,7 @@ async fn test_chainlock_validation_with_masternode_engine() { }; // Create the SPV client - let mut client = DashSpvClient::new(config).await.unwrap(); + let mut client = DashSpvClient::new(config, network, storage, wallet).await.unwrap(); // Add genesis header // Note: storage_mut() is not available in current API @@ -271,14 +286,11 @@ async fn test_chainlock_validation_with_masternode_engine() { let chain_state = ChainState::new(); // Note: storage_mut() is not available in current API // let storage = client.storage_mut(); - let result = - client.chainlock_manager().validate_pending_chainlocks(&chain_state, &mut *storage).await; - - // Should fail validation due to invalid signature - // This is expected since our mock ChainLock has an invalid signature - assert!(result.is_ok()); // The validation process itself should complete + // Skip this test section as it needs to be rewritten for the new client API + return; } +#[ignore = "mock implementation incomplete"] #[tokio::test] async fn test_chainlock_queue_and_process_flow() { init_logging(); @@ -288,8 +300,13 @@ async fn test_chainlock_queue_and_process_flow() { let storage_path = temp_dir.path().to_path_buf(); // Create storage - let mut storage = Box::new(DiskStorageManager::new(storage_path).await.unwrap()); - let network = Box::new(MockNetworkManager::new()); + let mut storage = DiskStorageManager::new(storage_path).await.unwrap(); + let network = MockNetworkManager::new(); + + // Create wallet manager + let wallet = Arc::new(RwLock::new(SPVWalletManager::with_base(WalletManager::< + ManagedWalletInfo, + >::new()))); // Create client config let config = ClientConfig { @@ -301,7 +318,7 @@ async fn test_chainlock_queue_and_process_flow() { }; // Create the SPV client - let client = DashSpvClient::new(config).await.unwrap(); + let client = DashSpvClient::new(config, network, storage, wallet).await.unwrap(); let chainlock_manager = client.chainlock_manager(); // Queue multiple ChainLocks @@ -325,16 +342,11 @@ async fn test_chainlock_queue_and_process_flow() { // Process pending (will fail validation but clear the queue) let chain_state = ChainState::new(); - let _ = chainlock_manager.validate_pending_chainlocks(&chain_state, &mut *storage).await; - - // Verify queue is cleared - { - // Note: pending_chainlocks is private, can't access directly - // let pending = chainlock_manager.pending_chainlocks.read().unwrap(); - // assert_eq!(pending.len(), 0); - } + // Skip this test as it needs to be rewritten for the new client API + return; } +#[ignore = "mock implementation incomplete"] #[tokio::test] async fn test_chainlock_manager_cache_operations() { init_logging(); @@ -344,8 +356,13 @@ async fn test_chainlock_manager_cache_operations() { let storage_path = temp_dir.path().to_path_buf(); // Create storage - let mut storage = Box::new(DiskStorageManager::new(storage_path).await.unwrap()); - let network = Box::new(MockNetworkManager::new()); + let mut storage = DiskStorageManager::new(storage_path).await.unwrap(); + let network = MockNetworkManager::new(); + + // Create wallet manager + let wallet = Arc::new(RwLock::new(SPVWalletManager::with_base(WalletManager::< + ManagedWalletInfo, + >::new()))); // Create client config let config = ClientConfig { @@ -357,19 +374,19 @@ async fn test_chainlock_manager_cache_operations() { }; // Create the SPV client - let client = DashSpvClient::new(config).await.unwrap(); + let client = DashSpvClient::new(config, network, storage, wallet).await.unwrap(); let chainlock_manager = client.chainlock_manager(); // Add test headers let genesis = genesis_block(Network::Dash).header; - let mut storage = client.storage(); + let storage = client.storage(); // storage.store_header(&genesis, 0).await.unwrap(); - // Create and process a ChainLock + // Create and process a ChainLock - skip for now as storage access pattern changed let chain_lock = create_test_chainlock(0, genesis.block_hash()); let chain_state = ChainState::new(); - let _ = - chainlock_manager.process_chain_lock(chain_lock.clone(), &chain_state, &mut *storage).await; + // Note: storage access pattern has changed in the new client API + // let _ = chainlock_manager.process_chain_lock(chain_lock.clone(), &chain_state, storage).await; // Test cache operations assert!(chainlock_manager.has_chain_lock_at_height(0)); @@ -389,6 +406,7 @@ async fn test_chainlock_manager_cache_operations() { assert_eq!(stats.lowest_locked_height, Some(0)); } +#[ignore = "mock implementation incomplete"] #[tokio::test] async fn test_client_chainlock_update_flow() { init_logging(); @@ -398,8 +416,13 @@ async fn test_client_chainlock_update_flow() { let storage_path = temp_dir.path().to_path_buf(); // Create storage and network - let storage = Box::new(DiskStorageManager::new(storage_path).await.unwrap()); - let network = Box::new(MockNetworkManager::new()); + let mut storage = DiskStorageManager::new(storage_path).await.unwrap(); + let network = MockNetworkManager::new(); + + // Create wallet manager + let wallet = Arc::new(RwLock::new(SPVWalletManager::with_base(WalletManager::< + ManagedWalletInfo, + >::new()))); // Create client config with masternodes enabled let config = ClientConfig { @@ -411,7 +434,7 @@ async fn test_client_chainlock_update_flow() { }; // Create the SPV client - let mut client = DashSpvClient::new(config).await.unwrap(); + let mut client = DashSpvClient::new(config, network, storage, wallet).await.unwrap(); // Initially, update should fail (no masternode engine) let updated = client.update_chainlock_validation().unwrap(); diff --git a/dash-spv/tests/edge_case_filter_sync_test.rs b/dash-spv/tests/edge_case_filter_sync_test.rs index b664da9c0..43961532f 100644 --- a/dash-spv/tests/edge_case_filter_sync_test.rs +++ b/dash-spv/tests/edge_case_filter_sync_test.rs @@ -3,7 +3,7 @@ //! NOTE: This test file is currently disabled due to incomplete mock NetworkManager implementation. //! TODO: Re-enable once NetworkManager trait methods are fully implemented. -#![cfg(skip_mock_implementation_incomplete)] +#![cfg(feature = "skip_mock_implementation_incomplete")] //! Tests for edge case handling in filter header sync, particularly at the tip. @@ -142,11 +142,13 @@ impl NetworkManager for MockNetworkManager { } } +#[ignore = "mock implementation incomplete"] #[tokio::test] async fn test_filter_sync_at_tip_edge_case() { let config = ClientConfig::new(Network::Dash); let received_heights = Arc::new(Mutex::new(HashSet::new())); - let mut filter_sync = FilterSyncManager::new(&config, received_heights); + let mut filter_sync: FilterSyncManager = + FilterSyncManager::new(&config, received_heights); let mut storage = MemoryStorageManager::new().await.unwrap(); let mut network = MockNetworkManager::new(); @@ -183,11 +185,13 @@ async fn test_filter_sync_at_tip_edge_case() { assert_eq!(sent_messages.len(), 0, "Should not send any messages when at tip"); } +#[ignore = "mock implementation incomplete"] #[tokio::test] async fn test_filter_sync_gap_detection_edge_case() { let config = ClientConfig::new(Network::Dash); let received_heights = Arc::new(Mutex::new(HashSet::new())); - let filter_sync = FilterSyncManager::new(&config, received_heights); + let filter_sync: FilterSyncManager = + FilterSyncManager::new(&config, received_heights); let mut storage = MemoryStorageManager::new().await.unwrap(); @@ -243,11 +247,13 @@ async fn test_filter_sync_gap_detection_edge_case() { assert_eq!(gap_size, 2); } +#[ignore = "mock implementation incomplete"] #[tokio::test] async fn test_no_invalid_getcfheaders_at_tip() { let config = ClientConfig::new(Network::Dash); let received_heights = Arc::new(Mutex::new(HashSet::new())); - let mut filter_sync = FilterSyncManager::new(&config, received_heights); + let mut filter_sync: FilterSyncManager = + FilterSyncManager::new(&config, received_heights); let mut storage = MemoryStorageManager::new().await.unwrap(); let mut network = MockNetworkManager::new(); diff --git a/dash-spv/tests/error_handling_test.rs b/dash-spv/tests/error_handling_test.rs index 050c313c5..6e04f887a 100644 --- a/dash-spv/tests/error_handling_test.rs +++ b/dash-spv/tests/error_handling_test.rs @@ -1,10 +1,10 @@ +#![cfg(feature = "skip_mock_implementation_incomplete")] + //! Comprehensive error handling tests for dash-spv //! -//! NOTE: This test file is currently disabled due to incomplete mock trait implementations. +//! NOTE: This test file is currently ignored due to incomplete mock trait implementations. //! TODO: Re-enable once StorageManager and NetworkManager trait methods are fully implemented. -#![cfg(skip_mock_implementation_incomplete)] - //! Comprehensive error handling tests for dash-spv //! //! This test suite validates error scenarios across all major components: @@ -14,6 +14,7 @@ //! - Recovery mechanisms (automatic retries, graceful degradation) //! - Error propagation through layers +use std::any::Any; use std::collections::HashMap; use std::io; use std::net::{IpAddr, Ipv4Addr, SocketAddr}; @@ -32,12 +33,12 @@ use dashcore_hashes::Hash; use tokio::sync::{mpsc, RwLock}; use dash_spv::error::*; -use dash_spv::network::TcpConnection; +use dash_spv::network::{NetworkManager, TcpConnection}; use dash_spv::storage::{DiskStorageManager, MemoryStorageManager, StorageManager}; use dash_spv::sync::sequential::phases::SyncPhase; use dash_spv::sync::sequential::recovery::{RecoveryManager, RecoveryStrategy}; -use dash_spv::types::{ChainState, MempoolState, UnconfirmedTransaction}; -use dash_spv::wallet::Utxo; +use dash_spv::types::{ChainState, MempoolState, PeerInfo, UnconfirmedTransaction}; +use key_wallet_manager::Utxo; /// Mock network manager for testing error scenarios struct MockNetworkManager { @@ -142,8 +143,52 @@ impl dash_spv::network::NetworkManager for MockNetworkManager { vec![] } - async fn send_ping(&mut self) -> NetworkResult<()> { - self.send_message(dashcore::network::message::NetworkMessage::Ping(1234)).await + async fn send_ping(&mut self) -> NetworkResult { + let nonce = 1234u64; + self.send_message(dashcore::network::message::NetworkMessage::Ping(nonce)).await?; + Ok(nonce) + } + + async fn handle_ping(&mut self, _nonce: u64) -> NetworkResult<()> { + Ok(()) + } + + fn handle_pong(&mut self, _nonce: u64) -> NetworkResult<()> { + Ok(()) + } + + fn should_ping(&self) -> bool { + false + } + + fn cleanup_old_pings(&mut self) {} + + fn get_message_sender(&self) -> mpsc::Sender { + // Create a dummy channel for testing + let (_tx, _rx) = mpsc::channel(1); + _tx + } + + async fn get_peer_best_height(&self) -> NetworkResult> { + Ok(Some(1000000)) + } + + async fn has_peer_with_service( + &self, + _service_flags: dashcore::network::constants::ServiceFlags, + ) -> bool { + true + } + + async fn get_peers_with_service( + &self, + _service_flags: dashcore::network::constants::ServiceFlags, + ) -> Vec { + vec![] + } + + async fn update_peer_dsq_preference(&mut self, _wants_dsq: bool) -> NetworkResult<()> { + Ok(()) } } @@ -346,14 +391,11 @@ impl StorageManager for MockStorageManager { async fn stats(&self) -> StorageResult { Ok(dash_spv::storage::StorageStats { - headers_count: 0, - filter_headers_count: 0, - filters_count: 0, - headers_size_bytes: 0, - filter_headers_size_bytes: 0, - filters_size_bytes: 0, - total_size_bytes: 0, - last_compaction: None, + header_count: 0, + filter_header_count: 0, + filter_count: 0, + total_size: 0, + component_sizes: std::collections::HashMap::new(), }) } @@ -533,10 +575,15 @@ impl StorageManager for MockStorageManager { } Ok(()) } + + async fn shutdown(&mut self) -> StorageResult<()> { + Ok(()) + } } // ===== Network Error Tests ===== +#[ignore = "mock implementation incomplete"] #[tokio::test] async fn test_network_connection_failure() { let addr = SocketAddr::new(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)), 9999); @@ -552,6 +599,7 @@ async fn test_network_connection_failure() { } } +#[ignore = "mock implementation incomplete"] #[tokio::test] async fn test_network_timeout_recovery() { let mut network = MockNetworkManager::new(); @@ -582,6 +630,7 @@ async fn test_network_timeout_recovery() { } } +#[ignore = "mock implementation incomplete"] #[tokio::test] async fn test_network_peer_disconnection() { let mut network = MockNetworkManager::new(); @@ -605,6 +654,7 @@ async fn test_network_peer_disconnection() { assert!(disconnect_occurred, "Expected peer disconnection"); } +#[ignore = "mock implementation incomplete"] #[tokio::test] async fn test_network_invalid_data_handling() { let mut network = MockNetworkManager::new(); @@ -620,6 +670,7 @@ async fn test_network_invalid_data_handling() { // ===== Storage Error Tests ===== +#[ignore = "mock implementation incomplete"] #[tokio::test] async fn test_storage_disk_full() { let mut storage = MockStorageManager::new(); @@ -636,6 +687,7 @@ async fn test_storage_disk_full() { } } +#[ignore = "mock implementation incomplete"] #[tokio::test] async fn test_storage_permission_denied() { let mut storage = MockStorageManager::new(); @@ -652,6 +704,7 @@ async fn test_storage_permission_denied() { } } +#[ignore = "mock implementation incomplete"] #[tokio::test] async fn test_storage_corruption_detection() { let mut storage = MockStorageManager::new(); @@ -667,6 +720,7 @@ async fn test_storage_corruption_detection() { } } +#[ignore = "mock implementation incomplete"] #[tokio::test] async fn test_storage_lock_poisoned() { let mut storage = MockStorageManager::new(); @@ -683,6 +737,7 @@ async fn test_storage_lock_poisoned() { } } +#[ignore = "mock implementation incomplete"] #[tokio::test] async fn test_storage_recovery_strategy() { let mut storage = MockStorageManager::new(); @@ -715,6 +770,7 @@ async fn test_storage_recovery_strategy() { // ===== Validation Error Tests ===== +#[ignore = "mock implementation incomplete"] #[tokio::test] async fn test_validation_invalid_proof_of_work() { let mut header = create_test_header(0); @@ -730,6 +786,7 @@ async fn test_validation_invalid_proof_of_work() { } } +#[ignore = "mock implementation incomplete"] #[tokio::test] async fn test_validation_invalid_header_chain() { let header1 = create_test_header(0); @@ -746,6 +803,7 @@ async fn test_validation_invalid_header_chain() { } } +#[ignore = "mock implementation incomplete"] #[tokio::test] async fn test_validation_recovery_strategy() { let mut recovery_manager = RecoveryManager::new(); @@ -840,6 +898,7 @@ fn test_error_messages_contain_context() { // ===== Recovery Mechanism Tests ===== +#[ignore = "mock implementation incomplete"] #[tokio::test] async fn test_exponential_backoff() { let mut recovery_manager = RecoveryManager::new(); @@ -873,6 +932,7 @@ async fn test_exponential_backoff() { assert!(delays[2] > delays[1]); } +#[ignore = "mock implementation incomplete"] #[tokio::test] async fn test_max_retry_limit() { let mut recovery_manager = RecoveryManager::new(); @@ -906,6 +966,7 @@ async fn test_max_retry_limit() { assert!(abort_occurred, "Expected abort after max retries"); } +#[ignore = "mock implementation incomplete"] #[tokio::test] async fn test_recovery_statistics() { let mut recovery_manager = RecoveryManager::new(); @@ -937,6 +998,7 @@ async fn test_recovery_statistics() { // ===== Error Propagation Tests ===== +#[ignore = "mock implementation incomplete"] #[tokio::test] async fn test_error_propagation_through_layers() { // Create a storage error @@ -1048,6 +1110,7 @@ fn test_parse_errors() { // ===== Real-world Scenario Tests ===== +#[ignore = "mock implementation incomplete"] #[tokio::test] async fn test_cascading_network_failures() { let mut network = MockNetworkManager::new(); @@ -1092,6 +1155,7 @@ async fn test_cascading_network_failures() { } } +#[ignore = "mock implementation incomplete"] #[tokio::test] async fn test_storage_corruption_recovery() { let temp_dir = tempfile::tempdir().unwrap(); @@ -1123,6 +1187,7 @@ async fn test_storage_corruption_recovery() { assert!(result.is_err()); } +#[ignore = "mock implementation incomplete"] #[tokio::test] async fn test_concurrent_error_handling() { let storage = Arc::new(RwLock::new(MockStorageManager::new())); @@ -1165,6 +1230,7 @@ async fn test_concurrent_error_handling() { // ===== Headers2 Specific Error Tests ===== +#[ignore = "mock implementation incomplete"] #[tokio::test] async fn test_headers2_decompression_failure() { let error = SyncError::Headers2DecompressionFailed("Invalid compressed data".to_string()); diff --git a/dash-spv/tests/error_recovery_integration_test.rs b/dash-spv/tests/error_recovery_integration_test.rs index 09308d5b3..2ca4d8b8a 100644 --- a/dash-spv/tests/error_recovery_integration_test.rs +++ b/dash-spv/tests/error_recovery_integration_test.rs @@ -3,7 +3,7 @@ //! NOTE: This test file is currently disabled due to incomplete mock trait implementations. //! TODO: Re-enable once StorageManager and NetworkManager trait methods are fully implemented. -#![cfg(skip_mock_implementation_incomplete)] +#![cfg(feature = "skip_mock_implementation_incomplete")] //! Integration tests for error recovery mechanisms //! @@ -16,14 +16,17 @@ use std::path::PathBuf; use std::sync::Arc; use std::time::Duration; -use dashcore::{block::Header as BlockHeader, hash_types::FilterHeader, BlockHash, Network}; +use dashcore::{block::Header as BlockHeader, hash_types::FilterHeader, BlockHash, Network, Txid}; use tokio::sync::{Mutex, RwLock}; use tokio::time::timeout; -use dash_spv::client::{Client, ClientConfig}; +use dash_spv::client::{ClientConfig, DashSpvClient}; use dash_spv::error::{NetworkError, SpvError, StorageError, SyncError, ValidationError}; -use dash_spv::storage::{DiskStorageManager, MemoryStorage, StorageManager}; +use dash_spv::storage::{ + sync_state::SyncCheckpoint, DiskStorageManager, MemoryStorage, StorageManager, +}; use dash_spv::sync::sequential::recovery::RecoveryManager; +use key_wallet_manager::Utxo; /// Test helper to simulate network interruptions struct NetworkInterruptor { @@ -119,6 +122,7 @@ impl StorageFailureSimulator { } } +#[ignore = "mock implementation incomplete"] #[tokio::test] async fn test_recovery_from_network_interruption_during_header_sync() { // This test simulates a network interruption during header synchronization @@ -191,7 +195,7 @@ async fn test_recovery_from_network_interruption_during_header_sync() { // Simulate storing a header let header = create_test_header(current_height); - storage.write().await.store_header(current_height, &header).await.unwrap(); + storage.write().await.store_headers(&[header]).await.unwrap(); current_height += 1; headers_in_batch += 1; @@ -216,10 +220,11 @@ async fn test_recovery_from_network_interruption_during_header_sync() { assert!(recovery_count > 0, "Should have had at least one recovery"); // Verify all headers were stored correctly - let stored_headers = storage.read().await.get_headers_range(0..target_height).await.unwrap(); + let stored_headers = storage.read().await.load_headers(0..target_height).await.unwrap(); assert_eq!(stored_headers.len(), target_height as usize); } +#[ignore = "mock implementation incomplete"] #[tokio::test] async fn test_recovery_from_storage_failure_during_sync() { // This test simulates storage failures during synchronization @@ -268,6 +273,7 @@ async fn test_recovery_from_storage_failure_during_sync() { assert!(last_successful_height >= 250, "Should have processed headers up to failure point"); } +#[ignore = "mock implementation incomplete"] #[tokio::test] async fn test_recovery_from_validation_errors() { // This test simulates validation errors and verifies recovery behavior @@ -305,7 +311,8 @@ async fn test_recovery_from_validation_errors() { } => { assert!(checkpoint.restart_height.is_some()); let restart_height = checkpoint.restart_height.unwrap(); - assert!(restart_height < phase.current_height()); + // Note: current_height method doesn't exist on SyncPhase + // assert!(restart_height < phase.current_height()); eprintln!( "Validation error '{}' triggers restart from height {}", val_error, restart_height @@ -322,6 +329,7 @@ async fn test_recovery_from_validation_errors() { } } +#[ignore = "mock implementation incomplete"] #[tokio::test] async fn test_concurrent_error_recovery() { // This test simulates multiple concurrent errors and verifies @@ -398,6 +406,7 @@ async fn test_concurrent_error_recovery() { } } +#[ignore = "mock implementation incomplete"] #[tokio::test] async fn test_recovery_statistics_tracking() { // This test verifies that recovery statistics are properly tracked @@ -479,11 +488,76 @@ impl MockNetworkManager { #[async_trait::async_trait] impl dash_spv::network::NetworkManager for MockNetworkManager { + fn as_any(&self) -> &dyn std::any::Any { + self + } + + async fn disconnect(&mut self) -> dash_spv::error::NetworkResult<()> { + Ok(()) + } + + fn is_connected(&self) -> bool { + true + } + + fn peer_info(&self) -> Vec { + vec![] + } + + async fn send_ping(&mut self) -> dash_spv::error::NetworkResult { + Ok(1234) + } + + async fn handle_ping(&mut self, _nonce: u64) -> dash_spv::error::NetworkResult<()> { + Ok(()) + } + + fn handle_pong(&mut self, _nonce: u64) -> dash_spv::error::NetworkResult<()> { + Ok(()) + } + + fn should_ping(&self) -> bool { + false + } + + fn cleanup_old_pings(&mut self) {} + + fn get_message_sender( + &self, + ) -> tokio::sync::mpsc::Sender { + let (_tx, _rx) = tokio::sync::mpsc::channel(1); + _tx + } + + async fn get_peer_best_height(&self) -> dash_spv::error::NetworkResult> { + Ok(Some(1000000)) + } + + async fn has_peer_with_service( + &self, + _service_flags: dashcore::network::constants::ServiceFlags, + ) -> bool { + true + } + + async fn get_peers_with_service( + &self, + _service_flags: dashcore::network::constants::ServiceFlags, + ) -> Vec { + vec![] + } + + async fn update_peer_dsq_preference( + &mut self, + _wants_dsq: bool, + ) -> dash_spv::error::NetworkResult<()> { + Ok(()) + } fn peer_count(&self) -> usize { 1 } - async fn connect(&mut self, _addr: SocketAddr) -> dash_spv::error::NetworkResult<()> { + async fn connect(&mut self) -> dash_spv::error::NetworkResult<()> { Ok(()) } @@ -512,25 +586,28 @@ impl MockStorageManager { #[async_trait::async_trait] impl StorageManager for MockStorageManager { - async fn store_header( + fn as_any_mut(&mut self) -> &mut dyn std::any::Any { + self + } + + async fn store_headers( &mut self, - _height: u32, - _header: &BlockHeader, + _headers: &[BlockHeader], ) -> dash_spv::error::StorageResult<()> { Ok(()) } - async fn get_header( + async fn load_headers( &self, - _height: u32, - ) -> dash_spv::error::StorageResult> { - Ok(None) + _range: std::ops::Range, + ) -> dash_spv::error::StorageResult> { + Ok(vec![]) } - async fn get_header_by_hash( + async fn get_header( &self, - _hash: &BlockHash, - ) -> dash_spv::error::StorageResult> { + _height: u32, + ) -> dash_spv::error::StorageResult> { Ok(None) } @@ -538,21 +615,20 @@ impl StorageManager for MockStorageManager { Ok(Some(0)) } - async fn get_headers_range( - &self, - _range: std::ops::Range, - ) -> dash_spv::error::StorageResult> { - Ok(vec![]) - } - - async fn store_filter_header( + async fn store_filter_headers( &mut self, - _height: u32, - _filter_header: &FilterHeader, + _headers: &[FilterHeader], ) -> dash_spv::error::StorageResult<()> { Ok(()) } + async fn load_filter_headers( + &self, + _range: std::ops::Range, + ) -> dash_spv::error::StorageResult> { + Ok(vec![]) + } + async fn get_filter_header( &self, _height: u32, @@ -571,62 +647,175 @@ impl StorageManager for MockStorageManager { Ok(()) } - async fn get_chain_state( + async fn load_chain_state( &self, ) -> dash_spv::error::StorageResult> { Ok(None) } - async fn compact_storage(&mut self) -> dash_spv::error::StorageResult<()> { + async fn store_filter( + &mut self, + _height: u32, + _filter: &[u8], + ) -> dash_spv::error::StorageResult<()> { Ok(()) } - async fn get_stats(&self) -> dash_spv::error::StorageResult { + async fn load_filter(&self, _height: u32) -> dash_spv::error::StorageResult>> { + Ok(None) + } + + async fn store_metadata( + &mut self, + _key: &str, + _value: &[u8], + ) -> dash_spv::error::StorageResult<()> { + Ok(()) + } + + async fn load_metadata(&self, _key: &str) -> dash_spv::error::StorageResult>> { + Ok(None) + } + + async fn clear(&mut self) -> dash_spv::error::StorageResult<()> { + Ok(()) + } + + async fn stats(&self) -> dash_spv::error::StorageResult { Ok(dash_spv::storage::StorageStats { - headers_count: 0, - filter_headers_count: 0, - filters_count: 0, - headers_size_bytes: 0, - filter_headers_size_bytes: 0, - filters_size_bytes: 0, - total_size_bytes: 0, - last_compaction: None, + header_count: 0, + filter_header_count: 0, + filter_count: 0, + total_size: 0, + component_sizes: std::collections::HashMap::new(), }) } - async fn get_utxos_by_address( + async fn get_header_height_by_hash( + &self, + _hash: &BlockHash, + ) -> dash_spv::error::StorageResult> { + Ok(None) + } + + async fn get_headers_batch( &self, - _address: &dashcore::Address, - ) -> dash_spv::error::StorageResult> { + _start_height: u32, + _end_height: u32, + ) -> dash_spv::error::StorageResult> { Ok(vec![]) } - async fn store_utxo( + async fn store_masternode_state( &mut self, - _outpoint: &dashcore::OutPoint, - _utxo: &dash_spv::wallet::Utxo, + _state: &dash_spv::storage::MasternodeState, ) -> dash_spv::error::StorageResult<()> { Ok(()) } - async fn remove_utxo( + async fn load_masternode_state( + &self, + ) -> dash_spv::error::StorageResult> { + Ok(None) + } + + async fn store_sync_state( &mut self, - _outpoint: &dashcore::OutPoint, - ) -> dash_spv::error::StorageResult> { + _state: &dash_spv::storage::PersistentSyncState, + ) -> dash_spv::error::StorageResult<()> { + Ok(()) + } + + async fn load_sync_state( + &self, + ) -> dash_spv::error::StorageResult> { Ok(None) } - async fn get_utxo( + async fn clear_sync_state(&mut self) -> dash_spv::error::StorageResult<()> { + Ok(()) + } + + async fn store_sync_checkpoint( + &mut self, + _height: u32, + _checkpoint: &SyncCheckpoint, + ) -> dash_spv::error::StorageResult<()> { + Ok(()) + } + + async fn get_sync_checkpoints( + &self, + _start_height: u32, + _end_height: u32, + ) -> dash_spv::error::StorageResult> { + Ok(vec![]) + } + + async fn store_chain_lock( + &mut self, + _height: u32, + _chain_lock: &dashcore::ChainLock, + ) -> dash_spv::error::StorageResult<()> { + Ok(()) + } + + async fn load_chain_lock( &self, - _outpoint: &dashcore::OutPoint, - ) -> dash_spv::error::StorageResult> { + _height: u32, + ) -> dash_spv::error::StorageResult> { Ok(None) } - async fn get_all_utxos( + async fn get_chain_locks( + &self, + _start_height: u32, + _end_height: u32, + ) -> dash_spv::error::StorageResult> { + Ok(vec![]) + } + + async fn store_instant_lock( + &mut self, + _txid: Txid, + _instant_lock: &dashcore::InstantLock, + ) -> dash_spv::error::StorageResult<()> { + Ok(()) + } + + async fn load_instant_lock( + &self, + _txid: Txid, + ) -> dash_spv::error::StorageResult> { + Ok(None) + } + + async fn store_mempool_transaction( + &mut self, + _txid: &Txid, + _tx: &dash_spv::types::UnconfirmedTransaction, + ) -> dash_spv::error::StorageResult<()> { + Ok(()) + } + + async fn remove_mempool_transaction( + &mut self, + _txid: &Txid, + ) -> dash_spv::error::StorageResult<()> { + Ok(()) + } + + async fn get_mempool_transaction( + &self, + _txid: &Txid, + ) -> dash_spv::error::StorageResult> { + Ok(None) + } + + async fn get_all_mempool_transactions( &self, ) -> dash_spv::error::StorageResult< - std::collections::HashMap, + std::collections::HashMap, > { Ok(std::collections::HashMap::new()) } @@ -638,25 +827,17 @@ impl StorageManager for MockStorageManager { Ok(()) } - async fn get_mempool_state( + async fn load_mempool_state( &self, ) -> dash_spv::error::StorageResult> { Ok(None) } - async fn store_masternode_state( - &mut self, - _state: &dash_spv::storage::MasternodeState, - ) -> dash_spv::error::StorageResult<()> { + async fn clear_mempool(&mut self) -> dash_spv::error::StorageResult<()> { Ok(()) } - async fn get_masternode_state( - &self, - ) -> dash_spv::error::StorageResult> { - Ok(None) + async fn shutdown(&mut self) -> dash_spv::error::StorageResult<()> { + Ok(()) } - - // Terminal block methods removed from StorageManager trait - // These methods are no longer part of the trait } diff --git a/dash-spv/tests/filter_header_verification_test.rs b/dash-spv/tests/filter_header_verification_test.rs index 02ffbf2eb..138f6691a 100644 --- a/dash-spv/tests/filter_header_verification_test.rs +++ b/dash-spv/tests/filter_header_verification_test.rs @@ -3,7 +3,7 @@ //! NOTE: This test file is currently disabled due to incomplete mock NetworkManager implementation. //! TODO: Re-enable once NetworkManager trait methods are fully implemented. -#![cfg(skip_mock_implementation_incomplete)] +#![cfg(feature = "skip_mock_implementation_incomplete")] //! Test to replicate the filter header chain verification failure observed in production. //! @@ -202,6 +202,7 @@ fn calculate_expected_filter_header( FilterHeader::from_byte_array(sha256d::Hash::hash(&data).to_byte_array()) } +#[ignore = "mock implementation incomplete"] #[tokio::test] async fn test_filter_header_verification_failure_reproduction() { let _ = env_logger::try_init(); @@ -214,7 +215,8 @@ async fn test_filter_header_verification_failure_reproduction() { let config = ClientConfig::new(Network::Dash); let received_heights = Arc::new(Mutex::new(HashSet::new())); - let mut filter_sync = FilterSyncManager::new(&config, received_heights); + let mut filter_sync: FilterSyncManager = + FilterSyncManager::new(&config, received_heights); // Step 1: Store initial headers to simulate having a synced header chain println!("Step 1: Setting up initial header chain..."); @@ -354,6 +356,7 @@ async fn test_filter_header_verification_failure_reproduction() { println!("different values for previous_filter_header, breaking chain continuity."); } +#[ignore = "mock implementation incomplete"] #[tokio::test] async fn test_overlapping_batches_from_different_peers() { let _ = env_logger::try_init(); @@ -374,7 +377,8 @@ async fn test_overlapping_batches_from_different_peers() { let config = ClientConfig::new(Network::Dash); let received_heights = Arc::new(Mutex::new(HashSet::new())); - let mut filter_sync = FilterSyncManager::new(&config, received_heights); + let mut filter_sync: FilterSyncManager = + FilterSyncManager::new(&config, received_heights); // Step 1: Set up headers for the full range we'll need println!("Step 1: Setting up header chain (heights 1-3000)..."); @@ -532,6 +536,7 @@ async fn test_overlapping_batches_from_different_peers() { ); } +#[ignore = "mock implementation incomplete"] #[tokio::test] async fn test_filter_header_verification_overlapping_batches() { let _ = env_logger::try_init(); @@ -546,7 +551,8 @@ async fn test_filter_header_verification_overlapping_batches() { let config = ClientConfig::new(Network::Dash); let received_heights = Arc::new(Mutex::new(HashSet::new())); - let mut filter_sync = FilterSyncManager::new(&config, received_heights); + let mut filter_sync: FilterSyncManager = + FilterSyncManager::new(&config, received_heights); // Set up initial headers - start from 1 for proper sync let initial_headers = create_test_headers_range(1, 2000); @@ -626,6 +632,7 @@ async fn test_filter_header_verification_overlapping_batches() { assert!(final_filter_tip >= batch1_end); // Should be at least as high as before } +#[ignore = "mock implementation incomplete"] #[tokio::test] async fn test_filter_header_verification_race_condition_simulation() { let _ = env_logger::try_init(); @@ -640,7 +647,8 @@ async fn test_filter_header_verification_race_condition_simulation() { let config = ClientConfig::new(Network::Dash); let received_heights = Arc::new(Mutex::new(HashSet::new())); - let mut filter_sync = FilterSyncManager::new(&config, received_heights); + let mut filter_sync: FilterSyncManager = + FilterSyncManager::new(&config, received_heights); // Set up headers - need enough for batch B (up to height 3000) let initial_headers = create_test_headers_range(1, 3001); diff --git a/dash-spv/tests/instantsend_integration_test.rs b/dash-spv/tests/instantsend_integration_test.rs index c17d13621..751431fa5 100644 --- a/dash-spv/tests/instantsend_integration_test.rs +++ b/dash-spv/tests/instantsend_integration_test.rs @@ -1,3 +1,9 @@ +#![cfg(feature = "skip_mock_implementation_incomplete")] + +// This test is currently disabled because the SPVWalletManager API has changed +// and these methods don't exist anymore. The test needs to be rewritten to use +// the new wallet interface. + // dash-spv/tests/instantsend_integration_test.rs // // TODO: These tests need to be updated to work with the new SPVWalletManager API @@ -8,9 +14,7 @@ // - get_balance (should be get_total_balance) // - process_verified_instantlock // -// Commenting out the entire file until the tests can be properly updated. - -#![cfg(skip_instantsend_tests)] +// These tests are currently ignored until they can be properly updated. use std::sync::Arc; use tokio::sync::RwLock; @@ -26,7 +30,10 @@ use dashcore::{ Witness, }; use dashcore_hashes::{sha256d, Hash}; -use key_wallet_manager::{spv_wallet_manager::SPVWalletManager, Utxo}; +use key_wallet::wallet::managed_wallet_info::ManagedWalletInfo; +use key_wallet_manager::{ + spv_wallet_manager::SPVWalletManager, wallet_manager::WalletManager, Utxo, +}; use rand::thread_rng; /// Helper to create a test wallet manager. @@ -95,6 +102,7 @@ fn create_signed_instantlock(tx: &Transaction, _sk: &SecretKey) } #[tokio::test] +#[ignore = "instantsend tests not yet updated"] async fn test_instantsend_end_to_end() { let wallet = create_test_wallet(); let address = create_test_address(); @@ -116,8 +124,12 @@ async fn test_instantsend_end_to_end() { false, // is_coinbase ); initial_utxo.is_confirmed = true; - wallet.write().await.add_utxo(initial_utxo).await.unwrap(); - wallet.write().await.add_watched_address(address).await.unwrap(); + + // TODO: The SPVWalletManager API has changed. These methods no longer exist: + // - add_utxo() - need to use WalletInterface methods or direct base access + // - add_watched_address() - need to use different approach for monitoring + // wallet.write().await.add_utxo(initial_utxo).await.unwrap(); + // wallet.write().await.add_watched_address(address).await.unwrap(); // 2. Create a transaction that spends the UTXO. let spend_amount = 80_000_000; @@ -147,8 +159,10 @@ async fn test_instantsend_end_to_end() { // not creating new UTXOs for us. We'll test InstantLock processing in the next section. // 5. Assert the wallet state has been updated correctly. - let utxos = wallet.read().await.get_utxos().await; - let spent_utxo = utxos.iter().find(|u| u.outpoint == initial_outpoint); + // TODO: get_utxos() method no longer exists on SPVWalletManager + // Need to access UTXOs through WalletInterface or base WalletManager + // let utxos = wallet.read().await.get_utxos().await; + // let spent_utxo = utxos.iter().find(|u| u.outpoint == initial_outpoint); // The original UTXO should now be marked as instant-locked. // Note: In a real scenario, the UTXO would be *removed* and a new *change* UTXO added. @@ -166,7 +180,8 @@ async fn test_instantsend_end_to_end() { // Let's create a new UTXO that represents a payment *to* us, and then InstantLock it. let wallet = create_test_wallet(); let address = create_test_address(); - wallet.write().await.add_watched_address(address.clone()).await.unwrap(); + // TODO: add_watched_address() method no longer exists + // wallet.write().await.add_watched_address(address.clone()).await.unwrap(); let incoming_amount = 50_000_000; // Create a transaction with a dummy input (from external source) @@ -192,12 +207,14 @@ async fn test_instantsend_end_to_end() { 0, // In mempool false, // is_coinbase ); - wallet.write().await.add_utxo(incoming_utxo).await.unwrap(); + // TODO: add_utxo() method no longer exists + // wallet.write().await.add_utxo(incoming_utxo).await.unwrap(); // Balance should be pending. - let balance1 = wallet.read().await.get_balance().await.unwrap(); - assert_eq!(balance1.pending, Amount::from_sat(incoming_amount)); - assert_eq!(balance1.instantlocked, Amount::ZERO); + // TODO: get_balance() method no longer exists - need to use get_total_balance() or similar + // let balance1 = wallet.read().await.get_balance().await.unwrap(); + // assert_eq!(balance1.pending, Amount::from_sat(incoming_amount)); + // assert_eq!(balance1.instantlocked, Amount::ZERO); // Create and process the InstantLock. let sk = SecretKey::::random(&mut thread_rng()); @@ -207,17 +224,20 @@ async fn test_instantsend_end_to_end() { let validator = dash_spv::validation::InstantLockValidator::new(); assert!(validator.validate(&instant_lock).is_ok()); - let updated = - wallet.write().await.process_verified_instantlock(incoming_tx.txid()).await.unwrap(); - assert!(updated); + // TODO: process_verified_instantlock() method no longer exists + // let updated = + // wallet.write().await.process_verified_instantlock(incoming_tx.txid()).await.unwrap(); + // assert!(updated); // Verify the UTXO is now marked as instant-locked. - let utxos = wallet.read().await.get_utxos().await; - let locked_utxo = utxos.iter().find(|u| u.outpoint == incoming_outpoint).unwrap(); - assert!(locked_utxo.is_instantlocked); + // TODO: get_utxos() method no longer exists + // let utxos = wallet.read().await.get_utxos().await; + // let locked_utxo = utxos.iter().find(|u| u.outpoint == incoming_outpoint).unwrap(); + // assert!(locked_utxo.is_instantlocked); // Verify the balance has moved from pending to instantlocked. - let balance2 = wallet.read().await.get_balance().await.unwrap(); - assert_eq!(balance2.pending, Amount::ZERO); - assert_eq!(balance2.instantlocked, Amount::from_sat(incoming_amount)); + // TODO: get_balance() method no longer exists + // let balance2 = wallet.read().await.get_balance().await.unwrap(); + // assert_eq!(balance2.pending, Amount::ZERO); + // assert_eq!(balance2.instantlocked, Amount::from_sat(incoming_amount)); } diff --git a/dash-spv/tests/integration_real_node_test.rs b/dash-spv/tests/integration_real_node_test.rs index 9385efba6..2841d8b0f 100644 --- a/dash-spv/tests/integration_real_node_test.rs +++ b/dash-spv/tests/integration_real_node_test.rs @@ -62,6 +62,7 @@ async fn check_node_availability() -> bool { } #[tokio::test] +#[ignore = "requires local Dash Core node"] async fn test_real_node_connectivity() { let _ = env_logger::try_init(); @@ -104,6 +105,7 @@ async fn test_real_node_connectivity() { } #[tokio::test] +#[ignore = "requires local Dash Core node"] async fn test_real_header_sync_genesis_to_1000() { let _ = env_logger::try_init(); @@ -178,6 +180,7 @@ async fn test_real_header_sync_genesis_to_1000() { } #[tokio::test] +#[ignore = "requires local Dash Core node"] async fn test_real_header_sync_up_to_10k() { let _ = env_logger::try_init(); @@ -328,6 +331,7 @@ async fn test_real_header_sync_up_to_10k() { } #[tokio::test] +#[ignore = "requires local Dash Core node"] async fn test_real_header_validation_with_node() { let _ = env_logger::try_init(); @@ -390,6 +394,7 @@ async fn test_real_header_validation_with_node() { } #[tokio::test] +#[ignore = "requires local Dash Core node"] async fn test_real_header_chain_continuity() { let _ = env_logger::try_init(); @@ -462,6 +467,7 @@ async fn test_real_header_chain_continuity() { } #[tokio::test] +#[ignore = "requires local Dash Core node"] async fn test_real_node_sync_resumption() { let _ = env_logger::try_init(); @@ -527,6 +533,7 @@ async fn test_real_node_sync_resumption() { } #[tokio::test] +#[ignore = "requires local Dash Core node"] async fn test_real_node_performance_benchmarks() { let _ = env_logger::try_init(); diff --git a/dash-spv/tests/multi_peer_test.rs b/dash-spv/tests/multi_peer_test.rs index 9beedb960..ba6aa24f3 100644 --- a/dash-spv/tests/multi_peer_test.rs +++ b/dash-spv/tests/multi_peer_test.rs @@ -189,7 +189,6 @@ async fn test_max_peer_limit() { let _ = env_logger::builder().is_test(true).try_init(); let temp_dir = TempDir::new().unwrap(); - let temp_path = temp_dir.path().to_path_buf(); let mut config = create_test_config(Network::Testnet, Some(temp_dir)); // Add at least one peer to avoid "No peers specified" error diff --git a/dash-spv/tests/qrinfo_integration_test.rs b/dash-spv/tests/qrinfo_integration_test.rs index 84673c26f..be29d1f45 100644 --- a/dash-spv/tests/qrinfo_integration_test.rs +++ b/dash-spv/tests/qrinfo_integration_test.rs @@ -11,7 +11,6 @@ use dashcore::{ BlockHash, Network, QuorumHash, Transaction, }; use dashcore_hashes::Hash; -use log::info; use std::time::Duration; /// Helper to generate test QRInfo data diff --git a/dash-spv/tests/rollback_test.rs b/dash-spv/tests/rollback_test.rs index fbbbb6537..b515dc2d7 100644 --- a/dash-spv/tests/rollback_test.rs +++ b/dash-spv/tests/rollback_test.rs @@ -3,7 +3,7 @@ //! NOTE: This test file is currently disabled due to incomplete mock StorageManager implementation. //! TODO: Re-enable once StorageManager trait methods are fully implemented. -#![cfg(skip_mock_implementation_incomplete)] +#![cfg(feature = "skip_mock_implementation_incomplete")] use dash_spv::storage::{DiskStorageManager, StorageManager}; use dashcore::{ @@ -14,6 +14,7 @@ use dashcore::{ use dashcore_hashes::Hash; use tempfile::TempDir; +#[ignore = "mock implementation incomplete"] #[tokio::test] #[ignore = "rollback_to_height not implemented in StorageManager trait"] async fn test_disk_storage_rollback() -> Result<(), Box> { @@ -73,6 +74,7 @@ async fn test_disk_storage_rollback() -> Result<(), Box> Ok(()) } +#[ignore = "mock implementation incomplete"] #[tokio::test] #[ignore = "rollback_to_height not implemented in StorageManager trait"] async fn test_disk_storage_rollback_filter_headers() -> Result<(), Box> { diff --git a/dash-spv/tests/smart_fetch_integration_test.rs b/dash-spv/tests/smart_fetch_integration_test.rs index 4983f7887..9a0b3d370 100644 --- a/dash-spv/tests/smart_fetch_integration_test.rs +++ b/dash-spv/tests/smart_fetch_integration_test.rs @@ -17,7 +17,7 @@ async fn test_smart_fetch_basic_dkg_windows() { assert!(!windows.is_empty()); // Each window should be within our range - for (height, window_list) in &windows { + for (_height, window_list) in &windows { for window in window_list { // Mining window should overlap with our range assert!(window.mining_end >= 1000 || window.mining_start <= 1100); diff --git a/dash-spv/tests/wallet_integration_test.rs b/dash-spv/tests/wallet_integration_test.rs index c5a7b956d..3a6456ab7 100644 --- a/dash-spv/tests/wallet_integration_test.rs +++ b/dash-spv/tests/wallet_integration_test.rs @@ -8,7 +8,7 @@ use tokio::sync::RwLock; use dash_spv::network::MultiPeerNetworkManager; use dash_spv::storage::MemoryStorageManager; use dash_spv::{ClientConfig, DashSpvClient}; -use dashcore::{Block, Network}; +use dashcore::Network; use key_wallet::wallet::managed_wallet_info::ManagedWalletInfo; use key_wallet_manager::spv_wallet_manager::SPVWalletManager; use key_wallet_manager::wallet_manager::WalletManager; @@ -63,7 +63,7 @@ async fn test_spv_client_start_stop() { #[tokio::test] async fn test_wallet_manager_basic_operations() { // Test basic wallet manager operations - let mut wallet_manager = SPVWalletManager::with_base(WalletManager::::new()); + let wallet_manager = SPVWalletManager::with_base(WalletManager::::new()); // Test that we can create a wallet manager // SPVWalletManager doesn't have get_watched_scripts method anymore diff --git a/dash/Cargo.toml b/dash/Cargo.toml index dd1e358c9..30f357511 100644 --- a/dash/Cargo.toml +++ b/dash/Cargo.toml @@ -79,6 +79,7 @@ bincode = { version= "=2.0.0-rc.3" } assert_matches = "1.5.0" dashcore = { path = ".", features = ["core-block-hash-use-x11", "message_verification", "quorum_validation", "signer"] } criterion = "0.5" +key-wallet = { path = "../key-wallet" } [[example]] diff --git a/dash/examples/ecdsa-psbt.rs b/dash/examples/ecdsa-psbt.rs index 7f1d64c28..abeef5e44 100644 --- a/dash/examples/ecdsa-psbt.rs +++ b/dash/examples/ecdsa-psbt.rs @@ -33,15 +33,15 @@ use std::collections::BTreeMap; use std::fmt; use std::str::FromStr; -use dashcore::bip32::{ - ChildNumber, DerivationPath, ExtendedPrivKey, ExtendedPubKey, Fingerprint, IntoDerivationPath, -}; use dashcore::consensus::encode; -use dashcore::psbt::{self, Input, Psbt, PsbtSighashType}; use dashcore::secp256k1::{Secp256k1, Signing, Verification}; use dashcore::{ Address, Amount, Network, OutPoint, PublicKey, ScriptBuf, Transaction, TxIn, TxOut, Witness, }; +use key_wallet::bip32::{ + ChildNumber, DerivationPath, ExtendedPrivKey, ExtendedPubKey, Fingerprint, IntoDerivationPath, +}; +use key_wallet::psbt::{self, Input, Psbt, PsbtSighashType}; type Result = std::result::Result; diff --git a/dash/examples/taproot-psbt.rs b/dash/examples/taproot-psbt.rs index c87b69317..1611106bf 100644 --- a/dash/examples/taproot-psbt.rs +++ b/dash/examples/taproot-psbt.rs @@ -78,12 +78,11 @@ const UTXO_3: P2trUtxo = P2trUtxo { use std::collections::BTreeMap; use std::str::FromStr; -use dashcore::bip32::{ChildNumber, DerivationPath, ExtendedPrivKey, ExtendedPubKey, Fingerprint}; use dashcore::consensus::encode; use dashcore::constants::COIN_VALUE; +use dashcore::hashes::Hash; use dashcore::key::{TapTweak, XOnlyPublicKey}; use dashcore::opcodes::all::{OP_CHECKSIG, OP_CLTV, OP_DROP}; -use dashcore::psbt::{self, Input, Output, Psbt, PsbtSighashType}; use dashcore::secp256k1::Secp256k1; use dashcore::sighash::{self, SighashCache, TapSighash, TapSighashType}; use dashcore::taproot::{self, LeafVersion, TapLeafHash, TaprootBuilder, TaprootSpendInfo}; @@ -91,6 +90,10 @@ use dashcore::{ Address, Amount, Network, OutPoint, ScriptBuf, Transaction, TxIn, TxOut, Witness, absolute, script, }; +use key_wallet::bip32::{ + ChildNumber, DerivationPath, ExtendedPrivKey, ExtendedPubKey, Fingerprint, +}; +use key_wallet::psbt::{self, Input, Output, Psbt, PsbtSighashType}; fn main() -> Result<(), Box> { let secp = Secp256k1::new(); @@ -785,7 +788,7 @@ fn sign_psbt_taproot( Some(_) => keypair, // no tweak for script spend }; - let sig = secp.sign_schnorr(&hash.into(), &keypair); + let sig = secp.sign_schnorr(hash.as_byte_array(), &keypair); let final_signature = taproot::Signature { sig, diff --git a/dash/src/blockdata/script/mod.rs b/dash/src/blockdata/script/mod.rs index 6cfeb40ba..619d1a98a 100644 --- a/dash/src/blockdata/script/mod.rs +++ b/dash/src/blockdata/script/mod.rs @@ -799,7 +799,7 @@ impl std::error::Error for Error { | UnknownSpentOutput(_) | Serialization => None, #[cfg(feature = "bitcoinconsensus")] - BitcoinConsensus(ref e) => Some(e), + BitcoinConsensus(_) => None, } } } diff --git a/rpc-client/src/lib.rs b/rpc-client/src/lib.rs index 1c1130043..1aaf1fba7 100644 --- a/rpc-client/src/lib.rs +++ b/rpc-client/src/lib.rs @@ -15,10 +15,10 @@ #![crate_name = "dashcore_rpc"] #![crate_type = "rlib"] +#![allow(unused)] #[macro_use] extern crate log; -#[allow(unused)] #[macro_use] // `macro_use` is needed for v1.24.0 compilation. extern crate serde; extern crate serde_json; 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 15c3a4442..d2246a252 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 @@ -315,9 +315,9 @@ bool dash_spv_ffi_client_is_filter_sync_available(struct FFIDashSpvClient *clien FFIBalance *dash_spv_ffi_client_get_address_balance(struct FFIDashSpvClient *client, const char *address); -struct FFIArray dash_spv_ffi_client_get_utxos(struct FFIDashSpvClient *client); +struct FFIArray *dash_spv_ffi_client_get_utxos(struct FFIDashSpvClient *client); -struct FFIArray dash_spv_ffi_client_get_utxos_for_address(struct FFIDashSpvClient *client, +struct FFIArray *dash_spv_ffi_client_get_utxos_for_address(struct FFIDashSpvClient *client, const char *address); int32_t dash_spv_ffi_client_set_event_callbacks(struct FFIDashSpvClient *client, @@ -337,7 +337,7 @@ int32_t dash_spv_ffi_client_watch_script(struct FFIDashSpvClient *client, const int32_t dash_spv_ffi_client_unwatch_script(struct FFIDashSpvClient *client, const char *script_hex); -struct FFIArray dash_spv_ffi_client_get_address_history(struct FFIDashSpvClient *client, +struct FFIArray *dash_spv_ffi_client_get_address_history(struct FFIDashSpvClient *client, const char *address); struct FFITransaction *dash_spv_ffi_client_get_transaction(struct FFIDashSpvClient *client, @@ -346,9 +346,9 @@ struct FFITransaction *dash_spv_ffi_client_get_transaction(struct FFIDashSpvClie int32_t dash_spv_ffi_client_broadcast_transaction(struct FFIDashSpvClient *client, const char *tx_hex); -struct FFIArray dash_spv_ffi_client_get_watched_addresses(struct FFIDashSpvClient *client); +struct FFIArray *dash_spv_ffi_client_get_watched_addresses(struct FFIDashSpvClient *client); -struct FFIArray dash_spv_ffi_client_get_watched_scripts(struct FFIDashSpvClient *client); +struct FFIArray *dash_spv_ffi_client_get_watched_scripts(struct FFIDashSpvClient *client); FFIBalance *dash_spv_ffi_client_get_total_balance(struct FFIDashSpvClient *client); @@ -363,7 +363,7 @@ int32_t dash_spv_ffi_client_is_transaction_confirmed(struct FFIDashSpvClient *cl void dash_spv_ffi_transaction_destroy(struct FFITransaction *tx); -struct FFIArray dash_spv_ffi_client_get_address_utxos(struct FFIDashSpvClient *client, +struct FFIArray *dash_spv_ffi_client_get_address_utxos(struct FFIDashSpvClient *client, const char *address); int32_t dash_spv_ffi_client_enable_mempool_tracking(struct FFIDashSpvClient *client,