Skip to content
Merged
Show file tree
Hide file tree
Changes from 32 commits
Commits
Show all changes
38 commits
Select commit Hold shift + click to select a range
a7636bf
feat(kotlin-sdk): split build/broadcast with reservation release for …
bfoss765 Jul 10, 2026
c94fea2
fix(kotlin-sdk): bound deferred-payment token lifetime; harden regist…
bfoss765 Jul 11, 2026
6766eb2
fix(kotlin-sdk): resolve rebase semantic conflicts onto feat/kotlin-s…
bfoss765 Jul 16, 2026
947ccdf
fix(kotlin-sdk): assert native code 26 for the stale-reservation-toke…
bfoss765 Jul 17, 2026
feba99f
fix(kotlin-sdk): bound the deferred-payment token on the reservation'…
bfoss765 Jul 17, 2026
5933d22
fix(kotlin-sdk): sweep deferred-payment tokens only when the final wa…
bfoss765 Jul 17, 2026
ea103b0
fix(kotlin-sdk): route deferred builds through the atomic finalize-an…
bfoss765 Jul 17, 2026
12143cb
fix(kotlin-sdk): delete the dead split register→broadcast chain
bfoss765 Jul 21, 2026
e0dc5e1
fix(kotlin-sdk): baseline the deferred token age on the pre-signing r…
bfoss765 Jul 21, 2026
4f7d295
fix(kotlin-sdk): give the V2 handle and token paths one wallet-genera…
bfoss765 Jul 21, 2026
74d26dd
fix(kotlin-sdk): validate the deferred token under the lock, consume …
bfoss765 Jul 21, 2026
8c2cd63
fix(kotlin-sdk): release deferred reservations at final-alias destroy…
bfoss765 Jul 21, 2026
6e0641a
fix(kotlin-sdk): split the conflated deferred-token error code into t…
bfoss765 Jul 21, 2026
ec4582c
docs(kotlin-sdk): correct buildSignedPayment KDoc to the finalize-and…
bfoss765 Jul 21, 2026
ce31710
fix(kotlin-sdk): make the deferred payment token an owning AutoClosea…
bfoss765 Jul 21, 2026
a11795b
style(kotlin-sdk): rustfmt wallet_manager.rs after the dead-chain del…
bfoss765 Jul 21, 2026
9f3d8f7
fix(kotlin-sdk): object-owning broadcast/release overloads for Signed…
bfoss765 Jul 21, 2026
f7a3dd4
fix(kotlin-sdk): retain a releasable account handle for CoinJoin-fund…
bfoss765 Jul 22, 2026
ec4f695
fix(kotlin-sdk): bind deferred-payment reservation cleanup to its own…
bfoss765 Jul 22, 2026
706f60d
fix(swift-sdk): surface deferred-token codes 26/27/28 as typed errors
bfoss765 Jul 22, 2026
80d0b1b
chore(rust-dashcore): pin to canonical dashpay dev rev with owner-tag…
bfoss765 Jul 22, 2026
6437d66
fix: renumber deferred-reservation error codes to 27/28/29
bfoss765 Jul 23, 2026
4cecd3d
fix(platform-wallet): owner-guard the broadcast-reject reservation re…
bfoss765 Jul 23, 2026
b9f4f01
fix(platform-wallet): enforce unique reservation ownership, stop wrap…
bfoss765 Jul 24, 2026
eb08094
fix(platform-wallet): bind registration to correct wallet generation …
bfoss765 Jul 25, 2026
0523def
fix(platform-wallet): linearize wallet removal with deferred broadcas…
bfoss765 Aug 1, 2026
7534892
fix(platform-wallet-ffi)!: move ErrorReservationWalletMismatch 29 -> …
bfoss765 Aug 1, 2026
473e91d
refactor(platform-wallet-ffi): drop stale HandleStorage::any left by …
bfoss765 Aug 1, 2026
a165bff
fix(platform-wallet): scope the lifecycle gate to the wallet generati…
bfoss765 Aug 2, 2026
83778f9
fix(platform-wallet): remove the wallet generation by identity, not b…
bfoss765 Aug 2, 2026
25acd7d
fix(platform-wallet): move the deferred-token trio off the codes #426…
bfoss765 Aug 3, 2026
9613e88
fix(kotlin-sdk): stop the stray 98 arm from shadowing PlatformWallet.…
bfoss765 Aug 4, 2026
d79c4ab
Merge v4.2-dev into port/v4.1/split-build-broadcast
QuantumExplorer Aug 6, 2026
9d4027e
fix(platform-wallet): reconcile stale deferred entries owner-guarded …
QuantumExplorer Aug 6, 2026
aa5d4e8
fix(platform-wallet): complete id-keyed side-registry teardown before…
QuantumExplorer Aug 6, 2026
9f994eb
fix(platform-wallet): FFI output hygiene for the deferred-payment sur…
QuantumExplorer Aug 6, 2026
fab4309
fix(kotlin-sdk): repair doc-comment opener lost in the v4.2-dev merge…
QuantumExplorer Aug 6, 2026
ebaca83
fix(kotlin-sdk): repair the second doc-opener/brace loss from the v4.…
QuantumExplorer Aug 6, 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
26 changes: 13 additions & 13 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
Expand Up @@ -239,6 +239,67 @@ sealed class DashSdkError(
class NotFound(message: String, cause: Throwable? = null) :
PlatformWallet(message, cause)

/**
* `ErrorTransactionBroadcastRejected` (native code 26). Core
* DEFINITIVELY rejected the core transaction: it is not on the network
* and will not get there. The build's UTXO reservation was released and,
* on the deferred (BIP70/BIP270) path, the token was consumed at the
* same time — so the inputs are spendable again and the token is gone.
*
* The definitive counterpart to [TransactionBroadcastUnconfirmed] (20),
* whose outcome is AMBIGUOUS and which therefore keeps its inputs
* reserved. Because the reservation and token are already gone, this is
* NOT retryable in place: address the rejection reason carried in the
* message, then rebuild with
* [buildSignedPayment][org.dashfoundation.dashsdk.wallet.ManagedPlatformWallet.buildSignedPayment]
* (deferred) or re-issue the send.
*/
class TransactionBroadcastRejected(message: String, cause: Throwable? = null) :
PlatformWallet(message, cause)

/**
* `ErrorStaleReservationToken` (native code 34). A deferred
* (BIP70/BIP270) [broadcastSigned][org.dashfoundation.dashsdk.wallet.ManagedPlatformWallet.broadcastSigned]
* token has outlived its funding reservation's 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 with
* [buildSignedPayment][org.dashfoundation.dashsdk.wallet.ManagedPlatformWallet.buildSignedPayment].
*
* 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 35). 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 36). 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 @@ -335,6 +396,20 @@ sealed class DashSdkError(
// RAW code via translateManagedIdentityNotFoundToZero (#4051)
// before this mapping ever runs. BREAKING for Kotlin hosts that
// caught DashSdkError.NotFound from platform-wallet operations.
//
// 98 is also what the wallet-was-REMOVED case returns on BOTH
// deferred-send paths:
// * deferred (BIP70/BIP270) TOKEN path — a signed-payment broadcast
// whose wallet is no longer registered in the manager, or a
// signed-payment finalize whose wallet was removed while it was
// being signed;
// * finalized-transaction HANDLE (V2) path — a tx-builder finalize
// whose wallet was removed or re-created during signing (no handle
// is published), or a V2 broadcast whose generation is gone.
// Every one reconciles the build's UTXO reservation before returning.
// Nothing was broadcast, and unlike ReservationWalletMismatch (36)
// no other live generation holds the payment either — so it is not
// retryable. See dashpay/platform#4185.
PLATFORM_WALLET_NOT_FOUND_CODE ->
PlatformWallet.NotFound(message, cause)
16 -> PlatformWallet.ShieldedBroadcastFailed(message, cause) // ErrorShieldedBroadcastFailed
Expand All @@ -345,6 +420,16 @@ sealed class DashSdkError(
23 -> PlatformWallet.AssetLockNotTracked(message, cause) // ErrorAssetLockNotTracked
24 -> PlatformWallet.AssetLockAlreadyConsumed(message, cause) // ErrorAssetLockAlreadyConsumed
25 -> PlatformWallet.AssetLockFundingMismatch(message, cause) // ErrorAssetLockFundingMismatch
26 -> PlatformWallet.TransactionBroadcastRejected(message, cause) // ErrorTransactionBroadcastRejected
// The deferred-token trio sits at the contiguous block 34-36 because
// 27-33 are claimed elsewhere: 27 ErrorShutdownIncomplete
// (dashpay/platform#4268, merged), 29 ErrorAssetLockInsufficientFunds
// (#4184), 31 ErrorSigningKeyUnavailable (#4183/#4259), 32
// ErrorTransactionBuild (#4247/#4256), 33 ErrorTransactionSigning
// (#4256). See packages/rs-platform-wallet-ffi/ERROR_CODE_REGISTRY.md.
34 -> PlatformWallet.StaleReservationToken(message, cause) // ErrorStaleReservationToken
35 -> PlatformWallet.ReservationTokenConsumed(message, cause) // ErrorReservationTokenConsumed
36 -> PlatformWallet.ReservationWalletMismatch(message, cause) // ErrorReservationWalletMismatch
Comment thread
coderabbitai[bot] marked this conversation as resolved.
// ErrorSigningKeyUnavailable — the STRUCTURED signer
// discriminator (dashpay/platform#4060 finding 7): the typed
// completion code rides the whole Rust round-trip, no message
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -262,6 +262,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` (34, aged out),
* `ErrorReservationTokenConsumed` (35, already consumed/unknown), or
* `ErrorReservationWalletMismatch` (36, 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,57 @@ 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,
)
// Native finalization has ALREADY inserted the payment and committed its
// reservation by the time this blob returns; the token only gains its
// owning NativeCleaner once fromRegisterBlob finishes constructing the
// SignedCoreTransaction. So if construction throws (allocation failure, a
// malformed blob from an ABI mismatch, or Cleaner-registration failure)
// the native token would be registered with no JVM owner able to release
// it, leaking the reservation until key-wallet's TTL. Parse the token
// first (its 8 big-endian bytes lead the blob) and release it defensively
// if ownership construction fails, mirroring the owner-guarded release on
// the rest of the deferred path (dashpay/platform#4185).
var token: Long? = null
return try {
token = java.nio.ByteBuffer.wrap(blob).long
ManagedPlatformWallet.SignedCoreTransaction.fromRegisterBlob(blob)
} catch (error: Throwable) {
token?.let { value ->
runCatching { WalletManagerNative.coreWalletReleaseSignedPayment(value) }
}
throw error
}
}

override fun close() {
cleanable.clean()
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,19 @@ class ManagedCoreWallet internal constructor(handle: Long) : AutoCloseable {
)
}

/**
* 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