Skip to content

Commit 19a9542

Browse files
llbartekllclaude
andcommitted
feat: add script_to_address and transaction_get_output_script FFI helpers
Adds two general-purpose FFI functions so downstream Swift consumers can decode output addresses on demand without requiring redundant fields in OutputDetail / FFIOutputDetail: - script_to_address(script_bytes, script_len, network) -> char* Wraps Address::from_script(). Returns a heap-allocated address string (freed via existing address_free) or null for non-standard scripts (OP_RETURN, bare multisig, etc.). - transaction_get_output_script(tx_bytes, tx_len, output_index, script_out, script_len_out, value_out) -> bool Deserializes raw tx bytes and extracts the script_pubkey + value at the given output index. Caller frees script bytes via transaction_output_script_free. This approach (per review feedback) keeps OutputDetail and FFIOutputDetail as { index, role } with no type divergence, while giving Swift the tools to resolve addresses from tx_data + index. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 88e8a9a commit 19a9542

3 files changed

Lines changed: 154 additions & 3 deletions

File tree

key-wallet-ffi/FFI_API.md

Lines changed: 54 additions & 3 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**: 260
7+
**Total Functions**: 263
88

99
## Table of Contents
1010

@@ -256,7 +256,7 @@ Functions: 109
256256

257257
### Address Management
258258

259-
Functions: 10
259+
Functions: 11
260260

261261
| Function | Description | Module |
262262
|----------|-------------|--------|
@@ -270,10 +270,11 @@ Functions: 10
270270
| `address_pool_get_addresses_in_range` | Get a range of addresses from the pool Returns an array of FFIAddressInfo... | address_pool |
271271
| `address_to_pubkey_hash` | Extract public key hash from P2PKH address # Safety - `address` must be a... | transaction |
272272
| `address_validate` | Validate an address # Safety - `address` must be a valid null-terminated C... | address |
273+
| `script_to_address` | Convert a script_pubkey to a human-readable address string | address |
273274

274275
### Transaction Management
275276

276-
Functions: 14
277+
Functions: 16
277278

278279
| Function | Description | Module |
279280
|----------|-------------|--------|
@@ -285,8 +286,10 @@ Functions: 14
285286
| `transaction_create` | Create a new empty transaction # Returns - Pointer to FFITransaction on... | transaction |
286287
| `transaction_deserialize` | Deserialize a transaction # Safety - `data` must be a valid pointer to... | transaction |
287288
| `transaction_destroy` | Destroy a transaction # Safety - `tx` must be a valid pointer to an... | transaction |
289+
| `transaction_get_output` | Extract output script_pubkey bytes and value at a given index from raw... | transaction |
288290
| `transaction_get_txid` | Get the transaction ID # Safety - `tx` must be a valid pointer to an... | transaction |
289291
| `transaction_get_txid_from_bytes` | Get transaction ID from raw transaction bytes # Safety - `tx_bytes` must be... | transaction |
292+
| `transaction_output_free` | Free script bytes returned by `transaction_get_output` | transaction |
290293
| `transaction_serialize` | Serialize a transaction # Safety - `tx` must be a valid pointer to an... | transaction |
291294
| `transaction_sighash` | Calculate signature hash for an input # Safety - `tx` must be a valid... | transaction |
292295
| `transaction_sign_input` | Sign a transaction input # Safety - `tx` must be a valid pointer to an... | transaction |
@@ -3567,6 +3570,22 @@ Validate an address # Safety - `address` must be a valid null-terminated C str
35673570

35683571
---
35693572

3573+
#### `script_to_address`
3574+
3575+
```c
3576+
script_to_address(script_bytes: *const u8, script_len: usize, network: FFINetwork,) -> *mut c_char
3577+
```
3578+
3579+
**Description:**
3580+
Convert a script_pubkey to a human-readable address string. Returns a heap-allocated C string that the caller must free with `address_free`, or null if the script cannot be decoded (e.g. OP_RETURN, bare multisig). # Safety - `script_bytes` must be a valid pointer to `script_len` bytes - `script_len` must be the exact length of the script
3581+
3582+
**Safety:**
3583+
- `script_bytes` must be a valid pointer to `script_len` bytes - `script_len` must be the exact length of the script
3584+
3585+
**Module:** `address`
3586+
3587+
---
3588+
35703589
### Transaction Management - Detailed
35713590

35723591
#### `transaction_add_input`
@@ -3694,6 +3713,22 @@ Destroy a transaction # Safety - `tx` must be a valid pointer to an FFITransact
36943713

36953714
---
36963715

3716+
#### `transaction_get_output`
3717+
3718+
```c
3719+
transaction_get_output(tx_bytes: *const u8, tx_len: usize, output_index: u32, script_out: *mut *mut u8, script_len_out: *mut usize, value_out: *mut u64,) -> bool
3720+
```
3721+
3722+
**Description:**
3723+
Extract output script_pubkey bytes and value at a given index from raw consensus-serialized transaction bytes. Returns false if tx parsing fails or index is out of bounds. On success, `script_out` receives a heap-allocated byte array that the caller must free with `transaction_output_free`, `script_len_out` receives its length, and `value_out` receives the output value in satoshis. # Safety - `tx_bytes` must be a valid pointer to `tx_len` bytes - `script_out`, `script_len_out`, `value_out` must be valid non-null pointers
3724+
3725+
**Safety:**
3726+
- `tx_bytes` must be a valid pointer to `tx_len` bytes - `script_out`, `script_len_out`, `value_out` must be valid non-null pointers
3727+
3728+
**Module:** `transaction`
3729+
3730+
---
3731+
36973732
#### `transaction_get_txid`
36983733

