Skip to content

feat(platform-wallet): CoinJoin-drain asset-lock funding for the shielded pool - #4327

Merged
QuantumExplorer merged 7 commits into
v4.2-devfrom
feat/coinjoin-drain-v4.2
Aug 7, 2026
Merged

feat(platform-wallet): CoinJoin-drain asset-lock funding for the shielded pool#4327
QuantumExplorer merged 7 commits into
v4.2-devfrom
feat/coinjoin-drain-v4.2

Conversation

@QuantumExplorer

@QuantumExplorer QuantumExplorer commented Aug 6, 2026

Copy link
Copy Markdown
Member

What

Lets the wallet's CoinJoin (mixed-coin) balance fund a ShieldFromAssetLock (Type 18) directly — one transaction draining every final CoinJoin UTXO into a single asset lock (lock value = Σ inputs − L1 fee), with the shielded recipient receiving lock_value − pool_fee. No transparent BIP44 intermediate hop, so the mixed coins are never parked on a reusable transparent address.

Builds on the key-wallet side merged in dashpay/rust-dashcore#915 (AssetLockFundingAccount + drain mode, already in this repo's current rev pin):

  • AssetLockFunding::FromAccountDrain { account } — resolver variant that drains the given funding account into a fresh tracked lock; FromWalletBalance and every existing flow are untouched.
  • build_asset_lock_transaction_with_funding / create_funded_asset_lock_proof_with_funding — funding-parameterized forms (AssetLockBuildAmount::Exact vs ::DrainAll); the historical entry points delegate with BIP44 + exact amount.
  • Drain sizing preflight in shielded_fund_from_asset_lock: estimates the drained lock value (Σ spendable − size-based fee) and rejects the flow before broadcasting when it could not clear the Type 18 pool fee — a dust lock that could never be consumed is never created. The post-resolution lock_value − pool_fee derivation is unchanged (reads the real on-chain value, so estimate drift is harmless).
  • Reservation release keyed by funding account family (drained CoinJoin inputs release on the CoinJoin account, not BIP44).
  • New FFI entry point platform_wallet_manager_shielded_fund_from_asset_lock_coinjoin_drain + Swift wrapper PlatformWalletManager.shieldedFundFromCoinJoinDrain(walletId:coinJoinAccountIndex:recipients:), mirroring the existing fund-from-asset-lock signer/worker-thread pattern. Resume-by-outpoint reuses the existing shieldedResumeFundFromAssetLock unchanged.

Consumer

dashpay/dashwallet-ios#858 — the post-migration "move your mixed coins" prompt offers a Shielded destination that runs this drain.

Verification

  • cargo check -p platform-wallet --features shielded and -p platform-wallet-ffi clean on top of v4.2-dev (signer bounds aligned with the ExtendedPubKeySigner migration).
  • Drain build math is covered by the key-wallet tests merged with rust-dashcore#915.
  • DashSDKFFI.xcframework rebuilt (ios + sim); the SDK example app and dashwallet-ios dashpay scheme build against it.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Added support for funding shielded asset locks by draining a selected CoinJoin account.
    • Added Swift SDK access for initiating CoinJoin drain-based shielded funding.
    • Drain operations can enforce minimum lock values and account for pool fees.
  • Bug Fixes

    • Improved tracking, proof validation, recovery, and finality handling for CoinJoin-funded asset locks.
    • Enhanced reservation cleanup when funding transactions are rejected or drains are too small.
  • Documentation

    • Updated SDK capability parity tracking and payment-history support status.

…lded pool

Consumes rust-dashcore feat/coinjoin-asset-lock-funding (rev bump to
1846079b): the key-wallet asset-lock builder can now fund from a CoinJoin
account in whole-balance drain mode (lock value = sum(inputs) - fee).

- AssetLockBuildAmount { Exact, DrainAll } + *_with_funding forms of
  build_asset_lock_transaction / broadcast_funded_asset_lock /
  create_funded_asset_lock_proof. The historical entry points delegate
  with Exact + Bip44, so identity/top-up/address-funding flows are
  unchanged. The tracked amount is read back from the built payload
  (for a drain it is only known post-build).
