From d558dd276ac96e7af29aa21a480b39217ff5c9b0 Mon Sep 17 00:00:00 2001 From: jvsena42 Date: Mon, 22 Dec 2025 15:09:18 -0300 Subject: [PATCH 1/4] fix: max sendable calc --- .../screens/wallets/send/SendAmountScreen.kt | 13 +++++- .../java/to/bitkit/viewmodels/AppViewModel.kt | 40 +++++++++++++++++++ 2 files changed, 52 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/to/bitkit/ui/screens/wallets/send/SendAmountScreen.kt b/app/src/main/java/to/bitkit/ui/screens/wallets/send/SendAmountScreen.kt index ebad53ac0..215902134 100644 --- a/app/src/main/java/to/bitkit/ui/screens/wallets/send/SendAmountScreen.kt +++ b/app/src/main/java/to/bitkit/ui/screens/wallets/send/SendAmountScreen.kt @@ -33,6 +33,7 @@ import to.bitkit.models.BalanceState import to.bitkit.models.BitcoinDisplayUnit import to.bitkit.models.NodeLifecycleState import to.bitkit.models.Toast +import to.bitkit.models.safe import to.bitkit.repositories.CurrencyState import to.bitkit.ui.LocalBalances import to.bitkit.ui.LocalCurrencies @@ -91,6 +92,12 @@ fun SendAmountScreen( currentOnEvent(SendEvent.AmountChange(amountInputUiState.sats.toULong())) } + LaunchedEffect(uiState.decodedInvoice, uiState.payMethod) { + if (uiState.payMethod == SendMethod.LIGHTNING && uiState.decodedInvoice != null) { + currentOnEvent(SendEvent.EstimateMaxRoutingFee) + } + } + SendAmountContent( walletUiState = walletUiState, uiState = uiState, @@ -190,7 +197,11 @@ private fun SendAmountNodeRunning( val availableAmount = when { isLnurlWithdraw -> uiState.lnurl.data.maxWithdrawableSat().toLong() uiState.payMethod == SendMethod.ONCHAIN -> balances.maxSendOnchainSats.toLong() - else -> balances.maxSendLightningSats.toLong() + else -> { + val maxLightning = balances.maxSendLightningSats + val routingFee = uiState.estimatedRoutingFee + (maxLightning.safe() - routingFee.safe()).toLong() + } } Column( diff --git a/app/src/main/java/to/bitkit/viewmodels/AppViewModel.kt b/app/src/main/java/to/bitkit/viewmodels/AppViewModel.kt index 6e7cd229f..c6258339f 100644 --- a/app/src/main/java/to/bitkit/viewmodels/AppViewModel.kt +++ b/app/src/main/java/to/bitkit/viewmodels/AppViewModel.kt @@ -472,6 +472,9 @@ class AppViewModel @Inject constructor( SendEvent.SwipeToPay -> onSwipeToPay() is SendEvent.ConfirmAmountWarning -> onConfirmAmountWarning(it.warning) SendEvent.DismissAmountWarning -> onDismissAmountWarning() + SendEvent.EstimateMaxRoutingFee -> viewModelScope.launch { + estimateMaxAmountRoutingFee() + } SendEvent.PayConfirmed -> onConfirmPay() SendEvent.ClearPayConfirmation -> _sendUiState.update { s -> s.copy(shouldConfirmPay = false) } SendEvent.BackToAmount -> setSendEffect(SendEffect.PopBack(SendRoute.Amount)) @@ -1458,6 +1461,41 @@ class AppViewModel @Inject constructor( } } + private suspend fun estimateMaxAmountRoutingFee() { + val currentState = _sendUiState.value + if (currentState.payMethod != SendMethod.LIGHTNING) return + + val decodedInvoice = currentState.decodedInvoice ?: return + val bolt11 = decodedInvoice.bolt11 + + val maxSendLightning = walletRepo.balanceState.value.maxSendLightningSats + if (maxSendLightning == 0uL) { + _sendUiState.update { it.copy(estimatedRoutingFee = 0uL) } + return + } + + val buffer = 2uL + val amountToEstimate = if (maxSendLightning > buffer) { + maxSendLightning - buffer + } else { + maxSendLightning + } + + val feeResult = lightningRepo.estimateRoutingFeesForAmount( + bolt11 = bolt11, + amountSats = amountToEstimate + ) + + feeResult.onSuccess { fee -> + _sendUiState.update { + it.copy(estimatedRoutingFee = fee + buffer) + } + }.onFailure { e -> + Logger.error("Failed to estimate routing fee for max amount", e, context = TAG) + _sendUiState.update { it.copy(estimatedRoutingFee = 0uL) } + } + } + private suspend fun getFeeEstimate(speed: TransactionSpeed? = null): Long { val currentState = _sendUiState.value return lightningRepo.calculateTotalFee( @@ -2001,6 +2039,7 @@ data class SendUiState( val feeRates: FeeRates? = null, val fee: SendFee? = null, val fees: Map = emptyMap(), + val estimatedRoutingFee: ULong = 0uL, ) enum class SanityWarning(@StringRes val message: Int, val testTag: String) { @@ -2064,6 +2103,7 @@ sealed interface SendEvent { data object SpeedAndFee : SendEvent data object PaymentMethodSwitch : SendEvent data class ConfirmAmountWarning(val warning: SanityWarning) : SendEvent + data object EstimateMaxRoutingFee : SendEvent data object DismissAmountWarning : SendEvent data object PayConfirmed : SendEvent data object ClearPayConfirmation : SendEvent From 8612beae18f011b0b9e33bc60b7c5b86d2d53b48 Mon Sep 17 00:00:00 2001 From: jvsena42 Date: Tue, 23 Dec 2025 09:02:29 -0300 Subject: [PATCH 2/4] fix: add comment --- app/src/main/java/to/bitkit/models/BalanceState.kt | 2 +- app/src/main/java/to/bitkit/viewmodels/AppViewModel.kt | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/to/bitkit/models/BalanceState.kt b/app/src/main/java/to/bitkit/models/BalanceState.kt index 140d81c55..07cfc7a23 100644 --- a/app/src/main/java/to/bitkit/models/BalanceState.kt +++ b/app/src/main/java/to/bitkit/models/BalanceState.kt @@ -6,7 +6,7 @@ import kotlinx.serialization.Serializable data class BalanceState( val totalOnchainSats: ULong = 0uL, val totalLightningSats: ULong = 0uL, - val maxSendLightningSats: ULong = 0uL, + val maxSendLightningSats: ULong = 0uL, // Without account routing fees val maxSendOnchainSats: ULong = 0uL, val balanceInTransferToSavings: ULong = 0uL, val balanceInTransferToSpending: ULong = 0uL, diff --git a/app/src/main/java/to/bitkit/viewmodels/AppViewModel.kt b/app/src/main/java/to/bitkit/viewmodels/AppViewModel.kt index c6258339f..584c54d11 100644 --- a/app/src/main/java/to/bitkit/viewmodels/AppViewModel.kt +++ b/app/src/main/java/to/bitkit/viewmodels/AppViewModel.kt @@ -475,6 +475,7 @@ class AppViewModel @Inject constructor( SendEvent.EstimateMaxRoutingFee -> viewModelScope.launch { estimateMaxAmountRoutingFee() } + SendEvent.PayConfirmed -> onConfirmPay() SendEvent.ClearPayConfirmation -> _sendUiState.update { s -> s.copy(shouldConfirmPay = false) } SendEvent.BackToAmount -> setSendEffect(SendEffect.PopBack(SendRoute.Amount)) From 327ba8c526013f9fd37373fa659865cbd9aed460 Mon Sep 17 00:00:00 2001 From: jvsena42 Date: Tue, 23 Dec 2025 09:27:08 -0300 Subject: [PATCH 3/4] fix: use individual balance instead of total balance --- app/src/main/java/to/bitkit/viewmodels/AppViewModel.kt | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/to/bitkit/viewmodels/AppViewModel.kt b/app/src/main/java/to/bitkit/viewmodels/AppViewModel.kt index 584c54d11..ea4020e46 100644 --- a/app/src/main/java/to/bitkit/viewmodels/AppViewModel.kt +++ b/app/src/main/java/to/bitkit/viewmodels/AppViewModel.kt @@ -1046,9 +1046,12 @@ class AppViewModel @Inject constructor( if (_sendUiState.value.showSanityWarningDialog != null) return val settings = settingsStore.data.first() - + val balanceToCheck = when (_sendUiState.value.payMethod) { + SendMethod.ONCHAIN -> walletRepo.balanceState.value.maxSendOnchainSats + SendMethod.LIGHTNING -> walletRepo.balanceState.value.maxSendLightningSats + } if ( - amountSats > BigDecimal.valueOf(walletRepo.balanceState.value.totalSats.toLong()) + amountSats > BigDecimal.valueOf(balanceToCheck.toLong()) .times(BigDecimal(MAX_BALANCE_FRACTION)).toLong().toUInt() && SanityWarning.OVER_HALF_BALANCE !in _sendUiState.value.confirmedWarnings ) { From 1fcb36b9b3ed33a3535391c02db5e88a35cd5224 Mon Sep 17 00:00:00 2001 From: jvsena42 Date: Tue, 23 Dec 2025 10:41:29 -0300 Subject: [PATCH 4/4] chore: implement safe check --- app/src/main/java/to/bitkit/viewmodels/AppViewModel.kt | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/app/src/main/java/to/bitkit/viewmodels/AppViewModel.kt b/app/src/main/java/to/bitkit/viewmodels/AppViewModel.kt index ea4020e46..59ce94a52 100644 --- a/app/src/main/java/to/bitkit/viewmodels/AppViewModel.kt +++ b/app/src/main/java/to/bitkit/viewmodels/AppViewModel.kt @@ -83,6 +83,7 @@ import to.bitkit.models.NewTransactionSheetType import to.bitkit.models.Suggestion import to.bitkit.models.Toast import to.bitkit.models.TransactionSpeed +import to.bitkit.models.safe import to.bitkit.models.toActivityFilter import to.bitkit.models.toTxType import to.bitkit.repositories.ActivityRepo @@ -1479,11 +1480,7 @@ class AppViewModel @Inject constructor( } val buffer = 2uL - val amountToEstimate = if (maxSendLightning > buffer) { - maxSendLightning - buffer - } else { - maxSendLightning - } + val amountToEstimate = maxSendLightning.safe() - buffer.safe() val feeResult = lightningRepo.estimateRoutingFeesForAmount( bolt11 = bolt11,