Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
91 changes: 88 additions & 3 deletions dash-spv-ffi/FFI_API.md
Original file line number Diff line number Diff line change
Expand Up @@ -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**: 39
**Total Functions**: 44

## Table of Contents

Expand All @@ -31,7 +31,7 @@ Functions: 3

### Configuration

Functions: 15
Functions: 19

| Function | Description | Module |
|----------|-------------|--------|
Expand All @@ -41,6 +41,10 @@ Functions: 15
| `dash_spv_ffi_config_get_network` | Gets the network type from the configuration # Safety - `config` must be a... | config |
| `dash_spv_ffi_config_mainnet` | No description | config |
| `dash_spv_ffi_config_new` | No description | config |
| `dash_spv_ffi_config_set_broadcast_acceptance_threshold` | Sets how many distinct non-recipient peers must announce a broadcast txid... | config |
| `dash_spv_ffi_config_set_broadcast_acceptance_timeout_secs` | Sets the timeout (in seconds) after which a pending broadcast with no... | config |
| `dash_spv_ffi_config_set_broadcast_holdout_count` | Withholds broadcast transactions from a fixed number of peers (clamped so... | config |
| `dash_spv_ffi_config_set_broadcast_holdout_half` | Withholds broadcast transactions from half of the connected peers (the... | config |
| `dash_spv_ffi_config_set_data_dir` | Sets the data directory for storing blockchain data # Safety - `config`... | config |
| `dash_spv_ffi_config_set_fetch_mempool_transactions` | Sets whether to fetch full mempool transaction data # Safety - `config`... | config |
| `dash_spv_ffi_config_set_masternode_sync_enabled` | Enables or disables masternode synchronization # Safety - `config` must be... | config |
Expand All @@ -63,11 +67,12 @@ Functions: 3

### Transaction Management

Functions: 1
Functions: 2

| Function | Description | Module |
|----------|-------------|--------|
| `dash_spv_ffi_client_broadcast_transaction` | Broadcasts a transaction to the Dash network via connected peers | client |
| `dash_spv_ffi_client_broadcast_transaction_and_wait` | Broadcasts a transaction and waits for its network-level outcome | client |

### Mempool Operations

Expand Down Expand Up @@ -252,6 +257,70 @@ dash_spv_ffi_config_new(network: FFINetwork) -> *mut FFIClientConfig

---

#### `dash_spv_ffi_config_set_broadcast_acceptance_threshold`

```c
dash_spv_ffi_config_set_broadcast_acceptance_threshold(config: *mut FFIClientConfig, threshold: u32,) -> i32
```

**Description:**
Sets how many distinct non-recipient peers must announce a broadcast txid back before it is reported as accepted. Must be > 0. # Safety - `config` must be a valid pointer to an FFIClientConfig created by dash_spv_ffi_config_new/mainnet/testnet - The caller must ensure the config pointer remains valid for the duration of this call

**Safety:**
- `config` must be a valid pointer to an FFIClientConfig created by dash_spv_ffi_config_new/mainnet/testnet - The caller must ensure the config pointer remains valid for the duration of this call

**Module:** `config`

---

#### `dash_spv_ffi_config_set_broadcast_acceptance_timeout_secs`

```c
dash_spv_ffi_config_set_broadcast_acceptance_timeout_secs(config: *mut FFIClientConfig, seconds: u32,) -> i32
```

**Description:**
Sets the timeout (in seconds) after which a pending broadcast with no acceptance signal is reported as uncertain. Must be > 0. # Safety - `config` must be a valid pointer to an FFIClientConfig created by dash_spv_ffi_config_new/mainnet/testnet - The caller must ensure the config pointer remains valid for the duration of this call

**Safety:**
- `config` must be a valid pointer to an FFIClientConfig created by dash_spv_ffi_config_new/mainnet/testnet - The caller must ensure the config pointer remains valid for the duration of this call

**Module:** `config`

---

#### `dash_spv_ffi_config_set_broadcast_holdout_count`

```c
dash_spv_ffi_config_set_broadcast_holdout_count(config: *mut FFIClientConfig, count: u32,) -> i32
```

**Description:**
Withholds broadcast transactions from a fixed number of peers (clamped so that at least one peer always receives the transaction). # Safety - `config` must be a valid pointer to an FFIClientConfig created by dash_spv_ffi_config_new/mainnet/testnet - The caller must ensure the config pointer remains valid for the duration of this call

**Safety:**
- `config` must be a valid pointer to an FFIClientConfig created by dash_spv_ffi_config_new/mainnet/testnet - The caller must ensure the config pointer remains valid for the duration of this call

**Module:** `config`

---

#### `dash_spv_ffi_config_set_broadcast_holdout_half`

```c
dash_spv_ffi_config_set_broadcast_holdout_half(config: *mut FFIClientConfig,) -> i32
```

**Description:**
Withholds broadcast transactions from half of the connected peers (the default policy). The withheld peers are the source of the `inv` echo that proves a broadcast propagated through the network. # Safety - `config` must be a valid pointer to an FFIClientConfig created by dash_spv_ffi_config_new/mainnet/testnet - The caller must ensure the config pointer remains valid for the duration of this call

**Safety:**
- `config` must be a valid pointer to an FFIClientConfig created by dash_spv_ffi_config_new/mainnet/testnet - The caller must ensure the config pointer remains valid for the duration of this call

**Module:** `config`

---

#### `dash_spv_ffi_config_set_data_dir`

```c
Expand Down Expand Up @@ -458,6 +527,22 @@ Broadcasts a transaction to the Dash network via connected peers. # Safety - `

---

#### `dash_spv_ffi_client_broadcast_transaction_and_wait`

```c
dash_spv_ffi_client_broadcast_transaction_and_wait(client: *mut FFIDashSpvClient, tx_bytes: *const u8, length: usize, timeout_secs: u32, out_result: *mut FFIBroadcastResult,) -> i32
```

**Description:**
Broadcasts a transaction and waits for its network-level outcome. Blocks until the network accepts the transaction (non-recipient peers announce it back, it is InstantSend-locked, or confirmed) or the timeout elapses (outcome `Uncertain`). `timeout_secs == 0` uses the configured acceptance timeout plus a small grace period. Requires mempool tracking to be enabled in the client config. # Safety - `client` must be a valid, non-null pointer to an initialized FFIDashSpvClient - `tx_bytes` must be a valid, non-null pointer to the transaction data - `length` must be the length of the transaction data in bytes - `out_result` must be a valid, non-null pointer to an FFIBroadcastResult

**Safety:**
- `client` must be a valid, non-null pointer to an initialized FFIDashSpvClient - `tx_bytes` must be a valid, non-null pointer to the transaction data - `length` must be the length of the transaction data in bytes - `out_result` must be a valid, non-null pointer to an FFIBroadcastResult

**Module:** `client`

---

### Mempool Operations - Detailed

#### `dash_spv_ffi_mempool_progress_destroy`
Expand Down
22 changes: 22 additions & 0 deletions dash-spv-ffi/src/bin/ffi_cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,27 @@ extern "C" fn on_sync_complete(header_tip: u32, cycle: u32, _user_data: *mut c_v
println!("[Sync] Sync complete at height: {} (cycle {})", header_tip, cycle);
}

extern "C" fn on_transaction_broadcast_result(
txid: *const [u8; 32],
status: FFIBroadcastStatus,
relayed_by: u32,
_user_data: *mut c_void,
) {
let txid_hex = unsafe { &*txid }.iter().rev().fold(String::new(), |mut acc, b| {
use std::fmt::Write;
let _ = write!(acc, "{:02x}", b);
acc
});
match status {
FFIBroadcastStatus::Accepted => {
println!("[Broadcast] {} accepted ({} peer(s) relayed it back)", txid_hex, relayed_by)
}
FFIBroadcastStatus::Uncertain => {
println!("[Broadcast] {} outcome uncertain (no network signal)", txid_hex)
}
}
}

// ============================================================================
// Network Event Callbacks
// ============================================================================
Expand Down Expand Up @@ -511,6 +532,7 @@ fn main() {
on_instantlock_received: Some(on_instantlock_received),
on_manager_error: Some(on_manager_error),
on_sync_complete: Some(on_sync_complete),
on_transaction_broadcast_result: Some(on_transaction_broadcast_result),
user_data: ptr::null_mut(),
},
network: FFINetworkEventCallbacks {
Expand Down
90 changes: 90 additions & 0 deletions dash-spv-ffi/src/callbacks.rs
Original file line number Diff line number Diff line change
Expand Up @@ -226,6 +226,35 @@ pub type OnManagerErrorCallback =
pub type OnSyncCompleteCallback =
Option<extern "C" fn(header_tip: u32, cycle: u32, user_data: *mut c_void)>;

/// Network-level outcome of a transaction broadcast.
///
/// There is no rejected state: modern Dash Core removed the BIP61 `reject`
/// message, so the p2p network provides no negative signal — a transaction
/// the network refuses surfaces as `Uncertain` (no echo within the timeout).
#[repr(C)]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum FFIBroadcastStatus {
/// Non-recipient peers announced the txid back (or it was
/// InstantSend-locked/confirmed) — the network accepted it.
Accepted = 0,
/// No acceptance signal arrived within the acceptance timeout.
Uncertain = 1,
}

/// Callback for SyncEvent::TransactionBroadcastResult
///
/// The `txid` pointer is borrowed and only valid for the duration of the
/// callback. `relayed_by` is the number of distinct non-recipient peers that
/// announced the txid back (meaningful for `Accepted`).
pub type OnTransactionBroadcastResultCallback = Option<
extern "C" fn(
txid: *const [u8; 32],
status: FFIBroadcastStatus,
relayed_by: u32,
user_data: *mut c_void,
),
>;

/// Sync event callbacks - one callback per SyncEvent variant.
///
/// Set only the callbacks you're interested in; unset callbacks will be ignored.
Expand All @@ -250,6 +279,7 @@ pub struct FFISyncEventCallbacks {
pub on_instantlock_received: OnInstantLockReceivedCallback,
pub on_manager_error: OnManagerErrorCallback,
pub on_sync_complete: OnSyncCompleteCallback,
pub on_transaction_broadcast_result: OnTransactionBroadcastResultCallback,
pub user_data: *mut c_void,
}

Expand Down Expand Up @@ -282,6 +312,7 @@ impl Default for FFISyncEventCallbacks {
on_instantlock_received: None,
on_manager_error: None,
on_sync_complete: None,
on_transaction_broadcast_result: None,
user_data: std::ptr::null_mut(),
}
}
Expand Down Expand Up @@ -438,6 +469,22 @@ impl FFISyncEventCallbacks {
cb(*header_tip, *cycle, self.user_data);
}
}
SyncEvent::TransactionBroadcastResult {
txid,
result,
} => {
if let Some(cb) = self.on_transaction_broadcast_result {
use dash_spv::BroadcastResult;
let txid_bytes = txid.as_byte_array();
let (status, relayed_by) = match result {
BroadcastResult::Accepted {
relayed_by,
} => (FFIBroadcastStatus::Accepted, *relayed_by as u32),
BroadcastResult::Uncertain => (FFIBroadcastStatus::Uncertain, 0),
};
cb(txid_bytes as *const [u8; 32], status, relayed_by, self.user_data);
}
}
}
}
}
Expand Down Expand Up @@ -1370,4 +1417,47 @@ mod tests {
});
assert_eq!(FIRED.load(Ordering::SeqCst), 0);
}

