What happened?
After a successful on-chain RBF boost, the home activity list is nondeterministic.
Expected (linking sticks): one pending boosted send (yellow timer / BoostingIcon, "Confirms in ±Xm", -10 422) and the +100 000 receive. The original send is hidden.
Race (linking lost): the replacement is a normal pending send (orange arrow, "Confirms in ±10m", no boost). The original stays visible as "Removed from Mempool". Receive is unchanged. Same amounts (-10 422). Balance is correct (89 578 = 100000 − 10422).
On-chain RBF is fine (fee 141 → 422 sat). Only activity metadata is wrong: isBoosted / boostTxIds on the replacement. Without boostTxIds, filterOutReplacedSentTransactions cannot hide the original. Without isBoosted, isBoosting() is false so BoostingIcon never appears.
Local repro 2026-09-21 10:11 (UTC+2) / log bitkit_2026-09-21_08-09-33.log: original 05b61da4…, replacement b21eb6e9…. Same UI seen earlier on master and on #1296 (seed paste — unrelated).
Expected behavior
- Home list after RBF: pending replacement with
BoostingIcon, original hidden.
- Detail of the replacement: boosted / confirming, not a plain send.
@boost_2 (BoostingIcon after BoostSuccessToast) is the correct assertion. Do not change e2e to accept the three-row list.
Steps to Reproduce
- New wallet on local / regtest Electrum (e2e or manual).
- Receive
100 000 on-chain and confirm it.
- Send
10 000 on-chain; leave it unconfirmed.
- Open that send → Boost → confirm RBF.
- After the boost success toast, look at the home activity list.
- Repeat. Sometimes the yellow timer + one send; sometimes orange arrow + "Removed from Mempool".
@boost_2 in bitkit-e2e-tests test/specs/boost.e2e.ts is this path. @boost_1 is CPFP and does not hit replace/evict.
Logs / Screenshots / Recordings
Expected (linking stuck):

Race (link overwritten) — 10:11 local, matches fail log 08-09-33:

