diff --git a/app/src/main/java/to/bitkit/viewmodels/TransferViewModel.kt b/app/src/main/java/to/bitkit/viewmodels/TransferViewModel.kt index 9a83e0472..83dbc06c1 100644 --- a/app/src/main/java/to/bitkit/viewmodels/TransferViewModel.kt +++ b/app/src/main/java/to/bitkit/viewmodels/TransferViewModel.kt @@ -28,6 +28,7 @@ import org.lightningdevkit.ldknode.ChannelDetails import to.bitkit.R import to.bitkit.data.CacheStore import to.bitkit.data.SettingsStore +import to.bitkit.env.Defaults import to.bitkit.ext.amountOnClose import to.bitkit.models.Toast import to.bitkit.models.TransactionSpeed @@ -194,6 +195,18 @@ class TransferViewModel @Inject constructor( viewModelScope.launch { val address = order.payment?.onchain?.address.orEmpty() + // Calculate if change would be dust and we should use sendAll + val spendableBalance = + lightningRepo.lightningState.value.balances?.spendableOnchainBalanceSats ?: 0uL + val txFee = lightningRepo.calculateTotalFee( + amountSats = spendableBalance, + address = address, + speed = speed, + ).getOrElse { 0uL } + + val expectedChange = spendableBalance.toLong() - order.feeSat.toLong() - txFee.toLong() + val shouldUseSendAll = expectedChange >= 0 && expectedChange < Defaults.dustLimit.toInt() + lightningRepo .sendOnChain( address = address, @@ -201,6 +214,7 @@ class TransferViewModel @Inject constructor( speed = speed, isTransfer = true, channelId = order.channel?.shortChannelId, + isMaxAmount = shouldUseSendAll, ) .onSuccess { txId -> cacheStore.addPaidOrder(orderId = order.id, txId = txId) @@ -297,30 +311,53 @@ class TransferViewModel @Inject constructor( isNodeRunning.first { it } } - // Calculate the LSP fee to the total balance - blocktankRepo.estimateOrderFee( + // Two-pass fee estimation to match actual order creation + // First pass: estimate with availableAmount to get approximate clientBalance + val values1 = blocktankRepo.calculateLiquidityOptions(availableAmount).getOrNull() + if (values1 == null) { + _spendingUiState.update { it.copy(isLoading = false) } + return@launch + } + val lspBalance1 = maxOf(values1.defaultLspBalanceSat, values1.minLspBalanceSat) + val feeEstimate1 = blocktankRepo.estimateOrderFee( spendingBalanceSats = availableAmount, - receivingBalanceSats = _transferValues.value.maxLspBalance + receivingBalanceSats = lspBalance1, + ).getOrNull() + + if (feeEstimate1 == null) { + _spendingUiState.update { it.copy(isLoading = false) } + return@launch + } + + val lspFees1 = feeEstimate1.networkFeeSat.safe() + feeEstimate1.serviceFeeSat.safe() + val approxClientBalance = availableAmount.safe() - lspFees1.safe() + + // Second pass: recalculate with actual clientBalance that order creation will use + val values2 = blocktankRepo.calculateLiquidityOptions(approxClientBalance).getOrNull() + if (values2 == null || values2.maxLspBalanceSat == 0uL) { + _spendingUiState.update { it.copy(isLoading = false, maxAllowedToSend = 0) } + return@launch + } + val lspBalance2 = maxOf(values2.defaultLspBalanceSat, values2.minLspBalanceSat) + + blocktankRepo.estimateOrderFee( + spendingBalanceSats = approxClientBalance, + receivingBalanceSats = lspBalance2, ).onSuccess { estimate -> maxLspFee = estimate.feeSat - - // Calculate the available balance to send after LSP fee - val balanceAfterLspFee = availableAmount.safe() - maxLspFee.safe() + val lspFees = estimate.networkFeeSat.safe() + estimate.serviceFeeSat.safe() + val maxClientBalance = availableAmount.safe() - lspFees.safe() _spendingUiState.update { - // Calculate the max available to send considering the current balance and LSP policy it.copy( - maxAllowedToSend = min( - _transferValues.value.maxClientBalance.toLong(), - balanceAfterLspFee.toLong() - ), + maxAllowedToSend = min(values2.maxClientBalanceSat.toLong(), maxClientBalance.toLong()), isLoading = false, - balanceAfterFee = availableAmount.toLong() + balanceAfterFee = availableAmount.toLong(), ) } }.onFailure { exception -> _spendingUiState.update { it.copy(isLoading = false) } - Logger.error("Failure", exception) + Logger.error("Failure", exception, context = TAG) setTransferEffect(TransferEffect.ToastException(exception)) } }