/// `TransactionBroadcastResult` dispatch must marshal the outcome fields
/// (status, relayed_by) for each variant.
#[test]
fn test_transaction_broadcast_result_dispatch() {
use dash_spv::BroadcastResult;

static STATUS: AtomicU32 = AtomicU32::new(u32::MAX);
static RELAYED: AtomicU32 = AtomicU32::new(u32::MAX);

extern "C" fn cb(
txid: *const [u8; 32],
status: FFIBroadcastStatus,
relayed_by: u32,
_user: *mut c_void,
) {
assert!(!txid.is_null());
STATUS.store(status as u32, Ordering::SeqCst);
RELAYED.store(relayed_by, Ordering::SeqCst);
}

let callbacks = FFISyncEventCallbacks {
on_transaction_broadcast_result: Some(cb),
..FFISyncEventCallbacks::default()
};
let txid = Txid::from_byte_array([7u8; 32]);

callbacks.dispatch(&SyncEvent::TransactionBroadcastResult {
txid,
result: BroadcastResult::Accepted {
relayed_by: 3,
},
});
assert_eq!(STATUS.load(Ordering::SeqCst), FFIBroadcastStatus::Accepted as u32);
assert_eq!(RELAYED.load(Ordering::SeqCst), 3);

callbacks.dispatch(&SyncEvent::TransactionBroadcastResult {
txid,
result: BroadcastResult::Uncertain,
});
assert_eq!(STATUS.load(Ordering::SeqCst), FFIBroadcastStatus::Uncertain as u32);
assert_eq!(RELAYED.load(Ordering::SeqCst), 0);
}
}
83 changes: 83 additions & 0 deletions dash-spv-ffi/src/client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -348,6 +348,89 @@ pub unsafe extern "C" fn dash_spv_ffi_client_broadcast_transaction(
}
}

