Skip to content

Commit 6fddaf5

Browse files
authored
Merge pull request #929 from synonymdev/fix/gift-card-falsse-positive
fix: await ln payment before gift card confetti
2 parents 4599e58 + 46aa7f7 commit 6fddaf5

4 files changed

Lines changed: 59 additions & 8 deletions

File tree

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

Lines changed: 42 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -20,13 +20,17 @@ import kotlinx.collections.immutable.persistentListOf
2020
import kotlinx.collections.immutable.toImmutableList
2121
import kotlinx.coroutines.CoroutineDispatcher
2222
import kotlinx.coroutines.CoroutineScope
23+
import kotlinx.coroutines.CoroutineStart
2324
import kotlinx.coroutines.SupervisorJob
25+
import kotlinx.coroutines.async
26+
import kotlinx.coroutines.coroutineScope
2427
import kotlinx.coroutines.currentCoroutineContext
2528
import kotlinx.coroutines.delay
2629
import kotlinx.coroutines.flow.MutableStateFlow
2730
import kotlinx.coroutines.flow.StateFlow
2831
import kotlinx.coroutines.flow.asStateFlow
2932
import kotlinx.coroutines.flow.distinctUntilChanged
33+
import kotlinx.coroutines.flow.filterIsInstance
3034
import kotlinx.coroutines.flow.first
3135
import kotlinx.coroutines.flow.flow
3236
import kotlinx.coroutines.flow.flowOn
@@ -37,7 +41,10 @@ import kotlinx.coroutines.flow.update
3741
import kotlinx.coroutines.isActive
3842
import kotlinx.coroutines.launch
3943
import kotlinx.coroutines.withContext
44+
import kotlinx.coroutines.withTimeoutOrNull
45+
import org.lightningdevkit.ldknode.Bolt11Invoice
4046
import org.lightningdevkit.ldknode.ChannelDetails
47+
import org.lightningdevkit.ldknode.Event
4148
import to.bitkit.async.ServiceQueue
4249
import to.bitkit.data.CacheStore
4350
import to.bitkit.di.BgDispatcher
@@ -46,6 +53,7 @@ import to.bitkit.ext.calculateRemoteBalance
4653
import to.bitkit.ext.nowTimestamp
4754
import to.bitkit.models.BlocktankBackupV1
4855
import to.bitkit.models.EUR
56+
import to.bitkit.models.msatCeilOf
4957
import to.bitkit.services.CoreService
5058
import to.bitkit.services.LightningService
5159
import to.bitkit.utils.Logger
@@ -459,26 +467,52 @@ class BlocktankRepo @Inject constructor(
459467
}
460468
}
461469

