Skip to content

feat(kotlin-sdk): coinjoin-drain shielded funding binding + typed asset-lock shortfall - #4361

Open
bfoss765 wants to merge 2 commits into
v4.2-devfrom
feat/coinjoin-drain-android-bindings
Open

feat(kotlin-sdk): coinjoin-drain shielded funding binding + typed asset-lock shortfall#4361
bfoss765 wants to merge 2 commits into
v4.2-devfrom
feat/coinjoin-drain-android-bindings

Conversation

@bfoss765

@bfoss765 bfoss765 commented Aug 10, 2026

Copy link
Copy Markdown
Collaborator

Supersedes #4349 — same change, recreated on a dashpay/platform branch per repo policy (no more personal-fork PRs). Commits and authorship unchanged; full review history on #4349.

The two Android-facing pieces #4327 left out. Both are small and independent of each other; they meet at one user action.

1. Kotlin/JNI binding for CoinJoin-drain shielded funding

#4327 landed the FFI export platform_wallet_manager_shielded_fund_from_asset_lock_coinjoin_drain and a Swift wrapper, but no Kotlin/JNI — so Android currently cannot call CoinJoin-funded shielding at all.

This adds the JNI export in rs-unified-sdk-jni and the Kotlin surface (FundingNative.shieldedFundFromCoinJoinDrain, PlatformWalletManager.shieldedFundFromCoinJoinDrain), shaped like the existing shieldedFundFromAssetLock binding and following the Swift wrapper's contract:

  • no amount — every final mixed-coin UTXO is consumed and the lock value is Σ inputs − L1 fee, computed Rust-side, so the mixed coins never hop through a transparent BIP44 address;
  • no surplus output — the single-recipient remainder flow pins the consensus surplus to zero, so the parameter is omitted rather than plumbed as null.

The negative-index guard matches the sibling binding's boundary check (a negative jint would otherwise bit-cast to a huge u32).

The mixed-funds migration in the Android wallet depends on this binding — it is the CoinJoin → Shielded path.

2. Typed asset-lock shortfall at its reserved code 29

The FFI error registry has reserved 29 for ErrorAssetLockInsufficientFunds since #4184, and the Swift and Kotlin mirrors already document the number — but the code was never allocated, because #4184 and its successor #4316 were both closed unmerged. That also left the producing PlatformWalletError::AssetLockInsufficientFunds variant absent, so there was nothing to map from.

This salvages the minimum needed to make the reserved code real (#4073):

  • the AssetLockInsufficientFunds { available, required } variant;
  • a builder-error mapper promoting every key-wallet coin-selection shortfall to it. InsufficientFunds shapes keep their own exact amounts; NoUtxosAvailable — the most extreme shortfall — previously fell through to the generic string form while partial shortfalls stayed typed, and now maps to available: 0 against the requested target. Every other builder error keeps its existing generic string;
  • the FFI variant at 29, its From arm, and tests pinning both the mapping and the number.

Without the From arm a shortfall flattens to ErrorUnknown (99), forcing hosts to substring-match the Display text. The amounts still ride the message — PlatformWalletFFIResult is ABI-frozen to code + message — but hosts can now branch on the code.

Two existing asset-lock tests asserted the old generic error for a "fails at input selection" rebuild; they now assert the typed shortfall with available: 0, which is a stronger statement of the same intent.

Why these two are in one PR

Draining an empty CoinJoin account is a coin-selection shortfall. The binding is how Android runs the migration; code 29 is how it explains the most likely failure without parsing English.

Testing

  • cargo test -p platform-wallet --lib — 611 passed
  • cargo test -p platform-wallet-ffi — 263 + 26 + 6 passed
  • cargo clippy -p platform-wallet -p platform-wallet-ffi -p rs-unified-sdk-jni --all-targets -- -D warnings — clean
  • cargo fmt --check — clean
  • ./gradlew :sdk:assembleDebug :sdk:testDebugUnitTest — BUILD SUCCESSFUL

The Halo 2 proving path itself is unchanged and untested here; this PR only adds the call surface to it.

Summary by CodeRabbit

  • New Features
    • Added the ability to fund a shielded asset lock by draining a wallet’s CoinJoin account into a single recipient.
    • Added validation for wallet IDs, recipient addresses, and account indexes.
  • Bug Fixes
    • Added clear, dedicated errors when available funds are insufficient for an asset lock.
    • Insufficient-funds errors now include available and required amounts for easier troubleshooting.

…et-lock shortfall

The two Android-facing pieces that #4327 left out.

#4327 added the CoinJoin-drain asset-lock funding FFI export
(`platform_wallet_manager_shielded_fund_from_asset_lock_coinjoin_drain`)
and its Swift wrapper, but no Kotlin/JNI, so Android could not call
CoinJoin-funded shielding at all. This adds the JNI export and the Kotlin
surface, following the Swift wrapper's contract: no amount (the lock value
is the builder's Sigma inputs - L1 fee) and no surplus output (the
single-recipient remainder flow pins the consensus surplus to zero).

