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/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..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 @@ -472,6 +473,10 @@ 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)) @@ -1042,9 +1047,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 ) { @@ -1458,6 +1466,37 @@ 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 = maxSendLightning.safe() - buffer.safe() + + 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 +2040,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 +2104,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