Skip to content
Merged
Show file tree
Hide file tree
Changes from 10 commits
Commits
Show all changes
46 commits
Select commit Hold shift + click to select a range
4327451
fix: gate sheets and rotate address on restore
ovitrif Nov 7, 2025
ba1e164
fix: gate new transaction sheet on restore
ovitrif Nov 7, 2025
e146b1c
refactor: unify should backup check
ovitrif Nov 7, 2025
9823b7f
chore: Cleanup and add todo
ovitrif Nov 7, 2025
bc53574
feat: dismiss keyboard on valid words paste
ovitrif Nov 7, 2025
918f111
refactor: encapsulate wallet init logic
ovitrif Nov 7, 2025
4aaa73b
chore: update bitkit-core to 0.1.24
ovitrif Nov 7, 2025
3fb3b06
refactor: use core tag metadata model
ovitrif Nov 7, 2025
ab5bacf
feat: backup & restore activity tags
ovitrif Nov 7, 2025
0ea7ab9
refactor: migrate RestoreWalletScreen to MVVM
ovitrif Nov 7, 2025
970a6b3
feat: nav to previous input on backspace if empty
ovitrif Nov 8, 2025
ce10188
feat: bold text in focused input
ovitrif Nov 8, 2025
7e22f02
fix: avoid validation errors on focused input
ovitrif Nov 8, 2025
ac21d53
feat: use bitkit-core for bip39 & checksum
ovitrif Nov 8, 2025
48534ef
feat: wipe core db on wipe wallet
ovitrif Nov 8, 2025
6a40ee1
feat: reset logs on wipe wallet
ovitrif Nov 11, 2025
896109a
Merge branch 'fix/rotate-address' into feat/backup-polish
ovitrif Nov 11, 2025
1ac1eb9
feat: reset blocktank repo data on wipe
ovitrif Nov 11, 2025
4e48d95
fix: logger crash in unit tests
ovitrif Nov 12, 2025
bfc497c
refactor: add activity.txType extension
ovitrif Nov 13, 2025
ca35a69
feat: reset activity state and wipe fixes
ovitrif Nov 14, 2025
4ecd67b
feat: integrate bitkit-core 0.1.27 minimally
ovitrif Nov 14, 2025
3efa06f
Merge branch 'master' into feat/backup-polish
ovitrif Nov 14, 2025
6effdff
chore: lint
ovitrif Nov 14, 2025
42a48c7
feat: use payload models for settings and widgets
ovitrif Nov 17, 2025
e699c54
fix: preserve backup times & fix race condition
ovitrif Nov 17, 2025
df10340
chore: backup status docs & comments
ovitrif Nov 17, 2025
67cfcf9
chore: fix params compiler ambiguity
ovitrif Nov 17, 2025
3aa6ff8
fix: notify observers after activity restore
ovitrif Nov 17, 2025
b5d724c
fix: restore wallet input cursor & text style
ovitrif Nov 17, 2025
b634464
refactor: extract wipe wallet use case
ovitrif Nov 17, 2025
c359bad
test: wipe wallet use case
ovitrif Nov 17, 2025
d831803
refactor: split restore screen content
ovitrif Nov 17, 2025
1f97313
chore: lint
ovitrif Nov 17, 2025
9994130
refactor: extract bip39 service
ovitrif Nov 17, 2025
d1f8e8e
test: restore screen viewmodel
ovitrif Nov 17, 2025
3ebd9d5
feat: backup relative dates
ovitrif Nov 18, 2025
59d9b48
chore: lint
ovitrif Nov 18, 2025
daed5db
fix: backup relative dates
ovitrif Nov 18, 2025
342e144
test: fix syncActivities success flow test
ovitrif Nov 18, 2025
1a47b85
test: validate wipe order
ovitrif Nov 18, 2025
907a54d
fix: support tab and newline mnemonic separators
ovitrif Nov 18, 2025
e173ae6
fix: dependencies repositories ordering
ovitrif Nov 18, 2025
f0e8371
chore: enable dynamic agent loading explicitly
ovitrif Nov 18, 2025
c6ed583
fix: clear widgets data on wipe
ovitrif Nov 19, 2025
0573515
Merge branch 'master' into feat/backup-polish
ovitrif Nov 19, 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
24 changes: 24 additions & 0 deletions app/src/main/java/to/bitkit/ext/TagMetadata.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package to.bitkit.ext

import com.synonym.bitkitcore.ActivityTagsMetadata
import to.bitkit.data.entities.TagMetadataEntity

fun TagMetadataEntity.toActivityTagsMetadata() = ActivityTagsMetadata(
id,
paymentHash,
txId,
address,
isReceive,
tags,
createdAt.toULong(),
)

