Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
dbc4279
feat(kotlin-sdk): split build/broadcast with reservation release for …
bfoss765 Jul 10, 2026
7f658eb
fix(kotlin-sdk): bound deferred-payment token lifetime; harden regist…
bfoss765 Jul 11, 2026
63f2926
fix(kotlin-sdk): resolve rebase semantic conflicts onto feat/kotlin-s…
bfoss765 Jul 16, 2026
dcabaaf
fix(kotlin-sdk): assert native code 26 for the stale-reservation-toke…
bfoss765 Jul 17, 2026
b20b295
fix(kotlin-sdk): bound the deferred-payment token on the reservation'…
bfoss765 Jul 17, 2026
26ee8b7
fix(kotlin-sdk): sweep deferred-payment tokens only when the final wa…
bfoss765 Jul 17, 2026
32cd702
fix(kotlin-sdk): route deferred builds through the atomic finalize-an…
bfoss765 Jul 17, 2026
3cefea9
fix(kotlin-sdk): delete the dead split register→broadcast chain
bfoss765 Jul 21, 2026
b2dfb4c
fix(kotlin-sdk): baseline the deferred token age on the pre-signing r…
bfoss765 Jul 21, 2026
ebb5e7a
fix(kotlin-sdk): give the V2 handle and token paths one wallet-genera…
bfoss765 Jul 21, 2026
45d146e
fix(kotlin-sdk): validate the deferred token under the lock, consume …
bfoss765 Jul 21, 2026
44be489
fix(kotlin-sdk): release deferred reservations at final-alias destroy…
bfoss765 Jul 21, 2026
2b41aab
fix(kotlin-sdk): split the conflated deferred-token error code into t…
bfoss765 Jul 21, 2026
6b52b10
docs(kotlin-sdk): correct buildSignedPayment KDoc to the finalize-and…
bfoss765 Jul 21, 2026
54b88d1
fix(kotlin-sdk): make the deferred payment token an owning AutoClosea…
bfoss765 Jul 21, 2026
2ebe209
style(kotlin-sdk): rustfmt wallet_manager.rs after the dead-chain del…
bfoss765 Jul 21, 2026
f15fdcc
fix(kotlin-sdk): object-owning broadcast/release overloads for Signed…
bfoss765 Jul 21, 2026
a253ecd
fix(kotlin-sdk): retain a releasable account handle for CoinJoin-fund…
bfoss765 Jul 22, 2026
7413497
fix(kotlin-sdk): bind deferred-payment reservation cleanup to its own…
bfoss765 Jul 22, 2026
199645b
fix(swift-sdk): surface deferred-token codes 26/27/28 as typed errors
bfoss765 Jul 22, 2026
911c8f7
docs(kotlin-sdk): correct broadcast KDoc to the split deferred-token …
bfoss765 Jul 22, 2026
2d29451
fix(platform-wallet): age-guard the V2 finalized-transaction handle b…
bfoss765 Jul 21, 2026
c64af1a
test+docs(v2-age-guard): boundary test covers both account types; Kot…
bfoss765 Jul 21, 2026
ea4f783
fix(platform-wallet): age-guard the V2 abandon/free reservation release
bfoss765 Jul 22, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,56 @@ sealed class DashSdkError(
cause,
)

/**
* `ErrorStaleReservationToken` (native code 26). A payment's funding
* reservation has outlived its lifetime: key-wallet's TTL may already
* have swept and re-selected the inputs, so acting on it could touch a
* newer, unrelated reservation. The call did NOT touch the network.
* NOT retryable in place — rebuild the payment.
*
* The code is shared by BOTH deferred-payment surfaces (the messages
* distinguish them): a deferred (BIP70/BIP270)
* [broadcastSigned][org.dashfoundation.dashsdk.wallet.ManagedPlatformWallet.broadcastSigned]
* token, rebuilt with
* [buildSignedPayment][org.dashfoundation.dashsdk.wallet.ManagedPlatformWallet.buildSignedPayment];
* and a token-less V2 finalized handle whose
* [broadcastTransaction][org.dashfoundation.dashsdk.wallet.ManagedCoreWallet.broadcastTransaction]
* aged past the same reservation bound (abandon still works at any age).
*
* Sibling of the other two deferred-token failures this code used to
* conflate: [ReservationTokenConsumed] (unknown / already broadcast /
* already released) and [ReservationWalletMismatch] (minted against a
* different wallet generation).
*/
class StaleReservationToken(message: String, cause: Throwable? = null) :
PlatformWallet(message, cause)

