diff --git a/app/detekt-baseline.xml b/app/detekt-baseline.xml index 54db9b23dc..a130bdd366 100644 --- a/app/detekt-baseline.xml +++ b/app/detekt-baseline.xml @@ -4,14 +4,12 @@ ComplexCondition:AuthCheckView.kt$(showBio && isBiometrySupported && !requirePin) || requireBiometrics ComplexCondition:ElectrumConfigViewModel.kt$ElectrumConfigViewModel$currentState.host.isBlank() || port == null || port <= 0 || protocol == null - ComplexCondition:HomeViewModel.kt$HomeViewModel$thresholdReached && isTimeOutOver && belowMaxWarnings && !_uiState.value.highBalanceSheetVisible ComplexCondition:MapWebViewClient.kt$MapWebViewClient$it.errorCode == ERROR_HOST_LOOKUP || it.errorCode == ERROR_CONNECT || it.errorCode == ERROR_TIMEOUT || it.errorCode == ERROR_FILE_NOT_FOUND ComplexCondition:ShopWebViewClient.kt$ShopWebViewClient$it.errorCode == ERROR_HOST_LOOKUP || it.errorCode == ERROR_CONNECT || it.errorCode == ERROR_TIMEOUT || it.errorCode == ERROR_FILE_NOT_FOUND ComposableParamOrder:ActivityDetailScreen.kt$ActivityDetailScreen ComposableParamOrder:ActivityExploreScreen.kt$ActivityExploreScreen ComposableParamOrder:ActivityIcon.kt$ActivityIcon ComposableParamOrder:ActivityIcon.kt$CircularIcon - ComposableParamOrder:AmountInput.kt$AmountInput ComposableParamOrder:AppStatus.kt$AppStatus ComposableParamOrder:AuthCheckView.kt$AuthCheckView ComposableParamOrder:AuthCheckView.kt$AuthCheckViewContent @@ -29,7 +27,6 @@ ComposableParamOrder:InfoScreenContent.kt$InfoScreenContent ComposableParamOrder:Money.kt$MoneyCaptionB ComposableParamOrder:OnboardingSlidesScreen.kt$OnboardingSlidesScreen - ComposableParamOrder:OnboardingSlidesScreen.kt$OnboardingTab ComposableParamOrder:PriceCard.kt$PriceCard ComposableParamOrder:ReceiveConfirmScreen.kt$ReceiveConfirmScreen ComposableParamOrder:ReportIssueScreen.kt$ReportIssueScreen @@ -71,7 +68,6 @@ CyclomaticComplexMethod:ActivityListGrouped.kt$private fun groupActivityItems(activityItems: List<Activity>): List<Any> CyclomaticComplexMethod:ActivityRow.kt$@Composable fun ActivityRow( item: Activity, onClick: (String) -> Unit, testTag: String, ) CyclomaticComplexMethod:ActivityRow.kt$@Composable private fun TransactionStatusText( txType: PaymentType, isLightning: Boolean, status: PaymentState?, isTransfer: Boolean, ) - 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) @@ -82,14 +78,12 @@ 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 = {}, 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: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, ) 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) DestructuringDeclarationWithTooManyEntries:ActivityRow.kt$val (_, _, _, _, _, displayUnit, primaryDisplay) = LocalCurrencies.current DestructuringDeclarationWithTooManyEntries:BalanceHeaderView.kt$val (_, _, _, _, _, displayUnit, primaryDisplay) = LocalCurrencies.current DestructuringDeclarationWithTooManyEntries:DefaultUnitSettingsScreen.kt$val (_, _, _, selectedCurrency, _, displayUnit, primaryDisplay) = LocalCurrencies.current @@ -108,7 +102,6 @@ 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: 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 @@ -120,19 +113,16 @@ ForbiddenComment:LightningNodeService.kt$LightningNodeService$// TODO: Get from resources ForbiddenComment:Notifications.kt$// TODO: review if needed: ForbiddenComment:SuccessScreen.kt$// TODO: verify backup - ForbiddenComment:TransferViewModel.kt$TransferViewModel$// TODO: showBottomSheet: forceTransfer FunctionOnlyReturningConstant:ShopWebViewInterface.kt$ShopWebViewInterface$@JavascriptInterface fun isReady(): Boolean ImplicitDefaultLocale:BlocksService.kt$BlocksService$String.format("%.2f", blockInfo.difficulty / 1_000_000_000_000.0) ImplicitDefaultLocale:PriceService.kt$PriceService$String.format("%.2f", price) InstanceOfCheckForException:LightningService.kt$LightningService$e is NodeException - LambdaParameterEventTrailing:AmountInput.kt$onSatsChange LambdaParameterEventTrailing:CalculatorCard.kt$onFiatChange LambdaParameterEventTrailing:QrScanningScreen.kt$onSubmitDebug LambdaParameterEventTrailing:ReceiveQrScreen.kt$onClickEditInvoice LambdaParameterEventTrailing:SettingsButtonRow.kt$onClick LambdaParameterEventTrailing:SuggestionCard.kt$onClick LambdaParameterEventTrailing:ToastView.kt$onDismiss - LambdaParameterInRestartableEffect:AmountInput.kt$onSatsChange LambdaParameterInRestartableEffect:AuthCheckView.kt$validatePin LambdaParameterInRestartableEffect:BiometricPrompt.kt$onSuccess LambdaParameterInRestartableEffect:BoostTransactionSheet.kt$onFailure @@ -151,7 +141,6 @@ LambdaParameterInRestartableEffect:QrScanningScreen.kt$onScanSuccess LambdaParameterInRestartableEffect:ReportIssueScreen.kt$navigateResultScreen LambdaParameterInRestartableEffect:SendCoinSelectionScreen.kt$onRender - LambdaParameterInRestartableEffect:SendConfirmScreen.kt$onEvent LambdaParameterInRestartableEffect:SendConfirmScreen.kt$onNavigateToPin LambdaParameterInRestartableEffect:SendPinCheckScreen.kt$onSuccess LambdaParameterInRestartableEffect:SendQuickPayScreen.kt$onPaymentComplete @@ -169,50 +158,29 @@ LongMethod:CoreService.kt$ActivityService$suspend fun generateRandomTestData(count: Int = 100) 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: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, ) LongParameterList:CoreService.kt$BlocktankService$( channelSizeSat: ULong, invoiceSat: ULong, invoiceDescription: String, nodeId: String, channelExpiryWeeks: UInt, options: CreateCjitOptions, ) LongParameterList:CoreService.kt$OnchainService$( mnemonicPhrase: String, derivationPathStr: String?, network: Network?, bip39Passphrase: String?, isChange: Boolean?, startIndex: UInt?, count: UInt?, ) - 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 lspNotificationsService: LspNotificationsService, 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: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:MonetaryVisualTransformation.kt$MonetaryVisualTransformation.<no name provided>$for LoopWithTooManyJumpStatements:TransferViewModel.kt$TransferViewModel$while MagicNumber:ActivityDetailScreen.kt$40 MagicNumber:ActivityExploreScreen.kt$40 - MagicNumber:ActivityIcon.kt$0.5f - MagicNumber:ActivityListViewModel.kt$ActivityListViewModel$1000 MagicNumber:ActivityListViewModel.kt$ActivityListViewModel$300 - MagicNumber:ActivityRepo.kt$ActivityRepo$1000 MagicNumber:AddressViewerScreen.kt$1500000L MagicNumber:AddressViewerScreen.kt$250000L MagicNumber:AddressViewerScreen.kt$50000L MagicNumber:AddressViewerViewModel.kt$AddressViewerViewModel$300 - MagicNumber:AllActivityScreen.kt$0.5f MagicNumber:AllActivityScreen.kt$0xFF161616 MagicNumber:AllActivityScreen.kt$0xFF1e1e1e - MagicNumber:AllActivityScreen.kt$1500f - MagicNumber:AmountInput.kt$8 - MagicNumber:AndroidKeyStore.kt$AndroidKeyStore$128 - MagicNumber:AndroidKeyStore.kt$AndroidKeyStore$256 - MagicNumber:AppStatus.kt$0.2f MagicNumber:AppStatus.kt$0.4f - MagicNumber:AppStatus.kt$0.5f - MagicNumber:AppStatus.kt$0.6f - MagicNumber:AppStatus.kt$2000 - MagicNumber:AppStatus.kt$600 MagicNumber:AppViewModel.kt$AppViewModel$1000 MagicNumber:AppViewModel.kt$AppViewModel$250 - MagicNumber:AppViewModel.kt$AppViewModel$300 MagicNumber:AppViewModel.kt$AppViewModel$500 MagicNumber:ArticleModel.kt$24 MagicNumber:ArticleModel.kt$30 @@ -220,103 +188,51 @@ MagicNumber:AutoReadClipboardHandler.kt$1000 MagicNumber:BackupNavSheetViewModel.kt$BackupNavSheetViewModel$200 MagicNumber:BackupRepo.kt$BackupRepo$60000 - MagicNumber:BackupSettingsScreen.kt$1000 - MagicNumber:BackupSettingsScreen.kt$60 MagicNumber:BackupsViewModel.kt$BackupsViewModel$500 - MagicNumber:BiometricCrypto.kt$BiometricCrypto$256 MagicNumber:BiometricsView.kt$5 - MagicNumber:Bip21Utils.kt$Bip21Utils$8 - MagicNumber:Bip39Utils.kt$0xFF MagicNumber:Bip39Utils.kt$12 - MagicNumber:Bip39Utils.kt$128 MagicNumber:Bip39Utils.kt$24 - MagicNumber:Bip39Utils.kt$256 - MagicNumber:Bip39Utils.kt$32 MagicNumber:Bip39Utils.kt$8 - MagicNumber:BlocksService.kt$BlocksService$1000L - MagicNumber:BlocksService.kt$BlocksService$1024.0 - MagicNumber:BlocksService.kt$BlocksService$1_000_000_000_000.0 - MagicNumber:BlocktankRepo.kt$BlocktankRepo$1.1 - MagicNumber:BlocktankRepo.kt$BlocktankRepo$1000 - MagicNumber:BlocktankRepo.kt$BlocktankRepo$225 - MagicNumber:BlocktankRepo.kt$BlocktankRepo$450 - MagicNumber:BlocktankRepo.kt$BlocktankRepo$495 - MagicNumber:Button.kt$0.5f MagicNumber:ChangePinConfirmScreen.kt$500 MagicNumber:ChannelDetailScreen.kt$1.5f MagicNumber:ChannelOrdersScreen.kt$10 MagicNumber:ChannelOrdersScreen.kt$100 - MagicNumber:ChannelOrdersScreen.kt$30 - MagicNumber:ChannelOrdersScreen.kt$40 - MagicNumber:ConfirmMnemonicScreen.kt$12 - MagicNumber:ConfirmMnemonicScreen.kt$24 MagicNumber:ConfirmMnemonicScreen.kt$300 MagicNumber:ContentView.kt$100 MagicNumber:ContentView.kt$500 - MagicNumber:Context.kt$1024 - MagicNumber:CoreService.kt$ActivityService$24 - MagicNumber:CoreService.kt$ActivityService$30L - MagicNumber:CoreService.kt$ActivityService$60 MagicNumber:CoreService.kt$ActivityService$64 - MagicNumber:CoreService.kt$ActivityService$8 - MagicNumber:Crypto.kt$Crypto$128 MagicNumber:Crypto.kt$Crypto$16 MagicNumber:Crypto.kt$Crypto$32 - MagicNumber:CurrencyService.kt$CurrencyService$1000L 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:HomeScreen.kt$0.5f MagicNumber:HomeScreen.kt$0.8f MagicNumber:HomeScreen.kt$3 MagicNumber:HttpModule.kt$HttpModule$30_000 MagicNumber:HttpModule.kt$HttpModule$60_000 MagicNumber:InitializingWalletView.kt$500 MagicNumber:InitializingWalletView.kt$99.9 - MagicNumber:LightningChannel.kt$0.5f - MagicNumber:LightningConnectionsViewModel.kt$LightningConnectionsViewModel$10 MagicNumber:LightningConnectionsViewModel.kt$LightningConnectionsViewModel$500 - MagicNumber:LiquidityScreen.kt$200_000L - MagicNumber:LiquidityScreen.kt$50_000L - MagicNumber:Logger.kt$Logger$1024L MagicNumber:Logger.kt$Logger$4 - MagicNumber:LogsRepo.kt$LogsRepo$3 - MagicNumber:MigrationService.kt$MigrationService$1000 - MagicNumber:MigrationService.kt$MigrationService$1000L MagicNumber:NewsService.kt$NewsService$10 MagicNumber:OnboardingSlidesScreen.kt$3 MagicNumber:OnboardingSlidesScreen.kt$4 MagicNumber:OnboardingSlidesScreen.kt$5 - MagicNumber:Perf.kt$1000.0 MagicNumber:PinConfirmScreen.kt$500 MagicNumber:PinPromptScreen.kt$0.8f - MagicNumber:PreviewItems.kt$10 - MagicNumber:PreviewItems.kt$3 MagicNumber:PriceCard.kt$1000 MagicNumber:PriceCard.kt$3.0 MagicNumber:PriceCard.kt$4.0 MagicNumber:PricePreviewScreen.kt$3.0 MagicNumber:PricePreviewScreen.kt$4.0 - MagicNumber:PriceService.kt$PriceService$100 - MagicNumber:PriceService.kt$PriceService$1000 - MagicNumber:PriceService.kt$PriceService$6 MagicNumber:ProgressSteps.kt$12f MagicNumber:QrScanningScreen.kt$100 - MagicNumber:QuickPaySettingsScreen.kt$10 - MagicNumber:QuickPaySettingsScreen.kt$20 - MagicNumber:QuickPaySettingsScreen.kt$5 - MagicNumber:QuickPaySettingsScreen.kt$50 - MagicNumber:ReceiveLiquidityScreen.kt$100.0 MagicNumber:ReceiveQrScreen.kt$17.33f MagicNumber:ReceiveQrScreen.kt$32 - MagicNumber:RectangleButton.kt$0.5f MagicNumber:RestoreWalletScreen.kt$12 MagicNumber:RestoreWalletScreen.kt$24 - MagicNumber:RestoreWalletScreen.kt$3 - MagicNumber:RestoreWalletScreen.kt$6 MagicNumber:SavingsConfirmScreen.kt$300 MagicNumber:SavingsProgressScreen.kt$2500 MagicNumber:SavingsProgressScreen.kt$5000 @@ -324,13 +240,9 @@ 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 - MagicNumber:SettingsTextButtonRow.kt$0.5f MagicNumber:SettingsViewModel.kt$SettingsViewModel$5000 MagicNumber:ShareSheet.kt$100 - MagicNumber:ShowMnemonicScreen.kt$0.075f MagicNumber:ShowMnemonicScreen.kt$12 MagicNumber:ShowMnemonicScreen.kt$24 MagicNumber:ShowMnemonicScreen.kt$300 @@ -340,23 +252,8 @@ MagicNumber:Slider.kt$50 MagicNumber:SpendingConfirmScreen.kt$300 MagicNumber:String.kt$3 - MagicNumber:SwipeToConfirm.kt$0.8f MagicNumber:SwipeToConfirm.kt$1500 MagicNumber:SwipeToConfirm.kt$500 - MagicNumber:TabBar.kt$0.5f - MagicNumber:TermsOfUseScreen.kt$0x52FF6600 - MagicNumber:Thread.kt$4 - MagicNumber:ToastView.kt$0XFF032E56 - MagicNumber:ToastView.kt$0XFF1D2F1C - MagicNumber:ToastView.kt$0XFF2B1637 - MagicNumber:ToastView.kt$0XFF3C1001 - MagicNumber:ToastView.kt$0XFF491F25 - MagicNumber:TransferViewModel.kt$TransferViewModel$0.025 - MagicNumber:TransferViewModel.kt$TransferViewModel$0.98 - MagicNumber:TransferViewModel.kt$TransferViewModel$225 - MagicNumber:TransferViewModel.kt$TransferViewModel$450 - MagicNumber:TransferViewModel.kt$TransferViewModel$495 - MagicNumber:WalletRepo.kt$WalletRepo$0.9 MatchingDeclarationName:AddressType.kt$AddressTypeInfo MatchingDeclarationName:Button.kt$ButtonSize MatchingDeclarationName:CoinSelectPreferenceScreen.kt$CoinSelectPreferenceTestTags @@ -368,7 +265,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: 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 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" @@ -455,7 +351,6 @@ ModifierMissing:FundingScreen.kt$FundingScreen ModifierMissing:HeadlinesEditScreen.kt$HeadlinesEditContent ModifierMissing:HeadlinesPreviewScreen.kt$HeadlinesPreviewContent - ModifierMissing:HighBalanceWarningSheet.kt$HighBalanceWarningContent ModifierMissing:HomeNav.kt$HomeNav ModifierMissing:InfoScreenContent.kt$InfoScreenContent ModifierMissing:InitializingWalletView.kt$InitializingWalletView @@ -464,7 +359,6 @@ ModifierMissing:LogsScreen.kt$LogDetailScreen ModifierMissing:LogsScreen.kt$LogsScreen ModifierMissing:Money.kt$MoneyDisplay - ModifierMissing:NewTransactionSheet.kt$NewTransactionSheetView ModifierMissing:OnboardingSlidesScreen.kt$OnboardingSlidesScreen ModifierMissing:PinChooseScreen.kt$PinChooseScreen ModifierMissing:PinSheet.kt$PinSheet @@ -472,7 +366,6 @@ ModifierMissing:PricePreviewScreen.kt$PricePreviewContent ModifierMissing:ProfileIntroScreen.kt$ProfileIntroScreen ModifierMissing:QrScanningScreen.kt$QrScanningScreen - ModifierMissing:QuickPayIntroScreen.kt$QuickPayIntroScreen ModifierMissing:QuickPaySettingsScreen.kt$QuickPaySettingsScreenContent ModifierMissing:ReceiveSheet.kt$ReceiveSheet ModifierMissing:ReportIssueResultScreen.kt$ReportIssueResultScreen @@ -493,7 +386,6 @@ ModifierMissing:Spacers.kt$FillWidth ModifierMissing:Spacers.kt$HorizontalSpacer ModifierMissing:Spacers.kt$VerticalSpacer - ModifierMissing:SpendingAdvancedScreen.kt$SpendingAdvancedScreen ModifierMissing:SpendingIntroScreen.kt$SpendingIntroScreen ModifierMissing:SpendingWalletScreen.kt$SpendingWalletScreen ModifierMissing:SplashScreen.kt$SplashScreen @@ -504,7 +396,6 @@ ModifierMissing:WeatherEditScreen.kt$WeatherEditContent ModifierMissing:WeatherPreviewScreen.kt$WeatherPreviewContent ModifierMissing:WidgetsIntroScreen.kt$WidgetsIntroScreen - ModifierNotUsedAtRoot:AmountInput.kt$modifier = modifier.clickableAlpha { currency.switchUnit() } ModifierNotUsedAtRoot:SettingsTextButtonRow.kt$modifier = modifier.then(if (!enabled) Modifier.alpha(0.5f) else Modifier) ModifierWithoutDefault:ReceiveQrScreen.kt$modifier ModifierWithoutDefault:SyncNodeView.kt$modifier @@ -592,7 +483,6 @@ TooGenericExceptionCaught:ServiceQueue.kt$ServiceQueue$e: Exception TooGenericExceptionCaught:SettingUpScreen.kt$e: Throwable TooGenericExceptionCaught:ShopWebViewInterface.kt$ShopWebViewInterface$e: Exception - TooGenericExceptionCaught:SpendingAdvancedScreen.kt$e: Throwable TooGenericExceptionCaught:TransferViewModel.kt$TransferViewModel$e: Throwable TooGenericExceptionCaught:VssBackupClient.kt$VssBackupClient$e: Throwable TooGenericExceptionCaught:WakeNodeWorker.kt$WakeNodeWorker$e: Exception @@ -611,23 +501,17 @@ TooManyFunctions:BlocktankRepo.kt$BlocktankRepo TooManyFunctions:BoostTransactionViewModel.kt$BoostTransactionViewModel : ViewModel TooManyFunctions:CacheStore.kt$CacheStore - TooManyFunctions:ChannelDetailScreen.kt$to.bitkit.ui.settings.lightning.ChannelDetailScreen.kt TooManyFunctions:ChannelOrdersScreen.kt$to.bitkit.ui.settings.ChannelOrdersScreen.kt - TooManyFunctions:ChannelStatusView.kt$to.bitkit.ui.settings.lightning.components.ChannelStatusView.kt TooManyFunctions:ContentView.kt$to.bitkit.ui.ContentView.kt TooManyFunctions:CoreService.kt$ActivityService TooManyFunctions:CoreService.kt$BlocktankService TooManyFunctions:DevSettingsViewModel.kt$DevSettingsViewModel : ViewModel TooManyFunctions:ElectrumConfigViewModel.kt$ElectrumConfigViewModel : ViewModel - TooManyFunctions:ExternalNodeViewModel.kt$ExternalNodeViewModel : ViewModel TooManyFunctions:HomeViewModel.kt$HomeViewModel : ViewModel TooManyFunctions:LightningConnectionsViewModel.kt$LightningConnectionsViewModel : ViewModel TooManyFunctions:LightningRepo.kt$LightningRepo TooManyFunctions:LightningService.kt$LightningService : BaseCoroutineScope TooManyFunctions:Logger.kt$Logger - TooManyFunctions:NodeInfoScreen.kt$to.bitkit.ui.NodeInfoScreen.kt - TooManyFunctions:SendAmountScreen.kt$to.bitkit.ui.screens.wallets.send.SendAmountScreen.kt - 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 @@ -639,22 +523,7 @@ TooManyFunctions:WidgetsStore.kt$WidgetsStore TopLevelPropertyNaming:DrawerMenu.kt$private const val zIndexMenu = 11f TopLevelPropertyNaming:DrawerMenu.kt$private const val zIndexScrim = 10f - UnusedPrivateProperty:ActivityRepoTest.kt$ActivityRepoTest$private val testOnChainActivity = mock<Activity.Onchain> { on { v1 } doReturn testOnChainActivityV1 } UnusedPrivateProperty:CurrencyRepoTest.kt$CurrencyRepoTest$private val toastEventBus: ToastEventBus = mock() - ViewModelForwarding:ActivityDetailScreen.kt$ActivityAddTagSheet( listViewModel = listViewModel, activityViewModel = detailViewModel, onDismiss = { showAddTagSheet = false }, ) - ViewModelForwarding:ContentView.kt$BackupSheet(sheet, appViewModel) - 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, ) - 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 ) }, ) - ViewModelForwarding:ContentView.kt$SpendingConfirmScreen( viewModel = transferViewModel, onBackClick = { navController.popBackStack() }, onCloseClick = { navController.navigateToHome() }, onLearnMoreClick = { navController.navigate(Routes.TransferLiquidity) }, onAdvancedClick = { navController.navigate(Routes.SpendingAdvanced) }, onConfirm = { navController.navigate(Routes.SettingUp) }, ) - ViewModelForwarding:HomeNav.kt$AllActivityScreen( viewModel = activityListViewModel, onBack = { activityListViewModel.clearFilters() walletNavController.popBackStack() }, onActivityItemClick = { rootNavController.navigateToActivityItem(it) }, ) - ViewModelForwarding:HomeNav.kt$HomeScreen( mainUiState = mainUiState, drawerState = drawerState, rootNavController = rootNavController, walletNavController = walletNavController, settingsViewModel = settingsViewModel, walletViewModel = walletViewModel, appViewModel = appViewModel, activityListViewModel = activityListViewModel, ) - ViewModelForwarding:HomeNav.kt$NavContent( walletNavController = walletNavController, rootNavController = rootNavController, mainUiState = uiState, drawerState = drawerState, settingsViewModel = settingsViewModel, appViewModel = appViewModel, walletViewModel = walletViewModel, activityListViewModel = activityListViewModel, ) - ViewModelForwarding:HomeScreen.kt$DeleteWidgetAlert(type, homeViewModel) WildcardImport:LightningChannel.kt$import androidx.compose.foundation.layout.* diff --git a/app/src/main/java/to/bitkit/env/Env.kt b/app/src/main/java/to/bitkit/env/Env.kt index dd8d6ff8c8..b9ed0ac979 100644 --- a/app/src/main/java/to/bitkit/env/Env.kt +++ b/app/src/main/java/to/bitkit/env/Env.kt @@ -3,10 +3,11 @@ package to.bitkit.env import android.os.Build import org.lightningdevkit.ldknode.LogLevel import org.lightningdevkit.ldknode.Network +import org.lightningdevkit.ldknode.PeerDetails import to.bitkit.BuildConfig import to.bitkit.ext.ensureDir +import to.bitkit.ext.parse import to.bitkit.models.BlocktankNotificationType -import to.bitkit.models.LnPeer import to.bitkit.utils.Logger import java.io.File import kotlin.io.path.Path @@ -24,8 +25,8 @@ internal object Env { // TODO: remove this to load from BT API instead val trustedLnPeers get() = when (network) { - Network.REGTEST -> listOf(Peers.btStaging) - Network.TESTNET -> listOf(Peers.btStaging) + Network.REGTEST -> listOf(Peers.staging) + Network.TESTNET -> listOf(Peers.staging) else -> TODO("Not yet implemented") } @@ -143,10 +144,8 @@ internal object Env { } object Peers { - val btStaging = LnPeer( - nodeId = "028a8910b0048630d4eb17af25668cdd7ea6f2d8ae20956e7a06e2ae46ebcb69fc", - address = "34.65.86.104:9400", - ) + val staging = + PeerDetails.parse("028a8910b0048630d4eb17af25668cdd7ea6f2d8ae20956e7a06e2ae46ebcb69fc@34.65.86.104:9400") } object ElectrumServers { diff --git a/app/src/main/java/to/bitkit/ext/LightningBalance.kt b/app/src/main/java/to/bitkit/ext/LightningBalance.kt index a05737cfc3..a1b2d2a22d 100644 --- a/app/src/main/java/to/bitkit/ext/LightningBalance.kt +++ b/app/src/main/java/to/bitkit/ext/LightningBalance.kt @@ -1,6 +1,6 @@ package to.bitkit.ext -import to.bitkit.models.LightningBalance +import org.lightningdevkit.ldknode.LightningBalance fun LightningBalance.amountSats(): ULong { return when (this) { diff --git a/app/src/main/java/to/bitkit/ext/PeerDetails.kt b/app/src/main/java/to/bitkit/ext/PeerDetails.kt new file mode 100644 index 0000000000..e30a6d9970 --- /dev/null +++ b/app/src/main/java/to/bitkit/ext/PeerDetails.kt @@ -0,0 +1,37 @@ +package to.bitkit.ext + +import org.lightningdevkit.ldknode.PeerDetails + +val PeerDetails.host get() = address.substringBefore(":") + +val PeerDetails.port get() = address.substringAfter(":") + +val PeerDetails.uri get() = "$nodeId@$address" + +fun PeerDetails.Companion.parse(uri: String): PeerDetails { + val parts = uri.split("@") + require(parts.size == 2) { "Invalid uri format, expected: '@:', got: '$uri'" } + + val nodeId = parts[0] + + val addressParts = parts[1].split(":") + require(addressParts.size == 2) { "Invalid uri format, expected: '@:', got: '$uri'" } + + val host = addressParts[0] + val port = addressParts[1] + val address = "$host:$port" + + return PeerDetails( + nodeId = nodeId, + address = address, + isConnected = false, + isPersisted = false, + ) +} + +fun PeerDetails.Companion.from(nodeId: String, host: String, port: String) = PeerDetails( + nodeId = nodeId, + address = "$host:$port", + isConnected = false, + isPersisted = false, +) diff --git a/app/src/main/java/to/bitkit/fcm/WakeNodeWorker.kt b/app/src/main/java/to/bitkit/fcm/WakeNodeWorker.kt index a47905aa5c..fd30c92f04 100644 --- a/app/src/main/java/to/bitkit/fcm/WakeNodeWorker.kt +++ b/app/src/main/java/to/bitkit/fcm/WakeNodeWorker.kt @@ -37,6 +37,7 @@ import to.bitkit.utils.Logger import to.bitkit.utils.withPerformanceLogging import kotlin.time.Duration.Companion.minutes +@Suppress("LongParameterList") @HiltWorker class WakeNodeWorker @AssistedInject constructor( @Assisted private val appContext: Context, @@ -120,25 +121,7 @@ class WakeNodeWorker @AssistedInject constructor( val showDetails = settingsStore.data.first().showNotificationDetails val openBitkitMessage = "Open Bitkit to see details" when (event) { - is Event.PaymentReceived -> { - bestAttemptContent?.title = "Payment Received" - val sats = event.amountMsat / 1000u - // Save for UI to pick up - NewTransactionSheetDetails.save( - appContext, - NewTransactionSheetDetails( - type = NewTransactionSheetType.LIGHTNING, - direction = NewTransactionSheetDirection.RECEIVED, - paymentHashOrTxId = event.paymentHash, - sats = sats.toLong(), - ) - ) - val content = if (showDetails) "$BITCOIN_SYMBOL $sats" else openBitkitMessage - bestAttemptContent?.body = content - if (self.notificationType == incomingHtlc) { - self.deliver() - } - } + is Event.PaymentReceived -> onPaymentReceived(event, showDetails, openBitkitMessage) is Event.ChannelPending -> { self.bestAttemptContent?.title = "Channel Opened" @@ -146,49 +129,8 @@ class WakeNodeWorker @AssistedInject constructor( // Don't deliver, give a chance for channelReady event to update the content if it's a turbo channel } - is Event.ChannelReady -> { - if (self.notificationType == cjitPaymentArrived) { - self.bestAttemptContent?.title = "Payment received" - self.bestAttemptContent?.body = "Via new channel" - - lightningRepo.getChannels()?.find { it.channelId == event.channelId }?.let { channel -> - val sats = channel.amountOnClose - val content = if (showDetails) "$BITCOIN_SYMBOL $sats" else openBitkitMessage - self.bestAttemptContent?.title = content - val cjitEntry = channel.let { blocktankRepo.getCjitEntry(it) } - if (cjitEntry != null) { - // Save for UI to pick up - NewTransactionSheetDetails.save( - appContext, - NewTransactionSheetDetails( - type = NewTransactionSheetType.LIGHTNING, - direction = NewTransactionSheetDirection.RECEIVED, - sats = sats.toLong(), - ) - ) - activityRepo.insertActivityFromCjit(cjitEntry = cjitEntry, channel = channel) - } - } - } else if (self.notificationType == orderPaymentConfirmed) { - self.bestAttemptContent?.title = "Channel opened" - self.bestAttemptContent?.body = "Ready to send" - } - self.deliver() - } - - is Event.ChannelClosed -> { - self.bestAttemptContent?.title = "Channel closed" - self.bestAttemptContent?.body = "Reason: ${event.reason}" - - if (self.notificationType == mutualClose) { - self.bestAttemptContent?.body = "Balance moved from spending to savings" - } else if (self.notificationType == orderPaymentConfirmed) { - self.bestAttemptContent?.title = "Channel failed to open in the background" - self.bestAttemptContent?.body = "Please try again" - } - - self.deliver() - } + is Event.ChannelReady -> onChannelReady(event, showDetails, openBitkitMessage) + is Event.ChannelClosed -> onChannelClosed(event) is Event.PaymentSuccessful -> Unit is Event.PaymentClaimable -> Unit @@ -205,6 +147,78 @@ class WakeNodeWorker @AssistedInject constructor( } } + private suspend fun onChannelClosed(event: Event.ChannelClosed) { + self.bestAttemptContent?.title = "Channel closed" + self.bestAttemptContent?.body = "Reason: ${event.reason}" + + if (self.notificationType == mutualClose) { + self.bestAttemptContent?.body = "Balance moved from spending to savings" + } else if (self.notificationType == orderPaymentConfirmed) { + self.bestAttemptContent?.title = "Channel failed to open in the background" + self.bestAttemptContent?.body = "Please try again" + } + + self.deliver() + } + + private suspend fun onPaymentReceived( + event: Event.PaymentReceived, + showDetails: Boolean, + openBitkitMessage: String, + ) { + bestAttemptContent?.title = "Payment Received" + val sats = event.amountMsat / 1000u + // Save for UI to pick up + NewTransactionSheetDetails.save( + appContext, + NewTransactionSheetDetails( + type = NewTransactionSheetType.LIGHTNING, + direction = NewTransactionSheetDirection.RECEIVED, + paymentHashOrTxId = event.paymentHash, + sats = sats.toLong(), + ) + ) + val content = if (showDetails) "$BITCOIN_SYMBOL $sats" else openBitkitMessage + bestAttemptContent?.body = content + if (self.notificationType == incomingHtlc) { + self.deliver() + } + } + + private suspend fun onChannelReady( + event: Event.ChannelReady, + showDetails: Boolean, + openBitkitMessage: String, + ) { + if (self.notificationType == cjitPaymentArrived) { + self.bestAttemptContent?.title = "Payment received" + self.bestAttemptContent?.body = "Via new channel" + + lightningRepo.getChannels()?.find { it.channelId == event.channelId }?.let { channel -> + val sats = channel.amountOnClose + val content = if (showDetails) "$BITCOIN_SYMBOL $sats" else openBitkitMessage + self.bestAttemptContent?.title = content + val cjitEntry = channel.let { blocktankRepo.getCjitEntry(it) } + if (cjitEntry != null) { + // Save for UI to pick up + NewTransactionSheetDetails.save( + appContext, + NewTransactionSheetDetails( + type = NewTransactionSheetType.LIGHTNING, + direction = NewTransactionSheetDirection.RECEIVED, + sats = sats.toLong(), + ) + ) + activityRepo.insertActivityFromCjit(cjitEntry = cjitEntry, channel = channel) + } + } + } else if (self.notificationType == orderPaymentConfirmed) { + self.bestAttemptContent?.title = "Channel opened" + self.bestAttemptContent?.body = "Ready to send" + } + self.deliver() + } + private suspend fun deliver() { lightningRepo.stop() diff --git a/app/src/main/java/to/bitkit/models/BalanceState.kt b/app/src/main/java/to/bitkit/models/BalanceState.kt index a28b0ce28a..140d81c552 100644 --- a/app/src/main/java/to/bitkit/models/BalanceState.kt +++ b/app/src/main/java/to/bitkit/models/BalanceState.kt @@ -1,213 +1,15 @@ package to.bitkit.models import kotlinx.serialization.Serializable -import org.lightningdevkit.ldknode.BalanceSource -import org.lightningdevkit.ldknode.BlockHash -import org.lightningdevkit.ldknode.ChannelId -import org.lightningdevkit.ldknode.PaymentHash -import org.lightningdevkit.ldknode.PaymentPreimage -import org.lightningdevkit.ldknode.PublicKey -import org.lightningdevkit.ldknode.Txid -import org.lightningdevkit.ldknode.BalanceDetails as LdkBalanceDetails -import org.lightningdevkit.ldknode.LightningBalance as LdkLightningBalance -import org.lightningdevkit.ldknode.PendingSweepBalance as LdkPendingSweepBalance @Serializable data class BalanceState( val totalOnchainSats: ULong = 0uL, val totalLightningSats: ULong = 0uL, - val maxSendLightningSats: ULong = 0uL, // TODO use where applicable + val maxSendLightningSats: ULong = 0uL, val maxSendOnchainSats: ULong = 0uL, val balanceInTransferToSavings: ULong = 0uL, val balanceInTransferToSpending: ULong = 0uL, ) { val totalSats get() = totalOnchainSats + totalLightningSats } - -// region BalanceDetails mapping - -// TODO replace when ldk-node exports uniffi kotlin bindings with serializable records -@Serializable -data class BalanceDetails( - var totalOnchainBalanceSats: ULong, - var spendableOnchainBalanceSats: ULong, - var totalAnchorChannelsReserveSats: ULong, - var totalLightningBalanceSats: ULong, - var lightningBalances: List, - var pendingBalancesFromChannelClosures: List, -) - -@Serializable -sealed class LightningBalance { - - @Serializable - data class ClaimableOnChannelClose( - val channelId: ChannelId, - val counterpartyNodeId: PublicKey, - val amountSatoshis: ULong, - val transactionFeeSatoshis: ULong, - val outboundPaymentHtlcRoundedMsat: ULong, - val outboundForwardedHtlcRoundedMsat: ULong, - val inboundClaimingHtlcRoundedMsat: ULong, - val inboundHtlcRoundedMsat: ULong, - ) : LightningBalance() - - @Serializable - data class ClaimableAwaitingConfirmations( - val channelId: ChannelId, - val counterpartyNodeId: PublicKey, - val amountSatoshis: ULong, - val confirmationHeight: UInt, - val source: BalanceSource, - ) : LightningBalance() - - @Serializable - data class ContentiousClaimable( - val channelId: ChannelId, - val counterpartyNodeId: PublicKey, - val amountSatoshis: ULong, - val timeoutHeight: UInt, - val paymentHash: PaymentHash, - val paymentPreimage: PaymentPreimage, - ) : LightningBalance() - - @Serializable - data class MaybeTimeoutClaimableHtlc( - val channelId: ChannelId, - val counterpartyNodeId: PublicKey, - val amountSatoshis: ULong, - val claimableHeight: UInt, - val paymentHash: PaymentHash, - val outboundPayment: Boolean, - ) : LightningBalance() - - @Serializable - data class MaybePreimageClaimableHtlc( - val channelId: ChannelId, - val counterpartyNodeId: PublicKey, - val amountSatoshis: ULong, - val expiryHeight: UInt, - val paymentHash: PaymentHash, - ) : LightningBalance() - - @Serializable - data class CounterpartyRevokedOutputClaimable( - val channelId: ChannelId, - val counterpartyNodeId: PublicKey, - val amountSatoshis: ULong, - ) : LightningBalance() -} - -@Serializable -sealed class PendingSweepBalance { - - @Serializable - data class PendingBroadcast( - val channelId: ChannelId?, - val amountSatoshis: ULong, - ) : PendingSweepBalance() - - @Serializable - data class BroadcastAwaitingConfirmation( - val channelId: ChannelId?, - val latestBroadcastHeight: UInt, - val latestSpendingTxid: Txid, - val amountSatoshis: ULong, - ) : PendingSweepBalance() - - @Serializable - data class AwaitingThresholdConfirmations( - val channelId: ChannelId?, - val latestSpendingTxid: Txid, - val confirmationHash: BlockHash, - val confirmationHeight: UInt, - val amountSatoshis: ULong, - ) : PendingSweepBalance() -} - -fun LdkBalanceDetails.toDomainModel() = BalanceDetails( - totalOnchainBalanceSats = totalOnchainBalanceSats, - spendableOnchainBalanceSats = spendableOnchainBalanceSats, - totalAnchorChannelsReserveSats = totalAnchorChannelsReserveSats, - totalLightningBalanceSats = totalLightningBalanceSats, - lightningBalances = lightningBalances.map { it.mapToLightningBalance() }, - pendingBalancesFromChannelClosures = pendingBalancesFromChannelClosures.map { it.mapToPendingSweepBalance() }, -) - -fun LdkLightningBalance.mapToLightningBalance() = when (this) { - is LdkLightningBalance.ClaimableOnChannelClose -> - LightningBalance.ClaimableOnChannelClose( - channelId = channelId, - counterpartyNodeId = counterpartyNodeId, - amountSatoshis = amountSatoshis, - transactionFeeSatoshis = transactionFeeSatoshis, - outboundPaymentHtlcRoundedMsat = outboundPaymentHtlcRoundedMsat, - outboundForwardedHtlcRoundedMsat = outboundForwardedHtlcRoundedMsat, - inboundClaimingHtlcRoundedMsat = inboundClaimingHtlcRoundedMsat, - inboundHtlcRoundedMsat = inboundHtlcRoundedMsat - ) - - is LdkLightningBalance.ClaimableAwaitingConfirmations -> LightningBalance.ClaimableAwaitingConfirmations( - channelId = channelId, - counterpartyNodeId = counterpartyNodeId, - amountSatoshis = amountSatoshis, - confirmationHeight = confirmationHeight, - source = source - ) - - is LdkLightningBalance.ContentiousClaimable -> LightningBalance.ContentiousClaimable( - channelId = channelId, - counterpartyNodeId = counterpartyNodeId, - amountSatoshis = amountSatoshis, - timeoutHeight = timeoutHeight, - paymentHash = paymentHash, - paymentPreimage = paymentPreimage - ) - - is LdkLightningBalance.CounterpartyRevokedOutputClaimable -> LightningBalance.CounterpartyRevokedOutputClaimable( - channelId = channelId, - counterpartyNodeId = counterpartyNodeId, - amountSatoshis = amountSatoshis - ) - - is LdkLightningBalance.MaybePreimageClaimableHtlc -> LightningBalance.MaybePreimageClaimableHtlc( - channelId = channelId, - counterpartyNodeId = counterpartyNodeId, - amountSatoshis = amountSatoshis, - expiryHeight = expiryHeight, - paymentHash = paymentHash - ) - - is LdkLightningBalance.MaybeTimeoutClaimableHtlc -> LightningBalance.MaybeTimeoutClaimableHtlc( - channelId = channelId, - counterpartyNodeId = counterpartyNodeId, - amountSatoshis = amountSatoshis, - claimableHeight = claimableHeight, - paymentHash = paymentHash, - outboundPayment = outboundPayment - ) -} - -fun LdkPendingSweepBalance.mapToPendingSweepBalance() = when (this) { - is LdkPendingSweepBalance.PendingBroadcast -> PendingSweepBalance.PendingBroadcast( - channelId = channelId, - amountSatoshis = amountSatoshis - ) - - is LdkPendingSweepBalance.BroadcastAwaitingConfirmation -> PendingSweepBalance.BroadcastAwaitingConfirmation( - channelId = channelId, - latestBroadcastHeight = latestBroadcastHeight, - latestSpendingTxid = latestSpendingTxid, - amountSatoshis = amountSatoshis - ) - - is LdkPendingSweepBalance.AwaitingThresholdConfirmations -> PendingSweepBalance.AwaitingThresholdConfirmations( - channelId = channelId, - latestSpendingTxid = latestSpendingTxid, - confirmationHash = confirmationHash, - confirmationHeight = confirmationHeight, - amountSatoshis = amountSatoshis - ) -} - -// endregion diff --git a/app/src/main/java/to/bitkit/models/LnPeer.kt b/app/src/main/java/to/bitkit/models/LnPeer.kt deleted file mode 100644 index 6bf1b75a47..0000000000 --- a/app/src/main/java/to/bitkit/models/LnPeer.kt +++ /dev/null @@ -1,62 +0,0 @@ -package to.bitkit.models - -import kotlinx.serialization.Serializable -import org.lightningdevkit.ldknode.PeerDetails - -@Serializable -data class LnPeer( - val nodeId: String, - val host: String, - val port: String, - val isConnected: Boolean = false, - val isPersisted: Boolean = false, -) { - constructor( - nodeId: String, - address: String, - ) : this( - nodeId, - address.substringBefore(":"), - address.substringAfter(":"), - ) - - constructor(peerDetails: PeerDetails) : this( - nodeId = peerDetails.nodeId, - host = peerDetails.address.substringBefore(":"), - port = peerDetails.address.substringAfter(":"), - isConnected = peerDetails.isConnected, - isPersisted = peerDetails.isPersisted, - ) - - val address get() = "$host:$port" - - override fun toString() = "$nodeId@$address" - - companion object { - fun parseUri(uriString: String): Result { - val uriComponents = uriString.split("@") - val nodeId = uriComponents[0] - - if (uriComponents.size != 2) { - return Result.failure(Exception("Invalid peer uri")) - } - - val address = uriComponents[1].split(":") - - if (address.size < 2) { - return Result.failure(Exception("Invalid peer uri")) - } - - val ip = address[0] - val port = address[1] - - return Result.success( - LnPeer( - nodeId = nodeId, - host = ip, - port = port, - ) - ) - } - } -} diff --git a/app/src/main/java/to/bitkit/models/OpenChannelResult.kt b/app/src/main/java/to/bitkit/models/OpenChannelResult.kt index 959c52a04d..a3dbacbec0 100644 --- a/app/src/main/java/to/bitkit/models/OpenChannelResult.kt +++ b/app/src/main/java/to/bitkit/models/OpenChannelResult.kt @@ -1,11 +1,12 @@ package to.bitkit.models import org.lightningdevkit.ldknode.ChannelConfig +import org.lightningdevkit.ldknode.PeerDetails import org.lightningdevkit.ldknode.UserChannelId data class OpenChannelResult( val userChannelId: UserChannelId, - val peer: LnPeer, + val peer: PeerDetails, val channelAmountSats: ULong, val pushToCounterpartySats: ULong? = null, val channelConfig: ChannelConfig? = null, diff --git a/app/src/main/java/to/bitkit/repositories/LightningRepo.kt b/app/src/main/java/to/bitkit/repositories/LightningRepo.kt index df6ad4b3ea..eddc82c267 100644 --- a/app/src/main/java/to/bitkit/repositories/LightningRepo.kt +++ b/app/src/main/java/to/bitkit/repositories/LightningRepo.kt @@ -23,12 +23,14 @@ import kotlinx.coroutines.withContext import kotlinx.coroutines.withTimeout import kotlinx.coroutines.withTimeoutOrNull import org.lightningdevkit.ldknode.Address +import org.lightningdevkit.ldknode.BalanceDetails import org.lightningdevkit.ldknode.BestBlock import org.lightningdevkit.ldknode.ChannelConfig import org.lightningdevkit.ldknode.ChannelDetails import org.lightningdevkit.ldknode.NodeStatus import org.lightningdevkit.ldknode.PaymentDetails import org.lightningdevkit.ldknode.PaymentId +import org.lightningdevkit.ldknode.PeerDetails import org.lightningdevkit.ldknode.SpendableUtxo import org.lightningdevkit.ldknode.Txid import to.bitkit.data.CacheStore @@ -37,9 +39,7 @@ import to.bitkit.data.keychain.Keychain import to.bitkit.di.BgDispatcher import to.bitkit.env.Env import to.bitkit.ext.getSatsPerVByteFor -import to.bitkit.models.BalanceDetails import to.bitkit.models.CoinSelectionPreference -import to.bitkit.models.LnPeer import to.bitkit.models.NodeLifecycleState import to.bitkit.models.OpenChannelResult import to.bitkit.models.TransactionMetadata @@ -63,8 +63,6 @@ import kotlin.time.Duration import kotlin.time.Duration.Companion.minutes import kotlin.time.Duration.Companion.seconds -private const val SYNC_TIMEOUT_MS = 20_000L - @Singleton class LightningRepo @Inject constructor( @BgDispatcher private val bgDispatcher: CoroutineDispatcher, @@ -421,7 +419,7 @@ class LightningRepo @Inject constructor( Result.success(Unit) } - suspend fun connectPeer(peer: LnPeer): Result = executeWhenNodeRunning("connectPeer") { + suspend fun connectPeer(peer: PeerDetails): Result = executeWhenNodeRunning("connectPeer") { lightningService.connectPeer(peer).onFailure { e -> return@executeWhenNodeRunning Result.failure(e) } @@ -429,7 +427,7 @@ class LightningRepo @Inject constructor( Result.success(Unit) } - suspend fun disconnectPeer(peer: LnPeer): Result = executeWhenNodeRunning("Disconnect peer") { + suspend fun disconnectPeer(peer: PeerDetails): Result = executeWhenNodeRunning("Disconnect peer") { lightningService.disconnectPeer(peer) syncState() Result.success(Unit) @@ -538,7 +536,9 @@ class LightningRepo @Inject constructor( channelId: String? = null, isMaxAmount: Boolean = false, ): Result = - executeWhenNodeRunning("Send on-chain") { + executeWhenNodeRunning("sendOnChain") { + require(address.isNotEmpty()) { "Send address cannot be empty" } + val transactionSpeed = speed ?: settingsStore.data.first().defaultTransactionSpeed val satsPerVByte = getFeeRateForSpeed(transactionSpeed, feeRates).getOrThrow().toUInt() @@ -662,7 +662,7 @@ class LightningRepo @Inject constructor( } suspend fun openChannel( - peer: LnPeer, + peer: PeerDetails, channelAmountSats: ULong, pushToCounterpartySats: ULong? = null, channelConfig: ChannelConfig? = null, @@ -721,7 +721,7 @@ class LightningRepo @Inject constructor( fun getStatus(): NodeStatus? = if (_lightningState.value.nodeLifecycleState.isRunning()) lightningService.status else null - fun getPeers(): List? = + fun getPeers(): List? = if (_lightningState.value.nodeLifecycleState.isRunning()) lightningService.peers else null fun getChannels(): List? = @@ -864,8 +864,9 @@ class LightningRepo @Inject constructor( } } - private companion object { - const val TAG = "LightningRepo" + companion object { + private const val TAG = "LightningRepo" + private const val SYNC_TIMEOUT_MS = 20_000L } } @@ -875,7 +876,7 @@ data class LightningState( val nodeId: String = "", val nodeStatus: NodeStatus? = null, val nodeLifecycleState: NodeLifecycleState = NodeLifecycleState.Stopped, - val peers: List = emptyList(), + val peers: List = emptyList(), val channels: List = emptyList(), val balances: BalanceDetails? = null, val isSyncingWallet: Boolean = false, diff --git a/app/src/main/java/to/bitkit/services/LightningService.kt b/app/src/main/java/to/bitkit/services/LightningService.kt index aa2746e284..9b27c25b34 100644 --- a/app/src/main/java/to/bitkit/services/LightningService.kt +++ b/app/src/main/java/to/bitkit/services/LightningService.kt @@ -13,6 +13,7 @@ import kotlinx.coroutines.withTimeout import org.lightningdevkit.ldknode.Address import org.lightningdevkit.ldknode.AnchorChannelsConfig import org.lightningdevkit.ldknode.BackgroundSyncConfig +import org.lightningdevkit.ldknode.BalanceDetails import org.lightningdevkit.ldknode.Bolt11Invoice import org.lightningdevkit.ldknode.Bolt11InvoiceDescription import org.lightningdevkit.ldknode.BuildException @@ -29,6 +30,7 @@ import org.lightningdevkit.ldknode.NodeException import org.lightningdevkit.ldknode.NodeStatus import org.lightningdevkit.ldknode.PaymentDetails import org.lightningdevkit.ldknode.PaymentId +import org.lightningdevkit.ldknode.PeerDetails import org.lightningdevkit.ldknode.SpendableUtxo import org.lightningdevkit.ldknode.Txid import org.lightningdevkit.ldknode.defaultConfig @@ -42,10 +44,8 @@ import to.bitkit.env.Env import to.bitkit.ext.DatePattern import to.bitkit.ext.totalNextOutboundHtlcLimitSats import to.bitkit.ext.uByteList -import to.bitkit.models.BalanceDetails -import to.bitkit.models.LnPeer +import to.bitkit.ext.uri import to.bitkit.models.OpenChannelResult -import to.bitkit.models.toDomainModel import to.bitkit.utils.LdkError import to.bitkit.utils.Logger import to.bitkit.utils.ServiceError @@ -59,7 +59,6 @@ import javax.inject.Singleton import kotlin.io.path.Path import kotlin.time.Duration import kotlin.time.Duration.Companion.seconds -import kotlin.toString typealias NodeEventHandler = suspend (Event) -> Unit @@ -74,7 +73,7 @@ class LightningService @Inject constructor( @Volatile var node: Node? = null - private lateinit var trustedLnPeers: List + private lateinit var trustedPeers: List suspend fun setup( walletIndex: Int, @@ -85,19 +84,20 @@ class LightningService @Inject constructor( val passphrase = keychain.loadString(Keychain.Key.BIP39_PASSPHRASE.name) // TODO get trustedLnPeers from blocktank info - this.trustedLnPeers = Env.trustedLnPeers + this.trustedPeers = Env.trustedLnPeers val dirPath = Env.ldkStoragePath(walletIndex) - val config = defaultConfig().apply { - storageDirPath = dirPath - network = Env.network + val trustedPeerNodeIds = trustedPeers.map { it.nodeId } - trustedPeers0conf = trustedLnPeers.map { it.nodeId } + val config = defaultConfig().copy( + storageDirPath = dirPath, + network = Env.network, + trustedPeers0conf = trustedPeerNodeIds, anchorChannelsConfig = AnchorChannelsConfig( - trustedPeersNoReserve = trustedPeers0conf, + trustedPeersNoReserve = trustedPeerNodeIds, perChannelReserveSats = 1u, ) - } + ) val builder = Builder.fromConfig(config).apply { setFilesystemLogger(generateLogFilePath(), Env.ldkLogLevel) @@ -274,7 +274,7 @@ class LightningService @Inject constructor( val node = this.node ?: throw ServiceError.NodeNotSetup ServiceQueue.LDK.background { - for (peer in trustedLnPeers) { + for (peer in trustedPeers) { try { node.connect(peer.nodeId, peer.address, persist = true) Logger.info("Connected to trusted peer: $peer") @@ -285,54 +285,59 @@ class LightningService @Inject constructor( } } - suspend fun connectPeer(peer: LnPeer): Result { + suspend fun connectPeer(peer: PeerDetails): Result { val node = this.node ?: throw ServiceError.NodeNotSetup - + val uri = peer.uri return ServiceQueue.LDK.background { try { - Logger.debug("Connecting peer: $peer") + Logger.debug("Connecting peer: $uri") node.connect(peer.nodeId, peer.address, persist = true) - Logger.info("Peer connected: $peer") + Logger.info("Peer connected: $uri") Result.success(Unit) } catch (e: NodeException) { val error = LdkError(e) - Logger.error("Peer connect error: $peer", error) + Logger.error("Peer connect error: $uri", error) Result.failure(error) } } } - suspend fun disconnectPeer(peer: LnPeer) { + suspend fun disconnectPeer(peer: PeerDetails) { val node = this.node ?: throw ServiceError.NodeNotSetup - Logger.debug("Disconnecting peer: $peer") + val uri = peer.uri + Logger.debug("Disconnecting peer: $uri") try { ServiceQueue.LDK.background { node.disconnect(peer.nodeId) } - Logger.info("Peer disconnected: $peer") + Logger.info("Peer disconnected: $uri") } catch (e: NodeException) { - Logger.warn("Peer disconnect error: $peer", LdkError(e)) + Logger.warn("Peer disconnect error: $uri", LdkError(e)) } } - private fun getLspPeers(): List { + private fun getLspPeers(): List { val lspPeers = Env.trustedLnPeers // TODO get from blocktank info.nodes[] when setup uses it to set trustedPeers0conf // pseudocode idea: - // val lspPeers = getInfo(refresh = true)?.nodes?.map { LnPeer(nodeId = it.pubkey, address = "TO_DO") } + // val lspPeers = getInfo(true)?.nodes?.map { PeerDetails.from(nodeId = it.pubkey, address = "TO DO") } return lspPeers } - fun hasExternalPeers() = peers?.any { p -> p.toString() !in getLspPeers().map { it.toString() } } == true + fun hasExternalPeers(): Boolean { + val ourPeers = this.peers.orEmpty().map { it.uri } + val lspPeers = getLspPeers().map { it.uri }.toSet() + return ourPeers.any { p -> p !in lspPeers } + } // endregion // region channels suspend fun openChannel( - peer: LnPeer, + peer: PeerDetails, channelAmountSats: ULong, pushToCounterpartySats: ULong? = null, channelConfig: ChannelConfig? = null, @@ -341,7 +346,7 @@ class LightningService @Inject constructor( return ServiceQueue.LDK.background { try { - Logger.debug("Initiating channel open (sats: $channelAmountSats) with peer: $peer") + Logger.debug("Initiating channel open (sats: $channelAmountSats) with peer: ${peer.uri}") val userChannelId = node.openChannel( nodeId = peer.nodeId, @@ -764,10 +769,10 @@ class LightningService @Inject constructor( // region state val nodeId: String? get() = node?.nodeId() - val balances: BalanceDetails? get() = node?.listBalances()?.toDomainModel() + val balances: BalanceDetails? get() = node?.listBalances() val status: NodeStatus? get() = node?.status() val config: Config? get() = node?.config() - val peers: List? get() = node?.listPeers()?.map(::LnPeer) + val peers: List? get() = node?.listPeers() val channels: List? get() = node?.listChannels() val payments: List? get() = node?.listPayments() diff --git a/app/src/main/java/to/bitkit/ui/NodeInfoScreen.kt b/app/src/main/java/to/bitkit/ui/NodeInfoScreen.kt index 4075548956..50d4be7dc3 100644 --- a/app/src/main/java/to/bitkit/ui/NodeInfoScreen.kt +++ b/app/src/main/java/to/bitkit/ui/NodeInfoScreen.kt @@ -31,19 +31,21 @@ import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.navigation.NavController import kotlinx.datetime.Clock +import org.lightningdevkit.ldknode.BalanceDetails import org.lightningdevkit.ldknode.BalanceSource import org.lightningdevkit.ldknode.BestBlock import org.lightningdevkit.ldknode.ChannelDetails +import org.lightningdevkit.ldknode.LightningBalance import org.lightningdevkit.ldknode.NodeStatus +import org.lightningdevkit.ldknode.PeerDetails import to.bitkit.R +import to.bitkit.env.Env import to.bitkit.ext.amountSats import to.bitkit.ext.balanceUiText import to.bitkit.ext.channelId import to.bitkit.ext.createChannelDetails import to.bitkit.ext.formatted -import to.bitkit.models.BalanceDetails -import to.bitkit.models.LightningBalance -import to.bitkit.models.LnPeer +import to.bitkit.ext.uri import to.bitkit.models.NodeLifecycleState import to.bitkit.models.Toast import to.bitkit.models.formatToModernDisplay @@ -108,7 +110,7 @@ private fun Content( onBack: () -> Unit = {}, onClose: () -> Unit = {}, onRefresh: () -> Unit = {}, - onDisconnectPeer: (LnPeer) -> Unit = {}, + onDisconnectPeer: (PeerDetails) -> Unit = {}, onCopy: (String) -> Unit = {}, ) { ScreenColumn { @@ -178,7 +180,7 @@ private fun NodeIdSection( modifier = Modifier .clickableAlpha( onClick = copyToClipboard(nodeId) { - onCopy(nodeId) + onCopy(it) } ) .testTag("LDKNodeID") @@ -307,7 +309,7 @@ private fun ChannelsSection( overflow = TextOverflow.MiddleEllipsis, modifier = Modifier.clickableAlpha( onClick = copyToClipboard(channel.channelId) { - onCopy(channel.channelId) + onCopy(it) } ) ) @@ -366,8 +368,8 @@ private fun ChannelsSection( @Composable private fun PeersSection( - peers: List, - onDisconnectPeer: (LnPeer) -> Unit, + peers: List, + onDisconnectPeer: (PeerDetails) -> Unit, onCopy: (String) -> Unit = {}, ) { Column(modifier = Modifier.fillMaxWidth()) { @@ -379,14 +381,14 @@ private fun PeersSection( modifier = Modifier.height(52.dp) ) { BodyM( - text = peer.toString(), + text = peer.uri, maxLines = 1, overflow = TextOverflow.MiddleEllipsis, modifier = Modifier .weight(1f) .clickableAlpha( - onClick = copyToClipboard(peer.toString()) { - onCopy(peer.toString()) + onClick = copyToClipboard(peer.uri) { + onCopy(it) } ) ) @@ -465,12 +467,7 @@ private fun PreviewDevMode() { latestChannelMonitorArchivalHeight = null, ), nodeId = "0348a2b7c2d3f4e5a6b7c8d9e0f1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d7e8f9", - peers = listOf( - LnPeer( - nodeId = "0248a2b7c2d3f4e5a6b7c8d9e0f1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d7e8f9", - address = "192.168.1.1:9735", - ), - ), + peers = listOf(Env.Peers.staging), channels = listOf( createChannelDetails().copy( channelId = "abc123def456789012345678901234567890123456789012345678901234567890", diff --git a/app/src/main/java/to/bitkit/ui/components/QrCodeImage.kt b/app/src/main/java/to/bitkit/ui/components/QrCodeImage.kt index f18b44ae6d..e3e8af358e 100644 --- a/app/src/main/java/to/bitkit/ui/components/QrCodeImage.kt +++ b/app/src/main/java/to/bitkit/ui/components/QrCodeImage.kt @@ -1,6 +1,8 @@ package to.bitkit.ui.components import android.graphics.Bitmap +import androidx.compose.animation.Crossfade +import androidx.compose.animation.core.tween import androidx.compose.foundation.Image import androidx.compose.foundation.background import androidx.compose.foundation.clickable @@ -21,6 +23,7 @@ import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.asImageBitmap import androidx.compose.ui.graphics.painter.BitmapPainter @@ -37,7 +40,6 @@ import androidx.compose.ui.unit.dp import androidx.core.graphics.createBitmap import com.google.zxing.BarcodeFormat import com.google.zxing.EncodeHintType -import com.google.zxing.WriterException import com.google.zxing.qrcode.QRCodeWriter import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch @@ -47,6 +49,10 @@ import to.bitkit.ui.theme.AppShapes import to.bitkit.ui.theme.AppThemeSurface import to.bitkit.ui.theme.Colors +private const val QUIET_ZONE_MIN = 2 +private const val QUIET_ZONE_MAX = 4 +private const val QUIET_ZONE_RATIO = 150 + @OptIn(ExperimentalMaterial3Api::class) @Composable fun QrCodeImage( @@ -63,11 +69,11 @@ fun QrCodeImage( val coroutineScope = rememberCoroutineScope() Box( - contentAlignment = Alignment.TopCenter, + contentAlignment = Alignment.Center, modifier = modifier - .background(Color.White, AppShapes.small) .aspectRatio(1f) - .padding(8.dp) + .clip(AppShapes.small) + .background(Color.White) ) { val bitmap = rememberQrBitmap(content, size) @@ -75,53 +81,61 @@ fun QrCodeImage( onBitmapGenerated(bitmap) } - if (bitmap != null) { - val imageComposable = @Composable { - Image( - painter = remember(bitmap) { BitmapPainter(bitmap.asImageBitmap()) }, - contentDescription = content, - contentScale = ContentScale.Inside, - modifier = Modifier - .clickable(enabled = tipMessage.isNotBlank()) { - coroutineScope.launch { - context.setClipboardText(content) - tooltipState.show() + Crossfade( + targetState = bitmap, + animationSpec = tween(durationMillis = 200), + label = "QR Code Crossfade" + ) { currentBitmap -> + if (currentBitmap != null) { + val imageComposable = @Composable { + Image( + painter = remember(currentBitmap) { BitmapPainter(currentBitmap.asImageBitmap()) }, + contentDescription = content, + contentScale = ContentScale.Inside, + modifier = Modifier + .clickable(enabled = tipMessage.isNotBlank()) { + coroutineScope.launch { + context.setClipboardText(content) + tooltipState.show() + } } - } - .then(testTag?.let { Modifier.testTag(it) } ?: Modifier) - ) + .then(testTag?.let { Modifier.testTag(it) } ?: Modifier) + ) + } + + if (tipMessage.isNotBlank()) { + Tooltip( + text = tipMessage, + tooltipState = tooltipState, + content = imageComposable, + ) + } else { + imageComposable() + } } + } - if (tipMessage.isNotBlank()) { - Tooltip( - text = tipMessage, - tooltipState = tooltipState, - content = imageComposable, + logoPainter?.let { + Box( + contentAlignment = Alignment.Center, + modifier = Modifier + .size(68.dp) + .background(Color.White, shape = CircleShape) + .align(Alignment.Center) + ) { + Image( + painter = it, + contentDescription = null, + modifier = Modifier.size(50.dp) ) - } else { - imageComposable() } + } - logoPainter?.let { - Box( - contentAlignment = Alignment.Center, - modifier = Modifier - .size(68.dp) - .background(Color.White, shape = CircleShape) - .align(Alignment.Center) - ) { - Image( - painter = it, - contentDescription = null, - modifier = Modifier.size(50.dp) - ) - } - } - } else { + if (bitmap == null) { CircularProgressIndicator( color = Colors.Black, - strokeWidth = 2.dp, - modifier = Modifier.align(Alignment.Center) + strokeWidth = 4.dp, + modifier = Modifier.size(68.dp) ) } } @@ -135,16 +149,16 @@ private fun rememberQrBitmap(content: String, size: Dp): Bitmap? { val sizePx = with(LocalDensity.current) { size.roundToPx() } LaunchedEffect(content, size) { - if (bitmap != null) return@LaunchedEffect + bitmap = null // Always reset to show loading indicator launch(Dispatchers.Default) { val qrCodeWriter = QRCodeWriter() - val encodeHints = mutableMapOf().apply { - this[EncodeHintType.MARGIN] = 0 - } + val quietZoneModules = (content.length / QUIET_ZONE_RATIO + 1).coerceIn(QUIET_ZONE_MIN, QUIET_ZONE_MAX) + + val encodeHints = mapOf(EncodeHintType.MARGIN to quietZoneModules) - val bitmapMatrix = try { + val bitmapMatrix = runCatching { qrCodeWriter.encode( content, BarcodeFormat.QR_CODE, @@ -152,29 +166,22 @@ private fun rememberQrBitmap(content: String, size: Dp): Bitmap? { sizePx, encodeHints, ) - } catch (_: WriterException) { - null - } + }.getOrElse { return@launch } - val matrixWidth = bitmapMatrix?.width ?: sizePx - val matrixHeight = bitmapMatrix?.height ?: sizePx - - val newBitmap = createBitmap( - width = bitmapMatrix?.width ?: sizePx, - height = bitmapMatrix?.height ?: sizePx - ) + val matrixWidth = bitmapMatrix.width + val matrixHeight = bitmapMatrix.height + val newBitmap = createBitmap(width = matrixWidth, height = matrixHeight) val pixels = IntArray(matrixWidth * matrixHeight) for (x in 0 until matrixWidth) { for (y in 0 until matrixHeight) { - val shouldColorPixel = bitmapMatrix?.get(x, y) ?: false - val pixelColor = - if (shouldColorPixel) { - android.graphics.Color.BLACK - } else { - android.graphics.Color.WHITE - } + val shouldColorPixel = bitmapMatrix[x, y] + val pixelColor = if (shouldColorPixel) { + android.graphics.Color.BLACK + } else { + android.graphics.Color.WHITE + } pixels[y * matrixWidth + x] = pixelColor } diff --git a/app/src/main/java/to/bitkit/ui/components/Tooltip.kt b/app/src/main/java/to/bitkit/ui/components/Tooltip.kt index 8457e030f2..dfd0425d84 100644 --- a/app/src/main/java/to/bitkit/ui/components/Tooltip.kt +++ b/app/src/main/java/to/bitkit/ui/components/Tooltip.kt @@ -20,7 +20,7 @@ fun Tooltip( text: String, tooltipState: TooltipState, modifier: Modifier = Modifier, - content: @Composable (() -> Unit) + content: @Composable () -> Unit ) { TooltipBox( modifier = modifier, @@ -48,6 +48,7 @@ fun Tooltip( } }, state = tooltipState, + focusable = false, content = content ) } diff --git a/app/src/main/java/to/bitkit/ui/components/settings/SectionHeader.kt b/app/src/main/java/to/bitkit/ui/components/settings/SectionHeader.kt index 3042f682f3..f654fbdb20 100644 --- a/app/src/main/java/to/bitkit/ui/components/settings/SectionHeader.kt +++ b/app/src/main/java/to/bitkit/ui/components/settings/SectionHeader.kt @@ -2,15 +2,21 @@ package to.bitkit.ui.components.settings import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.WindowInsets import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.safeContent +import androidx.compose.material3.HorizontalDivider import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import to.bitkit.ui.components.Caption13Up +import to.bitkit.ui.shared.util.screen import to.bitkit.ui.theme.AppThemeSurface import to.bitkit.ui.theme.Colors @@ -19,22 +25,51 @@ fun SectionHeader( title: String, modifier: Modifier = Modifier, color: Color = Colors.White64, + padding: PaddingValues = PaddingValues(top = 16.dp), + height: Dp = 50.dp, ) { Column( verticalArrangement = Arrangement.Center, modifier = modifier .fillMaxWidth() - .padding(top = 16.dp) - .height(50.dp) + .padding(padding) + .height(height) ) { Caption13Up(text = title, color = color) } } -@Preview +@Preview(showSystemUi = true) @Composable private fun Preview() { AppThemeSurface { - SectionHeader("General") + Column( + modifier = Modifier + .screen(insets = WindowInsets.safeContent) + ) { + SectionHeader("Default") + HorizontalDivider() + SectionHeader( + title = "Colors.Brand", + color = Colors.Brand, + ) + HorizontalDivider() + SectionHeader( + title = "Dp.Unspecified", + height = Dp.Unspecified, + ) + HorizontalDivider() + SectionHeader( + title = "PaddingValues.Zero", + padding = PaddingValues.Zero, + ) + HorizontalDivider() + SectionHeader( + title = "PaddingValues.Zero + Dp.Unspecified", + padding = PaddingValues.Zero, + height = Dp.Unspecified, + ) + HorizontalDivider() + } } } diff --git a/app/src/main/java/to/bitkit/ui/screens/transfer/external/ExternalConnectionScreen.kt b/app/src/main/java/to/bitkit/ui/screens/transfer/external/ExternalConnectionScreen.kt index c1e5128c74..f34f1ad6dc 100644 --- a/app/src/main/java/to/bitkit/ui/screens/transfer/external/ExternalConnectionScreen.kt +++ b/app/src/main/java/to/bitkit/ui/screens/transfer/external/ExternalConnectionScreen.kt @@ -33,9 +33,12 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.lifecycle.SavedStateHandle import kotlinx.coroutines.flow.filterNotNull +import org.lightningdevkit.ldknode.PeerDetails import to.bitkit.R +import to.bitkit.ext.from import to.bitkit.ext.getClipboardText -import to.bitkit.models.LnPeer +import to.bitkit.ext.host +import to.bitkit.ext.port import to.bitkit.ui.Routes import to.bitkit.ui.components.BodyM import to.bitkit.ui.components.ButtonSize @@ -105,7 +108,7 @@ fun ExternalConnectionScreen( @Composable private fun ExternalConnectionContent( uiState: ExternalNodeContract.UiState, - onContinueClick: (LnPeer) -> Unit = {}, + onContinueClick: (PeerDetails) -> Unit = {}, onScanClick: () -> Unit = {}, onPasteClick: () -> Unit = {}, onBackClick: () -> Unit = {}, @@ -220,10 +223,12 @@ private fun ExternalConnectionContent( ) PrimaryButton( text = stringResource(R.string.common__continue), - onClick = { onContinueClick(LnPeer(nodeId = nodeId, host = host, port = port)) }, + onClick = { onContinueClick(PeerDetails.from(nodeId = nodeId, host = host, port = port)) }, enabled = isValid, isLoading = uiState.isLoading, - modifier = Modifier.weight(1f).testTag("ExternalContinue") + modifier = Modifier + .weight(1f) + .testTag("ExternalContinue") ) } Spacer(modifier = Modifier.height(16.dp)) diff --git a/app/src/main/java/to/bitkit/ui/screens/transfer/external/ExternalNodeViewModel.kt b/app/src/main/java/to/bitkit/ui/screens/transfer/external/ExternalNodeViewModel.kt index e3060b0880..80f2aad4ed 100644 --- a/app/src/main/java/to/bitkit/ui/screens/transfer/external/ExternalNodeViewModel.kt +++ b/app/src/main/java/to/bitkit/ui/screens/transfer/external/ExternalNodeViewModel.kt @@ -13,13 +13,14 @@ import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch import org.lightningdevkit.ldknode.Event +import org.lightningdevkit.ldknode.PeerDetails import org.lightningdevkit.ldknode.UserChannelId import to.bitkit.R import to.bitkit.data.CacheStore import to.bitkit.data.SettingsStore import to.bitkit.ext.WatchResult +import to.bitkit.ext.parse import to.bitkit.ext.watchUntil -import to.bitkit.models.LnPeer import to.bitkit.models.Toast import to.bitkit.models.TransactionMetadata import to.bitkit.models.TransactionSpeed @@ -67,7 +68,7 @@ class ExternalNodeViewModel @Inject constructor( } } - fun onConnectionContinue(peer: LnPeer) { + fun onConnectionContinue(peer: PeerDetails) { viewModelScope.launch { _uiState.update { it.copy(isLoading = true) } @@ -90,7 +91,7 @@ class ExternalNodeViewModel @Inject constructor( fun parseNodeUri(uriString: String) { viewModelScope.launch { - val result = LnPeer.parseUri(uriString.trim()) + val result = runCatching { PeerDetails.parse(uriString) } if (result.isSuccess) { _uiState.update { it.copy(peer = result.getOrNull()) } @@ -229,7 +230,7 @@ class ExternalNodeViewModel @Inject constructor( interface ExternalNodeContract { data class UiState( val isLoading: Boolean = false, - val peer: LnPeer? = null, + val peer: PeerDetails? = null, val amount: Amount = Amount(), val networkFee: Long = 0, val customFeeRate: UInt? = null, diff --git a/app/src/main/java/to/bitkit/ui/screens/transfer/external/LnurlChannelScreen.kt b/app/src/main/java/to/bitkit/ui/screens/transfer/external/LnurlChannelScreen.kt index f8abe21ef2..9e1236f983 100644 --- a/app/src/main/java/to/bitkit/ui/screens/transfer/external/LnurlChannelScreen.kt +++ b/app/src/main/java/to/bitkit/ui/screens/transfer/external/LnurlChannelScreen.kt @@ -23,7 +23,9 @@ 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.LnPeer +import to.bitkit.env.Env +import to.bitkit.ext.host +import to.bitkit.ext.port import to.bitkit.ui.Routes import to.bitkit.ui.components.BodyM import to.bitkit.ui.components.Caption13Up @@ -173,13 +175,7 @@ private fun InfoRow( private fun Preview() { AppThemeSurface { Content( - uiState = LnurlChannelUiState( - peer = LnPeer( - nodeId = "12345678901234567890123456789012345678901234567890", - host = "127.0.0.1", - port = "9735", - ) - ), + uiState = LnurlChannelUiState(peer = Env.Peers.staging), ) } } diff --git a/app/src/main/java/to/bitkit/ui/screens/transfer/external/LnurlChannelViewModel.kt b/app/src/main/java/to/bitkit/ui/screens/transfer/external/LnurlChannelViewModel.kt index 49b1cb3eac..21891070df 100644 --- a/app/src/main/java/to/bitkit/ui/screens/transfer/external/LnurlChannelViewModel.kt +++ b/app/src/main/java/to/bitkit/ui/screens/transfer/external/LnurlChannelViewModel.kt @@ -9,8 +9,9 @@ import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch +import org.lightningdevkit.ldknode.PeerDetails import to.bitkit.R -import to.bitkit.models.LnPeer +import to.bitkit.ext.parse import to.bitkit.models.Toast import to.bitkit.repositories.LightningRepo import to.bitkit.ui.Routes @@ -37,7 +38,7 @@ class LnurlChannelViewModel @Inject constructor( viewModelScope.launch { lightningRepo.fetchLnurlChannelInfo(params.uri) .onSuccess { channelInfo -> - val peer = LnPeer.parseUri(channelInfo.uri).getOrElse { + val peer = runCatching { PeerDetails.parse(channelInfo.uri) }.getOrElse { errorToast(it) return@onSuccess } @@ -91,7 +92,7 @@ class LnurlChannelViewModel @Inject constructor( } data class LnurlChannelUiState( - val peer: LnPeer? = null, + val peer: PeerDetails? = null, val isConnecting: Boolean = false, val isConnected: Boolean = false, ) diff --git a/app/src/main/java/to/bitkit/ui/screens/wallets/HomeScreen.kt b/app/src/main/java/to/bitkit/ui/screens/wallets/HomeScreen.kt index 7006f85739..8ab356d07c 100644 --- a/app/src/main/java/to/bitkit/ui/screens/wallets/HomeScreen.kt +++ b/app/src/main/java/to/bitkit/ui/screens/wallets/HomeScreen.kt @@ -460,99 +460,7 @@ private fun Content( ) } } else { - Column( - modifier = Modifier.fillMaxWidth(), - verticalArrangement = Arrangement.spacedBy(16.dp) - ) { - homeUiState.widgetsWithPosition.forEach { widgetsWithPosition -> - when (widgetsWithPosition.type) { - WidgetType.BLOCK -> { - homeUiState.currentBlock?.run { - BlockCard( - showWidgetTitle = homeUiState.showWidgetTitles, - showBlock = homeUiState.blocksPreferences.showBlock, - showTime = homeUiState.blocksPreferences.showTime, - showDate = homeUiState.blocksPreferences.showDate, - showTransactions = homeUiState.blocksPreferences.showTransactions, - showSize = homeUiState.blocksPreferences.showSize, - showSource = homeUiState.blocksPreferences.showSource, - time = time, - date = date, - transactions = transactionCount, - size = size, - source = source, - block = height, - modifier = Modifier - .fillMaxWidth() - .testTag("BlocksWidget") - ) - } - } - - WidgetType.CALCULATOR -> { - currencyViewModel?.let { - CalculatorCard( - currencyViewModel = it, - showWidgetTitle = homeUiState.showWidgetTitles, - modifier = Modifier.fillMaxWidth() - ) - } - } - - WidgetType.FACTS -> { - homeUiState.currentFact?.run { - FactsCard( - showWidgetTitle = homeUiState.showWidgetTitles, - showSource = homeUiState.factsPreferences.showSource, - headline = homeUiState.currentFact, - modifier = Modifier.fillMaxWidth() - ) - } - } - - WidgetType.NEWS -> { - homeUiState.currentArticle?.run { - HeadlineCard( - showWidgetTitle = homeUiState.showWidgetTitles, - showTime = homeUiState.headlinePreferences.showTime, - showSource = homeUiState.headlinePreferences.showSource, - headline = title, - time = timeAgo, - source = publisher, - link = link, - modifier = Modifier - .fillMaxWidth() - .testTag("NewsWidget") - ) - } - } - - WidgetType.PRICE -> { - homeUiState.currentPrice?.run { - PriceCard( - showWidgetTitle = homeUiState.showWidgetTitles, - pricePreferences = homeUiState.pricePreferences, - priceDTO = homeUiState.currentPrice, - modifier = Modifier - .fillMaxWidth() - .testTag("PriceWidget") - ) - } - } - - WidgetType.WEATHER -> { - homeUiState.currentWeather?.run { - WeatherCard( - showWidgetTitle = homeUiState.showWidgetTitles, - weatherModel = this, - preferences = homeUiState.weatherPreferences, - modifier = Modifier.fillMaxWidth() - ) - } - } - } - } - } + Widgets(homeUiState) } Spacer(modifier = Modifier.height(32.dp)) @@ -594,6 +502,103 @@ private fun Content( } } +@Composable +private fun Widgets(homeUiState: HomeUiState) { + Column( + modifier = Modifier.fillMaxWidth(), + verticalArrangement = Arrangement.spacedBy(16.dp) + ) { + homeUiState.widgetsWithPosition.forEach { widgetsWithPosition -> + when (widgetsWithPosition.type) { + WidgetType.BLOCK -> { + homeUiState.currentBlock?.run { + BlockCard( + showWidgetTitle = homeUiState.showWidgetTitles, + showBlock = homeUiState.blocksPreferences.showBlock, + showTime = homeUiState.blocksPreferences.showTime, + showDate = homeUiState.blocksPreferences.showDate, + showTransactions = homeUiState.blocksPreferences.showTransactions, + showSize = homeUiState.blocksPreferences.showSize, + showSource = homeUiState.blocksPreferences.showSource, + time = time, + date = date, + transactions = transactionCount, + size = size, + source = source, + block = height, + modifier = Modifier + .fillMaxWidth() + .testTag("BlocksWidget") + ) + } + } + + WidgetType.CALCULATOR -> { + currencyViewModel?.let { + CalculatorCard( + currencyViewModel = it, + showWidgetTitle = homeUiState.showWidgetTitles, + modifier = Modifier.fillMaxWidth() + ) + } + } + + WidgetType.FACTS -> { + homeUiState.currentFact?.run { + FactsCard( + showWidgetTitle = homeUiState.showWidgetTitles, + showSource = homeUiState.factsPreferences.showSource, + headline = homeUiState.currentFact, + modifier = Modifier.fillMaxWidth() + ) + } + } + + WidgetType.NEWS -> { + homeUiState.currentArticle?.run { + HeadlineCard( + showWidgetTitle = homeUiState.showWidgetTitles, + showTime = homeUiState.headlinePreferences.showTime, + showSource = homeUiState.headlinePreferences.showSource, + headline = title, + time = timeAgo, + source = publisher, + link = link, + modifier = Modifier + .fillMaxWidth() + .testTag("NewsWidget") + ) + } + } + + WidgetType.PRICE -> { + homeUiState.currentPrice?.run { + PriceCard( + showWidgetTitle = homeUiState.showWidgetTitles, + pricePreferences = homeUiState.pricePreferences, + priceDTO = homeUiState.currentPrice, + modifier = Modifier + .fillMaxWidth() + .testTag("PriceWidget") + ) + } + } + + WidgetType.WEATHER -> { + homeUiState.currentWeather?.run { + WeatherCard( + showWidgetTitle = homeUiState.showWidgetTitles, + weatherModel = this, + preferences = homeUiState.weatherPreferences, + modifier = Modifier.fillMaxWidth() + ) + } + } + } + } + } +} + @Composable @OptIn(ExperimentalMaterial3Api::class) private fun TopBar( 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 21411ed73b..ed9eb60b68 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 @@ -386,7 +386,7 @@ private fun ActivityDetailContent( .fillMaxWidth() .clickableAlpha( onClick = copyToClipboard(message) { - onCopy(message) + onCopy(it) } ) ) { diff --git a/app/src/main/java/to/bitkit/ui/screens/wallets/activity/ActivityExploreScreen.kt b/app/src/main/java/to/bitkit/ui/screens/wallets/activity/ActivityExploreScreen.kt index 8eefa7c23f..3410218928 100644 --- a/app/src/main/java/to/bitkit/ui/screens/wallets/activity/ActivityExploreScreen.kt +++ b/app/src/main/java/to/bitkit/ui/screens/wallets/activity/ActivityExploreScreen.kt @@ -196,7 +196,7 @@ private fun LightningDetails( value = preimage, modifier = Modifier.clickableAlpha( onClick = copyToClipboard(preimage) { - onCopy(preimage) + onCopy(it) } ), ) @@ -206,7 +206,7 @@ private fun LightningDetails( value = paymentHash, modifier = Modifier.clickableAlpha( onClick = copyToClipboard(paymentHash) { - onCopy(paymentHash) + onCopy(it) } ), ) @@ -215,7 +215,7 @@ private fun LightningDetails( value = invoice, modifier = Modifier.clickableAlpha( onClick = copyToClipboard(invoice) { - onCopy(invoice) + onCopy(it) } ), ) @@ -235,7 +235,7 @@ private fun ColumnScope.OnchainDetails( modifier = Modifier .clickableAlpha( onClick = copyToClipboard(txId) { - onCopy(txId) + onCopy(it) } ) .testTag("TXID") diff --git a/app/src/main/java/to/bitkit/ui/settings/ChannelOrdersScreen.kt b/app/src/main/java/to/bitkit/ui/settings/ChannelOrdersScreen.kt index 6808f4b5a9..3c9fb99fb3 100644 --- a/app/src/main/java/to/bitkit/ui/settings/ChannelOrdersScreen.kt +++ b/app/src/main/java/to/bitkit/ui/settings/ChannelOrdersScreen.kt @@ -1,15 +1,11 @@ package to.bitkit.ui.settings -import androidx.compose.animation.core.Spring -import androidx.compose.animation.core.animateFloatAsState -import androidx.compose.animation.core.spring import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues 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.lazy.LazyColumn import androidx.compose.foundation.lazy.items @@ -19,26 +15,16 @@ import androidx.compose.material3.CardDefaults import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Scaffold import androidx.compose.material3.Surface -import androidx.compose.material3.Text 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.rememberCoroutineScope -import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.scale -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.platform.LocalClipboardManager -import androidx.compose.ui.text.AnnotatedString -import androidx.compose.ui.text.font.FontFamily import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp -import androidx.compose.ui.unit.sp import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.synonym.bitkitcore.BtBolt11InvoiceState import com.synonym.bitkitcore.BtOrderState @@ -54,17 +40,26 @@ import com.synonym.bitkitcore.IBtPayment import com.synonym.bitkitcore.IDiscount import com.synonym.bitkitcore.ILspNode import com.synonym.bitkitcore.IcJitEntry -import kotlinx.coroutines.delay import kotlinx.coroutines.launch import to.bitkit.models.formatToModernDisplay import to.bitkit.ui.Routes import to.bitkit.ui.blocktankViewModel import to.bitkit.ui.components.BodyS +import to.bitkit.ui.components.BodySSB +import to.bitkit.ui.components.Caption +import to.bitkit.ui.components.Caption13Up +import to.bitkit.ui.components.CaptionB +import to.bitkit.ui.components.Footnote +import to.bitkit.ui.components.HorizontalSpacer import to.bitkit.ui.components.PrimaryButton +import to.bitkit.ui.components.VerticalSpacer +import to.bitkit.ui.components.settings.SectionHeader import to.bitkit.ui.scaffold.AppTopBar import to.bitkit.ui.shared.util.clickableAlpha +import to.bitkit.ui.theme.AppShapes import to.bitkit.ui.theme.AppThemeSurface import to.bitkit.ui.theme.Colors +import to.bitkit.ui.utils.copyToClipboard import to.bitkit.utils.Logger @Composable @@ -77,98 +72,60 @@ fun ChannelOrdersScreen( val orders by blocktank.orders.collectAsStateWithLifecycle() val cJitEntries by blocktank.cJitEntries.collectAsStateWithLifecycle() - LaunchedEffect(Unit) { - blocktank.refreshOrders() - } + LaunchedEffect(Unit) { blocktank.refreshOrders() } - ChannelOrdersView( + Content( orders = orders, cJitEntries = cJitEntries, - onBackClick = onBackClick, - onOrderItemClick = onOrderItemClick, - onCjitItemClick = onCjitItemClick, + onBack = onBackClick, + onClickOrder = onOrderItemClick, + onClickCjit = onCjitItemClick, ) } @Composable -private fun ChannelOrdersView( +private fun Content( orders: List, cJitEntries: List, - onBackClick: () -> Unit, - onOrderItemClick: (String) -> Unit, - onCjitItemClick: (String) -> Unit, + modifier: Modifier = Modifier, + onBack: () -> Unit = {}, + onClickOrder: (String) -> Unit = {}, + onClickCjit: (String) -> Unit = {}, ) { Scaffold( - topBar = { - AppTopBar( - titleText = "Channel Orders", - onBackClick = onBackClick, - ) - } + topBar = { AppTopBar(titleText = "Channel Orders", onBackClick = onBack) }, + modifier = modifier, ) { padding -> LazyColumn( - modifier = Modifier.padding(padding) + contentPadding = PaddingValues(horizontal = 16.dp), + modifier = Modifier + .padding(padding) ) { - item { - Text( - text = "Orders", - style = MaterialTheme.typography.titleMedium, - modifier = Modifier.padding(16.dp) - ) + stickyHeader { + SectionHeader(title = "Orders", padding = PaddingValues.Zero) } - orders.let { orders -> if (orders.isEmpty()) { item { - Text( - text = "No orders found…", - color = Color.Gray, - modifier = Modifier.padding(16.dp) - ) + BodyS(text = "No CJIT entries found…") } } else { items(orders) { order -> - Card( - colors = cardColors, - modifier = Modifier - .fillMaxWidth() - .padding(16.dp) - .clickable { onOrderItemClick(order.id) } - ) { - OrderRow(order = order) - } + OrderCard(order, onClickOrder) } } } - - item { - Text( - text = "CJIT Entries", - style = MaterialTheme.typography.titleMedium, - modifier = Modifier.padding(16.dp) - ) + stickyHeader { + SectionHeader(title = "CJIT Entries", padding = PaddingValues.Zero) } - cJitEntries.let { entries -> if (entries.isEmpty()) { item { - Text( - text = "No CJIT entries found…", - color = Color.Gray, - modifier = Modifier.padding(16.dp) - ) + BodyS(text = "No CJIT entries found…") } } else { items(entries) { entry -> - Card( - colors = cardColors, - modifier = Modifier - .fillMaxWidth() - .padding(16.dp) - .clickable { onCjitItemClick(entry.id) } - ) { - CJitRow(entry = entry) - } + CJitCard(entry, onClickCjit) } } } @@ -179,160 +136,99 @@ private fun ChannelOrdersView( @Composable fun OrderDetailScreen( orderItem: Routes.OrderDetail, - onBackClick: () -> Unit, + onBackClick: () -> Unit = {}, ) { val blocktank = blocktankViewModel ?: return val orders by blocktank.orders.collectAsStateWithLifecycle() val order = orders.find { it.id == orderItem.id } ?: return - OrderDetailView( + val coroutineScope = rememberCoroutineScope() + + OrderDetailContent( order = order, - onBackClick = onBackClick, + onBack = onBackClick, + onClickOpen = { + coroutineScope.launch { + Logger.info("Opening channel for order ${order.id}") + try { + blocktank.openChannel(orderId = order.id) + Logger.info("Channel opened for order ${order.id}") + } catch (e: Throwable) { + Logger.error("Error opening channel for order ${order.id}", e) + } + } + }, ) } @Composable -private fun OrderDetailView( +private fun OrderDetailContent( order: IBtOrder, - onBackClick: () -> Unit, + onBack: () -> Unit = {}, + onClickOpen: () -> Unit = {}, ) { - val coroutineScope = rememberCoroutineScope() Scaffold( - topBar = { - AppTopBar( - titleText = "Order Details", - onBackClick = onBackClick, - ) - } + topBar = { AppTopBar(titleText = "Order Details", onBackClick = onBack) } ) { padding -> LazyColumn( + contentPadding = PaddingValues(16.dp), modifier = Modifier.padding(padding) ) { - // Order Details item { - Card( - colors = cardColors, - modifier = Modifier - .fillMaxWidth() - .padding(16.dp) - ) { - Column(modifier = Modifier.padding(16.dp)) { - Text(text = "Order Details", style = MaterialTheme.typography.titleMedium) - Spacer(modifier = Modifier.height(8.dp)) - DetailRow("ID", order.id) - DetailRow("Onchain txs", order.payment.onchain.transactions.size.toString()) - DetailRow("State", order.state.toString()) - DetailRow("State 2", order.state2.toString()) - DetailRow("LSP Balance", order.lspBalanceSat.formatToModernDisplay()) - DetailRow("Client Balance", order.clientBalanceSat.formatToModernDisplay()) - DetailRow("Total Fee", order.feeSat.formatToModernDisplay()) - DetailRow("Network Fee", order.networkFeeSat.formatToModernDisplay()) - DetailRow("Service Fee", order.serviceFeeSat.formatToModernDisplay()) - } + InfoCard(header = "Order Details") { + DetailRow("ID", order.id) + DetailRow("Onchain txs", order.payment?.onchain?.transactions?.size?.toString() ?: "0") + DetailRow("State", order.state.toString()) + DetailRow("State 2", order.state2.toString()) + DetailRow("LSP Balance", order.lspBalanceSat.formatToModernDisplay()) + DetailRow("Client Balance", order.clientBalanceSat.formatToModernDisplay()) + DetailRow("Total Fee", order.feeSat.formatToModernDisplay()) + DetailRow("Network Fee", order.networkFeeSat.formatToModernDisplay()) + DetailRow("Service Fee", order.serviceFeeSat.formatToModernDisplay()) } } - - // Channel Settings item { - Card( - colors = cardColors, - modifier = Modifier - .fillMaxWidth() - .padding(16.dp) - ) { - Column(modifier = Modifier.padding(16.dp)) { - Text(text = "Channel Settings", style = MaterialTheme.typography.titleMedium) - Spacer(modifier = Modifier.height(8.dp)) - DetailRow("Zero Conf", if (order.zeroConf) "Yes" else "No") - DetailRow("Zero Reserve", if (order.zeroReserve) "Yes" else "No") - order.clientNodeId?.let { - DetailRow("Client Node ID", it) - } - DetailRow("Expiry Weeks", order.channelExpiryWeeks.toString()) - DetailRow("Channel Expires", order.channelExpiresAt) - DetailRow("Order Expires", order.orderExpiresAt) + InfoCard(header = "Channel Settings") { + DetailRow("Zero Conf", if (order.zeroConf) "Yes" else "No") + DetailRow("Zero Reserve", if (order.zeroReserve) "Yes" else "No") + order.clientNodeId?.let { + DetailRow("Client Node ID", it) } + DetailRow("Expiry Weeks", order.channelExpiryWeeks.toString()) + DetailRow("Channel Expires", order.channelExpiresAt) + DetailRow("Order Expires", order.orderExpiresAt) } } - - // LSP Information item { - Card( - colors = cardColors, - modifier = Modifier - .fillMaxWidth() - .padding(16.dp) - ) { - Column(modifier = Modifier.padding(16.dp)) { - Text(text = "LSP Information", style = MaterialTheme.typography.titleMedium) - Spacer(modifier = Modifier.height(8.dp)) - DetailRow("Alias", order.lspNode.alias) - DetailRow("Node ID", order.lspNode.pubkey) - order.lnurl?.let { - DetailRow("LNURL", it) - } + InfoCard(header = "LSP Info") { + DetailRow("Alias", order.lspNode?.alias.orEmpty()) + DetailRow("Node ID", order.lspNode?.pubkey.orEmpty()) + order.lnurl?.let { + DetailRow("LNURL", it) } } } - - // Discount Section order.couponCode?.let { couponCode -> item { - Card( - colors = cardColors, - modifier = Modifier - .fillMaxWidth() - .padding(16.dp) - ) { - Column(modifier = Modifier.padding(16.dp)) { - Text( - text = "Discount", - style = MaterialTheme.typography.titleMedium - ) - Spacer(modifier = Modifier.height(8.dp)) - DetailRow("Coupon Code", couponCode) - order.discount?.let { discount -> - DetailRow("Discount Type", discount.code) - DetailRow("Value", discount.absoluteSat.formatToModernDisplay()) - } + InfoCard(header = "Discount") { + DetailRow("Coupon Code", couponCode) + order.discount?.let { discount -> + DetailRow("Discount Type", discount.code) + DetailRow("Value", discount.absoluteSat.formatToModernDisplay()) } } } } - - // Timestamps Section item { - Card( - colors = cardColors, - modifier = Modifier - .fillMaxWidth() - .padding(16.dp) - ) { - Column(modifier = Modifier.padding(16.dp)) { - Text(text = "Timestamps", style = MaterialTheme.typography.titleMedium) - Spacer(modifier = Modifier.height(8.dp)) - DetailRow("Created", order.createdAt) - DetailRow("Updated", order.updatedAt) - } + InfoCard(header = "Timestamps") { + DetailRow("Created", order.createdAt) + DetailRow("Updated", order.updatedAt) } } - - // Open Channel Button if (order.state2 == BtOrderState2.PAID) { item { - val blocktank = blocktankViewModel ?: return@item PrimaryButton( text = "Open Channel", - onClick = { - coroutineScope.launch { - Logger.info("Opening channel for order ${order.id}") - try { - blocktank.openChannel(orderId = order.id) - Logger.info("Channel opened for order ${order.id}") - } catch (e: Throwable) { - Logger.error("Error opening channel for order ${order.id}", e) - } - } - }, + onClick = onClickOpen, modifier = Modifier.padding(horizontal = 16.dp) ) } @@ -344,364 +240,262 @@ private fun OrderDetailView( @Composable fun CJitDetailScreen( cjitItem: Routes.CjitDetail, - onBackClick: () -> Unit, + onBackClick: () -> Unit = {}, ) { val blocktank = blocktankViewModel ?: return val cJitEntries by blocktank.cJitEntries.collectAsStateWithLifecycle() val entry = cJitEntries.find { it.id == cjitItem.id } ?: return - CJitDetailView( + CJitDetailContent( entry = entry, - onBackClick = onBackClick, + onBack = onBackClick, ) } @Composable -private fun CJitDetailView( +private fun CJitDetailContent( entry: IcJitEntry, - onBackClick: () -> Unit, + onBack: () -> Unit = {}, ) { Scaffold( - topBar = { - AppTopBar( - titleText = "CJIT Entry Details", - onBackClick = onBackClick, - ) - } + topBar = { AppTopBar(titleText = "CJIT Details", onBackClick = onBack) } ) { padding -> LazyColumn( + contentPadding = PaddingValues(horizontal = 16.dp), modifier = Modifier.padding(padding) ) { - // Entry Details Section item { - Card( - colors = cardColors, - modifier = Modifier - .fillMaxWidth() - .padding(16.dp) - ) { - Column(modifier = Modifier.padding(16.dp)) { - Text(text = "Entry Details", style = MaterialTheme.typography.titleMedium) - Spacer(modifier = Modifier.height(8.dp)) - DetailRow(label = "ID", value = entry.id) - DetailRow(label = "State", value = entry.state.toString()) - DetailRow(label = "Channel Size", value = entry.channelSizeSat.formatToModernDisplay()) - entry.channelOpenError?.let { error -> - DetailRow(label = "Error", value = error, isError = true) - } + InfoCard(header = "CJIT Details") { + DetailRow(label = "ID", value = entry.id) + DetailRow(label = "State", value = entry.state.toString()) + DetailRow(label = "Channel Size", value = entry.channelSizeSat.formatToModernDisplay()) + entry.channelOpenError?.let { error -> + DetailRow(label = "Error", value = error, isError = true) } } } - - // Fees Section item { - Card( - colors = cardColors, - modifier = Modifier - .fillMaxWidth() - .padding(16.dp) - ) { - Column(modifier = Modifier.padding(16.dp)) { - Text(text = "Fees", style = MaterialTheme.typography.titleMedium) - Spacer(modifier = Modifier.height(8.dp)) - DetailRow(label = "Total Fee", value = entry.feeSat.formatToModernDisplay()) - DetailRow(label = "Network Fee", value = entry.networkFeeSat.formatToModernDisplay()) - DetailRow(label = "Service Fee", value = entry.serviceFeeSat.formatToModernDisplay()) - } + InfoCard(header = "Fees") { + DetailRow(label = "Total Fee", value = entry.feeSat.formatToModernDisplay()) + DetailRow(label = "Network Fee", value = entry.networkFeeSat.formatToModernDisplay()) + DetailRow(label = "Service Fee", value = entry.serviceFeeSat.formatToModernDisplay()) } } - - // Channel Settings Section item { - Card( - colors = cardColors, - modifier = Modifier - .fillMaxWidth() - .padding(16.dp) - ) { - Column(modifier = Modifier.padding(16.dp)) { - Text(text = "Channel Settings", style = MaterialTheme.typography.titleMedium) - Spacer(modifier = Modifier.height(8.dp)) - DetailRow(label = "Node ID", value = entry.nodeId) - DetailRow(label = "Expiry Weeks", value = "${entry.channelExpiryWeeks}") - } + InfoCard(header = "Channel Settings") { + DetailRow(label = "Node ID", value = entry.nodeId) + DetailRow(label = "Expiry Weeks", value = "${entry.channelExpiryWeeks}") } } - - // LSP Information Section item { - Card( - colors = cardColors, - modifier = Modifier - .fillMaxWidth() - .padding(16.dp) - ) { - Column(modifier = Modifier.padding(16.dp)) { - Text(text = "LSP Information", style = MaterialTheme.typography.titleMedium) - Spacer(modifier = Modifier.height(8.dp)) - DetailRow(label = "Alias", value = entry.lspNode.alias) - DetailRow(label = "Node ID", value = entry.lspNode.pubkey) - } + InfoCard(header = "LSP Information") { + DetailRow(label = "Alias", value = entry.lspNode.alias) + DetailRow(label = "Node ID", value = entry.lspNode.pubkey) } } - - // Discount Section if (entry.couponCode.isNotEmpty()) { item { - Card( - colors = cardColors, - modifier = Modifier - .fillMaxWidth() - .padding(16.dp) - ) { - Column(modifier = Modifier.padding(16.dp)) { - Text(text = "Discount", style = MaterialTheme.typography.titleMedium) - Spacer(modifier = Modifier.height(8.dp)) - DetailRow(label = "Coupon Code", value = entry.couponCode) - entry.discount?.let { discount -> - DetailRow(label = "Discount Type", value = discount.code) - DetailRow(label = "Value", value = "${discount.absoluteSat}") - } + InfoCard(header = "Discount") { + DetailRow(label = "Coupon Code", value = entry.couponCode) + entry.discount?.let { discount -> + DetailRow(label = "Discount Type", value = discount.code) + DetailRow(label = "Value", value = "${discount.absoluteSat}") } } } } - - // Timestamps Section item { - Card( - colors = cardColors, - modifier = Modifier - .fillMaxWidth() - .padding(16.dp) - ) { - Column(modifier = Modifier.padding(16.dp)) { - Text(text = "Timestamps", style = MaterialTheme.typography.titleMedium) - Spacer(modifier = Modifier.height(8.dp)) - DetailRow(label = "Created", value = entry.createdAt) - DetailRow(label = "Updated", value = entry.updatedAt) - DetailRow(label = "Expires", value = entry.expiresAt) - } + InfoCard(header = "Timestamps") { + DetailRow(label = "Created", value = entry.createdAt) + DetailRow(label = "Updated", value = entry.updatedAt) + DetailRow(label = "Expires", value = entry.expiresAt) } } } } } -// region Helpers - -private val cardColors: CardColors - @Composable get() = CardDefaults.cardColors(containerColor = Colors.White10) +private val cardColors: CardColors @Composable get() = CardDefaults.cardColors(containerColor = Colors.White10) @Composable -private fun CopyableText(text: String) { - val clipboardManager = LocalClipboardManager.current - var isPressed by remember { mutableStateOf(false) } - val scale by animateFloatAsState( - targetValue = if (isPressed) 0.95f else 1f, - animationSpec = spring( - dampingRatio = Spring.DampingRatioMediumBouncy, - stiffness = Spring.StiffnessLow - ), - label = "scale" - ) - val coroutineScope = rememberCoroutineScope() - - Text( - text = text, - fontSize = if (text.length > 20) 10.sp else 12.sp, - fontFamily = FontFamily.Monospace, - maxLines = 1, - overflow = TextOverflow.Ellipsis, - modifier = Modifier - .scale(scale) - .clickableAlpha { - clipboardManager.setText(AnnotatedString(text)) - coroutineScope.launch { - isPressed = true - delay(100) - isPressed = false - } +private fun InfoCard( + header: String, + modifier: Modifier = Modifier, + content: @Composable () -> Unit, +) { + Column(modifier = modifier) { + SectionHeader(header, padding = PaddingValues.Zero) + Card( + colors = cardColors, + modifier = Modifier.fillMaxWidth() + ) { + Column(modifier = Modifier.padding(16.dp)) { + content() } - ) + } + } } @Composable -private fun OrderRow(order: IBtOrder) { - Column( - verticalArrangement = Arrangement.spacedBy(8.dp), +private fun OrderCard(model: IBtOrder, onClick: (String) -> Unit) { + Card( + colors = cardColors, modifier = Modifier .fillMaxWidth() - .padding(8.dp) + .clickable { onClick(model.id) } ) { - Row( - modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.SpaceBetween, - verticalAlignment = Alignment.CenterVertically + Column( + verticalArrangement = Arrangement.spacedBy(16.dp), + modifier = Modifier + .fillMaxWidth() + .padding(16.dp) ) { - CopyableText(text = order.id) - Surface( - color = Colors.White16, - shape = MaterialTheme.shapes.small + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically ) { - Text( - text = order.state2.toString(), - style = MaterialTheme.typography.bodySmall, - modifier = Modifier.padding(4.dp) + CaptionB( + text = model.id, + maxLines = 1, + overflow = TextOverflow.MiddleEllipsis, + modifier = Modifier.clickableAlpha(onClick = copyToClipboard(model.id)) ) + Surface(color = Colors.White16, shape = AppShapes.small) { + Footnote( + text = model.state2.toString(), + color = Colors.White64, + maxLines = 1, + modifier = Modifier.padding(4.dp) + ) + } } - } - Row( - horizontalArrangement = Arrangement.SpaceBetween, - modifier = Modifier.fillMaxWidth(), - ) { - InfoCell(label = "LSP Balance", value = order.lspBalanceSat.formatToModernDisplay()) - InfoCell( - label = "Client Balance", - value = order.clientBalanceSat.formatToModernDisplay(), - alignment = Alignment.End - ) - } + Row( + horizontalArrangement = Arrangement.SpaceBetween, + modifier = Modifier.fillMaxWidth(), + ) { + InfoCell(label = "LSP Balance", value = model.lspBalanceSat.formatToModernDisplay()) + InfoCell( + label = "Client Balance", + value = model.clientBalanceSat.formatToModernDisplay(), + alignment = Alignment.End + ) + } - Row( - horizontalArrangement = Arrangement.SpaceBetween, - modifier = Modifier.fillMaxWidth(), - ) { - InfoCell(label = "Fees", value = order.feeSat.formatToModernDisplay()) - InfoCell( - label = "Expires", - value = order.channelExpiresAt.take(10), - alignment = Alignment.End - ) + Row( + horizontalArrangement = Arrangement.SpaceBetween, + modifier = Modifier.fillMaxWidth(), + ) { + InfoCell(label = "Fees", value = model.feeSat.formatToModernDisplay()) + InfoCell( + label = "Expires", + value = model.channelExpiresAt.take(10), + alignment = Alignment.End + ) + } } } } @Composable -private fun CJitRow(entry: IcJitEntry) { - Column( +private fun CJitCard(model: IcJitEntry, onClick: (String) -> Unit) { + Card( + colors = cardColors, modifier = Modifier .fillMaxWidth() - .padding(8.dp), - verticalArrangement = Arrangement.spacedBy(8.dp) + .clickable { onClick(model.id) } ) { - Row( - modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.SpaceBetween, - verticalAlignment = Alignment.CenterVertically + Column( + verticalArrangement = Arrangement.spacedBy(16.dp), + modifier = Modifier + .fillMaxWidth() + .padding(16.dp) ) { - CopyableText(text = entry.id) - Surface( - color = Colors.White16, - shape = MaterialTheme.shapes.small + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically ) { - Text( - text = entry.state.toString(), - style = MaterialTheme.typography.bodySmall, - modifier = Modifier.padding(4.dp) + CaptionB( + text = model.id, + maxLines = 1, + overflow = TextOverflow.MiddleEllipsis, + modifier = Modifier.clickableAlpha(onClick = copyToClipboard(model.id)) ) + Surface(color = Colors.White16, shape = MaterialTheme.shapes.small) { + Footnote( + text = model.state.toString(), + color = Colors.White64, + maxLines = 1, + modifier = Modifier.padding(4.dp) + ) + } } - } - Row( - modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.SpaceBetween - ) { - InfoCell(label = "Channel Size", value = "${entry.channelSizeSat.formatToModernDisplay()} sats") - InfoCell( - label = "Fees", - value = "${entry.feeSat.formatToModernDisplay()} sats", - alignment = Alignment.End - ) - } + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween + ) { + InfoCell(label = "Channel Size", value = "${model.channelSizeSat.formatToModernDisplay()} sats") + InfoCell( + label = "Fees", + value = "${model.feeSat.formatToModernDisplay()} sats", + alignment = Alignment.End + ) + } - entry.channelOpenError?.let { error -> - Text( - text = error, - fontSize = if (error.length > 50) 10.sp else 12.sp, - color = MaterialTheme.colorScheme.error, - maxLines = 2, - overflow = TextOverflow.Ellipsis - ) + Column { + model.channelOpenError?.let { error -> + Caption( + text = error, + color = MaterialTheme.colorScheme.error, + maxLines = 2, + overflow = TextOverflow.Ellipsis, + ) + } + VerticalSpacer(4.dp) + Footnote(text = "Expires: ${model.expiresAt.take(10)}", color = Colors.White32) + } } - - Text( - text = "Expires: ${entry.expiresAt.take(10)}", - style = MaterialTheme.typography.bodySmall, - color = Color.Gray - ) } } @Composable private fun InfoCell(label: String, value: String, alignment: Alignment.Horizontal = Alignment.Start) { Column(horizontalAlignment = alignment) { - Text( - text = label, - style = MaterialTheme.typography.bodySmall, - color = Color.Gray - ) - Text( - text = value, - style = MaterialTheme.typography.bodyMedium - ) + Caption13Up(text = label, color = Colors.White64) + VerticalSpacer(4.dp) + BodySSB(text = value) } } @Composable private fun DetailRow(label: String, value: String, isError: Boolean = false) { - val clipboardManager = LocalClipboardManager.current - var isPressed by remember { mutableStateOf(false) } - val scale by animateFloatAsState( - targetValue = if (isPressed) 0.95f else 1f, - animationSpec = spring( - dampingRatio = Spring.DampingRatioMediumBouncy, - stiffness = Spring.StiffnessLow - ), - label = "scale" - ) - val coroutineScope = rememberCoroutineScope() - - val fontSize = when { - value.length > 40 -> 11.sp - value.length > 30 -> 12.sp - else -> 13.sp - } - Row( horizontalArrangement = Arrangement.SpaceBetween, verticalAlignment = Alignment.CenterVertically, modifier = Modifier .fillMaxWidth() - .padding(vertical = 6.dp), + .padding(vertical = 6.dp) ) { - BodyS( + Caption( text = label, color = Colors.White64, + overflow = TextOverflow.MiddleEllipsis, + maxLines = 1, ) - Text( + HorizontalSpacer(16.dp) + Caption( text = value, - fontSize = fontSize, color = if (isError) MaterialTheme.colorScheme.error else MaterialTheme.colorScheme.onSurface, textAlign = TextAlign.End, - modifier = Modifier - .scale(scale) - .clickableAlpha { - clipboardManager.setText(AnnotatedString(value)) - coroutineScope.launch { - isPressed = true - delay(100) - isPressed = false - } - } + overflow = TextOverflow.MiddleEllipsis, + maxLines = 1, + modifier = Modifier.clickableAlpha(onClick = copyToClipboard(value)) ) } } -// endregion - -// region Preview - @Suppress("SpellCheckingInspection") private val order = IBtOrder( id = "order-3c564573-ec4b-b502-5e6fe930435f", @@ -806,38 +600,42 @@ private val cjitEntry = IcJitEntry( @Preview(showSystemUi = true) @Composable -private fun ChannelOrdersViewPreview() { +private fun Preview() { AppThemeSurface { - ChannelOrdersView( - orders = mutableListOf(order), - cJitEntries = mutableListOf(cjitEntry), - onBackClick = { }, - onOrderItemClick = { }, - onCjitItemClick = { }, + Content( + orders = listOf(order), + cJitEntries = listOf(cjitEntry), ) } } @Preview(showSystemUi = true) @Composable -private fun OrderDetailViewPreview() { +private fun PreviewEmpty() { AppThemeSurface { - OrderDetailView( + Content( + orders = emptyList(), + cJitEntries = emptyList(), + ) + } +} + +@Preview(showSystemUi = true) +@Composable +private fun PreviewOrderDetail() { + AppThemeSurface { + OrderDetailContent( order = order, - onBackClick = { }, ) } } @Preview(showSystemUi = true) @Composable -private fun CJitDetailViewPreview() { +private fun PreviewCJitDetail() { AppThemeSurface { - CJitDetailView( + CJitDetailContent( entry = cjitEntry, - onBackClick = { }, ) } } - -// endregion diff --git a/app/src/main/java/to/bitkit/ui/settings/backgroundPayments/BackgroundPaymentsSettings.kt b/app/src/main/java/to/bitkit/ui/settings/backgroundPayments/BackgroundPaymentsSettings.kt index fa5b82d101..2ebd7aa59d 100644 --- a/app/src/main/java/to/bitkit/ui/settings/backgroundPayments/BackgroundPaymentsSettings.kt +++ b/app/src/main/java/to/bitkit/ui/settings/backgroundPayments/BackgroundPaymentsSettings.kt @@ -94,6 +94,7 @@ private fun Content( ) if (hasPermission) { + @Suppress("MaxLineLength") // TODO transifex BodyM( text = "Background payments are enabled. You can receive funds even when the app is closed (if your device is connected to the internet).", color = Colors.White64, diff --git a/app/src/main/java/to/bitkit/ui/settings/general/GeneralSettingsScreen.kt b/app/src/main/java/to/bitkit/ui/settings/general/GeneralSettingsScreen.kt index dda09b0731..f4a002cd49 100644 --- a/app/src/main/java/to/bitkit/ui/settings/general/GeneralSettingsScreen.kt +++ b/app/src/main/java/to/bitkit/ui/settings/general/GeneralSettingsScreen.kt @@ -87,8 +87,8 @@ private fun GeneralSettingsContent( primaryDisplay: PrimaryDisplay, defaultTransactionSpeed: TransactionSpeed, selectedLanguage: String, - showTagsButton: Boolean = false, notificationsGranted: Boolean, + showTagsButton: Boolean = false, onBackClick: () -> Unit = {}, onCloseClick: () -> Unit = {}, onLocalCurrencyClick: () -> Unit = {}, @@ -175,9 +175,8 @@ private fun Preview() { selectedCurrency = "USD", primaryDisplay = PrimaryDisplay.BITCOIN, defaultTransactionSpeed = TransactionSpeed.Medium, - showTagsButton = true, selectedLanguage = Language.SYSTEM_DEFAULT.displayName, - notificationsGranted = true + notificationsGranted = true, ) } } diff --git a/app/src/main/java/to/bitkit/ui/settings/lightning/ChannelDetailScreen.kt b/app/src/main/java/to/bitkit/ui/settings/lightning/ChannelDetailScreen.kt index cde26d5a21..47b01bd4cd 100644 --- a/app/src/main/java/to/bitkit/ui/settings/lightning/ChannelDetailScreen.kt +++ b/app/src/main/java/to/bitkit/ui/settings/lightning/ChannelDetailScreen.kt @@ -530,8 +530,8 @@ private fun getChannelStatus( blocktankOrder?.let { order -> when { order.state2 == BtOrderState2.EXPIRED || - order.payment.state2 == BtPaymentState2.CANCELED || - order.payment.state2 == BtPaymentState2.REFUNDED -> { + order.payment?.state2 == BtPaymentState2.CANCELED || + order.payment?.state2 == BtPaymentState2.REFUNDED -> { return ChannelStatusUi.CLOSED } diff --git a/app/src/main/java/to/bitkit/ui/settings/lightning/LightningConnectionsViewModel.kt b/app/src/main/java/to/bitkit/ui/settings/lightning/LightningConnectionsViewModel.kt index dde193e759..97b2caaaa1 100644 --- a/app/src/main/java/to/bitkit/ui/settings/lightning/LightningConnectionsViewModel.kt +++ b/app/src/main/java/to/bitkit/ui/settings/lightning/LightningConnectionsViewModel.kt @@ -229,7 +229,7 @@ class LightningConnectionsViewModel @Inject constructor( createChannelDetails().copy( channelId = order.id, - counterpartyNodeId = order.lspNode.pubkey, + counterpartyNodeId = order.lspNode?.pubkey.orEmpty(), fundingTxo = order.channel?.fundingTx?.let { OutPoint(txid = it.id, vout = it.vout.toUInt()) }, channelValueSats = order.clientBalanceSat + order.lspBalanceSat, outboundCapacityMsat = order.clientBalanceSat * 1000u, @@ -246,7 +246,7 @@ class LightningConnectionsViewModel @Inject constructor( createChannelDetails().copy( channelId = order.id, - counterpartyNodeId = order.lspNode.pubkey, + counterpartyNodeId = order.lspNode?.pubkey.orEmpty(), fundingTxo = order.channel?.fundingTx?.let { OutPoint(txid = it.id, vout = it.vout.toUInt()) }, channelValueSats = order.clientBalanceSat + order.lspBalanceSat, outboundCapacityMsat = order.clientBalanceSat * 1000u, diff --git a/app/src/main/java/to/bitkit/ui/settings/lightning/components/ChannelStatusView.kt b/app/src/main/java/to/bitkit/ui/settings/lightning/components/ChannelStatusView.kt index 6b15420b47..900bac9266 100644 --- a/app/src/main/java/to/bitkit/ui/settings/lightning/components/ChannelStatusView.kt +++ b/app/src/main/java/to/bitkit/ui/settings/lightning/components/ChannelStatusView.kt @@ -59,6 +59,7 @@ fun ChannelStatusView( } } +@Suppress("CyclomaticComplexMethod") @Composable private fun getStatusInfo( channel: ChannelUi, @@ -122,7 +123,7 @@ private fun getStatusInfo( ) } - when (order.payment.state2) { + when (order.payment?.state2) { BtPaymentState2.CANCELED -> { return StatusInfo( iconRes = R.drawable.ic_x, @@ -172,6 +173,8 @@ private fun getStatusInfo( statusColor = Colors.Purple ) } + + null -> Unit } } @@ -279,7 +282,7 @@ private fun PreviewPaymentCanceled() { details = createChannelDetails(), ), blocktankOrder = mockOrder().copy( - payment = mockOrder().payment.copy( + payment = mockOrder().payment?.copy( state2 = BtPaymentState2.CANCELED, ), ), @@ -297,7 +300,7 @@ private fun PreviewRefundAvailable() { details = createChannelDetails(), ), blocktankOrder = mockOrder().copy( - payment = mockOrder().payment.copy( + payment = mockOrder().payment?.copy( state2 = BtPaymentState2.REFUND_AVAILABLE, ), ), @@ -315,7 +318,7 @@ private fun PreviewRefunded() { details = createChannelDetails(), ), blocktankOrder = mockOrder().copy( - payment = mockOrder().payment.copy( + payment = mockOrder().payment?.copy( state2 = BtPaymentState2.REFUNDED, ), ), @@ -347,7 +350,7 @@ private fun PreviewPaymentPaid() { details = createChannelDetails(), ), blocktankOrder = mockOrder().copy( - payment = mockOrder().payment.copy( + payment = mockOrder().payment?.copy( state2 = BtPaymentState2.PAID, ), ), diff --git a/app/src/main/java/to/bitkit/ui/sheets/BackupSheet.kt b/app/src/main/java/to/bitkit/ui/sheets/BackupSheet.kt index 8373d4cd61..bc9bd5cee2 100644 --- a/app/src/main/java/to/bitkit/ui/sheets/BackupSheet.kt +++ b/app/src/main/java/to/bitkit/ui/sheets/BackupSheet.kt @@ -39,7 +39,7 @@ fun BackupSheet( ) { val navController = rememberNavController() val uiState by viewModel.uiState.collectAsStateWithLifecycle() - val onDismissSheet by rememberUpdatedState { onDismiss } + val currentOnDismiss by rememberUpdatedState(onDismiss) LaunchedEffect(Unit) { viewModel.loadMnemonicData() @@ -70,7 +70,7 @@ fun BackupSheet( ) BackupContract.SideEffect.NavigateToMetadata -> navController.navigate(BackupRoute.Metadata) - BackupContract.SideEffect.DismissSheet -> onDismissSheet() + BackupContract.SideEffect.DismissSheet -> currentOnDismiss() } } } @@ -88,7 +88,7 @@ fun BackupSheet( composableWithDefaultTransitions { BackupIntroScreen( hasFunds = LocalBalances.current.totalSats > 0u, - onClose = onDismissSheet(), + onClose = currentOnDismiss, onConfirm = { navController.navigate(BackupRoute.ShowMnemonic) }, ) } diff --git a/app/src/main/java/to/bitkit/ui/utils/Clipboard.kt b/app/src/main/java/to/bitkit/ui/utils/Clipboard.kt index fea2a23968..440f552240 100644 --- a/app/src/main/java/to/bitkit/ui/utils/Clipboard.kt +++ b/app/src/main/java/to/bitkit/ui/utils/Clipboard.kt @@ -15,7 +15,7 @@ import to.bitkit.R fun copyToClipboard( text: String, label: String = stringResource(R.string.app_name), - block: (() -> Unit)? = null, + callback: ((String) -> Unit)? = null, ): () -> Unit { val clipboard = LocalClipboard.current val haptic = LocalHapticFeedback.current @@ -26,7 +26,7 @@ fun copyToClipboard( val clipData = ClipData.newPlainText(label, text) clipboard.setClipEntry(ClipEntry(clipData)) haptic.performHapticFeedback(HapticFeedbackType.Confirm) - block?.invoke() + callback?.invoke(text) } } } diff --git a/app/src/main/java/to/bitkit/ui/utils/RequestNotificationPermissions.kt b/app/src/main/java/to/bitkit/ui/utils/RequestNotificationPermissions.kt index 21c815ec4c..490ff9d03d 100644 --- a/app/src/main/java/to/bitkit/ui/utils/RequestNotificationPermissions.kt +++ b/app/src/main/java/to/bitkit/ui/utils/RequestNotificationPermissions.kt @@ -10,10 +10,12 @@ import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberUpdatedState import androidx.compose.runtime.setValue import androidx.compose.ui.platform.LocalContext import androidx.lifecycle.Lifecycle import androidx.lifecycle.LifecycleEventObserver +import androidx.lifecycle.compose.LocalLifecycleOwner @Composable fun RequestNotificationPermissions( @@ -21,7 +23,8 @@ fun RequestNotificationPermissions( showPermissionDialog: Boolean = true, ) { val context = LocalContext.current - val lifecycleOwner = androidx.lifecycle.compose.LocalLifecycleOwner.current + val lifecycleOwner = LocalLifecycleOwner.current + val currentOnPermissionChange by rememberUpdatedState(onPermissionChange) // Check if permission is required (Android 13+) val requiresPermission = Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU @@ -38,6 +41,17 @@ fun RequestNotificationPermissions( onPermissionChange(granted) } + // Request permission on first composition if needed + LaunchedEffect(Unit) { + val currentPermissionState = NotificationUtils.areNotificationsEnabled(context) + isGranted = currentPermissionState + currentOnPermissionChange(currentPermissionState) + + if (!currentPermissionState && requiresPermission && showPermissionDialog) { + launcher.launch(Manifest.permission.POST_NOTIFICATIONS) + } + } + // Monitor lifecycle to check permission when returning from settings DisposableEffect(lifecycleOwner) { val observer = LifecycleEventObserver { _, event -> @@ -45,7 +59,7 @@ fun RequestNotificationPermissions( val currentPermissionState = NotificationUtils.areNotificationsEnabled(context) if (currentPermissionState != isGranted) { isGranted = currentPermissionState - onPermissionChange(currentPermissionState) + currentOnPermissionChange(currentPermissionState) } } } @@ -56,15 +70,4 @@ fun RequestNotificationPermissions( lifecycleOwner.lifecycle.removeObserver(observer) } } - - // Request permission on first composition if needed - LaunchedEffect(Unit) { - val currentPermissionState = NotificationUtils.areNotificationsEnabled(context) - isGranted = currentPermissionState - onPermissionChange(currentPermissionState) - - if (!currentPermissionState && requiresPermission && showPermissionDialog) { - launcher.launch(Manifest.permission.POST_NOTIFICATIONS) - } - } } diff --git a/app/src/main/java/to/bitkit/usecases/DeriveBalanceStateUseCase.kt b/app/src/main/java/to/bitkit/usecases/DeriveBalanceStateUseCase.kt index 5d4a7270d5..a239c3ca70 100644 --- a/app/src/main/java/to/bitkit/usecases/DeriveBalanceStateUseCase.kt +++ b/app/src/main/java/to/bitkit/usecases/DeriveBalanceStateUseCase.kt @@ -1,6 +1,7 @@ package to.bitkit.usecases import kotlinx.coroutines.flow.first +import org.lightningdevkit.ldknode.BalanceDetails import org.lightningdevkit.ldknode.ChannelDetails import to.bitkit.data.SettingsStore import to.bitkit.data.entities.TransferEntity @@ -8,7 +9,6 @@ import to.bitkit.ext.amountSats import to.bitkit.ext.channelId import to.bitkit.ext.minusOrZero import to.bitkit.ext.totalNextOutboundHtlcLimitSats -import to.bitkit.models.BalanceDetails import to.bitkit.models.BalanceState import to.bitkit.repositories.LightningRepo import to.bitkit.repositories.TransferRepo diff --git a/app/src/main/java/to/bitkit/viewmodels/TransferViewModel.kt b/app/src/main/java/to/bitkit/viewmodels/TransferViewModel.kt index cef8ab5dd0..6d88e87c4a 100644 --- a/app/src/main/java/to/bitkit/viewmodels/TransferViewModel.kt +++ b/app/src/main/java/to/bitkit/viewmodels/TransferViewModel.kt @@ -206,7 +206,7 @@ class TransferViewModel @Inject constructor( viewModelScope.launch { lightningRepo .sendOnChain( - address = order.payment.onchain.address, + address = order.payment?.onchain?.address.orEmpty(), sats = order.feeSat, speed = speed, isTransfer = true, diff --git a/app/src/main/java/to/bitkit/viewmodels/WalletViewModel.kt b/app/src/main/java/to/bitkit/viewmodels/WalletViewModel.kt index 0a6cff1ca5..d72543a46e 100644 --- a/app/src/main/java/to/bitkit/viewmodels/WalletViewModel.kt +++ b/app/src/main/java/to/bitkit/viewmodels/WalletViewModel.kt @@ -19,9 +19,9 @@ import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch import org.lightningdevkit.ldknode.ChannelDetails import org.lightningdevkit.ldknode.NodeStatus +import org.lightningdevkit.ldknode.PeerDetails import to.bitkit.data.SettingsStore import to.bitkit.di.BgDispatcher -import to.bitkit.models.LnPeer import to.bitkit.models.NodeLifecycleState import to.bitkit.models.Toast import to.bitkit.repositories.BackupRepo @@ -181,7 +181,7 @@ class WalletViewModel @Inject constructor( } } - fun disconnectPeer(peer: LnPeer) { + fun disconnectPeer(peer: PeerDetails) { viewModelScope.launch { lightningRepo.disconnectPeer(peer) .onSuccess { @@ -309,7 +309,7 @@ data class MainUiState( val bip21: String = "", val nodeStatus: NodeStatus? = null, val nodeLifecycleState: NodeLifecycleState = NodeLifecycleState.Stopped, - val peers: List = emptyList(), + val peers: List = emptyList(), val channels: List = emptyList(), val isRefreshing: Boolean = false, val receiveOnSpendingBalance: Boolean = true, diff --git a/app/src/test/java/to/bitkit/repositories/LightningRepoTest.kt b/app/src/test/java/to/bitkit/repositories/LightningRepoTest.kt index 092882baea..4d31d2e002 100644 --- a/app/src/test/java/to/bitkit/repositories/LightningRepoTest.kt +++ b/app/src/test/java/to/bitkit/repositories/LightningRepoTest.kt @@ -9,6 +9,7 @@ import org.junit.Test import org.lightningdevkit.ldknode.ChannelDetails import org.lightningdevkit.ldknode.NodeStatus import org.lightningdevkit.ldknode.PaymentDetails +import org.lightningdevkit.ldknode.PeerDetails import org.lightningdevkit.ldknode.SpendableUtxo import org.mockito.kotlin.any import org.mockito.kotlin.anyOrNull @@ -28,9 +29,9 @@ import to.bitkit.data.SettingsData import to.bitkit.data.SettingsStore import to.bitkit.data.keychain.Keychain import to.bitkit.ext.createChannelDetails +import to.bitkit.ext.from import to.bitkit.models.BalanceState import to.bitkit.models.CoinSelectionPreference -import to.bitkit.models.LnPeer import to.bitkit.models.NodeLifecycleState import to.bitkit.models.OpenChannelResult import to.bitkit.models.TransactionMetadata @@ -200,7 +201,7 @@ class LightningRepoTest : BaseUnitTest() { @Test fun `openChannel should fail when node is not running`() = test { - val testPeer = LnPeer("nodeId", "host", "9735") + val testPeer = PeerDetails.from("nodeId", "host", "9735") val result = sut.openChannel(testPeer, 100000uL) assertTrue(result.isFailure) } @@ -208,7 +209,7 @@ class LightningRepoTest : BaseUnitTest() { @Test fun `openChannel should succeed when node is running`() = test { startNodeForTesting() - val peer = LnPeer("nodeId", "host", "9735") + val peer = PeerDetails.from("nodeId", "host", "9735") val userChannelId = "testChannelId" val channelAmountSats = 100_000uL whenever(lightningService.openChannel(peer, channelAmountSats, null, null)) @@ -279,7 +280,7 @@ class LightningRepoTest : BaseUnitTest() { startNodeForTesting() val testNodeId = "test_node_id" val testStatus = mock() - val testPeers = listOf(mock()) + val testPeers = listOf(mock()) val testChannels = listOf(mock()) whenever(lightningService.nodeId).thenReturn(testNodeId) @@ -344,7 +345,7 @@ class LightningRepoTest : BaseUnitTest() { @Test fun `disconnectPeer should fail when node is not running`() = test { - val testPeer = LnPeer("nodeId", "host", "9735") + val testPeer = PeerDetails.from("nodeId", "host", "9735") val result = sut.disconnectPeer(testPeer) assertTrue(result.isFailure) } @@ -352,7 +353,7 @@ class LightningRepoTest : BaseUnitTest() { @Test fun `disconnectPeer should succeed when node is running`() = test { startNodeForTesting() - val testPeer = LnPeer("nodeId", "host", "9735") + val testPeer = PeerDetails.from("nodeId", "host", "9735") whenever(lightningService.disconnectPeer(any())).thenReturn(Unit) val result = sut.disconnectPeer(testPeer) diff --git a/app/src/test/java/to/bitkit/repositories/TransferRepoTest.kt b/app/src/test/java/to/bitkit/repositories/TransferRepoTest.kt index 4d3c4fbd0a..1a945c1c68 100644 --- a/app/src/test/java/to/bitkit/repositories/TransferRepoTest.kt +++ b/app/src/test/java/to/bitkit/repositories/TransferRepoTest.kt @@ -9,7 +9,9 @@ import kotlinx.coroutines.flow.flowOf import kotlinx.datetime.Clock import org.junit.Before import org.junit.Test +import org.lightningdevkit.ldknode.BalanceDetails import org.lightningdevkit.ldknode.ChannelDetails +import org.lightningdevkit.ldknode.LightningBalance import org.lightningdevkit.ldknode.OutPoint import org.mockito.kotlin.any import org.mockito.kotlin.doReturn @@ -22,8 +24,6 @@ import org.mockito.kotlin.wheneverBlocking import to.bitkit.data.dao.TransferDao import to.bitkit.data.entities.TransferEntity import to.bitkit.ext.createChannelDetails -import to.bitkit.models.BalanceDetails -import to.bitkit.models.LightningBalance import to.bitkit.models.TransferType import to.bitkit.test.BaseUnitTest import kotlin.test.assertEquals diff --git a/app/src/test/java/to/bitkit/ui/WalletViewModelTest.kt b/app/src/test/java/to/bitkit/ui/WalletViewModelTest.kt index 790bf24d80..2a92cc8644 100644 --- a/app/src/test/java/to/bitkit/ui/WalletViewModelTest.kt +++ b/app/src/test/java/to/bitkit/ui/WalletViewModelTest.kt @@ -6,6 +6,7 @@ import kotlinx.coroutines.test.advanceUntilIdle import org.junit.Assert.assertEquals import org.junit.Before import org.junit.Test +import org.lightningdevkit.ldknode.PeerDetails import org.mockito.kotlin.any import org.mockito.kotlin.anyOrNull import org.mockito.kotlin.mock @@ -13,7 +14,7 @@ import org.mockito.kotlin.never import org.mockito.kotlin.verify import org.mockito.kotlin.whenever import to.bitkit.data.SettingsStore -import to.bitkit.models.LnPeer +import to.bitkit.ext.from import to.bitkit.repositories.BackupRepo import to.bitkit.repositories.BlocktankRepo import to.bitkit.repositories.LightningRepo @@ -82,7 +83,7 @@ class WalletViewModelTest : BaseUnitTest() { @Test fun `disconnectPeer should call lightningRepo disconnectPeer and send success toast`() = test { - val testPeer = LnPeer("nodeId", "host", "9735") + val testPeer = PeerDetails.from("nodeId", "host", "9735") whenever(lightningRepo.disconnectPeer(testPeer)).thenReturn(Result.success(Unit)) sut.disconnectPeer(testPeer) @@ -93,7 +94,7 @@ class WalletViewModelTest : BaseUnitTest() { @Test fun `disconnectPeer should call lightningRepo disconnectPeer and send failure toast`() = test { - val testPeer = LnPeer("nodeId", "host", "9735") + val testPeer = PeerDetails.from("nodeId", "host", "9735") val testError = Exception("Test error") whenever(lightningRepo.disconnectPeer(testPeer)).thenReturn(Result.failure(testError)) diff --git a/app/src/test/java/to/bitkit/usecases/DeriveBalanceStateUseCaseTest.kt b/app/src/test/java/to/bitkit/usecases/DeriveBalanceStateUseCaseTest.kt index e1525d61fa..b9a7e48c30 100644 --- a/app/src/test/java/to/bitkit/usecases/DeriveBalanceStateUseCaseTest.kt +++ b/app/src/test/java/to/bitkit/usecases/DeriveBalanceStateUseCaseTest.kt @@ -4,8 +4,10 @@ import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.flowOf import org.junit.Before import org.junit.Test +import org.lightningdevkit.ldknode.BalanceDetails import org.lightningdevkit.ldknode.BalanceSource import org.lightningdevkit.ldknode.ChannelDetails +import org.lightningdevkit.ldknode.LightningBalance import org.mockito.kotlin.any import org.mockito.kotlin.anyOrNull import org.mockito.kotlin.doReturn @@ -15,8 +17,6 @@ import org.mockito.kotlin.wheneverBlocking import to.bitkit.data.SettingsData import to.bitkit.data.SettingsStore import to.bitkit.data.entities.TransferEntity -import to.bitkit.models.BalanceDetails -import to.bitkit.models.LightningBalance import to.bitkit.models.TransferType import to.bitkit.repositories.LightningRepo import to.bitkit.repositories.LightningState diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 274e2d6e47..c2bfef852b 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -44,7 +44,7 @@ activity-compose = { module = "androidx.activity:activity-compose", version.ref appcompat = { module = "androidx.appcompat:appcompat", version.ref = "appcompat" } barcode-scanning = { module = "com.google.mlkit:barcode-scanning", version.ref = "barcodeScanning" } biometric = { module = "androidx.biometric:biometric", version.ref = "biometric" } -bitkitcore = { module = "com.synonym:bitkit-core-android", version = "0.1.10" } +bitkitcore = { module = "com.synonym:bitkit-core-android", version = "0.1.18" } bouncycastle-provider-jdk = { module = "org.bouncycastle:bcprov-jdk18on", version.ref = "bouncyCastle" } camera-camera2 = { module = "androidx.camera:camera-camera2", version.ref = "camera" } camera-lifecycle = { module = "androidx.camera:camera-lifecycle", version.ref = "camera" } @@ -82,7 +82,8 @@ 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.3" } # fork +#ldk-node-android = { module = "org.lightningdevkit:ldk-node-android", version = "0.6.2-rc.4" } # local +ldk-node-android = { module = "com.github.synonymdev:ldk-node", version = "v0.6.2-rc.4" } # 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" }