fun ActivityTagsMetadata.toTagMetadataEntity() = TagMetadataEntity(
id,
paymentHash,
txId,
address,
isReceive,
tags,
createdAt.toLong(),
)
6 changes: 4 additions & 2 deletions app/src/main/java/to/bitkit/models/BackupPayloads.kt
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
package to.bitkit.models

import com.synonym.bitkitcore.Activity
import com.synonym.bitkitcore.ActivityTags
import com.synonym.bitkitcore.ActivityTagsMetadata
import com.synonym.bitkitcore.ClosedChannelDetails
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
Expand All @@ -21,7 +22,7 @@ data class WalletBackupV1(
data class MetadataBackupV1(
val version: Int = 1,
val createdAt: Long,
val tagMetadata: List<TagMetadataEntity>,
val tagMetadata: List<ActivityTagsMetadata>,
val cache: AppCacheData,
)

Expand All @@ -39,5 +40,6 @@ data class ActivityBackupV1(
val version: Int = 1,
val createdAt: Long,
val activities: List<Activity>,
val activityTags: List<ActivityTags>,
val closedChannels: List<ClosedChannelDetails>,
)
13 changes: 13 additions & 0 deletions app/src/main/java/to/bitkit/repositories/ActivityRepo.kt
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package to.bitkit.repositories

import com.synonym.bitkitcore.Activity
import com.synonym.bitkitcore.ActivityFilter
import com.synonym.bitkitcore.ActivityTags
import com.synonym.bitkitcore.ClosedChannelDetails
import com.synonym.bitkitcore.IcJitEntry
import com.synonym.bitkitcore.LightningActivity
Expand Down Expand Up @@ -622,6 +623,17 @@ class ActivityRepo @Inject constructor(
}
}

/**
* Get all [ActivityTags] for backup
*/
suspend fun getAllActivityTags(): Result<List<ActivityTags>> = withContext(bgDispatcher) {
return@withContext runCatching {
coreService.activity.getAllActivityTags()
}.onFailure { e ->
Logger.error("getAllActivityTags error", e, context = TAG)
}
}

