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
45 changes: 40 additions & 5 deletions .claude/commands/pr.md
Original file line number Diff line number Diff line change
@@ -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
---
Expand Down Expand Up @@ -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' <commit_sha>`
- 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:
Expand Down Expand Up @@ -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: `<!-- VIDEO_1: Record the send flow by scanning a LN invoice and setting amount to 5000 sats -->`

- **If media files were found in `.ai/pr/`:**
- For each media file, add a labeled markdown placeholder in the Preview section
- Use format: `<!-- MEDIA: .ai/pr/filename.ext — Upload this file here via GitHub web UI -->` as a marker
- Add a brief description comment based on the filename (e.g., `pay2blink.mp4` -> "Pay to Blink flow recording")
- Example output:
```
### Preview

<!-- MEDIA: .ai/pr/pay2blink.mp4 — Upload this file here via GitHub web UI -->
```

- **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: `<!-- VIDEO_1: Record the send flow by scanning a LN invoice and setting amount to 5000 sats -->`

### 7. Save PR Description
Before creating the PR:
Expand All @@ -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

Expand All @@ -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]
Expand Down
18 changes: 18 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -214,6 +214,24 @@ suspend fun getData(): Result<Data> = 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

Expand Down
4 changes: 4 additions & 0 deletions app/src/main/java/to/bitkit/data/backup/VssBackupClient.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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<Unit>()
private val setupMutex = Mutex()

Expand Down Expand Up @@ -119,6 +122,7 @@ class VssBackupClient @Inject constructor(
vssStoreIdProvider.clearCache()
Logger.debug("VSS client reset", context = TAG)
}

suspend fun putObject(
key: String,
data: ByteArray,
Expand Down
113 changes: 113 additions & 0 deletions app/src/main/java/to/bitkit/data/backup/VssBackupClientLdk.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
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
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,
) {
companion object {
private const val TAG = "VssBackupClientLdk"
}

@AssistedFactory
interface Factory {
fun create(awaitSetup: suspend () -> Unit): VssBackupClientLdk
}

suspend fun getObject(
key: String,
namespace: LdkNamespace = LdkNamespace.Default,
): Result<VssItem?> = withContext(ioDispatcher) {
awaitSetup()
Logger.verbose("VSS LDK 'getObject' call for '$key'", context = TAG)
runCatching {
vssGetLdk(key = key, namespace = namespace)
}.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,
namespace: LdkNamespace = LdkNamespace.Default,
): Result<VssItem> = withContext(ioDispatcher) {
awaitSetup()
Logger.verbose("VSS LDK 'putObject' call for '$key'", context = TAG)
runCatching {
vssStoreLdk(key = key, value = data, namespace = namespace)
}.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,
namespace: LdkNamespace = LdkNamespace.Default,
): Result<Boolean> = withContext(ioDispatcher) {
awaitSetup()
Logger.verbose("VSS LDK 'deleteObject' call for '$key'", context = TAG)
runCatching {
vssDeleteLdk(key = key, namespace = namespace)
}.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(
namespace: LdkNamespace = LdkNamespace.Default,
): Result<List<KeyVersion>> = withContext(ioDispatcher) {
awaitSetup()
Logger.verbose("VSS LDK 'listKeys' call", context = TAG)
runCatching {
vssListKeysLdk(namespace = namespace)
}.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(
namespace: LdkNamespace = LdkNamespace.Default,
): Result<List<VssItem>> = withContext(ioDispatcher) {
awaitSetup()
Logger.verbose("VSS LDK 'listItems' call", context = TAG)
runCatching {
vssListLdk(namespace = namespace)
}.onSuccess {
Logger.verbose("VSS LDK 'listItems' success - found ${it.size} item(s)", context = TAG)
}.onFailure {
Logger.verbose("VSS LDK 'listItems' error", it, context = TAG)
}
}
}
13 changes: 7 additions & 6 deletions app/src/main/java/to/bitkit/repositories/LightningRepo.kt
Original file line number Diff line number Diff line change
Expand Up @@ -325,19 +325,20 @@ 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)

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)
Expand Down
Loading
Loading