Skip to content

feat(key-wallet): CoinJoin funding account + drain mode for asset-lock builds - #915

Merged
xdustinface merged 2 commits into
devfrom
feat/coinjoin-asset-lock-funding
Jul 27, 2026
Merged

feat(key-wallet): CoinJoin funding account + drain mode for asset-lock builds#915
xdustinface merged 2 commits into
devfrom
feat/coinjoin-asset-lock-funding

Conversation

@QuantumExplorer

@QuantumExplorerQuantumExplorer commented Jul 23, 2026

Copy link
Copy Markdown
Member

What

The asset-lock builders (build_asset_lock / build_asset_lock_with_signer) previously hardcoded their funding source to standard BIP44 accounts. This adds:

  • AssetLockFundingAccount { Bip44, CoinJoin } — which account family supplies (and signs) the funding UTXOs. CoinJoin funding lets mixed coins fund an asset lock directly, without first sweeping them through a transparent BIP44 address (which links the mixed UTXOs to a reusable transparent address for an extra hop).
  • Drain mode — consume every final UTXO of the funding account and rewrite the single credit output's value to Σ inputs − fee. The TransactionBuilder's SelectionStrategy::All path now also rewrites the asset-lock payload credit output so the payload always mirrors the on-chain OP_RETURN burn (a mismatch is consensus-invalid), and a zero-value drain is rejected with InsufficientFunds.
  • CoinJoin funding is drain-only — partial spends would need CoinJoin change re-denomination, which the builder does not do; a non-drain CoinJoin build is rejected with InvalidData.

key-wallet-ffi's build_asset_lock entry point keeps its exact behavior (BIP44, non-drain).

Why

Consumed by dashpay/platform (AssetLockFunding::DrainAccountBalance + a shielded_fund_from_asset_lock_coinjoin_drain FFI) so the dashwallet-ios post-migration "move your mixed coins" flow can offer a Shielded destination that moves the whole CoinJoin balance into the shielded pool in one transaction, with no transparent intermediate hop.

Tests

  • test_drain_coinjoin_asset_lock — all CoinJoin UTXOs consumed, burn = Σ − fee, payload credit mirrors the burn, no change output, inputs signed.
  • test_drain_requires_single_credit_output, test_coinjoin_funding_requires_drain — guard rejections.
  • Full key-wallet (551) + key-wallet-manager (48) unit suites pass.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features
    • Asset-lock transactions can now be funded from either standard wallet accounts or CoinJoin accounts.
    • CoinJoin funding now supports drain-only transactions that consume all available inputs without creating change.
  • Bug Fixes
    • Drain transactions now validate configuration and ensure credit output values match the burned amount after fees.
    • Improved checks for insufficient funds and invalid drain setups (for example, incorrect credit-output counts and non-drain CoinJoin funding).

…k builds
Asset-lock builders (build_asset_lock / build_asset_lock_with_signer)
previously hardcoded the funding source to standard BIP44 accounts. This
adds:
- AssetLockFundingAccount { Bip44, CoinJoin }: which account family
supplies (and signs) the funding UTXOs. CoinJoin lets mixed coins fund
an asset lock directly, without first sweeping them through a
transparent BIP44 address.
- drain mode: consume every final UTXO of the funding account and
rewrite the single credit output's value to sum(inputs) - fee. The
TransactionBuilder's SelectionStrategy::All path now also rewrites the
asset-lock payload credit output so the payload always mirrors the
on-chain OP_RETURN burn (a mismatch is consensus-invalid), and rejects
a zero-value drain.
- CoinJoin funding is drain-only: partial spends would need CoinJoin
change re-denomination, which the builder does not do; a non-drain
CoinJoin build is rejected with InvalidData.
The key-wallet-ffi build_asset_lock entry point keeps its behavior
(BIP44, non-drain).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@coderabbitai

coderabbitaiBot commented Jul 23, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 69a9e1fc-f31f-4d79-a73a-bb4fd5ef02e8

📥 Commits

Reviewing files that changed from the base of the PR and between 1846079 and 82a7b0d.

📒 Files selected for processing (2)
  • key-wallet/src/wallet/managed_wallet_info/asset_lock_builder.rs
  • key-wallet/src/wallet/managed_wallet_info/transaction_builder.rs
🚧 Files skipped from review as they are similar to previous changes (2)
  • key-wallet/src/wallet/managed_wallet_info/transaction_builder.rs
  • key-wallet/src/wallet/managed_wallet_info/asset_lock_builder.rs

📝 Walkthrough

Walkthrough

Asset-lock construction now accepts BIP44 or CoinJoin funding accounts. CoinJoin funding is restricted to drain mode, drain transactions select all inputs, and asset-lock payload values are validated against the drained amount. FFI and tests use the updated API.

Changes

Asset-lock funding and drain flow

