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 ) }, ) 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/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/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/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 e45783463..02e8f1e4a 100644 --- a/app/src/main/java/to/bitkit/models/TransactionSpeed.kt +++ b/app/src/main/java/to/bitkit/models/TransactionSpeed.kt @@ -26,6 +26,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/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/LightningRepo.kt b/app/src/main/java/to/bitkit/repositories/LightningRepo.kt index 997150079..032096ce8 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 @@ -87,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) @@ -106,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 @@ -480,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( @@ -531,11 +522,11 @@ class LightningRepo @Inject constructor( Result.success(txId) } - private suspend fun determineUtxosToSpend( + 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 @@ -543,21 +534,23 @@ class LightningRepo @Inject constructor( val allSpendableUtxos = lightningService.listSpendableOutputs().getOrThrow() if (coinSelectionPreference == CoinSelectionPreference.Consolidate) { - Logger.info("Consolidating by spending all ${allSpendableUtxos.size} UTXOs", context = TAG) - return allSpendableUtxos + Logger.debug("Consolidating by spending all ${allSpendableUtxos.size} UTXOs", context = TAG) + return@withContext 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, algorithm = coinSelectionAlgorithm, satsPerVByte = satsPerVByte, utxos = allSpendableUtxos, - ).getOrThrow() + ).onSuccess { + Logger.debug("Selected ${it.size} UTXOs", context = TAG) + }.getOrThrow() } else { null // let ldk-node handle utxos } @@ -579,10 +572,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 +594,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/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/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/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..0fe14ba68 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() { @@ -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, UTXOs=${utxosToSpend?.size}, satsPerVByte=$satsPerVByte" + ) return ServiceQueue.LDK.background { return@background try { 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/components/Money.kt b/app/src/main/java/to/bitkit/ui/components/Money.kt index 5853d989f..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,10 +32,34 @@ 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 = 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 -> - BodySSB(text = text.withAccent(accentColor = Colors.White64)) + BodyMSB( + text = text.withAccent(accentColor = accent), + color = color, + modifier = modifier, + ) } } 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)) 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/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..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 @@ -125,7 +125,6 @@ private fun Content( Icon( painterResource(R.drawable.ic_pencil_simple), contentDescription = null, - tint = Colors.White, modifier = Modifier.size(16.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..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 @@ -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 @@ -46,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 @@ -53,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 @@ -304,11 +305,9 @@ 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), - 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)) @@ -321,57 +320,71 @@ private fun OnChainDescription( modifier = Modifier .fillMaxHeight() .weight(1f) - .clickable { onEvent(SendEvent.SpeedAndFee) } - .padding(top = 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_simple), - contentDescription = null, - tint = Colors.White, - 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 = uiState.fee, accent = Colors.White) + BodySSB(")") + } + Icon( + painterResource(R.drawable.ic_pencil_simple), + 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) } - .padding(top = 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) } - Spacer(modifier = Modifier.weight(1f)) - HorizontalDivider(modifier = Modifier.padding(top = 16.dp)) + HorizontalDivider() } - } } } @@ -406,8 +419,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..d2c099eda --- /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.BodyM +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") + BodyM("Lint hack " + uiState.speed.toString()) + + 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/screens/wallets/send/SendFeeRateScreen.kt b/app/src/main/java/to/bitkit/ui/screens/wallets/send/SendFeeRateScreen.kt new file mode 100644 index 000000000..842820b86 --- /dev/null +++ b/app/src/main/java/to/bitkit/ui/screens/wallets/send/SendFeeRateScreen.kt @@ -0,0 +1,238 @@ +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 +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.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 +import to.bitkit.ui.components.BottomSheetPreview +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 +fun SendFeeRateScreen( + sendUiState: SendUiState, + onBack: () -> Unit, + onContinue: () -> Unit, + onSelect: (TransactionSpeed) -> Unit, + viewModel: SendFeeViewModel = hiltViewModel(), +) { + val uiState by viewModel.uiState.collectAsStateWithLifecycle() + + LaunchedEffect(Unit) { + viewModel.init(sendUiState) + } + + Content( + uiState = uiState, + onBack = onBack, + onContinue = onContinue, + onSelect = { onSelect(it.toSpeed()) }, + ) +} + +@Composable +private fun Content( + uiState: SendFeeUiState, + modifier: Modifier = Modifier, + onBack: () -> Unit = {}, + onContinue: () -> Unit = {}, + onSelect: (FeeRate) -> Unit = {}, +) { + Column( + modifier = modifier + .fillMaxSize() + .gradientBackground() + .navigationBarsPadding() + .testTag("speed_screen") + ) { + SheetTopBar(stringResource(R.string.wallet__send_fee_speed), onBack = onBack) + if (uiState.fees.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) + ) + uiState.fees.map { (feeRate, sats) -> + FeeItem( + feeRate = feeRate, + sats = sats, + isSelected = uiState.selected == feeRate, + isDisabled = false, // TODO + onClick = { onSelect(feeRate) }, + modifier = Modifier.testTag("fee_${feeRate.name}_button"), + ) + } + + 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) + } +} + +@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 = painterResource(feeRate.icon), + contentDescription = null, + tint = when { + isDisabled -> Colors.Gray3 + else -> feeRate.color + }, + modifier = Modifier.size(32.dp), + ) + HorizontalSpacer(16.dp) + Column { + BodyMSB(stringResource(feeRate.title), color = color) + BodySSB(stringResource(feeRate.description), 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) + } + } + } + } +} + +@Suppress("MagicNumber") +@Preview(showSystemUi = true) +@Composable +private fun Preview() { + AppThemeSurface { + BottomSheetPreview { + Content( + uiState = SendFeeUiState( + fees = mapOf( + FeeRate.FAST to 4000L, + FeeRate.NORMAL to 3000L, + FeeRate.SLOW to 2000L, + FeeRate.CUSTOM to 0L, + ), + selected = FeeRate.NORMAL, + ), + modifier = Modifier.sheetHeight(), + ) + } + } +} + +@Suppress("MagicNumber") +@Preview(showSystemUi = true) +@Composable +private fun PreviewCustom() { + AppThemeSurface { + BottomSheetPreview { + Content( + uiState = SendFeeUiState( + 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 new file mode 100644 index 000000000..1dda3a11e --- /dev/null +++ b/app/src/main/java/to/bitkit/ui/screens/wallets/send/SendFeeViewModel.kt @@ -0,0 +1,34 @@ +package to.bitkit.ui.screens.wallets.send + +import androidx.lifecycle.ViewModel +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.update +import to.bitkit.models.FeeRate +import to.bitkit.viewmodels.SendUiState +import javax.inject.Inject + +@HiltViewModel +class SendFeeViewModel @Inject constructor( +) : ViewModel() { + private val _uiState = MutableStateFlow(SendFeeUiState()) + val uiState = _uiState.asStateFlow() + + private lateinit var sendUiState: SendUiState + + fun init(sendUiState: SendUiState) { + this.sendUiState = sendUiState + _uiState.update { + it.copy( + selected = FeeRate.fromSpeed(sendUiState.speed), + fees = sendUiState.fees + ) + } + } +} + +data class SendFeeUiState( + val fees: Map = emptyMap(), + val selected: FeeRate? = null, +) 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/BoostTransactionSheet.kt b/app/src/main/java/to/bitkit/ui/sheets/BoostTransactionSheet.kt index 781a42d33..e0ad721a0 100644 --- a/app/src/main/java/to/bitkit/ui/sheets/BoostTransactionSheet.kt +++ b/app/src/main/java/to/bitkit/ui/sheets/BoostTransactionSheet.kt @@ -256,7 +256,6 @@ private fun DefaultModeContent( Icon( painter = painterResource(R.drawable.ic_pencil_simple), - tint = Colors.White, contentDescription = stringResource(R.string.common__edit), modifier = Modifier .size(16.dp) 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..affae7ecf 100644 --- a/app/src/main/java/to/bitkit/ui/sheets/SendSheet.kt +++ b/app/src/main/java/to/bitkit/ui/sheets/SendSheet.kt @@ -21,11 +21,13 @@ 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 -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 @@ -41,14 +43,13 @@ 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() } } - Column( modifier = Modifier .fillMaxWidth() @@ -63,11 +64,13 @@ 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) is SendEffect.NavigateToWithdrawConfirm -> navController.navigate(SendRoute.WithdrawConfirm) is SendEffect.NavigateToWithdrawError -> navController.navigate(SendRoute.WithdrawError) + is SendEffect.NavigateToFee -> navController.navigate(SendRoute.FeeRate) } } } @@ -117,6 +120,23 @@ fun SendSheet( onContinue = { utxos -> appViewModel.setSendEvent(SendEvent.CoinSelectionContinue(utxos)) }, ) } + composableWithDefaultTransitions { + val sendUiState by appViewModel.sendUiState.collectAsStateWithLifecycle() + SendFeeRateScreen( + sendUiState = sendUiState, + onBack = { navController.popBackStack() }, + onContinue = { navController.popBackStack() }, + onSelect = { appViewModel.setSendEvent(SendEvent.SpeedChange(it)) }, + ) + } + composableWithDefaultTransitions { + val sendUiState by appViewModel.sendUiState.collectAsStateWithLifecycle() + SendFeeCustomScreen( + uiState = sendUiState, + onBack = { navController.popBackStack() }, + onContinue = {}, // TODO + ) + } composableWithDefaultTransitions { val uiState by appViewModel.sendUiState.collectAsStateWithLifecycle() SendConfirmScreen( @@ -139,7 +159,7 @@ fun SendSheet( } composableWithDefaultTransitions { val uiState by appViewModel.sendUiState.collectAsStateWithLifecycle() - WithDrawErrorScreen( + WithdrawErrorScreen( uiState = uiState, onBack = { navController.popBackStack() }, onClickScan = { navController.navigate(SendRoute.QrScanner) }, @@ -239,6 +259,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/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 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..ce1ece0c7 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,20 +83,21 @@ 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) } 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) } @@ -110,8 +112,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 { diff --git a/app/src/main/java/to/bitkit/viewmodels/AppViewModel.kt b/app/src/main/java/to/bitkit/viewmodels/AppViewModel.kt index edc304aaf..289cd49d0 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 @@ -22,6 +23,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 @@ -33,6 +37,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 @@ -45,6 +50,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 @@ -53,6 +59,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 @@ -63,6 +70,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 @@ -73,8 +81,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 @@ -93,6 +101,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() { @@ -269,6 +278,7 @@ class AppViewModel @Inject constructor( } // region send + private fun observeSendEvents() { viewModelScope.launch { sendEvents.collect { @@ -289,8 +299,9 @@ class AppViewModel @Inject constructor( is SendEvent.CoinSelectionContinue -> onCoinSelectionContinue(it.utxos) is SendEvent.CommentChange -> onCommentChange(it.value) + is SendEvent.SpeedChange -> onSpeedChange(it.speed) - SendEvent.SpeedAndFee -> toast(Exception("Coming soon: Speed and Fee")) + SendEvent.SpeedAndFee -> setSendEffect(SendEffect.NavigateToFee) SendEvent.SwipeToPay -> onSwipeToPay() SendEvent.ConfirmAmountWarning -> onConfirmAmountWarning() SendEvent.DismissAmountWarning -> onDismissAmountWarning() @@ -353,6 +364,30 @@ 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 && satsPerVByteChanged) 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) + } + private fun onPaymentMethodSwitch() { val nextPaymentMethod = when (_sendUiState.value.payMethod) { SendMethod.ONCHAIN -> SendMethod.LIGHTNING @@ -370,8 +405,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 @@ -383,14 +420,16 @@ class AppViewModel @Inject constructor( return } - setSendEffect(SendEffect.NavigateToReview) + refreshOnchainSendIfNeeded() + setSendEffect(SendEffect.NavigateToConfirm) } - private fun onCoinSelectionContinue(utxos: List) { + private suspend fun onCoinSelectionContinue(utxos: List) { _sendUiState.update { it.copy(selectedUtxos = utxos) } - setSendEffect(SendEffect.NavigateToReview) + refreshFeeEstimates() + setSendEffect(SendEffect.NavigateToConfirm) } private fun validateAmount( @@ -399,26 +438,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 + SendMethod.ONCHAIN -> amount > Env.TransactionDefaults.dustLimit.toULong() } } @@ -448,16 +485,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) @@ -501,10 +538,11 @@ class AppViewModel @Inject constructor( ) if (quickPayHandled) return + refreshOnchainSendIfNeeded() if (isMainScanner) { showSheet(Sheet.Send(SendRoute.Confirm)) } else { - setSendEffect(SendEffect.NavigateToReview) + setSendEffect(SendEffect.NavigateToConfirm) } return } @@ -554,7 +592,7 @@ class AppViewModel @Inject constructor( if (isMainScanner) { showSheet(Sheet.Send(SendRoute.Confirm)) } else { - setSendEffect(SendEffect.NavigateToReview) + setSendEffect(SendEffect.NavigateToConfirm) } return } @@ -580,7 +618,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 } @@ -602,7 +639,7 @@ class AppViewModel @Inject constructor( if (isMainScanner) { showSheet(Sheet.Send(SendRoute.Confirm)) } else { - setSendEffect(SendEffect.NavigateToReview) + setSendEffect(SendEffect.NavigateToConfirm) } return } @@ -627,7 +664,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 } @@ -807,7 +843,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()) @@ -929,7 +965,6 @@ class AppViewModel @Inject constructor( val lnurl = _sendUiState.value.lnurl as? LnurlParams.LnurlWithdraw if (lnurl == null) { - resetSendState() setSendEffect(SendEffect.NavigateToWithdrawError) return@launch } @@ -949,7 +984,6 @@ class AppViewModel @Inject constructor( ).getOrNull() if (invoice == null) { - resetSendState() setSendEffect(SendEffect.NavigateToWithdrawError) return@launch } @@ -967,7 +1001,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) @@ -993,11 +1026,11 @@ class AppViewModel @Inject constructor( } private suspend fun sendOnchain(address: String, amount: ULong): Result { - val utxos = _sendUiState.value.selectedUtxos return lightningRepo.sendOnChain( address = address, sats = amount, - utxosToSpend = utxos, + speed = _sendUiState.value.speed, + utxosToSpend = _sendUiState.value.selectedUtxos, ) } @@ -1033,10 +1066,6 @@ class AppViewModel @Inject constructor( } } - private fun getMinOnchainTx(): ULong { - return Env.TransactionDefaults.dustLimit.toULong() - } - fun clearClipboardForAutoRead() { viewModelScope.launch { val isAutoReadClipboardEnabled = settingsStore.data.first().enableAutoReadClipboard @@ -1048,8 +1077,99 @@ class AppViewModel @Inject constructor( fun resetQuickPayData() = _quickPayData.update { null } - fun resetSendState() { - _sendUiState.value = SendUiState() + /** Reselect utxos for current amount & speed then refresh fees using updated utxos */ + private fun refreshOnchainSendIfNeeded() { + val currentState = _sendUiState.value + if (currentState.payMethod != SendMethod.ONCHAIN || + currentState.amount == 0uL || + currentState.address.isEmpty() + ) { + return + } + + // refresh in background + viewModelScope.launch(bgDispatcher) { + // preselect utxos for deterministic fee estimation + if (settingsStore.data.first().coinSelectAuto && currentState.selectedUtxos == null) { + lightningRepo.getFeeRateForSpeed(currentState.speed, currentState.feeRates) + .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 + + 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 (currentState.feeRates?.getSatsPerVByteFor(speed) != 0u) getFeeEstimate(speed) else 0 + + 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, + feeRates = currentState.feeRates, + ).getOrDefault(0u).toLong() + } + + suspend fun resetSendState() { + 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 @@ -1231,8 +1351,13 @@ class AppViewModel @Inject constructor( proceedWithPayment() } } + + companion object { + private const val TAG = "AppViewModel" + } } + // region send contract data class SendUiState( val address: String = "", @@ -1251,9 +1376,11 @@ data class SendUiState( val selectedUtxos: List? = null, val lnurl: LnurlParams? = null, val isLoading: Boolean = false, - val speed: TransactionSpeed? = null, - val utxosToSpend: List? = null, + val speed: TransactionSpeed = TransactionSpeed.default(), val comment: String = "", + val feeRates: FeeRates? = null, + val fee: Long = 0, + val fees: Map = emptyMap(), ) enum class AmountWarning(@StringRes val message: Int) { @@ -1269,11 +1396,13 @@ 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() data object NavigateToCoinSelection : SendEffect() data object NavigateToQuickPay : SendEffect() + data object NavigateToFee : SendEffect() data class PaymentSuccess(val sheet: NewTransactionSheetDetails? = null) : SendEffect() } @@ -1306,6 +1435,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 { 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 @@ diff --git a/app/src/main/res/drawable/ic_pencil_simple.xml b/app/src/main/res/drawable/ic_pencil_simple.xml index 6200edc97..6957ccfdd 100644 --- a/app/src/main/res/drawable/ic_pencil_simple.xml +++ b/app/src/main/res/drawable/ic_pencil_simple.xml @@ -6,14 +6,14 @@ diff --git a/app/src/test/java/to/bitkit/repositories/LightningRepoTest.kt b/app/src/test/java/to/bitkit/repositories/LightningRepoTest.kt index 13427a20b..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 @@ -369,13 +374,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" ) @@ -432,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) + } } 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) 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