It also allocates the typed asset-lock shortfall at its long-reserved code
29. The FFI error registry has held 29 for `ErrorAssetLockInsufficientFunds`
since #4184, and every host mirror already documents the number, but the
code was never allocated: #4184 and its successor #4316 were both closed
unmerged, leaving the producing `PlatformWalletError` variant absent too.
Without it an asset-lock coin-selection shortfall flattens to
`ErrorUnknown` (99) and hosts must substring-match the Display text. The
empty-candidate-set case now stays on the same structured path as a partial
shortfall instead of falling through to the generic string form.

Draining an empty CoinJoin account is exactly a coin-selection shortfall,
so the two halves meet at the same call: the Android mixed-funds migration
needs the binding to run and the typed code to explain a failure.

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

coderabbitai Bot commented Aug 10, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

The PR adds typed asset-lock insufficient-funds errors with native code 29. It also adds CoinJoin account drain funding across the Rust platform wallet, JNI bridge, and Kotlin SDK.

Changes

Asset-lock CoinJoin funding

Layer / File(s) Summary
Typed asset-lock errors
packages/rs-platform-wallet/src/error.rs, packages/rs-platform-wallet-ffi/src/error.rs, packages/kotlin-sdk/sdk/src/main/kotlin/.../DashSdkError.kt, packages/.../DashSdkErrorTest.kt
Asset-lock shortfalls now use dedicated Rust, FFI, and Kotlin error types. Native code 29 maps to PlatformWallet.AssetLockInsufficientFunds. Tests verify code, message, and error distinction.
Builder shortfall mapping
packages/rs-platform-wallet/src/wallet/asset_lock/build.rs
Coin-selection failures now preserve available and required amounts. Empty UTXO sets use typed zero-available errors. Rebuild tests assert the typed result.
CoinJoin drain integration
packages/rs-unified-sdk-jni/src/funding.rs, packages/kotlin-sdk/sdk/src/main/kotlin/.../FundingNative.kt, packages/kotlin-sdk/sdk/src/main/kotlin/.../PlatformWalletManager.kt
The SDK can drain a CoinJoin account into one shielded asset lock. The JNI and Kotlin layers validate inputs and return native status or mapped errors.

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

Sequence Diagram(s)

sequenceDiagram
  participant KotlinCaller
  participant PlatformWalletManager
  participant FundingNative
  participant JNI
  participant RustPlatformWallet

  KotlinCaller->>PlatformWalletManager: call shieldedFundFromCoinJoinDrain
  PlatformWalletManager->>FundingNative: pass wallet, recipient, and account index
  FundingNative->>JNI: invoke native drain method
  JNI->>RustPlatformWallet: drain CoinJoin account into asset lock
  RustPlatformWallet-->>JNI: return status or typed error code
  JNI-->>FundingNative: forward result
  FundingNative-->>PlatformWalletManager: complete or raise mapped error
Loading

Possibly related PRs

Suggested reviewers: quantumexplorer, lklimek

🚥 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 summarizes both main changes: the Kotlin CoinJoin-drain funding binding and typed asset-lock shortfall handling.
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-android-bindings

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

@thepastaclaw

thepastaclaw commented Aug 10, 2026

Copy link
Copy Markdown
Collaborator

✅ Final review complete — no blockers (commit bf9a134)

@codecov

codecov Bot commented Aug 10, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 87.63%. Comparing base (6373e00) to head (bf9a134).
⚠️ Report is 1 commits behind head on v4.2-dev.

