From 4d17bfd353c307f008157481266a3508cf1f4006 Mon Sep 17 00:00:00 2001 From: Joao Victor Sena Date: Mon, 28 Apr 2025 08:16:34 -0300 Subject: [PATCH 01/40] feat: layout --- .../wallets/receive/EditInvoiceScreen.kt | 63 +++++++++++++++++-- 1 file changed, 59 insertions(+), 4 deletions(-) diff --git a/app/src/main/java/to/bitkit/ui/screens/wallets/receive/EditInvoiceScreen.kt b/app/src/main/java/to/bitkit/ui/screens/wallets/receive/EditInvoiceScreen.kt index 9ce0e7abb..6bf2ae2b3 100644 --- a/app/src/main/java/to/bitkit/ui/screens/wallets/receive/EditInvoiceScreen.kt +++ b/app/src/main/java/to/bitkit/ui/screens/wallets/receive/EditInvoiceScreen.kt @@ -8,6 +8,8 @@ import androidx.compose.animation.slideInVertically import androidx.compose.animation.slideOutVertically import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.ExperimentalLayoutApi +import androidx.compose.foundation.layout.FlowRow import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize @@ -16,6 +18,7 @@ import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.navigationBarsPadding import androidx.compose.foundation.layout.padding import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme import androidx.compose.material3.TextField import androidx.compose.runtime.Composable @@ -26,6 +29,7 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.platform.testTag +import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp @@ -35,10 +39,12 @@ import to.bitkit.models.PrimaryDisplay import to.bitkit.ui.LocalCurrencies import to.bitkit.ui.components.AmountInputHandler import to.bitkit.ui.components.BodySSB +import to.bitkit.ui.components.ButtonSize import to.bitkit.ui.components.Caption13Up import to.bitkit.ui.components.Keyboard import to.bitkit.ui.components.NumberPadTextField import to.bitkit.ui.components.PrimaryButton +import to.bitkit.ui.components.TagButton import to.bitkit.ui.components.UnitButton import to.bitkit.ui.currencyViewModel import to.bitkit.ui.scaffold.SheetTopBar @@ -59,6 +65,7 @@ fun EditInvoiceScreen( var input: String by remember { mutableStateOf("") } var noteText by remember { mutableStateOf("") } var satsString by remember { mutableStateOf("") } + var tags by remember { mutableStateOf(listOf("")) } var keyboardVisible by remember { mutableStateOf(false) } AmountInputHandler( @@ -75,16 +82,20 @@ fun EditInvoiceScreen( noteText = noteText, primaryDisplay = currencyUiState.primaryDisplay, displayUnit = currencyUiState.displayUnit, + tags = tags, onBack = onBack, onTextChanged = { newNote -> noteText = newNote }, keyboardVisible = keyboardVisible, onClickBalance = { keyboardVisible = true }, onInputChanged = { newText -> input = newText }, onContinueKeyboard = { keyboardVisible = false }, - onContinueGeneral = { updateInvoice(satsString.toULongOrNull(), noteText) } + onContinueGeneral = { updateInvoice(satsString.toULongOrNull(), noteText) }, + onClickAddTag = {}, //TODO + onClickTag = {}//TODO ) } +@OptIn(ExperimentalLayoutApi::class) @Composable fun EditInvoiceContent( input: String, @@ -92,11 +103,14 @@ fun EditInvoiceContent( keyboardVisible: Boolean, primaryDisplay: PrimaryDisplay, displayUnit: BitcoinDisplayUnit, + tags: List, onBack: () -> Unit, onContinueKeyboard: () -> Unit, onClickBalance: () -> Unit, onContinueGeneral: () -> Unit, + onClickAddTag: () -> Unit, onTextChanged: (String) -> Unit, + onClickTag: (String) -> Unit, onInputChanged: (String) -> Unit, ) { Column( @@ -214,6 +228,38 @@ fun EditInvoiceContent( Spacer(modifier = Modifier.height(16.dp)) + Caption13Up(text = stringResource(R.string.wallet__tags), color = Colors.White64) + Spacer(modifier = Modifier.height(8.dp)) + FlowRow( + horizontalArrangement = Arrangement.spacedBy(8.dp), + verticalArrangement = Arrangement.spacedBy(8.dp), + modifier = Modifier + .fillMaxWidth() + .padding(bottom = 16.dp) + ) { + tags.map { tagText -> + TagButton( + text = tagText, + isSelected = false, + displayIconClose = true, + onClick = { onClickTag(tagText) }, + ) + } + } + PrimaryButton( + text = stringResource(R.string.wallet__tags_add), + size = ButtonSize.Small, + onClick = { onClickAddTag() }, + icon = { + Icon( + painter = painterResource(R.drawable.ic_tag), + contentDescription = null, + tint = Colors.Brand + ) + }, + fullWidth = false + ) + Spacer(modifier = Modifier.weight(1f)) PrimaryButton( @@ -244,7 +290,10 @@ private fun Preview() { onClickBalance = {}, onInputChanged = {}, onContinueGeneral = {}, - onContinueKeyboard = {} + onContinueKeyboard = {}, + tags = listOf(), + onClickAddTag = {}, + onClickTag = {} ) } } @@ -264,7 +313,10 @@ private fun Preview2() { onClickBalance = {}, onInputChanged = {}, onContinueGeneral = {}, - onContinueKeyboard = {} + onContinueKeyboard = {}, + tags = listOf("Team", "Dinner", "Home", "Work"), + onClickAddTag = {}, + onClickTag = {} ) } } @@ -284,7 +336,10 @@ private fun Preview3() { onClickBalance = {}, onInputChanged = {}, onContinueGeneral = {}, - onContinueKeyboard = {} + onContinueKeyboard = {}, + tags = listOf("Team", "Dinner"), + onClickAddTag = {}, + onClickTag = {} ) } } From 85b312c5df7efafeaa97cb2b43adaa9ec83ce9cd Mon Sep 17 00:00:00 2001 From: Joao Victor Sena Date: Mon, 28 Apr 2025 08:40:00 -0300 Subject: [PATCH 02/40] feat: navigation --- .../screens/wallets/receive/EditInvoiceScreen.kt | 5 +++-- .../screens/wallets/receive/ReceiveQrScreen.kt | 16 ++++++++++++++++ 2 files changed, 19 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/to/bitkit/ui/screens/wallets/receive/EditInvoiceScreen.kt b/app/src/main/java/to/bitkit/ui/screens/wallets/receive/EditInvoiceScreen.kt index 6bf2ae2b3..041bd62e5 100644 --- a/app/src/main/java/to/bitkit/ui/screens/wallets/receive/EditInvoiceScreen.kt +++ b/app/src/main/java/to/bitkit/ui/screens/wallets/receive/EditInvoiceScreen.kt @@ -59,6 +59,7 @@ import to.bitkit.viewmodels.CurrencyUiState fun EditInvoiceScreen( currencyUiState: CurrencyUiState = LocalCurrencies.current, updateInvoice: (ULong?, String) -> Unit, + onClickAddTag: () -> Unit, onBack: () -> Unit, ) { val currencyVM = currencyViewModel ?: return @@ -90,8 +91,8 @@ fun EditInvoiceScreen( onInputChanged = { newText -> input = newText }, onContinueKeyboard = { keyboardVisible = false }, onContinueGeneral = { updateInvoice(satsString.toULongOrNull(), noteText) }, - onClickAddTag = {}, //TODO - onClickTag = {}//TODO + onClickAddTag = onClickAddTag, + onClickTag = { tagToRemove -> tags = tags.filterNot { it == tagToRemove } } ) } diff --git a/app/src/main/java/to/bitkit/ui/screens/wallets/receive/ReceiveQrScreen.kt b/app/src/main/java/to/bitkit/ui/screens/wallets/receive/ReceiveQrScreen.kt index 5ba8ddd3b..07dec4dbd 100644 --- a/app/src/main/java/to/bitkit/ui/screens/wallets/receive/ReceiveQrScreen.kt +++ b/app/src/main/java/to/bitkit/ui/screens/wallets/receive/ReceiveQrScreen.kt @@ -55,6 +55,7 @@ import to.bitkit.ui.components.Headline import to.bitkit.ui.components.PrimaryButton import to.bitkit.ui.components.QrCodeImage import to.bitkit.ui.scaffold.SheetTopBar +import to.bitkit.ui.screens.wallets.send.AddTagScreen import to.bitkit.ui.shared.PagerWithIndicator import to.bitkit.ui.shared.util.gradientBackground import to.bitkit.ui.shared.util.shareText @@ -72,6 +73,7 @@ private object ReceiveRoutes { const val CONFIRM = "confirm" const val LIQUIDITY = "liquidity" const val EDIT_INVOICE = "edit_invoice" + const val ADD_TAG = "add_tag" } @Composable @@ -167,8 +169,22 @@ fun ReceiveQrSheet( updateInvoice = { sats, description -> wallet.updateBip21Invoice(amountSats = sats, description = description) navController.popBackStack() + }, + onClickAddTag = { + navController.navigate(ReceiveRoutes.ADD_TAG) + } + ) + } + composable(ReceiveRoutes.ADD_TAG) { + AddTagScreen( + onBack = { + navController.popBackStack() + }, + onTagSelected = { + navController.popBackStack() } ) + } } } From 6382f9b60b588b38b865af5f985e8061da9f4cfc Mon Sep 17 00:00:00 2001 From: Joao Victor Sena Date: Mon, 28 Apr 2025 09:16:32 -0300 Subject: [PATCH 03/40] feat: add and remove tag logic --- .../wallets/receive/EditInvoiceScreen.kt | 8 +++++--- .../wallets/receive/ReceiveQrScreen.kt | 15 +++++++++++---- .../to/bitkit/viewmodels/WalletViewModel.kt | 19 ++++++++++++++++++- 3 files changed, 34 insertions(+), 8 deletions(-) diff --git a/app/src/main/java/to/bitkit/ui/screens/wallets/receive/EditInvoiceScreen.kt b/app/src/main/java/to/bitkit/ui/screens/wallets/receive/EditInvoiceScreen.kt index 041bd62e5..d53ad8213 100644 --- a/app/src/main/java/to/bitkit/ui/screens/wallets/receive/EditInvoiceScreen.kt +++ b/app/src/main/java/to/bitkit/ui/screens/wallets/receive/EditInvoiceScreen.kt @@ -54,19 +54,21 @@ import to.bitkit.ui.theme.AppTextFieldDefaults import to.bitkit.ui.theme.AppThemeSurface import to.bitkit.ui.theme.Colors import to.bitkit.viewmodels.CurrencyUiState +import to.bitkit.viewmodels.MainUiState @Composable fun EditInvoiceScreen( currencyUiState: CurrencyUiState = LocalCurrencies.current, + walletUiState: MainUiState, updateInvoice: (ULong?, String) -> Unit, onClickAddTag: () -> Unit, + onClickTag: (String) -> Unit, onBack: () -> Unit, ) { val currencyVM = currencyViewModel ?: return var input: String by remember { mutableStateOf("") } var noteText by remember { mutableStateOf("") } var satsString by remember { mutableStateOf("") } - var tags by remember { mutableStateOf(listOf("")) } var keyboardVisible by remember { mutableStateOf(false) } AmountInputHandler( @@ -83,7 +85,7 @@ fun EditInvoiceScreen( noteText = noteText, primaryDisplay = currencyUiState.primaryDisplay, displayUnit = currencyUiState.displayUnit, - tags = tags, + tags = walletUiState.selectedTags, onBack = onBack, onTextChanged = { newNote -> noteText = newNote }, keyboardVisible = keyboardVisible, @@ -92,7 +94,7 @@ fun EditInvoiceScreen( onContinueKeyboard = { keyboardVisible = false }, onContinueGeneral = { updateInvoice(satsString.toULongOrNull(), noteText) }, onClickAddTag = onClickAddTag, - onClickTag = { tagToRemove -> tags = tags.filterNot { it == tagToRemove } } + onClickTag = onClickTag ) } diff --git a/app/src/main/java/to/bitkit/ui/screens/wallets/receive/ReceiveQrScreen.kt b/app/src/main/java/to/bitkit/ui/screens/wallets/receive/ReceiveQrScreen.kt index 07dec4dbd..2a59890df 100644 --- a/app/src/main/java/to/bitkit/ui/screens/wallets/receive/ReceiveQrScreen.kt +++ b/app/src/main/java/to/bitkit/ui/screens/wallets/receive/ReceiveQrScreen.kt @@ -36,6 +36,7 @@ import androidx.compose.ui.text.AnnotatedString import androidx.compose.ui.tooling.preview.Devices.PIXEL_TABLET import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp +import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.navigation.compose.NavHost import androidx.navigation.compose.composable import androidx.navigation.compose.rememberNavController @@ -164,7 +165,9 @@ fun ReceiveQrSheet( } } composable(ReceiveRoutes.EDIT_INVOICE) { + val walletUiState by wallet.uiState.collectAsStateWithLifecycle() EditInvoiceScreen( + walletUiState = walletUiState, onBack = { navController.popBackStack() }, updateInvoice = { sats, description -> wallet.updateBip21Invoice(amountSats = sats, description = description) @@ -172,7 +175,10 @@ fun ReceiveQrSheet( }, onClickAddTag = { navController.navigate(ReceiveRoutes.ADD_TAG) - } + }, + onClickTag = { tagToRemove -> + wallet.removeTag(tagToRemove) + }, ) } composable(ReceiveRoutes.ADD_TAG) { @@ -180,7 +186,8 @@ fun ReceiveQrSheet( onBack = { navController.popBackStack() }, - onTagSelected = { + onTagSelected = { tag -> + wallet.addTagToSelected(tag) navController.popBackStack() } ) @@ -257,7 +264,7 @@ private fun ReceiveQrScreen( } AnimatedVisibility(walletState.nodeLifecycleState.isRunning() && walletState.channels.isNotEmpty()) { Column { - AnimatedVisibility (!walletState.receiveOnSpendingBalance) { + AnimatedVisibility(!walletState.receiveOnSpendingBalance) { Headline( text = stringResource(R.string.wallet__receive_text_lnfunds).withAccent(accentColor = Colors.Purple) ) @@ -298,7 +305,7 @@ private fun ReceiveLightningFunds( onCjitToggle: (Boolean) -> Unit, ) { Column { - AnimatedVisibility (!cjitActive.value && cjitInvoice.value == null) { + AnimatedVisibility(!cjitActive.value && cjitInvoice.value == null) { Headline( text = stringResource(R.string.wallet__receive_text_lnfunds).withAccent(accentColor = Colors.Purple) ) diff --git a/app/src/main/java/to/bitkit/viewmodels/WalletViewModel.kt b/app/src/main/java/to/bitkit/viewmodels/WalletViewModel.kt index 7f68ef6cd..0dbb9fa56 100644 --- a/app/src/main/java/to/bitkit/viewmodels/WalletViewModel.kt +++ b/app/src/main/java/to/bitkit/viewmodels/WalletViewModel.kt @@ -510,6 +510,22 @@ class WalletViewModel @Inject constructor( } } } + + fun addTagToSelected(newTag: String) { + _uiState.update { + it.copy( + selectedTags = (it.selectedTags + newTag).distinct() + ) + } + } + + fun removeTag(tag: String) { + _uiState.update { + it.copy( + selectedTags = it.selectedTags.filterNot { tagItem -> tagItem == tag } + ) + } + } } data class MainUiState( @@ -525,5 +541,6 @@ data class MainUiState( val isRefreshing: Boolean = false, val receiveOnSpendingBalance: Boolean = true, val bip21AmountSats: ULong? = null, - val bip21Description: String = "" + val bip21Description: String = "", + val selectedTags: List = listOf(), ) From 9520cb95723bc03805751846ac3d97d85b5526f6 Mon Sep 17 00:00:00 2001 From: Joao Victor Sena Date: Mon, 28 Apr 2025 10:22:58 -0300 Subject: [PATCH 04/40] refactor: adapt attachTagsToActivity method --- .../java/to/bitkit/viewmodels/AppViewModel.kt | 27 +++++++++++++++---- 1 file changed, 22 insertions(+), 5 deletions(-) diff --git a/app/src/main/java/to/bitkit/viewmodels/AppViewModel.kt b/app/src/main/java/to/bitkit/viewmodels/AppViewModel.kt index 1548c5ef5..d60b6cbc9 100644 --- a/app/src/main/java/to/bitkit/viewmodels/AppViewModel.kt +++ b/app/src/main/java/to/bitkit/viewmodels/AppViewModel.kt @@ -196,6 +196,7 @@ class AppViewModel @Inject constructor( try { when (event) { is Event.PaymentReceived -> { + //TODO IMPLEMENT TAG HERE showNewTransactionSheet( NewTransactionSheetDetails( type = NewTransactionSheetType.LIGHTNING, @@ -523,7 +524,13 @@ class AppViewModel @Inject constructor( val result = sendOnchain(validatedAddress.address, amount) if (result.isSuccess) { val txId = result.getOrNull() - attachTagsToActivity(paymentHashOrTxId = txId, type = ActivityFilter.ONCHAIN) + val tags = _sendUiState.value.selectedTags + attachTagsToActivity( + paymentHashOrTxId = txId, + type = ActivityFilter.ONCHAIN, + txType = PaymentType.SENT, + tags = tags + ) Logger.info("Onchain send result txid: $txId") setSendEffect( SendEffect.PaymentSuccess( @@ -551,7 +558,13 @@ class AppViewModel @Inject constructor( if (result.isSuccess) { val paymentHash = result.getOrNull() Logger.info("Lightning send result payment hash: $paymentHash") - attachTagsToActivity(paymentHashOrTxId = paymentHash, type = ActivityFilter.LIGHTNING) + val tags = _sendUiState.value.selectedTags + attachTagsToActivity( + paymentHashOrTxId = paymentHash, + type = ActivityFilter.LIGHTNING, + txType = PaymentType.SENT, + tags = tags + ) setSendEffect(SendEffect.PaymentSuccess()) resetSendState() } else { @@ -584,8 +597,12 @@ class AppViewModel @Inject constructor( } } - private fun attachTagsToActivity(paymentHashOrTxId: String?, type: ActivityFilter) { - val tags = _sendUiState.value.selectedTags + private fun attachTagsToActivity( + paymentHashOrTxId: String?, + type: ActivityFilter, + txType: PaymentType, + tags: List + ) { Logger.debug("attachTagsToActivity $tags") if (tags.isEmpty()) { Logger.debug("selectedTags empty") @@ -598,7 +615,7 @@ class AppViewModel @Inject constructor( } viewModelScope.launch(Dispatchers.IO) { - val activity = coreService.activity.get(filter = type, txType = PaymentType.SENT, limit = 1u).firstOrNull() + val activity = coreService.activity.get(filter = type, txType = txType, limit = 1u).firstOrNull() if (activity == null) { Logger.error(msg = "Activity not found") From 5d8ef33d5bc9e3f18ff1c7135f0bdd0a5d8ab9b6 Mon Sep 17 00:00:00 2001 From: Joao Victor Sena Date: Mon, 28 Apr 2025 10:40:58 -0300 Subject: [PATCH 05/40] feat: apply tags WIP --- app/src/main/java/to/bitkit/viewmodels/AppViewModel.kt | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/to/bitkit/viewmodels/AppViewModel.kt b/app/src/main/java/to/bitkit/viewmodels/AppViewModel.kt index d60b6cbc9..23eef7ef6 100644 --- a/app/src/main/java/to/bitkit/viewmodels/AppViewModel.kt +++ b/app/src/main/java/to/bitkit/viewmodels/AppViewModel.kt @@ -195,8 +195,13 @@ class AppViewModel @Inject constructor( ldkNodeEventBus.events.collect { event -> try { when (event) { - is Event.PaymentReceived -> { - //TODO IMPLEMENT TAG HERE + is Event.PaymentReceived -> { //TODO HANDLE ON CHAIN EVENTS + attachTagsToActivity( + paymentHashOrTxId = event.paymentHash, + type = ActivityFilter.LIGHTNING, + txType = PaymentType.RECEIVED, + tags = listOf() //TODO GET + ) showNewTransactionSheet( NewTransactionSheetDetails( type = NewTransactionSheetType.LIGHTNING, From 039b539093f340559ac73a40b313f0769db624a2 Mon Sep 17 00:00:00 2001 From: Joao Victor Sena Date: Tue, 29 Apr 2025 08:37:20 -0300 Subject: [PATCH 06/40] feat: implement InvoiceTag table --- app/src/main/java/to/bitkit/data/AppDb.kt | 6 ++++ .../java/to/bitkit/data/dao/InvoiceTagDao.kt | 30 +++++++++++++++++++ .../bitkit/data/entities/InvoiceTagEntity.kt | 24 +++++++++++++++ .../typeConverters/StringListConverter.kt | 15 ++++++++++ 4 files changed, 75 insertions(+) create mode 100644 app/src/main/java/to/bitkit/data/dao/InvoiceTagDao.kt create mode 100644 app/src/main/java/to/bitkit/data/entities/InvoiceTagEntity.kt create mode 100644 app/src/main/java/to/bitkit/data/typeConverters/StringListConverter.kt diff --git a/app/src/main/java/to/bitkit/data/AppDb.kt b/app/src/main/java/to/bitkit/data/AppDb.kt index 8792d3fb3..0a3252826 100644 --- a/app/src/main/java/to/bitkit/data/AppDb.kt +++ b/app/src/main/java/to/bitkit/data/AppDb.kt @@ -6,6 +6,7 @@ import androidx.room.Database import androidx.room.Query import androidx.room.Room import androidx.room.RoomDatabase +import androidx.room.TypeConverters import androidx.room.Upsert import androidx.sqlite.db.SupportSQLiteDatabase import androidx.work.CoroutineWorker @@ -16,14 +17,19 @@ import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.flow.Flow import to.bitkit.BuildConfig import to.bitkit.data.entities.ConfigEntity +import to.bitkit.data.entities.InvoiceTagEntity +import to.bitkit.data.typeConverters.StringListConverter import to.bitkit.env.Env @Database( entities = [ ConfigEntity::class, + InvoiceTagEntity::class ], version = 1, ) + +@TypeConverters(StringListConverter::class) abstract class AppDb : RoomDatabase() { abstract fun configDao(): ConfigDao diff --git a/app/src/main/java/to/bitkit/data/dao/InvoiceTagDao.kt b/app/src/main/java/to/bitkit/data/dao/InvoiceTagDao.kt new file mode 100644 index 000000000..987072df9 --- /dev/null +++ b/app/src/main/java/to/bitkit/data/dao/InvoiceTagDao.kt @@ -0,0 +1,30 @@ +package to.bitkit.data.dao + +import androidx.room.Dao +import androidx.room.Delete +import androidx.room.Insert +import androidx.room.OnConflictStrategy +import androidx.room.Query +import to.bitkit.data.entities.InvoiceTagEntity + +@Dao +interface InvoiceTagDao { + + @Insert(onConflict = OnConflictStrategy.REPLACE) + suspend fun saveInvoice(invoiceTag: InvoiceTagEntity) + + @Query("SELECT * FROM invoice_tag WHERE paymentHash = :paymentHash") + suspend fun searchInvoice(paymentHash: String) + + @Delete + suspend fun deleteInvoice(invoiceTag: InvoiceTagEntity) + + @Query("DELETE FROM invoice_tag WHERE paymentHash = :paymentHash") + suspend fun deleteInvoiceByPaymentHash(paymentHash: String) + + @Query("DELETE FROM invoice_tag") + suspend fun deleteAllInvoices() + + @Query("DELETE FROM invoice_tag WHERE createdAt < :expirationTimeStamp") + suspend fun deleteExpiredInvoices(expirationTimeStamp: Long) +} diff --git a/app/src/main/java/to/bitkit/data/entities/InvoiceTagEntity.kt b/app/src/main/java/to/bitkit/data/entities/InvoiceTagEntity.kt new file mode 100644 index 000000000..1341f5d98 --- /dev/null +++ b/app/src/main/java/to/bitkit/data/entities/InvoiceTagEntity.kt @@ -0,0 +1,24 @@ +package to.bitkit.data.entities + +import androidx.room.Entity +import androidx.room.PrimaryKey +import androidx.room.TypeConverter + +@Entity(tableName = "invoice_tag") +data class InvoiceTagEntity( + @PrimaryKey val paymentHash: String, + val tags: List, + val createdAt: Long +) + +class StringListConverter { + @TypeConverter + fun fromString(value: String): List { + return value.split(",").map { it.trim() } + } + + @TypeConverter + fun fromList(list: List): String { + return list.joinToString(",") + } +} diff --git a/app/src/main/java/to/bitkit/data/typeConverters/StringListConverter.kt b/app/src/main/java/to/bitkit/data/typeConverters/StringListConverter.kt new file mode 100644 index 000000000..b41f6bd6f --- /dev/null +++ b/app/src/main/java/to/bitkit/data/typeConverters/StringListConverter.kt @@ -0,0 +1,15 @@ +package to.bitkit.data.typeConverters + +import androidx.room.TypeConverter + +class StringListConverter { + @TypeConverter + fun fromString(value: String): List { + return value.split(",").map { it.trim() } + } + + @TypeConverter + fun fromList(list: List): String { + return list.joinToString(",") + } +} From 7136d5ba9cbec0860bea48530bf008e34735bd49 Mon Sep 17 00:00:00 2001 From: Joao Victor Sena Date: Tue, 29 Apr 2025 08:38:00 -0300 Subject: [PATCH 07/40] feat: implement InvoiceTag table --- .../to/bitkit/data/entities/InvoiceTagEntity.kt | 13 ------------- 1 file changed, 13 deletions(-) diff --git a/app/src/main/java/to/bitkit/data/entities/InvoiceTagEntity.kt b/app/src/main/java/to/bitkit/data/entities/InvoiceTagEntity.kt index 1341f5d98..168037177 100644 --- a/app/src/main/java/to/bitkit/data/entities/InvoiceTagEntity.kt +++ b/app/src/main/java/to/bitkit/data/entities/InvoiceTagEntity.kt @@ -2,7 +2,6 @@ package to.bitkit.data.entities import androidx.room.Entity import androidx.room.PrimaryKey -import androidx.room.TypeConverter @Entity(tableName = "invoice_tag") data class InvoiceTagEntity( @@ -10,15 +9,3 @@ data class InvoiceTagEntity( val tags: List, val createdAt: Long ) - -class StringListConverter { - @TypeConverter - fun fromString(value: String): List { - return value.split(",").map { it.trim() } - } - - @TypeConverter - fun fromList(list: List): String { - return list.joinToString(",") - } -} From 8526b346bf7a23bb8fc1284bb25b46c7c46fd5fb Mon Sep 17 00:00:00 2001 From: Joao Victor Sena Date: Tue, 29 Apr 2025 09:19:43 -0300 Subject: [PATCH 08/40] feat: create saveInvoice method --- app/src/main/java/to/bitkit/data/AppDb.kt | 2 ++ .../java/to/bitkit/repositories/WalletRepo.kt | 28 +++++++++++++++++-- 2 files changed, 28 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/to/bitkit/data/AppDb.kt b/app/src/main/java/to/bitkit/data/AppDb.kt index 0a3252826..d0f229342 100644 --- a/app/src/main/java/to/bitkit/data/AppDb.kt +++ b/app/src/main/java/to/bitkit/data/AppDb.kt @@ -16,6 +16,7 @@ import androidx.work.WorkerParameters import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.flow.Flow import to.bitkit.BuildConfig +import to.bitkit.data.dao.InvoiceTagDao import to.bitkit.data.entities.ConfigEntity import to.bitkit.data.entities.InvoiceTagEntity import to.bitkit.data.typeConverters.StringListConverter @@ -32,6 +33,7 @@ import to.bitkit.env.Env @TypeConverters(StringListConverter::class) abstract class AppDb : RoomDatabase() { abstract fun configDao(): ConfigDao + abstract fun invoiceTagDao(): InvoiceTagDao companion object { private val DB_NAME = "${BuildConfig.APPLICATION_ID}.${Env.network.name.lowercase()}.sqlite" diff --git a/app/src/main/java/to/bitkit/repositories/WalletRepo.kt b/app/src/main/java/to/bitkit/repositories/WalletRepo.kt index fd2989a3c..81eee1b58 100644 --- a/app/src/main/java/to/bitkit/repositories/WalletRepo.kt +++ b/app/src/main/java/to/bitkit/repositories/WalletRepo.kt @@ -1,17 +1,22 @@ package to.bitkit.repositories import android.content.Context +import android.icu.util.Calendar import com.google.firebase.messaging.FirebaseMessaging import dagger.hilt.android.qualifiers.ApplicationContext import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.tasks.await import kotlinx.coroutines.withContext +import kotlinx.datetime.LocalDateTime import org.lightningdevkit.ldknode.Network +import org.lightningdevkit.ldknode.PaymentHash +import org.lightningdevkit.ldknode.Txid import to.bitkit.data.AppDb import to.bitkit.data.AppStorage import to.bitkit.data.SettingsStore import to.bitkit.data.entities.ConfigEntity +import to.bitkit.data.entities.InvoiceTagEntity import to.bitkit.data.keychain.Keychain import to.bitkit.di.BgDispatcher import to.bitkit.env.Env @@ -32,7 +37,7 @@ import javax.inject.Singleton @Singleton class WalletRepo @Inject constructor( @BgDispatcher private val bgDispatcher: CoroutineDispatcher, - @ApplicationContext private val appContext: Context, + @ApplicationContext private val appContext: Context, private val appStorage: AppStorage, private val db: AppDb, private val keychain: Keychain, @@ -166,7 +171,8 @@ class WalletRepo @Inject constructor( suspend fun getBlocktankInfo(): Result = withContext(bgDispatcher) { try { - val info = coreService.blocktank.info(refresh = true) ?: return@withContext Result.failure(Exception("Couldn't get info")) + val info = coreService.blocktank.info(refresh = true) + ?: return@withContext Result.failure(Exception("Couldn't get info")) Result.success(info) } catch (e: Throwable) { Logger.error("Blocktank info error", e) @@ -238,7 +244,25 @@ class WalletRepo @Inject constructor( return db.configDao().getAll() } + suspend fun saveInvoice(txId: Txid, tags: List) = withContext(bgDispatcher) { + try { + db.invoiceTagDao().saveInvoice( + invoiceTag = InvoiceTagEntity( + paymentHash = txId, + tags = tags, + createdAt = Calendar.getInstance().time.time + ) + ) + } catch (e: Throwable) { + Logger.error("saveInvoice error", e, context = TAG) + } + } + private fun generateEntropyMnemonic(): String { return org.lightningdevkit.ldknode.generateEntropyMnemonic() } + + private companion object { + const val TAG = "WalletRepo" + } } From 3d008818eb2a8862b9a15537a196c7cd717d63b5 Mon Sep 17 00:00:00 2001 From: Joao Victor Sena Date: Tue, 29 Apr 2025 09:27:15 -0300 Subject: [PATCH 09/40] feat: create searchInvoice method --- app/src/main/java/to/bitkit/data/dao/InvoiceTagDao.kt | 4 ++-- app/src/main/java/to/bitkit/repositories/WalletRepo.kt | 10 ++++++++++ 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/to/bitkit/data/dao/InvoiceTagDao.kt b/app/src/main/java/to/bitkit/data/dao/InvoiceTagDao.kt index 987072df9..0b474b12c 100644 --- a/app/src/main/java/to/bitkit/data/dao/InvoiceTagDao.kt +++ b/app/src/main/java/to/bitkit/data/dao/InvoiceTagDao.kt @@ -13,8 +13,8 @@ interface InvoiceTagDao { @Insert(onConflict = OnConflictStrategy.REPLACE) suspend fun saveInvoice(invoiceTag: InvoiceTagEntity) - @Query("SELECT * FROM invoice_tag WHERE paymentHash = :paymentHash") - suspend fun searchInvoice(paymentHash: String) + @Query("SELECT * FROM invoice_tag WHERE paymentHash = :paymentHash LIMIT 1") + suspend fun searchInvoice(paymentHash: String) : InvoiceTagEntity? @Delete suspend fun deleteInvoice(invoiceTag: InvoiceTagEntity) diff --git a/app/src/main/java/to/bitkit/repositories/WalletRepo.kt b/app/src/main/java/to/bitkit/repositories/WalletRepo.kt index 81eee1b58..00862e0a0 100644 --- a/app/src/main/java/to/bitkit/repositories/WalletRepo.kt +++ b/app/src/main/java/to/bitkit/repositories/WalletRepo.kt @@ -258,6 +258,16 @@ class WalletRepo @Inject constructor( } } + suspend fun searchInvoice(txId: Txid): Result = withContext(bgDispatcher) { + return@withContext try { + val invoiceTag = db.invoiceTagDao().searchInvoice(paymentHash = txId) ?: return@withContext Result.failure(Exception("Result not found")) + Result.success(invoiceTag) + } catch (e: Throwable) { + Logger.error("saveInvoice error", e, context = TAG) + Result.failure(e) + } + } + private fun generateEntropyMnemonic(): String { return org.lightningdevkit.ldknode.generateEntropyMnemonic() } From 59e1e0be7b033b5904dc7a4fc8f7e7fbffbcffb1 Mon Sep 17 00:00:00 2001 From: Joao Victor Sena Date: Tue, 29 Apr 2025 09:38:33 -0300 Subject: [PATCH 10/40] feat: create deleteInvoice method --- .../main/java/to/bitkit/repositories/WalletRepo.kt | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/to/bitkit/repositories/WalletRepo.kt b/app/src/main/java/to/bitkit/repositories/WalletRepo.kt index 00862e0a0..d266bc65b 100644 --- a/app/src/main/java/to/bitkit/repositories/WalletRepo.kt +++ b/app/src/main/java/to/bitkit/repositories/WalletRepo.kt @@ -260,14 +260,22 @@ class WalletRepo @Inject constructor( suspend fun searchInvoice(txId: Txid): Result = withContext(bgDispatcher) { return@withContext try { - val invoiceTag = db.invoiceTagDao().searchInvoice(paymentHash = txId) ?: return@withContext Result.failure(Exception("Result not found")) + val invoiceTag = db.invoiceTagDao().searchInvoice(paymentHash = txId) ?: return@withContext Result.failure(Exception("Invoice not found")) Result.success(invoiceTag) } catch (e: Throwable) { - Logger.error("saveInvoice error", e, context = TAG) + Logger.error("searchInvoice error", e, context = TAG) Result.failure(e) } } + suspend fun deleteInvoice(txId: Txid) = withContext(bgDispatcher) { + try { + db.invoiceTagDao().deleteInvoiceByPaymentHash(paymentHash = txId) + } catch (e: Throwable) { + Logger.error("deleteInvoice error", e, context = TAG) + } + } + private fun generateEntropyMnemonic(): String { return org.lightningdevkit.ldknode.generateEntropyMnemonic() } From a15cef906d8a4692c7c189120593794dc52a8eef Mon Sep 17 00:00:00 2001 From: Joao Victor Sena Date: Tue, 29 Apr 2025 09:39:35 -0300 Subject: [PATCH 11/40] feat: implement deleteAllInvoices method --- app/src/main/java/to/bitkit/repositories/WalletRepo.kt | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/app/src/main/java/to/bitkit/repositories/WalletRepo.kt b/app/src/main/java/to/bitkit/repositories/WalletRepo.kt index d266bc65b..1d332bbf6 100644 --- a/app/src/main/java/to/bitkit/repositories/WalletRepo.kt +++ b/app/src/main/java/to/bitkit/repositories/WalletRepo.kt @@ -85,6 +85,7 @@ class WalletRepo @Inject constructor( appStorage.clear() settingsStore.wipe() coreService.activity.removeAll() + deleteAllInvoices() Result.success(Unit) } catch (e: Throwable) { Logger.error("Wipe wallet error", e) @@ -276,6 +277,14 @@ class WalletRepo @Inject constructor( } } + suspend fun deleteAllInvoices() = withContext(bgDispatcher) { + try { + db.invoiceTagDao().deleteAllInvoices() + } catch (e: Throwable) { + Logger.error("deleteAllInvoices error", e, context = TAG) + } + } + private fun generateEntropyMnemonic(): String { return org.lightningdevkit.ldknode.generateEntropyMnemonic() } From 624fa1d66880a6936506448df6752b722522ddc3 Mon Sep 17 00:00:00 2001 From: Joao Victor Sena Date: Tue, 29 Apr 2025 09:43:41 -0300 Subject: [PATCH 12/40] feat: create deleteExpiredInvoices method --- .../java/to/bitkit/repositories/WalletRepo.kt | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/app/src/main/java/to/bitkit/repositories/WalletRepo.kt b/app/src/main/java/to/bitkit/repositories/WalletRepo.kt index 1d332bbf6..e8a86b24b 100644 --- a/app/src/main/java/to/bitkit/repositories/WalletRepo.kt +++ b/app/src/main/java/to/bitkit/repositories/WalletRepo.kt @@ -8,9 +8,7 @@ import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.tasks.await import kotlinx.coroutines.withContext -import kotlinx.datetime.LocalDateTime import org.lightningdevkit.ldknode.Network -import org.lightningdevkit.ldknode.PaymentHash import org.lightningdevkit.ldknode.Txid import to.bitkit.data.AppDb import to.bitkit.data.AppStorage @@ -24,10 +22,8 @@ import to.bitkit.models.BalanceState import to.bitkit.models.NewTransactionSheetDetails import to.bitkit.models.NewTransactionSheetDirection import to.bitkit.models.NewTransactionSheetType -import to.bitkit.models.Toast import to.bitkit.services.BlocktankNotificationsService import to.bitkit.services.CoreService -import to.bitkit.ui.shared.toast.ToastEventBus import to.bitkit.utils.Bip21Utils import to.bitkit.utils.Logger import uniffi.bitkitcore.IBtInfo @@ -285,6 +281,18 @@ class WalletRepo @Inject constructor( } } + suspend fun deleteExpiredInvoices() = withContext(bgDispatcher) { + try { + val twoDaysExpiration = Calendar.getInstance().apply { + add(Calendar.DAY_OF_YEAR, 2) + }.time.time + + db.invoiceTagDao().deleteExpiredInvoices(expirationTimeStamp = twoDaysExpiration) + } catch (e: Throwable) { + Logger.error("deleteExpiredInvoices error", e, context = TAG) + } + } + private fun generateEntropyMnemonic(): String { return org.lightningdevkit.ldknode.generateEntropyMnemonic() } From 3da0512a95bd61250ba0bd205ad45ad77cc96db0 Mon Sep 17 00:00:00 2001 From: Joao Victor Sena Date: Tue, 29 Apr 2025 09:44:29 -0300 Subject: [PATCH 13/40] feat: create deleteExpiredInvoices method --- app/src/main/java/to/bitkit/repositories/WalletRepo.kt | 1 + 1 file changed, 1 insertion(+) diff --git a/app/src/main/java/to/bitkit/repositories/WalletRepo.kt b/app/src/main/java/to/bitkit/repositories/WalletRepo.kt index e8a86b24b..f17ba6e4e 100644 --- a/app/src/main/java/to/bitkit/repositories/WalletRepo.kt +++ b/app/src/main/java/to/bitkit/repositories/WalletRepo.kt @@ -243,6 +243,7 @@ class WalletRepo @Inject constructor( suspend fun saveInvoice(txId: Txid, tags: List) = withContext(bgDispatcher) { try { + deleteExpiredInvoices() db.invoiceTagDao().saveInvoice( invoiceTag = InvoiceTagEntity( paymentHash = txId, From e8017d6fb2ed649de4d86d207b3fc03e93126e66 Mon Sep 17 00:00:00 2001 From: Joao Victor Sena Date: Tue, 29 Apr 2025 10:25:30 -0300 Subject: [PATCH 14/40] feat: save invoice with tags --- .../java/to/bitkit/repositories/WalletRepo.kt | 24 +++++++++++++------ .../to/bitkit/viewmodels/WalletViewModel.kt | 4 +--- 2 files changed, 18 insertions(+), 10 deletions(-) diff --git a/app/src/main/java/to/bitkit/repositories/WalletRepo.kt b/app/src/main/java/to/bitkit/repositories/WalletRepo.kt index f17ba6e4e..a0bd8cd58 100644 --- a/app/src/main/java/to/bitkit/repositories/WalletRepo.kt +++ b/app/src/main/java/to/bitkit/repositories/WalletRepo.kt @@ -27,6 +27,7 @@ import to.bitkit.services.CoreService import to.bitkit.utils.Bip21Utils import to.bitkit.utils.Logger import uniffi.bitkitcore.IBtInfo +import uniffi.bitkitcore.Scanner import javax.inject.Inject import javax.inject.Singleton @@ -241,16 +242,25 @@ class WalletRepo @Inject constructor( return db.configDao().getAll() } - suspend fun saveInvoice(txId: Txid, tags: List) = withContext(bgDispatcher) { + suspend fun saveInvoiceWithTags(bip21Invoice: String, tags: List) = withContext(bgDispatcher) { try { deleteExpiredInvoices() - db.invoiceTagDao().saveInvoice( - invoiceTag = InvoiceTagEntity( - paymentHash = txId, - tags = tags, - createdAt = Calendar.getInstance().time.time + val decoded = uniffi.bitkitcore.decode(bip21Invoice) + val paymentHashOrAddress = when(decoded) { + is Scanner.Lightning -> decoded.invoice.paymentHash + is Scanner.OnChain -> decoded.invoice.address + else -> null + } + + paymentHashOrAddress?.let { + db.invoiceTagDao().saveInvoice( + invoiceTag = InvoiceTagEntity( + paymentHash = decoded.toString(), + tags = tags, + createdAt = Calendar.getInstance().time.time + ) ) - ) + } } catch (e: Throwable) { Logger.error("saveInvoice error", e, context = TAG) } diff --git a/app/src/main/java/to/bitkit/viewmodels/WalletViewModel.kt b/app/src/main/java/to/bitkit/viewmodels/WalletViewModel.kt index 3fd86818a..8dacc413f 100644 --- a/app/src/main/java/to/bitkit/viewmodels/WalletViewModel.kt +++ b/app/src/main/java/to/bitkit/viewmodels/WalletViewModel.kt @@ -9,7 +9,6 @@ import androidx.lifecycle.viewModelScope import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.qualifiers.ApplicationContext import kotlinx.coroutines.CoroutineDispatcher -import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.filter @@ -18,7 +17,6 @@ import kotlinx.coroutines.launch import org.lightningdevkit.ldknode.BalanceDetails import org.lightningdevkit.ldknode.ChannelDetails import org.lightningdevkit.ldknode.Event -import org.lightningdevkit.ldknode.Network import org.lightningdevkit.ldknode.NodeStatus import to.bitkit.di.BgDispatcher import to.bitkit.env.Env @@ -308,7 +306,7 @@ class WalletViewModel @Inject constructor( lightningInvoice = walletRepo.getBolt11() ) walletRepo.setBip21(newBip21) - + walletRepo.saveInvoiceWithTags(bip21Invoice = newBip21, tags = _uiState.value.selectedTags) syncState() } } From 2841c693402ab5dfa1ca38e7ecfda0a763d53852 Mon Sep 17 00:00:00 2001 From: Joao Victor Sena Date: Tue, 29 Apr 2025 10:36:53 -0300 Subject: [PATCH 15/40] feat: attach invoice to tags --- app/src/main/java/to/bitkit/viewmodels/AppViewModel.kt | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/to/bitkit/viewmodels/AppViewModel.kt b/app/src/main/java/to/bitkit/viewmodels/AppViewModel.kt index 29a178577..4a9936181 100644 --- a/app/src/main/java/to/bitkit/viewmodels/AppViewModel.kt +++ b/app/src/main/java/to/bitkit/viewmodels/AppViewModel.kt @@ -35,6 +35,7 @@ import to.bitkit.models.Toast import to.bitkit.models.toActivityFilter import to.bitkit.models.toTxType import to.bitkit.repositories.LightningRepo +import to.bitkit.repositories.WalletRepo import to.bitkit.services.CoreService import to.bitkit.services.LdkNodeEventBus import to.bitkit.services.ScannerService @@ -57,6 +58,7 @@ class AppViewModel @Inject constructor( private val keychain: Keychain, private val scannerService: ScannerService, private val lightningService: LightningRepo, + private val walletRepo: WalletRepo, private val coreService: CoreService, private val ldkNodeEventBus: LdkNodeEventBus, private val settingsStore: SettingsStore, @@ -204,12 +206,13 @@ class AppViewModel @Inject constructor( ldkNodeEventBus.events.collect { event -> try { when (event) { - is Event.PaymentReceived -> { //TODO HANDLE ON CHAIN EVENTS + is Event.PaymentReceived -> { + val tags = walletRepo.searchInvoice(txId = event.paymentHash).getOrNull()?.tags.orEmpty() //TODO CREATE METHOD attachTagsToActivity( paymentHashOrTxId = event.paymentHash, type = ActivityFilter.LIGHTNING, txType = PaymentType.RECEIVED, - tags = listOf() //TODO GET + tags = tags ) showNewTransactionSheet( NewTransactionSheetDetails( From 9a16cba046af038fdcd601f981a36af64aa54fa1 Mon Sep 17 00:00:00 2001 From: Joao Victor Sena Date: Tue, 29 Apr 2025 11:47:00 -0300 Subject: [PATCH 16/40] fix: extract payment hash --- .../java/to/bitkit/repositories/WalletRepo.kt | 26 ++++++++++++++++--- .../java/to/bitkit/viewmodels/AppViewModel.kt | 2 +- 2 files changed, 23 insertions(+), 5 deletions(-) diff --git a/app/src/main/java/to/bitkit/repositories/WalletRepo.kt b/app/src/main/java/to/bitkit/repositories/WalletRepo.kt index a0bd8cd58..f5402f74b 100644 --- a/app/src/main/java/to/bitkit/repositories/WalletRepo.kt +++ b/app/src/main/java/to/bitkit/repositories/WalletRepo.kt @@ -28,6 +28,7 @@ import to.bitkit.utils.Bip21Utils import to.bitkit.utils.Logger import uniffi.bitkitcore.IBtInfo import uniffi.bitkitcore.Scanner +import uniffi.bitkitcore.decode import javax.inject.Inject import javax.inject.Singleton @@ -242,20 +243,21 @@ class WalletRepo @Inject constructor( return db.configDao().getAll() } + @OptIn(ExperimentalStdlibApi::class) suspend fun saveInvoiceWithTags(bip21Invoice: String, tags: List) = withContext(bgDispatcher) { try { deleteExpiredInvoices() - val decoded = uniffi.bitkitcore.decode(bip21Invoice) + val decoded = decode(bip21Invoice) val paymentHashOrAddress = when(decoded) { - is Scanner.Lightning -> decoded.invoice.paymentHash - is Scanner.OnChain -> decoded.invoice.address + is Scanner.Lightning -> decoded.invoice.paymentHash.toHexString() + is Scanner.OnChain -> decoded.extractLightningHashOrAddress() else -> null } paymentHashOrAddress?.let { db.invoiceTagDao().saveInvoice( invoiceTag = InvoiceTagEntity( - paymentHash = decoded.toString(), + paymentHash = paymentHashOrAddress, tags = tags, createdAt = Calendar.getInstance().time.time ) @@ -304,6 +306,22 @@ class WalletRepo @Inject constructor( } } + @OptIn(ExperimentalStdlibApi::class) + private suspend fun Scanner.OnChain.extractLightningHashOrAddress(): String { + val address = this.invoice.address + val lightningInvoice: String = this.invoice.params?.get("lightning") ?: address + val decoded = decode(lightningInvoice) + + val paymentHash = when(decoded) { + is Scanner.Lightning -> decoded.invoice.paymentHash.toHexString() + else -> null + } ?: address + + return paymentHash + + return address + } + private fun generateEntropyMnemonic(): String { return org.lightningdevkit.ldknode.generateEntropyMnemonic() } diff --git a/app/src/main/java/to/bitkit/viewmodels/AppViewModel.kt b/app/src/main/java/to/bitkit/viewmodels/AppViewModel.kt index 4a9936181..2fb3fbfa9 100644 --- a/app/src/main/java/to/bitkit/viewmodels/AppViewModel.kt +++ b/app/src/main/java/to/bitkit/viewmodels/AppViewModel.kt @@ -207,7 +207,7 @@ class AppViewModel @Inject constructor( try { when (event) { is Event.PaymentReceived -> { - val tags = walletRepo.searchInvoice(txId = event.paymentHash).getOrNull()?.tags.orEmpty() //TODO CREATE METHOD + val tags = walletRepo.searchInvoice(txId = event.paymentHash).getOrNull()?.tags.orEmpty() //TODO EXTRACT TO METHOD attachTagsToActivity( paymentHashOrTxId = event.paymentHash, type = ActivityFilter.LIGHTNING, From dc5038765d707843744d4c7f658b9f0db1efc629 Mon Sep 17 00:00:00 2001 From: Joao Victor Sena Date: Tue, 29 Apr 2025 11:47:48 -0300 Subject: [PATCH 17/40] fix: skip saveInvoiceWithTags with empty tags --- app/src/main/java/to/bitkit/repositories/WalletRepo.kt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/src/main/java/to/bitkit/repositories/WalletRepo.kt b/app/src/main/java/to/bitkit/repositories/WalletRepo.kt index f5402f74b..0b2dca83d 100644 --- a/app/src/main/java/to/bitkit/repositories/WalletRepo.kt +++ b/app/src/main/java/to/bitkit/repositories/WalletRepo.kt @@ -245,6 +245,8 @@ class WalletRepo @Inject constructor( @OptIn(ExperimentalStdlibApi::class) suspend fun saveInvoiceWithTags(bip21Invoice: String, tags: List) = withContext(bgDispatcher) { + if (tags.isEmpty()) return@withContext + try { deleteExpiredInvoices() val decoded = decode(bip21Invoice) From 2883e7de9840a4052a9edc4bc1257f3549ab2848 Mon Sep 17 00:00:00 2001 From: Joao Victor Sena Date: Tue, 29 Apr 2025 12:03:08 -0300 Subject: [PATCH 18/40] fix: clear tag --- app/src/main/java/to/bitkit/viewmodels/WalletViewModel.kt | 1 + 1 file changed, 1 insertion(+) diff --git a/app/src/main/java/to/bitkit/viewmodels/WalletViewModel.kt b/app/src/main/java/to/bitkit/viewmodels/WalletViewModel.kt index 8dacc413f..226855426 100644 --- a/app/src/main/java/to/bitkit/viewmodels/WalletViewModel.kt +++ b/app/src/main/java/to/bitkit/viewmodels/WalletViewModel.kt @@ -307,6 +307,7 @@ class WalletViewModel @Inject constructor( ) walletRepo.setBip21(newBip21) walletRepo.saveInvoiceWithTags(bip21Invoice = newBip21, tags = _uiState.value.selectedTags) + _uiState.update { it.copy(selectedTags = listOf()) } syncState() } } From 3e68d0991595f54f8722f91cac3e4276e22fed5a Mon Sep 17 00:00:00 2001 From: Joao Victor Sena Date: Tue, 29 Apr 2025 13:20:22 -0300 Subject: [PATCH 19/40] fix: move description state to viewmodel --- .../ui/screens/wallets/receive/EditInvoiceScreen.kt | 8 ++++---- .../bitkit/ui/screens/wallets/receive/ReceiveQrScreen.kt | 3 +++ app/src/main/java/to/bitkit/viewmodels/WalletViewModel.kt | 4 ++++ 3 files changed, 11 insertions(+), 4 deletions(-) diff --git a/app/src/main/java/to/bitkit/ui/screens/wallets/receive/EditInvoiceScreen.kt b/app/src/main/java/to/bitkit/ui/screens/wallets/receive/EditInvoiceScreen.kt index d53ad8213..708d6b62d 100644 --- a/app/src/main/java/to/bitkit/ui/screens/wallets/receive/EditInvoiceScreen.kt +++ b/app/src/main/java/to/bitkit/ui/screens/wallets/receive/EditInvoiceScreen.kt @@ -63,11 +63,11 @@ fun EditInvoiceScreen( updateInvoice: (ULong?, String) -> Unit, onClickAddTag: () -> Unit, onClickTag: (String) -> Unit, + onDescriptionUpdate: (String) -> Unit, onBack: () -> Unit, ) { val currencyVM = currencyViewModel ?: return var input: String by remember { mutableStateOf("") } - var noteText by remember { mutableStateOf("") } var satsString by remember { mutableStateOf("") } var keyboardVisible by remember { mutableStateOf(false) } @@ -82,17 +82,17 @@ fun EditInvoiceScreen( EditInvoiceContent( input = input, - noteText = noteText, + noteText = walletUiState.bip21Description, primaryDisplay = currencyUiState.primaryDisplay, displayUnit = currencyUiState.displayUnit, tags = walletUiState.selectedTags, onBack = onBack, - onTextChanged = { newNote -> noteText = newNote }, + onTextChanged = onDescriptionUpdate, keyboardVisible = keyboardVisible, onClickBalance = { keyboardVisible = true }, onInputChanged = { newText -> input = newText }, onContinueKeyboard = { keyboardVisible = false }, - onContinueGeneral = { updateInvoice(satsString.toULongOrNull(), noteText) }, + onContinueGeneral = { updateInvoice(satsString.toULongOrNull(), walletUiState.bip21Description) }, onClickAddTag = onClickAddTag, onClickTag = onClickTag ) diff --git a/app/src/main/java/to/bitkit/ui/screens/wallets/receive/ReceiveQrScreen.kt b/app/src/main/java/to/bitkit/ui/screens/wallets/receive/ReceiveQrScreen.kt index 2a59890df..bc376ffda 100644 --- a/app/src/main/java/to/bitkit/ui/screens/wallets/receive/ReceiveQrScreen.kt +++ b/app/src/main/java/to/bitkit/ui/screens/wallets/receive/ReceiveQrScreen.kt @@ -179,6 +179,9 @@ fun ReceiveQrSheet( onClickTag = { tagToRemove -> wallet.removeTag(tagToRemove) }, + onDescriptionUpdate = { newText -> + wallet.updateBip21Description(newText = newText) + } ) } composable(ReceiveRoutes.ADD_TAG) { diff --git a/app/src/main/java/to/bitkit/viewmodels/WalletViewModel.kt b/app/src/main/java/to/bitkit/viewmodels/WalletViewModel.kt index 226855426..1c45dcc92 100644 --- a/app/src/main/java/to/bitkit/viewmodels/WalletViewModel.kt +++ b/app/src/main/java/to/bitkit/viewmodels/WalletViewModel.kt @@ -525,6 +525,10 @@ class WalletViewModel @Inject constructor( ) } } + + fun updateBip21Description(newText: String) { + _uiState.update { it.copy(bip21Description = newText) } + } } data class MainUiState( From b2b7cd468993387a44a70fb8ea1e29aee1514a47 Mon Sep 17 00:00:00 2001 From: Joao Victor Sena Date: Tue, 29 Apr 2025 13:30:37 -0300 Subject: [PATCH 20/40] fix: reset wallet state --- app/src/main/java/to/bitkit/viewmodels/WalletViewModel.kt | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/to/bitkit/viewmodels/WalletViewModel.kt b/app/src/main/java/to/bitkit/viewmodels/WalletViewModel.kt index 1c45dcc92..06e554702 100644 --- a/app/src/main/java/to/bitkit/viewmodels/WalletViewModel.kt +++ b/app/src/main/java/to/bitkit/viewmodels/WalletViewModel.kt @@ -307,7 +307,7 @@ class WalletViewModel @Inject constructor( ) walletRepo.setBip21(newBip21) walletRepo.saveInvoiceWithTags(bip21Invoice = newBip21, tags = _uiState.value.selectedTags) - _uiState.update { it.copy(selectedTags = listOf()) } + resetWalletState() syncState() } } @@ -529,6 +529,10 @@ class WalletViewModel @Inject constructor( fun updateBip21Description(newText: String) { _uiState.update { it.copy(bip21Description = newText) } } + + private fun resetWalletState() { + _uiState.update { it.copy(selectedTags = listOf(), bip21Description = "") } + } } data class MainUiState( From 2643c9b7375ec4983619352afa292aaf005b112f Mon Sep 17 00:00:00 2001 From: Joao Victor Sena Date: Wed, 30 Apr 2025 07:45:59 -0300 Subject: [PATCH 21/40] feat: add todo --- app/src/main/java/to/bitkit/viewmodels/AppViewModel.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/java/to/bitkit/viewmodels/AppViewModel.kt b/app/src/main/java/to/bitkit/viewmodels/AppViewModel.kt index 2fb3fbfa9..d66d4d91f 100644 --- a/app/src/main/java/to/bitkit/viewmodels/AppViewModel.kt +++ b/app/src/main/java/to/bitkit/viewmodels/AppViewModel.kt @@ -682,7 +682,7 @@ class AppViewModel @Inject constructor( amount: ULong? = null, ): Result { return try { - val hash = lightningService.payInvoice(bolt11 = bolt11, sats = amount).getOrNull() + val hash = lightningService.payInvoice(bolt11 = bolt11, sats = amount).getOrNull() //TODO HANDLE FAILURE IN OTHER PR // Wait until matching payment event is received val result = ldkNodeEventBus.events.watchUntil { event -> From 0094c2d9cb4b238b2e62a7e26c147bc63bede184 Mon Sep 17 00:00:00 2001 From: Joao Victor Sena Date: Wed, 30 Apr 2025 08:51:20 -0300 Subject: [PATCH 22/40] fix: handle activity delay --- .../java/to/bitkit/viewmodels/AppViewModel.kt | 27 +++++++++++++++++-- 1 file changed, 25 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/to/bitkit/viewmodels/AppViewModel.kt b/app/src/main/java/to/bitkit/viewmodels/AppViewModel.kt index d66d4d91f..88890cfda 100644 --- a/app/src/main/java/to/bitkit/viewmodels/AppViewModel.kt +++ b/app/src/main/java/to/bitkit/viewmodels/AppViewModel.kt @@ -52,6 +52,7 @@ import uniffi.bitkitcore.OnChainInvoice import uniffi.bitkitcore.PaymentType import uniffi.bitkitcore.Scanner import javax.inject.Inject +import kotlin.time.Duration.Companion.seconds @HiltViewModel class AppViewModel @Inject constructor( @@ -632,7 +633,23 @@ class AppViewModel @Inject constructor( } viewModelScope.launch(Dispatchers.IO) { - val activity = coreService.activity.get(filter = type, txType = txType, limit = 1u).firstOrNull() + var activity = coreService.activity.get(filter = type, txType = txType, limit = 10u).firstOrNull { activityItem -> + when (activityItem) { + is Activity.Lightning -> paymentHashOrTxId == activityItem.v1.id + is Activity.Onchain -> paymentHashOrTxId == activityItem.v1.txId + } + } + + if (activity == null) { + Logger.warn("activity not found, trying again after delay") + delay(5.seconds) + activity = coreService.activity.get(filter = type, txType = txType, limit = 10u).firstOrNull { activityItem -> + when (activityItem) { + is Activity.Lightning -> paymentHashOrTxId == activityItem.v1.id + is Activity.Onchain -> paymentHashOrTxId == activityItem.v1.txId + } + } + } if (activity == null) { Logger.error(msg = "Activity not found") @@ -647,6 +664,8 @@ class AppViewModel @Inject constructor( tags = tags ).onFailure { Logger.error("Error attaching tags $tags") + }.onSuccess { + Logger.info("Success attatching tags $tags to activity ${activity.v1.id}") } } else { Logger.error("Different activity id. Expected: $paymentHashOrTxId found: ${activity.v1.id}") @@ -658,7 +677,11 @@ class AppViewModel @Inject constructor( coreService.activity.appendTags( toActivityId = activity.v1.id, tags = tags - ) + ).onFailure { + Logger.error("Error attaching tags $tags") + }.onSuccess { + Logger.info("Success attatching tags $tags to activity ${activity.v1.id}") + } } else { Logger.error("Different txId. Expected: $paymentHashOrTxId found: ${activity.v1.txId}") } From 884041ea1ab9423eca4587a45684619af99c1db8 Mon Sep 17 00:00:00 2001 From: Joao Victor Sena Date: Wed, 30 Apr 2025 08:52:33 -0300 Subject: [PATCH 23/40] feat: delete invoice after success --- app/src/main/java/to/bitkit/viewmodels/AppViewModel.kt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/src/main/java/to/bitkit/viewmodels/AppViewModel.kt b/app/src/main/java/to/bitkit/viewmodels/AppViewModel.kt index 88890cfda..5f979ec5a 100644 --- a/app/src/main/java/to/bitkit/viewmodels/AppViewModel.kt +++ b/app/src/main/java/to/bitkit/viewmodels/AppViewModel.kt @@ -666,6 +666,7 @@ class AppViewModel @Inject constructor( Logger.error("Error attaching tags $tags") }.onSuccess { Logger.info("Success attatching tags $tags to activity ${activity.v1.id}") + walletRepo.deleteInvoice(txId = paymentHashOrTxId) } } else { Logger.error("Different activity id. Expected: $paymentHashOrTxId found: ${activity.v1.id}") @@ -681,6 +682,7 @@ class AppViewModel @Inject constructor( Logger.error("Error attaching tags $tags") }.onSuccess { Logger.info("Success attatching tags $tags to activity ${activity.v1.id}") + walletRepo.deleteInvoice(txId = paymentHashOrTxId) } } else { Logger.error("Different txId. Expected: $paymentHashOrTxId found: ${activity.v1.txId}") From 8766537e433b8a0d331d35db4e45b3c413f91fe1 Mon Sep 17 00:00:00 2001 From: Joao Victor Sena Date: Wed, 30 Apr 2025 13:39:54 -0300 Subject: [PATCH 24/40] feat: schema changes --- app/schemas/to.bitkit.data.AppDb/1.json | 36 +++++++++++++++++++++++-- 1 file changed, 34 insertions(+), 2 deletions(-) diff --git a/app/schemas/to.bitkit.data.AppDb/1.json b/app/schemas/to.bitkit.data.AppDb/1.json index 4b5011545..21f3ff982 100644 --- a/app/schemas/to.bitkit.data.AppDb/1.json +++ b/app/schemas/to.bitkit.data.AppDb/1.json @@ -2,7 +2,7 @@ "formatVersion": 1, "database": { "version": 1, - "identityHash": "ea0d5b36d92a5a3fb1523c3064686f7d", + "identityHash": "548162ed64d13ae0bed807c23709b850", "entities": [ { "tableName": "config", @@ -23,12 +23,44 @@ }, "indices": [], "foreignKeys": [] + }, + { + "tableName": "invoice_tag", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`paymentHash` TEXT NOT NULL, `tags` TEXT NOT NULL, `createdAt` INTEGER NOT NULL, PRIMARY KEY(`paymentHash`))", + "fields": [ + { + "fieldPath": "paymentHash", + "columnName": "paymentHash", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "tags", + "columnName": "tags", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "createdAt", + "columnName": "createdAt", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "paymentHash" + ] + }, + "indices": [], + "foreignKeys": [] } ], "views": [], "setupQueries": [ "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", - "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'ea0d5b36d92a5a3fb1523c3064686f7d')" + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '548162ed64d13ae0bed807c23709b850')" ] } } \ No newline at end of file From f3b9da1e499f14875ddd5bdc3995f603f2233cdb Mon Sep 17 00:00:00 2001 From: Joao Victor Sena Date: Thu, 1 May 2025 08:41:54 -0300 Subject: [PATCH 25/40] refactor: extract method handleTags --- .../java/to/bitkit/viewmodels/AppViewModel.kt | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/app/src/main/java/to/bitkit/viewmodels/AppViewModel.kt b/app/src/main/java/to/bitkit/viewmodels/AppViewModel.kt index 5f979ec5a..fa759741b 100644 --- a/app/src/main/java/to/bitkit/viewmodels/AppViewModel.kt +++ b/app/src/main/java/to/bitkit/viewmodels/AppViewModel.kt @@ -208,13 +208,7 @@ class AppViewModel @Inject constructor( try { when (event) { is Event.PaymentReceived -> { - val tags = walletRepo.searchInvoice(txId = event.paymentHash).getOrNull()?.tags.orEmpty() //TODO EXTRACT TO METHOD - attachTagsToActivity( - paymentHashOrTxId = event.paymentHash, - type = ActivityFilter.LIGHTNING, - txType = PaymentType.RECEIVED, - tags = tags - ) + handleTags(event) showNewTransactionSheet( NewTransactionSheetDetails( type = NewTransactionSheetType.LIGHTNING, @@ -275,6 +269,16 @@ class AppViewModel @Inject constructor( } } + private suspend fun handleTags(event: Event.PaymentReceived) { + val tags = walletRepo.searchInvoice(txId = event.paymentHash).getOrNull()?.tags.orEmpty() + attachTagsToActivity( + paymentHashOrTxId = event.paymentHash, + type = ActivityFilter.LIGHTNING, + txType = PaymentType.RECEIVED, + tags = tags + ) + } + private fun checkGeoStatus() { viewModelScope.launch { try { From 1d08f2861ca2cf2813bffbcdb43a5f52d20ac437 Mon Sep 17 00:00:00 2001 From: Joao Victor Sena Date: Thu, 1 May 2025 08:53:14 -0300 Subject: [PATCH 26/40] refactor: move attachTagsToActivity to repository --- .../java/to/bitkit/repositories/WalletRepo.kt | 89 +++++++++++++++++++ .../java/to/bitkit/viewmodels/AppViewModel.kt | 84 +---------------- 2 files changed, 92 insertions(+), 81 deletions(-) diff --git a/app/src/main/java/to/bitkit/repositories/WalletRepo.kt b/app/src/main/java/to/bitkit/repositories/WalletRepo.kt index 0b2dca83d..0d2c28e59 100644 --- a/app/src/main/java/to/bitkit/repositories/WalletRepo.kt +++ b/app/src/main/java/to/bitkit/repositories/WalletRepo.kt @@ -2,10 +2,14 @@ package to.bitkit.repositories import android.content.Context import android.icu.util.Calendar +import androidx.lifecycle.viewModelScope import com.google.firebase.messaging.FirebaseMessaging import dagger.hilt.android.qualifiers.ApplicationContext import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.delay import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.launch import kotlinx.coroutines.tasks.await import kotlinx.coroutines.withContext import org.lightningdevkit.ldknode.Network @@ -26,11 +30,15 @@ import to.bitkit.services.BlocktankNotificationsService import to.bitkit.services.CoreService import to.bitkit.utils.Bip21Utils import to.bitkit.utils.Logger +import uniffi.bitkitcore.Activity +import uniffi.bitkitcore.ActivityFilter import uniffi.bitkitcore.IBtInfo +import uniffi.bitkitcore.PaymentType import uniffi.bitkitcore.Scanner import uniffi.bitkitcore.decode import javax.inject.Inject import javax.inject.Singleton +import kotlin.time.Duration.Companion.seconds @Singleton class WalletRepo @Inject constructor( @@ -308,6 +316,87 @@ class WalletRepo @Inject constructor( } } + suspend fun attachTagsToActivity( + paymentHashOrTxId: String?, + type: ActivityFilter, + txType: PaymentType, + tags: List + ) : Result = withContext(bgDispatcher) { + Logger.debug("attachTagsToActivity $tags") + if (tags.isEmpty()) { + Logger.debug("selectedTags empty") + return@withContext Result.failure(Exception("selectedTags empty")) + } + + if (paymentHashOrTxId == null) { + Logger.error(msg = "null paymentHashOrTxId") + return@withContext Result.failure(Exception("null paymentHashOrTxId")) + } + + var activity = coreService.activity.get(filter = type, txType = txType, limit = 10u).firstOrNull { activityItem -> + when (activityItem) { + is Activity.Lightning -> paymentHashOrTxId == activityItem.v1.id + is Activity.Onchain -> paymentHashOrTxId == activityItem.v1.txId + } + } + + if (activity == null) { + Logger.warn("activity not found, trying again after delay") + delay(5.seconds) + activity = coreService.activity.get(filter = type, txType = txType, limit = 10u).firstOrNull { activityItem -> + when (activityItem) { + is Activity.Lightning -> paymentHashOrTxId == activityItem.v1.id + is Activity.Onchain -> paymentHashOrTxId == activityItem.v1.txId + } + } + } + + if (activity == null) { + Logger.error(msg = "Activity not found") + return@withContext Result.failure(Exception("Activity not found")) + } + + return@withContext when (activity) { + is Activity.Lightning -> { + if (paymentHashOrTxId == activity.v1.id) { + coreService.activity.appendTags( + toActivityId = activity.v1.id, + tags = tags + ).onFailure { e -> + Logger.error("Error attaching tags $tags", e) + return@withContext Result.failure(Exception("Error attaching tags $tags")) + }.onSuccess { + Logger.info("Success attatching tags $tags to activity ${activity.v1.id}") + deleteInvoice(txId = paymentHashOrTxId) + return@withContext Result.success(Unit) + } + } else { + Logger.error("Different activity id. Expected: $paymentHashOrTxId found: ${activity.v1.id}") + return@withContext Result.failure(Exception("Error attaching tags $tags")) + } + } + + is Activity.Onchain -> { + if (paymentHashOrTxId == activity.v1.txId) { + coreService.activity.appendTags( + toActivityId = activity.v1.id, + tags = tags + ).onFailure { + Logger.error("Error attaching tags $tags") + return@withContext Result.failure(Exception("Error attaching tags $tags")) + }.onSuccess { + Logger.info("Success attatching tags $tags to activity ${activity.v1.id}") + deleteInvoice(txId = paymentHashOrTxId) + return@onSuccess + } + } else { + Logger.error("Different txId. Expected: $paymentHashOrTxId found: ${activity.v1.txId}") + return@withContext Result.failure(Exception("Error attaching tags $tags")) + } + } + } + } + @OptIn(ExperimentalStdlibApi::class) private suspend fun Scanner.OnChain.extractLightningHashOrAddress(): String { val address = this.invoice.address diff --git a/app/src/main/java/to/bitkit/viewmodels/AppViewModel.kt b/app/src/main/java/to/bitkit/viewmodels/AppViewModel.kt index fa759741b..01f54592e 100644 --- a/app/src/main/java/to/bitkit/viewmodels/AppViewModel.kt +++ b/app/src/main/java/to/bitkit/viewmodels/AppViewModel.kt @@ -52,7 +52,6 @@ import uniffi.bitkitcore.OnChainInvoice import uniffi.bitkitcore.PaymentType import uniffi.bitkitcore.Scanner import javax.inject.Inject -import kotlin.time.Duration.Companion.seconds @HiltViewModel class AppViewModel @Inject constructor( @@ -271,7 +270,7 @@ class AppViewModel @Inject constructor( private suspend fun handleTags(event: Event.PaymentReceived) { val tags = walletRepo.searchInvoice(txId = event.paymentHash).getOrNull()?.tags.orEmpty() - attachTagsToActivity( + walletRepo.attachTagsToActivity( paymentHashOrTxId = event.paymentHash, type = ActivityFilter.LIGHTNING, txType = PaymentType.RECEIVED, @@ -547,7 +546,7 @@ class AppViewModel @Inject constructor( if (result.isSuccess) { val txId = result.getOrNull() val tags = _sendUiState.value.selectedTags - attachTagsToActivity( + walletRepo.attachTagsToActivity( paymentHashOrTxId = txId, type = ActivityFilter.ONCHAIN, txType = PaymentType.SENT, @@ -581,7 +580,7 @@ class AppViewModel @Inject constructor( val paymentHash = result.getOrNull() Logger.info("Lightning send result payment hash: $paymentHash") val tags = _sendUiState.value.selectedTags - attachTagsToActivity( + walletRepo.attachTagsToActivity( paymentHashOrTxId = paymentHash, type = ActivityFilter.LIGHTNING, txType = PaymentType.SENT, @@ -619,83 +618,6 @@ class AppViewModel @Inject constructor( } } - private fun attachTagsToActivity( - paymentHashOrTxId: String?, - type: ActivityFilter, - txType: PaymentType, - tags: List - ) { - Logger.debug("attachTagsToActivity $tags") - if (tags.isEmpty()) { - Logger.debug("selectedTags empty") - return - } - - if (paymentHashOrTxId == null) { - Logger.error(msg = "null paymentHashOrTxId") - return - } - - viewModelScope.launch(Dispatchers.IO) { - var activity = coreService.activity.get(filter = type, txType = txType, limit = 10u).firstOrNull { activityItem -> - when (activityItem) { - is Activity.Lightning -> paymentHashOrTxId == activityItem.v1.id - is Activity.Onchain -> paymentHashOrTxId == activityItem.v1.txId - } - } - - if (activity == null) { - Logger.warn("activity not found, trying again after delay") - delay(5.seconds) - activity = coreService.activity.get(filter = type, txType = txType, limit = 10u).firstOrNull { activityItem -> - when (activityItem) { - is Activity.Lightning -> paymentHashOrTxId == activityItem.v1.id - is Activity.Onchain -> paymentHashOrTxId == activityItem.v1.txId - } - } - } - - if (activity == null) { - Logger.error(msg = "Activity not found") - return@launch - } - - when (activity) { - is Activity.Lightning -> { - if (paymentHashOrTxId == activity.v1.id) { - coreService.activity.appendTags( - toActivityId = activity.v1.id, - tags = tags - ).onFailure { - Logger.error("Error attaching tags $tags") - }.onSuccess { - Logger.info("Success attatching tags $tags to activity ${activity.v1.id}") - walletRepo.deleteInvoice(txId = paymentHashOrTxId) - } - } else { - Logger.error("Different activity id. Expected: $paymentHashOrTxId found: ${activity.v1.id}") - } - } - - is Activity.Onchain -> { - if (paymentHashOrTxId == activity.v1.txId) { - coreService.activity.appendTags( - toActivityId = activity.v1.id, - tags = tags - ).onFailure { - Logger.error("Error attaching tags $tags") - }.onSuccess { - Logger.info("Success attatching tags $tags to activity ${activity.v1.id}") - walletRepo.deleteInvoice(txId = paymentHashOrTxId) - } - } else { - Logger.error("Different txId. Expected: $paymentHashOrTxId found: ${activity.v1.txId}") - } - } - } - } - } - private suspend fun sendOnchain(address: String, amount: ULong): Result { return lightningService.sendOnChain(address = address, amount).onFailure { toast( From aaf3e58fa1b3d32eb8968757fb5e67a7ee28b143 Mon Sep 17 00:00:00 2001 From: Joao Victor Sena Date: Thu, 1 May 2025 09:07:12 -0300 Subject: [PATCH 27/40] refactor: split attachTagsToActivity method in smaller parts --- .../java/to/bitkit/repositories/WalletRepo.kt | 135 +++++++++--------- 1 file changed, 69 insertions(+), 66 deletions(-) diff --git a/app/src/main/java/to/bitkit/repositories/WalletRepo.kt b/app/src/main/java/to/bitkit/repositories/WalletRepo.kt index 0d2c28e59..6609fcae7 100644 --- a/app/src/main/java/to/bitkit/repositories/WalletRepo.kt +++ b/app/src/main/java/to/bitkit/repositories/WalletRepo.kt @@ -258,7 +258,7 @@ class WalletRepo @Inject constructor( try { deleteExpiredInvoices() val decoded = decode(bip21Invoice) - val paymentHashOrAddress = when(decoded) { + val paymentHashOrAddress = when (decoded) { is Scanner.Lightning -> decoded.invoice.paymentHash.toHexString() is Scanner.OnChain -> decoded.extractLightningHashOrAddress() else -> null @@ -280,7 +280,9 @@ class WalletRepo @Inject constructor( suspend fun searchInvoice(txId: Txid): Result = withContext(bgDispatcher) { return@withContext try { - val invoiceTag = db.invoiceTagDao().searchInvoice(paymentHash = txId) ?: return@withContext Result.failure(Exception("Invoice not found")) + val invoiceTag = db.invoiceTagDao().searchInvoice(paymentHash = txId) ?: return@withContext Result.failure( + Exception("Invoice not found") + ) Result.success(invoiceTag) } catch (e: Throwable) { Logger.error("searchInvoice error", e, context = TAG) @@ -321,89 +323,90 @@ class WalletRepo @Inject constructor( type: ActivityFilter, txType: PaymentType, tags: List - ) : Result = withContext(bgDispatcher) { - Logger.debug("attachTagsToActivity $tags") - if (tags.isEmpty()) { - Logger.debug("selectedTags empty") - return@withContext Result.failure(Exception("selectedTags empty")) - } - - if (paymentHashOrTxId == null) { - Logger.error(msg = "null paymentHashOrTxId") - return@withContext Result.failure(Exception("null paymentHashOrTxId")) - } + ): Result = withContext(bgDispatcher) { + Logger.debug("attachTagsToActivity $tags", context = TAG) - var activity = coreService.activity.get(filter = type, txType = txType, limit = 10u).firstOrNull { activityItem -> - when (activityItem) { - is Activity.Lightning -> paymentHashOrTxId == activityItem.v1.id - is Activity.Onchain -> paymentHashOrTxId == activityItem.v1.txId + when { + tags.isEmpty() -> { + Logger.debug("selectedTags empty", context = TAG) + return@withContext Result.failure(IllegalArgumentException("selectedTags empty")) } - } - if (activity == null) { - Logger.warn("activity not found, trying again after delay") - delay(5.seconds) - activity = coreService.activity.get(filter = type, txType = txType, limit = 10u).firstOrNull { activityItem -> - when (activityItem) { - is Activity.Lightning -> paymentHashOrTxId == activityItem.v1.id - is Activity.Onchain -> paymentHashOrTxId == activityItem.v1.txId - } + paymentHashOrTxId == null -> { + Logger.error(msg = "null paymentHashOrTxId", context = TAG) + return@withContext Result.failure(IllegalArgumentException("null paymentHashOrTxId")) } } - if (activity == null) { - Logger.error(msg = "Activity not found") - return@withContext Result.failure(Exception("Activity not found")) + val activity = findActivityWithRetry( + paymentHashOrTxId = paymentHashOrTxId, + type = type, + txType = txType + ) ?: return@withContext Result.failure(IllegalStateException("Activity not found")) + + if (!activity.matchesId(paymentHashOrTxId)) { + Logger.error( + "ID mismatch. Expected: $paymentHashOrTxId found: ${activity.idValue}", + context = TAG + ) + return@withContext Result.failure(IllegalStateException("Activity ID mismatch")) } - return@withContext when (activity) { - is Activity.Lightning -> { - if (paymentHashOrTxId == activity.v1.id) { - coreService.activity.appendTags( - toActivityId = activity.v1.id, - tags = tags - ).onFailure { e -> - Logger.error("Error attaching tags $tags", e) - return@withContext Result.failure(Exception("Error attaching tags $tags")) - }.onSuccess { - Logger.info("Success attatching tags $tags to activity ${activity.v1.id}") - deleteInvoice(txId = paymentHashOrTxId) - return@withContext Result.success(Unit) - } - } else { - Logger.error("Different activity id. Expected: $paymentHashOrTxId found: ${activity.v1.id}") - return@withContext Result.failure(Exception("Error attaching tags $tags")) - } + coreService.activity.appendTags( + toActivityId = activity.idValue, + tags = tags + ).fold( + onFailure = { error -> + Logger.error("Error attaching tags $tags", error, context = TAG) + Result.failure(Exception("Error attaching tags $tags", error)) + }, + onSuccess = { + Logger.info("Success attaching tags $tags to activity ${activity.idValue}", context = TAG) + deleteInvoice(txId = paymentHashOrTxId) + Result.success(Unit) } + ) + } - is Activity.Onchain -> { - if (paymentHashOrTxId == activity.v1.txId) { - coreService.activity.appendTags( - toActivityId = activity.v1.id, - tags = tags - ).onFailure { - Logger.error("Error attaching tags $tags") - return@withContext Result.failure(Exception("Error attaching tags $tags")) - }.onSuccess { - Logger.info("Success attatching tags $tags to activity ${activity.v1.id}") - deleteInvoice(txId = paymentHashOrTxId) - return@onSuccess - } - } else { - Logger.error("Different txId. Expected: $paymentHashOrTxId found: ${activity.v1.txId}") - return@withContext Result.failure(Exception("Error attaching tags $tags")) - } - } + private suspend fun findActivityWithRetry( + paymentHashOrTxId: String, + type: ActivityFilter, + txType: PaymentType + ): Activity? { + + suspend fun findActivity(): Activity? = coreService.activity.get( + filter = type, + txType = txType, + limit = 10u + ).firstOrNull { it.matchesId(paymentHashOrTxId) } + + var activity = findActivity() + if (activity == null) { + Logger.warn("activity not found, trying again after delay", context = TAG) + delay(5.seconds) + activity = findActivity() } + return activity + } + + private fun Activity.matchesId(paymentHashOrTxId: String): Boolean = when (this) { + is Activity.Lightning -> paymentHashOrTxId == v1.id + is Activity.Onchain -> paymentHashOrTxId == v1.txId } + private val Activity.idValue: String + get() = when (this) { + is Activity.Lightning -> v1.id + is Activity.Onchain -> v1.txId + } + @OptIn(ExperimentalStdlibApi::class) private suspend fun Scanner.OnChain.extractLightningHashOrAddress(): String { val address = this.invoice.address val lightningInvoice: String = this.invoice.params?.get("lightning") ?: address val decoded = decode(lightningInvoice) - val paymentHash = when(decoded) { + val paymentHash = when (decoded) { is Scanner.Lightning -> decoded.invoice.paymentHash.toHexString() else -> null } ?: address From b4f2a26defd36fbece56dd163873f21e8f7a5222 Mon Sep 17 00:00:00 2001 From: Joao Victor Sena Date: Thu, 1 May 2025 09:40:00 -0300 Subject: [PATCH 28/40] feat: Keep screen on and set brightness to max while ReceiveQrScreen composable is active --- .../wallets/receive/ReceiveQrScreen.kt | 24 +++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/app/src/main/java/to/bitkit/ui/screens/wallets/receive/ReceiveQrScreen.kt b/app/src/main/java/to/bitkit/ui/screens/wallets/receive/ReceiveQrScreen.kt index bc376ffda..7f35a0743 100644 --- a/app/src/main/java/to/bitkit/ui/screens/wallets/receive/ReceiveQrScreen.kt +++ b/app/src/main/java/to/bitkit/ui/screens/wallets/receive/ReceiveQrScreen.kt @@ -1,5 +1,7 @@ package to.bitkit.ui.screens.wallets.receive +import android.app.Activity +import android.view.WindowManager import androidx.compose.animation.AnimatedVisibility import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column @@ -18,6 +20,7 @@ import androidx.compose.material3.CardDefaults import androidx.compose.material3.Icon import androidx.compose.material3.Switch import androidx.compose.runtime.Composable +import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.MutableState import androidx.compose.runtime.getValue @@ -209,6 +212,27 @@ private fun ReceiveQrScreen( onClickEditInvoice: () -> Unit, onClickReceiveOnSpending: () -> Unit, ) { + val context = LocalContext.current + val window = remember(context) { (context as Activity).window } + + // Keep screen on and set brightness to max while this composable is active + DisposableEffect(Unit) { + val originalBrightness = window.attributes.screenBrightness + val originalFlags = window.attributes.flags + + window.attributes = window.attributes.apply { + screenBrightness = WindowManager.LayoutParams.BRIGHTNESS_OVERRIDE_FULL + flags = WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON + } + + onDispose { + window.attributes = window.attributes.apply { + screenBrightness = originalBrightness + flags = originalFlags + } + } + } + val qrLogoImageRes by remember(walletState, cjitInvoice.value) { val resId = when { cjitInvoice.value?.isNotEmpty() == true -> R.drawable.ic_ln_circle From c9c4257dd6509bde1be3f448b1b2a97f049c526c Mon Sep 17 00:00:00 2001 From: Joao Victor Sena Date: Thu, 1 May 2025 10:42:58 -0300 Subject: [PATCH 29/40] feat: wrap method in executeWhenNodeRunning --- .../to/bitkit/models/NodeLifecycleState.kt | 1 + .../to/bitkit/repositories/LightningRepo.kt | 214 ++++++++++-------- 2 files changed, 118 insertions(+), 97 deletions(-) diff --git a/app/src/main/java/to/bitkit/models/NodeLifecycleState.kt b/app/src/main/java/to/bitkit/models/NodeLifecycleState.kt index bd7b13073..5b6c66fc7 100644 --- a/app/src/main/java/to/bitkit/models/NodeLifecycleState.kt +++ b/app/src/main/java/to/bitkit/models/NodeLifecycleState.kt @@ -12,6 +12,7 @@ sealed class NodeLifecycleState { fun isRunningOrStarting() = this is Running || this is Starting fun isStarting() = this is Starting fun isRunning() = this is Running + fun canRun() = this.isRunningOrStarting() || this is Initializing val displayState: String get() = when (this) { diff --git a/app/src/main/java/to/bitkit/repositories/LightningRepo.kt b/app/src/main/java/to/bitkit/repositories/LightningRepo.kt index 5692a401f..6e3735154 100644 --- a/app/src/main/java/to/bitkit/repositories/LightningRepo.kt +++ b/app/src/main/java/to/bitkit/repositories/LightningRepo.kt @@ -4,7 +4,9 @@ import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.first import kotlinx.coroutines.withContext +import kotlinx.coroutines.withTimeoutOrNull import org.lightningdevkit.ldknode.Address import org.lightningdevkit.ldknode.BalanceDetails import org.lightningdevkit.ldknode.Bolt11Invoice @@ -25,6 +27,7 @@ import to.bitkit.utils.Logger import javax.inject.Inject import javax.inject.Singleton import kotlin.time.Duration +import kotlin.time.Duration.Companion.minutes @Singleton class LightningRepo @Inject constructor( @@ -36,12 +39,73 @@ class LightningRepo @Inject constructor( private val _nodeLifecycleState: MutableStateFlow = MutableStateFlow(NodeLifecycleState.Stopped) val nodeLifecycleState = _nodeLifecycleState.asStateFlow() + /** + * Executes the provided operation only if the node is running. + * If the node is not running, waits for it to be running for a specified timeout. + * + * @param operationName Name of the operation for logging + * @param waitTimeout Duration to wait for the node to be running + * @param operation Lambda to execute when the node is running + * @return Result of the operation, or failure if node isn't running or operation fails + */ + private suspend fun executeWhenNodeRunning( + operationName: String, + waitTimeout: Duration = 1.minutes, + operation: suspend () -> Result + ): Result = withContext(bgDispatcher) { + // If node is already running, execute immediately + Logger.debug("Operation called: $operationName", context = TAG) + + if (nodeLifecycleState.value.isRunning()) { + return@withContext executeOperation(operationName, operation) + } + + // If node is not in a state that can become running, fail fast + if (!nodeLifecycleState.value.canRun()) { + return@withContext Result.failure( + Exception("Cannot execute $operationName: Node is ${nodeLifecycleState.value} and not starting") + ) + } + + val nodeRunning = withTimeoutOrNull(waitTimeout) { + if (nodeLifecycleState.value.isRunning()) { + return@withTimeoutOrNull true + } + + // Otherwise, wait for it to transition to running state + Logger.debug("Waiting for node runs to execute $operationName", context = TAG) + _nodeLifecycleState.first { it.isRunning() } + Logger.debug("Operation executed: $operationName", context = TAG) + true + } ?: false + + if (!nodeRunning) { + return@withContext Result.failure( + Exception("Timeout waiting for node to be running to execute $operationName") + ) + } + + return@withContext executeOperation(operationName, operation) + } + + private suspend fun executeOperation( + operationName: String, + operation: suspend () -> Result + ): Result { + return try { + operation() + } catch (e: Throwable) { + Logger.error("$operationName error", e, context = TAG) + Result.failure(e) + } + } + suspend fun setup(walletIndex: Int): Result = withContext(bgDispatcher) { return@withContext try { lightningService.setup(walletIndex) Result.success(Unit) } catch (e: Throwable) { - Logger.error("Node setup error", e) + Logger.error("Node setup error", e, context = TAG) Result.failure(e) } } @@ -79,7 +143,7 @@ class LightningRepo @Inject constructor( _nodeLifecycleState.value = NodeLifecycleState.Running Result.success(Unit) } catch (e: Throwable) { - Logger.error("Node start error", e) + Logger.error("Node start error", e, context = TAG) _nodeLifecycleState.value = NodeLifecycleState.ErrorStarting(e) Result.failure(e) } @@ -96,19 +160,14 @@ class LightningRepo @Inject constructor( _nodeLifecycleState.value = NodeLifecycleState.Stopped Result.success(Unit) } catch (e: Throwable) { - Logger.error("Node stop error", e) + Logger.error("Node stop error", e, context = TAG) Result.failure(e) } } - suspend fun sync(): Result = withContext(bgDispatcher) { - try { - lightningService.sync() - Result.success(Unit) - } catch (e: Throwable) { - Logger.error("Sync error", e) - Result.failure(e) - } + suspend fun sync(): Result = executeWhenNodeRunning("Sync") { + lightningService.sync() + Result.success(Unit) } suspend fun wipeStorage(walletIndex: Int): Result = withContext(bgDispatcher) { @@ -116,131 +175,92 @@ class LightningRepo @Inject constructor( lightningService.wipeStorage(walletIndex) Result.success(Unit) } catch (e: Throwable) { - Logger.error("Wipe storage error", e) + Logger.error("Wipe storage error", e, context = TAG) Result.failure(e) } } - suspend fun connectToTrustedPeers(): Result = withContext(bgDispatcher) { - try { - lightningService.connectToTrustedPeers() - Result.success(Unit) - } catch (e: Throwable) { - Logger.error("Connect to trusted peers error", e) - Result.failure(e) - } + suspend fun connectToTrustedPeers(): Result = executeWhenNodeRunning("Connect to trusted peers") { + lightningService.connectToTrustedPeers() + Result.success(Unit) } - suspend fun disconnectPeer(peer: LnPeer): Result = withContext(bgDispatcher) { - try { - lightningService.disconnectPeer(peer) - Result.success(Unit) - } catch (e: Throwable) { - Logger.error("Disconnect peer error", e) - Result.failure(e) - } + suspend fun disconnectPeer(peer: LnPeer): Result = executeWhenNodeRunning("Disconnect peer") { + lightningService.disconnectPeer(peer) + Result.success(Unit) } - suspend fun newAddress(): Result = withContext(bgDispatcher) { - try { - val address = lightningService.newAddress() - Result.success(address) - } catch (e: Throwable) { - Logger.error("New address error", e) - Result.failure(e) - } + suspend fun newAddress(): Result = executeWhenNodeRunning("New address") { + val address = lightningService.newAddress() + Result.success(address) } - suspend fun checkAddressUsage(address: String): Result = withContext(bgDispatcher) { - try { - val addressInfo = addressChecker.getAddressInfo(address) - val hasTransactions = addressInfo.chain_stats.tx_count > 0 || addressInfo.mempool_stats.tx_count > 0 - Result.success(hasTransactions) - } catch (e: Throwable) { - Logger.error("Check address usage error", e) - Result.failure(e) - } + suspend fun checkAddressUsage(address: String): Result = executeWhenNodeRunning("Check address usage") { + val addressInfo = addressChecker.getAddressInfo(address) + val hasTransactions = addressInfo.chain_stats.tx_count > 0 || addressInfo.mempool_stats.tx_count > 0 + Result.success(hasTransactions) } suspend fun createInvoice( amountSats: ULong? = null, description: String, expirySeconds: UInt = 86_400u - ): Result = withContext(bgDispatcher) { - try { - val invoice = lightningService.receive(amountSats, description, expirySeconds) - Result.success(invoice) - } catch (e: Throwable) { - Logger.error("Create invoice error", e) - Result.failure(e) - } + ): Result = executeWhenNodeRunning("Create invoice") { + val invoice = lightningService.receive(amountSats, description, expirySeconds) + Result.success(invoice) } - suspend fun payInvoice(bolt11: String, sats: ULong? = null): Result = withContext(bgDispatcher) { - try { + suspend fun payInvoice(bolt11: String, sats: ULong? = null): Result = + executeWhenNodeRunning("Pay invoice") { val paymentId = lightningService.send(bolt11 = bolt11, sats = sats) Result.success(paymentId) - } catch (e: Throwable) { - Logger.error("Pay invoice error", e) - Result.failure(e) } - } - suspend fun sendOnChain(address: Address, sats: ULong): Result = withContext(bgDispatcher) { - try { + suspend fun sendOnChain(address: Address, sats: ULong): Result = + executeWhenNodeRunning("Send on-chain") { val txId = lightningService.send(address = address, sats = sats) Result.success(txId) - } catch (e: Throwable) { - Logger.error("sendOnChain error", e) - Result.failure(e) } - } - suspend fun getPayments(): Result> = withContext(bgDispatcher) { - try { - val payments = lightningService.payments - ?: return@withContext Result.failure(Exception("It wasn't possible get the payments")) - Result.success(payments) - } catch (e: Throwable) { - Logger.error("getPayments error", e) - Result.failure(e) - } + suspend fun getPayments(): Result> = executeWhenNodeRunning("Get payments") { + val payments = lightningService.payments + ?: return@executeWhenNodeRunning Result.failure(Exception("It wasn't possible get the payments")) + Result.success(payments) } suspend fun openChannel( peer: LnPeer, channelAmountSats: ULong, pushToCounterpartySats: ULong? = null - ): Result = withContext(bgDispatcher) { - try { - val result = lightningService.openChannel(peer, channelAmountSats, pushToCounterpartySats) - result - } catch (e: Throwable) { - Logger.error("Open channel error", e) - Result.failure(e) - } + ): Result = executeWhenNodeRunning("Open channel") { + lightningService.openChannel(peer, channelAmountSats, pushToCounterpartySats) } suspend fun closeChannel(userChannelId: String, counterpartyNodeId: String): Result = - withContext(bgDispatcher) { - try { - lightningService.closeChannel(userChannelId, counterpartyNodeId) - Result.success(Unit) - } catch (e: Throwable) { - Logger.error("Close channel error", e) - Result.failure(e) - } + executeWhenNodeRunning("Close channel") { + lightningService.closeChannel(userChannelId, counterpartyNodeId) + Result.success(Unit) } - fun canSend(amountSats: ULong): Boolean = lightningService.canSend(amountSats) + fun canSend(amountSats: ULong): Boolean = + nodeLifecycleState.value.isRunning() && lightningService.canSend(amountSats) fun getSyncFlow(): Flow = lightningService.syncFlow() - fun getNodeId(): String? = lightningService.nodeId - fun getBalances(): BalanceDetails? = lightningService.balances - fun getStatus(): NodeStatus? = lightningService.status - fun getPeers(): List? = lightningService.peers - fun getChannels(): List? = lightningService.channels + fun getNodeId(): String? = if (nodeLifecycleState.value.isRunning()) lightningService.nodeId else null + + fun getBalances(): BalanceDetails? = if (nodeLifecycleState.value.isRunning()) lightningService.balances else null + + fun getStatus(): NodeStatus? = if (nodeLifecycleState.value.isRunning()) lightningService.status else null - fun hasChannels(): Boolean = lightningService.channels?.isNotEmpty() == true + fun getPeers(): List? = if (nodeLifecycleState.value.isRunning()) lightningService.peers else null + + fun getChannels(): List? = + if (nodeLifecycleState.value.isRunning()) lightningService.channels else null + + fun hasChannels(): Boolean = nodeLifecycleState.value.isRunning() && lightningService.channels?.isNotEmpty() == true + + private companion object { + const val TAG = "LightningRepo" + } } From d0171d4a92a43befca8b5c9691e2d3790a26c6df Mon Sep 17 00:00:00 2001 From: Joao Victor Sena Date: Thu, 1 May 2025 10:52:17 -0300 Subject: [PATCH 30/40] feat: remove comment --- app/src/main/java/to/bitkit/repositories/LightningRepo.kt | 1 - 1 file changed, 1 deletion(-) diff --git a/app/src/main/java/to/bitkit/repositories/LightningRepo.kt b/app/src/main/java/to/bitkit/repositories/LightningRepo.kt index 6e3735154..e6399b4ef 100644 --- a/app/src/main/java/to/bitkit/repositories/LightningRepo.kt +++ b/app/src/main/java/to/bitkit/repositories/LightningRepo.kt @@ -53,7 +53,6 @@ class LightningRepo @Inject constructor( waitTimeout: Duration = 1.minutes, operation: suspend () -> Result ): Result = withContext(bgDispatcher) { - // If node is already running, execute immediately Logger.debug("Operation called: $operationName", context = TAG) if (nodeLifecycleState.value.isRunning()) { From 41ddbd55c4f87d7d6bf3f3a6e5b05bbf99e12c2f Mon Sep 17 00:00:00 2001 From: Joao Victor Sena Date: Fri, 2 May 2025 07:40:58 -0300 Subject: [PATCH 31/40] fix: add executeWhenNodeRunning in stop method --- .../java/to/bitkit/repositories/LightningRepo.kt | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/app/src/main/java/to/bitkit/repositories/LightningRepo.kt b/app/src/main/java/to/bitkit/repositories/LightningRepo.kt index e6399b4ef..813fa7c00 100644 --- a/app/src/main/java/to/bitkit/repositories/LightningRepo.kt +++ b/app/src/main/java/to/bitkit/repositories/LightningRepo.kt @@ -154,10 +154,12 @@ class LightningRepo @Inject constructor( } try { - _nodeLifecycleState.value = NodeLifecycleState.Stopping - lightningService.stop() - _nodeLifecycleState.value = NodeLifecycleState.Stopped - Result.success(Unit) + executeWhenNodeRunning("stop") { + _nodeLifecycleState.value = NodeLifecycleState.Stopping + lightningService.stop() + _nodeLifecycleState.value = NodeLifecycleState.Stopped + Result.success(Unit) + } } catch (e: Throwable) { Logger.error("Node stop error", e, context = TAG) Result.failure(e) @@ -169,7 +171,7 @@ class LightningRepo @Inject constructor( Result.success(Unit) } - suspend fun wipeStorage(walletIndex: Int): Result = withContext(bgDispatcher) { + suspend fun wipeStorage(walletIndex: Int): Result = withContext(bgDispatcher) { //TODO HANDLE (err: 'Node is still running') try { lightningService.wipeStorage(walletIndex) Result.success(Unit) From 0055cf0a6046c39efcc846b6eb6987d0678941b5 Mon Sep 17 00:00:00 2001 From: Joao Victor Sena Date: Fri, 2 May 2025 07:46:18 -0300 Subject: [PATCH 32/40] fix: wait for node stops before wipe the storage --- .../to/bitkit/repositories/LightningRepo.kt | 18 +++++++++++------- .../to/bitkit/viewmodels/WalletViewModel.kt | 3 --- 2 files changed, 11 insertions(+), 10 deletions(-) diff --git a/app/src/main/java/to/bitkit/repositories/LightningRepo.kt b/app/src/main/java/to/bitkit/repositories/LightningRepo.kt index 813fa7c00..2bb2b6dfd 100644 --- a/app/src/main/java/to/bitkit/repositories/LightningRepo.kt +++ b/app/src/main/java/to/bitkit/repositories/LightningRepo.kt @@ -171,13 +171,17 @@ class LightningRepo @Inject constructor( Result.success(Unit) } - suspend fun wipeStorage(walletIndex: Int): Result = withContext(bgDispatcher) { //TODO HANDLE (err: 'Node is still running') - try { - lightningService.wipeStorage(walletIndex) - Result.success(Unit) - } catch (e: Throwable) { - Logger.error("Wipe storage error", e, context = TAG) - Result.failure(e) + suspend fun wipeStorage(walletIndex: Int): Result = withContext(bgDispatcher) { + stop().onSuccess { + return@withContext try { + lightningService.wipeStorage(walletIndex) + Result.success(Unit) + } catch (e: Throwable) { + Logger.error("Wipe storage error", e, context = TAG) + Result.failure(e) + } + }.onFailure { e -> + return@withContext Result.failure(e) } } diff --git a/app/src/main/java/to/bitkit/viewmodels/WalletViewModel.kt b/app/src/main/java/to/bitkit/viewmodels/WalletViewModel.kt index 254b7eeba..775cfdd92 100644 --- a/app/src/main/java/to/bitkit/viewmodels/WalletViewModel.kt +++ b/app/src/main/java/to/bitkit/viewmodels/WalletViewModel.kt @@ -373,9 +373,6 @@ class WalletViewModel @Inject constructor( fun wipeStorage() { viewModelScope.launch(bgDispatcher) { - if (lightningRepo.nodeLifecycleState.value.isRunningOrStarting()) { - stopLightningNode() - } walletRepo.wipeWallet() .onSuccess { lightningRepo.wipeStorage(walletIndex = 0) From 4fa6eb136d299d45695e7f22703e9ab9551db214 Mon Sep 17 00:00:00 2001 From: Joao Victor Sena Date: Fri, 2 May 2025 07:53:22 -0300 Subject: [PATCH 33/40] fix: get time from system --- app/src/main/java/to/bitkit/repositories/WalletRepo.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/java/to/bitkit/repositories/WalletRepo.kt b/app/src/main/java/to/bitkit/repositories/WalletRepo.kt index 6609fcae7..dfd48e059 100644 --- a/app/src/main/java/to/bitkit/repositories/WalletRepo.kt +++ b/app/src/main/java/to/bitkit/repositories/WalletRepo.kt @@ -269,7 +269,7 @@ class WalletRepo @Inject constructor( invoiceTag = InvoiceTagEntity( paymentHash = paymentHashOrAddress, tags = tags, - createdAt = Calendar.getInstance().time.time + createdAt = System.currentTimeMillis() ) ) } From a28c7647112156c33e33c413da8fe8342827ce4d Mon Sep 17 00:00:00 2001 From: Joao Victor Sena Date: Fri, 2 May 2025 07:55:01 -0300 Subject: [PATCH 34/40] refactor: replace toHexString with toHex --- app/src/main/java/to/bitkit/repositories/WalletRepo.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/to/bitkit/repositories/WalletRepo.kt b/app/src/main/java/to/bitkit/repositories/WalletRepo.kt index dfd48e059..765820061 100644 --- a/app/src/main/java/to/bitkit/repositories/WalletRepo.kt +++ b/app/src/main/java/to/bitkit/repositories/WalletRepo.kt @@ -22,6 +22,7 @@ import to.bitkit.data.entities.InvoiceTagEntity import to.bitkit.data.keychain.Keychain import to.bitkit.di.BgDispatcher import to.bitkit.env.Env +import to.bitkit.ext.toHex import to.bitkit.models.BalanceState import to.bitkit.models.NewTransactionSheetDetails import to.bitkit.models.NewTransactionSheetDirection @@ -400,14 +401,13 @@ class WalletRepo @Inject constructor( is Activity.Onchain -> v1.txId } - @OptIn(ExperimentalStdlibApi::class) private suspend fun Scanner.OnChain.extractLightningHashOrAddress(): String { val address = this.invoice.address val lightningInvoice: String = this.invoice.params?.get("lightning") ?: address val decoded = decode(lightningInvoice) val paymentHash = when (decoded) { - is Scanner.Lightning -> decoded.invoice.paymentHash.toHexString() + is Scanner.Lightning -> decoded.invoice.paymentHash.toHex() else -> null } ?: address From 489aab7cc6ddbe6ddf83a86949eebcab4476a231 Mon Sep 17 00:00:00 2001 From: Joao Victor Sena Date: Fri, 2 May 2025 07:56:35 -0300 Subject: [PATCH 35/40] refactor: replace toHexString with toHex --- app/src/main/java/to/bitkit/repositories/WalletRepo.kt | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/app/src/main/java/to/bitkit/repositories/WalletRepo.kt b/app/src/main/java/to/bitkit/repositories/WalletRepo.kt index 765820061..89434b58a 100644 --- a/app/src/main/java/to/bitkit/repositories/WalletRepo.kt +++ b/app/src/main/java/to/bitkit/repositories/WalletRepo.kt @@ -252,7 +252,6 @@ class WalletRepo @Inject constructor( return db.configDao().getAll() } - @OptIn(ExperimentalStdlibApi::class) suspend fun saveInvoiceWithTags(bip21Invoice: String, tags: List) = withContext(bgDispatcher) { if (tags.isEmpty()) return@withContext @@ -260,7 +259,7 @@ class WalletRepo @Inject constructor( deleteExpiredInvoices() val decoded = decode(bip21Invoice) val paymentHashOrAddress = when (decoded) { - is Scanner.Lightning -> decoded.invoice.paymentHash.toHexString() + is Scanner.Lightning -> decoded.invoice.paymentHash.toHex() is Scanner.OnChain -> decoded.extractLightningHashOrAddress() else -> null } From 1e61ccf9d9df806e96d26a9e98f7cc091e600dc0 Mon Sep 17 00:00:00 2001 From: Joao Victor Sena Date: Fri, 2 May 2025 08:02:22 -0300 Subject: [PATCH 36/40] fix: expiration calculation --- app/src/main/java/to/bitkit/repositories/WalletRepo.kt | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/app/src/main/java/to/bitkit/repositories/WalletRepo.kt b/app/src/main/java/to/bitkit/repositories/WalletRepo.kt index 89434b58a..f6770612d 100644 --- a/app/src/main/java/to/bitkit/repositories/WalletRepo.kt +++ b/app/src/main/java/to/bitkit/repositories/WalletRepo.kt @@ -12,6 +12,8 @@ import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.launch import kotlinx.coroutines.tasks.await import kotlinx.coroutines.withContext +import kotlinx.datetime.Clock +import kotlinx.datetime.Instant import org.lightningdevkit.ldknode.Network import org.lightningdevkit.ldknode.Txid import to.bitkit.data.AppDb @@ -39,6 +41,7 @@ import uniffi.bitkitcore.Scanner import uniffi.bitkitcore.decode import javax.inject.Inject import javax.inject.Singleton +import kotlin.time.Duration.Companion.days import kotlin.time.Duration.Companion.seconds @Singleton @@ -308,11 +311,8 @@ class WalletRepo @Inject constructor( suspend fun deleteExpiredInvoices() = withContext(bgDispatcher) { try { - val twoDaysExpiration = Calendar.getInstance().apply { - add(Calendar.DAY_OF_YEAR, 2) - }.time.time - - db.invoiceTagDao().deleteExpiredInvoices(expirationTimeStamp = twoDaysExpiration) + val twoDaysAgoMillis = Clock.System.now().minus(2.days).toEpochMilliseconds() + db.invoiceTagDao().deleteExpiredInvoices(expirationTimeStamp = twoDaysAgoMillis) } catch (e: Throwable) { Logger.error("deleteExpiredInvoices error", e, context = TAG) } From c9e283f823904f9005aec949924bba02789d7c4f Mon Sep 17 00:00:00 2001 From: Joao Victor Sena Date: Fri, 2 May 2025 08:09:32 -0300 Subject: [PATCH 37/40] fix: use address as fallback --- app/src/main/java/to/bitkit/repositories/WalletRepo.kt | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/app/src/main/java/to/bitkit/repositories/WalletRepo.kt b/app/src/main/java/to/bitkit/repositories/WalletRepo.kt index f6770612d..07d822999 100644 --- a/app/src/main/java/to/bitkit/repositories/WalletRepo.kt +++ b/app/src/main/java/to/bitkit/repositories/WalletRepo.kt @@ -405,14 +405,10 @@ class WalletRepo @Inject constructor( val lightningInvoice: String = this.invoice.params?.get("lightning") ?: address val decoded = decode(lightningInvoice) - val paymentHash = when (decoded) { + return when (decoded) { is Scanner.Lightning -> decoded.invoice.paymentHash.toHex() - else -> null - } ?: address - - return paymentHash - - return address + else -> address + } } private fun generateEntropyMnemonic(): String { From 07eccd80f6c20a026239343d03ab361c4adf4892 Mon Sep 17 00:00:00 2001 From: Joao Victor Sena Date: Fri, 2 May 2025 08:18:09 -0300 Subject: [PATCH 38/40] fix: rollback change --- app/schemas/to.bitkit.data.AppDb/1.json | 38 ++----------------------- 1 file changed, 3 insertions(+), 35 deletions(-) diff --git a/app/schemas/to.bitkit.data.AppDb/1.json b/app/schemas/to.bitkit.data.AppDb/1.json index 21f3ff982..a02aaecb9 100644 --- a/app/schemas/to.bitkit.data.AppDb/1.json +++ b/app/schemas/to.bitkit.data.AppDb/1.json @@ -2,7 +2,7 @@ "formatVersion": 1, "database": { "version": 1, - "identityHash": "548162ed64d13ae0bed807c23709b850", + "identityHash": "ea0d5b36d92a5a3fb1523c3064686f7d", "entities": [ { "tableName": "config", @@ -23,44 +23,12 @@ }, "indices": [], "foreignKeys": [] - }, - { - "tableName": "invoice_tag", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`paymentHash` TEXT NOT NULL, `tags` TEXT NOT NULL, `createdAt` INTEGER NOT NULL, PRIMARY KEY(`paymentHash`))", - "fields": [ - { - "fieldPath": "paymentHash", - "columnName": "paymentHash", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "tags", - "columnName": "tags", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "createdAt", - "columnName": "createdAt", - "affinity": "INTEGER", - "notNull": true - } - ], - "primaryKey": { - "autoGenerate": false, - "columnNames": [ - "paymentHash" - ] - }, - "indices": [], - "foreignKeys": [] } ], "views": [], "setupQueries": [ "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", - "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '548162ed64d13ae0bed807c23709b850')" + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'ea0d5b36d92a5a3fb1523c3064686f7d')" ] } -} \ No newline at end of file +} From 82bb7a0a9d2eb0163e009fb643b20d53f4a1577b Mon Sep 17 00:00:00 2001 From: Joao Victor Sena Date: Fri, 2 May 2025 08:26:20 -0300 Subject: [PATCH 39/40] fix: migration --- app/schemas/to.bitkit.data.AppDb/2.json | 66 +++++++++++++++++++++++ app/src/main/java/to/bitkit/data/AppDb.kt | 2 +- 2 files changed, 67 insertions(+), 1 deletion(-) create mode 100644 app/schemas/to.bitkit.data.AppDb/2.json diff --git a/app/schemas/to.bitkit.data.AppDb/2.json b/app/schemas/to.bitkit.data.AppDb/2.json new file mode 100644 index 000000000..d8c69daa6 --- /dev/null +++ b/app/schemas/to.bitkit.data.AppDb/2.json @@ -0,0 +1,66 @@ +{ + "formatVersion": 1, + "database": { + "version": 2, + "identityHash": "548162ed64d13ae0bed807c23709b850", + "entities": [ + { + "tableName": "config", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`walletIndex` INTEGER NOT NULL, PRIMARY KEY(`walletIndex`))", + "fields": [ + { + "fieldPath": "walletIndex", + "columnName": "walletIndex", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "walletIndex" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "invoice_tag", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`paymentHash` TEXT NOT NULL, `tags` TEXT NOT NULL, `createdAt` INTEGER NOT NULL, PRIMARY KEY(`paymentHash`))", + "fields": [ + { + "fieldPath": "paymentHash", + "columnName": "paymentHash", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "tags", + "columnName": "tags", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "createdAt", + "columnName": "createdAt", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "paymentHash" + ] + }, + "indices": [], + "foreignKeys": [] + } + ], + "views": [], + "setupQueries": [ + "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '548162ed64d13ae0bed807c23709b850')" + ] + } +} \ No newline at end of file diff --git a/app/src/main/java/to/bitkit/data/AppDb.kt b/app/src/main/java/to/bitkit/data/AppDb.kt index d0f229342..89a7dc2cf 100644 --- a/app/src/main/java/to/bitkit/data/AppDb.kt +++ b/app/src/main/java/to/bitkit/data/AppDb.kt @@ -27,7 +27,7 @@ import to.bitkit.env.Env ConfigEntity::class, InvoiceTagEntity::class ], - version = 1, + version = 2, ) @TypeConverters(StringListConverter::class) From 49f5b799399a0428d40cb9961d285d33939f9853 Mon Sep 17 00:00:00 2001 From: Joao Victor Sena Date: Fri, 2 May 2025 08:36:29 -0300 Subject: [PATCH 40/40] refactor rename method resetWalletState --- app/src/main/java/to/bitkit/viewmodels/WalletViewModel.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/to/bitkit/viewmodels/WalletViewModel.kt b/app/src/main/java/to/bitkit/viewmodels/WalletViewModel.kt index 775cfdd92..2443f3e13 100644 --- a/app/src/main/java/to/bitkit/viewmodels/WalletViewModel.kt +++ b/app/src/main/java/to/bitkit/viewmodels/WalletViewModel.kt @@ -306,7 +306,7 @@ class WalletViewModel @Inject constructor( ) walletRepo.setBip21(newBip21) walletRepo.saveInvoiceWithTags(bip21Invoice = newBip21, tags = _uiState.value.selectedTags) - resetWalletState() + clearTagsAndBip21DescriptionState() syncState() } } @@ -526,7 +526,7 @@ class WalletViewModel @Inject constructor( _uiState.update { it.copy(bip21Description = newText) } } - private fun resetWalletState() { + private fun clearTagsAndBip21DescriptionState() { _uiState.update { it.copy(selectedTags = listOf(), bip21Description = "") } } }