diff --git a/app/src/main/java/to/bitkit/repositories/BlocktankRepo.kt b/app/src/main/java/to/bitkit/repositories/BlocktankRepo.kt index 6cc9804a2..22e907d61 100644 --- a/app/src/main/java/to/bitkit/repositories/BlocktankRepo.kt +++ b/app/src/main/java/to/bitkit/repositories/BlocktankRepo.kt @@ -425,7 +425,15 @@ class BlocktankRepo @Inject constructor( ): Result = withContext(bgDispatcher) { runCatching { require(code.isNotBlank()) { "Gift code cannot be blank" } - require(amount > 0u) { "Gift amount must be positive" } + + if (amount == 0uL) { + Logger.warn( + "Gift amount is 0 - proceeding anyway as backend may provide actual amount", + context = TAG + ) + } + + Logger.debug("Starting gift code claim: amount=$amount, timeout=$waitTimeout", context = TAG) lightningRepo.executeWhenNodeRunning( operationName = "claimGiftCode", @@ -436,14 +444,25 @@ class BlocktankRepo @Inject constructor( val channels = lightningRepo.getChannelsAsync().getOrThrow() val maxInboundCapacity = channels.calculateRemoteBalance() - if (maxInboundCapacity >= amount) { + Logger.debug( + "Liquidity check: maxInbound=$maxInboundCapacity, required=$amount", + context = TAG + ) + + if (amount > 0uL && maxInboundCapacity >= amount) { + Logger.debug("Sufficient liquidity available, claiming with existing channel", context = TAG) Result.success(claimGiftCodeWithLiquidity(code)) } else { + if (amount == 0uL) { + Logger.debug("Amount unknown (0), defaulting to channel opening path", context = TAG) + } else { + Logger.debug("Insufficient liquidity, opening new channel", context = TAG) + } Result.success(claimGiftCodeWithoutLiquidity(code, amount)) } }.getOrThrow() }.onFailure { - Logger.error("Failed to claim gift code", it, context = TAG) + Logger.error("Failed to claim gift code: ${it.message}", it, context = TAG) } } @@ -454,26 +473,39 @@ class BlocktankRepo @Inject constructor( expirySeconds = 3600u, ).getOrThrow() - ServiceQueue.CORE.background { + Logger.debug("Created invoice for gift code, requesting payment from LSP", context = TAG) + + val result = ServiceQueue.CORE.background { giftPay(invoice = invoice) } + Logger.debug("Gift payment request completed: $result", context = TAG) + return GiftClaimResult.SuccessWithLiquidity } private suspend fun claimGiftCodeWithoutLiquidity(code: String, amount: ULong): GiftClaimResult { val nodeId = lightningService.nodeId ?: throw ServiceError.NodeNotStarted() + Logger.debug("Creating gift order for code (insufficient liquidity)", context = TAG) + val order = ServiceQueue.CORE.background { giftOrder(clientNodeId = nodeId, code = "blocktank-gift-code:$code") } - val orderId = checkNotNull(order.orderId) { "Order ID is null" } + val orderId = checkNotNull(order.orderId) { "Order ID is null after gift order creation" } + Logger.debug("Gift order created: $orderId", context = TAG) val openedOrder = openChannel(orderId).getOrThrow() + Logger.debug("Channel opened for gift order: ${openedOrder.id}", context = TAG) + + val fundingTxId = openedOrder.channel?.fundingTx?.id + if (fundingTxId == null) { + Logger.warn("Channel opened but funding transaction ID is null", context = TAG) + } return GiftClaimResult.SuccessWithoutLiquidity( - paymentHashOrTxId = openedOrder.channel?.fundingTx?.id ?: orderId, + paymentHashOrTxId = fundingTxId ?: orderId, sats = amount.toLong(), invoice = openedOrder.payment?.bolt11Invoice?.request ?: "", code = code, diff --git a/app/src/main/java/to/bitkit/ui/sheets/GiftViewModel.kt b/app/src/main/java/to/bitkit/ui/sheets/GiftViewModel.kt index d7a1bc8e4..f62d07020 100644 --- a/app/src/main/java/to/bitkit/ui/sheets/GiftViewModel.kt +++ b/app/src/main/java/to/bitkit/ui/sheets/GiftViewModel.kt @@ -53,22 +53,32 @@ class GiftViewModel @Inject constructor( } this.code = code this.amount = amount + + if (amount == 0uL) { + Logger.warn("Gift amount is 0 from QR code - this may be incorrect", context = TAG) + } + viewModelScope.launch(bgDispatcher) { claimGift() } } private suspend fun claimGift() = withContext(bgDispatcher) { - if (isClaiming) return@withContext + if (isClaiming) { + Logger.debug("Gift claim already in progress, skipping", context = TAG) + return@withContext + } isClaiming = true try { + Logger.debug("Claiming gift: code=$code, amount=$amount", context = TAG) blocktankRepo.claimGiftCode( code = code, amount = amount, waitTimeout = NODE_STARTUP_TIMEOUT_MS.milliseconds, ).fold( onSuccess = { result -> + Logger.debug("Gift claim successful: $result", context = TAG) when (result) { is GiftClaimResult.SuccessWithLiquidity -> { _navigationEvent.emit(GiftRoute.Success) @@ -113,12 +123,28 @@ class GiftViewModel @Inject constructor( } private suspend fun handleGiftClaimError(error: Throwable) { - Logger.error("Gift claim failed: $error", error, context = TAG) + val errorMessage = buildString { + append("Gift claim failed: ") + append(error.message ?: error.toString()) + error.cause?.let { + append(" (cause: ${it.message ?: it})") + } + } + Logger.error(errorMessage, error, context = TAG) val route = when { - errorContains(error, "GIFT_CODE_ALREADY_USED") -> GiftRoute.Used - errorContains(error, "GIFT_CODE_USED_UP") -> GiftRoute.UsedUp - else -> GiftRoute.Error + errorContains(error, "GIFT_CODE_ALREADY_USED") -> { + Logger.info("Gift code was already used", context = TAG) + GiftRoute.Used + } + errorContains(error, "GIFT_CODE_USED_UP") -> { + Logger.info("Gift code promotion depleted", context = TAG) + GiftRoute.UsedUp + } + else -> { + Logger.error("Unhandled gift claim error type: ${error::class.simpleName}", context = TAG) + GiftRoute.Error + } } _navigationEvent.emit(route)