Skip to content
Merged
5 changes: 4 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 @@ -16,7 +16,10 @@ interface TagMetadataDao {
suspend fun insert(tagMetadata: TagMetadataEntity)

@Upsert
suspend fun upsert(tagMetadata: TagMetadataEntity)
suspend fun upsert(entity: TagMetadataEntity)

@Upsert
suspend fun upsert(entities: List<TagMetadataEntity>)

@Query("SELECT * FROM tag_metadata")
fun observeAll(): Flow<List<TagMetadataEntity>>
Expand Down
3 changes: 3 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 @@ -17,6 +17,9 @@ interface TransferDao {
@Upsert
suspend fun upsert(transfer: TransferEntity)

@Upsert
suspend fun upsert(transfers: List<TransferEntity>)

@Update
suspend fun update(transfer: TransferEntity)

Expand Down
2 changes: 2 additions & 0 deletions app/src/main/java/to/bitkit/models/BackupPayloads.kt
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package to.bitkit.models

import com.synonym.bitkitcore.Activity
import com.synonym.bitkitcore.ClosedChannelDetails
import com.synonym.bitkitcore.IBtInfo
import com.synonym.bitkitcore.IBtOrder
import com.synonym.bitkitcore.IcJitEntry
Expand Down Expand Up @@ -38,4 +39,5 @@ data class ActivityBackupV1(
val version: Int = 1,
val createdAt: Long,
val activities: List<Activity>,
val closedChannels: List<ClosedChannelDetails>,
)
25 changes: 19 additions & 6 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.ClosedChannelDetails
import com.synonym.bitkitcore.IcJitEntry
import com.synonym.bitkitcore.LightningActivity
import com.synonym.bitkitcore.PaymentState
Expand Down Expand Up @@ -29,6 +30,7 @@ import to.bitkit.ext.amountOnClose
import to.bitkit.ext.matchesPaymentId
import to.bitkit.ext.nowTimestamp
import to.bitkit.ext.rawId
import to.bitkit.models.ActivityBackupV1
import to.bitkit.services.CoreService
import to.bitkit.utils.AddressChecker
import to.bitkit.utils.Logger
Expand Down Expand Up @@ -166,9 +168,6 @@ class ActivityRepo @Inject constructor(
}
}

/**
* Gets activities with specified filters
*/
suspend fun getActivities(
filter: ActivityFilter? = null,
txType: PaymentType? = null,
Expand Down Expand Up @@ -198,9 +197,6 @@ class ActivityRepo @Inject constructor(
}
}

/**
* Gets a specific activity by ID
*/
suspend fun getActivity(id: String): Result<Activity?> = withContext(bgDispatcher) {
return@withContext runCatching {
coreService.activity.getActivity(id)
Expand All @@ -209,6 +205,16 @@ class ActivityRepo @Inject constructor(
}
}

suspend fun getClosedChannels(
sortDirection: SortDirection = SortDirection.ASC,
): Result<List<ClosedChannelDetails>> = withContext(bgDispatcher) {
runCatching {
coreService.activity.closedChannels(sortDirection)
}.onFailure { e ->
Logger.error("Error getting closed channels (sortDirection=$sortDirection)", e, context = TAG)
}
}

/**
* Updates an activity
* @param forceUpdate use it if you want update a deleted activity
Expand Down Expand Up @@ -643,6 +649,13 @@ class ActivityRepo @Inject constructor(
}
}

suspend fun restoreFromBackup(backup: ActivityBackupV1): Result<Unit> = withContext(bgDispatcher) {
return@withContext runCatching {
coreService.activity.upsertList(backup.activities)
coreService.activity.upsertClosedChannelList(backup.closedChannels)
}
}

// MARK: - Development/Testing Methods

/**
Expand Down
42 changes: 13 additions & 29 deletions app/src/main/java/to/bitkit/repositories/BackupRepo.kt
Original file line number Diff line number Diff line change
Expand Up @@ -371,10 +371,12 @@ class BackupRepo @Inject constructor(

BackupCategory.ACTIVITY -> {
val activities = activityRepo.getActivities().getOrDefault(emptyList())
val closedChannels = activityRepo.getClosedChannels().getOrDefault(emptyList())

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

json.encodeToString(payload).toByteArray()
Expand All @@ -399,47 +401,29 @@ class BackupRepo @Inject constructor(
}
performRestore(BackupCategory.WALLET) { dataBytes ->
val parsed = json.decodeFromString<WalletBackupV1>(String(dataBytes))

parsed.transfers.forEach { transfer ->
db.transferDao().upsert(transfer)
}

db.transferDao().upsert(parsed.transfers)
Logger.debug("Restored ${parsed.transfers.size} transfers", context = TAG)
}
performRestore(BackupCategory.METADATA) { dataBytes ->
val parsed = json.decodeFromString<MetadataBackupV1>(String(dataBytes))

parsed.tagMetadata.forEach { entity ->
db.tagMetadataDao().upsert(entity)
}

db.tagMetadataDao().upsert(parsed.tagMetadata)
cacheStore.update { parsed.cache }

Logger.debug("Restored caches and ${parsed.tagMetadata.size} tags metadata", context = TAG)
Logger.debug("Restored caches and ${parsed.tagMetadata.size} tags metadata records", TAG)
}
performRestore(BackupCategory.BLOCKTANK) { dataBytes ->
val parsed = json.decodeFromString<BlocktankBackupV1>(String(dataBytes))

// TODO: Restore orders, CJIT entries, and info to bitkit-core using synonymdev/bitkit-core#46
// For now, trigger a refresh from the server to sync the data
blocktankRepo.refreshInfo()
blocktankRepo.refreshOrders()

Logger.debug(
"Triggered Blocktank refresh (${parsed.orders.size} orders," +
"${parsed.cjitEntries.size} CJIT entries," +
"info=${parsed.info != null} backed up)",
context = TAG,
)
blocktankRepo.restoreFromBackup(parsed).onSuccess {
Logger.debug("Restored ${parsed.orders.size} orders, ${parsed.cjitEntries.size} CJITs", TAG)
}
}
performRestore(BackupCategory.ACTIVITY) { dataBytes ->
val parsed = json.decodeFromString<ActivityBackupV1>(String(dataBytes))

parsed.activities.forEach { activity ->
activityRepo.upsertActivity(activity)
activityRepo.restoreFromBackup(parsed).onSuccess {
Logger.debug(
"Restored ${parsed.activities.size} activities, ${parsed.closedChannels.size} closed channels",
context = TAG,
)
}

Logger.debug("Restored ${parsed.activities.size} activities", context = TAG)
}

Logger.info("Full restore success", context = TAG)
Expand Down
22 changes: 22 additions & 0 deletions app/src/main/java/to/bitkit/repositories/BlocktankRepo.kt
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ import to.bitkit.data.CacheStore
import to.bitkit.di.BgDispatcher
import to.bitkit.env.Env
import to.bitkit.ext.nowTimestamp
import to.bitkit.models.BlocktankBackupV1
import to.bitkit.models.EUR_CURRENCY
import to.bitkit.services.CoreService
import to.bitkit.services.LightningService
Expand Down Expand Up @@ -374,6 +375,27 @@ class BlocktankRepo @Inject constructor(
_blocktankState.update { BlocktankState() }
}

suspend fun restoreFromBackup(backup: BlocktankBackupV1): Result<Unit> = withContext(bgDispatcher) {
return@withContext runCatching {
coreService.blocktank.upsertOrderList(backup.orders)
coreService.blocktank.upsertCjitList(backup.cjitEntries)
backup.info?.let { info ->
coreService.blocktank.setInfo(info)
}

// We don't refresh orders here because we rely on the polling mechanism.
// We also don't restore `paidOrders` the refresh interval uses restored paidOrderIds to rebuild the list.

_blocktankState.update {
it.copy(
orders = backup.orders,
cjitEntries = backup.cjitEntries,
info = backup.info,
)
}
}
}

companion object {
private const val TAG = "BlocktankRepo"
private const val DEFAULT_CHANNEL_EXPIRY_WEEKS = 6u
Expand Down
66 changes: 52 additions & 14 deletions app/src/main/java/to/bitkit/services/CoreService.kt
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import com.synonym.bitkitcore.Activity
import com.synonym.bitkitcore.ActivityFilter
import com.synonym.bitkitcore.BtOrderState2
import com.synonym.bitkitcore.CJitStateEnum
import com.synonym.bitkitcore.ClosedChannelDetails
import com.synonym.bitkitcore.CreateCjitOptions
import com.synonym.bitkitcore.CreateOrderOptions
import com.synonym.bitkitcore.FeeRates
Expand All @@ -26,6 +27,7 @@ import com.synonym.bitkitcore.deleteActivityById
import com.synonym.bitkitcore.estimateOrderFeeFull
import com.synonym.bitkitcore.getActivities
import com.synonym.bitkitcore.getActivityById
import com.synonym.bitkitcore.getAllClosedChannels
import com.synonym.bitkitcore.getAllUniqueTags
import com.synonym.bitkitcore.getCjitEntries
import com.synonym.bitkitcore.getInfo
Expand All @@ -39,7 +41,13 @@ import com.synonym.bitkitcore.refreshActiveOrders
import com.synonym.bitkitcore.removeTags
import com.synonym.bitkitcore.updateActivity
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 io.ktor.client.HttpClient
import io.ktor.client.request.get
import io.ktor.http.HttpStatusCode
Expand Down Expand Up @@ -199,10 +207,20 @@ class ActivityService(
}
}

suspend fun upsert(activity: Activity) {
ServiceQueue.CORE.background {
upsertActivity(activity)
}
suspend fun upsert(activity: Activity) = ServiceQueue.CORE.background {
upsertActivity(activity)
}

suspend fun upsertList(activities: List<Activity>) = ServiceQueue.CORE.background {
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? {
Expand Down Expand Up @@ -267,6 +285,12 @@ class ActivityService(
}
}

suspend fun closedChannels(
sortDirection: SortDirection,
): List<ClosedChannelDetails> = ServiceQueue.CORE.background {
getAllClosedChannels(sortDirection)
}

/**
* Maps all `PaymentDetails` from LDK Node to bitkit-core [Activity] records.
*
Expand Down Expand Up @@ -325,7 +349,7 @@ class ActivityService(
}
}

private suspend fun processBolt11(
private fun processBolt11(
kind: PaymentKind.Bolt11,
payment: PaymentDetails,
state: PaymentState,
Expand Down Expand Up @@ -421,6 +445,7 @@ class ActivityService(
confirmed = isConfirmed,
timestamp = timestamp,
isBoosted = false,
boostTxIds = emptyList(),
isTransfer = false,
doesExist = true,
confirmTimestamp = confirmedTimestamp,
Expand Down Expand Up @@ -471,11 +496,11 @@ class ActivityService(
)

repeat(count) { i ->
val isLightning = Random.Default.nextBoolean()
val isLightning = Random.nextBoolean()
val value = (1000..1_000_000).random().toULong()
val txTimestamp =
(timestamp.toLong() - (0..30L * 24 * 60 * 60).random()).toULong() // Random time in last 30 days
val txType = if (Random.Default.nextBoolean()) PaymentType.SENT else PaymentType.RECEIVED
val txType = if (Random.nextBoolean()) PaymentType.SENT else PaymentType.RECEIVED
val status = when ((0..10).random()) {
in 0..7 -> PaymentState.SUCCEEDED // 80% chance
8 -> PaymentState.PENDING // 10% chance
Expand All @@ -497,7 +522,7 @@ class ActivityService(
invoice = "lnbc$value",
message = possibleMessages.random(),
timestamp = txTimestamp,
preimage = if (Random.Default.nextBoolean()) "preimage$i" else null,
preimage = if (Random.nextBoolean()) "preimage$i" else null,
createdAt = txTimestamp,
updatedAt = txTimestamp
)
Expand All @@ -513,16 +538,17 @@ class ActivityService(
fee = (100..10_000).random().toULong(),
feeRate = (1..100).random().toULong(),
address = "bc1...$i",
confirmed = Random.Default.nextBoolean(),
confirmed = Random.nextBoolean(),
timestamp = txTimestamp,
isBoosted = Random.Default.nextBoolean(),
isTransfer = Random.Default.nextBoolean(),
isBoosted = Random.nextBoolean(),
boostTxIds = emptyList(),
isTransfer = Random.nextBoolean(),
doesExist = true,
confirmTimestamp = if (Random.Default.nextBoolean()) txTimestamp + 3600.toULong() else null,
channelId = if (Random.Default.nextBoolean()) "channel$i" else null,
confirmTimestamp = if (Random.nextBoolean()) txTimestamp + 3600.toULong() else null,
channelId = if (Random.nextBoolean()) "channel$i" else null,
transferTxId = null,
createdAt = txTimestamp,
updatedAt = txTimestamp
updatedAt = txTimestamp,
)
)
}
Expand Down Expand Up @@ -664,6 +690,18 @@ class BlocktankService(
}
}

suspend fun setInfo(info: IBtInfo) = ServiceQueue.CORE.background {
upsertInfo(info)
}

suspend fun upsertOrderList(orders: List<IBtOrder>) = ServiceQueue.CORE.background {
upsertOrders(orders)
}

suspend fun upsertCjitList(cjitEntries: List<IcJitEntry>) = ServiceQueue.CORE.background {
upsertCjitEntries(cjitEntries)
}

// MARK: - Regtest methods
suspend fun regtestMine(count: UInt = 1u) {
com.synonym.bitkitcore.regtestMine(count = count)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -693,6 +693,7 @@ private fun PreviewOnchain() {
confirmed = true,
timestamp = (System.currentTimeMillis() / 1000 - 3600).toULong(),
isBoosted = false,
boostTxIds = emptyList(),
isTransfer = false,
doesExist = true,
confirmTimestamp = (System.currentTimeMillis() / 1000).toULong(),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -361,6 +361,7 @@ private fun PreviewOnchain() {
confirmed = true,
timestamp = (System.currentTimeMillis() / 1000 - 3600).toULong(),
isBoosted = false,
boostTxIds = emptyList(),
isTransfer = false,
doesExist = true,
confirmTimestamp = (System.currentTimeMillis() / 1000).toULong(),
Expand Down
Loading