Skip to content
Open
Show file tree
Hide file tree
Changes from 8 commits
Commits
Show all changes
111 commits
Select commit Hold shift + click to select a range
171e4cb
chore: bump rust-dashcore to the latest dev revision
jeanpierreroma Aug 14, 2026
d325049
fix(swift-sdk): delete the mirror of a transaction the wallet swept
jeanpierreroma Aug 14, 2026
49e5a5f
fix: address review — keep the winner's coin spent, fail a sweep that…
jeanpierreroma Aug 14, 2026
f435dcc
fix: hold a swept loser's inputs when the winner is not in the store
jeanpierreroma Aug 14, 2026
b57fb20
fix: carry the outpoints a sweep released instead of inferring them
jeanpierreroma Aug 14, 2026
4e0bd3a
fix(kotlin-sdk): do not free a coin a later transaction already re-cl…
jeanpierreroma Aug 14, 2026
6060cb4
fix(platform-wallet-storage): apply transaction sweeps in the SQLite …
jeanpierreroma Aug 14, 2026
b172c0a
fix(platform-wallet-storage): defer a release to a surviving record's…
jeanpierreroma Aug 14, 2026
04a76c4
fix: keep sweeps as ordered batches instead of one folded release set
jeanpierreroma Aug 15, 2026
0d81ce1
fix: let a record arriving after its sweep reinstate the transaction
jeanpierreroma Aug 15, 2026
46f74e9
fix: keep a sweep's releases when only one of its losers is reinstated
jeanpierreroma Aug 16, 2026
73852a9
fix: keep a sweep's claim durable when its funding TXO is still unknown
jeanpierreroma Aug 16, 2026
8b58966
fix(kotlin-sdk): declare the tombstone column's default so the upgrad…
jeanpierreroma Aug 16, 2026
4b38b64
fix: repoint chained-sweep tombstones instead of losing them
jeanpierreroma Aug 16, 2026
253ed2b
fix: fail closed when a persister has not attested sweep-removal support
jeanpierreroma Aug 16, 2026
f503cd5
fix: release JNI local references per sweep batch
jeanpierreroma Aug 16, 2026
78fd059
fix: withhold the watermark from the round whose sweep cannot be applied
jeanpierreroma Aug 16, 2026
a23f132
perf(platform-wallet-storage): hoist the invariant sweep claim sets
jeanpierreroma Aug 16, 2026
fe594eb
fix(swift-sdk): fail the round when the sweep's wallet lookup throws
jeanpierreroma Aug 16, 2026
38b5f7b
perf(platform-wallet-storage): skip the survivor set on a sweepless r…
jeanpierreroma Aug 16, 2026
eff8209
fix: scope a swept transaction's per-input decisions to the owning wa…
jeanpierreroma Aug 16, 2026
3a87366
fix: make a swept transaction's exclusion durable in every wallet's c…
jeanpierreroma Aug 16, 2026
6a30295
test(swift-sdk): pin the throwing wallet-lookup branch with a real seam
jeanpierreroma Aug 16, 2026
7bfa701
fix(kotlin-sdk): re-export v12 so the schema records the tombstone de…
jeanpierreroma Aug 16, 2026
b73e4c9
chore: repin rust-dashcore to the merged sweep-released-outpoints commit
jeanpierreroma Aug 17, 2026
83982b8
fix(swift-sdk): reinstate a swept transaction when a later record rev…
jeanpierreroma Aug 17, 2026
32010a5
fix(kotlin-sdk): reinstate a swept transaction when a later record re…
jeanpierreroma Aug 17, 2026
15579f0
fix(rs-unified-sdk-jni): emit an account's transactions before its UTXOs
jeanpierreroma Aug 17, 2026
de8879c
fix(rs-unified-sdk-jni): add onWalletChangesetTransactionsSwept to th…
jeanpierreroma Aug 17, 2026
33758cf
test(rs-platform-wallet): bound a_coalesced_sweep_and_watermark_never…
jeanpierreroma Aug 17, 2026
ea8a193
fix(swift-sdk): reconcile detached sweep tombstones when the winner r…
jeanpierreroma Aug 17, 2026
df88cda
fix(platform-wallet-ffi): carry sweeps on the size-tagged extension, …
jeanpierreroma Aug 17, 2026
c4c8a1f
fix(platform-wallet-ffi): require an atomic round before attesting sw…
jeanpierreroma Aug 17, 2026
ab2da2b
fix(platform-wallet): default `sweeps` when deserializing a pre-sweep…
jeanpierreroma Aug 17, 2026
ecfefdc
fix(swift-sdk): disable autosave so the atomic round is actually atomic
jeanpierreroma Aug 18, 2026
8e3c0aa
fix(platform-wallet-ffi): expose the sweep arrays as *const, not *mut
jeanpierreroma Aug 18, 2026
fab1a46
fix(platform-wallet-ffi): allow the manager entry point its argument …
jeanpierreroma Aug 18, 2026
b3aeb1c
fix(swift-sdk): follow the sweep arrays' const-ness into the test helper
jeanpierreroma Aug 18, 2026
ada626a
fix(kotlin-sdk): keep the sweep's SQL arity fixed regardless of relea…
jeanpierreroma Aug 18, 2026
5e6d4d4
perf(swift-sdk): index the open round's unsaved inserts
jeanpierreroma Aug 18, 2026
1fdc3fa
fix(kotlin-sdk): build the sweep's release set once, and free release…
jeanpierreroma Aug 18, 2026
363a330
fix(swift-sdk): free a released pending input instead of leaving it a…
jeanpierreroma Aug 18, 2026
ab54e0c
fix(kotlin-sdk): keep the sweep tombstone when the winner's own pendi…
jeanpierreroma Aug 18, 2026
8d3b37c
fix(swift-sdk): keep the sweep tombstone when the winner's own pendin…
jeanpierreroma Aug 18, 2026
5f8ea5f
fix(kotlin-sdk): clear a released coin's supersededByTxid with the ho…
jeanpierreroma Aug 18, 2026
4a14991
fix(swift-sdk): apply sweep releases by outpoint, clearing the dead w…
jeanpierreroma Aug 18, 2026
f4f6882
fix(kotlin-sdk): stamp the winner on a sweep hold so re-delivery cann…
jeanpierreroma Aug 18, 2026
ac36707
fix(swift-sdk): stamp the winner on a sweep hold so re-delivery canno…
jeanpierreroma Aug 18, 2026
cda18de
fix(kotlin-sdk): keep a stamped hold spent when the winner's own spen…
jeanpierreroma Aug 18, 2026
d35021f
fix(swift-sdk): stop the winner's own unconfirmed arrival downgrading…
jeanpierreroma Aug 18, 2026
24f7ef4
fix(platform-wallet-storage): apply a sweep's releases even when the …
jeanpierreroma Aug 18, 2026
d120e4a
fix(platform-wallet-storage): don't resurrect a co-swept parent's out…
jeanpierreroma Aug 18, 2026
55bf35e
fix(platform-wallet): cascade a sweep into the tracked asset locks it…
jeanpierreroma Aug 18, 2026
af55994
fix(platform-wallet): fail a sent DashPay payment whose transaction w…
jeanpierreroma Aug 18, 2026
7b5514f
fix(kotlin-sdk): make a hand-declared sweep capability fail structura…
jeanpierreroma Aug 18, 2026
679fb31
fix(platform-wallet-storage): scope the co-swept skip to parents whos…
jeanpierreroma Aug 18, 2026
7597157
style(platform-wallet): fold the sent-payment eligibility match into …
jeanpierreroma Aug 18, 2026
c6f801d
refactor(platform-wallet): share the fault-and-freeze bookkeeping bet…
jeanpierreroma Aug 18, 2026
010c1a8
refactor(platform-wallet-ffi): single-source the negotiated slot gate…
jeanpierreroma Aug 18, 2026
817608a
test(kotlin-sdk): pin the two-wallet released-pending deadlock the Sw…
jeanpierreroma Aug 18, 2026
aeac2cd
docs(kotlin-sdk): drop references to the SQL repoint helpers the bind…
jeanpierreroma Aug 18, 2026
1308279
docs(swift-sdk): stop describing the autosave fallback this branch di…
jeanpierreroma Aug 18, 2026
6e458d1
test(swift-sdk): pin the record-pass downgrade guard independently of…
jeanpierreroma Aug 18, 2026
d3cedc0
fix(platform-wallet): let a reinstating asset-lock upsert cancel a fo…
jeanpierreroma Aug 18, 2026
86568b4
fix(platform-wallet-storage): delete a co-swept parent's output outri…
jeanpierreroma Aug 18, 2026
56ae3c3
perf(swift-sdk): stop rescanning the round's unsaved inserts once per…
jeanpierreroma Aug 18, 2026
5cfca15
fix(platform-wallet): retry a swept payment's failure persist instead…
jeanpierreroma Aug 18, 2026
0414469
docs(platform-wallet): define CORE_SWEEP_REMOVAL by observable durabi…
jeanpierreroma Aug 18, 2026
1c8909e
perf(platform-wallet): hash the swept-txid set once before scanning t…
jeanpierreroma Aug 18, 2026
f99789b
fix(platform-wallet): ride the swept payment's Failed flip on the swe…
jeanpierreroma Aug 18, 2026
78bb226
fix(platform-wallet): retract a reinstated txid's payment flip from t…
jeanpierreroma Aug 18, 2026
45c70ec
fix(platform-wallet): stop stale reconcile evidence terminally confir…
jeanpierreroma Aug 18, 2026
fbf8eec
test(platform-wallet-storage): pin the synthetic-spent-row route into…
jeanpierreroma Aug 18, 2026
f9f3e66
fix(kotlin-sdk): give the asset-lock removal the consumed-row guard t…
jeanpierreroma Aug 18, 2026
0b1e705
chore(platform-wallet): route the invitation outpoint through the one…
jeanpierreroma Aug 18, 2026
c9becb9
fix(kotlin-sdk): delete a co-swept loser's pending claims instead of …
jeanpierreroma Aug 18, 2026
e4c940d
fix(swift-sdk): delete a co-swept loser's pending claims instead of t…
jeanpierreroma Aug 18, 2026
aab4998
fix(platform-wallet): withhold the sweep's payment flip from a backen…
jeanpierreroma Aug 18, 2026
a4dc829
fix(kotlin-sdk): keep a Consumed asset lock terminal through the upse…
jeanpierreroma Aug 18, 2026
7e676ec
chore: repin rust-dashcore to the merged linear-retain revision
jeanpierreroma Aug 19, 2026
559475d
fix(platform-wallet): re-validate staged payment flips against live m…
jeanpierreroma Aug 19, 2026
53d74e1
perf(platform-wallet): index superseded payment keys before filtering…
jeanpierreroma Aug 19, 2026
3918f86
perf(platform-wallet): scope the revalidation lock hold to the store …
jeanpierreroma Aug 19, 2026
1e709a8
fix(platform-wallet): ride the reinstatement confirmation on the rein…
jeanpierreroma Aug 19, 2026
be5afc3
fix(platform-wallet): give DASHPAY_PAYMENTS a name in the diagnostics…
jeanpierreroma Aug 19, 2026
6e6c30b
chore: repin rust-dashcore to the linear descendant-closure revision
jeanpierreroma Aug 19, 2026
5698419
Merge remote-tracking branch 'origin/v4.2-dev' into chore/bump-rust-d…
jeanpierreroma Aug 19, 2026
80ec1cb
fix(platform-wallet): require the atomic round before staging payment…
jeanpierreroma Aug 20, 2026
57a88e2
fix(platform-wallet-storage): bound the lifetime of unmaterialised sw…
jeanpierreroma Aug 20, 2026
1d16f59
fix(kotlin-sdk): bound the lifetime of swept pending-input tombstones
jeanpierreroma Aug 20, 2026
78ae25d
test(platform-wallet-storage): keep the re-point fixture inside the c…
jeanpierreroma Aug 20, 2026
1753535
fix(swift-sdk): bound the lifetime of swept pending-input tombstones
jeanpierreroma Aug 20, 2026
16e8891
refactor(platform-wallet): extract the sweep's payment-flip coupling …
jeanpierreroma Aug 20, 2026
59d5023
perf(unified-sdk-jni): allocate the sweep winner once per batch, not …
jeanpierreroma Aug 22, 2026
98bc011
fix(platform-wallet): never drop balance snapshots under wallets-map …
jeanpierreroma Aug 22, 2026
29a0f91
Merge remote-tracking branch 'origin/v4.2-dev' into chore/bump-rust-d…
jeanpierreroma Aug 22, 2026
81f6acf
fix(platform-wallet): key sweep-tombstone lifetime on the winner's mi…
jeanpierreroma Aug 24, 2026
d18f961
Merge remote-tracking branch 'origin/v4.2-dev' into chore/bump-rust-d…
jeanpierreroma Aug 24, 2026
1505912
fix(platform-wallet): preserve an unstamped tombstone for mempool-con…
jeanpierreroma Aug 24, 2026
1c02faf
docs(platform-wallet): state the ordered, non-commutative changeset m…
jeanpierreroma Aug 24, 2026
a76bc79
docs(platform-wallet-storage): align the V007 contract with unstamped…
jeanpierreroma Aug 24, 2026
4a02bf8
docs(kotlin-sdk): define CORE_SWEEP_REMOVAL as ordered exclusion, not…
jeanpierreroma Aug 24, 2026
5088fa4
fix(platform-wallet): refuse a sweep release for a coin a stored sett…
jeanpierreroma Aug 25, 2026
612d1d9
fix(platform-wallet-storage): harden the stored-claims veto - settled…
jeanpierreroma Aug 25, 2026
4fee0e8
docs(platform-wallet): align the sweep-batch contract with ordered, u…
jeanpierreroma Aug 25, 2026
4271795
chore(platform): merge v4.2-dev
shumkov Sep 7, 2026
f91d127
fix(platform-wallet): update rust-dashcore integration
shumkov Sep 7, 2026
7837691
fix(platform-wallet): preserve sweep persistence invariants
shumkov Sep 7, 2026
22db17a
fix(platform-wallet): carry a swept loser's inputs to every persister…
shumkov Sep 7, 2026
b1dbd99
fix(platform-wallet): preserve all swept input holds
shumkov Sep 7, 2026
7c14265
fix(platform-wallet): hold winner-linked swept inputs
shumkov Sep 7, 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
24 changes: 12 additions & 12 deletions Cargo.lock

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

