Skip to content

refactor(platform-wallet): expose the new core TransactionBuilder API#3970

Open
ZocoLini wants to merge 1 commit into
v4.1-devfrom
feat/core-tx-builder-ffi
Open

refactor(platform-wallet): expose the new core TransactionBuilder API#3970
ZocoLini wants to merge 1 commit into
v4.1-devfrom
feat/core-tx-builder-ffi

Conversation

@ZocoLini

@ZocoLini ZocoLini commented Jun 29, 2026

Copy link
Copy Markdown
Collaborator
  • Core transaction builder exposed over FFI
  • set_gap_limit for any account type (focus: CoinJoin)
  • Swift wrappers: CoreTransactionBuilder (fluent API), CoreTransaction, ManagedCoreWallet.broadcastTransaction/setGapLimit, and migration of SendViewModel (core→core) to the new build→broadcast flow

Checklist:

  • I have performed a self-review of my own code
  • I have commented my code, particularly in hard-to-understand areas
  • I have added or updated relevant unit/integration/functional/e2e tests
  • I have added "!" to the title and described breaking changes in the corresponding section if my code contains any
  • I have made corresponding changes to the documentation if needed

For repository code-owners and collaborators only

  • I have assigned this pull request to a milestone

Summary by CodeRabbit

  • New Features

    • Added support for building and signing core wallet transactions before broadcasting.
    • Added an option to increase an account’s address gap limit.
    • Improved transaction handling with clearer separation between signing and broadcasting.
  • Bug Fixes

    • Updated send flow to better handle core-to-core payments and multi-recipient transactions.
    • Improved reservation handling so failed broadcasts can retry more reliably.

@coderabbitai

coderabbitai Bot commented Jun 29, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

This PR adds a new Rust FFI transaction builder module for core wallets, exposing C-callable functions to configure outputs, funding, inputs, fees, and sign transactions. It refactors broadcast and address APIs around signed FFICoreTransaction objects, adds a gap-limit setter, and updates Swift SDK bindings and the example app's send flow accordingly.

Changes

Core wallet transaction and address flow

Layer / File(s) Summary
Core wallet gap limit and reservation-aware broadcast
packages/rs-platform-wallet/src/wallet/core/wallet.rs, packages/rs-platform-wallet/src/wallet/core/broadcast.rs
Adds CoreWallet::set_gap_limit to update an account's address gap limit, and replaces send_to_addresses with broadcast_transaction_releasing_reservation, which reconciles UTXO reservations on failure; tests rewritten accordingly.
FFI address and broadcast entrypoints
packages/rs-platform-wallet-ffi/src/core_wallet/addresses.rs, packages/rs-platform-wallet-ffi/src/core_wallet/broadcast.rs, packages/rs-platform-wallet-ffi/src/core_wallet/mod.rs
Adds core_wallet_set_gap_limit FFI function; changes core_wallet_broadcast_transaction to accept an FFICoreTransaction pointer plus account type/index, dispatching to reservation-aware or plain broadcast; removes core_wallet_send_to_addresses and core_wallet_free_tx_bytes; wires the new transaction_builder module.
FFI transaction builder API
packages/rs-platform-wallet-ffi/src/core_wallet/transaction_builder.rs
New module introducing FFITransactionBuilder/FFICoreTransaction opaque types, CoreAccountTypeFFI/CoreSelectionStrategyFFI enums, and exported functions to create the builder, add outputs/inputs, set change address, fee rate, selection strategy, height, special payload, funding, build-and-sign, and destroy/free lifecycle functions.
Swift wallet and transaction wrappers
packages/swift-sdk/Sources/SwiftDashSDK/PlatformWallet/CoreWallet/CoreTransactionBuilder.swift, packages/swift-sdk/Sources/SwiftDashSDK/PlatformWallet/CoreWallet/ManagedCoreWallet.swift
Adds CoreTransaction and CoreTransactionBuilder Swift wrappers over the new FFI; adds setGapLimit and a broadcastTransaction(_ tx: CoreTransaction) method on ManagedCoreWallet, removing prior AccountType/sendToAddresses/raw-bytes broadcast APIs.
Example app send flow
packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Core/ViewModels/SendViewModel.swift, packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Core/Views/SendTransactionView.swift
Updates executeSend to accept ManagedPlatformWallet instead of ManagedCoreWallet, builds a signed transaction via CoreTransactionBuilder for core-to-core sends, and broadcasts it; updates the calling view accordingly.

Estimated code review effort: 4 (Complex) | ~60 minutes

Sequence Diagram(s)

sequenceDiagram
  participant SendViewModel
  participant CoreTransactionBuilder
  participant ManagedPlatformWallet
  participant RustFFI
  participant CoreWallet

  SendViewModel->>CoreTransactionBuilder: addOutput / setFunding / buildSigned
  CoreTransactionBuilder->>RustFFI: core_wallet_tx_builder_* calls
  RustFFI->>CoreWallet: set funding, add inputs, build and sign
  CoreTransactionBuilder-->>SendViewModel: CoreTransaction
  SendViewModel->>ManagedPlatformWallet: broadcastTransaction(CoreTransaction)
  ManagedPlatformWallet->>RustFFI: core_wallet_broadcast_transaction(tx, account_type, account_index)
  RustFFI->>CoreWallet: broadcast_transaction_releasing_reservation or broadcast_transaction
  CoreWallet-->>ManagedPlatformWallet: txid
Loading

Possibly related PRs

  • dashpay/platform#3634: Both PRs modify the core_wallet/broadcast.rs FFI surface, changing the same core_wallet_send_to_addresses/broadcast_transaction-related entrypoints.
  • dashpay/platform#3904: Both PRs modify SendViewModel.executeSend's .coreToCore path, overlapping on the same send logic.
  • dashpay/platform#3985: Introduces the same reservation-release-on-rejection/preserve-on-ambiguous-failure broadcast behavior now wired into core_wallet_broadcast_transaction.

Suggested labels: ready for final review

Suggested reviewers: shumkov, llbartekll, thepastaclaw

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main change: exposing the new core TransactionBuilder API in platform-wallet.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/core-tx-builder-ffi

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.

@ZocoLini ZocoLini force-pushed the feat/core-tx-builder-ffi branch 2 times, most recently from 68065ab to 3159ebb Compare July 1, 2026 08:04
@shumkov shumkov added this to the v4.1.0 milestone Jul 2, 2026
@shumkov shumkov changed the base branch from v4.0-dev to v4.1-dev July 2, 2026 07:51
@ZocoLini ZocoLini changed the title refactor(platform-wallet-ffi): expose the new core TransactionBuilder API refactor(platform-wallet): expose the new core TransactionBuilder API Jul 2, 2026
@ZocoLini ZocoLini marked this pull request as ready for review July 2, 2026 07:58
@thepastaclaw

thepastaclaw commented Jul 2, 2026

Copy link
Copy Markdown
Collaborator

✅ Review complete (commit 3161856)

@ZocoLini ZocoLini force-pushed the feat/core-tx-builder-ffi branch 2 times, most recently from 5265458 to f265289 Compare July 2, 2026 09:30
@ZocoLini ZocoLini changed the title refactor(platform-wallet): expose the new core TransactionBuilder API refactor(platform-wallet): expose the new core TransactionBuilder API + rust-dashcore bump Jul 2, 2026
@codecov

