Skip to content

Commit 3124ac0

Browse files
committed
refactor: skip ldk-node replayed event using cache
1 parent ffc0674 commit 3124ac0

File tree

8 files changed

+60
-62
lines changed

8 files changed

+60
-62
lines changed

app/src/main/java/to/bitkit/data/CacheStore.kt

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,10 @@ class CacheStore @Inject constructor(
113113
}
114114
}
115115

116+
suspend fun setLastLightningPayment(paymentId: String) {
117+
store.updateData { it.copy(lastLightningPaymentId = paymentId) }
118+
}
119+
116120
suspend fun reset() {
117121
store.updateData { AppCacheData() }
118122
Logger.info("Deleted all app cached data.")
@@ -134,5 +138,6 @@ data class AppCacheData(
134138
val backupStatuses: Map<BackupCategory, BackupItemStatus> = mapOf(),
135139
val deletedActivities: List<String> = listOf(),
136140
val activitiesPendingDelete: List<String> = listOf(),
141+
val lastLightningPaymentId: String? = null,
137142
val pendingBoostActivities: List<PendingBoostActivity> = listOf(),
138143
)

app/src/main/java/to/bitkit/domain/commands/NotifyPaymentReceived.kt

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,18 +8,18 @@ sealed interface NotifyPaymentReceived {
88

99
sealed interface Command : NotifyPaymentReceived {
1010
val sats: ULong
11-
val paymentId: String
11+
val paymentHashOrTxId: String
1212
val includeNotification: Boolean
1313

1414
data class Lightning(
1515
override val sats: ULong,
16-
override val paymentId: String,
16+
override val paymentHashOrTxId: String,
1717
override val includeNotification: Boolean = false,
1818
) : Command
1919

2020
data class Onchain(
2121
override val sats: ULong,
22-
override val paymentId: String,
22+
override val paymentHashOrTxId: String,
2323
override val includeNotification: Boolean = false,
2424
) : Command
2525

@@ -28,15 +28,15 @@ sealed interface NotifyPaymentReceived {
2828
when (event) {
2929
is Event.PaymentReceived -> Lightning(
3030
sats = event.amountMsat / 1000u,
31-
paymentId = event.paymentHash,
31+
paymentHashOrTxId = event.paymentHash,
3232
includeNotification = includeNotification,
3333
)
3434

3535
is Event.OnchainTransactionReceived -> {
3636
val amountSats = event.details.amountSats
3737
Onchain(
3838
sats = amountSats.toULong(),
39-
paymentId = event.txid,
39+
paymentHashOrTxId = event.txid,
4040
includeNotification = includeNotification,
4141
).takeIf {
4242
amountSats > 0

app/src/main/java/to/bitkit/domain/commands/NotifyPaymentReceivedHandler.kt

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -35,12 +35,11 @@ class NotifyPaymentReceivedHandler @Inject constructor(
3535
command: NotifyPaymentReceived.Command,
3636
): Result<NotifyPaymentReceived.Result> = withContext(ioDispatcher) {
3737
runCatching {
38-
delay(DELAY_FOR_ACTIVITY_SYNC_MS)
39-
4038
val shouldShow = when (command) {
4139
is NotifyPaymentReceived.Command.Lightning -> true
4240
is NotifyPaymentReceived.Command.Onchain -> {
43-
activityRepo.shouldShowPaymentReceived(command.paymentId, command.sats)
41+
delay(DELAY_FOR_ACTIVITY_SYNC_MS)
42+
activityRepo.shouldShowPaymentReceived(command.paymentHashOrTxId, command.sats)
4443
}
4544
}
4645

@@ -53,7 +52,7 @@ class NotifyPaymentReceivedHandler @Inject constructor(
5352
is NotifyPaymentReceived.Command.Onchain -> NewTransactionSheetType.ONCHAIN
5453
},
5554
direction = NewTransactionSheetDirection.RECEIVED,
56-
paymentHashOrTxId = command.paymentId,
55+
paymentHashOrTxId = command.paymentHashOrTxId,
5756
sats = satsLong,
5857
)
5958

app/src/main/java/to/bitkit/repositories/LightningRepo.kt

Lines changed: 17 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -87,7 +87,7 @@ class LightningRepo @Inject constructor(
8787

8888
private val scope = CoroutineScope(bgDispatcher + SupervisorJob())
8989

90-
private var cachedEventHandler: NodeEventHandler? = null
90+
private var _eventHandler: NodeEventHandler? = null
9191
private val _isRecoveryMode = MutableStateFlow(false)
9292
val isRecoveryMode = _isRecoveryMode.asStateFlow()
9393

@@ -193,6 +193,8 @@ class LightningRepo @Inject constructor(
193193
try {
194194
_lightningState.update { it.copy(nodeLifecycleState = NodeLifecycleState.Starting) }
195195

196+
this@LightningRepo._eventHandler = eventHandler
197+
196198
// Setup if not already setup
197199
if (lightningService.node == null) {
198200
val setupResult = setup(walletIndex, customServerUrl, customRgsServerUrl)
@@ -211,21 +213,12 @@ class LightningRepo @Inject constructor(
211213
if (getStatus()?.isRunning == true) {
212214
Logger.info("LDK node already running", context = TAG)
213215
_lightningState.update { it.copy(nodeLifecycleState = NodeLifecycleState.Running) }
214-
lightningService.listenForEvents(onEvent = { event ->
215-
handleLdkEvent(event)
216-
eventHandler?.invoke(event)
217-
})
216+
lightningService.listenForEvents(::onEvent)
218217
return@withContext Result.success(Unit)
219218
}
220219

221220
// Start the node service
222-
lightningService.start(timeout) { event ->
223-
handleLdkEvent(event)
224-
eventHandler?.invoke(event)
225-
ldkNodeEventBus.emit(event)
226-
}
227-
228-
this@LightningRepo.cachedEventHandler = eventHandler
221+
lightningService.start(timeout, ::onEvent)
229222

230223
_lightningState.update { it.copy(nodeLifecycleState = NodeLifecycleState.Running) }
231224

@@ -266,6 +259,12 @@ class LightningRepo @Inject constructor(
266259
}
267260
}
268261

262+
private suspend fun onEvent(event: Event) {
263+
handleLdkEvent(event)
264+
_eventHandler?.invoke(event)
265+
ldkNodeEventBus.emit(event)
266+
}
267+
269268
fun setRecoveryMode(enabled: Boolean) {
270269
_isRecoveryMode.value = enabled
271270
}
@@ -330,18 +329,21 @@ class LightningRepo @Inject constructor(
330329
refreshChannelCache()
331330
}
332331
}
332+
333333
is Event.ChannelReady -> {
334334
scope.launch {
335335
refreshChannelCache()
336336
}
337337
}
338+
338339
is Event.ChannelClosed -> {
339340
val channelId = event.channelId
340341
val reason = event.reason?.toString() ?: ""
341342
scope.launch {
342343
registerClosedChannel(channelId, reason)
343344
}
344345
}
346+
345347
else -> {
346348
// Other events don't need special handling
347349
}
@@ -436,7 +438,7 @@ class LightningRepo @Inject constructor(
436438
start(
437439
shouldRetry = false,
438440
customServerUrl = newServerUrl,
439-
eventHandler = cachedEventHandler,
441+
eventHandler = _eventHandler,
440442
).onFailure { startError ->
441443
Logger.warn("Failed ldk-node config change, attempting recovery…")
442444
restartWithPreviousConfig()
@@ -463,7 +465,7 @@ class LightningRepo @Inject constructor(
463465
start(
464466
shouldRetry = false,
465467
customRgsServerUrl = newRgsUrl,
466-
eventHandler = cachedEventHandler,
468+
eventHandler = _eventHandler,
467469
).onFailure { startError ->
468470
Logger.warn("Failed ldk-node config change, attempting recovery…")
469471
restartWithPreviousConfig()
@@ -488,7 +490,7 @@ class LightningRepo @Inject constructor(
488490

489491
start(
490492
shouldRetry = false,
491-
eventHandler = cachedEventHandler,
493+
eventHandler = _eventHandler,
492494
).onSuccess {
493495
Logger.debug("Successfully started node with previous config")
494496
}.onFailure { e ->

app/src/main/java/to/bitkit/services/LightningService.kt

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import kotlinx.coroutines.flow.flow
99
import kotlinx.coroutines.flow.flowOn
1010
import kotlinx.coroutines.isActive
1111
import kotlinx.coroutines.launch
12+
import kotlinx.coroutines.withContext
1213
import kotlinx.coroutines.withTimeout
1314
import org.lightningdevkit.ldknode.Address
1415
import org.lightningdevkit.ldknode.AnchorChannelsConfig
@@ -674,11 +675,11 @@ class LightningService @Inject constructor(
674675
// region events
675676
private var shouldListenForEvents = true
676677

677-
suspend fun listenForEvents(onEvent: NodeEventHandler? = null) {
678+
suspend fun listenForEvents(onEvent: NodeEventHandler? = null) = withContext(bgDispatcher) {
678679
while (shouldListenForEvents) {
679-
val node = this.node ?: let {
680+
val node = this@LightningService.node ?: let {
680681
Logger.error(ServiceError.NodeNotStarted.message.orEmpty())
681-
return
682+
return@withContext
682683
}
683684
val event = node.nextEventAsync()
684685
Logger.debug("LDK-node event fired: ${jsonLogOf(event)}")

app/src/main/java/to/bitkit/ui/sheets/GiftSheet.kt

Lines changed: 5 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -33,25 +33,21 @@ fun GiftSheet(
3333

3434
val onSuccessState = rememberUpdatedState { details: NewTransactionSheetDetails ->
3535
appViewModel.hideSheet()
36-
appViewModel.showNewTransactionSheet(details = details, event = null)
36+
appViewModel.showNewTransactionSheet(details)
3737
}
3838

3939
LaunchedEffect(Unit) {
4040
viewModel.successEvent.collect { details ->
41-
onSuccessState.value(details)
41+
onSuccessState.value.invoke(details)
4242
}
4343
}
4444

4545
LaunchedEffect(Unit) {
4646
viewModel.navigationEvent.collect { route ->
4747
when (route) {
48-
is GiftRoute.Success -> {
49-
appViewModel.hideSheet()
50-
}
51-
else -> {
52-
navController.navigate(route) {
53-
popUpTo(GiftRoute.Loading) { inclusive = false }
54-
}
48+
is GiftRoute.Success -> appViewModel.hideSheet()
49+
else -> navController.navigate(route) {
50+
popUpTo(GiftRoute.Loading) { inclusive = false }
5551
}
5652
}
5753
}

app/src/main/java/to/bitkit/viewmodels/AppViewModel.kt

Lines changed: 14 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ import org.lightningdevkit.ldknode.SpendableUtxo
5353
import org.lightningdevkit.ldknode.Txid
5454
import to.bitkit.BuildConfig
5555
import to.bitkit.R
56+
import to.bitkit.data.CacheStore
5657
import to.bitkit.data.SettingsStore
5758
import to.bitkit.data.keychain.Keychain
5859
import to.bitkit.data.resetPin
@@ -124,6 +125,7 @@ class AppViewModel @Inject constructor(
124125
private val blocktankRepo: BlocktankRepo,
125126
private val appUpdaterService: AppUpdaterService,
126127
private val notifyPaymentReceivedHandler: NotifyPaymentReceivedHandler,
128+
private val cacheStore: CacheStore,
127129
) : ViewModel() {
128130
val healthState = healthRepo.healthState
129131

@@ -332,7 +334,6 @@ class AppViewModel @Inject constructor(
332334
direction = NewTransactionSheetDirection.RECEIVED,
333335
sats = amount,
334336
),
335-
event,
336337
)
337338
activityRepo.insertActivityFromCjit(cjitEntry = cjitEntry, channel = channel)
338339
} else {
@@ -346,10 +347,21 @@ class AppViewModel @Inject constructor(
346347

347348
private fun notifyPaymentReceived(event: Event) {
348349
val command = NotifyPaymentReceived.Command.from(event) ?: return
350+
349351
viewModelScope.launch(bgDispatcher) {
352+
// Skip lightning payment events replayed by ldk-node on startup
353+
if (command is NotifyPaymentReceived.Command.Lightning) {
354+
val cachedId = cacheStore.data.first().lastLightningPaymentId
355+
if (command.paymentHashOrTxId == cachedId) {
356+
Logger.debug("Skipping replayed Lightning payment: ${command.paymentHashOrTxId}", context = TAG)
357+
return@launch
358+
}
359+
cacheStore.setLastLightningPayment(command.paymentHashOrTxId)
360+
}
361+
350362
notifyPaymentReceivedHandler(command).onSuccess { result ->
351363
if (result is NotifyPaymentReceived.Result.ShowSheet) {
352-
showNewTransactionSheet(result.details, event)
364+
showNewTransactionSheet(result.details)
353365
}
354366
}
355367
}
@@ -1409,7 +1421,6 @@ class AppViewModel @Inject constructor(
14091421

14101422
fun showNewTransactionSheet(
14111423
details: NewTransactionSheetDetails,
1412-
event: Event? = null,
14131424
) = viewModelScope.launch {
14141425
if (backupRepo.isRestoring.value) return@launch
14151426

@@ -1418,22 +1429,6 @@ class AppViewModel @Inject constructor(
14181429
return@launch
14191430
}
14201431

1421-
if (event is Event.PaymentReceived) {
1422-
val activity = activityRepo.findActivityByPaymentId(
1423-
paymentHashOrTxId = event.paymentHash,
1424-
type = ActivityFilter.ALL,
1425-
txType = PaymentType.RECEIVED,
1426-
retry = false,
1427-
).getOrNull()
1428-
1429-
// TODO check if this is still needed now that we're disabling the sheet during restore
1430-
// TODO Temporary fix while ldk-node bug is not fixed https://github.com/synonymdev/bitkit-android/pull/297
1431-
if (activity != null) {
1432-
Logger.warn("Activity ${activity.rawId()} already exists, skipping sheet", context = TAG)
1433-
return@launch
1434-
}
1435-
}
1436-
14371432
hideSheet()
14381433

14391434
_showNewTransaction.update { true }

app/src/test/java/to/bitkit/domain/commands/NotifyPaymentReceivedHandlerTest.kt

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -61,8 +61,8 @@ class NotifyPaymentReceivedHandlerTest : BaseUnitTest() {
6161
}
6262

6363
@Test
64-
fun `lightning payment returns ShowSheet by default`() = test {
65-
val command = NotifyPaymentReceived.Command.Lightning(sats = 1000uL, paymentId = "hash123")
64+
fun `lightning payment returns ShowSheet`() = test {
65+
val command = NotifyPaymentReceived.Command.Lightning(sats = 1000uL, paymentHashOrTxId = "hash123")
6666

6767
val result = sut(command)
6868

@@ -80,7 +80,7 @@ class NotifyPaymentReceivedHandlerTest : BaseUnitTest() {
8080
fun `lightning payment returns ShowNotification when includeNotification is true`() = test {
8181
val command = NotifyPaymentReceived.Command.Lightning(
8282
sats = 1000uL,
83-
paymentId = "hash123",
83+
paymentHashOrTxId = "hash123",
8484
includeNotification = true,
8585
)
8686

@@ -99,7 +99,7 @@ class NotifyPaymentReceivedHandlerTest : BaseUnitTest() {
9999
@Test
100100
fun `onchain payment returns ShowSheet when shouldShowPaymentReceived returns true`() = test {
101101
whenever(activityRepo.shouldShowPaymentReceived(any(), any())).thenReturn(true)
102-
val command = NotifyPaymentReceived.Command.Onchain(sats = 5000uL, paymentId = "txid456")
102+
val command = NotifyPaymentReceived.Command.Onchain(sats = 5000uL, paymentHashOrTxId = "txid456")
103103

104104
val result = sut(command)
105105

@@ -116,7 +116,7 @@ class NotifyPaymentReceivedHandlerTest : BaseUnitTest() {
116116
@Test
117117
fun `onchain payment returns Skip when shouldShowPaymentReceived is false`() = test {
118118
whenever(activityRepo.shouldShowPaymentReceived(any(), any())).thenReturn(false)
119-
val command = NotifyPaymentReceived.Command.Onchain(sats = 5000uL, paymentId = "txid456")
119+
val command = NotifyPaymentReceived.Command.Onchain(sats = 5000uL, paymentHashOrTxId = "txid456")
120120

121121
val result = sut(command)
122122

@@ -128,7 +128,7 @@ class NotifyPaymentReceivedHandlerTest : BaseUnitTest() {
128128
@Test
129129
fun `onchain payment calls shouldShowPaymentReceived with correct parameters`() = test {
130130
whenever(activityRepo.shouldShowPaymentReceived(any(), any())).thenReturn(true)
131-
val command = NotifyPaymentReceived.Command.Onchain(sats = 7500uL, paymentId = "txid789")
131+
val command = NotifyPaymentReceived.Command.Onchain(sats = 7500uL, paymentHashOrTxId = "txid789")
132132

133133
sut(command)
134134

@@ -137,7 +137,7 @@ class NotifyPaymentReceivedHandlerTest : BaseUnitTest() {
137137

138138
@Test
139139
fun `lightning payment does not call shouldShowPaymentReceived`() = test {
140-
val command = NotifyPaymentReceived.Command.Lightning(sats = 1000uL, paymentId = "hash123")
140+
val command = NotifyPaymentReceived.Command.Lightning(sats = 1000uL, paymentHashOrTxId = "hash123")
141141

142142
sut(command)
143143

0 commit comments

Comments
 (0)