Layer / File(s)Summary
Funding-account API and routing
key-wallet/src/wallet/managed_wallet_info/asset_lock_builder.rs
Adds AssetLockFundingAccount, updates both builder APIs, resolves BIP44 or CoinJoin accounts, and applies shared drain and CoinJoin validation.
Asset-lock drain assembly
key-wallet/src/wallet/managed_wallet_info/transaction_builder.rs, key-wallet/src/wallet/managed_wallet_info/asset_lock_builder.rs
Drain assembly computes the drained value, validates output and payload cardinality, synchronizes the payload credit amount, and adds CoinJoin drain tests.
Call-site migration and regression coverage
key-wallet-ffi/src/transaction.rs, key-wallet/src/wallet/managed_wallet_info/asset_lock_builder.rs
Updates the FFI invocation and builder and signer tests to pass structured funding-account values while preserving existing assertions.

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

Sequence Diagram(s)

sequenceDiagram
participant FFI
participant ManagedWalletInfo
participant TransactionBuilder
FFI->>ManagedWalletInfo: build asset-lock transaction
ManagedWalletInfo->>TransactionBuilder: select BIP44 or CoinJoin funding inputs
TransactionBuilder-->>ManagedWalletInfo: assemble drained transaction
ManagedWalletInfo-->>FFI: serialize transaction output
Loading

Possibly related PRs

Suggested reviewers:llbartekll, xdustinface

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check nameStatusExplanation
Description Check✅ PassedCheck skipped - CodeRabbit’s high-level summary is enabled.
Title check✅ PassedThe title accurately summarizes the main change: CoinJoin funding support and drain mode for asset-lock builds.
Docstring Coverage✅ PassedNo functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check✅ PassedCheck skipped because no linked issues were found for this pull request.
Out of Scope Changes check✅ PassedCheck 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/coinjoin-asset-lock-funding

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.

@coderabbitaicoderabbitaiBot 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

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

271-289: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Duplicated drain/CoinJoin validation block.

The exact same 19-line validation (drain && credit_output_fundings.len() != 1 and CoinJoin && !drain) is repeated verbatim in build_asset_lock (Lines 271-289) and build_asset_lock_with_signer (Lines 393-411). Extracting it into a small private helper avoids future drift between the two entry points.

♻️ Proposed extraction
+fn validate_drain_funding(+ funding_account: AssetLockFundingAccount,+ credit_output_count: usize,+ drain: bool,+) -> Result<(), AssetLockError> {+ if drain && credit_output_count != 1 {+ return Err(AssetLockError::Builder(BuilderError::InvalidData(+ "drain asset lock requires exactly one credit output".into(),+ )));+ }+ if matches!(funding_account, AssetLockFundingAccount::CoinJoin { .. }) && !drain {+ return Err(AssetLockError::Builder(BuilderError::InvalidData(+ "CoinJoin-funded asset locks support drain mode only".into(),+ )));+ }+ Ok(())+}

Then in both build_asset_lock and build_asset_lock_with_signer, replace the duplicated blocks with:

- if drain && credit_output_fundings.len() != 1 {- return Err(AssetLockError::Builder(BuilderError::InvalidData(- "drain asset lock requires exactly one credit output".into(),- )));- }- if matches!(funding_account, AssetLockFundingAccount::CoinJoin { .. }) && !drain {- return Err(AssetLockError::Builder(BuilderError::InvalidData(- "CoinJoin-funded asset locks support drain mode only".into(),- )));- }+ validate_drain_funding(funding_account, credit_output_fundings.len(), drain)?;

Also applies to: 393-411