codecov Bot commented Jul 2, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 52.54%. Comparing base (9f9092c) to head (909484a).
⚠️ Report is 1 commits behind head on v4.1-dev.

❗ There is a different number of reports uploaded between BASE (9f9092c) and HEAD (909484a). Click for more details.

HEAD has 2 uploads less than BASE
Flag BASE (9f9092c) HEAD (909484a)
rust 3 1
Additional details and impacted files
@@              Coverage Diff              @@
##           v4.1-dev    #3970       +/-   ##
=============================================
- Coverage     87.18%   52.54%   -34.64%     
=============================================
  Files          2632       11     -2621     
  Lines        327563     1707   -325856     
=============================================
- Hits         285592      897   -284695     
+ Misses        41971      810    -41161     
Components Coverage Δ
dpp ∅ <ø> (∅)
drive ∅ <ø> (∅)
drive-abci ∅ <ø> (∅)
sdk ∅ <ø> (∅)
dapi-client ∅ <ø> (∅)
platform-version ∅ <ø> (∅)
platform-value ∅ <ø> (∅)
platform-wallet ∅ <ø> (∅)
drive-proof-verifier ∅ <ø> (∅)
🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@ZocoLini ZocoLini force-pushed the feat/core-tx-builder-ffi branch from f265289 to b6969a7 Compare July 2, 2026 09:50

@thepastaclaw thepastaclaw left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Code Review

The PR cleanly exposes key-wallet's TransactionBuilder through the FFI/Swift wrappers, but the take/store pattern is unsafe on the two async, wallet-lookup-backed steps (set_funding, add_inputs_from_outpoints): they take the builder out via mem::take before the fallible lookups, and on error the drained default is silently left in the handle — discarding all previously-configured outputs/change/fee/payload. This is a real hazard for the PR's own Send flow. A build_signed docstring inaccuracy and unchecked cross-network address parsing are additional concerns.

🔴 2 blocking | 🟡 4 suggestion(s) | 💬 3 nitpick(s)

🤖 Prompt for all review comments with AI agents
These findings are from an automated code review. Verify each finding against the current code and only fix it if needed.

In `packages/rs-platform-wallet-ffi/src/core_wallet/transaction_builder.rs`:
- [BLOCKING] packages/rs-platform-wallet-ffi/src/core_wallet/transaction_builder.rs:282-318: set_funding silently empties the builder on any lookup failure
  `take_builder()` is called at line 282, before the fallible wallet/account lookups inside the async block (`get_wallet_and_info_mut`, the account-type match, `managed_account_mut` — lines 286-300). Because `take_builder` is `std::mem::take`, it swaps `TransactionBuilder::default()` into the FFI handle's inner slot and hands the live builder to the async closure. If any lookup fails, the closure returns `Err`, the outer `match funded { Err(e) => ... }` never calls `store_builder`, and `taken` is dropped. The caller's `FFITransactionBuilder` is left holding an empty default — every prior `add_output`, `set_change_address`, `set_fee_rate`, `set_special_payload` call is silently discarded. The PR's own SendViewModel pattern is `addOutput × N → setFunding → buildSigned` on a single builder, so a transient setFunding error (e.g. wrong account index, wallet momentarily unavailable) that the Swift caller catches and retries will silently produce a transaction with zero outputs. Restore the taken builder on the error branch (or defer `take_builder` until after every fallible lookup has succeeded).
- [BLOCKING] packages/rs-platform-wallet-ffi/src/core_wallet/transaction_builder.rs:353-386: add_inputs_from_outpoints has the same state-loss defect as set_funding
  Same shape as the set_funding finding: `let taken = (*builder).take_builder();` at line 353 runs before the async wallet lookup and the per-outpoint `managed.utxos.get(op)` checks (lines 356-372). If the wallet isn't found, the managed account isn't found, or any single requested outpoint isn't in the account's UTXO set (an explicitly-documented error path per the doc comment at lines 321-324), the closure returns `Err` and the outer error arm returns without calling `store_builder`, leaving the FFI handle pointing at an empty default builder. A single bad outpoint wipes every previously-configured output/change/fee/funding rather than leaving the builder untouched or reporting only the failed input-selection step. Defer `take_builder` until after the fallible lookups, or restore the taken builder on the error branch.
- [SUGGESTION] packages/rs-platform-wallet-ffi/src/core_wallet/transaction_builder.rs:389-397: build_signed docstring claims it frees the builder — it does not
  The doc says "This function also frees the builder". The implementation only calls `take_builder()` (line 417), which leaves a defaulted `TransactionBuilder` in place via `mem::take`; neither the outer `Box<FFITransactionBuilder>` (line 139) nor the inner `Box<TransactionBuilder>` (line 138) is freed. `core_wallet_tx_builder_destroy` is still required. The Swift wrapper is safe because `CoreTransactionBuilder.deinit` unconditionally calls destroy (CoreTransactionBuilder.swift:72), but any other C-ABI consumer that trusts the comment literally will leak the outer + inner boxes on every send (or attempt a double-free if they then also call destroy on what they think is a freed handle). Correct the doc to say the internal state is consumed but the handle must still be released via `core_wallet_tx_builder_destroy`.
- [SUGGESTION] packages/rs-platform-wallet-ffi/src/core_wallet/transaction_builder.rs:144-181: add_output / set_change_address accept cross-network addresses via assume_checked()
  Both entry points parse the caller-supplied address as `DashAddress::from_str(addr_str)?.assume_checked()`, which bypasses the network-tag check on the `NetworkUnchecked` result. Because `add_output` and `set_change_address` run before a wallet handle is attached, there is no network context to validate against at those call sites — but `set_funding` and `build_signed` both hold a wallet handle whose `network()` is known. A testnet address handed to a mainnet builder (or vice versa) currently passes through and gets embedded verbatim into the output script, with no user-visible error. Defer network validation to `set_funding`/`build_signed` where `wallet.network()` is available and reject addresses whose network does not match (`require_network`), or at minimum document that the caller owns network validation.
- [SUGGESTION] packages/rs-platform-wallet-ffi/src/core_wallet/transaction_builder.rs:337-339: check_ptr!(outpoints) rejects a legitimate zero-length call
  `check_ptr!(outpoints)` unconditionally errors if the pointer is null, even when `outpoints_len == 0`. Swift's `Array.withUnsafeBufferPointer` on an empty array commonly yields a nil `baseAddress`, so a legitimate "add zero extra inputs" call would deterministically fail with `ErrorNullPointer` even though `slice::from_raw_parts` would never dereference the pointer for a zero-length slice. The removed `core_wallet_send_to_addresses` handled this correctly with a `count > 0` guard on the pointer checks. Guard the null check on `outpoints_len > 0`.
- [SUGGESTION] packages/rs-platform-wallet-ffi/src/core_wallet/transaction_builder.rs:419-435: build_signed acquires a write lock but only reads state
  The async block in `core_wallet_tx_builder_build_signed` opens `wallet_manager().write().await` and uses `get_wallet_info_mut`, but only performs immutable reads (`last_processed_height`, `managed_account(&info.core_wallet.accounts, ...)`) — nothing under the write lock is actually mutated. Holding the write lock through the synchronous mnemonic-resolver round trip serializes unrelated read paths behind every sign step. Prefer `read().await` + `get_wallet_info` here.

