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
11 changes: 9 additions & 2 deletions dash-spv-ffi/src/bin/ffi_cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -172,12 +172,15 @@ fn read_balance(balance: *const FFIBalance) -> FFIBalance {
unsafe { *balance }
}

#[allow(clippy::too_many_arguments)]
extern "C" fn on_transaction_detected(
wallet_id: *const c_char,
record: *const FFITransactionRecord,
balance: *const FFIBalance,
_account_balances: *const dash_spv_ffi::FFIAccountBalance,
account_balances_count: u32,
_addresses_derived: *const dash_spv_ffi::FFIDerivedAddress,
addresses_derived_count: u32,
_user_data: *mut c_void,
) {
let wallet_short = short_wallet(wallet_id);
Expand All @@ -189,7 +192,7 @@ extern "C" fn on_transaction_detected(
let b = read_balance(balance);
let txid_hex = hex::encode(r.txid);
println!(
"[Wallet] TX detected: wallet={}..., txid={}, account_kind={:?}, account_index={}, amount={} duffs, balance[confirmed={}, unconfirmed={}], changed_accounts={}",
"[Wallet] TX detected: wallet={}..., txid={}, account_kind={:?}, account_index={}, amount={} duffs, balance[confirmed={}, unconfirmed={}], changed_accounts={}, derived={}",
wallet_short,
txid_hex,
r.account_type.kind,
Expand All @@ -198,6 +201,7 @@ extern "C" fn on_transaction_detected(
b.confirmed,
b.unconfirmed,
account_balances_count,
addresses_derived_count,
);
}

Expand Down Expand Up @@ -243,12 +247,14 @@ extern "C" fn on_wallet_block_processed(
balance: *const FFIBalance,
_account_balances: *const dash_spv_ffi::FFIAccountBalance,
account_balances_count: u32,
_addresses_derived: *const dash_spv_ffi::FFIDerivedAddress,
addresses_derived_count: u32,
_user_data: *mut c_void,
) {
let wallet_short = short_wallet(wallet_id);
let b = read_balance(balance);
println!(
"[Wallet] Block processed: wallet={}..., height={}, inserted={}, updated={}, matured={}, balance[confirmed={}, unconfirmed={}, immature={}, locked={}], changed_accounts={}",
"[Wallet] Block processed: wallet={}..., height={}, inserted={}, updated={}, matured={}, balance[confirmed={}, unconfirmed={}, immature={}, locked={}], changed_accounts={}, derived={}",
wallet_short,
height,
inserted_count,
Expand All @@ -259,6 +265,7 @@ extern "C" fn on_wallet_block_processed(
b.immature,
b.locked,
account_balances_count,
addresses_derived_count,
);
}

Expand Down
138 changes: 138 additions & 0 deletions dash-spv-ffi/src/callbacks.rs
Original file line number Diff line number Diff line change
Expand Up @@ -567,6 +567,106 @@ impl FFIAccountBalance {
}
}

// ============================================================================
// FFIDerivedAddress - One address derived during gap-limit maintenance
// ============================================================================

/// Pool the derived address belongs to.
///
/// Mirrors `key_wallet::managed_account::address_pool::AddressPoolType`
/// 1:1 — kept distinct from the existing `FFIAddressPoolType` (which
/// collapses Absent / AbsentHardened into a single `Single` variant) so
/// event consumers can distinguish hardened single-pool variants
/// (Provider operator keys, etc.) from non-hardened ones.
#[repr(C)]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum FFIDerivedAddressPoolType {
External = 0,
Internal = 1,
Absent = 2,
AbsentHardened = 3,
}

impl From<key_wallet::managed_account::address_pool::AddressPoolType>
for FFIDerivedAddressPoolType
{
fn from(t: key_wallet::managed_account::address_pool::AddressPoolType) -> Self {
use key_wallet::managed_account::address_pool::AddressPoolType as P;
match t {
P::External => FFIDerivedAddressPoolType::External,
P::Internal => FFIDerivedAddressPoolType::Internal,
P::Absent => FFIDerivedAddressPoolType::Absent,
P::AbsentHardened => FFIDerivedAddressPoolType::AbsentHardened,
}
}
}

/// One address derived as a side effect of gap-limit maintenance during
/// transaction or block processing.
///
/// Wallet events deliver an array of these so persisters can mirror the
/// on-disk address pool transactionally with the tx/block records that
/// triggered the derivation. Without this, UTXOs landing on freshly
/// derived addresses orphan their parent address row at the persister.
///
/// `account_type` follows the same memory rules as on
/// [`FFIAccountBalance`]: the embedded `identity_user` / `identity_friend`
/// pointers are owned by the `FFIAccountType` and freed when the array is
/// dropped after the callback returns. `address` is a heap-allocated
/// null-terminated UTF-8 string, owned by this struct and freed on drop.
/// Consumers that need to retain the data past the callback must copy
/// every owning field — not just retain pointers.
#[repr(C)]
pub struct FFIDerivedAddress {
/// Owning-account descriptor (discriminant + indices + identity ids).
pub account_type: FFIAccountType,
/// Pool within the account that derived this address.
pub pool_type: FFIDerivedAddressPoolType,
/// Derivation index within the pool. Combined with `account_type`
/// and `pool_type`, this fully determines the derivation path —
/// consumers that need a rendered path can recompute it
/// deterministically.
pub derivation_index: u32,
/// Heap-allocated null-terminated UTF-8 string. Owned by this
/// struct; freed when the struct is dropped.
pub address: *mut c_char,
/// 33-byte compressed ECDSA public key (inline, no allocation).
pub public_key: [u8; 33],
}

impl FFIDerivedAddress {
fn from_slice(addresses: &[key_wallet_manager::DerivedAddress]) -> Vec<Self> {
addresses
.iter()
.map(|d| {
let address_str = d.address.to_string();
let c_address = CString::new(address_str).unwrap_or_else(|_| CString::default());
FFIDerivedAddress {
account_type: FFIAccountType::from(&d.account_type),
pool_type: FFIDerivedAddressPoolType::from(d.pool_type),
derivation_index: d.derivation_index,
address: c_address.into_raw(),
public_key: d.public_key,
}
})
.collect()
}
}

impl Drop for FFIDerivedAddress {
fn drop(&mut self) {
if !self.address.is_null() {
// SAFETY: `address` was constructed via `CString::into_raw` in
// `FFIDerivedAddress::from_slice`, so reclaiming via
// `CString::from_raw` is the matching free.
let _ = unsafe { CString::from_raw(self.address) };
self.address = std::ptr::null_mut();
}
// `account_type` has its own Drop impl that frees the
// identity_user / identity_friend allocations when applicable.
}
}

// ============================================================================
// FFIWalletEventCallbacks - One callback per WalletEvent variant
// ============================================================================
Expand All @@ -584,13 +684,22 @@ impl FFIAccountBalance {
/// entries for a normal transaction); accounts whose balance is unchanged
/// are omitted. The array is null with a zero count when no per-account
/// balance changed.
///
/// `addresses_derived` is an array of size `addresses_derived_count` of
/// addresses derived as a side effect of gap-limit maintenance while
/// processing this transaction, attributed to the same account as
/// `record`. Empty in the common case (null pointer with zero count).
/// Persisters should write these rows transactionally with `record` so
/// UTXOs landing on freshly-derived addresses retain a parent row.
pub type OnTransactionDetectedCallback = Option<
extern "C" fn(
wallet_id: *const c_char,
record: *const FFITransactionRecord,
balance: *const FFIBalance,
account_balances: *const FFIAccountBalance,
account_balances_count: u32,
addresses_derived: *const FFIDerivedAddress,
addresses_derived_count: u32,
user_data: *mut c_void,
),
>;
Expand Down Expand Up @@ -629,6 +738,13 @@ pub type OnTransactionInstantLockedCallback = Option<
/// balance *after* the block was processed. `account_balances` follows the
/// same contract as on [`OnTransactionDetectedCallback`].
///
/// `addresses_derived` is an array of size `addresses_derived_count` of
/// addresses derived as a side effect of gap-limit maintenance across
/// every record in the block, deduplicated by
/// `(account_type, pool_type, derivation_index)`. Empty in the common
/// case (null pointer with zero count). Persisters should write these
/// rows transactionally with the inserted/updated records.
///
/// All array pointers and their contents are borrowed and only valid for the
/// duration of the callback.
pub type OnWalletBlockProcessedCallback = Option<
Expand All @@ -644,6 +760,8 @@ pub type OnWalletBlockProcessedCallback = Option<
balance: *const FFIBalance,
account_balances: *const FFIAccountBalance,
account_balances_count: u32,
addresses_derived: *const FFIDerivedAddress,
addresses_derived_count: u32,
user_data: *mut c_void,
),
>;
Expand Down Expand Up @@ -784,29 +902,39 @@ impl FFIWalletEventCallbacks {
record,
balance,
account_balances,
addresses_derived,
} => {
if let Some(cb) = self.on_transaction_detected {
let wallet_id_hex = hex::encode(wallet_id);
let c_wallet_id = CString::new(wallet_id_hex).unwrap_or_default();
let ffi_record = FFITransactionRecord::from(record.as_ref());
let ffi_balance = FFIBalance::from(*balance);
let ffi_account_balances = FFIAccountBalance::from_map(account_balances);
let ffi_addresses_derived = FFIDerivedAddress::from_slice(addresses_derived);
let account_balances_ptr = if ffi_account_balances.is_empty() {
ptr::null()
} else {
ffi_account_balances.as_ptr()
};
let addresses_derived_ptr = if ffi_addresses_derived.is_empty() {
ptr::null()
} else {
ffi_addresses_derived.as_ptr()
};

cb(
c_wallet_id.as_ptr(),
&ffi_record as *const FFITransactionRecord,
&ffi_balance as *const FFIBalance,
account_balances_ptr,
ffi_account_balances.len() as u32,
addresses_derived_ptr,
ffi_addresses_derived.len() as u32,
self.user_data,
);

drop(ffi_account_balances);
drop(ffi_addresses_derived);
}
}
WalletEvent::TransactionInstantLocked {
Expand Down Expand Up @@ -851,6 +979,7 @@ impl FFIWalletEventCallbacks {
matured,
balance,
account_balances,
addresses_derived,
} => {
if let Some(cb) = self.on_block_processed {
let wallet_id_hex = hex::encode(wallet_id);
Expand All @@ -863,6 +992,7 @@ impl FFIWalletEventCallbacks {
matured.iter().map(FFITransactionRecord::from).collect();
let ffi_balance = FFIBalance::from(*balance);
let ffi_account_balances = FFIAccountBalance::from_map(account_balances);
let ffi_addresses_derived = FFIDerivedAddress::from_slice(addresses_derived);

// Pass a null pointer when an array is empty so C/Swift
// consumers that null-check before reading don't see a
Expand All @@ -887,6 +1017,11 @@ impl FFIWalletEventCallbacks {
} else {
ffi_account_balances.as_ptr()
};
let addresses_derived_ptr = if ffi_addresses_derived.is_empty() {
ptr::null()
} else {
ffi_addresses_derived.as_ptr()
};

cb(
c_wallet_id.as_ptr(),
Expand All @@ -900,13 +1035,16 @@ impl FFIWalletEventCallbacks {
&ffi_balance as *const FFIBalance,
account_balances_ptr,
ffi_account_balances.len() as u32,
addresses_derived_ptr,
ffi_addresses_derived.len() as u32,
self.user_data,
);

drop(ffi_inserted);
drop(ffi_updated);
drop(ffi_matured);
drop(ffi_account_balances);
drop(ffi_addresses_derived);
}
}
WalletEvent::SyncHeightAdvanced {
Expand Down
4 changes: 4 additions & 0 deletions dash-spv-ffi/tests/dashd_sync/callbacks.rs
Original file line number Diff line number Diff line change
Expand Up @@ -421,6 +421,8 @@ extern "C" fn on_transaction_detected(
balance: *const FFIBalance,
account_balances: *const FFIAccountBalance,
account_balances_count: u32,
_addresses_derived: *const dash_spv_ffi::FFIDerivedAddress,
_addresses_derived_count: u32,
user_data: *mut c_void,
) {
let Some(tracker) = (unsafe { tracker_from(user_data) }) else {
Expand Down Expand Up @@ -493,6 +495,8 @@ extern "C" fn on_wallet_block_processed(
balance: *const FFIBalance,
account_balances: *const FFIAccountBalance,
account_balances_count: u32,
_addresses_derived: *const dash_spv_ffi::FFIDerivedAddress,
_addresses_derived_count: u32,
user_data: *mut c_void,
) {
let Some(tracker) = (unsafe { tracker_from(user_data) }) else {
Expand Down
Loading
Loading