From b27d0e0aecda2f759b4a01f7452d0c51cda9d3e0 Mon Sep 17 00:00:00 2001 From: jvsena42 Date: Tue, 5 May 2026 06:30:44 -0300 Subject: [PATCH 1/5] fix: await for lighting receive event to prevent false positive from gift card flow --- .../to/bitkit/repositories/BlocktankRepo.kt | 49 ++++++++++++++++--- app/src/main/java/to/bitkit/utils/Errors.kt | 1 + 2 files changed, 43 insertions(+), 7 deletions(-) diff --git a/app/src/main/java/to/bitkit/repositories/BlocktankRepo.kt b/app/src/main/java/to/bitkit/repositories/BlocktankRepo.kt index 43f123ab45..991f9aff7f 100644 --- a/app/src/main/java/to/bitkit/repositories/BlocktankRepo.kt +++ b/app/src/main/java/to/bitkit/repositories/BlocktankRepo.kt @@ -20,13 +20,17 @@ import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.toImmutableList import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.CoroutineStart import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.async +import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.currentCoroutineContext import kotlinx.coroutines.delay import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.filterIsInstance import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.flow import kotlinx.coroutines.flow.flowOn @@ -37,7 +41,10 @@ import kotlinx.coroutines.flow.update import kotlinx.coroutines.isActive import kotlinx.coroutines.launch import kotlinx.coroutines.withContext +import kotlinx.coroutines.withTimeoutOrNull +import org.lightningdevkit.ldknode.Bolt11Invoice import org.lightningdevkit.ldknode.ChannelDetails +import org.lightningdevkit.ldknode.Event import to.bitkit.async.ServiceQueue import to.bitkit.data.CacheStore import to.bitkit.di.BgDispatcher @@ -46,6 +53,7 @@ import to.bitkit.ext.calculateRemoteBalance import to.bitkit.ext.nowTimestamp import to.bitkit.models.BlocktankBackupV1 import to.bitkit.models.EUR +import to.bitkit.models.msatCeilOf import to.bitkit.services.CoreService import to.bitkit.services.LightningService import to.bitkit.utils.Logger @@ -458,26 +466,52 @@ class BlocktankRepo @Inject constructor( } } - private suspend fun claimGiftCodeWithLiquidity(code: String, amount: ULong): GiftClaimResult { + private suspend fun claimGiftCodeWithLiquidity(code: String, amount: ULong): GiftClaimResult = coroutineScope { val invoice = lightningRepo.createInvoice( amountSats = null, description = "blocktank-gift-code:$code", expirySeconds = 3600u, ).getOrThrow() + val expectedPaymentHash = Bolt11Invoice.fromStr(invoice).paymentHash() + Logger.debug("Created invoice for gift code, requesting payment from LSP", context = TAG) + val paymentReceivedDeferred = async(start = CoroutineStart.UNDISPATCHED) { + lightningRepo.nodeEvents + .filterIsInstance() + .first { it.paymentHash == expectedPaymentHash } + } + val giftResponse = ServiceQueue.CORE.background { giftPay(invoice = invoice) } - Logger.debug("Gift payment request completed: id=${giftResponse.id}", context = TAG) + Logger.debug( + "Gift payment request completed: id='${giftResponse.id}', awaiting LDK PaymentReceived", + context = TAG, + ) + + val paymentReceived = withTimeoutOrNull(GIFT_PAYMENT_RECEIVE_TIMEOUT) { + paymentReceivedDeferred.await() + } + + if (paymentReceived == null) { + paymentReceivedDeferred.cancel() + throw ServiceError.GiftClaimPaymentNotReceived() + } + + Logger.debug( + "Gift payment confirmed by LDK: hash='${paymentReceived.paymentHash}', " + + "amountMsat='${paymentReceived.amountMsat}'", + context = TAG, + ) + + val receivedSats = msatCeilOf(paymentReceived.amountMsat).toLong() - return GiftClaimResult.SuccessWithLiquidity( - paymentHashOrTxId = giftResponse.bolt11PaymentId ?: giftResponse.id, - sats = giftResponse.bolt11Payment?.paidSat?.toLong() - ?: giftResponse.appliedGiftCode?.giftSat?.toLong() - ?: amount.toLong(), + GiftClaimResult.SuccessWithLiquidity( + paymentHashOrTxId = paymentReceived.paymentHash, + sats = receivedSats.takeIf { it > 0 } ?: amount.toLong(), invoice = invoice, code = code, ) @@ -517,6 +551,7 @@ class BlocktankRepo @Inject constructor( private const val DEFAULT_SOURCE = "bitkit-android" private const val PEER_CONNECTION_DELAY_MS = 2_000L private val TIMEOUT_GIFT_CODE = 30.seconds + private val GIFT_PAYMENT_RECEIVE_TIMEOUT = 45.seconds } } diff --git a/app/src/main/java/to/bitkit/utils/Errors.kt b/app/src/main/java/to/bitkit/utils/Errors.kt index 5dd62ed956..5ec25ced03 100644 --- a/app/src/main/java/to/bitkit/utils/Errors.kt +++ b/app/src/main/java/to/bitkit/utils/Errors.kt @@ -21,6 +21,7 @@ sealed class ServiceError(message: String) : AppError(message) { class CurrencyRateUnavailable : ServiceError("Currency rate unavailable") class BlocktankInfoUnavailable : ServiceError("Blocktank info not available") class GeoBlocked : ServiceError("Geo blocked user") + class GiftClaimPaymentNotReceived : ServiceError("Gift claim payment not received") } class HttpError(message: String, val code: Int = 500, cause: Throwable? = null) : AppError(message, cause) From 51c999feb948ee808cf9c94a53da52f34309a6c7 Mon Sep 17 00:00:00 2001 From: jvsena42 Date: Tue, 5 May 2026 06:33:39 -0300 Subject: [PATCH 2/5] doc: add changelog fragment for gift card false positive fix --- changelog.d/next/929.fixed.md | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog.d/next/929.fixed.md diff --git a/changelog.d/next/929.fixed.md b/changelog.d/next/929.fixed.md new file mode 100644 index 0000000000..6a4c2270a7 --- /dev/null +++ b/changelog.d/next/929.fixed.md @@ -0,0 +1 @@ +Prevent false-positive confetti when a Lightning gift code redemption fails after the LSP request. From e3688209006f352041e4d497733640d7c0af28cb Mon Sep 17 00:00:00 2001 From: jvsena42 Date: Tue, 5 May 2026 08:02:14 -0300 Subject: [PATCH 3/5] chore: add debug-only qa delay before awaiting gift payment --- .../java/to/bitkit/repositories/BlocktankRepo.kt | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/app/src/main/java/to/bitkit/repositories/BlocktankRepo.kt b/app/src/main/java/to/bitkit/repositories/BlocktankRepo.kt index 991f9aff7f..9b8dc83c57 100644 --- a/app/src/main/java/to/bitkit/repositories/BlocktankRepo.kt +++ b/app/src/main/java/to/bitkit/repositories/BlocktankRepo.kt @@ -492,6 +492,15 @@ class BlocktankRepo @Inject constructor( context = TAG, ) + if (Env.isDebug && GIFT_QA_PRE_RECEIVE_DELAY > Duration.ZERO) { + Logger.debug( + "QA window open: sleeping '$GIFT_QA_PRE_RECEIVE_DELAY' before awaiting LDK PaymentReceived " + + "(disable wifi now to simulate routing failure)", + context = TAG, + ) + delay(GIFT_QA_PRE_RECEIVE_DELAY) + } + val paymentReceived = withTimeoutOrNull(GIFT_PAYMENT_RECEIVE_TIMEOUT) { paymentReceivedDeferred.await() } @@ -552,6 +561,11 @@ class BlocktankRepo @Inject constructor( private const val PEER_CONNECTION_DELAY_MS = 2_000L private val TIMEOUT_GIFT_CODE = 30.seconds private val GIFT_PAYMENT_RECEIVE_TIMEOUT = 45.seconds + + // QA aid: in debug builds only, pause after `giftPay` returns and before awaiting + // the LDK PaymentReceived event, so a tester can disable wifi/peer to simulate a + // routing failure. Set to Duration.ZERO to disable. + private val GIFT_QA_PRE_RECEIVE_DELAY: Duration = 15.seconds } } From 5b59166572eeb8034c892f48588696ba9db9f5c7 Mon Sep 17 00:00:00 2001 From: jvsena42 Date: Tue, 5 May 2026 08:46:31 -0300 Subject: [PATCH 4/5] fix: skip launching intent on activity recreate --- app/src/main/java/to/bitkit/ui/MainActivity.kt | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/to/bitkit/ui/MainActivity.kt b/app/src/main/java/to/bitkit/ui/MainActivity.kt index 421853b1b8..a2b9dfedec 100644 --- a/app/src/main/java/to/bitkit/ui/MainActivity.kt +++ b/app/src/main/java/to/bitkit/ui/MainActivity.kt @@ -83,7 +83,11 @@ class MainActivity : FragmentActivity() { desc = getString(R.string.notification__channel_node__body), importance = NotificationManager.IMPORTANCE_LOW ) - appViewModel.handleDeeplinkIntent(intent) + // Skip on Activity recreation (e.g. locale change) — Android re-delivers the + // launching intent and would otherwise re-trigger deeplink flows like the gift sheet. + if (savedInstanceState == null) { + appViewModel.handleDeeplinkIntent(intent) + } installSplashScreen() enableAppEdgeToEdge() From 64b14f4cf3dd00ed211b0b119560c5b8701a7635 Mon Sep 17 00:00:00 2001 From: jvsena42 Date: Tue, 5 May 2026 08:47:54 -0300 Subject: [PATCH 5/5] doc: update changelog fragment with both gift card fixes --- changelog.d/next/929.fixed.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/changelog.d/next/929.fixed.md b/changelog.d/next/929.fixed.md index 6a4c2270a7..4b970ab00b 100644 --- a/changelog.d/next/929.fixed.md +++ b/changelog.d/next/929.fixed.md @@ -1 +1 @@ -Prevent false-positive confetti when a Lightning gift code redemption fails after the LSP request. +Fix gift card flow showing false-positive confetti when the LSP payment fails, and re-opening unexpectedly after an app language change.