Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
8db6319
fix(asset-lock): multi-account funding + watch-only exclusion, riding…
bfoss765 Jul 16, 2026
857b007
fix(asset-lock): FFI code for AssetLockInsufficientFunds + single-pri…
bfoss765 Jul 21, 2026
ab00db0
fix(ffi): renumber asset-lock error codes 26/27 -> 29/30
bfoss765 Jul 21, 2026
b53edb6
fix(asset-lock): reclassify fee-band shortfall to consent-required
bfoss765 Jul 21, 2026
5ba827b
fix(swift-sdk): make PlatformWalletError switch exhaustive (#4184 blo…
bfoss765 Jul 22, 2026
3cde050
feat(swift-example): cross-domain funding consent toggle (#4184)
bfoss765 Jul 22, 2026
da35530
chore(asset-lock): drop tracker tokens from build.rs; note C-ABI brea…
bfoss765 Jul 22, 2026
1ec6d52
refactor(kotlin-sdk): re-scope shieldedFundFromAssetLock to a single …
bfoss765 Jul 22, 2026
464971e
fix(swift-sdk): re-scope shieldedFundFromAssetLock to optional fundin…
bfoss765 Jul 23, 2026
7da66a3
fix(kotlin-sdk): remove dangling cross-domain references left by the …
bfoss765 Jul 23, 2026
51bee5d
fix(asset-lock): reserve selected-account inputs + restore FFI/JNI co…
bfoss765 Jul 23, 2026
fa71587
fix(asset-lock): pass selected account xpub to set_funding (#4184 rev…
bfoss765 Jul 24, 2026
550b6bc
fix(asset-lock): release UTXO reservation on every abandon-before-bro…
bfoss765 Jul 26, 2026
9c1c89b
fix(asset-lock): close the delegated builder's unreleasable reservati…
bfoss765 Aug 1, 2026
d02669e
fix(asset-lock): satisfy the wallet fmt + clippy CI gates
bfoss765 Aug 2, 2026
8dfb227
docs(ffi): use the crate's dominant "behavior" spelling in the ABI note
bfoss765 Aug 3, 2026
11c3677
docs(asset-lock): mark the abandon-path reservation release PRIVACY-D…
bfoss765 Aug 3, 2026
2029cd0
test(asset-lock): cover selected-account rollback on post-reservation…
bfoss765 Aug 5, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,21 @@ sealed class DashSdkError(
class AssetLockFundingMismatch(message: String, cause: Throwable? = null) :
PlatformWallet(message, cause)

/**
* `ErrorAssetLockInsufficientFunds` (native code 29). Asset-lock coin
* selection came up short over the *permitted* funding set — the requested
* amount plus the L1 fee exceeds the spendable funds the selector was
* allowed to draw on. Raised strictly pre-broadcast (while BUILDING the
* asset-lock transaction), so nothing reached the wire. The structured
* `available`/`required` duff amounts travel in [message]
* (`"asset lock coin selection is short: available N duffs, required M
* duffs"`); the typed code lets callers branch without substring-matching.
* Distinct from [CoreInsufficientFunds] (code 22), which is the atomic
* Core-send selector rather than the asset-lock builder.
*/
class AssetLockInsufficientFunds(message: String, cause: Throwable? = null) :
PlatformWallet(message, cause)

/**
* `ErrorShieldedNoRecordedAnchor` (native code 19). A shielded spend
* could not be built against a Platform-recorded anchor because the
Expand Down Expand Up @@ -345,11 +360,11 @@ sealed class DashSdkError(
23 -> PlatformWallet.AssetLockNotTracked(message, cause) // ErrorAssetLockNotTracked
24 -> PlatformWallet.AssetLockAlreadyConsumed(message, cause) // ErrorAssetLockAlreadyConsumed
25 -> PlatformWallet.AssetLockFundingMismatch(message, cause) // ErrorAssetLockFundingMismatch
29 -> PlatformWallet.AssetLockInsufficientFunds(message, cause) // ErrorAssetLockInsufficientFunds
// ErrorSigningKeyUnavailable — the STRUCTURED signer
// discriminator (dashpay/platform#4060 finding 7): the typed
// completion code rides the whole Rust round-trip, no message
// sniffing involved. (Codes 26-30 are reserved by sibling PRs
// #4185 / #4184 — see PlatformWalletFFIResultCode.)
// sniffing involved.
31 -> PlatformWallet.SigningKeyUnavailable(message, cause)
else ->
// @Deprecated fallback — see the code-6 arm; code 31 is the
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,8 +49,15 @@ internal object FundingNative {
* `platform_wallet_manager_shielded_fund_from_asset_lock`).
* [recipientRaw43] is the 43-byte raw Orchard address; [surplusOutput]
* is the optional 21-byte remainder platform address (null = none);
* [coreSignerHandle] is the manager's `MnemonicResolverHandle`. Blocks
* for the ~30s Halo 2 proof; the note arrives on the next shielded sync.
* [coreSignerHandle] is the manager's `MnemonicResolverHandle`.
* [fundingPath] is an optional UTF-8 BIP32 derivation-path string
* (dashpay/platform#4184) naming the single funds account whose UTXOs fund
* the lock: null (the default) funds from the unmixed BIP44 account at
* [fundingAccountIndex]; an explicit account-level path (e.g. the DIP-9
* CoinJoin account path) funds strictly from that one account, with no union
* across accounts and no consent gate. A shortfall throws
* [org.dashfoundation.dashsdk.errors.DashSdkError.PlatformWallet.AssetLockInsufficientFunds].
* Blocks for the ~30s Halo 2 proof; the note arrives on the next shielded sync.
*/
external fun shieldedFundFromAssetLock(
managerHandle: Long,
Expand All @@ -60,6 +67,7 @@ internal object FundingNative {
recipientRaw43: ByteArray,
surplusOutput: ByteArray?,
coreSignerHandle: Long,
fundingPath: String?,
)

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1397,16 +1397,30 @@ class PlatformWalletManager(
* @param walletId the 32-byte wallet id.
* @param recipientRaw43 the 43-byte raw Orchard payment address
* (11-byte diversifier + 32-byte pk_d).
* @param fundingAccountIndex the Core BIP44 account funding the lock.
* @param fundingAccountIndex the Core BIP44 account that always sinks the
* asset lock's transparent CHANGE, and — when [fundingPath] is `null` —
* is also the account whose UTXOs fund the lock. With an explicit
* [fundingPath] the inputs come exclusively from the account that path
* names and this index remains only the (still required) change sink.
* @param amountDuffs the L1 lock amount in duffs.
* @param surplusOutput optional 21-byte remainder platform address.
* @param fundingPath optional UTF-8 BIP32 derivation-path string
* (dashpay/platform#4184) naming the single funds account whose UTXOs fund
* the lock. Defaults to `null`, which funds from the unmixed BIP44 account
* at [fundingAccountIndex]. Pass an explicit account-level path (e.g. the
* DIP-9 CoinJoin account path) to fund strictly from that one account — to
* shield previously-mixed CoinJoin coins, for instance. There is no union
* across accounts and no consent gate: exactly one funding source
* participates, and if it cannot cover the lock this throws
* [org.dashfoundation.dashsdk.errors.DashSdkError.PlatformWallet.AssetLockInsufficientFunds].
*/
suspend fun shieldedFundFromAssetLock(
walletId: ByteArray,
recipientRaw43: ByteArray,
amountDuffs: Long,
fundingAccountIndex: Int = 0,
surplusOutput: ByteArray? = null,
fundingPath: String? = null,
): Unit = teardownGate.op {
require(amountDuffs > 0) { "amountDuffs must be positive, got $amountDuffs" }
require(fundingAccountIndex >= 0) {
Expand All @@ -1421,6 +1435,7 @@ class PlatformWalletManager(
recipientRaw43,
surplusOutput,
mnemonicResolverHandle,
fundingPath,
)
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,9 @@ class DashSdkErrorTest {
23 to DashSdkError.PlatformWallet.AssetLockNotTracked::class,
24 to DashSdkError.PlatformWallet.AssetLockAlreadyConsumed::class,
25 to DashSdkError.PlatformWallet.AssetLockFundingMismatch::class,
// dashpay/platform#4073 request 3 + #4184: the asset-lock shortfall
// gets a dedicated type, not Generic.
29 to DashSdkError.PlatformWallet.AssetLockInsufficientFunds::class,
)
recoveryCodes.forEach { (code, expected) ->
val mapped = DashSdkError.fromNative(DashSDKException(offset + code, "recovery"))
Expand Down
14 changes: 14 additions & 0 deletions packages/rs-platform-wallet-ffi/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -202,6 +202,20 @@ Strings and arrays returned by the library must be freed using the provided free
- `platform_wallet_bytes_free()`
- `platform_wallet_identifier_array_free()`

## ABI stability / Release notes

This crate exposes a C ABI. Changes that alter an exported function's
signature or the numeric value of a result code are **breaking** for C/Swift/JNI
consumers and must be called out here.

- **C-ABI break:** `platform_wallet_manager_shielded_fund_from_asset_lock`
gained a trailing `funding_path_ptr: *const u8, funding_path_len: usize`
parameter — a UTF-8 BIP32 derivation path selecting the single source
account (a null pointer / zero length = the unmixed BIP44 account). Callers
linking the old symbol must be recompiled against the regenerated header;
a null path reproduces the previous single-account behavior. Adds result
code `ERROR_ASSET_LOCK_INSUFFICIENT_FUNDS = 29`.

## License

MIT
Expand Down
4 changes: 4 additions & 0 deletions packages/rs-platform-wallet-ffi/src/asset_lock/build.rs
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,8 @@ pub unsafe extern "C" fn asset_lock_manager_build_transaction(
funding,
identity_index,
&signer,
// Non-shielded funding always uses the single BIP44 account.
None,
))
});
let result = unwrap_option_or_return!(option);
Expand Down Expand Up @@ -149,6 +151,8 @@ pub unsafe extern "C" fn asset_lock_manager_create_funded_proof(
funding,
identity_index,
&signer,
// Non-shielded funding always uses the single BIP44 account.
None,
))
});
let result = unwrap_option_or_return!(option);
Expand Down
87 changes: 76 additions & 11 deletions packages/rs-platform-wallet-ffi/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -215,13 +215,18 @@ pub enum PlatformWalletFFIResultCode {
/// join instead of erroring. Swift mirror:
/// `PlatformWalletResultCode.errorShutdownIncomplete`.
ErrorShutdownIncomplete = 27,
// Codes 28-30 are NOT claimed here. 28 and 30 are reserved (vacated by the
// deferred-payment reservation-token trio on dashpay/platform#4185/#4256
// when it moved to 34-36) and 29 belongs to ErrorAssetLockInsufficientFunds
// on the asset-lock funding branch (dashpay/platform#4184). Allocating any
// of them here too would merge without a textual conflict and silently
// misclassify across hosts. See
// packages/rs-platform-wallet-ffi/ERROR_CODE_REGISTRY.md.
// 28 is deliberately skipped: the deferred-payment reservation-token trio
// (ErrorStaleReservationToken / ErrorReservationTokenConsumed /
// ErrorReservationWalletMismatch) held 27/28/30 on the split-build-broadcast
// branch (dashpay/platform#4185) while this branch was numbered. Allocating
// into that range here would merge without a textual conflict and silently
// misclassify across hosts. See packages/rs-platform-wallet-ffi/ERROR_CODE_REGISTRY.md.
/// Asset-lock coin selection came up short over the *permitted* funding set
/// (dashpay/platform#4073 request 3). Carries the structured
/// `available`/`required` duff amounts in the message string. Distinct from
/// [`Self::ErrorCoreInsufficientFunds`] (22), which is the atomic Core-send
/// selector, not the asset-lock builder.
ErrorAssetLockInsufficientFunds = 29,
/// A state transition could not be signed because the signer has no
/// usable private key for the requested public key — the stored blob is
/// missing, stranded, or written under a different Keystore/Keychain
Expand Down Expand Up @@ -331,14 +336,20 @@ impl<T> From<Option<T>> for PlatformWalletFFIResult {
}
}

impl From<PlatformWalletError> for PlatformWalletFFIResult {
fn from(error: PlatformWalletError) -> Self {
impl PlatformWalletFFIResultCode {
/// Map a [`PlatformWalletError`] to its dedicated FFI code without consuming
/// the error or allocating a message. Kept separate from the `From` impl so
/// FFI entry points that want to keep a *prefixed* message (for host log
/// matchers) can still surface the typed code instead of flattening to
/// `ErrorWalletOperation`/`ErrorUnknown`. The two must stay in lockstep — the
/// `From` impl delegates here.
pub fn for_platform_wallet_error(error: &PlatformWalletError) -> Self {
// Map the typed wallet error variants explicitly so they
// don't flatten to ErrorUnknown at the FFI boundary. The
// catch-all ErrorUnknown remains for variants the FFI hasn't
// assigned a dedicated code yet — those still carry the
// typed Display rendering as the message.
let code = match &error {
match error {
PlatformWalletError::NoSpendableInputs { .. }
| PlatformWalletError::OnlyOutputAddressesFunded { .. }
| PlatformWalletError::OnlyDustInputs { .. } => {
Expand Down Expand Up @@ -405,6 +416,16 @@ impl From<PlatformWalletError> for PlatformWalletFFIResult {
PlatformWalletError::ShutdownIncomplete(..) => {
PlatformWalletFFIResultCode::ErrorShutdownIncomplete
}
// The asset-lock coin-selection shortfall (dashpay/platform#4073
// request 3). Without this arm it flattened to `ErrorUnknown(99)`,
// hiding a typed shortfall behind the catch-all and forcing hosts to
// string-match the Display text. The structured `available`/`required`
// duff amounts still travel in the message (the by-value
// `PlatformWalletFFIResult` has no out-params for them), but the code
// now lets a host branch on the shortfall without parsing text.
PlatformWalletError::AssetLockInsufficientFunds { .. } => {
PlatformWalletFFIResultCode::ErrorAssetLockInsufficientFunds
}
// A signer failure can also reach this blanket impl wrapped as
// `PlatformWalletError::Sdk(dash_sdk::Error::Protocol(..))` (any
// wallet operation that propagates the SDK error via `?`). The
Expand All @@ -424,7 +445,13 @@ impl From<PlatformWalletError> for PlatformWalletFFIResult {
PlatformWalletFFIResultCode::ErrorSigningKeyUnavailable
}
_ => PlatformWalletFFIResultCode::ErrorUnknown,
};
}
}
}

impl From<PlatformWalletError> for PlatformWalletFFIResult {
fn from(error: PlatformWalletError) -> Self {
let code = PlatformWalletFFIResultCode::for_platform_wallet_error(&error);
PlatformWalletFFIResult::err(code, error.to_string())
}
}
Expand Down Expand Up @@ -771,6 +798,44 @@ mod tests {
}
}

/// The asset-lock coin-selection shortfall must cross the FFI boundary as the
/// dedicated `ErrorAssetLockInsufficientFunds` (29) code — NOT `ErrorUnknown`
/// (99) as it did before this arm existed (dashpay/platform#4073 request 3) —
/// and its structured `available`/`required` duffs must survive verbatim in
/// the message so hosts can still parse the amounts.
#[test]
fn asset_lock_insufficient_funds_maps_to_dedicated_code() {
let err = PlatformWalletError::AssetLockInsufficientFunds {
available: 18_000_000,
required: 100_000_000,
};
let rendered = err.to_string();
// Guard the exact text hosts (dash-wallet) substring-match on.
assert!(
rendered.contains("asset lock coin selection is short"),
"shortfall Display text changed — coordinate dash-wallet's matcher \
(rendered: {rendered})"
);
let result: PlatformWalletFFIResult = err.into();
assert_eq!(
result.code,
PlatformWalletFFIResultCode::ErrorAssetLockInsufficientFunds,
"must not flatten to ErrorUnknown(99) (rendered: {rendered})"
);
assert_ne!(
result.code as i32,
PlatformWalletFFIResultCode::ErrorUnknown as i32
);
assert!(!result.message.is_null());
let msg = unsafe { std::ffi::CStr::from_ptr(result.message) }
.to_string_lossy()
.into_owned();
assert_eq!(
msg, rendered,
"structured available/required duffs must survive the FFI boundary verbatim"
);
}

/// `WalletAlreadyExists` maps to the dedicated
/// `ErrorWalletAlreadyExists` FFI code rather than flattening to
/// `ErrorUnknown`, so multi-network wallet create/enable callers can
Expand Down
Loading
Loading