suspend fun saveTagsMetadata(
id: String,
paymentHash: String? = null,
Expand Down Expand Up @@ -652,6 +664,7 @@ class ActivityRepo @Inject constructor(
suspend fun restoreFromBackup(backup: ActivityBackupV1): Result<Unit> = withContext(bgDispatcher) {
return@withContext runCatching {
coreService.activity.upsertList(backup.activities)
coreService.activity.upsertTags(backup.activityTags)
coreService.activity.upsertClosedChannelList(backup.closedChannels)
}
}
Expand Down
51 changes: 33 additions & 18 deletions app/src/main/java/to/bitkit/repositories/BackupRepo.kt
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,14 @@ import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Job
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.drop
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import kotlinx.datetime.Clock
Expand All @@ -27,6 +31,8 @@ import to.bitkit.di.IoDispatcher
import to.bitkit.di.json
import to.bitkit.ext.formatPlural
import to.bitkit.ext.nowMillis
import to.bitkit.ext.toActivityTagsMetadata
import to.bitkit.ext.toTagMetadataEntity
import to.bitkit.models.ActivityBackupV1
import to.bitkit.models.BackupCategory
import to.bitkit.models.BackupItemStatus
Expand Down Expand Up @@ -63,7 +69,8 @@ class BackupRepo @Inject constructor(
private val dataListenerJobs = mutableListOf<Job>()
private var periodicCheckJob: Job? = null
private var isObserving = false
private var isRestoring = false
private val _isRestoring = MutableStateFlow(false)
val isRestoring: StateFlow<Boolean> = _isRestoring.asStateFlow()

private var lastNotificationTime = 0L

Expand Down Expand Up @@ -119,7 +126,7 @@ class BackupRepo @Inject constructor(
old.synced == new.synced && old.required == new.required
}
.collect { status ->
if (status.isRequired && !status.running && !isRestoring) {
if (status.shouldBackup()) {
scheduleBackup(category)
}
}
Expand All @@ -137,7 +144,7 @@ class BackupRepo @Inject constructor(
.distinctUntilChanged()
.drop(1)
.collect {
if (isRestoring) return@collect
if (isRestoring.value) return@collect
markBackupRequired(BackupCategory.SETTINGS)
}
}
Expand All @@ -148,7 +155,7 @@ class BackupRepo @Inject constructor(
.distinctUntilChanged()
.drop(1)
.collect {
if (isRestoring) return@collect
if (isRestoring.value) return@collect
markBackupRequired(BackupCategory.WIDGETS)
}
}
Expand All @@ -160,7 +167,7 @@ class BackupRepo @Inject constructor(
.distinctUntilChanged()
.drop(1)
.collect {
if (isRestoring) return@collect
if (isRestoring.value) return@collect
markBackupRequired(BackupCategory.WALLET)
}
}
Expand All @@ -172,7 +179,7 @@ class BackupRepo @Inject constructor(
.distinctUntilChanged()
.drop(1)
.collect {
if (isRestoring) return@collect
if (isRestoring.value) return@collect
markBackupRequired(BackupCategory.METADATA)
}
}
Expand All @@ -185,7 +192,7 @@ class BackupRepo @Inject constructor(
.distinctUntilChanged()
.drop(1)
.collect {
if (isRestoring) return@collect
if (isRestoring.value) return@collect
markBackupRequired(BackupCategory.METADATA)
}
}
Expand All @@ -196,7 +203,7 @@ class BackupRepo @Inject constructor(
blocktankRepo.blocktankState
.drop(1)
.collect {
if (isRestoring) return@collect
if (isRestoring.value) return@collect
markBackupRequired(BackupCategory.BLOCKTANK)
}
}
Expand All @@ -207,7 +214,7 @@ class BackupRepo @Inject constructor(
activityRepo.activitiesChanged
.drop(1)
.collect {
if (isRestoring) return@collect
if (isRestoring.value) return@collect
markBackupRequired(BackupCategory.ACTIVITY)
}
}
Expand All @@ -220,7 +227,7 @@ class BackupRepo @Inject constructor(
val lastSync = lightningService.status?.latestLightningWalletSyncTimestamp?.toLong()
?.let { it * 1000 } // Convert seconds to millis
?: return@collect
if (isRestoring) return@collect
if (isRestoring.value) return@collect
cacheStore.updateBackupStatus(BackupCategory.LIGHTNING_CONNECTIONS) {
it.copy(required = lastSync, synced = lastSync, running = false)
}
Expand Down Expand Up @@ -265,7 +272,7 @@ class BackupRepo @Inject constructor(

// Double-check if backup is still needed
val status = cacheStore.backupStatuses.first()[category] ?: BackupItemStatus()
if (status.isRequired && !isRestoring) {
if (status.shouldBackup()) {
triggerBackup(category)
} else {
// Backup no longer needed, reset running flag
Expand Down Expand Up @@ -361,7 +368,7 @@ class BackupRepo @Inject constructor(
}

BackupCategory.METADATA -> {
val tagMetadata = db.tagMetadataDao().getAll()
val tagMetadata = db.tagMetadataDao().getAll().map { it.toActivityTagsMetadata() }
val cacheData = cacheStore.data.first()

val payload = MetadataBackupV1(
Expand Down Expand Up @@ -389,10 +396,12 @@ class BackupRepo @Inject constructor(
BackupCategory.ACTIVITY -> {
val activities = activityRepo.getActivities().getOrDefault(emptyList())
val closedChannels = activityRepo.getClosedChannels().getOrDefault(emptyList())
val activityTags = activityRepo.getAllActivityTags().getOrDefault(emptyList())

val payload = ActivityBackupV1(
createdAt = currentTimeMillis(),
activities = activities,
activityTags = activityTags,
closedChannels = closedChannels,
)

Expand All @@ -407,16 +416,19 @@ class BackupRepo @Inject constructor(
): Result<Unit> = withContext(ioDispatcher) {
Logger.debug("Full restore starting", context = TAG)

isRestoring = true
_isRestoring.update { true }

return@withContext try {
performRestore(BackupCategory.METADATA) { dataBytes ->
val parsed = json.decodeFromString<MetadataBackupV1>(String(dataBytes))
cacheStore.update { parsed.cache }
cacheStore.update {
parsed.cache.copy(onchainAddress = "") // Fore onchain address rotation
}
Logger.debug("Restored caches: ${jsonLogOf(parsed.cache.copy(cachedRates = emptyList()))}", TAG)
onCacheRestored()
db.tagMetadataDao().upsert(parsed.tagMetadata)
Logger.debug("Restored caches and ${parsed.tagMetadata.size} tags metadata records", TAG)
val tagMetadata = parsed.tagMetadata.map { it.toTagMetadataEntity() }
db.tagMetadataDao().upsert(tagMetadata)
Logger.debug("Restored caches and ${tagMetadata.size} tags metadata records", TAG)
}

performRestore(BackupCategory.SETTINGS) { dataBytes ->
Expand All @@ -442,7 +454,8 @@ class BackupRepo @Inject constructor(
val parsed = json.decodeFromString<ActivityBackupV1>(String(dataBytes))
activityRepo.restoreFromBackup(parsed).onSuccess {
Logger.debug(
"Restored ${parsed.activities.size} activities, ${parsed.closedChannels.size} closed channels",
"Restored ${parsed.activities.size} activities, ${parsed.activityTags.size} activity tags, " +
"${parsed.closedChannels.size} closed channels",
context = TAG,
)
}
Expand All @@ -454,7 +467,7 @@ class BackupRepo @Inject constructor(
Logger.warn("Full restore error", e = e, context = TAG)
Result.failure(e)
} finally {
isRestoring = false
_isRestoring.update { true }
}
}

Expand Down Expand Up @@ -492,6 +505,8 @@ class BackupRepo @Inject constructor(

private fun currentTimeMillis(): Long = nowMillis(clock)

private fun BackupItemStatus.shouldBackup() = this.isRequired && !this.running && !isRestoring.value

companion object {
private const val TAG = "BackupRepo"

Expand Down
24 changes: 15 additions & 9 deletions app/src/main/java/to/bitkit/services/CoreService.kt
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

import com.synonym.bitkitcore.Activity
import com.synonym.bitkitcore.ActivityFilter
import com.synonym.bitkitcore.ActivityTags
import com.synonym.bitkitcore.ActivityTagsMetadata
import com.synonym.bitkitcore.BtOrderState2
import com.synonym.bitkitcore.CJitStateEnum
import com.synonym.bitkitcore.ClosedChannelDetails
Expand All @@ -28,6 +30,7 @@
import com.synonym.bitkitcore.getActivities
import com.synonym.bitkitcore.getActivityById
import com.synonym.bitkitcore.getAllClosedChannels
import com.synonym.bitkitcore.getAllTagMetadata
import com.synonym.bitkitcore.getAllUniqueTags
import com.synonym.bitkitcore.getCjitEntries
import com.synonym.bitkitcore.getInfo
Expand All @@ -44,7 +47,6 @@
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
Expand Down Expand Up @@ -215,14 +217,6 @@
upsertActivities(activities)
}

suspend fun upsertClosedChannelItem(closedChannel: ClosedChannelDetails) = ServiceQueue.CORE.background {
upsertClosedChannel(closedChannel)
}

suspend fun upsertClosedChannelList(closedChannels: List<ClosedChannelDetails>) = ServiceQueue.CORE.background {
upsertClosedChannels(closedChannels)
}

suspend fun getActivity(id: String): Activity? {
return ServiceQueue.CORE.background {
getActivityById(id)
Expand Down Expand Up @@ -285,6 +279,18 @@
}
}

suspend fun upsertTags(activityTags: List<ActivityTags>) = ServiceQueue.CORE.background {
com.synonym.bitkitcore.upsertTags(activityTags)
}

suspend fun getAllActivityTags(): List<ActivityTags> = ServiceQueue.CORE.background {
getAllTagMetadata().map { ActivityTags(it.id, tags = it.tags) }
}

suspend fun upsertClosedChannelList(closedChannels: List<ClosedChannelDetails>) = ServiceQueue.CORE.background {
upsertClosedChannels(closedChannels)
}

suspend fun closedChannels(
sortDirection: SortDirection,
): List<ClosedChannelDetails> = ServiceQueue.CORE.background {
Expand Down
6 changes: 2 additions & 4 deletions app/src/main/java/to/bitkit/ui/MainActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -151,7 +151,8 @@ class MainActivity : FragmentActivity() {
}
)

if (appViewModel.showNewTransaction) {
val showNewTransaction by appViewModel.showNewTransaction.collectAsStateWithLifecycle()
if (showNewTransaction) {
NewTransactionSheet(
appViewModel = appViewModel,
currencyViewModel = currencyViewModel,
Expand Down Expand Up @@ -241,8 +242,6 @@ private fun OnboardingNav(
scope.launch {
runCatching {
appViewModel.resetIsAuthenticatedState()
walletViewModel.setInitNodeLifecycleState()
walletViewModel.setRestoringWalletState()
walletViewModel.restoreWallet(mnemonic, passphrase)
}.onFailure {
appViewModel.toast(it)
Expand All @@ -258,7 +257,6 @@ private fun OnboardingNav(
scope.launch {
runCatching {
appViewModel.resetIsAuthenticatedState()
walletViewModel.setInitNodeLifecycleState()
walletViewModel.createWallet(bip39Passphrase = passphrase)
}.onFailure {
appViewModel.toast(it)
Expand Down
Loading