🤖 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
271 - 289, Extract the duplicated drain and CoinJoin validation from
build_asset_lock and build_asset_lock_with_signer into a small private helper
that accepts the relevant funding account, drain flag, and
credit_output_fundings, returning the existing AssetLockError on invalid input.
Replace both inline validation blocks with calls to this helper, preserving the
current validation order and error messages.
🤖 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/src/wallet/managed_wallet_info/transaction_builder.rs`:
- Around line 353-380: Hoist the drained == 0 validation in the
SelectionStrategy::All path to immediately after calculating drained and before
updating the destination output or entering the special_payload branch. Return
the existing BuilderError::InsufficientFunds error for zero-value drains, then
remove the duplicate check from the AssetLockPayloadType handling while
preserving its credit output update.
---
Nitpick comments:
In `@key-wallet/src/wallet/managed_wallet_info/asset_lock_builder.rs`:
- Around line 271-289: Extract the duplicated drain and CoinJoin validation from
build_asset_lock and build_asset_lock_with_signer into a small private helper
that accepts the relevant funding account, drain flag, and
credit_output_fundings, returning the existing AssetLockError on invalid input.
Replace both inline validation blocks with calls to this helper, preserving the
current validation order and error messages.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

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

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: aef4ebcf-7d56-47db-a153-67371ddb5891

📥 Commits

Reviewing files that changed from the base of the PR and between 70d4bf8 and 1846079.

📒 Files selected for processing (3)
  • 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_builder.rs

@codecov

codecovBot commented Jul 23, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 92.40924% with 23 lines in your changes missing coverage. Please review.
✅ Project coverage is 74.68%. Comparing base (19690d3) to head (82a7b0d).
⚠️ Report is 5 commits behind head on dev.

Files with missing linesPatch %Lines
...c/wallet/managed_wallet_info/asset_lock_builder.rs95.72%12 Missing ⚠️
.../wallet/managed_wallet_info/transaction_builder.rs61.11%7 Missing ⚠️
key-wallet-ffi/src/transaction.rs0.00%4 Missing ⚠️
Additional details and impacted files
@@ Coverage Diff @@## dev #915 +/- ##
==========================================
+ Coverage 74.54% 74.68% +0.13% 
==========================================
Files 327 328 +1 Lines 75032 76451 +1419 ==========================================
+ Hits 55936 57099 +1163 - Misses 19096 19352 +256 
FlagCoverage Δ
core77.29% <ø> (ø)
ffi49.23% <0.00%> (-1.72%)⬇️
rpc20.00% <ø> (ø)
spv91.17% <ø> (-0.04%)⬇️
wallet75.46% <93.64%> (+1.05%)⬆️
Files with missing linesCoverage Δ
key-wallet-ffi/src/transaction.rs0.00% <0.00%> (ø)
.../wallet/managed_wallet_info/transaction_builder.rs87.05% <61.11%> (-0.54%)⬇️
...c/wallet/managed_wallet_info/asset_lock_builder.rs90.72% <95.72%> (+2.59%)⬆️

... and 48 files with indirect coverage changes

…drain validation
- The `drained == 0` rejection now guards every `SelectionStrategy::All`
build, not just the asset-lock payload branch, so a plain drain can no
longer produce a zero-value output.
- Extract the duplicated drain/CoinJoin funding validation from
`build_asset_lock` / `build_asset_lock_with_signer` into a shared
`validate_drain_funding` helper.
- cargo fmt over the new asset-lock builder tests (CI pre-commit failure).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@github-actionsgithub-actionsBot added the ready-for-review CodeRabbit has approved this PR label Jul 27, 2026
@xdustinface
xdustinface merged commit 9c87ccb into devJul 27, 2026
38 checks passed
@xdustinface
xdustinface deleted the feat/coinjoin-asset-lock-funding branch July 27, 2026 13:21
bfoss765 added a commit to bfoss765/rust-dashcore that referenced this pull request Aug 1, 2026
Resolves the conflict flagged on PR dashpay#916 against the current `dev`
(9cbe4e7). Only one file conflicted:
`key-wallet/src/wallet/managed_wallet_info/asset_lock_builder.rs`, in two
places, both the same shape.
`dev`'s dashpay#915 (CoinJoin funding account + drain mode) refactored both
asset-lock builders from a single chained `TransactionBuilder::new()...`
expression into a two-step `let mut builder = ...` so it can
conditionally apply `set_selection_strategy(SelectionStrategy::All)` for
drain mode. This branch had changed the same statement's destructuring to
pick up the new third element, the `Option<ReservationToken>`.
Resolution takes `dev`'s two-step builder form and moves this branch's
three-element destructuring down onto the `let (transaction, fee, ...) =
builder` line that consumes it, i.e. both changes are kept:
let (transaction, fee, reservation_token) = builder
.set_funding(...)
.require_final_inputs()
.build_signed_reserved(...)
.await?;
No semantic conflict underneath: `dev` did not touch
`managed_account/reservation.rs`, where the owner-tagged reservation
logic actually lives, and the owner-guarded release blocks added by
b3c216a still sit after the build in both builders, unchanged.
`managed_core_funds_account.rs` and `transaction_builder.rs` auto-merged.
Validated after the merge:
cargo build -p key-wallet -p key-wallet-manager -p dash-spv \
-p key-wallet-ffi -p dashcore -> clean
cargo test -p key-wallet --lib -> 603 passed, 0 failed
(incl. 23 reservation tests, TOCTOU regression among them)
cargo test -p dashcore --lib -> 577 passed, 0 failed
cargo test -p key-wallet-manager --lib -> 49 passed, 0 failed
cargo test -p dash-spv --lib -> 494 passed, 0 failed
cargo clippy -p key-wallet -p dashcore --all-targets -- -D warnings
-> exit 0, 0 warnings
Note for the platform#4185 re-pin: this branch's own public API
(`ReservationToken`, `build_unsigned_reserved`/`build_signed_reserved`,
`ManagedCoreFundsAccount::release_reservation_if_owner`,
`AssetLockResult::reservation_token`) is unchanged by this merge. However
`dev`'s dashpay#915 independently changed `build_asset_lock` /
`build_asset_lock_with_signer` to take `AssetLockFundingAccount` instead
of a bare `account_index`, plus a new `drain: bool`. That is a `dev`-side
break that #4185 will hit on re-pin regardless of this PR.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ready-for-reviewCodeRabbit has approved this PR

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@QuantumExplorer@xdustinface