- AssetLockFunding::DrainAccountBalance { account } resolver variant —
  the CoinJoin -> Shielded migration path (no transparent intermediate
  hop). The shielded fund preflight gains a drain sizing guard: refuse a
  drain whose balance (minus an upper-bound L1 fee) could not clear the
  Type 18 pool fee, so an unrecoverable dust lock is never broadcast.
- Reservation release after a rejected broadcast is funding-family-aware
  (ReservedFundingAccount: Standard | CoinJoin); proof upgrade's
  funding-tx lookup falls back to the CoinJoin account map.
- New FFI entry point
  platform_wallet_manager_shielded_fund_from_asset_lock_coinjoin_drain
  and Swift wrapper PlatformWalletManager.shieldedFundFromCoinJoinDrain
  (same recipient/resume contract as shieldedFundFromAssetLock).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Aug 6, 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: e0c3c603-f09f-44fe-8c60-d4ff5747afcf

📥 Commits

Reviewing files that changed from the base of the PR and between b2b6c12 and 074a21e.

📒 Files selected for processing (1)
  • packages/swift-sdk/SwiftTests/SwiftDashSDKIntegrationTests/Support/TestWallet.swift

📝 Walkthrough

Walkthrough

The PR adds CoinJoin account drain funding for shielded asset locks. It extends asset-lock building, reservation handling, proof lookup, finality resolution, and Swift/Rust wallet APIs. It also updates SDK parity metadata for DashPay payment history.

Changes

CoinJoin drain funding

Layer / File(s) Summary
Funding-aware asset-lock build and reservation pipeline
packages/rs-platform-wallet/src/wallet/asset_lock/build.rs, packages/rs-platform-wallet/src/wallet/reservations.rs
Asset-lock builders support exact and drain-all funding for BIP44 and CoinJoin accounts. Reservation tokens, computed lock amounts, minimum checks, and account-aware rejection cleanup are supported.
Drain orchestration and fee floor
packages/rs-platform-wallet/src/wallet/asset_lock/orchestration.rs, packages/rs-platform-wallet/src/wallet/shielded/fund_from_asset_lock.rs
The resolver handles account-balance drains. Shielded funding sets the minimum lock value above the pool fee.
Family-aware proof lookup and recovery
packages/rs-platform-wallet/src/wallet/asset_lock/sync/proof.rs, packages/rs-platform-wallet/src/wallet/asset_lock/sync/recovery.rs
Proof validation, ChainLock waiting, diagnostics, and recovery find funding records in BIP44 or CoinJoin accounts.
Shielded CoinJoin drain API
packages/swift-sdk/Sources/SwiftDashSDK/PlatformWallet/PlatformWalletManagerShieldedFunding.swift, packages/rs-platform-wallet-ffi/src/shielded_send.rs, packages/swift-sdk/SwiftTests/SwiftDashSDKIntegrationTests/Support/TestWallet.swift
Swift and Rust expose CoinJoin drain funding. The flow validates inputs, runs asynchronously, creates one shielded note, waits for finality, and returns FFI errors.

SDK parity metadata

Layer / File(s) Summary
DashPay payment history parity tracking
docs/sdk/sdk-parity-manifest.json, packages/kotlin-sdk/PARITY_SUMMARY.md
The parity manifest clears the Kotlin host limitation reason. The parity summary adds the capability and updates totals and coverage counts.

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

Sequence Diagram(s)

sequenceDiagram
  participant PlatformWalletManager
  participant RustFFI
  participant AssetLockResolver
  participant CoinJoinAccount
  participant ChainLock

  PlatformWalletManager->>RustFFI: invoke shielded CoinJoin drain
  RustFFI->>AssetLockResolver: resolve DrainAccountBalance
  AssetLockResolver->>CoinJoinAccount: consume all selected account UTXOs
  AssetLockResolver->>ChainLock: wait for finality
  ChainLock-->>RustFFI: return proof or typed broadcast error
  RustFFI-->>PlatformWalletManager: return FFI result
Loading

Possibly related PRs

Suggested reviewers: lklimek, llbartekll, shumkov

🚥 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: CoinJoin-drain asset-lock funding for the shielded pool.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
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 💡 1
🛠️ Fix failing CI checks 💡
  • Create stacked PR
  • Commit on current branch
