From 5f5ae6a710f3377f23d5fa80dcde444eefcc85a0 Mon Sep 17 00:00:00 2001 From: Ovi Trif Date: Mon, 3 Nov 2025 21:06:08 +0100 Subject: [PATCH 01/12] chore: update bitkit-core --- .../java/to/bitkit/services/CoreService.kt | 20 ++++++++++--------- gradle/libs.versions.toml | 2 +- 2 files changed, 12 insertions(+), 10 deletions(-) diff --git a/app/src/main/java/to/bitkit/services/CoreService.kt b/app/src/main/java/to/bitkit/services/CoreService.kt index c875b3199..9bbce8323 100644 --- a/app/src/main/java/to/bitkit/services/CoreService.kt +++ b/app/src/main/java/to/bitkit/services/CoreService.kt @@ -421,6 +421,7 @@ class ActivityService( confirmed = isConfirmed, timestamp = timestamp, isBoosted = false, + boostTxIds = emptyList(), isTransfer = false, doesExist = true, confirmTimestamp = confirmedTimestamp, @@ -471,11 +472,11 @@ class ActivityService( ) repeat(count) { i -> - val isLightning = Random.Default.nextBoolean() + val isLightning = Random.nextBoolean() val value = (1000..1_000_000).random().toULong() val txTimestamp = (timestamp.toLong() - (0..30L * 24 * 60 * 60).random()).toULong() // Random time in last 30 days - val txType = if (Random.Default.nextBoolean()) PaymentType.SENT else PaymentType.RECEIVED + val txType = if (Random.nextBoolean()) PaymentType.SENT else PaymentType.RECEIVED val status = when ((0..10).random()) { in 0..7 -> PaymentState.SUCCEEDED // 80% chance 8 -> PaymentState.PENDING // 10% chance @@ -497,7 +498,7 @@ class ActivityService( invoice = "lnbc$value", message = possibleMessages.random(), timestamp = txTimestamp, - preimage = if (Random.Default.nextBoolean()) "preimage$i" else null, + preimage = if (Random.nextBoolean()) "preimage$i" else null, createdAt = txTimestamp, updatedAt = txTimestamp ) @@ -513,16 +514,17 @@ class ActivityService( fee = (100..10_000).random().toULong(), feeRate = (1..100).random().toULong(), address = "bc1...$i", - confirmed = Random.Default.nextBoolean(), + confirmed = Random.nextBoolean(), timestamp = txTimestamp, - isBoosted = Random.Default.nextBoolean(), - isTransfer = Random.Default.nextBoolean(), + isBoosted = Random.nextBoolean(), + boostTxIds = emptyList(), + isTransfer = Random.nextBoolean(), doesExist = true, - confirmTimestamp = if (Random.Default.nextBoolean()) txTimestamp + 3600.toULong() else null, - channelId = if (Random.Default.nextBoolean()) "channel$i" else null, + confirmTimestamp = if (Random.nextBoolean()) txTimestamp + 3600.toULong() else null, + channelId = if (Random.nextBoolean()) "channel$i" else null, transferTxId = null, createdAt = txTimestamp, - updatedAt = txTimestamp + updatedAt = txTimestamp, ) ) } diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index c2bfef852..2f9c7e81e 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -44,7 +44,7 @@ activity-compose = { module = "androidx.activity:activity-compose", version.ref appcompat = { module = "androidx.appcompat:appcompat", version.ref = "appcompat" } barcode-scanning = { module = "com.google.mlkit:barcode-scanning", version.ref = "barcodeScanning" } biometric = { module = "androidx.biometric:biometric", version.ref = "biometric" } -bitkitcore = { module = "com.synonym:bitkit-core-android", version = "0.1.18" } +bitkitcore = { module = "com.synonym:bitkit-core-android", version = "0.1.22" } bouncycastle-provider-jdk = { module = "org.bouncycastle:bcprov-jdk18on", version.ref = "bouncyCastle" } camera-camera2 = { module = "androidx.camera:camera-camera2", version.ref = "camera" } camera-lifecycle = { module = "androidx.camera:camera-lifecycle", version.ref = "camera" } From 39f46764117ce4a31f4eded86e467dc8ea489c9c Mon Sep 17 00:00:00 2001 From: Ovi Trif Date: Mon, 3 Nov 2025 21:19:32 +0100 Subject: [PATCH 02/12] feat: expose new core apis via service --- .../java/to/bitkit/services/CoreService.kt | 33 ++++++++++++++++++- 1 file changed, 32 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/to/bitkit/services/CoreService.kt b/app/src/main/java/to/bitkit/services/CoreService.kt index 9bbce8323..c61b80b00 100644 --- a/app/src/main/java/to/bitkit/services/CoreService.kt +++ b/app/src/main/java/to/bitkit/services/CoreService.kt @@ -4,6 +4,7 @@ import com.synonym.bitkitcore.Activity import com.synonym.bitkitcore.ActivityFilter import com.synonym.bitkitcore.BtOrderState2 import com.synonym.bitkitcore.CJitStateEnum +import com.synonym.bitkitcore.ClosedChannelDetails import com.synonym.bitkitcore.CreateCjitOptions import com.synonym.bitkitcore.CreateOrderOptions import com.synonym.bitkitcore.FeeRates @@ -39,7 +40,13 @@ import com.synonym.bitkitcore.refreshActiveOrders import com.synonym.bitkitcore.removeTags import com.synonym.bitkitcore.updateActivity import com.synonym.bitkitcore.updateBlocktankUrl +import com.synonym.bitkitcore.upsertActivities import com.synonym.bitkitcore.upsertActivity +import com.synonym.bitkitcore.upsertCjitEntries +import com.synonym.bitkitcore.upsertClosedChannel +import com.synonym.bitkitcore.upsertClosedChannels +import com.synonym.bitkitcore.upsertInfo +import com.synonym.bitkitcore.upsertOrders import io.ktor.client.HttpClient import io.ktor.client.request.get import io.ktor.http.HttpStatusCode @@ -205,6 +212,18 @@ class ActivityService( } } + suspend fun upsert(activities: List) = ServiceQueue.CORE.background { + upsertActivities(activities) + } + + suspend fun upsert(closedChannel: ClosedChannelDetails) = ServiceQueue.CORE.background { + upsertClosedChannel(closedChannel) + } + + suspend fun upsert(closedChannels: List) = ServiceQueue.CORE.background { + upsertClosedChannels(closedChannels) + } + suspend fun getActivity(id: String): Activity? { return ServiceQueue.CORE.background { getActivityById(id) @@ -325,7 +344,7 @@ class ActivityService( } } - private suspend fun processBolt11( + private fun processBolt11( kind: PaymentKind.Bolt11, payment: PaymentDetails, state: PaymentState, @@ -666,6 +685,18 @@ class BlocktankService( } } + suspend fun upsert(info: IBtInfo) = ServiceQueue.CORE.background { + upsertInfo(info) + } + + suspend fun upsert(orders: List) = ServiceQueue.CORE.background { + upsertOrders(orders) + } + + suspend fun upsert(cjitEntries: List) = ServiceQueue.CORE.background { + upsertCjitEntries(cjitEntries) + } + // MARK: - Regtest methods suspend fun regtestMine(count: UInt = 1u) { com.synonym.bitkitcore.regtestMine(count = count) From e37e7f4ca94e7794b9e827593c7243875585604e Mon Sep 17 00:00:00 2001 From: Ovi Trif Date: Mon, 3 Nov 2025 21:40:53 +0100 Subject: [PATCH 03/12] feat: restore blocktank state from backup --- .../java/to/bitkit/repositories/BackupRepo.kt | 23 +++++++++---------- .../to/bitkit/repositories/BlocktankRepo.kt | 22 ++++++++++++++++++ 2 files changed, 33 insertions(+), 12 deletions(-) diff --git a/app/src/main/java/to/bitkit/repositories/BackupRepo.kt b/app/src/main/java/to/bitkit/repositories/BackupRepo.kt index 17ace5b98..7ee5c67f6 100644 --- a/app/src/main/java/to/bitkit/repositories/BackupRepo.kt +++ b/app/src/main/java/to/bitkit/repositories/BackupRepo.kt @@ -415,22 +415,21 @@ class BackupRepo @Inject constructor( cacheStore.update { parsed.cache } - Logger.debug("Restored caches and ${parsed.tagMetadata.size} tags metadata", context = TAG) + Logger.debug("Restored caches and ${parsed.tagMetadata.size} tags metadata records", context = TAG) } performRestore(BackupCategory.BLOCKTANK) { dataBytes -> val parsed = json.decodeFromString(String(dataBytes)) - // TODO: Restore orders, CJIT entries, and info to bitkit-core using synonymdev/bitkit-core#46 - // For now, trigger a refresh from the server to sync the data - blocktankRepo.refreshInfo() - blocktankRepo.refreshOrders() - - Logger.debug( - "Triggered Blocktank refresh (${parsed.orders.size} orders," + - "${parsed.cjitEntries.size} CJIT entries," + - "info=${parsed.info != null} backed up)", - context = TAG, - ) + blocktankRepo.restoreFromBackup(parsed) + .onSuccess { + Logger.debug( + "Restored LSP info, ${parsed.orders.size} orders, ${parsed.cjitEntries.size} CJIT entries", + context = TAG, + ) + }.onFailure { e -> + Logger.warn("Failed to restore LSP info, orders and CJIT entries", e, context = TAG) + } + } performRestore(BackupCategory.ACTIVITY) { dataBytes -> val parsed = json.decodeFromString(String(dataBytes)) diff --git a/app/src/main/java/to/bitkit/repositories/BlocktankRepo.kt b/app/src/main/java/to/bitkit/repositories/BlocktankRepo.kt index 31a0002fe..558470308 100644 --- a/app/src/main/java/to/bitkit/repositories/BlocktankRepo.kt +++ b/app/src/main/java/to/bitkit/repositories/BlocktankRepo.kt @@ -31,6 +31,7 @@ import to.bitkit.data.CacheStore import to.bitkit.di.BgDispatcher import to.bitkit.env.Env import to.bitkit.ext.nowTimestamp +import to.bitkit.models.BlocktankBackupV1 import to.bitkit.models.EUR_CURRENCY import to.bitkit.services.CoreService import to.bitkit.services.LightningService @@ -374,6 +375,27 @@ class BlocktankRepo @Inject constructor( _blocktankState.update { BlocktankState() } } + suspend fun restoreFromBackup(backup: BlocktankBackupV1): Result = withContext(bgDispatcher) { + return@withContext runCatching { + coreService.blocktank.upsert(backup.orders) + coreService.blocktank.upsert(backup.cjitEntries) + backup.info?.let { info -> + coreService.blocktank.upsert(info) + } + + // We don't refresh orders here because we rely on the polling mechanism. + // We also don't restore `paidOrders` as the refresh should do it using the paidOrderIds from restored cache. + + _blocktankState.update { + it.copy( + orders = backup.orders, + cjitEntries = backup.cjitEntries, + info = backup.info, + ) + } + } + } + companion object { private const val TAG = "BlocktankRepo" private const val DEFAULT_CHANNEL_EXPIRY_WEEKS = 6u From e33883f6d5305bf8aab0b30b08a10935e8ed9531 Mon Sep 17 00:00:00 2001 From: Ovi Trif Date: Mon, 3 Nov 2025 23:01:51 +0100 Subject: [PATCH 04/12] feat: backup closed channels & restore activities using bulk upsert --- .../java/to/bitkit/models/BackupPayloads.kt | 2 ++ .../to/bitkit/repositories/ActivityRepo.kt | 25 ++++++++++++++----- .../java/to/bitkit/repositories/BackupRepo.kt | 17 +++++++++---- .../java/to/bitkit/services/CoreService.kt | 13 +++++++--- 4 files changed, 42 insertions(+), 15 deletions(-) diff --git a/app/src/main/java/to/bitkit/models/BackupPayloads.kt b/app/src/main/java/to/bitkit/models/BackupPayloads.kt index 59b006015..16b48d9c4 100644 --- a/app/src/main/java/to/bitkit/models/BackupPayloads.kt +++ b/app/src/main/java/to/bitkit/models/BackupPayloads.kt @@ -1,6 +1,7 @@ package to.bitkit.models import com.synonym.bitkitcore.Activity +import com.synonym.bitkitcore.ClosedChannelDetails import com.synonym.bitkitcore.IBtInfo import com.synonym.bitkitcore.IBtOrder import com.synonym.bitkitcore.IcJitEntry @@ -38,4 +39,5 @@ data class ActivityBackupV1( val version: Int = 1, val createdAt: Long, val activities: List, + val closedChannels: List, ) diff --git a/app/src/main/java/to/bitkit/repositories/ActivityRepo.kt b/app/src/main/java/to/bitkit/repositories/ActivityRepo.kt index cbd35954b..d7a69c097 100644 --- a/app/src/main/java/to/bitkit/repositories/ActivityRepo.kt +++ b/app/src/main/java/to/bitkit/repositories/ActivityRepo.kt @@ -2,6 +2,7 @@ package to.bitkit.repositories import com.synonym.bitkitcore.Activity import com.synonym.bitkitcore.ActivityFilter +import com.synonym.bitkitcore.ClosedChannelDetails import com.synonym.bitkitcore.IcJitEntry import com.synonym.bitkitcore.LightningActivity import com.synonym.bitkitcore.PaymentState @@ -29,6 +30,7 @@ import to.bitkit.ext.amountOnClose import to.bitkit.ext.matchesPaymentId import to.bitkit.ext.nowTimestamp import to.bitkit.ext.rawId +import to.bitkit.models.ActivityBackupV1 import to.bitkit.services.CoreService import to.bitkit.utils.AddressChecker import to.bitkit.utils.Logger @@ -166,9 +168,6 @@ class ActivityRepo @Inject constructor( } } - /** - * Gets activities with specified filters - */ suspend fun getActivities( filter: ActivityFilter? = null, txType: PaymentType? = null, @@ -198,9 +197,6 @@ class ActivityRepo @Inject constructor( } } - /** - * Gets a specific activity by ID - */ suspend fun getActivity(id: String): Result = withContext(bgDispatcher) { return@withContext runCatching { coreService.activity.getActivity(id) @@ -209,6 +205,16 @@ class ActivityRepo @Inject constructor( } } + suspend fun getClosedChannels( + sortDirection: SortDirection = SortDirection.ASC, + ): Result> = withContext(bgDispatcher) { + runCatching { + coreService.activity.closedChannels(sortDirection) + }.onFailure { e -> + Logger.error("Error getting closed channels (sortDirection=${SortDirection})", e, context = TAG) + } + } + /** * Updates an activity * @param forceUpdate use it if you want update a deleted activity @@ -643,6 +649,13 @@ class ActivityRepo @Inject constructor( } } + suspend fun restoreFromBackup(backup: ActivityBackupV1): Result = withContext(bgDispatcher) { + return@withContext runCatching { + coreService.activity.upsert(backup.activities) + coreService.activity.upsert(backup.closedChannels) + } + } + // MARK: - Development/Testing Methods /** diff --git a/app/src/main/java/to/bitkit/repositories/BackupRepo.kt b/app/src/main/java/to/bitkit/repositories/BackupRepo.kt index 7ee5c67f6..a72916f47 100644 --- a/app/src/main/java/to/bitkit/repositories/BackupRepo.kt +++ b/app/src/main/java/to/bitkit/repositories/BackupRepo.kt @@ -371,10 +371,12 @@ class BackupRepo @Inject constructor( BackupCategory.ACTIVITY -> { val activities = activityRepo.getActivities().getOrDefault(emptyList()) + val closedChannels = activityRepo.getClosedChannels().getOrDefault(emptyList()) val payload = ActivityBackupV1( createdAt = currentTimeMillis(), activities = activities, + closedChannels = closedChannels, ) json.encodeToString(payload).toByteArray() @@ -434,11 +436,16 @@ class BackupRepo @Inject constructor( performRestore(BackupCategory.ACTIVITY) { dataBytes -> val parsed = json.decodeFromString(String(dataBytes)) - parsed.activities.forEach { activity -> - activityRepo.upsertActivity(activity) - } - - Logger.debug("Restored ${parsed.activities.size} activities", context = TAG) + activityRepo.restoreFromBackup(parsed) + .onSuccess { + Logger.debug( + "Restored ${parsed.activities.size} activities and " + + "${parsed.closedChannels.size} closed channels", + context = TAG, + ) + }.onFailure { e -> + Logger.warn("Failed to restore activities and closed channels", e, context = TAG) + } } Logger.info("Full restore success", context = TAG) diff --git a/app/src/main/java/to/bitkit/services/CoreService.kt b/app/src/main/java/to/bitkit/services/CoreService.kt index c61b80b00..3d0f6c9b1 100644 --- a/app/src/main/java/to/bitkit/services/CoreService.kt +++ b/app/src/main/java/to/bitkit/services/CoreService.kt @@ -27,6 +27,7 @@ import com.synonym.bitkitcore.deleteActivityById import com.synonym.bitkitcore.estimateOrderFeeFull import com.synonym.bitkitcore.getActivities import com.synonym.bitkitcore.getActivityById +import com.synonym.bitkitcore.getAllClosedChannels import com.synonym.bitkitcore.getAllUniqueTags import com.synonym.bitkitcore.getCjitEntries import com.synonym.bitkitcore.getInfo @@ -206,10 +207,8 @@ class ActivityService( } } - suspend fun upsert(activity: Activity) { - ServiceQueue.CORE.background { - upsertActivity(activity) - } + suspend fun upsert(activity: Activity) = ServiceQueue.CORE.background { + upsertActivity(activity) } suspend fun upsert(activities: List) = ServiceQueue.CORE.background { @@ -286,6 +285,12 @@ class ActivityService( } } + suspend fun closedChannels( + sortDirection: SortDirection, + ): List = ServiceQueue.CORE.background { + getAllClosedChannels(sortDirection) + } + /** * Maps all `PaymentDetails` from LDK Node to bitkit-core [Activity] records. * From 259884549af760dd51df7b2790518b534c88b445 Mon Sep 17 00:00:00 2001 From: Ovi Trif Date: Mon, 3 Nov 2025 23:35:45 +0100 Subject: [PATCH 05/12] feat: use list upsert to restore room db entities --- .../java/to/bitkit/data/dao/TagMetadataDao.kt | 5 +- .../java/to/bitkit/data/dao/TransferDao.kt | 3 ++ .../java/to/bitkit/repositories/BackupRepo.kt | 46 ++++++------------- 3 files changed, 20 insertions(+), 34 deletions(-) diff --git a/app/src/main/java/to/bitkit/data/dao/TagMetadataDao.kt b/app/src/main/java/to/bitkit/data/dao/TagMetadataDao.kt index b25bb1e56..79f13f6ee 100644 --- a/app/src/main/java/to/bitkit/data/dao/TagMetadataDao.kt +++ b/app/src/main/java/to/bitkit/data/dao/TagMetadataDao.kt @@ -16,7 +16,10 @@ interface TagMetadataDao { suspend fun insert(tagMetadata: TagMetadataEntity) @Upsert - suspend fun upsert(tagMetadata: TagMetadataEntity) + suspend fun upsert(entities: TagMetadataEntity) + + @Upsert + suspend fun upsert(tagMetadataList: List) @Query("SELECT * FROM tag_metadata") fun observeAll(): Flow> diff --git a/app/src/main/java/to/bitkit/data/dao/TransferDao.kt b/app/src/main/java/to/bitkit/data/dao/TransferDao.kt index b37b777c9..72f630625 100644 --- a/app/src/main/java/to/bitkit/data/dao/TransferDao.kt +++ b/app/src/main/java/to/bitkit/data/dao/TransferDao.kt @@ -17,6 +17,9 @@ interface TransferDao { @Upsert suspend fun upsert(transfer: TransferEntity) + @Upsert + suspend fun upsert(transfers: List) + @Update suspend fun update(transfer: TransferEntity) diff --git a/app/src/main/java/to/bitkit/repositories/BackupRepo.kt b/app/src/main/java/to/bitkit/repositories/BackupRepo.kt index a72916f47..c32dad555 100644 --- a/app/src/main/java/to/bitkit/repositories/BackupRepo.kt +++ b/app/src/main/java/to/bitkit/repositories/BackupRepo.kt @@ -401,51 +401,31 @@ class BackupRepo @Inject constructor( } performRestore(BackupCategory.WALLET) { dataBytes -> val parsed = json.decodeFromString(String(dataBytes)) - - parsed.transfers.forEach { transfer -> - db.transferDao().upsert(transfer) - } - + db.transferDao().upsert(parsed.transfers) Logger.debug("Restored ${parsed.transfers.size} transfers", context = TAG) } performRestore(BackupCategory.METADATA) { dataBytes -> val parsed = json.decodeFromString(String(dataBytes)) - - parsed.tagMetadata.forEach { entity -> - db.tagMetadataDao().upsert(entity) - } - + db.tagMetadataDao().upsert(parsed.tagMetadata) cacheStore.update { parsed.cache } - Logger.debug("Restored caches and ${parsed.tagMetadata.size} tags metadata records", context = TAG) } performRestore(BackupCategory.BLOCKTANK) { dataBytes -> val parsed = json.decodeFromString(String(dataBytes)) - - blocktankRepo.restoreFromBackup(parsed) - .onSuccess { - Logger.debug( - "Restored LSP info, ${parsed.orders.size} orders, ${parsed.cjitEntries.size} CJIT entries", - context = TAG, - ) - }.onFailure { e -> - Logger.warn("Failed to restore LSP info, orders and CJIT entries", e, context = TAG) - } - + blocktankRepo.restoreFromBackup(parsed).onSuccess { + Logger.debug( + "Restored ${parsed.orders.size} orders, ${parsed.cjitEntries.size} CJITs", context = TAG, + ) + } } performRestore(BackupCategory.ACTIVITY) { dataBytes -> val parsed = json.decodeFromString(String(dataBytes)) - - activityRepo.restoreFromBackup(parsed) - .onSuccess { - Logger.debug( - "Restored ${parsed.activities.size} activities and " + - "${parsed.closedChannels.size} closed channels", - context = TAG, - ) - }.onFailure { e -> - Logger.warn("Failed to restore activities and closed channels", e, context = TAG) - } + activityRepo.restoreFromBackup(parsed).onSuccess { + Logger.debug( + "Restored ${parsed.activities.size} activities, ${parsed.closedChannels.size} closed channels", + context = TAG, + ) + } } Logger.info("Full restore success", context = TAG) From 6344eb181259aa3fe08ad47dc91581de6de94f9f Mon Sep 17 00:00:00 2001 From: Ovi Trif Date: Tue, 4 Nov 2025 15:37:28 +0100 Subject: [PATCH 06/12] fix: pass empty list for boostTxIds --- .../ui/screens/wallets/activity/ActivityDetailScreen.kt | 1 + .../ui/screens/wallets/activity/ActivityExploreScreen.kt | 1 + .../ui/screens/wallets/activity/components/ActivityIcon.kt | 4 ++++ .../bitkit/ui/screens/wallets/activity/utils/PreviewItems.kt | 2 ++ 4 files changed, 8 insertions(+) diff --git a/app/src/main/java/to/bitkit/ui/screens/wallets/activity/ActivityDetailScreen.kt b/app/src/main/java/to/bitkit/ui/screens/wallets/activity/ActivityDetailScreen.kt index ed9eb60b6..318ea6f72 100644 --- a/app/src/main/java/to/bitkit/ui/screens/wallets/activity/ActivityDetailScreen.kt +++ b/app/src/main/java/to/bitkit/ui/screens/wallets/activity/ActivityDetailScreen.kt @@ -693,6 +693,7 @@ private fun PreviewOnchain() { confirmed = true, timestamp = (System.currentTimeMillis() / 1000 - 3600).toULong(), isBoosted = false, + boostTxIds = emptyList(), isTransfer = false, doesExist = true, confirmTimestamp = (System.currentTimeMillis() / 1000).toULong(), diff --git a/app/src/main/java/to/bitkit/ui/screens/wallets/activity/ActivityExploreScreen.kt b/app/src/main/java/to/bitkit/ui/screens/wallets/activity/ActivityExploreScreen.kt index 341021892..d6a611ea5 100644 --- a/app/src/main/java/to/bitkit/ui/screens/wallets/activity/ActivityExploreScreen.kt +++ b/app/src/main/java/to/bitkit/ui/screens/wallets/activity/ActivityExploreScreen.kt @@ -361,6 +361,7 @@ private fun PreviewOnchain() { confirmed = true, timestamp = (System.currentTimeMillis() / 1000 - 3600).toULong(), isBoosted = false, + boostTxIds = emptyList(), isTransfer = false, doesExist = true, confirmTimestamp = (System.currentTimeMillis() / 1000).toULong(), diff --git a/app/src/main/java/to/bitkit/ui/screens/wallets/activity/components/ActivityIcon.kt b/app/src/main/java/to/bitkit/ui/screens/wallets/activity/components/ActivityIcon.kt index dc8fb6d48..540c02c2e 100644 --- a/app/src/main/java/to/bitkit/ui/screens/wallets/activity/components/ActivityIcon.kt +++ b/app/src/main/java/to/bitkit/ui/screens/wallets/activity/components/ActivityIcon.kt @@ -206,6 +206,7 @@ private fun Preview() { confirmed = true, timestamp = (System.currentTimeMillis() / 1000).toULong(), isBoosted = false, + boostTxIds = emptyList(), isTransfer = false, doesExist = true, confirmTimestamp = (System.currentTimeMillis() / 1000).toULong(), @@ -231,6 +232,7 @@ private fun Preview() { confirmed = false, timestamp = (System.currentTimeMillis() / 1000).toULong(), isBoosted = true, + boostTxIds = emptyList(), isTransfer = false, doesExist = true, confirmTimestamp = (System.currentTimeMillis() / 1000).toULong(), @@ -256,6 +258,7 @@ private fun Preview() { confirmed = false, timestamp = (System.currentTimeMillis() / 1000).toULong(), isBoosted = true, + boostTxIds = emptyList(), isTransfer = false, doesExist = true, confirmTimestamp = (System.currentTimeMillis() / 1000).toULong(), @@ -281,6 +284,7 @@ private fun Preview() { confirmed = true, timestamp = (System.currentTimeMillis() / 1000).toULong(), isBoosted = false, + boostTxIds = emptyList(), isTransfer = true, doesExist = true, confirmTimestamp = (System.currentTimeMillis() / 1000).toULong(), diff --git a/app/src/main/java/to/bitkit/ui/screens/wallets/activity/utils/PreviewItems.kt b/app/src/main/java/to/bitkit/ui/screens/wallets/activity/utils/PreviewItems.kt index 68c4b1584..85067bb97 100644 --- a/app/src/main/java/to/bitkit/ui/screens/wallets/activity/utils/PreviewItems.kt +++ b/app/src/main/java/to/bitkit/ui/screens/wallets/activity/utils/PreviewItems.kt @@ -30,6 +30,7 @@ val previewActivityItems = buildList { confirmed = true, timestamp = today.epochSecond(), isBoosted = false, + boostTxIds = emptyList(), isTransfer = true, doesExist = true, confirmTimestamp = today.epochSecond(), @@ -93,6 +94,7 @@ val previewActivityItems = buildList { confirmed = false, timestamp = thisMonth.epochSecond(), isBoosted = false, + boostTxIds = emptyList(), isTransfer = true, doesExist = true, confirmTimestamp = today.epochSecond() + 3600u, From 8b62cf1632aa08df677bd6bc044abbd5e558ce40 Mon Sep 17 00:00:00 2001 From: Ovi Trif Date: Tue, 4 Nov 2025 15:40:36 +0100 Subject: [PATCH 07/12] chore: fix build --- .../main/java/to/bitkit/repositories/ActivityRepo.kt | 4 ++-- .../java/to/bitkit/repositories/BlocktankRepo.kt | 6 +++--- app/src/main/java/to/bitkit/services/CoreService.kt | 12 ++++++------ 3 files changed, 11 insertions(+), 11 deletions(-) diff --git a/app/src/main/java/to/bitkit/repositories/ActivityRepo.kt b/app/src/main/java/to/bitkit/repositories/ActivityRepo.kt index d7a69c097..9683f03da 100644 --- a/app/src/main/java/to/bitkit/repositories/ActivityRepo.kt +++ b/app/src/main/java/to/bitkit/repositories/ActivityRepo.kt @@ -651,8 +651,8 @@ class ActivityRepo @Inject constructor( suspend fun restoreFromBackup(backup: ActivityBackupV1): Result = withContext(bgDispatcher) { return@withContext runCatching { - coreService.activity.upsert(backup.activities) - coreService.activity.upsert(backup.closedChannels) + coreService.activity.upsertList(backup.activities) + coreService.activity.upsertClosedChannelList(backup.closedChannels) } } diff --git a/app/src/main/java/to/bitkit/repositories/BlocktankRepo.kt b/app/src/main/java/to/bitkit/repositories/BlocktankRepo.kt index 558470308..a4923b401 100644 --- a/app/src/main/java/to/bitkit/repositories/BlocktankRepo.kt +++ b/app/src/main/java/to/bitkit/repositories/BlocktankRepo.kt @@ -377,10 +377,10 @@ class BlocktankRepo @Inject constructor( suspend fun restoreFromBackup(backup: BlocktankBackupV1): Result = withContext(bgDispatcher) { return@withContext runCatching { - coreService.blocktank.upsert(backup.orders) - coreService.blocktank.upsert(backup.cjitEntries) + coreService.blocktank.upsertOrderList(backup.orders) + coreService.blocktank.upsertCjitList(backup.cjitEntries) backup.info?.let { info -> - coreService.blocktank.upsert(info) + coreService.blocktank.setInfo(info) } // We don't refresh orders here because we rely on the polling mechanism. diff --git a/app/src/main/java/to/bitkit/services/CoreService.kt b/app/src/main/java/to/bitkit/services/CoreService.kt index 3d0f6c9b1..62c7c7153 100644 --- a/app/src/main/java/to/bitkit/services/CoreService.kt +++ b/app/src/main/java/to/bitkit/services/CoreService.kt @@ -211,15 +211,15 @@ class ActivityService( upsertActivity(activity) } - suspend fun upsert(activities: List) = ServiceQueue.CORE.background { + suspend fun upsertList(activities: List) = ServiceQueue.CORE.background { upsertActivities(activities) } - suspend fun upsert(closedChannel: ClosedChannelDetails) = ServiceQueue.CORE.background { + suspend fun upsertClosedChannelItem(closedChannel: ClosedChannelDetails) = ServiceQueue.CORE.background { upsertClosedChannel(closedChannel) } - suspend fun upsert(closedChannels: List) = ServiceQueue.CORE.background { + suspend fun upsertClosedChannelList(closedChannels: List) = ServiceQueue.CORE.background { upsertClosedChannels(closedChannels) } @@ -690,15 +690,15 @@ class BlocktankService( } } - suspend fun upsert(info: IBtInfo) = ServiceQueue.CORE.background { + suspend fun setInfo(info: IBtInfo) = ServiceQueue.CORE.background { upsertInfo(info) } - suspend fun upsert(orders: List) = ServiceQueue.CORE.background { + suspend fun upsertOrderList(orders: List) = ServiceQueue.CORE.background { upsertOrders(orders) } - suspend fun upsert(cjitEntries: List) = ServiceQueue.CORE.background { + suspend fun upsertCjitList(cjitEntries: List) = ServiceQueue.CORE.background { upsertCjitEntries(cjitEntries) } From 197c7463d3e3d58073d1ad1b8c7111795d023d5a Mon Sep 17 00:00:00 2001 From: Ovi Trif Date: Tue, 4 Nov 2025 15:43:35 +0100 Subject: [PATCH 08/12] feat: last backup time in backup sheet --- .../backups/BackupNavSheetViewModel.kt | 44 ++++++++++++++++--- .../ui/settings/backups/MetadataScreen.kt | 28 +++++++++--- 2 files changed, 60 insertions(+), 12 deletions(-) diff --git a/app/src/main/java/to/bitkit/ui/settings/backups/BackupNavSheetViewModel.kt b/app/src/main/java/to/bitkit/ui/settings/backups/BackupNavSheetViewModel.kt index 02396f977..48a498c87 100644 --- a/app/src/main/java/to/bitkit/ui/settings/backups/BackupNavSheetViewModel.kt +++ b/app/src/main/java/to/bitkit/ui/settings/backups/BackupNavSheetViewModel.kt @@ -10,12 +10,17 @@ import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asSharedFlow import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch import to.bitkit.R +import to.bitkit.data.CacheStore import to.bitkit.data.SettingsStore import to.bitkit.data.keychain.Keychain +import to.bitkit.models.BackupCategory +import to.bitkit.models.HealthState import to.bitkit.models.Toast +import to.bitkit.repositories.HealthRepo import to.bitkit.ui.settings.backups.BackupContract.SideEffect import to.bitkit.ui.settings.backups.BackupContract.UiState import to.bitkit.ui.shared.toast.ToastEventBus @@ -27,6 +32,8 @@ class BackupNavSheetViewModel @Inject constructor( @ApplicationContext private val context: Context, private val settingsStore: SettingsStore, private val keychain: Keychain, + private val healthRepo: HealthRepo, + private val cacheStore: CacheStore, ) : ViewModel() { private val _uiState = MutableStateFlow(UiState()) @@ -37,6 +44,34 @@ class BackupNavSheetViewModel @Inject constructor( private fun setEffect(effect: SideEffect) = viewModelScope.launch { _effects.emit(effect) } + companion object { + private const val TAG = "BackupNavSheetViewModel" + } + + init { + collectState() + } + + private fun collectState() { + viewModelScope.launch { + combine(healthRepo.healthState, cacheStore.backupStatuses) { healthState, backupStatuses -> + when (healthState.backups) { + HealthState.ERROR -> null + else -> { + BackupCategory.entries + .filter { it != BackupCategory.LIGHTNING_CONNECTIONS } + .maxOfOrNull { category -> backupStatuses[category]?.synced ?: 0L } + .takeIf { it != 0L } + } + } + }.collect { lastBackupTimeMs -> + _uiState.update { + it.copy(lastBackupTimeMs = lastBackupTimeMs) + } + } + } + } + fun loadMnemonicData() { viewModelScope.launch { try { @@ -50,7 +85,7 @@ class BackupNavSheetViewModel @Inject constructor( ) } } catch (e: Throwable) { - Logger.error("Error loading mnemonic", e) + Logger.error("Error loading mnemonic", e, context = TAG) ToastEventBus.send( type = Toast.ToastType.WARNING, title = context.getString(R.string.security__mnemonic_error), @@ -109,11 +144,6 @@ class BackupNavSheetViewModel @Inject constructor( } fun onMultipleDevicesContinue() { - // TODO: get from actual repository state - val lastBackupTimeMs = System.currentTimeMillis() - _uiState.update { - it.copy(lastBackupTimeMs = lastBackupTimeMs) - } setEffect(SideEffect.NavigateToMetadata) } @@ -136,7 +166,7 @@ interface BackupContract { val bip39Passphrase: String = "", val showMnemonic: Boolean = false, val enteredPassphrase: String = "", - val lastBackupTimeMs: Long = System.currentTimeMillis(), + val lastBackupTimeMs: Long? = null, ) sealed interface SideEffect { diff --git a/app/src/main/java/to/bitkit/ui/settings/backups/MetadataScreen.kt b/app/src/main/java/to/bitkit/ui/settings/backups/MetadataScreen.kt index 75f56d78f..b7c54bb0c 100644 --- a/app/src/main/java/to/bitkit/ui/settings/backups/MetadataScreen.kt +++ b/app/src/main/java/to/bitkit/ui/settings/backups/MetadataScreen.kt @@ -44,11 +44,19 @@ fun MetadataScreen( @Composable private fun MetadataContent( - lastBackupTimeMs: Long, + lastBackupTimeMs: Long?, onDismiss: () -> Unit, onBack: () -> Unit, ) { - val latestBackupTime = remember(lastBackupTimeMs) { lastBackupTimeMs.toLocalizedTimestamp() } + val backupTimeText = remember(lastBackupTimeMs) { + runCatching { lastBackupTimeMs?.toLocalizedTimestamp() }.getOrNull() + } + val lastBackupText = backupTimeText + ?.let { stringResource(R.string.security__mnemonic_latest_backup).replace("{time}", it) } + ?: run { + val err = stringResource(R.string.settings__status__backup__error) + stringResource(R.string.security__mnemonic_latest_backup).replace("{time}", err) + } Column( modifier = Modifier @@ -80,9 +88,7 @@ private fun MetadataContent( ) BodyS( - text = stringResource(R.string.security__mnemonic_latest_backup) - .replace("{time}", latestBackupTime) - .withBold(), + text = lastBackupText.withBold(), modifier = Modifier.testTag("backup_time_text") ) @@ -110,3 +116,15 @@ private fun Preview() { ) } } + +@Preview +@Composable +private fun PreviewFailed() { + AppThemeSurface { + MetadataContent( + lastBackupTimeMs = null, + onDismiss = {}, + onBack = {}, + ) + } +} From 29ab4a1a7f4ae0085eddbe51dab22ad007260481 Mon Sep 17 00:00:00 2001 From: Ovi Trif Date: Tue, 4 Nov 2025 17:37:44 +0100 Subject: [PATCH 09/12] chore: cleanup duplicate error strings --- .../java/to/bitkit/viewmodels/TransferViewModel.kt | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/app/src/main/java/to/bitkit/viewmodels/TransferViewModel.kt b/app/src/main/java/to/bitkit/viewmodels/TransferViewModel.kt index 6d88e87c4..ef6834b5f 100644 --- a/app/src/main/java/to/bitkit/viewmodels/TransferViewModel.kt +++ b/app/src/main/java/to/bitkit/viewmodels/TransferViewModel.kt @@ -240,8 +240,9 @@ class TransferViewModel @Inject constructor( Logger.debug("Refreshing order '$orderId'") val order = blocktankRepo.getOrder(orderId, refresh = true).getOrNull() if (order == null) { - error = Exception("Order not found '$orderId'") - Logger.error("Order not found '$orderId'", context = TAG) + error = Exception("Order not found '$orderId'").also { + Logger.error(it.message, context = TAG) + } break } @@ -250,8 +251,9 @@ class TransferViewModel @Inject constructor( Logger.debug("LN setup step: $step") if (order.state2 == BtOrderState2.EXPIRED) { - error = Exception("Order expired '$orderId'") - Logger.error("Order expired '$orderId'", context = TAG) + error = Exception("Order expired '$orderId'").also { + Logger.error(it.message, context = TAG) + } break } if (step > 2) { From ae5eabe771ba0062cea7f68bb112a08476e3368b Mon Sep 17 00:00:00 2001 From: Ovi Trif Date: Tue, 4 Nov 2025 17:41:22 +0100 Subject: [PATCH 10/12] chore: lint --- app/src/main/java/to/bitkit/repositories/ActivityRepo.kt | 2 +- app/src/main/java/to/bitkit/repositories/BackupRepo.kt | 6 ++---- app/src/main/java/to/bitkit/repositories/BlocktankRepo.kt | 2 +- 3 files changed, 4 insertions(+), 6 deletions(-) diff --git a/app/src/main/java/to/bitkit/repositories/ActivityRepo.kt b/app/src/main/java/to/bitkit/repositories/ActivityRepo.kt index 9683f03da..0d476281c 100644 --- a/app/src/main/java/to/bitkit/repositories/ActivityRepo.kt +++ b/app/src/main/java/to/bitkit/repositories/ActivityRepo.kt @@ -211,7 +211,7 @@ class ActivityRepo @Inject constructor( runCatching { coreService.activity.closedChannels(sortDirection) }.onFailure { e -> - Logger.error("Error getting closed channels (sortDirection=${SortDirection})", e, context = TAG) + Logger.error("Error getting closed channels (sortDirection=$sortDirection)", e, context = TAG) } } diff --git a/app/src/main/java/to/bitkit/repositories/BackupRepo.kt b/app/src/main/java/to/bitkit/repositories/BackupRepo.kt index c32dad555..00a980634 100644 --- a/app/src/main/java/to/bitkit/repositories/BackupRepo.kt +++ b/app/src/main/java/to/bitkit/repositories/BackupRepo.kt @@ -408,14 +408,12 @@ class BackupRepo @Inject constructor( val parsed = json.decodeFromString(String(dataBytes)) db.tagMetadataDao().upsert(parsed.tagMetadata) cacheStore.update { parsed.cache } - Logger.debug("Restored caches and ${parsed.tagMetadata.size} tags metadata records", context = TAG) + Logger.debug("Restored caches and ${parsed.tagMetadata.size} tags metadata records", TAG) } performRestore(BackupCategory.BLOCKTANK) { dataBytes -> val parsed = json.decodeFromString(String(dataBytes)) blocktankRepo.restoreFromBackup(parsed).onSuccess { - Logger.debug( - "Restored ${parsed.orders.size} orders, ${parsed.cjitEntries.size} CJITs", context = TAG, - ) + Logger.debug("Restored ${parsed.orders.size} orders, ${parsed.cjitEntries.size} CJITs", TAG) } } performRestore(BackupCategory.ACTIVITY) { dataBytes -> diff --git a/app/src/main/java/to/bitkit/repositories/BlocktankRepo.kt b/app/src/main/java/to/bitkit/repositories/BlocktankRepo.kt index a4923b401..2a1388aae 100644 --- a/app/src/main/java/to/bitkit/repositories/BlocktankRepo.kt +++ b/app/src/main/java/to/bitkit/repositories/BlocktankRepo.kt @@ -384,7 +384,7 @@ class BlocktankRepo @Inject constructor( } // We don't refresh orders here because we rely on the polling mechanism. - // We also don't restore `paidOrders` as the refresh should do it using the paidOrderIds from restored cache. + // We also don't restore `paidOrders` the refresh interval uses restored paidOrderIds to rebuild the list. _blocktankState.update { it.copy( From ddc809538c1e7337bbd7fbdbb0aad5130241fb35 Mon Sep 17 00:00:00 2001 From: Ovi Trif Date: Wed, 5 Nov 2025 14:53:40 +0100 Subject: [PATCH 11/12] fix: tests --- .../java/to/bitkit/ui/sheets/BoostTransactionViewModelTest.kt | 1 + 1 file changed, 1 insertion(+) diff --git a/app/src/test/java/to/bitkit/ui/sheets/BoostTransactionViewModelTest.kt b/app/src/test/java/to/bitkit/ui/sheets/BoostTransactionViewModelTest.kt index 85d6421fc..f25be6e2c 100644 --- a/app/src/test/java/to/bitkit/ui/sheets/BoostTransactionViewModelTest.kt +++ b/app/src/test/java/to/bitkit/ui/sheets/BoostTransactionViewModelTest.kt @@ -51,6 +51,7 @@ class BoostTransactionViewModelTest : BaseUnitTest() { confirmed = false, timestamp = 1234567890UL, isBoosted = false, + boostTxIds = emptyList(), isTransfer = false, doesExist = true, confirmTimestamp = null, From 16df3f91786dff4c1a971b062237dc4626fbab52 Mon Sep 17 00:00:00 2001 From: Ovi Trif Date: Wed, 5 Nov 2025 14:57:32 +0100 Subject: [PATCH 12/12] chore: review --- app/src/main/java/to/bitkit/data/dao/TagMetadataDao.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/to/bitkit/data/dao/TagMetadataDao.kt b/app/src/main/java/to/bitkit/data/dao/TagMetadataDao.kt index 79f13f6ee..8e03451f6 100644 --- a/app/src/main/java/to/bitkit/data/dao/TagMetadataDao.kt +++ b/app/src/main/java/to/bitkit/data/dao/TagMetadataDao.kt @@ -16,10 +16,10 @@ interface TagMetadataDao { suspend fun insert(tagMetadata: TagMetadataEntity) @Upsert - suspend fun upsert(entities: TagMetadataEntity) + suspend fun upsert(entity: TagMetadataEntity) @Upsert - suspend fun upsert(tagMetadataList: List) + suspend fun upsert(entities: List) @Query("SELECT * FROM tag_metadata") fun observeAll(): Flow>