From c362f1ddb4ab0b639fe6fbb75aa6def8d5cbc196 Mon Sep 17 00:00:00 2001 From: Bartosz Rozwarski Date: Mon, 13 Apr 2026 08:30:17 +0200 Subject: [PATCH 1/3] feat: add address and value fields to FFIOutputDetail MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit OutputDetail and FFIOutputDetail previously only had { index, role }, dropping the output address and value on the floor. The wallet crate already resolves output addresses at managed_account/mod.rs:480-484 (via Address::from_script) — they just weren't stored in the struct. - Add `address: Option
` and `value: u64` to OutputDetail (key-wallet/src/managed_account/transaction_record.rs) - Populate at construction from resolved_outputs and output.value (key-wallet/src/managed_account/mod.rs) - Add `address: *mut c_char` and `value: u64` to FFIOutputDetail (key-wallet-ffi/src/types.rs) - Map in both key-wallet-ffi and dash-spv-ffi FFI conversions - Update free function to free output address strings - Fix test constructor to include new fields This enables downstream Swift consumers (dashwallet-ios) to display "Received at" and "Sent to" addresses in the transaction detail screen. Co-Authored-By: Claude Opus 4.6 (1M context) --- dash-spv-ffi/src/callbacks.rs | 15 +++++++++++ key-wallet-ffi/src/managed_account.rs | 25 ++++++++++++++----- key-wallet-ffi/src/types.rs | 2 ++ key-wallet/src/managed_account/mod.rs | 2 ++ .../src/managed_account/transaction_record.rs | 6 +++++ 5 files changed, 44 insertions(+), 6 deletions(-) diff --git a/dash-spv-ffi/src/callbacks.rs b/dash-spv-ffi/src/callbacks.rs index b7b300e5d..fc6a54568 100644 --- a/dash-spv-ffi/src/callbacks.rs +++ b/dash-spv-ffi/src/callbacks.rs @@ -731,6 +731,13 @@ impl FFIWalletEventCallbacks { .map(|d| FFIOutputDetail { index: d.index, role: FFIOutputRole::from(d.role), + value: d.value, + address: match &d.address { + Some(addr) => { + CString::new(addr.to_string()).unwrap_or_default().into_raw() + } + None => std::ptr::null_mut(), + }, }) .collect(); @@ -784,6 +791,14 @@ impl FFIWalletEventCallbacks { } } } + // Free the CString addresses from output details + for detail in output_details { + if !detail.address.is_null() { + unsafe { + drop(CString::from_raw(detail.address)); + } + } + } // SAFETY: Free the heap-allocated IS lock bytes produced by // `From` after the callback returns. if !islock_data.is_null() && islock_len > 0 { diff --git a/key-wallet-ffi/src/managed_account.rs b/key-wallet-ffi/src/managed_account.rs index fa91a46dd..77ca85902 100644 --- a/key-wallet-ffi/src/managed_account.rs +++ b/key-wallet-ffi/src/managed_account.rs @@ -791,6 +791,13 @@ pub unsafe extern "C" fn managed_core_account_get_transactions( .map(|d| FFIOutputDetail { index: d.index, role: FFIOutputRole::from(d.role), + value: d.value, + address: match &d.address { + Some(addr) => { + std::ffi::CString::new(addr.to_string()).unwrap_or_default().into_raw() + } + None => std::ptr::null_mut(), + }, }) .collect::>() .into_boxed_slice(); @@ -841,12 +848,16 @@ pub unsafe extern "C" fn managed_core_account_free_transactions( drop(Box::from_raw(slice as *mut [FFIInputDetail])); } - // Free output details + // Free output detail addresses first, then the array if !record.output_details.is_null() && record.output_details_count > 0 { - drop(Box::from_raw(std::ptr::slice_from_raw_parts_mut( - record.output_details, - record.output_details_count, - ))); + let slice = + std::slice::from_raw_parts_mut(record.output_details, record.output_details_count); + for detail in slice.iter() { + if !detail.address.is_null() { + drop(std::ffi::CString::from_raw(detail.address)); + } + } + drop(Box::from_raw(slice as *mut [FFIOutputDetail])); } // Free tx data @@ -2027,7 +2038,7 @@ mod tests { let addr = std::ffi::CString::new("XtestAddress123").unwrap(); let input_slice = vec![FFIInputDetail { index: 0, - value: 100000, + value: 0, address: addr.into_raw(), }] .into_boxed_slice(); @@ -2038,6 +2049,8 @@ mod tests { let output_slice = vec![FFIOutputDetail { index: 0, role: FFIOutputRole::Received, + value: 0, + address: std::ptr::null_mut(), }] .into_boxed_slice(); r0.output_details_count = output_slice.len(); diff --git a/key-wallet-ffi/src/types.rs b/key-wallet-ffi/src/types.rs index 12495596b..c86f51576 100644 --- a/key-wallet-ffi/src/types.rs +++ b/key-wallet-ffi/src/types.rs @@ -930,6 +930,8 @@ pub struct FFIInputDetail { pub struct FFIOutputDetail { pub index: u32, pub role: FFIOutputRole, + pub value: u64, + pub address: *mut std::os::raw::c_char, } #[cfg(test)] diff --git a/key-wallet/src/managed_account/mod.rs b/key-wallet/src/managed_account/mod.rs index 16a1c6539..f988e9189 100644 --- a/key-wallet/src/managed_account/mod.rs +++ b/key-wallet/src/managed_account/mod.rs @@ -504,6 +504,8 @@ impl ManagedCoreAccount { output_details.push(OutputDetail { index: idx as u32, role, + address: resolved_outputs[idx].clone(), + value: output.value, }); } diff --git a/key-wallet/src/managed_account/transaction_record.rs b/key-wallet/src/managed_account/transaction_record.rs index 478c1970e..e75cfcc73 100644 --- a/key-wallet/src/managed_account/transaction_record.rs +++ b/key-wallet/src/managed_account/transaction_record.rs @@ -37,6 +37,12 @@ pub struct OutputDetail { pub index: u32, /// Role of this output from the wallet's perspective pub role: OutputRole, + /// Decoded address (None for non-standard scripts) + #[cfg_attr(feature = "serde", serde(default))] + pub address: Option
, + /// Value in satoshis + #[cfg_attr(feature = "serde", serde(default))] + pub value: u64, } /// Role of a transaction output from the wallet's perspective From 3f526cb7f6986a969dc689a9c110beeb6e74618e Mon Sep 17 00:00:00 2001 From: Bartosz Rozwarski Date: Tue, 14 Apr 2026 20:37:24 +0200 Subject: [PATCH 2/3] fix: remove serde defaults from OutputDetail new fields MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Deserialization of old records without address/value should fail rather than silently defaulting — forces a resync which repopulates the fields correctly. Not used in production yet. Co-Authored-By: Claude Opus 4.6 (1M context) --- key-wallet/src/managed_account/transaction_record.rs | 2 -- 1 file changed, 2 deletions(-) diff --git a/key-wallet/src/managed_account/transaction_record.rs b/key-wallet/src/managed_account/transaction_record.rs index e75cfcc73..e4eca2c36 100644 --- a/key-wallet/src/managed_account/transaction_record.rs +++ b/key-wallet/src/managed_account/transaction_record.rs @@ -38,10 +38,8 @@ pub struct OutputDetail { /// Role of this output from the wallet's perspective pub role: OutputRole, /// Decoded address (None for non-standard scripts) - #[cfg_attr(feature = "serde", serde(default))] pub address: Option
, /// Value in satoshis - #[cfg_attr(feature = "serde", serde(default))] pub value: u64, } From f7d9a0389011770e8a1b44f06a07759c4c648b76 Mon Sep 17 00:00:00 2001 From: Bartosz Rozwarski Date: Tue, 14 Apr 2026 20:36:07 +0200 Subject: [PATCH 3/3] Apply suggestion from @xdustinface Co-authored-by: Kevin Rombach <35775977+xdustinface@users.noreply.github.com> --- key-wallet-ffi/src/types.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/key-wallet-ffi/src/types.rs b/key-wallet-ffi/src/types.rs index c86f51576..71ee3ac08 100644 --- a/key-wallet-ffi/src/types.rs +++ b/key-wallet-ffi/src/types.rs @@ -931,7 +931,7 @@ pub struct FFIOutputDetail { pub index: u32, pub role: FFIOutputRole, pub value: u64, - pub address: *mut std::os::raw::c_char, + pub address: *mut c_char, } #[cfg(test)]