Skip to content

feat: add address and value fields to FFIOutputDetail#640

Merged
xdustinface merged 3 commits intov0.42-devfrom
feat/output-detail-address
Apr 14, 2026
Merged

feat: add address and value fields to FFIOutputDetail#640
xdustinface merged 3 commits intov0.42-devfrom
feat/output-detail-address

Conversation

@llbartekll
Copy link
Copy Markdown
Contributor

@llbartekll llbartekll commented Apr 13, 2026

Summary

  • Adds address: Option<Address> and value: u64 to OutputDetail (key-wallet crate)
  • Adds address: *mut c_char and value: u64 to FFIOutputDetail (key-wallet-ffi + dash-spv-ffi)
  • Purely additive — existing consumers reading index and role are unaffected

Motivation

The tx detail screen in dashwallet-ios needs output addresses to display "Received at" / "Sent to" sections. The wallet crate already resolves output addresses via Address::from_script during transaction processing but wasn't storing them in OutputDetail. This change surfaces what's already computed.

Test plan

  • cargo fmt passes
  • cargo test -p key-wallet-ffi passes
  • cargo test -p key-wallet passes
  • Downstream: rebuild xcframework, verify addresses appear in dashwallet-ios tx detail screen

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Convert raw script bytes into human-readable addresses for supported networks.
    • Inspect transaction outputs from raw transaction bytes to return an output’s script and value, plus a matching way to free returned script memory.
    • Wallet event callbacks now include per-output values and derived addresses (network-aware).
  • Documentation

    • API reference updated to document the new address & transaction output APIs and memory ownership rules.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Apr 13, 2026

Warning

Rate limit exceeded

@llbartekll has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 58 minutes and 9 seconds before requesting another review.

Your organization is not enrolled in usage-based pricing. Contact your admin to enable usage-based pricing to continue reviews beyond the rate limit, or try again in 58 minutes and 9 seconds.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 1140f6ae-3aba-46ca-8759-ed1d2a2cd420

📥 Commits

Reviewing files that changed from the base of the PR and between 3d6cd97 and f7d9a03.

📒 Files selected for processing (5)
  • dash-spv-ffi/src/callbacks.rs
  • key-wallet-ffi/src/managed_account.rs
  • key-wallet-ffi/src/types.rs
  • key-wallet/src/managed_account/mod.rs
  • key-wallet/src/managed_account/transaction_record.rs
📝 Walkthrough

Walkthrough

Added FFI functions to convert scripts to addresses and to extract transaction outputs, extended FFI types and account transaction conversions to include output value and address, and updated SPV FFI callbacks and client wiring to provide network context for address derivation; docs updated and a CLI struct init adjusted.

Changes

Cohort / File(s) Summary
Address FFI
key-wallet-ffi/src/address.rs
Added #[no_mangle] pub unsafe extern "C" fn script_to_address(script_bytes: *const u8, script_len: usize, network: FFINetwork) -> *mut c_char which builds a dashcore::ScriptBuf from bytes, calls Address::from_script, and returns a heap CString on success or null on failure.
Transaction FFI
key-wallet-ffi/src/transaction.rs
Added transaction_get_output(...) -> bool to deserialize consensus tx bytes, validate output_index, return a heap-allocated copy of the selected output's script_pubkey and write its value via out-params; added transaction_output_free(script, len) to free that buffer (no-op on null/len==0).
FFI Types & Account mapping
key-wallet-ffi/src/types.rs, key-wallet-ffi/src/managed_account.rs
Extended FFIOutputDetail with value: u64 and address: *mut c_char. managed_core_account_get_transactions now fills value and heap-allocates address per output (nullable); managed_core_account_free_transactions now frees each address CString before deallocating the array. Test adjusted accordingly.
SPV Callbacks & Client
dash-spv-ffi/src/callbacks.rs, dash-spv-ffi/src/client.rs, dash-spv-ffi/src/bin/ffi_cli.rs
Added FFINetwork field to FFIWalletEventCallbacks (default Mainnet). dispatch now derives output address and value for TransactionReceived using self.network and frees those CString addresses after callback. dash_spv_ffi_client_new now sets callbacks.wallet.network from client_config.network. CLI callback init changed to use ..Default::default() to fill remaining fields.
Docs / API
key-wallet-ffi/FFI_API.md
Updated autogenerated API docs and Table of Contents to document new functions: script_to_address, transaction_get_output, and transaction_output_free, and adjusted total function count and sections.

Sequence Diagram(s)

