Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
8dfd9cd
feat: wallet backup boosts & transfers
ovitrif Oct 31, 2025
6f839ff
feat: metadata backup tags & tx
ovitrif Oct 31, 2025
7d39688
feat: metadata backup tags & tx
ovitrif Oct 31, 2025
3356cd8
feat: activity backup
ovitrif Oct 31, 2025
3b9e330
feat: lightning connections status sync time
ovitrif Oct 31, 2025
f696a1f
fix: disable retry for ldk instead of activities
ovitrif Oct 31, 2025
d1a2b84
feat: comment out contacts backup category
ovitrif Oct 31, 2025
2cc9e5c
test: fix activity repo tests
ovitrif Oct 31, 2025
a3012f6
refactor: use clock for currentTimeMillis
ovitrif Oct 31, 2025
f510480
fix: remove notifyActivitiesChanged from sync
ovitrif Oct 31, 2025
3f2275d
refactor: encapsulate backup categories props
ovitrif Oct 31, 2025
ca140c1
refactor: rename to BackupCategory
ovitrif Oct 31, 2025
63912aa
refactor: backup & restore all caches
ovitrif Oct 31, 2025
a8658c7
refactor: rename LDK_ACTIVITY to ACTIVITY
ovitrif Oct 31, 2025
0655c69
chore: lint
ovitrif Oct 31, 2025
1b19a51
refactor: use upsert
ovitrif Oct 31, 2025
446ee64
refactor… rename saveTagMetadata to insert
ovitrif Oct 31, 2025
bee7811
refactor: use if guards
ovitrif Nov 1, 2025
3f9ea1f
chore: cleanup
ovitrif Nov 1, 2025
f8cb834
refactor: move node start callback last
ovitrif Nov 1, 2025
91e0a8b
fix: use main dispatcher to collect state
ovitrif Nov 2, 2025
366a8bc
chore: cleanup comments
ovitrif Nov 2, 2025
00c17a9
chore: update gitignore
ovitrif Nov 3, 2025
a510992
fix: remove dupe `updateInvoice` call on receive confirm screen
ovitrif Nov 3, 2025
9450857
fix: bip21 wallet state management on events
ovitrif Nov 2, 2025
8dea165
test: bip21 wallet state management fix
ovitrif Nov 3, 2025
eb0b963
chore: lint
ovitrif Nov 3, 2025
fbcc17d
fix: log messages after refactors
ovitrif Nov 3, 2025
3fdd2f3
fix: remove backup statuses from observer to avoid infinite loop
ovitrif Nov 3, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,11 @@
.externalNativeBuild
.cxx
local.properties
# AI
.ai
.cursor
*.local.*
CLAUDE.md

