From 373890ed632ab5b105883a0966f85d123ab1de22 Mon Sep 17 00:00:00 2001 From: Ovi Trif Date: Mon, 14 Jul 2025 23:03:55 +0200 Subject: [PATCH 01/15] chore: cleanup custom fee settings screen --- .../settings/transactionSpeed/CustomFeeSettingsScreen.kt | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/app/src/main/java/to/bitkit/ui/settings/transactionSpeed/CustomFeeSettingsScreen.kt b/app/src/main/java/to/bitkit/ui/settings/transactionSpeed/CustomFeeSettingsScreen.kt index 89b41dbef..d66699565 100644 --- a/app/src/main/java/to/bitkit/ui/settings/transactionSpeed/CustomFeeSettingsScreen.kt +++ b/app/src/main/java/to/bitkit/ui/settings/transactionSpeed/CustomFeeSettingsScreen.kt @@ -43,12 +43,13 @@ fun CustomFeeSettingsScreen( ) { val settings = settingsViewModel ?: return val customFeeRate = settings.defaultTransactionSpeed.collectAsStateWithLifecycle() + val currency = currencyViewModel ?: return + var converted: ConvertedAmount? by remember { mutableStateOf(null) } + var input by remember { mutableStateOf((customFeeRate.value as? TransactionSpeed.Custom)?.satsPerVByte?.toString() ?: "") } - val currency = currencyViewModel ?: return val totalFee = Env.TransactionDefaults.recommendedBaseFee * (input.toUIntOrNull() ?: 0u) - var converted: ConvertedAmount? by remember { mutableStateOf(null) } LaunchedEffect(input) { val inputNum = input.toLongOrNull() ?: 0 @@ -116,7 +117,7 @@ private fun CustomFeeSettingsContent( Spacer(modifier = Modifier.height(16.dp)) LargeRow( prefix = null, - text = if (input.isEmpty()) "0" else input, + text = input.ifEmpty { "0" }, symbol = BITCOIN_SYMBOL, showSymbol = true, ) From b09515360e6630f7e4b5e79f370cc2eba1659ab5 Mon Sep 17 00:00:00 2001 From: Ovi Trif Date: Tue, 15 Jul 2025 12:47:29 +0200 Subject: [PATCH 02/15] refactor: move amounts to viewmodel --- .../transfer/external/ExternalAmountScreen.kt | 52 +++++++++---------- .../external/ExternalConfirmScreen.kt | 6 +-- .../viewmodels/ExternalNodeViewModel.kt | 29 +++++++++-- 3 files changed, 53 insertions(+), 34 deletions(-) diff --git a/app/src/main/java/to/bitkit/ui/screens/transfer/external/ExternalAmountScreen.kt b/app/src/main/java/to/bitkit/ui/screens/transfer/external/ExternalAmountScreen.kt index 184303bfd..51302ae43 100644 --- a/app/src/main/java/to/bitkit/ui/screens/transfer/external/ExternalAmountScreen.kt +++ b/app/src/main/java/to/bitkit/ui/screens/transfer/external/ExternalAmountScreen.kt @@ -10,12 +10,9 @@ import androidx.compose.foundation.layout.imePadding import androidx.compose.foundation.layout.padding import androidx.compose.material3.HorizontalDivider import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableLongStateOf -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.saveable.rememberSaveable -import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource @@ -38,6 +35,7 @@ import to.bitkit.ui.theme.AppThemeSurface import to.bitkit.ui.theme.Colors import to.bitkit.ui.utils.withAccent import to.bitkit.viewmodels.ExternalNodeViewModel +import to.bitkit.viewmodels.ExternalNodeContract import kotlin.math.roundToLong @Composable @@ -47,19 +45,27 @@ fun ExternalAmountScreen( onBackClick: () -> Unit, onCloseClick: () -> Unit, ) { - ExternalAmountContent( - onContinueClick = { satsAmount -> - viewModel.onAmountContinue(satsAmount) - onContinue() - }, + val uiState by viewModel.uiState.collectAsState() + + LaunchedEffect(Unit) { viewModel.updateMaxAmount() } + + Content( + amountState = uiState.amount, + onAmountChange = { sats -> viewModel.onAmountChange(sats) }, + onAmountOverride = { sats -> viewModel.onAmountOverride(sats) }, + onContinueClick = { onContinue() }, onBackClick = onBackClick, onCloseClick = onCloseClick, ) } @Composable -private fun ExternalAmountContent( - onContinueClick: (Long) -> Unit = {}, +private fun Content( + maxAmount: Long = 0L, + amountState: ExternalNodeContract.UiState.Amount = ExternalNodeContract.UiState.Amount(), + onAmountChange: (Long) -> Unit = {}, + onAmountOverride: (Long) -> Unit = {}, + onContinueClick: () -> Unit = {}, onBackClick: () -> Unit = {}, onCloseClick: () -> Unit = {}, ) { @@ -75,9 +81,6 @@ private fun ExternalAmountContent( .fillMaxSize() .imePadding() ) { - var satsAmount by rememberSaveable { mutableLongStateOf(0) } - var overrideSats: Long? by remember { mutableStateOf(null) } - val availableAmount = LocalBalances.current.totalOnchainSats Spacer(modifier = Modifier.height(16.dp)) @@ -86,10 +89,9 @@ private fun ExternalAmountContent( AmountInput( primaryDisplay = LocalCurrencies.current.primaryDisplay, - overrideSats = overrideSats, + overrideSats = amountState.overrideSats, ) { sats -> - satsAmount = sats - overrideSats = null + onAmountChange(sats) } Spacer(modifier = Modifier.weight(1f)) @@ -116,7 +118,7 @@ private fun ExternalAmountContent( color = Colors.Purple, onClick = { val quarter = (availableAmount.toDouble() / 4.0).roundToLong() - overrideSats = quarter + onAmountOverride(quarter) }, ) // Max Button @@ -124,8 +126,7 @@ private fun ExternalAmountContent( text = stringResource(R.string.common__max), color = Colors.Purple, onClick = { - val max = (availableAmount.toDouble() * 0.9).toLong() // TODO calc max amount - overrideSats = max + onAmountOverride(maxAmount) }, ) } @@ -134,8 +135,8 @@ private fun ExternalAmountContent( PrimaryButton( text = stringResource(R.string.common__continue), - onClick = { onContinueClick(satsAmount) }, - enabled = satsAmount != 0L, + onClick = { onContinueClick() }, + enabled = amountState.sats != 0L, ) Spacer(modifier = Modifier.height(16.dp)) @@ -143,11 +144,10 @@ private fun ExternalAmountContent( } } -@Preview(showSystemUi = true, showBackground = true) +@Preview(showSystemUi = true) @Composable private fun Preview() { AppThemeSurface { - ExternalAmountContent( - ) + Content() } } diff --git a/app/src/main/java/to/bitkit/ui/screens/transfer/external/ExternalConfirmScreen.kt b/app/src/main/java/to/bitkit/ui/screens/transfer/external/ExternalConfirmScreen.kt index 635995c2c..57784c712 100644 --- a/app/src/main/java/to/bitkit/ui/screens/transfer/external/ExternalConfirmScreen.kt +++ b/app/src/main/java/to/bitkit/ui/screens/transfer/external/ExternalConfirmScreen.kt @@ -95,7 +95,7 @@ private fun ExternalConfirmContent( ) { val networkFee = 0L // TODO calculate txFee val serviceFee = 0L - val totalFee = uiState.localBalance + networkFee + val totalFee = uiState.amount.sats + networkFee Spacer(modifier = Modifier.height(16.dp)) Display(text = stringResource(R.string.lightning__transfer__confirm).withAccent(accentColor = Colors.Purple)) @@ -141,7 +141,7 @@ private fun ExternalConfirmContent( ) { FeeInfo( label = stringResource(R.string.lightning__spending_confirm__amount), - amount = uiState.localBalance, + amount = uiState.amount.sats, ) FeeInfo( label = stringResource(R.string.lightning__spending_confirm__total), @@ -179,7 +179,7 @@ private fun ExternalConfirmScreenPreview() { AppThemeSurface { ExternalConfirmContent( uiState = ExternalNodeContract.UiState( - localBalance = 45_500L, + amount = ExternalNodeContract.UiState.Amount(sats = 45_500L), ) ) } diff --git a/app/src/main/java/to/bitkit/viewmodels/ExternalNodeViewModel.kt b/app/src/main/java/to/bitkit/viewmodels/ExternalNodeViewModel.kt index 5b4a1b12c..95603abdc 100644 --- a/app/src/main/java/to/bitkit/viewmodels/ExternalNodeViewModel.kt +++ b/app/src/main/java/to/bitkit/viewmodels/ExternalNodeViewModel.kt @@ -18,6 +18,7 @@ import to.bitkit.ext.WatchResult import to.bitkit.ext.watchUntil import to.bitkit.models.LnPeer import to.bitkit.models.Toast +import to.bitkit.repositories.WalletRepo import to.bitkit.services.LdkNodeEventBus import to.bitkit.services.LightningService import to.bitkit.ui.shared.toast.ToastEventBus @@ -30,6 +31,7 @@ class ExternalNodeViewModel @Inject constructor( @ApplicationContext private val context: Context, private val lightningService: LightningService, private val ldkNodeEventBus: LdkNodeEventBus, + private val walletRepo: WalletRepo, ) : ViewModel() { private val _uiState = MutableStateFlow(UiState()) val uiState = _uiState.asStateFlow() @@ -38,6 +40,13 @@ class ExternalNodeViewModel @Inject constructor( val effects = _effects.asSharedFlow() private fun setEffect(effect: SideEffect) = viewModelScope.launch { _effects.emit(effect) } + fun updateMaxAmount() { + val totalOnchainSats = walletRepo.balanceState.value.totalOnchainSats + val maxAmount = (totalOnchainSats.toDouble() * 0.9).toLong() + + _uiState.update { it.copy(amount = it.amount.copy(max = maxAmount)) } + } + fun onConnectionContinue(peer: LnPeer) { viewModelScope.launch { _uiState.update { it.copy(isLoading = true) } @@ -74,8 +83,12 @@ class ExternalNodeViewModel @Inject constructor( } } - fun onAmountContinue(satsAmount: Long) { - _uiState.update { it.copy(localBalance = satsAmount) } + fun onAmountChange(sats: Long) { + _uiState.update { it.copy(amount = it.amount.copy(sats = sats, overrideSats = null)) } + } + + fun onAmountOverride(sats: Long?) { + _uiState.update { it.copy(amount = it.amount.copy(overrideSats = sats)) } } fun onConfirm() { @@ -84,7 +97,7 @@ class ExternalNodeViewModel @Inject constructor( val result = lightningService.openChannel( peer = requireNotNull(_uiState.value.peer), - channelAmountSats = _uiState.value.localBalance.toULong(), + channelAmountSats = _uiState.value.amount.sats.toULong(), ) if (result.isSuccess) { @@ -143,8 +156,14 @@ interface ExternalNodeContract { data class UiState( val isLoading: Boolean = false, val peer: LnPeer? = null, - val localBalance: Long = 0, - ) + val amount: Amount = Amount(), + ) { + data class Amount( + val sats: Long = 0, + val max: Long = 0, + val overrideSats: Long? = null, + ) + } sealed interface SideEffect { data object ConnectionSuccess : SideEffect From 920f110474f2d21601916acc751c3be61826d07d Mon Sep 17 00:00:00 2001 From: Ovi Trif Date: Tue, 15 Jul 2025 13:35:17 +0200 Subject: [PATCH 03/15] fix: trim node uri input --- app/src/main/java/to/bitkit/viewmodels/ExternalNodeViewModel.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/java/to/bitkit/viewmodels/ExternalNodeViewModel.kt b/app/src/main/java/to/bitkit/viewmodels/ExternalNodeViewModel.kt index 95603abdc..e41761f45 100644 --- a/app/src/main/java/to/bitkit/viewmodels/ExternalNodeViewModel.kt +++ b/app/src/main/java/to/bitkit/viewmodels/ExternalNodeViewModel.kt @@ -70,7 +70,7 @@ class ExternalNodeViewModel @Inject constructor( fun parseNodeUri(uriString: String) { viewModelScope.launch { - val result = LnPeer.parseUri(uriString) + val result = LnPeer.parseUri(uriString.trim()) if (result.isSuccess) { _uiState.update { it.copy(peer = result.getOrNull()) } From c3d956b81f65a6a81d7cba5700b25730d71314dd Mon Sep 17 00:00:00 2001 From: Ovi Trif Date: Tue, 15 Jul 2025 13:41:23 +0200 Subject: [PATCH 04/15] fix: max amount override --- .../ui/screens/transfer/external/ExternalAmountScreen.kt | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/app/src/main/java/to/bitkit/ui/screens/transfer/external/ExternalAmountScreen.kt b/app/src/main/java/to/bitkit/ui/screens/transfer/external/ExternalAmountScreen.kt index 51302ae43..ad57aa78a 100644 --- a/app/src/main/java/to/bitkit/ui/screens/transfer/external/ExternalAmountScreen.kt +++ b/app/src/main/java/to/bitkit/ui/screens/transfer/external/ExternalAmountScreen.kt @@ -61,7 +61,6 @@ fun ExternalAmountScreen( @Composable private fun Content( - maxAmount: Long = 0L, amountState: ExternalNodeContract.UiState.Amount = ExternalNodeContract.UiState.Amount(), onAmountChange: (Long) -> Unit = {}, onAmountOverride: (Long) -> Unit = {}, @@ -126,7 +125,7 @@ private fun Content( text = stringResource(R.string.common__max), color = Colors.Purple, onClick = { - onAmountOverride(maxAmount) + onAmountOverride(amountState.max) }, ) } From 0c5c595cf078abf4bf5b318cac1930bb379578b5 Mon Sep 17 00:00:00 2001 From: Ovi Trif Date: Tue, 15 Jul 2025 13:52:17 +0200 Subject: [PATCH 05/15] feat: calc max send amount --- .../java/to/bitkit/repositories/WalletRepo.kt | 55 +++++++++---------- .../transfer/external/ExternalAmountScreen.kt | 12 ++-- .../viewmodels/ExternalNodeViewModel.kt | 34 ++++++++++-- 3 files changed, 61 insertions(+), 40 deletions(-) diff --git a/app/src/main/java/to/bitkit/repositories/WalletRepo.kt b/app/src/main/java/to/bitkit/repositories/WalletRepo.kt index eafc7f625..88fd632bf 100644 --- a/app/src/main/java/to/bitkit/repositories/WalletRepo.kt +++ b/app/src/main/java/to/bitkit/repositories/WalletRepo.kt @@ -328,6 +328,32 @@ class WalletRepo @Inject constructor( } } + suspend fun getMaxSendAmount(): ULong = withContext(bgDispatcher) { + val totalOnchainSats = balanceState.value.totalOnchainSats + if (totalOnchainSats == 0uL) { + return@withContext 0uL + } + + try { + val minFeeBuffer = 1000uL + val amountSats = (totalOnchainSats - minFeeBuffer).coerceAtLeast(0uL) + val fee = lightningRepo.calculateTotalFee( + address = getOnchainAddress(), + amountSats = amountSats, + ).getOrThrow() + + val maxSendable = (totalOnchainSats - fee).coerceAtLeast(0uL) + + return@withContext maxSendable + } catch (_: Throwable) { + Logger.debug("Could not calculate max send amount, using fallback 90% of total", context = TAG) + + val fallbackMax = (totalOnchainSats.toDouble() * 0.9).toULong() + return@withContext fallbackMax + } + } + + // Settings suspend fun setShowEmptyState(show: Boolean) { settingsStore.update { it.copy(showEmptyState = show) } @@ -491,35 +517,6 @@ class WalletRepo @Inject constructor( } } - suspend fun deleteActivityById(id: String) = withContext(bgDispatcher) { - runCatching { - coreService.activity.delete(id) - }.onFailure { e -> - Logger.error(msg = "Error deleteActivityById. id:$id", e = e, context = TAG) - } - } - - - suspend fun getOnChainActivityByTxId(txId: String, txType: PaymentType): Result { - return runCatching { - findActivityWithRetry( - paymentHashOrTxId = txId, - txType = txType, - type = ActivityFilter.ONCHAIN - ) ?: return Result.failure(Exception("Activity not found")) - }.onFailure { e -> - Logger.error(msg = "Error getOnChainActivityByTxId. txId:$txId, txType:$txType", e = e, context = TAG) - } - } - - suspend fun updateActivity(id: String, updatedActivity: Activity): Result { - return runCatching { - coreService.activity.update(id, updatedActivity) - }.onFailure { e -> - Logger.error(msg = "Error updateActivity. id:$id, updatedActivity:$updatedActivity", e = e, context = TAG) - } - } - suspend fun attachTagsToActivity( paymentHashOrTxId: String?, type: ActivityFilter, diff --git a/app/src/main/java/to/bitkit/ui/screens/transfer/external/ExternalAmountScreen.kt b/app/src/main/java/to/bitkit/ui/screens/transfer/external/ExternalAmountScreen.kt index ad57aa78a..ed7860d10 100644 --- a/app/src/main/java/to/bitkit/ui/screens/transfer/external/ExternalAmountScreen.kt +++ b/app/src/main/java/to/bitkit/ui/screens/transfer/external/ExternalAmountScreen.kt @@ -37,6 +37,7 @@ import to.bitkit.ui.utils.withAccent import to.bitkit.viewmodels.ExternalNodeViewModel import to.bitkit.viewmodels.ExternalNodeContract import kotlin.math.roundToLong +import kotlin.math.min @Composable fun ExternalAmountScreen( @@ -47,8 +48,6 @@ fun ExternalAmountScreen( ) { val uiState by viewModel.uiState.collectAsState() - LaunchedEffect(Unit) { viewModel.updateMaxAmount() } - Content( amountState = uiState.amount, onAmountChange = { sats -> viewModel.onAmountChange(sats) }, @@ -80,7 +79,7 @@ private fun Content( .fillMaxSize() .imePadding() ) { - val availableAmount = LocalBalances.current.totalOnchainSats + val totalOnchainSats = LocalBalances.current.totalOnchainSats Spacer(modifier = Modifier.height(16.dp)) Display(stringResource(R.string.lightning__external_amount__title).withAccent(accentColor = Colors.Purple)) @@ -107,7 +106,7 @@ private fun Content( color = Colors.White64, ) Spacer(modifier = Modifier.height(8.dp)) - MoneySSB(sats = availableAmount.toLong()) + MoneySSB(sats = amountState.max.toLong()) } Spacer(modifier = Modifier.weight(1f)) UnitButton(color = Colors.Purple) @@ -116,8 +115,9 @@ private fun Content( text = stringResource(R.string.lightning__spending_amount__quarter), color = Colors.Purple, onClick = { - val quarter = (availableAmount.toDouble() / 4.0).roundToLong() - onAmountOverride(quarter) + val quarterOfTotal = (totalOnchainSats.toDouble() / 4.0).roundToLong() + val cappedQuarter = min(quarterOfTotal, amountState.max) + onAmountOverride(cappedQuarter) }, ) // Max Button diff --git a/app/src/main/java/to/bitkit/viewmodels/ExternalNodeViewModel.kt b/app/src/main/java/to/bitkit/viewmodels/ExternalNodeViewModel.kt index e41761f45..4505bb0e9 100644 --- a/app/src/main/java/to/bitkit/viewmodels/ExternalNodeViewModel.kt +++ b/app/src/main/java/to/bitkit/viewmodels/ExternalNodeViewModel.kt @@ -18,6 +18,7 @@ import to.bitkit.ext.WatchResult import to.bitkit.ext.watchUntil import to.bitkit.models.LnPeer import to.bitkit.models.Toast +import to.bitkit.models.formatToModernDisplay import to.bitkit.repositories.WalletRepo import to.bitkit.services.LdkNodeEventBus import to.bitkit.services.LightningService @@ -40,11 +41,17 @@ class ExternalNodeViewModel @Inject constructor( val effects = _effects.asSharedFlow() private fun setEffect(effect: SideEffect) = viewModelScope.launch { _effects.emit(effect) } - fun updateMaxAmount() { - val totalOnchainSats = walletRepo.balanceState.value.totalOnchainSats - val maxAmount = (totalOnchainSats.toDouble() * 0.9).toLong() + init { + observeState() + } - _uiState.update { it.copy(amount = it.amount.copy(max = maxAmount)) } + private fun observeState() { + viewModelScope.launch { + walletRepo.balanceState.collect { + val maxAmount = walletRepo.getMaxSendAmount() + _uiState.update { it.copy(amount = it.amount.copy(max = maxAmount.toLong())) } + } + } } fun onConnectionContinue(peer: LnPeer) { @@ -84,11 +91,28 @@ class ExternalNodeViewModel @Inject constructor( } fun onAmountChange(sats: Long) { + val maxAmount = _uiState.value.amount.max + + if (sats > maxAmount) { + viewModelScope.launch { + ToastEventBus.send( + type = Toast.ToastType.ERROR, + title = context.getString(R.string.lightning__spending_amount__error_max__title), + description = context.getString(R.string.lightning__spending_amount__error_max__description) + .replace("{amount}", maxAmount.formatToModernDisplay()), + ) + } + return + } + _uiState.update { it.copy(amount = it.amount.copy(sats = sats, overrideSats = null)) } } fun onAmountOverride(sats: Long?) { - _uiState.update { it.copy(amount = it.amount.copy(overrideSats = sats)) } + val maxAmount = _uiState.value.amount.max + val cappedSats = sats?.let { minOf(it, maxAmount) } ?: 0L + + _uiState.update { it.copy(amount = it.amount.copy(overrideSats = cappedSats)) } } fun onConfirm() { From 83a655d7dd3bd93b483b6337995c329ebbf7be64 Mon Sep 17 00:00:00 2001 From: Ovi Trif Date: Tue, 15 Jul 2025 16:49:24 +0200 Subject: [PATCH 06/15] feat: improve error logging --- app/src/main/java/to/bitkit/repositories/LightningRepo.kt | 4 ++-- app/src/main/java/to/bitkit/repositories/WalletRepo.kt | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/app/src/main/java/to/bitkit/repositories/LightningRepo.kt b/app/src/main/java/to/bitkit/repositories/LightningRepo.kt index c8f6bc797..0f85e1b30 100644 --- a/app/src/main/java/to/bitkit/repositories/LightningRepo.kt +++ b/app/src/main/java/to/bitkit/repositories/LightningRepo.kt @@ -510,9 +510,9 @@ class LightningRepo @Inject constructor( utxosToSpend = utxosToSpend, ) Result.success(fee) - } catch (e: Throwable) { + } catch (_: Throwable) { val fallbackFee = 1000uL - Logger.error("Estimate fee error, using conservative fallback of $fallbackFee", e, context = TAG) + Logger.warn("Error calculating fee, using fallback of $fallbackFee", context = TAG) Result.success(fallbackFee) } } diff --git a/app/src/main/java/to/bitkit/repositories/WalletRepo.kt b/app/src/main/java/to/bitkit/repositories/WalletRepo.kt index 88fd632bf..2b0951c15 100644 --- a/app/src/main/java/to/bitkit/repositories/WalletRepo.kt +++ b/app/src/main/java/to/bitkit/repositories/WalletRepo.kt @@ -346,7 +346,7 @@ class WalletRepo @Inject constructor( return@withContext maxSendable } catch (_: Throwable) { - Logger.debug("Could not calculate max send amount, using fallback 90% of total", context = TAG) + Logger.debug("Could not calculate max send amount, using as fallback 90% of total", context = TAG) val fallbackMax = (totalOnchainSats.toDouble() * 0.9).toULong() return@withContext fallbackMax From e2fe923436da570d4fe208cb930c41518e184bc0 Mon Sep 17 00:00:00 2001 From: Ovi Trif Date: Tue, 15 Jul 2025 17:13:06 +0200 Subject: [PATCH 07/15] refactor: calculateTotalFee api to support null address --- .../to/bitkit/repositories/LightningRepo.kt | 12 ++++++---- .../java/to/bitkit/repositories/WalletRepo.kt | 5 +---- .../activity/BoostTransactionViewModel.kt | 6 ++--- .../wallets/send/CoinSelectionViewModel.kt | 2 +- .../bitkit/repositories/LightningRepoTest.kt | 3 +++ .../activity/BoostTransactionViewModelTest.kt | 22 ++++++------------- 6 files changed, 22 insertions(+), 28 deletions(-) diff --git a/app/src/main/java/to/bitkit/repositories/LightningRepo.kt b/app/src/main/java/to/bitkit/repositories/LightningRepo.kt index 0f85e1b30..3bbee28cb 100644 --- a/app/src/main/java/to/bitkit/repositories/LightningRepo.kt +++ b/app/src/main/java/to/bitkit/repositories/LightningRepo.kt @@ -23,6 +23,7 @@ import org.lightningdevkit.ldknode.PaymentId import org.lightningdevkit.ldknode.SpendableUtxo import org.lightningdevkit.ldknode.Txid import org.lightningdevkit.ldknode.UserChannelId +import to.bitkit.data.CacheStore import to.bitkit.data.SettingsStore import to.bitkit.data.keychain.Keychain import to.bitkit.di.BgDispatcher @@ -56,6 +57,7 @@ class LightningRepo @Inject constructor( private val blocktankNotificationsService: BlocktankNotificationsService, private val firebaseMessaging: FirebaseMessaging, private val keychain: Keychain, + private val cacheStore: CacheStore, ) { private val _lightningState = MutableStateFlow(LightningState()) val lightningState = _lightningState.asStateFlow() @@ -398,7 +400,7 @@ class LightningRepo @Inject constructor( suspend fun createLnurlInvoice( address: String, - amountSatoshis: ULong + amountSatoshis: ULong, ): Result = executeWhenNodeRunning("getLnUrlInvoice") { val invoice = getLnurlInvoice(address, amountSatoshis) Result.success(invoice) @@ -494,17 +496,19 @@ class LightningRepo @Inject constructor( } suspend fun calculateTotalFee( - address: Address, amountSats: ULong, + address: Address? = null, speed: TransactionSpeed? = null, utxosToSpend: List? = null, ): Result = withContext(bgDispatcher) { return@withContext try { - val transactionSpeed = speed ?: settingsStore.data.map { it.defaultTransactionSpeed }.first() + val transactionSpeed = speed ?: settingsStore.data.first().defaultTransactionSpeed val satsPerVByte = getFeeRateForSpeed(transactionSpeed).getOrThrow().toUInt() + val addressOrDefault = address ?: cacheStore.data.first().onchainAddress + val fee = lightningService.calculateTotalFee( - address = address, + address = addressOrDefault, amountSats = amountSats, satsPerVByte = satsPerVByte, utxosToSpend = utxosToSpend, diff --git a/app/src/main/java/to/bitkit/repositories/WalletRepo.kt b/app/src/main/java/to/bitkit/repositories/WalletRepo.kt index 2b0951c15..b7b300316 100644 --- a/app/src/main/java/to/bitkit/repositories/WalletRepo.kt +++ b/app/src/main/java/to/bitkit/repositories/WalletRepo.kt @@ -337,10 +337,7 @@ class WalletRepo @Inject constructor( try { val minFeeBuffer = 1000uL val amountSats = (totalOnchainSats - minFeeBuffer).coerceAtLeast(0uL) - val fee = lightningRepo.calculateTotalFee( - address = getOnchainAddress(), - amountSats = amountSats, - ).getOrThrow() + val fee = lightningRepo.calculateTotalFee(amountSats).getOrThrow() val maxSendable = (totalOnchainSats - fee).coerceAtLeast(0uL) diff --git a/app/src/main/java/to/bitkit/ui/screens/wallets/activity/BoostTransactionViewModel.kt b/app/src/main/java/to/bitkit/ui/screens/wallets/activity/BoostTransactionViewModel.kt index 82e4b19db..3bf06d404 100644 --- a/app/src/main/java/to/bitkit/ui/screens/wallets/activity/BoostTransactionViewModel.kt +++ b/app/src/main/java/to/bitkit/ui/screens/wallets/activity/BoostTransactionViewModel.kt @@ -27,7 +27,7 @@ import javax.inject.Inject class BoostTransactionViewModel @Inject constructor( private val lightningRepo: LightningRepo, private val walletRepo: WalletRepo, - private val activityRepo: ActivityRepo + private val activityRepo: ActivityRepo, ) : ViewModel() { private val _uiState = MutableStateFlow(BoostTransactionUiState()) @@ -83,7 +83,6 @@ class BoostTransactionViewModel @Inject constructor( // TODO ideally include utxos for a better fee estimate val totalFeeResult = lightningRepo.calculateTotalFee( - address = walletRepo.getOnchainAddress(), amountSats = activityContent.value, speed = TransactionSpeed.Custom(feeRateResult.getOrDefault(0u).toUInt()), ) @@ -177,7 +176,7 @@ class BoostTransactionViewModel @Inject constructor( lightningRepo.accelerateByCpfp( satsPerVByte = _uiState.value.feeRate.toUInt(), originalTxId = activity.v1.txId, - destinationAddress = walletRepo.getOnchainAddress() + destinationAddress = walletRepo.getOnchainAddress(), ).fold( onSuccess = { newTxId -> handleBoostSuccess(newTxId, isRBF = false) @@ -228,7 +227,6 @@ class BoostTransactionViewModel @Inject constructor( viewModelScope.launch { lightningRepo .calculateTotalFee( - address = walletRepo.getOnchainAddress(), amountSats = requireNotNull(activity).v1.value, speed = TransactionSpeed.Custom(newFeeRate.toUInt()), ) diff --git a/app/src/main/java/to/bitkit/ui/screens/wallets/send/CoinSelectionViewModel.kt b/app/src/main/java/to/bitkit/ui/screens/wallets/send/CoinSelectionViewModel.kt index bfa37f49b..bca890465 100644 --- a/app/src/main/java/to/bitkit/ui/screens/wallets/send/CoinSelectionViewModel.kt +++ b/app/src/main/java/to/bitkit/ui/screens/wallets/send/CoinSelectionViewModel.kt @@ -143,8 +143,8 @@ class CoinSelectionViewModel @Inject constructor( ): ULong { return lightningRepo .calculateTotalFee( - address = address, amountSats = amountSats, + address = address, utxosToSpend = utxosToSpend, ) .map { fee -> amountSats + fee } diff --git a/app/src/test/java/to/bitkit/repositories/LightningRepoTest.kt b/app/src/test/java/to/bitkit/repositories/LightningRepoTest.kt index 16f60f7bb..db98d224d 100644 --- a/app/src/test/java/to/bitkit/repositories/LightningRepoTest.kt +++ b/app/src/test/java/to/bitkit/repositories/LightningRepoTest.kt @@ -16,6 +16,7 @@ import org.mockito.kotlin.mock import org.mockito.kotlin.verify import org.mockito.kotlin.whenever import org.mockito.kotlin.wheneverBlocking +import to.bitkit.data.CacheStore import to.bitkit.data.SettingsData import to.bitkit.data.SettingsStore import to.bitkit.data.keychain.Keychain @@ -44,6 +45,7 @@ class LightningRepoTest : BaseUnitTest() { private val blocktankNotificationsService: BlocktankNotificationsService = mock() private val firebaseMessaging: FirebaseMessaging = mock() private val keychain: Keychain = mock() + private val cacheStore: CacheStore = mock() @Before fun setUp() { @@ -57,6 +59,7 @@ class LightningRepoTest : BaseUnitTest() { blocktankNotificationsService = blocktankNotificationsService, firebaseMessaging = firebaseMessaging, keychain = keychain, + cacheStore = cacheStore, ) } diff --git a/app/src/test/java/to/bitkit/ui/screens/wallets/activity/BoostTransactionViewModelTest.kt b/app/src/test/java/to/bitkit/ui/screens/wallets/activity/BoostTransactionViewModelTest.kt index c9427bc44..7b19e0b6d 100644 --- a/app/src/test/java/to/bitkit/ui/screens/wallets/activity/BoostTransactionViewModelTest.kt +++ b/app/src/test/java/to/bitkit/ui/screens/wallets/activity/BoostTransactionViewModelTest.kt @@ -2,7 +2,6 @@ package to.bitkit.ui.screens.wallets.activity import app.cash.turbine.test import com.synonym.bitkitcore.Activity -import com.synonym.bitkitcore.ActivityFilter import com.synonym.bitkitcore.OnchainActivity import com.synonym.bitkitcore.PaymentType import kotlinx.coroutines.test.runTest @@ -35,7 +34,7 @@ class BoostTransactionViewModelSimplifiedTest : BaseUnitTest() { // Test data private val mockTxId = "test_txid_123" private val mockNewTxId = "new_txid_456" - private val mockAddress = "bc1qtest123" + private val mockAddress = "bc1rt1test123" private val testFeeRate = 10UL private val testTotalFee = 1000UL private val testValue = 50000UL @@ -89,7 +88,7 @@ class BoostTransactionViewModelSimplifiedTest : BaseUnitTest() { fun `setupActivity should set loading state initially`() = runTest { whenever(lightningRepo.getFeeRateForSpeed(any())) .thenReturn(Result.success(testFeeRate)) - whenever(lightningRepo.calculateTotalFee(any(), any(), anyOrNull(), anyOrNull())) + whenever(lightningRepo.calculateTotalFee(any(), anyOrNull(), anyOrNull(), anyOrNull())) .thenReturn(Result.success(testTotalFee)) sut.uiState.test { @@ -106,15 +105,13 @@ class BoostTransactionViewModelSimplifiedTest : BaseUnitTest() { fun `setupActivity should call correct repository methods for sent transaction`() = runTest { whenever(lightningRepo.getFeeRateForSpeed(TransactionSpeed.Fast)) .thenReturn(Result.success(testFeeRate)) - whenever(walletRepo.getOnchainAddress()) - .thenReturn(mockAddress) - whenever(lightningRepo.calculateTotalFee(any(), any(), anyOrNull(), anyOrNull())) + whenever(lightningRepo.calculateTotalFee(any(), anyOrNull(), anyOrNull(), anyOrNull())) .thenReturn(Result.success(testTotalFee)) sut.setupActivity(mockActivitySent) verify(lightningRepo).getFeeRateForSpeed(TransactionSpeed.Fast) - verify(lightningRepo).calculateTotalFee(any(), any(), anyOrNull(), anyOrNull()) + verify(lightningRepo).calculateTotalFee(any(), anyOrNull(), anyOrNull(), anyOrNull()) } @Test @@ -155,9 +152,7 @@ class BoostTransactionViewModelSimplifiedTest : BaseUnitTest() { fun `onChangeAmount should emit OnMaxFee when at maximum rate`() = runTest { whenever(lightningRepo.getFeeRateForSpeed(any())) .thenReturn(Result.success(100UL)) // MAX_FEE_RATE - whenever(walletRepo.getOnchainAddress()) - .thenReturn(mockAddress) - whenever(lightningRepo.calculateTotalFee(any(), any(), anyOrNull(), anyOrNull())) + whenever(lightningRepo.calculateTotalFee(any(), anyOrNull(), anyOrNull(), anyOrNull())) .thenReturn(Result.success(testTotalFee)) sut.setupActivity(mockActivitySent) @@ -172,9 +167,7 @@ class BoostTransactionViewModelSimplifiedTest : BaseUnitTest() { fun `onChangeAmount should emit OnMinFee when at minimum rate`() = runTest { whenever(lightningRepo.getFeeRateForSpeed(any())) .thenReturn(Result.success(1UL)) // MIN_FEE_RATE - whenever(walletRepo.getOnchainAddress()) - .thenReturn(mockAddress) - whenever(lightningRepo.calculateTotalFee(any(), any(), anyOrNull(), anyOrNull())) + whenever(lightningRepo.calculateTotalFee(any(), anyOrNull(), anyOrNull(), anyOrNull())) .thenReturn(Result.success(testTotalFee)) sut.setupActivity(mockActivitySent) @@ -204,7 +197,7 @@ class BoostTransactionViewModelSimplifiedTest : BaseUnitTest() { whenever(lightningRepo.calculateCpfpFeeRate(any())) .thenReturn(Result.success(testFeeRate)) - whenever(lightningRepo.calculateTotalFee(any(), any(), anyOrNull(), anyOrNull())) + whenever(lightningRepo.calculateTotalFee(any(), anyOrNull(), anyOrNull(), anyOrNull())) .thenReturn(Result.success(testTotalFee)) whenever(walletRepo.getOnchainAddress()) .thenReturn(mockAddress) @@ -225,7 +218,6 @@ class BoostTransactionViewModelSimplifiedTest : BaseUnitTest() { ) ).thenReturn(Result.success(Activity.Onchain(v1 = newActivity))) - // Fix: Mock updateActivity with 3 parameters (likely id, activity, and a third parameter) whenever(activityRepo.updateActivity(any(), any(), any())) .thenReturn(Result.success(Unit)) From 9d75372802bf6770f65170afb64a0f1566d320d7 Mon Sep 17 00:00:00 2001 From: Ovi Trif Date: Tue, 15 Jul 2025 17:37:02 +0200 Subject: [PATCH 08/15] chore: cleanup confirm screen --- .../screens/transfer/external/ExternalAmountScreen.kt | 2 +- .../screens/transfer/external/ExternalConfirmScreen.kt | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/app/src/main/java/to/bitkit/ui/screens/transfer/external/ExternalAmountScreen.kt b/app/src/main/java/to/bitkit/ui/screens/transfer/external/ExternalAmountScreen.kt index ed7860d10..d3764ae66 100644 --- a/app/src/main/java/to/bitkit/ui/screens/transfer/external/ExternalAmountScreen.kt +++ b/app/src/main/java/to/bitkit/ui/screens/transfer/external/ExternalAmountScreen.kt @@ -106,7 +106,7 @@ private fun Content( color = Colors.White64, ) Spacer(modifier = Modifier.height(8.dp)) - MoneySSB(sats = amountState.max.toLong()) + MoneySSB(sats = amountState.max) } Spacer(modifier = Modifier.weight(1f)) UnitButton(color = Colors.Purple) diff --git a/app/src/main/java/to/bitkit/ui/screens/transfer/external/ExternalConfirmScreen.kt b/app/src/main/java/to/bitkit/ui/screens/transfer/external/ExternalConfirmScreen.kt index 57784c712..01e5316fd 100644 --- a/app/src/main/java/to/bitkit/ui/screens/transfer/external/ExternalConfirmScreen.kt +++ b/app/src/main/java/to/bitkit/ui/screens/transfer/external/ExternalConfirmScreen.kt @@ -64,7 +64,7 @@ fun ExternalConfirmScreen( } } - ExternalConfirmContent( + Content( uiState = uiState, onConfirm = { viewModel.onConfirm() }, onNetworkFeeClick = onNetworkFeeClick, @@ -74,7 +74,7 @@ fun ExternalConfirmScreen( } @Composable -private fun ExternalConfirmContent( +private fun Content( uiState: ExternalNodeContract.UiState, onConfirm: () -> Unit = {}, onNetworkFeeClick: () -> Unit = {}, @@ -173,11 +173,11 @@ private fun ExternalConfirmContent( } } -@Preview(showSystemUi = true, showBackground = true) +@Preview(showSystemUi = true) @Composable -private fun ExternalConfirmScreenPreview() { +private fun Preview() { AppThemeSurface { - ExternalConfirmContent( + Content( uiState = ExternalNodeContract.UiState( amount = ExternalNodeContract.UiState.Amount(sats = 45_500L), ) From bcc43a41bb355c5b71964d2e6975d7cf8010b086 Mon Sep 17 00:00:00 2001 From: Ovi Trif Date: Tue, 15 Jul 2025 17:57:25 +0200 Subject: [PATCH 09/15] feat: calc network fee --- .../external/ExternalConfirmScreen.kt | 3 +- .../viewmodels/ExternalNodeViewModel.kt | 31 ++++++++++++++++--- 2 files changed, 29 insertions(+), 5 deletions(-) diff --git a/app/src/main/java/to/bitkit/ui/screens/transfer/external/ExternalConfirmScreen.kt b/app/src/main/java/to/bitkit/ui/screens/transfer/external/ExternalConfirmScreen.kt index 01e5316fd..b26e1a458 100644 --- a/app/src/main/java/to/bitkit/ui/screens/transfer/external/ExternalConfirmScreen.kt +++ b/app/src/main/java/to/bitkit/ui/screens/transfer/external/ExternalConfirmScreen.kt @@ -93,7 +93,7 @@ private fun Content( .fillMaxSize() .verticalScroll(rememberScrollState()) ) { - val networkFee = 0L // TODO calculate txFee + val networkFee = uiState.networkFee val serviceFee = 0L val totalFee = uiState.amount.sats + networkFee @@ -180,6 +180,7 @@ private fun Preview() { Content( uiState = ExternalNodeContract.UiState( amount = ExternalNodeContract.UiState.Amount(sats = 45_500L), + networkFee = 2_100L, ) ) } diff --git a/app/src/main/java/to/bitkit/viewmodels/ExternalNodeViewModel.kt b/app/src/main/java/to/bitkit/viewmodels/ExternalNodeViewModel.kt index 4505bb0e9..53513abb7 100644 --- a/app/src/main/java/to/bitkit/viewmodels/ExternalNodeViewModel.kt +++ b/app/src/main/java/to/bitkit/viewmodels/ExternalNodeViewModel.kt @@ -19,6 +19,7 @@ import to.bitkit.ext.watchUntil import to.bitkit.models.LnPeer import to.bitkit.models.Toast import to.bitkit.models.formatToModernDisplay +import to.bitkit.repositories.LightningRepo import to.bitkit.repositories.WalletRepo import to.bitkit.services.LdkNodeEventBus import to.bitkit.services.LightningService @@ -33,6 +34,7 @@ class ExternalNodeViewModel @Inject constructor( private val lightningService: LightningService, private val ldkNodeEventBus: LdkNodeEventBus, private val walletRepo: WalletRepo, + private val lightningRepo: LightningRepo, ) : ViewModel() { private val _uiState = MutableStateFlow(UiState()) val uiState = _uiState.asStateFlow() @@ -106,13 +108,33 @@ class ExternalNodeViewModel @Inject constructor( } _uiState.update { it.copy(amount = it.amount.copy(sats = sats, overrideSats = null)) } + + updateNetworkFee() } - fun onAmountOverride(sats: Long?) { - val maxAmount = _uiState.value.amount.max - val cappedSats = sats?.let { minOf(it, maxAmount) } ?: 0L + fun onAmountOverride(sats: Long) { + val max = _uiState.value.amount.max + val nextAmount = minOf(sats, max) + + _uiState.update { it.copy(amount = it.amount.copy(overrideSats = nextAmount)) } + + updateNetworkFee() + } - _uiState.update { it.copy(amount = it.amount.copy(overrideSats = cappedSats)) } + private fun updateNetworkFee() { + viewModelScope.launch { + val amountSats = _uiState.value.amount.sats + if (amountSats <= 0) { + _uiState.update { it.copy(networkFee = 0L) } + return@launch + } + + val fee = lightningRepo.calculateTotalFee( + amountSats = amountSats.toULong(), + ).getOrDefault(1000uL) + + _uiState.update { it.copy(networkFee = fee.toLong()) } + } } fun onConfirm() { @@ -181,6 +203,7 @@ interface ExternalNodeContract { val isLoading: Boolean = false, val peer: LnPeer? = null, val amount: Amount = Amount(), + val networkFee: Long = 0, ) { data class Amount( val sats: Long = 0, From 168143d3d3962e28e9fc52fe8cf3a4cddb9beee2 Mon Sep 17 00:00:00 2001 From: Ovi Trif Date: Tue, 15 Jul 2025 19:20:48 +0200 Subject: [PATCH 10/15] feat: custom fee screen --- .../to/bitkit/repositories/LightningRepo.kt | 2 +- app/src/main/java/to/bitkit/ui/ContentView.kt | 8 +- .../transfer/external/ExternalAmountScreen.kt | 10 +- .../external/ExternalFeeCustomScreen.kt | 169 ++++++++++++++++-- .../CustomFeeSettingsScreen.kt | 1 - .../viewmodels/ExternalNodeViewModel.kt | 33 +++- 6 files changed, 199 insertions(+), 24 deletions(-) diff --git a/app/src/main/java/to/bitkit/repositories/LightningRepo.kt b/app/src/main/java/to/bitkit/repositories/LightningRepo.kt index 3bbee28cb..ec97ea7e5 100644 --- a/app/src/main/java/to/bitkit/repositories/LightningRepo.kt +++ b/app/src/main/java/to/bitkit/repositories/LightningRepo.kt @@ -430,7 +430,7 @@ class LightningRepo @Inject constructor( utxosToSpend: List? = null, ): Result = executeWhenNodeRunning("Send on-chain") { - val transactionSpeed = speed ?: settingsStore.data.map { it.defaultTransactionSpeed }.first() + val transactionSpeed = speed ?: settingsStore.data.first().defaultTransactionSpeed val fees = coreService.blocktank.getFees().getOrThrow() val satsPerVByte = fees.getSatsPerVByteFor(transactionSpeed) diff --git a/app/src/main/java/to/bitkit/ui/ContentView.kt b/app/src/main/java/to/bitkit/ui/ContentView.kt index 68336d992..cefb8fe1f 100644 --- a/app/src/main/java/to/bitkit/ui/ContentView.kt +++ b/app/src/main/java/to/bitkit/ui/ContentView.kt @@ -615,9 +615,13 @@ private fun RootNavHost( ) } composableWithDefaultTransitions { + val parentEntry = remember(it) { navController.getBackStackEntry(Routes.ExternalNav) } + val viewModel = hiltViewModel(parentEntry) + ExternalFeeCustomScreen( - onBackClick = { navController.popBackStack() }, - onCloseClick = { navController.popBackStack(inclusive = true) }, + viewModel = viewModel, + onBack = { navController.popBackStack() }, + onClose = { navController.navigateToHome() }, ) } } diff --git a/app/src/main/java/to/bitkit/ui/screens/transfer/external/ExternalAmountScreen.kt b/app/src/main/java/to/bitkit/ui/screens/transfer/external/ExternalAmountScreen.kt index d3764ae66..dd029175d 100644 --- a/app/src/main/java/to/bitkit/ui/screens/transfer/external/ExternalAmountScreen.kt +++ b/app/src/main/java/to/bitkit/ui/screens/transfer/external/ExternalAmountScreen.kt @@ -10,7 +10,6 @@ import androidx.compose.foundation.layout.imePadding import androidx.compose.foundation.layout.padding import androidx.compose.material3.HorizontalDivider import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.ui.Alignment @@ -34,10 +33,10 @@ import to.bitkit.ui.scaffold.ScreenColumn import to.bitkit.ui.theme.AppThemeSurface import to.bitkit.ui.theme.Colors import to.bitkit.ui.utils.withAccent -import to.bitkit.viewmodels.ExternalNodeViewModel import to.bitkit.viewmodels.ExternalNodeContract -import kotlin.math.roundToLong +import to.bitkit.viewmodels.ExternalNodeViewModel import kotlin.math.min +import kotlin.math.roundToLong @Composable fun ExternalAmountScreen( @@ -52,7 +51,10 @@ fun ExternalAmountScreen( amountState = uiState.amount, onAmountChange = { sats -> viewModel.onAmountChange(sats) }, onAmountOverride = { sats -> viewModel.onAmountOverride(sats) }, - onContinueClick = { onContinue() }, + onContinueClick = { + viewModel.onAmountContinue() + onContinue() + }, onBackClick = onBackClick, onCloseClick = onCloseClick, ) diff --git a/app/src/main/java/to/bitkit/ui/screens/transfer/external/ExternalFeeCustomScreen.kt b/app/src/main/java/to/bitkit/ui/screens/transfer/external/ExternalFeeCustomScreen.kt index e9e2f662d..289f69325 100644 --- a/app/src/main/java/to/bitkit/ui/screens/transfer/external/ExternalFeeCustomScreen.kt +++ b/app/src/main/java/to/bitkit/ui/screens/transfer/external/ExternalFeeCustomScreen.kt @@ -1,40 +1,185 @@ package to.bitkit.ui.screens.transfer.external -import androidx.compose.foundation.layout.Box +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.fadeIn +import androidx.compose.animation.fadeOut +import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.material3.Text +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding import androidx.compose.runtime.Composable -import androidx.compose.ui.Alignment +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import kotlinx.coroutines.launch import to.bitkit.R +import to.bitkit.models.BITCOIN_SYMBOL +import to.bitkit.models.Toast +import to.bitkit.ui.components.BodyM +import to.bitkit.ui.components.Caption13Up +import to.bitkit.ui.components.Display +import to.bitkit.ui.components.FillHeight +import to.bitkit.ui.components.KEY_DELETE +import to.bitkit.ui.components.LargeRow +import to.bitkit.ui.components.NumberPadSimple +import to.bitkit.ui.components.PrimaryButton +import to.bitkit.ui.components.VerticalSpacer +import to.bitkit.ui.currencyViewModel import to.bitkit.ui.scaffold.AppTopBar import to.bitkit.ui.scaffold.CloseNavIcon import to.bitkit.ui.scaffold.ScreenColumn +import to.bitkit.ui.shared.toast.ToastEventBus import to.bitkit.ui.theme.AppThemeSurface +import to.bitkit.ui.theme.Colors +import to.bitkit.ui.utils.withAccent +import to.bitkit.viewmodels.ExternalNodeViewModel @Composable fun ExternalFeeCustomScreen( - onBackClick: () -> Unit = {}, - onCloseClick: () -> Unit = {}, + viewModel: ExternalNodeViewModel, + onBack: () -> Unit, + onClose: () -> Unit, ) { + val uiState by viewModel.uiState.collectAsState() + val currency = currencyViewModel ?: return + val scope = rememberCoroutineScope() + + val context = LocalContext.current + + var input by remember { + mutableStateOf(uiState.customFeeRate?.toString() ?: "") + } + + LaunchedEffect(input) { + val feeRate = input.toUIntOrNull() ?: 0u + viewModel.onCustomFeeRateChange(feeRate) + } + + val totalFeeText = remember(uiState.networkFee) { + if (uiState.networkFee == 0L) { + "" + } else { + currency.convert(uiState.networkFee) + ?.let { + context.getString(R.string.wallet__send_fee_total_fiat) + .replace("{feeSats}", "${uiState.networkFee}") + .replace("{fiatSymbol}", it.symbol) + .replace("{fiatFormatted}", it.formatted) + } ?: context.getString(R.string.wallet__send_fee_total).replace("{feeSats}", "${uiState.networkFee}") + } + } + + Content( + input = input, + totalFeeText = totalFeeText, + onKeyPress = { key -> + when (key) { + KEY_DELETE -> input = if (input.isNotEmpty()) input.dropLast(1) else "" + else -> if (input.length < 3) input = (input + key).trimStart('0') + } + }, + onContinue = { + val feeRate = input.toUIntOrNull() ?: 0u + if (feeRate == 0u) { + scope.launch { + ToastEventBus.send( + type = Toast.ToastType.INFO, + title = context.getString(R.string.wallet__min_possible_fee_rate), + description = context.getString(R.string.wallet__min_possible_fee_rate_msg), + ) + } + return@Content + } + onBack() + }, + onBack = onBack, + onClose = onClose, + ) +} + +@Composable +private fun Content( + input: String, + totalFeeText: String, + onKeyPress: (String) -> Unit = {}, + onContinue: () -> Unit = {}, + onBack: () -> Unit = {}, + onClose: () -> Unit = {}, +) { + val feeRate = input.toUIntOrNull() ?: 0u + val isValid = feeRate != 0u + ScreenColumn { AppTopBar( titleText = stringResource(R.string.lightning__external__nav_title), - onBackClick = onBackClick, - actions = { CloseNavIcon(onCloseClick) }, + onBackClick = onBack, + actions = { CloseNavIcon(onClose) }, ) - Box(modifier = Modifier.fillMaxSize()) { - Text("TODO: ExternalFeeCustomScreen", modifier = Modifier.align(Alignment.Center)) + Column( + modifier = Modifier + .padding(horizontal = 16.dp) + .fillMaxSize() + ) { + VerticalSpacer(16.dp) + Display(stringResource(R.string.lightning__transfer__custom_fee).withAccent(accentColor = Colors.Purple)) + + FillHeight(1f) + + Column { + Caption13Up(stringResource(R.string.common__sat_vbyte), color = Colors.White64) + + VerticalSpacer(16.dp) + LargeRow( + prefix = null, + text = input.ifEmpty { "0" }, + symbol = BITCOIN_SYMBOL, + showSymbol = true, + ) + VerticalSpacer(8.dp) + + Column( + modifier = Modifier.height(22.dp) + ) { + if (isValid) { + AnimatedVisibility(visible = totalFeeText.isNotEmpty(), enter = fadeIn(), exit = fadeOut()) { + BodyM(totalFeeText, color = Colors.White64) + } + } + } + } + + FillHeight(1f) + + NumberPadSimple( + onPress = onKeyPress, + modifier = Modifier.height(350.dp) + ) + + PrimaryButton( + onClick = onContinue, + text = stringResource(R.string.common__continue) + ) + VerticalSpacer(16.dp) } } } -@Preview(showSystemUi = true, showBackground = true) +@Preview(showSystemUi = true) @Composable -private fun ExternalFeeCustomScreenPreview() { +private fun Preview() { AppThemeSurface { - ExternalFeeCustomScreen() + Content( + input = "5", + totalFeeText = "₿ 256 for average transaction ($0.25)" + ) } } diff --git a/app/src/main/java/to/bitkit/ui/settings/transactionSpeed/CustomFeeSettingsScreen.kt b/app/src/main/java/to/bitkit/ui/settings/transactionSpeed/CustomFeeSettingsScreen.kt index d66699565..f6062c921 100644 --- a/app/src/main/java/to/bitkit/ui/settings/transactionSpeed/CustomFeeSettingsScreen.kt +++ b/app/src/main/java/to/bitkit/ui/settings/transactionSpeed/CustomFeeSettingsScreen.kt @@ -4,7 +4,6 @@ import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.navigationBarsPadding import androidx.compose.foundation.layout.padding import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect diff --git a/app/src/main/java/to/bitkit/viewmodels/ExternalNodeViewModel.kt b/app/src/main/java/to/bitkit/viewmodels/ExternalNodeViewModel.kt index 53513abb7..40c86315e 100644 --- a/app/src/main/java/to/bitkit/viewmodels/ExternalNodeViewModel.kt +++ b/app/src/main/java/to/bitkit/viewmodels/ExternalNodeViewModel.kt @@ -9,15 +9,19 @@ import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asSharedFlow import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch import org.lightningdevkit.ldknode.Event import org.lightningdevkit.ldknode.UserChannelId import to.bitkit.R +import to.bitkit.data.SettingsStore +import to.bitkit.env.Env import to.bitkit.ext.WatchResult import to.bitkit.ext.watchUntil import to.bitkit.models.LnPeer import to.bitkit.models.Toast +import to.bitkit.models.TransactionSpeed import to.bitkit.models.formatToModernDisplay import to.bitkit.repositories.LightningRepo import to.bitkit.repositories.WalletRepo @@ -35,6 +39,7 @@ class ExternalNodeViewModel @Inject constructor( private val ldkNodeEventBus: LdkNodeEventBus, private val walletRepo: WalletRepo, private val lightningRepo: LightningRepo, + private val settingsStore: SettingsStore, ) : ViewModel() { private val _uiState = MutableStateFlow(UiState()) val uiState = _uiState.asStateFlow() @@ -108,8 +113,6 @@ class ExternalNodeViewModel @Inject constructor( } _uiState.update { it.copy(amount = it.amount.copy(sats = sats, overrideSats = null)) } - - updateNetworkFee() } fun onAmountOverride(sats: Long) { @@ -117,26 +120,47 @@ class ExternalNodeViewModel @Inject constructor( val nextAmount = minOf(sats, max) _uiState.update { it.copy(amount = it.amount.copy(overrideSats = nextAmount)) } + } - updateNetworkFee() + fun onAmountContinue() { + viewModelScope.launch { + val speed = settingsStore.data.first().defaultTransactionSpeed + val defaultSatsPerVbyte = lightningRepo.getFeeRateForSpeed(speed).getOrThrow().toUInt() + _uiState.update { + it.copy( + customFeeRate = defaultSatsPerVbyte, + ) + } + updateNetworkFee() + } } private fun updateNetworkFee() { viewModelScope.launch { val amountSats = _uiState.value.amount.sats - if (amountSats <= 0) { + val customFeeRate = _uiState.value.customFeeRate + + if (amountSats <= Env.TransactionDefaults.recommendedBaseFee.toLong() || customFeeRate == 0u) { _uiState.update { it.copy(networkFee = 0L) } return@launch } + val speed = customFeeRate?.let { TransactionSpeed.Custom(it) } + val fee = lightningRepo.calculateTotalFee( amountSats = amountSats.toULong(), + speed = speed, ).getOrDefault(1000uL) _uiState.update { it.copy(networkFee = fee.toLong()) } } } + fun onCustomFeeRateChange(feeRate: UInt) { + _uiState.update { it.copy(customFeeRate = feeRate) } + updateNetworkFee() + } + fun onConfirm() { viewModelScope.launch { _uiState.update { it.copy(isLoading = true) } @@ -204,6 +228,7 @@ interface ExternalNodeContract { val peer: LnPeer? = null, val amount: Amount = Amount(), val networkFee: Long = 0, + val customFeeRate: UInt? = null, ) { data class Amount( val sats: Long = 0, From 02292d88714367e5a94ab100879fb1f235bf44fb Mon Sep 17 00:00:00 2001 From: Ovi Trif Date: Tue, 15 Jul 2025 20:00:14 +0200 Subject: [PATCH 11/15] fix: network fee ui glitch --- .../external/ExternalConfirmScreen.kt | 40 +++++++++++++------ .../viewmodels/ExternalNodeViewModel.kt | 1 + 2 files changed, 29 insertions(+), 12 deletions(-) diff --git a/app/src/main/java/to/bitkit/ui/screens/transfer/external/ExternalConfirmScreen.kt b/app/src/main/java/to/bitkit/ui/screens/transfer/external/ExternalConfirmScreen.kt index b26e1a458..139d9bb40 100644 --- a/app/src/main/java/to/bitkit/ui/screens/transfer/external/ExternalConfirmScreen.kt +++ b/app/src/main/java/to/bitkit/ui/screens/transfer/external/ExternalConfirmScreen.kt @@ -1,5 +1,8 @@ package to.bitkit.ui.screens.transfer.external +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.fadeIn +import androidx.compose.animation.fadeOut import androidx.compose.foundation.Image import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column @@ -14,7 +17,6 @@ import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.verticalScroll -import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.Icon import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect @@ -117,18 +119,19 @@ private fun Content( color = Colors.White64, ) Spacer(modifier = Modifier.height(8.dp)) - Row(verticalAlignment = Alignment.CenterVertically) { - MoneySSB(sats = networkFee) - Spacer(modifier = Modifier.width(4.dp)) - Icon( - painterResource(R.drawable.ic_pencil_simple), - contentDescription = null, - tint = Colors.White, - modifier = Modifier.size(16.dp) - ) + + AnimatedVisibility(visible = networkFee > 0L, enter = fadeIn(), exit = fadeOut()) { + Row(verticalAlignment = Alignment.CenterVertically) { + MoneySSB(sats = networkFee) + Spacer(modifier = Modifier.width(4.dp)) + Icon( + painterResource(R.drawable.ic_pencil_simple), + contentDescription = null, + tint = Colors.White, + modifier = Modifier.size(16.dp) + ) + } } - Spacer(modifier = Modifier.weight(1f)) - HorizontalDivider(modifier = Modifier.padding(top = 16.dp)) } FeeInfo( label = stringResource(R.string.lightning__spending_confirm__lsp_fee), @@ -185,3 +188,16 @@ private fun Preview() { ) } } + +@Preview(showSystemUi = true) +@Composable +private fun PreviewFeeLoading() { + AppThemeSurface { + Content( + uiState = ExternalNodeContract.UiState( + amount = ExternalNodeContract.UiState.Amount(sats = 45_500L), + networkFee = 0L, + ) + ) + } +} diff --git a/app/src/main/java/to/bitkit/viewmodels/ExternalNodeViewModel.kt b/app/src/main/java/to/bitkit/viewmodels/ExternalNodeViewModel.kt index 40c86315e..1a4edc5dd 100644 --- a/app/src/main/java/to/bitkit/viewmodels/ExternalNodeViewModel.kt +++ b/app/src/main/java/to/bitkit/viewmodels/ExternalNodeViewModel.kt @@ -129,6 +129,7 @@ class ExternalNodeViewModel @Inject constructor( _uiState.update { it.copy( customFeeRate = defaultSatsPerVbyte, + networkFee = 0L, ) } updateNetworkFee() From 19a613529ff263ac7da20aeed157927d144ef36a Mon Sep 17 00:00:00 2001 From: Ovi Trif Date: Tue, 15 Jul 2025 20:04:48 +0200 Subject: [PATCH 12/15] refactor: move viewmodel to ui package --- app/src/main/java/to/bitkit/ui/ContentView.kt | 2 +- .../ui/screens/transfer/external/ExternalAmountScreen.kt | 2 -- .../ui/screens/transfer/external/ExternalConfirmScreen.kt | 4 +--- .../screens/transfer/external/ExternalConnectionScreen.kt | 4 +--- .../ui/screens/transfer/external/ExternalFeeCustomScreen.kt | 1 - .../screens/transfer/external}/ExternalNodeViewModel.kt | 6 +++--- 6 files changed, 6 insertions(+), 13 deletions(-) rename app/src/main/java/to/bitkit/{viewmodels => ui/screens/transfer/external}/ExternalNodeViewModel.kt (97%) diff --git a/app/src/main/java/to/bitkit/ui/ContentView.kt b/app/src/main/java/to/bitkit/ui/ContentView.kt index cefb8fe1f..eb66d0ede 100644 --- a/app/src/main/java/to/bitkit/ui/ContentView.kt +++ b/app/src/main/java/to/bitkit/ui/ContentView.kt @@ -142,7 +142,7 @@ import to.bitkit.viewmodels.AppViewModel import to.bitkit.viewmodels.BackupsViewModel import to.bitkit.viewmodels.BlocktankViewModel import to.bitkit.viewmodels.CurrencyViewModel -import to.bitkit.viewmodels.ExternalNodeViewModel +import to.bitkit.ui.screens.transfer.external.ExternalNodeViewModel import to.bitkit.viewmodels.MainScreenEffect import to.bitkit.viewmodels.RestoreState import to.bitkit.viewmodels.SendEvent diff --git a/app/src/main/java/to/bitkit/ui/screens/transfer/external/ExternalAmountScreen.kt b/app/src/main/java/to/bitkit/ui/screens/transfer/external/ExternalAmountScreen.kt index dd029175d..b1b009471 100644 --- a/app/src/main/java/to/bitkit/ui/screens/transfer/external/ExternalAmountScreen.kt +++ b/app/src/main/java/to/bitkit/ui/screens/transfer/external/ExternalAmountScreen.kt @@ -33,8 +33,6 @@ import to.bitkit.ui.scaffold.ScreenColumn import to.bitkit.ui.theme.AppThemeSurface import to.bitkit.ui.theme.Colors import to.bitkit.ui.utils.withAccent -import to.bitkit.viewmodels.ExternalNodeContract -import to.bitkit.viewmodels.ExternalNodeViewModel import kotlin.math.min import kotlin.math.roundToLong diff --git a/app/src/main/java/to/bitkit/ui/screens/transfer/external/ExternalConfirmScreen.kt b/app/src/main/java/to/bitkit/ui/screens/transfer/external/ExternalConfirmScreen.kt index 139d9bb40..3a881efc6 100644 --- a/app/src/main/java/to/bitkit/ui/screens/transfer/external/ExternalConfirmScreen.kt +++ b/app/src/main/java/to/bitkit/ui/screens/transfer/external/ExternalConfirmScreen.kt @@ -43,9 +43,7 @@ import to.bitkit.ui.shared.util.clickableAlpha import to.bitkit.ui.theme.AppThemeSurface import to.bitkit.ui.theme.Colors import to.bitkit.ui.utils.withAccent -import to.bitkit.viewmodels.ExternalNodeContract -import to.bitkit.viewmodels.ExternalNodeContract.SideEffect -import to.bitkit.viewmodels.ExternalNodeViewModel +import to.bitkit.ui.screens.transfer.external.ExternalNodeContract.SideEffect @Composable fun ExternalConfirmScreen( diff --git a/app/src/main/java/to/bitkit/ui/screens/transfer/external/ExternalConnectionScreen.kt b/app/src/main/java/to/bitkit/ui/screens/transfer/external/ExternalConnectionScreen.kt index 9682edaf6..7a83722e6 100644 --- a/app/src/main/java/to/bitkit/ui/screens/transfer/external/ExternalConnectionScreen.kt +++ b/app/src/main/java/to/bitkit/ui/screens/transfer/external/ExternalConnectionScreen.kt @@ -50,9 +50,7 @@ import to.bitkit.ui.screens.scanner.SCAN_RESULT_KEY import to.bitkit.ui.theme.AppThemeSurface import to.bitkit.ui.theme.Colors import to.bitkit.ui.utils.withAccent -import to.bitkit.viewmodels.ExternalNodeContract -import to.bitkit.viewmodels.ExternalNodeContract.SideEffect -import to.bitkit.viewmodels.ExternalNodeViewModel +import to.bitkit.ui.screens.transfer.external.ExternalNodeContract.SideEffect @Composable fun ExternalConnectionScreen( diff --git a/app/src/main/java/to/bitkit/ui/screens/transfer/external/ExternalFeeCustomScreen.kt b/app/src/main/java/to/bitkit/ui/screens/transfer/external/ExternalFeeCustomScreen.kt index 289f69325..2ac299577 100644 --- a/app/src/main/java/to/bitkit/ui/screens/transfer/external/ExternalFeeCustomScreen.kt +++ b/app/src/main/java/to/bitkit/ui/screens/transfer/external/ExternalFeeCustomScreen.kt @@ -41,7 +41,6 @@ import to.bitkit.ui.shared.toast.ToastEventBus import to.bitkit.ui.theme.AppThemeSurface import to.bitkit.ui.theme.Colors import to.bitkit.ui.utils.withAccent -import to.bitkit.viewmodels.ExternalNodeViewModel @Composable fun ExternalFeeCustomScreen( diff --git a/app/src/main/java/to/bitkit/viewmodels/ExternalNodeViewModel.kt b/app/src/main/java/to/bitkit/ui/screens/transfer/external/ExternalNodeViewModel.kt similarity index 97% rename from app/src/main/java/to/bitkit/viewmodels/ExternalNodeViewModel.kt rename to app/src/main/java/to/bitkit/ui/screens/transfer/external/ExternalNodeViewModel.kt index 1a4edc5dd..5aab13f74 100644 --- a/app/src/main/java/to/bitkit/viewmodels/ExternalNodeViewModel.kt +++ b/app/src/main/java/to/bitkit/ui/screens/transfer/external/ExternalNodeViewModel.kt @@ -1,4 +1,4 @@ -package to.bitkit.viewmodels +package to.bitkit.ui.screens.transfer.external import android.content.Context import androidx.lifecycle.ViewModel @@ -28,8 +28,8 @@ import to.bitkit.repositories.WalletRepo import to.bitkit.services.LdkNodeEventBus import to.bitkit.services.LightningService import to.bitkit.ui.shared.toast.ToastEventBus -import to.bitkit.viewmodels.ExternalNodeContract.SideEffect -import to.bitkit.viewmodels.ExternalNodeContract.UiState +import to.bitkit.ui.screens.transfer.external.ExternalNodeContract.SideEffect +import to.bitkit.ui.screens.transfer.external.ExternalNodeContract.UiState import javax.inject.Inject @HiltViewModel From db30b27af20d0c382ae2a29b4d12a86470c8a175 Mon Sep 17 00:00:00 2001 From: Ovi Trif Date: Tue, 15 Jul 2025 20:14:01 +0200 Subject: [PATCH 13/15] chore: add todo for using customFeeRate --- .../ui/screens/transfer/external/ExternalNodeViewModel.kt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/to/bitkit/ui/screens/transfer/external/ExternalNodeViewModel.kt b/app/src/main/java/to/bitkit/ui/screens/transfer/external/ExternalNodeViewModel.kt index 5aab13f74..c734e035d 100644 --- a/app/src/main/java/to/bitkit/ui/screens/transfer/external/ExternalNodeViewModel.kt +++ b/app/src/main/java/to/bitkit/ui/screens/transfer/external/ExternalNodeViewModel.kt @@ -166,7 +166,8 @@ class ExternalNodeViewModel @Inject constructor( viewModelScope.launch { _uiState.update { it.copy(isLoading = true) } - val result = lightningService.openChannel( + // TODO: pass customFeeRate to ldk-node when supported + val result = lightningRepo.openChannel( peer = requireNotNull(_uiState.value.peer), channelAmountSats = _uiState.value.amount.sats.toULong(), ) From 7f5a15dba688c2cdd216e717afdb437978987889 Mon Sep 17 00:00:00 2001 From: Ovi Trif Date: Tue, 15 Jul 2025 20:37:56 +0200 Subject: [PATCH 14/15] chore: code cleanup --- app/src/main/java/to/bitkit/repositories/WalletRepo.kt | 2 -- .../ui/screens/transfer/external/ExternalAmountScreen.kt | 4 ++-- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/app/src/main/java/to/bitkit/repositories/WalletRepo.kt b/app/src/main/java/to/bitkit/repositories/WalletRepo.kt index b7b300316..fc42ab961 100644 --- a/app/src/main/java/to/bitkit/repositories/WalletRepo.kt +++ b/app/src/main/java/to/bitkit/repositories/WalletRepo.kt @@ -338,13 +338,11 @@ class WalletRepo @Inject constructor( val minFeeBuffer = 1000uL val amountSats = (totalOnchainSats - minFeeBuffer).coerceAtLeast(0uL) val fee = lightningRepo.calculateTotalFee(amountSats).getOrThrow() - val maxSendable = (totalOnchainSats - fee).coerceAtLeast(0uL) return@withContext maxSendable } catch (_: Throwable) { Logger.debug("Could not calculate max send amount, using as fallback 90% of total", context = TAG) - val fallbackMax = (totalOnchainSats.toDouble() * 0.9).toULong() return@withContext fallbackMax } diff --git a/app/src/main/java/to/bitkit/ui/screens/transfer/external/ExternalAmountScreen.kt b/app/src/main/java/to/bitkit/ui/screens/transfer/external/ExternalAmountScreen.kt index b1b009471..664be8b01 100644 --- a/app/src/main/java/to/bitkit/ui/screens/transfer/external/ExternalAmountScreen.kt +++ b/app/src/main/java/to/bitkit/ui/screens/transfer/external/ExternalAmountScreen.kt @@ -10,13 +10,13 @@ import androidx.compose.foundation.layout.imePadding import androidx.compose.foundation.layout.padding import androidx.compose.material3.HorizontalDivider import androidx.compose.runtime.Composable -import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp +import androidx.lifecycle.compose.collectAsStateWithLifecycle import to.bitkit.R import to.bitkit.ui.LocalBalances import to.bitkit.ui.LocalCurrencies @@ -43,7 +43,7 @@ fun ExternalAmountScreen( onBackClick: () -> Unit, onCloseClick: () -> Unit, ) { - val uiState by viewModel.uiState.collectAsState() + val uiState by viewModel.uiState.collectAsStateWithLifecycle() Content( amountState = uiState.amount, From 18130c7de6a6b2d695836c19acd106ea6a1e16e1 Mon Sep 17 00:00:00 2001 From: Ovi Trif Date: Tue, 15 Jul 2025 20:39:19 +0200 Subject: [PATCH 15/15] chore: cleanup network fee code --- .../transfer/external/ExternalNodeViewModel.kt | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/app/src/main/java/to/bitkit/ui/screens/transfer/external/ExternalNodeViewModel.kt b/app/src/main/java/to/bitkit/ui/screens/transfer/external/ExternalNodeViewModel.kt index c734e035d..dec4085b0 100644 --- a/app/src/main/java/to/bitkit/ui/screens/transfer/external/ExternalNodeViewModel.kt +++ b/app/src/main/java/to/bitkit/ui/screens/transfer/external/ExternalNodeViewModel.kt @@ -16,7 +16,6 @@ import org.lightningdevkit.ldknode.Event import org.lightningdevkit.ldknode.UserChannelId import to.bitkit.R import to.bitkit.data.SettingsStore -import to.bitkit.env.Env import to.bitkit.ext.WatchResult import to.bitkit.ext.watchUntil import to.bitkit.models.LnPeer @@ -27,9 +26,9 @@ import to.bitkit.repositories.LightningRepo import to.bitkit.repositories.WalletRepo import to.bitkit.services.LdkNodeEventBus import to.bitkit.services.LightningService -import to.bitkit.ui.shared.toast.ToastEventBus import to.bitkit.ui.screens.transfer.external.ExternalNodeContract.SideEffect import to.bitkit.ui.screens.transfer.external.ExternalNodeContract.UiState +import to.bitkit.ui.shared.toast.ToastEventBus import javax.inject.Inject @HiltViewModel @@ -136,12 +135,17 @@ class ExternalNodeViewModel @Inject constructor( } } + fun onCustomFeeRateChange(feeRate: UInt) { + _uiState.update { it.copy(customFeeRate = feeRate) } + updateNetworkFee() + } + private fun updateNetworkFee() { viewModelScope.launch { val amountSats = _uiState.value.amount.sats val customFeeRate = _uiState.value.customFeeRate - if (amountSats <= Env.TransactionDefaults.recommendedBaseFee.toLong() || customFeeRate == 0u) { + if (amountSats <= 0 || customFeeRate == 0u) { _uiState.update { it.copy(networkFee = 0L) } return@launch } @@ -157,11 +161,6 @@ class ExternalNodeViewModel @Inject constructor( } } - fun onCustomFeeRateChange(feeRate: UInt) { - _uiState.update { it.copy(customFeeRate = feeRate) } - updateNetworkFee() - } - fun onConfirm() { viewModelScope.launch { _uiState.update { it.copy(isLoading = true) }