Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
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
2 changes: 1 addition & 1 deletion packages/kotlin-sdk/KotlinExampleApp/TEST_PLAN.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<CoreAddressPoolRestoreData>,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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)

Expand All @@ -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)

Expand All @@ -200,34 +199,6 @@ 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. */
external fun coreTxBuilderFinalize(
builder: Long,
Expand All @@ -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

Expand All @@ -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.
Expand Down Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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)
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down Expand Up @@ -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 {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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.
*
Expand Down
61 changes: 0 additions & 61 deletions packages/rs-platform-wallet-ffi/src/core_wallet/broadcast.rs
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -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;
Expand Down
Loading
Loading