Additional details and impacted files
@@            Coverage Diff            @@
##           v4.2-dev    #4361   +/-   ##
=========================================
  Coverage     87.63%   87.63%           
=========================================
  Files          2670     2670           
  Lines        339449   339449           
=========================================
  Hits         297467   297467           
  Misses        41982    41982           
Components Coverage Δ
dpp 88.86% <ø> (ø)
drive 86.25% <ø> (ø)
drive-abci 89.66% <ø> (ø)
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.

@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/Sol only (Phase 2 disabled)

The new JNI/Kotlin drain binding follows the sibling safety patterns, but the typed asset-lock shortfall remains incomplete across three paths: empty drains carry a 0/0 amount pair, exact BIP44 funding erases code 29, and Swift decodes code 29 as unknown. These are client-facing error-contract defects rather than consensus blockers, so the review contains three suggestions.
Source: reviewer backend model gpt-5.6-sol (Codex general, FFI engineer, and Rust quality lanes); final verifier backend model gpt-5.6-sol (Codex). Orchestration-only: openclaw-agent/cliproxy/gpt-5.6-sol (not reviewer evidence).

Validated zero-blocker Codex/Sol precheck evidence was promoted to final because Phase 2 (Sonnet/Opus) is temporarily disabled. This is Codex/Sol-only final validation, not Codex + Sonnet/Opus coverage.

Review provenance

  • Codex reviewers: gpt-5.6-sol — general (completed), gpt-5.6-sol — ffi-engineer (completed), gpt-5.6-sol — rust-quality (completed)
  • Verifier: gpt-5.6-sol — verifier
  • Sonnet/Opus: not run (Phase 2 disabled — temporary Codex/Sol-only final)
  • Secondary pass: disabled (temporary_phase2_sonnet_disable)

🟡 3 suggestion(s)

2 additional finding(s) omitted (not in diff).