16 changes: 8 additions & 8 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -52,14 +52,14 @@ members = [
]

[workspace.dependencies]
dashcore = { git = "https://github.com/dashpay/rust-dashcore", rev = "173ffac0fdc0c73dda0626cf385bbcfcf2437aeb" }
dash-network-seeds = { git = "https://github.com/dashpay/rust-dashcore", rev = "173ffac0fdc0c73dda0626cf385bbcfcf2437aeb" }
dash-spv = { git = "https://github.com/dashpay/rust-dashcore", rev = "173ffac0fdc0c73dda0626cf385bbcfcf2437aeb" }
key-wallet = { git = "https://github.com/dashpay/rust-dashcore", rev = "173ffac0fdc0c73dda0626cf385bbcfcf2437aeb" }
key-wallet-ffi = { git = "https://github.com/dashpay/rust-dashcore", rev = "173ffac0fdc0c73dda0626cf385bbcfcf2437aeb" }
key-wallet-manager = { git = "https://github.com/dashpay/rust-dashcore", rev = "173ffac0fdc0c73dda0626cf385bbcfcf2437aeb" }
dash-network = { git = "https://github.com/dashpay/rust-dashcore", rev = "173ffac0fdc0c73dda0626cf385bbcfcf2437aeb" }
dashcore-rpc = { git = "https://github.com/dashpay/rust-dashcore", rev = "173ffac0fdc0c73dda0626cf385bbcfcf2437aeb" }
dashcore = { git = "https://github.com/dashpay/rust-dashcore", rev = "51eafd8c82371ba88305ceb35b4c1604062bc6c8" }
dash-network-seeds = { git = "https://github.com/dashpay/rust-dashcore", rev = "51eafd8c82371ba88305ceb35b4c1604062bc6c8" }
dash-spv = { git = "https://github.com/dashpay/rust-dashcore", rev = "51eafd8c82371ba88305ceb35b4c1604062bc6c8" }
key-wallet = { git = "https://github.com/dashpay/rust-dashcore", rev = "51eafd8c82371ba88305ceb35b4c1604062bc6c8" }
key-wallet-ffi = { git = "https://github.com/dashpay/rust-dashcore", rev = "51eafd8c82371ba88305ceb35b4c1604062bc6c8" }
key-wallet-manager = { git = "https://github.com/dashpay/rust-dashcore", rev = "51eafd8c82371ba88305ceb35b4c1604062bc6c8" }
dash-network = { git = "https://github.com/dashpay/rust-dashcore", rev = "51eafd8c82371ba88305ceb35b4c1604062bc6c8" }
dashcore-rpc = { git = "https://github.com/dashpay/rust-dashcore", rev = "51eafd8c82371ba88305ceb35b4c1604062bc6c8" }

