feat: expose full TransactionRecord through FFI and wallet events#614
feat: expose full TransactionRecord through FFI and wallet events#614QuantumExplorer merged 4 commits intov0.42-devfrom
TransactionRecord through FFI and wallet events#614Conversation
📝 WalkthroughWalkthroughReplaces multiple transaction callback parameters with a single Changes
Sequence Diagram(s)sequenceDiagram
autonumber
participant WalletCore as Wallet Core
participant Manager as WalletManager
participant FFI as FFI Layer
participant Client as C Client
rect rgba(200,200,255,0.5)
WalletCore->>Manager: detect new transaction (tx)
Manager->>WalletCore: record_transaction(tx) -> TransactionRecord
Manager->>FFI: build FFITransactionRecord (tx_data, input/output details, label)
end
rect rgba(200,255,200,0.5)
FFI->>Client: invoke on_transaction_received(wallet_id, account_index, record_ptr, user_data)
Client-->>FFI: returns
FFI->>FFI: free nested allocations for record_ptr
end
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes Possibly related PRs
Suggested labels
Suggested reviewers
Poem
🚥 Pre-merge checks | ✅ 3✅ Passed checks (3 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Warning Review ran into problems🔥 ProblemsTimed out fetching pipeline failures after 30000ms 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. Comment |
|
@CodeRabbit review |
✅ Actions performedReview triggered.
|
461105c to
a2bd33f
Compare
|
@CodeRabbit review |
✅ Actions performedReview triggered.
|
There was a problem hiding this comment.
Actionable comments posted: 3
🧹 Nitpick comments (2)
key-wallet-manager/src/event_tests.rs (1)
12-92: Add one lifecycle test for non-index account types.Current updates validate the new
recordpayload well, but mostly through indexed account paths. Add aTransactionReceivedassertion for a non-index account type to catch dropped-event regressions innew_recordspropagation.As per coding guidelines, "Write unit tests for new functionality".
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@key-wallet-manager/src/event_tests.rs` around lines 12 - 92, Add a new lifecycle test that exercises a non-index account type and asserts a TransactionReceived event is emitted and carries the full `record` payload; use the same helpers as other tests (setup_manager_with_wallet, create_tx_paying_to, subscribe_events, assert_single_event) and call manager.check_transaction_in_all_wallets(&tx, TransactionContext::Mempool, true, true).await (or TransactionContext::InBlock for first-seen-in-block variants) to trigger new_records propagation, then match WalletEvent::TransactionReceived and assert record.context, record.txid == tx.txid(), ev_wid == wallet_id, and record.net_amount == TX_AMOUNT as i64 to catch dropped-event regressions for non-index accounts.key-wallet-ffi/src/managed_account.rs (1)
665-667: Scope the ownership note to the owned transaction API.
FFITransactionRecordis now also passed as a borrowed callback payload indash-spv-ffi/src/callbacks.rs:537-546. Keepingmanaged_core_account_free_transactionsin the type-level doc makes the ownership contract look universal, which can steer FFI callers toward freeing borrowed callback memory.Suggested wording
-/// Heap-allocated fields must be freed with `managed_core_account_free_transactions`. +/// When returned by `managed_core_account_get_transactions`, heap-allocated fields +/// must be freed with `managed_core_account_free_transactions`. +/// +/// This struct is also used as a borrowed callback payload in `dash-spv-ffi`; +/// borrowed callback records and their nested pointers are only valid for the +/// duration of the callback and must not be freed by the caller.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@key-wallet-ffi/src/managed_account.rs` around lines 665 - 667, The type-level doc for FFITransactionRecord wrongly implies all instances must be freed with managed_core_account_free_transactions; update the comment to scope that ownership contract to the "owned transaction" API only (e.g., instances returned by managed_core_account_free_transactions or the managed account owned API) and explicitly state that callback-provided/borrowed FFITransactionRecord payloads are not owned and must not be freed by callers (reference FFITransactionRecord and managed_core_account_free_transactions and the borrowed callback usage).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@dash-spv-ffi/src/callbacks.rs`:
- Around line 758-763: The code currently converts record.label into an owned
raw C string with CString::new(...).unwrap().into_raw(), leaking memory and
risking a panic on interior NULs; instead, create a local CString (e.g., let
c_label = CString::new(record.label.as_deref().unwrap_or("")).map_err(|e| /* map
to our error type */)?), keep it in scope across the cb(...) invocation and pass
c_label.as_ptr() (or c_label.as_ptr() as *mut _ if mutable is required) so it is
not into_raw'd, and propagate/handle the CString::new error instead of
unwrap/expect; apply the same change to the other occurrence mentioned (lines
~772-779) and replace unwrap() calls with proper error handling using the
crate's error type.
In `@dash-spv-ffi/tests/dashd_sync/callbacks.rs`:
- Around line 354-357: The two separate pushes to tracker.received_txids and
tracker.received_amounts can interleave across threads and break per-callback
pairing; change the callback in callbacks.rs so it captures the entire
FFITransactionRecord snapshot (use the unsafe deref of record to read r.txid,
r.net_amount and any new fields) and then perform a single lock/unlock to push
one combined snapshot (e.g., push a struct or tuple) into a new
tracker.received_snapshots Vec, replacing the two separate pushes to
received_txids and received_amounts to ensure each entry represents one callback
atomically.
In `@key-wallet/src/transaction_checking/wallet_checker.rs`:
- Around line 127-130: The code currently only pushes new_records when
account_match.account_type_match.account_index() returns Some, which discards
records for non-index account types and prevents downstream TransactionReceived
events; change the logic in the wallet checking flow so that record =
account.record_transaction(tx, &account_match, context, tx_type) is always
appended to result.new_records (e.g., push a variant that includes None for the
account index or push all records unconditionally and let downstream handle None
indices), updating any consumers to handle Option<usize> if necessary; then add
a regression unit test that simulates a non-index account receiving a
transaction and asserts that TransactionReceived is emitted for that account
type to prevent future regressions.
---
Nitpick comments:
In `@key-wallet-ffi/src/managed_account.rs`:
- Around line 665-667: The type-level doc for FFITransactionRecord wrongly
implies all instances must be freed with managed_core_account_free_transactions;
update the comment to scope that ownership contract to the "owned transaction"
API only (e.g., instances returned by managed_core_account_free_transactions or
the managed account owned API) and explicitly state that
callback-provided/borrowed FFITransactionRecord payloads are not owned and must
not be freed by callers (reference FFITransactionRecord and
managed_core_account_free_transactions and the borrowed callback usage).
In `@key-wallet-manager/src/event_tests.rs`:
- Around line 12-92: Add a new lifecycle test that exercises a non-index account
type and asserts a TransactionReceived event is emitted and carries the full
`record` payload; use the same helpers as other tests
(setup_manager_with_wallet, create_tx_paying_to, subscribe_events,
assert_single_event) and call manager.check_transaction_in_all_wallets(&tx,
TransactionContext::Mempool, true, true).await (or TransactionContext::InBlock
for first-seen-in-block variants) to trigger new_records propagation, then match
WalletEvent::TransactionReceived and assert record.context, record.txid ==
tx.txid(), ev_wid == wallet_id, and record.net_amount == TX_AMOUNT as i64 to
catch dropped-event regressions for non-index accounts.
🪄 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: f6a6bc2b-105d-48bb-a6df-bc96e0ecee56
📒 Files selected for processing (13)
dash-spv-ffi/src/bin/ffi_cli.rsdash-spv-ffi/src/callbacks.rsdash-spv-ffi/tests/dashd_sync/callbacks.rsdash-spv/tests/dashd_sync/helpers.rskey-wallet-ffi/src/managed_account.rskey-wallet-ffi/src/types.rskey-wallet-manager/src/event_tests.rskey-wallet-manager/src/events.rskey-wallet-manager/src/lib.rskey-wallet-manager/src/test_helpers.rskey-wallet/src/managed_account/mod.rskey-wallet/src/transaction_checking/account_checker.rskey-wallet/src/transaction_checking/wallet_checker.rs
Codecov Report❌ Patch coverage is Additional details and impacted files@@ Coverage Diff @@
## v0.42-dev #614 +/- ##
=============================================
+ Coverage 67.45% 67.53% +0.08%
=============================================
Files 320 320
Lines 67949 68169 +220
=============================================
+ Hits 45832 46038 +206
- Misses 22117 22131 +14
|
There was a problem hiding this comment.
♻️ Duplicate comments (1)
dash-spv-ffi/src/callbacks.rs (1)
758-763:⚠️ Potential issue | 🟠 MajorMemory leak:
labelCString is allocated but never freed after callback.The
labelfield usesCString::new(...).unwrap().into_raw()which transfers ownership to the raw pointer. Unlikeinput_detailsaddresses which are freed after the callback (lines 772-779), thelabelis never reclaimed, leaking memory for every labeled transaction event.Additionally,
unwrap()can panic on labels containing interior NUL bytes.🔧 Suggested fix: Keep CString alive across callback
+ let label_cstring = record + .label + .as_ref() + .map(|l| CString::new(l.as_str()).unwrap_or_default()); + let ffi_record = FFITransactionRecord { // ... other fields ... - label: record - .label - .as_ref() - .map(|l| CString::new(l.as_str()).unwrap().into_raw()) - .unwrap_or(std::ptr::null_mut()), + label: label_cstring + .as_ref() + .map_or(std::ptr::null_mut(), |c| c.as_ptr() as *mut _), };As per coding guidelines,
**/src/**/*.rs: Avoidunwrap()andexpect()in library code; use proper error types.🤖 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 758 - 763, The code allocates a CString for record.label using CString::new(...).unwrap().into_raw() and never frees it, leaking on every callback and risking panics from unwrap() on interior NULs; change the approach in the callback construction (the usage around label and record.label) to safely create the CString without unwrap (return a Result or propagate an error) and ensure the raw pointer is reclaimed after the callback returns (mirror how input_details pointers are freed in the block that releases them), or keep the CString owned for the callback lifetime and convert to a raw pointer only for the call then call CString::from_raw(...) to free it immediately after; update any functions that build or consume label to accept a nullable pointer safely and remove unwrap() usage.
🧹 Nitpick comments (2)
key-wallet-ffi/src/types.rs (1)
861-874: Consider adding#[derive(Debug)]toFFIInputDetailandFFIOutputDetailfor consistency.The new FFI enums (
FFITransactionDirection,FFITransactionType,FFIOutputRole) all have#[derive(Debug, Clone, Copy)], but these structs lack derives. WhileFFIInputDetailcan't deriveDebugdirectly due to the raw pointer,FFIOutputDetailcould benefit from it for debugging consistency with other FFI types.♻️ Proposed fix for FFIOutputDetail
/// FFI-compatible output detail #[repr(C)] +#[derive(Debug, Clone, Copy)] pub struct FFIOutputDetail { pub index: u32, pub role: FFIOutputRole, }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@key-wallet-ffi/src/types.rs` around lines 861 - 874, FFIOutputDetail should derive Debug for consistency with the other FFI types — add #[derive(Debug, Clone, Copy)] (or at least #[derive(Debug)]) to the FFIOutputDetail declaration; FFIInputDetail cannot use the automatic Debug derive because it contains a raw C string pointer, so implement Debug manually for FFIInputDetail (impl std::fmt::Debug for FFIInputDetail) and format index and value directly and print the address safely (either as the raw pointer or by converting to &CStr inside an unsafe block with clear fallback) to avoid deriving on the raw pointer.dash-spv-ffi/src/callbacks.rs (1)
740-756: Document the borrow semantics of array pointers in FFITransactionRecord.The
input_details,output_details, andtx_datapointers point to stack-allocated Vecs that become invalid after the callback returns. While the callback documentation mentions the record is "borrowed," callers might assume they can callmanaged_core_account_free_transactionson callback-delivered records, which would cause undefined behavior since these pointers weren't heap-allocated via that function.Consider adding a comment clarifying that records from callbacks have different ownership semantics than records from
managed_core_account_get_transactions.🤖 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 740 - 756, Add a clear comment to FFITransactionRecord documenting that the pointers input_details, output_details, and tx_data are borrows into stack/temporary Vecs provided only for the duration of the callback and must not be freed or passed to managed_core_account_free_transactions; explicitly state that ownership semantics differ from records returned by managed_core_account_get_transactions (which are heap-owned and must be freed), and mention callers who need persistent data should clone or allocate their own copies before the callback returns.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Duplicate comments:
In `@dash-spv-ffi/src/callbacks.rs`:
- Around line 758-763: The code allocates a CString for record.label using
CString::new(...).unwrap().into_raw() and never frees it, leaking on every
callback and risking panics from unwrap() on interior NULs; change the approach
in the callback construction (the usage around label and record.label) to safely
create the CString without unwrap (return a Result or propagate an error) and
ensure the raw pointer is reclaimed after the callback returns (mirror how
input_details pointers are freed in the block that releases them), or keep the
CString owned for the callback lifetime and convert to a raw pointer only for
the call then call CString::from_raw(...) to free it immediately after; update
any functions that build or consume label to accept a nullable pointer safely
and remove unwrap() usage.
---
Nitpick comments:
In `@dash-spv-ffi/src/callbacks.rs`:
- Around line 740-756: Add a clear comment to FFITransactionRecord documenting
that the pointers input_details, output_details, and tx_data are borrows into
stack/temporary Vecs provided only for the duration of the callback and must not
be freed or passed to managed_core_account_free_transactions; explicitly state
that ownership semantics differ from records returned by
managed_core_account_get_transactions (which are heap-owned and must be freed),
and mention callers who need persistent data should clone or allocate their own
copies before the callback returns.
In `@key-wallet-ffi/src/types.rs`:
- Around line 861-874: FFIOutputDetail should derive Debug for consistency with
the other FFI types — add #[derive(Debug, Clone, Copy)] (or at least
#[derive(Debug)]) to the FFIOutputDetail declaration; FFIInputDetail cannot use
the automatic Debug derive because it contains a raw C string pointer, so
implement Debug manually for FFIInputDetail (impl std::fmt::Debug for
FFIInputDetail) and format index and value directly and print the address safely
(either as the raw pointer or by converting to &CStr inside an unsafe block with
clear fallback) to avoid deriving on the raw pointer.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: e5a607be-a459-4b28-9036-25cdc4167292
📒 Files selected for processing (13)
dash-spv-ffi/src/bin/ffi_cli.rsdash-spv-ffi/src/callbacks.rsdash-spv-ffi/tests/dashd_sync/callbacks.rsdash-spv/tests/dashd_sync/helpers.rskey-wallet-ffi/src/managed_account.rskey-wallet-ffi/src/types.rskey-wallet-manager/src/event_tests.rskey-wallet-manager/src/events.rskey-wallet-manager/src/lib.rskey-wallet-manager/src/test_helpers.rskey-wallet/src/managed_account/mod.rskey-wallet/src/transaction_checking/account_checker.rskey-wallet/src/transaction_checking/wallet_checker.rs
✅ Files skipped from review due to trivial changes (1)
- dash-spv-ffi/tests/dashd_sync/callbacks.rs
🚧 Files skipped from review as they are similar to previous changes (7)
- dash-spv/tests/dashd_sync/helpers.rs
- key-wallet/src/managed_account/mod.rs
- key-wallet-manager/src/event_tests.rs
- key-wallet/src/transaction_checking/account_checker.rs
- key-wallet/src/transaction_checking/wallet_checker.rs
- key-wallet-manager/src/events.rs
- dash-spv-ffi/src/bin/ffi_cli.rs
- Add FFI types for transaction direction, type, output role, and input/output details - Enrich `FFITransactionRecord` with classification, details, label and serialized tx bytes - Simplify `WalletEvent::TransactionReceived` to carry `Box<TransactionRecord>` - Return `TransactionRecord` from `record_transaction` and propagate via `TransactionCheckResult` - Refactor `OnTransactionReceivedCallback` to pass `*const FFITransactionRecord`
Addresses CodeRabbit review comment on PR #614 #614 (comment)
Addresses CodeRabbit review comment on PR #614 #614 (comment)
a2bd33f to
0452c0a
Compare
Addresses CodeRabbit review comment on PR #614 #614 (comment)
0452c0a to
06490f9
Compare
The test sends 1 DASH from the same wallet (same mnemonic) that the SPV client tracks, making it an internal transfer where both inputs and outputs belong to the wallet. The `net_amount` therefore equals approximately `-fee`, not +100_000_000. Replace the strict amount pairing assertion with a txid presence check and non-zero net_amount verification.
There was a problem hiding this comment.
♻️ Duplicate comments (1)
key-wallet/src/transaction_checking/wallet_checker.rs (1)
127-130:⚠️ Potential issue | 🟠 MajorStill drops
TransactionReceivedrecords for non-index accounts.Lines 128-130 still discard the freshly recorded transaction whenever
account_index()isNone, so non-index account types can update wallet state without ever reachingnew_recordsor the downstreamWalletEvent::TransactionReceivedpath. Please carry a richer account identifier here (Option<u32>or account-type metadata) instead of filtering the record out, and add a regression test for a non-index account match. As per coding guidelines, "Write unit tests for new functionality".🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@key-wallet/src/transaction_checking/wallet_checker.rs` around lines 127 - 130, The code currently drops TransactionReceived records when account_match.account_type_match.account_index() is None; change result.new_records to carry a richer identifier (e.g., change its element from (u32, Record) to (Option<u32>, Record) or include account-type metadata) so the record is always pushed: update the push site in wallet_checker.rs (currently using account.record_transaction and result.new_records.push((account_index, record))) to push the Option<u32> or metadata instead, then update downstream consumers (the collection that feeds WalletEvent::TransactionReceived) to accept the new identifier shape and preserve behavior for Some(account_index) and None; finally add a unit test that creates a non-index account match and asserts WalletEvent::TransactionReceived is produced for that case.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Duplicate comments:
In `@key-wallet/src/transaction_checking/wallet_checker.rs`:
- Around line 127-130: The code currently drops TransactionReceived records when
account_match.account_type_match.account_index() is None; change
result.new_records to carry a richer identifier (e.g., change its element from
(u32, Record) to (Option<u32>, Record) or include account-type metadata) so the
record is always pushed: update the push site in wallet_checker.rs (currently
using account.record_transaction and result.new_records.push((account_index,
record))) to push the Option<u32> or metadata instead, then update downstream
consumers (the collection that feeds WalletEvent::TransactionReceived) to accept
the new identifier shape and preserve behavior for Some(account_index) and None;
finally add a unit test that creates a non-index account match and asserts
WalletEvent::TransactionReceived is produced for that case.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: f216da13-7c17-4161-bf46-5c6f5d88f5fe
📒 Files selected for processing (15)
dash-spv-ffi/src/bin/ffi_cli.rsdash-spv-ffi/src/callbacks.rsdash-spv-ffi/tests/dashd_sync/callbacks.rsdash-spv-ffi/tests/dashd_sync/tests_callback.rsdash-spv-ffi/tests/dashd_sync/tests_transaction.rsdash-spv/tests/dashd_sync/helpers.rskey-wallet-ffi/src/managed_account.rskey-wallet-ffi/src/types.rskey-wallet-manager/src/event_tests.rskey-wallet-manager/src/events.rskey-wallet-manager/src/lib.rskey-wallet-manager/src/test_helpers.rskey-wallet/src/managed_account/mod.rskey-wallet/src/transaction_checking/account_checker.rskey-wallet/src/transaction_checking/wallet_checker.rs
🚧 Files skipped from review as they are similar to previous changes (6)
- dash-spv/tests/dashd_sync/helpers.rs
- key-wallet-manager/src/test_helpers.rs
- key-wallet/src/transaction_checking/account_checker.rs
- key-wallet-manager/src/event_tests.rs
- key-wallet-manager/src/events.rs
- dash-spv-ffi/src/bin/ffi_cli.rs
FFITransactionRecordwith classification, details, label and serialized tx bytesWalletEvent::TransactionReceivedto carryBox<TransactionRecord>TransactionRecordfromrecord_transactionand propagate viaTransactionCheckResultOnTransactionReceivedCallbackto pass*const FFITransactionRecordBased on:
key-walletnaming #612Summary by CodeRabbit
New Features
Bug Fixes
Tests