🤖 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/build.rs`:
- [SUGGESTION] packages/rs-platform-wallet/src/wallet/asset_lock/build.rs:193: Empty CoinJoin drains report a zero required amount
  `DrainAll` is converted to the builder's zero-valued output placeholder at lines 126-132, and that placeholder is passed here as the fallback `required` amount. When an empty CoinJoin account produces `SelectionError::NoUtxosAvailable`, the new mapper therefore returns `AssetLockInsufficientFunds { available: 0, required: 0 }`, even though the operation cannot proceed with zero funds. The shielded flow has already computed a positive `minimum_lock_duffs` from the Type 18 pool fee and threads it through `AssetLockBuildAmount::DrainAll`; use that floor for amount-less selection errors so the advertised available/required pair describes the actual shortfall.

In `packages/rs-platform-wallet-ffi/src/shielded_send.rs`:
- [SUGGESTION] packages/rs-platform-wallet-ffi/src/shielded_send.rs:1024-1030: Exact-amount shielded funding erases the typed shortfall
  The PR promotes asset-lock builder shortfalls to `PlatformWalletError::AssetLockInsufficientFunds` and maps that variant to FFI code 29, but this existing exact-amount endpoint replaces every wallet error with `ErrorWalletOperation` (6). Consequently, Kotlin's newly documented `shieldedFundFromAssetLock` producer can never raise `DashSdkError.PlatformWallet.AssetLockInsufficientFunds`; only the CoinJoin-drain sibling preserves the typed conversion. Route the new shortfall variant through `From<PlatformWalletError>` while retaining the endpoint's existing catch-all behavior and contextual message for unrelated failures.

In `packages/swift-sdk/Sources/SwiftDashSDK/PlatformWallet/PlatformWalletResult.swift`:
- [SUGGESTION] packages/swift-sdk/Sources/SwiftDashSDK/PlatformWallet/PlatformWalletResult.swift:89-97: Swift does not decode the newly allocated FFI error code
  Rust now emits `ErrorAssetLockInsufficientFunds` at code 29, including from the existing Swift `shieldedFundFromCoinJoinDrain` wrapper. Swift only mentions the reservation in this comment: `PlatformWalletResultCode` has no code-29 case, `init(ffi:)` has no corresponding C-enum arm and falls through to `.errorUnknown`, and `PlatformWalletError` has no typed shortfall case. Add code 29 to the Swift result enum, C-enum conversion, typed error construction and description handling, plus a regression mapping test analogous to the existing code mappings.

Comment thread packages/rs-platform-wallet/src/wallet/asset_lock/build.rs Outdated
…r, in typed shortfalls

An empty CoinJoin drain surfaced AssetLockInsufficientFunds
{ available: 0, required: 0 } because the drain's zero-valued
credit-output placeholder was passed as the requested amount. The
shielded flow already threads the positive minimum through
AssetLockBuildAmount::DrainAll; use that floor for amount-less
selection errors so the advertised pair describes the actual gap.

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.

Actionable comments posted: 1

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

Inline comments:
In `@packages/rs-platform-wallet/src/error.rs`:
- Around line 272-278: Update the drain-shortfall documentation in
packages/rs-platform-wallet/src/error.rs lines 272-278 to state that required
equals minimum_lock_duffs when a drain floor is configured, and equals zero only
when no floor exists. Also update the mapper contract in
packages/rs-platform-wallet/src/wallet/asset_lock/build.rs lines 994-999 to
match the minimum_lock_duffs.unwrap_or(0) value passed by the asset-lock build
flow.
🪄 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: 34dd7946-0802-4d9a-b188-4162636f6c89

📥 Commits

Reviewing files that changed from the base of the PR and between 86f3878 and bf9a134.

📒 Files selected for processing (8)
  • packages/kotlin-sdk/sdk/src/main/kotlin/org/dashfoundation/dashsdk/errors/DashSdkError.kt
  • packages/kotlin-sdk/sdk/src/main/kotlin/org/dashfoundation/dashsdk/ffi/FundingNative.kt
  • packages/kotlin-sdk/sdk/src/main/kotlin/org/dashfoundation/dashsdk/wallet/PlatformWalletManager.kt
  • packages/kotlin-sdk/sdk/src/test/kotlin/org/dashfoundation/dashsdk/errors/DashSdkErrorTest.kt
  • packages/rs-platform-wallet-ffi/src/error.rs
  • packages/rs-platform-wallet/src/error.rs
  • packages/rs-platform-wallet/src/wallet/asset_lock/build.rs
  • packages/rs-unified-sdk-jni/src/funding.rs

Comment on lines +272 to +278
/// On a *drain* build (whole-account funding, e.g. the CoinJoin → shielded
/// migration) the requested target is the zero credit-output placeholder
/// that key-wallet rewrites to `Σ inputs − fee`, so an empty account
/// surfaces here as `available: 0, required: 0` — the "this account has
/// nothing to drain" signal. The real floor for a drain is the Type 18 pool
/// fee, enforced downstream against the built payload once the lock value
/// is known.

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.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Correct the drain-shortfall documentation.

An empty drain now reports the positive minimum_lock_duffs as required when the caller provides a drain floor. The current text still promises required: 0.

  • packages/rs-platform-wallet/src/error.rs#L272-L278: describe required: minimum_lock_duffs for a drain with a configured floor. Describe zero only when no floor exists.
  • packages/rs-platform-wallet/src/wallet/asset_lock/build.rs#L994-L999: update the mapper contract to match the minimum_lock_duffs.unwrap_or(0) value passed at Lines 200-205.
📍 Affects 2 files
  • packages/rs-platform-wallet/src/error.rs#L272-L278 (this comment)
  • packages/rs-platform-wallet/src/wallet/asset_lock/build.rs#L994-L999
🤖 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/error.rs` around lines 272 - 278, Update the
drain-shortfall documentation in packages/rs-platform-wallet/src/error.rs lines
272-278 to state that required equals minimum_lock_duffs when a drain floor is
configured, and equals zero only when no floor exists. Also update the mapper
contract in packages/rs-platform-wallet/src/wallet/asset_lock/build.rs lines
994-999 to match the minimum_lock_duffs.unwrap_or(0) value passed by the
asset-lock build flow.

@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/Sol only (Phase 2 disabled)

The Kotlin/JNI CoinJoin-drain binding follows the existing ownership and boundary-validation patterns, and the latest commit fixes the previously reported empty-drain amount pair. Three in-scope suggestions remain: exact-amount funding still erases code 29, Swift does not decode code 29, and the public drain-shortfall documentation still describes the removed 0/0 behavior.
Source: reviewers gpt-5.6-sol (general) and gpt-5.6-sol (FFI engineer); final verifier gpt-5.6-sol. Orchestration-only: openclaw-agent/cliproxy/gpt-5.6-sol (not reviewer evidence).