/**
* `ErrorReservationTokenConsumed` (native code 27). A deferred
* (BIP70/BIP270) [broadcastSigned][org.dashfoundation.dashsdk.wallet.ManagedPlatformWallet.broadcastSigned]
* token is unknown, already broadcast, or already released — the guard
* that turns a double-broadcast (or a broadcast after release) into a
* typed error instead of a second send. The call did NOT touch the
* network. NOT retryable: rebuild the payment with
* [buildSignedPayment][org.dashfoundation.dashsdk.wallet.ManagedPlatformWallet.buildSignedPayment].
* (Release is idempotent and never raises this.)
*/
class ReservationTokenConsumed(message: String, cause: Throwable? = null) :
PlatformWallet(message, cause)

/**
* `ErrorReservationWalletMismatch` (native code 28). A deferred
* (BIP70/BIP270) [broadcastSigned][org.dashfoundation.dashsdk.wallet.ManagedPlatformWallet.broadcastSigned]
* token was minted against a different wallet *generation* than the one
* broadcasting it (e.g. a wallet re-created under the same id); its
* reservation lives in that other generation's reservation set. The call
* did NOT touch the network and did NOT consume the rightful owner's
* token. NOT retryable through this handle: rebuild the payment with
* [buildSignedPayment][org.dashfoundation.dashsdk.wallet.ManagedPlatformWallet.buildSignedPayment].
*/
class ReservationWalletMismatch(message: String, cause: Throwable? = null) :
PlatformWallet(message, cause)

/**
* Any other `PlatformWalletFFIResultCode` without a dedicated type.
* Carries the platform-wallet [nativeCode] (already de-offset) and
Expand Down Expand Up @@ -245,6 +295,9 @@ sealed class DashSdkError(
23 -> PlatformWallet.AssetLockNotTracked(message, cause) // ErrorAssetLockNotTracked
24 -> PlatformWallet.AssetLockAlreadyConsumed(message, cause) // ErrorAssetLockAlreadyConsumed
25 -> PlatformWallet.AssetLockFundingMismatch(message, cause) // ErrorAssetLockFundingMismatch
26 -> PlatformWallet.StaleReservationToken(message, cause) // ErrorStaleReservationToken
27 -> PlatformWallet.ReservationTokenConsumed(message, cause) // ErrorReservationTokenConsumed
28 -> PlatformWallet.ReservationWalletMismatch(message, cause) // ErrorReservationWalletMismatch
else -> PlatformWallet.Generic(code, message, cause)
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -246,6 +246,45 @@ internal object WalletManagerNative {
*/
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
* native call. Selection and reservation commit as a single unit under the
* wallet-manager lock, closing the double-selection window. CONSUMES
* [builder]. [accountType]/[accountIndex] identify the funding account
* (0 BIP44, 1 BIP32, 2 CoinJoin); [coreSignerHandle] is a
* `MnemonicResolverHandle`.
*
* Returns a big-endian BLOB decoded into a `SignedCoreTransaction`:
* `u64 token, u64 feeDuffs, u32 txidLen, txid utf8, u32 txBytesLen, txBytes`.
*/
external fun coreWalletFinalizeSignedPayment(
builder: Long,
walletHandle: Long,
accountType: Int,
accountIndex: Int,
coreSignerHandle: Long,
): ByteArray

/**
* `core_wallet_signed_payment_broadcast` — broadcast the payment behind
* [token], reconciling its reservation on failure and consuming the token.
* Rather than double-broadcasting, an unusable token throws one of three
* sibling codes — `ErrorStaleReservationToken` (26, aged out),
* `ErrorReservationTokenConsumed` (27, already consumed/unknown), or
* `ErrorReservationWalletMismatch` (28, different wallet generation).
* [coreHandle] must resolve to the wallet the token was minted against.
* Returns the txid as a lowercase hex string.
*/
external fun coreWalletBroadcastSignedPayment(coreHandle: Long, token: Long): String

