Skip to content

Commit 15a84d4

Browse files
committed
feat: report/propagate client start/run failures
We currently just silently ignore any failure inside the sync coordinator or `monitor_network` thread. This PR propagates them and also adds a FFI callback structure which can be set to receive notifications about errors inside the SPV client.
1 parent 30c9e2b commit 15a84d4

File tree

7 files changed

+318
-30
lines changed

7 files changed

+318
-30
lines changed

dash-spv-ffi/FFI_API.md

Lines changed: 36 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ This document provides a comprehensive reference for all FFI (Foreign Function I
44

55
**Auto-generated**: This documentation is automatically generated from the source code. Do not edit manually.
66

7-
**Total Functions**: 48
7+
**Total Functions**: 50
88

99
## Table of Contents
1010

@@ -86,12 +86,14 @@ Functions: 2
8686

8787
### Event Callbacks
8888

89-
Functions: 4
89+
Functions: 6
9090

9191
| Function | Description | Module |
9292
|----------|-------------|--------|
93+
| `dash_spv_ffi_client_clear_client_error_callback` | Clear the client error callback | client |
9394
| `dash_spv_ffi_client_clear_network_event_callbacks` | Clear network event callbacks | client |
9495
| `dash_spv_ffi_client_clear_progress_callback` | Clear progress callback | client |
96+
| `dash_spv_ffi_client_set_client_error_callback` | Set a callback for fatal client errors (start failure, sync thread crash) | client |
9597
| `dash_spv_ffi_client_set_network_event_callbacks` | Set network event callbacks for push-based event notifications | client |
9698
| `dash_spv_ffi_client_set_progress_callback` | Set progress callback for sync progress updates | client |
9799

@@ -599,6 +601,22 @@ This function is unsafe because: - The caller must ensure all pointers are valid
599601

600602
### Event Callbacks - Detailed
601603

604+
#### `dash_spv_ffi_client_clear_client_error_callback`
605+
606+
```c
607+
dash_spv_ffi_client_clear_client_error_callback(client: *mut FFIDashSpvClient,) -> i32
608+
```
609+
610+
**Description:**
611+
Clear the client error callback. # Safety - `client` must be a valid, non-null pointer to an `FFIDashSpvClient`.
612+
613+
**Safety:**
614+
- `client` must be a valid, non-null pointer to an `FFIDashSpvClient`.
615+
616+
**Module:** `client`
617+
618+
---
619+
602620
#### `dash_spv_ffi_client_clear_network_event_callbacks`
603621
604622
```c
@@ -631,6 +649,22 @@ Clear progress callback. # Safety - `client` must be a valid, non-null pointer
631649
632650
---
633651
652+
#### `dash_spv_ffi_client_set_client_error_callback`
653+
654+
```c
655+
dash_spv_ffi_client_set_client_error_callback(client: *mut FFIDashSpvClient, callback: FFIClientErrorCallback,) -> i32
656+
```
657+
658+
**Description:**
659+
Set a callback for fatal client errors (start failure, sync thread crash). # Safety - `client` must be a valid, non-null pointer to an `FFIDashSpvClient`. - The `callback` struct and its `user_data` must remain valid until the callback is cleared. - The callback must be thread-safe as it may be called from a background thread.
660+
661+
**Safety:**
662+
- `client` must be a valid, non-null pointer to an `FFIDashSpvClient`. - The `callback` struct and its `user_data` must remain valid until the callback is cleared. - The callback must be thread-safe as it may be called from a background thread.
663+
664+
**Module:** `client`
665+
666+
---
667+
634668
#### `dash_spv_ffi_client_set_network_event_callbacks`
635669

636670
```c

dash-spv-ffi/include/dash_spv_ffi.h

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -434,6 +434,23 @@ typedef struct FFIProgressCallback {
434434
void *user_data;
435435
} FFIProgressCallback;
436436

437+
/**
438+
* Callback for fatal client errors (e.g. start failure, monitor thread crash).
439+
*
440+
* The `error` string pointer is borrowed and only valid for the duration
441+
* of the callback. Callers must copy the string if they need to retain it
442+
* after the callback returns.
443+
*/
444+
typedef void (*OnClientErrorCallback)(const char *error, void *user_data);
445+
446+
/**
447+
* Client error callback configuration.
448+
*/
449+
typedef struct FFIClientErrorCallback {
450+
OnClientErrorCallback on_error;
451+
void *user_data;
452+
} FFIClientErrorCallback;
453+
437454
/**
438455
* FFIResult type for error handling
439456
*/
@@ -680,6 +697,27 @@ int32_t dash_spv_ffi_client_set_progress_callback(struct FFIDashSpvClient *clien
680697
*/
681698
int32_t dash_spv_ffi_client_clear_progress_callback(struct FFIDashSpvClient *client) ;
682699

700+
/**
701+
* Set a callback for fatal client errors (start failure, sync thread crash).
702+
*
703+
* # Safety
704+
* - `client` must be a valid, non-null pointer to an `FFIDashSpvClient`.
705+
* - The `callback` struct and its `user_data` must remain valid until the callback is cleared.
706+
* - The callback must be thread-safe as it may be called from a background thread.
707+
*/
708+
709+
int32_t dash_spv_ffi_client_set_client_error_callback(struct FFIDashSpvClient *client,
710+
struct FFIClientErrorCallback callback)
711+
;
712+
713+
/**
714+
* Clear the client error callback.
715+
*
716+
* # Safety
717+
* - `client` must be a valid, non-null pointer to an `FFIDashSpvClient`.
718+
*/
719+
int32_t dash_spv_ffi_client_clear_client_error_callback(struct FFIDashSpvClient *client) ;
720+
683721
struct FFIClientConfig *dash_spv_ffi_config_new(FFINetwork network) ;
684722

685723
struct FFIClientConfig *dash_spv_ffi_config_mainnet(void) ;

dash-spv-ffi/src/callbacks.rs

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -573,6 +573,47 @@ impl Default for FFIWalletEventCallbacks {
573573
}
574574
}
575575

576+
// ============================================================================
577+
// FFIClientErrorCallback - Fatal client-level errors
578+
// ============================================================================
579+
580+
/// Callback for fatal client errors (e.g. start failure, monitor thread crash).
581+
///
582+
/// The `error` string pointer is borrowed and only valid for the duration
583+
/// of the callback. Callers must copy the string if they need to retain it
584+
/// after the callback returns.
585+
pub type OnClientErrorCallback =
586+
Option<extern "C" fn(error: *const c_char, user_data: *mut c_void)>;
587+
588+
/// Client error callback configuration.
589+
#[repr(C)]
590+
pub struct FFIClientErrorCallback {
591+
pub on_error: OnClientErrorCallback,
592+
pub user_data: *mut c_void,
593+
}
594+
595+
unsafe impl Send for FFIClientErrorCallback {}
596+
unsafe impl Sync for FFIClientErrorCallback {}
597+
598+
impl Default for FFIClientErrorCallback {
599+
fn default() -> Self {
600+
Self {
601+
on_error: None,
602+
user_data: std::ptr::null_mut(),
603+
}
604+
}
605+
}
606+
607+
impl FFIClientErrorCallback {
608+
/// Dispatch a client error to the callback.
609+
pub fn dispatch(&self, error: &str) {
610+
if let Some(cb) = self.on_error {
611+
let c_error = CString::new(error).unwrap_or_default();
612+
cb(c_error.as_ptr(), self.user_data);
613+
}
614+
}
615+
}
616+
576617
impl FFIWalletEventCallbacks {
577618
/// Dispatch a WalletEvent to the appropriate callback.
578619
pub fn dispatch(&self, event: &key_wallet_manager::WalletEvent) {

dash-spv-ffi/src/client.rs

Lines changed: 56 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
use crate::{
2-
null_check, set_last_error, FFIClientConfig, FFIErrorCode, FFINetworkEventCallbacks,
3-
FFIProgressCallback, FFISyncEventCallbacks, FFISyncProgress, FFIWalletEventCallbacks,
4-
FFIWalletManager,
2+
null_check, set_last_error, FFIClientConfig, FFIClientErrorCallback, FFIErrorCode,
3+
FFINetworkEventCallbacks, FFIProgressCallback, FFISyncEventCallbacks, FFISyncProgress,
4+
FFIWalletEventCallbacks, FFIWalletManager,
55
};
66
// Import wallet types from key-wallet-ffi
77
use key_wallet_ffi::FFIWalletManager as KeyWalletFFIWalletManager;
@@ -120,6 +120,7 @@ pub struct FFIDashSpvClient {
120120
network_event_callbacks: Arc<Mutex<Option<FFINetworkEventCallbacks>>>,
121121
wallet_event_callbacks: Arc<Mutex<Option<FFIWalletEventCallbacks>>>,
122122
progress_callback: Arc<Mutex<Option<FFIProgressCallback>>>,
123+
client_error_callback: Arc<Mutex<Option<FFIClientErrorCallback>>>,
123124
}
124125

125126
/// Create a new SPV client and return an opaque pointer.
@@ -179,6 +180,7 @@ pub unsafe extern "C" fn dash_spv_ffi_client_new(
179180
network_event_callbacks: Arc::new(Mutex::new(None)),
180181
wallet_event_callbacks: Arc::new(Mutex::new(None)),
181182
progress_callback: Arc::new(Mutex::new(None)),
183+
client_error_callback: Arc::new(Mutex::new(None)),
182184
};
183185
Box::into_raw(Box::new(ffi_client))
184186
}
@@ -313,18 +315,7 @@ pub unsafe extern "C" fn dash_spv_ffi_client_run(client: *mut FFIDashSpvClient)
313315

314316
let client = &(*client);
315317

316-
tracing::info!("dash_spv_ffi_client_run: starting client");
317-
318-
// Start the client first
319-
let start_result = client.runtime.block_on(async { client.inner.start().await });
320-
321-
if let Err(e) = start_result {
322-
tracing::error!("dash_spv_ffi_client_run: start failed: {}", e);
323-
set_last_error(&e.to_string());
324-
return FFIErrorCode::from(e) as i32;
325-
}
326-
327-
tracing::info!("dash_spv_ffi_client_run: client started, setting up event monitoring");
318+
tracing::info!("dash_spv_ffi_client_run: setting up event monitoring");
328319

329320
let shutdown_token = client.shutdown_token.clone();
330321

@@ -386,13 +377,27 @@ pub unsafe extern "C" fn dash_spv_ffi_client_run(client: *mut FFIDashSpvClient)
386377
));
387378
}
388379