sequenceDiagram
    participant C as C Caller
    participant FFI as key-wallet-ffi
    participant Script as dashcore::ScriptBuf
    participant Addr as key_wallet::Address

    C->>FFI: script_to_address(script_bytes, script_len, network)
    FFI->>Script: construct ScriptBuf from bytes
    Script-->>FFI: script object / error
    FFI->>Addr: Address::from_script(script, network)
    Addr-->>FFI: address string / error
    FFI-->>C: *mut c_char (heap string) or null
Loading
sequenceDiagram
    participant C as C Caller
    participant FFI as key-wallet-ffi
    participant Tx as dashcore::Transaction

    C->>FFI: transaction_get_output(tx_bytes, tx_len, output_index, ...)
    FFI->>Tx: deserialize Transaction from bytes
    Tx-->>FFI: Transaction or error
    FFI->>Tx: access tx.output[output_index]
    Tx-->>FFI: script_pubkey bytes + value or index error
    FFI->>FFI: allocate heap buffer, copy script bytes
    FFI-->>C: true + script_ptr, script_len, value  (or false on error)
    C->>FFI: transaction_output_free(script_ptr, script_len)
    FFI-->>FFI: reconstruct Vec and drop (no-op if null/len==0)
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Poem

🐰 I nibble bytes and stitch a trail,
scripts become addresses without fail,
outputs named and values freed,
FFI burrows widened, swift as a speed.

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Title check ⚠️ Warning The PR title describes adding address and value fields to FFIOutputDetail, but the actual implementation adds FFI helper functions (script_to_address, transaction_get_output) and modifies OutputDetail without persisting address/value in the struct as the title suggests. Update the title to accurately reflect the final implementation: e.g., 'feat: add FFI helpers for address derivation and transaction output introspection' or similar, since the struct extension approach was replaced with helper functions per reviewer feedback.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/output-detail-address

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@codecov
Copy link
Copy Markdown

codecov bot commented Apr 13, 2026

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 67.99%. Comparing base (88e8a9a) to head (f7d9a03).
⚠️ Report is 10 commits behind head on v0.42-dev.

Additional details and impacted files
@@              Coverage Diff              @@
##           v0.42-dev     #640      +/-   ##
=============================================
+ Coverage      67.81%   67.99%   +0.17%     
=============================================
  Files            318      318              
  Lines          67976    67823     -153     
=============================================
+ Hits           46100    46115      +15     
+ Misses         21876    21708     -168     
Flag Coverage Δ
core 75.21% <ø> (ø)
ffi 39.52% <100.00%> (+1.23%) ⬆️
rpc 20.00% <ø> (+0.07%) ⬆️
spv 85.34% <ø> (+0.11%) ⬆️
wallet 67.62% <100.00%> (+0.06%) ⬆️
Files with missing lines Coverage Δ
dash-spv-ffi/src/callbacks.rs 79.88% <100.00%> (+6.08%) ⬆️
key-wallet-ffi/src/managed_account.rs 50.96% <100.00%> (-3.89%) ⬇️
key-wallet-ffi/src/types.rs 78.40% <ø> (ø)
key-wallet/src/managed_account/mod.rs 54.15% <100.00%> (+0.35%) ⬆️
...y-wallet/src/managed_account/transaction_record.rs 100.00% <ø> (ø)

... and 45 files with indirect coverage changes

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
dash-spv-ffi/src/callbacks.rs (1)

728-742: ⚠️ Potential issue | 🟠 Major

Free output_details[*].address after the callback.

Each CString::into_raw() here transfers ownership, but this dispatch path only frees input_details[*].address. output_details[*].address will leak on every TransactionReceived event that includes a decoded output address.

♻️ Suggested cleanup
                     cb(
                         c_wallet_id.as_ptr(),
                         *account_index,
                         &ffi_record as *const FFITransactionRecord,
                         self.user_data,
                     );
 
                     // Free the CString addresses from input details
                     for detail in input_details {
                         if !detail.address.is_null() {
                             unsafe {
                                 drop(CString::from_raw(detail.address));
                             }
                         }
                     }
+                    for detail in output_details {
+                        if !detail.address.is_null() {
+                            unsafe {
+                                drop(CString::from_raw(detail.address));
+                            }
+                        }
+                    }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@dash-spv-ffi/src/callbacks.rs` around lines 728 - 742, The code creates
CStrings for output addresses with CString::into_raw() when building
output_details (FFIOutputDetail) but never frees them, causing leaks on
TransactionReceived paths; mirror the cleanup done for input_details by
iterating over output_details after the callback and calling
CString::from_raw(ptr) for each non-null address to take back ownership and drop
it (ensure you check for std::ptr::null_mut() before from_raw), referencing the
output_details variable and FFIOutputDetail::address so the free logic is
colocated with the existing input_details free routine.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@key-wallet-ffi/src/types.rs`:
- Around line 930-934: You changed the FFI struct layout by adding fields to
FFIOutputDetail (adding value: u64 and address: *mut std::os::raw::c_char) which
breaks the C ABI for consumers; to fix, revert the struct layout change or
coordinate a safe migration: keep the original repr(C) layout for
FFIOutputDetail, introduce a new struct (e.g., FFIOutputDetailV2) with the
additional fields, and update internal code to produce the new struct only for
consumers that are rebuilt together; ensure all FFI-producing functions and any
constructors/serializers that return *mut FFIOutputDetail are adjusted to return
the appropriate type and document the required binding updates for downstream
consumers.

