Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
63 changes: 56 additions & 7 deletions app/src/main/java/to/bitkit/repositories/BlocktankRepo.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -458,26 +466,61 @@ 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<Event.PaymentReceived>()
.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,
)

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)
}

return GiftClaimResult.SuccessWithLiquidity(
paymentHashOrTxId = giftResponse.bolt11PaymentId ?: giftResponse.id,
sats = giftResponse.bolt11Payment?.paidSat?.toLong()
?: giftResponse.appliedGiftCode?.giftSat?.toLong()
?: amount.toLong(),
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()

GiftClaimResult.SuccessWithLiquidity(
paymentHashOrTxId = paymentReceived.paymentHash,
sats = receivedSats.takeIf { it > 0 } ?: amount.toLong(),
invoice = invoice,
code = code,
)
Expand Down Expand Up @@ -517,6 +560,12 @@ 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

// 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
}
}

Expand Down
6 changes: 5 additions & 1 deletion app/src/main/java/to/bitkit/ui/MainActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
1 change: 1 addition & 0 deletions app/src/main/java/to/bitkit/utils/Errors.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
1 change: 1 addition & 0 deletions changelog.d/next/929.fixed.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Fix gift card flow showing false-positive confetti when the LSP payment fails, and re-opening unexpectedly after an app language change.
Loading