389-
// Spawn the sync monitoring task
380+
let error_callback = client.client_error_callback.clone();
390381
let spv_client = client.inner.clone();
391382
tasks.push(client.runtime.spawn(async move {
392383
tracing::debug!("Sync task: starting monitor_network");
393384

385+
if let Err(e) = spv_client.start().await {
386+
tracing::error!("Sync thread: client start error: {}", e);
387+
let guard = error_callback.lock().unwrap();
388+
if let Some(cb) = guard.as_ref() {
389+
cb.dispatch(&e.to_string());
390+
}
391+
return;
392+
}
393+
394394
if let Err(e) = spv_client.monitor_network(shutdown_token).await {
395395
tracing::error!("Sync task: sync error: {}", e);
396+
let guard = error_callback.lock().unwrap();
397+
if let Some(cb) = guard.as_ref() {
398+
cb.dispatch(&e.to_string());
399+
}
400+
drop(guard);
396401
}
397402

398403
tracing::debug!("Sync task: exiting");
@@ -714,3 +719,38 @@ pub unsafe extern "C" fn dash_spv_ffi_client_clear_progress_callback(
714719

715720
FFIErrorCode::Success as i32
716721
}
722+
723+
/// Set a callback for fatal client errors (start failure, sync thread crash).
724+
///
725+
/// # Safety
726+
/// - `client` must be a valid, non-null pointer to an `FFIDashSpvClient`.
727+
/// - The `callback` struct and its `user_data` must remain valid until the callback is cleared.
728+
/// - The callback must be thread-safe as it may be called from a background thread.
729+
#[no_mangle]
730+
pub unsafe extern "C" fn dash_spv_ffi_client_set_client_error_callback(
731+
client: *mut FFIDashSpvClient,
732+
callback: FFIClientErrorCallback,
733+
) -> i32 {
734+
null_check!(client);
735+
736+
let client = &(*client);
737+
*client.client_error_callback.lock().unwrap() = Some(callback);
738+
739+
FFIErrorCode::Success as i32
740+
}
741+
742+
/// Clear the client error callback.
743+
///
744+
/// # Safety
745+
/// - `client` must be a valid, non-null pointer to an `FFIDashSpvClient`.
746+
#[no_mangle]
747+
pub unsafe extern "C" fn dash_spv_ffi_client_clear_client_error_callback(
748+
client: *mut FFIDashSpvClient,
749+
) -> i32 {
750+
null_check!(client);
751+
752+
let client = &(*client);
753+
*client.client_error_callback.lock().unwrap() = None;
754+
755+
FFIErrorCode::Success as i32
756+
}

0 commit comments

Comments
 (0)