From d1f20861f4a709449cb103db4e708bf5d944ff66 Mon Sep 17 00:00:00 2001 From: Ovi Trif Date: Tue, 5 Aug 2025 15:27:53 +0200 Subject: [PATCH 01/36] refactor: remove color from pencil icon --- .../bitkit/ui/screens/transfer/FundingAdvancedScreen.kt | 5 ++--- .../ui/screens/transfer/external/ExternalConfirmScreen.kt | 3 +-- .../bitkit/ui/screens/wallets/receive/ReceiveQrScreen.kt | 2 +- .../bitkit/ui/screens/wallets/send/SendConfirmScreen.kt | 3 +-- .../bitkit/ui/screens/wallets/send/SendRecipientScreen.kt | 2 +- .../java/to/bitkit/ui/sheets/BoostTransactionSheet.kt | 3 +-- .../res/drawable/{ic_pencil_simple.xml => ic_pencil.xml} | 6 +++--- .../drawable/{ic_pencil_purple.xml => ic_pencil_full.xml} | 8 ++++---- 8 files changed, 14 insertions(+), 18 deletions(-) rename app/src/main/res/drawable/{ic_pencil_simple.xml => ic_pencil.xml} (92%) rename app/src/main/res/drawable/{ic_pencil_purple.xml => ic_pencil_full.xml} (91%) diff --git a/app/src/main/java/to/bitkit/ui/screens/transfer/FundingAdvancedScreen.kt b/app/src/main/java/to/bitkit/ui/screens/transfer/FundingAdvancedScreen.kt index 2f8b254f4..a56e8d8cd 100644 --- a/app/src/main/java/to/bitkit/ui/screens/transfer/FundingAdvancedScreen.kt +++ b/app/src/main/java/to/bitkit/ui/screens/transfer/FundingAdvancedScreen.kt @@ -10,7 +10,6 @@ import androidx.compose.foundation.layout.size import androidx.compose.material3.Icon import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.Color import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview @@ -69,9 +68,9 @@ fun FundingAdvancedScreen( label = stringResource(R.string.lightning__funding_advanced__button2), icon = { Icon( - painter = painterResource(R.drawable.ic_pencil_purple), + painter = painterResource(R.drawable.ic_pencil_full), contentDescription = null, - tint = Color.Unspecified, + tint = Colors.Purple, modifier = Modifier.size(28.dp), ) }, 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 3a881efc6..34dd198ed 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 @@ -123,9 +123,8 @@ private fun Content( MoneySSB(sats = networkFee) Spacer(modifier = Modifier.width(4.dp)) Icon( - painterResource(R.drawable.ic_pencil_simple), + painterResource(R.drawable.ic_pencil), contentDescription = null, - tint = Colors.White, modifier = Modifier.size(16.dp) ) } diff --git a/app/src/main/java/to/bitkit/ui/screens/wallets/receive/ReceiveQrScreen.kt b/app/src/main/java/to/bitkit/ui/screens/wallets/receive/ReceiveQrScreen.kt index 2b19f7591..fb4165f93 100644 --- a/app/src/main/java/to/bitkit/ui/screens/wallets/receive/ReceiveQrScreen.kt +++ b/app/src/main/java/to/bitkit/ui/screens/wallets/receive/ReceiveQrScreen.kt @@ -266,7 +266,7 @@ private fun ReceiveQrSlide( color = Colors.White10, icon = { Icon( - painter = painterResource(R.drawable.ic_pencil_simple), + painter = painterResource(R.drawable.ic_pencil), contentDescription = null, tint = Colors.Brand, modifier = Modifier.size(18.dp) diff --git a/app/src/main/java/to/bitkit/ui/screens/wallets/send/SendConfirmScreen.kt b/app/src/main/java/to/bitkit/ui/screens/wallets/send/SendConfirmScreen.kt index aeaf2fb77..51d53e76c 100644 --- a/app/src/main/java/to/bitkit/ui/screens/wallets/send/SendConfirmScreen.kt +++ b/app/src/main/java/to/bitkit/ui/screens/wallets/send/SendConfirmScreen.kt @@ -338,9 +338,8 @@ private fun OnChainDescription( ) BodySSB(text = "Normal (₿ 210)") // TODO GET FROM STATE Icon( - painterResource(R.drawable.ic_pencil_simple), + painterResource(R.drawable.ic_pencil), contentDescription = null, - tint = Colors.White, modifier = Modifier.size(16.dp) ) } diff --git a/app/src/main/java/to/bitkit/ui/screens/wallets/send/SendRecipientScreen.kt b/app/src/main/java/to/bitkit/ui/screens/wallets/send/SendRecipientScreen.kt index 1e082875b..205239773 100644 --- a/app/src/main/java/to/bitkit/ui/screens/wallets/send/SendRecipientScreen.kt +++ b/app/src/main/java/to/bitkit/ui/screens/wallets/send/SendRecipientScreen.kt @@ -88,7 +88,7 @@ fun SendRecipientScreen( label = stringResource(R.string.wallet__recipient_manual), icon = { Icon( - painter = painterResource(R.drawable.ic_pencil_simple), + painter = painterResource(R.drawable.ic_pencil), contentDescription = null, tint = Colors.Brand, modifier = Modifier.size(28.dp), diff --git a/app/src/main/java/to/bitkit/ui/sheets/BoostTransactionSheet.kt b/app/src/main/java/to/bitkit/ui/sheets/BoostTransactionSheet.kt index 781a42d33..e17b4f9f1 100644 --- a/app/src/main/java/to/bitkit/ui/sheets/BoostTransactionSheet.kt +++ b/app/src/main/java/to/bitkit/ui/sheets/BoostTransactionSheet.kt @@ -255,8 +255,7 @@ private fun DefaultModeContent( ) Icon( - painter = painterResource(R.drawable.ic_pencil_simple), - tint = Colors.White, + painter = painterResource(R.drawable.ic_pencil), contentDescription = stringResource(R.string.common__edit), modifier = Modifier .size(16.dp) diff --git a/app/src/main/res/drawable/ic_pencil_simple.xml b/app/src/main/res/drawable/ic_pencil.xml similarity index 92% rename from app/src/main/res/drawable/ic_pencil_simple.xml rename to app/src/main/res/drawable/ic_pencil.xml index 6200edc97..6957ccfdd 100644 --- a/app/src/main/res/drawable/ic_pencil_simple.xml +++ b/app/src/main/res/drawable/ic_pencil.xml @@ -6,14 +6,14 @@ diff --git a/app/src/main/res/drawable/ic_pencil_purple.xml b/app/src/main/res/drawable/ic_pencil_full.xml similarity index 91% rename from app/src/main/res/drawable/ic_pencil_purple.xml rename to app/src/main/res/drawable/ic_pencil_full.xml index 67cac3528..a9fa94b10 100644 --- a/app/src/main/res/drawable/ic_pencil_purple.xml +++ b/app/src/main/res/drawable/ic_pencil_full.xml @@ -6,18 +6,18 @@ From 4bae300739120dc71311e36db93aaf9fc067a284 Mon Sep 17 00:00:00 2001 From: Ovi Trif Date: Tue, 5 Aug 2025 15:40:29 +0200 Subject: [PATCH 02/36] fix: small money text color --- app/src/main/java/to/bitkit/ui/components/Money.kt | 6 +++++- .../main/java/to/bitkit/ui/components/NumberPadTextField.kt | 2 +- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/to/bitkit/ui/components/Money.kt b/app/src/main/java/to/bitkit/ui/components/Money.kt index 5853d989f..9c33c0a6c 100644 --- a/app/src/main/java/to/bitkit/ui/components/Money.kt +++ b/app/src/main/java/to/bitkit/ui/components/Money.kt @@ -33,9 +33,13 @@ fun MoneyDisplay( fun MoneySSB( sats: Long, unit: PrimaryDisplay = LocalCurrencies.current.primaryDisplay, + color: Color = MaterialTheme.colorScheme.primary, ) { rememberMoneyText(sats = sats, unit = unit)?.let { text -> - BodySSB(text = text.withAccent(accentColor = Colors.White64)) + BodySSB( + text = text.withAccent(accentColor = Colors.White64), + color = color, + ) } } diff --git a/app/src/main/java/to/bitkit/ui/components/NumberPadTextField.kt b/app/src/main/java/to/bitkit/ui/components/NumberPadTextField.kt index d8391d2f7..1d31c09bb 100644 --- a/app/src/main/java/to/bitkit/ui/components/NumberPadTextField.kt +++ b/app/src/main/java/to/bitkit/ui/components/NumberPadTextField.kt @@ -220,7 +220,7 @@ fun MoneyAmount( horizontalAlignment = Alignment.Start ) { - MoneySSB(sats = satoshis, unit = unit.not()) + MoneySSB(sats = satoshis, unit = unit.not(), color = Colors.White64) Spacer(modifier = Modifier.height(12.dp)) From f94c54a7f320164b71a1107f5850462e1a12dcb9 Mon Sep 17 00:00:00 2001 From: Ovi Trif Date: Tue, 5 Aug 2025 15:44:45 +0200 Subject: [PATCH 03/36] feat: send speed and fee screens scaffold & nav --- .../screens/wallets/send/SendConfirmScreen.kt | 9 +- .../wallets/send/SendFeeCustomScreen.kt | 85 +++++++++++++++++++ .../screens/wallets/send/SendFeeRateScreen.kt | 83 ++++++++++++++++++ .../java/to/bitkit/ui/sheets/SendSheet.kt | 25 ++++++ .../java/to/bitkit/viewmodels/AppViewModel.kt | 3 +- 5 files changed, 199 insertions(+), 6 deletions(-) create mode 100644 app/src/main/java/to/bitkit/ui/screens/wallets/send/SendFeeCustomScreen.kt create mode 100644 app/src/main/java/to/bitkit/ui/screens/wallets/send/SendFeeRateScreen.kt diff --git a/app/src/main/java/to/bitkit/ui/screens/wallets/send/SendConfirmScreen.kt b/app/src/main/java/to/bitkit/ui/screens/wallets/send/SendConfirmScreen.kt index 51d53e76c..c714b117b 100644 --- a/app/src/main/java/to/bitkit/ui/screens/wallets/send/SendConfirmScreen.kt +++ b/app/src/main/java/to/bitkit/ui/screens/wallets/send/SendConfirmScreen.kt @@ -1,6 +1,5 @@ package to.bitkit.ui.screens.wallets.send -import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column @@ -321,9 +320,9 @@ private fun OnChainDescription( modifier = Modifier .fillMaxHeight() .weight(1f) - .clickable { onEvent(SendEvent.SpeedAndFee) } - .padding(top = 16.dp) + .clickableAlpha { onEvent(SendEvent.SpeedAndFee) } ) { + VerticalSpacer(16.dp) Caption13Up(text = stringResource(R.string.wallet__send_fee_and_speed), color = Colors.White64) Spacer(modifier = Modifier.height(8.dp)) Row( @@ -351,8 +350,8 @@ private fun OnChainDescription( .fillMaxHeight() .weight(1f) .clickableAlpha { onEvent(SendEvent.SpeedAndFee) } - .padding(top = 16.dp) ) { + VerticalSpacer(16.dp) Caption13Up(text = stringResource(R.string.wallet__send_confirming_in), color = Colors.White64) Spacer(modifier = Modifier.height(8.dp)) Row( @@ -405,8 +404,8 @@ private fun LightningDescription( modifier = Modifier .fillMaxHeight() .weight(1f) - .padding(top = 16.dp) ) { + VerticalSpacer(16.dp) Caption13Up(text = stringResource(R.string.wallet__send_fee_and_speed), color = Colors.White64) Spacer(modifier = Modifier.height(8.dp)) Row( diff --git a/app/src/main/java/to/bitkit/ui/screens/wallets/send/SendFeeCustomScreen.kt b/app/src/main/java/to/bitkit/ui/screens/wallets/send/SendFeeCustomScreen.kt new file mode 100644 index 000000000..f3c827c42 --- /dev/null +++ b/app/src/main/java/to/bitkit/ui/screens/wallets/send/SendFeeCustomScreen.kt @@ -0,0 +1,85 @@ +package to.bitkit.ui.screens.wallets.send + +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.navigationBarsPadding +import androidx.compose.foundation.layout.padding +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.testTag +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import to.bitkit.R +import to.bitkit.ui.components.BottomSheetPreview +import to.bitkit.ui.components.Display +import to.bitkit.ui.components.FillHeight +import to.bitkit.ui.components.PrimaryButton +import to.bitkit.ui.components.VerticalSpacer +import to.bitkit.ui.components.settings.SectionHeader +import to.bitkit.ui.scaffold.SheetTopBar +import to.bitkit.ui.shared.modifiers.sheetHeight +import to.bitkit.ui.shared.util.gradientBackground +import to.bitkit.ui.theme.AppThemeSurface +import to.bitkit.viewmodels.SendUiState + +@Composable +fun SendFeeCustomScreen( + uiState: SendUiState, + onBack: () -> Unit, + onContinue: () -> Unit, +) { + Content( + uiState = uiState, + onBack = onBack, + onContinue = onContinue, + ) +} + +@Composable +private fun Content( + uiState: SendUiState, + modifier: Modifier = Modifier, + onBack: () -> Unit = {}, + onContinue: () -> Unit = {}, +) { + Column( + modifier = modifier + .fillMaxSize() + .gradientBackground() + .navigationBarsPadding() + .testTag("fee_screen") + ) { + SheetTopBar(stringResource(R.string.wallet__send_fee_custom), onBack = onBack) + Column( + modifier = Modifier.padding(horizontal = 16.dp) + ) { + SectionHeader(stringResource(R.string.wallet__send_fee_and_speed)) + Display("TODO") + + FillHeight(min = 16.dp) + + PrimaryButton( + text = stringResource(R.string.common__continue), + onClick = onContinue, + modifier = Modifier.testTag("continue_btn") + ) + VerticalSpacer(16.dp) + } + } +} + +@Preview(showSystemUi = true) +@Composable +private fun Preview() { + AppThemeSurface { + BottomSheetPreview { + Content( + uiState = SendUiState(), + modifier = Modifier.sheetHeight(), + ) + } + } +} + +// TODO nav diff --git a/app/src/main/java/to/bitkit/ui/screens/wallets/send/SendFeeRateScreen.kt b/app/src/main/java/to/bitkit/ui/screens/wallets/send/SendFeeRateScreen.kt new file mode 100644 index 000000000..a526324c1 --- /dev/null +++ b/app/src/main/java/to/bitkit/ui/screens/wallets/send/SendFeeRateScreen.kt @@ -0,0 +1,83 @@ +package to.bitkit.ui.screens.wallets.send + +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.navigationBarsPadding +import androidx.compose.foundation.layout.padding +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.testTag +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import to.bitkit.R +import to.bitkit.ui.components.BottomSheetPreview +import to.bitkit.ui.components.Display +import to.bitkit.ui.components.FillHeight +import to.bitkit.ui.components.PrimaryButton +import to.bitkit.ui.components.VerticalSpacer +import to.bitkit.ui.components.settings.SectionHeader +import to.bitkit.ui.scaffold.SheetTopBar +import to.bitkit.ui.shared.modifiers.sheetHeight +import to.bitkit.ui.shared.util.gradientBackground +import to.bitkit.ui.theme.AppThemeSurface +import to.bitkit.viewmodels.SendUiState + +@Composable +fun SendFeeRateScreen( + uiState: SendUiState, + onBack: () -> Unit, + onContinue: () -> Unit, +) { + Content( + uiState = uiState, + onBack = onBack, + onContinue = onContinue, + ) +} + +@Composable +private fun Content( + uiState: SendUiState, + modifier: Modifier = Modifier, + onBack: () -> Unit = {}, + onContinue: () -> Unit = {}, +) { + Column( + modifier = modifier + .fillMaxSize() + .gradientBackground() + .navigationBarsPadding() + .testTag("speed_screen") + ) { + SheetTopBar(stringResource(R.string.wallet__send_fee_speed), onBack = onBack) + Column( + modifier = Modifier.padding(horizontal = 16.dp) + ) { + SectionHeader(stringResource(R.string.wallet__send_fee_and_speed)) + Display("TODO") + + FillHeight(min = 16.dp) + + PrimaryButton( + text = stringResource(R.string.common__continue), + onClick = onContinue, + modifier = Modifier.testTag("continue_btn") + ) + VerticalSpacer(16.dp) + } + } +} + +@Preview(showSystemUi = true) +@Composable +private fun Preview() { + AppThemeSurface { + BottomSheetPreview { + Content( + uiState = SendUiState(), + modifier = Modifier.sheetHeight(), + ) + } + } +} diff --git a/app/src/main/java/to/bitkit/ui/sheets/SendSheet.kt b/app/src/main/java/to/bitkit/ui/sheets/SendSheet.kt index 4f4e5eec3..b56c531bd 100644 --- a/app/src/main/java/to/bitkit/ui/sheets/SendSheet.kt +++ b/app/src/main/java/to/bitkit/ui/sheets/SendSheet.kt @@ -21,6 +21,8 @@ import to.bitkit.ui.screens.wallets.send.SendAmountScreen import to.bitkit.ui.screens.wallets.send.SendCoinSelectionScreen import to.bitkit.ui.screens.wallets.send.SendConfirmScreen import to.bitkit.ui.screens.wallets.send.SendErrorScreen +import to.bitkit.ui.screens.wallets.send.SendFeeCustomScreen +import to.bitkit.ui.screens.wallets.send.SendFeeRateScreen import to.bitkit.ui.screens.wallets.send.SendPinCheckScreen import to.bitkit.ui.screens.wallets.send.SendQuickPayScreen import to.bitkit.ui.screens.wallets.send.SendRecipientScreen @@ -68,6 +70,7 @@ fun SendSheet( is SendEffect.NavigateToQuickPay -> navController.navigate(SendRoute.QuickPay) is SendEffect.NavigateToWithdrawConfirm -> navController.navigate(SendRoute.WithdrawConfirm) is SendEffect.NavigateToWithdrawError -> navController.navigate(SendRoute.WithdrawError) + is SendEffect.NavigateToFee -> navController.navigate(SendRoute.FeeRate) } } } @@ -117,6 +120,22 @@ fun SendSheet( onContinue = { utxos -> appViewModel.setSendEvent(SendEvent.CoinSelectionContinue(utxos)) }, ) } + composableWithDefaultTransitions { + val sendUiState by appViewModel.sendUiState.collectAsStateWithLifecycle() + SendFeeRateScreen( + uiState = sendUiState, + onBack = { navController.popBackStack() }, + onContinue = {}, // TODO + ) + } + composableWithDefaultTransitions { + val sendUiState by appViewModel.sendUiState.collectAsStateWithLifecycle() + SendFeeCustomScreen( + uiState = sendUiState, + onBack = { navController.popBackStack() }, + onContinue = {}, // TODO + ) + } composableWithDefaultTransitions { val uiState by appViewModel.sendUiState.collectAsStateWithLifecycle() SendConfirmScreen( @@ -239,6 +258,12 @@ sealed interface SendRoute { @Serializable data object QuickPay : SendRoute + @Serializable + data object FeeRate : SendRoute + + @Serializable + data object FeeCustom : SendRoute + @Serializable data object Confirm : SendRoute diff --git a/app/src/main/java/to/bitkit/viewmodels/AppViewModel.kt b/app/src/main/java/to/bitkit/viewmodels/AppViewModel.kt index edc304aaf..1877a1fd5 100644 --- a/app/src/main/java/to/bitkit/viewmodels/AppViewModel.kt +++ b/app/src/main/java/to/bitkit/viewmodels/AppViewModel.kt @@ -290,7 +290,7 @@ class AppViewModel @Inject constructor( is SendEvent.CommentChange -> onCommentChange(it.value) - SendEvent.SpeedAndFee -> toast(Exception("Coming soon: Speed and Fee")) + SendEvent.SpeedAndFee -> setSendEffect(SendEffect.NavigateToFee) SendEvent.SwipeToPay -> onSwipeToPay() SendEvent.ConfirmAmountWarning -> onConfirmAmountWarning() SendEvent.DismissAmountWarning -> onDismissAmountWarning() @@ -1274,6 +1274,7 @@ sealed class SendEffect { data object NavigateToWithdrawError : SendEffect() data object NavigateToCoinSelection : SendEffect() data object NavigateToQuickPay : SendEffect() + data object NavigateToFee : SendEffect() data class PaymentSuccess(val sheet: NewTransactionSheetDetails? = null) : SendEffect() } From 6eaf21ce6c9cea951873e2910cf18efad33d3fda Mon Sep 17 00:00:00 2001 From: Ovi Trif Date: Tue, 5 Aug 2025 17:19:18 +0200 Subject: [PATCH 04/36] feat: fee items ui --- .../java/to/bitkit/models/TransactionSpeed.kt | 36 +++++ .../java/to/bitkit/ui/components/Money.kt | 22 +++- .../main/java/to/bitkit/ui/components/Text.kt | 19 +++ .../screens/wallets/send/SendFeeRateScreen.kt | 123 ++++++++++++++++-- 4 files changed, 188 insertions(+), 12 deletions(-) diff --git a/app/src/main/java/to/bitkit/models/TransactionSpeed.kt b/app/src/main/java/to/bitkit/models/TransactionSpeed.kt index e45783463..bc97817b2 100644 --- a/app/src/main/java/to/bitkit/models/TransactionSpeed.kt +++ b/app/src/main/java/to/bitkit/models/TransactionSpeed.kt @@ -1,6 +1,8 @@ package to.bitkit.models import androidx.compose.runtime.Composable +import androidx.compose.ui.graphics.painter.Painter +import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import kotlinx.serialization.KSerializer import kotlinx.serialization.Serializable @@ -64,3 +66,37 @@ fun TransactionSpeed.transactionSpeedUiText(): String { is TransactionSpeed.Custom -> stringResource(R.string.settings__fee__custom__value) } } + +enum class FeeRate { + FAST, NORMAL, SLOW, CUSTOM; + + @Composable + fun uiTitle(): String { + return when (this) { + FAST -> stringResource(R.string.fee__fast__title) + NORMAL -> stringResource(R.string.fee__normal__title) + SLOW -> stringResource(R.string.fee__slow__title) + CUSTOM -> stringResource(R.string.fee__custom__title) + } + } + + @Composable + fun uiDescription(): String { + return when (this) { + FAST -> stringResource(R.string.fee__fast__description) + NORMAL -> stringResource(R.string.fee__normal__description) + SLOW -> stringResource(R.string.fee__slow__description) + CUSTOM -> stringResource(R.string.fee__custom__description) + } + } + + @Composable + fun uiIcon(): Painter { + return when (this) { + FAST -> painterResource(R.drawable.ic_speed_fast) + NORMAL -> painterResource(R.drawable.ic_speed_normal) + SLOW -> painterResource(R.drawable.ic_speed_slow) + CUSTOM -> painterResource(R.drawable.ic_settings) + } + } +} diff --git a/app/src/main/java/to/bitkit/ui/components/Money.kt b/app/src/main/java/to/bitkit/ui/components/Money.kt index 9c33c0a6c..52ab194b5 100644 --- a/app/src/main/java/to/bitkit/ui/components/Money.kt +++ b/app/src/main/java/to/bitkit/ui/components/Money.kt @@ -32,13 +32,33 @@ fun MoneyDisplay( @Composable fun MoneySSB( sats: Long, + modifier: Modifier = Modifier, unit: PrimaryDisplay = LocalCurrencies.current.primaryDisplay, color: Color = MaterialTheme.colorScheme.primary, + accent: Color = Colors.White64, ) { rememberMoneyText(sats = sats, unit = unit)?.let { text -> BodySSB( - text = text.withAccent(accentColor = Colors.White64), + text = text.withAccent(accentColor = accent), + color = color, + modifier = modifier, + ) + } +} + +@Composable +fun MoneyMSB( + sats: Long, + modifier: Modifier = Modifier, + unit: PrimaryDisplay = LocalCurrencies.current.primaryDisplay, + color: Color = MaterialTheme.colorScheme.primary, + accent: Color = Colors.White64, +) { + rememberMoneyText(sats = sats, unit = unit)?.let { text -> + BodyMSB( + text = text.withAccent(accentColor = accent), color = color, + modifier = modifier, ) } } diff --git a/app/src/main/java/to/bitkit/ui/components/Text.kt b/app/src/main/java/to/bitkit/ui/components/Text.kt index 9c4198149..3c31d63a2 100644 --- a/app/src/main/java/to/bitkit/ui/components/Text.kt +++ b/app/src/main/java/to/bitkit/ui/components/Text.kt @@ -196,6 +196,25 @@ fun BodyMSB( maxLines: Int = Int.MAX_VALUE, overflow: TextOverflow = TextOverflow.Clip, textAlign: TextAlign = TextAlign.Start, +) { + BodyMSB( + text = AnnotatedString(text), + color = color, + maxLines = maxLines, + overflow = overflow, + modifier = modifier, + textAlign = textAlign, + ) +} + +@Composable +fun BodyMSB( + text: AnnotatedString, + modifier: Modifier = Modifier, + color: Color = MaterialTheme.colorScheme.primary, + maxLines: Int = Int.MAX_VALUE, + overflow: TextOverflow = TextOverflow.Clip, + textAlign: TextAlign = TextAlign.Start, ) { Text( text = text, diff --git a/app/src/main/java/to/bitkit/ui/screens/wallets/send/SendFeeRateScreen.kt b/app/src/main/java/to/bitkit/ui/screens/wallets/send/SendFeeRateScreen.kt index a526324c1..349497fca 100644 --- a/app/src/main/java/to/bitkit/ui/screens/wallets/send/SendFeeRateScreen.kt +++ b/app/src/main/java/to/bitkit/ui/screens/wallets/send/SendFeeRateScreen.kt @@ -1,26 +1,45 @@ package to.bitkit.ui.screens.wallets.send +import androidx.compose.foundation.background import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row 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.foundation.layout.size +import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.testTag import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import to.bitkit.R +import to.bitkit.models.FeeRate +import to.bitkit.models.PrimaryDisplay +import to.bitkit.ui.LocalCurrencies +import to.bitkit.ui.components.BodyMSB +import to.bitkit.ui.components.BodySSB import to.bitkit.ui.components.BottomSheetPreview -import to.bitkit.ui.components.Display import to.bitkit.ui.components.FillHeight +import to.bitkit.ui.components.FillWidth +import to.bitkit.ui.components.HorizontalSpacer +import to.bitkit.ui.components.MoneyMSB +import to.bitkit.ui.components.MoneySSB import to.bitkit.ui.components.PrimaryButton import to.bitkit.ui.components.VerticalSpacer import to.bitkit.ui.components.settings.SectionHeader import to.bitkit.ui.scaffold.SheetTopBar import to.bitkit.ui.shared.modifiers.sheetHeight +import to.bitkit.ui.shared.util.clickableAlpha import to.bitkit.ui.shared.util.gradientBackground import to.bitkit.ui.theme.AppThemeSurface +import to.bitkit.ui.theme.Colors import to.bitkit.viewmodels.SendUiState @Composable @@ -51,20 +70,102 @@ private fun Content( .testTag("speed_screen") ) { SheetTopBar(stringResource(R.string.wallet__send_fee_speed), onBack = onBack) - Column( + SectionHeader( + title = stringResource(R.string.wallet__send_fee_and_speed), modifier = Modifier.padding(horizontal = 16.dp) - ) { - SectionHeader(stringResource(R.string.wallet__send_fee_and_speed)) - Display("TODO") + ) + FeeItem( + feeRate = FeeRate.FAST, + sats = 1790, + isSelected = false, + isDisabled = false, + onClick = {}, + ) + FeeItem( + feeRate = FeeRate.NORMAL, + sats = 1350, + isSelected = true, + isDisabled = false, + onClick = {}, + ) + FeeItem( + feeRate = FeeRate.SLOW, + sats = 850, + isSelected = false, + isDisabled = true, + onClick = {}, + ) + FeeItem( + feeRate = FeeRate.CUSTOM, + sats = 10, + isSelected = false, + isDisabled = false, + onClick = {}, + ) + + FillHeight(min = 16.dp) - FillHeight(min = 16.dp) + PrimaryButton( + text = stringResource(R.string.common__continue), + onClick = onContinue, + modifier = Modifier + .padding(horizontal = 16.dp) + .testTag("continue_btn") + ) + VerticalSpacer(16.dp) + } +} - PrimaryButton( - text = stringResource(R.string.common__continue), - onClick = onContinue, - modifier = Modifier.testTag("continue_btn") +@Composable +private fun FeeItem( + feeRate: FeeRate, + sats: Long, + onClick: () -> Unit, + modifier: Modifier = Modifier, + isSelected: Boolean = false, + isDisabled: Boolean = false, + unit: PrimaryDisplay = LocalCurrencies.current.primaryDisplay, +) { + val color = if (isDisabled) Colors.Gray3 else MaterialTheme.colorScheme.primary + val accent = if (isDisabled) Colors.Gray3 else MaterialTheme.colorScheme.secondary + Column( + modifier = modifier + .clickableAlpha(onClick = onClick) + .then( + if (isSelected) Modifier.background(Colors.White06) else Modifier + ), + ) { + HorizontalDivider(Modifier.padding(horizontal = 16.dp)) + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier + .padding(horizontal = 16.dp) + .height(90.dp) + ) { + Icon( + painter = feeRate.uiIcon(), + contentDescription = null, + tint = when { + isDisabled -> Colors.Gray3 + feeRate != FeeRate.CUSTOM -> Colors.Brand + else -> Color.Unspecified + }, + modifier = Modifier.size(32.dp), ) - VerticalSpacer(16.dp) + HorizontalSpacer(16.dp) + Column { + BodyMSB(feeRate.uiTitle(), color = color) + BodySSB(feeRate.uiDescription(), color = accent) + } + FillWidth() + if (sats != 0L) { + Column( + horizontalAlignment = Alignment.End, + ) { + MoneyMSB(sats, color = color, accent = accent) + MoneySSB(sats, unit = unit.not(), color = accent, accent = accent) + } + } } } } From 734233713db599bae14d9c714952c17772482c49 Mon Sep 17 00:00:00 2001 From: Ovi Trif Date: Tue, 5 Aug 2025 17:20:01 +0200 Subject: [PATCH 05/36] fix: insets in previews --- app/src/main/java/to/bitkit/ui/theme/Defaults.kt | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/to/bitkit/ui/theme/Defaults.kt b/app/src/main/java/to/bitkit/ui/theme/Defaults.kt index f8a91b35b..a15a8f78c 100644 --- a/app/src/main/java/to/bitkit/ui/theme/Defaults.kt +++ b/app/src/main/java/to/bitkit/ui/theme/Defaults.kt @@ -18,7 +18,9 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.Immutable import androidx.compose.runtime.Stable import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.LocalInspectionMode import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp import kotlin.time.Duration.Companion.milliseconds @Immutable @@ -129,11 +131,19 @@ val ScreenTransitionMs = AnimationConstants.DefaultDurationMillis.milliseconds / object Insets { val Top: Dp @Composable - get() = WindowInsets.statusBars.asPaddingValues().calculateTopPadding() + get() { + val isPreview = LocalInspectionMode.current + if (isPreview) return 32.dp + return WindowInsets.statusBars.asPaddingValues().calculateTopPadding() + } val Bottom: Dp @Composable - get() = WindowInsets.navigationBars.asPaddingValues().calculateBottomPadding() + get() { + val isPreview = LocalInspectionMode.current + if (isPreview) return 32.dp + return WindowInsets.navigationBars.asPaddingValues().calculateBottomPadding() + } } val TopBarHeight: Dp From 281013f6db2b3a0c35034ca92808116d7eb952d3 Mon Sep 17 00:00:00 2001 From: Ovi Trif Date: Tue, 5 Aug 2025 19:32:10 +0200 Subject: [PATCH 06/36] feat: speed screen viewmodel --- .../java/to/bitkit/models/TransactionSpeed.kt | 74 +++++++----- .../screens/wallets/send/SendFeeRateScreen.kt | 106 ++++++++++++------ .../screens/wallets/send/SendFeeViewModel.kt | 66 +++++++++++ .../java/to/bitkit/ui/sheets/SendSheet.kt | 7 +- .../java/to/bitkit/viewmodels/AppViewModel.kt | 18 ++- 5 files changed, 204 insertions(+), 67 deletions(-) create mode 100644 app/src/main/java/to/bitkit/ui/screens/wallets/send/SendFeeViewModel.kt diff --git a/app/src/main/java/to/bitkit/models/TransactionSpeed.kt b/app/src/main/java/to/bitkit/models/TransactionSpeed.kt index bc97817b2..7995f23e4 100644 --- a/app/src/main/java/to/bitkit/models/TransactionSpeed.kt +++ b/app/src/main/java/to/bitkit/models/TransactionSpeed.kt @@ -1,6 +1,9 @@ package to.bitkit.models +import androidx.annotation.DrawableRes +import androidx.annotation.StringRes import androidx.compose.runtime.Composable +import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.painter.Painter import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource @@ -12,6 +15,7 @@ import kotlinx.serialization.descriptors.SerialDescriptor import kotlinx.serialization.encoding.Decoder import kotlinx.serialization.encoding.Encoder import to.bitkit.R +import to.bitkit.ui.theme.Colors @Serializable(with = TransactionSpeedSerializer::class) sealed class TransactionSpeed { @@ -28,6 +32,8 @@ sealed class TransactionSpeed { } companion object { + fun entries() = listOf(Fast, Medium, Slow, Custom(0u)) + fun fromString(value: String): TransactionSpeed = when { value == "fast" -> Fast value == "medium" -> Medium @@ -67,36 +73,54 @@ fun TransactionSpeed.transactionSpeedUiText(): String { } } -enum class FeeRate { - FAST, NORMAL, SLOW, CUSTOM; - - @Composable - fun uiTitle(): String { - return when (this) { - FAST -> stringResource(R.string.fee__fast__title) - NORMAL -> stringResource(R.string.fee__normal__title) - SLOW -> stringResource(R.string.fee__slow__title) - CUSTOM -> stringResource(R.string.fee__custom__title) - } - } +enum class FeeRate( + @StringRes val title: Int, + @StringRes val description: Int, + @DrawableRes val icon: Int, + val color: Color, +) { + FAST( + title = R.string.fee__fast__title, + description = R.string.fee__fast__description, + color = Colors.Brand, + icon = R.drawable.ic_speed_fast, + ), + NORMAL( + title = R.string.fee__normal__title, + description = R.string.fee__normal__description, + color = Colors.Brand, + icon = R.drawable.ic_speed_normal, + ), + SLOW( + title = R.string.fee__slow__title, + description = R.string.fee__slow__description, + color = Colors.Brand, + icon = R.drawable.ic_speed_slow, + ), + CUSTOM( + title = R.string.fee__custom__title, + description = R.string.fee__custom__description, + color = Colors.White64, + icon = R.drawable.ic_settings, + ); - @Composable - fun uiDescription(): String { + fun toSpeed(): TransactionSpeed { return when (this) { - FAST -> stringResource(R.string.fee__fast__description) - NORMAL -> stringResource(R.string.fee__normal__description) - SLOW -> stringResource(R.string.fee__slow__description) - CUSTOM -> stringResource(R.string.fee__custom__description) + FAST -> TransactionSpeed.Fast + NORMAL -> TransactionSpeed.Medium + SLOW -> TransactionSpeed.Slow + CUSTOM -> TransactionSpeed.Custom(0u) } } - @Composable - fun uiIcon(): Painter { - return when (this) { - FAST -> painterResource(R.drawable.ic_speed_fast) - NORMAL -> painterResource(R.drawable.ic_speed_normal) - SLOW -> painterResource(R.drawable.ic_speed_slow) - CUSTOM -> painterResource(R.drawable.ic_settings) + companion object { + fun fromSpeed(speed: TransactionSpeed): FeeRate { + return when (speed) { + is TransactionSpeed.Fast -> FAST + is TransactionSpeed.Medium -> NORMAL + is TransactionSpeed.Slow -> SLOW + is TransactionSpeed.Custom -> CUSTOM + } } } } diff --git a/app/src/main/java/to/bitkit/ui/screens/wallets/send/SendFeeRateScreen.kt b/app/src/main/java/to/bitkit/ui/screens/wallets/send/SendFeeRateScreen.kt index 349497fca..6389891b6 100644 --- a/app/src/main/java/to/bitkit/ui/screens/wallets/send/SendFeeRateScreen.kt +++ b/app/src/main/java/to/bitkit/ui/screens/wallets/send/SendFeeRateScreen.kt @@ -1,6 +1,7 @@ package to.bitkit.ui.screens.wallets.send import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxSize @@ -8,20 +9,27 @@ import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.navigationBarsPadding import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size +import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.testTag +import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp +import androidx.hilt.navigation.compose.hiltViewModel +import androidx.lifecycle.compose.collectAsStateWithLifecycle import to.bitkit.R import to.bitkit.models.FeeRate import to.bitkit.models.PrimaryDisplay +import to.bitkit.models.TransactionSpeed import to.bitkit.ui.LocalCurrencies import to.bitkit.ui.components.BodyMSB import to.bitkit.ui.components.BodySSB @@ -44,23 +52,33 @@ import to.bitkit.viewmodels.SendUiState @Composable fun SendFeeRateScreen( - uiState: SendUiState, + sendUiState: SendUiState, onBack: () -> Unit, onContinue: () -> Unit, + onSelect: (TransactionSpeed) -> Unit, + viewModel: SendFeeViewModel = hiltViewModel(), ) { + val uiState by viewModel.uiState.collectAsStateWithLifecycle() + + LaunchedEffect(Unit) { + viewModel.load(sendUiState.speed) + } + Content( uiState = uiState, onBack = onBack, onContinue = onContinue, + onSelect = { onSelect(it.toSpeed()) }, ) } @Composable private fun Content( - uiState: SendUiState, + uiState: SendFeeUiState, modifier: Modifier = Modifier, onBack: () -> Unit = {}, onContinue: () -> Unit = {}, + onSelect: (FeeRate) -> Unit = {}, ) { Column( modifier = modifier @@ -70,38 +88,31 @@ private fun Content( .testTag("speed_screen") ) { SheetTopBar(stringResource(R.string.wallet__send_fee_speed), onBack = onBack) + if (uiState.items.isEmpty()) { + Box(Modifier.fillMaxSize()) { + CircularProgressIndicator( + strokeWidth = 2.dp, + color = Colors.White32, + modifier = Modifier + .padding(16.dp) + .align(Alignment.Center) + ) + } + return + } SectionHeader( title = stringResource(R.string.wallet__send_fee_and_speed), modifier = Modifier.padding(horizontal = 16.dp) ) - FeeItem( - feeRate = FeeRate.FAST, - sats = 1790, - isSelected = false, - isDisabled = false, - onClick = {}, - ) - FeeItem( - feeRate = FeeRate.NORMAL, - sats = 1350, - isSelected = true, - isDisabled = false, - onClick = {}, - ) - FeeItem( - feeRate = FeeRate.SLOW, - sats = 850, - isSelected = false, - isDisabled = true, - onClick = {}, - ) - FeeItem( - feeRate = FeeRate.CUSTOM, - sats = 10, - isSelected = false, - isDisabled = false, - onClick = {}, - ) + uiState.items.map { (feeRate, sats) -> + FeeItem( + feeRate = feeRate, + sats = sats, + isSelected = uiState.selected == feeRate, + isDisabled = false, // TODO + onClick = { onSelect(feeRate) }, + ) + } FillHeight(min = 16.dp) @@ -143,19 +154,18 @@ private fun FeeItem( .height(90.dp) ) { Icon( - painter = feeRate.uiIcon(), + painter = painterResource(feeRate.icon), contentDescription = null, tint = when { isDisabled -> Colors.Gray3 - feeRate != FeeRate.CUSTOM -> Colors.Brand - else -> Color.Unspecified + else -> feeRate.color }, modifier = Modifier.size(32.dp), ) HorizontalSpacer(16.dp) Column { - BodyMSB(feeRate.uiTitle(), color = color) - BodySSB(feeRate.uiDescription(), color = accent) + BodyMSB(stringResource(feeRate.title), color = color) + BodySSB(stringResource(feeRate.description), color = accent) } FillWidth() if (sats != 0L) { @@ -176,7 +186,31 @@ private fun Preview() { AppThemeSurface { BottomSheetPreview { Content( - uiState = SendUiState(), + uiState = SendFeeUiState( + items = mapOf( + FeeRate.FAST to 4000L, + FeeRate.NORMAL to 3000L, + FeeRate.SLOW to 2000L, + FeeRate.CUSTOM to 0L, + ), + selected = FeeRate.NORMAL, + ), + modifier = Modifier.sheetHeight(), + ) + } + } +} + +@Preview(showSystemUi = true) +@Composable +private fun PreviewEmpty() { + AppThemeSurface { + BottomSheetPreview { + Content( + uiState = SendFeeUiState( + items = mapOf(), + selected = FeeRate.NORMAL, + ), modifier = Modifier.sheetHeight(), ) } diff --git a/app/src/main/java/to/bitkit/ui/screens/wallets/send/SendFeeViewModel.kt b/app/src/main/java/to/bitkit/ui/screens/wallets/send/SendFeeViewModel.kt new file mode 100644 index 000000000..c4877fe5c --- /dev/null +++ b/app/src/main/java/to/bitkit/ui/screens/wallets/send/SendFeeViewModel.kt @@ -0,0 +1,66 @@ +package to.bitkit.ui.screens.wallets.send + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.update +import kotlinx.coroutines.launch +import to.bitkit.data.SettingsStore +import to.bitkit.di.BgDispatcher +import to.bitkit.ext.getSatsPerVByteFor +import to.bitkit.models.FeeRate +import to.bitkit.models.TransactionSpeed +import to.bitkit.repositories.BlocktankRepo +import javax.inject.Inject + +@HiltViewModel +class SendFeeViewModel @Inject constructor( + @BgDispatcher private val bgDispatcher: CoroutineDispatcher, + private val blocktankRepo: BlocktankRepo, + private val settingsStore: SettingsStore, +) : ViewModel() { + private val _uiState = MutableStateFlow(SendFeeUiState()) + val uiState = _uiState.asStateFlow() + + init { + collectState() + } + + private fun collectState() { + viewModelScope.launch(bgDispatcher) { + blocktankRepo.blocktankState.map { it.info?.onchain?.feeRates }.collect { feeRates -> + _uiState.update { + it.copy( + items = TransactionSpeed.entries().associate { speed -> + val rate = FeeRate.fromSpeed(speed) + val sats = feeRates?.getSatsPerVByteFor(speed) ?: 0u + rate to sats.toLong() + } + ) + } + } + } + } + + fun load(speed: TransactionSpeed?) { + viewModelScope.launch(bgDispatcher) { blocktankRepo.refreshInfo() } + viewModelScope.launch { + val selectedSpeed = speed ?: settingsStore.data.first().defaultTransactionSpeed + _uiState.update { + it.copy( + selected = FeeRate.fromSpeed(selectedSpeed), + ) + } + } + } +} + +data class SendFeeUiState( + val items: Map = emptyMap(), + val selected: FeeRate? = null, +) diff --git a/app/src/main/java/to/bitkit/ui/sheets/SendSheet.kt b/app/src/main/java/to/bitkit/ui/sheets/SendSheet.kt index b56c531bd..a2816cb5e 100644 --- a/app/src/main/java/to/bitkit/ui/sheets/SendSheet.kt +++ b/app/src/main/java/to/bitkit/ui/sheets/SendSheet.kt @@ -51,6 +51,8 @@ fun SendSheet( } } + + Column( modifier = Modifier .fillMaxWidth() @@ -123,9 +125,10 @@ fun SendSheet( composableWithDefaultTransitions { val sendUiState by appViewModel.sendUiState.collectAsStateWithLifecycle() SendFeeRateScreen( - uiState = sendUiState, + sendUiState = sendUiState, onBack = { navController.popBackStack() }, - onContinue = {}, // TODO + onContinue = { navController.popBackStack() }, + onSelect = { appViewModel.setSendEvent(SendEvent.SpeedChange(it)) }, ) } composableWithDefaultTransitions { diff --git a/app/src/main/java/to/bitkit/viewmodels/AppViewModel.kt b/app/src/main/java/to/bitkit/viewmodels/AppViewModel.kt index 1877a1fd5..fa1553915 100644 --- a/app/src/main/java/to/bitkit/viewmodels/AppViewModel.kt +++ b/app/src/main/java/to/bitkit/viewmodels/AppViewModel.kt @@ -73,8 +73,8 @@ import to.bitkit.services.CoreService import to.bitkit.services.LdkNodeEventBus import to.bitkit.ui.Routes import to.bitkit.ui.components.Sheet -import to.bitkit.ui.sheets.SendRoute import to.bitkit.ui.shared.toast.ToastEventBus +import to.bitkit.ui.sheets.SendRoute import to.bitkit.utils.Logger import java.math.BigDecimal import javax.inject.Inject @@ -289,6 +289,7 @@ class AppViewModel @Inject constructor( is SendEvent.CoinSelectionContinue -> onCoinSelectionContinue(it.utxos) is SendEvent.CommentChange -> onCommentChange(it.value) + is SendEvent.SpeedChange -> onSpeedChange(it.speed) SendEvent.SpeedAndFee -> setSendEffect(SendEffect.NavigateToFee) SendEvent.SwipeToPay -> onSwipeToPay() @@ -353,6 +354,14 @@ class AppViewModel @Inject constructor( } } + private fun onSpeedChange(speed: TransactionSpeed) { + _sendUiState.update { + it.copy(speed = speed) + } + setSendEffect(SendEffect.NavigateToReview) + } + + private fun onPaymentMethodSwitch() { val nextPaymentMethod = when (_sendUiState.value.payMethod) { SendMethod.ONCHAIN -> SendMethod.LIGHTNING @@ -992,12 +1001,12 @@ class AppViewModel @Inject constructor( } } - private suspend fun sendOnchain(address: String, amount: ULong): Result { - val utxos = _sendUiState.value.selectedUtxos + private suspend fun sendOnchain(address: String, amount: ULong, speed: TransactionSpeed? = null): Result { return lightningRepo.sendOnChain( address = address, sats = amount, - utxosToSpend = utxos, + speed = _sendUiState.value.speed, + utxosToSpend = _sendUiState.value.selectedUtxos, ) } @@ -1307,6 +1316,7 @@ sealed class SendEvent { data object ConfirmAmountWarning : SendEvent() data object DismissAmountWarning : SendEvent() data object PayConfirmed : SendEvent() + data class SpeedChange(val speed: TransactionSpeed) : SendEvent() } sealed interface LnurlParams { From 618b8a88ba5564f0109c3354f7c8bf47aba64741 Mon Sep 17 00:00:00 2001 From: Ovi Trif Date: Tue, 5 Aug 2025 20:24:49 +0200 Subject: [PATCH 07/36] fix: use selected utxos to estimate send fee --- app/src/main/java/to/bitkit/viewmodels/AppViewModel.kt | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/app/src/main/java/to/bitkit/viewmodels/AppViewModel.kt b/app/src/main/java/to/bitkit/viewmodels/AppViewModel.kt index fa1553915..b2bf85922 100644 --- a/app/src/main/java/to/bitkit/viewmodels/AppViewModel.kt +++ b/app/src/main/java/to/bitkit/viewmodels/AppViewModel.kt @@ -361,7 +361,6 @@ class AppViewModel @Inject constructor( setSendEffect(SendEffect.NavigateToReview) } - private fun onPaymentMethodSwitch() { val nextPaymentMethod = when (_sendUiState.value.payMethod) { SendMethod.ONCHAIN -> SendMethod.LIGHTNING @@ -816,7 +815,7 @@ class AppViewModel @Inject constructor( amountSats = amountSats, address = _sendUiState.value.address, speed = _sendUiState.value.speed, - utxosToSpend = _sendUiState.value.utxosToSpend + utxosToSpend = _sendUiState.value.selectedUtxos, ).getOrNull() ?: return if (totalFee > BigDecimal.valueOf(amountSats.toLong()) @@ -1261,7 +1260,6 @@ data class SendUiState( val lnurl: LnurlParams? = null, val isLoading: Boolean = false, val speed: TransactionSpeed? = null, - val utxosToSpend: List? = null, val comment: String = "", ) From 952bfcb6ff1005abb53a58996ddf80127292839c Mon Sep 17 00:00:00 2001 From: Ovi Trif Date: Tue, 5 Aug 2025 20:41:28 +0200 Subject: [PATCH 08/36] feat: calculate send amount fee by rate --- .../java/to/bitkit/models/TransactionSpeed.kt | 2 - .../screens/wallets/send/SendFeeRateScreen.kt | 11 ++- .../screens/wallets/send/SendFeeViewModel.kt | 79 ++++++++++++++++--- 3 files changed, 71 insertions(+), 21 deletions(-) diff --git a/app/src/main/java/to/bitkit/models/TransactionSpeed.kt b/app/src/main/java/to/bitkit/models/TransactionSpeed.kt index 7995f23e4..34532cdbf 100644 --- a/app/src/main/java/to/bitkit/models/TransactionSpeed.kt +++ b/app/src/main/java/to/bitkit/models/TransactionSpeed.kt @@ -32,8 +32,6 @@ sealed class TransactionSpeed { } companion object { - fun entries() = listOf(Fast, Medium, Slow, Custom(0u)) - fun fromString(value: String): TransactionSpeed = when { value == "fast" -> Fast value == "medium" -> Medium diff --git a/app/src/main/java/to/bitkit/ui/screens/wallets/send/SendFeeRateScreen.kt b/app/src/main/java/to/bitkit/ui/screens/wallets/send/SendFeeRateScreen.kt index 6389891b6..94f93d1bb 100644 --- a/app/src/main/java/to/bitkit/ui/screens/wallets/send/SendFeeRateScreen.kt +++ b/app/src/main/java/to/bitkit/ui/screens/wallets/send/SendFeeRateScreen.kt @@ -18,7 +18,6 @@ import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.testTag import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource @@ -61,7 +60,7 @@ fun SendFeeRateScreen( val uiState by viewModel.uiState.collectAsStateWithLifecycle() LaunchedEffect(Unit) { - viewModel.load(sendUiState.speed) + viewModel.init(sendUiState) } Content( @@ -88,7 +87,7 @@ private fun Content( .testTag("speed_screen") ) { SheetTopBar(stringResource(R.string.wallet__send_fee_speed), onBack = onBack) - if (uiState.items.isEmpty()) { + if (uiState.fees.isEmpty()) { Box(Modifier.fillMaxSize()) { CircularProgressIndicator( strokeWidth = 2.dp, @@ -104,7 +103,7 @@ private fun Content( title = stringResource(R.string.wallet__send_fee_and_speed), modifier = Modifier.padding(horizontal = 16.dp) ) - uiState.items.map { (feeRate, sats) -> + uiState.fees.map { (feeRate, sats) -> FeeItem( feeRate = feeRate, sats = sats, @@ -187,7 +186,7 @@ private fun Preview() { BottomSheetPreview { Content( uiState = SendFeeUiState( - items = mapOf( + fees = mapOf( FeeRate.FAST to 4000L, FeeRate.NORMAL to 3000L, FeeRate.SLOW to 2000L, @@ -208,7 +207,7 @@ private fun PreviewEmpty() { BottomSheetPreview { Content( uiState = SendFeeUiState( - items = mapOf(), + fees = mapOf(), selected = FeeRate.NORMAL, ), modifier = Modifier.sheetHeight(), diff --git a/app/src/main/java/to/bitkit/ui/screens/wallets/send/SendFeeViewModel.kt b/app/src/main/java/to/bitkit/ui/screens/wallets/send/SendFeeViewModel.kt index c4877fe5c..e2270c0a9 100644 --- a/app/src/main/java/to/bitkit/ui/screens/wallets/send/SendFeeViewModel.kt +++ b/app/src/main/java/to/bitkit/ui/screens/wallets/send/SendFeeViewModel.kt @@ -2,65 +2,118 @@ package to.bitkit.ui.screens.wallets.send import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope +import com.synonym.bitkitcore.FeeRates import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.async +import kotlinx.coroutines.awaitAll +import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext import to.bitkit.data.SettingsStore import to.bitkit.di.BgDispatcher import to.bitkit.ext.getSatsPerVByteFor import to.bitkit.models.FeeRate import to.bitkit.models.TransactionSpeed import to.bitkit.repositories.BlocktankRepo +import to.bitkit.repositories.LightningRepo +import to.bitkit.viewmodels.SendUiState import javax.inject.Inject @HiltViewModel class SendFeeViewModel @Inject constructor( @BgDispatcher private val bgDispatcher: CoroutineDispatcher, + private val lightningRepo: LightningRepo, private val blocktankRepo: BlocktankRepo, private val settingsStore: SettingsStore, ) : ViewModel() { private val _uiState = MutableStateFlow(SendFeeUiState()) val uiState = _uiState.asStateFlow() - init { + private lateinit var sendUiState: SendUiState + + fun init(sendUiState: SendUiState) { + this.sendUiState = sendUiState + viewModelScope.launch { + val selectedSpeed = sendUiState.speed ?: settingsStore.data.first().defaultTransactionSpeed + _uiState.update { + it.copy( + selected = FeeRate.fromSpeed(selectedSpeed), + ) + } + } + viewModelScope.launch(bgDispatcher) { blocktankRepo.refreshInfo() } collectState() } private fun collectState() { viewModelScope.launch(bgDispatcher) { blocktankRepo.blocktankState.map { it.info?.onchain?.feeRates }.collect { feeRates -> + // init first _uiState.update { it.copy( - items = TransactionSpeed.entries().associate { speed -> + fees = speedEntries().associate { speed -> val rate = FeeRate.fromSpeed(speed) - val sats = feeRates?.getSatsPerVByteFor(speed) ?: 0u - rate to sats.toLong() + val sats = feeRates?.getSatsPerVByteFor(speed)?.toLong() ?: 0 + rate to sats } ) } + loadFees(feeRates) } } } - fun load(speed: TransactionSpeed?) { - viewModelScope.launch(bgDispatcher) { blocktankRepo.refreshInfo() } - viewModelScope.launch { - val selectedSpeed = speed ?: settingsStore.data.first().defaultTransactionSpeed - _uiState.update { - it.copy( - selected = FeeRate.fromSpeed(selectedSpeed), - ) + private suspend fun loadFees(feeRates: FeeRates?) { + val newFees = getFees(feeRates) + _uiState.update { it.copy(fees = newFees) } + } + + private suspend fun getFees(feeRates: FeeRates?): Map { + return withContext(bgDispatcher) { + return@withContext coroutineScope { + speedEntries() + .map { speed -> + async { + val rate = FeeRate.fromSpeed(speed) + val fee = getFeeSatsForSpeed(speed, feeRates) + rate to fee + } + }.awaitAll() + .toMap() } } } + + private suspend fun getFeeSatsForSpeed(speed: TransactionSpeed, feeRates: FeeRates?): Long { + if (feeRates?.getSatsPerVByteFor(speed)?.toLong() == 0L) return 0L + + return lightningRepo.calculateTotalFee( + amountSats = sendUiState.amount, + utxosToSpend = sendUiState.selectedUtxos, + speed = speed, + ).getOrDefault(0u).toLong() + } + + private fun speedEntries(): List { + return listOf( + TransactionSpeed.Fast, + TransactionSpeed.Medium, + TransactionSpeed.Slow, + when (val speed = sendUiState.speed) { + is TransactionSpeed.Custom -> speed + else -> TransactionSpeed.Custom(0u) + } + ) + } } data class SendFeeUiState( - val items: Map = emptyMap(), + val fees: Map = emptyMap(), val selected: FeeRate? = null, ) From 0754427db889f9e8a45cbf81c1436a2b1f2c3c78 Mon Sep 17 00:00:00 2001 From: Ovi Trif Date: Tue, 5 Aug 2025 20:52:11 +0200 Subject: [PATCH 09/36] fix: navigate back from speed screen --- app/src/main/java/to/bitkit/ui/sheets/SendSheet.kt | 1 + app/src/main/java/to/bitkit/viewmodels/AppViewModel.kt | 3 ++- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/to/bitkit/ui/sheets/SendSheet.kt b/app/src/main/java/to/bitkit/ui/sheets/SendSheet.kt index a2816cb5e..b8b6fd599 100644 --- a/app/src/main/java/to/bitkit/ui/sheets/SendSheet.kt +++ b/app/src/main/java/to/bitkit/ui/sheets/SendSheet.kt @@ -68,6 +68,7 @@ fun SendSheet( is SendEffect.NavigateToScan -> navController.navigate(SendRoute.QrScanner) is SendEffect.NavigateToCoinSelection -> navController.navigate(SendRoute.CoinSelection) is SendEffect.NavigateToReview -> navController.navigate(SendRoute.Confirm) + is SendEffect.PopBack -> navController.popBackStack() is SendEffect.PaymentSuccess -> onComplete(it.sheet) is SendEffect.NavigateToQuickPay -> navController.navigate(SendRoute.QuickPay) is SendEffect.NavigateToWithdrawConfirm -> navController.navigate(SendRoute.WithdrawConfirm) diff --git a/app/src/main/java/to/bitkit/viewmodels/AppViewModel.kt b/app/src/main/java/to/bitkit/viewmodels/AppViewModel.kt index b2bf85922..2ffe53db7 100644 --- a/app/src/main/java/to/bitkit/viewmodels/AppViewModel.kt +++ b/app/src/main/java/to/bitkit/viewmodels/AppViewModel.kt @@ -358,7 +358,7 @@ class AppViewModel @Inject constructor( _sendUiState.update { it.copy(speed = speed) } - setSendEffect(SendEffect.NavigateToReview) + setSendEffect(SendEffect.PopBack) } private fun onPaymentMethodSwitch() { @@ -1277,6 +1277,7 @@ sealed class SendEffect { data object NavigateToAmount : SendEffect() data object NavigateToScan : SendEffect() data object NavigateToReview : SendEffect() + data object PopBack : SendEffect() data object NavigateToWithdrawConfirm : SendEffect() data object NavigateToWithdrawError : SendEffect() data object NavigateToCoinSelection : SendEffect() From 432241ce95a7662453d5485455dc7e4f4f4bab4a Mon Sep 17 00:00:00 2001 From: Ovi Trif Date: Tue, 5 Aug 2025 23:05:22 +0200 Subject: [PATCH 10/36] fix: use the speed selected in settings for send --- .../main/java/to/bitkit/data/SettingsStore.kt | 2 +- .../java/to/bitkit/models/TransactionSpeed.kt | 2 ++ .../screens/wallets/send/SendFeeRateScreen.kt | 25 ++++++++++++++++--- .../screens/wallets/send/SendFeeViewModel.kt | 9 +++---- .../java/to/bitkit/ui/sheets/SendSheet.kt | 3 --- .../java/to/bitkit/viewmodels/AppViewModel.kt | 9 +++++-- 6 files changed, 35 insertions(+), 15 deletions(-) diff --git a/app/src/main/java/to/bitkit/data/SettingsStore.kt b/app/src/main/java/to/bitkit/data/SettingsStore.kt index 3271e394d..0b9784ffe 100644 --- a/app/src/main/java/to/bitkit/data/SettingsStore.kt +++ b/app/src/main/java/to/bitkit/data/SettingsStore.kt @@ -70,7 +70,7 @@ data class SettingsData( val primaryDisplay: PrimaryDisplay = PrimaryDisplay.BITCOIN, val displayUnit: BitcoinDisplayUnit = BitcoinDisplayUnit.MODERN, val selectedCurrency: String = "USD", - val defaultTransactionSpeed: TransactionSpeed = TransactionSpeed.Medium, + val defaultTransactionSpeed: TransactionSpeed = TransactionSpeed.default(), val showEmptyBalanceView: Boolean = true, val hasSeenSpendingIntro: Boolean = false, val hasSeenWidgetsIntro: Boolean = false, diff --git a/app/src/main/java/to/bitkit/models/TransactionSpeed.kt b/app/src/main/java/to/bitkit/models/TransactionSpeed.kt index 34532cdbf..68656d870 100644 --- a/app/src/main/java/to/bitkit/models/TransactionSpeed.kt +++ b/app/src/main/java/to/bitkit/models/TransactionSpeed.kt @@ -32,6 +32,8 @@ sealed class TransactionSpeed { } companion object { + fun default(): TransactionSpeed = Medium + fun fromString(value: String): TransactionSpeed = when { value == "fast" -> Fast value == "medium" -> Medium diff --git a/app/src/main/java/to/bitkit/ui/screens/wallets/send/SendFeeRateScreen.kt b/app/src/main/java/to/bitkit/ui/screens/wallets/send/SendFeeRateScreen.kt index 94f93d1bb..76bbbd7e2 100644 --- a/app/src/main/java/to/bitkit/ui/screens/wallets/send/SendFeeRateScreen.kt +++ b/app/src/main/java/to/bitkit/ui/screens/wallets/send/SendFeeRateScreen.kt @@ -110,6 +110,7 @@ private fun Content( isSelected = uiState.selected == feeRate, isDisabled = false, // TODO onClick = { onSelect(feeRate) }, + modifier = Modifier.testTag("fee_${feeRate.name}_button"), ) } @@ -202,16 +203,34 @@ private fun Preview() { @Preview(showSystemUi = true) @Composable -private fun PreviewEmpty() { +private fun PreviewCustom() { AppThemeSurface { BottomSheetPreview { Content( uiState = SendFeeUiState( - fees = mapOf(), - selected = FeeRate.NORMAL, + fees = mapOf( + FeeRate.FAST to 4000L, + FeeRate.NORMAL to 3000L, + FeeRate.SLOW to 2000L, + FeeRate.CUSTOM to 6000L, + ), + selected = FeeRate.CUSTOM, ), modifier = Modifier.sheetHeight(), ) } } } + +@Preview(showSystemUi = true) +@Composable +private fun PreviewEmpty() { + AppThemeSurface { + BottomSheetPreview { + Content( + uiState = SendFeeUiState(), + modifier = Modifier.sheetHeight(), + ) + } + } +} diff --git a/app/src/main/java/to/bitkit/ui/screens/wallets/send/SendFeeViewModel.kt b/app/src/main/java/to/bitkit/ui/screens/wallets/send/SendFeeViewModel.kt index e2270c0a9..d83edaf7e 100644 --- a/app/src/main/java/to/bitkit/ui/screens/wallets/send/SendFeeViewModel.kt +++ b/app/src/main/java/to/bitkit/ui/screens/wallets/send/SendFeeViewModel.kt @@ -10,12 +10,10 @@ import kotlinx.coroutines.awaitAll import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asStateFlow -import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch import kotlinx.coroutines.withContext -import to.bitkit.data.SettingsStore import to.bitkit.di.BgDispatcher import to.bitkit.ext.getSatsPerVByteFor import to.bitkit.models.FeeRate @@ -30,7 +28,6 @@ class SendFeeViewModel @Inject constructor( @BgDispatcher private val bgDispatcher: CoroutineDispatcher, private val lightningRepo: LightningRepo, private val blocktankRepo: BlocktankRepo, - private val settingsStore: SettingsStore, ) : ViewModel() { private val _uiState = MutableStateFlow(SendFeeUiState()) val uiState = _uiState.asStateFlow() @@ -40,10 +37,9 @@ class SendFeeViewModel @Inject constructor( fun init(sendUiState: SendUiState) { this.sendUiState = sendUiState viewModelScope.launch { - val selectedSpeed = sendUiState.speed ?: settingsStore.data.first().defaultTransactionSpeed _uiState.update { it.copy( - selected = FeeRate.fromSpeed(selectedSpeed), + selected = FeeRate.fromSpeed(sendUiState.speed), ) } } @@ -54,7 +50,7 @@ class SendFeeViewModel @Inject constructor( private fun collectState() { viewModelScope.launch(bgDispatcher) { blocktankRepo.blocktankState.map { it.info?.onchain?.feeRates }.collect { feeRates -> - // init first + // init ui first _uiState.update { it.copy( fees = speedEntries().associate { speed -> @@ -64,6 +60,7 @@ class SendFeeViewModel @Inject constructor( } ) } + // TODO try move to appViewModel and trigger load in bg onAmountContinue + store in sendUiState loadFees(feeRates) } } diff --git a/app/src/main/java/to/bitkit/ui/sheets/SendSheet.kt b/app/src/main/java/to/bitkit/ui/sheets/SendSheet.kt index b8b6fd599..f141a8695 100644 --- a/app/src/main/java/to/bitkit/ui/sheets/SendSheet.kt +++ b/app/src/main/java/to/bitkit/ui/sheets/SendSheet.kt @@ -50,9 +50,6 @@ fun SendSheet( appViewModel.resetQuickPayData() } } - - - Column( modifier = Modifier .fillMaxWidth() diff --git a/app/src/main/java/to/bitkit/viewmodels/AppViewModel.kt b/app/src/main/java/to/bitkit/viewmodels/AppViewModel.kt index 2ffe53db7..b8a5afcba 100644 --- a/app/src/main/java/to/bitkit/viewmodels/AppViewModel.kt +++ b/app/src/main/java/to/bitkit/viewmodels/AppViewModel.kt @@ -269,6 +269,7 @@ class AppViewModel @Inject constructor( } // region send + private fun observeSendEvents() { viewModelScope.launch { sendEvents.collect { @@ -1057,7 +1058,11 @@ class AppViewModel @Inject constructor( fun resetQuickPayData() = _quickPayData.update { null } fun resetSendState() { - _sendUiState.value = SendUiState() + viewModelScope.launch { + _sendUiState.value = SendUiState( + speed = settingsStore.data.first().defaultTransactionSpeed, + ) + } } // endregion @@ -1259,7 +1264,7 @@ data class SendUiState( val selectedUtxos: List? = null, val lnurl: LnurlParams? = null, val isLoading: Boolean = false, - val speed: TransactionSpeed? = null, + val speed: TransactionSpeed = TransactionSpeed.default(), val comment: String = "", ) From b78bbfe8333b5c4e591d142303a27956670db728 Mon Sep 17 00:00:00 2001 From: Ovi Trif Date: Tue, 5 Aug 2025 23:30:12 +0200 Subject: [PATCH 11/36] feat: reuse fetched fee rates --- .../main/java/to/bitkit/repositories/LightningRepo.kt | 11 ++++++++--- .../ui/screens/wallets/send/SendFeeViewModel.kt | 11 +++++++---- 2 files changed, 15 insertions(+), 7 deletions(-) diff --git a/app/src/main/java/to/bitkit/repositories/LightningRepo.kt b/app/src/main/java/to/bitkit/repositories/LightningRepo.kt index 997150079..a3e354ba6 100644 --- a/app/src/main/java/to/bitkit/repositories/LightningRepo.kt +++ b/app/src/main/java/to/bitkit/repositories/LightningRepo.kt @@ -1,6 +1,7 @@ package to.bitkit.repositories import com.google.firebase.messaging.FirebaseMessaging +import com.synonym.bitkitcore.FeeRates import com.synonym.bitkitcore.LightningInvoice import com.synonym.bitkitcore.Scanner import com.synonym.bitkitcore.createWithdrawCallbackUrl @@ -579,10 +580,11 @@ class LightningRepo @Inject constructor( address: Address? = null, speed: TransactionSpeed? = null, utxosToSpend: List? = null, + feeRates: FeeRates? = null, ): Result = withContext(bgDispatcher) { return@withContext try { val transactionSpeed = speed ?: settingsStore.data.first().defaultTransactionSpeed - val satsPerVByte = getFeeRateForSpeed(transactionSpeed).getOrThrow().toUInt() + val satsPerVByte = getFeeRateForSpeed(transactionSpeed, feeRates).getOrThrow().toUInt() val addressOrDefault = address ?: cacheStore.data.first().onchainAddress @@ -600,9 +602,12 @@ class LightningRepo @Inject constructor( } } - suspend fun getFeeRateForSpeed(speed: TransactionSpeed): Result = withContext(bgDispatcher) { + suspend fun getFeeRateForSpeed( + speed: TransactionSpeed, + feeRates: FeeRates? = null, + ): Result = withContext(bgDispatcher) { return@withContext runCatching { - val fees = coreService.blocktank.getFees().getOrThrow() + val fees = feeRates ?: coreService.blocktank.getFees().getOrThrow() val satsPerVByte = fees.getSatsPerVByteFor(speed) satsPerVByte.toULong() }.onFailure { e -> diff --git a/app/src/main/java/to/bitkit/ui/screens/wallets/send/SendFeeViewModel.kt b/app/src/main/java/to/bitkit/ui/screens/wallets/send/SendFeeViewModel.kt index d83edaf7e..0b820a74c 100644 --- a/app/src/main/java/to/bitkit/ui/screens/wallets/send/SendFeeViewModel.kt +++ b/app/src/main/java/to/bitkit/ui/screens/wallets/send/SendFeeViewModel.kt @@ -78,7 +78,11 @@ class SendFeeViewModel @Inject constructor( .map { speed -> async { val rate = FeeRate.fromSpeed(speed) - val fee = getFeeSatsForSpeed(speed, feeRates) + val fee = if (feeRates?.getSatsPerVByteFor(speed)?.toLong() != 0L) { + getFeeForSpeed(speed, feeRates) + } else { + 0 + } rate to fee } }.awaitAll() @@ -87,13 +91,12 @@ class SendFeeViewModel @Inject constructor( } } - private suspend fun getFeeSatsForSpeed(speed: TransactionSpeed, feeRates: FeeRates?): Long { - if (feeRates?.getSatsPerVByteFor(speed)?.toLong() == 0L) return 0L - + private suspend fun getFeeForSpeed(speed: TransactionSpeed, feeRates: FeeRates?): Long { return lightningRepo.calculateTotalFee( amountSats = sendUiState.amount, utxosToSpend = sendUiState.selectedUtxos, speed = speed, + feeRates = feeRates, ).getOrDefault(0u).toLong() } From 06bd762f85298fc66d34668ec737130364f65d67 Mon Sep 17 00:00:00 2001 From: Ovi Trif Date: Wed, 6 Aug 2025 00:09:09 +0200 Subject: [PATCH 12/36] feat: use fee rate details in send confirm screen --- .../screens/wallets/send/SendConfirmScreen.kt | 94 +++++++++++-------- 1 file changed, 56 insertions(+), 38 deletions(-) diff --git a/app/src/main/java/to/bitkit/ui/screens/wallets/send/SendConfirmScreen.kt b/app/src/main/java/to/bitkit/ui/screens/wallets/send/SendConfirmScreen.kt index c714b117b..517e8ac50 100644 --- a/app/src/main/java/to/bitkit/ui/screens/wallets/send/SendConfirmScreen.kt +++ b/app/src/main/java/to/bitkit/ui/screens/wallets/send/SendConfirmScreen.kt @@ -45,6 +45,7 @@ import to.bitkit.R import to.bitkit.ext.DatePattern import to.bitkit.ext.commentAllowed import to.bitkit.ext.formatted +import to.bitkit.models.FeeRate import to.bitkit.ui.components.BalanceHeaderView import to.bitkit.ui.components.BiometricsView import to.bitkit.ui.components.BodySSB @@ -52,6 +53,7 @@ import to.bitkit.ui.components.BottomSheetPreview import to.bitkit.ui.components.ButtonSize import to.bitkit.ui.components.Caption13Up import to.bitkit.ui.components.FillHeight +import to.bitkit.ui.components.MoneySSB import to.bitkit.ui.components.PrimaryButton import to.bitkit.ui.components.SwipeToConfirm import to.bitkit.ui.components.TagButton @@ -303,6 +305,7 @@ private fun OnChainDescription( uiState: SendUiState, onEvent: (SendEvent) -> Unit, ) { + val fee by remember(uiState.speed) { mutableStateOf(FeeRate.fromSpeed(uiState.speed)) } Column(modifier = Modifier.fillMaxWidth()) { Caption13Up( text = stringResource(R.string.wallet__send_to), @@ -320,56 +323,71 @@ private fun OnChainDescription( modifier = Modifier .fillMaxHeight() .weight(1f) - .clickableAlpha { onEvent(SendEvent.SpeedAndFee) } ) { - VerticalSpacer(16.dp) - Caption13Up(text = stringResource(R.string.wallet__send_fee_and_speed), color = Colors.White64) - Spacer(modifier = Modifier.height(8.dp)) - Row( - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.spacedBy(4.dp), + Column( + modifier = Modifier + .fillMaxWidth() + .clickableAlpha { onEvent(SendEvent.SpeedAndFee) } ) { - Icon( - painterResource(R.drawable.ic_speed_normal), - contentDescription = null, - tint = Colors.Brand, - modifier = Modifier.size(16.dp) - ) - BodySSB(text = "Normal (₿ 210)") // TODO GET FROM STATE - Icon( - painterResource(R.drawable.ic_pencil), - contentDescription = null, - modifier = Modifier.size(16.dp) - ) + VerticalSpacer(16.dp) + Caption13Up(stringResource(R.string.wallet__send_fee_and_speed), color = Colors.White64) + VerticalSpacer(8.dp) + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(4.dp), + ) { + Icon( + painterResource(fee.icon), + contentDescription = null, + tint = fee.color, + modifier = Modifier.size(16.dp) + ) + Row { + BodySSB(stringResource(fee.title) + " (") + MoneySSB(sats = 210, accent = Colors.White) // TODO get from state + BodySSB(")") + } + Icon( + painterResource(R.drawable.ic_pencil), + contentDescription = null, + modifier = Modifier.size(16.dp) + ) + } + FillHeight() + VerticalSpacer(16.dp) } - Spacer(modifier = Modifier.weight(1f)) - HorizontalDivider(modifier = Modifier.padding(top = 16.dp)) + HorizontalDivider() } Column( modifier = Modifier .fillMaxHeight() .weight(1f) - .clickableAlpha { onEvent(SendEvent.SpeedAndFee) } ) { - VerticalSpacer(16.dp) - Caption13Up(text = stringResource(R.string.wallet__send_confirming_in), color = Colors.White64) - Spacer(modifier = Modifier.height(8.dp)) - Row( - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.spacedBy(4.dp), + Column( + modifier = Modifier + .fillMaxWidth() + .clickableAlpha { onEvent(SendEvent.SpeedAndFee) } ) { - Icon( - painterResource(R.drawable.ic_clock), - contentDescription = null, - tint = Colors.Brand, - modifier = Modifier.size(16.dp) - ) - BodySSB(text = "± 20-60 minutes") // TODO GET FROM STATE + VerticalSpacer(16.dp) + Caption13Up(text = stringResource(R.string.wallet__send_confirming_in), color = Colors.White64) + Spacer(modifier = Modifier.height(8.dp)) + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(4.dp), + ) { + Icon( + painterResource(R.drawable.ic_clock), + contentDescription = null, + tint = Colors.Brand, + modifier = Modifier.size(16.dp) + ) + BodySSB(stringResource(fee.description)) + } + FillHeight() + VerticalSpacer(16.dp) + HorizontalDivider() } - Spacer(modifier = Modifier.weight(1f)) - HorizontalDivider(modifier = Modifier.padding(top = 16.dp)) } - } } } From 96c79e6cc9f98fc76070e535cdf3f13398e4978d Mon Sep 17 00:00:00 2001 From: Ovi Trif Date: Wed, 6 Aug 2025 00:10:43 +0200 Subject: [PATCH 13/36] refactor: rename back to ic_pencil_simple --- .../ui/screens/transfer/external/ExternalConfirmScreen.kt | 2 +- .../to/bitkit/ui/screens/wallets/receive/ReceiveQrScreen.kt | 2 +- .../java/to/bitkit/ui/screens/wallets/send/SendConfirmScreen.kt | 2 +- .../to/bitkit/ui/screens/wallets/send/SendRecipientScreen.kt | 2 +- app/src/main/java/to/bitkit/ui/sheets/BoostTransactionSheet.kt | 2 +- .../main/res/drawable/{ic_pencil.xml => ic_pencil_simple.xml} | 0 6 files changed, 5 insertions(+), 5 deletions(-) rename app/src/main/res/drawable/{ic_pencil.xml => ic_pencil_simple.xml} (100%) 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 34dd198ed..a61f783b7 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 @@ -123,7 +123,7 @@ private fun Content( MoneySSB(sats = networkFee) Spacer(modifier = Modifier.width(4.dp)) Icon( - painterResource(R.drawable.ic_pencil), + painterResource(R.drawable.ic_pencil_simple), contentDescription = null, modifier = Modifier.size(16.dp) ) diff --git a/app/src/main/java/to/bitkit/ui/screens/wallets/receive/ReceiveQrScreen.kt b/app/src/main/java/to/bitkit/ui/screens/wallets/receive/ReceiveQrScreen.kt index fb4165f93..2b19f7591 100644 --- a/app/src/main/java/to/bitkit/ui/screens/wallets/receive/ReceiveQrScreen.kt +++ b/app/src/main/java/to/bitkit/ui/screens/wallets/receive/ReceiveQrScreen.kt @@ -266,7 +266,7 @@ private fun ReceiveQrSlide( color = Colors.White10, icon = { Icon( - painter = painterResource(R.drawable.ic_pencil), + painter = painterResource(R.drawable.ic_pencil_simple), contentDescription = null, tint = Colors.Brand, modifier = Modifier.size(18.dp) diff --git a/app/src/main/java/to/bitkit/ui/screens/wallets/send/SendConfirmScreen.kt b/app/src/main/java/to/bitkit/ui/screens/wallets/send/SendConfirmScreen.kt index 517e8ac50..8efcf19b3 100644 --- a/app/src/main/java/to/bitkit/ui/screens/wallets/send/SendConfirmScreen.kt +++ b/app/src/main/java/to/bitkit/ui/screens/wallets/send/SendConfirmScreen.kt @@ -348,7 +348,7 @@ private fun OnChainDescription( BodySSB(")") } Icon( - painterResource(R.drawable.ic_pencil), + painterResource(R.drawable.ic_pencil_simple), contentDescription = null, modifier = Modifier.size(16.dp) ) diff --git a/app/src/main/java/to/bitkit/ui/screens/wallets/send/SendRecipientScreen.kt b/app/src/main/java/to/bitkit/ui/screens/wallets/send/SendRecipientScreen.kt index 205239773..1e082875b 100644 --- a/app/src/main/java/to/bitkit/ui/screens/wallets/send/SendRecipientScreen.kt +++ b/app/src/main/java/to/bitkit/ui/screens/wallets/send/SendRecipientScreen.kt @@ -88,7 +88,7 @@ fun SendRecipientScreen( label = stringResource(R.string.wallet__recipient_manual), icon = { Icon( - painter = painterResource(R.drawable.ic_pencil), + painter = painterResource(R.drawable.ic_pencil_simple), contentDescription = null, tint = Colors.Brand, modifier = Modifier.size(28.dp), diff --git a/app/src/main/java/to/bitkit/ui/sheets/BoostTransactionSheet.kt b/app/src/main/java/to/bitkit/ui/sheets/BoostTransactionSheet.kt index e17b4f9f1..e0ad721a0 100644 --- a/app/src/main/java/to/bitkit/ui/sheets/BoostTransactionSheet.kt +++ b/app/src/main/java/to/bitkit/ui/sheets/BoostTransactionSheet.kt @@ -255,7 +255,7 @@ private fun DefaultModeContent( ) Icon( - painter = painterResource(R.drawable.ic_pencil), + painter = painterResource(R.drawable.ic_pencil_simple), contentDescription = stringResource(R.string.common__edit), modifier = Modifier .size(16.dp) diff --git a/app/src/main/res/drawable/ic_pencil.xml b/app/src/main/res/drawable/ic_pencil_simple.xml similarity index 100% rename from app/src/main/res/drawable/ic_pencil.xml rename to app/src/main/res/drawable/ic_pencil_simple.xml From efea810c91d68772bdb424d75c89ce0bac3d6538 Mon Sep 17 00:00:00 2001 From: Ovi Trif Date: Wed, 6 Aug 2025 14:36:28 +0200 Subject: [PATCH 14/36] chore: fix WithdrawErrorScreen caps --- .../bitkit/ui/screens/wallets/withdraw/WithdrawErrorScreen.kt | 4 ++-- app/src/main/java/to/bitkit/ui/sheets/SendSheet.kt | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/app/src/main/java/to/bitkit/ui/screens/wallets/withdraw/WithdrawErrorScreen.kt b/app/src/main/java/to/bitkit/ui/screens/wallets/withdraw/WithdrawErrorScreen.kt index 76d5a18da..bae5d34bf 100644 --- a/app/src/main/java/to/bitkit/ui/screens/wallets/withdraw/WithdrawErrorScreen.kt +++ b/app/src/main/java/to/bitkit/ui/screens/wallets/withdraw/WithdrawErrorScreen.kt @@ -31,7 +31,7 @@ import to.bitkit.ui.theme.Colors import to.bitkit.viewmodels.SendUiState @Composable -fun WithDrawErrorScreen( +fun WithdrawErrorScreen( uiState: SendUiState, onBack: () -> Unit, onClickScan: () -> Unit, @@ -102,7 +102,7 @@ fun WithDrawErrorScreen( private fun Preview() { AppThemeSurface { BottomSheetPreview { - WithDrawErrorScreen( + WithdrawErrorScreen( uiState = SendUiState( amount = 250_000u ), diff --git a/app/src/main/java/to/bitkit/ui/sheets/SendSheet.kt b/app/src/main/java/to/bitkit/ui/sheets/SendSheet.kt index f141a8695..0c1dd4981 100644 --- a/app/src/main/java/to/bitkit/ui/sheets/SendSheet.kt +++ b/app/src/main/java/to/bitkit/ui/sheets/SendSheet.kt @@ -26,7 +26,7 @@ import to.bitkit.ui.screens.wallets.send.SendFeeRateScreen import to.bitkit.ui.screens.wallets.send.SendPinCheckScreen import to.bitkit.ui.screens.wallets.send.SendQuickPayScreen import to.bitkit.ui.screens.wallets.send.SendRecipientScreen -import to.bitkit.ui.screens.wallets.withdraw.WithDrawErrorScreen +import to.bitkit.ui.screens.wallets.withdraw.WithdrawErrorScreen import to.bitkit.ui.screens.wallets.withdraw.WithdrawConfirmScreen import to.bitkit.ui.settings.support.SupportScreen import to.bitkit.ui.shared.modifiers.sheetHeight @@ -159,7 +159,7 @@ fun SendSheet( } composableWithDefaultTransitions { val uiState by appViewModel.sendUiState.collectAsStateWithLifecycle() - WithDrawErrorScreen( + WithdrawErrorScreen( uiState = uiState, onBack = { navController.popBackStack() }, onClickScan = { navController.navigate(SendRoute.QrScanner) }, From 6363daace91a02c4ebc35d9ceede99293098bf68 Mon Sep 17 00:00:00 2001 From: Ovi Trif Date: Wed, 6 Aug 2025 14:42:25 +0200 Subject: [PATCH 15/36] fix: remove divider transparency onclick --- .../to/bitkit/ui/screens/wallets/send/SendConfirmScreen.kt | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/app/src/main/java/to/bitkit/ui/screens/wallets/send/SendConfirmScreen.kt b/app/src/main/java/to/bitkit/ui/screens/wallets/send/SendConfirmScreen.kt index 8efcf19b3..37a135b6a 100644 --- a/app/src/main/java/to/bitkit/ui/screens/wallets/send/SendConfirmScreen.kt +++ b/app/src/main/java/to/bitkit/ui/screens/wallets/send/SendConfirmScreen.kt @@ -307,10 +307,7 @@ private fun OnChainDescription( ) { val fee by remember(uiState.speed) { mutableStateOf(FeeRate.fromSpeed(uiState.speed)) } Column(modifier = Modifier.fillMaxWidth()) { - Caption13Up( - text = stringResource(R.string.wallet__send_to), - color = Colors.White64, - ) + Caption13Up(text = stringResource(R.string.wallet__send_to), color = Colors.White64) Spacer(modifier = Modifier.height(8.dp)) BodySSB(text = uiState.address, maxLines = 1, overflow = TextOverflow.MiddleEllipsis) HorizontalDivider(modifier = Modifier.padding(top = 16.dp)) @@ -385,8 +382,8 @@ private fun OnChainDescription( } FillHeight() VerticalSpacer(16.dp) - HorizontalDivider() } + HorizontalDivider() } } } From 6713a698965b59e27ab7182e8bf30a066473498f Mon Sep 17 00:00:00 2001 From: Ovi Trif Date: Wed, 6 Aug 2025 14:49:40 +0200 Subject: [PATCH 16/36] fix: resetSendState race conditions --- app/src/main/java/to/bitkit/ui/ContentView.kt | 3 +-- .../java/to/bitkit/ui/sheets/SendSheet.kt | 4 +-- .../java/to/bitkit/viewmodels/AppViewModel.kt | 27 ++++++++----------- 3 files changed, 14 insertions(+), 20 deletions(-) diff --git a/app/src/main/java/to/bitkit/ui/ContentView.kt b/app/src/main/java/to/bitkit/ui/ContentView.kt index d7cbacb4c..e0ade4ad2 100644 --- a/app/src/main/java/to/bitkit/ui/ContentView.kt +++ b/app/src/main/java/to/bitkit/ui/ContentView.kt @@ -76,7 +76,6 @@ import to.bitkit.ui.screens.wallets.activity.ActivityExploreScreen import to.bitkit.ui.screens.wallets.activity.DateRangeSelectorSheet import to.bitkit.ui.screens.wallets.activity.TagSelectorSheet import to.bitkit.ui.screens.wallets.receive.ReceiveSheet -import to.bitkit.ui.sheets.SendSheet import to.bitkit.ui.screens.wallets.suggestion.BuyIntroScreen import to.bitkit.ui.screens.widgets.AddWidgetsScreen import to.bitkit.ui.screens.widgets.WidgetsIntroScreen @@ -137,6 +136,7 @@ import to.bitkit.ui.settings.transactionSpeed.TransactionSpeedSettingsScreen import to.bitkit.ui.sheets.BackupSheet import to.bitkit.ui.sheets.LnurlAuthSheet import to.bitkit.ui.sheets.PinSheet +import to.bitkit.ui.sheets.SendSheet import to.bitkit.ui.utils.AutoReadClipboardHandler import to.bitkit.ui.utils.composableWithDefaultTransitions import to.bitkit.ui.utils.screenSlideIn @@ -326,7 +326,6 @@ fun ContentView( walletViewModel = walletViewModel, startDestination = sheet.route, onComplete = { txSheet -> - appViewModel.resetSendState() appViewModel.hideSheet() appViewModel.clearClipboardForAutoRead() txSheet?.let { appViewModel.showNewTransactionSheet(it) } diff --git a/app/src/main/java/to/bitkit/ui/sheets/SendSheet.kt b/app/src/main/java/to/bitkit/ui/sheets/SendSheet.kt index 0c1dd4981..e680eb5bf 100644 --- a/app/src/main/java/to/bitkit/ui/sheets/SendSheet.kt +++ b/app/src/main/java/to/bitkit/ui/sheets/SendSheet.kt @@ -26,8 +26,8 @@ import to.bitkit.ui.screens.wallets.send.SendFeeRateScreen import to.bitkit.ui.screens.wallets.send.SendPinCheckScreen import to.bitkit.ui.screens.wallets.send.SendQuickPayScreen import to.bitkit.ui.screens.wallets.send.SendRecipientScreen -import to.bitkit.ui.screens.wallets.withdraw.WithdrawErrorScreen import to.bitkit.ui.screens.wallets.withdraw.WithdrawConfirmScreen +import to.bitkit.ui.screens.wallets.withdraw.WithdrawErrorScreen import to.bitkit.ui.settings.support.SupportScreen import to.bitkit.ui.shared.modifiers.sheetHeight import to.bitkit.ui.utils.composableWithDefaultTransitions @@ -43,8 +43,8 @@ fun SendSheet( startDestination: SendRoute = SendRoute.Recipient, onComplete: (NewTransactionSheetDetails?) -> Unit, ) { - // Reset on new user-initiated send LaunchedEffect(startDestination) { + // always reset state on new user-initiated send if (startDestination == SendRoute.Recipient) { appViewModel.resetSendState() appViewModel.resetQuickPayData() diff --git a/app/src/main/java/to/bitkit/viewmodels/AppViewModel.kt b/app/src/main/java/to/bitkit/viewmodels/AppViewModel.kt index b8a5afcba..e04f4b069 100644 --- a/app/src/main/java/to/bitkit/viewmodels/AppViewModel.kt +++ b/app/src/main/java/to/bitkit/viewmodels/AppViewModel.kt @@ -33,6 +33,7 @@ import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext import org.lightningdevkit.ldknode.Event import org.lightningdevkit.ldknode.PaymentId import org.lightningdevkit.ldknode.SpendableUtxo @@ -457,16 +458,16 @@ class AppViewModel @Inject constructor( } } - private suspend fun handleScan(result: String) { + private suspend fun handleScan(result: String) = withContext(bgDispatcher) { + // always reset state on new scan + resetSendState() + resetQuickPayData() + val scan = runCatching { decode(result) } .onFailure { Logger.error("Failed to decode scan result: '$result'", it) } .onSuccess { Logger.info("Handling scan data: $it") } .getOrNull() - // always reset state on new scan - resetSendState() - resetQuickPayData() - when (scan) { is Scanner.OnChain -> onScanOnchain(scan.invoice) is Scanner.Lightning -> onScanLightning(scan.invoice) @@ -589,7 +590,6 @@ class AppViewModel @Inject constructor( title = context.getString(R.string.other__lnurl_pay_error), description = context.getString(R.string.other__lnurl_pay_error_no_capacity), ) - resetSendState() return } @@ -636,7 +636,6 @@ class AppViewModel @Inject constructor( title = context.getString(R.string.other__lnurl_withdr_error), description = context.getString(R.string.other__lnurl_withdr_error_minmax) ) - resetSendState() return } @@ -938,7 +937,6 @@ class AppViewModel @Inject constructor( val lnurl = _sendUiState.value.lnurl as? LnurlParams.LnurlWithdraw if (lnurl == null) { - resetSendState() setSendEffect(SendEffect.NavigateToWithdrawError) return@launch } @@ -958,7 +956,6 @@ class AppViewModel @Inject constructor( ).getOrNull() if (invoice == null) { - resetSendState() setSendEffect(SendEffect.NavigateToWithdrawError) return@launch } @@ -976,7 +973,6 @@ class AppViewModel @Inject constructor( hideSheet() _sendUiState.update { it.copy(isLoading = false) } mainScreenEffect(MainScreenEffect.Navigate(Routes.Home)) - resetSendState() }.onFailure { _sendUiState.update { it.copy(isLoading = false) } setSendEffect(SendEffect.NavigateToWithdrawError) @@ -1057,12 +1053,11 @@ class AppViewModel @Inject constructor( fun resetQuickPayData() = _quickPayData.update { null } - fun resetSendState() { - viewModelScope.launch { - _sendUiState.value = SendUiState( - speed = settingsStore.data.first().defaultTransactionSpeed, - ) - } + suspend fun resetSendState() { + _sendUiState.value = SendUiState( + speed = settingsStore.data.first().defaultTransactionSpeed, + ) + Logger.debug("Send state reset") } // endregion From 510ee36be726cc4f4447e47a861369960bb64e49 Mon Sep 17 00:00:00 2001 From: Ovi Trif Date: Wed, 6 Aug 2025 15:02:30 +0200 Subject: [PATCH 17/36] refactor: extract FeeRate --- app/src/main/java/to/bitkit/models/FeeRate.kt | 59 +++++++++++++++++++ .../java/to/bitkit/models/TransactionSpeed.kt | 58 ------------------ 2 files changed, 59 insertions(+), 58 deletions(-) create mode 100644 app/src/main/java/to/bitkit/models/FeeRate.kt diff --git a/app/src/main/java/to/bitkit/models/FeeRate.kt b/app/src/main/java/to/bitkit/models/FeeRate.kt new file mode 100644 index 000000000..0ca13499e --- /dev/null +++ b/app/src/main/java/to/bitkit/models/FeeRate.kt @@ -0,0 +1,59 @@ +package to.bitkit.models + +import androidx.annotation.DrawableRes +import androidx.annotation.StringRes +import androidx.compose.ui.graphics.Color +import to.bitkit.R +import to.bitkit.ui.theme.Colors + +enum class FeeRate( + @StringRes val title: Int, + @StringRes val description: Int, + @DrawableRes val icon: Int, + val color: Color, +) { + FAST( + title = R.string.fee__fast__title, + description = R.string.fee__fast__description, + color = Colors.Brand, + icon = R.drawable.ic_speed_fast, + ), + NORMAL( + title = R.string.fee__normal__title, + description = R.string.fee__normal__description, + color = Colors.Brand, + icon = R.drawable.ic_speed_normal, + ), + SLOW( + title = R.string.fee__slow__title, + description = R.string.fee__slow__description, + color = Colors.Brand, + icon = R.drawable.ic_speed_slow, + ), + CUSTOM( + title = R.string.fee__custom__title, + description = R.string.fee__custom__description, + color = Colors.White64, + icon = R.drawable.ic_settings, + ); + + fun toSpeed(): TransactionSpeed { + return when (this) { + FAST -> TransactionSpeed.Fast + NORMAL -> TransactionSpeed.Medium + SLOW -> TransactionSpeed.Slow + CUSTOM -> TransactionSpeed.Custom(0u) + } + } + + companion object { + fun fromSpeed(speed: TransactionSpeed): FeeRate { + return when (speed) { + is TransactionSpeed.Fast -> FAST + is TransactionSpeed.Medium -> NORMAL + is TransactionSpeed.Slow -> SLOW + is TransactionSpeed.Custom -> CUSTOM + } + } + } +} diff --git a/app/src/main/java/to/bitkit/models/TransactionSpeed.kt b/app/src/main/java/to/bitkit/models/TransactionSpeed.kt index 68656d870..02e8f1e4a 100644 --- a/app/src/main/java/to/bitkit/models/TransactionSpeed.kt +++ b/app/src/main/java/to/bitkit/models/TransactionSpeed.kt @@ -1,11 +1,6 @@ package to.bitkit.models -import androidx.annotation.DrawableRes -import androidx.annotation.StringRes import androidx.compose.runtime.Composable -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.graphics.painter.Painter -import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import kotlinx.serialization.KSerializer import kotlinx.serialization.Serializable @@ -15,7 +10,6 @@ import kotlinx.serialization.descriptors.SerialDescriptor import kotlinx.serialization.encoding.Decoder import kotlinx.serialization.encoding.Encoder import to.bitkit.R -import to.bitkit.ui.theme.Colors @Serializable(with = TransactionSpeedSerializer::class) sealed class TransactionSpeed { @@ -72,55 +66,3 @@ fun TransactionSpeed.transactionSpeedUiText(): String { is TransactionSpeed.Custom -> stringResource(R.string.settings__fee__custom__value) } } - -enum class FeeRate( - @StringRes val title: Int, - @StringRes val description: Int, - @DrawableRes val icon: Int, - val color: Color, -) { - FAST( - title = R.string.fee__fast__title, - description = R.string.fee__fast__description, - color = Colors.Brand, - icon = R.drawable.ic_speed_fast, - ), - NORMAL( - title = R.string.fee__normal__title, - description = R.string.fee__normal__description, - color = Colors.Brand, - icon = R.drawable.ic_speed_normal, - ), - SLOW( - title = R.string.fee__slow__title, - description = R.string.fee__slow__description, - color = Colors.Brand, - icon = R.drawable.ic_speed_slow, - ), - CUSTOM( - title = R.string.fee__custom__title, - description = R.string.fee__custom__description, - color = Colors.White64, - icon = R.drawable.ic_settings, - ); - - fun toSpeed(): TransactionSpeed { - return when (this) { - FAST -> TransactionSpeed.Fast - NORMAL -> TransactionSpeed.Medium - SLOW -> TransactionSpeed.Slow - CUSTOM -> TransactionSpeed.Custom(0u) - } - } - - companion object { - fun fromSpeed(speed: TransactionSpeed): FeeRate { - return when (speed) { - is TransactionSpeed.Fast -> FAST - is TransactionSpeed.Medium -> NORMAL - is TransactionSpeed.Slow -> SLOW - is TransactionSpeed.Custom -> CUSTOM - } - } - } -} From 8be67a5d55f3f8b1d5b8fbb57a244bd836974206 Mon Sep 17 00:00:00 2001 From: Ovi Trif Date: Wed, 6 Aug 2025 16:25:27 +0200 Subject: [PATCH 18/36] feat: fee amount on send review screen --- .../screens/wallets/send/SendConfirmScreen.kt | 2 +- .../java/to/bitkit/viewmodels/AppViewModel.kt | 45 ++++++++++++++++++- 2 files changed, 44 insertions(+), 3 deletions(-) diff --git a/app/src/main/java/to/bitkit/ui/screens/wallets/send/SendConfirmScreen.kt b/app/src/main/java/to/bitkit/ui/screens/wallets/send/SendConfirmScreen.kt index 37a135b6a..b7733da18 100644 --- a/app/src/main/java/to/bitkit/ui/screens/wallets/send/SendConfirmScreen.kt +++ b/app/src/main/java/to/bitkit/ui/screens/wallets/send/SendConfirmScreen.kt @@ -341,7 +341,7 @@ private fun OnChainDescription( ) Row { BodySSB(stringResource(fee.title) + " (") - MoneySSB(sats = 210, accent = Colors.White) // TODO get from state + MoneySSB(sats = uiState.fee, accent = Colors.White) BodySSB(")") } Icon( diff --git a/app/src/main/java/to/bitkit/viewmodels/AppViewModel.kt b/app/src/main/java/to/bitkit/viewmodels/AppViewModel.kt index e04f4b069..3a3bc6d13 100644 --- a/app/src/main/java/to/bitkit/viewmodels/AppViewModel.kt +++ b/app/src/main/java/to/bitkit/viewmodels/AppViewModel.kt @@ -357,9 +357,14 @@ class AppViewModel @Inject constructor( } private fun onSpeedChange(speed: TransactionSpeed) { - _sendUiState.update { - it.copy(speed = speed) + if (speed !is TransactionSpeed.Custom) { + _sendUiState.update { + it.copy(speed = speed) + } + } else { + // TODO implement custom fee screen in next PRs } + refreshFeeIfNeeded() setSendEffect(SendEffect.PopBack) } @@ -393,6 +398,7 @@ class AppViewModel @Inject constructor( return } + refreshFeeIfNeeded() setSendEffect(SendEffect.NavigateToReview) } @@ -400,6 +406,7 @@ class AppViewModel @Inject constructor( _sendUiState.update { it.copy(selectedUtxos = utxos) } + refreshFeeIfNeeded() setSendEffect(SendEffect.NavigateToReview) } @@ -511,6 +518,7 @@ class AppViewModel @Inject constructor( ) if (quickPayHandled) return + refreshFeeIfNeeded() if (isMainScanner) { showSheet(Sheet.Send(SendRoute.Confirm)) } else { @@ -1053,6 +1061,34 @@ class AppViewModel @Inject constructor( fun resetQuickPayData() = _quickPayData.update { null } + private fun refreshFeeIfNeeded() { + val currentState = _sendUiState.value + if (currentState.payMethod != SendMethod.ONCHAIN || + currentState.amount == 0uL || + currentState.address.isEmpty() + ) { + return + } + + viewModelScope.launch(bgDispatcher) { + lightningRepo.calculateTotalFee( + amountSats = currentState.amount, + address = currentState.address, + speed = currentState.speed, + utxosToSpend = currentState.selectedUtxos, + ).onSuccess { feeSat -> + _sendUiState.update { + it.copy(fee = feeSat.toLong()) + } + }.onFailure { e -> + Logger.warn("Failed to calculate send fee", e = e, context = TAG) + _sendUiState.update { + it.copy(fee = 0) + } + } + } + } + suspend fun resetSendState() { _sendUiState.value = SendUiState( speed = settingsStore.data.first().defaultTransactionSpeed, @@ -1239,6 +1275,10 @@ class AppViewModel @Inject constructor( proceedWithPayment() } } + + companion object { + private const val TAG = "AppViewModel" + } } // region send contract @@ -1261,6 +1301,7 @@ data class SendUiState( val isLoading: Boolean = false, val speed: TransactionSpeed = TransactionSpeed.default(), val comment: String = "", + val fee: Long = 0, ) enum class AmountWarning(@StringRes val message: Int) { From 927619d9760a38719fe67f183921aef9ced8ffe4 Mon Sep 17 00:00:00 2001 From: Ovi Trif Date: Wed, 6 Aug 2025 17:40:55 +0200 Subject: [PATCH 19/36] chore: fix lint --- .../to/bitkit/ui/screens/wallets/send/SendFeeCustomScreen.kt | 4 ++-- .../to/bitkit/ui/screens/wallets/send/SendFeeRateScreen.kt | 2 ++ config/detekt/detekt.yml | 2 +- 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/app/src/main/java/to/bitkit/ui/screens/wallets/send/SendFeeCustomScreen.kt b/app/src/main/java/to/bitkit/ui/screens/wallets/send/SendFeeCustomScreen.kt index f3c827c42..d2c099eda 100644 --- a/app/src/main/java/to/bitkit/ui/screens/wallets/send/SendFeeCustomScreen.kt +++ b/app/src/main/java/to/bitkit/ui/screens/wallets/send/SendFeeCustomScreen.kt @@ -11,6 +11,7 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import to.bitkit.R +import to.bitkit.ui.components.BodyM import to.bitkit.ui.components.BottomSheetPreview import to.bitkit.ui.components.Display import to.bitkit.ui.components.FillHeight @@ -56,6 +57,7 @@ private fun Content( ) { SectionHeader(stringResource(R.string.wallet__send_fee_and_speed)) Display("TODO") + BodyM("Lint hack " + uiState.speed.toString()) FillHeight(min = 16.dp) @@ -81,5 +83,3 @@ private fun Preview() { } } } - -// TODO nav diff --git a/app/src/main/java/to/bitkit/ui/screens/wallets/send/SendFeeRateScreen.kt b/app/src/main/java/to/bitkit/ui/screens/wallets/send/SendFeeRateScreen.kt index 76bbbd7e2..842820b86 100644 --- a/app/src/main/java/to/bitkit/ui/screens/wallets/send/SendFeeRateScreen.kt +++ b/app/src/main/java/to/bitkit/ui/screens/wallets/send/SendFeeRateScreen.kt @@ -180,6 +180,7 @@ private fun FeeItem( } } +@Suppress("MagicNumber") @Preview(showSystemUi = true) @Composable private fun Preview() { @@ -201,6 +202,7 @@ private fun Preview() { } } +@Suppress("MagicNumber") @Preview(showSystemUi = true) @Composable private fun PreviewCustom() { diff --git a/config/detekt/detekt.yml b/config/detekt/detekt.yml index e68e27cd6..40d81cce9 100644 --- a/config/detekt/detekt.yml +++ b/config/detekt/detekt.yml @@ -634,7 +634,7 @@ style: - '1' - '2' ignoreHashCodeFunction: true - ignorePropertyDeclaration: false + ignorePropertyDeclaration: true ignoreLocalVariableDeclaration: false ignoreConstantDeclaration: true ignoreCompanionObjectPropertyDeclaration: true From c2c533c27e55fe9b7702880d2863ccbff5d6ac72 Mon Sep 17 00:00:00 2001 From: Ovi Trif Date: Wed, 6 Aug 2025 19:14:02 +0200 Subject: [PATCH 20/36] feat: lower log level for overly frequent logs --- .../java/to/bitkit/repositories/LightningRepo.kt | 12 ++++++------ .../main/java/to/bitkit/repositories/WalletRepo.kt | 2 +- app/src/main/java/to/bitkit/services/CoreService.kt | 5 ++--- .../main/java/to/bitkit/services/LightningService.kt | 4 ++-- 4 files changed, 11 insertions(+), 12 deletions(-) diff --git a/app/src/main/java/to/bitkit/repositories/LightningRepo.kt b/app/src/main/java/to/bitkit/repositories/LightningRepo.kt index a3e354ba6..8ff34522c 100644 --- a/app/src/main/java/to/bitkit/repositories/LightningRepo.kt +++ b/app/src/main/java/to/bitkit/repositories/LightningRepo.kt @@ -88,7 +88,7 @@ class LightningRepo @Inject constructor( waitTimeout: Duration = 1.minutes, operation: suspend () -> Result, ): Result = withContext(bgDispatcher) { - Logger.debug("Operation called: $operationName", context = TAG) + Logger.verbose("Operation called: $operationName", context = TAG) if (_lightningState.value.nodeLifecycleState.isRunning()) { return@withContext executeOperation(operationName, operation) @@ -107,7 +107,7 @@ class LightningRepo @Inject constructor( } // Otherwise, wait for it to transition to running state - Logger.debug("Waiting for node runs to execute $operationName", context = TAG) + Logger.verbose("Waiting for node runs to execute $operationName", context = TAG) _lightningState.first { it.nodeLifecycleState.isRunning() } Logger.debug("Operation executed: $operationName", context = TAG) true @@ -532,7 +532,7 @@ class LightningRepo @Inject constructor( Result.success(txId) } - private suspend fun determineUtxosToSpend( + suspend fun determineUtxosToSpend( sats: ULong, satsPerVByte: UInt, ): List? { @@ -544,14 +544,14 @@ class LightningRepo @Inject constructor( val allSpendableUtxos = lightningService.listSpendableOutputs().getOrThrow() if (coinSelectionPreference == CoinSelectionPreference.Consolidate) { - Logger.info("Consolidating by spending all ${allSpendableUtxos.size} UTXOs", context = TAG) + Logger.debug("Consolidating by spending all ${allSpendableUtxos.size} UTXOs", context = TAG) return allSpendableUtxos } val coinSelectionAlgorithm = coinSelectionPreference.toCoinSelectAlgorithm().getOrThrow() - Logger.info("Selecting UTXOs with algorithm: $coinSelectionAlgorithm for sats: $sats", context = TAG) - Logger.debug("All spendable UTXOs: $allSpendableUtxos", context = TAG) + Logger.debug("Selecting UTXOs with algorithm: $coinSelectionAlgorithm for sats: $sats", context = TAG) + Logger.verbose("All spendable UTXOs: $allSpendableUtxos", context = TAG) lightningService.selectUtxosWithAlgorithm( targetAmountSats = sats, diff --git a/app/src/main/java/to/bitkit/repositories/WalletRepo.kt b/app/src/main/java/to/bitkit/repositories/WalletRepo.kt index 0d06a9404..151c6703b 100644 --- a/app/src/main/java/to/bitkit/repositories/WalletRepo.kt +++ b/app/src/main/java/to/bitkit/repositories/WalletRepo.kt @@ -146,7 +146,7 @@ class WalletRepo @Inject constructor( } suspend fun syncNodeAndWallet(): Result = withContext(bgDispatcher) { - Logger.debug("Refreshing node and wallet state…") + Logger.verbose("Refreshing node and wallet state…") syncBalances() lightningRepo.sync().onSuccess { syncBalances() diff --git a/app/src/main/java/to/bitkit/services/CoreService.kt b/app/src/main/java/to/bitkit/services/CoreService.kt index b7f99faca..ec3c08c81 100644 --- a/app/src/main/java/to/bitkit/services/CoreService.kt +++ b/app/src/main/java/to/bitkit/services/CoreService.kt @@ -111,13 +111,12 @@ class CoreService @Inject constructor( /** Returns true if geo blocked */ suspend fun checkGeoStatus(): Boolean? { return ServiceQueue.CORE.background { - Logger.info("Checking geo status…", context = "GeoCheck") + Logger.verbose("Checking geo status…", context = "GeoCheck") val response = httpClient.get(Env.geoCheckUrl) - Logger.debug("Received geo status response: ${response.status.value}", context = "GeoCheck") when (response.status.value) { HttpStatusCode.OK.value -> { - Logger.info("Region allowed", context = "GeoCheck") + Logger.verbose("Region allowed", context = "GeoCheck") false } diff --git a/app/src/main/java/to/bitkit/services/LightningService.kt b/app/src/main/java/to/bitkit/services/LightningService.kt index 6d2b2ccf4..1765cb5ec 100644 --- a/app/src/main/java/to/bitkit/services/LightningService.kt +++ b/app/src/main/java/to/bitkit/services/LightningService.kt @@ -222,12 +222,12 @@ class LightningService @Inject constructor( suspend fun sync() { val node = this.node ?: throw ServiceError.NodeNotSetup - Logger.debug("Syncing LDK…") + Logger.verbose("Syncing LDK…") ServiceQueue.LDK.background { node.syncWallets() // launch { setMaxDustHtlcExposureForCurrentChannels() } } - Logger.info("LDK synced") + Logger.debug("LDK synced") } // private fun setMaxDustHtlcExposureForCurrentChannels() { From 317ef866991fe81f5f10b23857007c65c35b1f69 Mon Sep 17 00:00:00 2001 From: Ovi Trif Date: Wed, 6 Aug 2025 20:47:36 +0200 Subject: [PATCH 21/36] feat: add utxos to logs for calculateTotalFee --- app/src/main/java/to/bitkit/services/LightningService.kt | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/to/bitkit/services/LightningService.kt b/app/src/main/java/to/bitkit/services/LightningService.kt index 1765cb5ec..296b86273 100644 --- a/app/src/main/java/to/bitkit/services/LightningService.kt +++ b/app/src/main/java/to/bitkit/services/LightningService.kt @@ -556,7 +556,9 @@ class LightningService @Inject constructor( ): ULong { val node = this.node ?: throw ServiceError.NodeNotSetup - Logger.info("Calculating fee for $amountSats sats to $address, satsPerVByte=$satsPerVByte") + Logger.info( + "Calculating fee for $amountSats sats to $address, satsPerVByte=$satsPerVByte, UTXOs=${utxosToSpend?.size}" + ) return ServiceQueue.LDK.background { return@background try { From 479c0b6790ce2f1b3e390d6f76a97238d0c3b1fd Mon Sep 17 00:00:00 2001 From: Ovi Trif Date: Wed, 6 Aug 2025 20:49:58 +0200 Subject: [PATCH 22/36] fix: dont validate amount for LN in onchain send --- .../java/to/bitkit/viewmodels/AppViewModel.kt | 26 +++++++++---------- 1 file changed, 12 insertions(+), 14 deletions(-) diff --git a/app/src/main/java/to/bitkit/viewmodels/AppViewModel.kt b/app/src/main/java/to/bitkit/viewmodels/AppViewModel.kt index 3a3bc6d13..285c3fc8a 100644 --- a/app/src/main/java/to/bitkit/viewmodels/AppViewModel.kt +++ b/app/src/main/java/to/bitkit/viewmodels/AppViewModel.kt @@ -416,26 +416,24 @@ class AppViewModel @Inject constructor( ): Boolean { if (value.isBlank()) return false val amount = value.toULongOrNull() ?: return false + if (amount == 0uL) return false - val lnurl = _sendUiState.value.lnurl - - val isValidLNAmount = when (lnurl) { - null -> lightningRepo.canSend(amount) - is LnurlParams.LnurlPay -> { - val minSat = lnurl.data.minSendableSat() - val maxSat = lnurl.data.maxSendableSat() + return when (payMethod) { + SendMethod.LIGHTNING -> when (val lnurl = _sendUiState.value.lnurl) { + null -> lightningRepo.canSend(amount) + is LnurlParams.LnurlPay -> { + val minSat = lnurl.data.minSendableSat() + val maxSat = lnurl.data.maxSendableSat() - amount in minSat..maxSat && lightningRepo.canSend(amount) - } + amount in minSat..maxSat && lightningRepo.canSend(amount) + } - is LnurlParams.LnurlWithdraw -> { - amount < lnurl.data.maxWithdrawableSat() + is LnurlParams.LnurlWithdraw -> { + amount < lnurl.data.maxWithdrawableSat() + } } - } - return when (payMethod) { SendMethod.ONCHAIN -> amount > getMinOnchainTx() - else -> isValidLNAmount && amount > 0uL } } From 8aeaa1541ee245d051c03b17287d164b999ea88d Mon Sep 17 00:00:00 2001 From: Ovi Trif Date: Wed, 6 Aug 2025 23:52:22 +0200 Subject: [PATCH 23/36] fix: preselect utxos for deterministic fee estimation --- .../to/bitkit/repositories/LightningRepo.kt | 10 +- .../to/bitkit/services/LightningService.kt | 2 +- .../screens/wallets/send/SendFeeViewModel.kt | 94 +------------- .../java/to/bitkit/viewmodels/AppViewModel.kt | 118 ++++++++++++++---- 4 files changed, 106 insertions(+), 118 deletions(-) diff --git a/app/src/main/java/to/bitkit/repositories/LightningRepo.kt b/app/src/main/java/to/bitkit/repositories/LightningRepo.kt index 8ff34522c..74c9102f2 100644 --- a/app/src/main/java/to/bitkit/repositories/LightningRepo.kt +++ b/app/src/main/java/to/bitkit/repositories/LightningRepo.kt @@ -535,8 +535,8 @@ class LightningRepo @Inject constructor( suspend fun determineUtxosToSpend( sats: ULong, satsPerVByte: UInt, - ): List? { - return runCatching { + ): List? = withContext(bgDispatcher) { + return@withContext runCatching { val settings = settingsStore.data.first() if (settings.coinSelectAuto) { val coinSelectionPreference = settings.coinSelectPreference @@ -545,7 +545,7 @@ class LightningRepo @Inject constructor( if (coinSelectionPreference == CoinSelectionPreference.Consolidate) { Logger.debug("Consolidating by spending all ${allSpendableUtxos.size} UTXOs", context = TAG) - return allSpendableUtxos + return@withContext allSpendableUtxos } val coinSelectionAlgorithm = coinSelectionPreference.toCoinSelectAlgorithm().getOrThrow() @@ -558,7 +558,9 @@ class LightningRepo @Inject constructor( algorithm = coinSelectionAlgorithm, satsPerVByte = satsPerVByte, utxos = allSpendableUtxos, - ).getOrThrow() + ).onSuccess { + Logger.debug("Selected ${it.size} UTXOs", context = TAG) + }.getOrThrow() } else { null // let ldk-node handle utxos } diff --git a/app/src/main/java/to/bitkit/services/LightningService.kt b/app/src/main/java/to/bitkit/services/LightningService.kt index 296b86273..0fe14ba68 100644 --- a/app/src/main/java/to/bitkit/services/LightningService.kt +++ b/app/src/main/java/to/bitkit/services/LightningService.kt @@ -557,7 +557,7 @@ class LightningService @Inject constructor( val node = this.node ?: throw ServiceError.NodeNotSetup Logger.info( - "Calculating fee for $amountSats sats to $address, satsPerVByte=$satsPerVByte, UTXOs=${utxosToSpend?.size}" + "Calculating fee for $amountSats sats to $address, UTXOs=${utxosToSpend?.size}, satsPerVByte=$satsPerVByte" ) return ServiceQueue.LDK.background { diff --git a/app/src/main/java/to/bitkit/ui/screens/wallets/send/SendFeeViewModel.kt b/app/src/main/java/to/bitkit/ui/screens/wallets/send/SendFeeViewModel.kt index 0b820a74c..db725dfeb 100644 --- a/app/src/main/java/to/bitkit/ui/screens/wallets/send/SendFeeViewModel.kt +++ b/app/src/main/java/to/bitkit/ui/screens/wallets/send/SendFeeViewModel.kt @@ -1,33 +1,16 @@ package to.bitkit.ui.screens.wallets.send import androidx.lifecycle.ViewModel -import androidx.lifecycle.viewModelScope -import com.synonym.bitkitcore.FeeRates import dagger.hilt.android.lifecycle.HiltViewModel -import kotlinx.coroutines.CoroutineDispatcher -import kotlinx.coroutines.async -import kotlinx.coroutines.awaitAll -import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asStateFlow -import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.update -import kotlinx.coroutines.launch -import kotlinx.coroutines.withContext -import to.bitkit.di.BgDispatcher -import to.bitkit.ext.getSatsPerVByteFor import to.bitkit.models.FeeRate -import to.bitkit.models.TransactionSpeed -import to.bitkit.repositories.BlocktankRepo -import to.bitkit.repositories.LightningRepo import to.bitkit.viewmodels.SendUiState import javax.inject.Inject @HiltViewModel class SendFeeViewModel @Inject constructor( - @BgDispatcher private val bgDispatcher: CoroutineDispatcher, - private val lightningRepo: LightningRepo, - private val blocktankRepo: BlocktankRepo, ) : ViewModel() { private val _uiState = MutableStateFlow(SendFeeUiState()) val uiState = _uiState.asStateFlow() @@ -36,81 +19,14 @@ class SendFeeViewModel @Inject constructor( fun init(sendUiState: SendUiState) { this.sendUiState = sendUiState - viewModelScope.launch { - _uiState.update { - it.copy( - selected = FeeRate.fromSpeed(sendUiState.speed), - ) - } + _uiState.update { + it.copy( + selected = FeeRate.fromSpeed(sendUiState.speed), + fees = sendUiState.fees + ) } - viewModelScope.launch(bgDispatcher) { blocktankRepo.refreshInfo() } - collectState() } - private fun collectState() { - viewModelScope.launch(bgDispatcher) { - blocktankRepo.blocktankState.map { it.info?.onchain?.feeRates }.collect { feeRates -> - // init ui first - _uiState.update { - it.copy( - fees = speedEntries().associate { speed -> - val rate = FeeRate.fromSpeed(speed) - val sats = feeRates?.getSatsPerVByteFor(speed)?.toLong() ?: 0 - rate to sats - } - ) - } - // TODO try move to appViewModel and trigger load in bg onAmountContinue + store in sendUiState - loadFees(feeRates) - } - } - } - - private suspend fun loadFees(feeRates: FeeRates?) { - val newFees = getFees(feeRates) - _uiState.update { it.copy(fees = newFees) } - } - - private suspend fun getFees(feeRates: FeeRates?): Map { - return withContext(bgDispatcher) { - return@withContext coroutineScope { - speedEntries() - .map { speed -> - async { - val rate = FeeRate.fromSpeed(speed) - val fee = if (feeRates?.getSatsPerVByteFor(speed)?.toLong() != 0L) { - getFeeForSpeed(speed, feeRates) - } else { - 0 - } - rate to fee - } - }.awaitAll() - .toMap() - } - } - } - - private suspend fun getFeeForSpeed(speed: TransactionSpeed, feeRates: FeeRates?): Long { - return lightningRepo.calculateTotalFee( - amountSats = sendUiState.amount, - utxosToSpend = sendUiState.selectedUtxos, - speed = speed, - feeRates = feeRates, - ).getOrDefault(0u).toLong() - } - - private fun speedEntries(): List { - return listOf( - TransactionSpeed.Fast, - TransactionSpeed.Medium, - TransactionSpeed.Slow, - when (val speed = sendUiState.speed) { - is TransactionSpeed.Custom -> speed - else -> TransactionSpeed.Custom(0u) - } - ) - } } data class SendFeeUiState( diff --git a/app/src/main/java/to/bitkit/viewmodels/AppViewModel.kt b/app/src/main/java/to/bitkit/viewmodels/AppViewModel.kt index 285c3fc8a..612b7286e 100644 --- a/app/src/main/java/to/bitkit/viewmodels/AppViewModel.kt +++ b/app/src/main/java/to/bitkit/viewmodels/AppViewModel.kt @@ -22,6 +22,9 @@ import com.synonym.bitkitcore.validateBitcoinAddress import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.qualifiers.ApplicationContext import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.async +import kotlinx.coroutines.awaitAll +import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.delay import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableStateFlow @@ -46,6 +49,7 @@ import to.bitkit.di.BgDispatcher import to.bitkit.env.Env import to.bitkit.ext.WatchResult import to.bitkit.ext.getClipboardText +import to.bitkit.ext.getSatsPerVByteFor import to.bitkit.ext.maxSendableSat import to.bitkit.ext.maxWithdrawableSat import to.bitkit.ext.minSendableSat @@ -54,6 +58,7 @@ import to.bitkit.ext.rawId import to.bitkit.ext.removeSpaces import to.bitkit.ext.setClipboardText import to.bitkit.ext.watchUntil +import to.bitkit.models.FeeRate import to.bitkit.models.NewTransactionSheetDetails import to.bitkit.models.NewTransactionSheetDirection import to.bitkit.models.NewTransactionSheetType @@ -64,6 +69,7 @@ import to.bitkit.models.toActivityFilter import to.bitkit.models.toCoreNetworkType import to.bitkit.models.toTxType import to.bitkit.repositories.ActivityRepo +import to.bitkit.repositories.BlocktankRepo import to.bitkit.repositories.ConnectivityRepo import to.bitkit.repositories.ConnectivityState import to.bitkit.repositories.CurrencyRepo @@ -94,6 +100,7 @@ class AppViewModel @Inject constructor( private val settingsStore: SettingsStore, private val currencyRepo: CurrencyRepo, private val activityRepo: ActivityRepo, + private val blocktankRepo: BlocktankRepo, connectivityRepo: ConnectivityRepo, healthRepo: HealthRepo, ) : ViewModel() { @@ -356,15 +363,18 @@ class AppViewModel @Inject constructor( } } - private fun onSpeedChange(speed: TransactionSpeed) { + private suspend fun onSpeedChange(speed: TransactionSpeed) { if (speed !is TransactionSpeed.Custom) { + val shouldResetUtxos = settingsStore.data.first().coinSelectAuto _sendUiState.update { - it.copy(speed = speed) + it.copy( + speed = speed, + selectedUtxos = if (shouldResetUtxos) null else it.selectedUtxos + ) } } else { - // TODO implement custom fee screen in next PRs + // TODO implement custom fee screen in next PRs + refresh sendUiState fee & fees[speed] for new custom fee } - refreshFeeIfNeeded() setSendEffect(SendEffect.PopBack) } @@ -385,8 +395,10 @@ class AppViewModel @Inject constructor( _sendUiState.update { it.copy( amount = amount.toULongOrNull() ?: 0u, + selectedUtxos = null, ) } + if (_sendUiState.value.payMethod != SendMethod.LIGHTNING && !settingsStore.data.first().coinSelectAuto) { setSendEffect(SendEffect.NavigateToCoinSelection) return @@ -398,15 +410,19 @@ class AppViewModel @Inject constructor( return } - refreshFeeIfNeeded() + refreshOnchainSendIfNeeded() setSendEffect(SendEffect.NavigateToReview) } - private fun onCoinSelectionContinue(utxos: List) { + private suspend fun onCoinSelectionContinue(utxos: List) { _sendUiState.update { it.copy(selectedUtxos = utxos) } - refreshFeeIfNeeded() + val fee = getFeeEstimate() + _sendUiState.update { + it.copy(fee = fee) + } + setSendEffect(SendEffect.NavigateToReview) } @@ -516,7 +532,7 @@ class AppViewModel @Inject constructor( ) if (quickPayHandled) return - refreshFeeIfNeeded() + refreshOnchainSendIfNeeded() if (isMainScanner) { showSheet(Sheet.Send(SendRoute.Confirm)) } else { @@ -1003,7 +1019,7 @@ class AppViewModel @Inject constructor( } } - private suspend fun sendOnchain(address: String, amount: ULong, speed: TransactionSpeed? = null): Result { + private suspend fun sendOnchain(address: String, amount: ULong): Result { return lightningRepo.sendOnChain( address = address, sats = amount, @@ -1059,7 +1075,7 @@ class AppViewModel @Inject constructor( fun resetQuickPayData() = _quickPayData.update { null } - private fun refreshFeeIfNeeded() { + private fun refreshOnchainSendIfNeeded() { val currentState = _sendUiState.value if (currentState.payMethod != SendMethod.ONCHAIN || currentState.amount == 0uL || @@ -1068,23 +1084,76 @@ class AppViewModel @Inject constructor( return } + // refresh in background viewModelScope.launch(bgDispatcher) { - lightningRepo.calculateTotalFee( - amountSats = currentState.amount, - address = currentState.address, - speed = currentState.speed, - utxosToSpend = currentState.selectedUtxos, - ).onSuccess { feeSat -> - _sendUiState.update { - it.copy(fee = feeSat.toLong()) - } - }.onFailure { e -> - Logger.warn("Failed to calculate send fee", e = e, context = TAG) - _sendUiState.update { - it.copy(fee = 0) - } + // preselect utxos for deterministic fee estimation + if (settingsStore.data.first().coinSelectAuto && currentState.selectedUtxos == null) { + lightningRepo.getFeeRateForSpeed(currentState.speed) + .mapCatching { satsPerVByte -> + lightningRepo.determineUtxosToSpend( + sats = currentState.amount, + satsPerVByte = satsPerVByte.toUInt(), + ) + } + .onSuccess { utxos -> + _sendUiState.update { + it.copy(selectedUtxos = utxos) + } + } + } + refreshFeeEstimates() + } + } + + private suspend fun refreshFeeEstimates() = withContext(bgDispatcher) { + val currentState = _sendUiState.value + + // Refresh blocktank info to get latest fee rates + blocktankRepo.refreshInfo() + + val feeRates = blocktankRepo.blocktankState.value.info?.onchain?.feeRates + val speeds = listOf( + TransactionSpeed.Fast, + TransactionSpeed.Medium, + TransactionSpeed.Slow, + when (val speed = currentState.speed) { + is TransactionSpeed.Custom -> speed + else -> TransactionSpeed.Custom(0u) } + ) + + var currentFee = 0L + val feesMap = coroutineScope { + speeds.map { speed -> + async { + val rate = FeeRate.fromSpeed(speed) + val fee = if (feeRates?.getSatsPerVByteFor(speed) != 0u) getFeeEstimate(speed) else 0L + + if (speed == currentState.speed) { + currentFee = fee + } + + rate to fee + } + }.awaitAll().toMap() } + + _sendUiState.update { + it.copy( + fees = feesMap, + fee = currentFee, + ) + } + } + + private suspend fun getFeeEstimate(speed: TransactionSpeed? = null): Long { + val currentState = _sendUiState.value + return lightningRepo.calculateTotalFee( + amountSats = currentState.amount, + address = currentState.address, + speed = speed ?: currentState.speed, + utxosToSpend = currentState.selectedUtxos, + ).getOrDefault(0u).toLong() } suspend fun resetSendState() { @@ -1300,6 +1369,7 @@ data class SendUiState( val speed: TransactionSpeed = TransactionSpeed.default(), val comment: String = "", val fee: Long = 0, + val fees: Map = emptyMap(), ) enum class AmountWarning(@StringRes val message: Int) { From bc69bebe2550a44c7a1e5fa7dd0e8997c1621394 Mon Sep 17 00:00:00 2001 From: Ovi Trif Date: Wed, 6 Aug 2025 23:58:43 +0200 Subject: [PATCH 24/36] chore: cleanup --- app/src/main/java/to/bitkit/viewmodels/AppViewModel.kt | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/app/src/main/java/to/bitkit/viewmodels/AppViewModel.kt b/app/src/main/java/to/bitkit/viewmodels/AppViewModel.kt index 612b7286e..5dce7e5e1 100644 --- a/app/src/main/java/to/bitkit/viewmodels/AppViewModel.kt +++ b/app/src/main/java/to/bitkit/viewmodels/AppViewModel.kt @@ -449,7 +449,7 @@ class AppViewModel @Inject constructor( } } - SendMethod.ONCHAIN -> amount > getMinOnchainTx() + SendMethod.ONCHAIN -> amount > Env.TransactionDefaults.dustLimit.toULong() } } @@ -1060,10 +1060,6 @@ class AppViewModel @Inject constructor( } } - private fun getMinOnchainTx(): ULong { - return Env.TransactionDefaults.dustLimit.toULong() - } - fun clearClipboardForAutoRead() { viewModelScope.launch { val isAutoReadClipboardEnabled = settingsStore.data.first().enableAutoReadClipboard @@ -1160,7 +1156,6 @@ class AppViewModel @Inject constructor( _sendUiState.value = SendUiState( speed = settingsStore.data.first().defaultTransactionSpeed, ) - Logger.debug("Send state reset") } // endregion From b509af05ba6876a74ce1ccbe4232dc54f1a6e357 Mon Sep 17 00:00:00 2001 From: Ovi Trif Date: Thu, 7 Aug 2025 00:20:28 +0200 Subject: [PATCH 25/36] fix: preload fee rates before each send flow --- .../to/bitkit/repositories/LightningRepo.kt | 14 ++--------- .../java/to/bitkit/viewmodels/AppViewModel.kt | 25 +++++++++++++------ 2 files changed, 19 insertions(+), 20 deletions(-) diff --git a/app/src/main/java/to/bitkit/repositories/LightningRepo.kt b/app/src/main/java/to/bitkit/repositories/LightningRepo.kt index 74c9102f2..032096ce8 100644 --- a/app/src/main/java/to/bitkit/repositories/LightningRepo.kt +++ b/app/src/main/java/to/bitkit/repositories/LightningRepo.kt @@ -481,28 +481,18 @@ class LightningRepo @Inject constructor( Result.success(paymentId) } - /** - * Sends bitcoin to an on-chain address - * - * @param address The bitcoin address to send to - * @param sats The amount in satoshis to send - * @param speed The desired transaction speed determining the fee rate. If null, the user's default speed is used. - * @param utxosToSpend Manually specify UTXO's to spend if not null. - * @return A `Result` with the `Txid` of sent transaction, or an error if the transaction fails - * or the fee rate cannot be retrieved. - */ - suspend fun sendOnChain( address: Address, sats: ULong, speed: TransactionSpeed? = null, utxosToSpend: List? = null, + feeRates: FeeRates? = null, isTransfer: Boolean = false, channelId: String? = null, ): Result = executeWhenNodeRunning("Send on-chain") { val transactionSpeed = speed ?: settingsStore.data.first().defaultTransactionSpeed - val satsPerVByte = getFeeRateForSpeed(transactionSpeed).getOrThrow().toUInt() + val satsPerVByte = getFeeRateForSpeed(transactionSpeed, feeRates).getOrThrow().toUInt() // if utxos are manually specified, use them, otherwise run auto coin select if enabled val finalUtxosToSpend = utxosToSpend ?: determineUtxosToSpend( diff --git a/app/src/main/java/to/bitkit/viewmodels/AppViewModel.kt b/app/src/main/java/to/bitkit/viewmodels/AppViewModel.kt index 5dce7e5e1..a6ce6ad92 100644 --- a/app/src/main/java/to/bitkit/viewmodels/AppViewModel.kt +++ b/app/src/main/java/to/bitkit/viewmodels/AppViewModel.kt @@ -9,6 +9,7 @@ import androidx.core.net.toUri import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.synonym.bitkitcore.ActivityFilter +import com.synonym.bitkitcore.FeeRates import com.synonym.bitkitcore.LightningInvoice import com.synonym.bitkitcore.LnurlAuthData import com.synonym.bitkitcore.LnurlChannelData @@ -1104,10 +1105,6 @@ class AppViewModel @Inject constructor( private suspend fun refreshFeeEstimates() = withContext(bgDispatcher) { val currentState = _sendUiState.value - // Refresh blocktank info to get latest fee rates - blocktankRepo.refreshInfo() - - val feeRates = blocktankRepo.blocktankState.value.info?.onchain?.feeRates val speeds = listOf( TransactionSpeed.Fast, TransactionSpeed.Medium, @@ -1123,7 +1120,7 @@ class AppViewModel @Inject constructor( speeds.map { speed -> async { val rate = FeeRate.fromSpeed(speed) - val fee = if (feeRates?.getSatsPerVByteFor(speed) != 0u) getFeeEstimate(speed) else 0L + val fee = if (currentState.feeRates?.getSatsPerVByteFor(speed) != 0u) getFeeEstimate(speed) else 0 if (speed == currentState.speed) { currentFee = fee @@ -1149,13 +1146,24 @@ class AppViewModel @Inject constructor( address = currentState.address, speed = speed ?: currentState.speed, utxosToSpend = currentState.selectedUtxos, + feeRates = currentState.feeRates, ).getOrDefault(0u).toLong() } suspend fun resetSendState() { - _sendUiState.value = SendUiState( - speed = settingsStore.data.first().defaultTransactionSpeed, - ) + val speed = settingsStore.data.first().defaultTransactionSpeed + val rates = let { + // Refresh blocktank info to get latest fee rates + blocktankRepo.refreshInfo() + blocktankRepo.blocktankState.value.info?.onchain?.feeRates + } + + _sendUiState.update { + SendUiState( + speed = speed, + feeRates = rates, + ) + } } // endregion @@ -1363,6 +1371,7 @@ data class SendUiState( val isLoading: Boolean = false, val speed: TransactionSpeed = TransactionSpeed.default(), val comment: String = "", + val feeRates: FeeRates? = null, val fee: Long = 0, val fees: Map = emptyMap(), ) From ef05e09dcf59b514ebc2853b4d90b8ca3765ddc7 Mon Sep 17 00:00:00 2001 From: Ovi Trif Date: Thu, 7 Aug 2025 00:25:19 +0200 Subject: [PATCH 26/36] refactor: rename to NavigateToConfirm --- .../ui/screens/wallets/send/SendFeeViewModel.kt | 1 - app/src/main/java/to/bitkit/ui/sheets/SendSheet.kt | 2 +- .../main/java/to/bitkit/viewmodels/AppViewModel.kt | 12 ++++++------ 3 files changed, 7 insertions(+), 8 deletions(-) diff --git a/app/src/main/java/to/bitkit/ui/screens/wallets/send/SendFeeViewModel.kt b/app/src/main/java/to/bitkit/ui/screens/wallets/send/SendFeeViewModel.kt index db725dfeb..1dda3a11e 100644 --- a/app/src/main/java/to/bitkit/ui/screens/wallets/send/SendFeeViewModel.kt +++ b/app/src/main/java/to/bitkit/ui/screens/wallets/send/SendFeeViewModel.kt @@ -26,7 +26,6 @@ class SendFeeViewModel @Inject constructor( ) } } - } data class SendFeeUiState( diff --git a/app/src/main/java/to/bitkit/ui/sheets/SendSheet.kt b/app/src/main/java/to/bitkit/ui/sheets/SendSheet.kt index e680eb5bf..affae7ecf 100644 --- a/app/src/main/java/to/bitkit/ui/sheets/SendSheet.kt +++ b/app/src/main/java/to/bitkit/ui/sheets/SendSheet.kt @@ -64,7 +64,7 @@ fun SendSheet( is SendEffect.NavigateToAddress -> navController.navigate(SendRoute.Address) is SendEffect.NavigateToScan -> navController.navigate(SendRoute.QrScanner) is SendEffect.NavigateToCoinSelection -> navController.navigate(SendRoute.CoinSelection) - is SendEffect.NavigateToReview -> navController.navigate(SendRoute.Confirm) + is SendEffect.NavigateToConfirm -> navController.navigate(SendRoute.Confirm) is SendEffect.PopBack -> navController.popBackStack() is SendEffect.PaymentSuccess -> onComplete(it.sheet) is SendEffect.NavigateToQuickPay -> navController.navigate(SendRoute.QuickPay) diff --git a/app/src/main/java/to/bitkit/viewmodels/AppViewModel.kt b/app/src/main/java/to/bitkit/viewmodels/AppViewModel.kt index a6ce6ad92..87376c54b 100644 --- a/app/src/main/java/to/bitkit/viewmodels/AppViewModel.kt +++ b/app/src/main/java/to/bitkit/viewmodels/AppViewModel.kt @@ -412,7 +412,7 @@ class AppViewModel @Inject constructor( } refreshOnchainSendIfNeeded() - setSendEffect(SendEffect.NavigateToReview) + setSendEffect(SendEffect.NavigateToConfirm) } private suspend fun onCoinSelectionContinue(utxos: List) { @@ -424,7 +424,7 @@ class AppViewModel @Inject constructor( it.copy(fee = fee) } - setSendEffect(SendEffect.NavigateToReview) + setSendEffect(SendEffect.NavigateToConfirm) } private fun validateAmount( @@ -537,7 +537,7 @@ class AppViewModel @Inject constructor( if (isMainScanner) { showSheet(Sheet.Send(SendRoute.Confirm)) } else { - setSendEffect(SendEffect.NavigateToReview) + setSendEffect(SendEffect.NavigateToConfirm) } return } @@ -587,7 +587,7 @@ class AppViewModel @Inject constructor( if (isMainScanner) { showSheet(Sheet.Send(SendRoute.Confirm)) } else { - setSendEffect(SendEffect.NavigateToReview) + setSendEffect(SendEffect.NavigateToConfirm) } return } @@ -634,7 +634,7 @@ class AppViewModel @Inject constructor( if (isMainScanner) { showSheet(Sheet.Send(SendRoute.Confirm)) } else { - setSendEffect(SendEffect.NavigateToReview) + setSendEffect(SendEffect.NavigateToConfirm) } return } @@ -1389,7 +1389,7 @@ sealed class SendEffect { data object NavigateToAddress : SendEffect() data object NavigateToAmount : SendEffect() data object NavigateToScan : SendEffect() - data object NavigateToReview : SendEffect() + data object NavigateToConfirm : SendEffect() data object PopBack : SendEffect() data object NavigateToWithdrawConfirm : SendEffect() data object NavigateToWithdrawError : SendEffect() From 29092ce9c339ea4f23f3e96835c107517ca58287 Mon Sep 17 00:00:00 2001 From: Ovi Trif Date: Thu, 7 Aug 2025 00:58:05 +0200 Subject: [PATCH 27/36] fix: refresh fee estimate on speed change --- app/src/main/java/to/bitkit/viewmodels/AppViewModel.kt | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/to/bitkit/viewmodels/AppViewModel.kt b/app/src/main/java/to/bitkit/viewmodels/AppViewModel.kt index 87376c54b..25a995c17 100644 --- a/app/src/main/java/to/bitkit/viewmodels/AppViewModel.kt +++ b/app/src/main/java/to/bitkit/viewmodels/AppViewModel.kt @@ -370,12 +370,14 @@ class AppViewModel @Inject constructor( _sendUiState.update { it.copy( speed = speed, - selectedUtxos = if (shouldResetUtxos) null else it.selectedUtxos + fee = it.fees.getOrDefault(FeeRate.fromSpeed(speed), 0), + selectedUtxos = if (shouldResetUtxos) null else it.selectedUtxos, ) } } else { // TODO implement custom fee screen in next PRs + refresh sendUiState fee & fees[speed] for new custom fee } + refreshOnchainSendIfNeeded() setSendEffect(SendEffect.PopBack) } From 3e15ec74e64f8947df7634641581e52c8a363151 Mon Sep 17 00:00:00 2001 From: Ovi Trif Date: Thu, 7 Aug 2025 00:59:18 +0200 Subject: [PATCH 28/36] fix: pass feeRates to getFeeRateForSpeed --- app/src/main/java/to/bitkit/viewmodels/AppViewModel.kt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/to/bitkit/viewmodels/AppViewModel.kt b/app/src/main/java/to/bitkit/viewmodels/AppViewModel.kt index 25a995c17..0049577ca 100644 --- a/app/src/main/java/to/bitkit/viewmodels/AppViewModel.kt +++ b/app/src/main/java/to/bitkit/viewmodels/AppViewModel.kt @@ -1074,6 +1074,7 @@ class AppViewModel @Inject constructor( fun resetQuickPayData() = _quickPayData.update { null } + /** Reselect utxos for current amount & speed then refresh fees using updated utxos */ private fun refreshOnchainSendIfNeeded() { val currentState = _sendUiState.value if (currentState.payMethod != SendMethod.ONCHAIN || @@ -1087,7 +1088,7 @@ class AppViewModel @Inject constructor( viewModelScope.launch(bgDispatcher) { // preselect utxos for deterministic fee estimation if (settingsStore.data.first().coinSelectAuto && currentState.selectedUtxos == null) { - lightningRepo.getFeeRateForSpeed(currentState.speed) + lightningRepo.getFeeRateForSpeed(currentState.speed, currentState.feeRates) .mapCatching { satsPerVByte -> lightningRepo.determineUtxosToSpend( sats = currentState.amount, From 12b8b3a1f98c06d15558b0f9fc2126d7fe16f217 Mon Sep 17 00:00:00 2001 From: Ovi Trif Date: Thu, 7 Aug 2025 01:38:24 +0200 Subject: [PATCH 29/36] fix: only reset utxos if stsPerVByte changes --- .../java/to/bitkit/viewmodels/AppViewModel.kt | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/app/src/main/java/to/bitkit/viewmodels/AppViewModel.kt b/app/src/main/java/to/bitkit/viewmodels/AppViewModel.kt index 0049577ca..289cd49d0 100644 --- a/app/src/main/java/to/bitkit/viewmodels/AppViewModel.kt +++ b/app/src/main/java/to/bitkit/viewmodels/AppViewModel.kt @@ -367,11 +367,18 @@ class AppViewModel @Inject constructor( private suspend fun onSpeedChange(speed: TransactionSpeed) { if (speed !is TransactionSpeed.Custom) { val shouldResetUtxos = settingsStore.data.first().coinSelectAuto + val currentState = _sendUiState.value + + // Only reset utxos if the satsPerVByte actually changes + val currentSatsPerVByte = currentState.feeRates?.getSatsPerVByteFor(currentState.speed) + val newSatsPerVByte = currentState.feeRates?.getSatsPerVByteFor(speed) + val satsPerVByteChanged = currentSatsPerVByte != newSatsPerVByte + _sendUiState.update { it.copy( speed = speed, fee = it.fees.getOrDefault(FeeRate.fromSpeed(speed), 0), - selectedUtxos = if (shouldResetUtxos) null else it.selectedUtxos, + selectedUtxos = if (shouldResetUtxos && satsPerVByteChanged) null else it.selectedUtxos, ) } } else { @@ -421,11 +428,7 @@ class AppViewModel @Inject constructor( _sendUiState.update { it.copy(selectedUtxos = utxos) } - val fee = getFeeEstimate() - _sendUiState.update { - it.copy(fee = fee) - } - + refreshFeeEstimates() setSendEffect(SendEffect.NavigateToConfirm) } @@ -1354,6 +1357,7 @@ class AppViewModel @Inject constructor( } } + // region send contract data class SendUiState( val address: String = "", From b13ad378a1e353d0f42968271fb78d48169f24c8 Mon Sep 17 00:00:00 2001 From: Ovi Trif Date: Thu, 7 Aug 2025 09:44:21 +0200 Subject: [PATCH 30/36] chore: fix tests --- .../test/java/to/bitkit/repositories/LightningRepoTest.kt | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/app/src/test/java/to/bitkit/repositories/LightningRepoTest.kt b/app/src/test/java/to/bitkit/repositories/LightningRepoTest.kt index 13427a20b..36822a0c0 100644 --- a/app/src/test/java/to/bitkit/repositories/LightningRepoTest.kt +++ b/app/src/test/java/to/bitkit/repositories/LightningRepoTest.kt @@ -369,13 +369,14 @@ class LightningRepoTest : BaseUnitTest() { // Create a spy to mock the getFeeRateForSpeed method val spySut = spy(sut) - doReturn(Result.success(10uL)).whenever(spySut).getFeeRateForSpeed(any()) + doReturn(Result.success(10uL)).whenever(spySut).getFeeRateForSpeed(any(), anyOrNull()) val result = spySut.sendOnChain( address = "test_address", sats = 1000uL, speed = TransactionSpeed.Fast, - utxosToSpend = null, // This was the missing parameter! + utxosToSpend = null, + feeRates = null, isTransfer = true, channelId = "test_channel_id" ) From 3fc00b0c30e46db313659eb66d32696e6cd0e2a4 Mon Sep 17 00:00:00 2001 From: Ovi Trif Date: Thu, 7 Aug 2025 11:05:11 +0200 Subject: [PATCH 31/36] feat: log error type --- app/src/main/java/to/bitkit/utils/Errors.kt | 2 +- app/src/main/java/to/bitkit/utils/Logger.kt | 19 +++++++++++-------- 2 files changed, 12 insertions(+), 9 deletions(-) diff --git a/app/src/main/java/to/bitkit/utils/Errors.kt b/app/src/main/java/to/bitkit/utils/Errors.kt index a3db26171..da57e7b8a 100644 --- a/app/src/main/java/to/bitkit/utils/Errors.kt +++ b/app/src/main/java/to/bitkit/utils/Errors.kt @@ -12,7 +12,7 @@ open class AppError(override val message: String? = null) : Exception(message) { private const val serialVersionUID = 1L } - constructor(cause: Throwable) : this(cause.message) + constructor(cause: Throwable) : this("${cause::class.simpleName}='${cause.message}'") fun readResolve(): Any { // Return a new instance of the class, or handle it if needed diff --git a/app/src/main/java/to/bitkit/utils/Logger.kt b/app/src/main/java/to/bitkit/utils/Logger.kt index 1566bc859..20d54324d 100644 --- a/app/src/main/java/to/bitkit/utils/Logger.kt +++ b/app/src/main/java/to/bitkit/utils/Logger.kt @@ -18,6 +18,7 @@ import java.util.concurrent.Executors object Logger { private const val TAG = "APP" + private const val COMPACT = false private val singleThreadDispatcher = Executors .newSingleThreadExecutor { Thread(it, "bitkit.log").apply { priority = Thread.NORM_PRIORITY - 1 } } @@ -69,9 +70,9 @@ object Logger { file: String = getCallerFile(), line: Int = getCallerLine(), ) { - val errMsg = e?.message?.let { " (err: '$it')" } ?: "" - val message = format("WARN⚠️: $msg$errMsg", context, file, line) - Log.w(TAG, message, e) + val errMsg = e?.let { "[${e::class.simpleName}='${e.message}']" }.orEmpty() + val message = format("WARN⚠️: $msg $errMsg", context, file, line) + if (COMPACT) Log.w(TAG, message) else Log.w(TAG, message, e) saveToFile(message) } @@ -82,9 +83,9 @@ object Logger { file: String = getCallerFile(), line: Int = getCallerLine(), ) { - val errMsg = e?.message?.let { " (err: '$it')" } ?: "" - val message = format("ERROR❌️: $msg$errMsg", context, file, line) - Log.e(TAG, message, e) + val errMsg = e?.let { "[${e::class.simpleName}='${e.message}']" }.orEmpty() + val message = format("ERROR❌️: $msg $errMsg", context, file, line) + if (COMPACT) Log.e(TAG, message) else Log.e(TAG, message, e) saveToFile(message) } @@ -110,8 +111,10 @@ object Logger { saveToFile(message) } - private fun format(message: Any, context: String, file: String, line: Int): String { - return "$message ${if (context.isNotEmpty()) "- $context " else ""}[$file:$line]" + private fun format(message: String, context: String, file: String, line: Int): String { + val message = message.trim() + val context = if (context.isNotEmpty()) "- $context" else "" + return "$message$context [$file:$line]" } private fun getCallerFile(): String { From deb5de7c3a3958b1fa80eebd92dce0218176b8bc Mon Sep 17 00:00:00 2001 From: Ovi Trif Date: Thu, 7 Aug 2025 11:11:47 +0200 Subject: [PATCH 32/36] feat: new lightning repo tests --- .../bitkit/repositories/LightningRepoTest.kt | 62 +++++++++++++++++++ 1 file changed, 62 insertions(+) diff --git a/app/src/test/java/to/bitkit/repositories/LightningRepoTest.kt b/app/src/test/java/to/bitkit/repositories/LightningRepoTest.kt index 36822a0c0..fac264218 100644 --- a/app/src/test/java/to/bitkit/repositories/LightningRepoTest.kt +++ b/app/src/test/java/to/bitkit/repositories/LightningRepoTest.kt @@ -2,12 +2,14 @@ package to.bitkit.repositories import app.cash.turbine.test import com.google.firebase.messaging.FirebaseMessaging +import com.synonym.bitkitcore.FeeRates import kotlinx.coroutines.flow.flowOf import org.junit.Before import org.junit.Test import org.lightningdevkit.ldknode.ChannelDetails import org.lightningdevkit.ldknode.NodeStatus import org.lightningdevkit.ldknode.PaymentDetails +import org.lightningdevkit.ldknode.SpendableUtxo import org.mockito.kotlin.any import org.mockito.kotlin.anyOrNull import org.mockito.kotlin.argumentCaptor @@ -26,11 +28,13 @@ import to.bitkit.data.SettingsStore import to.bitkit.data.dto.TransactionMetadata import to.bitkit.data.keychain.Keychain import to.bitkit.ext.createChannelDetails +import to.bitkit.models.CoinSelectionPreference import to.bitkit.models.ElectrumServer import to.bitkit.models.LnPeer import to.bitkit.models.NodeLifecycleState import to.bitkit.models.TransactionSpeed import to.bitkit.services.BlocktankNotificationsService +import to.bitkit.services.BlocktankService import to.bitkit.services.CoreService import to.bitkit.services.LdkNodeEventBus import to.bitkit.services.LightningService @@ -38,6 +42,7 @@ import to.bitkit.services.LnurlService import to.bitkit.test.BaseUnitTest import kotlin.test.assertEquals import kotlin.test.assertFalse +import kotlin.test.assertNotNull import kotlin.test.assertNull import kotlin.test.assertTrue @@ -433,4 +438,61 @@ class LightningRepoTest : BaseUnitTest() { assertTrue(result.isFailure) } + + @Test + fun `getFeeRateForSpeed should use provided feeRates`() = test { + val mockFeeRates = mock() + whenever(mockFeeRates.mid).thenReturn(20u) + + val result = sut.getFeeRateForSpeed(TransactionSpeed.Medium, mockFeeRates) + + assertTrue(result.isSuccess) + assertEquals(20uL, result.getOrNull()) + } + + @Test + fun `getFeeRateForSpeed should fetch from blocktank when feeRates is null`() = test { + val mockFeeRates = mock() + whenever(mockFeeRates.fast).thenReturn(30u) + val blocktank = mock() + whenever(blocktank.getFees()).thenReturn(Result.success(mockFeeRates)) + whenever(coreService.blocktank).thenReturn(blocktank) + + val result = sut.getFeeRateForSpeed(TransactionSpeed.Fast, null) + + assertTrue(result.isSuccess) + assertEquals(30uL, result.getOrNull()) + } + + @Test + fun `determineUtxosToSpend should return null when coinSelectAuto is false`() = test { + val mockSettingsData = SettingsData(coinSelectAuto = false) + whenever(settingsStore.data).thenReturn(flowOf(mockSettingsData)) + + val result = sut.determineUtxosToSpend(1000uL, 10u) + + assertNull(result) + } + + @Test + fun `determineUtxosToSpend should return all UTXOs when preference is Consolidate`() = test { + val mockSettingsData = SettingsData( + coinSelectAuto = true, + coinSelectPreference = CoinSelectionPreference.Consolidate + ) + whenever(settingsStore.data).thenReturn(flowOf(mockSettingsData)) + + val mockUtxos = listOf( + mock(), + mock(), + mock() + ) + whenever(lightningService.listSpendableOutputs()).thenReturn(Result.success(mockUtxos)) + + val result = sut.determineUtxosToSpend(1000uL, 10u) + + assertNotNull(result) + assertEquals(3, result.size) + assertEquals(mockUtxos, result) + } } From 5a932018bf4fc25365bc0b8f8e31e4d92478c85e Mon Sep 17 00:00:00 2001 From: Ovi Trif Date: Thu, 7 Aug 2025 11:49:27 +0200 Subject: [PATCH 33/36] chore: fix boost tests --- .../sheets/BoostTransactionViewModelTest.kt | 26 +++++++++---------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/app/src/test/java/to/bitkit/ui/sheets/BoostTransactionViewModelTest.kt b/app/src/test/java/to/bitkit/ui/sheets/BoostTransactionViewModelTest.kt index 6d4c91e7d..827a46fe8 100644 --- a/app/src/test/java/to/bitkit/ui/sheets/BoostTransactionViewModelTest.kt +++ b/app/src/test/java/to/bitkit/ui/sheets/BoostTransactionViewModelTest.kt @@ -88,9 +88,9 @@ class BoostTransactionViewModelTest : BaseUnitTest() { @Test fun `setupActivity should set loading state initially`() = runTest { - whenever(lightningRepo.getFeeRateForSpeed(any())) + whenever(lightningRepo.getFeeRateForSpeed(any(), anyOrNull())) .thenReturn(Result.success(testFeeRate)) - whenever(lightningRepo.calculateTotalFee(any(), anyOrNull(), anyOrNull(), anyOrNull())) + whenever(lightningRepo.calculateTotalFee(any(), anyOrNull(), anyOrNull(), anyOrNull(), anyOrNull())) .thenReturn(Result.success(testTotalFee)) sut.uiState.test { @@ -105,15 +105,15 @@ class BoostTransactionViewModelTest : BaseUnitTest() { @Test fun `setupActivity should call correct repository methods for sent transaction`() = runTest { - whenever(lightningRepo.getFeeRateForSpeed(TransactionSpeed.Fast)) + whenever(lightningRepo.getFeeRateForSpeed(eq(TransactionSpeed.Fast), anyOrNull())) .thenReturn(Result.success(testFeeRate)) - whenever(lightningRepo.calculateTotalFee(any(), anyOrNull(), anyOrNull(), anyOrNull())) + whenever(lightningRepo.calculateTotalFee(any(), anyOrNull(), anyOrNull(), anyOrNull(), anyOrNull())) .thenReturn(Result.success(testTotalFee)) sut.setupActivity(mockActivitySent) - verify(lightningRepo).getFeeRateForSpeed(TransactionSpeed.Fast) - verify(lightningRepo).calculateTotalFee(any(), anyOrNull(), anyOrNull(), anyOrNull()) + verify(lightningRepo).getFeeRateForSpeed(eq(TransactionSpeed.Fast), anyOrNull()) + verify(lightningRepo).calculateTotalFee(any(), anyOrNull(), anyOrNull(), anyOrNull(), anyOrNull()) } @Test @@ -128,7 +128,7 @@ class BoostTransactionViewModelTest : BaseUnitTest() { sut.setupActivity(receivedActivity) verify(lightningRepo).calculateCpfpFeeRate(eq(mockTxId)) - verify(lightningRepo, never()).getFeeRateForSpeed(any()) + verify(lightningRepo, never()).getFeeRateForSpeed(any(), anyOrNull()) } @Test @@ -152,9 +152,9 @@ class BoostTransactionViewModelTest : BaseUnitTest() { @Test fun `onChangeAmount should emit OnMaxFee when at maximum rate`() = runTest { - whenever(lightningRepo.getFeeRateForSpeed(any())) + whenever(lightningRepo.getFeeRateForSpeed(any(), anyOrNull())) .thenReturn(Result.success(100UL)) // MAX_FEE_RATE - whenever(lightningRepo.calculateTotalFee(any(), anyOrNull(), anyOrNull(), anyOrNull())) + whenever(lightningRepo.calculateTotalFee(any(), anyOrNull(), anyOrNull(), anyOrNull(), anyOrNull())) .thenReturn(Result.success(testTotalFee)) sut.setupActivity(mockActivitySent) @@ -167,9 +167,9 @@ class BoostTransactionViewModelTest : BaseUnitTest() { @Test fun `onChangeAmount should emit OnMinFee when at minimum rate`() = runTest { - whenever(lightningRepo.getFeeRateForSpeed(any())) + whenever(lightningRepo.getFeeRateForSpeed(any(), anyOrNull())) .thenReturn(Result.success(1UL)) // MIN_FEE_RATE - whenever(lightningRepo.calculateTotalFee(any(), anyOrNull(), anyOrNull(), anyOrNull())) + whenever(lightningRepo.calculateTotalFee(any(), anyOrNull(), anyOrNull(), anyOrNull(), anyOrNull())) .thenReturn(Result.success(testTotalFee)) sut.setupActivity(mockActivitySent) @@ -182,7 +182,7 @@ class BoostTransactionViewModelTest : BaseUnitTest() { @Test fun `setupActivity failure should emit OnBoostFailed`() = runTest { - whenever(lightningRepo.getFeeRateForSpeed(any())) + whenever(lightningRepo.getFeeRateForSpeed(any(), anyOrNull())) .thenReturn(Result.failure(Exception("Fee estimation failed"))) sut.boostTransactionEffect.test { @@ -199,7 +199,7 @@ class BoostTransactionViewModelTest : BaseUnitTest() { whenever(lightningRepo.calculateCpfpFeeRate(any())) .thenReturn(Result.success(testFeeRate)) - whenever(lightningRepo.calculateTotalFee(any(), anyOrNull(), anyOrNull(), anyOrNull())) + whenever(lightningRepo.calculateTotalFee(any(), anyOrNull(), anyOrNull(), anyOrNull(), anyOrNull())) .thenReturn(Result.success(testTotalFee)) whenever(walletRepo.getOnchainAddress()) .thenReturn(mockAddress) From 3e671b7d590eadf4d76c5e8debc61c1e610291eb Mon Sep 17 00:00:00 2001 From: Ovi Trif Date: Thu, 7 Aug 2025 12:02:44 +0200 Subject: [PATCH 34/36] chore: update detekt baseline --- app/detekt-baseline.xml | 54 ++++++----------------------------------- 1 file changed, 7 insertions(+), 47 deletions(-) diff --git a/app/detekt-baseline.xml b/app/detekt-baseline.xml index d7f87b843..9ca135adb 100644 --- a/app/detekt-baseline.xml +++ b/app/detekt-baseline.xml @@ -25,7 +25,6 @@ ArgumentListWrapping:Bip39Test.kt$Bip39Test$(listOf("abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon").validBip39Checksum()) ArgumentListWrapping:BlocktankRegtestScreen.kt$( verticalAlignment = CenterVertically, modifier = Modifier .padding(vertical = 4.dp) .fillMaxWidth() ) ArgumentListWrapping:BlocktankRegtestScreen.kt$("Initiating channel close with fundingTxId: $fundingTxId, vout: $vout, forceCloseAfter: $forceCloseAfter") - ArgumentListWrapping:BoostTransactionViewModel.kt$BoostTransactionViewModel$("Activity $newTxId not found. Caching data to try again on next sync", e = error, context = TAG) ArgumentListWrapping:EditInvoiceVM.kt$EditInvoiceVM$(effect) ArgumentListWrapping:ExternalConfirmScreen.kt$(R.string.lightning__transfer__confirm) ArgumentListWrapping:ExternalConfirmScreen.kt$(accentColor = Colors.Purple) @@ -101,7 +100,6 @@ ComposableParamOrder:AuthCheckView.kt$PinPad ComposableParamOrder:BalanceHeaderView.kt$BalanceHeader ComposableParamOrder:BlockCard.kt$BlockCard - ComposableParamOrder:BoostTransactionSheet.kt$BoostTransactionContent ComposableParamOrder:BoostTransactionSheet.kt$BoostTransactionSheet ComposableParamOrder:BoostTransactionSheet.kt$QuantityButton ComposableParamOrder:CalculatorCard.kt$CalculatorCard @@ -192,7 +190,6 @@ EnumNaming:BlocktankNotificationType.kt$BlocktankNotificationType$mutualClose EnumNaming:BlocktankNotificationType.kt$BlocktankNotificationType$orderPaymentConfirmed EnumNaming:BlocktankNotificationType.kt$BlocktankNotificationType$wakeToTimeout - Filename:BoostTransactionViewModelTest.kt$to.bitkit.ui.screens.wallets.activity.BoostTransactionViewModelTest.kt Filename:vss_rust_client_ffi.kt$uniffi.vss_rust_client_ffi.vss_rust_client_ffi.kt ForbiddenComment:ActivityDetailScreen.kt$/* TODO: Implement assign functionality */ ForbiddenComment:ActivityDetailScreen.kt$// TODO: handle isTransfer @@ -334,14 +331,12 @@ ImplicitDefaultLocale:BlocksService.kt$BlocksService$String.format("%.2f", blockInfo.difficulty / 1_000_000_000_000.0) ImplicitDefaultLocale:PriceService.kt$PriceService$String.format("%.2f", price) ImportOrdering:ActivityListFilter.kt$import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.width import androidx.compose.material3.Tab import androidx.compose.material3.TabRow import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.LocalFocusManager import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import to.bitkit.R import to.bitkit.ui.components.SearchInputIconButton import to.bitkit.ui.components.SearchInput import to.bitkit.ui.theme.AppThemeSurface - ImportOrdering:AppViewModel.kt$import android.content.Context import androidx.annotation.StringRes import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue import androidx.core.net.toUri import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.synonym.bitkitcore.ActivityFilter import com.synonym.bitkitcore.LightningInvoice import com.synonym.bitkitcore.LnurlAuthData import com.synonym.bitkitcore.LnurlChannelData import com.synonym.bitkitcore.LnurlPayData import com.synonym.bitkitcore.LnurlWithdrawData import com.synonym.bitkitcore.OnChainInvoice import com.synonym.bitkitcore.PaymentType import com.synonym.bitkitcore.Scanner import com.synonym.bitkitcore.decode import com.synonym.bitkitcore.validateBitcoinAddress import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.qualifiers.ApplicationContext import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.delay import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.asSharedFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch import org.lightningdevkit.ldknode.Event import org.lightningdevkit.ldknode.PaymentId import org.lightningdevkit.ldknode.SpendableUtxo import org.lightningdevkit.ldknode.Txid import to.bitkit.R import to.bitkit.data.SettingsStore import to.bitkit.data.keychain.Keychain import to.bitkit.data.resetPin import to.bitkit.di.BgDispatcher import to.bitkit.env.Env import to.bitkit.ext.WatchResult import to.bitkit.ext.getClipboardText import to.bitkit.ext.maxSendableSat import to.bitkit.ext.maxWithdrawableSat import to.bitkit.ext.minSendableSat import to.bitkit.ext.minWithdrawableSat import to.bitkit.ext.rawId import to.bitkit.ext.removeSpaces import to.bitkit.ext.setClipboardText import to.bitkit.ext.watchUntil import to.bitkit.models.NewTransactionSheetDetails import to.bitkit.models.NewTransactionSheetDirection import to.bitkit.models.NewTransactionSheetType import to.bitkit.models.Suggestion import to.bitkit.models.Toast import to.bitkit.models.TransactionSpeed import to.bitkit.models.toActivityFilter import to.bitkit.models.toCoreNetworkType import to.bitkit.models.toTxType import to.bitkit.repositories.ActivityRepo import to.bitkit.repositories.ConnectivityRepo import to.bitkit.repositories.ConnectivityState import to.bitkit.repositories.CurrencyRepo import to.bitkit.repositories.HealthRepo import to.bitkit.repositories.LightningRepo import to.bitkit.repositories.WalletRepo import to.bitkit.services.CoreService import to.bitkit.services.LdkNodeEventBus import to.bitkit.ui.Routes import to.bitkit.ui.components.Sheet import to.bitkit.ui.sheets.SendRoute import to.bitkit.ui.shared.toast.ToastEventBus import to.bitkit.utils.Logger import java.math.BigDecimal import javax.inject.Inject ImportOrdering:BackupNavSheetViewModel.kt$import android.content.Context import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.qualifiers.ApplicationContext import kotlinx.coroutines.delay import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asSharedFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch import to.bitkit.R import to.bitkit.data.SettingsStore import to.bitkit.data.keychain.Keychain import to.bitkit.models.Toast import to.bitkit.ui.shared.toast.ToastEventBus import to.bitkit.utils.Logger import to.bitkit.ui.settings.backups.BackupContract.SideEffect import to.bitkit.ui.settings.backups.BackupContract.UiState import javax.inject.Inject ImportOrdering:BackupSettingsScreen.kt$import androidx.annotation.DrawableRes import androidx.compose.foundation.background import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.verticalScroll import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.Icon import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.platform.testTag import androidx.compose.ui.res.painterResource 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 androidx.navigation.NavController import to.bitkit.R import to.bitkit.ext.toLocalizedTimestamp import to.bitkit.models.BackupCategory import to.bitkit.models.BackupItemStatus import to.bitkit.models.uiIcon import to.bitkit.models.uiTitle import to.bitkit.ui.Routes import to.bitkit.ui.appViewModel import to.bitkit.ui.backupsViewModel import to.bitkit.ui.components.AuthCheckAction import to.bitkit.ui.components.BodyMSB import to.bitkit.ui.components.Sheet import to.bitkit.ui.components.CaptionB import to.bitkit.ui.components.settings.SectionHeader import to.bitkit.ui.components.settings.SettingsButtonRow import to.bitkit.ui.navigateToAuthCheck import to.bitkit.ui.navigateToHome import to.bitkit.ui.scaffold.AppTopBar import to.bitkit.ui.scaffold.CloseNavIcon import to.bitkit.ui.scaffold.ScreenColumn import to.bitkit.ui.settingsViewModel import to.bitkit.ui.shared.util.clickableAlpha import to.bitkit.ui.theme.AppThemeSurface import to.bitkit.ui.theme.Colors import to.bitkit.viewmodels.BackupCategoryUiState import to.bitkit.viewmodels.BackupStatusUiState import to.bitkit.viewmodels.toUiState ImportOrdering:BalanceHeaderView.kt$import androidx.compose.animation.AnimatedContent import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.material3.Icon import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalInspectionMode import androidx.compose.ui.res.painterResource 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.models.BITCOIN_SYMBOL import to.bitkit.models.ConvertedAmount import to.bitkit.models.PrimaryDisplay import to.bitkit.models.formatToModernDisplay import to.bitkit.ui.LocalCurrencies import to.bitkit.ui.currencyViewModel import to.bitkit.ui.settingsViewModel import to.bitkit.ui.shared.animations.BalanceAnimations import to.bitkit.ui.shared.modifiers.swipeToHide import to.bitkit.ui.shared.UiConstants import to.bitkit.ui.shared.util.clickableAlpha import to.bitkit.ui.theme.AppThemeSurface import to.bitkit.ui.theme.Colors ImportOrdering:ChangePinConfirmScreen.kt$import androidx.compose.animation.AnimatedVisibility import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.navigation.NavController import kotlinx.coroutines.delay import to.bitkit.R import to.bitkit.env.Env import to.bitkit.ui.appViewModel import to.bitkit.ui.components.BodyM import to.bitkit.ui.components.BodyS import to.bitkit.ui.components.KEY_DELETE import to.bitkit.ui.components.PinDots import to.bitkit.ui.components.NumberPadSimple import to.bitkit.ui.navigateToHome import to.bitkit.ui.navigateToChangePinResult import to.bitkit.ui.scaffold.AppTopBar import to.bitkit.ui.scaffold.CloseNavIcon import to.bitkit.ui.scaffold.ScreenColumn import to.bitkit.ui.theme.AppThemeSurface import to.bitkit.ui.theme.Colors ImportOrdering:ChangePinNewScreen.kt$import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue 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.navigation.NavController import to.bitkit.R import to.bitkit.env.Env import to.bitkit.ui.components.BodyM import to.bitkit.ui.components.KEY_DELETE import to.bitkit.ui.components.PinDots import to.bitkit.ui.components.NumberPadSimple import to.bitkit.ui.navigateToChangePinConfirm import to.bitkit.ui.navigateToHome import to.bitkit.ui.scaffold.AppTopBar import to.bitkit.ui.scaffold.CloseNavIcon import to.bitkit.ui.scaffold.ScreenColumn import to.bitkit.ui.theme.AppThemeSurface import to.bitkit.ui.theme.Colors ImportOrdering:ChangePinScreen.kt$import androidx.compose.animation.AnimatedVisibility import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.navigation.NavController import to.bitkit.R import to.bitkit.env.Env import to.bitkit.ui.appViewModel import to.bitkit.ui.components.BodyM import to.bitkit.ui.components.BodyS import to.bitkit.ui.components.KEY_DELETE import to.bitkit.ui.components.PinDots import to.bitkit.ui.components.NumberPadSimple import to.bitkit.ui.navigateToChangePinNew import to.bitkit.ui.navigateToHome import to.bitkit.ui.scaffold.AppTopBar import to.bitkit.ui.scaffold.CloseNavIcon import to.bitkit.ui.scaffold.ScreenColumn import to.bitkit.ui.shared.util.clickableAlpha import to.bitkit.ui.theme.AppThemeSurface import to.bitkit.ui.theme.Colors - ImportOrdering:ContentView.kt$import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue import androidx.compose.ui.platform.LocalContext import androidx.hilt.navigation.compose.hiltViewModel import androidx.lifecycle.Lifecycle import androidx.lifecycle.LifecycleEventObserver import androidx.lifecycle.compose.LocalLifecycleOwner import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.navigation.NavController import androidx.navigation.NavDestination.Companion.hasRoute import androidx.navigation.NavGraphBuilder import androidx.navigation.NavHostController import androidx.navigation.NavOptions import androidx.navigation.compose.NavHost import androidx.navigation.compose.composable import androidx.navigation.compose.navigation import androidx.navigation.compose.rememberNavController import androidx.navigation.toRoute import kotlinx.coroutines.delay import kotlinx.coroutines.launch import kotlinx.serialization.Serializable import to.bitkit.models.NewTransactionSheetDetails import to.bitkit.models.NodeLifecycleState import to.bitkit.models.Toast import to.bitkit.models.WidgetType import to.bitkit.ui.components.AuthCheckScreen import to.bitkit.ui.components.Sheet import to.bitkit.ui.components.SheetHost import to.bitkit.ui.onboarding.InitializingWalletView import to.bitkit.ui.onboarding.WalletRestoreErrorView import to.bitkit.ui.onboarding.WalletRestoreSuccessView import to.bitkit.ui.screens.profile.CreateProfileScreen import to.bitkit.ui.screens.profile.ProfileIntroScreen import to.bitkit.ui.screens.scanner.QrScanningScreen import to.bitkit.ui.screens.scanner.SCAN_REQUEST_KEY import to.bitkit.ui.screens.settings.DevSettingsScreen import to.bitkit.ui.screens.settings.FeeSettingsScreen import to.bitkit.ui.screens.shop.ShopIntroScreen import to.bitkit.ui.screens.shop.shopDiscover.ShopDiscoverScreen import to.bitkit.ui.screens.shop.shopWebView.ShopWebViewScreen import to.bitkit.ui.screens.transfer.FundingAdvancedScreen import to.bitkit.ui.screens.transfer.FundingScreen import to.bitkit.ui.screens.transfer.LiquidityScreen import to.bitkit.ui.screens.transfer.SavingsAdvancedScreen import to.bitkit.ui.screens.transfer.SavingsAvailabilityScreen import to.bitkit.ui.screens.transfer.SavingsConfirmScreen import to.bitkit.ui.screens.transfer.SavingsIntroScreen import to.bitkit.ui.screens.transfer.SavingsProgressScreen import to.bitkit.ui.screens.transfer.SettingUpScreen import to.bitkit.ui.screens.transfer.SpendingAdvancedScreen import to.bitkit.ui.screens.transfer.SpendingAmountScreen import to.bitkit.ui.screens.transfer.SpendingConfirmScreen import to.bitkit.ui.screens.transfer.SpendingIntroScreen import to.bitkit.ui.screens.transfer.TransferIntroScreen import to.bitkit.ui.screens.transfer.external.ExternalAmountScreen import to.bitkit.ui.screens.transfer.external.ExternalConfirmScreen import to.bitkit.ui.screens.transfer.external.ExternalConnectionScreen import to.bitkit.ui.screens.transfer.external.ExternalFeeCustomScreen import to.bitkit.ui.screens.transfer.external.ExternalNodeViewModel import to.bitkit.ui.screens.transfer.external.ExternalSuccessScreen import to.bitkit.ui.screens.transfer.external.LnurlChannelScreen import to.bitkit.ui.screens.wallets.HomeNav import to.bitkit.ui.screens.wallets.activity.ActivityDetailScreen import to.bitkit.ui.screens.wallets.activity.ActivityExploreScreen import to.bitkit.ui.screens.wallets.activity.DateRangeSelectorSheet import to.bitkit.ui.screens.wallets.activity.TagSelectorSheet import to.bitkit.ui.screens.wallets.receive.ReceiveSheet import to.bitkit.ui.sheets.SendSheet import to.bitkit.ui.screens.wallets.suggestion.BuyIntroScreen import to.bitkit.ui.screens.widgets.AddWidgetsScreen import to.bitkit.ui.screens.widgets.WidgetsIntroScreen import to.bitkit.ui.screens.widgets.blocks.BlocksEditScreen import to.bitkit.ui.screens.widgets.blocks.BlocksPreviewScreen import to.bitkit.ui.screens.widgets.blocks.BlocksViewModel import to.bitkit.ui.screens.widgets.calculator.CalculatorPreviewScreen import to.bitkit.ui.screens.widgets.facts.FactsEditScreen import to.bitkit.ui.screens.widgets.facts.FactsPreviewScreen import to.bitkit.ui.screens.widgets.facts.FactsViewModel import to.bitkit.ui.screens.widgets.headlines.HeadlinesEditScreen import to.bitkit.ui.screens.widgets.headlines.HeadlinesPreviewScreen import to.bitkit.ui.screens.widgets.headlines.HeadlinesViewModel import to.bitkit.ui.screens.widgets.price.PriceEditScreen import to.bitkit.ui.screens.widgets.price.PricePreviewScreen import to.bitkit.ui.screens.widgets.price.PriceViewModel import to.bitkit.ui.screens.widgets.weather.WeatherEditScreen import to.bitkit.ui.screens.widgets.weather.WeatherPreviewScreen import to.bitkit.ui.screens.widgets.weather.WeatherViewModel import to.bitkit.ui.settings.AboutScreen import to.bitkit.ui.settings.AdvancedSettingsScreen import to.bitkit.ui.settings.BackupSettingsScreen import to.bitkit.ui.settings.BlocktankRegtestScreen import to.bitkit.ui.settings.CJitDetailScreen import to.bitkit.ui.settings.ChannelOrdersScreen import to.bitkit.ui.settings.LogDetailScreen import to.bitkit.ui.settings.LogsScreen import to.bitkit.ui.settings.OrderDetailScreen import to.bitkit.ui.settings.SecuritySettingsScreen import to.bitkit.ui.settings.SettingsScreen import to.bitkit.ui.settings.advanced.AddressViewerScreen import to.bitkit.ui.settings.advanced.CoinSelectPreferenceScreen import to.bitkit.ui.settings.advanced.ElectrumConfigScreen import to.bitkit.ui.settings.advanced.RgsServerScreen import to.bitkit.ui.settings.appStatus.AppStatusScreen import to.bitkit.ui.settings.backups.ResetAndRestoreScreen import to.bitkit.ui.settings.general.DefaultUnitSettingsScreen import to.bitkit.ui.settings.general.GeneralSettingsScreen import to.bitkit.ui.settings.general.LocalCurrencySettingsScreen import to.bitkit.ui.settings.general.TagsSettingsScreen import to.bitkit.ui.settings.general.WidgetsSettingsScreen import to.bitkit.ui.settings.lightning.ChannelDetailScreen import to.bitkit.ui.settings.lightning.CloseConnectionScreen import to.bitkit.ui.settings.lightning.LightningConnectionsScreen import to.bitkit.ui.settings.lightning.LightningConnectionsViewModel import to.bitkit.ui.settings.pin.ChangePinConfirmScreen import to.bitkit.ui.settings.pin.ChangePinNewScreen import to.bitkit.ui.settings.pin.ChangePinResultScreen import to.bitkit.ui.settings.pin.ChangePinScreen import to.bitkit.ui.settings.pin.DisablePinScreen import to.bitkit.ui.settings.quickPay.QuickPayIntroScreen import to.bitkit.ui.settings.quickPay.QuickPaySettingsScreen import to.bitkit.ui.settings.support.ReportIssueResultScreen import to.bitkit.ui.settings.support.ReportIssueScreen import to.bitkit.ui.settings.support.SupportScreen import to.bitkit.ui.settings.transactionSpeed.CustomFeeSettingsScreen import to.bitkit.ui.settings.transactionSpeed.TransactionSpeedSettingsScreen import to.bitkit.ui.sheets.BackupSheet import to.bitkit.ui.sheets.LnurlAuthSheet import to.bitkit.ui.sheets.PinSheet import to.bitkit.ui.utils.AutoReadClipboardHandler import to.bitkit.ui.utils.composableWithDefaultTransitions import to.bitkit.ui.utils.screenSlideIn import to.bitkit.ui.utils.screenSlideOut import to.bitkit.utils.Logger import to.bitkit.viewmodels.ActivityListViewModel import to.bitkit.viewmodels.AppViewModel import to.bitkit.viewmodels.BackupsViewModel import to.bitkit.viewmodels.BlocktankViewModel import to.bitkit.viewmodels.CurrencyViewModel import to.bitkit.viewmodels.MainScreenEffect import to.bitkit.viewmodels.RestoreState import to.bitkit.viewmodels.SettingsViewModel import to.bitkit.viewmodels.TransferViewModel import to.bitkit.viewmodels.WalletViewModel ImportOrdering:CryptoTest.kt$import org.junit.Before import org.junit.Test import to.bitkit.env.Env.DERIVATION_NAME import to.bitkit.ext.fromBase64 import to.bitkit.ext.fromHex import to.bitkit.ext.toHex import to.bitkit.ext.toBase64 import to.bitkit.fcm.EncryptedNotification import kotlin.test.assertContentEquals import kotlin.test.assertEquals ImportOrdering:ExternalConfirmScreen.kt$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 import androidx.compose.foundation.layout.IntrinsicSize import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding 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.Icon 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 import androidx.compose.ui.Alignment.Companion.CenterHorizontally import androidx.compose.ui.Modifier import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import to.bitkit.R import to.bitkit.ui.components.Caption13Up import to.bitkit.ui.components.Display import to.bitkit.ui.components.FeeInfo import to.bitkit.ui.components.MoneySSB import to.bitkit.ui.components.SwipeToConfirm import to.bitkit.ui.scaffold.AppTopBar import to.bitkit.ui.scaffold.CloseNavIcon import to.bitkit.ui.scaffold.ScreenColumn 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.ui.screens.transfer.external.ExternalNodeContract.SideEffect ImportOrdering:ExternalConnectionScreen.kt$import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.imePadding import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.foundation.verticalScroll import androidx.compose.material3.Icon import androidx.compose.runtime.Composable 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.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.input.ImeAction import androidx.compose.ui.text.input.KeyboardCapitalization import androidx.compose.ui.text.input.KeyboardType import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.lifecycle.SavedStateHandle import kotlinx.coroutines.flow.filterNotNull import to.bitkit.R import to.bitkit.ext.getClipboardText import to.bitkit.models.LnPeer import to.bitkit.ui.Routes import to.bitkit.ui.components.BodyM import to.bitkit.ui.components.ButtonSize import to.bitkit.ui.components.Caption13Up import to.bitkit.ui.components.Display import to.bitkit.ui.components.PrimaryButton import to.bitkit.ui.components.SecondaryButton import to.bitkit.ui.components.TextInput import to.bitkit.ui.scaffold.AppTopBar import to.bitkit.ui.scaffold.CloseNavIcon import to.bitkit.ui.scaffold.ScreenColumn 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.ui.screens.transfer.external.ExternalNodeContract.SideEffect @@ -354,7 +349,6 @@ ImportOrdering:PinConfirmScreen.kt$import androidx.compose.animation.AnimatedVisibility import androidx.compose.foundation.background import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxWidth 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 import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import kotlinx.coroutines.delay import to.bitkit.R import to.bitkit.env.Env import to.bitkit.ui.appViewModel import to.bitkit.ui.components.BodyM import to.bitkit.ui.components.BodyS import to.bitkit.ui.components.KEY_DELETE import to.bitkit.ui.components.PinDots import to.bitkit.ui.components.NumberPadSimple import to.bitkit.ui.scaffold.SheetTopBar import to.bitkit.ui.shared.util.gradientBackground import to.bitkit.ui.theme.AppThemeSurface import to.bitkit.ui.theme.Colors ImportOrdering:PinDots.kt$import androidx.compose.foundation.background import androidx.compose.foundation.border import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.shape.CircleShape import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.unit.dp import to.bitkit.env.Env import to.bitkit.ui.theme.Colors ImportOrdering:ResetAndRestoreScreen.kt$import androidx.compose.foundation.Image import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.platform.testTag import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.navigation.NavController import to.bitkit.R import to.bitkit.ui.appViewModel import to.bitkit.ui.components.BodyM import to.bitkit.ui.components.Sheet import to.bitkit.ui.components.PrimaryButton import to.bitkit.ui.components.SecondaryButton import to.bitkit.ui.navigateToHome import to.bitkit.ui.scaffold.AppAlertDialog import to.bitkit.ui.scaffold.AppTopBar import to.bitkit.ui.scaffold.CloseNavIcon import to.bitkit.ui.scaffold.ScreenColumn import to.bitkit.ui.theme.AppThemeSurface import to.bitkit.ui.theme.Colors import to.bitkit.ui.walletViewModel - ImportOrdering:SheetHost.kt$import androidx.activity.compose.BackHandler import androidx.compose.animation.core.animateFloatAsState import androidx.compose.animation.core.tween import androidx.compose.foundation.background import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.ColumnScope import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.material3.BottomSheetScaffold import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.MaterialTheme import androidx.compose.material3.SheetState import androidx.compose.material3.SheetValue import androidx.compose.material3.rememberBottomSheetScaffoldState import androidx.compose.material3.rememberModalBottomSheetState import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp import kotlinx.coroutines.launch import to.bitkit.ui.sheets.SendRoute import to.bitkit.ui.sheets.BackupRoute import to.bitkit.ui.sheets.PinRoute import to.bitkit.ui.theme.AppShapes import to.bitkit.ui.theme.Colors ImportOrdering:WakeNodeWorker.kt$import android.content.Context import androidx.hilt.work.HiltWorker import androidx.work.CoroutineWorker import androidx.work.WorkerParameters import androidx.work.workDataOf import dagger.assisted.Assisted import dagger.assisted.AssistedInject import kotlinx.coroutines.CompletableDeferred import kotlinx.coroutines.withTimeout import kotlinx.serialization.json.JsonObject import kotlinx.serialization.json.JsonPrimitive import kotlinx.serialization.json.contentOrNull import kotlinx.serialization.json.jsonObject import org.lightningdevkit.ldknode.Event import to.bitkit.di.json import to.bitkit.models.NewTransactionSheetDetails import to.bitkit.models.NewTransactionSheetDirection import to.bitkit.models.NewTransactionSheetType import to.bitkit.models.BlocktankNotificationType import to.bitkit.models.BlocktankNotificationType.cjitPaymentArrived import to.bitkit.models.BlocktankNotificationType.incomingHtlc import to.bitkit.models.BlocktankNotificationType.mutualClose import to.bitkit.models.BlocktankNotificationType.orderPaymentConfirmed import to.bitkit.models.BlocktankNotificationType.wakeToTimeout import to.bitkit.repositories.LightningRepo import to.bitkit.services.CoreService import to.bitkit.ui.pushNotification import to.bitkit.utils.Logger import to.bitkit.utils.withPerformanceLogging import kotlin.time.Duration.Companion.minutes ImportOrdering:WalletBalanceView.kt$import androidx.compose.animation.AnimatedContent import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.RowScope import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.material3.Icon import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.painter.Painter import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle import to.bitkit.models.ConvertedAmount import to.bitkit.models.PrimaryDisplay import to.bitkit.ui.LocalCurrencies import to.bitkit.ui.currencyViewModel import to.bitkit.ui.settingsViewModel import to.bitkit.ui.shared.animations.BalanceAnimations import to.bitkit.ui.shared.UiConstants import to.bitkit.ui.theme.Colors ImportOrdering:vss_rust_client_ffi.kt$import com.sun.jna.Library import com.sun.jna.IntegerType import com.sun.jna.Native import com.sun.jna.Pointer import com.sun.jna.Structure import com.sun.jna.Callback import com.sun.jna.ptr.* import java.nio.ByteBuffer import java.nio.ByteOrder import java.nio.CharBuffer import java.nio.charset.CodingErrorAction import java.util.concurrent.ConcurrentHashMap import kotlin.coroutines.resume import kotlinx.coroutines.CancellableContinuation import kotlinx.coroutines.suspendCancellableCoroutine @@ -381,7 +375,6 @@ Indentation:vss_rust_client_ffi.kt$VssException.UnknownException$ Indentation:vss_rust_client_ffi.kt$_UniFFILib.Companion$ InstanceOfCheckForException:LightningService.kt$LightningService$e is NodeException - LambdaParameterEventTrailing:BoostTransactionSheet.kt$onSwipe LambdaParameterEventTrailing:CalculatorCard.kt$onFiatChange LambdaParameterEventTrailing:ReceiveQrScreen.kt$onClickEditInvoice LambdaParameterEventTrailing:SettingsButtonRow.kt$onClick @@ -419,9 +412,9 @@ LambdaParameterInRestartableEffect:SpendingAmountScreen.kt$toast LambdaParameterInRestartableEffect:SpendingAmountScreen.kt$toastException LargeClass:AppViewModel.kt$AppViewModel : ViewModel + LargeClass:LightningRepo.kt$LightningRepo LongMethod:AppViewModel.kt$AppViewModel$private fun observeLdkNodeEvents() LongMethod:AppViewModel.kt$AppViewModel$private suspend fun proceedWithPayment() - LongMethod:BoostTransactionViewModel.kt$BoostTransactionViewModel$private suspend fun updateActivity(newTxId: Txid, isRBF: Boolean): Result<Unit> LongMethod:ContentView.kt$private fun NavGraphBuilder.widgets( navController: NavHostController, settingsViewModel: SettingsViewModel, currencyViewModel: CurrencyViewModel, ) LongMethod:CoreService.kt$ActivityService$suspend fun generateRandomTestData(count: Int = 100) LongMethod:CoreService.kt$ActivityService$suspend fun syncLdkNodePayments(payments: List<PaymentDetails>, forceUpdate: Boolean = false) @@ -429,7 +422,7 @@ LongMethod:MainActivity.kt$MainActivity$override fun onCreate(savedInstanceState: Bundle?) LongMethod:WakeNodeWorker.kt$WakeNodeWorker$private suspend fun handleLdkEvent(event: Event) LongParameterList:ActivityRepo.kt$ActivityRepo$( filter: ActivityFilter? = null, txType: PaymentType? = null, tags: List<String>? = null, search: String? = null, minDate: ULong? = null, maxDate: ULong? = null, limit: UInt? = null, sortDirection: SortDirection? = null, ) - LongParameterList:AppViewModel.kt$AppViewModel$( @ApplicationContext private val context: Context, @BgDispatcher private val bgDispatcher: CoroutineDispatcher, private val keychain: Keychain, private val lightningRepo: LightningRepo, private val walletRepo: WalletRepo, private val coreService: CoreService, private val ldkNodeEventBus: LdkNodeEventBus, private val settingsStore: SettingsStore, private val currencyRepo: CurrencyRepo, private val activityRepo: ActivityRepo, connectivityRepo: ConnectivityRepo, healthRepo: HealthRepo, ) + LongParameterList:AppViewModel.kt$AppViewModel$( @ApplicationContext private val context: Context, @BgDispatcher private val bgDispatcher: CoroutineDispatcher, private val keychain: Keychain, private val lightningRepo: LightningRepo, private val walletRepo: WalletRepo, private val coreService: CoreService, private val ldkNodeEventBus: LdkNodeEventBus, private val settingsStore: SettingsStore, private val currencyRepo: CurrencyRepo, private val activityRepo: ActivityRepo, private val blocktankRepo: BlocktankRepo, connectivityRepo: ConnectivityRepo, healthRepo: HealthRepo, ) LongParameterList:BiometricPrompt.kt$( activity: Context, title: String, cancelButtonText: String, onAuthSucceed: () -> Unit, onAuthFailed: (() -> Unit), onAuthError: ((errorCode: Int, errString: CharSequence) -> Unit), ) LongParameterList:BiometricPrompt.kt$( activity: Context, title: String, cancelButtonText: String, onAuthSucceeded: () -> Unit, onAuthFailed: (() -> Unit), onAuthError: ((errorCode: Int, errString: CharSequence) -> Unit), onUnsupported: () -> Unit, ) LongParameterList:CoreService.kt$ActivityService$( filter: ActivityFilter? = null, txType: PaymentType? = null, tags: List<String>? = null, search: String? = null, minDate: ULong? = null, maxDate: ULong? = null, limit: UInt? = null, sortDirection: SortDirection? = null, ) @@ -438,7 +431,7 @@ LongParameterList:DevSettingsViewModel.kt$DevSettingsViewModel$( @ApplicationContext private val context: Context, @BgDispatcher private val bgDispatcher: CoroutineDispatcher, private val firebaseMessaging: FirebaseMessaging, private val lightningRepo: LightningRepo, private val walletRepo: WalletRepo, private val widgetsStore: WidgetsStore, private val currencyRepo: CurrencyRepo, private val logsRepo: LogsRepo, private val cacheStore: CacheStore, private val blocktankRepo: BlocktankRepo, ) LongParameterList:LightningConnectionsViewModel.kt$LightningConnectionsViewModel$( @ApplicationContext private val context: Context, @BgDispatcher private val bgDispatcher: CoroutineDispatcher, private val lightningRepo: LightningRepo, internal val blocktankRepo: BlocktankRepo, private val logsRepo: LogsRepo, private val addressChecker: AddressChecker, private val ldkNodeEventBus: LdkNodeEventBus, private val walletRepo: WalletRepo, ) LongParameterList:LightningRepo.kt$LightningRepo$( @BgDispatcher private val bgDispatcher: CoroutineDispatcher, private val lightningService: LightningService, private val ldkNodeEventBus: LdkNodeEventBus, private val settingsStore: SettingsStore, private val coreService: CoreService, private val blocktankNotificationsService: BlocktankNotificationsService, private val firebaseMessaging: FirebaseMessaging, private val keychain: Keychain, private val lnurlService: LnurlService, private val cacheStore: CacheStore, ) - LongParameterList:LightningRepo.kt$LightningRepo$( address: Address, sats: ULong, speed: TransactionSpeed? = null, utxosToSpend: List<SpendableUtxo>? = null, isTransfer: Boolean = false, channelId: String? = null, ) + LongParameterList:LightningRepo.kt$LightningRepo$( address: Address, sats: ULong, speed: TransactionSpeed? = null, utxosToSpend: List<SpendableUtxo>? = null, feeRates: FeeRates? = null, isTransfer: Boolean = false, channelId: String? = null, ) LongParameterList:LightningRepo.kt$LightningRepo$( walletIndex: Int = 0, timeout: Duration? = null, shouldRetry: Boolean = true, eventHandler: NodeEventHandler? = null, customServer: ElectrumServer? = null, customRgsServerUrl: String? = null, ) LongParameterList:Nav.kt$( typeMap: Map<KType, NavType<*>> = emptyMap(), deepLinks: List<NavDeepLink> = emptyList(), noinline enterTransition: (AnimatedContentTransitionScope<NavBackStackEntry>.() -> EnterTransition?)? = { screenSlideIn }, noinline exitTransition: (AnimatedContentTransitionScope<NavBackStackEntry>.() -> ExitTransition?)? = { screenScaleOut }, noinline popEnterTransition: (AnimatedContentTransitionScope<NavBackStackEntry>.() -> EnterTransition?)? = { screenScaleIn }, noinline popExitTransition: (AnimatedContentTransitionScope<NavBackStackEntry>.() -> ExitTransition?)? = { screenSlideOut }, noinline content: @Composable AnimatedContentScope.(NavBackStackEntry) -> Unit, ) LongParameterList:Notifications.kt$( title: String?, text: String?, extras: Bundle? = null, bigText: String? = null, id: Int = Random.nextInt(), context: Context, ) @@ -464,7 +457,6 @@ MagicNumber:AllActivityScreen.kt$0xFF1e1e1e MagicNumber:AllActivityScreen.kt$1500f MagicNumber:AmountInput.kt$8 - MagicNumber:AndroidKeyStore.kt$AndroidKeyStore$12 MagicNumber:AndroidKeyStore.kt$AndroidKeyStore$128 MagicNumber:AndroidKeyStore.kt$AndroidKeyStore$256 MagicNumber:AppStatus.kt$0.2f @@ -479,7 +471,6 @@ MagicNumber:AppViewModel.kt$AppViewModel$250 MagicNumber:AppViewModel.kt$AppViewModel$300 MagicNumber:AppViewModel.kt$AppViewModel$500 - MagicNumber:AppViewModel.kt$AppViewModel$5000 MagicNumber:ArticleModel.kt$24 MagicNumber:ArticleModel.kt$30 MagicNumber:ArticleModel.kt$60 @@ -491,11 +482,6 @@ MagicNumber:BackupSettingsScreen.kt$5 MagicNumber:BackupSettingsScreen.kt$60 MagicNumber:BackupsViewModel.kt$BackupsViewModel$500 - MagicNumber:BalanceAnimations.kt$BalanceAnimations$200 - MagicNumber:BalanceAnimations.kt$BalanceAnimations$300 - MagicNumber:BalanceAnimations.kt$BalanceAnimations$350 - MagicNumber:BalanceAnimations.kt$BalanceAnimations$380 - MagicNumber:BalanceAnimations.kt$BalanceAnimations$400 MagicNumber:BiometricCrypto.kt$BiometricCrypto$256 MagicNumber:BiometricsView.kt$5 MagicNumber:Bip21Utils.kt$Bip21Utils$8 @@ -514,7 +500,6 @@ MagicNumber:BlocktankRepo.kt$BlocktankRepo$225 MagicNumber:BlocktankRepo.kt$BlocktankRepo$450 MagicNumber:BlocktankRepo.kt$BlocktankRepo$495 - MagicNumber:BlocktankViewModel.kt$BlocktankViewModel$5000 MagicNumber:Button.kt$0.5f MagicNumber:ChangePinConfirmScreen.kt$500 MagicNumber:ChannelDetailScreen.kt$1.5f @@ -522,20 +507,6 @@ MagicNumber:ChannelOrdersScreen.kt$100 MagicNumber:ChannelOrdersScreen.kt$30 MagicNumber:ChannelOrdersScreen.kt$40 - MagicNumber:Colors.kt$Colors$0xFF000000 - MagicNumber:Colors.kt$Colors$0xFF0085FF - MagicNumber:Colors.kt$Colors$0xFF151515 - MagicNumber:Colors.kt$Colors$0xFF1C1C1D - MagicNumber:Colors.kt$Colors$0xFF3A343C - MagicNumber:Colors.kt$Colors$0xFF48484A - MagicNumber:Colors.kt$Colors$0xFF636366 - MagicNumber:Colors.kt$Colors$0xFF75BF72 - MagicNumber:Colors.kt$Colors$0xFF8E8E93 - MagicNumber:Colors.kt$Colors$0xFFB95CE8 - MagicNumber:Colors.kt$Colors$0xFFE95164 - MagicNumber:Colors.kt$Colors$0xFFFF4400 - MagicNumber:Colors.kt$Colors$0xFFFFD200 - MagicNumber:Colors.kt$Colors$0xFFFFFFFF MagicNumber:ConfirmMnemonicScreen.kt$12 MagicNumber:ConfirmMnemonicScreen.kt$24 MagicNumber:ConfirmMnemonicScreen.kt$300 @@ -557,14 +528,12 @@ MagicNumber:Crypto.kt$Crypto$16 MagicNumber:Crypto.kt$Crypto$32 MagicNumber:CurrencyService.kt$CurrencyService$1000L - MagicNumber:CurrencyService.kt$CurrencyService$3 MagicNumber:ElectrumConfigViewModel.kt$ElectrumConfigViewModel$65535 MagicNumber:ElectrumServer.kt$50001 MagicNumber:ElectrumServer.kt$50002 MagicNumber:ElectrumServer.kt$60001 MagicNumber:ElectrumServer.kt$60002 MagicNumber:ExternalConnectionScreen.kt$66 - MagicNumber:FeeSettingsViewModel.kt$FeeSettingsViewModel$5000 MagicNumber:HomeScreen.kt$0.5f MagicNumber:HomeScreen.kt$0.8f MagicNumber:HomeScreen.kt$3 @@ -594,7 +563,6 @@ MagicNumber:PinConfirmScreen.kt$500 MagicNumber:PinPromptScreen.kt$0.8f MagicNumber:PreviewItems.kt$10 - MagicNumber:PreviewItems.kt$1000 MagicNumber:PreviewItems.kt$3 MagicNumber:PriceCard.kt$1000 MagicNumber:PriceCard.kt$3.0 @@ -643,10 +611,7 @@ MagicNumber:SwipeToConfirm.kt$1500 MagicNumber:SwipeToConfirm.kt$500 MagicNumber:TabBar.kt$0.5f - MagicNumber:TabBar.kt$40 MagicNumber:TermsOfUseScreen.kt$0x52FF6600 - MagicNumber:Theme.kt$0xFF212121 - MagicNumber:Theme.kt$0xFFF4F4F4 MagicNumber:Thread.kt$4 MagicNumber:ToastView.kt$0XFF032E56 MagicNumber:ToastView.kt$0XFF1D2F1C @@ -684,7 +649,6 @@ MatchingDeclarationName:AddressType.kt$AddressTypeInfo MatchingDeclarationName:AdvancedSettingsScreen.kt$AdvancedSettingsTestTags MatchingDeclarationName:BackupSettingsScreen.kt$BackupSettingsTestTags - MatchingDeclarationName:BoostTransactionViewModelTest.kt$BoostTransactionViewModelSimplifiedTest : BaseUnitTest MatchingDeclarationName:Button.kt$ButtonSize MatchingDeclarationName:CoinSelectPreferenceScreen.kt$CoinSelectPreferenceTestTags MatchingDeclarationName:LightningChannel.kt$ChannelStatusUi @@ -721,7 +685,6 @@ MaxLineLength:BlocksEditScreen.kt$enabled = blocksPreferences.run { showBlock || showTime || showDate || showTransactions || showSize || showSource } MaxLineLength:BlocktankRegtestScreen.kt$Logger.debug("Initiating channel close with fundingTxId: $fundingTxId, vout: $vout, forceCloseAfter: $forceCloseAfter") MaxLineLength:BlocktankRepo.kt$BlocktankRepo$"Buying channel with lspBalanceSat: $receivingBalanceSats, channelExpiryWeeks: $channelExpiryWeeks, options: $options" - MaxLineLength:BoostTransactionViewModel.kt$BoostTransactionViewModel$Logger.error("Activity $newTxId not found. Caching data to try again on next sync", e = error, context = TAG) MaxLineLength:ChannelOrdersScreen.kt$lnurl = "LNURL1DP68GURN8GHJ7CTSDYH8XARPVUHXYMR0VD4HGCTWDVH8GME0VFKX7CMTW3SKU6E0V9CXJTMKXGHKCTENVV6NVDP4XUEJ6ETRX33Z6DPEXU6Z6C34XQEZ6DT9XENX2WFNXQ6RXDTXGQAH4MLNURL1DP68GURN8GHJ7CTSDYH8XARPVUHXYMR0VD4HGCTWDVH8GME0VFKX7CMTW3SKU6E0V9CXJTMKXGHKCTENVV6NVDP4XUEJ6ETRX33Z6DPEXU6Z6C34XQEZ6DT9XENX2WFNXQ6RXDTXGQAH4M" MaxLineLength:CoreService.kt$CoreService$// val blocktankPeers = getInfo(refresh = true)?.nodes?.map { LnPeer(nodeId = it.pubkey, address = "TO_DO") }.orEmpty() MaxLineLength:CryptoTest.kt$CryptoTest$val ciphertext = "l2fInfyw64gO12odo8iipISloQJ45Rc4WjFmpe95brdaAMDq+T/L9ZChcmMCXnR0J6BXd8sSIJe/0bmby8uSZZJuVCzwF76XHfY5oq0Y1/hKzyZTn8nG3dqfiLHnAPy1tZFQfm5ALgjwWnViYJLXoGFpXs7kLMA=".fromBase64() @@ -783,7 +746,6 @@ MaximumLineLength:BlocksEditScreen.kt$ MaximumLineLength:BlocktankRegtestScreen.kt$ MaximumLineLength:BlocktankRepo.kt$BlocktankRepo$ - MaximumLineLength:BoostTransactionViewModel.kt$BoostTransactionViewModel$ MaximumLineLength:ChannelOrdersScreen.kt$ MaximumLineLength:CryptoTest.kt$CryptoTest$ MaximumLineLength:EditInvoiceVM.kt$EditInvoiceVM$ @@ -930,7 +892,6 @@ MultiLineIfElse:Slider.kt$emptyList() MultiLineIfElse:Slider.kt$steps.indices.map { index -> val numSteps = (steps.size - 1).coerceAtLeast(1) (index.toFloat() / numSteps) * sliderWidth } MultipleEmitters:ActivityExploreScreen.kt$LightningDetails - MultipleEmitters:BoostTransactionSheet.kt$DefaultModeContent MultipleEmitters:DrawerMenu.kt$DrawerMenu MultipleEmitters:OnboardingSlidesScreen.kt$OnboardingSlidesScreen MultipleEmitters:SendConfirmScreen.kt$LnurlCommentSection @@ -949,7 +910,6 @@ NoBlankLineBeforeRbrace:PriceService.kt$PriceService$ NoBlankLineBeforeRbrace:SendAddressScreen.kt$ NoBlankLineBeforeRbrace:SendAmountScreen.kt$ - NoBlankLineBeforeRbrace:SendConfirmScreen.kt$ NoBlankLineBeforeRbrace:ShareSheet.kt$ NoBlankLineBeforeRbrace:SuggestionCard.kt$ NoBlankLineBeforeRbrace:WeatherCard.kt$ @@ -962,6 +922,7 @@ NoConsecutiveBlankLines:ActivityRepoTest.kt$ActivityRepoTest$ NoConsecutiveBlankLines:AddWidgetsScreen.kt$ NoConsecutiveBlankLines:AddressChecker.kt$AddressChecker$ + NoConsecutiveBlankLines:AppViewModel.kt$ NoConsecutiveBlankLines:Bip39Test.kt$Bip39Test$ NoConsecutiveBlankLines:BlocksEditScreen.kt$ NoConsecutiveBlankLines:BlocksService.kt$ @@ -1040,7 +1001,6 @@ NoUnusedImports:PinDots.kt$to.bitkit.ui.components.PinDots.kt NoUnusedImports:PriceCard.kt$to.bitkit.ui.screens.widgets.price.PriceCard.kt NoUnusedImports:QrCodeImage.kt$to.bitkit.ui.components.QrCodeImage.kt - NoUnusedImports:SendFeeRateScreen.kt$to.bitkit.ui.screens.wallets.send.SendFeeRateScreen.kt NoUnusedImports:ShopDiscoverScreen.kt$to.bitkit.ui.screens.shop.shopDiscover.ShopDiscoverScreen.kt NoUnusedImports:ShopWebViewInterface.kt$to.bitkit.ui.screens.shop.shopWebView.ShopWebViewInterface.kt NoUnusedImports:ShopWebViewScreen.kt$to.bitkit.ui.screens.shop.shopWebView.ShopWebViewScreen.kt @@ -1178,7 +1138,6 @@ SpacingAroundComma:vss_rust_client_ffi.kt$, SpacingAroundComma:vss_rust_client_ffi.kt$VssFilterType.PREFIX$, SpacingAroundComma:vss_rust_client_ffi.kt$_UniFFILib$, - SpacingAroundKeyword:Activities.kt$when SpacingAroundKeyword:PricePreviewScreen.kt$when SpacingAroundKeyword:WeatherModel.kt$when SpacingAroundKeyword:vss_rust_client_ffi.kt$FfiConverterTypeVssError$when @@ -1186,6 +1145,7 @@ SpacingAroundOperators:NodeInfoScreen.kt$?: SpacingAroundOperators:WeatherEditScreen.kt$= SpacingAroundParens:Bip39Utils.kt$( + SpacingAroundParens:SendFeeViewModel.kt$SendFeeViewModel$( SpacingAroundParens:vss_rust_client_ffi.kt$KeyValue$( SpacingAroundParens:vss_rust_client_ffi.kt$KeyVersion$( SpacingAroundParens:vss_rust_client_ffi.kt$ListKeyVersionsResponse$( @@ -1318,9 +1278,9 @@ UnnecessaryParenthesesBeforeTrailingLambda:vss_rust_client_ffi.kt$() UnnecessaryParenthesesBeforeTrailingLambda:vss_rust_client_ffi.kt$RustBuffer.Companion$() UnusedParameter:ActivityRow.kt$confirmed: Boolean? - UnusedParameter:SendFeeRateScreen.kt$uiState: SendUiState UnusedPrivateProperty:ActivityListViewModel.kt$ActivityListViewModel$private val lightningRepo: LightningRepo UnusedPrivateProperty:ActivityRepoTest.kt$ActivityRepoTest$private val testOnChainActivity = mock<Activity.Onchain> { on { v1 } doReturn testOnChainActivityV1 } + UnusedPrivateProperty:AppViewModel.kt$AppViewModel.Companion$private const val TAG = "AppViewModel" UnusedPrivateProperty:CurrencyRepoTest.kt$CurrencyRepoTest$private val toastEventBus: ToastEventBus = mock() UseCheckOrError:CurrencyRepo.kt$CurrencyRepo$throw IllegalStateException( "Rate not found for currency: $targetCurrency. Available currencies: ${ _currencyState.value.rates.joinToString { it.quote } }" ) VariableNaming:vss_rust_client_ffi.kt$RustCallStatus$@JvmField var error_buf: RustBuffer.ByValue = RustBuffer.ByValue() @@ -1332,7 +1292,7 @@ ViewModelForwarding:ContentView.kt$LnurlAuthSheet(sheet, appViewModel) ViewModelForwarding:ContentView.kt$PinSheet(sheet, appViewModel) ViewModelForwarding:ContentView.kt$RootNavHost( navController = navController, walletViewModel = walletViewModel, appViewModel = appViewModel, activityListViewModel = activityListViewModel, settingsViewModel = settingsViewModel, currencyViewModel = currencyViewModel, transferViewModel = transferViewModel, ) - ViewModelForwarding:ContentView.kt$SendSheet( appViewModel = appViewModel, walletViewModel = walletViewModel, startDestination = sheet.route, onComplete = { txSheet -> appViewModel.resetSendState() appViewModel.hideSheet() appViewModel.clearClipboardForAutoRead() txSheet?.let { appViewModel.showNewTransactionSheet(it) } } ) + ViewModelForwarding:ContentView.kt$SendSheet( appViewModel = appViewModel, walletViewModel = walletViewModel, startDestination = sheet.route, onComplete = { txSheet -> appViewModel.hideSheet() appViewModel.clearClipboardForAutoRead() txSheet?.let { appViewModel.showNewTransactionSheet(it) } } ) ViewModelForwarding:ContentView.kt$SettingUpScreen( viewModel = transferViewModel, onCloseClick = { navController.popBackStack<Routes.TransferRoot>(inclusive = true) }, onContinueClick = { navController.popBackStack<Routes.TransferRoot>(inclusive = true) }, ) ViewModelForwarding:ContentView.kt$SpendingAdvancedScreen( viewModel = transferViewModel, onBackClick = { navController.popBackStack() }, onCloseClick = { navController.navigateToHome() }, onOrderCreated = { navController.popBackStack<Routes.SpendingConfirm>(inclusive = false) }, ) ViewModelForwarding:ContentView.kt$SpendingAmountScreen( viewModel = transferViewModel, onBackClick = { navController.popBackStack() }, onCloseClick = { navController.navigateToHome() }, onOrderCreated = { navController.navigate(Routes.SpendingConfirm) }, toastException = { appViewModel.toast(it) }, toast = { title, description -> appViewModel.toast( type = Toast.ToastType.ERROR, title = title, description = description ) }, ) From 896db2704bea6abda0f91a3c2cabcfcb38fabe8f Mon Sep 17 00:00:00 2001 From: Ovi Trif Date: Thu, 7 Aug 2025 12:15:30 +0200 Subject: [PATCH 35/36] feat: lower log level for overly frequent logs --- .../java/to/bitkit/data/BlocktankHttpClient.kt | 2 +- .../to/bitkit/data/backup/VssBackupClient.kt | 16 ++++++++-------- .../java/to/bitkit/repositories/BackupRepo.kt | 4 ++-- .../java/to/bitkit/repositories/BlocktankRepo.kt | 7 ++----- .../java/to/bitkit/repositories/WidgetsRepo.kt | 6 +++--- app/src/main/java/to/bitkit/utils/Logger.kt | 5 +++-- 6 files changed, 19 insertions(+), 21 deletions(-) diff --git a/app/src/main/java/to/bitkit/data/BlocktankHttpClient.kt b/app/src/main/java/to/bitkit/data/BlocktankHttpClient.kt index a62a25922..02504e0ee 100644 --- a/app/src/main/java/to/bitkit/data/BlocktankHttpClient.kt +++ b/app/src/main/java/to/bitkit/data/BlocktankHttpClient.kt @@ -16,7 +16,7 @@ class BlocktankHttpClient @Inject constructor( ) { suspend fun fetchLatestRates(): FxRateResponse { val response = client.get(Env.btcRatesServer) - Logger.debug("Http call: $response") + Logger.verbose("Http call: $response") return when (response.status.isSuccess()) { true -> response.body() diff --git a/app/src/main/java/to/bitkit/data/backup/VssBackupClient.kt b/app/src/main/java/to/bitkit/data/backup/VssBackupClient.kt index f0279b49b..b03f3326d 100644 --- a/app/src/main/java/to/bitkit/data/backup/VssBackupClient.kt +++ b/app/src/main/java/to/bitkit/data/backup/VssBackupClient.kt @@ -25,7 +25,7 @@ class VssBackupClient @Inject constructor( suspend fun setup() = withContext(bgDispatcher) { try { withTimeout(30.seconds) { - Logger.debug("VSS client setting up…", context = TAG) + Logger.verbose("VSS client setting up…", context = TAG) vssNewClient( baseUrl = Env.vssServerUrl, storeId = vssStoreIdProvider.getVssStoreId(), @@ -44,34 +44,34 @@ class VssBackupClient @Inject constructor( data: ByteArray, ): Result = withContext(bgDispatcher) { isSetup.await() - Logger.debug("VSS 'putObject' call for '$key'", context = TAG) + Logger.verbose("VSS 'putObject' call for '$key'", context = TAG) runCatching { vssStore( key = key, value = data, ) }.onSuccess { - Logger.debug("VSS 'putObject' success for '$key' at version: ${it.version}", context = TAG) + Logger.verbose("VSS 'putObject' success for '$key' at version: ${it.version}", context = TAG) }.onFailure { e -> - Logger.error("VSS 'putObject' error for '$key'", e = e, context = TAG) + Logger.verbose("VSS 'putObject' error for '$key'", e = e, context = TAG) } } suspend fun getObject(key: String): Result = withContext(bgDispatcher) { isSetup.await() - Logger.debug("VSS 'getObject' call for '$key'", context = TAG) + Logger.verbose("VSS 'getObject' call for '$key'", context = TAG) runCatching { vssGet( key = key, ) }.onSuccess { if (it == null) { - Logger.warn("VSS 'getObject' success null for '$key'", context = TAG) + Logger.verbose("VSS 'getObject' success null for '$key'", context = TAG) } else { - Logger.debug("VSS 'getObject' success for '$key'", context = TAG) + Logger.verbose("VSS 'getObject' success for '$key'", context = TAG) } }.onFailure { e -> - Logger.error("VSS 'getObject' error for '$key'", e = e, context = TAG) + Logger.verbose("VSS 'getObject' error for '$key'", e = e, context = TAG) } } diff --git a/app/src/main/java/to/bitkit/repositories/BackupRepo.kt b/app/src/main/java/to/bitkit/repositories/BackupRepo.kt index a82962b84..effd408e8 100644 --- a/app/src/main/java/to/bitkit/repositories/BackupRepo.kt +++ b/app/src/main/java/to/bitkit/repositories/BackupRepo.kt @@ -153,7 +153,7 @@ class BackupRepo @Inject constructor( cacheStore.updateBackupStatus(category) { it.copy(required = System.currentTimeMillis()) } - Logger.debug("Marked backup required for: '$category'", context = TAG) + Logger.verbose("Marked backup required for: '$category'", context = TAG) } } @@ -161,7 +161,7 @@ class BackupRepo @Inject constructor( // Cancel existing backup job for this category backupJobs[category]?.cancel() - Logger.debug("Scheduling backup for: '$category'", context = TAG) + Logger.verbose("Scheduling backup for: '$category'", context = TAG) backupJobs[category] = scope.launch { delay(BACKUP_DEBOUNCE) diff --git a/app/src/main/java/to/bitkit/repositories/BlocktankRepo.kt b/app/src/main/java/to/bitkit/repositories/BlocktankRepo.kt index 6e0d369e1..d9f871b0b 100644 --- a/app/src/main/java/to/bitkit/repositories/BlocktankRepo.kt +++ b/app/src/main/java/to/bitkit/repositories/BlocktankRepo.kt @@ -116,7 +116,7 @@ class BlocktankRepo @Inject constructor( isRefreshing = true try { - Logger.debug("Refreshing blocktank orders…", context = TAG) + Logger.verbose("Refreshing blocktank orders…", context = TAG) val paidOrderIds = cacheStore.data.first().paidOrders.keys @@ -142,10 +142,7 @@ class BlocktankRepo @Inject constructor( ) } - Logger.debug( - "Orders refreshed: ${orders.size} orders, ${cjitEntries.size} cjit entries", - context = TAG - ) + Logger.debug("Orders refreshed: ${orders.size} orders, ${cjitEntries.size} cjit entries", context = TAG) } catch (e: Throwable) { Logger.error("Failed to refresh orders", e, context = TAG) } finally { diff --git a/app/src/main/java/to/bitkit/repositories/WidgetsRepo.kt b/app/src/main/java/to/bitkit/repositories/WidgetsRepo.kt index be1cd125d..4fdf9f7fa 100644 --- a/app/src/main/java/to/bitkit/repositories/WidgetsRepo.kt +++ b/app/src/main/java/to/bitkit/repositories/WidgetsRepo.kt @@ -145,10 +145,10 @@ class WidgetsRepo @Inject constructor( service.fetchData() .onSuccess { data -> updateStore(data) - Logger.debug("Updated $widgetType widget successfully") + Logger.verbose("Updated $widgetType widget successfully") } - .onFailure { error -> - Logger.warn(e = error, msg = "Failed to update $widgetType widget", context = TAG) + .onFailure { e -> + Logger.verbose("Failed to update $widgetType widget", e = e, context = TAG) } _refreshStates.update { it + (widgetType to false) } diff --git a/app/src/main/java/to/bitkit/utils/Logger.kt b/app/src/main/java/to/bitkit/utils/Logger.kt index 20d54324d..a18e76bbb 100644 --- a/app/src/main/java/to/bitkit/utils/Logger.kt +++ b/app/src/main/java/to/bitkit/utils/Logger.kt @@ -18,7 +18,7 @@ import java.util.concurrent.Executors object Logger { private const val TAG = "APP" - private const val COMPACT = false + private const val COMPACT = true private val singleThreadDispatcher = Executors .newSingleThreadExecutor { Thread(it, "bitkit.log").apply { priority = Thread.NORM_PRIORITY - 1 } } @@ -91,12 +91,13 @@ object Logger { fun verbose( msg: String?, + e: Throwable? = null, context: String = "", file: String = getCallerFile(), line: Int = getCallerLine(), ) { val message = format("VERBOSE: $msg", context, file, line) - Log.v(TAG, message) + if (COMPACT) Log.v(TAG, message) else Log.v(TAG, message, e) saveToFile(message) } From d0245fb8d9b30cf5d886835e87cbc11e9d11ac22 Mon Sep 17 00:00:00 2001 From: Ovi Trif Date: Thu, 7 Aug 2025 12:46:32 +0200 Subject: [PATCH 36/36] chore: logger fixes --- app/src/main/java/to/bitkit/utils/Logger.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/to/bitkit/utils/Logger.kt b/app/src/main/java/to/bitkit/utils/Logger.kt index a18e76bbb..ce1ece0c7 100644 --- a/app/src/main/java/to/bitkit/utils/Logger.kt +++ b/app/src/main/java/to/bitkit/utils/Logger.kt @@ -18,7 +18,7 @@ import java.util.concurrent.Executors object Logger { private const val TAG = "APP" - private const val COMPACT = true + private const val COMPACT = false private val singleThreadDispatcher = Executors .newSingleThreadExecutor { Thread(it, "bitkit.log").apply { priority = Thread.NORM_PRIORITY - 1 } } @@ -114,7 +114,7 @@ object Logger { private fun format(message: String, context: String, file: String, line: Int): String { val message = message.trim() - val context = if (context.isNotEmpty()) "- $context" else "" + val context = if (context.isNotEmpty()) " - $context" else "" return "$message$context [$file:$line]" }