In `@key-wallet/src/managed_account/transaction_record.rs`:
- Around line 40-43: The TransactionRecord struct's fields address and value
need serde defaults to avoid deserialization failures for older persisted data;
add the attribute #[cfg_attr(feature = "serde", serde(default))] to both the pub
address: Option<Address> and pub value: u64 fields in transaction_record.rs so
missing address defaults to None and missing value defaults to 0 when the
"serde" feature is enabled.

---

Outside diff comments:
In `@dash-spv-ffi/src/callbacks.rs`:
- Around line 728-742: The code creates CStrings for output addresses with
CString::into_raw() when building output_details (FFIOutputDetail) but never
frees them, causing leaks on TransactionReceived paths; mirror the cleanup done
for input_details by iterating over output_details after the callback and
calling CString::from_raw(ptr) for each non-null address to take back ownership
and drop it (ensure you check for std::ptr::null_mut() before from_raw),
referencing the output_details variable and FFIOutputDetail::address so the free
logic is colocated with the existing input_details free routine.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: bb2446aa-88b1-4323-8c61-bba9b71d7eb3

📥 Commits

Reviewing files that changed from the base of the PR and between 383b306 and 8be40f8.

📒 Files selected for processing (5)
  • dash-spv-ffi/src/callbacks.rs
  • key-wallet-ffi/src/managed_account.rs
  • key-wallet-ffi/src/types.rs
  • key-wallet/src/managed_account/mod.rs
  • key-wallet/src/managed_account/transaction_record.rs

Comment thread key-wallet-ffi/src/types.rs Outdated
Comment thread key-wallet/src/managed_account/transaction_record.rs
Copy link
Copy Markdown
Collaborator

@xdustinface xdustinface left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks alright generally but i actually intentionally didn't add those because we store redundant data when they are added to the record since the TransactionRecord contains the full Transaction so with the index of the OutputDetail you can get TxOut.script_pubkey and convert it.

However.. it looks like there is no script_to_address function in the FFI but there might be one in the swift SDK? Can you have a look around there?

Or.. we could maybe just add it in the FFIOutputDetails and calculate it in the FFI when requested.

Im leaning more towards the first option (calculation helper) since we then don't diverge the FFI type from the rust type.

Would that work or is there something about all this that justifies storing address/amount twice?

Comment thread key-wallet-ffi/src/types.rs Outdated
Comment thread key-wallet/src/managed_account/transaction_record.rs Outdated
@llbartekll llbartekll force-pushed the feat/output-detail-address branch from 7c66864 to ece3a61 Compare April 13, 2026 13:15
@llbartekll
Copy link
Copy Markdown
Contributor Author

Looks alright generally but i actually intentionally didn't add those because we store redundant data when they are added to the record since the TransactionRecord contains the full Transaction so with the index of the OutputDetail you can get TxOut.script_pubkey and convert it.

However.. it looks like there is no script_to_address function in the FFI but there might be one in the swift SDK? Can you have a look around there?

Or.. we could maybe just add it in the FFIOutputDetails and calculate it in the FFI when requested.

Im leaning more towards the first option (calculation helper) since we then don't diverge the FFI type from the rust type.

Would that work or is there something about all this that justifies storing address/amount twice?

makes sense, couldn't find a script_to_address equivalent in the swift SDK, so replaced the struct extension with two FFI helpers: script_to_address and transaction_get_output_script.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (1)
key-wallet-ffi/src/transaction.rs (1)

523-537: Initialize out-params to safe defaults before fallible paths.

When returning false, stale caller values may remain in script_out/script_len_out/value_out. Pre-initialize them to deterministic defaults.