Comment thread packages/rs-platform-wallet-ffi/src/core_wallet/transaction_builder.rs Outdated
Comment thread packages/rs-platform-wallet-ffi/src/core_wallet/transaction_builder.rs Outdated
Comment thread packages/rs-platform-wallet-ffi/src/core_wallet/transaction_builder.rs Outdated
llbartekll
llbartekll previously approved these changes Jul 2, 2026

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 4

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@packages/rs-platform-wallet-ffi/src/core_wallet/transaction_builder.rs`:
- Around line 147-151: The FFI transaction builder currently trusts the
caller-provided network and can validate or sign against a wallet from a
different network. Update `core_wallet_tx_builder_set_funding` and
`core_wallet_tx_builder_build_signed` in `transaction_builder.rs` to compare
`(*builder).network` with `wallet.network()` and reject mismatches before
proceeding. Use the existing `FFITransactionBuilder`, `set_funding`, and
`build_signed` paths as the enforcement points, and keep the `build_signed`
check as a backstop even if funding was never set.

In `@packages/rs-platform-wallet/src/wallet/core/broadcast.rs`:
- Around line 7-10: The `broadcast_transaction` doc comment is outdated because
it still describes building and signing via `TransactionBuilder`, while the
method now only broadcasts an already signed `Transaction`. Update the docs in
`broadcast_transaction` to state that it broadcasts the caller-provided signed
transaction and remove the misleading `TransactionBuilder` reference, keeping
the summary focused on broadcast-only behavior.

In
`@packages/swift-sdk/Sources/SwiftDashSDK/PlatformWallet/CoreWallet/CoreTransactionBuilder.swift`:
- Around line 82-175: The public mutators on CoreTransactionBuilder can still be
called after buildSigned has consumed and freed the underlying Rust builder,
which leaves later FFI calls using an invalid handle. Add the same lifecycle
check used by buildSigned, such as ensureUsable, at the start of every remaining
mutator including setFunding, addInputs, addOutput, setChangeAddress,
setFeeRate, setSelectionStrategy, setCurrentHeight, and setSpecialPayload so
each method fails fast once consumed is true.

In
`@packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Core/ViewModels/SendViewModel.swift`:
- Around line 462-479: The send flow in SendViewModel is doing synchronous
CoreTransactionBuilder build/sign/broadcast work on the MainActor, which can
block the UI. Move the transaction creation and core.broadcastTransaction path
out of the `@MainActor` context by performing the CoreTransactionBuilder
operations and broadcast on a background task/queue, then only return to the
MainActor for UI state updates. Keep the logic around builder.addOutput,
setFunding, buildSigned, and core.broadcastTransaction intact, but ensure that
the expensive FFI calls are not executed on the main thread.
🪄 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: defaults

Review profile: CHILL

Plan: Pro

Run ID: 648827b8-f4f3-43a4-aa78-02dee85d632a

📥 Commits

Reviewing files that changed from the base of the PR and between 4341d07 and 5e3862d.

⛔ Files ignored due to path filters (1)
  • Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (14)
  • Cargo.toml
  • packages/rs-dpp/src/state_transition/mod.rs
  • packages/rs-dpp/src/state_transition/state_transitions/address_funds/address_funding_from_asset_lock_transition/signing_tests.rs
  • packages/rs-dpp/src/state_transition/state_transitions/shielded/shield_from_asset_lock_transition/signing_tests.rs
  • packages/rs-platform-wallet-ffi/src/core_wallet/addresses.rs
  • packages/rs-platform-wallet-ffi/src/core_wallet/broadcast.rs
  • packages/rs-platform-wallet-ffi/src/core_wallet/mod.rs
  • packages/rs-platform-wallet-ffi/src/core_wallet/transaction_builder.rs
  • packages/rs-platform-wallet/src/wallet/core/broadcast.rs
  • packages/rs-platform-wallet/src/wallet/core/wallet.rs
  • packages/rs-sdk-ffi/src/mnemonic_resolver_core_signer.rs
  • packages/swift-sdk/Sources/SwiftDashSDK/PlatformWallet/CoreWallet/CoreTransactionBuilder.swift
  • packages/swift-sdk/Sources/SwiftDashSDK/PlatformWallet/CoreWallet/ManagedCoreWallet.swift
  • packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Core/ViewModels/SendViewModel.swift

Comment thread packages/rs-platform-wallet/src/wallet/core/broadcast.rs

@thepastaclaw thepastaclaw left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Code Review

Incremental review of 5e3862d vs b6969a7: the delta cleanly resolves 8 of 9 prior findings on transaction_builder.rs (state-loss in set_funding/add_inputs_from_outpoints, cross-network address acceptance, build_signed docstring/free semantics, zero-length outpoints, write-lock-for-read path, transaction_free double-free footgun, and the missing thread-safety doc). Only prior-9 (set_special_payload silently discards bincode trailing bytes) is unchanged and carried forward. Two new suggestion/nitpick observations in the delta: build_signed reclaims the builder before validating core_signer_handle/out_tx, and core_wallet_tx_builder_destroy nulls b.inner on a Box that immediately drops. No blocking issues.

🟡 1 suggestion(s) | 💬 2 nitpick(s)

Carried-forward findings already raised (1)

These findings were not re-posted as new inline comments because an existing review thread already covers them.

  • [NITPICK] (deduped existing open thread) packages/rs-platform-wallet-ffi/src/core_wallet/transaction_builder.rs:278-287: set_special_payload silently accepts trailing bytes after a valid bincode payload — Carried forward from prior-9, unchanged in this delta. bincode::decode_from_slice returns (value, bytes_consumed), but the code destructures as Ok((p, _)) and discards the count. A caller that hands over a buffer containing a valid TransactionPayload followed by any trailing bytes (framin...
🤖 Prompt for all review comments with AI agents
These findings are from an automated code review. Verify each finding against the current code and only fix it if needed.

In `packages/rs-platform-wallet-ffi/src/core_wallet/transaction_builder.rs`:
- [SUGGESTION] packages/rs-platform-wallet-ffi/src/core_wallet/transaction_builder.rs:439-446: build_signed reclaims the builder before validating core_signer_handle / out_tx
  The current order is: `check_ptr!(builder)` → `Box::from_raw(builder)` and `Box::from_raw(ffi.inner)` → `check_ptr!(core_signer_handle)` → `check_ptr!(out_tx)`. If a caller passes a valid `builder` but a null `core_signer_handle` or null `out_tx`, the builder is silently freed before the FFI returns `ErrorNullPointer`. Memory is correct (Rust drops the reclaimed boxes on early return), but the caller loses their in-progress builder with no way to retry and the behavior is asymmetric versus the usual FFI convention ("error return → pointer is untouched"). The Swift wrapper compensates because `CoreTransactionBuilder.buildSigned` sets `consumed = true` unconditionally, but any non-Swift consumer that trusts the convention would double-free. Moving the two additional pointer checks above the `Box::from_raw` calls preserves the "always consumed on success/build failure" contract while surfacing programmer errors non-destructively.

Comment thread packages/rs-platform-wallet-ffi/src/core_wallet/transaction_builder.rs Outdated
@ZocoLini ZocoLini force-pushed the feat/core-tx-builder-ffi branch 2 times, most recently from d55453f to 909484a Compare July 2, 2026 14:39

@thepastaclaw thepastaclaw left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Code Review

Incremental review of PR #3970 at d55453f. The latest delta (5e3862d..d55453f) is a mechanical follow-up to an upstream key-wallet trait split: extended_public_key is dropped from four Signer impls (three rs-dpp test doubles and rs-sdk-ffi's MnemonicResolverCoreSigner), plus rust-dashcore/key-wallet Cargo pins bumped. No new findings in this delta. packages/rs-platform-wallet-ffi/src/core_wallet/transaction_builder.rs is byte-identical to the prior head, so all three prior nitpick/suggestion findings on that file are carried forward at the same line ranges. All eight previously-fixed findings remain fixed. No blockers.

🟡 1 suggestion(s) | 💬 2 nitpick(s)

Carried-forward findings already raised (3)

These findings were not re-posted as new inline comments because an existing review thread already covers them.

  • [NITPICK] (deduped existing open thread) packages/rs-platform-wallet-ffi/src/core_wallet/transaction_builder.rs:278-287: set_special_payload silently accepts trailing bytes after a valid bincode payload — Carried forward from the prior review — transaction_builder.rs is unchanged in this delta. bincode::decode_from_slice(bytes, config::standard()) returns (payload, bytes_consumed), but line 280 binds the second element to _ and discards it. If a Swift caller passes a buffer containing a we...
  • [SUGGESTION] (deduped existing open thread) packages/rs-platform-wallet-ffi/src/core_wallet/transaction_builder.rs:439-446: build_signed reclaims the builder before validating core_signer_handle / out_tx — Carried forward from the prior review — file unchanged. check_ptr!(builder) passes, then both heap boxes are unconditionally reclaimed via Box::from_raw (442-443) before check_ptr!(core_signer_handle) and check_ptr!(out_tx) (445-446). This is intentional per the inline comment and matches...
  • [NITPICK] (deduped existing open thread) packages/rs-platform-wallet-ffi/src/core_wallet/transaction_builder.rs:503-506: core_wallet_tx_builder_destroy nulls b.inner right before b is dropped — Carried forward from the prior review — file unchanged. b is a local Box<FFITransactionBuilder> reclaimed from the raw pointer and dropped (freeing the outer allocation) at the end of the function scope. The write b.inner = std::ptr::null_mut(); on line 506 mutates a field nothing subsequen...
🤖 Prompt for all review comments with AI agents
These findings are from an automated code review. Verify each finding against the current code and only fix it if needed.

In `packages/rs-platform-wallet-ffi/src/core_wallet/transaction_builder.rs`:
- [SUGGESTION] packages/rs-platform-wallet-ffi/src/core_wallet/transaction_builder.rs:439-446: build_signed reclaims the builder before validating core_signer_handle / out_tx
  Carried forward from the prior review — file unchanged. `check_ptr!(builder)` passes, then both heap boxes are unconditionally reclaimed via `Box::from_raw` (442-443) before `check_ptr!(core_signer_handle)` and `check_ptr!(out_tx)` (445-446). This is intentional per the inline comment and matches the Swift wrapper's unconditional `consumed = true` after the FFI call, so the shipped consumer is memory-safe. However, the asymmetry (null `builder` returns error and leaves it untouched; null `core_signer_handle`/`out_tx` returns error but has already silently freed the builder) violates the usual FFI convention of "error return implies inputs untouched" and is not documented in the `# Safety` block. Any other binding built against that convention would double-free by calling `core_wallet_tx_builder_destroy` afterward. Moving the two null-checks above the reclaim would restore convention without breaking the Swift wrapper (which sets `consumed = true` regardless). If keeping the current ordering, extend the `# Safety` comment to state the builder is always consumed once `builder` itself is non-null.

