From 676c2638b053d05727cf5906288da4e65094ef60 Mon Sep 17 00:00:00 2001 From: Ovi Trif Date: Tue, 10 Feb 2026 13:37:09 +0100 Subject: [PATCH 1/6] fix: delete vss network graph using new ffi client --- AGENTS.md | 18 ++ .../to/bitkit/data/backup/VssBackupClient.kt | 4 + .../bitkit/data/backup/VssBackupClientLdk.kt | 98 +++++++++ .../to/bitkit/repositories/LightningRepo.kt | 37 +++- .../to/bitkit/services/LightningService.kt | 201 +++--------------- .../main/java/to/bitkit/utils/LogDumperLdk.kt | 167 +++++++++++++++ .../bitkit/data/backup/VssBackupClientTest.kt | 2 + .../bitkit/repositories/LightningRepoTest.kt | 27 +-- gradle/libs.versions.toml | 2 +- 9 files changed, 349 insertions(+), 207 deletions(-) create mode 100644 app/src/main/java/to/bitkit/data/backup/VssBackupClientLdk.kt create mode 100644 app/src/main/java/to/bitkit/utils/LogDumperLdk.kt diff --git a/AGENTS.md b/AGENTS.md index 915bbbc62..f602de264 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -214,6 +214,24 @@ suspend fun getData(): Result = withContext(Dispatchers.IO) { - ALWAYS add imports instead of inline fully-qualified names - PREFER to place `@Suppress()` annotations at the narrowest possible scope - ALWAYS wrap suspend functions in `withContext(bgDispatcher)` if in domain layer, using ctor injected prop `@BgDispatcher private val bgDispatcher: CoroutineDispatcher` +- ALWAYS position companion object at the top of the class + +### Device Debugging (adb) + +- App IDs per flavor: `to.bitkit.dev` (dev/regtest), `to.bitkit.tnet` (testnet), `to.bitkit` (mainnet) +- ALWAYS use `adb shell "run-as to.bitkit.dev ..."` to access the app's private data directory (debug builds only) +- App files root: `files/` (relative, inside `run-as` context) +- Key paths: + - `files/logs/` — app log files (e.g. `bitkit_2026-02-09_21-04-16.log`) + - `files/bitcoin/wallet0/ldk/` — LDK node storage (graph cache, dumps) + - `files/bitcoin/wallet0/core/` — bitkit-core storage + - `files/datastore/` — DataStore preferences and JSON stores +- To read a file: `adb shell "run-as to.bitkit.dev cat files/logs/bitkit_YYYY-MM-DD_HH-MM-SS.log"` +- To list files: `adb shell "run-as to.bitkit.dev ls -la files/logs/"` +- To find files: `adb shell "run-as to.bitkit.dev find files/ -name '*.log' -o -name '*.txt'"` +- ALWAYS download device files to `.ai/{name}_{timestamp}/` when needed for debugging (e.g. `.ai/logs_1770671066/`) +- To download: `adb shell "run-as to.bitkit.dev cat files/path/to/file" > .ai/folder_timestamp/filename` +- ALWAYS try reading device logs automatically via adb BEFORE asking user to provide log files ### Architecture Guidelines diff --git a/app/src/main/java/to/bitkit/data/backup/VssBackupClient.kt b/app/src/main/java/to/bitkit/data/backup/VssBackupClient.kt index 4d6c1151b..ead188fcd 100644 --- a/app/src/main/java/to/bitkit/data/backup/VssBackupClient.kt +++ b/app/src/main/java/to/bitkit/data/backup/VssBackupClient.kt @@ -30,7 +30,10 @@ class VssBackupClient @Inject constructor( @IoDispatcher private val ioDispatcher: CoroutineDispatcher, private val vssStoreIdProvider: VssStoreIdProvider, private val keychain: Keychain, + private val ldkFactory: VssBackupClientLdk.Factory, ) { + val ldk by lazy((LazyThreadSafetyMode.PUBLICATION)) { ldkFactory.create { isSetup.await() } } + private var isSetup = CompletableDeferred() private val setupMutex = Mutex() @@ -119,6 +122,7 @@ class VssBackupClient @Inject constructor( vssStoreIdProvider.clearCache() Logger.debug("VSS client reset", context = TAG) } + suspend fun putObject( key: String, data: ByteArray, diff --git a/app/src/main/java/to/bitkit/data/backup/VssBackupClientLdk.kt b/app/src/main/java/to/bitkit/data/backup/VssBackupClientLdk.kt new file mode 100644 index 000000000..5b1ae16ae --- /dev/null +++ b/app/src/main/java/to/bitkit/data/backup/VssBackupClientLdk.kt @@ -0,0 +1,98 @@ +package to.bitkit.data.backup + +import com.synonym.vssclient.KeyVersion +import com.synonym.vssclient.VssItem +import com.synonym.vssclient.vssDeleteLdk +import com.synonym.vssclient.vssGetLdk +import com.synonym.vssclient.vssListKeysLdk +import com.synonym.vssclient.vssListLdk +import com.synonym.vssclient.vssStoreLdk +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.withContext +import to.bitkit.di.IoDispatcher +import to.bitkit.utils.Logger + +class VssBackupClientLdk @AssistedInject constructor( + @IoDispatcher private val ioDispatcher: CoroutineDispatcher, + @Assisted private val awaitSetup: suspend () -> Unit, +) { + @AssistedFactory + interface Factory { + fun create(awaitSetup: suspend () -> Unit): VssBackupClientLdk + } + + suspend fun getObject(key: String): Result = withContext(ioDispatcher) { + awaitSetup() + Logger.verbose("VSS LDK 'getObject' call for '$key'", context = TAG) + runCatching { + vssGetLdk(key = key) + }.onSuccess { + if (it == null) { + Logger.verbose("VSS LDK 'getObject' success null for '$key'", context = TAG) + } else { + Logger.verbose("VSS LDK 'getObject' success for '$key'", context = TAG) + } + }.onFailure { + Logger.verbose("VSS LDK 'getObject' error for '$key'", it, context = TAG) + } + } + + suspend fun putObject(key: String, data: ByteArray): Result = withContext(ioDispatcher) { + awaitSetup() + Logger.verbose("VSS LDK 'putObject' call for '$key'", context = TAG) + runCatching { + vssStoreLdk(key = key, value = data) + }.onSuccess { + Logger.verbose("VSS LDK 'putObject' success for '$key' at version: '${it.version}'", context = TAG) + }.onFailure { + Logger.verbose("VSS LDK 'putObject' error for '$key'", it, context = TAG) + } + } + + suspend fun deleteObject(key: String): Result = withContext(ioDispatcher) { + awaitSetup() + Logger.verbose("VSS LDK 'deleteObject' call for '$key'", context = TAG) + runCatching { + vssDeleteLdk(key = key) + }.onSuccess { wasDeleted -> + if (wasDeleted) { + Logger.verbose("VSS LDK 'deleteObject' success for '$key' - key was deleted", context = TAG) + } else { + Logger.verbose("VSS LDK 'deleteObject' success for '$key' - key did not exist", context = TAG) + } + }.onFailure { + Logger.verbose("VSS LDK 'deleteObject' error for '$key'", it, context = TAG) + } + } + + suspend fun listKeys(): Result> = withContext(ioDispatcher) { + awaitSetup() + Logger.verbose("VSS LDK 'listKeys' call", context = TAG) + runCatching { + vssListKeysLdk() + }.onSuccess { + Logger.verbose("VSS LDK 'listKeys' success - found ${it.size} key(s)", context = TAG) + }.onFailure { + Logger.verbose("VSS LDK 'listKeys' error", it, context = TAG) + } + } + + suspend fun listItems(): Result> = withContext(ioDispatcher) { + awaitSetup() + Logger.verbose("VSS LDK 'listItems' call", context = TAG) + runCatching { + vssListLdk() + }.onSuccess { + Logger.verbose("VSS LDK 'listItems' success - found ${it.size} item(s)", context = TAG) + }.onFailure { + Logger.verbose("VSS LDK 'listItems' error", it, context = TAG) + } + } + + companion object { + private const val TAG = "VssBackupClientLdk" + } +} diff --git a/app/src/main/java/to/bitkit/repositories/LightningRepo.kt b/app/src/main/java/to/bitkit/repositories/LightningRepo.kt index 8c03b0443..e1617f754 100644 --- a/app/src/main/java/to/bitkit/repositories/LightningRepo.kt +++ b/app/src/main/java/to/bitkit/repositories/LightningRepo.kt @@ -248,6 +248,16 @@ class LightningRepo @Inject constructor( } } + private suspend fun dumpGraphToFile(walletIndex: Int, label: String) { + runCatching { + val outputDir = Env.ldkStoragePath(walletIndex) + val timestamp = System.currentTimeMillis() + lightningService.exportNetworkGraphToFile(outputDir, "graph_dump_${label}_$timestamp.txt") + }.onFailure { + Logger.warn("Failed to dump graph to file ($label)", it, context = TAG) + } + } + private suspend fun fetchTrustedPeers(): List? = runCatching { val info = coreService.blocktank.info(refresh = false) ?: coreService.blocktank.info(refresh = true) @@ -325,19 +335,34 @@ class LightningRepo @Inject constructor( updateGeoBlockState() refreshChannelCache() - // Validate network graph has trusted peers (RGS cache can become stale) - if (shouldValidateGraph && !lightningService.validateNetworkGraph()) { + if (shouldValidateGraph && !lightningService.validateNetworkGraphHasTrustedPeers()) { Logger.warn("Network graph is stale, resetting and restarting...", context = TAG) + + dumpGraphToFile(walletIndex, "before_reset") + + runCatching { + vssBackupClient.setup(walletIndex).getOrThrow() + val allKeys = vssBackupClient.listKeys().getOrThrow() + val allKeysStr = allKeys.map { "${it.key}@v${it.version}" } + Logger.info("VSS all keys before reset: $allKeysStr", context = TAG) + val ldkKeys = vssBackupClient.ldk.listKeys().getOrThrow() + val ldkKeysStr = ldkKeys.map { "${it.key}@v${it.version}" } + Logger.info("VSS LDK keys before reset: $ldkKeysStr", context = TAG) + }.onFailure { + Logger.warn("Failed to list VSS keys before reset", it, context = TAG) + } + lightningService.stop() lightningService.resetNetworkGraph(walletIndex) - // Also clear stale graph from VSS to prevent fallback restoration + runCatching { vssBackupClient.setup(walletIndex).getOrThrow() - vssBackupClient.deleteObject("network_graph").getOrThrow() - Logger.info("Cleared stale network graph from VSS", context = TAG) + vssBackupClient.ldk.deleteObject("network_graph").getOrThrow() + Logger.info("Cleared stale network graph from VSS (first delete)", context = TAG) }.onFailure { - Logger.warn("Failed to clear graph from VSS", it, context = TAG) + Logger.warn("Failed to clear graph from VSS (first delete)", it, context = TAG) } + _lightningState.update { it.copy(nodeLifecycleState = NodeLifecycleState.Stopped) } shouldRestartForGraphReset = true return@withLock Result.success(Unit) diff --git a/app/src/main/java/to/bitkit/services/LightningService.kt b/app/src/main/java/to/bitkit/services/LightningService.kt index 4df47a556..33973dafc 100644 --- a/app/src/main/java/to/bitkit/services/LightningService.kt +++ b/app/src/main/java/to/bitkit/services/LightningService.kt @@ -50,6 +50,7 @@ import to.bitkit.ext.uri import to.bitkit.models.OpenChannelResult import to.bitkit.utils.LdkError import to.bitkit.utils.LdkLogWriter +import to.bitkit.utils.LogDumperLdk import to.bitkit.utils.Logger import to.bitkit.utils.ServiceError import to.bitkit.utils.jsonLogOf @@ -69,6 +70,7 @@ class LightningService @Inject constructor( private val keychain: Keychain, private val vssStoreIdProvider: VssStoreIdProvider, private val settingsStore: SettingsStore, + private val logDumperLdk: LogDumperLdk, ) : BaseCoroutineScope(bgDispatcher) { @Volatile @@ -262,11 +264,6 @@ class LightningService @Inject constructor( Logger.info("LDK storage wiped", context = TAG) } - /** - * Resets the network graph cache, forcing a full RGS sync on next startup. - * This is useful when the cached graph is stale or missing nodes. - * Note: Node must be stopped before calling this. - */ fun resetNetworkGraph(walletIndex: Int) { if (node != null) throw ServiceError.NodeStillRunning() Logger.warn("Resetting network graph cache…", context = TAG) @@ -280,15 +277,12 @@ class LightningService @Inject constructor( } } - /** - * Validates that all trusted peers are present in the network graph. - * Returns false if all trusted peers are missing, indicating the graph cache is stale. - */ - fun validateNetworkGraph(): Boolean { + fun validateNetworkGraphHasTrustedPeers(): Boolean { val node = this.node ?: return true val graph = node.networkGraph() - val graphNodes = graph.listNodes().toSet() - if (graphNodes.isEmpty()) { + + val nodes = graph.listNodes().toSet() + if (nodes.isEmpty()) { val rgsTimestamp = node.status().latestRgsSnapshotTimestamp if (rgsTimestamp != null) { Logger.warn("Network graph is empty despite RGS timestamp $rgsTimestamp", context = TAG) @@ -297,23 +291,28 @@ class LightningService @Inject constructor( Logger.debug("Network graph is empty, skipping validation", context = TAG) return true } - val missingPeers = trustedPeers.filter { it.nodeId !in graphNodes } - if (missingPeers.size == trustedPeers.size) { + + val peersMissing = trustedPeers.filter { it.nodeId !in nodes } + if (peersMissing.size == trustedPeers.size) { + val rgsTimestamp = node.status().latestRgsSnapshotTimestamp Logger.warn( - "Network graph missing all ${trustedPeers.size} trusted peers", + "Network graph missing all ${trustedPeers.size} trusted peers " + + "(graphNodes=${nodes.size}, channels=${graph.listChannels().size}, " + + "rgsTimestamp=$rgsTimestamp)", context = TAG, ) return false } - if (missingPeers.isNotEmpty()) { + + if (peersMissing.isNotEmpty()) { Logger.debug( - "Network graph missing ${missingPeers.size}/${trustedPeers.size} trusted peers", + "Network graph missing ${peersMissing.size}/${trustedPeers.size} trusted peers", context = TAG, ) } - val presentCount = trustedPeers.size - missingPeers.size + val countMissing = trustedPeers.size - peersMissing.size Logger.debug( - "Network graph validated: $presentCount/${trustedPeers.size} trusted peers present", + "Network graph validated: $countMissing/${trustedPeers.size} trusted peers present", context = TAG, ) return true @@ -615,7 +614,7 @@ class LightningService @Inject constructor( } } }.onFailure { - dumpNetworkGraphInfo(bolt11) + logDumperLdk.dumpNetworkGraphInfo(node, trustedPeers, bolt11) }.getOrThrow() } @@ -866,138 +865,6 @@ class LightningService @Inject constructor( val payments: List? get() = node?.listPayments() // endregion - // region debug - @Suppress("LongMethod") - fun dumpNetworkGraphInfo(bolt11: String) { - val node = this.node ?: run { - Logger.error("Node not available for network graph dump", context = TAG) - return - } - val nodeIdPreviewLength = 20 - - val sb = StringBuilder() - sb.appendLine("\n\n=== ROUTE NOT FOUND - NETWORK GRAPH DUMP ===\n") - - // 1. Invoice Info - runCatching { - val invoice = Bolt11Invoice.fromStr(bolt11) - sb.appendLine("Invoice Info:") - sb.appendLine(" - Payment Hash: ${invoice.paymentHash()}") - sb.appendLine(" - Invoice: $bolt11") - }.getOrElse { - sb.appendLine("Failed to parse bolt11 invoice: $it") - } - - // 2. Our Node Info - sb.appendLine("\nOur Node Info:") - sb.appendLine(" - Node ID: ${node.nodeId()}") - - // 3. Our Channels - sb.appendLine("\nOur Channels:") - val channels = node.listChannels() - sb.appendLine(" Total channels: ${channels.size}") - - var totalOutboundMsat = 0UL - var totalInboundMsat = 0UL - var usableChannels = 0 - var announcedChannels = 0 - - channels.forEachIndexed { index, channel -> - totalOutboundMsat += channel.outboundCapacityMsat - totalInboundMsat += channel.inboundCapacityMsat - if (channel.isUsable) usableChannels++ - if (channel.isAnnounced) announcedChannels++ - - sb.appendLine(" Channel ${index + 1}:") - sb.appendLine(" - Channel ID: ${channel.channelId}") - sb.appendLine(" - Counterparty: ${channel.counterpartyNodeId}") - sb.appendLine( - " - Ready: ${channel.isChannelReady}, Usable: ${channel.isUsable}, " + - "Announced: ${channel.isAnnounced}" - ) - sb.appendLine( - " - Outbound: ${channel.outboundCapacityMsat} msat, " + - "Inbound: ${channel.inboundCapacityMsat} msat" - ) - } - - sb.appendLine("\n Channel Summary:") - sb.appendLine(" - Usable channels: $usableChannels/${channels.size}") - sb.appendLine(" - Announced channels: $announcedChannels/${channels.size}") - sb.appendLine(" - Total Outbound: $totalOutboundMsat msat") - sb.appendLine(" - Total Inbound: $totalInboundMsat msat") - - // 4. Our Peers - sb.appendLine("\nOur Peers:") - val peers = node.listPeers() - sb.appendLine(" Total peers: ${peers.size}") - - peers.forEachIndexed { index, peer -> - sb.appendLine(" Peer ${index + 1}: ${peer.nodeId.take(nodeIdPreviewLength)}... @ ${peer.address}") - sb.appendLine(" - Connected: ${peer.isConnected}, Persisted: ${peer.isPersisted}") - } - - // 5. RGS Configuration - sb.appendLine("\nRGS Configuration:") - sb.appendLine(" - RGS Server URL: ${Env.ldkRgsServerUrl ?: "Not configured"}") - - val nodeStatus = node.status() - nodeStatus.latestRgsSnapshotTimestamp?.let { rgsTimestamp -> - val date = java.util.Date(rgsTimestamp.toLong() * 1000) - val timeAgoMs = System.currentTimeMillis() - date.time - val hoursAgo = (timeAgoMs / 3600000).toInt() - val minutesAgo = ((timeAgoMs % 3600000) / 60000).toInt() - - sb.appendLine(" - Last RGS Snapshot: $date") - if (hoursAgo > 0) { - sb.appendLine(" - Time since update: ${hoursAgo}h ${minutesAgo}m ago") - } else { - sb.appendLine(" - Time since update: ${minutesAgo}m ago") - } - sb.appendLine(" - Timestamp: $rgsTimestamp") - } ?: run { - sb.appendLine(" - Last RGS Snapshot: Never synced") - sb.appendLine(" - WARNING: Network graph may be empty or stale!") - } - - // 6. Network Graph Data - sb.appendLine("\nRGS Network Graph Data:") - val networkGraph = node.networkGraph() - val allNodes = networkGraph.listNodes() - val allChannels = networkGraph.listChannels() - - sb.appendLine(" Total nodes: ${allNodes.size}") - sb.appendLine(" Total channels: ${allChannels.size}") - - // Check for trusted peers in graph - sb.appendLine("\n Checking for trusted peers in network graph:") - var foundTrustedNodes = 0 - trustedPeers.forEach { peer -> - val nodeId = peer.nodeId - if (allNodes.any { it == nodeId }) { - foundTrustedNodes++ - sb.appendLine(" OK: ${nodeId.take(nodeIdPreviewLength)}... found in graph") - } else { - sb.appendLine(" MISSING: ${nodeId.take(nodeIdPreviewLength)}... NOT in graph") - } - } - sb.appendLine(" Summary: $foundTrustedNodes/${trustedPeers.size} trusted peers found in graph") - - // Show first 10 nodes - val nodesToShow = minOf(10, allNodes.size) - sb.appendLine("\n First $nodesToShow nodes:") - allNodes.take(nodesToShow).forEachIndexed { index, nodeId -> - sb.appendLine(" ${index + 1}. $nodeId") - } - if (allNodes.size > nodesToShow) { - sb.appendLine(" ... and ${allNodes.size - nodesToShow} more nodes") - } - - sb.appendLine("\n=== END NETWORK GRAPH DUMP ===\n") - - Logger.info(sb.toString(), context = TAG) - } - fun getNetworkGraphInfo(): NetworkGraphInfo? { val node = this.node ?: return null @@ -1013,33 +880,13 @@ class LightningService @Inject constructor( }.getOrNull() } - suspend fun exportNetworkGraphToFile(outputDir: String): Result { + suspend fun exportNetworkGraphToFile( + outputDir: String, + fileName: String = "network_graph_nodes.txt", + ): Result { val node = this.node ?: return Result.failure(ServiceError.NodeNotSetup()) - - return withContext(bgDispatcher) { - runCatching { - val graph = node.networkGraph() - val nodes = graph.listNodes() - - val outputFile = File(outputDir, "network_graph_nodes.txt") - outputFile.bufferedWriter().use { writer -> - writer.write("Network Graph Nodes Export\n") - writer.write("Total nodes: ${nodes.size}\n") - writer.write("Exported at: ${System.currentTimeMillis()}\n") - writer.write("---\n") - nodes.forEachIndexed { index, nodeId -> - writer.write("${index + 1}. $nodeId\n") - } - } - - Logger.info("Exported ${nodes.size} nodes to ${outputFile.absolutePath}", context = TAG) - outputFile - }.onFailure { - Logger.error("Failed to export network graph to file", it, context = TAG) - } - } + return logDumperLdk.exportNetworkGraphToFile(node, outputDir, fileName) } - // endregion companion object { private const val TAG = "LightningService" diff --git a/app/src/main/java/to/bitkit/utils/LogDumperLdk.kt b/app/src/main/java/to/bitkit/utils/LogDumperLdk.kt new file mode 100644 index 000000000..16b1b8cca --- /dev/null +++ b/app/src/main/java/to/bitkit/utils/LogDumperLdk.kt @@ -0,0 +1,167 @@ +package to.bitkit.utils + +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.withContext +import org.lightningdevkit.ldknode.Bolt11Invoice +import org.lightningdevkit.ldknode.Node +import org.lightningdevkit.ldknode.PeerDetails +import to.bitkit.di.IoDispatcher +import to.bitkit.env.Env +import java.io.File +import javax.inject.Inject +import javax.inject.Singleton + +@Singleton +class LogDumperLdk @Inject constructor( + @IoDispatcher private val ioDispatcher: CoroutineDispatcher, +) { + companion object { + private const val TAG = "LogDumperLdk" + } + + @Suppress("LongMethod") + fun dumpNetworkGraphInfo(node: Node, trustedPeers: List, bolt11: String) { + val nodeIdPreviewLen = 20 + + val sb = StringBuilder() + sb.appendLine("\n\n=== ROUTE NOT FOUND - NETWORK GRAPH DUMP ===\n") + + runCatching { + val invoice = Bolt11Invoice.fromStr(bolt11) + sb.appendLine("Invoice Info:") + sb.appendLine(" - Payment Hash: ${invoice.paymentHash()}") + sb.appendLine(" - Invoice: $bolt11") + }.getOrElse { + sb.appendLine("Failed to parse bolt11 invoice: $it") + } + + sb.appendLine("\nOur Node Info:") + sb.appendLine(" - Node ID: ${node.nodeId()}") + + sb.appendLine("\nOur Channels:") + val channels = node.listChannels() + sb.appendLine(" Total channels: ${channels.size}") + + var totalOutboundMsat = 0UL + var totalInboundMsat = 0UL + var usableChannels = 0 + var announcedChannels = 0 + + channels.forEachIndexed { index, channel -> + totalOutboundMsat += channel.outboundCapacityMsat + totalInboundMsat += channel.inboundCapacityMsat + if (channel.isUsable) usableChannels++ + if (channel.isAnnounced) announcedChannels++ + + sb.appendLine(" Channel ${index + 1}:") + sb.appendLine(" - Channel ID: ${channel.channelId}") + sb.appendLine(" - Counterparty: ${channel.counterpartyNodeId}") + sb.appendLine( + " - Ready: ${channel.isChannelReady}, Usable: ${channel.isUsable}, " + + "Announced: ${channel.isAnnounced}" + ) + sb.appendLine( + " - Outbound: ${channel.outboundCapacityMsat} msat, " + + "Inbound: ${channel.inboundCapacityMsat} msat" + ) + } + + sb.appendLine("\n Channel Summary:") + sb.appendLine(" - Usable channels: $usableChannels/${channels.size}") + sb.appendLine(" - Announced channels: $announcedChannels/${channels.size}") + sb.appendLine(" - Total Outbound: $totalOutboundMsat msat") + sb.appendLine(" - Total Inbound: $totalInboundMsat msat") + + sb.appendLine("\nOur Peers:") + val peers = node.listPeers() + sb.appendLine(" Total peers: ${peers.size}") + + peers.forEachIndexed { index, peer -> + sb.appendLine(" Peer ${index + 1}: ${peer.nodeId.take(nodeIdPreviewLen)}... @ ${peer.address}") + sb.appendLine(" - Connected: ${peer.isConnected}, Persisted: ${peer.isPersisted}") + } + + sb.appendLine("\nRGS Configuration:") + sb.appendLine(" - RGS Server URL: ${Env.ldkRgsServerUrl ?: "Not configured"}") + + val nodeStatus = node.status() + nodeStatus.latestRgsSnapshotTimestamp?.let { rgsTimestamp -> + val date = java.util.Date(rgsTimestamp.toLong() * 1000) + val timeAgoMs = System.currentTimeMillis() - date.time + val hoursAgo = (timeAgoMs / 3600000).toInt() + val minutesAgo = ((timeAgoMs % 3600000) / 60000).toInt() + + sb.appendLine(" - Last RGS Snapshot: $date") + if (hoursAgo > 0) { + sb.appendLine(" - Time since update: ${hoursAgo}h ${minutesAgo}m ago") + } else { + sb.appendLine(" - Time since update: ${minutesAgo}m ago") + } + sb.appendLine(" - Timestamp: $rgsTimestamp") + } ?: run { + sb.appendLine(" - Last RGS Snapshot: Never synced") + sb.appendLine(" - WARNING: Network graph may be empty or stale!") + } + + sb.appendLine("\nRGS Network Graph Data:") + val networkGraph = node.networkGraph() + val allNodes = networkGraph.listNodes() + val allChannels = networkGraph.listChannels() + + sb.appendLine(" Total nodes: ${allNodes.size}") + sb.appendLine(" Total channels: ${allChannels.size}") + + sb.appendLine("\n Checking for trusted peers in network graph:") + var foundTrustedNodes = 0 + trustedPeers.forEach { peer -> + val nodeId = peer.nodeId + if (allNodes.any { it == nodeId }) { + foundTrustedNodes++ + sb.appendLine(" OK: ${nodeId.take(nodeIdPreviewLen)}... found in graph") + } else { + sb.appendLine(" MISSING: ${nodeId.take(nodeIdPreviewLen)}... NOT in graph") + } + } + sb.appendLine(" Summary: $foundTrustedNodes/${trustedPeers.size} trusted peers found in graph") + + val nodesToShow = minOf(10, allNodes.size) + sb.appendLine("\n First $nodesToShow nodes:") + allNodes.take(nodesToShow).forEachIndexed { index, nodeId -> + sb.appendLine(" ${index + 1}. $nodeId") + } + if (allNodes.size > nodesToShow) { + sb.appendLine(" ... and ${allNodes.size - nodesToShow} more nodes") + } + + sb.appendLine("\n=== END NETWORK GRAPH DUMP ===\n") + + Logger.info(sb.toString(), context = TAG) + } + + suspend fun exportNetworkGraphToFile( + node: Node, + outputDir: String, + fileName: String = "network_graph_nodes.txt", + ): Result = withContext(ioDispatcher) { + runCatching { + val graph = node.networkGraph() + val nodes = graph.listNodes() + + val outputFile = File(outputDir, fileName) + outputFile.bufferedWriter().use { writer -> + writer.write("Network Graph Nodes Export\n") + writer.write("Total nodes: ${nodes.size}\n") + writer.write("Exported at: ${System.currentTimeMillis()}\n") + writer.write("---\n") + nodes.forEachIndexed { index, nodeId -> + writer.write("${index + 1}. $nodeId\n") + } + } + + Logger.info("Exported ${nodes.size} nodes to ${outputFile.absolutePath}", context = TAG) + outputFile + }.onFailure { + Logger.error("Failed to export network graph to file", it, context = TAG) + } + } +} diff --git a/app/src/test/java/to/bitkit/data/backup/VssBackupClientTest.kt b/app/src/test/java/to/bitkit/data/backup/VssBackupClientTest.kt index 2c4d00216..2a3d58dce 100644 --- a/app/src/test/java/to/bitkit/data/backup/VssBackupClientTest.kt +++ b/app/src/test/java/to/bitkit/data/backup/VssBackupClientTest.kt @@ -19,6 +19,7 @@ class VssBackupClientTest : BaseUnitTest() { private val vssStoreIdProvider = mock() private val keychain = mock() + private val ldkFactory = mock() @Before fun setUp() = runBlocking { @@ -26,6 +27,7 @@ class VssBackupClientTest : BaseUnitTest() { ioDispatcher = testDispatcher, vssStoreIdProvider = vssStoreIdProvider, keychain = keychain, + ldkFactory = ldkFactory, ) } diff --git a/app/src/test/java/to/bitkit/repositories/LightningRepoTest.kt b/app/src/test/java/to/bitkit/repositories/LightningRepoTest.kt index 703263fe0..44eb903ad 100644 --- a/app/src/test/java/to/bitkit/repositories/LightningRepoTest.kt +++ b/app/src/test/java/to/bitkit/repositories/LightningRepoTest.kt @@ -70,8 +70,12 @@ class LightningRepoTest : BaseUnitTest() { @Before fun setUp() = runBlocking { + whenever(lightningService.setup(any(), anyOrNull(), anyOrNull(), anyOrNull(), anyOrNull())).thenReturn(Unit) + whenever(lightningService.start(anyOrNull(), any())).thenReturn(Unit) whenever(coreService.isGeoBlocked()).thenReturn(false) whenever(connectivityRepo.isOnline).thenReturn(MutableStateFlow(ConnectivityState.CONNECTED)) + whenever(settingsStore.data).thenReturn(flowOf(SettingsData())) + whenever(lightningService.validateNetworkGraphHasTrustedPeers()).thenReturn(true) sut = LightningRepo( bgDispatcher = testDispatcher, lightningService = lightningService, @@ -91,11 +95,7 @@ class LightningRepoTest : BaseUnitTest() { private suspend fun startNodeForTesting() { sut.setInitNodeLifecycleState() whenever(lightningService.node).thenReturn(mock()) - whenever(lightningService.setup(any(), anyOrNull(), anyOrNull(), anyOrNull(), anyOrNull())).thenReturn(Unit) - whenever(lightningService.start(anyOrNull(), any())).thenReturn(Unit) whenever(lightningService.sync()).thenReturn(Unit) - whenever(lightningService.validateNetworkGraph()).thenReturn(true) - whenever(settingsStore.data).thenReturn(flowOf(SettingsData())) val blocktank = mock() whenever(coreService.blocktank).thenReturn(blocktank) whenever(blocktank.info(any())).thenReturn(null) @@ -109,9 +109,6 @@ class LightningRepoTest : BaseUnitTest() { fun `start should transition through correct states`() = test { sut.setInitNodeLifecycleState() whenever(lightningService.node).thenReturn(mock()) - whenever(lightningService.setup(any(), anyOrNull(), anyOrNull(), anyOrNull(), anyOrNull())).thenReturn(Unit) - whenever(lightningService.start(anyOrNull(), any())).thenReturn(Unit) - whenever(lightningService.validateNetworkGraph()).thenReturn(true) val blocktank = mock() whenever(coreService.blocktank).thenReturn(blocktank) whenever(blocktank.info(any())).thenReturn(null) @@ -391,11 +388,7 @@ class LightningRepoTest : BaseUnitTest() { whenever(connectivityRepo.isOnline).thenReturn(MutableStateFlow(ConnectivityState.DISCONNECTED)) sut.setInitNodeLifecycleState() whenever(lightningService.node).thenReturn(mock()) - whenever(lightningService.setup(any(), anyOrNull(), anyOrNull(), anyOrNull(), anyOrNull())).thenReturn(Unit) - whenever(lightningService.start(anyOrNull(), any())).thenReturn(Unit) - whenever(lightningService.validateNetworkGraph()).thenReturn(true) whenever(lightningService.sync()).thenThrow(RuntimeException("Sync failed")) - whenever(settingsStore.data).thenReturn(flowOf(SettingsData())) val blocktank = mock() whenever(coreService.blocktank).thenReturn(blocktank) whenever(blocktank.info(any())).thenReturn(null) @@ -625,10 +618,6 @@ class LightningRepoTest : BaseUnitTest() { fun `start should load trusted peers from blocktank info`() = test { sut.setInitNodeLifecycleState() whenever(lightningService.node).thenReturn(null) - whenever(lightningService.setup(any(), anyOrNull(), anyOrNull(), anyOrNull(), anyOrNull())).thenReturn(Unit) - whenever(lightningService.start(anyOrNull(), any())).thenReturn(Unit) - whenever(lightningService.validateNetworkGraph()).thenReturn(true) - whenever(settingsStore.data).thenReturn(flowOf(SettingsData())) val blocktank = mock() whenever(coreService.blocktank).thenReturn(blocktank) @@ -670,10 +659,6 @@ class LightningRepoTest : BaseUnitTest() { fun `start should pass null trusted peers when blocktank returns null`() = test { sut.setInitNodeLifecycleState() whenever(lightningService.node).thenReturn(null) - whenever(lightningService.setup(any(), anyOrNull(), anyOrNull(), anyOrNull(), anyOrNull())).thenReturn(Unit) - whenever(lightningService.start(anyOrNull(), any())).thenReturn(Unit) - whenever(lightningService.validateNetworkGraph()).thenReturn(true) - whenever(settingsStore.data).thenReturn(flowOf(SettingsData())) val blocktank = mock() whenever(coreService.blocktank).thenReturn(blocktank) @@ -690,15 +675,11 @@ class LightningRepoTest : BaseUnitTest() { fun `start should not retry when node lifecycle state is Running`() = test { sut.setInitNodeLifecycleState() whenever(lightningService.node).thenReturn(null) - whenever(lightningService.setup(any(), anyOrNull(), anyOrNull(), anyOrNull(), anyOrNull())).thenReturn(Unit) - whenever(settingsStore.data).thenReturn(flowOf(SettingsData())) val blocktank = mock() whenever(coreService.blocktank).thenReturn(blocktank) whenever(blocktank.info(any())).thenReturn(null) // lightningService.start() succeeds (state becomes Running at line 241) - whenever(lightningService.start(anyOrNull(), any())).thenReturn(Unit) - whenever(lightningService.validateNetworkGraph()).thenReturn(true) // lightningService.nodeId throws during syncState() (called at line 244, AFTER state = Running) whenever(lightningService.nodeId).thenThrow(RuntimeException("error during syncState")) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 9962b072b..8d04acb5e 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -82,7 +82,7 @@ test-junit-ext = { module = "androidx.test.ext:junit", version = "1.3.0" } test-mockito-kotlin = { module = "org.mockito.kotlin:mockito-kotlin", version = "6.2.2" } test-robolectric = { module = "org.robolectric:robolectric", version = "4.16.1" } test-turbine = { group = "app.cash.turbine", name = "turbine", version = "1.2.1" } -vss-client = { module = "com.synonym:vss-client-android", version = "0.4.0" } +vss-client = { module = "com.synonym:vss-client-android", version = "0.5.1" } work-runtime-ktx = { module = "androidx.work:work-runtime-ktx", version = "2.11.0" } zxing = { module = "com.google.zxing:core", version = "3.5.4" } lottie = { module = "com.airbnb.android:lottie-compose", version = "6.7.1" } From f6343bb734835510692b2ad09460f5387ea165c6 Mon Sep 17 00:00:00 2001 From: Ovi Trif Date: Tue, 10 Feb 2026 14:52:43 +0100 Subject: [PATCH 2/6] chore: lint and cleanup --- .../to/bitkit/repositories/LightningRepo.kt | 24 ----------------- .../to/bitkit/services/LightningService.kt | 27 +++++++++++-------- .../to/bitkit/services/MigrationService.kt | 2 +- 3 files changed, 17 insertions(+), 36 deletions(-) diff --git a/app/src/main/java/to/bitkit/repositories/LightningRepo.kt b/app/src/main/java/to/bitkit/repositories/LightningRepo.kt index e1617f754..fa038a826 100644 --- a/app/src/main/java/to/bitkit/repositories/LightningRepo.kt +++ b/app/src/main/java/to/bitkit/repositories/LightningRepo.kt @@ -248,16 +248,6 @@ class LightningRepo @Inject constructor( } } - private suspend fun dumpGraphToFile(walletIndex: Int, label: String) { - runCatching { - val outputDir = Env.ldkStoragePath(walletIndex) - val timestamp = System.currentTimeMillis() - lightningService.exportNetworkGraphToFile(outputDir, "graph_dump_${label}_$timestamp.txt") - }.onFailure { - Logger.warn("Failed to dump graph to file ($label)", it, context = TAG) - } - } - private suspend fun fetchTrustedPeers(): List? = runCatching { val info = coreService.blocktank.info(refresh = false) ?: coreService.blocktank.info(refresh = true) @@ -338,20 +328,6 @@ class LightningRepo @Inject constructor( if (shouldValidateGraph && !lightningService.validateNetworkGraphHasTrustedPeers()) { Logger.warn("Network graph is stale, resetting and restarting...", context = TAG) - dumpGraphToFile(walletIndex, "before_reset") - - runCatching { - vssBackupClient.setup(walletIndex).getOrThrow() - val allKeys = vssBackupClient.listKeys().getOrThrow() - val allKeysStr = allKeys.map { "${it.key}@v${it.version}" } - Logger.info("VSS all keys before reset: $allKeysStr", context = TAG) - val ldkKeys = vssBackupClient.ldk.listKeys().getOrThrow() - val ldkKeysStr = ldkKeys.map { "${it.key}@v${it.version}" } - Logger.info("VSS LDK keys before reset: $ldkKeysStr", context = TAG) - }.onFailure { - Logger.warn("Failed to list VSS keys before reset", it, context = TAG) - } - lightningService.stop() lightningService.resetNetworkGraph(walletIndex) diff --git a/app/src/main/java/to/bitkit/services/LightningService.kt b/app/src/main/java/to/bitkit/services/LightningService.kt index 33973dafc..4f1365aa7 100644 --- a/app/src/main/java/to/bitkit/services/LightningService.kt +++ b/app/src/main/java/to/bitkit/services/LightningService.kt @@ -73,6 +73,11 @@ class LightningService @Inject constructor( private val logDumperLdk: LogDumperLdk, ) : BaseCoroutineScope(bgDispatcher) { + companion object { + private const val TAG = "LightningService" + private const val NODE_ID_PREVIEW_LEN = 20 + } + @Volatile var node: Node? = null @@ -292,11 +297,12 @@ class LightningService @Inject constructor( return true } - val peersMissing = trustedPeers.filter { it.nodeId !in nodes } - if (peersMissing.size == trustedPeers.size) { + val missing = trustedPeers.filter { it.nodeId !in nodes } + if (missing.size == trustedPeers.size) { val rgsTimestamp = node.status().latestRgsSnapshotTimestamp + val missingIds = missing.joinToString { it.nodeId.take(NODE_ID_PREVIEW_LEN) } Logger.warn( - "Network graph missing all ${trustedPeers.size} trusted peers " + + "Network graph missing all ${trustedPeers.size} trusted peers: [$missingIds] " + "(graphNodes=${nodes.size}, channels=${graph.listChannels().size}, " + "rgsTimestamp=$rgsTimestamp)", context = TAG, @@ -304,15 +310,18 @@ class LightningService @Inject constructor( return false } - if (peersMissing.isNotEmpty()) { + if (missing.isNotEmpty()) { + val ids = missing.joinToString { it.nodeId } Logger.debug( - "Network graph missing ${peersMissing.size}/${trustedPeers.size} trusted peers", + "Network graph missing ${missing.size}/${trustedPeers.size} trusted peers: [$ids]", context = TAG, ) } - val countMissing = trustedPeers.size - peersMissing.size + + val total = trustedPeers.size + val count = total - missing.size Logger.debug( - "Network graph validated: $countMissing/${trustedPeers.size} trusted peers present", + "Network graph validated: $count/$total trusted peers present", context = TAG, ) return true @@ -887,10 +896,6 @@ class LightningService @Inject constructor( val node = this.node ?: return Result.failure(ServiceError.NodeNotSetup()) return logDumperLdk.exportNetworkGraphToFile(node, outputDir, fileName) } - - companion object { - private const val TAG = "LightningService" - } } @Serializable diff --git a/app/src/main/java/to/bitkit/services/MigrationService.kt b/app/src/main/java/to/bitkit/services/MigrationService.kt index 676c38cbe..7456c8686 100644 --- a/app/src/main/java/to/bitkit/services/MigrationService.kt +++ b/app/src/main/java/to/bitkit/services/MigrationService.kt @@ -1446,7 +1446,7 @@ class MigrationService @Inject constructor( } } - @Suppress("LongMethod", "CyclomaticComplexMethod") + @Suppress("LongMethod", "CyclomaticComplexMethod", "NestedBlockDepth") suspend fun reapplyMetadataAfterSync() { loadPersistedMigrationData() From de50b85f459854b16d2f84a938b4f4ee12d83af1 Mon Sep 17 00:00:00 2001 From: Ovi Trif Date: Tue, 10 Feb 2026 20:31:12 +0100 Subject: [PATCH 3/6] chore: update `/pr` to use `.ai/pr` context --- .claude/commands/pr.md | 45 +++++++++++++++++++++++++++++++++++++----- 1 file changed, 40 insertions(+), 5 deletions(-) diff --git a/.claude/commands/pr.md b/.claude/commands/pr.md index 928aac570..2a5505ef9 100644 --- a/.claude/commands/pr.md +++ b/.claude/commands/pr.md @@ -1,5 +1,5 @@ --- -description: Create a PR on GitHub for the current branch +description: "Create a PR on GitHub, e.g. /pr --draft -- focus on the new wallet sync logic" argument_hint: "[branch] [--dry] [--draft] [-- instructions]" allowed_tools: Bash, Read, Glob, Grep, Write, AskUserQuestion, mcp__github__create_pull_request, mcp__github__list_pull_requests, mcp__github__get_file_contents, mcp__github__issue_read --- @@ -49,6 +49,15 @@ If no base branch argument provided, detect the repo's default branch: - If instructions reference a specific commit SHA (pattern like `commit [a-f0-9]{7,40}`): - Read full commit message: `git log -1 --format='%B' ` - Store instructions for use in description generation +- **Read instruction files from `.ai/pr/`:** + - Scan `.ai/pr/` for markdown files: `ls .ai/pr/*.md 2>/dev/null` + - Read each `.md` file found (using the Read tool) + - Treat their contents as supplementary instructions for description generation, merged with any `--` instructions + - These file-based instructions follow the same priority rules as custom `--` instructions (see Step 6, "Custom Instructions") +- **Scan for media files in `.ai/pr/`:** + - List non-markdown files: `ls .ai/pr/* 2>/dev/null | grep -vE '\.md$'` + - Supported types: `.png`, `.jpg`, `.jpeg`, `.gif`, `.mp4`, `.mov`, `.webm`, `.webp` + - Store the list of found media files with their paths for use in Preview section (Step 6) ### 4. Extract Linked Issues Scan commits for issue references: @@ -142,9 +151,22 @@ Example: **Preview Section (conditional):** Only include if the PR template (`.github/pull_request_template.md`) contains a `### Preview` heading: -- Create placeholders for media: `IMAGE_1`, `VIDEO_2`, etc. -- Add code comment under each placeholder describing what it should show -- Example: `` + +- **If media files were found in `.ai/pr/`:** + - For each media file, add a labeled markdown placeholder in the Preview section + - Use format: `` as a marker + - Add a brief description comment based on the filename (e.g., `pay2blink.mp4` -> "Pay to Blink flow recording") + - Example output: + ``` + ### Preview + + + ``` + +- **If no media files found in `.ai/pr/`:** + - Fall back to generated placeholders: `IMAGE_1`, `VIDEO_2`, etc. + - Add code comment under each placeholder describing what it should show + - Example: `` ### 7. Save PR Description Before creating the PR: @@ -159,6 +181,10 @@ gh pr create --base $base --title "..." --body "..." [--draft] ``` - Add `--draft` flag if draft mode selected - If actual PR number differs from predicted, rename the saved file +- **If media files exist in `.ai/pr/`:** + - Output the PR edit URL for easy access (append `/edit` to the PR URL) + - Instruct the user to drag-and-drop each media file into the Preview section via the GitHub web UI + - Note: GitHub does not support programmatic media upload to PR bodies ### 9. Output Summary @@ -185,7 +211,16 @@ Suggested reviewers: ``` **Media TODOs (only if Preview section was included):** -If the PR description includes a Preview section with media placeholders, append: +If the PR description includes a Preview section, append media action items: + +If media files were found in `.ai/pr/`: +``` +Media to upload (drag-and-drop into Preview section on GitHub): +- .ai/pr/pay2blink.mp4 +- .ai/pr/screenshot.png +``` + +If no media files were found (generated placeholders): ``` ## TODOs - [ ] IMAGE_1: [description] From f120581aa172c9b28d8d59503042df94580d4003 Mon Sep 17 00:00:00 2001 From: Ovi Trif Date: Tue, 10 Feb 2026 20:38:08 +0100 Subject: [PATCH 4/6] chore: update vss-rust-client-ffi --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 8d04acb5e..171368c1f 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -82,7 +82,7 @@ test-junit-ext = { module = "androidx.test.ext:junit", version = "1.3.0" } test-mockito-kotlin = { module = "org.mockito.kotlin:mockito-kotlin", version = "6.2.2" } test-robolectric = { module = "org.robolectric:robolectric", version = "4.16.1" } test-turbine = { group = "app.cash.turbine", name = "turbine", version = "1.2.1" } -vss-client = { module = "com.synonym:vss-client-android", version = "0.5.1" } +vss-client = { module = "com.synonym:vss-client-android", version = "0.5.2" } work-runtime-ktx = { module = "androidx.work:work-runtime-ktx", version = "2.11.0" } zxing = { module = "com.google.zxing:core", version = "3.5.4" } lottie = { module = "com.airbnb.android:lottie-compose", version = "6.7.1" } From b1693c04f3c4fa44825700dbcbbae809966bca61 Mon Sep 17 00:00:00 2001 From: Ovi Trif Date: Tue, 10 Feb 2026 20:47:21 +0100 Subject: [PATCH 5/6] chore: lint --- .../main/java/to/bitkit/data/backup/VssBackupClientLdk.kt | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/app/src/main/java/to/bitkit/data/backup/VssBackupClientLdk.kt b/app/src/main/java/to/bitkit/data/backup/VssBackupClientLdk.kt index 5b1ae16ae..07cb01529 100644 --- a/app/src/main/java/to/bitkit/data/backup/VssBackupClientLdk.kt +++ b/app/src/main/java/to/bitkit/data/backup/VssBackupClientLdk.kt @@ -19,6 +19,10 @@ class VssBackupClientLdk @AssistedInject constructor( @IoDispatcher private val ioDispatcher: CoroutineDispatcher, @Assisted private val awaitSetup: suspend () -> Unit, ) { + companion object { + private const val TAG = "VssBackupClientLdk" + } + @AssistedFactory interface Factory { fun create(awaitSetup: suspend () -> Unit): VssBackupClientLdk @@ -91,8 +95,4 @@ class VssBackupClientLdk @AssistedInject constructor( Logger.verbose("VSS LDK 'listItems' error", it, context = TAG) } } - - companion object { - private const val TAG = "VssBackupClientLdk" - } } From e556b04ea4f732768b4fef1393d7fa8cc7618ae3 Mon Sep 17 00:00:00 2001 From: Ovi Trif Date: Tue, 10 Feb 2026 23:56:21 +0100 Subject: [PATCH 6/6] chore: update vss-rust-client-ffi Co-Authored-By: Claude Opus 4.6 --- .../bitkit/data/backup/VssBackupClientLdk.kt | 35 +++++++++++++------ gradle/libs.versions.toml | 2 +- 2 files changed, 26 insertions(+), 11 deletions(-) diff --git a/app/src/main/java/to/bitkit/data/backup/VssBackupClientLdk.kt b/app/src/main/java/to/bitkit/data/backup/VssBackupClientLdk.kt index 07cb01529..d35fe6295 100644 --- a/app/src/main/java/to/bitkit/data/backup/VssBackupClientLdk.kt +++ b/app/src/main/java/to/bitkit/data/backup/VssBackupClientLdk.kt @@ -1,6 +1,7 @@ package to.bitkit.data.backup import com.synonym.vssclient.KeyVersion +import com.synonym.vssclient.LdkNamespace import com.synonym.vssclient.VssItem import com.synonym.vssclient.vssDeleteLdk import com.synonym.vssclient.vssGetLdk @@ -28,11 +29,14 @@ class VssBackupClientLdk @AssistedInject constructor( fun create(awaitSetup: suspend () -> Unit): VssBackupClientLdk } - suspend fun getObject(key: String): Result = withContext(ioDispatcher) { + suspend fun getObject( + key: String, + namespace: LdkNamespace = LdkNamespace.Default, + ): Result = withContext(ioDispatcher) { awaitSetup() Logger.verbose("VSS LDK 'getObject' call for '$key'", context = TAG) runCatching { - vssGetLdk(key = key) + vssGetLdk(key = key, namespace = namespace) }.onSuccess { if (it == null) { Logger.verbose("VSS LDK 'getObject' success null for '$key'", context = TAG) @@ -44,11 +48,15 @@ class VssBackupClientLdk @AssistedInject constructor( } } - suspend fun putObject(key: String, data: ByteArray): Result = withContext(ioDispatcher) { + suspend fun putObject( + key: String, + data: ByteArray, + namespace: LdkNamespace = LdkNamespace.Default, + ): Result = withContext(ioDispatcher) { awaitSetup() Logger.verbose("VSS LDK 'putObject' call for '$key'", context = TAG) runCatching { - vssStoreLdk(key = key, value = data) + vssStoreLdk(key = key, value = data, namespace = namespace) }.onSuccess { Logger.verbose("VSS LDK 'putObject' success for '$key' at version: '${it.version}'", context = TAG) }.onFailure { @@ -56,11 +64,14 @@ class VssBackupClientLdk @AssistedInject constructor( } } - suspend fun deleteObject(key: String): Result = withContext(ioDispatcher) { + suspend fun deleteObject( + key: String, + namespace: LdkNamespace = LdkNamespace.Default, + ): Result = withContext(ioDispatcher) { awaitSetup() Logger.verbose("VSS LDK 'deleteObject' call for '$key'", context = TAG) runCatching { - vssDeleteLdk(key = key) + vssDeleteLdk(key = key, namespace = namespace) }.onSuccess { wasDeleted -> if (wasDeleted) { Logger.verbose("VSS LDK 'deleteObject' success for '$key' - key was deleted", context = TAG) @@ -72,11 +83,13 @@ class VssBackupClientLdk @AssistedInject constructor( } } - suspend fun listKeys(): Result> = withContext(ioDispatcher) { + suspend fun listKeys( + namespace: LdkNamespace = LdkNamespace.Default, + ): Result> = withContext(ioDispatcher) { awaitSetup() Logger.verbose("VSS LDK 'listKeys' call", context = TAG) runCatching { - vssListKeysLdk() + vssListKeysLdk(namespace = namespace) }.onSuccess { Logger.verbose("VSS LDK 'listKeys' success - found ${it.size} key(s)", context = TAG) }.onFailure { @@ -84,11 +97,13 @@ class VssBackupClientLdk @AssistedInject constructor( } } - suspend fun listItems(): Result> = withContext(ioDispatcher) { + suspend fun listItems( + namespace: LdkNamespace = LdkNamespace.Default, + ): Result> = withContext(ioDispatcher) { awaitSetup() Logger.verbose("VSS LDK 'listItems' call", context = TAG) runCatching { - vssListLdk() + vssListLdk(namespace = namespace) }.onSuccess { Logger.verbose("VSS LDK 'listItems' success - found ${it.size} item(s)", context = TAG) }.onFailure { diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 171368c1f..5c526def3 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -82,7 +82,7 @@ test-junit-ext = { module = "androidx.test.ext:junit", version = "1.3.0" } test-mockito-kotlin = { module = "org.mockito.kotlin:mockito-kotlin", version = "6.2.2" } test-robolectric = { module = "org.robolectric:robolectric", version = "4.16.1" } test-turbine = { group = "app.cash.turbine", name = "turbine", version = "1.2.1" } -vss-client = { module = "com.synonym:vss-client-android", version = "0.5.2" } +vss-client = { module = "com.synonym:vss-client-android", version = "0.5.5" } work-runtime-ktx = { module = "androidx.work:work-runtime-ktx", version = "2.11.0" } zxing = { module = "com.google.zxing:core", version = "3.5.4" } lottie = { module = "com.airbnb.android:lottie-compose", version = "6.7.1" }