Skip to content

Commit eb2fe0d

Browse files
committed
ai: agent control interface
1 parent a60a10b commit eb2fe0d

4 files changed

Lines changed: 225 additions & 1 deletion

File tree

.claude/plugins/blocktank-api/skills/lsp/SKILL.md

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,36 @@ On HTTP errors (4xx/5xx), the script prints the status code to stderr and the er
121121
2. **Close channel**`POST /regtest/channel/close` with `fundingTxId`, `vout`, and `forceCloseAfterSec: 0` for immediate close
122122
3. **Mine blocks**`POST /regtest/chain/mine` with `count: 6` to finalize the closure
123123

124+
### Workflow D: Automated Invoice Payments
125+
126+
Bulk-create and pay invoices to populate the app with payment activity.
127+
128+
**Prerequisites:** Dev debug build installed, wallet set up, LDK node running, open channel with inbound capacity, ADB connected.
129+
130+
**Run with defaults** (21 invoices of 1..21 sats, mine 150 blocks in batches of 10):
131+
132+
```bash
133+
"${CLAUDE_PLUGIN_ROOT}/skills/lsp/scripts/pay-invoices.sh"
134+
```
135+
136+
**Custom parameters** via env vars:
137+
138+
```bash
139+
INVOICE_COUNT=10 DESCRIPTION="test-ovi-{i}" MINE_TOTAL=60 MINE_BATCH=10 \
140+
"${CLAUDE_PLUGIN_ROOT}/skills/lsp/scripts/pay-invoices.sh"
141+
```
142+
143+
- `DESCRIPTION` — invoice description; `{i}` is replaced with the invoice index (default: `dev-payment-{i}`)
144+
145+
The script uses the `DevToolsProvider` ContentProvider (dev builds only) to create invoices on the app's LDK node via `adb shell content call`, then pays each via the LSP's `POST /regtest/channel/pay` endpoint.
146+
147+
**Create a single invoice manually:**
148+
149+
```bash
150+
adb shell "content call --uri content://to.bitkit.dev.devtools \
151+
--method createInvoice --arg '{\"amount\":1000,\"description\":\"test\"}'"
152+
```
153+
124154
## State Machines
125155