@thepastaclaw thepastaclaw left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Code Review

Incremental review reconciled against prior findings. The latest delta (d55453f..909484a) is a single 3-line cleanup in core_wallet_tx_builder_destroy removing the redundant b.inner = null_mut() write, which exactly closes prior-3. Two prior findings remain unaddressed and are carried forward: prior-1 (set_special_payload silently accepts trailing bytes after bincode decode) and prior-2 (build_signed reclaims/frees the builder before validating core_signer_handle/out_tx). No new defects were introduced by this delta.

🟡 1 suggestion(s) | 💬 1 nitpick(s)

Carried-forward findings already raised (2)

These findings were not re-posted as new inline comments because an existing review thread already covers them.

  • [NITPICK] (deduped existing open thread) packages/rs-platform-wallet-ffi/src/core_wallet/transaction_builder.rs:277-287: set_special_payload silently accepts trailing bytes after a valid bincode payload — Carried forward from the prior review (prior-1); the latest delta did not touch this hunk. bincode::decode_from_slice(bytes, config::standard()) returns (payload, bytes_consumed), but line 280 destructures as Ok((p, _)) and discards the consumed count. If a caller passes a buffer whose firs...
  • [SUGGESTION] (deduped existing open thread) packages/rs-platform-wallet-ffi/src/core_wallet/transaction_builder.rs:439-446: build_signed reclaims the builder before validating core_signer_handle / out_tx — Carried forward from the prior review (prior-2); the latest delta did not touch this block. check_ptr!(builder) passes, then both heap boxes (FFITransactionBuilder and the inner TransactionBuilder) are unconditionally reclaimed via Box::from_raw (442-443) before `check_ptr!(core_signer_ha...
🤖 Prompt for all review comments with AI agents
These findings are from an automated code review. Verify each finding against the current code and only fix it if needed.

In `packages/rs-platform-wallet-ffi/src/core_wallet/transaction_builder.rs`:
- [SUGGESTION] packages/rs-platform-wallet-ffi/src/core_wallet/transaction_builder.rs:439-446: build_signed reclaims the builder before validating core_signer_handle / out_tx
  Carried forward from the prior review (prior-2); the latest delta did not touch this block. `check_ptr!(builder)` passes, then both heap boxes (`FFITransactionBuilder` and the inner `TransactionBuilder`) are unconditionally reclaimed via `Box::from_raw` (442-443) before `check_ptr!(core_signer_handle)` and `check_ptr!(out_tx)` (445-446). This is memory-safe as shipped (the reclaimed values simply drop on the early-return path) and matches the Swift wrapper's unconditional `consumed = true` after the call, but the asymmetry — null `builder` returns error and leaves it untouched, while null `core_signer_handle`/`out_tx` returns error yet silently frees the builder — breaks the usual FFI convention of "error return implies inputs untouched" and is not documented in the `# Safety` block. A future non-Swift binding built to that convention would call `core_wallet_tx_builder_destroy` after the error and double-free. Reordering the null checks before the two `Box::from_raw` calls keeps the `build_signed`-consumes-builder invariant on the success path while giving callers a recoverable error on trivial argument-validation failures.

self.wallet_id
}

