diff --git a/app/detekt-baseline.xml b/app/detekt-baseline.xml
index 8584fc7fd..9ece0cf15 100644
--- a/app/detekt-baseline.xml
+++ b/app/detekt-baseline.xml
@@ -79,21 +79,21 @@
CyclomaticComplexMethod:AmountInput.kt$@Composable fun AmountInput( modifier: Modifier = Modifier, defaultValue: Long = 0, primaryDisplay: PrimaryDisplay, showConversion: Boolean = false, overrideSats: Long? = null, onSatsChange: (Long) -> Unit, )
CyclomaticComplexMethod:AppStatusScreen.kt$@Composable private fun Content( uiState: AppStatusUiState = AppStatusUiState(), onBack: () -> Unit = {}, onClose: () -> Unit = {}, onInternetClick: () -> Unit = {}, onElectrumClick: () -> Unit = {}, onNodeClick: () -> Unit = {}, onChannelsClick: () -> Unit = {}, onBackupClick: () -> Unit = {}, )
CyclomaticComplexMethod:AppViewModel.kt$AppViewModel$private fun observeSendEvents()
+ CyclomaticComplexMethod:AppViewModel.kt$AppViewModel$private suspend fun handleSanityChecks(amountSats: ULong)
CyclomaticComplexMethod:BlocktankRegtestScreen.kt$@Composable fun BlocktankRegtestScreen( navController: NavController, viewModel: BlocktankRegtestViewModel = hiltViewModel(), )
CyclomaticComplexMethod:ChannelDetailScreen.kt$@OptIn(ExperimentalMaterial3Api::class) @Composable private fun Content( channel: ChannelUi, blocktankOrders: List<IBtOrder> = emptyList(), cjitEntries: List<IcJitEntry> = emptyList(), txDetails: TxDetails? = null, isRefreshing: Boolean = false, onBack: () -> Unit = {}, onClose: () -> Unit = {}, onRefresh: () -> Unit = {}, onCopyText: (String) -> Unit = {}, onOpenUrl: (String) -> Unit = {}, onSupport: (Any) -> Unit = {}, onCloseConnection: () -> Unit = {}, )
CyclomaticComplexMethod:ConfirmMnemonicScreen.kt$@Composable fun ConfirmMnemonicScreen( uiState: BackupContract.UiState, onContinue: () -> Unit, onBack: () -> Unit, )
CyclomaticComplexMethod:ContentView.kt$@Composable fun ContentView( appViewModel: AppViewModel, walletViewModel: WalletViewModel, blocktankViewModel: BlocktankViewModel, currencyViewModel: CurrencyViewModel, activityListViewModel: ActivityListViewModel, transferViewModel: TransferViewModel, settingsViewModel: SettingsViewModel, backupsViewModel: BackupsViewModel, )
- CyclomaticComplexMethod:CoreService.kt$ActivityService$suspend fun syncLdkNodePayments(payments: List<PaymentDetails>, forceUpdate: Boolean = false)
+ CyclomaticComplexMethod:CoreService.kt$ActivityService$private suspend fun processOnchainPayment( kind: PaymentKind.Onchain, payment: PaymentDetails, forceUpdate: Boolean, )
CyclomaticComplexMethod:HealthRepo.kt$HealthRepo$private fun collectState()
CyclomaticComplexMethod:HomeScreen.kt$@Composable fun HomeScreen( mainUiState: MainUiState, drawerState: DrawerState, rootNavController: NavController, walletNavController: NavHostController, settingsViewModel: SettingsViewModel, walletViewModel: WalletViewModel, appViewModel: AppViewModel, activityListViewModel: ActivityListViewModel, homeViewModel: HomeViewModel = hiltViewModel(), )
- CyclomaticComplexMethod:HomeScreen.kt$@OptIn(ExperimentalMaterial3Api::class, ExperimentalHazeMaterialsApi::class) @Composable private fun Content( mainUiState: MainUiState, homeUiState: HomeUiState, rootNavController: NavController, walletNavController: NavController, drawerState: DrawerState, hazeState: HazeState = rememberHazeState(), latestActivities: List<Activity>?, onClickProfile: () -> Unit = {}, onRefresh: () -> Unit = {}, onRemoveSuggestion: (Suggestion) -> Unit = {}, onClickSuggestion: (Suggestion) -> Unit = {}, onClickAddWidget: () -> Unit = {}, onClickEnableEdit: () -> Unit = {}, onClickConfirmEdit: () -> Unit = {}, onClickEditWidget: (WidgetType) -> Unit = {}, onClickDeleteWidget: (WidgetType) -> Unit = {}, onMoveWidget: (Int, Int) -> Unit = { _, _ -> }, onDismissEmptyState: () -> Unit = {}, onDismissHighBalanceSheet: () -> Unit = {}, onClickEmptyActivityRow: () -> Unit = {}, balances: BalanceState = LocalBalances.current, )
- CyclomaticComplexMethod:InactivityTracker.kt$@Composable fun InactivityTracker( app: AppViewModel, settings: SettingsViewModel, modifier: Modifier = Modifier, content: @Composable () -> Unit, )
+ CyclomaticComplexMethod:HomeScreen.kt$@OptIn(ExperimentalMaterial3Api::class, ExperimentalHazeMaterialsApi::class) @Composable private fun Content( mainUiState: MainUiState, homeUiState: HomeUiState, rootNavController: NavController, walletNavController: NavController, drawerState: DrawerState, hazeState: HazeState = rememberHazeState(), latestActivities: List<Activity>?, onClickProfile: () -> Unit = {}, onRefresh: () -> Unit = {}, onRemoveSuggestion: (Suggestion) -> Unit = {}, onClickSuggestion: (Suggestion) -> Unit = {}, onClickAddWidget: () -> Unit = {}, onClickEditWidgetList: () -> Unit = {}, onClickEditWidget: (WidgetType) -> Unit = {}, onClickDeleteWidget: (WidgetType) -> Unit = {}, onMoveWidget: (Int, Int) -> Unit = { _, _ -> }, onDismissEmptyState: () -> Unit = {}, onDismissHighBalanceSheet: () -> Unit = {}, onClickEmptyActivityRow: () -> Unit = {}, balances: BalanceState = LocalBalances.current, )
CyclomaticComplexMethod:LightningService.kt$LightningService$private fun logEvent(event: Event)
- CyclomaticComplexMethod:NumberPadTextField.kt$@Composable fun AmountInputHandler( input: String, primaryDisplay: PrimaryDisplay, displayUnit: BitcoinDisplayUnit, onInputChanged: (String) -> Unit, onAmountCalculated: (String) -> Unit, currencyVM: CurrencyViewModel, overrideSats: Long? = null, )
- CyclomaticComplexMethod:NumberPadTextField.kt$@Composable fun NumberPadTextField( input: String, displayUnit: BitcoinDisplayUnit, primaryDisplay: PrimaryDisplay, modifier: Modifier = Modifier, )
+ CyclomaticComplexMethod:NumberPadTextField.kt$@Composable fun AmountInputHandler( input: String, primaryDisplay: PrimaryDisplay, displayUnit: BitcoinDisplayUnit, onInputChanged: (String) -> Unit, onAmountCalculated: (String) -> Unit, currencyVM: CurrencyViewModel = hiltViewModel(), overrideSats: Long? = null, )
+ CyclomaticComplexMethod:NumberPadTextField.kt$@Composable fun NumberPadTextField( input: String, displayUnit: BitcoinDisplayUnit, primaryDisplay: PrimaryDisplay, modifier: Modifier = Modifier, showSecondaryField: Boolean = true, )
CyclomaticComplexMethod:ReceiveQrScreen.kt$@Composable fun ReceiveQrScreen( cjitInvoice: MutableState<String?>, cjitActive: MutableState<Boolean>, walletState: MainUiState, onCjitToggle: (Boolean) -> Unit, onClickEditInvoice: () -> Unit, onClickReceiveOnSpending: () -> Unit, modifier: Modifier = Modifier, )
CyclomaticComplexMethod:RestoreWalletScreen.kt$@Composable fun RestoreWalletView( onBackClick: () -> Unit, onRestoreClick: (mnemonic: String, passphrase: String?) -> Unit, )
- CyclomaticComplexMethod:SendSheet.kt$@Composable fun SendSheet( appViewModel: AppViewModel, walletViewModel: WalletViewModel, startDestination: SendRoute = SendRoute.Recipient, onComplete: (NewTransactionSheetDetails?) -> Unit, )
+ CyclomaticComplexMethod:SendSheet.kt$@Composable fun SendSheet( appViewModel: AppViewModel, walletViewModel: WalletViewModel, startDestination: SendRoute = SendRoute.Recipient, )
CyclomaticComplexMethod:SettingsButtonRow.kt$@Composable fun SettingsButtonRow( title: String, modifier: Modifier = Modifier, subtitle: String? = null, value: SettingsButtonValue = SettingsButtonValue.None, description: String? = null, iconRes: Int? = null, iconTint: Color = Color.Unspecified, iconSize: Dp = 32.dp, maxLinesSubtitle: Int = Int.MAX_VALUE, enabled: Boolean = true, loading: Boolean = false, onClick: () -> Unit, )
CyclomaticComplexMethod:Slider.kt$@Composable fun StepSlider( value: Int, steps: List<Int>, onValueChange: (Int) -> Unit, modifier: Modifier = Modifier, )
CyclomaticComplexMethod:WakeNodeWorker.kt$WakeNodeWorker$private suspend fun handleLdkEvent(event: Event)
@@ -115,24 +115,18 @@
ForbiddenComment:ActivityDetailScreen.kt$/* TODO: Implement assign functionality */
ForbiddenComment:ActivityListViewModel.kt$ActivityListViewModel$// TODO: sync only on specific events for better performance
ForbiddenComment:ActivityRow.kt$// TODO: calculate confirmsIn text
- ForbiddenComment:AppViewModel.kt$AppViewModel$// TODO: fee is not the sats sent. Need to get this amount from elsewhere like send flow or something.
ForbiddenComment:AppViewModel.kt$AppViewModel$// TODO: handle ONLY cjit as payment received. This makes it look like any channel confirmed is a received payment.
ForbiddenComment:BackupNavSheetViewModel.kt$BackupNavSheetViewModel$// TODO: get from actual repository state
ForbiddenComment:BackupRepo.kt$BackupRepo$// TODO: Add other backup categories as they get implemented:
ForbiddenComment:BoostTransactionViewModel.kt$BoostTransactionUiState$// TODO: Implement dynamic time estimation
ForbiddenComment:ChannelStatusView.kt$// TODO: handle closed channels marking & detection
ForbiddenComment:ContentView.kt$// TODO: display as sheet
- ForbiddenComment:CoreService.kt$ActivityService$// TODO: find address
- ForbiddenComment:CoreService.kt$ActivityService$// TODO: get from linked order
- ForbiddenComment:CoreService.kt$ActivityService$// TODO: get from somewhere
- ForbiddenComment:CoreService.kt$ActivityService$// TODO: handle when paying for order
ForbiddenComment:Env.kt$Env$// TODO: remove this to load from BT API instead
ForbiddenComment:ExternalNodeViewModel.kt$ExternalNodeViewModel$// TODO: pass customFeeRate to ldk-node when supported
ForbiddenComment:LightningConnectionsViewModel.kt$LightningConnectionsViewModel$// TODO: sort channels to get consistent index; node.listChannels returns a list in random order
ForbiddenComment:LightningNodeService.kt$LightningNodeService$// TODO: Get from resources
ForbiddenComment:Notifications.kt$// TODO: review if needed:
ForbiddenComment:SuccessScreen.kt$// TODO: verify backup
- ForbiddenComment:TransferIntroScreen.kt$// TODO: show on first LN suggestion card click
ForbiddenComment:TransferViewModel.kt$TransferViewModel$// TODO: showBottomSheet: forceTransfer
FunctionOnlyReturningConstant:RepoModule.kt$RepoModule$@Provides @Named("enablePolling") fun provideEnablePolling(): Boolean
FunctionOnlyReturningConstant:ShopWebViewInterface.kt$ShopWebViewInterface$@JavascriptInterface fun isReady(): Boolean
@@ -172,23 +166,23 @@
LambdaParameterInRestartableEffect:SendPinCheckScreen.kt$onSuccess
LambdaParameterInRestartableEffect:SendQuickPayScreen.kt$onPaymentComplete
LambdaParameterInRestartableEffect:SendQuickPayScreen.kt$onShowError
- LambdaParameterInRestartableEffect:SendSheet.kt$onComplete
LambdaParameterInRestartableEffect:SheetHost.kt$onDismiss
LambdaParameterInRestartableEffect:SpendingAmountScreen.kt$onOrderCreated
LambdaParameterInRestartableEffect:SpendingAmountScreen.kt$toast
LambdaParameterInRestartableEffect:SpendingAmountScreen.kt$toastException
LargeClass:AppViewModel.kt$AppViewModel : ViewModel
LargeClass:LightningRepo.kt$LightningRepo
+ LongMethod:ActivityRepo.kt$ActivityRepo$private suspend fun syncTagsMetadata()
LongMethod:AppViewModel.kt$AppViewModel$private fun observeLdkNodeEvents()
LongMethod:AppViewModel.kt$AppViewModel$private suspend fun proceedWithPayment()
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)
LongMethod:LightningRepo.kt$LightningRepo$suspend fun start( walletIndex: Int = 0, timeout: Duration? = null, shouldRetry: Boolean = true, eventHandler: NodeEventHandler? = null, customServer: ElectrumServer? = null, customRgsServerUrl: String? = null, ): Result<Unit>
LongMethod:LightningService.kt$LightningService$private fun logEvent(event: Event)
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:ActivityRepo.kt$ActivityRepo$( id: String, paymentHash: String? = null, txId: String? = null, address: String, isReceive: Boolean, tags: List<String>, )
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, )
@@ -200,12 +194,10 @@
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, 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, )
LongParameterList:TransferViewModel.kt$TransferViewModel$( @ApplicationContext private val context: Context, private val lightningRepo: LightningRepo, private val blocktankRepo: BlocktankRepo, private val walletRepo: WalletRepo, private val currencyRepo: CurrencyRepo, private val settingsStore: SettingsStore, private val cacheStore: CacheStore, )
LongParameterList:WalletRepo.kt$WalletRepo$( @BgDispatcher private val bgDispatcher: CoroutineDispatcher, private val db: AppDb, private val keychain: Keychain, private val coreService: CoreService, private val settingsStore: SettingsStore, private val addressChecker: AddressChecker, private val lightningRepo: LightningRepo, private val cacheStore: CacheStore, )
LongParameterList:WidgetsRepo.kt$WidgetsRepo$( @BgDispatcher private val bgDispatcher: CoroutineDispatcher, private val newsService: NewsService, private val factsService: FactsService, private val blocksService: BlocksService, private val weatherService: WeatherService, private val priceService: PriceService, private val widgetsStore: WidgetsStore, private val settingsStore: SettingsStore, )
- LoopWithTooManyJumpStatements:CoreService.kt$ActivityService$for
LoopWithTooManyJumpStatements:MonetaryVisualTransformation.kt$MonetaryVisualTransformation.<no name provided>$for
LoopWithTooManyJumpStatements:TransferViewModel.kt$TransferViewModel$while
MagicNumber:ActivityDetailScreen.kt$40
@@ -231,8 +223,6 @@
MagicNumber:AppStatus.kt$0.6f
MagicNumber:AppStatus.kt$2000
MagicNumber:AppStatus.kt$600
- MagicNumber:AppViewModel.kt$AppViewModel$0.5
- MagicNumber:AppViewModel.kt$AppViewModel$10
MagicNumber:AppViewModel.kt$AppViewModel$1000
MagicNumber:AppViewModel.kt$AppViewModel$250
MagicNumber:AppViewModel.kt$AppViewModel$300
@@ -304,7 +294,6 @@
MagicNumber:HomeScreen.kt$3
MagicNumber:HttpModule.kt$HttpModule$30_000
MagicNumber:HttpModule.kt$HttpModule$60_000
- MagicNumber:InactivityTracker.kt$1000
MagicNumber:InitializingWalletView.kt$500
MagicNumber:InitializingWalletView.kt$99.9
MagicNumber:Keyboard.kt$0.2f
@@ -316,7 +305,6 @@
MagicNumber:Logger.kt$Logger$1024L
MagicNumber:Logger.kt$Logger$4
MagicNumber:LogsRepo.kt$LogsRepo$3
- MagicNumber:MainActivity.kt$MainActivity$4
MagicNumber:MigrationService.kt$MigrationService$1000
MagicNumber:MigrationService.kt$MigrationService$1000L
MagicNumber:NewsService.kt$NewsService$10
@@ -354,7 +342,10 @@
MagicNumber:SavingsConfirmScreen.kt$300
MagicNumber:SavingsProgressScreen.kt$2500
MagicNumber:SavingsProgressScreen.kt$5000
+ MagicNumber:SendConfirmScreen.kt$1_234
MagicNumber:SendConfirmScreen.kt$300
+ MagicNumber:SendConfirmScreen.kt$43
+ MagicNumber:SendConfirmScreen.kt$654_321
MagicNumber:SettingUpScreen.kt$3
MagicNumber:SettingUpScreen.kt$5000
MagicNumber:SettingsButtonRow.kt$0.5f
@@ -399,7 +390,6 @@
MatchingDeclarationName:SavingsProgressScreen.kt$SavingsProgressState
MatchingDeclarationName:SettingsButtonRow.kt$SettingsButtonValue
MaxLineLength:ActivityDetailScreen.kt$description = "Unable to increase the fee any further. Otherwise, it will exceed half the current input balance"
- MaxLineLength:AppViewModel.kt$AppViewModel$// TODO: fee is not the sats sent. Need to get this amount from elsewhere like send flow or something.
MaxLineLength:AppViewModel.kt$AppViewModel$// TODO: handle ONLY cjit as payment received. This makes it look like any channel confirmed is a received payment.
MaxLineLength:Bip39Test.kt$Bip39Test$"AbAnDoN abandon ABANDON abandon abandon abandon abandon abandon abandon abandon abandon about".toWordList().validBip39Checksum()
MaxLineLength:Bip39Test.kt$Bip39Test$"abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon art" to true
@@ -431,14 +421,12 @@
MaxLineLength:LightningService.kt$LightningService$"⛔ Channel closed: channelId: $channelId userChannelId: $userChannelId counterpartyNodeId: $counterpartyNodeId reason: $reason"
MaxLineLength:LightningService.kt$LightningService$"👐 Channel ready: channelId: $channelId userChannelId: $userChannelId counterpartyNodeId: $counterpartyNodeId"
MaxLineLength:LightningService.kt$LightningService$"🫰 Payment claimable: paymentId: $paymentId paymentHash: $paymentHash claimableAmountMsat: $claimableAmountMsat"
- MaxLineLength:Nav.kt$* Adds the [androidx.compose.runtime.Composable] to the [androidx.navigation.NavGraphBuilder] with the default screen transitions.
MaxLineLength:ReceiveLiquidityScreen.kt$if (isAdditional) R.string.wallet__receive_liquidity__label_additional else R.string.wallet__receive_liquidity__label
MaxLineLength:ReceiveLiquidityScreen.kt$if (isAdditional) R.string.wallet__receive_liquidity__nav_title_additional else R.string.wallet__receive_liquidity__nav_title
MaxLineLength:ReceiveLiquidityScreen.kt$if (isAdditional) R.string.wallet__receive_liquidity__text_additional else R.string.wallet__receive_liquidity__text
MaxLineLength:SecuritySettingsScreen.kt$if (isPinEnabled) R.string.settings__security__pin_enabled else R.string.settings__security__pin_disabled
MaxLineLength:SendAddressScreen.kt$addressInput = "bitcoin:bc17tq4mtkq86vte7a26e0za560kgflwqsvxznmer5?lightning=LNBC1PQUVNP8KHGPLNF6REGS3VY5F40AJFUN4S2JUDQQNP4TK9MP6LWWLWTC3XX3UUEVYZ4EVQU3X4NQDX348QPP5WJC9DWNTAFN7FZEZFVDC3MHV67SX2LD2MG602E3LEZDMFT29JLWQSP54QKM4G8A2KD5RGEKACA3CH4XV4M2MQDN62F8S2CCRES9QYYSGQCQPCXQRRSSRZJQWQKZS03MNNHSTKR9DN2XQRC8VW5X6CEWAL8C6RW6QQ3T02T3R"
MaxLineLength:SettingsScreen.kt$if (newValue) R.string.settings__dev_enabled_message else R.string.settings__dev_disabled_message
- MaxLineLength:VssStoreIdProvider.kt$VssStoreIdProvider$"Do not run this on mainnet until VSS auth is implemented. Below hack is a temporary fix and not safe for mainnet."
MaxLineLength:WeatherService.kt$WeatherService$val avgFeeUsd = currencyRepo.convertSatsToFiat(avgFeeSats.toLong(), currency = USD_CURRENCY).getOrNull() ?: return FeeCondition.AVERAGE
MaximumLineLength:ActivityDetailScreen.kt$
MaximumLineLength:Bip39Test.kt$Bip39Test$
@@ -457,7 +445,6 @@
MaximumLineLength:SecuritySettingsScreen.kt$
MaximumLineLength:SendAddressScreen.kt$
MaximumLineLength:SettingsScreen.kt$
- MaximumLineLength:VssStoreIdProvider.kt$VssStoreIdProvider$
MaximumLineLength:WeatherService.kt$WeatherService$
MayBeConst:Env.kt$Env$val walletSyncIntervalSecs = 10_uL // TODO review
MayBeConst:Env.kt$Env.TransactionDefaults$/** * Minimum value in sats for an output. Outputs below the dust limit may not be processed because the fees * required to include them in a block would be greater than the value of the transaction itself. * */ val dustLimit = 546u
@@ -584,7 +571,6 @@
ParameterNaming:RestoreWalletScreen.kt$onValueChanged
ParameterNaming:SendAmountScreen.kt$onInputChanged
ParameterNaming:SpendingAdvancedScreen.kt$onOrderCreated
- ParameterNaming:SpendingAmountScreen.kt$onAmountChanged
ParameterNaming:SpendingAmountScreen.kt$onOrderCreated
ParameterNaming:TransactionSpeedSettingsScreen.kt$onSpeedSelected
PreviewPublic:CameraPermissionView.kt$PreviewDenied
@@ -611,12 +597,10 @@
SpreadOperator:RestoreWalletScreen.kt$(*Array(24) { "" })
SwallowedException:Crypto.kt$Crypto$e: Exception
SwallowedException:FcmService.kt$FcmService$e: SerializationException
- ThrowingExceptionsWithoutMessageOrCause:ActivityRepo.kt$ActivityRepo$Exception()
TooGenericExceptionCaught:ActivityDetailViewModel.kt$ActivityDetailViewModel$e: Exception
TooGenericExceptionCaught:ActivityDetailViewModel.kt$ActivityDetailViewModel$e: Throwable
TooGenericExceptionCaught:ActivityListViewModel.kt$ActivityListViewModel$e: Exception
TooGenericExceptionCaught:ActivityRepo.kt$ActivityRepo$e: Exception
- TooGenericExceptionCaught:ActivityRepo.kt$ActivityRepo$e: Throwable
TooGenericExceptionCaught:AddressChecker.kt$AddressChecker$e: Exception
TooGenericExceptionCaught:AppViewModel.kt$AppViewModel$e: Exception
TooGenericExceptionCaught:AppViewModel.kt$AppViewModel$e: Throwable
@@ -630,7 +614,6 @@
TooGenericExceptionCaught:ChannelOrdersScreen.kt$e: Throwable
TooGenericExceptionCaught:ContentView.kt$e: Throwable
TooGenericExceptionCaught:CoreService.kt$ActivityService$e: Exception
- TooGenericExceptionCaught:CoreService.kt$ActivityService$e: Throwable
TooGenericExceptionCaught:CoreService.kt$CoreService$e: Exception
TooGenericExceptionCaught:Crypto.kt$Crypto$e: Exception
TooGenericExceptionCaught:CurrencyRepo.kt$CurrencyRepo$e: Exception
@@ -642,7 +625,6 @@
TooGenericExceptionCaught:Logger.kt$Logger$e: Throwable
TooGenericExceptionCaught:LogsRepo.kt$LogsRepo$e: Exception
TooGenericExceptionCaught:LogsViewModel.kt$LogsViewModel$e: Exception
- TooGenericExceptionCaught:MainActivity.kt$MainActivity$e: Throwable
TooGenericExceptionCaught:NewTransactionSheetDetails.kt$NewTransactionSheetDetails.Companion$e: Exception
TooGenericExceptionCaught:PriceService.kt$PriceService$e: Exception
TooGenericExceptionCaught:QrScanningScreen.kt$e: Exception
@@ -691,6 +673,7 @@
TooManyFunctions:SendConfirmScreen.kt$to.bitkit.ui.screens.wallets.send.SendConfirmScreen.kt
TooManyFunctions:SettingsViewModel.kt$SettingsViewModel : ViewModel
TooManyFunctions:TOS.kt$to.bitkit.ui.onboarding.TOS.kt
+ TooManyFunctions:TagMetadataDao.kt$TagMetadataDao
TooManyFunctions:Text.kt$to.bitkit.ui.components.Text.kt
TooManyFunctions:Text.kt$to.bitkit.ui.utils.Text.kt
TooManyFunctions:TransferViewModel.kt$TransferViewModel : ViewModel
@@ -700,8 +683,6 @@
TooManyFunctions:WidgetsStore.kt$WidgetsStore
TopLevelPropertyNaming:DrawerMenu.kt$private const val zIndexMenu = 11f
TopLevelPropertyNaming:DrawerMenu.kt$private const val zIndexScrim = 10f
- UnusedParameter:HomeScreen.kt$onClickEnableEdit: () -> Unit = {}
- UnusedPrivateProperty:ActivityListViewModel.kt$ActivityListViewModel$private val lightningRepo: LightningRepo
UnusedPrivateProperty:ActivityRepoTest.kt$ActivityRepoTest$private val testOnChainActivity = mock<Activity.Onchain> { on { v1 } doReturn testOnChainActivityV1 }
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 } }" )
@@ -710,7 +691,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.hideSheet() appViewModel.clearClipboardForAutoRead() txSheet?.let { appViewModel.showNewTransactionSheet(it) } } )
+ ViewModelForwarding:ContentView.kt$SendSheet( appViewModel = appViewModel, walletViewModel = walletViewModel, startDestination = sheet.route, )
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/androidTest/java/to/bitkit/services/RoutingFeeEstimationTest.kt b/app/src/androidTest/java/to/bitkit/services/RoutingFeeEstimationTest.kt
new file mode 100644
index 000000000..c70725734
--- /dev/null
+++ b/app/src/androidTest/java/to/bitkit/services/RoutingFeeEstimationTest.kt
@@ -0,0 +1,343 @@
+package to.bitkit.services
+
+import android.content.Context
+import androidx.test.core.app.ApplicationProvider
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import dagger.hilt.android.testing.HiltAndroidRule
+import dagger.hilt.android.testing.HiltAndroidTest
+import kotlinx.coroutines.delay
+import kotlinx.coroutines.runBlocking
+import org.junit.After
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.lightningdevkit.ldknode.Bolt11Invoice
+import org.lightningdevkit.ldknode.NodeException
+import to.bitkit.data.CacheStore
+import to.bitkit.data.keychain.Keychain
+import to.bitkit.env.Env
+import to.bitkit.repositories.WalletRepo
+import to.bitkit.utils.LdkError
+import javax.inject.Inject
+import kotlin.test.assertEquals
+import kotlin.test.assertIs
+import kotlin.test.assertNotNull
+import kotlin.test.assertTrue
+
+@HiltAndroidTest
+@RunWith(AndroidJUnit4::class)
+class RoutingFeeEstimationTest {
+
+ companion object {
+ private const val NODE_STARTUP_MAX_RETRIES = 10
+ private const val NODE_STARTUP_RETRY_DELAY_MS = 1000L
+ private const val DEFAULT_INVOICE_EXPIRY_SECS = 3600u
+ }
+
+ @get:Rule
+ var hiltRule = HiltAndroidRule(this)
+
+ @Inject
+ lateinit var keychain: Keychain
+
+ @Inject
+ lateinit var lightningService: LightningService
+
+ @Inject
+ lateinit var walletRepo: WalletRepo
+
+ @Inject
+ lateinit var cacheStore: CacheStore
+
+ private val walletIndex = 0
+ private val appContext = ApplicationProvider.getApplicationContext()
+
+ @Before
+ fun setUp() {
+ // Use unique storage path per test to avoid DataStore conflicts
+ val testStoragePath = "${appContext.filesDir.absolutePath}/${System.currentTimeMillis()}"
+ Env.initAppStoragePath(testStoragePath)
+ hiltRule.inject()
+ println("Starting RoutingFeeEstimation test setup with storage: $testStoragePath")
+
+ runBlocking {
+ println("Wiping keychain before test")
+ keychain.wipe()
+ println("Keychain wiped successfully")
+ }
+ }
+
+ @After
+ fun tearDown() {
+ runBlocking {
+ println("Tearing down RoutingFeeEstimation test")
+
+ if (lightningService.status?.isRunning == true) {
+ try {
+ lightningService.stop()
+ } catch (e: Exception) {
+ println("Error stopping lightning service: ${e.message}")
+ }
+ }
+ try {
+ lightningService.wipeStorage(walletIndex = walletIndex)
+ } catch (e: Exception) {
+ println("Error wiping lightning storage: ${e.message}")
+ }
+
+ println("Resetting cache store to clear DataStore")
+ try {
+ cacheStore.reset()
+ println("Cache store reset successfully")
+ } catch (e: Exception) {
+ println("Error resetting cache store: ${e.message}")
+ }
+
+ println("Wiping keychain after test")
+ keychain.wipe()
+ println("Keychain wiped successfully")
+ }
+ }
+
+
+ @Test
+ fun testNewRoutingFeeMethodsExist() = runBlocking {
+ runNode()
+
+ val paymentAmountSats = 1000uL
+ val invoice = createInvoiceWithAmount(
+ amountSats = paymentAmountSats,
+ description = "Method existence test"
+ )
+
+ // Just test that the methods exist and can be called - don't worry about results
+ val bolt11Payment = lightningService.node!!.bolt11Payment()
+
+ println("Testing estimateRoutingFees method...")
+ runCatching {
+ bolt11Payment.estimateRoutingFees(invoice)
+ }.fold(
+ onSuccess = { fees ->
+ println("estimateRoutingFees returned: $fees msat")
+ assertTrue(true, "Method exists and returned a value")
+ },
+ onFailure = { error ->
+ println("estimateRoutingFees threw: ${error.message}")
+ // Method exists if it throws a specific error rather than NoSuchMethodError
+ assertTrue(
+ !(error.message?.contains("NoSuchMethodError") == true),
+ "Method should exist (got: ${error.message})"
+ )
+ }
+ )
+
+ val zeroInvoice = createZeroAmountInvoice("Zero amount method test")
+ println("Testing estimateRoutingFeesUsingAmount method...")
+ runCatching {
+ bolt11Payment.estimateRoutingFeesUsingAmount(zeroInvoice, 1_000_000uL)
+ }.fold(
+ onSuccess = { fees ->
+ println("estimateRoutingFeesUsingAmount returned: $fees msat")
+ assertTrue(true, "Method exists and returned a value")
+ },
+ onFailure = { error ->
+ println("estimateRoutingFeesUsingAmount threw: ${error.message}")
+ // Method exists if it throws a specific error rather than NoSuchMethodError
+ assertTrue(
+ !(error.message?.contains("NoSuchMethodError") == true),
+ "Method should exist (got: ${error.message})"
+ )
+ }
+ )
+ }
+
+ @Test
+ fun estimateRoutingFeesForVariableAmountInvoice() = runBlocking {
+ runNode()
+
+ val invoice = createZeroAmountInvoice("Variable amount fee estimation test")
+ val paymentAmountMsat = 5_000_000uL
+
+ runCatching {
+ lightningService.node!!.bolt11Payment()
+ .estimateRoutingFeesUsingAmount(invoice, paymentAmountMsat)
+ }.fold(
+ onSuccess = { estimatedFeesMsat ->
+ assertFeesAreReasonable(estimatedFeesMsat, paymentAmountMsat)
+ },
+ onFailure = { error ->
+ handleExpectedRoutingError(error as NodeException)
+ }
+ )
+ }
+
+ @Test
+ fun routeNotFoundErrorIsHandledProperly() = runBlocking {
+ runNode()
+
+ val largeAmountSats = 1_000_000uL
+ val invoiceToSelf = createInvoiceWithAmount(
+ amountSats = largeAmountSats,
+ description = "Route error handling test"
+ )
+
+ runCatching {
+ lightningService.node!!.bolt11Payment().estimateRoutingFees(invoiceToSelf)
+ }.fold(
+ onSuccess = { estimatedFeesMsat ->
+ assertTrue(
+ estimatedFeesMsat >= 0u,
+ "If routing unexpectedly succeeds, fees should be non-negative"
+ )
+ },
+ onFailure = { error ->
+ assertRoutingErrorOccurred(error as NodeException)
+ }
+ )
+ }
+
+ @Test
+ fun zeroAmountPaymentIsHandledGracefully() = runBlocking {
+ runNode()
+
+ val invoice = createZeroAmountInvoice("Zero amount validation test")
+ val zeroAmountMsat = 0uL
+
+ runCatching {
+ lightningService.node!!.bolt11Payment()
+ .estimateRoutingFeesUsingAmount(invoice, zeroAmountMsat)
+ }.fold(
+ onSuccess = { estimatedFeesMsat ->
+ assertEquals(
+ 0uL,
+ estimatedFeesMsat,
+ "Zero amount should result in zero fees"
+ )
+ },
+ onFailure = {
+ assertTrue(
+ true,
+ "Zero amount payment throwing an error is acceptable"
+ )
+ }
+ )
+ }
+
+ @Test
+ fun routingFeesScaleWithPaymentAmount() = runBlocking {
+ runNode()
+
+ val invoice = createZeroAmountInvoice("Fee scaling test")
+ val smallAmountMsat = 1_000_000uL
+ val largeAmountMsat = 10_000_000uL
+
+ runCatching {
+ val smallAmountFeesMsat = lightningService.node!!.bolt11Payment()
+ .estimateRoutingFeesUsingAmount(invoice, smallAmountMsat)
+
+ val largeAmountFeesMsat = lightningService.node!!.bolt11Payment()
+ .estimateRoutingFeesUsingAmount(invoice, largeAmountMsat)
+
+ Pair(smallAmountFeesMsat, largeAmountFeesMsat)
+ }.fold(
+ onSuccess = { (smallAmountFeesMsat, largeAmountFeesMsat) ->
+ assertFeesScaleProperly(
+ smallAmountFeesMsat,
+ largeAmountFeesMsat,
+ smallAmountMsat,
+ largeAmountMsat
+ )
+ },
+ onFailure = { error ->
+ handleExpectedRoutingError(error as NodeException)
+ }
+ )
+ }
+
+ // region utils
+
+ private suspend fun runNode() {
+ println("Creating new wallet")
+ walletRepo.createWallet(bip39Passphrase = null)
+
+ println("Setting up lightning service")
+ lightningService.setup(walletIndex = walletIndex)
+
+ println("Starting lightning node")
+ lightningService.start()
+ println("Lightning node started successfully")
+
+ waitForNodeInitialization()
+
+ println("Syncing wallet")
+ lightningService.sync()
+ println("Wallet sync complete")
+ }
+
+ private suspend fun waitForNodeInitialization() {
+ repeat(NODE_STARTUP_MAX_RETRIES) {
+ if (lightningService.node != null) return
+ delay(NODE_STARTUP_RETRY_DELAY_MS)
+ }
+ assertNotNull(lightningService.node, "Node should be initialized within timeout")
+ }
+
+ private suspend fun createInvoiceWithAmount(amountSats: ULong, description: String): Bolt11Invoice {
+ val invoiceString = lightningService.receive(
+ sat = amountSats,
+ description = description,
+ expirySecs = DEFAULT_INVOICE_EXPIRY_SECS
+ )
+ return Bolt11Invoice.fromStr(invoiceString)
+ }
+
+ private suspend fun createZeroAmountInvoice(description: String): Bolt11Invoice {
+ val invoiceString = lightningService.receive(
+ sat = null,
+ description = description,
+ expirySecs = DEFAULT_INVOICE_EXPIRY_SECS
+ )
+ return Bolt11Invoice.fromStr(invoiceString)
+ }
+
+ private fun assertFeesAreReasonable(estimatedFeesMsat: ULong, paymentAmountMsat: ULong) {
+ assertTrue(
+ estimatedFeesMsat >= 0u,
+ "Estimated fees should be non-negative, got: $estimatedFeesMsat"
+ )
+ assertTrue(
+ estimatedFeesMsat < paymentAmountMsat,
+ "Estimated fees should be less than payment amount. Fees: $estimatedFeesMsat, Amount: $paymentAmountMsat msat"
+ )
+ }
+
+ private fun handleExpectedRoutingError(error: NodeException) {
+ when (error) {
+ is NodeException.RouteNotFound -> {
+ assertTrue(true, "RouteNotFound error is acceptable in test environment")
+ }
+ else -> throw LdkError(error)
+ }
+ }
+
+ private fun assertRoutingErrorOccurred(error: NodeException) {
+ assertIs(error)
+ }
+
+ private fun assertFeesScaleProperly(
+ smallFees: ULong,
+ largeFees: ULong,
+ smallAmount: ULong,
+ largeAmount: ULong,
+ ) {
+ assertTrue(
+ largeFees >= smallFees,
+ "Fees for larger amounts should be >= fees for smaller amounts. Small: $smallFees, Large: $largeFees"
+ )
+ assertFeesAreReasonable(smallFees, smallAmount)
+ assertFeesAreReasonable(largeFees, largeAmount)
+ }
+
+ // endregion
+}
diff --git a/app/src/main/java/to/bitkit/data/CacheStore.kt b/app/src/main/java/to/bitkit/data/CacheStore.kt
index 40751c16d..6e7a2dc21 100644
--- a/app/src/main/java/to/bitkit/data/CacheStore.kt
+++ b/app/src/main/java/to/bitkit/data/CacheStore.kt
@@ -2,8 +2,7 @@ package to.bitkit.data
import android.content.Context
import androidx.datastore.core.DataStore
-import androidx.datastore.core.DataStoreFactory
-import androidx.datastore.dataStoreFile
+import androidx.datastore.dataStore
import dagger.hilt.android.qualifiers.ApplicationContext
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.first
@@ -21,14 +20,16 @@ import to.bitkit.utils.Logger
import javax.inject.Inject
import javax.inject.Singleton
+private val Context.appCacheDataStore: DataStore by dataStore(
+ fileName = "app_cache.json",
+ serializer = AppCacheSerializer
+)
+
@Singleton
class CacheStore @Inject constructor(
@ApplicationContext private val context: Context,
) {
- private val store: DataStore = DataStoreFactory.create(
- serializer = AppCacheSerializer,
- produceFile = { context.dataStoreFile("app_cache.json") },
- )
+ private val store = context.appCacheDataStore
val data: Flow = store.data
diff --git a/app/src/main/java/to/bitkit/data/SettingsStore.kt b/app/src/main/java/to/bitkit/data/SettingsStore.kt
index f7ccb9a0a..422baf333 100644
--- a/app/src/main/java/to/bitkit/data/SettingsStore.kt
+++ b/app/src/main/java/to/bitkit/data/SettingsStore.kt
@@ -2,8 +2,7 @@ package to.bitkit.data
import android.content.Context
import androidx.datastore.core.DataStore
-import androidx.datastore.core.DataStoreFactory
-import androidx.datastore.dataStoreFile
+import androidx.datastore.dataStore
import dagger.hilt.android.qualifiers.ApplicationContext
import kotlinx.coroutines.flow.Flow
import kotlinx.serialization.Serializable
@@ -19,14 +18,16 @@ import to.bitkit.utils.Logger
import javax.inject.Inject
import javax.inject.Singleton
+private val Context.settingsDataStore: DataStore by dataStore(
+ fileName = "settings.json",
+ serializer = SettingsSerializer
+)
+
@Singleton
class SettingsStore @Inject constructor(
@ApplicationContext private val context: Context,
) {
- private val store: DataStore = DataStoreFactory.create(
- serializer = SettingsSerializer,
- produceFile = { context.dataStoreFile("settings.json") },
- )
+ private val store = context.settingsDataStore
val data: Flow = store.data
diff --git a/app/src/main/java/to/bitkit/data/WidgetsStore.kt b/app/src/main/java/to/bitkit/data/WidgetsStore.kt
index 50b9d7143..27f5dfee2 100644
--- a/app/src/main/java/to/bitkit/data/WidgetsStore.kt
+++ b/app/src/main/java/to/bitkit/data/WidgetsStore.kt
@@ -2,8 +2,7 @@ package to.bitkit.data
import android.content.Context
import androidx.datastore.core.DataStore
-import androidx.datastore.core.DataStoreFactory
-import androidx.datastore.dataStoreFile
+import androidx.datastore.dataStore
import dagger.hilt.android.qualifiers.ApplicationContext
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.first
@@ -26,14 +25,16 @@ import to.bitkit.utils.Logger
import javax.inject.Inject
import javax.inject.Singleton
+private val Context.widgetsDataStore: DataStore by dataStore(
+ fileName = "widgets.json",
+ serializer = WidgetsSerializer
+)
+
@Singleton
class WidgetsStore @Inject constructor(
@ApplicationContext private val context: Context,
) {
- private val store: DataStore = DataStoreFactory.create(
- serializer = WidgetsSerializer,
- produceFile = { context.dataStoreFile("widgets.json") },
- )
+ private val store = context.widgetsDataStore
val data: Flow = store.data
val articlesFlow: Flow> = data.map { it.articles }
diff --git a/app/src/main/java/to/bitkit/repositories/ActivityRepo.kt b/app/src/main/java/to/bitkit/repositories/ActivityRepo.kt
index 1ece70d48..5928011c6 100644
--- a/app/src/main/java/to/bitkit/repositories/ActivityRepo.kt
+++ b/app/src/main/java/to/bitkit/repositories/ActivityRepo.kt
@@ -601,7 +601,6 @@ class ActivityRepo @Inject constructor(
tags: List,
): Result = withContext(bgDispatcher) {
return@withContext runCatching {
-
require(tags.isNotEmpty())
val entity = TagMetadataEntity(
@@ -622,7 +621,6 @@ class ActivityRepo @Inject constructor(
}
}
-
// MARK: - Development/Testing Methods
/**
diff --git a/app/src/main/java/to/bitkit/repositories/LightningRepo.kt b/app/src/main/java/to/bitkit/repositories/LightningRepo.kt
index a76db343c..86dadd38a 100644
--- a/app/src/main/java/to/bitkit/repositories/LightningRepo.kt
+++ b/app/src/main/java/to/bitkit/repositories/LightningRepo.kt
@@ -805,6 +805,30 @@ class LightningRepo @Inject constructor(
}
}
+ suspend fun estimateRoutingFees(bolt11: String): Result =
+ executeWhenNodeRunning("estimateRoutingFees") {
+ Logger.info("Estimating routing fees for bolt11: $bolt11")
+ lightningService.estimateRoutingFees(bolt11)
+ .onSuccess {
+ Logger.info("Routing fees estimated: $it")
+ }
+ .onFailure {
+ Logger.error("Routing fees estimation failed", it)
+ }
+ }
+
+ suspend fun estimateRoutingFeesForAmount(bolt11: String, amountSats: ULong): Result =
+ executeWhenNodeRunning("estimateRoutingFeesForAmount") {
+ Logger.info("Estimating routing fees for amount: $amountSats")
+ lightningService.estimateRoutingFeesForAmount(bolt11, amountSats)
+ .onSuccess {
+ Logger.info("Routing fees estimated: $it")
+ }
+ .onFailure {
+ Logger.error("Routing fees estimation failed", it)
+ }
+ }
+
private companion object {
const val TAG = "LightningRepo"
}
diff --git a/app/src/main/java/to/bitkit/services/LightningService.kt b/app/src/main/java/to/bitkit/services/LightningService.kt
index 26def7951..d578a9633 100644
--- a/app/src/main/java/to/bitkit/services/LightningService.kt
+++ b/app/src/main/java/to/bitkit/services/LightningService.kt
@@ -449,6 +449,41 @@ class LightningService @Inject constructor(
}
}
}
+
+ suspend fun estimateRoutingFees(bolt11: String): Result {
+ val node = this.node ?: throw ServiceError.NodeNotSetup
+
+ return ServiceQueue.LDK.background {
+ return@background try {
+ val invoice = Bolt11Invoice.fromStr(bolt11)
+ val feesMsat = node.bolt11Payment().estimateRoutingFees(invoice)
+ val feeSat = feesMsat / 1000u
+ Result.success(feeSat)
+ } catch (e: Exception) {
+ Result.failure(
+ if (e is NodeException) LdkError(e) else e
+ )
+ }
+ }
+ }
+
+ suspend fun estimateRoutingFeesForAmount(bolt11: String, amountSats: ULong): Result {
+ val node = this.node ?: throw ServiceError.NodeNotSetup
+
+ return ServiceQueue.LDK.background {
+ return@background try {
+ val invoice = Bolt11Invoice.fromStr(bolt11)
+ val amountMsat = amountSats * 1000u
+ val feesMsat = node.bolt11Payment().estimateRoutingFeesUsingAmount(invoice, amountMsat)
+ val feeSat = feesMsat / 1000u
+ Result.success(feeSat)
+ } catch (e: Exception) {
+ Result.failure(
+ if (e is NodeException) LdkError(e) else e
+ )
+ }
+ }
+ }
// endregion
// region utxo selection
diff --git a/app/src/main/java/to/bitkit/ui/screens/wallets/activity/ActivityDetailScreen.kt b/app/src/main/java/to/bitkit/ui/screens/wallets/activity/ActivityDetailScreen.kt
index 4473862be..e2ea8888b 100644
--- a/app/src/main/java/to/bitkit/ui/screens/wallets/activity/ActivityDetailScreen.kt
+++ b/app/src/main/java/to/bitkit/ui/screens/wallets/activity/ActivityDetailScreen.kt
@@ -96,7 +96,6 @@ fun ActivityDetailScreen(
return
}
-
val app = appViewModel ?: return
val copyToastTitle = stringResource(R.string.common__copied)
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 aa9706a41..d5f9baf5b 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
@@ -51,6 +51,7 @@ import to.bitkit.ext.DatePattern
import to.bitkit.ext.commentAllowed
import to.bitkit.ext.formatted
import to.bitkit.models.FeeRate
+import to.bitkit.models.TransactionSpeed
import to.bitkit.ui.components.BalanceHeaderView
import to.bitkit.ui.components.BiometricsView
import to.bitkit.ui.components.BodySSB
@@ -74,9 +75,10 @@ import to.bitkit.ui.theme.AppThemeSurface
import to.bitkit.ui.theme.Colors
import to.bitkit.ui.utils.rememberBiometricAuthSupported
import to.bitkit.ui.utils.withAccent
-import to.bitkit.viewmodels.SanityWarning
import to.bitkit.viewmodels.LnurlParams
+import to.bitkit.viewmodels.SanityWarning
import to.bitkit.viewmodels.SendEvent
+import to.bitkit.viewmodels.SendFee
import to.bitkit.viewmodels.SendMethod
import to.bitkit.viewmodels.SendUiState
import java.time.Instant
@@ -272,7 +274,9 @@ private fun LnurlCommentSection(
onValueChange = { onEvent(SendEvent.CommentChange(it)) },
minLines = 3,
maxLines = 3,
- modifier = Modifier.fillMaxWidth().testTag("CommentInput")
+ modifier = Modifier
+ .fillMaxWidth()
+ .testTag("CommentInput")
)
}
@@ -363,11 +367,16 @@ private fun OnChainDescription(
tint = fee.color,
modifier = Modifier.size(16.dp)
)
- uiState.fee.takeIf { it > 0 }
- ?.let { rememberMoneyText(it) }
- ?.let {
+ (uiState.fee as? SendFee.OnChain)?.value
+ ?.takeIf { it > 0 }
+ ?.let { feeSat ->
+ val feeText = let {
+ val prefix = stringResource(fee.title)
+ val value = rememberMoneyText(feeSat, showSymbol = true)
+ "$prefix ($value)"
+ }
BodySSB(
- text = "${stringResource(fee.title)} ($it)".withAccent(accentColor = Colors.White),
+ text = feeText.withAccent(accentColor = Colors.White),
maxLines = 1,
overflow = TextOverflow.MiddleEllipsis,
)
@@ -470,7 +479,20 @@ private fun LightningDescription(
tint = Colors.Purple,
modifier = Modifier.size(16.dp)
)
- BodySSB(text = "Instant (±$0.01)") // TODO GET FROM STATE
+ (uiState.fee as? SendFee.Lightning)?.value
+ ?.takeIf { it > 0 }
+ ?.let { feeSat ->
+ val feeText = let {
+ val prefix = stringResource(R.string.fee__instant__title)
+ val value = rememberMoneyText(feeSat, showSymbol = true)
+ "$prefix (± $value)"
+ }
+ BodySSB(
+ text = feeText.withAccent(accentColor = Colors.White),
+ maxLines = 1,
+ overflow = TextOverflow.MiddleEllipsis,
+ )
+ } ?: BodySSB(text = stringResource(R.string.fee__instant__title))
}
Spacer(modifier = Modifier.weight(1f))
HorizontalDivider(modifier = Modifier.padding(vertical = 16.dp))
@@ -534,7 +556,6 @@ private fun sendUiState() = SendUiState(
payeeNodeId = null,
description = "Some invoice description",
),
- fee = 45554,
)
@Preview(showSystemUi = true, group = "onchain")
@@ -545,6 +566,8 @@ private fun PreviewOnChain() {
Content(
uiState = sendUiState().copy(
selectedTags = listOf("car", "house", "uber"),
+ fee = SendFee.OnChain(1_234),
+ speed = TransactionSpeed.Medium,
),
isLoading = false,
showBiometrics = false,
@@ -561,8 +584,10 @@ private fun PreviewOnChainLongFeeSmallScreen() {
BottomSheetPreview {
Content(
uiState = sendUiState().copy(
+ amount = 2_345_678u,
selectedTags = listOf("car", "house", "uber"),
- fee = 654321,
+ fee = SendFee.OnChain(654_321),
+ speed = TransactionSpeed.Custom(12_345u),
),
isLoading = false,
showBiometrics = false,
@@ -580,7 +605,7 @@ private fun PreviewOnChainFeeLoading() {
Content(
uiState = sendUiState().copy(
selectedTags = listOf("car", "house", "uber"),
- fee = 0,
+ fee = null,
),
isLoading = false,
showBiometrics = false,
@@ -600,6 +625,7 @@ private fun PreviewLightning() {
amount = 6_543u,
payMethod = SendMethod.LIGHTNING,
selectedTags = emptyList(),
+ fee = SendFee.Lightning(43),
),
isLoading = false,
showBiometrics = false,
diff --git a/app/src/main/java/to/bitkit/utils/Errors.kt b/app/src/main/java/to/bitkit/utils/Errors.kt
index da57e7b8a..605ba382e 100644
--- a/app/src/main/java/to/bitkit/utils/Errors.kt
+++ b/app/src/main/java/to/bitkit/utils/Errors.kt
@@ -122,6 +122,7 @@ class LdkError(private val inner: LdkException) : AppError("Unknown LDK error.")
is NodeException.NoSpendableOutputs -> "No spendable outputs"
is NodeException.TransactionAlreadyConfirmed -> "Transaction already confirmed"
is NodeException.TransactionNotFound -> "Transaction not found"
+ is NodeException.RouteNotFound -> "Failed to find a route for fee estimation."
else -> exception.message
}?.let { "LDK Node error: $it" }
}
diff --git a/app/src/main/java/to/bitkit/viewmodels/AppViewModel.kt b/app/src/main/java/to/bitkit/viewmodels/AppViewModel.kt
index 86bd8ecfc..4ee11ab6e 100644
--- a/app/src/main/java/to/bitkit/viewmodels/AppViewModel.kt
+++ b/app/src/main/java/to/bitkit/viewmodels/AppViewModel.kt
@@ -258,7 +258,6 @@ class AppViewModel @Inject constructor(
}.onFailure { e ->
Logger.warn("Failed displaying sheet for event: $Event", e)
}
-
}
is Event.PaymentClaimable -> Unit
@@ -397,7 +396,7 @@ class AppViewModel @Inject constructor(
_sendUiState.update {
it.copy(
speed = speed,
- fee = fee,
+ fee = SendFee.OnChain(fee),
selectedUtxos = if (shouldResetUtxos) null else it.selectedUtxos,
)
}
@@ -439,6 +438,8 @@ class AppViewModel @Inject constructor(
}
refreshOnchainSendIfNeeded()
+ estimateLightningRoutingFeesIfNeeded()
+
setSendEffect(SendEffect.NavigateToConfirm)
}
@@ -1216,11 +1217,31 @@ class AppViewModel @Inject constructor(
_sendUiState.update {
it.copy(
fees = feesMap,
- fee = currentFee,
+ fee = SendFee.OnChain(currentFee),
)
}
}
+ private suspend fun estimateLightningRoutingFeesIfNeeded() {
+ val currentState = _sendUiState.value
+ if (currentState.payMethod != SendMethod.LIGHTNING) return
+ val decodedInvoice = currentState.decodedInvoice ?: return
+
+ val feeResult = if (decodedInvoice.amountSatoshis > 0uL) {
+ lightningRepo.estimateRoutingFees(decodedInvoice.bolt11)
+ } else {
+ lightningRepo.estimateRoutingFeesForAmount(decodedInvoice.bolt11, currentState.amount)
+ }
+
+ feeResult.onSuccess { fee ->
+ _sendUiState.update {
+ it.copy(
+ fee = SendFee.Lightning(fee.toLong())
+ )
+ }
+ }
+ }
+
private suspend fun getFeeEstimate(speed: TransactionSpeed? = null): Long {
val currentState = _sendUiState.value
return lightningRepo.calculateTotalFee(
@@ -1506,7 +1527,7 @@ data class SendUiState(
val speed: TransactionSpeed = TransactionSpeed.default(),
val comment: String = "",
val feeRates: FeeRates? = null,
- val fee: Long = 0,
+ val fee: SendFee? = null,
val fees: Map = emptyMap(),
)
@@ -1518,6 +1539,11 @@ enum class SanityWarning(@StringRes val message: Int, val testTag: String) {
// TODO SendDialog5 https://github.com/synonymdev/bitkit/blob/master/src/screens/Wallets/Send/ReviewAndSend.tsx#L457-L466
}
+sealed class SendFee(open val value: Long) {
+ data class OnChain(override val value: Long) : SendFee(value)
+ data class Lightning(override val value: Long) : SendFee(value)
+}
+
enum class SendMethod { ONCHAIN, LIGHTNING }
sealed class SendEffect {
diff --git a/app/src/test/java/to/bitkit/repositories/LightningRepoTest.kt b/app/src/test/java/to/bitkit/repositories/LightningRepoTest.kt
index fac264218..90c742276 100644
--- a/app/src/test/java/to/bitkit/repositories/LightningRepoTest.kt
+++ b/app/src/test/java/to/bitkit/repositories/LightningRepoTest.kt
@@ -495,4 +495,80 @@ class LightningRepoTest : BaseUnitTest() {
assertEquals(3, result.size)
assertEquals(mockUtxos, result)
}
+
+ @Test
+ fun `estimateRoutingFees should fail when node is not running`() = test {
+ val result = sut.estimateRoutingFees("lnbc1u1p0abcde")
+ assertTrue(result.isFailure)
+ }
+
+ @Test
+ fun `estimateRoutingFees should succeed when node is running`() = test {
+ startNodeForTesting()
+ val testBolt11 = "lnbc1u1p0abcde"
+ val expectedFeesSats = 50uL
+
+ whenever(lightningService.estimateRoutingFees(testBolt11))
+ .thenReturn(Result.success(expectedFeesSats))
+
+ val result = sut.estimateRoutingFees(testBolt11)
+
+ assertTrue(result.isSuccess)
+ assertEquals(expectedFeesSats, result.getOrNull())
+ verify(lightningService).estimateRoutingFees(testBolt11)
+ }
+
+ @Test
+ fun `estimateRoutingFees should handle service failure`() = test {
+ startNodeForTesting()
+ val testBolt11 = "lnbc1u1p0abcde"
+ val serviceError = RuntimeException("Service error")
+
+ whenever(lightningService.estimateRoutingFees(testBolt11))
+ .thenReturn(Result.failure(serviceError))
+
+ val result = sut.estimateRoutingFees(testBolt11)
+
+ assertTrue(result.isFailure)
+ assertEquals(serviceError, result.exceptionOrNull())
+ }
+
+ @Test
+ fun `estimateRoutingFeesForAmount should fail when node is not running`() = test {
+ val result = sut.estimateRoutingFeesForAmount("lnbc1u1p0abcde", 1000uL)
+ assertTrue(result.isFailure)
+ }
+
+ @Test
+ fun `estimateRoutingFeesForAmount should succeed when node is running`() = test {
+ startNodeForTesting()
+ val testBolt11 = "lnbc1u1p0abcde"
+ val testAmount = 1000uL
+ val expectedFeesSats = 25uL
+
+ whenever(lightningService.estimateRoutingFeesForAmount(testBolt11, testAmount))
+ .thenReturn(Result.success(expectedFeesSats))
+
+ val result = sut.estimateRoutingFeesForAmount(testBolt11, testAmount)
+
+ assertTrue(result.isSuccess)
+ assertEquals(expectedFeesSats, result.getOrNull())
+ verify(lightningService).estimateRoutingFeesForAmount(testBolt11, testAmount)
+ }
+
+ @Test
+ fun `estimateRoutingFeesForAmount should handle service failure`() = test {
+ startNodeForTesting()
+ val testBolt11 = "lnbc1u1p0abcde"
+ val testAmount = 1000uL
+ val serviceError = RuntimeException("Service error")
+
+ whenever(lightningService.estimateRoutingFeesForAmount(testBolt11, testAmount))
+ .thenReturn(Result.failure(serviceError))
+
+ val result = sut.estimateRoutingFeesForAmount(testBolt11, testAmount)
+
+ assertTrue(result.isFailure)
+ assertEquals(serviceError, result.exceptionOrNull())
+ }
}
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index b63197897..7f8c23be8 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -1,7 +1,7 @@
[versions]
accompanistPermissions = "0.36.0"
activityCompose = "1.10.1"
-agp = "8.12.0"
+agp = "8.13.0"
appcompat = "1.7.0"
barcodeScanning = "17.3.0"
biometric = "1.4.0-alpha02"
@@ -85,7 +85,7 @@ ktor-client-logging = { module = "io.ktor:ktor-client-logging", version.ref = "k
ktor-client-okhttp = { module = "io.ktor:ktor-client-okhttp", version.ref = "ktor" }
ktor-serialization-kotlinx-json = { module = "io.ktor:ktor-serialization-kotlinx-json", version.ref = "ktor" }
#ldk-node-android = { module = "org.lightningdevkit:ldk-node-android", version = "0.6.2" } # upstream
-ldk-node-android = { module = "com.github.synonymdev:ldk-node", version = "v0.6.2-rc.1" } # fork
+ldk-node-android = { module = "com.github.synonymdev:ldk-node", version = "v0.6.2-rc.3" } # fork
lifecycle-process = { group = "androidx.lifecycle", name = "lifecycle-process", version.ref = "lifecycle" }
lifecycle-runtime-compose = { module = "androidx.lifecycle:lifecycle-runtime-compose", version.ref = "lifecycle" }
lifecycle-runtime-ktx = { module = "androidx.lifecycle:lifecycle-runtime-ktx", version.ref = "lifecycle" }