/// Network-level outcome of a broadcast, as returned by
/// `dash_spv_ffi_client_broadcast_transaction_and_wait`.
///
/// Plain value type — nothing to free.
#[repr(C)]
pub struct FFIBroadcastResult {
/// The determined outcome.
pub status: crate::FFIBroadcastStatus,
/// Distinct non-recipient peers that announced the txid back
/// (meaningful for `Accepted`).
pub relayed_by: u32,
}

/// Broadcasts a transaction and waits for its network-level outcome.
///
/// Blocks until the network accepts the transaction (non-recipient peers
/// announce it back, it is InstantSend-locked, or confirmed) or the timeout
/// elapses (outcome `Uncertain`). `timeout_secs == 0` uses the configured
/// acceptance timeout plus a small grace period.
///
/// Requires mempool tracking to be enabled in the client config.
///
/// # Safety
///
/// - `client` must be a valid, non-null pointer to an initialized FFIDashSpvClient
/// - `tx_bytes` must be a valid, non-null pointer to the transaction data
/// - `length` must be the length of the transaction data in bytes
/// - `out_result` must be a valid, non-null pointer to an FFIBroadcastResult
#[no_mangle]
pub unsafe extern "C" fn dash_spv_ffi_client_broadcast_transaction_and_wait(
client: *mut FFIDashSpvClient,
tx_bytes: *const u8,
length: usize,
timeout_secs: u32,
out_result: *mut FFIBroadcastResult,
) -> i32 {
null_check!(client);
null_check!(tx_bytes);
null_check!(out_result);

let tx_bytes = std::slice::from_raw_parts(tx_bytes, length);

let tx = match dashcore::consensus::deserialize::<dashcore::Transaction>(tx_bytes) {
Ok(t) => t,
Err(e) => {
set_last_error(&format!("Invalid transaction: {}", e));
return FFIErrorCode::InvalidArgument as i32;
}
};

let client = &(*client);
let spv_client = client.inner.clone();
let timeout = (timeout_secs > 0).then(|| std::time::Duration::from_secs(timeout_secs as u64));

let result = client
.runtime
.block_on(async { spv_client.broadcast_transaction_and_wait(&tx, timeout).await });

match result {
Ok(outcome) => {
use dash_spv::BroadcastResult;
let ffi = match outcome {
BroadcastResult::Accepted {
relayed_by,
} => FFIBroadcastResult {
status: crate::FFIBroadcastStatus::Accepted,
relayed_by: relayed_by as u32,
},
BroadcastResult::Uncertain => FFIBroadcastResult {
status: crate::FFIBroadcastStatus::Uncertain,
relayed_by: 0,
},
};
std::ptr::write(out_result, ffi);
FFIErrorCode::Success as i32
}
Err(e) => {
set_last_error(&format!("Failed to broadcast transaction: {}", e));
FFIErrorCode::from(e) as i32
}
}
}

/// Destroy the client and free associated resources.
///
/// # Safety
Expand Down
Loading
Loading