pub fn wallet_manager(&self) -> &Arc<RwLock<WalletManager<PlatformWalletInfo>>> {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Why do we need this getter?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

It is being used in:

pub unsafe extern "C" fn core_wallet_tx_builder_set_funding(...)

shumkov
shumkov previously approved these changes Jul 4, 2026
@ZocoLini ZocoLini force-pushed the feat/core-tx-builder-ffi branch from 909484a to 65075b2 Compare July 6, 2026 08:57

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@packages/rs-platform-wallet-ffi/src/core_wallet/broadcast.rs`:
- Around line 19-21: The # Safety documentation for the broadcast FFI entrypoint
is garbled and incomplete; update the safety clause in the core_wallet broadcast
function to clearly state the full caller contract for handle, tx, and out_txid.
Use the broadcast symbol’s existing doc comment near the FFI wrapper to replace
the broken sentence with a complete, precise requirement that matches the actual
invariants callers must satisfy.
🪄 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: defaults

Review profile: CHILL

Plan: Pro

Run ID: 7926ec09-3572-40d5-b079-28a2ece63772

📥 Commits

Reviewing files that changed from the base of the PR and between 5e3862d and 65075b2.

📒 Files selected for processing (9)
  • packages/rs-platform-wallet-ffi/src/core_wallet/addresses.rs
  • packages/rs-platform-wallet-ffi/src/core_wallet/broadcast.rs
  • packages/rs-platform-wallet-ffi/src/core_wallet/mod.rs
  • packages/rs-platform-wallet-ffi/src/core_wallet/transaction_builder.rs
  • packages/rs-platform-wallet/src/wallet/core/broadcast.rs
  • packages/rs-platform-wallet/src/wallet/core/wallet.rs
  • packages/swift-sdk/Sources/SwiftDashSDK/PlatformWallet/CoreWallet/CoreTransactionBuilder.swift
  • packages/swift-sdk/Sources/SwiftDashSDK/PlatformWallet/CoreWallet/ManagedCoreWallet.swift
  • packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Core/ViewModels/SendViewModel.swift
🚧 Files skipped from review as they are similar to previous changes (7)
  • packages/rs-platform-wallet-ffi/src/core_wallet/mod.rs
  • packages/rs-platform-wallet-ffi/src/core_wallet/addresses.rs
  • packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Core/ViewModels/SendViewModel.swift
  • packages/rs-platform-wallet/src/wallet/core/wallet.rs
  • packages/swift-sdk/Sources/SwiftDashSDK/PlatformWallet/CoreWallet/ManagedCoreWallet.swift
  • packages/swift-sdk/Sources/SwiftDashSDK/PlatformWallet/CoreWallet/CoreTransactionBuilder.swift
  • packages/rs-platform-wallet-ffi/src/core_wallet/transaction_builder.rs

Comment thread packages/rs-platform-wallet-ffi/src/core_wallet/broadcast.rs Outdated
@ZocoLini ZocoLini force-pushed the feat/core-tx-builder-ffi branch 4 times, most recently from 5b0cff2 to 9458b7a Compare July 6, 2026 09:19
@ZocoLini ZocoLini changed the title refactor(platform-wallet): expose the new core TransactionBuilder API + rust-dashcore bump refactor(platform-wallet): expose the new core TransactionBuilder API Jul 6, 2026

@thepastaclaw thepastaclaw left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Code Review

Source: reviewers claude/opus + codex/gpt-5.5 for general, rust-quality, and ffi-engineer; verifier claude/opus; policy gate review_policy.enforce_backport_prereq_policy applied.

Cumulative review at 9458b7a7, including the required latest delta 65075b23..9458b7a7. The latest delta is broad because the branch absorbed current base changes, but the focused current PR surface remains the core TransactionBuilder FFI/Swift flow.

Result: 1 blocking, 1 suggestion, 2 nitpicks.

Prior finding reconciliation:

  • prior-1: STILL VALID. set_special_payload still decodes with Ok((p, _)) and discards the bincode consumed-byte count at transaction_builder.rs:293-303.
  • prior-2: STILL VALID. build_signed is unchanged from 65075b23 at transaction_builder.rs:455-462 and still reclaims the builder before validating core_signer_handle / out_tx; the comment documents intent but does not remove the public C-ABI ownership asymmetry.

Carried-forward prior findings:

  • [SUGGESTION] transaction_builder.rs:455-462: build_signed consumes the builder before validating signer/output pointers. A null signer or out pointer returns an FFI error after the builder has been consumed, unlike a null builder error. The Swift wrapper handles this today, but the public C ABI remains surprising for future non-Swift callers.
  • [NITPICK] transaction_builder.rs:293-303: set_special_payload accepts trailing bytes after a valid bincode payload. Checking consumed == bytes.len() would catch caller-side framing mistakes at the FFI boundary.

New latest-delta/current-head findings:

  • [BLOCKING] transaction_builder.rs:317-364: split set_funding / build_signed breaks key-wallet's documented atomic read-then-reserve invariant. set_funding snapshots spendable UTXOs and releases the wallet lock before assemble_unsigned later reserves the selected inputs, so two FFI callers can observe the same UTXO as free and build conflicting transactions. The atomic send path keeps those steps under one wallet write lock; the split C ABI cannot.
  • [NITPICK] transaction_builder.rs:266-277: core_wallet_tx_builder_set_current_height is public, but set_funding and build_signed both overwrite the value with the wallet's processed height. Document that the wallet height wins or remove the redundant setter.
Prompt for all review comments with AI agents
These findings are from an automated code review. Verify each finding against the current code and only fix it if needed.

In `packages/rs-platform-wallet-ffi/src/core_wallet/transaction_builder.rs`:
- [BLOCKING] lines 317-364: Split set_funding/build_signed FFI breaks key-wallet's documented atomic read-then-reserve invariant.
- [SUGGESTION] lines 455-462: build_signed reclaims the builder before validating signer/output pointers. (carried forward)
- [NITPICK] lines 266-277: core_wallet_tx_builder_set_current_height is silently overridden by set_funding and build_signed.
- [NITPICK] lines 293-303: set_special_payload silently accepts trailing bytes after a valid bincode payload. (carried forward)

llbartekll
llbartekll previously approved these changes Jul 6, 2026
shumkov
shumkov previously approved these changes Jul 6, 2026
@ZocoLini ZocoLini dismissed stale reviews from shumkov and llbartekll via 38cc69c July 6, 2026 13:21
@ZocoLini ZocoLini force-pushed the feat/core-tx-builder-ffi branch from 9458b7a to 38cc69c Compare July 6, 2026 13:21

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🧹 Nitpick comments (1)
packages/rs-platform-wallet-ffi/src/core_wallet/addresses.rs (1)

63-64: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Safety docs omit the account_type invariant.

CoreAccountTypeFFI is a #[repr(C)] enum crossing the FFI boundary; passing an out-of-range discriminant from the caller is undefined behavior before account_type.into() even runs. The # Safety section only documents handle. Consider documenting that account_type must be a valid enum value (matching the sibling functions' convention).

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/rs-platform-wallet-ffi/src/core_wallet/addresses.rs` around lines 63
- 64, The safety docs for the address FFI helper currently only mention
`handle`, but the `account_type` argument also crosses the FFI boundary and must
be a valid `CoreAccountTypeFFI` value. Update the `# Safety` section in the
`addresses.rs` helper to explicitly state that `account_type` must be a valid
enum discriminant, matching the convention used by the sibling address
functions, so the contract is clear before `account_type.into()` is used.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@packages/rs-platform-wallet-ffi/src/core_wallet/addresses.rs`:
- Around line 63-64: The safety docs for the address FFI helper currently only
mention `handle`, but the `account_type` argument also crosses the FFI boundary
and must be a valid `CoreAccountTypeFFI` value. Update the `# Safety` section in
the `addresses.rs` helper to explicitly state that `account_type` must be a
valid enum discriminant, matching the convention used by the sibling address
functions, so the contract is clear before `account_type.into()` is used.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 7b490f70-14f5-4ebc-a728-c3837ebc9fde

📥 Commits

Reviewing files that changed from the base of the PR and between 65075b2 and 38cc69c.

📒 Files selected for processing (12)
  • packages/rs-platform-wallet-ffi/src/core_wallet/addresses.rs
  • packages/rs-platform-wallet-ffi/src/core_wallet/broadcast.rs
  • packages/rs-platform-wallet-ffi/src/core_wallet/mod.rs
  • packages/rs-platform-wallet-ffi/src/core_wallet/transaction_builder.rs
  • packages/rs-platform-wallet/src/wallet/core/broadcast.rs
  • packages/rs-platform-wallet/src/wallet/core/wallet.rs
  • packages/swift-sdk/Sources/SwiftDashSDK/PlatformWallet/CoreWallet/CoreTransactionBuilder.swift
  • packages/swift-sdk/Sources/SwiftDashSDK/PlatformWallet/CoreWallet/ManagedCoreWallet.swift
  • packages/swift-sdk/Sources/SwiftDashSDK/PlatformWallet/ManagedPlatformWallet.swift
  • packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Core/ViewModels/SendViewModel.swift
  • packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Core/Views/SendTransactionView.swift
  • packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Views/WithdrawPlatformAddressView.swift
💤 Files with no reviewable changes (6)
  • packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Views/WithdrawPlatformAddressView.swift
  • packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Core/ViewModels/SendViewModel.swift
  • packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Core/Views/SendTransactionView.swift
  • packages/swift-sdk/Sources/SwiftDashSDK/PlatformWallet/ManagedPlatformWallet.swift
  • packages/swift-sdk/Sources/SwiftDashSDK/PlatformWallet/CoreWallet/CoreTransactionBuilder.swift
  • packages/swift-sdk/Sources/SwiftDashSDK/PlatformWallet/CoreWallet/ManagedCoreWallet.swift
🚧 Files skipped from review as they are similar to previous changes (5)
  • packages/rs-platform-wallet-ffi/src/core_wallet/mod.rs
  • packages/rs-platform-wallet/src/wallet/core/wallet.rs
  • packages/rs-platform-wallet-ffi/src/core_wallet/broadcast.rs
  • packages/rs-platform-wallet/src/wallet/core/broadcast.rs
  • packages/rs-platform-wallet-ffi/src/core_wallet/transaction_builder.rs

@ZocoLini ZocoLini force-pushed the feat/core-tx-builder-ffi branch from 38cc69c to 3ec2ace Compare July 6, 2026 13:40

@thepastaclaw thepastaclaw left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Code Review

Source: reviewers = claude ffi-engineer opus; claude general opus; codex ffi-engineer gpt-5.5; codex general gpt-5.5; verifier = claude opus; policy gate = review_policy.enforce_backport_prereq_policy.

Incremental + cumulative review for 3ec2ace1, including git diff 38cc69cb..3ec2ace1. The latest push is dominated by merged base-branch address-sync/height-pin work; the TransactionBuilder/broadcast surface from this PR is unchanged from 38cc69cb.

Prior finding reconciliation:

  • prior-1: STILL VALID. CoinJoin funding still attaches account reservations, while CoinJoin broadcast still goes through the plain non-releasing path.
  • prior-2: STILL VALID. core_wallet_tx_builder_add_inputs_from_outpoints still takes both a wallet and builder but lacks the builder-vs-wallet network guard used by set_funding and build_signed.

Carried-forward prior findings:

  • [SUGGESTION] packages/rs-platform-wallet-ffi/src/core_wallet/transaction_builder.rs:72-85: CoinJoin-funded builds reserve UTXOs but broadcast never releases them on rejection.
  • [NITPICK] packages/rs-platform-wallet-ffi/src/core_wallet/transaction_builder.rs:416-476: add_inputs_from_outpoints skips the wallet-vs-builder network check its sibling FFIs perform.

New findings in latest delta:

  • None.

Result: 1 suggestion, 1 nitpick. No blockers.

Prompt for all review comments with AI agents
These findings are from an automated code review. Verify each finding against the current code and only fix it if needed.

In `packages/rs-platform-wallet-ffi/src/core_wallet/transaction_builder.rs`:
- [SUGGESTION] packages/rs-platform-wallet-ffi/src/core_wallet/transaction_builder.rs:72-85: CoinJoin-funded builds reserve UTXOs but broadcast never releases them on rejection
  `CoreAccountTypeFFI::CoinJoin` funds via `core_wallet_tx_builder_set_funding`, which resolves the CoinJoin account through `managed_account_mut` (BIP44/BIP32/CoinJoin all share `ManagedCoreFundsAccount`) and calls `TransactionBuilder::set_funding(managed, account)` regardless of variant. That path attaches the account's `ReservationSet`, and `assemble_unsigned` inside `build_signed` reserves the selected inputs the same way it does for standard accounts. However, `as_standard_account_type` maps `CoinJoin` to `None`, and the `None` arm of `core_wallet_broadcast_transaction` invokes plain `broadcast_transaction`, never `broadcast_transaction_releasing_reservation`. On a definitive broadcast rejection for a CoinJoin-funded transaction, the reservation therefore stays held until the TTL backstop, blocking immediate retries. Either wire CoinJoin through reservation-releasing broadcast reconciliation or make `set_funding` skip attaching reservations for the CoinJoin variant.

- [NITPICK] packages/rs-platform-wallet-ffi/src/core_wallet/transaction_builder.rs:416-476: `add_inputs_from_outpoints` skips the wallet-vs-builder network check its sibling FFIs perform
  `core_wallet_tx_builder_set_funding` and `core_wallet_tx_builder_build_signed` both reject a builder whose `FFINetwork` does not match `wallet.network()`. `core_wallet_tx_builder_add_inputs_from_outpoints` also takes both the builder and a wallet handle but performs no such check before walking the account UTXO map and mutating the builder. `build_signed` re-catches the mismatch, so this cannot produce a wrong-network signed transaction, but one of the three wallet-aware FFI entry points silently mutates a foreign-network builder while the other two fail fast. Align the three entry points for consistency.

@ZocoLini ZocoLini force-pushed the feat/core-tx-builder-ffi branch from 3ec2ace to 4c3dd04 Compare July 6, 2026 14:03

@thepastaclaw thepastaclaw left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Code Review

Source: reviewers = claude ffi-engineer opus; claude general opus; claude rust-quality opus; codex ffi-engineer gpt-5.5; codex general gpt-5.5; codex rust-quality gpt-5.5; verifier = claude opus; specialists = rust-quality, ffi-engineer; policy gate = review_policy.enforce_backport_prereq_policy.

Prior reconciliation: prior-1 and prior-2 both remain STILL VALID at head 4c3dd04. Carried-forward findings: 2 (CoinJoin reservations not released on broadcast rejection; add_inputs_from_outpoints skips network guard). New findings in latest delta: none — the delta is a clean refactor migrating broadcast/address APIs from ManagedPlatformWallet to ManagedCoreWallet with matched Swift call-site updates and no new defects introduced.

Prior finding reconciliation:

  • prior-1: STILL VALID. transaction_builder.rs is byte-identical between 3ec2ace and 4c3dd04. broadcast.rs line 48 still routes the None arm (CoinJoin via as_standard_account_type at line 82) through plain wallet.broadcast_transaction(&tx), while set_funding (line 393) still attaches the CoinJoin managed account's ReservationSet uniformly with BIP44/BIP32.
  • prior-2: STILL VALID. core_wallet_tx_builder_set_funding (lines 358-365) and core_wallet_tx_builder_build_signed (lines 509-517) both enforce builder_network == wallet.network(); core_wallet_tx_builder_add_inputs_from_outpoints (lines 416-476) still resolves the wallet handle and mutates the builder without the equivalent guard.

Carried-forward prior findings:

  • [SUGGESTION] packages/rs-platform-wallet-ffi/src/core_wallet/transaction_builder.rs:72-85: CoinJoin-funded builds reserve UTXOs but broadcast never releases them on rejection
  • [NITPICK] packages/rs-platform-wallet-ffi/src/core_wallet/transaction_builder.rs:416-476: add_inputs_from_outpoints skips the wallet-vs-builder network check its sibling FFIs perform

New findings in latest delta:

  • None.

🟡 1 suggestion(s) | 💬 1 nitpick(s)

🤖 Prompt for all review comments with AI agents
These findings are from an automated code review. Verify each finding against the current code and only fix it if needed.

In `packages/rs-platform-wallet-ffi/src/core_wallet/transaction_builder.rs`:
- [SUGGESTION] packages/rs-platform-wallet-ffi/src/core_wallet/transaction_builder.rs:72-85: CoinJoin-funded builds reserve UTXOs but broadcast never releases them on rejection
  The CoinJoin reserve/release contract is asymmetric across the FFI. `core_wallet_tx_builder_set_funding` routes all three account variants through `managed_account_mut`, which returns a `ManagedCoreFundsAccount` for CoinJoin identically to BIP44/BIP32, and `TransactionBuilder::set_funding(managed, account)` attaches that account's `ReservationSet` regardless of variant — so `build_signed` reserves the selected CoinJoin inputs. However, `as_standard_account_type` maps `CoinJoin` to `None` (line 82), and the `None` arm of `core_wallet_broadcast_transaction` (broadcast.rs:48) calls plain `wallet.broadcast_transaction(&tx)` instead of `broadcast_transaction_releasing_reservation`. Consequence: on a definitive broadcast rejection for a CoinJoin-funded transaction, Swift sees the failure and can retry, but Rust keeps the CoinJoin inputs reserved until the TTL backstop clears them, blocking that retry from reselecting those UTXOs. The doc comment at lines 73-77 (and broadcast.rs:16-17) frames CoinJoin as having no reservation to reconcile, which contradicts `set_funding`'s actual behavior. Fix by either wiring the CoinJoin arm through `broadcast_transaction_releasing_reservation` (extending reconciliation to CoinJoin) or by making `set_funding` skip attaching reservations for the CoinJoin variant so the reserve/release story stays symmetric.

@ZocoLini ZocoLini force-pushed the feat/core-tx-builder-ffi branch from 4c3dd04 to 2bf1dff Compare July 6, 2026 14:25
@ZocoLini ZocoLini force-pushed the feat/core-tx-builder-ffi branch from 2bf1dff to 3161856 Compare July 6, 2026 14:42

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🧹 Nitpick comments (1)
packages/rs-platform-wallet/src/wallet/core/wallet.rs (1)

69-119: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Duplicated account-type dispatch logic.

The two match account_type { BIP44 => ..., BIP32 => ..., CoinJoin => ... } blocks (xpub lookup and managed-account lookup) repeat the same dispatch pattern already implemented as standalone helpers (managed_account/managed_account_mut) in packages/rs-platform-wallet-ffi/src/core_wallet/transaction_builder.rs. Consider extracting a small private helper here (and/or moving the dispatch into key_wallet so both crates can share it) to avoid re-deriving the same match arms whenever a new account type is added.

♻️ Example helper extraction
fn managed_account_mut<'a>(
    accounts: &'a mut ManagedAccountCollection,
    account_type: AccountTypePreference,
    account_index: u32,
) -> Option<&'a mut ManagedCoreFundsAccount> {
    match account_type {
        AccountTypePreference::BIP44 => accounts.standard_bip44_accounts.get_mut(&account_index),
        AccountTypePreference::BIP32 => accounts.standard_bip32_accounts.get_mut(&account_index),
        AccountTypePreference::CoinJoin => accounts.coinjoin_accounts.get_mut(&account_index),
    }
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/rs-platform-wallet/src/wallet/core/wallet.rs` around lines 69 - 119,
The `set_gap_limit` method repeats the same `AccountTypePreference` dispatch
twice for xpub and managed-account lookup, which should be centralized. Extract
a small private helper in `wallet::core::wallet::set_gap_limit` (or share the
existing `managed_account`/`managed_account_mut` pattern from
`transaction_builder.rs`) so both lookups use one dispatch path, keeping the
account-type matching in one place for easier maintenance when new account types
are added.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@packages/rs-platform-wallet/src/wallet/core/wallet.rs`:
- Around line 69-119: The `set_gap_limit` method repeats the same
`AccountTypePreference` dispatch twice for xpub and managed-account lookup,
which should be centralized. Extract a small private helper in
`wallet::core::wallet::set_gap_limit` (or share the existing
`managed_account`/`managed_account_mut` pattern from `transaction_builder.rs`)
so both lookups use one dispatch path, keeping the account-type matching in one
place for easier maintenance when new account types are added.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: c1d36b68-9b69-4cae-9ace-b16dbda3fb40