Suggested hardening
 pub unsafe extern "C" fn transaction_get_output_script(
@@
 ) -> bool {
     if tx_bytes.is_null() || script_out.is_null() || script_len_out.is_null() || value_out.is_null()
     {
         return false;
     }
+
+    *script_out = ptr::null_mut();
+    *script_len_out = 0;
+    *value_out = 0;
 
     let tx_slice = slice::from_raw_parts(tx_bytes, tx_len);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@key-wallet-ffi/src/transaction.rs` around lines 523 - 537, Before any
fallible operations in the function, set the out-parameters to safe
deterministic defaults so caller state cannot be left stale: assign script_out
to null (or equivalent empty pointer), script_len_out to 0, and value_out to 0
before performing the null-checks and before calling slice::from_raw_parts,
consensus::deserialize, or tx.output.get; keep these initializations near the
top of the function (before the early returns that currently return false) so
every error path returns with known values for script_out, script_len_out, and
value_out.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@key-wallet-ffi/src/address.rs`:
- Line 140: The code path in the match arm returning a CString for Ok(addr) uses
CString::new(...).unwrap_or_default().into_raw(), which can yield a pointer to
an empty string on conversion failure; update the Ok branch in the function (the
match arm currently containing Ok(addr) => ...) to handle
CString::new(addr.to_string())'s Result explicitly and return a null pointer on
Err (e.g., map or match the CString::new result and call into_raw() only on Ok,
otherwise return std::ptr::null_mut()), preserving the function's
null-on-failure contract.

---

Nitpick comments:
In `@key-wallet-ffi/src/transaction.rs`:
- Around line 523-537: Before any fallible operations in the function, set the
out-parameters to safe deterministic defaults so caller state cannot be left
stale: assign script_out to null (or equivalent empty pointer), script_len_out
to 0, and value_out to 0 before performing the null-checks and before calling
slice::from_raw_parts, consensus::deserialize, or tx.output.get; keep these
initializations near the top of the function (before the early returns that
currently return false) so every error path returns with known values for
script_out, script_len_out, and value_out.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: c307d8a7-1b9a-4af2-976f-751d487f2499

📥 Commits

Reviewing files that changed from the base of the PR and between 8be40f8 and ece3a61.

📒 Files selected for processing (2)
  • key-wallet-ffi/src/address.rs
  • key-wallet-ffi/src/transaction.rs

Comment thread key-wallet-ffi/src/address.rs Outdated
@llbartekll llbartekll requested a review from xdustinface April 13, 2026 13:20
@llbartekll llbartekll force-pushed the feat/output-detail-address branch 3 times, most recently from e8ca18b to 19a9542 Compare April 13, 2026 17:20
@ZocoLini
Copy link
Copy Markdown
Collaborator

Which method are you calling to get the transaction?? 'managed_core_account_get_transactions'??

@llbartekll
Copy link
Copy Markdown
Contributor Author

Which method are you calling to get the transaction?? 'managed_core_account_get_transactions'??

correct, I only modified public func getTransactions() -> [WalletTransaction] { so that WalletTransaction contains outputs now

Copy link
Copy Markdown
Collaborator

@xdustinface xdustinface left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@llbartekll Seems like i was wrong with favouring Option 1 before. I guess since we dont have a proper way yet to inspect transactions in the FFI side this way feels also not quite right because we now deserialise the whole transaction for every output we pull out of it.

Lets maybe rather go with the address + value in the FFIOutputDetails? I was thinking more about all this and i think eventually it should also be in the OutputDetails on the rust side but we should overhaul the transaction storage situation first.

Or @ZocoLini you said you are working on the swift transaction record, did you have any more thoughts about this? If not lets just

@llbartekll llbartekll force-pushed the feat/output-detail-address branch 2 times, most recently from 3d6cd97 to 9ba9f17 Compare April 14, 2026 18:24
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<Address>` 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) <noreply@anthropic.com>
@llbartekll llbartekll force-pushed the feat/output-detail-address branch from 9ba9f17 to c362f1d Compare April 14, 2026 18:35
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) <noreply@anthropic.com>
Comment thread key-wallet/src/managed_account/transaction_record.rs Outdated
@llbartekll llbartekll force-pushed the feat/output-detail-address branch from 2d2978b to 3f526cb Compare April 14, 2026 19:40
Co-authored-by: Kevin Rombach <35775977+xdustinface@users.noreply.github.com>
Copy link
Copy Markdown
Collaborator

@ZocoLini ZocoLini left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

perfect

@github-actions github-actions bot added the ready-for-review CodeRabbit has approved this PR label Apr 14, 2026
@llbartekll llbartekll requested a review from xdustinface April 14, 2026 20:25
@xdustinface xdustinface merged commit b5cb491 into v0.42-dev Apr 14, 2026
40 checks passed
@xdustinface xdustinface deleted the feat/output-detail-address branch April 14, 2026 22:01
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ready-for-review CodeRabbit has approved this PR

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants