Skip to content

Commit 1273542

Browse files
authored
Merge pull request #689 from synonymdev/claude/issue-680-20260120-1548
fix: improve gift code flow
2 parents 3b33fb2 + 17546eb commit 1273542

7 files changed

Lines changed: 108 additions & 54 deletions

File tree

AGENTS.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -178,6 +178,7 @@ suspend fun getData(): Result<Data> = withContext(Dispatchers.IO) {
178178
- NEVER use `runBlocking` in suspend functions
179179
- ALWAYS pass the TAG as context to `Logger` calls, e.g. `Logger.debug("message", context = TAG)`
180180
- NEVER add `e = ` named parameter to Logger calls
181+
- NEVER manually append the `Throwable`'s message or any other props to the string passed as the 1st param of `Logger.*` calls, its internals are already enriching the final log message with the details of the `Throwable` passed via the `e` arg
181182
- ALWAYS log errors at the final handling layer where the error is acted upon, not in intermediate layers that just propagate it
182183
- ALWAYS use the Result API instead of try-catch
183184
- NEVER wrap methods returning `Result<T>` in try-catch
@@ -221,4 +222,4 @@ suspend fun getData(): Result<Data> = withContext(Dispatchers.IO) {
221222
- Use `LightningRepo` to defining the business logic for the node operations, usually delegating to `LightningService`
222223
- Use `WakeNodeWorker` to manage the handling of remote notifications received via cloud messages
223224
- Use `*Services` to wrap rust library code exposed via bindings
224-
- Use CQRS pattern of Command + Handler like it's done in the `NotifyPaymentReceived` + `NotifyPaymentReceivedHandler` setup
225+
- Use CQRS pattern of Command + Handler like it's done in the `NotifyPaymentReceived` + `NotifyPaymentReceivedHandler` setup

app/build.gradle.kts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -54,8 +54,8 @@ android {
5454
applicationId = "to.bitkit"
5555
minSdk = 28
5656
targetSdk = 36
57-
versionCode = 171
58-
versionName = "2.0.0-rc.5"
57+
versionCode = 172
58+
versionName = "2.0.0-rc.6"
5959
testInstrumentationRunner = "to.bitkit.test.HiltTestRunner"
6060
vectorDrawables {
6161
useSupportLibrary = true

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

Lines changed: 51 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -427,6 +427,8 @@ class BlocktankRepo @Inject constructor(
427427
require(code.isNotBlank()) { "Gift code cannot be blank" }
428428
require(amount > 0u) { "Gift amount must be positive" }
429429

430+
Logger.debug("Starting gift code claim: amount=$amount, timeout=$waitTimeout", context = TAG)
431+
430432
lightningRepo.executeWhenNodeRunning(
431433
operationName = "claimGiftCode",
432434
waitTimeout = waitTimeout,
@@ -436,9 +438,16 @@ class BlocktankRepo @Inject constructor(
436438
val channels = lightningRepo.getChannelsAsync().getOrThrow()
437439
val maxInboundCapacity = channels.calculateRemoteBalance()
438440

441+
Logger.debug(
442+
"Liquidity check: maxInbound=$maxInboundCapacity, required=$amount",
443+
context = TAG
444+
)
445+
439446
if (maxInboundCapacity >= amount) {
440-
Result.success(claimGiftCodeWithLiquidity(code))
447+
Logger.debug("Sufficient liquidity available, claiming with existing channel", context = TAG)
448+
Result.success(claimGiftCodeWithLiquidity(code, amount))
441449
} else {
450+
Logger.debug("Insufficient liquidity, opening new channel", context = TAG)
442451
Result.success(claimGiftCodeWithoutLiquidity(code, amount))
443452
}
444453
}.getOrThrow()
@@ -447,33 +456,53 @@ class BlocktankRepo @Inject constructor(
447456
}
448457
}
449458

450-
private suspend fun claimGiftCodeWithLiquidity(code: String): GiftClaimResult {
459+
private suspend fun claimGiftCodeWithLiquidity(code: String, amount: ULong): GiftClaimResult {
451460
val invoice = lightningRepo.createInvoice(
452461
amountSats = null,
453462
description = "blocktank-gift-code:$code",
454463
expirySeconds = 3600u,
455464
).getOrThrow()
456465

457-
ServiceQueue.CORE.background {
466+
Logger.debug("Created invoice for gift code, requesting payment from LSP", context = TAG)
467+
468+
val giftResponse = ServiceQueue.CORE.background {
458469
giftPay(invoice = invoice)
459470
}
460471

461-
return GiftClaimResult.SuccessWithLiquidity
472+
Logger.debug("Gift payment request completed: id=${giftResponse.id}", context = TAG)
473+
474+
return GiftClaimResult.SuccessWithLiquidity(
475+
paymentHashOrTxId = giftResponse.bolt11PaymentId ?: giftResponse.id,
476+
sats = giftResponse.bolt11Payment?.paidSat?.toLong()
477+
?: giftResponse.appliedGiftCode?.giftSat?.toLong()
478+
?: amount.toLong(),
479+
invoice = invoice,
480+
code = code,
481+
)
462482
}
463483

464484
private suspend fun claimGiftCodeWithoutLiquidity(code: String, amount: ULong): GiftClaimResult {
465485
val nodeId = lightningService.nodeId ?: throw ServiceError.NodeNotStarted()
466486

487+
Logger.debug("Creating gift order for code (insufficient liquidity)", context = TAG)
488+
467489
val order = ServiceQueue.CORE.background {
468490
giftOrder(clientNodeId = nodeId, code = "blocktank-gift-code:$code")
469491
}
470492

471-
val orderId = checkNotNull(order.orderId) { "Order ID is null" }
493+
val orderId = checkNotNull(order.orderId) { "Order ID is null after gift order creation" }
494+
Logger.debug("Gift order created: $orderId", context = TAG)
472495

473496
val openedOrder = openChannel(orderId).getOrThrow()
497+
Logger.debug("Channel opened for gift order: ${openedOrder.id}", context = TAG)
498+
499+
val fundingTxId = openedOrder.channel?.fundingTx?.id
500+
if (fundingTxId == null) {
501+
Logger.warn("Channel opened but funding transaction ID is null", context = TAG)
502+
}
474503

475504
return GiftClaimResult.SuccessWithoutLiquidity(
476-
paymentHashOrTxId = openedOrder.channel?.fundingTx?.id ?: orderId,
505+
paymentHashOrTxId = fundingTxId ?: orderId,
477506
sats = amount.toLong(),
478507
invoice = openedOrder.payment?.bolt11Invoice?.request ?: "",
479508
code = code,
@@ -498,11 +527,22 @@ data class BlocktankState(
498527
)
499528

500529
sealed class GiftClaimResult {
501-
object SuccessWithLiquidity : GiftClaimResult()
530+
abstract val paymentHashOrTxId: String
531+
abstract val sats: Long
532+
abstract val invoice: String
533+
abstract val code: String
534+
535+
data class SuccessWithLiquidity(
536+
override val paymentHashOrTxId: String,
537+
override val sats: Long,
538+
override val invoice: String,
539+
override val code: String,
540+
) : GiftClaimResult()
541+
502542
data class SuccessWithoutLiquidity(
503-
val paymentHashOrTxId: String,
504-
val sats: Long,
505-
val invoice: String,
506-
val code: String,
543+
override val paymentHashOrTxId: String,
544+
override val sats: Long,
545+
override val invoice: String,
546+
override val code: String,
507547
) : GiftClaimResult()
508548
}

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

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -34,12 +34,8 @@ import to.bitkit.ui.shared.util.gradientBackground
3434
import to.bitkit.ui.theme.Colors
3535

3636
@Composable
37-
fun GiftLoading(
38-
viewModel: GiftViewModel,
39-
) {
40-
Content(
41-
amount = viewModel.amount,
42-
)
37+
fun GiftLoading(amount: ULong) {
38+
Content(amount = amount)
4339
}
4440

4541
@Composable

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

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -65,9 +65,7 @@ fun GiftSheet(
6565
startDestination = GiftRoute.Loading,
6666
) {
6767
composableWithDefaultTransitions<GiftRoute.Loading> {
68-
GiftLoading(
69-
viewModel = viewModel,
70-
)
68+
GiftLoading(amount = sheet.amount)
7169
}
7270
composableWithDefaultTransitions<GiftRoute.Used> {
7371
GiftErrorSheet(

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

Lines changed: 42 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -59,66 +59,78 @@ class GiftViewModel @Inject constructor(
5959
}
6060

6161
private suspend fun claimGift() = withContext(bgDispatcher) {
62-
if (isClaiming) return@withContext
62+
if (isClaiming) {
63+
Logger.debug("Gift claim already in progress, skipping", context = TAG)
64+
return@withContext
65+
}
6366
isClaiming = true
6467

6568
try {
69+
Logger.debug("Claiming gift: code=$code, amount=$amount", context = TAG)
6670
blocktankRepo.claimGiftCode(
6771
code = code,
6872
amount = amount,
6973
waitTimeout = NODE_STARTUP_TIMEOUT_MS.milliseconds,
7074
).fold(
71-
onSuccess = { result ->
72-
when (result) {
73-
is GiftClaimResult.SuccessWithLiquidity -> {
74-
_navigationEvent.emit(GiftRoute.Success)
75-
}
76-
is GiftClaimResult.SuccessWithoutLiquidity -> {
77-
insertGiftActivity(result)
78-
_successEvent.emit(
79-
NewTransactionSheetDetails(
80-
type = NewTransactionSheetType.LIGHTNING,
81-
direction = NewTransactionSheetDirection.RECEIVED,
82-
paymentHashOrTxId = result.paymentHashOrTxId,
83-
sats = result.sats,
84-
)
85-
)
86-
_navigationEvent.emit(GiftRoute.Success)
87-
}
75+
onSuccess = {
76+
Logger.debug("Gift claim successful: $it", context = TAG)
77+
if (it is GiftClaimResult.SuccessWithoutLiquidity) {
78+
insertGiftActivity(it.paymentHashOrTxId, it.sats, it.invoice, it.code)
8879
}
80+
_successEvent.emit(
81+
NewTransactionSheetDetails(
82+
type = NewTransactionSheetType.LIGHTNING,
83+
direction = NewTransactionSheetDirection.RECEIVED,
84+
paymentHashOrTxId = it.paymentHashOrTxId,
85+
sats = it.sats,
86+
)
87+
)
88+
_navigationEvent.emit(GiftRoute.Success)
8989
},
90-
onFailure = { error ->
91-
handleGiftClaimError(error)
92-
}
90+
onFailure = { handleGiftClaimError(it) },
9391
)
9492
} finally {
9593
isClaiming = false
9694
}
9795
}
9896

99-
private suspend fun insertGiftActivity(result: GiftClaimResult.SuccessWithoutLiquidity) {
97+
private suspend fun insertGiftActivity(
98+
paymentHashOrTxId: String,
99+
sats: Long,
100+
invoice: String,
101+
code: String,
102+
) {
100103
val nowTimestamp = nowTimestamp().epochSecond.toULong()
101104

102105
val lightningActivity = LightningActivity.create(
103-
id = result.paymentHashOrTxId,
106+
id = paymentHashOrTxId,
104107
txType = PaymentType.RECEIVED,
105108
status = PaymentState.SUCCEEDED,
106-
value = result.sats.toULong(),
107-
invoice = result.invoice,
109+
value = sats.toULong(),
110+
invoice = invoice,
108111
timestamp = nowTimestamp,
109-
message = result.code,
112+
message = code,
110113
)
111114

112115
activityRepo.insertActivity(Activity.Lightning(lightningActivity)).getOrThrow()
113116
}
114117

115118
private suspend fun handleGiftClaimError(error: Throwable) {
116-
Logger.error("Gift claim failed: $error", error, context = TAG)
119+
Logger.error("Gift claim failed", error, context = TAG)
117120

118121
val route = when {
119-
errorContains(error, "GIFT_CODE_ALREADY_USED") -> GiftRoute.Used
120-
errorContains(error, "GIFT_CODE_USED_UP") -> GiftRoute.UsedUp
121-
else -> GiftRoute.Error
122+
errorContains(error, "GIFT_CODE_ALREADY_USED") -> {
123+
Logger.info("Gift code was already used", context = TAG)
124+
GiftRoute.Used
125+
}
126+
errorContains(error, "GIFT_CODE_USED_UP") -> {
127+
Logger.info("Gift code promotion depleted", context = TAG)
128+
GiftRoute.UsedUp
129+
}
130+
else -> {
131+
Logger.error("Unhandled gift claim error type", error, context = TAG)
132+
GiftRoute.Error
133+
}
122134
}
123135

124136
_navigationEvent.emit(route)

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

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -255,7 +255,14 @@ class AppViewModel @Inject constructor(
255255
viewModelScope.launch {
256256
timedSheetManager.currentSheet.collect { sheetType ->
257257
if (sheetType != null) {
258-
showSheet(Sheet.TimedSheet(sheetType))
258+
val currentSheet = _currentSheet.value
259+
val isHighPrioritySheetShowing = currentSheet is Sheet.Gift ||
260+
currentSheet is Sheet.Send ||
261+
currentSheet is Sheet.LnurlAuth ||
262+
currentSheet is Sheet.Pin
263+
if (!isHighPrioritySheetShowing) {
264+
showSheet(Sheet.TimedSheet(sheetType))
265+
}
259266
} else {
260267
// Clear the timed sheet when manager sets it to null
261268
_currentSheet.update { current ->

0 commit comments

Comments
 (0)