Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
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,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,
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
Loading