126156
### Order States (`state2`)
Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
#!/usr/bin/env bash
2+
set -euo pipefail
3+
4+
# Automated LN invoice creation + payment via Blocktank LSP
5+
#
6+
# Prerequisites:
7+
# - Dev debug build installed on device/emulator with wallet set up and LDK node running
8+
# - ADB connected to the device
9+
# - An open Lightning channel with sufficient inbound capacity
10+
#
11+
# Usage:
12+
# pay-invoices.sh
13+
# INVOICE_COUNT=10 pay-invoices.sh
14+
#
15+
# Each invoice amount equals its index (1 sat, 2 sats, ... N sats).
16+
# Invoices are created via the DevToolsProvider ContentProvider (dev builds only).
17+
#
18+
# Environment:
19+
# APP_ID App package (default: to.bitkit.dev)
20+
# INVOICE_COUNT Number of invoices to create and pay (default: 21)
21+
# DESCRIPTION Invoice description. Use {i} as index placeholder (default: dev-payment-{i})
22+
# MINE_TOTAL Total blocks to mine after payments (default: 150)
23+
# MINE_BATCH Blocks per mining call (default: 10)
24+
# PAY_DELAY Seconds between payments (default: 3)
25+
26+
APP_ID="${APP_ID:-to.bitkit.dev}"
27+
INVOICE_COUNT="${INVOICE_COUNT:-21}"
28+
DESCRIPTION="${DESCRIPTION:-dev-payment-{i}}"
29+
MINE_TOTAL="${MINE_TOTAL:-150}"
30+
MINE_BATCH="${MINE_BATCH:-10}"
31+
PAY_DELAY="${PAY_DELAY:-3}"
32+
33+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
34+
LSP="$SCRIPT_DIR/lsp.sh"
35+
AUTHORITY="$APP_ID.devtools"
36+
37+
create_invoice() {
38+
local amount="$1"
39+
local index="$2"
40+
local desc="${DESCRIPTION//\{i\}/$index}"
41+
local raw
42+
raw=$(adb shell "content call --uri content://$AUTHORITY \
43+
--method createInvoice \
44+
--arg '{\"amount\":$amount,\"description\":\"$desc\"}'")
45+
46+
# Extract JSON from Bundle output: Result: Bundle[{result={"bolt11":"..."}}]
47+
local json
48+
json=$(echo "$raw" | sed -n 's/.*result=\({.*}\).*/\1/p')
49+
50+
if [ -z "$json" ]; then
51+
echo "ERROR: Failed to parse result from: $raw" >&2
52+
return 1
53+
fi
54+
55+
local bolt11
56+
bolt11=$(echo "$json" | sed -n 's/.*"bolt11":"\([^"]*\)".*/\1/p')
57+
58+
if [ -z "$bolt11" ]; then
59+
local error
60+
error=$(echo "$json" | sed -n 's/.*"message":"\([^"]*\)".*/\1/p')
61+
echo "ERROR: ${error:-$json}" >&2
62+
return 1
63+
fi
64+
65+
echo "$bolt11"
66+
}
67+
68+
# Phase 1: Create and pay invoices
69+
echo "=== Creating and paying $INVOICE_COUNT invoices (1..$INVOICE_COUNT sats) ==="
70+
for i in $(seq 1 "$INVOICE_COUNT"); do
71+
echo ""
72+
echo "--- Invoice $i/$INVOICE_COUNT ($i sats) ---"
73+
74+
echo " Creating invoice..."
75+
invoice=$(create_invoice "$i" "$i") || exit 1
76+
echo " Invoice: ${invoice:0:30}..."
77+
78+
echo " Paying via LSP..."
79+
"$LSP" POST /regtest/channel/pay "{\"invoice\":\"$invoice\"}" > /dev/null
80+
echo " Paid."
81+
82+
sleep "$PAY_DELAY"
83+
done
84+
85+
echo ""
86+
echo "=== $INVOICE_COUNT invoices paid ==="
87+
88+
# Phase 2: Mine blocks
89+
batches=$((MINE_TOTAL / MINE_BATCH))
90+
if [ "$batches" -gt 0 ]; then
91+
echo ""
92+
echo "=== Mining $MINE_TOTAL blocks in $batches batches of $MINE_BATCH ==="
93+
for i in $(seq 1 "$batches"); do
94+
echo " Batch $i/$batches ($MINE_BATCH blocks)..."
95+
"$LSP" POST /regtest/chain/mine "{\"count\":$MINE_BATCH}" > /dev/null
96+
sleep 1
97+
done
98+
fi
99+
100+
echo ""
101+
echo "=== Done: $INVOICE_COUNT invoices paid, $((batches * MINE_BATCH)) blocks mined ==="

app/src/debug/AndroidManifest.xml

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,5 +4,11 @@
44

55
<application
66
android:usesCleartextTraffic="true"
7-
tools:ignore="MissingApplicationIcon" />
7+
tools:ignore="MissingApplicationIcon">
8+
9+
<provider
10+
android:name="to.bitkit.dev.DevToolsProvider"
11+
android:authorities="${applicationId}.devtools"
12+
android:exported="true" />
13+
</application>
814
</manifest>
Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
package to.bitkit.dev
2+
3+
import android.content.ContentProvider
4+
import android.content.ContentValues
5+
import android.net.Uri
6+
import android.os.Bundle
7+
import androidx.core.os.bundleOf
8+
import dagger.hilt.EntryPoint
9+
import dagger.hilt.InstallIn
10+
import dagger.hilt.android.EntryPointAccessors
11+
import dagger.hilt.components.SingletonComponent
12+
import kotlinx.serialization.Serializable
13+
import kotlinx.serialization.json.Json
14+
import to.bitkit.async.ServiceQueue
15+
import to.bitkit.repositories.LightningRepo
16+
17+
class DevToolsProvider : ContentProvider() {
18+
19+
@EntryPoint
20+
@InstallIn(SingletonComponent::class)
21+
interface Dependencies {
22+
fun lightningRepo(): LightningRepo
23+
}
24+
25+
private val deps: Dependencies by lazy {
26+
EntryPointAccessors.fromApplication(requireNotNull(context), Dependencies::class.java)
27+
}
28+
29+
override fun call(method: String, arg: String?, extras: Bundle?): Bundle = runCatching {
30+
val command = requireNotNull(DevCommand.parse(method, arg)) { "Unknown command: '$method'" }
31+
ServiceQueue.LDK.blocking { command.execute(deps) }
32+
}.getOrElse {
33+
DevResult.Error(it.message)
34+
}.toBundle()
35+
36+
override fun onCreate() = true
37+
override fun getType(uri: Uri): String? = null
38+
override fun insert(uri: Uri, values: ContentValues?): Uri? = null
39+
override fun delete(uri: Uri, sel: String?, args: Array<String>?) = 0
40+
override fun update(uri: Uri, values: ContentValues?, sel: String?, args: Array<String>?) = 0
41+
override fun query(uri: Uri, proj: Array<String>?, sel: String?, args: Array<String>?, sort: String?) = null
42+
}
43+
44+
private sealed interface DevCommand {
45+
46+
companion object {
47+
fun parse(method: String, arg: String?): DevCommand? = when (method) {
48+
CreateInvoice.METHOD -> CreateInvoice.parse(arg)
49+
else -> null
50+
}
51+
}
52+
53+
suspend fun execute(deps: DevToolsProvider.Dependencies): DevResult
54+
55+
data class CreateInvoice(val args: Args) : DevCommand {
56+
companion object {
57+
const val METHOD = "createInvoice"
58+
fun parse(arg: String?) = CreateInvoice(arg.deserialize<Args>())
59+
}
60+
61+
@Serializable
62+
data class Args(val amount: ULong? = null, val description: String = "dev-invoice")
63+
64+
override suspend fun execute(deps: DevToolsProvider.Dependencies) =
65+
deps.lightningRepo().createInvoice(args.amount, args.description).fold(
66+
onSuccess = { DevResult.Invoice(it) },
67+
onFailure = { DevResult.Error(it.message) },
68+
)
69+
}
70+
}
71+
72+
@Serializable
73+
private sealed interface DevResult {
74+
75+
companion object {
76+
private const val KEY_RESULT = "result"
77+
}
78+
79+
@Serializable data class Invoice(val bolt11: String) : DevResult
80+
81+
@Serializable data class Error(val message: String? = null) : DevResult
82+
83+
fun toBundle() = bundleOf(KEY_RESULT to Json.encodeToString(this))
84+
}
85+
86+
private inline fun <reified T> String?.deserialize(): T =
87+
if (isNullOrBlank()) Json.decodeFromString("{}") else Json.decodeFromString(this)

0 commit comments

Comments
 (0)