Skip to content
Merged
Show file tree
Hide file tree
Changes from 23 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
5 changes: 5 additions & 0 deletions app/src/main/java/to/bitkit/ext/Activities.kt
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,11 @@ fun Activity.rawId(): String = when (this) {
is Activity.Onchain -> v1.id
}

fun Activity.txType(): PaymentType = when (this) {
is Activity.Lightning -> v1.txType
is Activity.Onchain -> v1.txType
}

/**
* Calculates the total value of an activity based on its type.
*
Expand Down
31 changes: 31 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,31 @@
package to.bitkit.ext

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

// TODO use PreActivityMetadata
fun TagMetadataEntity.toActivityTagsMetadata() = PreActivityMetadata(
paymentId = id,
createdAt = createdAt.toULong(),
tags = tags,
paymentHash = paymentHash,
txId = txId,
address = address,
isReceive = isReceive,
feeRate = 0u,
isTransfer = false,
channelId = "",
)

fun PreActivityMetadata.toTagMetadataEntity() = TagMetadataEntity(
id = paymentId,
createdAt = createdAt.toLong(),
tags = tags,
paymentHash = paymentHash,
txId = txId,
address = address.orEmpty(),
isReceive = isReceive,
// feeRate = 0u,
// isTransfer = false,
// channelId = "",
)
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.ClosedChannelDetails
import com.synonym.bitkitcore.IBtInfo
import com.synonym.bitkitcore.IBtOrder
import com.synonym.bitkitcore.IcJitEntry
import com.synonym.bitkitcore.PreActivityMetadata
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<PreActivityMetadata>,
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>,
)
139 changes: 94 additions & 45 deletions app/src/main/java/to/bitkit/repositories/ActivityRepo.kt
Original file line number Diff line number Diff line change
@@ -1,12 +1,15 @@
package to.bitkit.repositories

import androidx.annotation.VisibleForTesting
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
import com.synonym.bitkitcore.PaymentState
import com.synonym.bitkitcore.PaymentType
import com.synonym.bitkitcore.PreActivityMetadata
import com.synonym.bitkitcore.SortDirection
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.TimeoutCancellationException
Expand All @@ -28,6 +31,7 @@
import to.bitkit.di.BgDispatcher
import to.bitkit.ext.amountOnClose
import to.bitkit.ext.matchesPaymentId
import to.bitkit.ext.nowMillis
import to.bitkit.ext.nowTimestamp
import to.bitkit.ext.rawId
import to.bitkit.models.ActivityBackupV1
Expand All @@ -53,53 +57,55 @@
) {
val isSyncingLdkNodePayments = MutableStateFlow(false)

private val _state = MutableStateFlow(ActivityState())
val state: StateFlow<ActivityState> = _state

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

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

suspend fun resetState() = withContext(bgDispatcher) {
_state.update { ActivityState() }
isSyncingLdkNodePayments.update { false }
notifyActivitiesChanged()
Logger.debug("Activity state reset", context = TAG)
}

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

return@withContext runCatching {
val result = runCatching {
withTimeout(SYNC_TIMEOUT_MS) {
Logger.debug("isSyncingLdkNodePayments = ${isSyncingLdkNodePayments.value}", context = TAG)
isSyncingLdkNodePayments.first { !it }
}

isSyncingLdkNodePayments.value = true
isSyncingLdkNodePayments.update { true }

deletePendingActivities()
return@withContext lightningRepo.getPayments()
.onSuccess { payments ->
Logger.debug("Got payments with success, syncing activities", context = TAG)
syncLdkNodePayments(payments = payments).onFailure { e ->
return@withContext Result.failure(e)
}
updateActivitiesMetadata()
syncTagsMetadata()
boostPendingActivities()
transferRepo.syncTransferStates()
isSyncingLdkNodePayments.value = false
return@withContext Result.success(Unit)
}.onFailure { e ->
Logger.error("Failed to sync ldk-node payments", e, context = TAG)
isSyncingLdkNodePayments.value = false
return@withContext Result.failure(e)
}.map { Unit }
}.onFailure { e ->
when (e) {
is TimeoutCancellationException -> {
isSyncingLdkNodePayments.value = false
Logger.warn("Timeout waiting for sync to complete, forcing reset", context = TAG)
}

else -> {
isSyncingLdkNodePayments.value = false
Logger.error("syncActivities error", e, context = TAG)
}
lightningRepo.getPayments().mapCatching { payments ->
Logger.debug("Got payments with success, syncing activities", context = TAG)
syncLdkNodePayments(payments).getOrThrow()
updateActivitiesMetadata()
syncTagsMetadata()
boostPendingActivities()
transferRepo.syncTransferStates()
getAllAvailableTags().map { }.getOrThrow()
}.getOrThrow()
}.onFailure { e ->
if (e is TimeoutCancellationException) {
Logger.warn("syncActivities timeout, forcing reset", context = TAG)
} else {
Logger.error("Failed to sync activities", e, context = TAG)
}
}

isSyncingLdkNodePayments.update { false }
notifyActivitiesChanged()

return@withContext result
}

/**
Expand Down Expand Up @@ -541,10 +547,11 @@
cacheStore.addActivityToPendingBoost(pendingBoostActivity)
}

/**
* Adds tags to an activity with business logic validation
*/
suspend fun addTagsToActivity(activityId: String, tags: List<String>): Result<Unit> = withContext(bgDispatcher) {
@VisibleForTesting
suspend fun addTagsToActivity(
activityId: String,
tags: List<String>,
): Result<Unit> = withContext(bgDispatcher) {
return@withContext runCatching {
checkNotNull(coreService.activity.getActivity(activityId)) { "Activity with ID $activityId not found" }

Expand Down Expand Up @@ -577,11 +584,9 @@
paymentHashOrTxId = paymentHashOrTxId,
type = type,
txType = txType
).onSuccess { activity ->
addTagsToActivity(activity.rawId(), tags = tags)
}.onFailure { e ->
return@withContext Result.failure(e)
}.map { Unit }
).mapCatching { activity ->
addTagsToActivity(activity.rawId(), tags = tags).getOrThrow()
}
}

