Skip to content

Commit 0994f9c

Browse files
committed
refactor: use bitkit-core for lsp liquidity calculations
1 parent 3cc5501 commit 0994f9c

File tree

4 files changed

+59
-148
lines changed

4 files changed

+59
-148
lines changed

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

Lines changed: 47 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,17 @@
11
package to.bitkit.repositories
22

33
import com.synonym.bitkitcore.BtOrderState2
4+
import com.synonym.bitkitcore.ChannelLiquidityOptions
5+
import com.synonym.bitkitcore.ChannelLiquidityParams
46
import com.synonym.bitkitcore.CreateCjitOptions
57
import com.synonym.bitkitcore.CreateOrderOptions
8+
import com.synonym.bitkitcore.DefaultLspBalanceParams
69
import com.synonym.bitkitcore.IBtEstimateFeeResponse2
710
import com.synonym.bitkitcore.IBtInfo
811
import com.synonym.bitkitcore.IBtOrder
912
import com.synonym.bitkitcore.IcJitEntry
13+
import com.synonym.bitkitcore.calculateChannelLiquidityOptions
14+
import com.synonym.bitkitcore.getDefaultLspBalance
1015
import com.synonym.bitkitcore.giftOrder
1116
import com.synonym.bitkitcore.giftPay
1217
import kotlinx.coroutines.CoroutineDispatcher
@@ -46,7 +51,6 @@ import javax.inject.Inject
4651
import javax.inject.Named
4752
import javax.inject.Singleton
4853
import kotlin.math.ceil
49-
import kotlin.math.min
5054
import kotlin.time.Duration
5155
import kotlin.time.Duration.Companion.seconds
5256

@@ -345,38 +349,55 @@ class BlocktankRepo @Inject constructor(
345349
refreshInfo()
346350
}
347351

348-
val maxLspBalance = _blocktankState.value.info?.options?.maxChannelSizeSat ?: 0uL
352+
val satsPerEur = getSatsPerEur()
353+
?: throw ServiceError.CurrencyRateUnavailable
349354

350-
// Calculate thresholds in sats
351-
val threshold1 = currencyRepo.convertFiatToSats(BigDecimal(225), EUR_CURRENCY).getOrNull()
352-
val threshold2 = currencyRepo.convertFiatToSats(BigDecimal(495), EUR_CURRENCY).getOrNull()
353-
val defaultLspBalanceSats = currencyRepo.convertFiatToSats(BigDecimal(450), EUR_CURRENCY).getOrNull()
354-
355-
Logger.debug("getDefaultLspBalance - clientBalance: $clientBalance", context = TAG)
356-
Logger.debug("getDefaultLspBalance - maxLspBalance: $maxLspBalance", context = TAG)
357-
Logger.debug(
358-
"getDefaultLspBalance - defaultLspBalance: $defaultLspBalanceSats",
359-
context = TAG
355+
val params = DefaultLspBalanceParams(
356+
clientBalanceSat = clientBalance,
357+
maxChannelSizeSat = _blocktankState.value.info?.options?.maxChannelSizeSat ?: 0uL,
358+
satsPerEur = satsPerEur
360359
)
361360

362-
if (threshold1 == null || threshold2 == null || defaultLspBalanceSats == null) {
363-
Logger.error("Failed to get rates for lspBalance calculation", context = TAG)
364-
throw ServiceError.CurrencyRateUnavailable
365-
}
361+
return@withContext getDefaultLspBalance(params)
362+
}
366363