📝 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-drain-v4.2

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

@thepastaclaw

thepastaclaw commented Aug 6, 2026

Copy link
Copy Markdown
Collaborator

🕓 Ready for review — next in queue (commit 074a21e)
Queue position: 1/2
ETA: start ~06:30 UTC · complete ~06:49 UTC (median 19m across 30 recent reviews; 2 slots)
Queued 17m ago · Last checked: 2026-08-07 06:30 UTC

…dedPubKeySigner bound

The funding-parameterized asset-lock forms were cherry-picked with the
older `S: Signer` bound; v4.2-dev's builder surface requires
`ExtendedPubKeySigner` (the selected-account xpub work). Also rewrite the
stale delegation comment.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

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

Preliminary review — Codex only

The new CoinJoin drain is not safe to merge: proof polling still searches only BIP44 in the paths used before validation, and the preflight can overstate the actual built lock enough to broadcast an unconsumable Type 18 outpoint. The rejection path also loses reservation ownership, and the FFI entry point flattens retry-critical broadcast outcome codes.
Source: reviewers codex/general=gpt-5.6-sol(completed); codex/security-auditor=gpt-5.6-sol(completed); codex/rust-quality=gpt-5.6-sol(completed); codex/ffi-engineer=gpt-5.6-sol(completed); verifier=codex/verifier=gpt-5.6-sol(completed); coordinator=openclaw-agent/cliproxy/gpt-5.6-sol(orchestration-only).

Validated blockers were found in the Codex precheck. Sonnet is deferred until a fresh Codex revalidation clears the blocker gate.

Review provenance

  • Codex reviewers: gpt-5.6-sol — general (completed), gpt-5.6-sol — security-auditor (completed), gpt-5.6-sol — rust-quality (completed), gpt-5.6-sol — ffi-engineer (completed)
  • Verifier: gpt-5.6-sol — verifier
  • Sonnet: not run (deferred by blocker gate)

