From 7cba0396c6abf59870678fcdc1613ff36c9e0659 Mon Sep 17 00:00:00 2001 From: Ovi Trif Date: Thu, 22 Aug 2024 23:04:15 +0200 Subject: [PATCH 01/27] refactor: Cleanup debug Manifest --- app/src/debug/AndroidManifest.xml | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/app/src/debug/AndroidManifest.xml b/app/src/debug/AndroidManifest.xml index 12d610c5d..15613a8d7 100644 --- a/app/src/debug/AndroidManifest.xml +++ b/app/src/debug/AndroidManifest.xml @@ -1,12 +1,8 @@ - + - \ No newline at end of file + tools:ignore="MissingApplicationIcon" /> + From 62f36cc5ef41579268db0d252a534fc121e82796 Mon Sep 17 00:00:00 2001 From: Ovi Trif Date: Mon, 2 Sep 2024 08:08:51 +0200 Subject: [PATCH 02/27] wip: Scaffold UI state --- app/src/main/java/to/bitkit/Constants.kt | 2 +- .../main/java/to/bitkit/ui/MainViewModel.kt | 25 ++++++++++++++++++- 2 files changed, 25 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/to/bitkit/Constants.kt b/app/src/main/java/to/bitkit/Constants.kt index 88fc03663..62974973f 100644 --- a/app/src/main/java/to/bitkit/Constants.kt +++ b/app/src/main/java/to/bitkit/Constants.kt @@ -28,7 +28,7 @@ internal const val SEED = "universe more push obey later jazz huge buzz magnet t internal val PEER_REMOTE = LnPeer( nodeId = "033f4d3032ce7f54224f4bd9747b50b7cd72074a859758e40e1ca46ffa79a34324", host = HOST, - port = "9736", + port = "9737", ) internal val PEER = LnPeer( diff --git a/app/src/main/java/to/bitkit/ui/MainViewModel.kt b/app/src/main/java/to/bitkit/ui/MainViewModel.kt index 30fb887e3..eaccdfffc 100644 --- a/app/src/main/java/to/bitkit/ui/MainViewModel.kt +++ b/app/src/main/java/to/bitkit/ui/MainViewModel.kt @@ -9,6 +9,9 @@ import com.google.firebase.messaging.FirebaseMessaging import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.launch import kotlinx.coroutines.tasks.await import org.lightningdevkit.ldknode.ChannelDetails @@ -43,12 +46,14 @@ class MainViewModel @Inject constructor( val btcAddress = mutableStateOf("Loading…") val btcBalance = mutableStateOf("Loading…") val mnemonic = mutableStateOf(SEED) - val peers = mutableStateListOf() val channels = mutableStateListOf() private val node = lightningService.node + val _uiState = MutableStateFlow(MainUiState.Loading) + val uiState = _uiState.asStateFlow() + init { viewModelScope.launch { sync() @@ -161,3 +166,21 @@ class MainViewModel @Inject constructor( fun MainViewModel.togglePeerConnection(peer: LnPeer) = if (peer.isConnected) disconnectPeer(peer.nodeId) else connectPeer(peer) + +sealed class MainUiState { + + data object Loading: MainUiState() + data class Content( + val ldkNodeId: String, + val ldkBalance: String, + val btcAddress: String, + val btcBalance: String, + val mnemonic: String, + val peers: List, + val channels: List, + ) : MainUiState() + data class Error( + val title: String, + val message: String, + ) : MainUiState() +} From 6794a27e245516abf5b2dc023837e43c24bd6408 Mon Sep 17 00:00:00 2001 From: Ovi Trif Date: Mon, 2 Sep 2024 08:20:24 +0200 Subject: [PATCH 03/27] refactor: Rename to WalletViewModel --- .../main/java/to/bitkit/di/ViewModelModule.kt | 9 ++++ .../main/java/to/bitkit/ui/MainActivity.kt | 17 ++++--- app/src/main/java/to/bitkit/ui/Nav.kt | 2 +- .../main/java/to/bitkit/ui/SettingsScreen.kt | 2 +- .../main/java/to/bitkit/ui/SharedViewModel.kt | 44 ++++++++++++++++--- .../main/java/to/bitkit/ui/WalletScreen.kt | 3 +- .../{MainViewModel.kt => WalletViewModel.kt} | 39 +--------------- .../to/bitkit/ui/settings/ChannelsScreen.kt | 4 +- .../to/bitkit/ui/settings/PaymentsScreen.kt | 4 +- .../java/to/bitkit/ui/settings/PeersScreen.kt | 4 +- 10 files changed, 67 insertions(+), 61 deletions(-) rename app/src/main/java/to/bitkit/ui/{MainViewModel.kt => WalletViewModel.kt} (81%) diff --git a/app/src/main/java/to/bitkit/di/ViewModelModule.kt b/app/src/main/java/to/bitkit/di/ViewModelModule.kt index 4b5931975..45433b5b6 100644 --- a/app/src/main/java/to/bitkit/di/ViewModelModule.kt +++ b/app/src/main/java/to/bitkit/di/ViewModelModule.kt @@ -5,6 +5,9 @@ import dagger.Provides import dagger.hilt.InstallIn import dagger.hilt.components.SingletonComponent import kotlinx.coroutines.CoroutineDispatcher +import to.bitkit.data.AppDb +import to.bitkit.data.keychain.KeychainStore +import to.bitkit.services.BitcoinService import to.bitkit.services.BlocktankService import to.bitkit.ui.SharedViewModel import javax.inject.Singleton @@ -16,11 +19,17 @@ object ViewModelModule { @Provides fun provideSharedViewModel( @BgDispatcher bgDispatcher: CoroutineDispatcher, + appDb: AppDb, + keychainStore: KeychainStore, blocktankService: BlocktankService, + bitcoinService: BitcoinService, ): SharedViewModel { return SharedViewModel( bgDispatcher, + appDb, + keychainStore, blocktankService, + bitcoinService, ) } } diff --git a/app/src/main/java/to/bitkit/ui/MainActivity.kt b/app/src/main/java/to/bitkit/ui/MainActivity.kt index 5898b4961..23851fa3a 100644 --- a/app/src/main/java/to/bitkit/ui/MainActivity.kt +++ b/app/src/main/java/to/bitkit/ui/MainActivity.kt @@ -56,18 +56,17 @@ import to.bitkit.ui.theme.AppThemeSurface @AndroidEntryPoint class MainActivity : ComponentActivity() { - private val viewModel by viewModels() - private val sharedViewModel by viewModels() + private val viewModel by viewModels() + private val walletViewModel by viewModels() override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - sharedViewModel.logInstanceHashCode() setContent { enableEdgeToEdge() AppThemeSurface { - MainScreen(viewModel) { - WalletScreen(viewModel) { + MainScreen(walletViewModel) { + WalletScreen(walletViewModel) { Card(modifier = Modifier.fillMaxWidth()) { Text( @@ -95,13 +94,13 @@ class MainActivity : ComponentActivity() { horizontalArrangement = Arrangement.SpaceAround, modifier = Modifier.fillMaxWidth(), ) { - TextButton(sharedViewModel::registerForNotifications) { Text("Register Device") } + TextButton(viewModel::registerForNotifications) { Text("Register Device") } TextButton(viewModel::debugLspNotifications) { Text("LSP Notification") } } } - Peers(viewModel.peers, viewModel::togglePeerConnection) - Channels(viewModel.channels, viewModel::closeChannel) + Peers(walletViewModel.peers, walletViewModel::togglePeerConnection) + Channels(walletViewModel.channels, walletViewModel::closeChannel) } } } @@ -112,7 +111,7 @@ class MainActivity : ComponentActivity() { @OptIn(ExperimentalMaterial3Api::class) @Composable private fun MainScreen( - viewModel: MainViewModel = hiltViewModel(), + viewModel: WalletViewModel = hiltViewModel(), startContent: @Composable () -> Unit = {}, ) { val navController = rememberNavController() diff --git a/app/src/main/java/to/bitkit/ui/Nav.kt b/app/src/main/java/to/bitkit/ui/Nav.kt index c9d02d9e5..554728dc4 100644 --- a/app/src/main/java/to/bitkit/ui/Nav.kt +++ b/app/src/main/java/to/bitkit/ui/Nav.kt @@ -24,7 +24,7 @@ import to.bitkit.ui.settings.PeersScreen fun AppNavHost( navController: NavHostController, modifier: Modifier = Modifier, - viewModel: MainViewModel = hiltViewModel(), + viewModel: WalletViewModel = hiltViewModel(), walletScreen: @Composable () -> Unit = {}, ) { // val screenViewModel = viewModel() diff --git a/app/src/main/java/to/bitkit/ui/SettingsScreen.kt b/app/src/main/java/to/bitkit/ui/SettingsScreen.kt index d452ae2b4..f5b7f5fe2 100644 --- a/app/src/main/java/to/bitkit/ui/SettingsScreen.kt +++ b/app/src/main/java/to/bitkit/ui/SettingsScreen.kt @@ -26,7 +26,7 @@ import to.bitkit.ui.shared.InfoField @Composable fun SettingsScreen( navController: NavController, - viewModel: MainViewModel, + viewModel: WalletViewModel, ) { Column( verticalArrangement = Arrangement.spacedBy(8.dp), diff --git a/app/src/main/java/to/bitkit/ui/SharedViewModel.kt b/app/src/main/java/to/bitkit/ui/SharedViewModel.kt index 8aa401dcd..1d74fb238 100644 --- a/app/src/main/java/to/bitkit/ui/SharedViewModel.kt +++ b/app/src/main/java/to/bitkit/ui/SharedViewModel.kt @@ -11,25 +11,26 @@ import kotlinx.coroutines.runBlocking import kotlinx.coroutines.tasks.await import to.bitkit.Tag.DEV import to.bitkit.Tag.LSP +import to.bitkit.data.AppDb +import to.bitkit.data.keychain.KeychainStore import to.bitkit.di.BgDispatcher +import to.bitkit.services.BitcoinService import to.bitkit.services.BlocktankService import javax.inject.Inject @HiltViewModel class SharedViewModel @Inject constructor( @BgDispatcher private val bgDispatcher: CoroutineDispatcher, + private val db: AppDb, + private val keychain: KeychainStore, private val blocktankService: BlocktankService, + private val bitcoinService: BitcoinService, ) : ViewModel() { fun warmupNode() { // TODO make it concurrent, and wait for all to finish before trying to access `lightningService.node`, etc… - logInstanceHashCode() runBlocking { to.bitkit.services.warmupNode() } } - fun logInstanceHashCode() { - Log.d(DEV, "${this::class.java.simpleName} hashCode: ${hashCode()}") - } - fun registerForNotifications(fcmToken: String? = null) { viewModelScope.launch(bgDispatcher) { val token = fcmToken ?: FirebaseMessaging.getInstance().token.await() @@ -41,4 +42,37 @@ class SharedViewModel @Inject constructor( } } } + + // region debug + fun debugDb() { + viewModelScope.launch { + db.configDao().getAll().collect { + Log.d(DEV, "${it.count()} entities in DB: $it") + } + } + } + + fun debugKeychain() { + viewModelScope.launch { + val key = "test" + if (keychain.exists(key)) { + keychain.delete(key) + } + keychain.saveString(key, "testValue") + } + } + + fun debugWipeBdk() { + bitcoinService.wipeStorage() + } + + fun debugLspNotifications() { + viewModelScope.launch(bgDispatcher) { + val token = FirebaseMessaging.getInstance().token.await() + blocktankService.testNotification(token) + } + } + + // endregion + } diff --git a/app/src/main/java/to/bitkit/ui/WalletScreen.kt b/app/src/main/java/to/bitkit/ui/WalletScreen.kt index 67462b8b8..ac935f35b 100644 --- a/app/src/main/java/to/bitkit/ui/WalletScreen.kt +++ b/app/src/main/java/to/bitkit/ui/WalletScreen.kt @@ -27,7 +27,7 @@ import to.bitkit.ui.shared.moneyString @Composable fun WalletScreen( - viewModel: MainViewModel, + viewModel: WalletViewModel, content: @Composable () -> Unit = {}, ) { Column( @@ -112,4 +112,3 @@ internal fun CopyToClipboardButton(text: String) { ) } } - diff --git a/app/src/main/java/to/bitkit/ui/MainViewModel.kt b/app/src/main/java/to/bitkit/ui/WalletViewModel.kt similarity index 81% rename from app/src/main/java/to/bitkit/ui/MainViewModel.kt rename to app/src/main/java/to/bitkit/ui/WalletViewModel.kt index eaccdfffc..362659d08 100644 --- a/app/src/main/java/to/bitkit/ui/MainViewModel.kt +++ b/app/src/main/java/to/bitkit/ui/WalletViewModel.kt @@ -10,7 +10,6 @@ import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.delay import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.launch import kotlinx.coroutines.tasks.await @@ -33,13 +32,10 @@ import to.bitkit.services.payInvoice import javax.inject.Inject @HiltViewModel -class MainViewModel @Inject constructor( +class WalletViewModel @Inject constructor( @BgDispatcher private val bgDispatcher: CoroutineDispatcher, private val bitcoinService: BitcoinService, private val lightningService: LightningService, - private val blocktankService: BlocktankService, - private val keychain: KeychainStore, - private val appDb: AppDb, ) : ViewModel() { val ldkNodeId = mutableStateOf("Loading…") val ldkBalance = mutableStateOf("Loading…") @@ -131,40 +127,9 @@ class MainViewModel @Inject constructor( } } - // region debug - fun debugDb() { - viewModelScope.launch { - appDb.configDao().getAll().collect { - Log.d(DEV, "${it.count()} entities in DB: $it") - } - } - } - - fun debugKeychain() { - viewModelScope.launch { - val key = "test" - if (keychain.exists(key)) { - keychain.delete(key) - } - keychain.saveString(key, "testValue") - } - } - - fun debugWipeBdk() { - bitcoinService.wipeStorage() - } - - fun debugLspNotifications() { - viewModelScope.launch(bgDispatcher) { - val token = FirebaseMessaging.getInstance().token.await() - blocktankService.testNotification(token) - } - } - - // endregion } -fun MainViewModel.togglePeerConnection(peer: LnPeer) = +fun WalletViewModel.togglePeerConnection(peer: LnPeer) = if (peer.isConnected) disconnectPeer(peer.nodeId) else connectPeer(peer) sealed class MainUiState { diff --git a/app/src/main/java/to/bitkit/ui/settings/ChannelsScreen.kt b/app/src/main/java/to/bitkit/ui/settings/ChannelsScreen.kt index 39fbcb1d9..6a0ea3e37 100644 --- a/app/src/main/java/to/bitkit/ui/settings/ChannelsScreen.kt +++ b/app/src/main/java/to/bitkit/ui/settings/ChannelsScreen.kt @@ -8,12 +8,12 @@ import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp -import to.bitkit.ui.MainViewModel +import to.bitkit.ui.WalletViewModel import to.bitkit.ui.shared.Channels @Composable fun ChannelsScreen( - viewModel: MainViewModel, + viewModel: WalletViewModel, ) { Column( verticalArrangement = Arrangement.spacedBy(24.dp), diff --git a/app/src/main/java/to/bitkit/ui/settings/PaymentsScreen.kt b/app/src/main/java/to/bitkit/ui/settings/PaymentsScreen.kt index 9dec9365f..f98c0023f 100644 --- a/app/src/main/java/to/bitkit/ui/settings/PaymentsScreen.kt +++ b/app/src/main/java/to/bitkit/ui/settings/PaymentsScreen.kt @@ -17,12 +17,12 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import to.bitkit.R import to.bitkit.ui.CopyToClipboardButton -import to.bitkit.ui.MainViewModel +import to.bitkit.ui.WalletViewModel import to.bitkit.ui.shared.InfoField @Composable fun PaymentsScreen( - viewModel: MainViewModel, + viewModel: WalletViewModel, ) { Column( verticalArrangement = Arrangement.spacedBy(8.dp), diff --git a/app/src/main/java/to/bitkit/ui/settings/PeersScreen.kt b/app/src/main/java/to/bitkit/ui/settings/PeersScreen.kt index d49c308fa..d0b502462 100644 --- a/app/src/main/java/to/bitkit/ui/settings/PeersScreen.kt +++ b/app/src/main/java/to/bitkit/ui/settings/PeersScreen.kt @@ -18,14 +18,14 @@ import androidx.compose.ui.unit.dp import to.bitkit.LnPeer import to.bitkit.PEER import to.bitkit.R -import to.bitkit.ui.MainViewModel +import to.bitkit.ui.WalletViewModel import to.bitkit.ui.shared.InfoField import to.bitkit.ui.shared.Peers import to.bitkit.ui.togglePeerConnection @Composable fun PeersScreen( - viewModel: MainViewModel, + viewModel: WalletViewModel, ) { Column( verticalArrangement = Arrangement.spacedBy(24.dp), From 2f3f3b21366941561b5de83c4f851cebfa9e4cd3 Mon Sep 17 00:00:00 2001 From: Ovi Trif Date: Mon, 2 Sep 2024 16:20:16 +0200 Subject: [PATCH 04/27] feat: Add LCE (Loading Content Error) UiState flows and UI --- app/src/main/java/to/bitkit/ext/List.kt | 3 +- .../java/to/bitkit/services/BitcoinService.kt | 2 +- .../main/java/to/bitkit/ui/MainActivity.kt | 133 ++++++++++++------ .../main/java/to/bitkit/ui/SettingsScreen.kt | 3 +- .../main/java/to/bitkit/ui/SharedViewModel.kt | 2 + .../main/java/to/bitkit/ui/WalletScreen.kt | 40 +++--- .../main/java/to/bitkit/ui/WalletViewModel.kt | 102 +++++++------- .../to/bitkit/ui/settings/ChannelsScreen.kt | 24 ++-- .../java/to/bitkit/ui/settings/PeersScreen.kt | 15 +- .../main/java/to/bitkit/ui/shared/Channels.kt | 3 +- .../main/java/to/bitkit/ui/shared/Peers.kt | 3 +- 11 files changed, 184 insertions(+), 146 deletions(-) diff --git a/app/src/main/java/to/bitkit/ext/List.kt b/app/src/main/java/to/bitkit/ext/List.kt index 2f4f329bf..057d45667 100644 --- a/app/src/main/java/to/bitkit/ext/List.kt +++ b/app/src/main/java/to/bitkit/ext/List.kt @@ -2,7 +2,8 @@ package to.bitkit.ext import androidx.compose.runtime.snapshots.SnapshotStateList -fun SnapshotStateList.syncTo(list: List) { +fun SnapshotStateList.syncTo(list: List): SnapshotStateList { clear() this += list + return this } diff --git a/app/src/main/java/to/bitkit/services/BitcoinService.kt b/app/src/main/java/to/bitkit/services/BitcoinService.kt index 67fbd081a..baa9ebe53 100644 --- a/app/src/main/java/to/bitkit/services/BitcoinService.kt +++ b/app/src/main/java/to/bitkit/services/BitcoinService.kt @@ -96,7 +96,7 @@ class BitcoinService @Inject constructor( // region state val balance get() = if (hasSynced) wallet.getBalance() else null - suspend fun getAddress(): String { + suspend fun getNextAddress(): String { return ServiceQueue.BDK.background { val addressInfo = wallet.revealNextAddress(KeychainKind.EXTERNAL).address addressInfo.asString() diff --git a/app/src/main/java/to/bitkit/ui/MainActivity.kt b/app/src/main/java/to/bitkit/ui/MainActivity.kt index 23851fa3a..be3601515 100644 --- a/app/src/main/java/to/bitkit/ui/MainActivity.kt +++ b/app/src/main/java/to/bitkit/ui/MainActivity.kt @@ -7,15 +7,15 @@ import androidx.activity.compose.setContent import androidx.activity.enableEdgeToEdge import androidx.activity.result.contract.ActivityResultContracts import androidx.activity.viewModels +import androidx.compose.animation.Crossfade import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size -import androidx.compose.foundation.rememberScrollState -import androidx.compose.foundation.verticalScroll import androidx.compose.material.icons.Icons import androidx.compose.material.icons.automirrored.filled.ArrowBack import androidx.compose.material.icons.filled.NotificationAdd @@ -23,6 +23,7 @@ import androidx.compose.material.icons.filled.NotificationsNone import androidx.compose.material.icons.filled.Refresh import androidx.compose.material3.Card import androidx.compose.material3.CenterAlignedTopAppBar +import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Icon import androidx.compose.material3.IconButton @@ -36,14 +37,18 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableIntStateOf +import androidx.compose.runtime.mutableStateListOf import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue +import androidx.compose.runtime.toMutableStateList +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import androidx.hilt.navigation.compose.hiltViewModel +import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.navigation.compose.currentBackStackEntryAsState import androidx.navigation.compose.rememberNavController import dagger.hilt.android.AndroidEntryPoint @@ -56,51 +61,22 @@ import to.bitkit.ui.theme.AppThemeSurface @AndroidEntryPoint class MainActivity : ComponentActivity() { - private val viewModel by viewModels() - private val walletViewModel by viewModels() + val viewModel by viewModels() + val walletViewModel by viewModels() override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - setContent { enableEdgeToEdge() AppThemeSurface { MainScreen(walletViewModel) { - WalletScreen(walletViewModel) { - - Card(modifier = Modifier.fillMaxWidth()) { - Text( - text = "Debug", - style = MaterialTheme.typography.titleMedium, - modifier = Modifier.padding(vertical = 12.dp, horizontal = 24.dp), - ) - Row( - horizontalArrangement = Arrangement.SpaceAround, - modifier = Modifier.fillMaxWidth(), - ) { - TextButton(viewModel::debugDb) { Text("Debug DB") } - TextButton(viewModel::debugKeychain) { Text("Debug Keychain") } - TextButton(viewModel::debugWipeBdk) { Text("Wipe BDK") } - } + val uiState = walletViewModel.uiState.collectAsStateWithLifecycle() + Crossfade(uiState, label = "ContentCrossfade") { + when (val state = it.value) { + is MainUiState.Loading -> LoadingScreen() + is MainUiState.Content -> WalletScreen(walletViewModel, state, debugUi(state)) + is MainUiState.Error -> ErrorScreen(state) } - - Card(modifier = Modifier.fillMaxWidth()) { - Text( - text = "Notifications", - style = MaterialTheme.typography.titleMedium, - modifier = Modifier.padding(vertical = 12.dp, horizontal = 24.dp), - ) - Row( - horizontalArrangement = Arrangement.SpaceAround, - modifier = Modifier.fillMaxWidth(), - ) { - TextButton(viewModel::registerForNotifications) { Text("Register Device") } - TextButton(viewModel::debugLspNotifications) { Text("LSP Notification") } - } - } - - Peers(walletViewModel.peers, walletViewModel::togglePeerConnection) - Channels(walletViewModel.channels, walletViewModel::closeChannel) } } } @@ -108,6 +84,7 @@ class MainActivity : ComponentActivity() { } } +// region scaffold @OptIn(ExperimentalMaterial3Api::class) @Composable private fun MainScreen( @@ -147,8 +124,7 @@ private fun MainScreen( modifier = Modifier, ) } - } - ) + }) }, bottomBar = { NavigationBar(tonalElevation = 5.dp) { @@ -178,9 +154,8 @@ private fun MainScreen( ) { padding -> Box( modifier = Modifier - .padding(padding) .fillMaxSize() - .verticalScroll(rememberScrollState()), + .padding(padding) ) { AppNavHost( navController = navController, @@ -191,7 +166,38 @@ private fun MainScreen( } } } +// endregion + +@Composable +fun LoadingScreen() { + Column( + verticalArrangement = Arrangement.Center, + horizontalAlignment = Alignment.CenterHorizontally, + modifier = Modifier.fillMaxSize(), + ) { + CircularProgressIndicator() + } +} +@Composable +fun ErrorScreen(uiState: MainUiState.Error) { + Column( + verticalArrangement = Arrangement.Center, + horizontalAlignment = Alignment.CenterHorizontally, + modifier = Modifier.fillMaxSize(), + ) { + Text( + text = uiState.title, + style = MaterialTheme.typography.titleLarge, + ) + Text( + text = uiState.message, + style = MaterialTheme.typography.titleSmall, + ) + } +} + +// region helpers @Composable private fun NotificationButton() { val context = LocalContext.current @@ -213,9 +219,7 @@ private fun NotificationButton() { pushNotification( title = "Bitkit Notification", text = "Short custom notification description", - bigText = "Much longer text that cannot fit one line " + - "because the lightning channel has been updated " + - "via a push notification bro…", + bigText = "Much longer text that cannot fit one line " + "because the lightning channel has been updated " + "via a push notification bro…", ) } Unit @@ -231,3 +235,40 @@ private fun NotificationButton() { ) } } +// endregion + +// region debug +fun MainActivity.debugUi(uiState: MainUiState.Content) = @Composable { + Card(modifier = Modifier.fillMaxWidth()) { + Text( + text = "Debug", + style = MaterialTheme.typography.titleMedium, + modifier = Modifier.padding(vertical = 12.dp, horizontal = 24.dp), + ) + Row( + horizontalArrangement = Arrangement.SpaceAround, + modifier = Modifier.fillMaxWidth(), + ) { + TextButton(viewModel::debugDb) { Text("Debug DB") } + TextButton(viewModel::debugKeychain) { Text("Debug Keychain") } + TextButton(viewModel::debugWipeBdk) { Text("Wipe BDK") } + } + } + Card(modifier = Modifier.fillMaxWidth()) { + Text( + text = "Notifications", + style = MaterialTheme.typography.titleMedium, + modifier = Modifier.padding(vertical = 12.dp, horizontal = 24.dp), + ) + Row( + horizontalArrangement = Arrangement.SpaceAround, + modifier = Modifier.fillMaxWidth(), + ) { + TextButton(viewModel::registerForNotifications) { Text("Register Device") } + TextButton(viewModel::debugLspNotifications) { Text("LSP Notification") } + } + } + Peers(uiState.peers.toMutableStateList(), walletViewModel::togglePeerConnection) + Channels(uiState.channels.toMutableStateList(), walletViewModel::closeChannel) +} +// endregion diff --git a/app/src/main/java/to/bitkit/ui/SettingsScreen.kt b/app/src/main/java/to/bitkit/ui/SettingsScreen.kt index f5b7f5fe2..4599b026c 100644 --- a/app/src/main/java/to/bitkit/ui/SettingsScreen.kt +++ b/app/src/main/java/to/bitkit/ui/SettingsScreen.kt @@ -21,6 +21,7 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import androidx.navigation.NavController import to.bitkit.R +import to.bitkit.SEED import to.bitkit.ui.shared.InfoField @Composable @@ -59,7 +60,7 @@ fun SettingsScreen( text = "Bitcoin Wallet", style = MaterialTheme.typography.titleMedium, ) - Mnemonic(viewModel.mnemonic.value) + Mnemonic(SEED) // TODO use value from viewModel.uiState } } diff --git a/app/src/main/java/to/bitkit/ui/SharedViewModel.kt b/app/src/main/java/to/bitkit/ui/SharedViewModel.kt index 1d74fb238..9a1eee59a 100644 --- a/app/src/main/java/to/bitkit/ui/SharedViewModel.kt +++ b/app/src/main/java/to/bitkit/ui/SharedViewModel.kt @@ -1,6 +1,7 @@ package to.bitkit.ui import android.util.Log +import androidx.compose.runtime.mutableStateOf import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.google.firebase.messaging.FirebaseMessaging @@ -9,6 +10,7 @@ import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.launch import kotlinx.coroutines.runBlocking import kotlinx.coroutines.tasks.await +import to.bitkit.SEED import to.bitkit.Tag.DEV import to.bitkit.Tag.LSP import to.bitkit.data.AppDb diff --git a/app/src/main/java/to/bitkit/ui/WalletScreen.kt b/app/src/main/java/to/bitkit/ui/WalletScreen.kt index ac935f35b..037679763 100644 --- a/app/src/main/java/to/bitkit/ui/WalletScreen.kt +++ b/app/src/main/java/to/bitkit/ui/WalletScreen.kt @@ -5,6 +5,8 @@ import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.size +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Add import androidx.compose.material.icons.filled.ContentCopy @@ -13,8 +15,6 @@ import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue -import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalClipboardManager @@ -28,14 +28,14 @@ import to.bitkit.ui.shared.moneyString @Composable fun WalletScreen( viewModel: WalletViewModel, + uiState: MainUiState.Content, content: @Composable () -> Unit = {}, ) { Column( verticalArrangement = Arrangement.spacedBy(24.dp), - modifier = Modifier, + modifier = Modifier.verticalScroll(rememberScrollState()), ) { Column(verticalArrangement = Arrangement.spacedBy(12.dp)) { - val ldkBalance by remember { viewModel.ldkBalance } Row( horizontalArrangement = Arrangement.SpaceBetween, verticalAlignment = Alignment.Bottom, @@ -45,23 +45,18 @@ fun WalletScreen( text = stringResource(R.string.lightning), style = MaterialTheme.typography.titleLarge, ) - Row { - Text( - text = moneyString(ldkBalance), - style = MaterialTheme.typography.titleSmall, - ) - } + Text( + text = moneyString(uiState.ldkBalance), + style = MaterialTheme.typography.titleSmall, + ) } - - val nodeId by remember { viewModel.ldkNodeId } InfoField( - value = nodeId, + value = uiState.ldkNodeId, label = stringResource(R.string.node_id), - trailingIcon = { CopyToClipboardButton(nodeId) }, + trailingIcon = { CopyToClipboardButton(uiState.ldkNodeId) }, ) } Column(verticalArrangement = Arrangement.spacedBy(12.dp)) { - val btcBalance by remember { viewModel.btcBalance } Row( horizontalArrangement = Arrangement.SpaceBetween, verticalAlignment = Alignment.Bottom, @@ -71,17 +66,14 @@ fun WalletScreen( text = stringResource(R.string.wallet), style = MaterialTheme.typography.titleLarge, ) - Row { - Text( - text = moneyString(btcBalance), - style = MaterialTheme.typography.titleSmall, - ) - } + Text( + text = moneyString(uiState.btcBalance), + style = MaterialTheme.typography.titleSmall, + ) } - val address by remember { viewModel.btcAddress } InfoField( - value = address, + value = uiState.btcAddress, label = stringResource(R.string.address), trailingIcon = { Row { @@ -92,7 +84,7 @@ fun WalletScreen( modifier = Modifier.size(16.dp), ) } - CopyToClipboardButton(address) + CopyToClipboardButton(uiState.btcAddress) } }, ) diff --git a/app/src/main/java/to/bitkit/ui/WalletViewModel.kt b/app/src/main/java/to/bitkit/ui/WalletViewModel.kt index 362659d08..27a7943ff 100644 --- a/app/src/main/java/to/bitkit/ui/WalletViewModel.kt +++ b/app/src/main/java/to/bitkit/ui/WalletViewModel.kt @@ -1,28 +1,18 @@ package to.bitkit.ui -import android.util.Log -import androidx.compose.runtime.mutableStateListOf -import androidx.compose.runtime.mutableStateOf import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope -import com.google.firebase.messaging.FirebaseMessaging import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.delay import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.launch -import kotlinx.coroutines.tasks.await import org.lightningdevkit.ldknode.ChannelDetails import to.bitkit.LnPeer import to.bitkit.SEED -import to.bitkit.Tag.DEV -import to.bitkit.data.AppDb -import to.bitkit.data.keychain.KeychainStore import to.bitkit.di.BgDispatcher -import to.bitkit.ext.syncTo import to.bitkit.services.BitcoinService -import to.bitkit.services.BlocktankService import to.bitkit.services.LightningService import to.bitkit.services.closeChannel import to.bitkit.services.connectPeer @@ -37,54 +27,56 @@ class WalletViewModel @Inject constructor( private val bitcoinService: BitcoinService, private val lightningService: LightningService, ) : ViewModel() { - val ldkNodeId = mutableStateOf("Loading…") - val ldkBalance = mutableStateOf("Loading…") - val btcAddress = mutableStateOf("Loading…") - val btcBalance = mutableStateOf("Loading…") - val mnemonic = mutableStateOf(SEED) - val peers = mutableStateListOf() - val channels = mutableStateListOf() - private val node = lightningService.node - val _uiState = MutableStateFlow(MainUiState.Loading) + private val _uiState = MutableStateFlow(MainUiState.Loading) val uiState = _uiState.asStateFlow() init { viewModelScope.launch { + delay(1000) // TODO replace with actual load time of ldk-node warmUp sync() } } private suspend fun sync() { + lightningService.sync() bitcoinService.syncWithRevealedSpks() - ldkNodeId.value = lightningService.nodeId - ldkBalance.value = lightningService.balances.totalLightningBalanceSats.toString() - btcAddress.value = bitcoinService.getAddress() - btcBalance.value = bitcoinService.balance?.total?.toSat().toString() - mnemonic.value = SEED - peers.syncTo(lightningService.peers) - channels.syncTo(lightningService.channels) + _uiState.value = MainUiState.Content( + ldkNodeId = lightningService.nodeId, + ldkBalance = lightningService.balances.totalLightningBalanceSats.toString(), + btcAddress = bitcoinService.getNextAddress(), + btcBalance = bitcoinService.balance?.total?.toSat().toString(), + mnemonic = SEED, + peers = lightningService.peers, + channels = lightningService.channels, + ) } fun getNewAddress() { - btcAddress.value = node.onchainPayment().newAddress() + updateContentState { it.copy(btcAddress = node.onchainPayment().newAddress()) } } fun connectPeer(peer: LnPeer) { lightningService.connectPeer(peer) - peers.replaceAll { - it.run { copy(isConnected = it.nodeId == nodeId) } + + updateContentState { + val peers = it.peers.toMutableList().apply { + replaceAll { p -> p.run { copy(isConnected = p.nodeId == nodeId) } } + } + it.copy(peers = peers) } - channels.syncTo(lightningService.channels) } - fun disconnectPeer(nodeId: String) { + private fun disconnectPeer(nodeId: String) { node.disconnect(nodeId) - peers.replaceAll { - it.takeIf { it.nodeId == nodeId }?.copy(isConnected = false) ?: it + + updateContentState { + val peers = it.peers.toMutableList().apply { + replaceAll { p -> p.takeIf { pp -> pp.nodeId == nodeId }?.copy(isConnected = false) ?: p } + } + it.copy(peers = peers) } - channels.syncTo(lightningService.channels) } fun payInvoice(invoice: String) { @@ -97,8 +89,9 @@ class WalletViewModel @Inject constructor( fun createInvoice() = lightningService.createInvoice() fun openChannel() { + val contentState = _uiState.value as? MainUiState.Content ?: error("No peer connected to open channel.") viewModelScope.launch(bgDispatcher) { - lightningService.openChannel(peers.first()) + lightningService.openChannel(contentState.peers.first()) sync() } } @@ -110,31 +103,32 @@ class WalletViewModel @Inject constructor( } } + private fun updateContentState(update: (MainUiState.Content) -> MainUiState.Content) { + val stateValue = this._uiState.value + if (stateValue is MainUiState.Content) { + this._uiState.value = update(stateValue) + } + } + + // region debug fun refresh() { + _uiState.value = MainUiState.Loading viewModelScope.launch { - "Refreshing…".also { - ldkNodeId.value = it - ldkBalance.value = it - btcAddress.value = it - btcBalance.value = it - } - peers.clear() - channels.clear() - - delay(50) - lightningService.sync() + delay(500) sync() } } + fun togglePeerConnection(peer: LnPeer) { + if (peer.isConnected) disconnectPeer(peer.nodeId) + else connectPeer(peer) + } + // endregion } -fun WalletViewModel.togglePeerConnection(peer: LnPeer) = - if (peer.isConnected) disconnectPeer(peer.nodeId) else connectPeer(peer) - +// region state sealed class MainUiState { - - data object Loading: MainUiState() + data object Loading : MainUiState() data class Content( val ldkNodeId: String, val ldkBalance: String, @@ -144,8 +138,10 @@ sealed class MainUiState { val peers: List, val channels: List, ) : MainUiState() + data class Error( - val title: String, - val message: String, + val title: String = "Error Title", + val message: String = "Error short description.", ) : MainUiState() } +// endregion diff --git a/app/src/main/java/to/bitkit/ui/settings/ChannelsScreen.kt b/app/src/main/java/to/bitkit/ui/settings/ChannelsScreen.kt index 6a0ea3e37..f73a3eec6 100644 --- a/app/src/main/java/to/bitkit/ui/settings/ChannelsScreen.kt +++ b/app/src/main/java/to/bitkit/ui/settings/ChannelsScreen.kt @@ -2,12 +2,15 @@ package to.bitkit.ui.settings import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.padding import androidx.compose.material3.Button +import androidx.compose.material3.Card import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp +import to.bitkit.ui.MainUiState import to.bitkit.ui.WalletViewModel import to.bitkit.ui.shared.Channels @@ -19,17 +22,18 @@ fun ChannelsScreen( verticalArrangement = Arrangement.spacedBy(24.dp), modifier = Modifier, ) { - Row( - horizontalArrangement = Arrangement.spacedBy(12.dp), + Card { + Text("⚠️ Please return to Home screen to see your updates…", Modifier.padding(12.dp)) + } + val peers = remember { (viewModel.uiState.value as? MainUiState.Content?)?.peers.orEmpty() } + val channels = remember { (viewModel.uiState.value as? MainUiState.Content)?.channels.orEmpty() } + Button( + onClick = { viewModel.openChannel() }, + enabled = peers.isNotEmpty() ) { - Button( - onClick = { viewModel.openChannel() }, - enabled = viewModel.peers.isNotEmpty() - ) { - Text("Open Channel") - } + Text("Open Channel") } - Channels(viewModel.channels, viewModel::closeChannel) + Channels(channels, viewModel::closeChannel) PayInvoice(viewModel::payInvoice) } } diff --git a/app/src/main/java/to/bitkit/ui/settings/PeersScreen.kt b/app/src/main/java/to/bitkit/ui/settings/PeersScreen.kt index d0b502462..f707d1a19 100644 --- a/app/src/main/java/to/bitkit/ui/settings/PeersScreen.kt +++ b/app/src/main/java/to/bitkit/ui/settings/PeersScreen.kt @@ -3,7 +3,9 @@ package to.bitkit.ui.settings import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding import androidx.compose.material3.Button +import androidx.compose.material3.Card import androidx.compose.material3.MaterialTheme import androidx.compose.material3.OutlinedTextField import androidx.compose.material3.Text @@ -18,10 +20,10 @@ import androidx.compose.ui.unit.dp import to.bitkit.LnPeer import to.bitkit.PEER import to.bitkit.R +import to.bitkit.ui.MainUiState import to.bitkit.ui.WalletViewModel import to.bitkit.ui.shared.InfoField import to.bitkit.ui.shared.Peers -import to.bitkit.ui.togglePeerConnection @Composable fun PeersScreen( @@ -31,13 +33,13 @@ fun PeersScreen( verticalArrangement = Arrangement.spacedBy(24.dp), modifier = Modifier, ) { + Card { + Text("⚠️ Please return to Home screen to see your updates…", Modifier.padding(12.dp)) + } var pubKey by remember { mutableStateOf(PEER.nodeId) } val host by remember { mutableStateOf(PEER.host) } var port by remember { mutableStateOf(PEER.port) } - - Column( - verticalArrangement = Arrangement.spacedBy(8.dp), - ) { + Column(verticalArrangement = Arrangement.spacedBy(8.dp)) { Text( text = "Connect to a Peer", style = MaterialTheme.typography.titleMedium, @@ -61,6 +63,7 @@ fun PeersScreen( Text(stringResource(R.string.connect)) } } - Peers(viewModel.peers, viewModel::togglePeerConnection) + val peers = remember { (viewModel.uiState.value as? MainUiState.Content?)?.peers.orEmpty() } + Peers(peers, viewModel::togglePeerConnection) } } diff --git a/app/src/main/java/to/bitkit/ui/shared/Channels.kt b/app/src/main/java/to/bitkit/ui/shared/Channels.kt index 5ee93b876..8b3d29809 100644 --- a/app/src/main/java/to/bitkit/ui/shared/Channels.kt +++ b/app/src/main/java/to/bitkit/ui/shared/Channels.kt @@ -26,7 +26,6 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember -import androidx.compose.runtime.snapshots.SnapshotStateList import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource @@ -37,7 +36,7 @@ import to.bitkit.R @Composable internal fun Channels( - channels: SnapshotStateList, + channels: List, onChannelClose: (ChannelDetails) -> Unit, ) { Column(verticalArrangement = Arrangement.spacedBy(24.dp)) { diff --git a/app/src/main/java/to/bitkit/ui/shared/Peers.kt b/app/src/main/java/to/bitkit/ui/shared/Peers.kt index 151ab1be8..668c3b703 100644 --- a/app/src/main/java/to/bitkit/ui/shared/Peers.kt +++ b/app/src/main/java/to/bitkit/ui/shared/Peers.kt @@ -17,7 +17,6 @@ import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme.colorScheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable -import androidx.compose.runtime.snapshots.SnapshotStateList import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource @@ -28,7 +27,7 @@ import to.bitkit.R @Composable internal fun Peers( - peers: SnapshotStateList, + peers: List, onToggle: (LnPeer) -> Unit, ) { Column( From 83205a6e99ff3f6668feb6960377a9e914ab93d3 Mon Sep 17 00:00:00 2001 From: Ovi Trif Date: Wed, 11 Sep 2024 18:19:20 +0200 Subject: [PATCH 05/27] chore: Update dependencies --- app/build.gradle.kts | 12 ++++++------ gradle/libs.versions.toml | 32 +++++++++++++++----------------- 2 files changed, 21 insertions(+), 23 deletions(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index afff04e76..b175da31e 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -96,12 +96,12 @@ dependencies { // Compose implementation(platform(libs.compose.bom)) androidTestImplementation(platform(libs.compose.bom)) - implementation(libs.material3) - implementation(libs.material.icons.extended) - implementation(libs.ui.tooling.preview) - debugImplementation(libs.ui.tooling) - debugImplementation(libs.ui.test.manifest) - androidTestImplementation(libs.ui.test.junit4) + implementation(libs.compose.material3) + implementation(libs.compose.material.icons.extended) + implementation(libs.compose.ui.tooling.preview) + debugImplementation(libs.compose.ui.tooling) + debugImplementation(libs.compose.ui.test.manifest) + androidTestImplementation(libs.compose.ui.test.junit4) // Compose Navigation implementation(libs.navigation.compose) androidTestImplementation(libs.navigation.testing) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 38fad6b2b..d663aaeef 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,28 +1,26 @@ [versions] -activityCompose = "1.9.1" -agp = "8.5.2" +activityCompose = "1.9.2" +agp = "8.6.0" appcompat = "1.7.0" bdk = "1.0.0-alpha.11" composeBom = "2024.06.00" # https://developer.android.com/develop/ui/compose/bom/bom-mapping coreKtx = "1.13.1" datastorePrefs = "1.1.1" espressoCore = "3.6.1" -firebaseBom = "33.1.2" +firebaseBom = "33.2.0" googleServices = "4.4.2" guava = "33.2.1-android" hilt = "2.51.1" -hiltCompiler = "1.2.0" -hiltNavigationCompose = "1.2.0" -hiltWork = "1.2.0" +hiltAndroidx = "1.2.0" junit = "4.13.2" junitExt = "1.2.1" kotlin = "1.9.24" ksp = "1.9.24-1.0.20" ktor = "2.3.12" ldkNode = "0.3.0" -lifecycle = "2.8.4" +lifecycle = "2.8.5" material = "1.12.0" -navCompose = "2.7.7" +navCompose = "2.8.0" room = "2.6.1" slf4j = "1.7.36" workRuntimeKtx = "2.9.1" @@ -32,6 +30,12 @@ activity-compose = { module = "androidx.activity:activity-compose", version.ref appcompat = { module = "androidx.appcompat:appcompat", version.ref = "appcompat" } bdk-android = { module = "org.bitcoindevkit:bdk-android", version.ref = "bdk" } compose-bom = { group = "androidx.compose", name = "compose-bom", version.ref = "composeBom" } +compose-material-icons-extended = { module = "androidx.compose.material:material-icons-extended" } +compose-material3 = { module = "androidx.compose.material3:material3" } +compose-ui-test-junit4 = { module = "androidx.compose.ui:ui-test-junit4" } +compose-ui-test-manifest = { module = "androidx.compose.ui:ui-test-manifest" } +compose-ui-tooling = { module = "androidx.compose.ui:ui-tooling" } +compose-ui-tooling-preview = { module = "androidx.compose.ui:ui-tooling-preview" } core-ktx = { module = "androidx.core:core-ktx", version.ref = "coreKtx" } datastore-preferences = { module = "androidx.datastore:datastore-preferences", version.ref = "datastorePrefs" } espresso-core = { module = "androidx.test.espresso:espresso-core", version.ref = "espressoCore" } @@ -41,9 +45,9 @@ firebase-messaging = { module = "com.google.firebase:firebase-messaging" } guava = { module = "com.google.guava:guava", version.ref = "guava" } hilt-android = { module = "com.google.dagger:hilt-android", version.ref = "hilt" } hilt-android-compiler = { module = "com.google.dagger:hilt-android-compiler", version.ref = "hilt" } -hilt-compiler = { module = "androidx.hilt:hilt-compiler", version.ref = "hiltCompiler" } -hilt-navigation-compose = { module = "androidx.hilt:hilt-navigation-compose", version.ref = "hiltNavigationCompose" } -hilt-work = { module = "androidx.hilt:hilt-work", version.ref = "hiltWork" } +hilt-compiler = { module = "androidx.hilt:hilt-compiler", version.ref = "hiltAndroidx" } +hilt-navigation-compose = { module = "androidx.hilt:hilt-navigation-compose", version.ref = "hiltAndroidx" } +hilt-work = { module = "androidx.hilt:hilt-work", version.ref = "hiltAndroidx" } junit-junit = { group = "junit", name = "junit", version.ref = "junit" } junit-ext = { module = "androidx.test.ext:junit", version.ref = "junitExt" } kotlin-bom = { module = "org.jetbrains.kotlin:kotlin-bom", version.ref = "kotlin" } @@ -61,8 +65,6 @@ lifecycle-viewmodel-compose = { module = "androidx.lifecycle:lifecycle-viewmodel lifecycle-viewmodel-ktx = { module = "androidx.lifecycle:lifecycle-viewmodel-ktx", version.ref = "lifecycle" } lifecycle-viewmodel-savedstate = { module = "androidx.lifecycle:lifecycle-viewmodel-savedstate", version.ref = "lifecycle" } material = { module = "com.google.android.material:material", version.ref = "material" } -material-icons-extended = { module = "androidx.compose.material:material-icons-extended" } -material3 = { module = "androidx.compose.material3:material3" } navigation-compose = { module = "androidx.navigation:navigation-compose", version.ref = "navCompose" } navigation-testing = { module = "androidx.navigation:navigation-testing", version.ref = "navCompose" } room-compiler = { group = "androidx.room", name = "room-compiler", version.ref = "room" } @@ -70,10 +72,6 @@ room-ktx = { group = "androidx.room", name = "room-ktx", version.ref = "room" } room-runtime = { group = "androidx.room", name = "room-runtime", version.ref = "room" } room-testing = { group = "androidx.room", name = "room-testing", version.ref = "room" } slf4j-simple = { group = "org.slf4j", name = "slf4j-simple", version.ref = "slf4j" } -ui-test-junit4 = { module = "androidx.compose.ui:ui-test-junit4" } -ui-test-manifest = { module = "androidx.compose.ui:ui-test-manifest" } -ui-tooling = { module = "androidx.compose.ui:ui-tooling" } -ui-tooling-preview = { module = "androidx.compose.ui:ui-tooling-preview" } work-runtime-ktx = { module = "androidx.work:work-runtime-ktx", version.ref = "workRuntimeKtx" } [plugins] From daf6d6103d21058684f1044f5c0e9ab7621efd2c Mon Sep 17 00:00:00 2001 From: Ovi Trif Date: Wed, 11 Sep 2024 18:21:03 +0200 Subject: [PATCH 06/27] chore: Migrate to Kotlin 2 and Use Compose Compiler --- app/build.gradle.kts | 14 ++++++++++---- build.gradle.kts | 1 + gradle/libs.versions.toml | 7 ++++--- 3 files changed, 15 insertions(+), 7 deletions(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index b175da31e..a6ce4c454 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -1,5 +1,8 @@ +import org.jetbrains.kotlin.compose.compiler.gradle.ComposeFeatureFlag + plugins { alias(libs.plugins.android.application) + alias(libs.plugins.compose.compiler) alias(libs.plugins.kotlin.android) alias(libs.plugins.kotlin.serialization) alias(libs.plugins.ksp) @@ -54,10 +57,6 @@ android { buildConfig = true compose = true } - composeOptions { - // https://developer.android.com/jetpack/androidx/releases/compose-kotlin#pre-release_kotlin_compatibility - kotlinCompilerExtensionVersion = "1.5.14" - } packaging { resources { excludes += "/META-INF/{AL2.0,LGPL2.1}" @@ -71,6 +70,13 @@ android { } } } +composeCompiler { + featureFlags = setOf( + ComposeFeatureFlag.StrongSkipping.disabled(), + ComposeFeatureFlag.OptimizeNonSkippingGroups, + ) + reportsDestination = layout.buildDirectory.dir("compose_compiler") +} dependencies { implementation(fileTree("libs") { include("*.aar") }) implementation(platform(libs.kotlin.bom)) diff --git a/build.gradle.kts b/build.gradle.kts index 0b22f4bc8..3e0ebc458 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -1,6 +1,7 @@ // Top-level build file where you can add configuration options common to all sub-projects/modules. plugins { alias(libs.plugins.android.application) apply false + alias(libs.plugins.compose.compiler) apply false alias(libs.plugins.google.services) apply false alias(libs.plugins.hilt.android) apply false // https://github.com/google/dagger/releases/ alias(libs.plugins.kotlin.android) apply false diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index d663aaeef..e0cc96cd3 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -3,7 +3,7 @@ activityCompose = "1.9.2" agp = "8.6.0" appcompat = "1.7.0" bdk = "1.0.0-alpha.11" -composeBom = "2024.06.00" # https://developer.android.com/develop/ui/compose/bom/bom-mapping +composeBom = "2024.09.01" # https://developer.android.com/develop/ui/compose/bom/bom-mapping coreKtx = "1.13.1" datastorePrefs = "1.1.1" espressoCore = "3.6.1" @@ -14,8 +14,8 @@ hilt = "2.51.1" hiltAndroidx = "1.2.0" junit = "4.13.2" junitExt = "1.2.1" -kotlin = "1.9.24" -ksp = "1.9.24-1.0.20" +kotlin = "2.0.20" +ksp = "2.0.20-1.0.25" ktor = "2.3.12" ldkNode = "0.3.0" lifecycle = "2.8.5" @@ -76,6 +76,7 @@ work-runtime-ktx = { module = "androidx.work:work-runtime-ktx", version.ref = "w [plugins] android-application = { id = "com.android.application", version.ref = "agp" } +compose-compiler = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" } google-services = { id = "com.google.gms.google-services", version.ref = "googleServices" } hilt-android = { id = "com.google.dagger.hilt.android", version.ref = "hilt" } kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" } From 8f0c65bab1e35c4fb485741901b9f541bae3c319 Mon Sep 17 00:00:00 2001 From: Ovi Trif Date: Wed, 11 Sep 2024 19:15:36 +0200 Subject: [PATCH 07/27] fix: ellipsisVisualTransformation on Kotlin 2 Compose --- .../main/java/to/bitkit/ui/SettingsScreen.kt | 1 + .../main/java/to/bitkit/ui/WalletScreen.kt | 2 ++ .../to/bitkit/ui/settings/PaymentsScreen.kt | 1 + .../java/to/bitkit/ui/settings/PeersScreen.kt | 2 +- .../java/to/bitkit/ui/shared/InfoField.kt | 3 ++- .../util/EllipsisVisualTransformation.kt | 27 ++++++++++++------- 6 files changed, 24 insertions(+), 12 deletions(-) diff --git a/app/src/main/java/to/bitkit/ui/SettingsScreen.kt b/app/src/main/java/to/bitkit/ui/SettingsScreen.kt index 4599b026c..b209d4635 100644 --- a/app/src/main/java/to/bitkit/ui/SettingsScreen.kt +++ b/app/src/main/java/to/bitkit/ui/SettingsScreen.kt @@ -96,6 +96,7 @@ private fun Mnemonic( InfoField( value = mnemonic, label = stringResource(R.string.mnemonic), + maxLength = 52, trailingIcon = { CopyToClipboardButton(mnemonic) }, ) } diff --git a/app/src/main/java/to/bitkit/ui/WalletScreen.kt b/app/src/main/java/to/bitkit/ui/WalletScreen.kt index 037679763..4a258e7af 100644 --- a/app/src/main/java/to/bitkit/ui/WalletScreen.kt +++ b/app/src/main/java/to/bitkit/ui/WalletScreen.kt @@ -53,6 +53,7 @@ fun WalletScreen( InfoField( value = uiState.ldkNodeId, label = stringResource(R.string.node_id), + maxLength = 44, trailingIcon = { CopyToClipboardButton(uiState.ldkNodeId) }, ) } @@ -75,6 +76,7 @@ fun WalletScreen( InfoField( value = uiState.btcAddress, label = stringResource(R.string.address), + maxLength = 36, trailingIcon = { Row { IconButton(onClick = viewModel::getNewAddress) { diff --git a/app/src/main/java/to/bitkit/ui/settings/PaymentsScreen.kt b/app/src/main/java/to/bitkit/ui/settings/PaymentsScreen.kt index f98c0023f..0ed18332e 100644 --- a/app/src/main/java/to/bitkit/ui/settings/PaymentsScreen.kt +++ b/app/src/main/java/to/bitkit/ui/settings/PaymentsScreen.kt @@ -34,6 +34,7 @@ fun PaymentsScreen( InfoField( value = invoiceToSend, label = "Send invoice", + maxLength = 44, trailingIcon = { CopyToClipboardButton(invoiceToSend) }, ) } diff --git a/app/src/main/java/to/bitkit/ui/settings/PeersScreen.kt b/app/src/main/java/to/bitkit/ui/settings/PeersScreen.kt index f707d1a19..ce12b68f4 100644 --- a/app/src/main/java/to/bitkit/ui/settings/PeersScreen.kt +++ b/app/src/main/java/to/bitkit/ui/settings/PeersScreen.kt @@ -51,7 +51,7 @@ fun PeersScreen( textStyle = MaterialTheme.typography.labelSmall, modifier = Modifier.fillMaxWidth(), ) - InfoField(value = host, label = "Host") + InfoField(value = host, label = "Host", maxLength = 72) OutlinedTextField( label = { Text("Port") }, value = port, diff --git a/app/src/main/java/to/bitkit/ui/shared/InfoField.kt b/app/src/main/java/to/bitkit/ui/shared/InfoField.kt index 7c998dd21..5f4daed1a 100644 --- a/app/src/main/java/to/bitkit/ui/shared/InfoField.kt +++ b/app/src/main/java/to/bitkit/ui/shared/InfoField.kt @@ -13,6 +13,7 @@ import to.bitkit.ui.shared.util.ellipsisVisualTransformation internal fun InfoField( value: String, label: String, + maxLength: Int, trailingIcon: @Composable (() -> Unit)? = null, onValueChange: (String) -> Unit = {}, ) { @@ -32,7 +33,7 @@ internal fun InfoField( ) }, textStyle = MaterialTheme.typography.labelSmall, - visualTransformation = ellipsisVisualTransformation(40), + visualTransformation = ellipsisVisualTransformation(maxLength), modifier = Modifier.fillMaxWidth(), ) } diff --git a/app/src/main/java/to/bitkit/ui/shared/util/EllipsisVisualTransformation.kt b/app/src/main/java/to/bitkit/ui/shared/util/EllipsisVisualTransformation.kt index 002115db2..401b45ed5 100644 --- a/app/src/main/java/to/bitkit/ui/shared/util/EllipsisVisualTransformation.kt +++ b/app/src/main/java/to/bitkit/ui/shared/util/EllipsisVisualTransformation.kt @@ -1,21 +1,28 @@ package to.bitkit.ui.shared.util -import androidx.compose.runtime.Composable import androidx.compose.ui.text.buildAnnotatedString import androidx.compose.ui.text.input.OffsetMapping import androidx.compose.ui.text.input.TransformedText import androidx.compose.ui.text.input.VisualTransformation -@Composable fun ellipsisVisualTransformation( maxLength: Int, ellipsis: String = "…", -) = VisualTransformation { - val text = if (it.length > maxLength) { - buildAnnotatedString { - append(it.take(maxLength - ellipsis.length)) - append(ellipsis) - } - } else it - TransformedText(text, OffsetMapping.Identity) +) = VisualTransformation { originalText -> + val transformedText = if (originalText.length > maxLength) buildAnnotatedString { + append(originalText.take(maxLength - ellipsis.length)) + append(ellipsis) + } + else originalText + + val oldLen = originalText.length + val newLen = transformedText.length + + val offsetMapping = object : OffsetMapping { + override fun originalToTransformed(offset: Int) = if (offset <= maxLength - ellipsis.length) offset else newLen + + override fun transformedToOriginal(offset: Int) = if (offset <= maxLength - ellipsis.length) offset else oldLen + } + + TransformedText(transformedText, offsetMapping) } From fdbc20687bf969dd8548ce6262ad4a5621c0e964 Mon Sep 17 00:00:00 2001 From: Ovi Trif Date: Wed, 11 Sep 2024 19:20:13 +0200 Subject: [PATCH 08/27] chore: Remove NDK version spec --- app/build.gradle.kts | 1 - 1 file changed, 1 deletion(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index a6ce4c454..a8f69fab0 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -13,7 +13,6 @@ plugins { android { namespace = "to.bitkit" compileSdk = 34 - ndkVersion = "26.1.10909125" // probably required by LDK bindings? - safer to keep it for now. defaultConfig { applicationId = "to.bitkit" minSdk = 28 From a0e5b1cae8049ed08e1ef4c5ac1d68405dd5dca2 Mon Sep 17 00:00:00 2001 From: Ovi Trif Date: Wed, 11 Sep 2024 22:11:58 +0200 Subject: [PATCH 09/27] feat: Declare error types --- app/src/main/java/to/bitkit/shared/Errors.kt | 418 +++++++++++++++++++ 1 file changed, 418 insertions(+) create mode 100644 app/src/main/java/to/bitkit/shared/Errors.kt diff --git a/app/src/main/java/to/bitkit/shared/Errors.kt b/app/src/main/java/to/bitkit/shared/Errors.kt new file mode 100644 index 000000000..7269bbbb9 --- /dev/null +++ b/app/src/main/java/to/bitkit/shared/Errors.kt @@ -0,0 +1,418 @@ +@file:Suppress("unused") + +package to.bitkit.shared + +import org.bitcoindevkit.AddressException +import org.bitcoindevkit.Bip32Exception +import org.bitcoindevkit.Bip39Exception +import org.bitcoindevkit.CalculateFeeException +import org.bitcoindevkit.CannotConnectException +import org.bitcoindevkit.CreateTxException +import org.bitcoindevkit.DescriptorException +import org.bitcoindevkit.DescriptorKeyException +import org.bitcoindevkit.ElectrumException +import org.bitcoindevkit.EsploraException +import org.bitcoindevkit.ExtractTxException +import org.bitcoindevkit.FeeRateException +import org.bitcoindevkit.ParseAmountException +import org.bitcoindevkit.PersistenceException +import org.bitcoindevkit.PsbtParseException +import org.bitcoindevkit.SignerException +import org.bitcoindevkit.TransactionException +import org.bitcoindevkit.TxidParseException +import org.bitcoindevkit.WalletCreationException +import org.lightningdevkit.ldknode.BuildException +import org.lightningdevkit.ldknode.NodeException + +open class AppError(override val message: String) : Exception(message) + +sealed class CustomServiceError(message: String) : AppError(message) { + class NodeNotSetup : CustomServiceError("Node is not setup.") + class NodeNotStarted : CustomServiceError("Node has not started.") + class OnchainWalletNotInitialized : CustomServiceError("Onchain wallet is not initialized.") + class LdkNodeSqliteAlreadyExists : CustomServiceError("LDK Node SQLite already exists.") + class LdkToLdkNodeMigration : CustomServiceError("Migration from LDK to LDK Node.") + class MnemonicNotFound : CustomServiceError("Mnemonic not found.") + class NodeStillRunning : CustomServiceError("Node is still running.") + class OnchainWalletStillRunning : CustomServiceError("Onchain wallet is still running.") + class InvalidNodeSigningMessage : CustomServiceError("Invalid node signing message.") +} + +sealed class KeychainError(message: String) : AppError(message) { + class FailedToSave : KeychainError("Failed to save.") + class FailedToSaveAlreadyExists : KeychainError("Failed to save, item already exists.") + class FailedToDelete : KeychainError("Failed to delete.") + class FailedToLoad : KeychainError("Failed to load.") + class KeychainWipeNotAllowed : KeychainError("Keychain wipe is not allowed.") +} + +sealed class BlocktankError(message: String) : AppError(message) { + class MissingResponse : BlocktankError("Missing response.") + class InvalidResponse : BlocktankError("Invalid response.") + class InvalidJson : BlocktankError("Invalid JSON.") + class MissingDeviceToken : BlocktankError("Missing device token.") +} + +sealed class LdkError(private val inner: LdkException) : AppError("Unknown LDK error.") { + constructor(inner: BuildException) : this(LdkException.Build(inner)) + constructor(inner: NodeException) : this(LdkException.Node(inner)) + + override val message get() = inner.message ?: super.message + + sealed interface LdkException { + val message: String? + + class Build(exception: BuildException) : LdkException { + override val message = when (exception) { + is BuildException.InvalidChannelMonitor -> "Invalid channel monitor." + is BuildException.InvalidSeedBytes -> "Invalid seed bytes." + is BuildException.InvalidSeedFile -> "Invalid seed file." + is BuildException.InvalidSystemTime -> "Invalid system time." + is BuildException.InvalidListeningAddresses -> "Invalid listening addresses." + is BuildException.ReadFailed -> "Read failed." + is BuildException.WriteFailed -> "Write failed." + is BuildException.StoragePathAccessFailed -> "Storage path access failed." + is BuildException.KvStoreSetupFailed -> "KV store setup failed." + is BuildException.WalletSetupFailed -> "Wallet setup failed." + is BuildException.LoggerSetupFailed -> "Logger setup failed." + else -> exception.message + }?.let { "LDK Build error: $it" } + } + + class Node(exception: NodeException) : LdkException { + override val message = when (exception) { + is NodeException.AlreadyRunning -> "The node is already running." + is NodeException.NotRunning -> "The node is not running." + is NodeException.OnchainTxCreationFailed -> "Failed to create on-chain transaction." + is NodeException.ConnectionFailed -> "Connection failed." + is NodeException.InvoiceCreationFailed -> "Invoice creation failed." + is NodeException.InvoiceRequestCreationFailed -> "Invoice request creation failed." + is NodeException.OfferCreationFailed -> "Offer creation failed." + is NodeException.RefundCreationFailed -> "Refund creation failed." + is NodeException.PaymentSendingFailed -> "Payment sending failed." + is NodeException.ProbeSendingFailed -> "Probe sending failed." + is NodeException.ChannelCreationFailed -> "Channel creation failed." + is NodeException.ChannelClosingFailed -> "Channel closing failed." + is NodeException.ChannelConfigUpdateFailed -> "Channel configuration update failed." + is NodeException.PersistenceFailed -> "Persistence failed." + is NodeException.FeerateEstimationUpdateFailed -> "Feerate estimation update failed." + is NodeException.FeerateEstimationUpdateTimeout -> "Feerate estimation update timeout." + is NodeException.WalletOperationFailed -> "Wallet operation failed." + is NodeException.WalletOperationTimeout -> "Wallet operation timeout." + is NodeException.OnchainTxSigningFailed -> "On-chain transaction signing failed." + is NodeException.MessageSigningFailed -> "Message signing failed." + is NodeException.TxSyncFailed -> "Transaction synchronization failed." + is NodeException.TxSyncTimeout -> "Transaction synchronization timeout." + is NodeException.GossipUpdateFailed -> "Gossip update failed." + is NodeException.GossipUpdateTimeout -> "Gossip update timeout." + is NodeException.LiquidityRequestFailed -> "Liquidity request failed." + is NodeException.InvalidAddress -> "Invalid address." + is NodeException.InvalidSocketAddress -> "Invalid socket address." + is NodeException.InvalidPublicKey -> "Invalid public key." + is NodeException.InvalidSecretKey -> "Invalid secret key." + is NodeException.InvalidOfferId -> "Invalid offer ID." + is NodeException.InvalidNodeId -> "Invalid node ID." + is NodeException.InvalidPaymentId -> "Invalid payment ID." + is NodeException.InvalidPaymentHash -> "Invalid payment hash." + is NodeException.InvalidPaymentPreimage -> "Invalid payment preimage." + is NodeException.InvalidPaymentSecret -> "Invalid payment secret." + is NodeException.InvalidAmount -> "Invalid amount." + is NodeException.InvalidInvoice -> "Invalid invoice." + is NodeException.InvalidOffer -> "Invalid offer." + is NodeException.InvalidRefund -> "Invalid refund." + is NodeException.InvalidChannelId -> "Invalid channel ID." + is NodeException.InvalidNetwork -> "Invalid network." + is NodeException.DuplicatePayment -> "Duplicate payment." + is NodeException.UnsupportedCurrency -> "Unsupported currency." + is NodeException.InsufficientFunds -> "Insufficient funds." + is NodeException.LiquiditySourceUnavailable -> "Liquidity source unavailable." + is NodeException.LiquidityFeeTooHigh -> "Liquidity fee too high." + else -> exception.message + }?.let { "LDK Node error: $it" } + } + } +} + +sealed class BdkError(private val inner: BdkException) : AppError("Unknown BDK error.") { + constructor(inner: AddressException) : this(BdkException.Address(inner)) + constructor(inner: Bip32Exception) : this(BdkException.Bip32(inner)) + constructor(inner: Bip39Exception) : this(BdkException.Bip39(inner)) + constructor(inner: CalculateFeeException) : this(BdkException.CalculateFee(inner)) + constructor(inner: CannotConnectException) : this(BdkException.CannotConnect(inner)) + constructor(inner: CreateTxException) : this(BdkException.CreateTx(inner)) + constructor(inner: DescriptorException) : this(BdkException.Descriptor(inner)) + constructor(inner: DescriptorKeyException) : this(BdkException.DescriptorKey(inner)) + constructor(inner: ElectrumException) : this(BdkException.Electrum(inner)) + constructor(inner: EsploraException) : this(BdkException.Esplora(inner)) + constructor(inner: ExtractTxException) : this(BdkException.ExtractTx(inner)) + constructor(inner: FeeRateException) : this(BdkException.FeeRate(inner)) + constructor(inner: ParseAmountException) : this(BdkException.ParseAmount(inner)) + constructor(inner: PersistenceException) : this(BdkException.Persistence(inner)) + constructor(inner: PsbtParseException) : this(BdkException.PsbtParse(inner)) + constructor(inner: SignerException) : this(BdkException.Signer(inner)) + constructor(inner: TransactionException) : this(BdkException.Transaction(inner)) + constructor(inner: TxidParseException) : this(BdkException.TxidParse(inner)) + constructor(inner: WalletCreationException) : this(BdkException.WalletCreation(inner)) + + override val message get() = inner.message ?: super.message + + sealed interface BdkException { + val message: String? + + class Address(exception: AddressException) : BdkException { + override val message = when (exception) { + is AddressException.OtherAddressErr -> "An unspecified address error occurred." + is AddressException.Base58 -> "Base58 encoding issue in the address." + is AddressException.Bech32 -> "Bech32 encoding issue in the address." + is AddressException.WitnessProgram -> "Witness program in address is invalid or corrupted." + is AddressException.ExcessiveScriptSize -> "The script size in the address is too large." + is AddressException.NetworkValidation -> "Network validation failed for the address." + is AddressException.UncompressedPubkey -> "Address contains an uncompressed public key." + is AddressException.UnrecognizedScript -> "Address script is not recognized or invalid." + is AddressException.WitnessVersion -> "Witness version in address is incorrect." + else -> exception.message + }?.let { "BDK Address error: $it" } + } + + class Bip32(exception: Bip32Exception) : BdkException { + override val message = when (exception) { + is Bip32Exception.Base58 -> "Base58 encoding issue in BIP32 key." + is Bip32Exception.InvalidChildNumber -> "Invalid child number in BIP32 key derivation." + is Bip32Exception.UnknownException -> "An unknown BIP32 error occurred." + is Bip32Exception.Hex -> "Hexadecimal encoding error in BIP32 key." + is Bip32Exception.CannotDeriveFromHardenedKey -> "Unable to derive key from a hardened key." + is Bip32Exception.InvalidDerivationPathFormat -> "Invalid format in BIP32 derivation path." + is Bip32Exception.InvalidPublicKeyHexLength -> "Public key hex length is invalid." + is Bip32Exception.Secp256k1 -> "SECP256k1 curve error in BIP32 key." + is Bip32Exception.UnknownVersion -> "Unknown BIP32 version." + is Bip32Exception.WrongExtendedKeyLength -> "Extended key length is incorrect." + is Bip32Exception.InvalidChildNumberFormat -> "Child number format in BIP32 key is invalid." + else -> exception.message + }?.let { "BIP32 error: $it" } + } + + class Bip39(exception: Bip39Exception) : BdkException { + override val message = when (exception) { + is Bip39Exception.AmbiguousLanguages -> "The specified language for BIP39 is ambiguous." + is Bip39Exception.BadEntropyBitCount -> "The entropy bit count in BIP39 is incorrect." + is Bip39Exception.BadWordCount -> "Word count in BIP39 mnemonic is incorrect." + is Bip39Exception.InvalidChecksum -> "Checksum validation failed in BIP39 mnemonic." + is Bip39Exception.UnknownWord -> "Unknown word found in BIP39 mnemonic." + else -> exception.message + }?.let { "BDK BIP39 error: $it" } + } + + class CalculateFee(exception: CalculateFeeException) : BdkException { + override val message = when (exception) { + is CalculateFeeException.MissingTxOut -> "Transaction output is missing during fee calculation." + is CalculateFeeException.NegativeFee -> "Calculated fee is negative." + else -> exception.message + }?.let { "BDK Calculate fee error: $it" } + } + + class CannotConnect(exception: CannotConnectException) : BdkException { + override val message = when (exception) { + is CannotConnectException.Include -> "Include-specific connection issue occurred." + else -> exception.message + }?.let { "BDK Cannot connect error: $it" } + } + + class CreateTx(exception: CreateTxException) : BdkException { + override val message = when (exception) { + is CreateTxException.ChangePolicyDescriptor -> "Error with change policy descriptor." + is CreateTxException.CoinSelection -> "Coin selection issue." + is CreateTxException.Descriptor -> "Descriptor issue." + is CreateTxException.FeeRateTooLow -> "Fee rate specified is too low." + is CreateTxException.FeeTooLow -> "The total fee is too low." + is CreateTxException.InsufficientFunds -> "Insufficient funds available." + is CreateTxException.LockTime -> "Lock time error." + is CreateTxException.MiniscriptPsbt -> "Miniscript PSBT error." + is CreateTxException.MissingKeyOrigin -> "Missing key origin information." + is CreateTxException.MissingNonWitnessUtxo -> "Missing non-witness UTXO." + is CreateTxException.NoRecipients -> "No recipients specified for the transaction." + is CreateTxException.NoUtxosSelected -> "No UTXOs selected for the transaction." + is CreateTxException.OutputBelowDustLimit -> "Transaction output is below the dust limit." + is CreateTxException.Persist -> "Persistence issue occurred during transaction creation." + is CreateTxException.Policy -> "Policy issue during transaction creation." + is CreateTxException.Psbt -> "PSBT issue during transaction creation." + is CreateTxException.RbfSequence -> "Replace-by-fee (RBF) sequence error." + is CreateTxException.RbfSequenceCsv -> "RBF sequence with CSV error." + is CreateTxException.SpendingPolicyRequired -> "Spending policy required but not provided." + is CreateTxException.UnknownUtxo -> "Unknown UTXO encountered during transaction creation." + is CreateTxException.Version0 -> "Version 0 specific issue." + is CreateTxException.Version1Csv -> "Version 1 CSV specific issue." + else -> exception.message + }?.let { "BDK Transaction creation error: $it" } + } + + class Descriptor(exception: DescriptorException) : BdkException { + override val message = when (exception) { + is DescriptorException.Base58 -> "Base58 encoding issue with the descriptor." + is DescriptorException.Bip32 -> "BIP32 specific error with the descriptor." + is DescriptorException.HardenedDerivationXpub -> "Error with hardened derivation in descriptor." + is DescriptorException.Hex -> "Hexadecimal encoding issue with the descriptor." + is DescriptorException.InvalidDescriptorCharacter -> "Invalid character found in descriptor." + is DescriptorException.InvalidDescriptorChecksum -> "Descriptor checksum validation failed." + is DescriptorException.InvalidHdKeyPath -> "Invalid HD key path in descriptor." + is DescriptorException.Key -> "Key issue in descriptor." + is DescriptorException.Miniscript -> "Miniscript issue with descriptor." + is DescriptorException.MultiPath -> "Multipath issue in descriptor." + is DescriptorException.Pk -> "Public key issue in descriptor." + is DescriptorException.Policy -> "Policy issue in descriptor." + else -> exception.message + }?.let { "BDK Descriptor error: $it" } + } + + class DescriptorKey(exception: DescriptorKeyException) : BdkException { + override val message = when (exception) { + is DescriptorKeyException.Bip32 -> "BIP32 key issue." + is DescriptorKeyException.InvalidKeyType -> "Invalid key type encountered." + is DescriptorKeyException.Parse -> "Error parsing the descriptor key." + else -> exception.message + }?.let { "BDK Descriptor key error: $it" } + } + + class Electrum(exception: ElectrumException) : BdkException { + override val message = when (exception) { + is ElectrumException.AllAttemptsErrored -> "All attempts to connect to Electrum server failed." + is ElectrumException.AlreadySubscribed -> "Already subscribed to the Electrum server." + is ElectrumException.Bitcoin -> "Bitcoin-specific error with Electrum server." + is ElectrumException.CouldNotCreateConnection -> "Failed to create a connection with Electrum server." + is ElectrumException.CouldntLockReader -> "Unable to lock reader for Electrum server." + is ElectrumException.Hex -> "Hexadecimal encoding issue with Electrum server response." + is ElectrumException.InvalidDnsNameException -> "Invalid DNS name provided for Electrum server." + is ElectrumException.InvalidResponse -> "Received an invalid response from the Electrum server." + is ElectrumException.IoException -> "I/O error occurred with Electrum server." + is ElectrumException.Json -> "JSON parsing error with Electrum server response." + is ElectrumException.Message -> "Message-related error with Electrum server." + is ElectrumException.MissingDomain -> "Missing domain in Electrum server configuration." + is ElectrumException.Mpsc -> "MPSC-related error with Electrum server." + is ElectrumException.NotSubscribed -> "Not subscribed to Electrum server." + is ElectrumException.Protocol -> "Protocol error with Electrum server." + is ElectrumException.RequestAlreadyConsumed -> "Request already consumed by Electrum server." + is ElectrumException.SharedIoException -> "Shared I/O error occurred in Electrum server." + else -> exception.message + }?.let { "BDK Electrum error: $it" } + } + + class Esplora(exception: EsploraException) : BdkException { + override val message = when (exception) { + is EsploraException.BitcoinEncoding -> "Bitcoin encoding error in Esplora." + is EsploraException.HeaderHashNotFound -> "Header hash not found in Esplora response." + is EsploraException.HeaderHeightNotFound -> "Header height not found in Esplora response." + is EsploraException.HexToArray -> "Error converting hex to array in Esplora." + is EsploraException.HexToBytes -> "Error converting hex to bytes in Esplora." + is EsploraException.HttpResponse -> "HTTP response error from Esplora server." + is EsploraException.InvalidHttpHeaderName -> "Invalid HTTP header name in Esplora response." + is EsploraException.InvalidHttpHeaderValue -> "Invalid HTTP header value in Esplora response." + is EsploraException.Minreq -> "Minimum requirements error in Esplora." + is EsploraException.Parsing -> "Parsing error with Esplora data." + is EsploraException.RequestAlreadyConsumed -> "Request already consumed by Esplora server." + is EsploraException.StatusCode -> "Unexpected status code from Esplora server." + is EsploraException.TransactionNotFound -> "Transaction not found in Esplora." + else -> exception.message + }?.let { "BDK Esplora error: $it" } + } + + class ExtractTx(exception: ExtractTxException) : BdkException { + override val message = when (exception) { + is ExtractTxException.AbsurdFeeRate -> "Absurd fee rate encountered." + is ExtractTxException.MissingInputValue -> "Missing input value." + is ExtractTxException.OtherExtractTxErr -> "An unspecified error occurred." + is ExtractTxException.SendingTooMuch -> "Sending amount exceeds allowable limits." + else -> exception.message + }?.let { "BDK Extract transaction error: $it" } + } + + class FeeRate(exception: FeeRateException) : BdkException { + override val message = when (exception) { + is FeeRateException.ArithmeticOverflow -> "Arithmetic overflow occurred while calculating fee rate." + else -> exception.message + }?.let { "BDK Fee rate error: $it" } + } + + class ParseAmount(exception: ParseAmountException) : BdkException { + override val message = when (exception) { + is ParseAmountException.InputTooLarge -> "Input amount is too large to parse." + is ParseAmountException.InvalidCharacter -> "Invalid character found in amount parsing." + is ParseAmountException.InvalidFormat -> "Amount format is invalid." + is ParseAmountException.Negative -> "Negative amount encountered where not expected." + is ParseAmountException.OtherParseAmountErr -> "An unspecified error occurred while parsing amount." + is ParseAmountException.PossiblyConfusingDenomination -> "Confusing denomination in amount parsing." + is ParseAmountException.TooBig -> "Amount is too large to process." + is ParseAmountException.TooPrecise -> "Amount precision is too high." + is ParseAmountException.UnknownDenomination -> "Unknown denomination encountered in amount parsing." + else -> exception.message + }?.let { "BDK Amount parsing error: $it" } + } + + class Persistence(exception: PersistenceException) : BdkException { + override val message = when (exception) { + is PersistenceException.Write -> "Write operation failed in persistence layer." + else -> exception.message + }?.let { "BDK Persistence error: $it" } + } + + class PsbtParse(exception: PsbtParseException) : BdkException { + override val message = when (exception) { + is PsbtParseException.Base64Encoding -> "Base64 encoding issue with PSBT." + is PsbtParseException.PsbtEncoding -> "Error in PSBT encoding." + else -> exception.message + }?.let { "BDK PSBT parsing error: $it" } + } + + class Signer(exception: SignerException) : BdkException { + override val message = when (exception) { + is SignerException.External -> "External signer error." + is SignerException.InputIndexOutOfRange -> "Input index out of range during signing." + is SignerException.InvalidKey -> "Invalid key encountered during signing." + is SignerException.InvalidNonWitnessUtxo -> "Invalid non-witness UTXO encountered." + is SignerException.InvalidSighash -> "Invalid sighash encountered during signing." + is SignerException.MiniscriptPsbt -> "Miniscript PSBT issue during signing." + is SignerException.MissingHdKeypath -> "Missing HD keypath in signing process." + is SignerException.MissingKey -> "Missing key in signing process." + is SignerException.MissingNonWitnessUtxo -> "Missing non-witness UTXO during signing." + is SignerException.MissingWitnessScript -> "Missing witness script during signing." + is SignerException.MissingWitnessUtxo -> "Missing witness UTXO during signing." + is SignerException.NonStandardSighash -> "Non-standard sighash encountered." + is SignerException.SighashException -> "Sighash exception encountered during signing." + is SignerException.UserCanceled -> "Signing operation was canceled by the user." + else -> exception.message + }?.let { "BDK Signer error: $it" } + } + + class Transaction(exception: TransactionException) : BdkException { + override val message = when (exception) { + is TransactionException.InvalidChecksum -> "Invalid checksum in transaction." + is TransactionException.Io -> "I/O error occurred with transaction processing." + is TransactionException.NonMinimalVarInt -> "Non-minimal VARINT encountered in transaction." + is TransactionException.OtherTransactionErr -> "An unspecified transaction error occurred." + is TransactionException.OversizedVectorAllocation -> "Vector allocation in transaction is oversized." + is TransactionException.ParseFailed -> "Transaction parsing failed." + is TransactionException.UnsupportedSegwitFlag -> "Unsupported SegWit flag encountered." + else -> exception.message + }?.let { "BDK Transaction error: $it" } + } + + class TxidParse(exception: TxidParseException) : BdkException { + override val message = when (exception) { + is TxidParseException.InvalidTxid -> "Invalid TXID format encountered." + else -> exception.message + }?.let { "BDK TXID parsing error: $it" } + } + + class WalletCreation(exception: WalletCreationException) : BdkException { + override val message = when (exception) { + is WalletCreationException.Descriptor -> "Descriptor error during wallet creation." + is WalletCreationException.InvalidMagicBytes -> "Invalid magic bytes encountered in wallet creation." + is WalletCreationException.Io -> "I/O error during wallet creation." + is WalletCreationException.LoadedDescriptorDoesNotMatch -> "Loaded descriptor doesn't match expected." + is WalletCreationException.LoadedGenesisDoesNotMatch -> "Loaded genesis block doesn't match expected." + is WalletCreationException.LoadedNetworkDoesNotMatch -> "Loaded network doesn't match expected." + is WalletCreationException.NotInitialized -> "Wallet has not been initialized." + is WalletCreationException.Persist -> "Persistence issue encountered during wallet creation." + else -> exception.message + }?.let { "BDK Wallet creation error: $it" } + } + } +} From 4453d8cdbddc1df6485df51cf693d45098d11340 Mon Sep 17 00:00:00 2001 From: Ovi Trif Date: Wed, 11 Sep 2024 22:57:18 +0200 Subject: [PATCH 10/27] refactor: Move Constants to env package --- app/src/main/java/to/bitkit/App.kt | 1 + app/src/main/java/to/bitkit/async/ServiceQueue.kt | 2 +- app/src/main/java/to/bitkit/data/AppDb.kt | 2 +- app/src/main/java/to/bitkit/data/RestApi.kt | 2 +- .../java/to/bitkit/data/keychain/KeychainStore.kt | 2 +- app/src/main/java/to/bitkit/{ => env}/Constants.kt | 8 +++----- app/src/main/java/to/bitkit/ext/Context.kt | 2 +- app/src/main/java/to/bitkit/fcm/FcmService.kt | 2 +- app/src/main/java/to/bitkit/fcm/Wake2PayWorker.kt | 2 +- .../main/java/to/bitkit/services/BitcoinService.kt | 8 ++++---- .../main/java/to/bitkit/services/BlocktankService.kt | 2 +- .../main/java/to/bitkit/services/LightningService.kt | 12 ++++++------ .../main/java/to/bitkit/services/MigrationService.kt | 4 ++-- app/src/main/java/to/bitkit/shared/Perf.kt | 2 +- app/src/main/java/to/bitkit/ui/Notifications.kt | 2 +- app/src/main/java/to/bitkit/ui/SettingsScreen.kt | 2 +- app/src/main/java/to/bitkit/ui/SharedViewModel.kt | 6 ++---- app/src/main/java/to/bitkit/ui/WalletViewModel.kt | 4 ++-- .../main/java/to/bitkit/ui/settings/PeersScreen.kt | 4 ++-- app/src/main/java/to/bitkit/ui/shared/Peers.kt | 2 +- 20 files changed, 34 insertions(+), 37 deletions(-) rename app/src/main/java/to/bitkit/{ => env}/Constants.kt (96%) diff --git a/app/src/main/java/to/bitkit/App.kt b/app/src/main/java/to/bitkit/App.kt index e4c10c2b6..48517898a 100644 --- a/app/src/main/java/to/bitkit/App.kt +++ b/app/src/main/java/to/bitkit/App.kt @@ -7,6 +7,7 @@ import android.os.Bundle import androidx.hilt.work.HiltWorkerFactory import androidx.work.Configuration import dagger.hilt.android.HiltAndroidApp +import to.bitkit.env.Env import javax.inject.Inject import kotlin.reflect.typeOf diff --git a/app/src/main/java/to/bitkit/async/ServiceQueue.kt b/app/src/main/java/to/bitkit/async/ServiceQueue.kt index b8dd2f3fd..1a8147168 100644 --- a/app/src/main/java/to/bitkit/async/ServiceQueue.kt +++ b/app/src/main/java/to/bitkit/async/ServiceQueue.kt @@ -6,7 +6,7 @@ import kotlinx.coroutines.ExecutorCoroutineDispatcher import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.asCoroutineDispatcher import kotlinx.coroutines.withContext -import to.bitkit.Tag.APP +import to.bitkit.env.Tag.APP import to.bitkit.ext.callerName import to.bitkit.shared.measured import java.util.concurrent.Executors diff --git a/app/src/main/java/to/bitkit/data/AppDb.kt b/app/src/main/java/to/bitkit/data/AppDb.kt index 371d4ba5d..fce4e741e 100644 --- a/app/src/main/java/to/bitkit/data/AppDb.kt +++ b/app/src/main/java/to/bitkit/data/AppDb.kt @@ -15,7 +15,7 @@ import androidx.work.WorkerParameters import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.flow.Flow import to.bitkit.BuildConfig -import to.bitkit.Env +import to.bitkit.env.Env import to.bitkit.data.entities.ConfigEntity @Database( diff --git a/app/src/main/java/to/bitkit/data/RestApi.kt b/app/src/main/java/to/bitkit/data/RestApi.kt index 3a3be5f1a..ddfc6294c 100644 --- a/app/src/main/java/to/bitkit/data/RestApi.kt +++ b/app/src/main/java/to/bitkit/data/RestApi.kt @@ -8,7 +8,7 @@ import io.ktor.client.request.setBody import io.ktor.client.statement.HttpResponse import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable -import to.bitkit.REST +import to.bitkit.env.REST import to.bitkit.ext.toHex import javax.inject.Inject diff --git a/app/src/main/java/to/bitkit/data/keychain/KeychainStore.kt b/app/src/main/java/to/bitkit/data/keychain/KeychainStore.kt index 1f6f67cc5..a2718c5f2 100644 --- a/app/src/main/java/to/bitkit/data/keychain/KeychainStore.kt +++ b/app/src/main/java/to/bitkit/data/keychain/KeychainStore.kt @@ -11,7 +11,7 @@ import androidx.datastore.preferences.preferencesDataStore import dagger.hilt.android.qualifiers.ApplicationContext import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.flow.first -import to.bitkit.Tag.APP +import to.bitkit.env.Tag.APP import to.bitkit.async.BaseCoroutineScope import to.bitkit.data.AppDb import to.bitkit.di.IoDispatcher diff --git a/app/src/main/java/to/bitkit/Constants.kt b/app/src/main/java/to/bitkit/env/Constants.kt similarity index 96% rename from app/src/main/java/to/bitkit/Constants.kt rename to app/src/main/java/to/bitkit/env/Constants.kt index 62974973f..37a12091d 100644 --- a/app/src/main/java/to/bitkit/Constants.kt +++ b/app/src/main/java/to/bitkit/env/Constants.kt @@ -1,11 +1,9 @@ -@file:Suppress("unused") - -package to.bitkit +package to.bitkit.env import android.util.Log import org.lightningdevkit.ldknode.PeerDetails -import to.bitkit.Tag.APP -import to.bitkit.env.Network +import to.bitkit.BuildConfig +import to.bitkit.env.Tag.APP import to.bitkit.ext.ensureDir import kotlin.io.path.Path import org.lightningdevkit.ldknode.Network as LdkNetwork diff --git a/app/src/main/java/to/bitkit/ext/Context.kt b/app/src/main/java/to/bitkit/ext/Context.kt index 57eb28d62..b5769f6a8 100644 --- a/app/src/main/java/to/bitkit/ext/Context.kt +++ b/app/src/main/java/to/bitkit/ext/Context.kt @@ -10,7 +10,7 @@ import android.util.Log import android.widget.Toast import androidx.core.app.NotificationManagerCompat import androidx.core.content.ContextCompat -import to.bitkit.Tag.APP +import to.bitkit.env.Tag.APP import to.bitkit.currentActivity import to.bitkit.ui.MainActivity import java.io.File diff --git a/app/src/main/java/to/bitkit/fcm/FcmService.kt b/app/src/main/java/to/bitkit/fcm/FcmService.kt index 84dfd386a..d32573656 100644 --- a/app/src/main/java/to/bitkit/fcm/FcmService.kt +++ b/app/src/main/java/to/bitkit/fcm/FcmService.kt @@ -12,7 +12,7 @@ import com.google.firebase.messaging.RemoteMessage import kotlinx.serialization.Serializable import kotlinx.serialization.SerializationException import kotlinx.serialization.encodeToString -import to.bitkit.Tag.FCM +import to.bitkit.env.Tag.FCM import to.bitkit.di.json import to.bitkit.ui.pushNotification import java.util.Date diff --git a/app/src/main/java/to/bitkit/fcm/Wake2PayWorker.kt b/app/src/main/java/to/bitkit/fcm/Wake2PayWorker.kt index ff5973e34..79895eeff 100644 --- a/app/src/main/java/to/bitkit/fcm/Wake2PayWorker.kt +++ b/app/src/main/java/to/bitkit/fcm/Wake2PayWorker.kt @@ -8,7 +8,7 @@ import androidx.work.WorkerParameters import androidx.work.workDataOf import dagger.assisted.Assisted import dagger.assisted.AssistedInject -import to.bitkit.Tag.FCM +import to.bitkit.env.Tag.FCM import to.bitkit.services.LightningService import to.bitkit.services.payInvoice import to.bitkit.services.warmupNode diff --git a/app/src/main/java/to/bitkit/services/BitcoinService.kt b/app/src/main/java/to/bitkit/services/BitcoinService.kt index baa9ebe53..92fb9c03c 100644 --- a/app/src/main/java/to/bitkit/services/BitcoinService.kt +++ b/app/src/main/java/to/bitkit/services/BitcoinService.kt @@ -9,10 +9,10 @@ import org.bitcoindevkit.EsploraClient import org.bitcoindevkit.KeychainKind import org.bitcoindevkit.Mnemonic import org.bitcoindevkit.Wallet -import to.bitkit.Env -import to.bitkit.REST -import to.bitkit.SEED -import to.bitkit.Tag.BDK +import to.bitkit.env.Env +import to.bitkit.env.REST +import to.bitkit.env.SEED +import to.bitkit.env.Tag.BDK import to.bitkit.async.BaseCoroutineScope import to.bitkit.di.BgDispatcher import to.bitkit.async.ServiceQueue diff --git a/app/src/main/java/to/bitkit/services/BlocktankService.kt b/app/src/main/java/to/bitkit/services/BlocktankService.kt index db1b42ae6..af0ccda96 100644 --- a/app/src/main/java/to/bitkit/services/BlocktankService.kt +++ b/app/src/main/java/to/bitkit/services/BlocktankService.kt @@ -2,7 +2,7 @@ package to.bitkit.services import android.util.Log import kotlinx.coroutines.CoroutineDispatcher -import to.bitkit.Tag.LSP +import to.bitkit.env.Tag.LSP import to.bitkit.async.BaseCoroutineScope import to.bitkit.async.ServiceQueue import to.bitkit.data.LspApi diff --git a/app/src/main/java/to/bitkit/services/LightningService.kt b/app/src/main/java/to/bitkit/services/LightningService.kt index 9954d00ee..39e996a11 100644 --- a/app/src/main/java/to/bitkit/services/LightningService.kt +++ b/app/src/main/java/to/bitkit/services/LightningService.kt @@ -9,12 +9,12 @@ import org.lightningdevkit.ldknode.Event import org.lightningdevkit.ldknode.LogLevel import org.lightningdevkit.ldknode.Node import org.lightningdevkit.ldknode.defaultConfig -import to.bitkit.Env -import to.bitkit.LnPeer -import to.bitkit.LnPeer.Companion.toLnPeer -import to.bitkit.REST -import to.bitkit.SEED -import to.bitkit.Tag.LDK +import to.bitkit.env.Env +import to.bitkit.env.LnPeer +import to.bitkit.env.LnPeer.Companion.toLnPeer +import to.bitkit.env.REST +import to.bitkit.env.SEED +import to.bitkit.env.Tag.LDK import to.bitkit.async.BaseCoroutineScope import to.bitkit.async.ServiceQueue import to.bitkit.di.BgDispatcher diff --git a/app/src/main/java/to/bitkit/services/MigrationService.kt b/app/src/main/java/to/bitkit/services/MigrationService.kt index 0feab329f..76a17fe64 100644 --- a/app/src/main/java/to/bitkit/services/MigrationService.kt +++ b/app/src/main/java/to/bitkit/services/MigrationService.kt @@ -7,8 +7,8 @@ import android.database.sqlite.SQLiteOpenHelper import android.util.Log import dagger.hilt.android.qualifiers.ApplicationContext import org.ldk.structs.KeysManager -import to.bitkit.Env -import to.bitkit.Tag.LDK +import to.bitkit.env.Env +import to.bitkit.env.Tag.LDK import to.bitkit.ext.hex import java.io.File import javax.inject.Inject diff --git a/app/src/main/java/to/bitkit/shared/Perf.kt b/app/src/main/java/to/bitkit/shared/Perf.kt index b424ef77c..b39ea367e 100644 --- a/app/src/main/java/to/bitkit/shared/Perf.kt +++ b/app/src/main/java/to/bitkit/shared/Perf.kt @@ -1,7 +1,7 @@ package to.bitkit.shared import android.util.Log -import to.bitkit.Tag.PERF +import to.bitkit.env.Tag.PERF import kotlin.system.measureTimeMillis internal inline fun measured( diff --git a/app/src/main/java/to/bitkit/ui/Notifications.kt b/app/src/main/java/to/bitkit/ui/Notifications.kt index f5876e2c2..30f884942 100644 --- a/app/src/main/java/to/bitkit/ui/Notifications.kt +++ b/app/src/main/java/to/bitkit/ui/Notifications.kt @@ -18,7 +18,7 @@ import androidx.core.app.NotificationCompat import com.google.android.gms.tasks.OnCompleteListener import com.google.firebase.messaging.FirebaseMessaging import to.bitkit.R -import to.bitkit.Tag.FCM +import to.bitkit.env.Tag.FCM import to.bitkit.currentActivity import to.bitkit.ext.notificationManager import to.bitkit.ext.notificationManagerCompat diff --git a/app/src/main/java/to/bitkit/ui/SettingsScreen.kt b/app/src/main/java/to/bitkit/ui/SettingsScreen.kt index b209d4635..8fa324622 100644 --- a/app/src/main/java/to/bitkit/ui/SettingsScreen.kt +++ b/app/src/main/java/to/bitkit/ui/SettingsScreen.kt @@ -21,7 +21,7 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import androidx.navigation.NavController import to.bitkit.R -import to.bitkit.SEED +import to.bitkit.env.SEED import to.bitkit.ui.shared.InfoField @Composable diff --git a/app/src/main/java/to/bitkit/ui/SharedViewModel.kt b/app/src/main/java/to/bitkit/ui/SharedViewModel.kt index 9a1eee59a..4ff2c93d3 100644 --- a/app/src/main/java/to/bitkit/ui/SharedViewModel.kt +++ b/app/src/main/java/to/bitkit/ui/SharedViewModel.kt @@ -1,7 +1,6 @@ package to.bitkit.ui import android.util.Log -import androidx.compose.runtime.mutableStateOf import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.google.firebase.messaging.FirebaseMessaging @@ -10,9 +9,8 @@ import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.launch import kotlinx.coroutines.runBlocking import kotlinx.coroutines.tasks.await -import to.bitkit.SEED -import to.bitkit.Tag.DEV -import to.bitkit.Tag.LSP +import to.bitkit.env.Tag.DEV +import to.bitkit.env.Tag.LSP import to.bitkit.data.AppDb import to.bitkit.data.keychain.KeychainStore import to.bitkit.di.BgDispatcher diff --git a/app/src/main/java/to/bitkit/ui/WalletViewModel.kt b/app/src/main/java/to/bitkit/ui/WalletViewModel.kt index 27a7943ff..fccf54f09 100644 --- a/app/src/main/java/to/bitkit/ui/WalletViewModel.kt +++ b/app/src/main/java/to/bitkit/ui/WalletViewModel.kt @@ -9,8 +9,8 @@ import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.launch import org.lightningdevkit.ldknode.ChannelDetails -import to.bitkit.LnPeer -import to.bitkit.SEED +import to.bitkit.env.LnPeer +import to.bitkit.env.SEED import to.bitkit.di.BgDispatcher import to.bitkit.services.BitcoinService import to.bitkit.services.LightningService diff --git a/app/src/main/java/to/bitkit/ui/settings/PeersScreen.kt b/app/src/main/java/to/bitkit/ui/settings/PeersScreen.kt index ce12b68f4..8f833da95 100644 --- a/app/src/main/java/to/bitkit/ui/settings/PeersScreen.kt +++ b/app/src/main/java/to/bitkit/ui/settings/PeersScreen.kt @@ -17,8 +17,8 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp -import to.bitkit.LnPeer -import to.bitkit.PEER +import to.bitkit.env.LnPeer +import to.bitkit.env.PEER import to.bitkit.R import to.bitkit.ui.MainUiState import to.bitkit.ui.WalletViewModel diff --git a/app/src/main/java/to/bitkit/ui/shared/Peers.kt b/app/src/main/java/to/bitkit/ui/shared/Peers.kt index 668c3b703..13881d4d1 100644 --- a/app/src/main/java/to/bitkit/ui/shared/Peers.kt +++ b/app/src/main/java/to/bitkit/ui/shared/Peers.kt @@ -22,7 +22,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp -import to.bitkit.LnPeer +import to.bitkit.env.LnPeer import to.bitkit.R @Composable From e27610dc500491cef5871281d56d4d0120579a7e Mon Sep 17 00:00:00 2001 From: Ovi Trif Date: Thu, 12 Sep 2024 18:26:50 +0200 Subject: [PATCH 11/27] chore: Print encrypted keychain value in ui test --- app/src/main/java/to/bitkit/data/keychain/KeychainStore.kt | 2 +- app/src/main/java/to/bitkit/ui/SharedViewModel.kt | 3 +++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/to/bitkit/data/keychain/KeychainStore.kt b/app/src/main/java/to/bitkit/data/keychain/KeychainStore.kt index a2718c5f2..4a4f156cc 100644 --- a/app/src/main/java/to/bitkit/data/keychain/KeychainStore.kt +++ b/app/src/main/java/to/bitkit/data/keychain/KeychainStore.kt @@ -48,7 +48,7 @@ class KeychainStore @Inject constructor( suspend fun delete(key: String) { context.keychain.edit { it.remove(key.indexed) } - Log.d(APP, "Deleted from keychain: $key ") + Log.d(APP, "Deleted from keychain: $key") } fun exists(key: String): Boolean { diff --git a/app/src/main/java/to/bitkit/ui/SharedViewModel.kt b/app/src/main/java/to/bitkit/ui/SharedViewModel.kt index 4ff2c93d3..adf9ac49b 100644 --- a/app/src/main/java/to/bitkit/ui/SharedViewModel.kt +++ b/app/src/main/java/to/bitkit/ui/SharedViewModel.kt @@ -14,6 +14,7 @@ import to.bitkit.env.Tag.LSP import to.bitkit.data.AppDb import to.bitkit.data.keychain.KeychainStore import to.bitkit.di.BgDispatcher +import to.bitkit.env.Tag.APP import to.bitkit.services.BitcoinService import to.bitkit.services.BlocktankService import javax.inject.Inject @@ -56,6 +57,8 @@ class SharedViewModel @Inject constructor( viewModelScope.launch { val key = "test" if (keychain.exists(key)) { + val value = keychain.loadString(key) + Log.d(APP, "Keychain entry: $key = $value") keychain.delete(key) } keychain.saveString(key, "testValue") From 6ed233c2581a9ecf3f46e0c2a36dc1253b4d223b Mon Sep 17 00:00:00 2001 From: Ovi Trif Date: Thu, 12 Sep 2024 22:19:03 +0200 Subject: [PATCH 12/27] feat: Use Keychain typed errors --- .../to/bitkit/data/keychain/KeychainStore.kt | 35 +++++++++++++------ app/src/main/java/to/bitkit/shared/Errors.kt | 30 ++++++++++++---- 2 files changed, 48 insertions(+), 17 deletions(-) diff --git a/app/src/main/java/to/bitkit/data/keychain/KeychainStore.kt b/app/src/main/java/to/bitkit/data/keychain/KeychainStore.kt index 4a4f156cc..10c0b9fd8 100644 --- a/app/src/main/java/to/bitkit/data/keychain/KeychainStore.kt +++ b/app/src/main/java/to/bitkit/data/keychain/KeychainStore.kt @@ -11,12 +11,15 @@ import androidx.datastore.preferences.preferencesDataStore import dagger.hilt.android.qualifiers.ApplicationContext import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.flow.first -import to.bitkit.env.Tag.APP import to.bitkit.async.BaseCoroutineScope import to.bitkit.data.AppDb import to.bitkit.di.IoDispatcher +import to.bitkit.env.Env +import to.bitkit.env.Network +import to.bitkit.env.Tag.APP import to.bitkit.ext.fromBase64 import to.bitkit.ext.toBase64 +import to.bitkit.shared.KeychainError import javax.inject.Inject class KeychainStore @Inject constructor( @@ -28,26 +31,36 @@ class KeychainStore @Inject constructor( private val keyStore by lazy { AndroidKeyStore(alias) } private val Context.keychain by preferencesDataStore(alias, scope = this) - val snapshot get() = runBlocking { context.keychain.data.first() } + val snapshot get() = runBlocking(this.coroutineContext) { context.keychain.data.first() } - fun loadString(key: String): String? = load(key)?.let { keyStore.decrypt(it) } + fun loadString(key: String): String? = load(key)?.let(keyStore::decrypt) private fun load(key: String): ByteArray? { - return snapshot[key.indexed]?.fromBase64() + try { + return snapshot[key.indexed]?.fromBase64() + } catch (e: Exception) { + throw KeychainError.FailedToLoad(key) + } } - suspend fun saveString(key: String, value: String) = save(key, value.let { keyStore.encrypt(it) }) + suspend fun saveString(key: String, value: String) = save(key, value.let(keyStore::encrypt)) private suspend fun save(key: String, encryptedValue: ByteArray) { - require(!exists(key)) { "Entry $key exists. Explicitly delete it first to update value." } - context.keychain.edit { it[key.indexed] = encryptedValue.toBase64() } - + if (exists(key)) throw KeychainError.FailedToSaveAlreadyExists(key) + try { + context.keychain.edit { it[key.indexed] = encryptedValue.toBase64() } + } catch (e: Exception) { + throw KeychainError.FailedToSave(key) + } Log.i(APP, "Saved to keychain: $key") } suspend fun delete(key: String) { - context.keychain.edit { it.remove(key.indexed) } - + try { + context.keychain.edit { it.remove(key.indexed) } + } catch (e: Exception) { + throw KeychainError.FailedToDelete(key) + } Log.d(APP, "Deleted from keychain: $key") } @@ -56,6 +69,8 @@ class KeychainStore @Inject constructor( } suspend fun wipe() { + if (!Env.isDebug || Env.network != Network.Regtest) throw KeychainError.KeychainWipeNotAllowed() + val keys = snapshot.asMap().keys context.keychain.edit { it.clear() } diff --git a/app/src/main/java/to/bitkit/shared/Errors.kt b/app/src/main/java/to/bitkit/shared/Errors.kt index 7269bbbb9..3c9d4ed97 100644 --- a/app/src/main/java/to/bitkit/shared/Errors.kt +++ b/app/src/main/java/to/bitkit/shared/Errors.kt @@ -1,4 +1,4 @@ -@file:Suppress("unused") +// @file:Suppress("unused") package to.bitkit.shared @@ -24,7 +24,17 @@ import org.bitcoindevkit.WalletCreationException import org.lightningdevkit.ldknode.BuildException import org.lightningdevkit.ldknode.NodeException -open class AppError(override val message: String) : Exception(message) +open class AppError(override val message: String) : Exception(message) { + companion object { + @Suppress("ConstPropertyName") + private const val serialVersionUID = 1L + } + + fun readResolve(): Any { + // Return a new instance of the class, or handle it if needed + return this + } +} sealed class CustomServiceError(message: String) : AppError(message) { class NodeNotSetup : CustomServiceError("Node is not setup.") @@ -39,11 +49,13 @@ sealed class CustomServiceError(message: String) : AppError(message) { } sealed class KeychainError(message: String) : AppError(message) { - class FailedToSave : KeychainError("Failed to save.") - class FailedToSaveAlreadyExists : KeychainError("Failed to save, item already exists.") - class FailedToDelete : KeychainError("Failed to delete.") - class FailedToLoad : KeychainError("Failed to load.") - class KeychainWipeNotAllowed : KeychainError("Keychain wipe is not allowed.") + class FailedToDelete(key: String) : KeychainError("Failed to delete $key from keychain.") + class FailedToLoad(key: String) : KeychainError("Failed to load $key from keychain.") + class FailedToSave(key: String) : KeychainError("Failed to save to $key keychain.") + class FailedToSaveAlreadyExists(key: String) : + KeychainError("Key $key already exists in keychain. Explicitly delete key before attempting to update value.") + + class KeychainWipeNotAllowed : KeychainError("Wiping keychain is only allowed in debug mode for regtest") } sealed class BlocktankError(message: String) : AppError(message) { @@ -53,6 +65,7 @@ sealed class BlocktankError(message: String) : AppError(message) { class MissingDeviceToken : BlocktankError("Missing device token.") } +// region ldk sealed class LdkError(private val inner: LdkException) : AppError("Unknown LDK error.") { constructor(inner: BuildException) : this(LdkException.Build(inner)) constructor(inner: NodeException) : this(LdkException.Node(inner)) @@ -132,7 +145,9 @@ sealed class LdkError(private val inner: LdkException) : AppError("Unknown LDK e } } } +// endregion +// region bdk sealed class BdkError(private val inner: BdkException) : AppError("Unknown BDK error.") { constructor(inner: AddressException) : this(BdkException.Address(inner)) constructor(inner: Bip32Exception) : this(BdkException.Bip32(inner)) @@ -416,3 +431,4 @@ sealed class BdkError(private val inner: BdkException) : AppError("Unknown BDK e } } } +// endregion From 9fc95f906cb6c317fb812dff72b30c2337d658d7 Mon Sep 17 00:00:00 2001 From: Ovi Trif Date: Sun, 15 Sep 2024 22:52:21 +0300 Subject: [PATCH 13/27] feat: Use typed errors in Lightning service --- app/src/main/java/to/bitkit/data/LspApi.kt | 1 - app/src/main/java/to/bitkit/ext/ByteArray.kt | 8 +- .../main/java/to/bitkit/fcm/Wake2PayWorker.kt | 1 - .../to/bitkit/services/BlocktankService.kt | 5 +- .../to/bitkit/services/LightningService.kt | 226 ++++++++++-------- app/src/main/java/to/bitkit/shared/Errors.kt | 24 +- .../main/java/to/bitkit/ui/WalletViewModel.kt | 36 +-- 7 files changed, 161 insertions(+), 140 deletions(-) diff --git a/app/src/main/java/to/bitkit/data/LspApi.kt b/app/src/main/java/to/bitkit/data/LspApi.kt index 0dff0fb8f..734140afa 100644 --- a/app/src/main/java/to/bitkit/data/LspApi.kt +++ b/app/src/main/java/to/bitkit/data/LspApi.kt @@ -3,7 +3,6 @@ package to.bitkit.data import io.ktor.client.HttpClient import io.ktor.client.request.post import io.ktor.client.request.setBody -import io.ktor.client.statement.HttpResponse import kotlinx.serialization.Serializable import javax.inject.Inject diff --git a/app/src/main/java/to/bitkit/ext/ByteArray.kt b/app/src/main/java/to/bitkit/ext/ByteArray.kt index dfdfc1afc..36492f358 100644 --- a/app/src/main/java/to/bitkit/ext/ByteArray.kt +++ b/app/src/main/java/to/bitkit/ext/ByteArray.kt @@ -26,11 +26,9 @@ fun String.asByteArray(): ByteArray { } fun Any.convertToByteArray(): ByteArray { - val bos = ByteArrayOutputStream() - val oos = ObjectOutputStream(bos) - oos.writeObject(this) - oos.flush() - return bos.toByteArray() + val byteArrayOutputStream = ByteArrayOutputStream() + ObjectOutputStream(byteArrayOutputStream).use { it.writeObject(this) } + return byteArrayOutputStream.toByteArray() } fun ByteArray.toBase64(flags: Int = Base64.DEFAULT): String = Base64.encodeToString(this, flags) diff --git a/app/src/main/java/to/bitkit/fcm/Wake2PayWorker.kt b/app/src/main/java/to/bitkit/fcm/Wake2PayWorker.kt index 79895eeff..397e0bfff 100644 --- a/app/src/main/java/to/bitkit/fcm/Wake2PayWorker.kt +++ b/app/src/main/java/to/bitkit/fcm/Wake2PayWorker.kt @@ -10,7 +10,6 @@ import dagger.assisted.Assisted import dagger.assisted.AssistedInject import to.bitkit.env.Tag.FCM import to.bitkit.services.LightningService -import to.bitkit.services.payInvoice import to.bitkit.services.warmupNode @HiltWorker diff --git a/app/src/main/java/to/bitkit/services/BlocktankService.kt b/app/src/main/java/to/bitkit/services/BlocktankService.kt index af0ccda96..6069889ad 100644 --- a/app/src/main/java/to/bitkit/services/BlocktankService.kt +++ b/app/src/main/java/to/bitkit/services/BlocktankService.kt @@ -2,13 +2,14 @@ package to.bitkit.services import android.util.Log import kotlinx.coroutines.CoroutineDispatcher -import to.bitkit.env.Tag.LSP import to.bitkit.async.BaseCoroutineScope import to.bitkit.async.ServiceQueue import to.bitkit.data.LspApi import to.bitkit.data.RegisterDeviceRequest import to.bitkit.data.TestNotificationRequest import to.bitkit.di.BgDispatcher +import to.bitkit.env.Tag.LSP +import to.bitkit.shared.ServiceError import java.time.Instant import java.time.format.DateTimeFormatter import java.time.temporal.ChronoUnit @@ -21,7 +22,7 @@ class BlocktankService @Inject constructor( ) : BaseCoroutineScope(bgDispatcher) { suspend fun registerDevice(deviceToken: String) { - val nodeId = requireNotNull(lightningService.nodeId) { "Node not started" } + val nodeId = lightningService.nodeId ?: throw ServiceError.NodeNotStarted Log.d(LSP, "Registering device for notifications…") diff --git a/app/src/main/java/to/bitkit/services/LightningService.kt b/app/src/main/java/to/bitkit/services/LightningService.kt index 39e996a11..509940259 100644 --- a/app/src/main/java/to/bitkit/services/LightningService.kt +++ b/app/src/main/java/to/bitkit/services/LightningService.kt @@ -4,21 +4,28 @@ import android.util.Log import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.Dispatchers import org.lightningdevkit.ldknode.AnchorChannelsConfig +import org.lightningdevkit.ldknode.BalanceDetails import org.lightningdevkit.ldknode.Builder +import org.lightningdevkit.ldknode.ChannelDetails import org.lightningdevkit.ldknode.Event import org.lightningdevkit.ldknode.LogLevel import org.lightningdevkit.ldknode.Node +import org.lightningdevkit.ldknode.NodeException +import org.lightningdevkit.ldknode.NodeStatus +import org.lightningdevkit.ldknode.PaymentDetails import org.lightningdevkit.ldknode.defaultConfig +import to.bitkit.async.BaseCoroutineScope +import to.bitkit.async.ServiceQueue +import to.bitkit.di.BgDispatcher import to.bitkit.env.Env import to.bitkit.env.LnPeer import to.bitkit.env.LnPeer.Companion.toLnPeer import to.bitkit.env.REST import to.bitkit.env.SEED import to.bitkit.env.Tag.LDK -import to.bitkit.async.BaseCoroutineScope -import to.bitkit.async.ServiceQueue -import to.bitkit.di.BgDispatcher import to.bitkit.ext.uByteList +import to.bitkit.shared.LdkError +import to.bitkit.shared.ServiceError import javax.inject.Inject class LightningService @Inject constructor( @@ -30,7 +37,7 @@ class LightningService @Inject constructor( } } - lateinit var node: Node + var node: Node? = null fun setup(mnemonic: String = SEED) { val dir = Env.Storage.ldk @@ -67,19 +74,19 @@ class LightningService @Inject constructor( } suspend fun start() { - assertNodeIsInitialised() + val node = this.node ?: throw ServiceError.NodeNotSetup Log.d(LDK, "Starting node…") - ServiceQueue.LDK.background { node.start() } - Log.i(LDK, "Node started") connectToTrustedPeers() } suspend fun stop() { + val node = this.node ?: throw ServiceError.NodeNotStarted + Log.d(LDK, "Stopping node…") ServiceQueue.LDK.background { node.stop() @@ -87,141 +94,158 @@ class LightningService @Inject constructor( Log.i(LDK, "Node stopped.") } - private suspend fun connectToTrustedPeers() { - ServiceQueue.LDK.background { - for (peer in Env.trustedLnPeers) { - connectPeer(peer) - } - } - } - suspend fun sync() { - Log.d(LDK, "Syncing node…") + val node = this.node ?: throw ServiceError.NodeNotSetup + Log.d(LDK, "Syncing node…") ServiceQueue.LDK.background { node.syncWallets() // setMaxDustHtlcExposureForCurrentChannels() } - Log.i(LDK, "Node synced") } suspend fun sign(message: String): String { - assertNodeIsInitialised() + val node = this.node ?: throw ServiceError.NodeNotSetup + val msg = runCatching { message.uByteList }.getOrNull() ?: throw ServiceError.InvalidNodeSigningMessage return ServiceQueue.LDK.background { - node.signMessage(message.uByteList) + node.signMessage(msg) + } + } + + // region peers + private suspend fun connectToTrustedPeers() { + for (peer in Env.trustedLnPeers) { + connectPeer(peer) } } - private fun assertNodeIsInitialised() = check(::node.isInitialized) { "LDK node is not initialised" } + suspend fun connectPeer(peer: LnPeer) { + val node = this.node ?: throw ServiceError.NodeNotSetup - // region state - val nodeId: String get() = node.nodeId() - val balances get() = node.listBalances() - val status get() = node.status() - val peers get() = node.listPeers().map { it.toLnPeer() } - val channels get() = node.listChannels() - val payments get() = node.listPayments() - // endregion -} + Log.d(LDK, "Connecting peer: $peer") -// region peers -internal fun LightningService.connectPeer(peer: LnPeer) { - Log.d(LDK, "Connecting peer: $peer") - val res = runCatching { - node.connect(peer.nodeId, peer.address, persist = true) - } - Log.i(LDK, "Connection ${if (res.isSuccess) "succeeded" else "failed"} with: $peer") -} -// endregion - -// region channels -internal suspend fun LightningService.openChannel(peer: LnPeer) { - - // sendToAddress - // mine 6 blocks & wait for esplora to pick up block - // wait for esplora to pick up tx - sync() - - ServiceQueue.LDK.background { - node.connectOpenChannel( - nodeId = peer.nodeId, - address = peer.address, - channelAmountSats = 50000u, - pushToCounterpartyMsat = null, - channelConfig = null, - announceChannel = true, - ) + val res = runCatching { + ServiceQueue.LDK.background { + node.connect(peer.nodeId, peer.address, persist = true) + } + }.onFailure { e -> + (e as? NodeException)?.let { throw LdkError(it) } + } + + Log.i(LDK, "Connection ${if (res.isSuccess) "succeeded" else "failed"} with: $peer") } + // endregion - sync() + // region channels + suspend fun openChannel(peer: LnPeer) { + val node = this.node ?: throw ServiceError.NodeNotSetup - val pendingEvent = node.nextEventAsync() - check(pendingEvent is Event.ChannelPending) { "Expected ChannelPending event, got $pendingEvent" } - node.eventHandled() + // sendToAddress + // mine 6 blocks & wait for esplora to pick up block + // wait for esplora to pick up tx + sync() - Log.d(LDK, "Channel pending with peer: ${peer.address}") - Log.d(LDK, "Channel funding txid: ${pendingEvent.fundingTxo.txid}") + ServiceQueue.LDK.background { + node.connectOpenChannel( + nodeId = peer.nodeId, + address = peer.address, + channelAmountSats = 50000u, + pushToCounterpartyMsat = null, + channelConfig = null, + announceChannel = true, + ) + } - // wait for counterparty to pickup event: ChannelPending - // wait for esplora to pick up tx: fundingTx - // mine 6 blocks & wait for esplora to pick up block - sync() + sync() - val readyEvent = node.nextEventAsync() - check(readyEvent is Event.ChannelReady) { "Expected ChannelReady event, got $readyEvent" } - node.eventHandled() + val pendingEvent = node.nextEventAsync() + check(pendingEvent is Event.ChannelPending) { "Expected ChannelPending event, got $pendingEvent" } + node.eventHandled() - // wait for counterparty to pickup event: ChannelReady + Log.d(LDK, "Channel pending with peer: ${peer.address}") + Log.d(LDK, "Channel funding txid: ${pendingEvent.fundingTxo.txid}") - Log.i(LDK, "Channel ready: ${readyEvent.userChannelId}") -} + // wait for counterparty to pickup event: ChannelPending + // wait for esplora to pick up tx: fundingTx + // mine 6 blocks & wait for esplora to pick up block + sync() + + val readyEvent = node.nextEventAsync() + check(readyEvent is Event.ChannelReady) { "Expected ChannelReady event, got $readyEvent" } + node.eventHandled() -internal suspend fun LightningService.closeChannel(userChannelId: String, counterpartyNodeId: String) { - node.closeChannel(userChannelId, counterpartyNodeId) + // wait for counterparty to pickup event: ChannelReady - val event = node.nextEventAsync() - check(event is Event.ChannelClosed) { "Expected ChannelClosed event, got $event" } - node.eventHandled() + Log.i(LDK, "Channel ready: ${readyEvent.userChannelId}") + } - // mine 1 block & wait for esplora to pick up block - sync() + suspend fun closeChannel(userChannelId: String, counterpartyNodeId: String) { + val node = this.node ?: throw ServiceError.NodeNotStarted - Log.i(LDK, "Channel closed: $userChannelId") -} -// endregion + ServiceQueue.LDK.background { + node.closeChannel(userChannelId, counterpartyNodeId) + } -// region payments -internal fun LightningService.createInvoice(): String { - return node.bolt11Payment().receive(amountMsat = 112u, description = "description", expirySecs = 7200u) -} + val event = node.nextEventAsync() + check(event is Event.ChannelClosed) { "Expected ChannelClosed event, got $event" } + node.eventHandled() + + // mine 1 block & wait for esplora to pick up block + sync() -internal suspend fun LightningService.payInvoice(invoice: String): Boolean { - Log.d(LDK, "Paying invoice: $invoice") + Log.i(LDK, "Channel closed: $userChannelId") + } + // endregion - node.bolt11Payment().send(invoice) - node.eventHandled() + // region payments + suspend fun createInvoice(amountSat: ULong, description: String, expirySecs: UInt): String { + val node = this.node ?: throw ServiceError.NodeNotSetup - when (val event = node.nextEventAsync()) { - is Event.PaymentSuccessful -> { - Log.i(LDK, "Payment successful for invoice: $invoice") + return ServiceQueue.LDK.background { + node.bolt11Payment().receive(amountMsat = amountSat * 1000u, description, expirySecs) } + } + + suspend fun payInvoice(invoice: String): Boolean { + val node = this.node ?: throw ServiceError.NodeNotSetup - is Event.PaymentFailed -> { - Log.e(LDK, "Payment error: ${event.reason}") - return false + Log.d(LDK, "Paying invoice: $invoice") + + ServiceQueue.LDK.background { + node.bolt11Payment().send(invoice) } + node.eventHandled() + + when (val event = node.nextEventAsync()) { + is Event.PaymentSuccessful -> { + Log.i(LDK, "Payment successful for invoice: $invoice") + return true + } + + is Event.PaymentFailed -> { + Log.e(LDK, "Payment error: ${event.reason}") + return false + } - else -> { - Log.e(LDK, "Expected PaymentSuccessful/PaymentFailed event, got $event") - return false + else -> { + Log.e(LDK, "Expected PaymentSuccessful/PaymentFailed event, got $event") + return false + } } } + // endregion - return true + // region state + val nodeId: String? get() = node?.nodeId() + val balances: BalanceDetails? get() = node?.listBalances() + val status: NodeStatus? get() = node?.status() + val peers: List? get() = node?.listPeers()?.map { it.toLnPeer() } + val channels: List? get() = node?.listChannels() + val payments: List? get() = node?.listPayments() + // endregion } -// endregion internal suspend fun warmupNode() { runCatching { diff --git a/app/src/main/java/to/bitkit/shared/Errors.kt b/app/src/main/java/to/bitkit/shared/Errors.kt index 3c9d4ed97..668a35386 100644 --- a/app/src/main/java/to/bitkit/shared/Errors.kt +++ b/app/src/main/java/to/bitkit/shared/Errors.kt @@ -36,16 +36,16 @@ open class AppError(override val message: String) : Exception(message) { } } -sealed class CustomServiceError(message: String) : AppError(message) { - class NodeNotSetup : CustomServiceError("Node is not setup.") - class NodeNotStarted : CustomServiceError("Node has not started.") - class OnchainWalletNotInitialized : CustomServiceError("Onchain wallet is not initialized.") - class LdkNodeSqliteAlreadyExists : CustomServiceError("LDK Node SQLite already exists.") - class LdkToLdkNodeMigration : CustomServiceError("Migration from LDK to LDK Node.") - class MnemonicNotFound : CustomServiceError("Mnemonic not found.") - class NodeStillRunning : CustomServiceError("Node is still running.") - class OnchainWalletStillRunning : CustomServiceError("Onchain wallet is still running.") - class InvalidNodeSigningMessage : CustomServiceError("Invalid node signing message.") +sealed class ServiceError(message: String) : AppError(message) { + data object NodeNotSetup : ServiceError("Node is not setup") + data object NodeNotStarted : ServiceError("Node is not started") + class OnchainWalletNotInitialized : ServiceError("Onchain wallet not created") + class LdkNodeSqliteAlreadyExists : ServiceError("LDK-node SQLite file already exists") + class LdkToLdkNodeMigration : ServiceError("LDK to LDK-node migration issue") + class MnemonicNotFound : ServiceError("Mnemonic not found") + class NodeStillRunning : ServiceError("Node is still running") + class OnchainWalletStillRunning : ServiceError("Onchain wallet is still running") + data object InvalidNodeSigningMessage : ServiceError("Invalid node signing message") } sealed class KeychainError(message: String) : AppError(message) { @@ -66,7 +66,7 @@ sealed class BlocktankError(message: String) : AppError(message) { } // region ldk -sealed class LdkError(private val inner: LdkException) : AppError("Unknown LDK error.") { +class LdkError(private val inner: LdkException) : AppError("Unknown LDK error.") { constructor(inner: BuildException) : this(LdkException.Build(inner)) constructor(inner: NodeException) : this(LdkException.Node(inner)) @@ -148,7 +148,7 @@ sealed class LdkError(private val inner: LdkException) : AppError("Unknown LDK e // endregion // region bdk -sealed class BdkError(private val inner: BdkException) : AppError("Unknown BDK error.") { +class BdkError(private val inner: BdkException) : AppError("Unknown BDK error.") { constructor(inner: AddressException) : this(BdkException.Address(inner)) constructor(inner: Bip32Exception) : this(BdkException.Bip32(inner)) constructor(inner: Bip39Exception) : this(BdkException.Bip39(inner)) diff --git a/app/src/main/java/to/bitkit/ui/WalletViewModel.kt b/app/src/main/java/to/bitkit/ui/WalletViewModel.kt index fccf54f09..bd62c0100 100644 --- a/app/src/main/java/to/bitkit/ui/WalletViewModel.kt +++ b/app/src/main/java/to/bitkit/ui/WalletViewModel.kt @@ -8,17 +8,14 @@ import kotlinx.coroutines.delay import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.launch +import kotlinx.coroutines.runBlocking import org.lightningdevkit.ldknode.ChannelDetails +import to.bitkit.di.BgDispatcher import to.bitkit.env.LnPeer import to.bitkit.env.SEED -import to.bitkit.di.BgDispatcher import to.bitkit.services.BitcoinService import to.bitkit.services.LightningService -import to.bitkit.services.closeChannel -import to.bitkit.services.connectPeer -import to.bitkit.services.createInvoice -import to.bitkit.services.openChannel -import to.bitkit.services.payInvoice +import to.bitkit.shared.ServiceError import javax.inject.Inject @HiltViewModel @@ -27,7 +24,7 @@ class WalletViewModel @Inject constructor( private val bitcoinService: BitcoinService, private val lightningService: LightningService, ) : ViewModel() { - private val node = lightningService.node + private val node by lazy { lightningService.node ?: throw ServiceError.NodeNotSetup } private val _uiState = MutableStateFlow(MainUiState.Loading) val uiState = _uiState.asStateFlow() @@ -43,13 +40,13 @@ class WalletViewModel @Inject constructor( lightningService.sync() bitcoinService.syncWithRevealedSpks() _uiState.value = MainUiState.Content( - ldkNodeId = lightningService.nodeId, - ldkBalance = lightningService.balances.totalLightningBalanceSats.toString(), + ldkNodeId = lightningService.nodeId.orEmpty(), + ldkBalance = lightningService.balances?.totalLightningBalanceSats.toString(), btcAddress = bitcoinService.getNextAddress(), btcBalance = bitcoinService.balance?.total?.toSat().toString(), mnemonic = SEED, - peers = lightningService.peers, - channels = lightningService.channels, + peers = lightningService.peers.orEmpty(), + channels = lightningService.channels.orEmpty(), ) } @@ -58,13 +55,14 @@ class WalletViewModel @Inject constructor( } fun connectPeer(peer: LnPeer) { - lightningService.connectPeer(peer) - - updateContentState { - val peers = it.peers.toMutableList().apply { - replaceAll { p -> p.run { copy(isConnected = p.nodeId == nodeId) } } + viewModelScope.launch { + lightningService.connectPeer(peer) + updateContentState { + val peers = it.peers.toMutableList().apply { + replaceAll { p -> p.run { copy(isConnected = p.nodeId == nodeId) } } + } + it.copy(peers = peers) } - it.copy(peers = peers) } } @@ -86,7 +84,9 @@ class WalletViewModel @Inject constructor( } } - fun createInvoice() = lightningService.createInvoice() + fun createInvoice(): String { + return runBlocking { lightningService.createInvoice(112u, "description", 7200u) } + } fun openChannel() { val contentState = _uiState.value as? MainUiState.Content ?: error("No peer connected to open channel.") From 46c04748c537cf0d71bb0c839ed6eaa4aad466fa Mon Sep 17 00:00:00 2001 From: Ovi Trif Date: Sun, 15 Sep 2024 23:04:38 +0300 Subject: [PATCH 14/27] feat: Use typed errors in Migration service --- .../java/to/bitkit/services/LdkMigrationTest.kt | 2 +- .../main/java/to/bitkit/services/MigrationService.kt | 10 +++++----- app/src/main/java/to/bitkit/shared/Errors.kt | 4 ++-- gradle/libs.versions.toml | 2 +- 4 files changed, 9 insertions(+), 9 deletions(-) diff --git a/app/src/androidTest/java/to/bitkit/services/LdkMigrationTest.kt b/app/src/androidTest/java/to/bitkit/services/LdkMigrationTest.kt index 3c0573229..f1adc5af5 100644 --- a/app/src/androidTest/java/to/bitkit/services/LdkMigrationTest.kt +++ b/app/src/androidTest/java/to/bitkit/services/LdkMigrationTest.kt @@ -30,7 +30,7 @@ class LdkMigrationTest { runBlocking { start() } assertTrue { nodeId == "02cd08b7b375e4263849121f9f0ffb2732a0b88d0fb74487575ac539b374f45a55" } - assertTrue { channels.isNotEmpty() } + assertTrue { channels?.isNotEmpty() == true } runBlocking { stop() } } diff --git a/app/src/main/java/to/bitkit/services/MigrationService.kt b/app/src/main/java/to/bitkit/services/MigrationService.kt index 76a17fe64..6d8ab85a0 100644 --- a/app/src/main/java/to/bitkit/services/MigrationService.kt +++ b/app/src/main/java/to/bitkit/services/MigrationService.kt @@ -10,6 +10,7 @@ import org.ldk.structs.KeysManager import to.bitkit.env.Env import to.bitkit.env.Tag.LDK import to.bitkit.ext.hex +import to.bitkit.shared.ServiceError import java.io.File import javax.inject.Inject import kotlin.io.path.Path @@ -26,8 +27,7 @@ class MigrationService @Inject constructor( // Skip if db already exists if (file.exists()) { - Log.d(LDK, "Migration skipped: ldk-node db exists at: $file") - return + throw ServiceError.LdkNodeSqliteAlreadyExists(file.path) } val path = file.path @@ -72,9 +72,9 @@ class MigrationService @Inject constructor( for (monitor in monitors) { val channelMonitor = read32BytesChannelMonitor(monitor, entropySource, signerProvider).takeIf { it.is_ok } ?.let { it as? ChannelMonitorDecodeResultTuple }?.res?._b - ?: throw Error("Could not read channel monitor using read32BytesChannelMonitor") + ?: throw ServiceError.LdkToLdkNodeMigration val fundingTx = channelMonitor._funding_txo._a._txid?.reversedArray()?.hex - ?: throw Error("Could not read txid from funding tx OutPoint of channel monitor") + ?: throw ServiceError.LdkToLdkNodeMigration val index = channelMonitor._funding_txo._a._index val key = "${fundingTx}_$index" @@ -114,6 +114,6 @@ class MigrationService @Inject constructor( private const val KEY = "key" private const val VALUE = "value" private const val LDK_DB_NAME = "$LDK_NODE_DATA.sqlite" - private const val LDK_DB_VERSION = 2 // TODO: check on each ldk-node version update + private const val LDK_DB_VERSION = 2 } } diff --git a/app/src/main/java/to/bitkit/shared/Errors.kt b/app/src/main/java/to/bitkit/shared/Errors.kt index 668a35386..e36d15559 100644 --- a/app/src/main/java/to/bitkit/shared/Errors.kt +++ b/app/src/main/java/to/bitkit/shared/Errors.kt @@ -40,8 +40,8 @@ sealed class ServiceError(message: String) : AppError(message) { data object NodeNotSetup : ServiceError("Node is not setup") data object NodeNotStarted : ServiceError("Node is not started") class OnchainWalletNotInitialized : ServiceError("Onchain wallet not created") - class LdkNodeSqliteAlreadyExists : ServiceError("LDK-node SQLite file already exists") - class LdkToLdkNodeMigration : ServiceError("LDK to LDK-node migration issue") + class LdkNodeSqliteAlreadyExists(path: String) : ServiceError("LDK-node SQLite file already exists at $path") + data object LdkToLdkNodeMigration : ServiceError("LDK to LDK-node migration issue") class MnemonicNotFound : ServiceError("Mnemonic not found") class NodeStillRunning : ServiceError("Node is still running") class OnchainWalletStillRunning : ServiceError("Onchain wallet is still running") diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index e0cc96cd3..1a5238892 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -17,7 +17,7 @@ junitExt = "1.2.1" kotlin = "2.0.20" ksp = "2.0.20-1.0.25" ktor = "2.3.12" -ldkNode = "0.3.0" +ldkNode = "0.3.0" # LDK_DB_VERSION in MirationService should match lib's sqlite DB version lifecycle = "2.8.5" material = "1.12.0" navCompose = "2.8.0" From 0cfed89d14950ddc5962f2f80f8eb333533e9333 Mon Sep 17 00:00:00 2001 From: Ovi Trif Date: Sun, 15 Sep 2024 23:38:17 +0300 Subject: [PATCH 15/27] feat: Use typed errors in OnChain service --- .../main/java/to/bitkit/di/ServicesModule.kt | 6 +-- .../main/java/to/bitkit/di/ViewModelModule.kt | 6 +-- .../to/bitkit/services/LightningService.kt | 8 ++- .../{BitcoinService.kt => OnChainService.kt} | 50 ++++++++++++------- app/src/main/java/to/bitkit/shared/Errors.kt | 6 +-- .../main/java/to/bitkit/ui/SharedViewModel.kt | 7 +-- .../main/java/to/bitkit/ui/WalletViewModel.kt | 10 ++-- 7 files changed, 57 insertions(+), 36 deletions(-) rename app/src/main/java/to/bitkit/services/{BitcoinService.kt => OnChainService.kt} (73%) diff --git a/app/src/main/java/to/bitkit/di/ServicesModule.kt b/app/src/main/java/to/bitkit/di/ServicesModule.kt index 654d8458e..fe297d41b 100644 --- a/app/src/main/java/to/bitkit/di/ServicesModule.kt +++ b/app/src/main/java/to/bitkit/di/ServicesModule.kt @@ -7,7 +7,7 @@ import dagger.Provides import dagger.hilt.InstallIn import dagger.hilt.components.SingletonComponent import kotlinx.coroutines.CoroutineDispatcher -import to.bitkit.services.BitcoinService +import to.bitkit.services.OnChainService import to.bitkit.services.LightningService @Module @@ -23,7 +23,7 @@ object ServicesModule { @Provides fun provideBitcoinService( @BgDispatcher bgDispatcher: CoroutineDispatcher, - ): BitcoinService { - return BitcoinService.shared + ): OnChainService { + return OnChainService.shared } } diff --git a/app/src/main/java/to/bitkit/di/ViewModelModule.kt b/app/src/main/java/to/bitkit/di/ViewModelModule.kt index 45433b5b6..cb59d0347 100644 --- a/app/src/main/java/to/bitkit/di/ViewModelModule.kt +++ b/app/src/main/java/to/bitkit/di/ViewModelModule.kt @@ -7,7 +7,7 @@ import dagger.hilt.components.SingletonComponent import kotlinx.coroutines.CoroutineDispatcher import to.bitkit.data.AppDb import to.bitkit.data.keychain.KeychainStore -import to.bitkit.services.BitcoinService +import to.bitkit.services.OnChainService import to.bitkit.services.BlocktankService import to.bitkit.ui.SharedViewModel import javax.inject.Singleton @@ -22,14 +22,14 @@ object ViewModelModule { appDb: AppDb, keychainStore: KeychainStore, blocktankService: BlocktankService, - bitcoinService: BitcoinService, + onChainService: OnChainService, ): SharedViewModel { return SharedViewModel( bgDispatcher, appDb, keychainStore, blocktankService, - bitcoinService, + onChainService, ) } } diff --git a/app/src/main/java/to/bitkit/services/LightningService.kt b/app/src/main/java/to/bitkit/services/LightningService.kt index 509940259..136cd2509 100644 --- a/app/src/main/java/to/bitkit/services/LightningService.kt +++ b/app/src/main/java/to/bitkit/services/LightningService.kt @@ -91,9 +91,15 @@ class LightningService @Inject constructor( ServiceQueue.LDK.background { node.stop() } + node.close().also { this.node = null } Log.i(LDK, "Node stopped.") } + fun wipeStorage() { + if (node != null) throw ServiceError.NodeStillRunning + TODO("Not yet implemented") + } + suspend fun sync() { val node = this.node ?: throw ServiceError.NodeNotSetup @@ -254,7 +260,7 @@ internal suspend fun warmupNode() { start() sync() } - BitcoinService.shared.apply { + OnChainService.shared.apply { setup() fullScan() } diff --git a/app/src/main/java/to/bitkit/services/BitcoinService.kt b/app/src/main/java/to/bitkit/services/OnChainService.kt similarity index 73% rename from app/src/main/java/to/bitkit/services/BitcoinService.kt rename to app/src/main/java/to/bitkit/services/OnChainService.kt index 92fb9c03c..e4154443a 100644 --- a/app/src/main/java/to/bitkit/services/BitcoinService.kt +++ b/app/src/main/java/to/bitkit/services/OnChainService.kt @@ -3,29 +3,31 @@ package to.bitkit.services import android.util.Log import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.Dispatchers +import org.bitcoindevkit.Balance import org.bitcoindevkit.Descriptor import org.bitcoindevkit.DescriptorSecretKey import org.bitcoindevkit.EsploraClient import org.bitcoindevkit.KeychainKind import org.bitcoindevkit.Mnemonic import org.bitcoindevkit.Wallet +import to.bitkit.async.BaseCoroutineScope +import to.bitkit.async.ServiceQueue +import to.bitkit.di.BgDispatcher import to.bitkit.env.Env import to.bitkit.env.REST import to.bitkit.env.SEED import to.bitkit.env.Tag.BDK -import to.bitkit.async.BaseCoroutineScope -import to.bitkit.di.BgDispatcher -import to.bitkit.async.ServiceQueue +import to.bitkit.shared.ServiceError import javax.inject.Inject import kotlin.io.path.Path import kotlin.io.path.pathString -class BitcoinService @Inject constructor( +class OnChainService @Inject constructor( @BgDispatcher bgDispatcher: CoroutineDispatcher, ) : BaseCoroutineScope(bgDispatcher) { companion object { val shared by lazy { - BitcoinService(Dispatchers.Default) + OnChainService(Dispatchers.Default) } } @@ -36,7 +38,7 @@ class BitcoinService @Inject constructor( private val esploraClient by lazy { EsploraClient(url = REST) } private val dbPath by lazy { Path(Env.Storage.bdk, "db.sqlite") } - private lateinit var wallet: Wallet + private var wallet: Wallet? = null suspend fun setup() { val network = Env.network.bdk @@ -57,7 +59,23 @@ class BitcoinService @Inject constructor( Log.i(BDK, "Wallet set up") } + fun stop() { + Log.d(BDK, "Stopping onchain wallet…") + wallet?.close().also { wallet = null } + Log.i(BDK, "Onchain wallet stopped") + } + + fun wipeStorage() { + if (wallet != null) throw ServiceError.OnchainWalletStillRunning + + Log.w(BDK, "Wiping onchain wallet storage…") + dbPath.toFile()?.parentFile?.deleteRecursively() + Log.i(BDK, "Onchain wallet storage wiped") + } + + // region scan suspend fun syncWithRevealedSpks() { + val wallet = this.wallet ?: throw ServiceError.OnchainWalletNotInitialized Log.d(BDK, "Wallet syncing…") ServiceQueue.BDK.background { @@ -71,6 +89,7 @@ class BitcoinService @Inject constructor( } suspend fun fullScan() { + val wallet = this.wallet ?: throw ServiceError.OnchainWalletNotInitialized Log.d(BDK, "Wallet full scan…") ServiceQueue.BDK.background { @@ -84,22 +103,17 @@ class BitcoinService @Inject constructor( Log.i(BDK, "Wallet fully scanned") } - - fun wipeStorage() { - Log.w(BDK, "Wiping wallet storage…") - - dbPath.toFile()?.parentFile?.deleteRecursively() - - Log.i(BDK, "Wallet storage wiped") - } + // endregion // region state - val balance get() = if (hasSynced) wallet.getBalance() else null + val balance: Balance? get() = if (hasSynced) wallet?.getBalance() else null + + suspend fun getAddress(): String { + val wallet = this.wallet ?: throw ServiceError.OnchainWalletNotInitialized - suspend fun getNextAddress(): String { return ServiceQueue.BDK.background { - val addressInfo = wallet.revealNextAddress(KeychainKind.EXTERNAL).address - addressInfo.asString() + val addressInfo = wallet.revealNextAddress(KeychainKind.EXTERNAL) + addressInfo.address.asString() } } // endregion diff --git a/app/src/main/java/to/bitkit/shared/Errors.kt b/app/src/main/java/to/bitkit/shared/Errors.kt index e36d15559..e1812c888 100644 --- a/app/src/main/java/to/bitkit/shared/Errors.kt +++ b/app/src/main/java/to/bitkit/shared/Errors.kt @@ -39,12 +39,12 @@ open class AppError(override val message: String) : Exception(message) { sealed class ServiceError(message: String) : AppError(message) { data object NodeNotSetup : ServiceError("Node is not setup") data object NodeNotStarted : ServiceError("Node is not started") - class OnchainWalletNotInitialized : ServiceError("Onchain wallet not created") + data object OnchainWalletNotInitialized : ServiceError("Onchain wallet not created") class LdkNodeSqliteAlreadyExists(path: String) : ServiceError("LDK-node SQLite file already exists at $path") data object LdkToLdkNodeMigration : ServiceError("LDK to LDK-node migration issue") class MnemonicNotFound : ServiceError("Mnemonic not found") - class NodeStillRunning : ServiceError("Node is still running") - class OnchainWalletStillRunning : ServiceError("Onchain wallet is still running") + data object NodeStillRunning : ServiceError("Node is still running") + data object OnchainWalletStillRunning : ServiceError("Onchain wallet is still running") data object InvalidNodeSigningMessage : ServiceError("Invalid node signing message") } diff --git a/app/src/main/java/to/bitkit/ui/SharedViewModel.kt b/app/src/main/java/to/bitkit/ui/SharedViewModel.kt index adf9ac49b..c71480583 100644 --- a/app/src/main/java/to/bitkit/ui/SharedViewModel.kt +++ b/app/src/main/java/to/bitkit/ui/SharedViewModel.kt @@ -15,7 +15,7 @@ import to.bitkit.data.AppDb import to.bitkit.data.keychain.KeychainStore import to.bitkit.di.BgDispatcher import to.bitkit.env.Tag.APP -import to.bitkit.services.BitcoinService +import to.bitkit.services.OnChainService import to.bitkit.services.BlocktankService import javax.inject.Inject @@ -25,7 +25,7 @@ class SharedViewModel @Inject constructor( private val db: AppDb, private val keychain: KeychainStore, private val blocktankService: BlocktankService, - private val bitcoinService: BitcoinService, + private val onChainService: OnChainService, ) : ViewModel() { fun warmupNode() { // TODO make it concurrent, and wait for all to finish before trying to access `lightningService.node`, etc… @@ -66,7 +66,8 @@ class SharedViewModel @Inject constructor( } fun debugWipeBdk() { - bitcoinService.wipeStorage() + onChainService.stop() + onChainService.wipeStorage() } fun debugLspNotifications() { diff --git a/app/src/main/java/to/bitkit/ui/WalletViewModel.kt b/app/src/main/java/to/bitkit/ui/WalletViewModel.kt index bd62c0100..53aab109f 100644 --- a/app/src/main/java/to/bitkit/ui/WalletViewModel.kt +++ b/app/src/main/java/to/bitkit/ui/WalletViewModel.kt @@ -13,7 +13,7 @@ import org.lightningdevkit.ldknode.ChannelDetails import to.bitkit.di.BgDispatcher import to.bitkit.env.LnPeer import to.bitkit.env.SEED -import to.bitkit.services.BitcoinService +import to.bitkit.services.OnChainService import to.bitkit.services.LightningService import to.bitkit.shared.ServiceError import javax.inject.Inject @@ -21,7 +21,7 @@ import javax.inject.Inject @HiltViewModel class WalletViewModel @Inject constructor( @BgDispatcher private val bgDispatcher: CoroutineDispatcher, - private val bitcoinService: BitcoinService, + private val onChainService: OnChainService, private val lightningService: LightningService, ) : ViewModel() { private val node by lazy { lightningService.node ?: throw ServiceError.NodeNotSetup } @@ -38,12 +38,12 @@ class WalletViewModel @Inject constructor( private suspend fun sync() { lightningService.sync() - bitcoinService.syncWithRevealedSpks() + onChainService.syncWithRevealedSpks() _uiState.value = MainUiState.Content( ldkNodeId = lightningService.nodeId.orEmpty(), ldkBalance = lightningService.balances?.totalLightningBalanceSats.toString(), - btcAddress = bitcoinService.getNextAddress(), - btcBalance = bitcoinService.balance?.total?.toSat().toString(), + btcAddress = onChainService.getAddress(), + btcBalance = onChainService.balance?.total?.toSat().toString(), mnemonic = SEED, peers = lightningService.peers.orEmpty(), channels = lightningService.channels.orEmpty(), From 5f05445dba92929f28cbbdd3dce92270e1f7d39c Mon Sep 17 00:00:00 2001 From: Ovi Trif Date: Sun, 15 Sep 2024 23:48:07 +0300 Subject: [PATCH 16/27] feat: Use LdkError --- .../java/to/bitkit/services/LightningService.kt | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/app/src/main/java/to/bitkit/services/LightningService.kt b/app/src/main/java/to/bitkit/services/LightningService.kt index 136cd2509..465a50212 100644 --- a/app/src/main/java/to/bitkit/services/LightningService.kt +++ b/app/src/main/java/to/bitkit/services/LightningService.kt @@ -5,6 +5,7 @@ import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.Dispatchers import org.lightningdevkit.ldknode.AnchorChannelsConfig import org.lightningdevkit.ldknode.BalanceDetails +import org.lightningdevkit.ldknode.BuildException import org.lightningdevkit.ldknode.Builder import org.lightningdevkit.ldknode.ChannelDetails import org.lightningdevkit.ldknode.Event @@ -68,7 +69,11 @@ class LightningService @Inject constructor( Log.d(LDK, "Setting up node…") - node = builder.build() + node = try { + builder.build() + } catch (e: BuildException) { + throw LdkError(e) + } Log.i(LDK, "Node set up") } @@ -132,15 +137,14 @@ class LightningService @Inject constructor( Log.d(LDK, "Connecting peer: $peer") - val res = runCatching { + try { ServiceQueue.LDK.background { node.connect(peer.nodeId, peer.address, persist = true) } - }.onFailure { e -> - (e as? NodeException)?.let { throw LdkError(it) } + Log.i(LDK, "Connection succeeded with: $peer") + } catch(e: NodeException) { + Log.w(LDK, "Connection failed with: $peer", LdkError(e)) } - - Log.i(LDK, "Connection ${if (res.isSuccess) "succeeded" else "failed"} with: $peer") } // endregion From 1d9e9f7b385cf939f80598d24aa9217a4c58620d Mon Sep 17 00:00:00 2001 From: Ovi Trif Date: Mon, 16 Sep 2024 12:11:28 +0300 Subject: [PATCH 17/27] feat: Generate keypair for BT push notifications --- app/build.gradle.kts | 1 + .../to/bitkit/data/keychain/AndroidKeyStore.kt | 4 ++-- .../to/bitkit/data/keychain/KeychainStore.kt | 10 ++++++++-- .../java/to/bitkit/services/BlocktankService.kt | 17 +++++++++++++++-- gradle/libs.versions.toml | 2 ++ 5 files changed, 28 insertions(+), 6 deletions(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index a8f69fab0..58cf55a57 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -86,6 +86,7 @@ dependencies { implementation(libs.datastore.preferences) // BDK + LDK implementation(libs.bdk.android) + implementation(libs.bitcoinj.core) implementation(libs.ldk.node.android) // Firebase implementation(platform(libs.firebase.bom)) diff --git a/app/src/main/java/to/bitkit/data/keychain/AndroidKeyStore.kt b/app/src/main/java/to/bitkit/data/keychain/AndroidKeyStore.kt index 74ccc213f..71a7cfacb 100644 --- a/app/src/main/java/to/bitkit/data/keychain/AndroidKeyStore.kt +++ b/app/src/main/java/to/bitkit/data/keychain/AndroidKeyStore.kt @@ -54,11 +54,11 @@ class AndroidKeyStore( return spec } - fun encrypt(data: String): ByteArray { + fun encrypt(data: ByteArray): ByteArray { val secretKey = keyStore.getKey(alias, password) as SecretKey val cipher = Cipher.getInstance(transformation).apply { init(Cipher.ENCRYPT_MODE, secretKey) } - val encryptedData = cipher.doFinal(data.toByteArray(Charsets.UTF_8)) + val encryptedData = cipher.doFinal(data) val iv = cipher.iv check(iv.size == ivLength) { "Unexpected IV length: ${iv.size} ≠ $ivLength" } diff --git a/app/src/main/java/to/bitkit/data/keychain/KeychainStore.kt b/app/src/main/java/to/bitkit/data/keychain/KeychainStore.kt index 10c0b9fd8..bcbb7db23 100644 --- a/app/src/main/java/to/bitkit/data/keychain/KeychainStore.kt +++ b/app/src/main/java/to/bitkit/data/keychain/KeychainStore.kt @@ -43,10 +43,12 @@ class KeychainStore @Inject constructor( } } - suspend fun saveString(key: String, value: String) = save(key, value.let(keyStore::encrypt)) + suspend fun saveString(key: String, value: String) = save(key, value.toByteArray()) - private suspend fun save(key: String, encryptedValue: ByteArray) { + suspend fun save(key: String, value: ByteArray) { if (exists(key)) throw KeychainError.FailedToSaveAlreadyExists(key) + + val encryptedValue = keyStore.encrypt(value) try { context.keychain.edit { it[key.indexed] = encryptedValue.toBase64() } } catch (e: Exception) { @@ -82,4 +84,8 @@ class KeychainStore @Inject constructor( val walletIndex = runBlocking { db.configDao().getAll().first() }.first().walletIndex return "${this}_$walletIndex".let(::stringPreferencesKey) } + + enum class Key { + PUSH_NOTIFICATION_PRIVATE_KEY, + } } diff --git a/app/src/main/java/to/bitkit/services/BlocktankService.kt b/app/src/main/java/to/bitkit/services/BlocktankService.kt index 6069889ad..13c7eaa40 100644 --- a/app/src/main/java/to/bitkit/services/BlocktankService.kt +++ b/app/src/main/java/to/bitkit/services/BlocktankService.kt @@ -2,11 +2,14 @@ package to.bitkit.services import android.util.Log import kotlinx.coroutines.CoroutineDispatcher +import org.bitcoinj.core.ECKey import to.bitkit.async.BaseCoroutineScope import to.bitkit.async.ServiceQueue import to.bitkit.data.LspApi import to.bitkit.data.RegisterDeviceRequest import to.bitkit.data.TestNotificationRequest +import to.bitkit.data.keychain.KeychainStore +import to.bitkit.data.keychain.KeychainStore.Key import to.bitkit.di.BgDispatcher import to.bitkit.env.Tag.LSP import to.bitkit.shared.ServiceError @@ -19,8 +22,10 @@ class BlocktankService @Inject constructor( @BgDispatcher bgDispatcher: CoroutineDispatcher, private val lspApi: LspApi, private val lightningService: LightningService, + private val keychainStore: KeychainStore, ) : BaseCoroutineScope(bgDispatcher) { + // region notifications suspend fun registerDevice(deviceToken: String) { val nodeId = lightningService.nodeId ?: throw ServiceError.NodeNotStarted @@ -31,8 +36,15 @@ class BlocktankService @Inject constructor( val signature = lightningService.sign(messageToSign) - // TODO: Use actual public key to enable decryption of the push notification payload - val publicKey = "03864ef025fde8fb587d989186ce6a4a186895ee44a926bfc370e2c366597a3f8f" + val keypair = ECKey() + val publicKey = keypair.publicKeyAsHex + Log.d(LSP, "Notification encryption public key: $publicKey") + + // New keypair for each token registration + if (keychainStore.exists(Key.PUSH_NOTIFICATION_PRIVATE_KEY.name)) { + keychainStore.delete(Key.PUSH_NOTIFICATION_PRIVATE_KEY.name) + } + keychainStore.save(Key.PUSH_NOTIFICATION_PRIVATE_KEY.name, keypair.privKeyBytes) val payload = RegisterDeviceRequest( deviceToken = deviceToken, @@ -63,4 +75,5 @@ class BlocktankService @Inject constructor( lspApi.testNotification(deviceToken, payload) } } + // endregion } diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 1a5238892..73f11adf5 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -3,6 +3,7 @@ activityCompose = "1.9.2" agp = "8.6.0" appcompat = "1.7.0" bdk = "1.0.0-alpha.11" +bitcoinj = "0.16.3" composeBom = "2024.09.01" # https://developer.android.com/develop/ui/compose/bom/bom-mapping coreKtx = "1.13.1" datastorePrefs = "1.1.1" @@ -29,6 +30,7 @@ workRuntimeKtx = "2.9.1" activity-compose = { module = "androidx.activity:activity-compose", version.ref = "activityCompose" } appcompat = { module = "androidx.appcompat:appcompat", version.ref = "appcompat" } bdk-android = { module = "org.bitcoindevkit:bdk-android", version.ref = "bdk" } +bitcoinj-core = { module = "org.bitcoinj:bitcoinj-core", version.ref = "bitcoinj" } # replace with secp-ffi if published compose-bom = { group = "androidx.compose", name = "compose-bom", version.ref = "composeBom" } compose-material-icons-extended = { module = "androidx.compose.material:material-icons-extended" } compose-material3 = { module = "androidx.compose.material3:material3" } From ee323a9abbdfbacfdcb280a610e89888eaf3c95c Mon Sep 17 00:00:00 2001 From: Ovi Trif Date: Mon, 16 Sep 2024 12:26:21 +0300 Subject: [PATCH 18/27] feat: Use typed errors in Blocktank service --- app/src/main/java/to/bitkit/data/LspApi.kt | 10 +++++++++- app/src/main/java/to/bitkit/shared/Errors.kt | 4 +--- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/app/src/main/java/to/bitkit/data/LspApi.kt b/app/src/main/java/to/bitkit/data/LspApi.kt index 734140afa..71032a47d 100644 --- a/app/src/main/java/to/bitkit/data/LspApi.kt +++ b/app/src/main/java/to/bitkit/data/LspApi.kt @@ -3,7 +3,9 @@ package to.bitkit.data import io.ktor.client.HttpClient import io.ktor.client.request.post import io.ktor.client.request.setBody +import io.ktor.client.statement.HttpResponse import kotlinx.serialization.Serializable +import to.bitkit.shared.BlocktankError import javax.inject.Inject interface LspApi { @@ -25,7 +27,13 @@ class BlocktankApi @Inject constructor( post("$notificationsApi/$deviceToken/test-notification", payload) } - private suspend inline fun post(url: String, payload: T) = client.post(url) { setBody(payload) } + private suspend inline fun post(url: String, payload: T): HttpResponse { + val response = client.post(url) { setBody(payload) } + return when (val statusCode = response.status.value) { + !in 200..299 -> throw BlocktankError.InvalidResponse(statusCode) + else -> response + } + } } @Serializable diff --git a/app/src/main/java/to/bitkit/shared/Errors.kt b/app/src/main/java/to/bitkit/shared/Errors.kt index e1812c888..8cc500bcb 100644 --- a/app/src/main/java/to/bitkit/shared/Errors.kt +++ b/app/src/main/java/to/bitkit/shared/Errors.kt @@ -59,10 +59,8 @@ sealed class KeychainError(message: String) : AppError(message) { } sealed class BlocktankError(message: String) : AppError(message) { - class MissingResponse : BlocktankError("Missing response.") - class InvalidResponse : BlocktankError("Invalid response.") + class InvalidResponse(status: Int) : BlocktankError("Invalid response status code $status.") class InvalidJson : BlocktankError("Invalid JSON.") - class MissingDeviceToken : BlocktankError("Missing device token.") } // region ldk From 599d6819770226e0732740ef903919f7d502d406 Mon Sep 17 00:00:00 2001 From: Ovi Trif Date: Mon, 16 Sep 2024 14:06:28 +0300 Subject: [PATCH 19/27] refactor: Env & const's reorg, cleanup, alignment with iOS --- app/build.gradle.kts | 2 - .../{KeychainStoreTest.kt => KeychainTest.kt} | 6 +- app/src/main/java/to/bitkit/data/AppDb.kt | 2 +- .../data/{LspApi.kt => BlocktankApi.kt} | 13 +-- .../main/java/to/bitkit/data/EsploraApi.kt | 89 +++++++++++++++++ app/src/main/java/to/bitkit/data/RestApi.kt | 96 ------------------- .../{KeychainStore.kt => Keychain.kt} | 6 +- app/src/main/java/to/bitkit/di/HttpModule.kt | 18 +--- .../main/java/to/bitkit/di/ServicesModule.kt | 4 +- .../main/java/to/bitkit/di/ViewModelModule.kt | 8 +- app/src/main/java/to/bitkit/env/Constants.kt | 76 +++------------ app/src/main/java/to/bitkit/env/LnPeer.kt | 51 ++++++++++ app/src/main/java/to/bitkit/ext/ByteArray.kt | 33 +++---- app/src/main/java/to/bitkit/ext/Context.kt | 2 +- app/src/main/java/to/bitkit/fcm/FcmService.kt | 2 +- .../to/bitkit/services/BlocktankService.kt | 20 ++-- .../to/bitkit/services/LightningService.kt | 3 +- .../java/to/bitkit/services/OnChainService.kt | 3 +- .../main/java/to/bitkit/ui/MainActivity.kt | 1 - .../main/java/to/bitkit/ui/Notifications.kt | 2 +- .../main/java/to/bitkit/ui/SharedViewModel.kt | 12 +-- .../main/java/to/bitkit/ui/WalletViewModel.kt | 2 +- .../java/to/bitkit/ui/settings/PeersScreen.kt | 10 +- .../main/java/to/bitkit/ui/shared/Peers.kt | 2 +- app/src/main/res/values/themes.xml | 2 +- gradle/libs.versions.toml | 2 - 26 files changed, 216 insertions(+), 251 deletions(-) rename app/src/androidTest/java/to/bitkit/data/keychain/{KeychainStoreTest.kt => KeychainTest.kt} (95%) rename app/src/main/java/to/bitkit/data/{LspApi.kt => BlocktankApi.kt} (86%) create mode 100644 app/src/main/java/to/bitkit/data/EsploraApi.kt delete mode 100644 app/src/main/java/to/bitkit/data/RestApi.kt rename app/src/main/java/to/bitkit/data/keychain/{KeychainStore.kt => Keychain.kt} (97%) create mode 100644 app/src/main/java/to/bitkit/env/LnPeer.kt diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 58cf55a57..d940aefad 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -142,8 +142,6 @@ dependencies { // testImplementation("org.mockito:mockito-core:5.12.0") // testImplementation("org.mockito.kotlin:mockito-kotlin:5.4.0") // testImplementation("org.robolectric:robolectric:4.13") - // Other - implementation(libs.guava) // for ByteArray.toHex()+ } ksp { // cool but strict: https://developer.android.com/jetpack/androidx/releases/room#2.6.0 diff --git a/app/src/androidTest/java/to/bitkit/data/keychain/KeychainStoreTest.kt b/app/src/androidTest/java/to/bitkit/data/keychain/KeychainTest.kt similarity index 95% rename from app/src/androidTest/java/to/bitkit/data/keychain/KeychainStoreTest.kt rename to app/src/androidTest/java/to/bitkit/data/keychain/KeychainTest.kt index 0e6c7514e..70f2440ff 100644 --- a/app/src/androidTest/java/to/bitkit/data/keychain/KeychainStoreTest.kt +++ b/app/src/androidTest/java/to/bitkit/data/keychain/KeychainTest.kt @@ -22,12 +22,12 @@ import kotlin.test.assertTrue @RunWith(AndroidJUnit4::class) @OptIn(ExperimentalCoroutinesApi::class) -class KeychainStoreTest : BaseTest() { +class KeychainTest : BaseTest() { private val appContext by lazy { ApplicationProvider.getApplicationContext() } private lateinit var db: AppDb - private lateinit var sut: KeychainStore + private lateinit var sut: Keychain @Before fun setUp() { @@ -42,7 +42,7 @@ class KeychainStoreTest : BaseTest() { } } - sut = KeychainStore( + sut = Keychain( db, appContext, testDispatcher, diff --git a/app/src/main/java/to/bitkit/data/AppDb.kt b/app/src/main/java/to/bitkit/data/AppDb.kt index fce4e741e..c35c69e36 100644 --- a/app/src/main/java/to/bitkit/data/AppDb.kt +++ b/app/src/main/java/to/bitkit/data/AppDb.kt @@ -15,8 +15,8 @@ import androidx.work.WorkerParameters import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.flow.Flow import to.bitkit.BuildConfig -import to.bitkit.env.Env import to.bitkit.data.entities.ConfigEntity +import to.bitkit.env.Env @Database( entities = [ diff --git a/app/src/main/java/to/bitkit/data/LspApi.kt b/app/src/main/java/to/bitkit/data/BlocktankApi.kt similarity index 86% rename from app/src/main/java/to/bitkit/data/LspApi.kt rename to app/src/main/java/to/bitkit/data/BlocktankApi.kt index 71032a47d..9ca4dc174 100644 --- a/app/src/main/java/to/bitkit/data/LspApi.kt +++ b/app/src/main/java/to/bitkit/data/BlocktankApi.kt @@ -7,23 +7,20 @@ import io.ktor.client.statement.HttpResponse import kotlinx.serialization.Serializable import to.bitkit.shared.BlocktankError import javax.inject.Inject +import javax.inject.Singleton -interface LspApi { - suspend fun registerDeviceForNotifications(payload: RegisterDeviceRequest) - suspend fun testNotification(deviceToken: String, payload: TestNotificationRequest) -} - +@Singleton class BlocktankApi @Inject constructor( private val client: HttpClient, -) : LspApi { +) { private val baseUrl = "https://api.stag.blocktank.to" private val notificationsApi = "$baseUrl/notifications/api/device" - override suspend fun registerDeviceForNotifications(payload: RegisterDeviceRequest) { + suspend fun registerDeviceForNotifications(payload: RegisterDeviceRequest) { post(notificationsApi, payload) } - override suspend fun testNotification(deviceToken: String, payload: TestNotificationRequest) { + suspend fun testNotification(deviceToken: String, payload: TestNotificationRequest) { post("$notificationsApi/$deviceToken/test-notification", payload) } diff --git a/app/src/main/java/to/bitkit/data/EsploraApi.kt b/app/src/main/java/to/bitkit/data/EsploraApi.kt new file mode 100644 index 000000000..14e42d82a --- /dev/null +++ b/app/src/main/java/to/bitkit/data/EsploraApi.kt @@ -0,0 +1,89 @@ +package to.bitkit.data + +import io.ktor.client.HttpClient +import io.ktor.client.call.body +import io.ktor.client.request.get +import io.ktor.client.request.post +import io.ktor.client.request.setBody +import io.ktor.client.statement.HttpResponse +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable +import to.bitkit.env.Env +import to.bitkit.ext.hex +import javax.inject.Inject +import javax.inject.Singleton + +@Singleton +class EsploraApi @Inject constructor( + private val client: HttpClient, +) { + private val baseUrl = Env.esploraUrl + + suspend fun getLatestBlockHash(): String { + val httpResponse: HttpResponse = client.get("$baseUrl/blocks/tip/hash") + return httpResponse.body() + } + + suspend fun getLatestBlockHeight(): Int { + val httpResponse: HttpResponse = client.get("$baseUrl/blocks/tip/height") + return httpResponse.body().toInt() + } + + suspend fun broadcastTx(tx: ByteArray): String { + val response: HttpResponse = client.post("$baseUrl/tx") { + setBody(tx.hex) + } + + return response.body() + } + + suspend fun getTx(txid: String): Tx { + return client.get("$baseUrl/tx/${txid}").body() + } + + suspend fun getTxHex(txid: String): String { + return client.get("$baseUrl/tx/${txid}/hex").body() + } + + suspend fun getHeader(hash: String): String { + return client.get("$baseUrl/block/${hash}/header").body() + } + + suspend fun getMerkleProof(txid: String): MerkleProof { + return client.get("$baseUrl/tx/${txid}/merkle-proof").body() + } + + suspend fun getOutputSpent(txid: String, outputIndex: Int): OutputSpent { + return client.get("$baseUrl/tx/${txid}/outspend/${outputIndex}").body() + } +} + +@Serializable +data class Tx( + val txid: String, + val status: TxStatus, +) { + @Serializable + data class TxStatus( + @SerialName("confirmed") + val isConfirmed: Boolean, + @SerialName("block_height") + val blockHeight: Int? = null, + @SerialName("block_hash") + val blockHash: String? = null, + ) +} + +@Serializable +data class OutputSpent( + val spent: Boolean, +) + +@Serializable +data class MerkleProof( + @SerialName("block_height") + val blockHeight: Int, + @Suppress("ArrayInDataClass") + val merkle: Array, + val pos: Int, +) diff --git a/app/src/main/java/to/bitkit/data/RestApi.kt b/app/src/main/java/to/bitkit/data/RestApi.kt deleted file mode 100644 index ddfc6294c..000000000 --- a/app/src/main/java/to/bitkit/data/RestApi.kt +++ /dev/null @@ -1,96 +0,0 @@ -package to.bitkit.data - -import io.ktor.client.HttpClient -import io.ktor.client.call.body -import io.ktor.client.request.get -import io.ktor.client.request.post -import io.ktor.client.request.setBody -import io.ktor.client.statement.HttpResponse -import kotlinx.serialization.SerialName -import kotlinx.serialization.Serializable -import to.bitkit.env.REST -import to.bitkit.ext.toHex -import javax.inject.Inject - -interface RestApi { - suspend fun getLatestBlockHash(): String - suspend fun getLatestBlockHeight(): Int - suspend fun broadcastTx(tx: ByteArray): String - suspend fun getTx(txid: String): Tx - suspend fun getTxHex(txid: String): String - suspend fun getHeader(hash: String): String - suspend fun getMerkleProof(txid: String): MerkleProof - suspend fun getOutputSpent(txid: String, outputIndex: Int): OutputSpent -} - -class EsploraApi @Inject constructor( - private val client: HttpClient, -) : RestApi { - override suspend fun getLatestBlockHash(): String { - val httpResponse: HttpResponse = client.get("$REST/blocks/tip/hash") - return httpResponse.body() - } - - override suspend fun getLatestBlockHeight(): Int { - val httpResponse: HttpResponse = client.get("$REST/blocks/tip/height") - return httpResponse.body().toInt() - } - - override suspend fun broadcastTx(tx: ByteArray): String { - val response: HttpResponse = client.post("$REST/tx") { - setBody(tx.toHex()) - } - - return response.body() - } - - override suspend fun getTx(txid: String): Tx { - return client.get("$REST/tx/${txid}").body() - } - - override suspend fun getTxHex(txid: String): String { - return client.get("$REST/tx/${txid}/hex").body() - } - - override suspend fun getHeader(hash: String): String { - return client.get("$REST/block/${hash}/header").body() - } - - override suspend fun getMerkleProof(txid: String): MerkleProof { - return client.get("$REST/tx/${txid}/merkle-proof").body() - } - - override suspend fun getOutputSpent(txid: String, outputIndex: Int): OutputSpent { - return client.get("$REST/tx/${txid}/outspend/${outputIndex}").body() - } -} - -@Serializable -data class Tx( - val txid: String, - val status: TxStatus, -) - -@Serializable -data class TxStatus( - @SerialName("confirmed") - val isConfirmed: Boolean, - @SerialName("block_height") - val blockHeight: Int? = null, - @SerialName("block_hash") - val blockHash: String? = null, -) - -@Serializable -data class OutputSpent( - val spent: Boolean, -) - -@Serializable -data class MerkleProof( - @SerialName("block_height") - val blockHeight: Int, - @Suppress("ArrayInDataClass") - val merkle: Array, - val pos: Int, -) diff --git a/app/src/main/java/to/bitkit/data/keychain/KeychainStore.kt b/app/src/main/java/to/bitkit/data/keychain/Keychain.kt similarity index 97% rename from app/src/main/java/to/bitkit/data/keychain/KeychainStore.kt rename to app/src/main/java/to/bitkit/data/keychain/Keychain.kt index bcbb7db23..c479e6d48 100644 --- a/app/src/main/java/to/bitkit/data/keychain/KeychainStore.kt +++ b/app/src/main/java/to/bitkit/data/keychain/Keychain.kt @@ -1,5 +1,3 @@ -@file:Suppress("unused") - package to.bitkit.data.keychain import android.content.Context @@ -21,8 +19,10 @@ import to.bitkit.ext.fromBase64 import to.bitkit.ext.toBase64 import to.bitkit.shared.KeychainError import javax.inject.Inject +import javax.inject.Singleton -class KeychainStore @Inject constructor( +@Singleton +class Keychain @Inject constructor( private val db: AppDb, @ApplicationContext private val context: Context, @IoDispatcher private val dispatcher: CoroutineDispatcher, diff --git a/app/src/main/java/to/bitkit/di/HttpModule.kt b/app/src/main/java/to/bitkit/di/HttpModule.kt index e0bcdd697..a4f2542dc 100644 --- a/app/src/main/java/to/bitkit/di/HttpModule.kt +++ b/app/src/main/java/to/bitkit/di/HttpModule.kt @@ -15,10 +15,6 @@ import io.ktor.http.ContentType import io.ktor.http.contentType import io.ktor.serialization.kotlinx.json.json import kotlinx.serialization.json.Json -import to.bitkit.data.BlocktankApi -import to.bitkit.data.EsploraApi -import to.bitkit.data.LspApi -import to.bitkit.data.RestApi import javax.inject.Singleton val json = Json { @@ -47,21 +43,9 @@ object HttpModule { install(ContentNegotiation) { json(json = json) } - defaultRequest { // Set default request properties + defaultRequest { contentType(ContentType.Application.Json) } } } - - @Provides - @Singleton - fun provideLspApi(blocktankApi: BlocktankApi): LspApi { - return blocktankApi - } - - @Provides - @Singleton - fun provideRestApi(esploraApi: EsploraApi): RestApi { - return esploraApi - } } diff --git a/app/src/main/java/to/bitkit/di/ServicesModule.kt b/app/src/main/java/to/bitkit/di/ServicesModule.kt index fe297d41b..5a86610de 100644 --- a/app/src/main/java/to/bitkit/di/ServicesModule.kt +++ b/app/src/main/java/to/bitkit/di/ServicesModule.kt @@ -1,4 +1,4 @@ -@file:Suppress("unused", "UNUSED_PARAMETER") +@file:Suppress("UNUSED_PARAMETER") package to.bitkit.di @@ -7,8 +7,8 @@ import dagger.Provides import dagger.hilt.InstallIn import dagger.hilt.components.SingletonComponent import kotlinx.coroutines.CoroutineDispatcher -import to.bitkit.services.OnChainService import to.bitkit.services.LightningService +import to.bitkit.services.OnChainService @Module @InstallIn(SingletonComponent::class) diff --git a/app/src/main/java/to/bitkit/di/ViewModelModule.kt b/app/src/main/java/to/bitkit/di/ViewModelModule.kt index cb59d0347..a9d26ef01 100644 --- a/app/src/main/java/to/bitkit/di/ViewModelModule.kt +++ b/app/src/main/java/to/bitkit/di/ViewModelModule.kt @@ -6,9 +6,9 @@ import dagger.hilt.InstallIn import dagger.hilt.components.SingletonComponent import kotlinx.coroutines.CoroutineDispatcher import to.bitkit.data.AppDb -import to.bitkit.data.keychain.KeychainStore -import to.bitkit.services.OnChainService +import to.bitkit.data.keychain.Keychain import to.bitkit.services.BlocktankService +import to.bitkit.services.OnChainService import to.bitkit.ui.SharedViewModel import javax.inject.Singleton @@ -20,14 +20,14 @@ object ViewModelModule { fun provideSharedViewModel( @BgDispatcher bgDispatcher: CoroutineDispatcher, appDb: AppDb, - keychainStore: KeychainStore, + keychain: Keychain, blocktankService: BlocktankService, onChainService: OnChainService, ): SharedViewModel { return SharedViewModel( bgDispatcher, appDb, - keychainStore, + keychain, blocktankService, onChainService, ) diff --git a/app/src/main/java/to/bitkit/env/Constants.kt b/app/src/main/java/to/bitkit/env/Constants.kt index 37a12091d..70de21ea8 100644 --- a/app/src/main/java/to/bitkit/env/Constants.kt +++ b/app/src/main/java/to/bitkit/env/Constants.kt @@ -1,7 +1,6 @@ package to.bitkit.env import android.util.Log -import org.lightningdevkit.ldknode.PeerDetails import to.bitkit.BuildConfig import to.bitkit.env.Tag.APP import to.bitkit.ext.ensureDir @@ -19,26 +18,27 @@ internal object Tag { const val PERF = "PERF" } -internal const val HOST = "10.0.2.2" -internal const val REST = "https://electrs-regtest.synonym.to" internal const val SEED = "universe more push obey later jazz huge buzz magnet team muscle robust" - -internal val PEER_REMOTE = LnPeer( - nodeId = "033f4d3032ce7f54224f4bd9747b50b7cd72074a859758e40e1ca46ffa79a34324", - host = HOST, - port = "9737", -) - -internal val PEER = LnPeer( - nodeId = "02faf2d1f5dc153e8931d8444c4439e46a81cb7eeadba8562e7fec3690c261ce87", - host = HOST, - port = "9737", -) // endregion // region env internal object Env { val isDebug = BuildConfig.DEBUG + val network = Network.Regtest + val trustedLnPeers = listOf( + LnPeers.remote, + // Peers.local, + ) + val ldkRgsServerUrl: String? + get() = when (network.ldk) { + LdkNetwork.BITCOIN -> "https://rapidsync.lightningdevkit.org/snapshot/" + else -> null + } + val esploraUrl: String + get() = when (network) { + Network.Regtest -> "https://electrs-regtest.synonym.to" + else -> TODO("Not yet implemented") + } object Storage { private var base = "" @@ -61,51 +61,5 @@ internal object Env { return absolutePath } } - - val network = Network.Regtest - - val trustedLnPeers = listOf( - PEER_REMOTE, - // PEER, - ) - - val ldkRgsServerUrl: String? - get() = when (network.ldk) { - LdkNetwork.BITCOIN -> "https://rapidsync.lightningdevkit.org/snapshot/" - else -> null - } } // endregion - -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, - isConnected: Boolean = false, - isPersisted: Boolean = false, - ) : this( - nodeId, - address.substringBefore(":"), - address.substringAfter(":"), - isConnected, - isPersisted, - ) - - val address get() = "$host:$port" - override fun toString() = "$nodeId@${address}" - - companion object { - fun PeerDetails.toLnPeer() = LnPeer( - nodeId = nodeId, - address = address, - isConnected = isConnected, - isPersisted = isPersisted, - ) - } -} diff --git a/app/src/main/java/to/bitkit/env/LnPeer.kt b/app/src/main/java/to/bitkit/env/LnPeer.kt new file mode 100644 index 000000000..0a55d23c4 --- /dev/null +++ b/app/src/main/java/to/bitkit/env/LnPeer.kt @@ -0,0 +1,51 @@ +package to.bitkit.env + +import org.lightningdevkit.ldknode.PeerDetails + +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, + isConnected: Boolean = false, + isPersisted: Boolean = false, + ) : this( + nodeId, + address.substringBefore(":"), + address.substringAfter(":"), + isConnected, + isPersisted, + ) + + val address get() = "$host:$port" + override fun toString() = "$nodeId@${address}" + + companion object { + fun PeerDetails.toLnPeer() = LnPeer( + nodeId = nodeId, + address = address, + isConnected = isConnected, + isPersisted = isPersisted, + ) + } +} + +internal object LnPeers { + private const val HOST = "10.0.2.2" + + val remote = LnPeer( + nodeId = "033f4d3032ce7f54224f4bd9747b50b7cd72074a859758e40e1ca46ffa79a34324", + host = HOST, + port = "9737", + ) + val local = LnPeer( + nodeId = "02faf2d1f5dc153e8931d8444c4439e46a81cb7eeadba8562e7fec3690c261ce87", + host = HOST, + port = "9738", + ) +} diff --git a/app/src/main/java/to/bitkit/ext/ByteArray.kt b/app/src/main/java/to/bitkit/ext/ByteArray.kt index 36492f358..cfab705ad 100644 --- a/app/src/main/java/to/bitkit/ext/ByteArray.kt +++ b/app/src/main/java/to/bitkit/ext/ByteArray.kt @@ -3,27 +3,26 @@ package to.bitkit.ext import android.util.Base64 -import com.google.common.io.BaseEncoding import java.io.ByteArrayOutputStream import java.io.ObjectOutputStream -fun ByteArray.toHex(): String { - return BaseEncoding.base16().encode(this).lowercase() -} - -// TODO check if this can be replaced with existing ByteArray.toHex() +// region hex val ByteArray.hex: String get() = joinToString("") { "%02x".format(it) } -val String.hex: ByteArray get() { - require(length % 2 == 0) { "Cannot convert string of uneven length to hex ByteArray: $this" } - return chunked(2) - .map { it.toInt(16).toByte() } - .toByteArray() -} +val String.hex: ByteArray + get() { + require(length % 2 == 0) { "Cannot convert string of uneven length to hex ByteArray: $this" } + return chunked(2) + .map { it.toInt(16).toByte() } + .toByteArray() + } +// endregion -fun String.asByteArray(): ByteArray { - return BaseEncoding.base16().decode(this.uppercase()) -} +// region base64 +fun ByteArray.toBase64(flags: Int = Base64.DEFAULT): String = Base64.encodeToString(this, flags) + +fun String.fromBase64(flags: Int = Base64.DEFAULT): ByteArray = Base64.decode(this, flags) +// endregion fun Any.convertToByteArray(): ByteArray { val byteArrayOutputStream = ByteArrayOutputStream() @@ -31,8 +30,4 @@ fun Any.convertToByteArray(): ByteArray { return byteArrayOutputStream.toByteArray() } -fun ByteArray.toBase64(flags: Int = Base64.DEFAULT): String = Base64.encodeToString(this, flags) - -fun String.fromBase64(flags: Int = Base64.DEFAULT): ByteArray = Base64.decode(this, flags) - val String.uByteList get() = this.toByteArray(Charsets.UTF_8).map { it.toUByte() } diff --git a/app/src/main/java/to/bitkit/ext/Context.kt b/app/src/main/java/to/bitkit/ext/Context.kt index b5769f6a8..5d387f633 100644 --- a/app/src/main/java/to/bitkit/ext/Context.kt +++ b/app/src/main/java/to/bitkit/ext/Context.kt @@ -10,8 +10,8 @@ import android.util.Log import android.widget.Toast import androidx.core.app.NotificationManagerCompat import androidx.core.content.ContextCompat -import to.bitkit.env.Tag.APP import to.bitkit.currentActivity +import to.bitkit.env.Tag.APP import to.bitkit.ui.MainActivity import java.io.File import java.io.FileOutputStream diff --git a/app/src/main/java/to/bitkit/fcm/FcmService.kt b/app/src/main/java/to/bitkit/fcm/FcmService.kt index d32573656..0f3b71a4a 100644 --- a/app/src/main/java/to/bitkit/fcm/FcmService.kt +++ b/app/src/main/java/to/bitkit/fcm/FcmService.kt @@ -12,8 +12,8 @@ import com.google.firebase.messaging.RemoteMessage import kotlinx.serialization.Serializable import kotlinx.serialization.SerializationException import kotlinx.serialization.encodeToString -import to.bitkit.env.Tag.FCM import to.bitkit.di.json +import to.bitkit.env.Tag.FCM import to.bitkit.ui.pushNotification import java.util.Date diff --git a/app/src/main/java/to/bitkit/services/BlocktankService.kt b/app/src/main/java/to/bitkit/services/BlocktankService.kt index 13c7eaa40..4a3daa642 100644 --- a/app/src/main/java/to/bitkit/services/BlocktankService.kt +++ b/app/src/main/java/to/bitkit/services/BlocktankService.kt @@ -5,11 +5,11 @@ import kotlinx.coroutines.CoroutineDispatcher import org.bitcoinj.core.ECKey import to.bitkit.async.BaseCoroutineScope import to.bitkit.async.ServiceQueue -import to.bitkit.data.LspApi +import to.bitkit.data.BlocktankApi import to.bitkit.data.RegisterDeviceRequest import to.bitkit.data.TestNotificationRequest -import to.bitkit.data.keychain.KeychainStore -import to.bitkit.data.keychain.KeychainStore.Key +import to.bitkit.data.keychain.Keychain +import to.bitkit.data.keychain.Keychain.Key import to.bitkit.di.BgDispatcher import to.bitkit.env.Tag.LSP import to.bitkit.shared.ServiceError @@ -20,9 +20,9 @@ import javax.inject.Inject class BlocktankService @Inject constructor( @BgDispatcher bgDispatcher: CoroutineDispatcher, - private val lspApi: LspApi, + private val api: BlocktankApi, private val lightningService: LightningService, - private val keychainStore: KeychainStore, + private val keychain: Keychain, ) : BaseCoroutineScope(bgDispatcher) { // region notifications @@ -41,10 +41,10 @@ class BlocktankService @Inject constructor( Log.d(LSP, "Notification encryption public key: $publicKey") // New keypair for each token registration - if (keychainStore.exists(Key.PUSH_NOTIFICATION_PRIVATE_KEY.name)) { - keychainStore.delete(Key.PUSH_NOTIFICATION_PRIVATE_KEY.name) + if (keychain.exists(Key.PUSH_NOTIFICATION_PRIVATE_KEY.name)) { + keychain.delete(Key.PUSH_NOTIFICATION_PRIVATE_KEY.name) } - keychainStore.save(Key.PUSH_NOTIFICATION_PRIVATE_KEY.name, keypair.privKeyBytes) + keychain.save(Key.PUSH_NOTIFICATION_PRIVATE_KEY.name, keypair.privKeyBytes) val payload = RegisterDeviceRequest( deviceToken = deviceToken, @@ -56,7 +56,7 @@ class BlocktankService @Inject constructor( ) ServiceQueue.LSP.background { - lspApi.registerDeviceForNotifications(payload) + api.registerDeviceForNotifications(payload) } } @@ -72,7 +72,7 @@ class BlocktankService @Inject constructor( ) ServiceQueue.LSP.background { - lspApi.testNotification(deviceToken, payload) + api.testNotification(deviceToken, payload) } } // endregion diff --git a/app/src/main/java/to/bitkit/services/LightningService.kt b/app/src/main/java/to/bitkit/services/LightningService.kt index 465a50212..712905f1d 100644 --- a/app/src/main/java/to/bitkit/services/LightningService.kt +++ b/app/src/main/java/to/bitkit/services/LightningService.kt @@ -21,7 +21,6 @@ import to.bitkit.di.BgDispatcher import to.bitkit.env.Env import to.bitkit.env.LnPeer import to.bitkit.env.LnPeer.Companion.toLnPeer -import to.bitkit.env.REST import to.bitkit.env.SEED import to.bitkit.env.Tag.LDK import to.bitkit.ext.uByteList @@ -58,7 +57,7 @@ class LightningService @Inject constructor( ) }) .apply { - setEsploraServer(REST) + setEsploraServer(Env.esploraUrl) if (Env.ldkRgsServerUrl != null) { setGossipSourceRgs(requireNotNull(Env.ldkRgsServerUrl)) } else { diff --git a/app/src/main/java/to/bitkit/services/OnChainService.kt b/app/src/main/java/to/bitkit/services/OnChainService.kt index e4154443a..6e3aa4246 100644 --- a/app/src/main/java/to/bitkit/services/OnChainService.kt +++ b/app/src/main/java/to/bitkit/services/OnChainService.kt @@ -14,7 +14,6 @@ import to.bitkit.async.BaseCoroutineScope import to.bitkit.async.ServiceQueue import to.bitkit.di.BgDispatcher import to.bitkit.env.Env -import to.bitkit.env.REST import to.bitkit.env.SEED import to.bitkit.env.Tag.BDK import to.bitkit.shared.ServiceError @@ -35,7 +34,7 @@ class OnChainService @Inject constructor( private val stopGap = 20_UL private var hasSynced = false - private val esploraClient by lazy { EsploraClient(url = REST) } + private val esploraClient by lazy { EsploraClient(url = Env.esploraUrl) } private val dbPath by lazy { Path(Env.Storage.bdk, "db.sqlite") } private var wallet: Wallet? = null diff --git a/app/src/main/java/to/bitkit/ui/MainActivity.kt b/app/src/main/java/to/bitkit/ui/MainActivity.kt index be3601515..beb276d3b 100644 --- a/app/src/main/java/to/bitkit/ui/MainActivity.kt +++ b/app/src/main/java/to/bitkit/ui/MainActivity.kt @@ -37,7 +37,6 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableIntStateOf -import androidx.compose.runtime.mutableStateListOf import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue diff --git a/app/src/main/java/to/bitkit/ui/Notifications.kt b/app/src/main/java/to/bitkit/ui/Notifications.kt index 30f884942..35e1b30b2 100644 --- a/app/src/main/java/to/bitkit/ui/Notifications.kt +++ b/app/src/main/java/to/bitkit/ui/Notifications.kt @@ -18,8 +18,8 @@ import androidx.core.app.NotificationCompat import com.google.android.gms.tasks.OnCompleteListener import com.google.firebase.messaging.FirebaseMessaging import to.bitkit.R -import to.bitkit.env.Tag.FCM import to.bitkit.currentActivity +import to.bitkit.env.Tag.FCM import to.bitkit.ext.notificationManager import to.bitkit.ext.notificationManagerCompat import to.bitkit.ext.requiresPermission diff --git a/app/src/main/java/to/bitkit/ui/SharedViewModel.kt b/app/src/main/java/to/bitkit/ui/SharedViewModel.kt index c71480583..e7476bf8d 100644 --- a/app/src/main/java/to/bitkit/ui/SharedViewModel.kt +++ b/app/src/main/java/to/bitkit/ui/SharedViewModel.kt @@ -9,21 +9,21 @@ import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.launch import kotlinx.coroutines.runBlocking import kotlinx.coroutines.tasks.await -import to.bitkit.env.Tag.DEV -import to.bitkit.env.Tag.LSP import to.bitkit.data.AppDb -import to.bitkit.data.keychain.KeychainStore +import to.bitkit.data.keychain.Keychain import to.bitkit.di.BgDispatcher import to.bitkit.env.Tag.APP -import to.bitkit.services.OnChainService +import to.bitkit.env.Tag.DEV +import to.bitkit.env.Tag.LSP import to.bitkit.services.BlocktankService +import to.bitkit.services.OnChainService import javax.inject.Inject @HiltViewModel class SharedViewModel @Inject constructor( @BgDispatcher private val bgDispatcher: CoroutineDispatcher, private val db: AppDb, - private val keychain: KeychainStore, + private val keychain: Keychain, private val blocktankService: BlocktankService, private val onChainService: OnChainService, ) : ViewModel() { @@ -76,7 +76,5 @@ class SharedViewModel @Inject constructor( blocktankService.testNotification(token) } } - // endregion - } diff --git a/app/src/main/java/to/bitkit/ui/WalletViewModel.kt b/app/src/main/java/to/bitkit/ui/WalletViewModel.kt index 53aab109f..59f4188a2 100644 --- a/app/src/main/java/to/bitkit/ui/WalletViewModel.kt +++ b/app/src/main/java/to/bitkit/ui/WalletViewModel.kt @@ -13,8 +13,8 @@ import org.lightningdevkit.ldknode.ChannelDetails import to.bitkit.di.BgDispatcher import to.bitkit.env.LnPeer import to.bitkit.env.SEED -import to.bitkit.services.OnChainService import to.bitkit.services.LightningService +import to.bitkit.services.OnChainService import to.bitkit.shared.ServiceError import javax.inject.Inject diff --git a/app/src/main/java/to/bitkit/ui/settings/PeersScreen.kt b/app/src/main/java/to/bitkit/ui/settings/PeersScreen.kt index 8f833da95..ed889bb10 100644 --- a/app/src/main/java/to/bitkit/ui/settings/PeersScreen.kt +++ b/app/src/main/java/to/bitkit/ui/settings/PeersScreen.kt @@ -17,9 +17,9 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp -import to.bitkit.env.LnPeer -import to.bitkit.env.PEER import to.bitkit.R +import to.bitkit.env.LnPeer +import to.bitkit.env.LnPeers import to.bitkit.ui.MainUiState import to.bitkit.ui.WalletViewModel import to.bitkit.ui.shared.InfoField @@ -36,9 +36,9 @@ fun PeersScreen( Card { Text("⚠️ Please return to Home screen to see your updates…", Modifier.padding(12.dp)) } - var pubKey by remember { mutableStateOf(PEER.nodeId) } - val host by remember { mutableStateOf(PEER.host) } - var port by remember { mutableStateOf(PEER.port) } + var pubKey by remember { mutableStateOf(LnPeers.local.nodeId) } + val host by remember { mutableStateOf(LnPeers.local.host) } + var port by remember { mutableStateOf(LnPeers.local.port) } Column(verticalArrangement = Arrangement.spacedBy(8.dp)) { Text( text = "Connect to a Peer", diff --git a/app/src/main/java/to/bitkit/ui/shared/Peers.kt b/app/src/main/java/to/bitkit/ui/shared/Peers.kt index 13881d4d1..544403de5 100644 --- a/app/src/main/java/to/bitkit/ui/shared/Peers.kt +++ b/app/src/main/java/to/bitkit/ui/shared/Peers.kt @@ -22,8 +22,8 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp -import to.bitkit.env.LnPeer import to.bitkit.R +import to.bitkit.env.LnPeer @Composable internal fun Peers( diff --git a/app/src/main/res/values/themes.xml b/app/src/main/res/values/themes.xml index 862899309..bf61882c1 100644 --- a/app/src/main/res/values/themes.xml +++ b/app/src/main/res/values/themes.xml @@ -1,4 +1,4 @@ - +