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
2 changes: 1 addition & 1 deletion app/src/main/java/to/bitkit/models/BalanceState.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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(
Expand Down
45 changes: 43 additions & 2 deletions app/src/main/java/to/bitkit/viewmodels/AppViewModel.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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))
Expand Down Expand Up @@ -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
) {
Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -2001,6 +2040,7 @@ data class SendUiState(
val feeRates: FeeRates? = null,
val fee: SendFee? = null,
val fees: Map<FeeRate, Long> = emptyMap(),
val estimatedRoutingFee: ULong = 0uL,
)

enum class SanityWarning(@StringRes val message: Int, val testTag: String) {
Expand Down Expand Up @@ -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
Expand Down
Loading