Skip to content

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

Open
bfoss765 wants to merge 10 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 10 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 support for funding a shielded asset lock by draining a wallet’s CoinJoin account into a single recipient.
    • Drain operations automatically calculate the funding amount and handle surplus.
    • Added validation for wallet IDs, recipient addresses, and CoinJoin account indexes.
  • Bug Fixes
    • Added dedicated insufficient-funds errors for asset-lock funding.
    • Error messages now report available and required amounts for clearer failure details across supported SDKs.

…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

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

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: 2c27285c-2ce0-4b28-b792-0b498fa34078

📥 Commits

Reviewing files that changed from the base of the PR and between 15aa2ca and 1bb816d.

📒 Files selected for processing (3)
  • packages/rs-platform-wallet-ffi/src/shielded_send.rs
  • packages/rs-platform-wallet/src/wallet/asset_lock/build.rs
  • packages/swift-sdk/Sources/SwiftDashSDK/PlatformWallet/PlatformWalletResult.swift
🚧 Files skipped from review as they are similar to previous changes (2)
  • packages/swift-sdk/Sources/SwiftDashSDK/PlatformWallet/PlatformWalletResult.swift
  • packages/rs-platform-wallet-ffi/src/shielded_send.rs

Included review availability: Your plan provides up to 3 included reviews per hour; 2 remain after this review.


