Skip to content

feat(key-wallet): fund an asset lock from a caller-chosen list of accounts - #944

Merged
QuantumExplorer merged 8 commits into
devfrom
feat/asset-lock-pooled-funding-caller-sources
Aug 10, 2026
Merged

feat(key-wallet): fund an asset lock from a caller-chosen list of accounts#944
QuantumExplorer merged 8 commits into
devfrom
feat/asset-lock-pooled-funding-caller-sources

Conversation

@QuantumExplorer

@QuantumExplorer QuantumExplorer commented Aug 10, 2026

Copy link
Copy Markdown
Member

Supersedes #935 — same change, recreated on a dashpay/rust-dashcore branch rather than a personal fork, with the FFI's funding policy handed back to the caller (see "The FFI does not choose the accounts" below). Original commit and authorship preserved (@bfoss765).

Issue being fixed or feature implemented

An asset lock could only ever be funded from ONE account (AssetLockFundingAccount: a BIP44 index, or a CoinJoin index for the drain flow). A wallet holding its balance across the standard families and its DashPay contact-receiving accounts had to sweep them into BIP44 first and lock out of that — an extra on-chain hop, an extra fee, and a transparent address reused for the privilege.

The send path stopped needing that sweep in dashpay/platform#4329. This is the same change for asset locks, so an invitation / identity registration / top-up funds from the union of the wallet's accounts in one transaction.

What was done?

Both build_asset_lock and build_asset_lock_with_signer now take a list of AccountTypePreference sources plus a source_index, and fold them through the same transaction_building::fund the send path uses:

  • coin selection draws from the union of the resolved accounts' UTXOs;
  • the first source supplies the change address;
  • overlapping sources fund each account once — fix(key-wallet): never offer an outpoint the builder already holds #931's duplicate-outpoint dedup in add_funding is what makes the repeated call safe;
  • derivation paths are collected per contributing account, since the inputs no longer share one resolver.

