Skip to content
2 changes: 1 addition & 1 deletion docs/sdk/sdk-parity-manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -1060,7 +1060,7 @@
"sdk": "not-applicable",
"example_app": "not-applicable",
"restart": "not_applicable",
"reason": "Android derives contact payment attribution from transaction history on reads and does not consume PaymentEntry rows (confirmed by the Android team during the sent-payment reconstruction review), so the JNI vtable deliberately leaves the slot None and there is nothing to persist or restore on this host."
"reason": null
}
},
"verification": [
Expand Down
15 changes: 8 additions & 7 deletions packages/kotlin-sdk/PARITY_SUMMARY.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,23 +2,23 @@
# Kotlin/Swift executable parity summary

Audit baseline: `PR #3999 @ 6dbc72a54df72d26eb9c4a014b425d2b95134e4e`
Capabilities tracked: **25**
Capabilities tracked: **26**

## Status counts

| Host | Surface | Supported | Partial | Unsupported | Not applicable |
| --- | --- | ---: | ---: | ---: | ---: |
| Swift | SDK | 15 | 8 | 1 | 1 |
| Swift | Example app | 4 | 12 | 1 | 8 |
| Kotlin | SDK | 13 | 12 | 0 | 0 |
| Kotlin | Example app | 5 | 12 | 0 | 8 |
| Swift | SDK | 15 | 9 | 1 | 1 |
| Swift | Example app | 4 | 12 | 1 | 9 |
| Kotlin | SDK | 13 | 12 | 0 | 1 |
| Kotlin | Example app | 5 | 12 | 0 | 9 |

## Restart coverage

| Host | Tested | Required | Not applicable |
| --- | ---: | ---: | ---: |
| Swift | 0 | 7 | 18 |
| Kotlin | 4 | 6 | 15 |
| Swift | 0 | 8 | 18 |
| Kotlin | 4 | 6 | 16 |

## Capability status

Expand All @@ -37,6 +37,7 @@ Capabilities tracked: **25**
| `core.compact_filter_rescan` | supported / partial / not_applicable | partial / partial / not_applicable |
| `dpns.contested_names_by_identity` | supported / partial / not_applicable | partial / partial / required |
| `persistence.sync_fault_latch` | supported / not-applicable / not_applicable | supported / not-applicable / not_applicable |
| `persistence.dashpay_payment_history` | partial / not-applicable / required | not-applicable / not-applicable / not_applicable |
| `network.masternode_discovery` | partial / not-applicable / not_applicable | partial / not-applicable / not_applicable |
| `identity.platform_address_auto_funding` | partial / partial / not_applicable | partial / partial / not_applicable |
| `tokens.authorization_decisions` | partial / partial / not_applicable | partial / partial / not_applicable |
Expand Down
119 changes: 119 additions & 0 deletions packages/rs-platform-wallet-ffi/src/shielded_send.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1030,6 +1030,125 @@ pub unsafe extern "C" fn platform_wallet_manager_shielded_fund_from_asset_lock(
PlatformWalletFFIResult::ok()
}

/// Fund the shielded pool by DRAINING the wallet's CoinJoin account
/// (`m/9'/coinType'/4'/account_index'`) into a single asset lock.
///
/// Sister to [`platform_wallet_manager_shielded_fund_from_asset_lock`],
/// with two differences:
///
/// 1. **Funding**: instead of coin-selecting an exact amount from a BIP44
/// account, every final CoinJoin UTXO is consumed and the lock value is
/// `Σ inputs − L1 fee`, computed by the builder. There is no amount
/// parameter, and the mixed coins never hop through a transparent BIP44
/// address — this is the CoinJoin → Shielded migration path.
/// 2. **No surplus output**: the single-recipient remainder flow pins the
/// consensus surplus to zero (see the resume sibling's doc), so the
/// parameter is omitted rather than plumbed.
///
/// The recipient receives `lock_value − pool_fee` credits. A stuck lock is
/// resumable via
/// [`platform_wallet_manager_shielded_resume_fund_from_asset_lock`] exactly
/// like a BIP44-funded one. The preflight rejects a drain whose balance
/// could not clear the Type 18 pool fee, so an unrecoverable dust lock is
/// never broadcast.
///
/// # Safety
/// - `wallet_id_bytes` must point to 32 readable bytes.
/// - `recipient_raw_43` must point to 43 readable bytes (raw Orchard
/// payment address: 11-byte diversifier + 32-byte pk_d).
/// - `core_signer_handle` must be a valid, non-destroyed
/// `*mut MnemonicResolverHandle` produced by
/// `dash_sdk_mnemonic_resolver_create`. The caller retains ownership.
#[no_mangle]
pub unsafe extern "C" fn platform_wallet_manager_shielded_fund_from_asset_lock_coinjoin_drain(
handle: Handle,
wallet_id_bytes: *const u8,
account_index: u32,
recipient_raw_43: *const u8,
core_signer_handle: *mut MnemonicResolverHandle,
) -> PlatformWalletFFIResult {
check_ptr!(wallet_id_bytes);
check_ptr!(recipient_raw_43);
check_ptr!(core_signer_handle);

let mut wallet_id = [0u8; 32];
std::ptr::copy_nonoverlapping(wallet_id_bytes, wallet_id.as_mut_ptr(), 32);

let mut recipient_bytes = [0u8; 43];
std::ptr::copy_nonoverlapping(recipient_raw_43, recipient_bytes.as_mut_ptr(), 43);
let recipient = match OrchardAddress::from_raw_bytes(&recipient_bytes) {
Ok(a) => a,
Err(e) => {
return PlatformWalletFFIResult::err(
PlatformWalletFFIResultCode::ErrorInvalidParameter,
format!("invalid Orchard recipient address: {e}"),
);
}
};

// The Type 18 live activity recorder writes to the coordinator's
// shared in-memory store, so resolve the coordinator alongside the
// wallet (same as the BIP44-funded sibling).
let (wallet, coordinator) = match resolve_wallet_and_coordinator(handle, &wallet_id) {
Ok(p) => p,
Err(result) => return result,
};
let network = wallet.network();

// Round-trip the resolver handle through `usize` so the worker
// future's capture is `Send + 'static`.
let core_signer_addr = core_signer_handle as usize;

// Run the proof on a worker thread (8 MB stack) — see the sibling for
// why the Halo 2 synthesis cannot run on the calling thread.
let result = block_on_worker(async move {
// SAFETY: see the fn-level safety doc — the resolver handle
// is pinned alive for the duration of this FFI call.
let asset_lock_signer = unsafe {
MnemonicResolverCoreSigner::new(
core_signer_addr as *mut MnemonicResolverHandle,
wallet_id,
network,
)
};
let prover = CachedOrchardProver::new();
wallet
.shielded_fund_from_asset_lock(
&coordinator,
AssetLockFunding::DrainAccountBalance {
account:
key_wallet::wallet::managed_wallet_info::asset_lock_builder::AssetLockFundingAccount::CoinJoin {
account_index,
},
// The shielded fund flow stamps the authoritative
// pool-fee floor before resolving the funding.
minimum_lock_duffs: None,
},
vec![(recipient, None)],
&asset_lock_signer,
&prover,
// Single-recipient remainder flow: surplus is structurally
// zero, so no surplus output.
None,
// Single real note, no anonymity-set fillers.
0,
None,
// User-facing funding: wait for the ChainLock indefinitely —
// a broadcast asset lock is pending finality, never failed.
None,
)
.await
});
match result {
Ok(()) => PlatformWalletFFIResult::ok(),
// Typed conversion — preserves the broadcast-outcome distinction
// (ErrorTransactionBroadcastUnconfirmed vs ...Rejected) so the host
// can choose resume/do-not-redrain for a possibly-broadcast
// whole-account lock vs safely retrying a rejected build.
Err(e) => e.into(),
}
}

/// Resume a shielded fund-from-asset-lock by outpoint.
///
/// Sister to [`platform_wallet_manager_shielded_fund_from_asset_lock`]:
Expand Down
Loading
Loading