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
44 changes: 38 additions & 6 deletions app/src/main/java/to/bitkit/repositories/BlocktankRepo.kt
Original file line number Diff line number Diff line change
Expand Up @@ -425,7 +425,15 @@ class BlocktankRepo @Inject constructor(
): Result<GiftClaimResult> = 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",
Expand All @@ -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)
}
}

Expand All @@ -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,
Expand Down
36 changes: 31 additions & 5 deletions app/src/main/java/to/bitkit/ui/sheets/GiftViewModel.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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)
Expand Down
Loading