📥 Commits

Reviewing files that changed from the base of the PR and between 38cc69c and 3161856.

📒 Files selected for processing (10)
  • packages/rs-platform-wallet-ffi/src/core_wallet/addresses.rs
  • packages/rs-platform-wallet-ffi/src/core_wallet/broadcast.rs
  • packages/rs-platform-wallet-ffi/src/core_wallet/mod.rs
  • packages/rs-platform-wallet-ffi/src/core_wallet/transaction_builder.rs
  • packages/rs-platform-wallet/src/wallet/core/broadcast.rs
  • packages/rs-platform-wallet/src/wallet/core/wallet.rs
  • packages/swift-sdk/Sources/SwiftDashSDK/PlatformWallet/CoreWallet/CoreTransactionBuilder.swift
  • packages/swift-sdk/Sources/SwiftDashSDK/PlatformWallet/CoreWallet/ManagedCoreWallet.swift
  • packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Core/ViewModels/SendViewModel.swift
  • packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Core/Views/SendTransactionView.swift
🚧 Files skipped from review as they are similar to previous changes (7)
  • packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Core/ViewModels/SendViewModel.swift
  • packages/rs-platform-wallet-ffi/src/core_wallet/mod.rs
  • packages/rs-platform-wallet-ffi/src/core_wallet/broadcast.rs
  • packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Core/Views/SendTransactionView.swift
  • packages/swift-sdk/Sources/SwiftDashSDK/PlatformWallet/CoreWallet/CoreTransactionBuilder.swift
  • packages/rs-platform-wallet-ffi/src/core_wallet/transaction_builder.rs
  • packages/rs-platform-wallet/src/wallet/core/broadcast.rs