tokio-metrics = "0.5"

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -294,6 +294,35 @@ abstract class NativePersistenceBridge {
/** Close the current account bucket. Descriptor `([BI)I`. */
open fun onWalletChangesetAccountEnd(walletId: ByteArray, accountIndex: Int): Int = 0

/**
* Transactions the wallet removed this round, as raw 32-byte txids,
* each paired by index with the transaction that settled its inputs,
* plus the outpoints the removals actually freed. Fired once after the
* per-account decomposition, and only when the round swept something.
* Descriptor `([B[[B[[B[[B)I`.
*
* Each removed transaction was a recorded spend that its winner beat to
* one of its inputs, so it can never confirm. Every other slot on this
* bus is additive; this is the only removal, and an implementation that
* ignores it keeps dead rows that are handed back at the next load and
* re-create a balance the wallet has already corrected.
*
* [releasedOutpoints] holds 36-byte keys (raw txid followed by a
* little-endian vout) and is wallet-scoped, not attributed per removal:
* an implementation holds every input of every row it deletes, so it
* only needs to know which of them came free. Everything else it holds
* was taken by the transaction that won those inputs and must stay
* spent. The set cannot be inferred from [supersededBy] — that
* transaction may pay entirely to outside addresses and never be
* reported here at all.
*/
open fun onWalletChangesetTransactionsSwept(
walletId: ByteArray,
txids: Array<ByteArray>,
supersededBy: Array<ByteArray>,
releasedOutpoints: Array<ByteArray>,
): Int = 0

// ── Identities ────────────────────────────────────────────────────

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -921,7 +921,15 @@ class PlatformWalletPersistenceHandler(
isConfirmed = isConfirmed,
isInstantLocked = isInstantLocked,
isLocked = isLocked,
isSpent = existing?.isSpent ?: false,
// The wallet is handing this outpoint over as a UTXO, so it
// holds it unspent — authoritative, and the only thing that
// lifts a mark with no spender behind it. The sweep path
// parks the inputs it cannot resolve in exactly that state
// (`holdSpentWithoutSpender`); a rescan re-delivering the
// coin lands here and frees it. A row whose spend is still
// on record keeps its flag — the pending drain below owns
// that transition.
isSpent = existing?.isSpent == true && existing.spendingTxid != null,
walletId = walletId,
txid = txid,
spendingTxid = existing?.spendingTxid,
Expand Down Expand Up @@ -985,6 +993,67 @@ class PlatformWalletPersistenceHandler(

override fun onWalletChangesetAccountEnd(walletId: ByteArray, accountIndex: Int): Int = 0

/**
* Delete the mirror of transactions the wallet swept.
*
* Each was a recorded spend that its winner beat to one of its inputs,
* so it can never confirm and Rust has already dropped it. Keeping the
* rows would hand them back at the next load and re-create a balance the
* wallet has already corrected.
*
* The TXOs the transaction created go with it (`txos.txid` cascades).
* The ones it *spent* split in two, and [releasedOutpoints] is the
* authority on which is which: an outpoint named there came free, and
* every other input the loser claimed was taken by the transaction that
* beat it and is gone for good.
*
* That split cannot be worked out here. A swept loser is always
* unconfirmed upstream, and this store flips `isSpent` only for a
* spender that reached a block, so the loser holds its inputs by link
* alone at `isSpent = 0`; deleting the row nils the link and every one
* of those coins would return to the restore set, the winner's
* included. Nor can the winner's own row settle it — it may pay only to
* outside addresses and never be recorded here, and even a relevant one
* is not guaranteed to land in the same round as the sweep.
*
* Both updates run before the delete: the foreign key nulls
* `spendingTxid` on delete, and after that nothing finds those rows.
*
* Transaction rows are keyed by txid alone, shared across wallets by
* design, and a sweep is a statement about the transaction rather than
* about one wallet's view of it — so the row goes without narrowing to
* the emitting wallet.
*/
override fun onWalletChangesetTransactionsSwept(
walletId: ByteArray,
txids: Array<ByteArray>,
supersededBy: Array<ByteArray>,
releasedOutpoints: Array<ByteArray>,
): Int = guarded {
stage(walletId) { db ->
if (db.walletDao().getByWalletId(walletId) == null) return@stage
// Hold every input first, then free the ones upstream named: the
// released set is wallet-scoped across the round's removals, so
// it is applied once rather than per transaction.
//
// The order is load-bearing, not cosmetic. Holding detaches the
// rows this round's removals still claim, and the release only
// touches detached rows — so a coin some later transaction in the
// same round already re-claimed keeps that claim instead of being
// freed out from under it.
for (txid in txids) {
db.txoDao().holdSpentWithoutSpender(txid)
}
for (outpoint in releasedOutpoints) {
db.txoDao().releaseByOutpoint(outpoint)
}
for (txid in txids) {
db.transactionDao().deleteByTxid(txid)
}
}
0
}

// ── Identities ────────────────────────────────────────────────────

override fun onPersistIdentityUpsert(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,47 @@ interface TxoDao {
@Query("SELECT * FROM txos WHERE spendingTxid = :spendingTxid AND isSpent = 0")
suspend fun getUnspentBySpendingTxid(spendingTxid: ByteArray): List<TxoEntity>

/**
* Hold every coin [spendingTxid] claimed out of the restore set, without
* naming a spender for them.
*
* Used when [spendingTxid] was swept: it can never confirm, so its claim
* is not a spend, but most of the coins it named really were taken — by
* the transaction that beat it. A swept transaction is always
* unconfirmed, so its inputs sit at `isSpent = 0`, and deleting it would
* otherwise return all of them, the consumed one included.
*
* Run this *before* deleting the transaction, while the link that
* identifies those rows is still there — the foreign key nulls
* `spendingTxid` on delete, and afterwards nothing finds them. Then
* clear the genuinely free ones with [releaseByOutpoint].
*/
@Query("UPDATE txos SET isSpent = 1, spendingTxid = NULL, spendingInputIndex = NULL WHERE spendingTxid = :spendingTxid")
suspend fun holdSpentWithoutSpender(spendingTxid: ByteArray)

/**
* Mark one outpoint unspent again — a coin a sweep released, meaning no
* surviving transaction spent it *at the time the sweep was computed*.
*
* Keyed by outpoint rather than by spender because that is how upstream
* reports it: the transaction that took the other inputs may never be
* recorded here at all, so the released set is the only authority on
* which coins came free.
*
* `spendingTxid IS NULL` is what keeps that from overreaching. A round
* can carry both a release and a later transaction that legitimately
* spends the freed coin — merging folds several events together, and
* every record is written before sweeps are processed — so by the time
* this runs the coin may already be claimed again. Only rows
* [holdSpentWithoutSpender] just detached qualify; anything a live
* transaction still claims keeps that claim.
*/
@Query(
"UPDATE txos SET isSpent = 0, spendingInputIndex = NULL " +
"WHERE outpoint = :outpoint AND spendingTxid IS NULL",
)
suspend fun releaseByOutpoint(outpoint: ByteArray)

@Upsert
suspend fun upsert(txo: TxoEntity)

Expand Down
Loading
Loading