Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
32 changes: 32 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,37 @@ 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,
reject_code: u8,
reject_reason: *const c_char,
_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::Rejected => {
let reason = if reject_reason.is_null() {
String::new()
} else {
unsafe { std::ffi::CStr::from_ptr(reject_reason) }.to_string_lossy().into_owned()
};
println!("[Broadcast] {} rejected (code {}): {}", txid_hex, reject_code, reason)
}
FFIBroadcastStatus::Uncertain => {
println!("[Broadcast] {} outcome uncertain (no network signal)", txid_hex)
}
}
}

// ============================================================================
// Network Event Callbacks
// ============================================================================
Expand Down Expand Up @@ -511,6 +542,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
144 changes: 144 additions & 0 deletions dash-spv-ffi/src/callbacks.rs
Original file line number Diff line number Diff line change
Expand Up @@ -226,6 +226,38 @@ 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.
#[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,
/// A peer rejected the transaction via a p2p `reject` message.
Rejected = 1,
/// No definitive signal arrived within the acceptance timeout.
Uncertain = 2,
}

/// Callback for SyncEvent::TransactionBroadcastResult
///
/// The `txid` and `reject_reason` pointers are 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`). `reject_code` is the raw p2p reject code and `reject_reason`
/// the peer-supplied reason string; both are only meaningful for `Rejected`
/// (`reject_reason` is null otherwise).
pub type OnTransactionBroadcastResultCallback = Option<
extern "C" fn(
txid: *const [u8; 32],
status: FFIBroadcastStatus,
relayed_by: u32,
reject_code: u8,
reject_reason: *const c_char,
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 +282,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 +315,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 +472,49 @@ 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();
match result {
BroadcastResult::Accepted {
relayed_by,
} => cb(
txid_bytes as *const [u8; 32],
FFIBroadcastStatus::Accepted,
*relayed_by as u32,
0,
ptr::null(),
self.user_data,
),
BroadcastResult::Rejected {
code,
reason,
} => {
let c_reason = CString::new(reason.as_str()).unwrap_or_default();
cb(
txid_bytes as *const [u8; 32],
FFIBroadcastStatus::Rejected,
0,
*code as u8,
c_reason.as_ptr(),
self.user_data,
);
}
BroadcastResult::Uncertain => cb(
txid_bytes as *const [u8; 32],
FFIBroadcastStatus::Uncertain,
0,
0,
ptr::null(),
self.user_data,
),
}
}
}
}
}
}
Expand Down Expand Up @@ -1370,4 +1447,71 @@ mod tests {
});
assert_eq!(FIRED.load(Ordering::SeqCst), 0);
}

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

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

extern "C" fn cb(
txid: *const [u8; 32],
status: FFIBroadcastStatus,
relayed_by: u32,
reject_code: u8,
reject_reason: *const c_char,
_user: *mut c_void,
) {
assert!(!txid.is_null());
STATUS.store(status as u32, Ordering::SeqCst);
RELAYED.store(relayed_by, Ordering::SeqCst);
CODE.store(reject_code as u32, Ordering::SeqCst);
let reason_len = if reject_reason.is_null() {
0
} else {
unsafe { std::ffi::CStr::from_ptr(reject_reason) }.to_bytes().len() as u32
};
REASON_LEN.store(reason_len, 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);
assert_eq!(CODE.load(Ordering::SeqCst), 0);
assert_eq!(REASON_LEN.load(Ordering::SeqCst), 0);

callbacks.dispatch(&SyncEvent::TransactionBroadcastResult {
txid,
result: BroadcastResult::Rejected {
code: RejectReason::Fee,
reason: "insufficient fee".to_string(),
},
});
assert_eq!(STATUS.load(Ordering::SeqCst), FFIBroadcastStatus::Rejected as u32);
assert_eq!(CODE.load(Ordering::SeqCst), RejectReason::Fee as u32);
assert_eq!(REASON_LEN.load(Ordering::SeqCst), "insufficient fee".len() as u32);

callbacks.dispatch(&SyncEvent::TransactionBroadcastResult {
txid,
result: BroadcastResult::Uncertain,
});
assert_eq!(STATUS.load(Ordering::SeqCst), FFIBroadcastStatus::Uncertain as u32);
}
}
112 changes: 112 additions & 0 deletions dash-spv-ffi/src/client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -348,6 +348,118 @@ 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`.
#[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,
/// Raw p2p reject code (meaningful for `Rejected`, 0 otherwise).
pub reject_code: u8,
/// Peer-supplied reject reason (empty unless `Rejected`). Must be freed
/// with `dash_spv_ffi_broadcast_result_destroy`.
pub reject_reason: crate::FFIString,
}

/// Free the strings owned by an FFIBroadcastResult.
///
/// # Safety
/// - `result` must be a valid pointer to an FFIBroadcastResult previously
/// filled in by `dash_spv_ffi_client_broadcast_transaction_and_wait`, and
/// must not be used after this call.
#[no_mangle]
pub unsafe extern "C" fn dash_spv_ffi_broadcast_result_destroy(result: *mut FFIBroadcastResult) {
if !result.is_null() {
crate::types::dash_spv_ffi_string_destroy(std::ptr::read(&(*result).reject_reason));
}
}

/// 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), a peer rejects
/// it, 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,
reject_code: 0,
reject_reason: crate::FFIString::new(""),
},
BroadcastResult::Rejected {
code,
reason,
} => FFIBroadcastResult {
status: crate::FFIBroadcastStatus::Rejected,
relayed_by: 0,
reject_code: code as u8,
reject_reason: crate::FFIString::new(&reason),
},
BroadcastResult::Uncertain => FFIBroadcastResult {
status: crate::FFIBroadcastStatus::Uncertain,
relayed_by: 0,
reject_code: 0,
reject_reason: crate::FFIString::new(""),
},
};
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