462-
private suspend fun claimGiftCodeWithLiquidity(code: String, amount: ULong): GiftClaimResult {
470+
private suspend fun claimGiftCodeWithLiquidity(code: String, amount: ULong): GiftClaimResult = coroutineScope {
463471
val invoice = lightningRepo.createInvoice(
464472
amountSats = null,
465473
description = "blocktank-gift-code:$code",
466474
expirySeconds = 1.hours.inWholeSeconds.toUInt(),
467475
).getOrThrow()
468476

477+
val expectedPaymentHash = Bolt11Invoice.fromStr(invoice).paymentHash()
478+
469479
Logger.debug("Created invoice for gift code, requesting payment from LSP", context = TAG)
470480

481+
val paymentReceivedDeferred = async(start = CoroutineStart.UNDISPATCHED) {
482+
lightningRepo.nodeEvents
483+
.filterIsInstance<Event.PaymentReceived>()
484+
.first { it.paymentHash == expectedPaymentHash }
485+
}
486+
471487
val giftResponse = ServiceQueue.CORE.background {
472488
giftPay(invoice = invoice)
473489
}
474490

475-
Logger.debug("Gift payment request completed: id=${giftResponse.id}", context = TAG)
491+
Logger.debug(
492+
"Gift payment request completed: id='${giftResponse.id}', awaiting LDK PaymentReceived",
493+
context = TAG,
494+
)
495+
496+
val paymentReceived = withTimeoutOrNull(GIFT_PAYMENT_RECEIVE_TIMEOUT) {
497+
paymentReceivedDeferred.await()
498+
}
499+
500+
if (paymentReceived == null) {
501+
paymentReceivedDeferred.cancel()
502+
throw ServiceError.GiftClaimPaymentNotReceived()
503+
}
504+
505+
Logger.debug(
506+
"Gift payment confirmed by LDK: hash='${paymentReceived.paymentHash}', " +
507+
"amountMsat='${paymentReceived.amountMsat}'",
508+
context = TAG,
509+
)
510+
511+
val receivedSats = msatCeilOf(paymentReceived.amountMsat).toLong()
476512

477-
return GiftClaimResult.SuccessWithLiquidity(
478-
paymentHashOrTxId = giftResponse.bolt11PaymentId ?: giftResponse.id,
479-
sats = giftResponse.bolt11Payment?.paidSat?.toLong()
480-
?: giftResponse.appliedGiftCode?.giftSat?.toLong()
481-
?: amount.toLong(),
513+
GiftClaimResult.SuccessWithLiquidity(
514+
paymentHashOrTxId = paymentReceived.paymentHash,
515+
sats = receivedSats.takeIf { it > 0 } ?: amount.toLong(),
482516
invoice = invoice,
483517
code = code,
484518
)
@@ -518,6 +552,7 @@ class BlocktankRepo @Inject constructor(
518552
private const val DEFAULT_SOURCE = "bitkit-android"
519553
private const val PEER_CONNECTION_DELAY_MS = 2_000L
520554
private val TIMEOUT_GIFT_CODE = 30.seconds
555+
private val GIFT_PAYMENT_RECEIVE_TIMEOUT = 45.seconds
521556
}
522557
}
523558

app/src/main/java/to/bitkit/ui/MainActivity.kt

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,10 @@ import to.bitkit.viewmodels.WalletViewModel
6363

6464
@AndroidEntryPoint
6565
class MainActivity : FragmentActivity() {
66+
private companion object {
67+
const val KEY_CONSUMED_DEEPLINK_URI = "consumed_deeplink_uri"
68+
}
69+
6670
private val appViewModel by viewModels<AppViewModel>()
6771
private val walletViewModel by viewModels<WalletViewModel>()
6872
private val blocktankViewModel by viewModels<BlocktankViewModel>()
@@ -83,7 +87,12 @@ class MainActivity : FragmentActivity() {
8387
desc = getString(R.string.notification__channel_node__body),
8488
importance = NotificationManager.IMPORTANCE_LOW
8589
)
86-
appViewModel.handleDeeplinkIntent(intent)
90+
91+
val consumedUri = savedInstanceState?.getString(KEY_CONSUMED_DEEPLINK_URI)
92+
val currentUri = intent?.data?.toString()
93+
if (currentUri == null || currentUri != consumedUri) {
94+
appViewModel.handleDeeplinkIntent(intent)
95+
}
8796

8897
installSplashScreen()
8998
enableAppEdgeToEdge()
@@ -201,6 +210,11 @@ class MainActivity : FragmentActivity() {
201210
appViewModel.handleDeeplinkIntent(intent)
202211
}
203212

213+
override fun onSaveInstanceState(outState: Bundle) {
214+
super.onSaveInstanceState(outState)
215+
intent?.data?.toString()?.let { outState.putString(KEY_CONSUMED_DEEPLINK_URI, it) }
216+
}
217+
204218
override fun onDestroy() {
205219
super.onDestroy()
206220
if (!settingsViewModel.notificationsGranted.value) {

app/src/main/java/to/bitkit/utils/Errors.kt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ sealed class ServiceError(message: String) : AppError(message) {
2121
class CurrencyRateUnavailable : ServiceError("Currency rate unavailable")
2222
class BlocktankInfoUnavailable : ServiceError("Blocktank info not available")
2323
class GeoBlocked : ServiceError("Geo blocked user")
24+
class GiftClaimPaymentNotReceived : ServiceError("Gift claim payment not received")
2425
}
2526

2627
class HttpError(message: String, val code: Int = 500, cause: Throwable? = null) : AppError(message, cause)

changelog.d/next/929.fixed.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Fix gift card flow showing false-positive confetti when the LSP payment fails, and re-opening unexpectedly after an app language change.

0 commit comments

Comments
 (0)