Reservation bookkeeping is the part that had to change shape, and it is where I would look hardest in review. A pooled build reserves in each contributing account's own ReservationSet under the one owner token. The post-build failure paths — credit-key derivation on the soft-wallet builder, the peek → sign → commit loop on the signer builder, both of which run after the transaction is already signed — therefore release across every funded account instead of just the one. Releasing a single account's set would have stranded the remaining inputs until the 24-block TTL sweep, with the caller holding no token to free them (it never received one on the error path). Release stays owner-guarded throughout (release_if_owner, platform#4185).

AssetLockResult gains funding_accounts, so a caller's rejected-broadcast release can reach every account holding a share of the reservation. It is the contributor list — accounts that actually supplied an input — not everything the source list offered: selection routinely takes nothing from most offered accounts, and a list naming every DashPay contact would make the caller's release and bookkeeping scale with the address book while claiming contributions that never happened.

fund's strictness rule now matches platform's finalize_transaction: a single named source is strict (a caller asking for exactly one account's funds must not silently be given another's), a pooled list skips the sources this wallet has nothing for — no BIP32 account, no contacts — and errors only when none of them funds anything. Without that, a pooled set would fail on the very wallets it is meant to serve. This also fixes the send path, where a multi-source list previously required every named account to exist (platform reimplemented the fold to work around exactly that).

The FFI does not choose the accounts

This is what changed from #935, which had key-wallet-ffi keep its C signature and quietly hardcode a [BIP44, BIP32, AllDashpayReceivingFunds] default internally. Which accounts a lock may spend from is a client library policy — it depends on what the client is funding, what it shows the user, and whether contact funds are in scope — and it is not something the FFI layer can know.

So wallet_build_and_sign_asset_lock_transaction takes the source list as a parameter:

bool wallet_build_and_sign_asset_lock_transaction(
    const FFIWalletManager *manager,
    const FFIWallet *wallet,
    const FFIAccountTypePreference *funding_sources,
    size_t funding_sources_count,
    uint32_t account_index,
    /* … unchanged … */);

FFIAccountTypePreference is a #[repr(C)] tag (FFIAccountTypePreferenceKind) plus the two 32-byte identity IDs the DashPay kinds read, so the full AccountTypePreference vocabulary crosses the boundary — including naming one specific contact's receiving account, which a hardcoded set could never express.

  • A client wanting today's behavior passes a single BIP44 source and gets exactly the pre-pooling result.
  • A client wanting the whole spendable balance passes the wider list.
  • funding_sources_count == 0 is rejected with InvalidInput rather than falling through to AccountTypePreference::DEFAULT — defaulting there would be this layer picking the policy again, just less visibly.

The first source supplies the change address, so the caller also decides where change lands.

CoinJoin is unchanged and stays out of the pool

CoinJoin funding remains drain-only, and it must now be the sole source: spending mixed outputs alongside transparent ones in one transaction links them and undoes the mixing — the same reasoning that keeps CoinJoin out of AccountTypePreference::DEFAULT. The AssetLockFundingAccount::CoinJoin + drain: true flow that dashpay/platform#4327 ships on converts to a single-element source list and behaves exactly as before (test_drain_coinjoin_asset_lock is untouched and passing).

Breaking changes

  • ManagedWalletInfo::build_asset_lock / build_asset_lock_with_signer take funding_sources: &[AccountTypePreference], source_index: u32 in place of funding_account: AssetLockFundingAccount. AssetLockFundingAccount remains as the drain flows' single-account vocabulary, with impl From<AssetLockFundingAccount> for AccountTypePreference for the conversion.
  • AssetLockResult gains funding_accounts: Vec<AccountType>.
  • AssetLockError::AccountNotFound(u32) is removed: account resolution is the builder's now, and it reports BuilderError::AccountNotFound naming the source that failed.
  • A multi-source sources list is lenient rather than strict (single-source and empty-list behavior unchanged).
  • C ABI: wallet_build_and_sign_asset_lock_transaction gains funding_sources / funding_sources_count ahead of account_index. Callers must pass at least one source; [BIP44] reproduces the previous behavior exactly.

How Has This Been Tested?

cargo test -p key-wallet -p key-wallet-ffi — 637 + 235 passed, 0 failed. cargo clippy -p key-wallet -p key-wallet-ffi --all-targets clean; cargo fmt --check clean. FFI_API.md regenerated via scripts/generate_ffi_docs.py.

New tests, weighted toward the reservation failure paths:

  • pooled_asset_lock_spans_the_standard_accounts — neither account covers the lock alone, so the build only succeeds by pooling; asserts every input is signed (proving paths were collected across accounts), change returns to the first source, each account reserves its own contribution, and funding_accounts names both.
  • credit_key_failure_releases_the_reservation_in_every_pooled_account and its signer_ sibling — force a failure in the window where the transaction is built, signed and reserved but the caller holds no token, and assert both pooled accounts came out with nothing reserved.
  • signing_failure_releases_the_reservation_in_every_pooled_account — same invariant one layer down, in the builder's own signer-failure release.
  • pooled_sources_skip_what_the_wallet_does_not_have, pooled_sources_that_resolve_to_nothing_are_an_error — the leniency rule and its floor.
  • coinjoin_cannot_be_pooled_with_transparent_sources (drain and exact-amount) — rejected before any wallet state is touched, with nothing reserved.
  • a_pooled_list_skips_absent_sources_where_a_single_one_is_strict — pins the send path's strictness rule on both sides.
  • every_funding_source_kind_maps_to_its_own_preference, dashpay_sources_carry_the_identity_ids_they_name — the new FFI conversion. A transposed arm there would fund from an account the caller never named and the build would still succeed, so nothing downstream would catch it.

Breaking Changes

See "Breaking changes" above.

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 made corresponding changes to the documentation

Summary by CodeRabbit

  • New Features

    • Asset-lock transactions can be funded from multiple ordered account sources.
    • Supported sources include standard, hardware, CoinJoin, and DashPay accounts.
    • Funds can be pooled across available sources, with change returned to the first source.
    • Transaction results identify contributing funding accounts.
  • Bug Fixes

    • Unavailable sources are skipped when multiple sources are provided.
    • Single-source requests retain strict validation.
    • CoinJoin funding is restricted to supported drain transactions.
    • Improved cleanup prevents reservations from remaining after failures.
  • Documentation

    • Updated API documentation with funding-source parameters and validation requirements.

…ounts

An asset lock could only ever be funded from ONE account. A wallet holding
its balance across the standard families and its DashPay contact-receiving
accounts had to sweep them into BIP44 first and lock out of that — an extra
on-chain hop, an extra fee, and a transparent address reused for the privilege.
The send path stopped needing that in dashpay/platform#4329; this is the same
change for asset locks.

Both builders now take a LIST of `AccountTypePreference` sources plus a
`source_index` instead of a single `AssetLockFundingAccount`, and fold them
through the same `transaction_building::fund` the send path uses: coin
selection draws from the union, the first source supplies the change address,
overlapping sources fund each account once (#931's dedup is what makes the
repeated `add_funding` safe), and derivation paths are collected across every
contributing account so the inputs can be signed.

Which accounts to pool is the CALLER's decision, not this library's. The FFI
entry point takes the source list as a parameter (`FFIAccountTypePreference`,
a tag plus the two identity IDs the DashPay kinds read) rather than applying a
default of its own: a client wanting today's behavior passes a single `BIP44`
source, one wanting the whole spendable balance passes the wider list, and one
funding out of a specific contact names that friendship. An empty list is
rejected instead of falling through to `AccountTypePreference::DEFAULT`, since
defaulting there would be this layer choosing a funding policy that only the
client library knows.

Reservation bookkeeping is the part that had to change shape. A pooled build
reserves in EACH contributing account's own set under the one owner token, so
the post-build failure paths — credit-key derivation on the soft-wallet
builder, the peek/sign/commit loop on the signer builder, both running after
the transaction is already signed — now release across every funded account
instead of just the one. Releasing a single account's set would have stranded
the rest of the inputs until the 24-block TTL sweep. `AssetLockResult` carries
the contributing accounts so the caller's rejected-broadcast release can reach
them all; it is the contributor list, not everything the sources offered, so a
wallet's address book does not inflate the caller's bookkeeping.

`fund`'s strictness rule now matches platform's: a SINGLE named source is
strict (a caller asking for exactly one account's funds must not silently be
given another's), while a pooled list skips the sources this wallet has nothing
for — no BIP32 account, no contacts — and errors only when none of them funds
anything. Without that, a pooled set would fail on the very wallets it is meant
to serve.

CoinJoin funding is unchanged and stays excluded from pooling: it remains
drain-only, and it must now be the sole source, because spending mixed outputs
alongside transparent ones in one transaction links them and undoes the mixing.
The `AssetLockFundingAccount::CoinJoin` + `drain: true` flow that
dashpay/platform#4327 ships on converts to a single-element source list and
behaves exactly as before.

`AssetLockError::AccountNotFound(u32)` is removed — account resolution is now
the builder's, and it reports `BuilderError::AccountNotFound` with the source
that failed. `AssetLockFundingAccount` remains as the drain flows' single-account
vocabulary, with a `From` conversion into the source list.

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Aug 10, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Warning

Review limit reached

@QuantumExplorer, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 11 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: f14535a2-61fa-4e34-ab86-c2fd65c5c835

📥 Commits

Reviewing files that changed from the base of the PR and between 6ab303e and c88f5d1.

📒 Files selected for processing (1)
  • key-wallet-ffi/tests/test_asset_lock_funding_sources.rs
📝 Walkthrough

Walkthrough

The asset-lock transaction API now accepts ordered funding-source preferences. Funding can pool UTXOs across available sources, route change to the first source, track contributors, and clean up reservations for soft-wallet and external-signer flows.

Changes

Asset-lock pooled funding

Layer / File(s) Summary
FFI funding-source contract
key-wallet-ffi/FFI_API.md, key-wallet-ffi/src/transaction.rs, key-wallet-ffi/tests/test_asset_lock_funding_sources.rs
The FFI adds funding-source types, identity fields, validation, conversion, documentation, and the updated asset-lock function signature. Tests cover source validation, conversion, and CoinJoin rejection.
Pooled funding selection
key-wallet/src/wallet/managed_wallet_info/transaction_building.rs
Funding returns PooledFunding. Single-source requests remain strict, while pooled requests skip unavailable sources and record funded accounts.
Asset-lock builders and reservation handling
key-wallet/src/wallet/managed_wallet_info/asset_lock_builder.rs
Soft-wallet and signer builders use pooled sources, enforce drain and CoinJoin rules, report contributing accounts, route change to the first source, and release reservations across failure paths. Tests cover these behaviors.

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

Sequence Diagram(s)

sequenceDiagram
  participant FFI
  participant ManagedWalletInfo
  participant TransactionFunding
  participant AssetLockBuilder
  FFI->>ManagedWalletInfo: Pass ordered funding sources
  ManagedWalletInfo->>TransactionFunding: Resolve pooled funding
  TransactionFunding-->>ManagedWalletInfo: Return builder, paths, and accounts
  ManagedWalletInfo->>AssetLockBuilder: Build and sign asset-lock transaction
  AssetLockBuilder-->>ManagedWalletInfo: Return contributors and reservation outcome
Loading

Possibly related PRs

Suggested labels: ready-for-review

Suggested reviewers: zocolini

🚥 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 clearly and concisely describes the main change: allowing asset-lock funding from a caller-selected list of accounts.
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/asset-lock-pooled-funding-caller-sources

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

@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: 2

🧹 Nitpick comments (5)
key-wallet/src/wallet/managed_wallet_info/transaction_building.rs (1)

281-286: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Strictness is derived from list length, so a caller cannot request strict pooling.

strict = preferences.len() == 1 couples the leniency policy to the number of sources. A caller that pools two sources and requires both to exist has no way to express that: an absent account is silently skipped. A caller that passes [BIP44, BIP44] also loses strictness for the one account it named.

The current rule is documented at Lines 267-273 and matches the PR intent, so this is not a defect. If a strict pooled request becomes necessary later, consider a separate flag or a small enum instead of inferring policy from len().

🤖 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 `@key-wallet/src/wallet/managed_wallet_info/transaction_building.rs` around
lines 281 - 286, Preserve the existing strictness behavior in the
transaction-building logic: keep strict derived from preferences.len() == 1 and
do not add a separate strictness flag or enum. No code change is required for
this comment.
key-wallet/src/wallet/managed_wallet_info/asset_lock_builder.rs (3)

474-518: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Extract the shared funding-and-reservation prologue.

Lines 361-405 and Lines 477-518 are now near-identical: builder construction with require_final_inputs, the drain strategy switch, the PooledFunding destructure, the ReservationSet capture over offered, and the reserved / release_reservations pair. The two copies differ only in comment wording.

The blocks that follow do differ — one passes wallet to build_signed_reserved and the other passes signer, and the tails build different AssetLockCreditKeys variants. A helper that returns the funded builder, the paths, the offered accounts, and the captured Vec<ReservationSet> would remove the duplication without touching either signing call.

This matters for the reservation logic specifically: a future fix applied to one copy and not the other reintroduces the stranding bug this PR closes.

🤖 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 `@key-wallet/src/wallet/managed_wallet_info/asset_lock_builder.rs` around lines
474 - 518, Extract the duplicated funding and reservation setup from the two
asset-lock build paths into a shared helper. Have it construct the builder with
require_final_inputs and the drain strategy, call fund, capture paths, offered
accounts, and each account’s ReservationSet, and return those values for the
existing signing flows; keep the wallet versus signer arguments and distinct
AssetLockCreditKeys tail logic unchanged.

300-310: 🚀 Performance & Scalability | 🔵 Trivial | 💤 Low value

Iterate the inputs, not each account's whole UTXO map.

The current filter scans every UTXO of every offered account. A transaction has few inputs, and an offered account can hold many UTXOs. Testing the inputs against each account's map inverts the loop and keeps the cost proportional to the input count.

ManagedCoreFundsAccount::utxos is a BTreeMap, so contains_key is a log-time lookup on a small map. The spent set is then unnecessary.

♻️ Proposed refactor
 fn contributing_accounts(
     accounts: &crate::account::ManagedAccountCollection,
     offered: &[AccountType],
     transaction: &Transaction,
 ) -> Vec<AccountType> {
-    let spent: HashSet<OutPoint> =
-        transaction.input.iter().map(|input| input.previous_output).collect();
     offered
         .iter()
         .copied()
         .filter(|account_type| {
             accounts.funds_account(account_type).is_some_and(|account| {
-                account.utxos.keys().any(|outpoint| spent.contains(outpoint))
+                transaction
+                    .input
+                    .iter()
+                    .any(|input| account.utxos.contains_key(&input.previous_output))
             })
         })
         .collect()
 }
🤖 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 `@key-wallet/src/wallet/managed_wallet_info/asset_lock_builder.rs` around lines
300 - 310, Update the account filtering logic around the spent-output collection
to iterate over transaction.input and check each input’s previous_output
directly against the offered account’s utxos using contains_key. Remove the
spent HashSet and preserve the existing account-type filtering and collection
behavior.

89-100: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Note that this conversion drops account_index.

Both arms discard account_index. AccountTypePreference carries no index, so the builders take it as the separate source_index argument. A caller that follows the instruction at Line 59 ("Convert with AccountTypePreference::from to hand one to a builder") and passes only the converted preference funds source_index 0, not the account it named.

Add a short doc comment on this From impl stating that the caller must also pass AssetLockFundingAccount::account_index as source_index. That keeps the two halves of the conversion together.

📝 Proposed doc addition
+/// The source family this account names.
+///
+/// The index does not survive the conversion: [`AccountTypePreference`] names a
+/// family only. Pass [`AssetLockFundingAccount::account_index`] as the
+/// builder's `source_index` alongside the converted preference.
 impl From<AssetLockFundingAccount> for AccountTypePreference {
     fn from(account: AssetLockFundingAccount) -> Self {
🤖 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 `@key-wallet/src/wallet/managed_wallet_info/asset_lock_builder.rs` around lines
89 - 100, Add a concise doc comment to the From<AssetLockFundingAccount> for
AccountTypePreference implementation stating that conversion preserves only the
account type and callers must separately pass
AssetLockFundingAccount::account_index as the builder’s source_index.
key-wallet-ffi/src/transaction.rs (1)

789-811: 🔒 Security & Privacy | 🔵 Trivial | ⚡ Quick win

Document the valid kind discriminants.

Add a # Safety requirement that every funding_sources[i].kind is one of FFIAccountTypePreferenceKind values 0..=5. If invalid values must return FFIErrorCode::InvalidInput, validate an integer-valued FFI representation before creating a Rust reference and use a fallible conversion.

🤖 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 `@key-wallet-ffi/src/transaction.rs` around lines 789 - 811, Document and
enforce valid kind discriminants in the FFI conversion surrounding
From<FFIAccountTypePreference> for AccountTypePreference: require each
funding_sources[i].kind to be within 0..=5, validate the raw integer before
constructing any Rust reference, and use a fallible conversion that returns
FFIErrorCode::InvalidInput for invalid values instead of relying on an
infallible match.

Source: Coding guidelines

🤖 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 `@key-wallet-ffi/FFI_API.md`:
- Around line 1308-1315: Add dedicated type-reference documentation in
FFI_API.md for FFIAccountTypePreference and FFIAccountTypePreferenceKind,
alongside the supporting function documentation or in a shared Type Reference
section. Document the struct’s C layout and field order, enum discriminant
values including BIP44 = 0, BIP32 = 1, and CoinJoin = 2 plus all remaining
variants, and specify which identity fields each kind uses.

In `@key-wallet-ffi/src/transaction.rs`:
- Around line 813-836: Add a note to the asset-lock function’s # Parameters
documentation stating that this entry point is non-drain and rejects
FFIAccountTypePreferenceKind::CoinJoin, including when it is the sole funding
source; callers must use the drain-mode entry point for CoinJoin funding. Do not
change the implementation.

---

Nitpick comments:
In `@key-wallet-ffi/src/transaction.rs`:
- Around line 789-811: Document and enforce valid kind discriminants in the FFI
conversion surrounding From<FFIAccountTypePreference> for AccountTypePreference:
require each funding_sources[i].kind to be within 0..=5, validate the raw
integer before constructing any Rust reference, and use a fallible conversion
that returns FFIErrorCode::InvalidInput for invalid values instead of relying on
an infallible match.

In `@key-wallet/src/wallet/managed_wallet_info/asset_lock_builder.rs`:
- Around line 474-518: Extract the duplicated funding and reservation setup from
the two asset-lock build paths into a shared helper. Have it construct the
builder with require_final_inputs and the drain strategy, call fund, capture
paths, offered accounts, and each account’s ReservationSet, and return those
values for the existing signing flows; keep the wallet versus signer arguments
and distinct AssetLockCreditKeys tail logic unchanged.
- Around line 300-310: Update the account filtering logic around the
spent-output collection to iterate over transaction.input and check each input’s
previous_output directly against the offered account’s utxos using contains_key.
Remove the spent HashSet and preserve the existing account-type filtering and
collection behavior.
- Around line 89-100: Add a concise doc comment to the
From<AssetLockFundingAccount> for AccountTypePreference implementation stating
that conversion preserves only the account type and callers must separately pass
AssetLockFundingAccount::account_index as the builder’s source_index.

In `@key-wallet/src/wallet/managed_wallet_info/transaction_building.rs`:
- Around line 281-286: Preserve the existing strictness behavior in the
transaction-building logic: keep strict derived from preferences.len() == 1 and
do not add a separate strictness flag or enum. No code change is required for
this comment.
🪄 Autofix

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 Plus

Run ID: 28105d79-a318-436a-ae7b-9f1719add8ec

📥 Commits

Reviewing files that changed from the base of the PR and between d91ad05 and 33fa5b8.

📒 Files selected for processing (4)
  • key-wallet-ffi/FFI_API.md
  • key-wallet-ffi/src/transaction.rs
  • key-wallet/src/wallet/managed_wallet_info/asset_lock_builder.rs
  • key-wallet/src/wallet/managed_wallet_info/transaction_building.rs

Comment thread key-wallet-ffi/FFI_API.md Outdated
Comment thread key-wallet-ffi/src/transaction.rs
@codecov

codecov Bot commented Aug 10, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 95.98394% with 20 lines in your changes missing coverage. Please review.
✅ Project coverage is 75.38%. Comparing base (d91ad05) to head (c88f5d1).

Files with missing lines Patch % Lines
key-wallet-ffi/src/transaction.rs 78.43% 11 Missing ⚠️
...c/wallet/managed_wallet_info/asset_lock_builder.rs 97.69% 9 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##              dev     #944      +/-   ##
==========================================
+ Coverage   75.17%   75.38%   +0.20%     
==========================================
  Files         328      328              
  Lines       78194    78541     +347     
==========================================
+ Hits        58783    59208     +425     
+ Misses      19411    19333      -78     
Flag Coverage Δ
core 77.29% <ø> (ø)
ffi 49.04% <78.43%> (+0.45%) ⬆️
rpc 20.00% <ø> (ø)
spv 91.39% <ø> (+0.07%) ⬆️
wallet 77.37% <97.98%> (+0.48%) ⬆️
Files with missing lines Coverage Δ
...wallet/managed_wallet_info/transaction_building.rs 95.50% <100.00%> (+1.97%) ⬆️
...c/wallet/managed_wallet_info/asset_lock_builder.rs 93.82% <97.69%> (+4.20%) ⬆️
key-wallet-ffi/src/transaction.rs 12.21% <78.43%> (+12.21%) ⬆️

... and 8 files with indirect coverage changes

QuantumExplorer and others added 2 commits August 11, 2026 00:29
`validate_funding_sources` is a private free function, so linking to it from
a public doc comment fails `rustdoc::private_intra_doc_links` under the
Documentation job's `-D warnings`. The link was also useless to a reader of
the public docs, who cannot follow it — state the CoinJoin rule inline instead.

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
…t lock

`wallet_build_and_sign_asset_lock_transaction` always builds with
`drain: false`, and `validate_funding_sources` rejects a CoinJoin source
outside drain mode — so a caller selecting that kind here always gets
`InvalidData`, even passing it alone. The kind's own doc described the
builder's rule ("sole source, drain only") without saying this entry point
never drains, which reads as though the sole-source form would work.

Raised by CodeRabbit on #944.

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
@QuantumExplorer

Copy link
Copy Markdown
Member Author

Both CodeRabbit points addressed or answered:

CoinJoin can never succeed through this entry point — correct, and fixed in f0d2cbf. wallet_build_and_sign_asset_lock_transaction always builds with drain: false, so validate_funding_sources rejects a CoinJoin source even when it is the sole one. The variant's doc described the builder's rule ("sole source, drain only") without saying this entry point never drains, which reads as though the sole-source form would work here. Both the variant doc and the funding_sources parameter doc now say so outright.

Type reference for FFIAccountTypePreference / FFIAccountTypePreferenceKind in FFI_API.md — not in this PR. FFI_API.md is generated by scripts/generate_ffi_docs.py, which only emits function entries; no #[repr(C)] type is documented there today, including FFIAssetLockFundingType right next to this change. Adding a type-reference section is a change to the generator that would affect all 261 entries, so it belongs in its own PR rather than riding along here. The variant values and field semantics are documented on the Rust items, and cbindgen emits them into the generated C header.

…ologue

`build_asset_lock` and `build_asset_lock_with_signer` had drifted into two
near-identical copies of the same prologue: payload assembly, the drain
strategy switch, `fund`, the per-account `ReservationSet` capture, and the
release closure. They differed only in which signer reached
`build_signed_reserved` and in comment wording.

That duplication is a hazard on this specific logic. The reservation capture
has to happen between funding and signing — a pooled build reserves in each
contributing account's own set, and both builders reach their release paths
after the transaction is already signed, with the caller holding no token. A
fix applied to one copy and not the other silently reintroduces stranded
inputs on the path that was missed.

`build_signed_asset_lock` is now the single prologue, generic over
`TransactionSigner` so the soft-wallet builder passes `wallet` and the signer
builder passes `signer`. `BuildReservations` owns the sets, the reserved
outpoints and the token, so releasing is one call that cannot reach only some
of the funded accounts.

Also from review:

- `contributing_accounts` iterates the transaction's inputs rather than every
  UTXO of every offered account. A transaction has few inputs; an offered
  account can hold many UTXOs.
- `From<AssetLockFundingAccount> for AccountTypePreference` documents that the
  index does not survive. The type's own doc said "convert to hand one to a
  builder", and a caller doing only that funds source_index 0 rather than the
  account it named.
- The FFI entry point documents that `funding_sources[i].kind` must be a
  declared discriminant, since reading any other value as the enum is UB and
  so cannot be rejected at the boundary.

Raised by CodeRabbit on #944.

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
coderabbitai[bot]
coderabbitai Bot previously approved these changes Aug 10, 2026
…urces

The new boundary had no test of its own: the entry point's marshalling body is
where the funding policy now lives, and nothing exercised it.

Three cases, driven against a created-but-unfunded wallet so an accepted call
fails in coin selection rather than at the guards — which is what tells the
two apart:

- an empty list is rejected with `InvalidInput` instead of being forwarded,
  where it would mean `AccountTypePreference::DEFAULT` and reinstate a policy
  this layer must not choose;
- a well-formed pooled list gets past the guards into the build;
- `CoinJoin` is rejected, pinning the behavior documented in f0d2cbf — this
  entry point always builds non-drain, so mixed funds can never back it.

Prompted by the patch-coverage report on #944.

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
@QuantumExplorer

Copy link
Copy Markdown
Member Author

Worked through the review nitpicks. Four applied in 5a19149, one declined.

Applied

  • Extract the shared funding-and-reservation prologue. This was the one worth doing — the point that a future fix applied to one builder and not the other reintroduces the stranding bug is exactly right, and the reservation capture has to sit between funding and signing on both paths. build_signed_asset_lock is now the single prologue, generic over TransactionSigner so the soft-wallet builder passes wallet and the signer builder passes signer. BuildReservations owns the sets, the reserved outpoints and the token, so release is one call that cannot reach only some of the funded accounts. The four reservation-release tests pass unchanged, which is the point of keeping them.
  • Iterate the inputs, not each account's UTXO map in contributing_accounts — applied as proposed; the spent set is gone.
  • Note that the From conversion drops account_index. Applied, and I also fixed the type-level doc that caused it: it said "convert with AccountTypePreference::from to hand one to a builder", which is the instruction that silently funds index 0.
  • Document the valid kind discriminants. Added as a # Safety requirement. I did not add a fallible conversion: reading an out-of-range value as the enum is already UB at the point the slice is materialized, so validating after the fact would not make it sound, and an integer-tag representation would drop the named enum from the generated header and diverge from every other repr(C) enum in the crate (FFIAssetLockFundingType, FFIAccountKind, FFINetwork). Worth a crate-wide decision, not a one-type exception.

Declined

  • Strictness derived from list length. Agreed it is not a defect — it is the documented rule and it matches platform's finalize_transaction. A separate flag would be the right shape if a strict pooled request ever appears; adding it now would be API surface with no caller.

Also added key-wallet-ffi/tests/test_asset_lock_funding_sources.rs (c1b7ede) after the patch-coverage report: the entry point's marshalling body is where the funding policy now lives and had no test. Three cases against an unfunded wallet — empty list rejected, well-formed pooled list reaches the builder, CoinJoin rejected — the last of which pins the behavior documented in f0d2cbf.

coderabbitai[bot]
coderabbitai Bot previously approved these changes Aug 10, 2026
…ty note

The `# Safety` requirement added in 5a19149 changed the doc comment that
`scripts/generate_ffi_docs.py` extracts, and the generated file was not
refreshed alongside it — which the verify-ffi pre-commit hook catches.

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>

@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: 2

🤖 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 `@key-wallet-ffi/tests/test_asset_lock_funding_sources.rs`:
- Around line 1-8: Rename the pull request title to use the required semantic
prefix: “feat: configurable pooled asset-lock funding”.
- Line 53: Update call_with_sources to accept an FFINetwork parameter and pass
it to wallet_manager_create instead of hardcoding Testnet. Extend every FFI
contract test using this helper to execute each case for both
FFINetwork::Mainnet and FFINetwork::Testnet.
🪄 Autofix

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 Plus

Run ID: e8107887-4713-4de6-b83d-31f50425c9d0

📥 Commits

Reviewing files that changed from the base of the PR and between 33fa5b8 and f07f8d5.

📒 Files selected for processing (4)
  • key-wallet-ffi/FFI_API.md
  • key-wallet-ffi/src/transaction.rs
  • key-wallet-ffi/tests/test_asset_lock_funding_sources.rs
  • key-wallet/src/wallet/managed_wallet_info/asset_lock_builder.rs
🚧 Files skipped from review as they are similar to previous changes (3)
  • key-wallet-ffi/FFI_API.md
  • key-wallet-ffi/src/transaction.rs
  • key-wallet/src/wallet/managed_wallet_info/asset_lock_builder.rs

Comment thread key-wallet-ffi/tests/test_asset_lock_funding_sources.rs
Comment thread key-wallet-ffi/tests/test_asset_lock_funding_sources.rs Outdated
QuantumExplorer and others added 2 commits August 11, 2026 01:35
`wallet_manager_get_wallet` returns an independently boxed clone that the
caller owns — its own docs say to free it with `wallet_free_const` — and the
test dropped it, leaking a whole `Wallet` per case. The Address Sanitizer job
caught it: 42144 bytes in 24 allocations, three tests' worth.

Also frees `tx_bytes` defensively. Every case here fails before a transaction
is produced, so it is always null today, but a future success case would leak
it the same way.

Verified by reproducing the exact CI figure locally under
`-Zsanitizer=address` with `detect_leaks=1`, then confirming it goes away.

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
The helper hardcoded Testnet, so every FFI contract case here skipped Mainnet
— against the repo's standing rule to test both configurations. Account
derivation is coin-type-scoped, so a guard exercised on one network only could
hide a network-conditional path.

`call_with_sources` now takes the network and each case loops over both,
naming the network in its assertion messages so a one-sided failure says which.

Raised by CodeRabbit on #944.

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
@QuantumExplorer
QuantumExplorer merged commit 5a80bd7 into dev Aug 10, 2026
38 of 42 checks passed
@QuantumExplorer
QuantumExplorer deleted the feat/asset-lock-pooled-funding-caller-sources branch August 10, 2026 19:06
@QuantumExplorer

Copy link
Copy Markdown
Member Author

Reviewed

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.

2 participants