-
Notifications
You must be signed in to change notification settings - Fork 3
fix: refresh receive after payment #1086
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from all commits
3259df9
30009ac
cb74553
199862f
0e7f432
2a3c258
2f968ad
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -4,6 +4,7 @@ import kotlinx.coroutines.CoroutineDispatcher | |
| import kotlinx.coroutines.delay | ||
| import kotlinx.coroutines.withContext | ||
| import to.bitkit.di.IoDispatcher | ||
| import to.bitkit.ext.runSuspendCatching | ||
| import to.bitkit.models.NewTransactionSheetDetails | ||
| import to.bitkit.models.NewTransactionSheetDirection | ||
| import to.bitkit.models.NewTransactionSheetType | ||
|
|
@@ -19,16 +20,21 @@ class NotifyPaymentReceivedHandler @Inject constructor( | |
| private val activityRepo: ActivityRepo, | ||
| private val receivedNotificationContent: ReceivedNotificationContent, | ||
| ) { | ||
| private val presentationClaimsLock = Any() | ||
| private val presentationClaims = mutableSetOf<String>() | ||
|
|
||
| suspend operator fun invoke( | ||
| command: NotifyPaymentReceived.Command, | ||
| ): Result<NotifyPaymentReceived.Result> = withContext(ioDispatcher) { | ||
| runCatching { | ||
| runSuspendCatching { | ||
| if (isPresentationClaimed(command)) return@runSuspendCatching NotifyPaymentReceived.Result.Skip | ||
|
|
||
| val shouldShow = when (command) { | ||
| is NotifyPaymentReceived.Command.Lightning -> shouldShowLightning(command) | ||
| is NotifyPaymentReceived.Command.Onchain -> shouldShowOnchain(command) | ||
| } | ||
|
|
||
| if (!shouldShow) return@runCatching NotifyPaymentReceived.Result.Skip | ||
| if (!shouldShow) return@runSuspendCatching NotifyPaymentReceived.Result.Skip | ||
|
|
||
| val details = buildSheetDetails(command) | ||
|
|
||
|
|
@@ -43,12 +49,39 @@ class NotifyPaymentReceivedHandler @Inject constructor( | |
| } | ||
| } | ||
|
|
||
| fun claimPresentation(command: NotifyPaymentReceived.Command): Boolean = claimPresentation(command) { true } | ||
|
|
||
| fun claimPresentation( | ||
| command: NotifyPaymentReceived.Command, | ||
| canPresent: () -> Boolean, | ||
| ): Boolean { | ||
| val key = presentationKey(command) ?: return false | ||
| return synchronized(presentationClaimsLock) { | ||
| canPresent() && presentationClaims.add(key) | ||
| } | ||
| } | ||
|
|
||
| suspend fun recordPresentation(command: NotifyPaymentReceived.Command) { | ||
| withContext(ioDispatcher) { | ||
| runSuspendCatching { markAsSeen(command) } | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. nit: could remove the key from |
||
| .onFailure { Logger.error("Failed to mark payment notification as presented", it, context = TAG) } | ||
| } | ||
| } | ||
|
|
||
| private fun isPresentationClaimed(command: NotifyPaymentReceived.Command): Boolean { | ||
| val key = presentationKey(command) ?: return false | ||
| return synchronized(presentationClaimsLock) { key in presentationClaims } | ||
| } | ||
|
|
||
| private fun presentationKey(command: NotifyPaymentReceived.Command): String? = when (command) { | ||
| is NotifyPaymentReceived.Command.Lightning -> command.event.paymentId?.let { "lightning:$it" } | ||
| is NotifyPaymentReceived.Command.Onchain -> "onchain:${command.event.txid}" | ||
| } | ||
|
|
||
| private suspend fun shouldShowLightning(command: NotifyPaymentReceived.Command.Lightning): Boolean { | ||
| val paymentId = command.event.paymentId ?: return false | ||
| delay(DELAY_FOR_ACTIVITY_SYNC_MS) | ||
| if (activityRepo.isActivitySeen(paymentId)) return false | ||
| activityRepo.markActivityAsSeen(paymentId) | ||
| return true | ||
| return !activityRepo.isActivitySeen(paymentId) | ||
| } | ||
|
|
||
| private suspend fun shouldShowOnchain(command: NotifyPaymentReceived.Command.Onchain): Boolean { | ||
|
|
@@ -60,12 +93,20 @@ class NotifyPaymentReceivedHandler @Inject constructor( | |
| command.event.txid, | ||
| command.event.details.amountSats.toULong(), | ||
| ) | ||
| if (shouldShowSheet) { | ||
| activityRepo.markOnchainActivityAsSeen(command.event.txid) | ||
| } | ||
| return shouldShowSheet | ||
| } | ||
|
|
||
| private suspend fun markAsSeen(command: NotifyPaymentReceived.Command) { | ||
| when (command) { | ||
| is NotifyPaymentReceived.Command.Lightning -> { | ||
| val paymentId = command.event.paymentId ?: return | ||
| activityRepo.markActivityAsSeen(paymentId) | ||
| } | ||
|
|
||
| is NotifyPaymentReceived.Command.Onchain -> activityRepo.markOnchainActivityAsSeen(command.event.txid) | ||
| } | ||
| } | ||
|
|
||
| private suspend fun retryShouldShowReceivedSheet(txid: String, amountSats: ULong): Boolean { | ||
| repeat(MAX_RETRIES) { | ||
| if (activityRepo.shouldShowReceivedSheet(txid, amountSats)) return true | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -42,6 +42,7 @@ import kotlinx.coroutines.withTimeoutOrNull | |
| import org.lightningdevkit.ldknode.Address | ||
| import org.lightningdevkit.ldknode.BalanceDetails | ||
| import org.lightningdevkit.ldknode.BestBlock | ||
| import org.lightningdevkit.ldknode.Bolt11Invoice | ||
| import org.lightningdevkit.ldknode.ChannelConfig | ||
| import org.lightningdevkit.ldknode.ChannelDataMigration | ||
| import org.lightningdevkit.ldknode.ChannelDetails | ||
|
|
@@ -65,6 +66,7 @@ import to.bitkit.env.Env | |
| import to.bitkit.ext.getSatsPerVByteFor | ||
| import to.bitkit.ext.nowMillis | ||
| import to.bitkit.ext.nowTimestamp | ||
| import to.bitkit.ext.runSuspendCatching | ||
| import to.bitkit.ext.toPeerDetailsList | ||
| import to.bitkit.ext.totalNextOutboundHtlcLimitSats | ||
| import to.bitkit.models.ALL_ADDRESS_TYPE_STRINGS | ||
|
|
@@ -73,6 +75,7 @@ import to.bitkit.models.NATIVE_WITNESS_TYPES | |
| import to.bitkit.models.NodeLifecycleState | ||
| import to.bitkit.models.OpenChannelResult | ||
| import to.bitkit.models.TransactionSpeed | ||
| import to.bitkit.models.WalletScope | ||
| import to.bitkit.models.safe | ||
| import to.bitkit.models.satsToMsat | ||
| import to.bitkit.models.toAddressType | ||
|
|
@@ -88,7 +91,6 @@ import to.bitkit.services.LnurlWithdrawResponse | |
| import to.bitkit.services.LspNotificationsService | ||
| import to.bitkit.services.NodeEventHandler | ||
| import to.bitkit.utils.AppError | ||
| import to.bitkit.models.WalletScope | ||
| import to.bitkit.utils.Logger | ||
| import to.bitkit.utils.ServiceError | ||
| import to.bitkit.utils.UrlValidator | ||
|
|
@@ -125,8 +127,9 @@ class LightningRepo @Inject constructor( | |
| private val _lightningState = MutableStateFlow(LightningState()) | ||
| val lightningState = _lightningState.asStateFlow() | ||
|
|
||
| private val _nodeEvents = MutableSharedFlow<Event>(extraBufferCapacity = 64) | ||
| val nodeEvents = _nodeEvents.asSharedFlow() | ||
| private val _nodeEventUpdates = MutableSharedFlow<NodeEventUpdate>(extraBufferCapacity = 64) | ||
| val nodeEventUpdates = _nodeEventUpdates.asSharedFlow() | ||
| val nodeEvents = nodeEventUpdates.map { it.event } | ||
|
|
||
| private val scope = CoroutineScope(bgDispatcher + SupervisorJob()) | ||
|
|
||
|
|
@@ -439,10 +442,55 @@ class LightningRepo @Inject constructor( | |
| private suspend fun onEvent(event: Event) { | ||
| handleLdkEvent(event) | ||
| recordProbeOutcome(event) | ||
| val settledReceiveInvoice = if (event is Event.PaymentReceived) { | ||
| val settledInvoice = invalidateSettledReceiveInvoice(event) | ||
| val paymentId = event.paymentId ?: event.paymentHash | ||
| runSuspendCatching { coreService.activity.handlePaymentEvent(paymentId) } | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. AppViewModel.handlePaymentReceived still calls |
||
| .onFailure { Logger.error("Failed to record received payment '$paymentId'", it, context = TAG) } | ||
| settledInvoice | ||
| } else { | ||
| null | ||
| } | ||
| _eventHandlers.toList().forEach { | ||
| runCatching { it.invoke(event) } | ||
| } | ||
| _nodeEvents.emit(event) | ||
| _nodeEventUpdates.emit( | ||
| NodeEventUpdate( | ||
| event = event, | ||
| settledReceiveInvoice = settledReceiveInvoice, | ||
| ), | ||
| ) | ||
| } | ||
|
|
||
| private suspend fun invalidateSettledReceiveInvoice(event: Event.PaymentReceived): SettledReceiveInvoice? { | ||
| val cachedReceive = runSuspendCatching { | ||
| cacheStore.data.first() | ||
| }.onFailure { | ||
| Logger.error("Failed to read cached receive invoice", it, context = TAG) | ||
| }.getOrNull() ?: return null | ||
| val cachedBolt11 = cachedReceive.bolt11 | ||
| if (cachedBolt11.isEmpty()) return null | ||
|
|
||
| val cachedPaymentHash = cachedReceive.bolt11PaymentHash.takeIf { it.isNotEmpty() } ?: runCatching { | ||
| Bolt11Invoice.fromStr(cachedBolt11).paymentHash() | ||
| }.onFailure { | ||
| Logger.error("Failed to parse cached receive invoice", it, context = TAG) | ||
| }.getOrNull() | ||
| if (cachedPaymentHash != event.paymentHash) return null | ||
|
|
||
| val invalidated = runSuspendCatching { | ||
| cacheStore.invalidateReceiveLightningInvoice(expectedBolt11 = cachedBolt11) | ||
| }.onFailure { | ||
| Logger.error("Failed to invalidate settled receive invoice", it, context = TAG) | ||
| }.getOrDefault(false) | ||
|
|
||
| return if (invalidated) { | ||
| SettledReceiveInvoice( | ||
| bolt11 = cachedBolt11, | ||
| ) | ||
| } else { | ||
| null | ||
| } | ||
| } | ||
|
|
||
| fun setRecoveryMode(enabled: Boolean) = _isRecoveryMode.update { enabled } | ||
|
|
@@ -1729,6 +1777,16 @@ class NodeRunTimeoutError(opName: String) : AppError("Timeout waiting for node t | |
| class GetPaymentsError : AppError("It wasn't possible get the payments") | ||
| class SyncUnhealthyError : AppError("Wallet sync failed before send") | ||
| class LnurlPayInvoiceMismatchError : AppError("The invoice did not match the requested payment. Payment cancelled.") | ||
|
|
||
| data class NodeEventUpdate( | ||
| val event: Event, | ||
| val settledReceiveInvoice: SettledReceiveInvoice? = null, | ||
| ) | ||
|
|
||
| data class SettledReceiveInvoice( | ||
| val bolt11: String, | ||
| ) | ||
|
|
||
| sealed class ProbeError(message: String) : AppError(message) { | ||
| class NoProbeHandles : ProbeError("No probe handles returned") | ||
| class TimedOut : ProbeError("Probe timed out") | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
could extract this duplicated logic to a
presentmethod like: