diff --git a/dash-spv-ffi/Cargo.toml b/dash-spv-ffi/Cargo.toml index 6341e94eb..3fc359a4e 100644 --- a/dash-spv-ffi/Cargo.toml +++ b/dash-spv-ffi/Cargo.toml @@ -23,6 +23,7 @@ log = "0.4" hex = "0.4" env_logger = "0.10" tracing = "0.1" +futures = "0.3" # Use key-wallet-ffi for all wallet-related FFI types key-wallet-ffi = { path = "../key-wallet-ffi" } # Still need these for SPV client internals (not for FFI types) diff --git a/dash-spv-ffi/FFI_API.md b/dash-spv-ffi/FFI_API.md index bb3c087c2..64b7084ac 100644 --- a/dash-spv-ffi/FFI_API.md +++ b/dash-spv-ffi/FFI_API.md @@ -4,7 +4,7 @@ This document provides a comprehensive reference for all FFI (Foreign Function I **Auto-generated**: This documentation is automatically generated from the source code. Do not edit manually. -**Total Functions**: 64 +**Total Functions**: 68 ## Table of Contents @@ -67,11 +67,12 @@ Functions: 26 ### Synchronization -Functions: 7 +Functions: 8 | Function | Description | Module | |----------|-------------|--------| | `dash_spv_ffi_client_cancel_sync` | Cancels the sync operation | client | +| `dash_spv_ffi_client_clear_sync_state` | Clear only the persisted sync-state snapshot | client | | `dash_spv_ffi_client_get_sync_progress` | Get the current sync progress snapshot | client | | `dash_spv_ffi_client_is_filter_sync_available` | Check if compact filter sync is currently available | client | | `dash_spv_ffi_client_sync_to_tip` | Sync the SPV client to the chain tip | client | @@ -135,7 +136,7 @@ Functions: 2 ### Utility Functions -Functions: 15 +Functions: 18 | Function | Description | Module | |----------|-------------|--------| @@ -144,7 +145,10 @@ Functions: 15 | `dash_spv_ffi_checkpoint_before_timestamp` | Get the last checkpoint at or before a given UNIX timestamp (seconds) | checkpoints | | `dash_spv_ffi_checkpoint_latest` | Get the latest checkpoint for the given network | checkpoints | | `dash_spv_ffi_checkpoints_between_heights` | Get all checkpoints between two heights (inclusive) | checkpoints | +| `dash_spv_ffi_client_clear_storage` | Clear all persisted SPV storage (headers, filters, metadata, sync state) | client | | `dash_spv_ffi_client_get_stats` | Get current runtime statistics for the SPV client | client | +| `dash_spv_ffi_client_get_tip_hash` | Get the current chain tip hash (32 bytes) if available | client | +| `dash_spv_ffi_client_get_tip_height` | Get the current chain tip height (absolute) | client | | `dash_spv_ffi_client_get_wallet_manager` | Get the wallet manager from the SPV client Returns an opaque pointer to FFIW... | client | | `dash_spv_ffi_client_record_send` | Record that we attempted to send a transaction by its txid | client | | `dash_spv_ffi_client_rescan_blockchain` | Request a rescan of the blockchain from a given height (not yet implemented) | client | @@ -632,7 +636,7 @@ dash_spv_ffi_client_cancel_sync(client: *mut FFIDashSpvClient) -> i32 ``` **Description:** -Cancels the sync operation. **Note**: This function currently only stops the SPV client and clears sync callbacks, but does not fully abort the ongoing sync process. The sync operation may continue running in the background until it completes naturally. Full sync cancellation with proper task abortion is not yet implemented. # Safety The client pointer must be valid and non-null. # Returns Returns 0 on success, or an error code on failure. +Cancels the sync operation. This stops the SPV client, clears callbacks, and joins active threads so the sync operation halts immediately. # Safety The client pointer must be valid and non-null. # Returns Returns 0 on success, or an error code on failure. **Safety:** The client pointer must be valid and non-null. @@ -641,6 +645,22 @@ The client pointer must be valid and non-null. --- +#### `dash_spv_ffi_client_clear_sync_state` + +```c +dash_spv_ffi_client_clear_sync_state(client: *mut FFIDashSpvClient,) -> i32 +``` + +**Description:** +Clear only the persisted sync-state snapshot. # Safety - `client` must be a valid, non-null pointer. + +**Safety:** +- `client` must be a valid, non-null pointer. + +**Module:** `client` + +--- + #### `dash_spv_ffi_client_get_sync_progress` ```c @@ -996,6 +1016,22 @@ Get all checkpoints between two heights (inclusive). Returns an `FFIArray` of ` --- +#### `dash_spv_ffi_client_clear_storage` + +```c +dash_spv_ffi_client_clear_storage(client: *mut FFIDashSpvClient) -> i32 +``` + +**Description:** +Clear all persisted SPV storage (headers, filters, metadata, sync state). # Safety - `client` must be a valid, non-null pointer. + +**Safety:** +- `client` must be a valid, non-null pointer. + +**Module:** `client` + +--- + #### `dash_spv_ffi_client_get_stats` ```c @@ -1012,6 +1048,38 @@ Get current runtime statistics for the SPV client. # Safety - `client` must be --- +#### `dash_spv_ffi_client_get_tip_hash` + +```c +dash_spv_ffi_client_get_tip_hash(client: *mut FFIDashSpvClient, out_hash: *mut u8,) -> i32 +``` + +**Description:** +Get the current chain tip hash (32 bytes) if available. # Safety - `client` must be a valid, non-null pointer. - `out_hash` must be a valid pointer to a 32-byte buffer. + +**Safety:** +- `client` must be a valid, non-null pointer. - `out_hash` must be a valid pointer to a 32-byte buffer. + +**Module:** `client` + +--- + +#### `dash_spv_ffi_client_get_tip_height` + +```c +dash_spv_ffi_client_get_tip_height(client: *mut FFIDashSpvClient, out_height: *mut u32,) -> i32 +``` + +**Description:** +Get the current chain tip height (absolute). # Safety - `client` must be a valid, non-null pointer. - `out_height` must be a valid, non-null pointer. + +**Safety:** +- `client` must be a valid, non-null pointer. - `out_height` must be a valid, non-null pointer. + +**Module:** `client` + +--- + #### `dash_spv_ffi_client_get_wallet_manager` ```c diff --git a/dash-spv-ffi/include/dash_spv_ffi.h b/dash-spv-ffi/include/dash_spv_ffi.h index cfe225e20..a5d104bf4 100644 --- a/dash-spv-ffi/include/dash_spv_ffi.h +++ b/dash-spv-ffi/include/dash_spv_ffi.h @@ -392,10 +392,8 @@ int32_t dash_spv_ffi_client_sync_to_tip_with_progress(struct FFIDashSpvClient *c /** * Cancels the sync operation. * - * **Note**: This function currently only stops the SPV client and clears sync callbacks, - * but does not fully abort the ongoing sync process. The sync operation may continue - * running in the background until it completes naturally. Full sync cancellation with - * proper task abortion is not yet implemented. + * This stops the SPV client, clears callbacks, and joins active threads so the sync + * operation halts immediately. * * # Safety * The client pointer must be valid and non-null. @@ -421,6 +419,40 @@ int32_t dash_spv_ffi_client_sync_to_tip_with_progress(struct FFIDashSpvClient *c */ struct FFISpvStats *dash_spv_ffi_client_get_stats(struct FFIDashSpvClient *client) ; +/** + * Get the current chain tip hash (32 bytes) if available. + * + * # Safety + * - `client` must be a valid, non-null pointer. + * - `out_hash` must be a valid pointer to a 32-byte buffer. + */ + int32_t dash_spv_ffi_client_get_tip_hash(struct FFIDashSpvClient *client, uint8_t *out_hash) ; + +/** + * Get the current chain tip height (absolute). + * + * # Safety + * - `client` must be a valid, non-null pointer. + * - `out_height` must be a valid, non-null pointer. + */ + int32_t dash_spv_ffi_client_get_tip_height(struct FFIDashSpvClient *client, uint32_t *out_height) ; + +/** + * Clear all persisted SPV storage (headers, filters, metadata, sync state). + * + * # Safety + * - `client` must be a valid, non-null pointer. + */ + int32_t dash_spv_ffi_client_clear_storage(struct FFIDashSpvClient *client) ; + +/** + * Clear only the persisted sync-state snapshot. + * + * # Safety + * - `client` must be a valid, non-null pointer. + */ + int32_t dash_spv_ffi_client_clear_sync_state(struct FFIDashSpvClient *client) ; + /** * Check if compact filter sync is currently available. * diff --git a/dash-spv-ffi/src/client.rs b/dash-spv-ffi/src/client.rs index dbd2288a0..a564b90f6 100644 --- a/dash-spv-ffi/src/client.rs +++ b/dash-spv-ffi/src/client.rs @@ -5,10 +5,13 @@ use crate::{ // Import wallet types from key-wallet-ffi use key_wallet_ffi::FFIWalletManager; +use dash_spv::storage::DiskStorageManager; use dash_spv::types::SyncStage; use dash_spv::DashSpvClient; +use dash_spv::Hash; use dashcore::Txid; +use futures::future::{AbortHandle, Abortable}; use once_cell::sync::Lazy; use std::collections::HashMap; use std::ffi::{CStr, CString}; @@ -100,13 +103,19 @@ struct SyncCallbackData { _marker: std::marker::PhantomData<()>, } +async fn wait_for_shutdown_signal(signal: Arc) { + while !signal.load(Ordering::Relaxed) { + tokio::time::sleep(Duration::from_millis(50)).await; + } +} + /// FFIDashSpvClient structure type InnerClient = DashSpvClient< key_wallet_manager::wallet_manager::WalletManager< key_wallet::wallet::managed_wallet_info::ManagedWalletInfo, >, dash_spv::network::MultiPeerNetworkManager, - dash_spv::storage::MemoryStorageManager, + DiskStorageManager, >; type SharedClient = Arc>>; @@ -144,11 +153,24 @@ pub unsafe extern "C" fn dash_spv_ffi_client_new( } }; - let client_config = config.clone_inner(); - let client_result = runtime.block_on(async { + let mut client_config = config.clone_inner(); + + let storage_path = client_config.storage_path.clone().unwrap_or_else(|| { + let mut path = std::env::temp_dir(); + path.push("dash-spv"); + path.push(format!("{:?}", client_config.network).to_lowercase()); + tracing::warn!( + "dash-spv FFI config missing storage path, falling back to temp dir {:?}", + path + ); + path + }); + client_config.storage_path = Some(storage_path.clone()); + + let client_result = runtime.block_on(async move { // Construct concrete implementations for generics let network = dash_spv::network::MultiPeerNetworkManager::new(&client_config).await; - let storage = dash_spv::storage::MemoryStorageManager::new().await; + let storage = DiskStorageManager::new(storage_path.clone()).await; let wallet = key_wallet_manager::wallet_manager::WalletManager::< key_wallet::wallet::managed_wallet_info::ManagedWalletInfo, >::new(); @@ -192,6 +214,19 @@ impl FFIDashSpvClient { self.runtime.block_on(f()) } + fn join_active_threads(&self) { + let handles = { + let mut guard = self.active_threads.lock().unwrap(); + std::mem::take(&mut *guard) + }; + + for handle in handles { + if let Err(e) = handle.join() { + tracing::error!("Failed to join active thread during cleanup: {:?}", e); + } + } + } + /// Start the event listener task to handle events from the SPV client. fn start_event_listener(&self) { let inner = self.inner.clone(); @@ -351,6 +386,44 @@ impl FFIDashSpvClient { } } +fn stop_client_internal(client: &FFIDashSpvClient) -> Result<(), dash_spv::SpvError> { + client.shutdown_signal.store(true, Ordering::Relaxed); + + // Ensure callbacks are cleared so no further progress/completion notifications fire. + { + let mut cb_guard = client.sync_callbacks.lock().unwrap(); + if let Some(ref callback_data) = *cb_guard { + CALLBACK_REGISTRY.lock().unwrap().unregister(callback_data.callback_id); + } + *cb_guard = None; + } + + client.join_active_threads(); + + let inner = client.inner.clone(); + let result = client.runtime.block_on(async { + let mut spv_client = { + let mut guard = inner.lock().unwrap(); + match guard.take() { + Some(client) => client, + None => { + return Err(dash_spv::SpvError::Storage(dash_spv::StorageError::NotFound( + "Client not initialized".to_string(), + ))) + } + } + }; + let res = spv_client.stop().await; + let mut guard = inner.lock().unwrap(); + *guard = Some(spv_client); + res + }); + + client.shutdown_signal.store(false, Ordering::Relaxed); + + result +} + /// Update the running client's configuration. /// /// # Safety @@ -428,6 +501,7 @@ pub unsafe extern "C" fn dash_spv_ffi_client_start(client: *mut FFIDashSpvClient match result { Ok(()) => { + client.shutdown_signal.store(false, Ordering::Relaxed); // Start event listener after successful start client.start_event_listener(); FFIErrorCode::Success as i32 @@ -448,27 +522,7 @@ pub unsafe extern "C" fn dash_spv_ffi_client_stop(client: *mut FFIDashSpvClient) null_check!(client); let client = &(*client); - let inner = client.inner.clone(); - - let result = client.runtime.block_on(async { - let mut spv_client = { - let mut guard = inner.lock().unwrap(); - match guard.take() { - Some(client) => client, - None => { - return Err(dash_spv::SpvError::Storage(dash_spv::StorageError::NotFound( - "Client not initialized".to_string(), - ))) - } - } - }; - let res = spv_client.stop().await; - let mut guard = inner.lock().unwrap(); - *guard = Some(spv_client); - res - }); - - match result { + match stop_client_internal(client) { Ok(()) => FFIErrorCode::Success as i32, Err(e) => { set_last_error(&e.to_string()); @@ -727,8 +781,7 @@ pub unsafe extern "C" fn dash_spv_ffi_client_sync_to_tip_with_progress( let inner = client.inner.clone(); let runtime = client.runtime.clone(); let sync_callbacks = client.sync_callbacks.clone(); - // Shared flag to coordinate internal threads during sync - let sync_running = Arc::new(AtomicBool::new(true)); + let shutdown_signal = client.shutdown_signal.clone(); // Take progress receiver from client let progress_receiver = { @@ -740,38 +793,51 @@ pub unsafe extern "C" fn dash_spv_ffi_client_sync_to_tip_with_progress( if let Some(mut receiver) = progress_receiver { let runtime_handle = runtime.handle().clone(); let sync_callbacks_clone = sync_callbacks.clone(); + let shutdown_signal_clone = shutdown_signal.clone(); let handle = std::thread::spawn(move || { runtime_handle.block_on(async move { - while let Some(progress) = receiver.recv().await { - // Handle callback in a thread-safe way - let should_stop = matches!(progress.sync_stage, SyncStage::Complete); - - // Create FFI progress - let ffi_progress = Box::new(FFIDetailedSyncProgress::from(progress)); - - // Call the callback using the registry - { - let cb_guard = sync_callbacks_clone.lock().unwrap(); - - if let Some(ref callback_data) = *cb_guard { - let registry = CALLBACK_REGISTRY.lock().unwrap(); - if let Some(CallbackInfo::Detailed { - progress_callback: Some(callback), - user_data, - .. - }) = registry.get(callback_data.callback_id) - { - // SAFETY: The callback and user_data are safely stored in the registry - // and accessed through thread-safe mechanisms. The registry ensures - // proper lifetime management without raw pointer passing across threads. - callback(ffi_progress.as_ref(), *user_data); + loop { + tokio::select! { + maybe_progress = receiver.recv() => { + match maybe_progress { + Some(progress) => { + // Handle callback in a thread-safe way + let should_stop = matches!(progress.sync_stage, SyncStage::Complete); + + // Create FFI progress + let ffi_progress = Box::new(FFIDetailedSyncProgress::from(progress)); + + // Call the callback using the registry + { + let cb_guard = sync_callbacks_clone.lock().unwrap(); + + if let Some(ref callback_data) = *cb_guard { + let registry = CALLBACK_REGISTRY.lock().unwrap(); + if let Some(CallbackInfo::Detailed { + progress_callback: Some(callback), + user_data, + .. + }) = registry.get(callback_data.callback_id) + { + // SAFETY: The callback and user_data are safely stored in the registry + // and accessed through thread-safe mechanisms. The registry ensures + // proper lifetime management without raw pointer passing across threads. + callback(ffi_progress.as_ref(), *user_data); + } + } + } + + if should_stop { + break; + } + } + None => break, } } - } - - if should_stop { - break; + _ = wait_for_shutdown_signal(shutdown_signal_clone.clone()) => { + break; + } } } }); @@ -784,30 +850,51 @@ 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 sync_running_for_join = sync_running.clone(); + let shutdown_signal_for_thread = shutdown_signal.clone(); + let stop_triggered_for_thread = Arc::new(AtomicBool::new(false)); let sync_handle = std::thread::spawn(move || { + let stop_triggered_for_callback = stop_triggered_for_thread.clone(); // Run monitoring loop - let monitor_result = runtime_handle.block_on(async move { - let mut spv_client = { - let mut guard = inner.lock().unwrap(); - match guard.take() { - Some(client) => client, - None => { - return Err(dash_spv::SpvError::Config( - "Client not initialized".to_string(), - )) + let monitor_result = runtime_handle.block_on({ + let inner = inner.clone(); + let shutdown_signal_for_thread = shutdown_signal_for_thread.clone(); + let stop_triggered_for_thread = stop_triggered_for_callback.clone(); + async move { + let mut spv_client = { + let mut guard = inner.lock().unwrap(); + match guard.take() { + Some(client) => client, + None => { + return Err(dash_spv::SpvError::Config( + "Client not initialized".to_string(), + )) + } } - } - }; - let res = spv_client.monitor_network().await; - let mut guard = inner.lock().unwrap(); - *guard = Some(spv_client); - res + }; + let (abort_handle, abort_registration) = AbortHandle::new_pair(); + let mut monitor_future = + Box::pin(Abortable::new(spv_client.monitor_network(), abort_registration)); + let result = tokio::select! { + res = &mut monitor_future => match res { + Ok(inner) => inner, + Err(_) => Ok(()), + }, + _ = wait_for_shutdown_signal(shutdown_signal_for_thread.clone()) => { + stop_triggered_for_thread.store(true, Ordering::Relaxed); + abort_handle.abort(); + match monitor_future.as_mut().await { + Ok(inner) => inner, + Err(_) => Ok(()), + } + } + }; + drop(monitor_future); + let mut guard = inner.lock().unwrap(); + *guard = Some(spv_client); + result + } }); - // Signal background handlers to stop - sync_running_for_join.store(false, Ordering::Relaxed); - // Send completion callback and cleanup { let mut cb_guard = sync_callbacks_clone.lock().unwrap(); @@ -819,31 +906,29 @@ pub unsafe extern "C" fn dash_spv_ffi_client_sync_to_tip_with_progress( .. }) = registry.unregister(callback_data.callback_id) { - match monitor_result { - Ok(_) => { - let msg = - CString::new("Sync completed successfully").unwrap_or_else(|_| { - CString::new("Sync completed") - .expect("hardcoded string is safe") - }); - // SAFETY: The callback and user_data are safely managed through the registry. - // The registry ensures proper lifetime management and thread safety. - // The string pointer is only valid for the duration of the callback. - callback(true, msg.as_ptr(), user_data); - // CString is automatically dropped here, which is safe because the callback - // should not store or use the pointer after it returns - } - Err(e) => { - let msg = match CString::new(format!("Sync failed: {}", e)) { - Ok(s) => s, - Err(_) => { - CString::new("Sync failed").expect("hardcoded string is safe") - } - }; - // SAFETY: Same as above - callback(false, msg.as_ptr(), user_data); - // CString is automatically dropped here, which is safe because the callback - // should not store or use the pointer after it returns + if stop_triggered_for_callback.load(Ordering::Relaxed) { + let msg = CString::new("Sync stopped by request").unwrap_or_else(|_| { + CString::new("Sync stopped").expect("hardcoded string is safe") + }); + callback(false, msg.as_ptr(), user_data); + } else { + match monitor_result { + Ok(_) => { + let msg = CString::new("Sync completed successfully") + .unwrap_or_else(|_| { + CString::new("Sync completed") + .expect("hardcoded string is safe") + }); + callback(true, msg.as_ptr(), user_data); + } + Err(e) => { + let msg = match CString::new(format!("Sync failed: {}", e)) { + Ok(s) => s, + Err(_) => CString::new("Sync failed") + .expect("hardcoded string is safe"), + }; + callback(false, msg.as_ptr(), user_data); + } } } } @@ -863,10 +948,8 @@ pub unsafe extern "C" fn dash_spv_ffi_client_sync_to_tip_with_progress( /// Cancels the sync operation. /// -/// **Note**: This function currently only stops the SPV client and clears sync callbacks, -/// but does not fully abort the ongoing sync process. The sync operation may continue -/// running in the background until it completes naturally. Full sync cancellation with -/// proper task abortion is not yet implemented. +/// This stops the SPV client, clears callbacks, and joins active threads so the sync +/// operation halts immediately. /// /// # Safety /// The client pointer must be valid and non-null. @@ -879,34 +962,8 @@ pub unsafe extern "C" fn dash_spv_ffi_client_cancel_sync(client: *mut FFIDashSpv let client = &(*client); - // Clear callbacks to stop progress updates and unregister from the registry - let mut cb_guard = client.sync_callbacks.lock().unwrap(); - if let Some(ref callback_data) = *cb_guard { - CALLBACK_REGISTRY.lock().unwrap().unregister(callback_data.callback_id); - } - *cb_guard = None; - - // TODO: Implement proper sync task cancellation using cancellation tokens or abort handles. - // Currently, this only stops the client, but the sync task may continue running in the background. - let inner = client.inner.clone(); - let result = client.runtime.block_on(async { - let mut spv_client = { - let mut guard = inner.lock().unwrap(); - match guard.take() { - Some(client) => client, - None => { - return Err(dash_spv::SpvError::Config("Client not initialized".to_string())) - } - } - }; - let res = spv_client.stop().await; - let mut guard = inner.lock().unwrap(); - *guard = Some(spv_client); - res - }); - - match result { - Ok(_) => FFIErrorCode::Success as i32, + match stop_client_internal(client) { + Ok(()) => FFIErrorCode::Success as i32, Err(e) => { set_last_error(&e.to_string()); FFIErrorCode::from(e) as i32 @@ -994,6 +1051,187 @@ pub unsafe extern "C" fn dash_spv_ffi_client_get_stats( } } +/// Get the current chain tip hash (32 bytes) if available. +/// +/// # Safety +/// - `client` must be a valid, non-null pointer. +/// - `out_hash` must be a valid pointer to a 32-byte buffer. +#[no_mangle] +pub unsafe extern "C" fn dash_spv_ffi_client_get_tip_hash( + client: *mut FFIDashSpvClient, + out_hash: *mut u8, +) -> i32 { + null_check!(client); + if out_hash.is_null() { + set_last_error("Null out_hash pointer"); + return FFIErrorCode::NullPointer as i32; + } + + let client = &(*client); + let inner = client.inner.clone(); + + let result = client.runtime.block_on(async { + let spv_client = { + let mut guard = inner.lock().unwrap(); + match guard.take() { + Some(c) => c, + None => { + return Err(dash_spv::SpvError::Config("Client not initialized".to_string())) + } + } + }; + let tip = spv_client.tip_hash().await; + let mut guard = inner.lock().unwrap(); + *guard = Some(spv_client); + Ok(tip) + }); + + match result { + Ok(Some(hash)) => { + let bytes = hash.to_byte_array(); + // SAFETY: out_hash points to a buffer with at least 32 bytes + std::ptr::copy_nonoverlapping(bytes.as_ptr(), out_hash, 32); + FFIErrorCode::Success as i32 + } + Ok(None) => { + set_last_error("No tip hash available"); + FFIErrorCode::StorageError as i32 + } + Err(e) => { + set_last_error(&e.to_string()); + FFIErrorCode::from(e) as i32 + } + } +} + +/// Get the current chain tip height (absolute). +/// +/// # Safety +/// - `client` must be a valid, non-null pointer. +/// - `out_height` must be a valid, non-null pointer. +#[no_mangle] +pub unsafe extern "C" fn dash_spv_ffi_client_get_tip_height( + client: *mut FFIDashSpvClient, + out_height: *mut u32, +) -> i32 { + null_check!(client); + if out_height.is_null() { + set_last_error("Null out_height pointer"); + return FFIErrorCode::NullPointer as i32; + } + + let client = &(*client); + let inner = client.inner.clone(); + + let result = client.runtime.block_on(async { + let spv_client = { + let mut guard = inner.lock().unwrap(); + match guard.take() { + Some(c) => c, + None => { + return Err(dash_spv::SpvError::Config("Client not initialized".to_string())) + } + } + }; + let height = spv_client.tip_height().await; + let mut guard = inner.lock().unwrap(); + *guard = Some(spv_client); + Ok(height) + }); + + match result { + Ok(height) => { + *out_height = height; + FFIErrorCode::Success as i32 + } + Err(e) => { + set_last_error(&e.to_string()); + FFIErrorCode::from(e) as i32 + } + } +} + +/// Clear all persisted SPV storage (headers, filters, metadata, sync state). +/// +/// # Safety +/// - `client` must be a valid, non-null pointer. +#[no_mangle] +pub unsafe extern "C" fn dash_spv_ffi_client_clear_storage(client: *mut FFIDashSpvClient) -> i32 { + null_check!(client); + + let client = &(*client); + let inner = client.inner.clone(); + + let result = client.runtime.block_on(async { + let mut spv_client = { + let mut guard = inner.lock().unwrap(); + match guard.take() { + Some(c) => c, + None => { + return Err(dash_spv::SpvError::Config("Client not initialized".to_string())) + } + } + }; + + // Try to stop before clearing to ensure no in-flight writes race the wipe. + if let Err(e) = spv_client.stop().await { + tracing::warn!("Failed to stop client before clearing storage: {}", e); + } + + let res = spv_client.clear_storage().await; + let mut guard = inner.lock().unwrap(); + *guard = Some(spv_client); + res + }); + + match result { + Ok(_) => FFIErrorCode::Success as i32, + Err(e) => { + set_last_error(&e.to_string()); + FFIErrorCode::from(e) as i32 + } + } +} + +/// Clear only the persisted sync-state snapshot. +/// +/// # Safety +/// - `client` must be a valid, non-null pointer. +#[no_mangle] +pub unsafe extern "C" fn dash_spv_ffi_client_clear_sync_state( + client: *mut FFIDashSpvClient, +) -> i32 { + null_check!(client); + + let client = &(*client); + let inner = client.inner.clone(); + + let result = client.runtime.block_on(async { + let mut spv_client = { + let mut guard = inner.lock().unwrap(); + match guard.take() { + Some(c) => c, + None => { + return Err(dash_spv::SpvError::Config("Client not initialized".to_string())) + } + } + }; + + let res = spv_client.clear_sync_state().await; + let mut guard = inner.lock().unwrap(); + *guard = Some(spv_client); + res + }); + + match result { + Ok(_) => FFIErrorCode::Success as i32, + Err(e) => { + set_last_error(&e.to_string()); + FFIErrorCode::from(e) as i32 + } + } +} + /// Check if compact filter sync is currently available. /// /// # Safety diff --git a/dash-spv/src/client/mod.rs b/dash-spv/src/client/mod.rs index 2372ae7e8..9e6e479e5 100644 --- a/dash-spv/src/client/mod.rs +++ b/dash-spv/src/client/mod.rs @@ -94,6 +94,30 @@ impl< S: StorageManager + Send + Sync + 'static, > DashSpvClient { + /// Returns the current chain tip hash if available. + pub async fn tip_hash(&self) -> Option { + let state = self.state.read().await; + state.tip_hash() + } + + /// Returns the current chain tip height (absolute), accounting for checkpoint base. + pub async fn tip_height(&self) -> u32 { + let state = self.state.read().await; + state.tip_height() + } + + /// Clear all persisted storage (headers, filters, state, sync state). + pub async fn clear_storage(&mut self) -> Result<()> { + let mut storage = self.storage.lock().await; + storage.clear().await.map_err(SpvError::Storage) + } + + /// Clear only the persisted sync state snapshot (keep headers/filters). + pub async fn clear_sync_state(&mut self) -> Result<()> { + let mut storage = self.storage.lock().await; + storage.clear_sync_state().await.map_err(SpvError::Storage) + } + /// Take the progress receiver for external consumption. pub fn take_progress_receiver( &mut self, 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 bf2c0073e..5ae81d3c3 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 @@ -149,6 +149,11 @@ typedef void (*WalletTransactionCallback)(const char *wallet_id, bool is_ours, void *user_data); +typedef void (*FilterHeadersProgressCallback)(uint32_t filter_height, + uint32_t header_height, + double percentage, + void *user_data); + typedef struct FFIEventCallbacks { BlockCallback on_block; TransactionCallback on_transaction; @@ -158,6 +163,7 @@ typedef struct FFIEventCallbacks { MempoolRemovedCallback on_mempool_transaction_removed; CompactFilterMatchedCallback on_compact_filter_matched; WalletTransactionCallback on_wallet_transaction; + FilterHeadersProgressCallback on_filter_headers_progress; void *user_data; } FFIEventCallbacks; @@ -415,6 +421,40 @@ int32_t dash_spv_ffi_client_sync_to_tip_with_progress(struct FFIDashSpvClient *c */ struct FFISpvStats *dash_spv_ffi_client_get_stats(struct FFIDashSpvClient *client) ; +/** + * Get the current chain tip hash (32 bytes) if available. + * + * # Safety + * - `client` must be a valid, non-null pointer. + * - `out_hash` must be a valid pointer to a 32-byte buffer. + */ + int32_t dash_spv_ffi_client_get_tip_hash(struct FFIDashSpvClient *client, uint8_t *out_hash) ; + +/** + * Get the current chain tip height (absolute). + * + * # Safety + * - `client` must be a valid, non-null pointer. + * - `out_height` must be a valid, non-null pointer. + */ + int32_t dash_spv_ffi_client_get_tip_height(struct FFIDashSpvClient *client, uint32_t *out_height) ; + +/** + * Clear all persisted SPV storage (headers, filters, metadata, sync state). + * + * # Safety + * - `client` must be a valid, non-null pointer. + */ + int32_t dash_spv_ffi_client_clear_storage(struct FFIDashSpvClient *client) ; + +/** + * Clear only the persisted sync-state snapshot. + * + * # Safety + * - `client` must be a valid, non-null pointer. + */ + int32_t dash_spv_ffi_client_clear_sync_state(struct FFIDashSpvClient *client) ; + /** * Check if compact filter sync is currently available. *