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

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

fun Activity.scopedId(): String = "${walletId()}:${rawId()}"

fun Activity.isFromHardwareWallet(): Boolean = walletId() != WalletScope.default

fun Activity.txType(): PaymentType = when (this) {
is Activity.Lightning -> v1.txType
is Activity.Onchain -> v1.txType
Expand Down
1 change: 1 addition & 0 deletions app/src/main/java/to/bitkit/models/HardwareWallet.kt
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ data class HwWalletBalance(
data class HwWalletReceivedTx(
val txid: String,
val sats: ULong,
val walletId: String,
)

sealed interface HwFundingAccount {
Expand Down
1 change: 0 additions & 1 deletion app/src/main/java/to/bitkit/models/KnownDevice.kt
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,5 @@ data class KnownDevice(
val xpubs: Map<String, String> = emptyMap(),
/** Bitkit-side funds label set by the user while pairing; null until renamed within Bitkit. */
val customLabel: String? = null,
/** Stable app-owned id for future wallet-scoped hardware activity metadata. */
val walletId: String = "",
)
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ data class NewTransactionSheetDetails(
val direction: NewTransactionSheetDirection,
val paymentHashOrTxId: String? = null,
val activityId: String? = null,
val activityWalletId: String? = null,
val sats: Long = 0,
val isLoadingDetails: Boolean = false,
) {
Expand Down
169 changes: 113 additions & 56 deletions app/src/main/java/to/bitkit/repositories/ActivityRepo.kt
Original file line number Diff line number Diff line change
Expand Up @@ -42,8 +42,10 @@ import to.bitkit.ext.matchesPaymentId
import to.bitkit.ext.nowMillis
import to.bitkit.ext.nowTimestamp
import to.bitkit.ext.rawId
import to.bitkit.ext.runSuspendCatching
import to.bitkit.models.ActivityBackupV1
import to.bitkit.models.PubkyPublicKeyFormat
import to.bitkit.models.WalletScope
import to.bitkit.services.CoreService
import to.bitkit.utils.AppError
import to.bitkit.utils.Logger
Expand Down Expand Up @@ -178,8 +180,10 @@ class ActivityRepo @Inject constructor(
private suspend fun findClosedChannelForTransaction(txid: String): String? =
coreService.activity.findClosedChannelForTransaction(txid, null)

suspend fun getOnchainActivityByTxId(txid: String): OnchainActivity? =
coreService.activity.getOnchainActivityByTxId(txid)
suspend fun getOnchainActivityByTxId(
txid: String,
walletId: String = WalletScope.default,
): OnchainActivity? = coreService.activity.getOnchainActivityByTxId(txid, walletId)

/**
* Checks if a transaction is inbound (received) by looking up the payment direction.
Expand Down Expand Up @@ -214,23 +218,31 @@ class ActivityRepo @Inject constructor(
notifyActivitiesChanged()
}

suspend fun syncHardwareOnchainActivity(activity: OnchainActivity): Result<Unit> = withContext(bgDispatcher) {
runCatching {
val existing = coreService.activity.getOnchainActivityByTxId(activity.txId) ?: return@runCatching
val confirmTimestamp = existing.confirmTimestamp ?: activity.confirmTimestamp ?: activity.timestamp
.takeIf { activity.confirmed }
val updated = existing.copy(
confirmed = existing.confirmed || activity.confirmed,
confirmTimestamp = confirmTimestamp,
doesExist = if (activity.confirmed) true else existing.doesExist,
fee = if (existing.fee == 0uL && activity.fee > 0uL) activity.fee else existing.fee,
updatedAt = maxOf(existing.updatedAt ?: 0uL, activity.updatedAt ?: activity.timestamp),
suspend fun persistHwSnapshot(
walletId: String,
activities: List<Activity>,
transactionDetails: List<BitkitCoreTransactionDetails>,
): Result<List<Activity>> = withContext(bgDispatcher) {
runSuspendCatching {
val persistedActivities = coreService.activity.replaceHwSnapshot(
walletId = walletId,
activities = activities,
transactionDetails = transactionDetails,
)
if (updated == existing) return@runCatching
coreService.activity.update(existing.id, Activity.Onchain(updated))
notifyActivitiesChanged()
persistedActivities
}.onFailure {
Logger.error("Failed to persist hardware activities for '$walletId'", it, context = TAG)
}
}

suspend fun deleteForWallet(walletId: String): Result<Unit> = withContext(bgDispatcher) {
runSuspendCatching {
val deleted = coreService.activity.deleteByWalletId(walletId)
notifyActivitiesChanged()
Logger.info("Deleted '$deleted' activities for hardware wallet '$walletId'", context = TAG)
}.onFailure {
Logger.error("Failed to sync hardware activity '${activity.txId}'", it, context = TAG)
Logger.error("Failed to delete activities for hardware wallet '$walletId'", it, context = TAG)
}
}

Expand Down Expand Up @@ -260,31 +272,50 @@ class ActivityRepo @Inject constructor(
return coreService.activity.shouldShowReceivedSheet(txid, value)
}

suspend fun isActivitySeen(activityId: String): Boolean {
return coreService.activity.isActivitySeen(activityId)
suspend fun isActivitySeen(
activityId: String,
walletId: String = WalletScope.default,
): Boolean {
return coreService.activity.isActivitySeen(activityId, walletId)
}

suspend fun markActivityAsSeen(activityId: String) {
coreService.activity.markActivityAsSeen(activityId)
suspend fun markActivityAsSeen(
activityId: String,
walletId: String = WalletScope.default,
) {
coreService.activity.markActivityAsSeen(activityId, walletId = walletId)
notifyActivitiesChanged()
}

suspend fun markOnchainActivityAsSeen(txid: String) {
coreService.activity.markOnchainActivityAsSeen(txid)
suspend fun markOnchainActivityAsSeen(
txid: String,
walletId: String = WalletScope.default,
) {
coreService.activity.markOnchainActivityAsSeen(txid, walletId = walletId)
notifyActivitiesChanged()
}

suspend fun getTransactionDetails(txid: String): Result<BitkitCoreTransactionDetails?> = runCatching {
coreService.activity.getTransactionDetails(txid)
suspend fun getTransactionDetails(
txid: String,
walletId: String = WalletScope.default,
): Result<BitkitCoreTransactionDetails?> = runSuspendCatching {
coreService.activity.getTransactionDetails(txid, walletId)
}

suspend fun getBoostTxDoesExist(boostTxIds: List<String>): Map<String, Boolean> {
return coreService.activity.getBoostTxDoesExist(boostTxIds)
suspend fun getBoostTxDoesExist(
boostTxIds: List<String>,
walletId: String = WalletScope.default,
): Map<String, Boolean> {
return coreService.activity.getBoostTxDoesExist(boostTxIds, walletId)
}

suspend fun isCpfpChildTransaction(txId: String): Boolean = coreService.activity.isCpfpChildTransaction(txId)
suspend fun isCpfpChildTransaction(
txId: String,
walletId: String = WalletScope.default,
): Boolean = coreService.activity.isCpfpChildTransaction(txId, walletId)

suspend fun getTxIdsInBoostTxIds(): Set<String> = coreService.activity.getTxIdsInBoostTxIds()
suspend fun getTxIdsInBoostTxIds(walletId: String = WalletScope.default): Set<String> =
coreService.activity.getTxIdsInBoostTxIds(walletId)

/**
* Gets a specific activity by payment hash or txID with retry logic
Expand Down Expand Up @@ -330,6 +361,7 @@ class ActivityRepo @Inject constructor(
}

suspend fun getActivities(
walletId: String? = WalletScope.default,
filter: ActivityFilter? = null,
txType: PaymentType? = null,
tags: List<String>? = null,
Expand All @@ -339,8 +371,18 @@ class ActivityRepo @Inject constructor(
limit: UInt? = null,
sortDirection: SortDirection? = null,
): Result<List<Activity>> = withContext(bgDispatcher) {
runCatching {
coreService.activity.get(filter, txType, tags, search, minDate, maxDate, limit, sortDirection)
runSuspendCatching {
coreService.activity.get(
walletId = walletId,
filter = filter,
txType = txType,
tags = tags,
search = search,
minDate = minDate,
maxDate = maxDate,
limit = limit,
sortDirection = sortDirection,
)
}.onFailure {
Logger.error(
"getActivities error. Parameters:" +
Expand All @@ -358,9 +400,12 @@ class ActivityRepo @Inject constructor(
}
}

suspend fun getActivity(id: String): Result<Activity?> = withContext(bgDispatcher) {
runCatching {
coreService.activity.getActivity(id)
suspend fun getActivity(
id: String,
walletId: String = WalletScope.default,
): Result<Activity?> = withContext(bgDispatcher) {
runSuspendCatching {
coreService.activity.getActivity(id, walletId)
}.onFailure {
Logger.error("getActivity error for ID: $id", it, context = TAG)
}
Expand Down Expand Up @@ -472,7 +517,7 @@ class ActivityRepo @Inject constructor(
}

private suspend fun getActivityByPaymentId(forPaymentId: String): Activity? =
coreService.activity.getActivity(forPaymentId)
coreService.activity.getActivity(forPaymentId, WalletScope.default)
?: getOnchainActivityByTxId(forPaymentId)?.let { Activity.Onchain(it) }

private fun Activity.withContact(normalizedKey: String?, updatedAt: ULong): Activity = when (this) {
Expand Down Expand Up @@ -530,7 +575,7 @@ class ActivityRepo @Inject constructor(
activity = activity
).onSuccess {
Logger.debug("Activity $id updated with success. new data: $activity", context = TAG)
val tags = coreService.activity.tags(activityIdToDelete)
val tags = coreService.activity.tags(activityIdToDelete, WalletScope.default)
addTagsToActivity(activityId = id, tags = tags)
}.onFailure {
Logger.error(
Expand Down Expand Up @@ -593,15 +638,15 @@ class ActivityRepo @Inject constructor(
}.awaitAll()
}

suspend fun deleteActivity(id: String): Result<Unit> = withContext(bgDispatcher) {
runCatching {
val deleted = coreService.activity.delete(id)
if (deleted) {
cacheStore.addActivityToDeletedList(id)
notifyActivitiesChanged()
} else {
return@withContext Result.failure(Exception("Activity not deleted"))
}
suspend fun deleteActivity(
id: String,
walletId: String = WalletScope.default,
): Result<Unit> = withContext(bgDispatcher) {
runSuspendCatching {
val deleted = coreService.activity.delete(id, walletId)
check(deleted) { "Activity not deleted" }
cacheStore.addActivityToDeletedList(id)
notifyActivitiesChanged()
}.onFailure {
Logger.error("deleteActivity error for ID: $id", it, context = TAG)
}
Expand Down Expand Up @@ -648,7 +693,7 @@ class ActivityRepo @Inject constructor(
runCatching {
requireNotNull(cjitEntry)
val id = channel.fundingTxo?.txid.orEmpty()
if (coreService.activity.getActivity(id) != null) {
if (coreService.activity.getActivity(id, WalletScope.default) != null) {
Logger.debug("Skipping CJIT activity insert: already exists for '$id'", context = TAG)
return@runCatching false
}
Expand Down Expand Up @@ -687,15 +732,18 @@ class ActivityRepo @Inject constructor(
suspend fun addTagsToActivity(
activityId: String,
tags: List<String>,
walletId: String = WalletScope.default,
): Result<Unit> = withContext(bgDispatcher) {
runCatching {
checkNotNull(coreService.activity.getActivity(activityId)) { "Activity with ID $activityId not found" }
runSuspendCatching {
checkNotNull(coreService.activity.getActivity(activityId, walletId)) {
"Activity with ID $activityId not found"
}

val existingTags = coreService.activity.tags(activityId)
val existingTags = coreService.activity.tags(activityId, walletId)
val newTags = tags.filter { it.isNotBlank() && it !in existingTags }

if (newTags.isNotEmpty()) {
coreService.activity.appendTags(activityId, newTags).getOrThrow()
coreService.activity.appendTags(activityId, newTags, walletId).getOrThrow()
notifyActivitiesChanged()
Logger.info("Added ${newTags.size} new tags to activity $activityId", context = TAG)
} else {
Expand Down Expand Up @@ -729,12 +777,18 @@ class ActivityRepo @Inject constructor(
/**
* Removes tags from an activity
*/
suspend fun removeTagsFromActivity(activityId: String, tags: List<String>): Result<Unit> =
suspend fun removeTagsFromActivity(
activityId: String,
tags: List<String>,
walletId: String = WalletScope.default,
): Result<Unit> =
withContext(bgDispatcher) {
runCatching {
checkNotNull(coreService.activity.getActivity(activityId)) { "Activity with ID $activityId not found" }
runSuspendCatching {
checkNotNull(coreService.activity.getActivity(activityId, walletId)) {
"Activity with ID $activityId not found"
}

coreService.activity.dropTags(activityId, tags)
coreService.activity.dropTags(activityId, tags, walletId)
notifyActivitiesChanged()
Logger.info("Removed ${tags.size} tags from activity $activityId", context = TAG)
}.onFailure {
Expand All @@ -745,9 +799,12 @@ class ActivityRepo @Inject constructor(
/**
* Gets all tags for an activity
*/
suspend fun getActivityTags(activityId: String): Result<List<String>> = withContext(bgDispatcher) {
runCatching {
coreService.activity.tags(activityId)
suspend fun getActivityTags(
activityId: String,
walletId: String = WalletScope.default,
): Result<List<String>> = withContext(bgDispatcher) {
runSuspendCatching {
coreService.activity.tags(activityId, walletId)
}.onFailure {
Logger.error("getActivityTags error for activity $activityId", it, context = TAG)
}
Expand Down
Loading
Loading