# Secrets
google-services.json
.env
Expand Down
1 change: 0 additions & 1 deletion app/src/main/java/to/bitkit/data/CacheStore.kt
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,6 @@ class CacheStore @Inject constructor(
private val store = context.appCacheDataStore

val data: Flow<AppCacheData> = store.data

val backupStatuses: Flow<Map<BackupCategory, BackupItemStatus>> = data.map { it.backupStatuses }

suspend fun update(transform: (AppCacheData) -> AppCacheData) {
Expand Down
2 changes: 1 addition & 1 deletion app/src/main/java/to/bitkit/data/SettingsStore.kt
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ import javax.inject.Singleton

private val Context.settingsDataStore: DataStore<SettingsData> by dataStore(
fileName = "settings.json",
serializer = SettingsSerializer
serializer = SettingsSerializer,
)

@Singleton
Expand Down
2 changes: 1 addition & 1 deletion app/src/main/java/to/bitkit/data/WidgetsStore.kt
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ import javax.inject.Singleton

private val Context.widgetsDataStore: DataStore<WidgetsData> by dataStore(
fileName = "widgets.json",
serializer = WidgetsSerializer
serializer = WidgetsSerializer,
)

@Singleton
Expand Down
10 changes: 9 additions & 1 deletion app/src/main/java/to/bitkit/data/dao/TagMetadataDao.kt
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,21 @@ import androidx.room.Delete
import androidx.room.Insert
import androidx.room.OnConflictStrategy
import androidx.room.Query
import androidx.room.Upsert
import kotlinx.coroutines.flow.Flow
import to.bitkit.data.entities.TagMetadataEntity

@Dao
interface TagMetadataDao {

@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun saveTagMetadata(tagMetadata: TagMetadataEntity)
suspend fun insert(tagMetadata: TagMetadataEntity)

@Upsert
suspend fun upsert(tagMetadata: TagMetadataEntity)

@Query("SELECT * FROM tag_metadata")
fun observeAll(): Flow<List<TagMetadataEntity>>

@Query("SELECT * FROM tag_metadata")
suspend fun getAll(): List<TagMetadataEntity>
Expand Down
10 changes: 10 additions & 0 deletions app/src/main/java/to/bitkit/data/dao/TransferDao.kt
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import androidx.room.Insert
import androidx.room.OnConflictStrategy
import androidx.room.Query
import androidx.room.Update
import androidx.room.Upsert
import kotlinx.coroutines.flow.Flow
import to.bitkit.data.entities.TransferEntity

Expand All @@ -13,9 +14,18 @@ interface TransferDao {
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insert(transfer: TransferEntity)

@Upsert
suspend fun upsert(transfer: TransferEntity)

@Update
suspend fun update(transfer: TransferEntity)

@Query("SELECT * FROM transfers")
suspend fun getAll(): List<TransferEntity>

@Query("SELECT * FROM transfers")
fun observeAll(): Flow<List<TransferEntity>>

@Query("SELECT * FROM transfers WHERE isSettled = 0")
fun getActiveTransfers(): Flow<List<TransferEntity>>

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@ package to.bitkit.data.entities

import androidx.room.Entity
import androidx.room.PrimaryKey
import kotlinx.serialization.Serializable

@Serializable
@Entity(tableName = "tag_metadata")
/**
* @param id This will be paymentHash, txId, or address depending on context
Expand Down
7 changes: 7 additions & 0 deletions app/src/main/java/to/bitkit/env/Env.kt
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,13 @@ internal object Env {
return "$BIT_REFILL_URL/$page/$BITREFILL_PARAMS"
}

/**
* Generates the storage path for a specified wallet index, network, and directory.
*
* Output format:
*
* `appStoragePath/network/walletN/dir`
*/
private fun storagePathOf(walletIndex: Int, network: String, dir: String): String {
require(::appStoragePath.isInitialized) { "App storage path should be 'context.filesDir.absolutePath'." }
val path = Path(appStoragePath, network, "wallet$walletIndex", dir)
Expand Down
2 changes: 2 additions & 0 deletions app/src/main/java/to/bitkit/ext/DateTime.kt
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
@file:Suppress("TooManyFunctions")

package to.bitkit.ext

import android.icu.text.DateFormat
Expand Down
62 changes: 62 additions & 0 deletions app/src/main/java/to/bitkit/models/BackupCategory.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
package to.bitkit.models

import androidx.annotation.DrawableRes
import androidx.annotation.StringRes
import kotlinx.serialization.Serializable
import to.bitkit.R

@Serializable
enum class BackupCategory(
@DrawableRes val icon: Int,
@StringRes val title: Int,
) {
LIGHTNING_CONNECTIONS(
icon = R.drawable.ic_lightning,
title = R.string.settings__backup__category_connections,
),
BLOCKTANK(
icon = R.drawable.ic_note,
title = R.string.settings__backup__category_connection_receipts,
),
ACTIVITY(
icon = R.drawable.ic_transfer,
title = R.string.settings__backup__category_transaction_log,
),
WALLET(
icon = R.drawable.ic_timer_alt,
title = R.string.settings__backup__category_wallet,
),
SETTINGS(
icon = R.drawable.ic_settings,
title = R.string.settings__backup__category_settings,
),
WIDGETS(
icon = R.drawable.ic_rectangles_two,
title = R.string.settings__backup__category_widgets,
),
METADATA(
icon = R.drawable.ic_tag,
title = R.string.settings__backup__category_tags,
),
// Descoped in v1, will return in v2:
// PROFILE(
// icon = R.drawable.ic_user,
// title = R.string.settings__backup__category_profile,
// ),
// CONTACTS(
// icon = R.drawable.ic_users,
// title = R.string.settings__backup__category_contacts,
// ),
}

/**
* @property running In progress
* @property synced Timestamp in ms of last time this backup was synced
* @property required Timestamp in ms of last time this backup was required
*/
@Serializable
data class BackupItemStatus(
val running: Boolean = false,
val synced: Long = 0L,
val required: Long = 0L,
)
41 changes: 41 additions & 0 deletions app/src/main/java/to/bitkit/models/BackupPayloads.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
package to.bitkit.models

import com.synonym.bitkitcore.Activity
import com.synonym.bitkitcore.IBtInfo
import com.synonym.bitkitcore.IBtOrder
import com.synonym.bitkitcore.IcJitEntry
import kotlinx.serialization.Serializable
import to.bitkit.data.AppCacheData
import to.bitkit.data.entities.TagMetadataEntity
import to.bitkit.data.entities.TransferEntity

@Serializable
data class WalletBackupV1(
val version: Int = 1,
val createdAt: Long,
val transfers: List<TransferEntity>,
)

@Serializable
data class MetadataBackupV1(
val version: Int = 1,
val createdAt: Long,
val tagMetadata: List<TagMetadataEntity>,
val cache: AppCacheData,
)

@Serializable
data class BlocktankBackupV1(
val version: Int = 1,
val createdAt: Long,
val orders: List<IBtOrder>,
val cjitEntries: List<IcJitEntry>,
val info: IBtInfo? = null,
)

@Serializable
data class ActivityBackupV1(
val version: Int = 1,
val createdAt: Long,
val activities: List<Activity>,
)
54 changes: 0 additions & 54 deletions app/src/main/java/to/bitkit/models/BackupStatus.kt

This file was deleted.

34 changes: 31 additions & 3 deletions app/src/main/java/to/bitkit/repositories/ActivityRepo.kt
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,12 @@ import kotlinx.coroutines.TimeoutCancellationException
import kotlinx.coroutines.async
import kotlinx.coroutines.awaitAll
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.withContext
import kotlinx.coroutines.withTimeout
import kotlinx.datetime.Clock
import org.lightningdevkit.ldknode.ChannelDetails
import org.lightningdevkit.ldknode.PaymentDetails
import to.bitkit.data.AppDb
Expand Down Expand Up @@ -44,9 +47,15 @@ class ActivityRepo @Inject constructor(
private val db: AppDb,
private val addressChecker: AddressChecker,
private val transferRepo: TransferRepo,
private val clock: Clock,
) {
val isSyncingLdkNodePayments = MutableStateFlow(false)

private val _activitiesChanged = MutableStateFlow(0L)
val activitiesChanged: StateFlow<Long> = _activitiesChanged

private fun notifyActivitiesChanged() = _activitiesChanged.update { clock.now().toEpochMilliseconds() }

suspend fun syncActivities(): Result<Unit> = withContext(bgDispatcher) {
Logger.debug("syncActivities called", context = TAG)

Expand Down Expand Up @@ -219,6 +228,7 @@ class ActivityRepo @Inject constructor(
)
}
coreService.activity.update(id, activity)
notifyActivitiesChanged()
}.onFailure { e ->
Logger.error("updateActivity error for ID: $id", e, context = TAG)
}
Expand Down Expand Up @@ -445,6 +455,7 @@ class ActivityRepo @Inject constructor(
val deleted = coreService.activity.delete(id)
if (deleted) {
cacheStore.addActivityToDeletedList(id)
notifyActivitiesChanged()
} else {
return@withContext Result.failure(Exception("Activity not deleted"))
}
Expand All @@ -463,11 +474,28 @@ class ActivityRepo @Inject constructor(
return@withContext Result.failure(Exception("Activity ${activity.rawId()} was deleted"))
}
coreService.activity.insert(activity)
notifyActivitiesChanged()
}.onFailure { e ->
Logger.error("insertActivity error", e, context = TAG)
}
}

/**
* Upserts an activity (insert or update if exists)
*/
suspend fun upsertActivity(activity: Activity): Result<Unit> = withContext(bgDispatcher) {
return@withContext runCatching {
if (activity.rawId() in cacheStore.data.first().deletedActivities) {
Logger.debug("Activity ${activity.rawId()} was deleted, skipping", context = TAG)
return@withContext Result.failure(Exception("Activity ${activity.rawId()} was deleted"))
}
coreService.activity.upsert(activity)
notifyActivitiesChanged()
}.onFailure { e ->
Logger.error("upsertActivity error", e, context = TAG)
}
}

/**
* Inserts a new activity for a fulfilled (channel ready) cjit channel order
*/
Expand Down Expand Up @@ -519,6 +547,7 @@ class ActivityRepo @Inject constructor(

if (newTags.isNotEmpty()) {
coreService.activity.appendTags(activityId, newTags).getOrThrow()
notifyActivitiesChanged()
Logger.info("Added ${newTags.size} new tags to activity $activityId", context = TAG)
} else {
Logger.info("No new tags to add to activity $activityId", context = TAG)
Expand Down Expand Up @@ -558,6 +587,7 @@ class ActivityRepo @Inject constructor(
checkNotNull(coreService.activity.getActivity(activityId)) { "Activity with ID $activityId not found" }

coreService.activity.dropTags(activityId, tags)
notifyActivitiesChanged()
Logger.info("Removed ${tags.size} tags from activity $activityId", context = TAG)
}.onFailure { e ->
Logger.error("removeTagsFromActivity error for activity $activityId", e, context = TAG)
Expand Down Expand Up @@ -606,9 +636,7 @@ class ActivityRepo @Inject constructor(
tags = tags,
createdAt = nowTimestamp().toEpochMilli()
)
db.tagMetadataDao().saveTagMetadata(
tagMetadata = entity
)
db.tagMetadataDao().insert(tagMetadata = entity)
Logger.debug("Tag metadata saved: $entity", context = TAG)
}.onFailure { e ->
Logger.error("getAllAvailableTags error", e, context = TAG)
Expand Down
Loading