Validated zero-blocker Codex/Sol precheck evidence was promoted to final because Phase 2 (Sonnet/Opus) is temporarily disabled. This is Codex/Sol-only final validation, not Codex + Sonnet/Opus coverage.

Review provenance

  • Codex reviewers: gpt-5.6-sol — general (completed), gpt-5.6-sol — ffi-engineer (completed)
  • Verifier: gpt-5.6-sol — verifier
  • Sonnet/Opus: not run (Phase 2 disabled — temporary Codex/Sol-only final)
  • Secondary pass: disabled (temporary_phase2_sonnet_disable)

🟡 3 suggestion(s)

2 additional finding(s) omitted (not in diff).

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

In `packages/rs-platform-wallet-ffi/src/shielded_send.rs`:
- [SUGGESTION] packages/rs-platform-wallet-ffi/src/shielded_send.rs:1024-1029: Exact-amount shielded funding erases the typed shortfall
  This endpoint converts every `PlatformWalletError` into `ErrorWalletOperation` (6), bypassing the new blanket conversion that maps `AssetLockInsufficientFunds` to code 29. The Kotlin `shieldedFundFromAssetLock` path invokes this exact endpoint and only constructs `DashSdkError.PlatformWallet.AssetLockInsufficientFunds` when native code 29 reaches it, so an exact-amount coin-selection shortfall still arrives as the generic `WalletOperation` error despite the newly documented typed contract. Preserve the endpoint's contextual catch-all for unrelated failures, but pass the new shortfall variant through `From<PlatformWalletError>`.

In `packages/swift-sdk/Sources/SwiftDashSDK/PlatformWallet/PlatformWalletResult.swift`:
- [SUGGESTION] packages/swift-sdk/Sources/SwiftDashSDK/PlatformWallet/PlatformWalletResult.swift:88-97: Swift does not decode the newly allocated FFI error code
  Rust now emits `ErrorAssetLockInsufficientFunds` with raw value 29, including from the CoinJoin-drain endpoint called by Swift before `result.check()`. Swift only mentions code 29 in the reservation comment: `PlatformWalletResultCode` has no code-29 case, `init(ffi:)` has no corresponding C-enum arm and therefore falls through to `.errorUnknown`, and `PlatformWalletError` has no typed asset-lock shortfall case. Add the result-code case, C-enum conversion, typed error construction and description handling, plus a mapping regression test that pins raw code 29.

In `packages/rs-platform-wallet/src/error.rs`:
- [SUGGESTION] packages/rs-platform-wallet/src/error.rs:272-278: Drain shortfall documentation still promises the removed 0/0 result
  The latest commit passes `minimum_lock_duffs.unwrap_or(0)` to `map_builder_error`, and the shielded flow replaces the caller's value with a positive Type 18 pool-fee floor before building. An empty shielded CoinJoin drain therefore reports `available: 0` and that positive floor as `required`, not 0/0 as these public variant docs claim. The mapper contract in `wallet/asset_lock/build.rs` lines 994-999 repeats the obsolete behavior; update both comments to say that `required` is the configured drain floor, or zero only when no floor is provided.

Comment on lines +272 to +278
/// On a *drain* build (whole-account funding, e.g. the CoinJoin → shielded
/// migration) the requested target is the zero credit-output placeholder
/// that key-wallet rewrites to `Σ inputs − fee`, so an empty account
/// surfaces here as `available: 0, required: 0` — the "this account has
/// nothing to drain" signal. The real floor for a drain is the Type 18 pool
/// fee, enforced downstream against the built payload once the lock value
/// is known.

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.

🟡 Suggestion: Drain shortfall documentation still promises the removed 0/0 result

The latest commit passes minimum_lock_duffs.unwrap_or(0) to map_builder_error, and the shielded flow replaces the caller's value with a positive Type 18 pool-fee floor before building. An empty shielded CoinJoin drain therefore reports available: 0 and that positive floor as required, not 0/0 as these public variant docs claim. The mapper contract in wallet/asset_lock/build.rs lines 994-999 repeats the obsolete behavior; update both comments to say that required is the configured drain floor, or zero only when no floor is provided.

source: ['codex', 'coderabbit']

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