@thepastaclaw thepastaclaw left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Code Review

Source: reviewers = claude general/rust-quality/ffi-engineer opus; codex general/rust-quality/ffi-engineer gpt-5.5; verifier = claude opus; specialists = rust-quality, ffi-engineer; policy gate = review_policy.enforce_backport_prereq_policy.

Incremental + cumulative review for 31618568, including the required latest delta git diff 2bf1dff7..31618568. That delta only touches packages/rs-platform-wallet-ffi/src/core_wallet/transaction_builder.rs: it documents the intentional CoinJoin reservation behavior and adds the missing builder-vs-wallet network guard.

Prior finding reconciliation:

  • prior-1: FIXED. core_wallet_tx_builder_add_inputs_from_outpoints now rejects builder/wallet network mismatches at transaction_builder.rs:430-443, matching set_funding and build_signed.

Carried-forward prior findings:

  • None.

New findings in latest delta:

  • None.

Cumulative current-head findings:

  • [BLOCKING] packages/rs-platform-wallet-ffi/src/core_wallet/transaction_builder.rs:421-493: Manual-input builds bypass UTXO reservations.
  • [BLOCKING] packages/rs-platform-wallet-ffi/src/core_wallet/transaction_builder.rs:55-117: Validate FFI enum raw values before matching.
  • [BLOCKING] packages/swift-sdk/Sources/SwiftDashSDK/PlatformWallet/CoreWallet/CoreTransactionBuilder.swift:96-189: Consumed builder can be reused through setters, passing a dangling Rust pointer.
  • [SUGGESTION] packages/rs-platform-wallet-ffi/src/core_wallet/transaction_builder.rs:170-598: Add FFI-level tests for the new transaction builder surface.
  • [NITPICK] packages/rs-platform-wallet-ffi/src/dashpay.rs:527: Rustdoc link points to removed send API.