/**
Expand Down Expand Up @@ -611,17 +616,49 @@
}
}

/**
* Gets all possible tags across all activities
*/
suspend fun getAllAvailableTags(): Result<List<String>> = withContext(bgDispatcher) {
return@withContext runCatching {
coreService.activity.allPossibleTags()
}.onSuccess { tags ->
_state.update { it.copy(tags = tags) }
}.onFailure { e ->
Logger.error("getAllAvailableTags error", e, context = TAG)
}
}

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

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

/**
* Upsert all [PreActivityMetadata]
*/
suspend fun upsertPreActivityMetadata(list: List<PreActivityMetadata>): Result<Unit> = withContext(bgDispatcher) {
return@withContext runCatching {
coreService.activity.upsertPreActivityMetadata(list)
}.onFailure { e ->
Logger.error("upsertPreActivityMetadata error", e, context = TAG)
}
}

suspend fun saveTagsMetadata(
id: String,
paymentHash: String? = null,
Expand Down Expand Up @@ -649,12 +686,20 @@
}
}

suspend fun restoreFromBackup(backup: ActivityBackupV1): Result<Unit> = withContext(bgDispatcher) {
suspend fun restoreFromBackup(payload: ActivityBackupV1): Result<Unit> = withContext(bgDispatcher) {
return@withContext runCatching {
coreService.activity.upsertList(backup.activities)
coreService.activity.upsertClosedChannelList(backup.closedChannels)
coreService.activity.upsertList(payload.activities)
coreService.activity.upsertTags(payload.activityTags)
coreService.activity.upsertClosedChannelList(payload.closedChannels)
}.onSuccess {
Logger.debug(
"Restored ${payload.activities.size} activities, ${payload.activityTags.size} activity tags, " +
"${payload.closedChannels.size} closed channels",
context = TAG,
)
}

}

Check warning

Code scanning / detekt

Detects blank lines before rbraces Warning

Unexpected blank line(s) before "}"

// MARK: - Development/Testing Methods

Expand Down Expand Up @@ -692,3 +737,7 @@
private const val TAG = "ActivityRepo"
}
}

data class ActivityState(
val tags: List<String> = emptyList(),
)
Loading
Loading