Skip to content
Open
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
85 changes: 85 additions & 0 deletions app/src/main/java/to/bitkit/models/ReceiveLiquidityDecision.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
package to.bitkit.models

enum class ReceiveLiquiditySource {
SAVINGS,
AUTO,
SPENDING,
}

sealed interface ReceiveAdditionalLiquidityAction {
data object None : ReceiveAdditionalLiquidityAction
data object ChooseAmount : ReceiveAdditionalLiquidityAction
data class CreateCjit(val amountSats: ULong) : ReceiveAdditionalLiquidityAction
data object GeoBlocked : ReceiveAdditionalLiquidityAction
}

data class ReceiveAdditionalLiquidityParams(
val source: ReceiveLiquiditySource,
val invoiceAmountSats: ULong,
val inboundCapacitySats: ULong?,
val minCjitSats: ULong?,
val maxCjitAmountSats: ULong?,
val isGeoBlocked: Boolean,
)

object ReceiveLiquidityDecision {
fun canCreateLightningInvoice(
hasReadyChannels: Boolean,
inboundCapacitySats: ULong?,
invoiceAmountSats: ULong?,
): Boolean {
if (!hasReadyChannels || inboundCapacitySats == null) return false

if (invoiceAmountSats == null || invoiceAmountSats == 0uL) {
return inboundCapacitySats > 0uL
}

return invoiceAmountSats <= inboundCapacitySats
}

fun additionalLiquidityAction(params: ReceiveAdditionalLiquidityParams): ReceiveAdditionalLiquidityAction {
return when {
params.source != ReceiveLiquiditySource.SPENDING -> ReceiveAdditionalLiquidityAction.None
!needsInboundLiquidity(params.invoiceAmountSats, params.inboundCapacitySats) ->
ReceiveAdditionalLiquidityAction.None
(params.inboundCapacitySats ?: 0uL) == 0uL -> ReceiveAdditionalLiquidityAction.None
params.isGeoBlocked -> ReceiveAdditionalLiquidityAction.GeoBlocked
shouldChooseAmount(params) -> ReceiveAdditionalLiquidityAction.ChooseAmount
else -> ReceiveAdditionalLiquidityAction.CreateCjit(params.invoiceAmountSats)
}
}

fun needsCjitLimitsForAdditionalLiquidity(
source: ReceiveLiquiditySource,
invoiceAmountSats: ULong,
inboundCapacitySats: ULong?,
isGeoBlocked: Boolean,
): Boolean {
if (source != ReceiveLiquiditySource.SPENDING) return false
if (!needsInboundLiquidity(invoiceAmountSats, inboundCapacitySats)) return false
if ((inboundCapacitySats ?: 0uL) == 0uL) return false

return !isGeoBlocked
}

fun needsInboundLiquidity(
invoiceAmountSats: ULong,
inboundCapacitySats: ULong?,
): Boolean {
val inbound = inboundCapacitySats ?: 0uL

if (invoiceAmountSats == 0uL) {
return inbound == 0uL
}

return invoiceAmountSats > inbound
}

private fun shouldChooseAmount(params: ReceiveAdditionalLiquidityParams): Boolean {
val min = params.minCjitSats ?: 0uL
val max = params.maxCjitAmountSats?.takeIf { it > 0uL } ?: return true
if (params.invoiceAmountSats == 0uL || min == 0uL) return true

return params.invoiceAmountSats < min || params.invoiceAmountSats > max
}
}
54 changes: 54 additions & 0 deletions app/src/main/java/to/bitkit/repositories/BlocktankRepo.kt
Original file line number Diff line number Diff line change
Expand Up @@ -252,7 +252,11 @@ class BlocktankRepo @Inject constructor(
runCatching {
if (coreService.isGeoBlocked()) throw ServiceError.GeoBlocked()
val nodeId = lightningService.nodeId ?: throw ServiceError.NodeNotStarted()
freshMaxChannelSizeSat()
val lspBalance = getDefaultLspBalance(clientBalance = amountSats)
if (!canFitChannelSize(amountSats, lspBalance)) {
throw ServiceError.ChannelSizeExceedsMaximum()
}
val channelSizeSat = amountSats + lspBalance

val cjitEntry = coreService.blocktank.createCjit(
Expand All @@ -272,6 +276,36 @@ class BlocktankRepo @Inject constructor(
}
}

suspend fun canCreateCjit(amountSats: ULong): Result<Boolean> = withContext(bgDispatcher) {
runCatching {
val maxChannelSizeSat = freshMaxChannelSizeSat() ?: return@runCatching true
return@runCatching canCreateCjit(amountSats, maxChannelSizeSat)
}.onFailure {
Logger.error("Failed to check CJIT limit", it, context = TAG)
}
}

suspend fun maxCjitAmountSats(): Result<ULong?> = withContext(bgDispatcher) {
runCatching {
val maxChannelSizeSat = freshMaxChannelSizeSat() ?: return@runCatching null
var lowerBound = 0uL
var upperBound = maxChannelSizeSat

while (lowerBound < upperBound) {
val candidate = lowerBound + (upperBound - lowerBound + 1uL) / 2uL
if (canCreateCjit(candidate, maxChannelSizeSat)) {
lowerBound = candidate
} else {
upperBound = candidate - 1uL
}
}

lowerBound
}.onFailure {
Logger.error("Failed to calculate max CJIT amount", it, context = TAG)
}
}

suspend fun createOrder(
spendingBalanceSats: ULong,
receivingBalanceSats: ULong = spendingBalanceSats * 2u,
Expand Down Expand Up @@ -409,6 +443,26 @@ class BlocktankRepo @Inject constructor(
return@withContext getDefaultLspBalance(params)
}

private suspend fun freshMaxChannelSizeSat(): ULong? {
refreshInfo().getOrThrow()

return _blocktankState.value.info?.options?.maxChannelSizeSat?.takeIf { it > 0uL }
}

private suspend fun canCreateCjit(amountSats: ULong, maxChannelSizeSat: ULong): Boolean {
if (amountSats > maxChannelSizeSat) return false

val lspBalance = getDefaultLspBalance(clientBalance = amountSats)
return amountSats <= maxChannelSizeSat && lspBalance <= maxChannelSizeSat - amountSats
}

private fun canFitChannelSize(amountSats: ULong, lspBalance: ULong): Boolean {
val maxChannelSizeSat = _blocktankState.value.info?.options?.maxChannelSizeSat?.takeIf { it > 0uL }
?: return true

return amountSats <= maxChannelSizeSat && lspBalance <= maxChannelSizeSat - amountSats
}

fun calculateLiquidityOptions(clientBalanceSat: ULong): Result<ChannelLiquidityOptions> {
val blocktankInfo = blocktankState.value.info
?: return Result.failure(ServiceError.BlocktankInfoUnavailable())
Expand Down
33 changes: 15 additions & 18 deletions app/src/main/java/to/bitkit/repositories/WalletRepo.kt
Original file line number Diff line number Diff line change
Expand Up @@ -28,16 +28,16 @@ import to.bitkit.data.SettingsStore
import to.bitkit.data.keychain.Keychain
import to.bitkit.di.BgDispatcher
import to.bitkit.env.Env
import to.bitkit.ext.filterOpen
import to.bitkit.ext.calculateRemoteBalance
import to.bitkit.ext.nowTimestamp
import to.bitkit.ext.runSuspendCatching
import to.bitkit.ext.toHex
import to.bitkit.models.ALL_ADDRESS_TYPE_STRINGS
import to.bitkit.models.AddressModel
import to.bitkit.models.BalanceState
import to.bitkit.models.DEFAULT_ADDRESS_TYPE_STRING
import to.bitkit.models.ReceiveLiquidityDecision
import to.bitkit.models.WalletScope
import to.bitkit.models.msatFloorOf
import to.bitkit.models.toAccountDerivationPath
import to.bitkit.models.toBalance
import to.bitkit.models.toDerivationPath
Expand Down Expand Up @@ -322,7 +322,7 @@ class WalletRepo @Inject constructor(
is Event.ChannelReady -> {
// Only refresh bolt11 if we can now receive on lightning
Logger.debug("refreshBip21ForEvent: $event", context = TAG)
if (lightningRepo.canReceive()) {
if (canCreateLightningInvoice(_walletState.value.bip21AmountSats)) {
lightningRepo.createInvoice(
amountSats = _walletState.value.bip21AmountSats,
description = _walletState.value.bip21Description,
Expand All @@ -336,7 +336,7 @@ class WalletRepo @Inject constructor(
is Event.ChannelClosed -> {
// Clear bolt11 if we can no longer receive on lightning
Logger.debug("refreshBip21ForEvent: $event", context = TAG)
if (!lightningRepo.canReceive()) {
if (!canCreateLightningInvoice(_walletState.value.bip21AmountSats)) {
setBolt11("")
updateBip21Url()
}
Expand Down Expand Up @@ -727,8 +727,7 @@ class WalletRepo @Inject constructor(
setBip21AmountSats(amountSats)
setBip21Description(description)

val canReceive = lightningRepo.canReceive()
if (canReceive) {
if (canCreateLightningInvoice(amountSats)) {
lightningRepo.createInvoice(amountSats, description).onSuccess {
setBolt11(it)
}
Expand All @@ -748,19 +747,17 @@ class WalletRepo @Inject constructor(
}
}

suspend fun shouldRequestAdditionalLiquidity(): Result<Boolean> = withContext(bgDispatcher) {
runCatching {
if (coreService.isGeoBlocked()) return@runCatching false

val channels = lightningRepo.lightningState.value.channels
if (channels.filterOpen().isEmpty()) return@runCatching false

val inboundBalanceSats = channels.sumOf { msatFloorOf(it.inboundCapacityMsat) }
fun inboundLiquiditySats(): ULong {
return lightningRepo.lightningState.value.channels.calculateRemoteBalance()
}

return@runCatching (_walletState.value.bip21AmountSats ?: 0uL) >= inboundBalanceSats
}.onFailure {
Logger.error("shouldRequestAdditionalLiquidity error", it, context = TAG)
}
private fun canCreateLightningInvoice(amountSats: ULong?): Boolean {
val channels = lightningRepo.lightningState.value.channels
return ReceiveLiquidityDecision.canCreateLightningInvoice(
hasReadyChannels = channels.any { it.isChannelReady },
inboundCapacitySats = channels.calculateRemoteBalance(),
invoiceAmountSats = amountSats,
)
}

private suspend fun Scanner.OnChain.extractLightningHash(): String? {
Expand Down
24 changes: 14 additions & 10 deletions app/src/main/java/to/bitkit/ui/ContentView.kt
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.Stable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.key
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
Expand Down Expand Up @@ -509,16 +510,19 @@ fun ContentView(
is Sheet.Receive -> {
val walletState by walletViewModel.walletState.collectAsStateWithLifecycle()
val connectivityState by appViewModel.isOnline.collectAsStateWithLifecycle()
ReceiveSheet(
appViewModel = appViewModel,
startRoute = sheet.route,
walletState = walletState,
isOffline = connectivityState != ConnectivityState.CONNECTED,
navigateToExternalConnection = {
navController.navigateTo(ExternalConnection())
appViewModel.hideSheet()
},
)

key(System.identityHashCode(sheet)) {
ReceiveSheet(
appViewModel = appViewModel,
startRoute = sheet.route,
walletState = walletState,
isOffline = connectivityState != ConnectivityState.CONNECTED,
navigateToExternalConnection = {
navController.navigateTo(ExternalConnection())
appViewModel.hideSheet()
},
)
}
}

Sheet.PaymentRequests -> PaymentRequestsSheet(
Expand Down
Loading
Loading