diff --git a/packages/kotlin-sdk/KotlinExampleApp/TEST_PLAN.md b/packages/kotlin-sdk/KotlinExampleApp/TEST_PLAN.md index 6478806f86..8cd3e7e181 100644 --- a/packages/kotlin-sdk/KotlinExampleApp/TEST_PLAN.md +++ b/packages/kotlin-sdk/KotlinExampleApp/TEST_PLAN.md @@ -101,7 +101,7 @@ Most Platform actions have hard preconditions. Establish these fixtures before s | CORE-07 | SPV sync (start / stop / progress) | Core | Essential | ✅ | | Global sync indicator (`GlobalSyncIndicator`) → `platform_wallet_manager_spv_*`. Headers/filters/masternodes advance to tip. | | CORE-08 | QR scan recipient | Core | Manual | ✅ | | `QrScannerScreen` (CameraX), reachable in the Send flow. Emulators can use a virtual camera scene but reliability varies — treat as `Tier=Manual`. | | CORE-09 | Multiple HD accounts (within one wallet) | Core | Common | ✅ | | Account selection / `AccountDetailScreen`; balances per `account_index`. | -| CORE-10 | Multi-recipient Core send | Core | Common | ✅ | | Send flow (`SendTransactionScreen`, Core→Core) → "Add recipient" appends extra address/amount rows → `CoreTransactionBuilder` (one `addOutput` per recipient) → `core_wallet_broadcast_transaction`. One tx with N outputs. | +| CORE-10 | Multi-recipient Core send | Core | Common | ✅ | | Send flow (`SendTransactionScreen`, Core→Core) → "Add recipient" appends extra address/amount rows → `CoreTransactionBuilder` (one `addOutput` per recipient) → `finalizeAtomic` + `core_wallet_broadcast_signed_transaction_v2`. One tx with N outputs. | #### Multiple wallets on one device diff --git a/packages/kotlin-sdk/sdk/src/main/kotlin/org/dashfoundation/dashsdk/ffi/NativePersistenceBridge.kt b/packages/kotlin-sdk/sdk/src/main/kotlin/org/dashfoundation/dashsdk/ffi/NativePersistenceBridge.kt index 54545aba0e..99cba6ec40 100644 --- a/packages/kotlin-sdk/sdk/src/main/kotlin/org/dashfoundation/dashsdk/ffi/NativePersistenceBridge.kt +++ b/packages/kotlin-sdk/sdk/src/main/kotlin/org/dashfoundation/dashsdk/ffi/NativePersistenceBridge.kt @@ -721,8 +721,8 @@ class WalletRestoreData( * * Without this, a restored UTXO on an address BEYOND the gap window * has no derivation-path mapping, so `managed.address_derivation_path` - * (called from `core_wallet_tx_builder_build_signed`) fails and the - * wallet cannot sign a core-to-core spend after a cold restart. Mirror + * (resolved during the signing finalizers) fails and the wallet cannot + * sign a core-to-core spend after a cold restart. Mirror * of the Swift `buildCoreAddressPoolBuffer` slice on `loadWalletList`. */ @JvmField val coreAddressPools: Array, diff --git a/packages/kotlin-sdk/sdk/src/main/kotlin/org/dashfoundation/dashsdk/ffi/WalletManagerNative.kt b/packages/kotlin-sdk/sdk/src/main/kotlin/org/dashfoundation/dashsdk/ffi/WalletManagerNative.kt index d6de4e201c..3d520a6912 100644 --- a/packages/kotlin-sdk/sdk/src/main/kotlin/org/dashfoundation/dashsdk/ffi/WalletManagerNative.kt +++ b/packages/kotlin-sdk/sdk/src/main/kotlin/org/dashfoundation/dashsdk/ffi/WalletManagerNative.kt @@ -134,15 +134,14 @@ internal object WalletManagerNative { // `packages/kotlin-sdk/CLAUDE.md`); the `CoreTransactionBuilder` Kotlin // class orchestrates the sequence, mirroring the Swift // `CoreTransactionBuilder` + the `.coreToCore` flow in - // `SendViewModel.swift`. `builder` and `tx` are opaque native pointers - // carried as `Long` (the builder's `FFITransactionBuilder`, and a - // heap-boxed `FFICoreTransaction` from [coreTxBuilderBuildSigned]). + // `SendViewModel.swift`. `builder` is an opaque native pointer (the + // builder's `FFITransactionBuilder`) carried as `Long`. /** * `core_wallet_tx_builder_new` — create a builder for [network] * (`Network.ffiValue`). Returns the builder pointer (0 after throwing); - * free with [coreTxBuilderDestroy] or [coreTxBuilderBuildSigned] (which - * consumes it). + * free with [coreTxBuilderDestroy] or the consuming finalizers + * [coreTxBuilderFinalize] / [coreWalletFinalizeSignedPayment]. */ external fun coreTxBuilderNew(network: Int): Long @@ -163,8 +162,8 @@ internal object WalletManagerNative { /** * `core_wallet_tx_builder_set_change_address` — override the change - * address (network-checked). Optional; the Core→Core send relies on - * [coreTxBuilderSetFunding], which also sets a change address. + * address (network-checked). Optional; the finalizers pick a change + * address themselves during funding selection. */ external fun coreTxBuilderSetChangeAddress(builder: Long, address: String) @@ -179,7 +178,7 @@ internal object WalletManagerNative { * `core_wallet_tx_builder_change_to_first_input` — route change to the * address of the first selected input (VIN0). MAYACHAIN identifies the * depositor by VIN0 and pays refunds there. Overrides the change address - * [coreTxBuilderSetFunding] assigned. + * funding selection assigned. */ external fun coreTxBuilderChangeToFirstInput(builder: Long) @@ -200,35 +199,7 @@ internal object WalletManagerNative { */ external fun coreTxBuilderSetCurrentHeight(builder: Long, height: Int) - /** - * `core_wallet_tx_builder_set_funding` — fund from a wallet account, - * setting inputs AND the change address. [accountType]: 0 BIP44, - * 1 BIP32, 2 CoinJoin. - */ - external fun coreTxBuilderSetFunding( - builder: Long, - walletHandle: Long, - accountType: Int, - accountIndex: Int, - ) - - /** - * `core_wallet_tx_builder_build_signed` — build + sign against the wallet - * account, resolving Core ECDSA signatures via [coreSignerHandle] (a - * `MnemonicResolverHandle`). CONSUMES the builder (do not reuse the - * builder handle afterwards). Returns an opaque built-transaction pointer - * (0 after throwing) for [coreWalletBroadcastTransaction] / - * [coreTransactionFree]. - */ - external fun coreTxBuilderBuildSigned( - builder: Long, - walletHandle: Long, - accountType: Int, - accountIndex: Int, - coreSignerHandle: Long, - ): Long - - /** Atomic V2 finalizer; consumes [builder] and returns an opaque registry handle. */ + /** Atomic V2 finalizer; consumes [builder] and returns an opaque signed-transaction V2 handle. */ external fun coreTxBuilderFinalize( builder: Long, walletHandle: Long, @@ -239,14 +210,16 @@ internal object WalletManagerNative { /** * `core_wallet_tx_builder_destroy` — free a builder from [coreTxBuilderNew] - * that was NOT consumed by [coreTxBuilderBuildSigned]. Safe on 0. + * that was NOT consumed by [coreTxBuilderFinalize] / + * [coreWalletFinalizeSignedPayment]. Safe on 0. */ external fun coreTxBuilderDestroy(builder: Long) /** * `platform_wallet_get_core` — resolve the transient core-wallet handle - * from a `PlatformWallet` handle, for [coreWalletBroadcastTransaction]. - * Free with [coreWalletDestroy]. Returns 0 after throwing. + * from a `PlatformWallet` handle, for + * [coreWalletBroadcastSignedTransactionV2]. Free with [coreWalletDestroy]. + * Returns 0 after throwing. */ external fun platformWalletGetCore(walletHandle: Long): Long @@ -272,19 +245,6 @@ internal object WalletManagerNative { coreSignerHandle: Long, ): String - /** - * `core_wallet_broadcast_transaction` — broadcast a transaction built by - * [coreTxBuilderBuildSigned]. [accountType]/[accountIndex] identify the - * funding account so a definitive rejection releases its UTXO - * reservation. Returns the txid as a lowercase hex string. - */ - external fun coreWalletBroadcastTransaction( - coreHandle: Long, - tx: Long, - accountType: Int, - accountIndex: Int, - ): String - /** * `core_wallet_next_receive_address` — the engine's next unused BIP-44 * EXTERNAL (receive) address for [accountIndex], base58-encoded. @@ -324,13 +284,6 @@ internal object WalletManagerNative { /** `core_wallet_destroy` — release a core handle from [platformWalletGetCore]. Safe on 0. */ external fun coreWalletDestroy(coreHandle: Long) - /** - * `core_wallet_transaction_free` — free a transaction from - * [coreTxBuilderBuildSigned] (its box AND the tx bytes it owns). Safe on - * 0; call exactly once per built transaction. - */ - external fun coreTransactionFree(tx: Long) - /** * `core_wallet_signed_payment_finalize` — atomically fund, reserve, sign, * AND register a builder for deferred (BIP70/BIP270) submission in one diff --git a/packages/kotlin-sdk/sdk/src/main/kotlin/org/dashfoundation/dashsdk/wallet/CoreTransactionBuilder.kt b/packages/kotlin-sdk/sdk/src/main/kotlin/org/dashfoundation/dashsdk/wallet/CoreTransactionBuilder.kt index 6aace10f3b..b6c71cd84f 100644 --- a/packages/kotlin-sdk/sdk/src/main/kotlin/org/dashfoundation/dashsdk/wallet/CoreTransactionBuilder.kt +++ b/packages/kotlin-sdk/sdk/src/main/kotlin/org/dashfoundation/dashsdk/wallet/CoreTransactionBuilder.kt @@ -24,9 +24,7 @@ import java.util.concurrent.atomic.AtomicLong * [ManagedPlatformWallet.sendToAddresses] (immediate broadcast) and * [ManagedPlatformWallet.buildSignedPayment] (deferred BIP70/BIP270 flows and * MAYACHAIN-style deposits — the option parameters there thread [addOpReturn], - * [preserveOutputOrder] and [changeToFirstInput] into the build). The old - * [setFunding] + [buildSigned] sequence remains only as a deprecated ABI - * compatibility path and is not used by SDK convenience sends. + * [preserveOutputOrder] and [changeToFirstInput] into the build). * * @param network the wallet network — output and change addresses are * validated against it Rust-side. @@ -118,51 +116,6 @@ class CoreTransactionBuilder internal constructor(network: Network) : AutoClosea WalletManagerNative.coreTxBuilderSetCurrentHeight(handle, height) } - /** Fund from the account's UTXOs and set its change address. */ - @Deprecated("Use finalizeAtomic; split funding/signing is not concurrency-safe") - internal fun setFunding( - wallet: ManagedPlatformWallet, - accountType: AccountType, - accountIndex: Int, - ): CoreTransactionBuilder = apply { - WalletManagerNative.coreTxBuilderSetFunding( - handle, - wallet.handle, - accountType.ffiValue, - accountIndex, - ) - } - - /** - * Build + sign against the account; returns the signed transaction - * WITHOUT broadcasting. CONSUMES the builder (it is freed Rust-side and - * this instance must not be reused afterwards). - * - * @param coreSignerHandle a `MnemonicResolverHandle` (the manager's - * resolver) used for the Core ECDSA signatures. - */ - @Deprecated("Use finalizeAtomic; split funding/signing is not concurrency-safe") - internal fun buildSigned( - wallet: ManagedPlatformWallet, - accountType: AccountType, - accountIndex: Int, - coreSignerHandle: Long, - ): CoreTransaction { - val builderPtr = handleRef.getAndSet(0) - check(builderPtr != 0L) { "CoreTransactionBuilder has been consumed or closed" } - // The FFI frees the builder on every path — mark it consumed (the - // AtomicLong swap above) so [close] / the cleaner never double-free - // it, matching Swift's `consumed = true` before the check. - val txPtr = WalletManagerNative.coreTxBuilderBuildSigned( - builderPtr, - wallet.handle, - accountType.ffiValue, - accountIndex, - coreSignerHandle, - ) - return CoreTransaction(txPtr, accountType, accountIndex) - } - /** * Consume this configured builder, atomically select and reserve inputs, * then sign after Rust has released its wallet-manager lock. @@ -199,11 +152,10 @@ class CoreTransactionBuilder internal constructor(network: Network) : AutoClosea /** * Consume this configured builder and, in ONE atomic native operation, * select + reserve + sign the inputs and register the built transaction for - * deferred (BIP70/BIP270) submission. The concurrency-safe replacement for - * the deprecated [setFunding] + [buildSigned] + register split: selection - * and reservation commit as a single unit under the wallet-manager lock, so - * concurrent deferred builds cannot double-select an input. Returns the - * decoded [ManagedPlatformWallet.SignedCoreTransaction]. + * deferred (BIP70/BIP270) submission. Selection and reservation commit as + * a single unit under the wallet-manager lock, so concurrent deferred + * builds cannot double-select an input. Returns the decoded + * [ManagedPlatformWallet.SignedCoreTransaction]. */ internal fun finalizeSignedPayment( wallet: ManagedPlatformWallet, @@ -295,42 +247,3 @@ class FinalizedCoreTransaction internal constructor(handle: Long, val fee: Long) } } } - -/** - * A built, signed core transaction — Android port of Swift's `CoreTransaction`. - * Broadcast it via [ManagedCoreWallet.broadcastTransaction]; its native bytes - * are freed on [close] or a [NativeCleaner] backstop. - * - * @property accountType the funding account captured at build time — - * [ManagedCoreWallet.broadcastTransaction] forwards it so a failed - * broadcast releases the UTXO reservation `buildSigned` took. - */ -class CoreTransaction internal constructor( - handle: Long, - val accountType: CoreTransactionBuilder.AccountType, - val accountIndex: Int, -) : AutoCloseable { - - private val handleRef = AtomicLong(handle) - private val cleanable = NativeCleaner.register(this, TransactionCleanup(handleRef)) - - /** Opaque native `FFICoreTransaction` pointer; throws if already freed. */ - internal val handle: Long - get() = handleRef.get().also { - check(it != 0L) { "CoreTransaction has been freed" } - } - - override fun close() { - cleanable.clean() - } - - /** Frees the native transaction (box + tx bytes) exactly once. */ - private class TransactionCleanup(private val handleRef: AtomicLong) : Runnable { - override fun run() { - val handle = handleRef.getAndSet(0) - if (handle != 0L) { - WalletManagerNative.coreTransactionFree(handle) - } - } - } -} diff --git a/packages/kotlin-sdk/sdk/src/main/kotlin/org/dashfoundation/dashsdk/wallet/ManagedCoreWallet.kt b/packages/kotlin-sdk/sdk/src/main/kotlin/org/dashfoundation/dashsdk/wallet/ManagedCoreWallet.kt index 54699f09ec..91926fc77b 100644 --- a/packages/kotlin-sdk/sdk/src/main/kotlin/org/dashfoundation/dashsdk/wallet/ManagedCoreWallet.kt +++ b/packages/kotlin-sdk/sdk/src/main/kotlin/org/dashfoundation/dashsdk/wallet/ManagedCoreWallet.kt @@ -27,21 +27,6 @@ class ManagedCoreWallet internal constructor(handle: Long) : AutoCloseable { check(it != 0L) { "ManagedCoreWallet has been closed" } } - /** - * Broadcast a transaction built by [CoreTransactionBuilder.buildSigned]. - * The funding account captured at build time is forwarded so a definitive - * broadcast rejection releases the UTXO reservation `buildSigned` took. - * Returns the txid as a lowercase hex string. - */ - @Deprecated("Use the atomic FinalizedCoreTransaction send path") - fun broadcastTransaction(tx: CoreTransaction): String = - WalletManagerNative.coreWalletBroadcastTransaction( - handle, - tx.handle, - tx.accountType.ffiValue, - tx.accountIndex, - ) - /** Consume and broadcast a V2 finalized transaction. */ fun broadcastTransaction(tx: FinalizedCoreTransaction): String = WalletManagerNative.coreWalletBroadcastSignedTransactionV2( @@ -98,8 +83,8 @@ class ManagedCoreWallet internal constructor(handle: Long) : AutoCloseable { * The engine's next unused BIP-44 INTERNAL (change) address for * [accountIndex], base58-encoded — the change-side twin of * [nextReceiveAddress]; same used-set semantics and cold-start - * caveat. Builds pick change themselves (`setFunding`); this - * accessor exists for callers that must NAME a change address + * caveat. Builds pick change themselves during funding selection; + * this accessor exists for callers that must NAME a change address * up front (e.g. `CoreTransactionBuilder.setChangeAddress`). */ fun nextChangeAddress(accountIndex: Int = 0): String { diff --git a/packages/kotlin-sdk/sdk/src/main/kotlin/org/dashfoundation/dashsdk/wallet/ManagedPlatformWallet.kt b/packages/kotlin-sdk/sdk/src/main/kotlin/org/dashfoundation/dashsdk/wallet/ManagedPlatformWallet.kt index 48564d2518..270751a611 100644 --- a/packages/kotlin-sdk/sdk/src/main/kotlin/org/dashfoundation/dashsdk/wallet/ManagedPlatformWallet.kt +++ b/packages/kotlin-sdk/sdk/src/main/kotlin/org/dashfoundation/dashsdk/wallet/ManagedPlatformWallet.kt @@ -156,8 +156,8 @@ class ManagedPlatformWallet internal constructor( } mapNativeErrors { val builder = CoreTransactionBuilder(network) - // `buildSigned` consumes the builder; `use` still safely destroys - // it on the pre-build failure paths (addOutput / setFunding throw). + // `finalizeAtomic` consumes the builder; `use` still safely + // destroys it on the pre-finalize failure paths (addOutput throws). val signedTx = builder.use { for ((address, amount) in recipients) { it.addOutput(address, amount) @@ -287,8 +287,8 @@ class ManagedPlatformWallet internal constructor( * `new → addOutput* → finalizeSignedPayment` build runs under the same * per-wallet teardown gate ([gate]) as [sendToAddresses]. The single atomic * finalize does select + reserve + sign + register under the wallet-manager - * lock (closing the funding/signing selection race the old setFunding + - * buildSigned split had), so once this returns the reservation holds the + * lock (closing the funding/signing selection race the former split + * fund-then-sign path had), so once this returns the reservation holds the * inputs and [broadcastSigned] / [releaseReservation] operate on the token * later. * diff --git a/packages/rs-platform-wallet-ffi/src/core_wallet/broadcast.rs b/packages/rs-platform-wallet-ffi/src/core_wallet/broadcast.rs index e8816d3956..71d6ffc1a0 100644 --- a/packages/rs-platform-wallet-ffi/src/core_wallet/broadcast.rs +++ b/packages/rs-platform-wallet-ffi/src/core_wallet/broadcast.rs @@ -1,6 +1,5 @@ //! FFI bindings for CoreWallet transaction broadcasting. -use super::transaction_builder::{CoreAccountTypeFFI, FFICoreTransaction}; use crate::error::*; use crate::handle::*; use crate::runtime::runtime; @@ -195,66 +194,6 @@ pub unsafe extern "C" fn core_wallet_signed_transaction_v2_bytes( PlatformWalletFFIResult::ok() } -/// Broadcast a transaction built by `core_wallet_tx_builder_build_signed`. -/// -/// `account_type`/`account_index` identify the funding account handed to -/// `core_wallet_tx_builder_set_funding` when the transaction was built: on a -/// definitive broadcast rejection its UTXO reservation is released so an -/// immediate retry can reselect the inputs; an ambiguous failure keeps it. -/// `CoinJoin` funding has no standard-account reservation to reconcile and is -/// broadcast plainly. -/// -/// # Safety -/// `handle` must be a valid core-wallet handle; `tx` must be a valid, -/// non-null pointer to an `FFICoreTransaction`; `out_txid` must be writable. -/// On accepted, rejected, and unknown network outcomes `out_txid` receives a -/// Rust-owned C string that the caller frees with -/// `platform_wallet_string_free`. Operational errors leave it null. -#[no_mangle] -pub unsafe extern "C" fn core_wallet_broadcast_transaction( - handle: Handle, - tx: *const FFICoreTransaction, - account_type: CoreAccountTypeFFI, - account_index: u32, - out_txid: *mut *mut c_char, -) -> PlatformWalletFFIResult { - check_ptr!(out_txid); - *out_txid = std::ptr::null_mut(); - check_ptr!(tx); - - let tx: dashcore::Transaction = - unwrap_result_or_return!(dashcore::consensus::deserialize((*tx).bytes())); - let local_txid = tx.txid(); - - let option = CORE_WALLET_STORAGE.with_item(handle, |wallet| { - runtime().block_on(async { - match account_type.as_standard_account_type() { - Some(account_type) => { - wallet - .broadcast_transaction_releasing_reservation( - account_type, - account_index, - &tx, - ) - .await - } - None => wallet.broadcast_transaction(&tx).await, - } - }) - }); - - let result = unwrap_option_or_return!(option); - - let (txid, ffi_result) = classify_broadcast_result(result, local_txid); - let Some(txid) = txid else { - return ffi_result; - }; - let c_str = unwrap_result_or_return!(std::ffi::CString::new(txid.to_string())); - *out_txid = c_str.into_raw(); - - ffi_result -} - #[cfg(test)] mod outcome_tests { use dashcore::hashes::Hash; diff --git a/packages/rs-platform-wallet-ffi/src/core_wallet/signed_payment.rs b/packages/rs-platform-wallet-ffi/src/core_wallet/signed_payment.rs index e945f37728..c0d4579763 100644 --- a/packages/rs-platform-wallet-ffi/src/core_wallet/signed_payment.rs +++ b/packages/rs-platform-wallet-ffi/src/core_wallet/signed_payment.rs @@ -1,19 +1,19 @@ //! FFI bindings for the deferred build → broadcast/release core-send lifecycle //! (BIP70 / BIP270 "sign now, submit on merchant ack"). //! -//! The one-shot [`core_wallet_broadcast_transaction`](super::broadcast) sends a -//! just-built transaction immediately. BIP70-style flows must split that: build -//! and sign now (reserving the funding UTXOs), hand the raw bytes to a merchant -//! server, then broadcast only on ack — or release the reservation on a nack / +//! The one-shot immediate send path (`core_wallet_tx_builder_finalize` + +//! `core_wallet_broadcast_signed_transaction_v2`) sends a just-built +//! transaction immediately. BIP70-style flows must split that: build and sign +//! now (reserving the funding UTXOs), hand the raw bytes to a merchant server, +//! then broadcast only on ack — or release the reservation on a nack / //! abandonment. These entry points wrap a single process-global //! [`SignedPaymentRegistry`] pinned to the production `SpvBroadcaster`; the //! registry owns the built transaction and its held reservation between build //! and submission and enforces the lifecycle invariants (no double-broadcast, //! idempotent release, tokens bound to their originating wallet instance). //! -//! These are ADDITIVE to the existing `core_wallet_tx_builder_*` / -//! `core_wallet_broadcast_transaction` surface — the immediate send path is -//! unchanged. +//! These are ADDITIVE to the `core_wallet_tx_builder_*` surface — the +//! immediate send path is unchanged. use crate::error::*; use crate::handle::{Handle, CORE_WALLET_STORAGE}; diff --git a/packages/rs-platform-wallet-ffi/src/core_wallet/transaction_builder.rs b/packages/rs-platform-wallet-ffi/src/core_wallet/transaction_builder.rs index 9b37485330..99b5677285 100644 --- a/packages/rs-platform-wallet-ffi/src/core_wallet/transaction_builder.rs +++ b/packages/rs-platform-wallet-ffi/src/core_wallet/transaction_builder.rs @@ -6,10 +6,8 @@ use crate::types::{FFINetwork, Network}; use crate::{check_ptr, unwrap_option_or_return, unwrap_result_or_return}; use dashcore::blockdata::transaction::special_transaction::TransactionPayload; use dashcore::hashes::Hash; -use dashcore::{Address as DashAddress, OutPoint, Transaction, Txid}; -use key_wallet::account::account_type::StandardAccountType; +use dashcore::{Address as DashAddress, OutPoint, Txid}; use key_wallet::account::ManagedAccountCollection; -use key_wallet::managed_account::managed_account_trait::ManagedAccountTrait; use key_wallet::managed_account::ManagedCoreFundsAccount; use key_wallet::wallet::managed_wallet_info::coin_selection::SelectionStrategy; use key_wallet::wallet::managed_wallet_info::fee::FeeRate; @@ -17,7 +15,6 @@ use key_wallet::wallet::managed_wallet_info::transaction_builder::{ TransactionBuilder, MAX_STANDARD_OP_RETURN_BYTES, }; use key_wallet::wallet::managed_wallet_info::transaction_building::AccountTypePreference; -use key_wallet::wallet::managed_wallet_info::wallet_info_interface::WalletInfoInterface; use rs_sdk_ffi::{MnemonicResolverCoreSigner, MnemonicResolverHandle}; use std::ffi::CString; use std::os::raw::{c_char, c_void}; @@ -36,8 +33,9 @@ pub struct FFITransactionBuilder { network: FFINetwork, } -/// Broadcast it with `core_wallet_broadcast_transaction`, then release it -/// with `core_wallet_transaction_free`. +/// Owned signed-transaction bytes handed across the C ABI as the `out_tx` +/// of `core_wallet_signed_payment_finalize`; release it with +/// `core_wallet_transaction_free`. #[repr(C)] pub struct FFICoreTransaction { tx_bytes: *mut u8, @@ -57,16 +55,6 @@ pub struct FFICoreSignedTransactionV2 { pub(crate) transaction: platform_wallet::SignedCoreTransaction, } -impl FFICoreTransaction { - pub(crate) fn bytes(&self) -> &[u8] { - if self.tx_bytes.is_null() || self.tx_len == 0 { - &[] - } else { - unsafe { std::slice::from_raw_parts(self.tx_bytes, self.tx_len) } - } - } -} - #[derive(Clone, Copy)] #[repr(C)] pub enum CoreAccountTypeFFI { @@ -87,11 +75,11 @@ impl From for AccountTypePreference { /// Atomically fund, reserve and sign a configured builder. /// -/// Unlike the deprecated `set_funding` + `build_signed` sequence, selection -/// and insertion into the account ReservationSet cannot interleave with a -/// competing finalizer. The wallet-manager lock is dropped before the host -/// mnemonic resolver is invoked. This function consumes `builder` on every -/// path after its pointer is accepted. +/// Selection and insertion into the account ReservationSet happen under one +/// wallet-manager lock, so they cannot interleave with a competing finalizer. +/// The wallet-manager lock is dropped before the host mnemonic resolver is +/// invoked. This function consumes `builder` on every path after its pointer +/// is accepted. /// /// On success `out_transaction_handle` receives an opaque V2 handle. Consume /// it with `core_wallet_broadcast_signed_transaction_v2` or @@ -190,11 +178,11 @@ pub unsafe extern "C" fn core_wallet_tx_builder_finalize( /// runs the same atomic `finalize_transaction`, where selection and insertion /// into the account `ReservationSet` commit as a single unit under the /// wallet-manager lock (signing happens after the lock is dropped). Routing the -/// deferred build through it closes the double-selection window that the -/// deprecated `set_funding` + `build_signed` + `register` sequence reopened once -/// the Kotlin per-wallet send mutex was removed: two concurrent deferred builds, -/// or a deferred build racing an immediate send, can no longer select the same -/// UTXO. Consumes `builder` on every path after its pointer is accepted. +/// deferred build through it closes the double-selection window the former +/// split fund-then-sign sequence reopened once the Kotlin per-wallet send mutex +/// was removed: two concurrent deferred builds, or a deferred build racing an +/// immediate send, can no longer select the same UTXO. Consumes `builder` on +/// every path after its pointer is accepted. /// /// Writes `out_token` (the reservation token for a later /// `core_wallet_signed_payment_broadcast` / `core_wallet_signed_payment_release`), @@ -374,28 +362,6 @@ pub unsafe extern "C" fn core_wallet_signed_payment_finalize( PlatformWalletFFIResult::ok() } -impl CoreAccountTypeFFI { - /// The `StandardAccountType` this maps to, or `None` for `CoinJoin`. - /// - /// A CoinJoin-funded build DOES end up with reserved UTXOs — `build_signed` - /// (via `assemble_unsigned`) reserves the selected inputs regardless of - /// account type, since `set_funding` attaches the shared `ReservationSet` - /// for every variant. But `reservations.rs`'s release-on-rejection is - /// defined only over `StandardAccountType` (BIP44/BIP32). Returning `None` - /// here routes CoinJoin through the plain broadcast, so a rejected CoinJoin - /// tx keeps its reservation until the TTL backstop. That is intentional: the - /// only CoinJoin funding path is a sweep — a single sender spending each - /// UTXO exactly once, with no concurrent build or retry to race — so there - /// is nothing to reconcile in practice. - pub(crate) fn as_standard_account_type(&self) -> Option { - match self { - CoreAccountTypeFFI::BIP44 => Some(StandardAccountType::BIP44Account), - CoreAccountTypeFFI::BIP32 => Some(StandardAccountType::BIP32Account), - CoreAccountTypeFFI::CoinJoin => None, - } - } -} - #[repr(C)] pub enum CoreSelectionStrategyFFI { SmallestFirst, @@ -433,18 +399,6 @@ fn managed_account( } } -fn managed_account_mut( - accounts: &mut ManagedAccountCollection, - source: AccountTypePreference, - account_index: u32, -) -> Option<&mut ManagedCoreFundsAccount> { - match source { - AccountTypePreference::BIP44 => accounts.standard_bip44_accounts.get_mut(&account_index), - AccountTypePreference::BIP32 => accounts.standard_bip32_accounts.get_mut(&account_index), - AccountTypePreference::CoinJoin => accounts.coinjoin_accounts.get_mut(&account_index), - } -} - impl FFITransactionBuilder { /// The inner builder taken out by value, leaving an empty one in its /// place. Pair with [`FFITransactionBuilder::store_builder`] to apply a @@ -466,8 +420,8 @@ impl FFITransactionBuilder { } /// Create a new transaction builder for `network`. Free with -/// `core_wallet_tx_builder_destroy` (or `core_wallet_tx_builder_build_signed`, -/// which consumes it). +/// `core_wallet_tx_builder_destroy` (or the consuming finalizers +/// `core_wallet_tx_builder_finalize` / `core_wallet_signed_payment_finalize`). /// /// # Safety /// The returned pointer is owned by the caller. @@ -660,10 +614,10 @@ pub unsafe extern "C" fn core_wallet_tx_builder_set_selection_strategy( /// Set the block height coin selection treats as the chain tip (used for /// coinbase maturity and locktime). /// -/// This value is advisory: `core_wallet_tx_builder_set_funding` and -/// `core_wallet_tx_builder_build_signed` both override it with the wallet's -/// last processed height when they run, so the wallet height always wins for -/// the funded/signed build. Use this only when building without a wallet. +/// This value is advisory: the wallet-aware finalizers override it with the +/// wallet's last processed height when they run, so the wallet height always +/// wins for the funded/signed build. Use this only when building without a +/// wallet. /// /// # Safety /// `builder` must be a valid, non-destroyed pointer. @@ -724,82 +678,6 @@ pub unsafe extern "C" fn core_wallet_tx_builder_set_special_payload( PlatformWalletFFIResult::ok() } -/// Fund the builder from the wallet account, setting inputs and change. -/// -/// # Concurrency limitation (known, intentionally not fixed here) -/// key-wallet's `set_funding` filters out UTXOs already recorded in the -/// account's shared `ReservationSet`, but the reservation for *this* build is -/// only taken at build time (`assemble_unsigned` inside `build_signed`). -/// Because the FFI splits `set_funding` and `build_signed` across the C ABI — -/// the wallet lock cannot be held across the boundary — two concurrent builds -/// on the SAME account can both pass `set_funding` before either reserves and -/// select the same UTXO, producing a double-spend at broadcast. Single-threaded -/// callers (the SDK's send flow) are unaffected; concurrent same-account sends -/// must serialize at the call site. -/// -/// # Safety -/// `builder` must be a valid, non-destroyed pointer; `wallet` a valid -/// platform-wallet handle. -#[no_mangle] -pub unsafe extern "C" fn core_wallet_tx_builder_set_funding( - builder: *mut FFITransactionBuilder, - wallet: Handle, - account_type: CoreAccountTypeFFI, - account_index: u32, -) -> PlatformWalletFFIResult { - check_ptr!(builder); - - let wallet = unwrap_option_or_return!(PLATFORM_WALLET_STORAGE.with_item(wallet, |w| w.clone())); - - // Reject a builder created for a different network than the wallet. - let builder_network: Network = (*builder).network.into(); - if builder_network != wallet.network() { - return PlatformWalletFFIResult::err( - PlatformWalletFFIResultCode::ErrorInvalidParameter, - "builder network does not match wallet network".to_string(), - ); - } - - let wallet_id = wallet.wallet_id(); - let source: AccountTypePreference = account_type.into(); - - let result = runtime().block_on(async { - let mut wm = wallet.wallet_manager().write().await; - let (w, info) = wm - .get_wallet_and_info_mut(&wallet_id) - .ok_or_else(|| "wallet not found".to_string())?; - - let account = match source { - AccountTypePreference::BIP44 => w.get_bip44_account(account_index), - AccountTypePreference::BIP32 => w.get_bip32_account(account_index), - AccountTypePreference::CoinJoin => w.get_coinjoin_account(account_index), - } - .ok_or_else(|| format!("wallet account {source:?} #{account_index} not found"))?; - - let height = info.core_wallet.last_processed_height(); - - let managed = managed_account_mut(&mut info.core_wallet.accounts, source, account_index) - .ok_or_else(|| format!("managed account {source:?} #{account_index} not found"))?; - - // Resolution succeeded — only now consume the builder so a lookup - // failure above can never leave it emptied. - let taken = (*builder).take_builder(); - let funded = taken - .set_current_height(height) - .set_funding(managed, account); - (*builder).store_builder(funded); - Ok::<_, String>(()) - }); - - match result { - Ok(()) => PlatformWalletFFIResult::ok(), - Err(e) => PlatformWalletFFIResult::err( - PlatformWalletFFIResultCode::ErrorWalletOperation, - format!("set_funding failed: {e}"), - ), - } -} - /// Add a caller-chosen subset of the account's UTXOs as inputs. `outpoints` /// are selected from the account's own UTXO set (the same ones /// `platform_wallet_account_utxos` returns). An outpoint not owned by the @@ -822,8 +700,8 @@ pub unsafe extern "C" fn core_wallet_tx_builder_add_inputs_from_outpoints( let wallet = unwrap_option_or_return!(PLATFORM_WALLET_STORAGE.with_item(wallet, |w| w.clone())); // Reject a builder created for a different network than the wallet, matching - // `set_funding` / `build_signed` so all three wallet-aware entry points fail - // fast instead of mutating a foreign-network builder. + // the wallet-aware finalizers so every wallet-aware entry point fails fast + // instead of mutating a foreign-network builder. let builder_network: Network = (*builder).network.into(); if builder_network != wallet.network() { return PlatformWalletFFIResult::err( @@ -882,90 +760,6 @@ pub unsafe extern "C" fn core_wallet_tx_builder_add_inputs_from_outpoints( } } -/// Build and sign, resolving signing paths from the wallet account. Returns -/// consensus-serialized signed bytes and the fee. -/// -/// This function also frees the builder -/// -/// # Safety -/// `builder` must be a valid, non-destroyed pointer; `wallet` a valid platform-wallet handle; -/// `core_signer_handle` a valid, non-destroyed resolver handle; `out_tx` a -/// writable pointer the caller later frees with `core_wallet_transaction_free`. -#[no_mangle] -#[allow(clippy::too_many_arguments)] -pub unsafe extern "C" fn core_wallet_tx_builder_build_signed( - builder: *mut FFITransactionBuilder, - wallet: Handle, - account_type: CoreAccountTypeFFI, - account_index: u32, - core_signer_handle: *mut MnemonicResolverHandle, - out_tx: *mut FFICoreTransaction, -) -> PlatformWalletFFIResult { - check_ptr!(builder); - // `build` consumes the builder: reclaim both heap boxes up front so they - // are freed on every return path below - let ffi = Box::from_raw(builder); - let inner = *Box::from_raw(ffi.inner as *mut TransactionBuilder); - - check_ptr!(core_signer_handle); - check_ptr!(out_tx); - - let wallet = unwrap_option_or_return!(PLATFORM_WALLET_STORAGE.with_item(wallet, |w| w.clone())); - - // Backstop network check: reject a builder built for a different network - // than the wallet, even when `set_funding` already validated it. - let builder_network: Network = ffi.network.into(); - if builder_network != wallet.network() { - return PlatformWalletFFIResult::err( - PlatformWalletFFIResultCode::ErrorInvalidParameter, - "builder network does not match wallet network".to_string(), - ); - } - - let wallet_id = wallet.wallet_id(); - let source: AccountTypePreference = account_type.into(); - let signer = MnemonicResolverCoreSigner::new(core_signer_handle, wallet_id, wallet.network()); - - let build = runtime().block_on(async { - let wm = wallet.wallet_manager().read().await; - let info = wm - .get_wallet_info(&wallet_id) - .ok_or_else(|| "wallet not found".to_string())?; - - let height = info.core_wallet.last_processed_height(); - - let managed = managed_account(&info.core_wallet.accounts, source, account_index) - .ok_or_else(|| format!("managed account {source:?} #{account_index} not found"))?; - - inner - .set_current_height(height) - .build_signed(&signer, |addr| managed.address_derivation_path(&addr)) - .await - .map_err(|e| e.to_string()) - }); - - let (tx, fee): (Transaction, u64) = match build { - Ok(v) => v, - Err(e) => { - return PlatformWalletFFIResult::err( - PlatformWalletFFIResultCode::ErrorWalletOperation, - format!("transaction build failed: {e}"), - ); - } - }; - - let serialized = dashcore::consensus::serialize(&tx); - let len = serialized.len(); - - *out_tx = FFICoreTransaction { - tx_bytes: Box::into_raw(serialized.into_boxed_slice()) as *mut u8, - tx_len: len, - fee, - }; - - PlatformWalletFFIResult::ok() -} - /// Destroy a transaction builder created by `core_wallet_tx_builder_new`. /// /// # Safety @@ -980,12 +774,12 @@ pub unsafe extern "C" fn core_wallet_tx_builder_destroy(builder: *mut FFITransac let _ = Box::from_raw(b.inner as *mut TransactionBuilder); } -/// Free a transaction returned by `core_wallet_tx_builder_build_signed`. +/// Free a transaction written by `core_wallet_signed_payment_finalize`. /// Idempotent: the fields are nulled, so a second call is a no-op. /// /// # Safety /// `tx` must be a valid pointer to an `FFICoreTransaction` from -/// `core_wallet_tx_builder_build_signed` (or null). +/// `core_wallet_signed_payment_finalize` (or null). #[no_mangle] pub unsafe extern "C" fn core_wallet_transaction_free(tx: *mut FFICoreTransaction) { if tx.is_null() { diff --git a/packages/rs-unified-sdk-jni/src/wallet_manager.rs b/packages/rs-unified-sdk-jni/src/wallet_manager.rs index 798d4f1183..d1ea887274 100644 --- a/packages/rs-unified-sdk-jni/src/wallet_manager.rs +++ b/packages/rs-unified-sdk-jni/src/wallet_manager.rs @@ -622,31 +622,24 @@ pub extern "system" fn Java_org_dashfoundation_dashsdk_ffi_WalletManagerNative_w // ── Core transaction builder (1:1 over `core_wallet_tx_builder_*`) ───── // // The base refactor replaced the one-shot `core_wallet_send_to_addresses` -// with a step-by-step builder (`transaction_builder.rs`) + a separate -// `core_wallet_broadcast_transaction`. Per `packages/kotlin-sdk/CLAUDE.md`, -// each builder step is exported as its OWN thin JNI trampoline (one export -// = one FFI call, no composite stitching); the Kotlin `CoreTransactionBuilder` -// class orchestrates the sequence, mirroring the Swift `CoreTransactionBuilder` -// + the `.coreToCore` flow in `SendViewModel.swift`. +// with a step-by-step builder (`transaction_builder.rs`) + separate broadcast +// entry points. Per `packages/kotlin-sdk/CLAUDE.md`, each builder step is +// exported as its OWN thin JNI trampoline (one export = one FFI call, no +// composite stitching); the Kotlin `CoreTransactionBuilder` class orchestrates +// the sequence, mirroring the Swift `CoreTransactionBuilder` + the +// `.coreToCore` flow in `SendViewModel.swift`. // -// Handles cross as `jlong`: -// - the `*mut FFITransactionBuilder` from [coreTxBuilderNew], and -// - a heap-boxed `FFICoreTransaction` from [coreTxBuilderBuildSigned]. -// -// `FFITransactionBuilder` / `FFICoreTransaction` have PRIVATE fields (the -// FFI crate is an rlib dependency, so cbindgen's C-side field visibility -// does not apply here). We therefore never read/construct their fields: the -// out `FFICoreTransaction` is allocated zeroed via `MaybeUninit` (a zeroed -// value is exactly Swift's `FFICoreTransaction(tx_bytes: nil, tx_len: 0, -// fee: 0)`), `build_signed` fills it, and it crosses to Kotlin as an opaque -// `jlong` that only [coreWalletBroadcastTransaction] / [coreTransactionFree] -// consume — matching the opaque handle discipline the rest of this module -// uses. +// The `*mut FFITransactionBuilder` from [coreTxBuilderNew] crosses as a +// `jlong`. It has PRIVATE fields (the FFI crate is an rlib dependency, so +// cbindgen's C-side field visibility does not apply here); we never +// read/construct its fields — it stays an opaque handle, matching the +// discipline the rest of this module uses. /// `core_wallet_tx_builder_new` — create a builder for `network` /// (`Network.ffiValue`: 0 Mainnet, 1 Testnet, 2 Devnet, 3 Regtest). Returns /// the `*mut FFITransactionBuilder` as a `jlong` (0 after throwing). Free -/// with [coreTxBuilderDestroy], or [coreTxBuilderBuildSigned] which consumes it. +/// with [coreTxBuilderDestroy], or the consuming finalizers +/// [coreTxBuilderFinalize] / [coreWalletFinalizeSignedPayment]. #[no_mangle] pub extern "system" fn Java_org_dashfoundation_dashsdk_ffi_WalletManagerNative_coreTxBuilderNew( mut env: JNIEnv, @@ -899,113 +892,6 @@ pub extern "system" fn Java_org_dashfoundation_dashsdk_ffi_WalletManagerNative_c }) } -/// `core_wallet_tx_builder_set_funding` — fund the builder from a wallet -/// account, setting inputs AND the change address. `account_type`: 0 BIP44, -/// 1 BIP32, 2 CoinJoin. Rejects negative type/index at the boundary. -#[no_mangle] -pub extern "system" fn Java_org_dashfoundation_dashsdk_ffi_WalletManagerNative_coreTxBuilderSetFunding( - mut env: JNIEnv, - _class: JClass, - builder: jlong, - wallet_handle: jlong, - account_type: jni::sys::jint, - account_index: jni::sys::jint, -) { - guard(&mut env, (), |env| { - if builder == 0 { - throw_sdk_exception(env, 1, "builder handle is 0"); - return; - } - let Some(account_type) = core_account_type(account_type) else { - throw_sdk_exception(env, 1, "accountType out of range (expected 0..=2)"); - return; - }; - if account_index < 0 { - throw_sdk_exception(env, 1, "accountIndex must be non-negative"); - return; - } - let result = unsafe { - platform_wallet_ffi::core_wallet_tx_builder_set_funding( - builder as *mut platform_wallet_ffi::FFITransactionBuilder, - wallet_handle as Handle, - account_type, - account_index as u32, - ) - }; - let _ = take_pwffi_error(env, result); - }) -} - -/// `core_wallet_tx_builder_build_signed` — build + sign against the wallet -/// account, resolving Core ECDSA signatures via the `MnemonicResolverHandle` -/// `core_signer_handle`. CONSUMES the builder (the FFI frees it on every -/// path), so Kotlin must not reuse the builder handle afterwards. -/// -/// Returns a heap-boxed `FFICoreTransaction` pointer as a `jlong` (0 after -/// throwing) — an opaque handle for [coreWalletBroadcastTransaction] and -/// [coreTransactionFree]. `account_type`: 0 BIP44, 1 BIP32, 2 CoinJoin. -#[no_mangle] -pub extern "system" fn Java_org_dashfoundation_dashsdk_ffi_WalletManagerNative_coreTxBuilderBuildSigned( - mut env: JNIEnv, - _class: JClass, - builder: jlong, - wallet_handle: jlong, - account_type: jni::sys::jint, - account_index: jni::sys::jint, - core_signer_handle: jlong, -) -> jlong { - guard(&mut env, 0, |env| { - if builder == 0 { - throw_sdk_exception(env, 1, "builder handle is 0"); - return 0; - } - let Some(account_type) = core_account_type(account_type) else { - throw_sdk_exception(env, 1, "accountType out of range (expected 0..=2)"); - return 0; - }; - if account_index < 0 { - throw_sdk_exception(env, 1, "accountIndex must be non-negative"); - return 0; - } - if core_signer_handle == 0 { - throw_sdk_exception(env, 1, "coreSignerHandle is 0"); - return 0; - } - - // Own an out `FFICoreTransaction` on the heap. Its fields are private - // to the FFI crate, so allocate it zeroed (== Swift's - // `FFICoreTransaction(tx_bytes: nil, tx_len: 0, fee: 0)`) rather than - // constructing it by field; `build_signed` fills it in place. - let mut boxed: Box> = - Box::new(std::mem::MaybeUninit::zeroed()); - let out_tx = boxed.as_mut_ptr(); - - let result = unsafe { - platform_wallet_ffi::core_wallet_tx_builder_build_signed( - builder as *mut platform_wallet_ffi::FFITransactionBuilder, - wallet_handle as Handle, - account_type, - account_index as u32, - core_signer_handle as *mut rs_sdk_ffi::MnemonicResolverHandle, - out_tx, - ) - }; - if take_pwffi_error(env, result) { - // build_signed already freed the builder on the error path; the - // out struct is still zeroed (null tx_bytes) — dropping `boxed` - // frees only the box, leaking nothing. - return 0; - } - - // Success: the box now holds an initialized FFICoreTransaction. Leak - // it to Kotlin as an opaque jlong; reclaimed by coreTransactionFree. - // `MaybeUninit` has the same layout as `T`, so the raw pointer from - // the leaked box points at the initialized value (build_signed - // returned Success, so it is initialized). - Box::into_raw(boxed).cast::() as jlong - }) -} - /// Atomic V2 finalizer: consumes a configured builder, performs funding and /// ReservationSet insertion indivisibly in platform-wallet, drops the manager /// lock, then invokes the mnemonic resolver to sign. @@ -1065,7 +951,8 @@ pub extern "system" fn Java_org_dashfoundation_dashsdk_ffi_WalletManagerNative_c } /// `core_wallet_tx_builder_destroy` — free a builder created by -/// [coreTxBuilderNew] that was NOT consumed by [coreTxBuilderBuildSigned]. +/// [coreTxBuilderNew] that was NOT consumed by [coreTxBuilderFinalize] / +/// [coreWalletFinalizeSignedPayment]. /// Safe on 0. #[no_mangle] pub extern "system" fn Java_org_dashfoundation_dashsdk_ffi_WalletManagerNative_coreTxBuilderDestroy( @@ -1187,7 +1074,7 @@ pub extern "system" fn Java_org_dashfoundation_dashsdk_ffi_WalletManagerNative_c } /// `platform_wallet_get_core` — resolve the transient core-wallet `Handle` -/// (as `jlong`) from a `PlatformWallet` handle, for [coreWalletBroadcastTransaction]. +/// (as `jlong`) from a `PlatformWallet` handle, for [coreWalletBroadcastSignedTransactionV2]. /// Free with [coreWalletDestroy]. Returns 0 after throwing. #[no_mangle] pub extern "system" fn Java_org_dashfoundation_dashsdk_ffi_WalletManagerNative_platformWalletGetCore( @@ -1210,63 +1097,6 @@ pub extern "system" fn Java_org_dashfoundation_dashsdk_ffi_WalletManagerNative_p }) } -/// `core_wallet_broadcast_transaction` — broadcast a transaction built by -/// [coreTxBuilderBuildSigned]. `account_type`/`account_index` identify the -/// funding account so a definitive rejection releases its UTXO reservation. -/// Returns the txid as a lowercase hex string (null after throwing). -#[no_mangle] -pub extern "system" fn Java_org_dashfoundation_dashsdk_ffi_WalletManagerNative_coreWalletBroadcastTransaction( - mut env: JNIEnv, - _class: JClass, - core_handle: jlong, - tx: jlong, - account_type: jni::sys::jint, - account_index: jni::sys::jint, -) -> jstring { - guard(&mut env, ptr::null_mut(), |env| { - if tx == 0 { - throw_sdk_exception(env, 1, "transaction handle is 0"); - return ptr::null_mut(); - } - let Some(account_type) = core_account_type(account_type) else { - throw_sdk_exception(env, 1, "accountType out of range (expected 0..=2)"); - return ptr::null_mut(); - }; - if account_index < 0 { - throw_sdk_exception(env, 1, "accountIndex must be non-negative"); - return ptr::null_mut(); - } - - let mut out_txid: *mut c_char = ptr::null_mut(); - let result = unsafe { - platform_wallet_ffi::core_wallet_broadcast_transaction( - core_handle as Handle, - tx as *const platform_wallet_ffi::FFICoreTransaction, - account_type, - account_index as u32, - &mut out_txid as *mut *mut c_char, - ) - }; - if take_pwffi_error(env, result) { - return ptr::null_mut(); - } - - if out_txid.is_null() { - throw_sdk_exception(env, 1, "broadcast returned a NULL txid"); - return ptr::null_mut(); - } - // Copy the txid out, then free the Rust-owned C string (same free the - // core wallet uses for its address C strings — `core_wallet_free_address`). - let txid = unsafe { CStr::from_ptr(out_txid) } - .to_string_lossy() - .into_owned(); - unsafe { platform_wallet_ffi::core_wallet_free_address(out_txid) }; - env.new_string(txid) - .map(|s| s.into_raw()) - .unwrap_or(ptr::null_mut()) - }) -} - /// `core_wallet_next_receive_address` — the engine's next unused BIP-44 /// EXTERNAL (receive) address for `account_index`, base58-encoded. /// @@ -1528,35 +1358,10 @@ pub extern "system" fn Java_org_dashfoundation_dashsdk_ffi_WalletManagerNative_c }) } -/// `core_wallet_transaction_free` — free a transaction from -/// [coreTxBuilderBuildSigned] (its heap box AND the tx bytes it owns). Safe -/// on 0; must be called exactly once per built transaction. -#[no_mangle] -pub extern "system" fn Java_org_dashfoundation_dashsdk_ffi_WalletManagerNative_coreTransactionFree( - mut env: JNIEnv, - _class: JClass, - tx: jlong, -) { - guard(&mut env, (), |_| { - if tx == 0 { - return; - } - // SAFETY: `tx` is a live FFICoreTransaction box from build_signed, - // consumed exactly once here. Free the tx bytes it owns, then reclaim - // the box itself. - let mut boxed = - unsafe { Box::from_raw(tx as *mut platform_wallet_ffi::FFICoreTransaction) }; - unsafe { - platform_wallet_ffi::core_wallet_transaction_free( - boxed.as_mut() as *mut platform_wallet_ffi::FFICoreTransaction - ) - }; - }) -} - // ── Deferred build → broadcast/release core-send (BIP70/BIP270) ─────── // -// ADDITIVE surface over the immediate `coreWalletBroadcastTransaction` path: +// ADDITIVE surface over the immediate [coreTxBuilderFinalize] + +// [coreWalletBroadcastSignedTransactionV2] send path: // [coreWalletFinalizeSignedPayment] atomically funds, reserves, signs, and // registers a builder in one native call, returning the raw bytes to hand to a // merchant server; the reservation is then broadcast on ack — or released on diff --git a/packages/swift-sdk/Sources/SwiftDashSDK/PlatformWallet/CoreWallet/CoreTransactionBuilder.swift b/packages/swift-sdk/Sources/SwiftDashSDK/PlatformWallet/CoreWallet/CoreTransactionBuilder.swift index b7eead9a97..a71bdd6607 100644 --- a/packages/swift-sdk/Sources/SwiftDashSDK/PlatformWallet/CoreWallet/CoreTransactionBuilder.swift +++ b/packages/swift-sdk/Sources/SwiftDashSDK/PlatformWallet/CoreWallet/CoreTransactionBuilder.swift @@ -1,40 +1,6 @@ import Foundation import DashSDKFFI -/// A built, signed core transaction. Broadcast it via -/// `ManagedCoreWallet.broadcastTransaction`; its bytes are freed when this -/// object is released. -public final class CoreTransaction { - var ffi: FFICoreTransaction - - /// The account that funded this transaction, captured at build time. - /// `broadcastTransaction` forwards it so a failed broadcast can release - /// the UTXO reservation `buildSigned` took on that account. - let accountType: CoreTransactionBuilder.AccountType - let accountIndex: UInt32 - - init( - ffi: FFICoreTransaction, - accountType: CoreTransactionBuilder.AccountType, - accountIndex: UInt32 - ) { - self.ffi = ffi - self.accountType = accountType - self.accountIndex = accountIndex - } - - deinit { withUnsafeMutablePointer(to: &ffi) { core_wallet_transaction_free($0) } } - - /// Network fee in duffs. - public var fee: UInt64 { ffi.fee } - - /// Consensus-serialized signed transaction bytes (copied out). - public var data: Data { - guard let p = ffi.tx_bytes, ffi.tx_len > 0 else { return Data() } - return Data(bytes: p, count: Int(ffi.tx_len)) - } -} - /// Ownership token for a Core transaction atomically funded and reserved in Rust. public final class FinalizedCoreTransaction { private var nativeHandle: Handle @@ -210,7 +176,7 @@ public final class CoreTransactionBuilder { } private let handle: UnsafeMutablePointer - /// Set once `buildSigned` has consumed the builder, so `deinit` skips the + /// Set once a finalizer has consumed the builder, so `deinit` skips the /// Rust-side destroy. private var consumed = false @@ -229,20 +195,6 @@ public final class CoreTransactionBuilder { } } - /// Fund from the account's UTXOs and set its change address. - @discardableResult - @available(*, deprecated, message: "Use finalizeAtomic; split funding/signing is not concurrency-safe") - public func setFunding( - wallet: ManagedPlatformWallet, - accountType: AccountType, - accountIndex: UInt32 - ) throws -> CoreTransactionBuilder { - try core_wallet_tx_builder_set_funding( - handle, wallet.handle, accountType.ffi, accountIndex - ).check() - return self - } - /// Add a chosen subset of the account's UTXOs (as returned by /// `PlatformWalletManager.accountUtxos`) as inputs. Each must belong to /// the account. @@ -355,42 +307,6 @@ public final class CoreTransactionBuilder { return self } - /// Build and sign against the account; returns the signed transaction - /// without broadcasting. Consumes the builder — it is freed on the Rust - /// side and this instance must not be reused afterwards. - @available(*, deprecated, message: "Use finalizeAtomic; split funding/signing is not concurrency-safe") - public func buildSigned( - wallet: ManagedPlatformWallet, - accountType: AccountType, - accountIndex: UInt32 - ) throws -> CoreTransaction { - guard !consumed else { - throw PlatformWalletError.unknown("CoreTransactionBuilder already consumed") - } - var out = FFICoreTransaction(tx_bytes: nil, tx_len: 0, fee: 0) - - let resolver = MnemonicResolver() - let result = withExtendedLifetime(resolver) { - core_wallet_tx_builder_build_signed( - handle, - wallet.handle, - accountType.ffi, - accountIndex, - resolver.handle, - &out - ) - } - // The FFI frees the builder on every path, so mark consumed before the check. - consumed = true - try result.check() - - guard out.tx_bytes != nil, out.tx_len > 0 else { - throw PlatformWalletError.unknown("FFI returned success but tx buffer was empty") - } - - return CoreTransaction(ffi: out, accountType: accountType, accountIndex: accountIndex) - } - /// Consume this configured builder, atomically select and reserve inputs, /// then sign after Rust has released the wallet-manager lock. public func finalizeAtomic( diff --git a/packages/swift-sdk/Sources/SwiftDashSDK/PlatformWallet/CoreWallet/ManagedCoreWallet.swift b/packages/swift-sdk/Sources/SwiftDashSDK/PlatformWallet/CoreWallet/ManagedCoreWallet.swift index 66798e6a0d..ec80b6402c 100644 --- a/packages/swift-sdk/Sources/SwiftDashSDK/PlatformWallet/CoreWallet/ManagedCoreWallet.swift +++ b/packages/swift-sdk/Sources/SwiftDashSDK/PlatformWallet/CoreWallet/ManagedCoreWallet.swift @@ -240,73 +240,6 @@ public class ManagedCoreWallet { // MARK: - Transactions - /// Broadcast a transaction built by `CoreTransactionBuilder.buildSigned`. - /// - /// The funding account captured at build time is forwarded so that a - /// definitive broadcast rejection releases the UTXO reservation - /// `buildSigned` took, letting an immediate retry reselect those inputs. - /// - /// Returns the authoritative accepted/rejected/unknown network outcome. - /// Throws only for local or FFI failures that prevented an outcome from - /// being determined. - public func broadcastTransactionWithOutcome( - _ tx: CoreTransaction - ) throws -> CoreTransactionBroadcastOutcome { - var txidPtr: UnsafeMutablePointer? = nil - let ffiResult = withUnsafePointer(to: tx.ffi) { txPtr in - core_wallet_broadcast_transaction( - handle, txPtr, tx.accountType.ffi, tx.accountIndex, &txidPtr - ) - } - let result = PlatformWalletResult(ffiResult) - - defer { - if let txidPtr { - platform_wallet_string_free(txidPtr) - } - } - - switch result.code { - case .success, .errorTransactionBroadcastRejected, - .errorTransactionBroadcastUnconfirmed: - guard let txidPtr else { - throw PlatformWalletError.nullPointer( - "core_wallet_broadcast_transaction returned a NULL txid pointer for \(result.code)" - ) - } - let txid = String(cString: txidPtr) - let reason = result.message ?? "" - - return try CoreTransactionBroadcastOutcome( - resultCode: result.code, - txid: txid, - reason: reason - ) - - default: - try result.throwIfError() - throw PlatformWalletError.unknown( - "core_wallet_broadcast_transaction returned an unexpected success state" - ) - } - } - - /// Compatibility wrapper preserving the former throwing API. - /// - /// New code should inspect `broadcastTransactionWithOutcome` so an unknown - /// outcome cannot be mistaken for a definitive rejection. - @available(*, deprecated, message: "Use broadcastTransactionWithOutcome(_:) and handle accepted/rejected/unknown") - public func broadcastTransaction(_ tx: CoreTransaction) throws -> String { - switch try broadcastTransactionWithOutcome(tx) { - case .accepted(let txid): - return txid - case .rejected(_, let reason): - throw PlatformWalletError.transactionBroadcastRejected(reason) - case .unknown(_, let reason): - throw PlatformWalletError.transactionBroadcastUnconfirmed(reason) - } - } - /// Consume and broadcast an atomically finalized transaction, returning /// the authoritative accepted/rejected/unknown network outcome. public func broadcastTransactionWithOutcome( diff --git a/packages/swift-sdk/SwiftExampleApp/TEST_PLAN.md b/packages/swift-sdk/SwiftExampleApp/TEST_PLAN.md index 7b77850701..d84b795873 100644 --- a/packages/swift-sdk/SwiftExampleApp/TEST_PLAN.md +++ b/packages/swift-sdk/SwiftExampleApp/TEST_PLAN.md @@ -120,7 +120,7 @@ Most Platform actions have hard preconditions. Establish these fixtures before s | CORE-07 | SPV sync (start / stop / progress) | Core | Essential | ✅ | | Global sync indicator (`ContentView`) → `platform_wallet_manager_spv_*`. Headers/filters/masternodes advance to tip. | | CORE-08 | QR scan recipient | Core | Manual | ✅ | | `QRScannerView`, reachable in the Send flow — but scanning needs a real camera the simulator doesn't have, so it can't be automated (`Tier=Manual`). On a device: Send → QR-scan button → point at a Dash address QR → recipient field populates. | | CORE-09 | Multiple HD accounts (within one wallet) | Core | Common | ✅ | | Account selection / `AccountDetailView`; balances per `account_index`. Distinct from holding multiple *wallets* — see CORE-14+. | -| CORE-10 | Multi-recipient Core send | Core | Common | ✅ | | Send flow (`SendTransactionView`, Core→Core) → "Add recipient" appends extra address/amount rows → `SendViewModel.coreRecipients` → one `CoreTransactionBuilder.addOutput` call per recipient → `buildSigned` + `ManagedCoreWallet.broadcastTransaction`. Rust coin-selects and builds one transaction with N outputs; balance drops by sum+fee. Verified: 2-output testnet send (txid `30010050…17f840fc`, txlock, 3 vouts) credited both recipients. | +| CORE-10 | Multi-recipient Core send | Core | Common | ✅ | | Send flow (`SendTransactionView`, Core→Core) → "Add recipient" appends extra address/amount rows → `SendViewModel.coreRecipients` → one `CoreTransactionBuilder.addOutput` call per recipient → `finalizeAtomic` + `ManagedCoreWallet.broadcastTransactionWithOutcome`. Rust coin-selects and builds one transaction with N outputs; balance drops by sum+fee. Verified: 2-output testnet send (txid `30010050…17f840fc`, txlock, 3 vouts) credited both recipients. | #### Multiple wallets on one device