/**
* `core_wallet_signed_payment_release` — release the funding reservation
* behind [token] and drop it. Idempotent: releasing an unknown /
* already-consumed token is a silent no-op.
*/
external fun coreWalletReleaseSignedPayment(token: Long)

/**
* Enumerate the wallet's Platform-payment addresses with cached credit
* balances, as a big-endian blob: `u32 rowCount` then per row
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,38 @@ class CoreTransactionBuilder internal constructor(network: Network) : AutoClosea
return FinalizedCoreTransaction(transaction, fee)
}

/**
* 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].
*/
internal fun finalizeSignedPayment(
wallet: ManagedPlatformWallet,
accountType: AccountType,
accountIndex: Int,
coreSignerHandle: Long,
): ManagedPlatformWallet.SignedCoreTransaction {
require(accountIndex >= 0) { "accountIndex must be non-negative" }
require(coreSignerHandle != 0L) { "coreSignerHandle must be non-zero" }
// Validate every borrowed dependency before transferring builder
// ownership. Once getAndSet(0) runs, JNI consumes the native builder.
val walletHandle = wallet.handle
val builderPtr = handleRef.getAndSet(0)
check(builderPtr != 0L) { "CoreTransactionBuilder has been consumed or closed" }
val blob = WalletManagerNative.coreWalletFinalizeSignedPayment(
builderPtr,
walletHandle,
accountType.ffiValue,
accountIndex,
coreSignerHandle,
)
return ManagedPlatformWallet.SignedCoreTransaction.fromRegisterBlob(blob)
}

override fun close() {
cleanable.clean()
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,21 +40,54 @@ class ManagedCoreWallet internal constructor(handle: Long) : AutoCloseable {
tx.accountIndex,
)

/** Consume and broadcast a V2 finalized transaction. */
/**
* Consume and broadcast a V2 finalized transaction. A handle held past the
* reservation age bound throws the typed
* [StaleReservationToken][org.dashfoundation.dashsdk.errors.DashSdkError.PlatformWallet.StaleReservationToken]
* (native code 26, shared with the deferred-token surface) instead of
* broadcasting against inputs key-wallet's TTL may have re-selected.
*
* On that refusal the handle has **already been consumed** by this call, so
* a follow-up [abandonTransaction] is an invalid-handle error, not a recovery
* path — there is nothing left to release, and the aged reservation is left
* for key-wallet's TTL to reclaim (releasing it by outpoint could free a
* newer build's reservation). Recover by rebuilding the transaction.
*/
fun broadcastTransaction(tx: FinalizedCoreTransaction): String =
WalletManagerNative.coreWalletBroadcastSignedTransactionV2(
handle,
tx.takeForBroadcast(),
)

/** Consume without sending and release the selected inputs immediately. */
/**
* Consume a finalized transaction without sending. Below the reservation age
* bound this releases the selected inputs immediately so a rebuild can
* reselect them. If the handle has aged past the bound the by-outpoint
* release is skipped — key-wallet's TTL may already have swept and
* re-reserved the outpoint, so releasing it could free a newer build's
* reservation — and the aged reservation is left for the TTL to reclaim; the
* handle is torn down either way.
*/
fun abandonTransaction(tx: FinalizedCoreTransaction) {
WalletManagerNative.coreWalletAbandonSignedTransactionV2(
handle,
tx.takeForAbandon(),
)
}

/**
* Broadcast the deferred payment behind [token] and return its txid. An
* unusable token surfaces as one of the three sibling deferred-token
* errors — aged out
* ([org.dashfoundation.dashsdk.errors.DashSdkError.PlatformWallet.StaleReservationToken]),
* already consumed / unknown
* ([org.dashfoundation.dashsdk.errors.DashSdkError.PlatformWallet.ReservationTokenConsumed]),
* or a different wallet generation
* ([org.dashfoundation.dashsdk.errors.DashSdkError.PlatformWallet.ReservationWalletMismatch]).
*/
internal fun broadcastSignedPayment(token: Long): String =
WalletManagerNative.coreWalletBroadcastSignedPayment(handle, token)

override fun close() {
cleanable.clean()
}
Expand Down
Loading
Loading