Result: 3 blocking, 1 suggestion, 1 nitpick.

Prompt for all review comments with AI agents
These findings are from an automated code review. Verify each finding against the current code and only fix it if needed.

In `packages/rs-platform-wallet-ffi/src/core_wallet/transaction_builder.rs`:
- [BLOCKING] packages/rs-platform-wallet-ffi/src/core_wallet/transaction_builder.rs:421-493: Manual-input builds bypass UTXO reservations
  `core_wallet_tx_builder_add_inputs_from_outpoints` validates that each outpoint belongs to the account, then calls key-wallet's `TransactionBuilder::add_inputs`, which only appends UTXOs. In key-wallet at rev 647fa98, `set_funding` is the only entry point that populates `self.reservations`; `assemble_unsigned` only reserves the selected inputs when `self.reservations` is `Some`. A builder funded solely through this new FFI/Swift `addInputs` path therefore returns a signed transaction without reserving those inputs, so a second build can immediately re-select the same UTXOs before broadcast, and the broadcast release-on-rejection path has nothing to release. This defeats the reservation guarantees this PR is explicitly adding for build-then-broadcast flows. Either populate the reservation set here (via the account's `ReservationSet`) so `assemble_unsigned` reserves the chosen inputs, or document that this entry point is only valid after `set_funding` and refuse to consume the builder otherwise.
- [BLOCKING] packages/rs-platform-wallet-ffi/src/core_wallet/transaction_builder.rs:55-117: Validate FFI enum raw values before matching
  `CoreAccountTypeFFI` and `CoreSelectionStrategyFFI` are new `#[repr(C)]` Rust enums accepted directly as parameters by exported C ABI functions (`core_wallet_tx_builder_set_selection_strategy`, `set_funding`, `add_inputs_from_outpoints`, `build_signed`, plus the neighboring broadcast/gap-limit FFIs). A foreign caller can pass any integer in an enum slot; an out-of-range discriminant is UB before `From`/`match` ever runs, not a recoverable `ErrorInvalidParameter`. This crate already documents and uses the safer pattern for exactly this reason — see `wallet_restore_types.rs:37-45` and `AccountTypeTagFFI::try_from_u8` — where raw `u8`/integer tags are carried across FFI and validated before matching. The new builder API should follow the same shape: take a raw integer parameter, validate with `try_from_*`, and return `ErrorInvalidParameter` on unknown values.
- [SUGGESTION] packages/rs-platform-wallet-ffi/src/core_wallet/transaction_builder.rs:170-598: Add FFI-level tests for the new transaction builder surface
  The new `core_wallet_tx_builder_*` ABI surface uses manual `Box::from_raw` ownership transfer, the `take_builder`/`store_builder` in-place mutation pattern, pointer validation via `check_ptr!`, and the `FFICoreTransaction` free path. The new tests in `rs-platform-wallet/src/wallet/core/broadcast.rs` exercise the Rust wallet reservation behavior directly but bypass the FFI layer where most of the new Rust quality risk lives. A small FFI integration test driving create → configure → fund → build_signed → free, plus error paths (invalid/null pointers, network mismatch, double-build), would catch regressions in the ABI/lifetime code that ordinary wallet tests cannot.

In `packages/swift-sdk/Sources/SwiftDashSDK/PlatformWallet/CoreWallet/CoreTransactionBuilder.swift`:
- [BLOCKING] packages/swift-sdk/Sources/SwiftDashSDK/PlatformWallet/CoreWallet/CoreTransactionBuilder.swift:96-189: Consumed builder can be reused through setters, passing a dangling Rust pointer
  `core_wallet_tx_builder_build_signed` at packages/rs-platform-wallet-ffi/src/core_wallet/transaction_builder.rs:517-518 consumes and frees both the `FFITransactionBuilder` and inner `TransactionBuilder` via `Box::from_raw`, and Swift sets `consumed = true` at line 216 before checking the result. However, only `buildSigned` guards on `consumed` (line 199). `setFunding`, `addInputs`, `addOutput`, `setChangeAddress`, `setFeeRate`, `setSelectionStrategy`, `setCurrentHeight`, and `setSpecialPayload` all continue to call their `core_wallet_tx_builder_*` counterparts with the same `handle` unconditionally. After any `buildSigned` — successful or throwing — a caller can hit any of these methods and pass a non-null but freed pointer into Rust. `check_ptr!` only rejects null, so Rust will dereference freed memory, causing UB or a crash. Guard every public mutating method on `consumed` before calling into Rust (throw the same 'already consumed' error).

In `packages/rs-platform-wallet-ffi/src/dashpay.rs`:
- [NITPICK] packages/rs-platform-wallet-ffi/src/dashpay.rs:527: Rustdoc link points to removed send API
  This intra-doc link still points at `crate::core_wallet::core_wallet_send_to_addresses`, but this PR removes that FFI in favor of the transaction-builder flow (grep confirms the only remaining reference is this comment). `cargo doc` will now warn about a broken link, and readers are pointed to an API that no longer exists.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants