Skip to content

Commit 702410b

Browse files
github-actions[bot]ovitrif
authored andcommitted
fix: improve gift code redemption flow
- Add comprehensive logging for gift claim operations - Fix amount=0 handling to proceed with backend validation - Improve error messages with full error chain context - Add operation guards to prevent duplicate claims - Validate channel funding transaction exists - Better error classification (Used/UsedUp/Error) Co-authored-by: Ovi Trif <ovitrif@users.noreply.github.com>
1 parent 894867e commit 702410b

2 files changed

Lines changed: 69 additions & 11 deletions

File tree

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

Lines changed: 38 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -425,7 +425,15 @@ class BlocktankRepo @Inject constructor(
425425
): Result<GiftClaimResult> = withContext(bgDispatcher) {
426426
runCatching {
427427
require(code.isNotBlank()) { "Gift code cannot be blank" }
428-
require(amount > 0u) { "Gift amount must be positive" }
428+
429+
if (amount == 0uL) {
430+
Logger.warn(
431+
"Gift amount is 0 - proceeding anyway as backend may provide actual amount",
432+
context = TAG
433+
)
434+
}
435+
436+
Logger.debug("Starting gift code claim: amount=$amount, timeout=$waitTimeout", context = TAG)
429437

430438
lightningRepo.executeWhenNodeRunning(
431439
operationName = "claimGiftCode",
@@ -436,14 +444,25 @@ class BlocktankRepo @Inject constructor(
436444
val channels = lightningRepo.getChannelsAsync().getOrThrow()
437445
val maxInboundCapacity = channels.calculateRemoteBalance()
438446

439-
if (maxInboundCapacity >= amount) {
447+
Logger.debug(
448+
"Liquidity check: maxInbound=$maxInboundCapacity, required=$amount",
449+
context = TAG
450+
)
451+
452+
if (amount > 0uL && maxInboundCapacity >= amount) {
453+
Logger.debug("Sufficient liquidity available, claiming with existing channel", context = TAG)
440454
Result.success(claimGiftCodeWithLiquidity(code))
441455
} else {
456+
if (amount == 0uL) {
457+
Logger.debug("Amount unknown (0), defaulting to channel opening path", context = TAG)
458+
} else {
459+
Logger.debug("Insufficient liquidity, opening new channel", context = TAG)
460+
}
442461
Result.success(claimGiftCodeWithoutLiquidity(code, amount))
443462
}
444463
}.getOrThrow()
445464
}.onFailure {
446-
Logger.error("Failed to claim gift code", it, context = TAG)
465+
Logger.error("Failed to claim gift code: ${it.message}", it, context = TAG)
447466
}
448467
}
449468

@@ -454,26 +473,39 @@ class BlocktankRepo @Inject constructor(
454473
expirySeconds = 3600u,
455474
).getOrThrow()
456475

457-
ServiceQueue.CORE.background {
476+
Logger.debug("Created invoice for gift code, requesting payment from LSP", context = TAG)
477+
478+
val result = ServiceQueue.CORE.background {
458479
giftPay(invoice = invoice)
459480
}
460481

482+
Logger.debug("Gift payment request completed: $result", context = TAG)
483+
461484
return GiftClaimResult.SuccessWithLiquidity
462485
}
463486

464487
private suspend fun claimGiftCodeWithoutLiquidity(code: String, amount: ULong): GiftClaimResult {
465488
val nodeId = lightningService.nodeId ?: throw ServiceError.NodeNotStarted()
466489

490+
Logger.debug("Creating gift order for code (insufficient liquidity)", context = TAG)
491+
467492
val order = ServiceQueue.CORE.background {
468493
giftOrder(clientNodeId = nodeId, code = "blocktank-gift-code:$code")
469494
}
470495

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

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

475507
return GiftClaimResult.SuccessWithoutLiquidity(
476-
paymentHashOrTxId = openedOrder.channel?.fundingTx?.id ?: orderId,
508+
paymentHashOrTxId = fundingTxId ?: orderId,
477509
sats = amount.toLong(),
478510
invoice = openedOrder.payment?.bolt11Invoice?.request ?: "",
479511
code = code,

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

Lines changed: 31 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -53,22 +53,32 @@ class GiftViewModel @Inject constructor(
5353
}
5454
this.code = code
5555
this.amount = amount
56+
57+
if (amount == 0uL) {
58+
Logger.warn("Gift amount is 0 from QR code - this may be incorrect", context = TAG)
59+
}
60+
5661
viewModelScope.launch(bgDispatcher) {
5762
claimGift()
5863
}
5964
}
6065

6166
private suspend fun claimGift() = withContext(bgDispatcher) {
62-
if (isClaiming) return@withContext
67+
if (isClaiming) {
68+
Logger.debug("Gift claim already in progress, skipping", context = TAG)
69+
return@withContext
70+
}
6371
isClaiming = true
6472

6573
try {
74+
Logger.debug("Claiming gift: code=$code, amount=$amount", context = TAG)
6675
blocktankRepo.claimGiftCode(
6776
code = code,
6877
amount = amount,
6978
waitTimeout = NODE_STARTUP_TIMEOUT_MS.milliseconds,
7079
).fold(
7180
onSuccess = { result ->
81+
Logger.debug("Gift claim successful: $result", context = TAG)
7282
when (result) {
7383
is GiftClaimResult.SuccessWithLiquidity -> {
7484
_navigationEvent.emit(GiftRoute.Success)
@@ -113,12 +123,28 @@ class GiftViewModel @Inject constructor(
113123
}
114124

115125
private suspend fun handleGiftClaimError(error: Throwable) {
116-
Logger.error("Gift claim failed: $error", error, context = TAG)
126+
val errorMessage = buildString {
127+
append("Gift claim failed: ")
128+
append(error.message ?: error.toString())
129+
error.cause?.let {
130+
append(" (cause: ${it.message ?: it})")
131+
}
132+
}
133+
Logger.error(errorMessage, error, context = TAG)
117134

118135
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
136+
errorContains(error, "GIFT_CODE_ALREADY_USED") -> {
137+
Logger.info("Gift code was already used", context = TAG)
138+
GiftRoute.Used
139+
}
140+
errorContains(error, "GIFT_CODE_USED_UP") -> {
141+
Logger.info("Gift code promotion depleted", context = TAG)
142+
GiftRoute.UsedUp
143+
}
144+
else -> {
145+
Logger.error("Unhandled gift claim error type: ${error::class.simpleName}", context = TAG)
146+
GiftRoute.Error
147+
}
122148
}
123149

124150
_navigationEvent.emit(route)

0 commit comments

Comments
 (0)