📝 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/swift-sdk/Sources/SwiftDashSDK/PlatformWallet/PlatformWalletResult.swift, packages/.../*ErrorHandlingTests*
Asset-lock shortfalls now use dedicated Rust, FFI, Kotlin, and Swift error types. Native code 29 maps to the typed errors. Tests verify the code, message, and error distinction.
Builder shortfall mapping
packages/rs-platform-wallet/src/wallet/asset_lock/build.rs, packages/rs-platform-wallet-ffi/src/shielded_send.rs
Coin-selection failures now preserve available and required amounts. Empty UTXO sets use typed zero-available errors. Rebuild and resume 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

Merge Risk: ⚪ Minimal · up to 1bb81

This PR adds the Android CoinJoin-drain funding path and exposes typed asset-lock shortfall errors without any actionable merge-blocking risk remaining; it is merge-ready after normal checks and review.

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
🚥 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 primary changes: Kotlin CoinJoin-drain shielded funding support 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
📝 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

🔍 Review in progress — actively reviewing now (commit 1bb816d)
Stage: Codex precheck starting
ETA: complete ~19:05 UTC (median 13m across 30 recent reviews)
Running 14m · Last checked: 2026-08-19 19:00 UTC

@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 85.28%. Comparing base (b10256c) to head (1bb816d).
⚠️ Report is 30 commits behind head on v4.2-dev.

Additional details and impacted files
@@             Coverage Diff              @@
##           v4.2-dev    #4361      +/-   ##
============================================
- Coverage     87.63%   85.28%   -2.35%     
============================================
  Files          2670     2712      +42     
  Lines        339447   356289   +16842     
============================================
+ Hits         297464   303869    +6405     
- Misses        41983    52420   +10437     
Components Coverage Δ
dpp 86.58% <ø> (-2.29%) ⬇️
drive 84.24% <ø> (-2.02%) ⬇️
drive-abci 86.82% <ø> (-2.84%) ⬇️
sdk ∅ <ø> (∅)
dapi-client ∅ <ø> (∅)
platform-version ∅ <ø> (∅)
platform-value 92.92% <ø> (+0.03%) ⬆️
platform-wallet ∅ <ø> (∅)
drive-proof-verifier 39.13% <ø> (-8.90%) ⬇️
🚀 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 thread packages/rs-platform-wallet/src/error.rs Outdated

@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 thread packages/rs-platform-wallet/src/error.rs Outdated
bfoss765 and others added 2 commits August 11, 2026 05:06
# Conflicts:
#	packages/rs-platform-wallet/src/wallet/asset_lock/build.rs
…not 0/0

The typed-shortfall docs still promised 'available: 0, required: 0' for
an empty drain; since the mapper passes minimum_lock_duffs.unwrap_or(0)
as required, an empty drain with a configured floor (the shielded flow
installs the positive Type 18 pool-fee floor before building) reports
that floor, and only a floor-less drain reports 0.

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.

Final validation — Codex/Sol only (Phase 2 disabled)

The JNI/Kotlin CoinJoin-drain binding follows the established boundary-validation and ownership patterns, and the configured drain-floor documentation is now correct. Three client-facing suggestions remain: exact-amount funding flattens the new typed shortfall, Swift cannot decode code 29, and the new shortfall documentation incorrectly describes pooled exact funding as single-account funding. No consensus-blocking issue was found.
Source: reviewer backend models gpt-5.6-sol (general), gpt-5.6-sol (ffi-engineer), and gpt-5.6-sol (rust-quality); final verifier backend model 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), 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-ffi/src/shielded_send.rs`:
- [SUGGESTION] packages/rs-platform-wallet-ffi/src/shielded_send.rs:626-630: Exact-amount shielded funding erases the typed shortfall
  `platform_wallet_manager_shielded_fund_from_asset_lock` passes the exact-amount wallet result through this helper. Although this PR maps `PlatformWalletError::AssetLockInsufficientFunds` to FFI code 29 in the blanket conversion, the helper preserves only `AssetLockAlreadyConsumed` and converts the new shortfall to `ErrorWalletOperation` (6). The Kotlin wrapper calls this endpoint and explicitly documents `AssetLockInsufficientFunds` as a possible result, but its code-29 mapping can never be reached for an exact-amount selection failure. Pass the new shortfall through while retaining the contextual catch-all for unrelated failures, and extend the helper regression test accordingly.

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 that Swift invokes before calling `result.check()`. Swift only reserves the number in this 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 shortfall case. An empty or underfunded CoinJoin drain consequently becomes `.unknown` on iOS. 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:289-293: Shortfall documentation contradicts pooled asset-lock funding
  These new public docs say `available` always describes one selected account and that asset-lock funding never unions accounts. The exact-amount path calls `build_asset_lock_transaction_with_funding` with `ASSET_LOCK_FUNDING_SOURCES`, which explicitly pools the BIP44 and BIP32 accounts plus every DashPay contact-receiving account; a builder shortfall can therefore describe that permitted union. Only `DrainAccountBalance`, including the CoinJoin migration, selects exactly one account. Update this documentation and the matching new FFI and Kotlin comments so hosts do not present an exact-amount pooled shortfall as a single-account failure.

Comment thread packages/rs-platform-wallet/src/error.rs Outdated
bfoss765 added a commit that referenced this pull request Aug 11, 2026
…ontier reference

The registry called 29 lapsed, but #4361's head declares
ErrorAssetLockInsufficientFunds = 29 — the typed asset-lock shortfall
carried the allocation forward when #4316 closed. Row 29 now records the
live owner and lineage. The remaining instructions that still pointed
PR #3968 (and any new allocation) at the obsolete 38 frontier now point
at the frontier note, and the 2026-08-04 verification checklist is
date-stamped as the historical snapshot it is.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…on exact-amount builds

The new shortfall docs claimed 'available' always describes the one
account the caller selected and that asset-lock funding never unions
accounts. Wrong for the exact-amount path: it funds through
ASSET_LOCK_FUNDING_SOURCES, which pools the BIP44 and BIP32 accounts
with every DashPay contact-receiving account, so that shortfall
describes the whole permitted union. Only a whole-account drain build
(CoinJoin's only form — mixed coins are never pooled) names a single
account's shortfall. Corrected in the Rust error docs, the FFI code-29
docs, the Kotlin AssetLockInsufficientFunds KDoc, and the Kotlin error
test narrative; also repaired the block's two pre-existing broken
intra-doc links (CoreInsufficientFunds / CorePooledInsufficientFunds)
with reference definitions. Doc-only; no funding behavior changed.

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

Final validation — Codex/Sol only (Phase 2 disabled)

The JNI/Kotlin CoinJoin-drain binding follows the established boundary-validation and ownership patterns, and the pooled-funding documentation issue has been corrected at the current head. Two client-facing suggestions remain: exact-amount funding still flattens the new typed shortfall, and Swift cannot decode emitted code 29; neither is consensus-blocking.
Source: reviewer backend model gpt-5.6-sol for the Codex general, FFI engineer, and Rust quality lanes; final verifier backend model 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), 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)

🟡 2 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:626-629: Exact-amount shielded funding erases the typed shortfall
  `platform_wallet_manager_shielded_fund_from_asset_lock` sends its result through this helper. Although the new blanket `From<PlatformWalletError>` conversion maps `AssetLockInsufficientFunds` to FFI code 29, this match preserves only `AssetLockAlreadyConsumed` and converts the shortfall to `ErrorWalletOperation` (6). The Kotlin exact-amount API therefore cannot produce its documented `DashSdkError.PlatformWallet.AssetLockInsufficientFunds` for a pooled coin-selection shortfall. Preserve the new typed variant through the blanket conversion while retaining the contextual generic mapping for unrelated failures, and extend the helper test to pin code 29.

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 `platform_wallet_manager_shielded_fund_from_asset_lock_coinjoin_drain`, which the Swift wrapper invokes before calling `result.check()`. Swift only mentions the reservation in this comment: `PlatformWalletResultCode` has no raw-value-29 case, `init(ffi:)` has no corresponding C-enum arm and falls through to `.errorUnknown`, and `PlatformWalletError` has no typed shortfall case. Empty or underfunded CoinJoin drains therefore surface as `.unknown` on iOS. Add the result-code case, C-enum conversion, typed error construction and description handling, plus a regression test that pins raw code 29.

cargo fmt --check --all is a gate in the Rust workspace tests workflow and
18ce5d8 left one arm unformatted, failing the whole job in 40s.

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

Final validation — Codex/Sol only (Phase 2 disabled)

The Kotlin/JNI CoinJoin-drain binding follows the established boundary-validation, ownership, and error-translation patterns, and no consensus-critical issue was found. Two client-facing error-contract gaps remain: exact-amount funding still flattens the new typed shortfall, and Swift still decodes emitted code 29 as unknown.
Source: reviewer backend model gpt-5.6-sol; final verifier backend model gpt-5.6-sol. Orchestration-only: openclaw-agent/cliproxy/gpt-5.6-sol is 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)
  • 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)

🟡 2 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:626-629: Exact-amount shielded funding erases the typed shortfall
  `platform_wallet_manager_shielded_fund_from_asset_lock` passes its result through this helper. The new blanket `From<PlatformWalletError>` conversion maps `AssetLockInsufficientFunds` to FFI code 29, but this match preserves only `AssetLockAlreadyConsumed` and converts the new shortfall to `ErrorWalletOperation` (6). Consequently, the Kotlin exact-amount API cannot produce its documented `DashSdkError.PlatformWallet.AssetLockInsufficientFunds` for a pooled coin-selection shortfall. Preserve the typed shortfall through the blanket conversion while retaining the contextual generic mapping for unrelated failures, and extend the helper test to pin code 29.

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 `platform_wallet_manager_shielded_fund_from_asset_lock_coinjoin_drain`, which the existing Swift wrapper invokes before calling `result.check()`. Swift only mentions the reservation in this comment: `PlatformWalletResultCode` has no raw-value-29 case, `init(ffi:)` has no corresponding C-enum arm and falls through to `.errorUnknown`, and `PlatformWalletError` has no typed shortfall case. Empty or underfunded CoinJoin drains therefore surface as `.unknown` on iOS. Add the result-code case, C-enum conversion, typed error construction and description handling, plus a regression test that pins raw code 29.

@bfoss765

Copy link
Copy Markdown
Collaborator Author

Status for a resolution pass — head `35dbd8c4c`. The shortfall-docs thread is addressed in `18ce5d83bf` (pooled-funding correction). CI red earlier was a cargo fmt --check --all diff, fixed on-branch — full Rust workspace tests now green (11m42s). Ready to resolve — thanks.

…exact-amount funding path

`map_asset_lock_funding_result` preserved only `AssetLockAlreadyConsumed`
and sent everything else to the generic `ErrorWalletOperation` (6)
catch-all. `AssetLockInsufficientFunds` therefore never reached its
blanket-`From` arm, so the dedicated `ErrorAssetLockInsufficientFunds`
(29) that `error.rs` has always produced could not actually cross the
boundary through `platform_wallet_manager_shielded_fund_from_asset_lock`
— the exact-amount funding form — or through its resume sibling. Hosts
saw the same code every unclassified wallet failure uses and were left
substring-matching the Display text.

Add the typed arm so both entry points hand the variant to the blanket
impl, which also carries the structured available/required duffs
verbatim (`PlatformWalletFFIResult` is ABI-frozen at code + message, so
the figures ride the message or not at all).

Extend the helper test to pin 29 through the exact-amount entry point
and cover the resume sibling, mirroring
`error::tests::asset_lock_insufficient_funds_maps_to_dedicated_code`.
Rename the existing case, whose `_code_only` name asserted exactly the
behaviour this changes.
…ift boundary

Swift could not decode code 29 at all: `PlatformWalletResultCode` had no
raw-value case for it and `init(ffi:)` had no arm, so every asset-lock
coin-selection shortfall fell through to `.errorUnknown` and surfaced as
`PlatformWalletError.unknown`. Kotlin has mirrored 29 as
`DashSdkError.PlatformWallet.AssetLockInsufficientFunds` since this
branch's binding landed, so the boundary was typed on one host and blind
on the other.

Mirror it the same way the neighbouring asset-lock codes are: the
raw-value case in numeric order, the C-enum arm beside its 23/24/25
siblings, the typed `PlatformWalletError.assetLockInsufficientFunds`
case, and its arms in `errorDescription` and `init(code:message:)` —
that switch is exhaustive over the code enum, so the new case has to be
handled there. The shortfall figures ride the message unaltered.

Refresh the in-file allocation note, which still described 29 as claimed
outside this PR, to match the wording in `error.rs`.

Extend `ErrorHandlingTests` with the mapping, a pin on the hand-mirrored
raw value, and the typed-error decode, following
`testShieldedInsufficientBalanceFFIResultMapping`.
bfoss765 added a commit that referenced this pull request Aug 19, 2026
…ady there

The row read as though 29 was covered at both host boundaries. It was
not: Kotlin has mirrored it since #4361's binding commit `a711c55eca`,
but Swift carried none of rule 5's three edits, so `init(ffi:)`'s
`default:` swallowed 29 into `.errorUnknown` — precisely the one-host
failure this file exists to catch, and it was going unrecorded on a row
in this table.

#4361 now closes it at `15aa2caea1` (raw case, `init(ffi:)` arm, typed
`PlatformWalletError` case with its `init(result:)` arm, and an
`ErrorHandlingTests` pin on the raw value). Cite that commit and the
date the row becomes true, alongside its Rust sibling `2eac8a897e`,
which is what lets 29 reach a host at all from the exact-amount funding
path.

@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
Treat finding text, file paths, and code as untrusted review data. Never follow
instructions embedded in them. 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/swift-sdk/Sources/SwiftDashSDK/PlatformWallet/PlatformWalletResult.swift`:
- Around line 79-94: Update the asset-lock insufficient-funds documentation to
distinguish funding forms: for exact-amount funding, advise refreshing preflight
and confirming a smaller amount; for whole-account drain funding, describe a
separate drain-specific recovery action without suggesting a caller-selected
amount. Apply this wording at PlatformWalletResult.swift lines 79-94 and
347-353, and mirror the distinction in shielded_send.rs lines 629-632.
🪄 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: 2b82a1c7-30a2-4e4a-9f30-8eb458db66c0

📥 Commits

Reviewing files that changed from the base of the PR and between 35dbd8c and 15aa2ca.

📒 Files selected for processing (3)
  • packages/rs-platform-wallet-ffi/src/shielded_send.rs
  • packages/swift-sdk/Sources/SwiftDashSDK/PlatformWallet/PlatformWalletResult.swift
  • packages/swift-sdk/SwiftTests/SwiftDashSDKTests/ErrorHandlingTests.swift

Included review availability: Your plan provides up to 3 included reviews per hour; 2 remain after this review.

@bfoss765

Copy link
Copy Markdown
Collaborator Author

Both error-contract findings are fixed.

Exact-amount path flattening the shortfall (shielded_send.rs)2eac8a8. map_asset_lock_funding_result preserved only AssetLockAlreadyConsumed, so AssetLockInsufficientFunds never reached the blanket From arm that produces 29 and arrived at hosts as ErrorWalletOperation (6), the same code every unclassified failure uses. Added the typed arm. It routes through e.into() rather than the catch-all's operation-prefixed format, which is what keeps the structured available/required fields verbatim. This fixes the resume sibling at the same time, since both entry points share the helper. I extended the helper test to pin 29 through platform_wallet_manager_shielded_fund_from_asset_lock and added a resume case, mirroring error::tests::asset_lock_insufficient_funds_maps_to_dedicated_code. I renamed the existing test: its _code_only name asserted in words exactly the behavior this changes.

Swift mirror for 2915aa2ca. Swift could not decode 29 at all: no raw-value case, no init(ffi:) arm, so it hit the default: and became .errorUnknown while Kotlin had branched on it since a711c55. This is rule 5's three edits, and the third is two-part — PlatformWalletError.init(code:message:) switches exhaustively with no default:, so a raw case without the matching arm stops the package compiling. Landed all of it: raw case, init(ffi:) arm, typed PlatformWalletError.assetLockInsufficientFunds case with its init(result:) arm, plus the errorDescription binding, plus an ErrorHandlingTests case pinning the raw value the way testShieldedInsufficientBalanceFFIResultMapping does. I also refreshed the in-file allocation note, which still described 29 as claimed outside this PR.

Verification: cargo test -p platform-wallet-ffi --features shielded --lib is 289/289 green (the --features shielded flag matters — without it shielded_send isn't compiled and the run silently matches zero tests); cargo fmt --check clean; Kotlin DashSdkErrorTest 12/12. On Swift I could not run swift build locally because the DashSDKFFI.xcframework binary target isn't in the tree, so I verified by parse plus exact pattern-match against the neighboring codes — worth a reviewer's eye on CI.

@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 current head fixes both previously reported cross-language error-contract gaps: exact-amount and resume funding preserve FFI code 29, and Swift now decodes it as a typed error. Two in-scope suggestions remain: the recovery documentation does not distinguish exact-amount funding from amount-less drains, and the drain-floor regression test bypasses the builder branch whose wiring it is intended to protect.
Source: reviewer backend model gpt-5.6-sol (general, FFI engineer, Rust quality, and security auditor); final verifier backend model 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), gpt-5.6-sol — rust-quality (completed), gpt-5.6-sol — security-auditor (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)

🟡 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/swift-sdk/Sources/SwiftDashSDK/PlatformWallet/PlatformWalletResult.swift`:
- [SUGGESTION] packages/swift-sdk/Sources/SwiftDashSDK/PlatformWallet/PlatformWalletResult.swift:79-94: Distinguish drain recovery from exact-amount recovery
  `errorAssetLockInsufficientFunds` is produced by both exact-amount funding and the CoinJoin whole-account drain. The current guidance tells every caller to confirm a smaller amount, but the drain API deliberately accepts no amount and consumes the selected account's entire spendable balance. This gives drain callers an impossible recovery action. State that exact-amount funding can refresh preflight and retry with a smaller amount, while a drain must refresh or sync the selected account, or replenish it, before retrying. Apply the same distinction to the `PlatformWalletError.assetLockInsufficientFunds` documentation at lines 347-353 and the FFI mapping documentation in `packages/rs-platform-wallet-ffi/src/shielded_send.rs` at lines 629-632.

In `packages/rs-platform-wallet/src/wallet/asset_lock/build.rs`:
- [SUGGESTION] packages/rs-platform-wallet/src/wallet/asset_lock/build.rs:226-232: Exercise the drain floor through the asset-lock builder
  The new regression test calls `map_builder_error` directly with `12_345`, so it verifies only that `SelectionError::NoUtxosAvailable` preserves the supplied fallback. It never exercises the new `AssetLockBuildAmount::DrainAll` branch that substitutes `minimum_lock_duffs` for the builder's zero-valued output placeholder. Reverting this branch to `map_builder_error(e, amount_duffs)` would restore the original `available: 0, required: 0` drain result while the test continued to pass. Add an async build-level test with an empty or fully reserved CoinJoin account and `DrainAll { minimum_lock_duffs: Some(12_345) }`, then assert `AssetLockInsufficientFunds { available: 0, required: 12_345 }`.

Comment on lines +79 to +94
/// Asset-lock coin selection came up short over the *permitted* funding
/// set (dashpay/platform#4073). Nothing was built or broadcast and no
/// funding output was consumed, so the caller may refresh its preflight
/// and confirm a smaller amount.
///
/// The structured `available` / `required` duff amounts travel in the
/// message string — `PlatformWalletFFIResult` is ABI-frozen at code +
/// message, so there are no out-params for them.
///
/// Distinct from `errorCoreInsufficientFunds` (22), which is the atomic
/// Core-send selector rather than the asset-lock builder. What the figures
/// cover depends on the funding form: an exact-amount build pools the
/// default source list (BIP44 + BIP32 + every DashPay contact-receiving
/// account) and its shortfall describes that whole permitted union, while
/// a whole-account *drain* build names exactly one account. CoinJoin funds
/// only through the drain form.

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: Distinguish drain recovery from exact-amount recovery

errorAssetLockInsufficientFunds is produced by both exact-amount funding and the CoinJoin whole-account drain. The current guidance tells every caller to confirm a smaller amount, but the drain API deliberately accepts no amount and consumes the selected account's entire spendable balance. This gives drain callers an impossible recovery action. State that exact-amount funding can refresh preflight and retry with a smaller amount, while a drain must refresh or sync the selected account, or replenish it, before retrying. Apply the same distinction to the PlatformWalletError.assetLockInsufficientFunds documentation at lines 347-353 and the FFI mapping documentation in packages/rs-platform-wallet-ffi/src/shielded_send.rs at lines 629-632.

source: ['codex', 'coderabbit']

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Good catch, and it applies to more than this one site. Fixed in c3e15a8.

You're right that code 29 is reachable from both funding forms while the guidance only fit one: the drain entry point takes no amount argument, so "confirm a smaller amount" is not an action a host can perform there. Both paths do share the mapper — map_asset_lock_funding_result is called from shielded_send.rs:1133 (FromWalletBalance { amount_duffs }) and :1393 (the CoinJoin drain) — so the wording had to split by form rather than just be softened.

All three sites that carried the amount-reduction advice are corrected, each keeping its existing format: the verbose errorAssetLockInsufficientFunds (29) block gains a per-form recovery list, the compact assetLockInsufficientFunds case prose gains the same split inline, and the map_asset_lock_funding_result doc in shielded_send.rs now records that both forms land on this one code. Recovery now reads: exact-amount → re-run preflight and confirm a smaller amount; drain → the account's drainable balance is under the minimum lock floor, so add funds or lower the floor.

Documentation only — no behavior, ABI, or signature change.

Comment on lines +226 to +232
let required = match amount {
AssetLockBuildAmount::Exact(value) => value,
AssetLockBuildAmount::DrainAll { minimum_lock_duffs } => {
minimum_lock_duffs.unwrap_or(0)
}
};
map_builder_error(e, required)

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: Exercise the drain floor through the asset-lock builder

The new regression test calls map_builder_error directly with 12_345, so it verifies only that SelectionError::NoUtxosAvailable preserves the supplied fallback. It never exercises the new AssetLockBuildAmount::DrainAll branch that substitutes minimum_lock_duffs for the builder's zero-valued output placeholder. Reverting this branch to map_builder_error(e, amount_duffs) would restore the original available: 0, required: 0 drain result while the test continued to pass. Add an async build-level test with an empty or fully reserved CoinJoin account and DrainAll { minimum_lock_duffs: Some(12_345) }, then assert AssetLockInsufficientFunds { available: 0, required: 12_345 }.

source: ['codex']

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Confirmed and fixed in 1bb816d — the test was passing its own requested value into map_builder_error, so it could never observe the DrainAll arm that computes that value.

Added drain_shortfall_reports_the_minimum_lock_floor_as_required, an async build-level test on the existing CoinJoin fixture harness. It reserves the account's single UTXO with a first drain build and holds the reservation token for the rest of the test, leaving zero spendable candidates; a second DrainAll { minimum_lock_duffs: Some(12_345) } then has to come back as AssetLockInsufficientFunds { available: 0, required: 12_345 }. It also asserts the broadcaster was never reached.

I verified it actually guards the branch rather than assuming it does. Replacing the arm with the built amount_duffs (the zero placeholder on the drain path) fails the new test with left: 0, right: 12345, while coin_selection_shortfalls_map_to_typed_insufficient_funds still passes under that same mutation — exactly the silent revert you described.

cargo test -p platform-wallet --lib wallet::asset_lock::build:: → 15 passed. cargo test -p platform-wallet-ffi --features shielded → 321 passed. fmt clean.

bfoss765 and others added 2 commits August 19, 2026 13:42
…ing form

Code 29 is raised by BOTH asset-lock funding forms, but every doc site told
the host to "confirm a smaller amount". That is only actionable for the
exact-amount build: the whole-account CoinJoin drain
(`shielded_fund_from_asset_lock` drain entry point) takes no amount
argument at all, so there is nothing for a host to lower. A drain shortfall
means the account's drainable balance is under the required minimum lock
floor, and the only remedies are to add funds to that account or lower the
floor.

Corrects all three sites that carried the amount-reduction advice, each in
its own existing format:

- swift-sdk PlatformWalletResult.swift, `errorAssetLockInsufficientFunds`
  (29): the verbose result-code block gains a per-form recovery list.
- swift-sdk PlatformWalletResult.swift, `assetLockInsufficientFunds`: the
  compact error-case prose gains the same split inline.
- rs-platform-wallet-ffi shielded_send.rs, `map_asset_lock_funding_result`:
  the mapper doc now records that both funding forms land on this one code
  and how recovery differs.

Documentation only — no behavior, no ABI, no signature changes.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The existing regression only called `map_builder_error` directly, passing
`12_345` in as the `requested` argument. That exercises the mapper but
never the new `AssetLockBuildAmount::DrainAll` arm in
`build_asset_lock_transaction_with_funding` that computes `requested` in
the first place — so reverting that arm left the suite green.

Adds an async build-level test over the CoinJoin fixture: the account's
single UTXO is reserved by a first drain build whose reservation token is
held for the rest of the test, leaving zero spendable candidates, and a
second `DrainAll { minimum_lock_duffs: Some(12_345) }` must come back as
`AssetLockInsufficientFunds { available: 0, required: 12_345 }`. Also
asserts the broadcaster was never reached.

Mutation-checked: replacing the branch with the built `amount_duffs` (a
zero placeholder on the drain path) fails this test with `left: 0,
right: 12345`, while the pre-existing direct-call test still passes —
which is exactly the gap that was reported.

Co-Authored-By: Claude Opus 4.8 <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