Logs:
Fail key lines (UTC), original 05b61da4… → replacement b21eb6e9…:
08:11:03.858 RBF transaction created successfully!
Original: 05b61da4… (141 sats) Replacement: b21eb6e9… (422 sats)
08:11:04.063 Boost successful. newTxId: b21eb6e9…
08:11:04.193 Synced 2 payments ← replacement not in the list yet
08:11:04.624 OnchainTransactionReceived b21eb6e9… (−10422)
05b61da4… was replaced by 1 transaction(s)
08:11:04.695 OnchainTransactionReplaced 05b61da4… → [b21eb6e9…]
08:11:04.917 Marked transaction 05b61da4… as replaced
08:11:05.012 Processing replacement b21eb6e9… that was already in payments list
(no activity row yet → created as a plain send)
08:11:05.064 Updated replacement b21eb6e9… with boostTxId 05b61da4…
08:11:06.138 Synced 3 payments ← after the boost link; overlapping writer still running
Bitkit Version
2.4.1 (188). Reproduced on master and on #1296. Not introduced by #1296.
Device / OS
Android emulator (Pixel), local docker Electrum / BACKEND=local e2e.
Reproducibility
Sometimes (<50%)
Additional context
- New wallet, on-chain only. No Lightning channel.
BoostTransactionViewModel.handleRBFUpdate only marks the original isBoosted=true, then syncAsync(). The replacement is linked later in CoreService.handleOnchainTransactionReplaced (doesExist=false on the original; isBoosted=true + original txid in boostTxIds on the replacement).
- Both fail (
08-09-33) and pass (08-24-45) hit Processing replacement … already in payments list then Updated replacement … with boostTxId. The pass then cancelled a competing syncActivities (JobCancellationException) and a later Synced 3 payments kept the link. The fail’s Synced 3 payments after the link did not. Same events; metadata does not always survive.
- Home filter:
Activity.isReplacedSentTransaction requires !doesExist && SENT && txId in boostTxIds. Icon: isBoosting() = isBoosted && !confirmed && doesExist.
- iOS: same
handleOnchainTransactionReplaced / isReplacedSentTransaction / boosting icon. Not reproduced on iOS in this pass.
- Closed the mistaken e2e change that treated the race UI as product: bitkit-e2e-tests#241.
- Fix proposal (not a patch):
processOnchainPayment / buildNewOnchainActivity can upsert a blank replacement (isBoosted=false) after handleOnchainTransactionReplaced has already stamped boostTxIds. ServiceQueue.CORE reenters when a background {} suspends; Received / Replaced / syncActivities overlap. Logs do not name the last unboosted write.
- Never clobber existing
isBoosted, boostTxIds, or doesExist=false on an on-chain upsert (this alone makes the race harmless).
- Create the replacement already boosted in
handleOnchainTransactionReplaced, instead of blank insert then stamp.
- Serialize on-chain LDK activity events and sync payments sequentially (iOS already sequential).
What happened?
After a successful on-chain RBF boost, the home activity list is nondeterministic.
Expected (linking sticks): one pending boosted send (yellow timer /
BoostingIcon, "Confirms in ±Xm",-10 422) and the+100 000receive. The original send is hidden.Race (linking lost): the replacement is a normal pending send (orange arrow, "Confirms in ±10m", no boost). The original stays visible as "Removed from Mempool". Receive is unchanged. Same amounts (
-10 422). Balance is correct (89 578= 100000 − 10422).On-chain RBF is fine (fee 141 → 422 sat). Only activity metadata is wrong:
isBoosted/boostTxIdson the replacement. WithoutboostTxIds,filterOutReplacedSentTransactionscannot hide the original. WithoutisBoosted,isBoosting()is false soBoostingIconnever appears.Local repro 2026-09-21 10:11 (UTC+2) / log
bitkit_2026-09-21_08-09-33.log: original05b61da4…, replacementb21eb6e9…. Same UI seen earlier onmasterand on #1296 (seed paste — unrelated).Expected behavior
BoostingIcon, original hidden.@boost_2(BoostingIconafterBoostSuccessToast) is the correct assertion. Do not change e2e to accept the three-row list.Steps to Reproduce
100 000on-chain and confirm it.10 000on-chain; leave it unconfirmed.@boost_2inbitkit-e2e-teststest/specs/boost.e2e.tsis this path.@boost_1is CPFP and does not hit replace/evict.Logs / Screenshots / Recordings
Expected (linking stuck):
Race (link overwritten) — 10:11 local, matches fail log
08-09-33:Logs:
Fail key lines (UTC), original
05b61da4…→ replacementb21eb6e9…:Bitkit Version
2.4.1 (188). Reproduced on
masterand on #1296. Not introduced by #1296.Device / OS
Android emulator (Pixel), local docker Electrum /
BACKEND=locale2e.Reproducibility
Sometimes (<50%)
Additional context
BoostTransactionViewModel.handleRBFUpdateonly marks the originalisBoosted=true, thensyncAsync(). The replacement is linked later inCoreService.handleOnchainTransactionReplaced(doesExist=falseon the original;isBoosted=true+ original txid inboostTxIdson the replacement).08-09-33) and pass (08-24-45) hitProcessing replacement … already in payments listthenUpdated replacement … with boostTxId. The pass then cancelled a competingsyncActivities(JobCancellationException) and a laterSynced 3 paymentskept the link. The fail’sSynced 3 paymentsafter the link did not. Same events; metadata does not always survive.Activity.isReplacedSentTransactionrequires!doesExist && SENT && txId in boostTxIds. Icon:isBoosting() = isBoosted && !confirmed && doesExist.handleOnchainTransactionReplaced/isReplacedSentTransaction/ boosting icon. Not reproduced on iOS in this pass.processOnchainPayment/buildNewOnchainActivitycan upsert a blank replacement (isBoosted=false) afterhandleOnchainTransactionReplacedhas already stampedboostTxIds.ServiceQueue.COREreenters when abackground {}suspends; Received / Replaced /syncActivitiesoverlap. Logs do not name the last unboosted write.isBoosted,boostTxIds, ordoesExist=falseon an on-chain upsert (this alone makes the race harmless).handleOnchainTransactionReplaced, instead of blank insert then stamp.