🔴 2 blocking | 🟡 2 suggestion(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/src/wallet/asset_lock/sync/proof.rs`:
- [BLOCKING] packages/rs-platform-wallet/src/wallet/asset_lock/sync/proof.rs:101-117: CoinJoin fallback is missing from the proof-wait paths
  The new CoinJoin fallback exists only in `validate_or_upgrade_proof`, which is reached after a proof has already been acquired. The fresh drain first calls `wait_for_proof`, whose diagnostic and authoritative lookups at lines 423-453 still search only `standard_bip44_accounts`. The timeout fallback in `upgrade_to_chain_lock_proof` and its `wait_for_chain_lock` loop are also BIP44-only, as is proofless recovery in `sync/recovery.rs`. Key-wallet records a transaction spending CoinJoin inputs under `coinjoin_accounts`; with `NoPlatformPersistence`, whose `get_core_tx_record` implementation returns `Ok(None)`, a valid SPV InstantSend or ChainLock record therefore remains invisible. The fresh operation waits 300 seconds and then the fallback fails with “Transaction ... not found”; resume repeats the same family-blind path after the asset lock has already been broadcast. Centralize transaction-record lookup across both account families, or persist the funding family in `TrackedAssetLock`, and use it in every proof, ChainLock, and recovery lookup.

In `packages/rs-platform-wallet/src/wallet/shielded/fund_from_asset_lock.rs`:
- [BLOCKING] packages/rs-platform-wallet/src/wallet/shielded/fund_from_asset_lock.rs:197-232: Drain preflight is not a conservative estimate of the builder output
  This guard can overstate the lock value it claims to lower-bound. First, the pinned builder's one-credit-output asset-lock base size is 81 bytes—8 bytes of header and locktime, two one-byte counts, a conservatively sized 34-byte burn output, a one-byte payload-length prefix, and a 36-byte payload—not 54 bytes, so the estimate is already 27 duffs too high at the configured 1 duff/byte rate. Second, this loop sums raw final, unlocked UTXOs, while `set_funding` excludes outpoints held in the account's `ReservationSet` and `SelectionStrategy::All` applies `Utxo::is_spendable(current_height)`, including coinbase maturity. Finally, the wallet-manager lock is released before acquiring `shield_guard` and performing the actual build, so the candidate set can change between the estimate and selection. A partially reserved account can consequently pass preflight using outputs the builder omits, after which the smaller lock is broadcast and the authoritative pool-fee subtraction fails. Because the asset-lock outpoint is single-use, this defeats the PR's stated guarantee that an unrecoverable dust lock is never broadcast. Make the safety decision from the actual built payload before broadcast, retaining the reservation owner token so an undersized build can be abandoned safely, or use a key-wallet quote API that shares the builder's complete selection and fee logic under the same funding lock.

In `packages/rs-platform-wallet/src/wallet/reservations.rs`:
- [SUGGESTION] packages/rs-platform-wallet/src/wallet/reservations.rs:108-115: Preserve the reservation ownership token through rejection cleanup
  The new CoinJoin rejection branch calls unconditional `release_reservation(tx)`, while the pinned key-wallet builder returns `AssetLockResult::reservation_token` and documents that cleanup after an `await` must use `release_reservation_if_owner`. The asset-lock build wrapper discards that token. If the original reservation is swept and the same CoinJoin outpoint is reserved by another build while broadcast is awaited, a late rejection from the first build removes the newer reservation and makes the input selectable for another conflicting transaction. This ownership defect predates the PR for BIP44 funding, but the added branch newly exposes CoinJoin drain inputs to it. Carry the token with the built transaction through the broadcast pipeline and perform owner-guarded release for definitive rejection.

In `packages/rs-platform-wallet-ffi/src/shielded_send.rs`:
- [SUGGESTION] packages/rs-platform-wallet-ffi/src/shielded_send.rs:1139-1145: Preserve asset-lock broadcast outcome codes across the Swift boundary
  This new entry point converts every `PlatformWalletError` to `ErrorWalletOperation`, discarding the typed distinction between `TransactionBroadcastUnconfirmed` and a definitive `TransactionBroadcast` rejection. The Rust flow deliberately keeps the tracked lock and reservation for an ambiguous outcome but untracks and releases after rejection, and the existing `From<PlatformWalletError>` conversion maps these variants to `ErrorTransactionBroadcastUnconfirmed` and `ErrorTransactionBroadcastRejected`. Swift already exposes dedicated cases for both. Flattening them prevents the caller from choosing resume/do-not-redrain behavior for a possibly broadcast whole-account lock versus correcting and safely retrying a rejected operation.

Comment thread packages/rs-platform-wallet/src/wallet/asset_lock/sync/proof.rs Outdated
Comment thread packages/rs-platform-wallet/src/wallet/shielded/fund_from_asset_lock.rs Outdated
Comment thread packages/rs-platform-wallet/src/wallet/reservations.rs Outdated
Comment thread packages/rs-platform-wallet-ffi/src/shielded_send.rs Outdated
- Family-aware funding-tx lookups everywhere (review blocker 1): the
  BIP44+CoinJoin record lookup is centralized in
  `sync::proof::funding_tx_record` and used by every proof, ChainLock-wait,
  and recovery path (`wait_for_proof`'s diagnostic + authoritative reads,
  `upgrade_to_chain_lock_proof`, `wait_for_chain_lock`,
  `validate_or_upgrade_proof`, and proofless recovery) — previously only
  `validate_or_upgrade_proof` had the CoinJoin fallback, so a drain lock's
  IS/CL record was invisible to the fresh-wait paths under
  `NoPlatformPersistence` and the flow burned the full 300 s timeout.

- Authoritative drain floor instead of a pre-build estimate (review
  blocker 2): the size/balance preflight is gone — it could overstate the
  drained value (wrong base size, no reservation/spendability filters,
  TOCTOU after the lock drop). `AssetLockBuildAmount::DrainAll` and
  `AssetLockFunding::DrainAccountBalance` now carry `minimum_lock_duffs`;
  the shielded fund flow stamps the pool-fee floor, and
  `broadcast_funded_asset_lock_with_funding` enforces it against the BUILT
  payload before anything is tracked or broadcast, abandoning an
  undersized build with an owner-guarded reservation release.

- Reservation ownership through cleanup (review suggestion 3): the
  builder's `ReservationToken` is threaded from
  `build_asset_lock_transaction_with_funding` through the funded pipeline;
  both the rejected-broadcast release and the undersized-drain abandon use
  `release_reservation_if_owner`, so a late cleanup can no longer clobber
  a newer build's reservation of the same outpoints. Token-less legacy
  callers keep the historical unconditional release.

- Typed FFI broadcast outcomes (review suggestion 4): the coinjoin-drain
  FFI entry point converts errors via `From<PlatformWalletError>`,
  preserving ErrorTransactionBroadcastUnconfirmed vs ...Rejected so the
  host can distinguish resume-don't-redrain from safe-retry.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@github-actions github-actions Bot added this to the v4.2.0 milestone Aug 6, 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.

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

763-772: 🗄️ Data Integrity & Integration | 🔵 Trivial | 💤 Low value

Consider failing instead of recording 0 for a missing asset-lock payload.

The _ => 0 arm silently records a locked amount of 0. For a DrainAll build with a minimum this is safe (the floor check aborts). For an Exact build the tracked row would store amount: 0, and the shielded CL-only path derives the shield amount from that tracked row (lookup_asset_lock_value_credits). The arm is unreachable with the current key-wallet builder, so this is defensive only.

♻️ Proposed change
-            _ => 0,
-        };
+            _ => {
+                return Err(PlatformWalletError::AssetLockTransaction(
+                    "built transaction carries no asset-lock payload".to_string(),
+                ));
+            }
+        };
🤖 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/asset_lock/build.rs` around lines 763
- 772, Update the locked_amount_duffs extraction in the asset-lock build flow to
fail immediately when special_transaction_payload is missing or not an
AssetLockPayloadType, instead of recording 0. Preserve summing credit_outputs
for valid asset-lock payloads so Exact and DrainAll continue using the built
amount.
🤖 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/asset_lock/build.rs`:
- Around line 763-772: Update the locked_amount_duffs extraction in the
asset-lock build flow to fail immediately when special_transaction_payload is
missing or not an AssetLockPayloadType, instead of recording 0. Preserve summing
credit_outputs for valid asset-lock payloads so Exact and DrainAll continue
using the built amount.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: e3471cc4-d788-4d0f-af8e-fc5f77b05119

📥 Commits

Reviewing files that changed from the base of the PR and between fb3e050 and 34098dd.

📒 Files selected for processing (8)
  • packages/rs-platform-wallet-ffi/src/shielded_send.rs
  • packages/rs-platform-wallet/src/wallet/asset_lock/build.rs
  • packages/rs-platform-wallet/src/wallet/asset_lock/orchestration.rs
  • packages/rs-platform-wallet/src/wallet/asset_lock/sync/proof.rs
  • packages/rs-platform-wallet/src/wallet/asset_lock/sync/recovery.rs
  • packages/rs-platform-wallet/src/wallet/reservations.rs
  • packages/rs-platform-wallet/src/wallet/shielded/fund_from_asset_lock.rs
  • packages/swift-sdk/Sources/SwiftDashSDK/PlatformWallet/PlatformWalletManagerShieldedFunding.swift

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

Final validation — Codex + Sonnet

carried-forward: three of the four prior findings are fixed; prior-7bd26f36-3 remains as one in-scope suggestion because the owner token is still applied to whichever wallet generation currently occupies the wallet ID. latest-delta: two in-scope test-coverage suggestions remain for the CoinJoin proof-record fallback and the pre-broadcast undersized-drain abandonment path; no blocking findings remain, and the existing targeted proof/build tests pass.
Source: reviewers codex/general=gpt-5.6-sol(completed); codex/security-auditor=gpt-5.6-sol(completed); codex/rust-quality=gpt-5.6-sol(completed); codex/ffi-engineer=gpt-5.6-sol(completed); claude/general=claude-sonnet-5(failed); claude/security-auditor=claude-sonnet-5(failed); claude/rust-quality=claude-sonnet-5(failed); claude/ffi-engineer=claude-sonnet-5(completed); claude/general=claude-sonnet-5(failed); claude/security-auditor=claude-sonnet-5(completed); claude/rust-quality=claude-sonnet-5(completed); claude/general=claude-sonnet-5(completed); verifier=codex/final-verifier=gpt-5.6-sol(completed) fallback_for_sonnet_verifier=true; coordinator=openclaw-agent/cliproxy/gpt-5.6-sol(orchestration-only).

Review provenance

  • Codex reviewers: gpt-5.6-sol — general (completed), gpt-5.6-sol — security-auditor (completed), gpt-5.6-sol — rust-quality (completed), gpt-5.6-sol — ffi-engineer (completed)
  • Verifier: gpt-5.6-sol — final-verifier (fallback for Sonnet verifier)
  • Sonnet reviewers: claude-sonnet-5 — general (failed), claude-sonnet-5 — security-auditor (failed), claude-sonnet-5 — rust-quality (failed), claude-sonnet-5 — ffi-engineer (completed), claude-sonnet-5 — general (failed), claude-sonnet-5 — security-auditor (completed), claude-sonnet-5 — rust-quality (completed), claude-sonnet-5 — general (completed)

🟡 2 suggestion(s)

1 carried-forward finding(s) already raised on this PR; not re-posting as new inline comments.

🤖 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/src/wallet/asset_lock/sync/proof.rs`:
- [SUGGESTION] packages/rs-platform-wallet/src/wallet/asset_lock/sync/proof.rs:55-69: Add a CoinJoin regression test for funding-record lookup
  The centralized helper correctly checks BIP44 and then CoinJoin, but the six tests in this module exercise only `record_or_persister`; none places a transaction exclusively in `coinjoin_accounts` or invokes `funding_tx_record`. This dispatch is the exact fix for the prior 300-second proof-wait failure and can regress while every current test remains green. Add at least a CoinJoin-only helper test and preferably drive `wait_for_proof` or proofless recovery with `NoPlatformPersistence`, where the in-memory CoinJoin record is authoritative.

In `packages/rs-platform-wallet/src/wallet/asset_lock/build.rs`:
- [SUGGESTION] packages/rs-platform-wallet/src/wallet/asset_lock/build.rs:782-811: Cover undersized drain abandonment before broadcast
  No platform-wallet test constructs `AssetLockBuildAmount::DrainAll`, so the branch that enforces this PR's central safety guarantee is untested. The existing build tests use exact-amount BIP44 funding and do not prove that an undersized completed payload is rejected before tracking or broadcast, or that its reservation is released through the returned owner token. Add a CoinJoin-funded test with a counting broadcaster, set `minimum_lock_duffs` above the built payload value, and assert that the broadcaster is never called, no tracked row is created, and an immediate subsequent drain can select the inputs again.

Comment thread packages/rs-platform-wallet/src/wallet/asset_lock/sync/proof.rs
Comment thread packages/rs-platform-wallet/src/wallet/asset_lock/build.rs
…ain abandonment

Review follow-ups on the CoinJoin-drain PR:

- funding_tx_record_finds_coinjoin_only_record / ..._finds_bip44_record:
  a record filed only under `coinjoin_accounts` (how key-wallet files a
  tx spending CoinJoin inputs) must be visible to the shared proof/
  recovery lookup — the exact regression behind the pre-fix 300 s
  proof-wait failure — and the historical BIP44 path still resolves.

- undersized_drain_abandoned_before_broadcast: over the CoinJoin-funded
  fixture, a drain whose floor exceeds the built lock value is refused
  with nothing broadcast (counting broadcaster at 0), no tracked row and
  no queued removal, and the owner-guarded reservation release lets an
  immediate follow-up drain over the same single-UTXO account select the
  inputs and broadcast.

Co-Authored-By: Claude Fable 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.

Caution

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

⚠️ Outside diff range comments (1)
packages/rs-platform-wallet/src/wallet/asset_lock/build.rs (1)

750-758: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Release the reservation when the invitation durability gate aborts.

The IdentityInvitation error path at lines 834-842 returns before tracking or broadcasting. It does not call release_reservation_after_rejected_broadcast.

The retained reservation_token then leaves the built inputs reserved with no tracked row that can resume the transaction. A retry can fail at input selection after a transient persistence or flush failure.

Release the reservation before returning from that abort path. Add a regression assertion that a subsequent build can reuse the input.

🤖 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/asset_lock/build.rs` around lines 750
- 758, Update the IdentityInvitation durability-gate error path to call
release_reservation_after_rejected_broadcast with the retained reservation_token
before returning the error. Add a regression assertion verifying that a
subsequent transaction build can reuse the released input.
🤖 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.

Outside diff comments:
In `@packages/rs-platform-wallet/src/wallet/asset_lock/build.rs`:
- Around line 750-758: Update the IdentityInvitation durability-gate error path
to call release_reservation_after_rejected_broadcast with the retained
reservation_token before returning the error. Add a regression assertion
verifying that a subsequent transaction build can reuse the released input.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 1af161d1-249d-4ea9-bc60-c35dcc1a8972

📥 Commits

Reviewing files that changed from the base of the PR and between 34098dd and 2728775.

📒 Files selected for processing (2)
  • packages/rs-platform-wallet/src/wallet/asset_lock/build.rs
  • packages/rs-platform-wallet/src/wallet/asset_lock/sync/proof.rs
🚧 Files skipped from review as they are similar to previous changes (1)
  • packages/rs-platform-wallet/src/wallet/asset_lock/sync/proof.rs

QuantumExplorer and others added 3 commits August 7, 2026 12:56
…arity entry

check_sdk_parity_manifest.py requires `reason: null` on hosts with no
parity gap (both statuses supported/not-applicable); the new
persistence.dashpay_payment_history entry landed on v4.2-dev with a
rationale string on its fully not-applicable kotlin host, turning the
parity gate red for every PR merged against the branch. Summary
regenerated with --write-summary.

The dropped rationale, for the record: Android derives contact payment
attribution from transaction history on reads and does not consume
PaymentEntry rows (confirmed by the Android team during the sent-payment
reconstruction review), so the JNI vtable deliberately leaves the slot
None and there is nothing to persist or restore on this host.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Newer strict-concurrency toolchains reject `makeTestWallet`'s
non-Sendable result crossing its async boundary (7 hard errors in the
Maya deposit integration suite on the CI runner's Swift). Same
justification as the existing `IntegrationTestEnv: @unchecked Sendable`:
immutable stored SDK handles, one test task drives a wrapper at a time.

Pre-existing on v4.2-dev — the swift-sdk CI job is path-filtered, so it
only surfaces on PRs that touch the package.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@codecov

codecov Bot commented Aug 7, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 87.78%. Comparing base (316ee7a) to head (074a21e).
⚠️ Report is 5 commits behind head on v4.2-dev.

Additional details and impacted files
@@             Coverage Diff              @@
##           v4.2-dev    #4327      +/-   ##
============================================
- Coverage     87.78%   87.78%   -0.01%     
============================================
  Files          2677     2677              
  Lines        342371   342371              
============================================
- Hits         300551   300550       -1     
- Misses        41820    41821       +1     
Components Coverage Δ
dpp 88.83% <ø> (ø)
drive 86.25% <ø> (ø)
drive-abci 89.66% <ø> (-0.01%) ⬇️
sdk ∅ <ø> (∅)
dapi-client ∅ <ø> (∅)
platform-version ∅ <ø> (∅)
platform-value 92.88% <ø> (ø)
platform-wallet ∅ <ø> (∅)
drive-proof-verifier 48.02% <ø> (ø)
🚀 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.

QuantumExplorer added a commit to dashpay/rust-dashcore that referenced this pull request Aug 10, 2026
…ounts (#944)

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

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>

* docs(key-wallet): drop the private intra-doc link from build_asset_lock

`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>

* docs(key-wallet-ffi): flag CoinJoin as unusable on the non-drain asset 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>

* refactor(key-wallet): share the asset-lock funding and reservation prologue

`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>

* test(key-wallet-ffi): cover the caller-supplied asset-lock funding sources

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>

* docs(key-wallet-ffi): regenerate FFI_API.md for the discriminant safety 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>

* test(key-wallet-ffi): free the wallet handle the asset-lock test borrows

`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>

* test(key-wallet-ffi): run the asset-lock source cases on both networks

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>

---------

Co-authored-by: bfoss765 <brian.foster@dash.org>
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
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