367-
// Safely calculate lspBalance to avoid arithmetic overflow
368-
var lspBalance: ULong = 0u
369-
if (defaultLspBalanceSats > clientBalance) {
370-
lspBalance = defaultLspBalanceSats - clientBalance
371-
}
372-
if (clientBalance > threshold1) {
373-
lspBalance = clientBalance
364+
fun calculateLiquidityOptions(clientBalanceSat: ULong): ChannelLiquidityOptions? {
365+
val blocktankInfo = blocktankState.value.info
366+
if (blocktankInfo == null) {
367+
Logger.warn("calculateLiquidityOptions: blocktankInfo is null", context = TAG)
368+
return null
374369
}
375-
if (clientBalance > threshold2) {
376-
lspBalance = maxLspBalance
370+
371+
val satsPerEur = getSatsPerEur()
372+
if (satsPerEur == null) {
373+
Logger.warn("calculateLiquidityOptions: satsPerEur is null", context = TAG)
374+
return null
377375
}
378376

379-
return@withContext min(lspBalance, maxLspBalance)
377+
val existingChannelsTotalSat = totalBtChannelsValueSats(blocktankInfo)
378+
379+
val params = ChannelLiquidityParams(
380+
clientBalanceSat = clientBalanceSat,
381+
existingChannelsTotalSat = existingChannelsTotalSat,
382+
minChannelSizeSat = blocktankInfo.options.minChannelSizeSat,
383+
maxChannelSizeSat = blocktankInfo.options.maxChannelSizeSat,
384+
satsPerEur = satsPerEur
385+
)
386+
387+
return calculateChannelLiquidityOptions(params)
388+
}
389+
390+
private fun getSatsPerEur(): ULong? {
391+
return currencyRepo.convertFiatToSats(BigDecimal(1), EUR_CURRENCY).getOrNull()
392+
}
393+
394+
private fun totalBtChannelsValueSats(info: IBtInfo?): ULong {
395+
val channels = lightningRepo.getChannels() ?: return 0u
396+
val btNodeIds = info?.nodes?.map { it.pubkey } ?: return 0u
397+
398+
val btChannels = channels.filter { btNodeIds.contains(it.counterpartyNodeId) }
399+
400+
return btChannels.sumOf { it.channelValueSats }
380401
}
381402

382403
suspend fun resetState() = withContext(bgDispatcher) {

app/src/main/java/to/bitkit/ui/screens/transfer/SpendingAdvancedScreen.kt

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@ import androidx.compose.foundation.layout.requiredHeight
1010
import androidx.compose.material3.HorizontalDivider
1111
import androidx.compose.runtime.Composable
1212
import androidx.compose.runtime.LaunchedEffect
13-
import androidx.compose.runtime.collectAsState
1413
import androidx.compose.runtime.getValue
1514
import androidx.compose.runtime.mutableStateOf
1615
import androidx.compose.runtime.remember
@@ -67,12 +66,12 @@ fun SpendingAdvancedScreen(
6766
val app = appViewModel ?: return
6867
val state by viewModel.spendingUiState.collectAsStateWithLifecycle()
6968
val order = state.order ?: return
70-
val transferValues by viewModel.transferValues.collectAsState()
7169
val amountUiState by amountInputViewModel.uiState.collectAsStateWithLifecycle()
7270
var isLoading by remember { mutableStateOf(false) }
7371

74-
LaunchedEffect(order.clientBalanceSat) {
72+
val transferValues by remember(order.clientBalanceSat) {
7573
viewModel.updateTransferValues(order.clientBalanceSat)
74+
mutableStateOf(viewModel.transferValues.value)
7675
}
7776

7877
LaunchedEffect(amountUiState.sats) {
@@ -102,7 +101,7 @@ fun SpendingAdvancedScreen(
102101

103102
val isValid = transferValues.let {
104103
val amount = amountUiState.sats.toULong()
105-
amount > 0u && amount in it.minLspBalance..it.maxLspBalance
104+
amount > 0u && it.maxLspBalance > 0u && amount in it.minLspBalance..it.maxLspBalance
106105
}
107106

108107
Content(

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

Lines changed: 8 additions & 117 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@ import android.content.Context
44
import androidx.lifecycle.ViewModel
55
import androidx.lifecycle.viewModelScope
66
import com.synonym.bitkitcore.BtOrderState2
7-
import com.synonym.bitkitcore.IBtInfo
87
import com.synonym.bitkitcore.IBtOrder
98
import dagger.hilt.android.lifecycle.HiltViewModel
109
import dagger.hilt.android.qualifiers.ApplicationContext
@@ -32,20 +31,16 @@ import to.bitkit.data.CacheStore
3231
import to.bitkit.data.SettingsStore
3332
import to.bitkit.env.Env
3433
import to.bitkit.ext.amountOnClose
35-
import to.bitkit.models.EUR_CURRENCY
3634
import to.bitkit.models.Toast
3735
import to.bitkit.models.TransactionSpeed
3836
import to.bitkit.models.TransferType
3937
import to.bitkit.models.safe
4038
import to.bitkit.repositories.BlocktankRepo
41-
import to.bitkit.repositories.CurrencyRepo
4239
import to.bitkit.repositories.LightningRepo
4340
import to.bitkit.repositories.TransferRepo
4441
import to.bitkit.repositories.WalletRepo
4542
import to.bitkit.ui.shared.toast.ToastEventBus
4643
import to.bitkit.utils.Logger
47-
import to.bitkit.utils.ServiceError
48-
import java.math.BigDecimal
4944
import javax.inject.Inject
5045
import kotlin.math.max
5146
import kotlin.math.min
@@ -63,7 +58,6 @@ class TransferViewModel @Inject constructor(
6358
private val lightningRepo: LightningRepo,
6459
private val blocktankRepo: BlocktankRepo,
6560
private val walletRepo: WalletRepo,
66-
private val currencyRepo: CurrencyRepo,
6761
private val settingsStore: SettingsStore,
6862
private val cacheStore: CacheStore,
6963
private val transferRepo: TransferRepo,
@@ -361,120 +355,17 @@ class TransferViewModel @Inject constructor(
361355
// region Balance Calc
362356

363357
fun updateTransferValues(clientBalanceSat: ULong) {
364-
viewModelScope.launch {
365-
_transferValues.value = calculateTransferValues(clientBalanceSat)
366-
}
367-
}
368-
369-
fun calculateTransferValues(clientBalanceSat: ULong): TransferValues {
370-
val blocktankInfo = blocktankRepo.blocktankState.value.info
371-
if (blocktankInfo == null) return TransferValues()
372-
373-
// Calculate the total value of existing Blocktank channels
374-
val channelsSize = totalBtChannelsValueSats(blocktankInfo)
375-
376-
val minChannelSizeSat = blocktankInfo.options.minChannelSizeSat
377-
val maxChannelSizeSat = blocktankInfo.options.maxChannelSizeSat
378-
379-
// Because LSP limits constantly change depending on network fees
380-
// Add a 2% buffer to avoid fluctuations while making the order
381-
val maxChannelSize1 = (maxChannelSizeSat.toDouble() * 0.98).roundToLong().toULong()
382-
383-
// The maximum channel size the user can open including existing channels
384-
val maxChannelSize2 = maxChannelSize1.safe() - channelsSize.safe()
385-
val maxChannelSizeAvailableToIncrease = min(maxChannelSize1, maxChannelSize2)
386-
387-
val minLspBalance = getMinLspBalance(clientBalanceSat, minChannelSizeSat)
388-
val maxLspBalance = maxChannelSizeAvailableToIncrease.safe() - clientBalanceSat.safe()
389-
val defaultLspBalance = getDefaultLspBalance(clientBalanceSat, maxLspBalance)
390-
val maxClientBalance = getMaxClientBalance(maxChannelSizeAvailableToIncrease)
391-
392-
if (maxChannelSizeAvailableToIncrease < clientBalanceSat) {
393-
Logger.warn(
394-
"Amount clientBalanceSat:$clientBalanceSat too large, max possible: $maxChannelSizeAvailableToIncrease",
395-
context = TAG
358+
val options = blocktankRepo.calculateLiquidityOptions(clientBalanceSat)
359+
_transferValues.value = if (options != null) {
360+
TransferValues(
361+
defaultLspBalance = options.defaultLspBalanceSat,
362+
minLspBalance = options.minLspBalanceSat,
363+
maxLspBalance = options.maxLspBalanceSat,
364+
maxClientBalance = options.maxClientBalanceSat,
396365
)
397-
}
398-
399-
if (defaultLspBalance !in minLspBalance..maxLspBalance) {
400-
Logger.warn(
401-
"Invalid defaultLspBalance:$defaultLspBalance " +
402-
"min possible:$maxLspBalance, " +
403-
"max possible: $minLspBalance",
404-
context = TAG
405-
)
406-
}
407-
408-
if (maxChannelSizeAvailableToIncrease <= 0u) {
409-
Logger.warn("Max channel size reached. current size: $channelsSize sats", context = TAG)
410-
}
411-
412-
if (maxClientBalance <= 0u) {
413-
Logger.warn("No liquidity available to purchase $maxClientBalance", context = TAG)
414-
}
415-
416-
return TransferValues(
417-
defaultLspBalance = defaultLspBalance,
418-
minLspBalance = minLspBalance,
419-
maxLspBalance = maxLspBalance,
420-
maxClientBalance = maxClientBalance,
421-
)
422-
}
423-
424-
private fun getDefaultLspBalance(clientBalanceSat: ULong, maxLspBalance: ULong): ULong {
425-
// Calculate thresholds in sats
426-
val threshold1 = currencyRepo.convertFiatToSats(BigDecimal(225), EUR_CURRENCY).getOrNull()
427-
val threshold2 = currencyRepo.convertFiatToSats(BigDecimal(495), EUR_CURRENCY).getOrNull()
428-
val defaultLspBalanceSats = currencyRepo.convertFiatToSats(BigDecimal(450), EUR_CURRENCY).getOrNull()
429-
430-
Logger.debug("getDefaultLspBalance - clientBalanceSat: $clientBalanceSat")
431-
Logger.debug("getDefaultLspBalance - maxLspBalance: $maxLspBalance")
432-
Logger.debug("getDefaultLspBalance - defaultLspBalanceSats: $defaultLspBalanceSats")
433-
434-
if (threshold1 == null || threshold2 == null || defaultLspBalanceSats == null) {
435-
Logger.error("Failed to get rates for lspBalance calculation", context = TAG)
436-
throw ServiceError.CurrencyRateUnavailable
437-
}
438-
439-
val lspBalance = if (clientBalanceSat < threshold1) { // 0-225€: LSP balance = 450€ - client balance
440-
defaultLspBalanceSats.safe() - clientBalanceSat.safe()
441-
} else if (clientBalanceSat < threshold2) { // 225-495€: LSP balance = client balance
442-
clientBalanceSat
443-
} else if (clientBalanceSat < maxLspBalance) { // 495-950€: LSP balance = max - client balance
444-
maxLspBalance.safe() - clientBalanceSat.safe()
445366
} else {
446-
maxLspBalance
367+
TransferValues()
447368
}
448-
449-
return min(lspBalance, maxLspBalance)
450-
}
451-
452-
private fun getMinLspBalance(clientBalance: ULong, minChannelSize: ULong): ULong {
453-
// LSP balance must be at least 2.5% of the channel size for LDK to accept (reserve balance)
454-
val ldkMinimum = (clientBalance.toDouble() * 0.025).toULong()
455-
// Channel size must be at least minChannelSize
456-
val lspMinimum = minChannelSize.safe() - clientBalance.safe()
457-
458-
return max(ldkMinimum, lspMinimum)
459-
}
460-
461-
private fun getMaxClientBalance(maxChannelSize: ULong): ULong {
462-
// Remote balance must be at least 2.5% of the channel size for LDK to accept (reserve balance)
463-
val minRemoteBalance = (maxChannelSize.toDouble() * 0.025).toULong()
464-
465-
return maxChannelSize.safe() - minRemoteBalance.safe()
466-
}
467-
468-
/** Calculates the total value of channels connected to Blocktank nodes */
469-
private fun totalBtChannelsValueSats(info: IBtInfo?): ULong {
470-
val channels = lightningRepo.getChannels() ?: return 0u
471-
val btNodeIds = info?.nodes?.map { it.pubkey } ?: return 0u
472-
473-
val btChannels = channels.filter { btNodeIds.contains(it.counterpartyNodeId) }
474-
475-
val totalValue = btChannels.sumOf { it.channelValueSats }
476-
477-
return totalValue
478369
}
479370

480371
// endregion

gradle/libs.versions.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ activity-compose = { module = "androidx.activity:activity-compose", version.ref
4444
appcompat = { module = "androidx.appcompat:appcompat", version.ref = "appcompat" }
4545
barcode-scanning = { module = "com.google.mlkit:barcode-scanning", version.ref = "barcodeScanning" }
4646
biometric = { module = "androidx.biometric:biometric", version.ref = "biometric" }
47-
bitkit-core = { module = "com.synonym:bitkit-core-android", version = "0.1.30" }
47+
bitkit-core = { module = "com.synonym:bitkit-core-android", version = "0.1.31" }
4848
bouncycastle-provider-jdk = { module = "org.bouncycastle:bcprov-jdk18on", version.ref = "bouncyCastle" }
4949
camera-camera2 = { module = "androidx.camera:camera-camera2", version.ref = "camera" }
5050
camera-lifecycle = { module = "androidx.camera:camera-lifecycle", version.ref = "camera" }

0 commit comments

Comments
 (0)