Skip to content
Merged
Show file tree
Hide file tree
Changes from 17 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
4 changes: 2 additions & 2 deletions app/src/main/java/to/bitkit/env/Env.kt
Original file line number Diff line number Diff line change
Expand Up @@ -116,10 +116,10 @@ internal object Env {
Logger.info("App storage path: $path")
}

val logDir: String
val logDir: File
get() {
require(::appStoragePath.isInitialized)
return File(appStoragePath).resolve("logs").ensureDir().path
return File(appStoragePath).resolve("logs").ensureDir()
}

fun ldkStoragePath(walletIndex: Int) = storagePathOf(walletIndex, network.name.lowercase(), "ldk")
Expand Down
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
24 changes: 17 additions & 7 deletions app/src/main/java/to/bitkit/repositories/BackupRepo.kt
Original file line number Diff line number Diff line change
Expand Up @@ -31,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 @@ -124,7 +126,7 @@ class BackupRepo @Inject constructor(
old.synced == new.synced && old.required == new.required
}
.collect { status ->
if (status.isRequired && !status.running && !isRestoring.value) {
if (status.shouldBackup()) {
scheduleBackup(category)
}
}
Expand Down Expand Up @@ -270,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.value) {
if (status.shouldBackup()) {
triggerBackup(category)
} else {
// Backup no longer needed, reset running flag
Expand Down Expand Up @@ -366,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().copy(onchainAddress = "") // Force onchain address rotation

val payload = MetadataBackupV1(
Expand Down Expand Up @@ -394,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 @@ -417,11 +421,14 @@ class BackupRepo @Inject constructor(
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 @@ -447,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 Down Expand Up @@ -497,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
3 changes: 1 addition & 2 deletions app/src/main/java/to/bitkit/repositories/LogsRepo.kt
Original file line number Diff line number Diff line change
Expand Up @@ -58,8 +58,7 @@ class LogsRepo @Inject constructor(
/** Lists log files sorted by newest first */
suspend fun getLogs(): Result<List<LogFile>> = withContext(bgDispatcher) {
try {
val logDir = runCatching { File(Env.logDir) }.getOrElse { return@withContext Result.failure(it) }
if (!logDir.exists()) return@withContext Result.failure(Exception("Logs dir not found"))
val logDir = Env.logDir

val logFiles = logDir
.listFiles { file -> file.extension == "log" }
Expand Down
5 changes: 2 additions & 3 deletions app/src/main/java/to/bitkit/repositories/WalletRepo.kt
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@ import org.lightningdevkit.ldknode.Event
import to.bitkit.data.AppDb
import to.bitkit.data.CacheStore
import to.bitkit.data.SettingsStore
import to.bitkit.data.backup.VssStoreIdProvider
import to.bitkit.data.entities.TagMetadataEntity
import to.bitkit.data.keychain.Keychain
import to.bitkit.di.BgDispatcher
Expand Down Expand Up @@ -51,7 +50,6 @@ class WalletRepo @Inject constructor(
private val lightningRepo: LightningRepo,
private val cacheStore: CacheStore,
private val deriveBalanceStateUseCase: DeriveBalanceStateUseCase,
private val vssStoreIdProvider: VssStoreIdProvider,
private val backupRepo: BackupRepo,
) {
private val repoScope = CoroutineScope(bgDispatcher + SupervisorJob())
Expand Down Expand Up @@ -249,7 +247,8 @@ class WalletRepo @Inject constructor(
settingsStore.reset()
cacheStore.reset()
// TODO CLEAN ACTIVITY'S AND UPDATE STATE. CHECK ActivityListViewModel.removeAllActivities
coreService.activity.removeAll()
coreService.wipeData()
Logger.resetSession()
setWalletExistsState()

return@withContext lightningRepo.wipeStorage(walletIndex = walletIndex)
Expand Down
37 changes: 28 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,7 @@ package to.bitkit.services

import com.synonym.bitkitcore.Activity
import com.synonym.bitkitcore.ActivityFilter
import com.synonym.bitkitcore.ActivityTags
import com.synonym.bitkitcore.BtOrderState2
import com.synonym.bitkitcore.CJitStateEnum
import com.synonym.bitkitcore.ClosedChannelDetails
Expand All @@ -28,6 +29,7 @@ import com.synonym.bitkitcore.estimateOrderFeeFull
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,10 +46,10 @@ 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 com.synonym.bitkitcore.wipeAllDatabases
import io.ktor.client.HttpClient
import io.ktor.client.request.get
import io.ktor.http.HttpStatusCode
Expand Down Expand Up @@ -167,6 +169,19 @@ class CoreService @Inject constructor(

return Pair(geoBlocked, shouldBlockLightningReceive)
}

suspend fun wipeData(): Result<Unit> = ServiceQueue.CORE.background {
runCatching {
val result = wipeAllDatabases()
Logger.info("Core DB wipe: $result", context = TAG)
}.onFailure { e ->
Logger.error("Core DB wipe error", e, context = TAG)
}
}

companion object {
private const val TAG = "CoreService"
}
}

// endregion
Expand Down Expand Up @@ -215,14 +230,6 @@ class ActivityService(
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 +292,18 @@ class ActivityService(
}
}

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
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,6 @@ import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import to.bitkit.ui.theme.AppThemeSurface
import to.bitkit.ui.theme.Colors
import to.bitkit.utils.bip39Words

@Composable
fun MnemonicWordsGrid(
Expand Down Expand Up @@ -98,12 +97,14 @@ private fun WordItem(
}
}

private val previewWords = List(8) { "word${it + 1}" }

@Preview
@Composable
private fun Preview() {
AppThemeSurface {
MnemonicWordsGrid(
actualWords = bip39Words.take(n = 12),
actualWords = previewWords,
showMnemonic = true,
)
}
Expand All @@ -114,7 +115,7 @@ private fun Preview() {
private fun PreviewHidden() {
AppThemeSurface {
MnemonicWordsGrid(
actualWords = bip39Words.take(n = 12),
actualWords = previewWords,
showMnemonic = false,
)
}
Expand Down
Loading