36993734
```c
@@ -3726,6 +3761,22 @@ Get transaction ID from raw transaction bytes # Safety - `tx_bytes` must be a v
37263761

37273762
---
37283763

3764+
#### `transaction_output_free`
3765+
3766+
```c
3767+
transaction_output_free(script: *mut u8, len: usize) -> ()
3768+
```
3769+
3770+
**Description:**
3771+
Free script bytes returned by `transaction_get_output`. # Safety - `script` must be a pointer returned by `transaction_get_output` - `len` must be the length returned alongside it - Must only be called once per allocation
3772+
3773+
**Safety:**
3774+
- `script` must be a pointer returned by `transaction_get_output` - `len` must be the length returned alongside it - Must only be called once per allocation
3775+
3776+
**Module:** `transaction`
3777+
3778+
---
3779+
37293780
#### `transaction_serialize`
37303781

37313782
```c

key-wallet-ffi/src/address.rs

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,38 @@ pub unsafe extern "C" fn address_validate(
113113
}
114114
}
115115

116+
/// Convert a script_pubkey to a human-readable address string.
117+
///
118+
/// Returns a heap-allocated C string that the caller must free with `address_free`,
119+
/// or null if the script cannot be decoded (e.g. OP_RETURN, bare multisig).
120+
///
121+
/// # Safety
122+
///
123+
/// - `script_bytes` must be a valid pointer to `script_len` bytes
124+
/// - `script_len` must be the exact length of the script
125+
#[no_mangle]
126+
pub unsafe extern "C" fn script_to_address(
127+
script_bytes: *const u8,
128+
script_len: usize,
129+
network: FFINetwork,
130+
) -> *mut c_char {
131+
if script_bytes.is_null() || script_len == 0 {
132+
return std::ptr::null_mut();
133+
}
134+
135+
let script_slice = std::slice::from_raw_parts(script_bytes, script_len);
136+
let script = dashcore::ScriptBuf::from(script_slice.to_vec());
137+
let network_rust: key_wallet::Network = network.into();
138+
139+
match key_wallet::Address::from_script(&script, network_rust) {
140+
Ok(addr) => match CString::new(addr.to_string()) {
141+
Ok(cstr) => cstr.into_raw(),
142+
Err(_) => std::ptr::null_mut(),
143+
},
144+
Err(_) => std::ptr::null_mut(),
145+
}
146+
}
147+
116148
/// Get address type
117149
///
118150
/// Returns:

key-wallet-ffi/src/transaction.rs

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -497,6 +497,74 @@ pub unsafe extern "C" fn transaction_bytes_free(tx_bytes: *mut u8) {
497497
}
498498
}
499499

500+
// MARK: - Transaction Output Introspection
501+
502+
/// Extract output script_pubkey bytes and value at a given index from raw
503+
/// consensus-serialized transaction bytes.
504+
///
505+
/// Returns false if tx parsing fails or index is out of bounds.
506+
/// On success, `script_out` receives a heap-allocated byte array that the
507+
/// caller must free with `transaction_output_free`, `script_len_out`
508+
/// receives its length, and `value_out` receives the output value in satoshis.
509+
///
510+
/// # Safety
511+
///
512+
/// - `tx_bytes` must be a valid pointer to `tx_len` bytes
513+
/// - `script_out`, `script_len_out`, `value_out` must be valid non-null pointers
514+
#[no_mangle]
515+
pub unsafe extern "C" fn transaction_get_output(
516+
tx_bytes: *const u8,
517+
tx_len: usize,
518+
output_index: u32,
519+
script_out: *mut *mut u8,
520+
script_len_out: *mut usize,
521+
value_out: *mut u64,
522+
) -> bool {
523+
if tx_bytes.is_null() || script_out.is_null() || script_len_out.is_null() || value_out.is_null()
524+
{
525+
return false;
526+
}
527+
528+
// Initialize out-params to safe defaults so callers never read stale
529+
// values on early-return (false) paths.
530+
*script_out = std::ptr::null_mut();
531+
*script_len_out = 0;
532+
*value_out = 0;
533+
534+
let tx_slice = slice::from_raw_parts(tx_bytes, tx_len);
535+
let tx: Transaction = match consensus::deserialize(tx_slice) {
536+
Ok(tx) => tx,
537+
Err(_) => return false,
538+
};
539+
540+
let output = match tx.output.get(output_index as usize) {
541+
Some(o) => o,
542+
None => return false,
543+
};
544+
545+
let script_bytes = output.script_pubkey.as_bytes();
546+
let mut boxed = script_bytes.to_vec().into_boxed_slice();
547+
*script_len_out = boxed.len();
548+
*script_out = boxed.as_mut_ptr();
549+
std::mem::forget(boxed);
550+
*value_out = output.value;
551+
true
552+
}
553+
554+
/// Free script bytes returned by `transaction_get_output`.
555+
///
556+
/// # Safety
557+
///
558+
/// - `script` must be a pointer returned by `transaction_get_output`
559+
/// - `len` must be the length returned alongside it
560+
/// - Must only be called once per allocation
561+
#[no_mangle]
562+
pub unsafe extern "C" fn transaction_output_free(script: *mut u8, len: usize) {
563+
if !script.is_null() && len > 0 {
564+
let _ = Vec::from_raw_parts(script, len, len);
565+
}
566+
}
567+
500568
// MARK: - Transaction Creation
501569

502570
/// Create a new empty transaction

0 commit comments

Comments
 (0)