Skip to content

Commit cb58510

Browse files
authored
Merge pull request #378 from synonymdev/fix/reserve-fee-amount
Reserve a fee amount when trying to sent all on-chain balance
2 parents 6b770ce + 585212f commit cb58510

File tree

11 files changed

+61
-35
lines changed

11 files changed

+61
-35
lines changed

app/src/main/java/to/bitkit/models/BalanceState.kt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,5 +7,6 @@ data class BalanceState(
77
val totalOnchainSats: ULong = 0uL,
88
val totalLightningSats: ULong = 0uL,
99
val maxSendLightningSats: ULong = 0uL, // TODO use where applicable
10+
val maxSendOnchainSats: ULong = 0uL,
1011
val totalSats: ULong = 0uL,
1112
)

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

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -520,6 +520,7 @@ class LightningRepo @Inject constructor(
520520
feeRates: FeeRates? = null,
521521
isTransfer: Boolean = false,
522522
channelId: String? = null,
523+
isMaxAmount: Boolean = false,
523524
): Result<Txid> =
524525
executeWhenNodeRunning("Send on-chain") {
525526
val transactionSpeed = speed ?: settingsStore.data.first().defaultTransactionSpeed
@@ -538,6 +539,7 @@ class LightningRepo @Inject constructor(
538539
sats = sats,
539540
satsPerVByte = satsPerVByte,
540541
utxosToSpend = finalUtxosToSpend,
542+
isMaxAmount = isMaxAmount
541543
)
542544
cacheStore.addTransactionMetadata(
543545
TransactionMetadata(
@@ -699,6 +701,10 @@ class LightningRepo @Inject constructor(
699701
fun getBalances(): BalanceDetails? =
700702
if (_lightningState.value.nodeLifecycleState.isRunning()) lightningService.balances else null
701703

704+
suspend fun getBalancesAsync(): Result<BalanceDetails> = executeWhenNodeRunning("getBalancesAsync") {
705+
Result.success(checkNotNull(lightningService.balances))
706+
}
707+
702708
fun getStatus(): NodeStatus? =
703709
if (_lightningState.value.nodeLifecycleState.isRunning()) lightningService.status else null
704710

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

Lines changed: 27 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ import to.bitkit.ext.totalNextOutboundHtlcLimitSats
3131
import to.bitkit.models.AddressModel
3232
import to.bitkit.models.BalanceState
3333
import to.bitkit.models.NodeLifecycleState
34+
import to.bitkit.models.TransactionSpeed
3435
import to.bitkit.models.toDerivationPath
3536
import to.bitkit.services.CoreService
3637
import to.bitkit.utils.AddressChecker
@@ -160,18 +161,22 @@ class WalletRepo @Inject constructor(
160161
}
161162

162163
suspend fun syncBalances() {
163-
lightningRepo.getBalances()?.let { balance ->
164+
lightningRepo.getBalancesAsync().onSuccess { balance ->
165+
_walletState.update { it.copy(balanceDetails = balance) }
166+
164167
val totalSats = balance.totalLightningBalanceSats + balance.totalOnchainBalanceSats
165168

166169
val newBalance = BalanceState(
167170
totalOnchainSats = balance.totalOnchainBalanceSats,
168171
totalLightningSats = balance.totalLightningBalanceSats,
169172
maxSendLightningSats = lightningRepo.getChannels()?.totalNextOutboundHtlcLimitSats() ?: 0u,
173+
maxSendOnchainSats = getMaxSendAmount(),
170174
totalSats = totalSats,
171175
)
172176
_balanceState.update { newBalance }
173-
_walletState.update { it.copy(balanceDetails = lightningRepo.getBalances()) }
174177
saveBalanceState(newBalance)
178+
}.onFailure {
179+
Logger.warn("Could not sync balances", context = TAG)
175180
}
176181
}
177182

@@ -318,24 +323,6 @@ class WalletRepo @Inject constructor(
318323
_balanceState.update { balanceState }
319324
}
320325

321-
suspend fun getMaxSendAmount(): ULong = withContext(bgDispatcher) {
322-
val totalOnchainSats = balanceState.value.totalOnchainSats
323-
if (totalOnchainSats == 0uL) {
324-
return@withContext 0uL
325-
}
326-
327-
try {
328-
val fee = lightningRepo.calculateTotalFee(totalOnchainSats).getOrThrow()
329-
val maxSendable = (totalOnchainSats - fee).coerceAtLeast(0u)
330-
331-
return@withContext maxSendable
332-
} catch (_: Throwable) {
333-
Logger.debug("Could not calculate max send amount, using as fallback 90% of total", context = TAG)
334-
val fallbackMax = (totalOnchainSats.toDouble() * 0.9).toULong()
335-
return@withContext fallbackMax
336-
}
337-
}
338-
339326
// BIP21 state management
340327
fun updateBip21AmountSats(amount: ULong?) {
341328
_walletState.update { it.copy(bip21AmountSats = amount) }
@@ -478,6 +465,25 @@ class WalletRepo @Inject constructor(
478465
}
479466
}
480467

468+
private suspend fun getMaxSendAmount(): ULong = withContext(bgDispatcher) {
469+
val spendableOnchainSats = walletState.value.balanceDetails?.spendableOnchainBalanceSats ?: 0uL
470+
if (spendableOnchainSats == 0uL) {
471+
return@withContext 0uL
472+
}
473+
val fallbackMaxFee = (spendableOnchainSats.toDouble() * FALLBACK_FEE_PERCENT).toULong()
474+
475+
val fee = lightningRepo.calculateTotalFee(
476+
amountSats = spendableOnchainSats,
477+
speed = TransactionSpeed.default(),
478+
utxosToSpend = lightningRepo.listSpendableOutputs().getOrNull()
479+
).onFailure {
480+
Logger.debug("Could not calculate max send fee, using as fallback 10% of total", context = TAG)
481+
}.getOrDefault(fallbackMaxFee)
482+
483+
val maxSendable = (spendableOnchainSats - fee).coerceAtLeast(0u)
484+
maxSendable
485+
}
486+
481487
private suspend fun Scanner.OnChain.extractLightningHash(): String? {
482488
val lightningInvoice: String = this.invoice.params?.get("lightning") ?: return null
483489
val decoded = decode(lightningInvoice)
@@ -494,6 +500,7 @@ class WalletRepo @Inject constructor(
494500

495501
private companion object {
496502
const val TAG = "WalletRepo"
503+
const val FALLBACK_FEE_PERCENT = 0.1
497504
}
498505
}
499506

app/src/main/java/to/bitkit/services/CoreService.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -414,7 +414,7 @@ class ActivityService(
414414
}
415415

416416
if (onChain.id in cacheStore.data.first().deletedActivities && !forceUpdate) {
417-
Logger.debug("Activity ${onChain.id} was already deleted, skipping", context = TAG)
417+
Logger.verbose("Activity ${onChain.id} was already deleted, skipping", context = TAG)
418418
return
419419
}
420420

app/src/main/java/to/bitkit/services/LightningService.kt

Lines changed: 16 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -447,18 +447,27 @@ class LightningService @Inject constructor(
447447
sats: ULong,
448448
satsPerVByte: UInt,
449449
utxosToSpend: List<SpendableUtxo>? = null,
450+
isMaxAmount: Boolean = false
450451
): Txid {
451452
val node = this.node ?: throw ServiceError.NodeNotSetup
452453

453-
Logger.info("Sending $sats sats to $address with satsPerVByte=$satsPerVByte")
454+
Logger.info("Sending $sats sats to $address, satsPerVByte=$satsPerVByte, isMaxAmount = $isMaxAmount")
454455

455456
return ServiceQueue.LDK.background {
456-
node.onchainPayment().sendToAddress(
457-
address = address,
458-
amountSats = sats,
459-
feeRate = convertVByteToKwu(satsPerVByte),
460-
utxosToSpend = utxosToSpend,
461-
)
457+
if (isMaxAmount) {
458+
node.onchainPayment().sendAllToAddress(
459+
address = address,
460+
retainReserve = true,
461+
feeRate = FeeRate.fromSatPerVbUnchecked(satsPerVByte.toULong()),
462+
)
463+
} else {
464+
node.onchainPayment().sendToAddress(
465+
address = address,
466+
amountSats = sats,
467+
feeRate = convertVByteToKwu(satsPerVByte),
468+
utxosToSpend = utxosToSpend,
469+
)
470+
}
462471
}
463472
}
464473

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ class ExternalNodeViewModel @Inject constructor(
5353
private fun observeState() {
5454
viewModelScope.launch {
5555
walletRepo.balanceState.collect {
56-
val maxAmount = walletRepo.getMaxSendAmount()
56+
val maxAmount = walletRepo.balanceState.value.maxSendOnchainSats
5757
_uiState.update { it.copy(amount = it.amount.copy(max = maxAmount.toLong())) }
5858
}
5959
}

app/src/main/java/to/bitkit/ui/screens/wallets/send/SendAmountScreen.kt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -102,7 +102,7 @@ fun SendAmountScreen(
102102
}.takeIf { canGoBack },
103103
onClickMax = { maxSats ->
104104
// TODO port the RN sendMax logic if still needed
105-
if (uiState.payMethod == SendMethod.LIGHTNING && uiState.lnurl == null) {
105+
if (uiState.lnurl == null) {
106106
app?.toast(
107107
type = Toast.ToastType.WARNING,
108108
title = context.getString(R.string.wallet__send_max_spending__title),
@@ -190,7 +190,7 @@ private fun SendAmountNodeRunning(
190190

191191
val availableAmount = when {
192192
isLnurlWithdraw -> uiState.lnurl.data.maxWithdrawableSat().toLong()
193-
uiState.payMethod == SendMethod.ONCHAIN -> balances.totalOnchainSats.toLong()
193+
uiState.payMethod == SendMethod.ONCHAIN -> balances.maxSendOnchainSats.toLong()
194194
else -> balances.maxSendLightningSats.toLong()
195195
}
196196

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1103,6 +1103,8 @@ class AppViewModel @Inject constructor(
11031103
sats = amount,
11041104
speed = _sendUiState.value.speed,
11051105
utxosToSpend = _sendUiState.value.selectedUtxos,
1106+
isMaxAmount = _sendUiState.value.payMethod == SendMethod.ONCHAIN &&
1107+
amount == walletRepo.balanceState.value.maxSendOnchainSats
11061108
)
11071109
}
11081110

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -218,7 +218,7 @@ class TransferViewModel @Inject constructor(
218218
_spendingUiState.update { it.copy(isLoading = true) }
219219

220220
// Get the max available balance discounting onChain fee
221-
val availableAmount = walletRepo.getMaxSendAmount()
221+
val availableAmount = walletRepo.balanceState.value.maxSendOnchainSats
222222

223223
withTimeoutOrNull(1.minutes) {
224224
isNodeRunning.first { it }

app/src/test/java/to/bitkit/repositories/LightningRepoTest.kt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -388,7 +388,8 @@ class LightningRepoTest : BaseUnitTest() {
388388
address = any(),
389389
sats = any(),
390390
satsPerVByte = any(),
391-
utxosToSpend = anyOrNull()
391+
utxosToSpend = anyOrNull(),
392+
isMaxAmount = any()
392393
)
393394
).thenReturn("testPaymentId")
394395

0 commit comments

Comments
 (0)