From 50597cabf9d679534c75f06182b7f7ac6d71db0e Mon Sep 17 00:00:00 2001 From: Ovi Trif Date: Fri, 20 Mar 2026 16:08:29 +0100 Subject: [PATCH 1/2] ai: lsp rest api agent skill --- .../blocktank-api/.claude-plugin/plugin.json | 4 + .claude/plugins/blocktank-api/README.md | 29 ++ .../skills/blocktank-api/SKILL.md | 166 ++++++ .../blocktank-api/references/api-reference.md | 472 ++++++++++++++++++ .../skills/blocktank-api/scripts/lsp.sh | 60 +++ README.md | 8 +- 6 files changed, 738 insertions(+), 1 deletion(-) create mode 100644 .claude/plugins/blocktank-api/.claude-plugin/plugin.json create mode 100644 .claude/plugins/blocktank-api/README.md create mode 100644 .claude/plugins/blocktank-api/skills/blocktank-api/SKILL.md create mode 100644 .claude/plugins/blocktank-api/skills/blocktank-api/references/api-reference.md create mode 100755 .claude/plugins/blocktank-api/skills/blocktank-api/scripts/lsp.sh diff --git a/.claude/plugins/blocktank-api/.claude-plugin/plugin.json b/.claude/plugins/blocktank-api/.claude-plugin/plugin.json new file mode 100644 index 000000000..d54015ed3 --- /dev/null +++ b/.claude/plugins/blocktank-api/.claude-plugin/plugin.json @@ -0,0 +1,4 @@ +{ + "name": "blocktank-api", + "description": "Interact with the Blocktank LSP API for Lightning channel testing during bitkit development." +} diff --git a/.claude/plugins/blocktank-api/README.md b/.claude/plugins/blocktank-api/README.md new file mode 100644 index 000000000..f47d7469d --- /dev/null +++ b/.claude/plugins/blocktank-api/README.md @@ -0,0 +1,29 @@ +# Blocktank API Plugin + +A Claude Code plugin that gives Claude knowledge of the full Blocktank LSP API, enabling it to autonomously create channels, fund them, mine blocks, pay invoices, and close channels during Blocktank LSP testing. + +## Usage + +Once installed, the skill auto-triggers when you mention things like: +- "mine blocks", "deposit sats", "pay invoice", "force close" +- "channel order", "CJIT", "blocktank", "LSP" + +Claude will use the `lsp.sh` script to make API calls directly. + +## Configuration + +The default API base URL is `https://api.stag0.blocktank.to/blocktank/api/v2` (staging). + +To override (e.g., for a local instance): + +```bash +export BLOCKTANK_API_URL=http://localhost:9000/api +``` + +## Cross-project reuse + +To use this plugin from another repo (e.g. bitkit-ios), symlink it into that project's `.claude/plugins/`: + +```bash +ln -s /path/to/bitkit-android/.claude/plugins/blocktank-api /path/to/other-repo/.claude/plugins/blocktank-api +``` diff --git a/.claude/plugins/blocktank-api/skills/blocktank-api/SKILL.md b/.claude/plugins/blocktank-api/skills/blocktank-api/SKILL.md new file mode 100644 index 000000000..2c811a9fe --- /dev/null +++ b/.claude/plugins/blocktank-api/skills/blocktank-api/SKILL.md @@ -0,0 +1,166 @@ +--- +name: blocktank-api +description: > + This skill should be used when the user asks to interact with the Blocktank LSP API, + "mine blocks", "deposit sats", "pay invoice", "force close a channel", "create a channel order", + "open a channel", "estimate fees", "create a CJIT channel", or mentions "blocktank", "regtest", + "LSP", or Lightning channel testing workflows during bitkit development. +version: 0.1.0 +--- + +# Blocktank LSP API + +Blocktank is the Lightning Service Provider (LSP) used by Bitkit. This skill provides full knowledge of its REST API and a utility script to call any endpoint from the command line. + +No authentication is required. All requests and responses use JSON. + +## Configuration + +**Default base URL:** `https://api.stag0.blocktank.to/blocktank/api/v2` (staging) + +Override with the `BLOCKTANK_API_URL` environment variable: +- Local instance: `http://localhost:9000/api` + +## API Script + +Call any endpoint using the bundled script: + +```bash +bash "${CLAUDE_PLUGIN_ROOT}/skills/blocktank-api/scripts/lsp.sh" [json_body] +``` + +Examples: + +```bash +# Get service info +bash "${CLAUDE_PLUGIN_ROOT}/skills/blocktank-api/scripts/lsp.sh" GET /info + +# Create a channel order +bash "${CLAUDE_PLUGIN_ROOT}/skills/blocktank-api/scripts/lsp.sh" POST /channels \ + '{"lspBalanceSat":100000,"channelExpiryWeeks":12}' + +# Mine 6 blocks +bash "${CLAUDE_PLUGIN_ROOT}/skills/blocktank-api/scripts/lsp.sh" POST /regtest/chain/mine \ + '{"count":6}' + +# Deposit to an address +bash "${CLAUDE_PLUGIN_ROOT}/skills/blocktank-api/scripts/lsp.sh" POST /regtest/chain/deposit \ + '{"address":"bcrt1q...","amountSat":500000}' +``` + +The script outputs raw JSON. Pipe to `jq` for formatting if needed. + +On HTTP errors (4xx/5xx), the script prints the status code to stderr and the error response body to stdout, then exits with code 1. + +## Endpoint Quick Reference + +### Service Info + +| Method | Path | Description | +|--------|------|-------------| +| GET | `/info` | Service info, LSP nodes, channel size limits, fee rates | + +### Channel Orders + +| Method | Path | Description | +|--------|------|-------------| +| POST | `/channels` | Create a channel order | +| GET | `/channels/:id` | Get order by ID | +| GET | `/channels?ids[]=` | Get multiple orders (1-50 IDs) | +| POST | `/channels/:id/open` | Open a paid channel | +| GET | `/channels/:id/min-0conf-tx-fee` | Get 0-conf fee window | +| POST | `/channels/estimate-fee` | Estimate order fee | +| POST | `/channels/estimate-fee-full` | Estimate fee with breakdown | + +### CJIT (Just-In-Time Channels) + +| Method | Path | Description | +|--------|------|-------------| +| POST | `/cjit` | Create a JIT channel entry | +| GET | `/cjit/:id` | Get CJIT entry status | + +### Gift + +| Method | Path | Description | +|--------|------|-------------| +| POST | `/gift/pay` | Pay a gift invoice | +| POST | `/gift/order` | Create a gift order | +| GET | `/gift/:id` | Get gift info | + +### Regtest Tools (regtest only) + +| Method | Path | Description | +|--------|------|-------------| +| POST | `/regtest/chain/mine` | Mine blocks (default: 1) | +| POST | `/regtest/chain/deposit` | Deposit sats to address (default: 100,000) | +| POST | `/regtest/channel/pay` | Pay a Lightning invoice | +| GET | `/regtest/channel/pay/:id` | Get payment status | +| POST | `/regtest/channel/close` | Force close a channel | + +## Common Workflows + +### Workflow A: Purchase a Channel + +1. **Get service info** — `GET /info` to retrieve LSP node pubkeys and channel size limits +2. **Create order** — `POST /channels` with `lspBalanceSat`, `channelExpiryWeeks`, and optional `clientBalanceSat` +3. **Extract payment info** — from response: `payment.onchain.address` (bitcoin address) and `feeSat` (amount to pay) +4. **Fund the order** — `POST /regtest/chain/deposit` with the payment address and fee amount +5. **Confirm payment** — `POST /regtest/chain/mine` with `count: 1` to mine a block +6. **Poll order status** — `GET /channels/:id` until `state2` becomes `paid` +7. **Open channel** — `POST /channels/:id/open` with the client's `connectionStringOrPubkey` +8. **Confirm channel** — `POST /regtest/chain/mine` with `count: 6` to fully confirm + +### Workflow B: CJIT Channel (Just-In-Time) + +1. **Get service info** — `GET /info` for node pubkeys and limits +2. **Create CJIT entry** — `POST /cjit` with `channelSizeSat`, `invoiceSat`, `nodeId`, `channelExpiryWeeks` +3. **Extract invoice** — from response: `invoice.request` (bolt11 invoice string) +4. **Client pays invoice** — the mobile app pays the invoice, triggering automatic channel opening +5. **Poll status** — `GET /cjit/:id` until `state` becomes `completed` + +### Workflow C: Force Close a Channel + +1. **Get order info** — `GET /channels/:id` to find `channel.fundingTx.id` and `channel.fundingTx.vout` +2. **Close channel** — `POST /regtest/channel/close` with `fundingTxId`, `vout`, and `forceCloseAfterSec: 0` for immediate close +3. **Mine blocks** — `POST /regtest/chain/mine` with `count: 6` to finalize the closure + +## State Machines + +### Order States (`state2`) + +``` +created → paid → executed + ↘ expired +``` + +- `created` — waiting for payment +- `paid` — payment confirmed, ready to open channel +- `executed` — channel opened successfully +- `expired` — order timed out + +### Payment States (`payment.state2`) + +``` +created → paid → refundAvailable → refunded + ↘ canceled +``` + +### Channel States (`channel.state`) + +``` +opening → open → closed +``` + +### CJIT States (`state`) + +``` +created → completed + ↘ expired + ↘ failed +``` + +## Detailed API Reference + +For full request/response schemas, field constraints, and error codes for every endpoint, consult: + +- **`references/api-reference.md`** — Complete API reference with all fields, types, defaults, and validation rules diff --git a/.claude/plugins/blocktank-api/skills/blocktank-api/references/api-reference.md b/.claude/plugins/blocktank-api/skills/blocktank-api/references/api-reference.md new file mode 100644 index 000000000..994c6ec54 --- /dev/null +++ b/.claude/plugins/blocktank-api/skills/blocktank-api/references/api-reference.md @@ -0,0 +1,472 @@ +# Blocktank LSP API Reference + +Complete reference for all Blocktank LSP HTTP API endpoints. + +Base URL: `https://api.stag0.blocktank.to/blocktank/api/v2` (staging) + +--- + +## Service Info + +### GET /info + +General information about the service, LSP nodes, and channel configuration limits. + +**Response:** + +```json +{ + "version": 2, + "versions": { + "http": "2.5.1", + "btc": "1.4.0", + "ln2": "1.25.0" + }, + "nodes": [ + { + "alias": "Blocktank", + "pubkey": "0296b2db342fcf87ea94d981757fdf4d3e545bd5cef4919f58b5d38dfdd73bf5c9", + "connectionStrings": [ + "0296b2db342fcf87ea94d981757fdf4d3e545bd5cef4919f58b5d38dfdd73bf5c9@172.19.0.2:9735" + ] + } + ], + "options": { + "minChannelSizeSat": 1000, + "maxChannelSizeSat": 3170000, + "minExpiryWeeks": 1, + "maxExpiryWeeks": 53, + "minPaymentConfirmations": 0, + "minHighRiskPaymentConfirmations": 1, + "max0ConfClientBalanceSat": 317000 + }, + "onchain": { + "feeRates": { + "fast": 54, + "mid": 50, + "slow": 49 + } + } +} +``` + +--- + +## Channel Orders + +### POST /channels + +Create a new channel order. + +**Request body:** + +| Field | Type | Required | Default | Description | +|-------|------|----------|---------|-------------| +| `lspBalanceSat` | integer | Yes | — | LSP-side balance. Min 20,000 sat. | +| `channelExpiryWeeks` | integer | Yes | — | Lease duration. Between minExpiryWeeks and maxExpiryWeeks from /info. | +| `clientBalanceSat` | integer | No | 0 | Client-side balance. Must be <= lspBalanceSat. | +| `zeroConf` | boolean | No | false | Turbo channel (0-conf channel open). | +| `zeroConfPayment` | boolean | No | null | Accept 0-conf onchain payment. Cannot use with clientBalanceSat. | +| `zeroReserve` | boolean | No | false | Zero channel reserve (dust limit). | +| `couponCode` | string | No | null | Discount code. Max 128 chars. | +| `discountCode` | string | No | null | Discount code (newer field). Max 128 chars. | +| `source` | string | No | null | Order source tracking. Max 128 chars. | +| `lspNodeId` | string | No | null | Specific LSP node pubkey. Must be from /info nodes list. | +| `clientNodeId` | string | No | null | Client node pubkey for compliance checks. | +| `signature` | string | No | null | Signature of `channelOpen-${timestamp}` by client node. | +| `timestamp` | string | No | null | ISO datetime used for signature. | +| `announceChannel` | boolean | No | false | Public channel. Cannot be true for zeroConf channels. | +| `refundOnchainAddress` | string | No | null | Refund address. Max 512 chars. | + +**Validation rules:** +- `lspBalanceSat + clientBalanceSat` must be between `minChannelSizeSat` and `maxChannelSizeSat` +- `zeroConfPayment` cannot be used when `clientBalanceSat > 0` +- If `signature` is provided, `clientNodeId` and `timestamp` must also be provided + +**Response (201):** Order object (see Order Schema below) + +**Errors:** 400 (validation error) + +### GET /channels/:id + +Get a single order by ID. + +**Path params:** `id` — UUID or 24-char hex legacy ID + +**Response (200):** Order object + +**Errors:** 404 (not found) + +### GET /channels?ids[]= + +Get multiple orders by IDs. + +**Query params:** `ids` — Array of UUIDs (1-50, no duplicates). Pass as `?ids[]=abc&ids[]=def` + +**Response (200):** Array of Order objects + +**Errors:** 404 (not found) + +### POST /channels/:id/open + +Open a channel for a paid order. + +**Path params:** `id` — UUID + +**Request body:** + +| Field | Type | Required | Description | +|-------|------|----------|-------------| +| `connectionStringOrPubkey` | string | Yes | `pubkey@host:port` or just `pubkey`. If pubkey only, client must have an active peer connection to the LSP. | +| `announceChannel` | boolean | No | Announce channel to network. Cannot be true for zeroConf. | + +**Response (200):** Updated Order object + +**Errors:** +- 400 — Order not in correct state +- 404 — Order not found +- 412 — Channel open failed (see ChannelOpenError) + +**Channel open error codes:** +- `WRONG_ORDER_STATE` — Order not in "paid" state +- `PEER_NOT_REACHABLE` — Cannot connect to peer +- `CHANNEL_REJECTED_BY_DESTINATION` — Peer rejected the channel +- `CHANNEL_REJECTED_BY_LSP` — LSP rejected the channel +- `BLOCKTANK_NOT_READY` — Service temporarily unavailable +- `UNKNOWN_ERROR` — Generic error + +### GET /channels/:id/min-0conf-tx-fee + +Get the minimum onchain fee for a 0-conf payment to be accepted. Valid for at least 2 minutes from time of calling. + +**Path params:** `id` — UUID + +**Response (200):** + +```json +{ + "id": "69ce39f6-4918-416e-9056-8dba678c8af2", + "satPerVByte": 24.3, + "validityEndsAt": "2023-07-28T07:39:00.342Z" +} +``` + +### POST /channels/estimate-fee + +Estimate channel order fee without creating an order. + +**Request body:** Same as POST /channels + +**Response (200):** + +```json +{ + "feeSat": 10192, + "min0ConfTxFee": { + "satPerVByte": 50.1, + "validityEndsAt": "2023-07-06T07:58:39.588Z" + } +} +``` + +### POST /channels/estimate-fee-full + +Estimate fee with network and service fee breakdown. + +**Request body:** Same as POST /channels + +**Response (200):** + +```json +{ + "feeSat": 10192, + "networkFeeSat": 5096, + "serviceFeeSat": 5096, + "min0ConfTxFee": { + "satPerVByte": 50.1, + "validityEndsAt": "2023-07-06T07:58:39.588Z" + } +} +``` + +--- + +## CJIT (Just-In-Time Channels) + +### POST /cjit + +Create a CJIT channel entry. The LSP creates a hold invoice; when paid, the channel opens automatically. + +**Request body:** + +| Field | Type | Required | Default | Description | +|-------|------|----------|---------|-------------| +| `channelSizeSat` | integer | Yes | — | Channel size. Must be >= invoiceSat. Between min/max from /info. | +| `invoiceSat` | integer | Yes | — | Invoice amount. Min 1. | +| `invoiceDescription` | string | No | "" | Invoice description. | +| `channelExpiryWeeks` | integer | Yes | — | Lease duration. Between min/max from /info. | +| `nodeId` | string | Yes | — | Pubkey of the node to open the channel to. | +| `couponCode` | string | No | null | Discount code. Max 128 chars. | +| `source` | string | No | null | Order source tracking. Max 128 chars. | +| `discountCode` | string | No | null | Discount code. Max 128 chars. | +| `zeroReserve` | boolean | No | false | Zero channel reserve. | + +**Response (201):** CJitEntry object (see CJitEntry Schema below) + +**Errors:** 400 (validation or compliance check failure) + +### GET /cjit/:id + +Get CJIT entry by ID. + +**Path params:** `id` — UUID + +**Response (200):** CJitEntry object + +**Errors:** 404 (not found) + +--- + +## Gift + +### POST /gift/pay + +Pay a gift invoice. + +**Request body:** + +| Field | Type | Required | Description | +|-------|------|----------|-------------| +| `invoice` | string | Yes | Valid bolt11 invoice string. | + +**Response (200):** Gift object + +**Errors:** 400 + +### POST /gift/order + +Create a gift order. + +**Request body:** + +| Field | Type | Required | Description | +|-------|------|----------|-------------| +| `clientNodeId` | string | Yes | Client node ID. | +| `code` | string | Yes | Gift code. | + +**Response (200):** Gift object + +**Errors:** 400 + +### GET /gift/:id + +Get gift by ID. + +**Path params:** `id` — UUID + +**Response (200):** Gift object + +**Errors:** 400 + +--- + +## Regtest Tools + +These endpoints are only available when the service is running on regtest network. + +### POST /regtest/chain/mine + +Mine blocks on the regtest chain. + +**Request body:** + +| Field | Type | Required | Default | Description | +|-------|------|----------|---------|-------------| +| `count` | integer | No | 1 | Number of blocks to mine. Min 1. | + +**Response (200):** Mining result + +### POST /regtest/chain/deposit + +Send satoshis to a regtest bitcoin address (faucet). + +**Request body:** + +| Field | Type | Required | Default | Description | +|-------|------|----------|---------|-------------| +| `address` | string | Yes | — | Regtest bitcoin address (must be a valid regtest address). | +| `amountSat` | integer | No | 100000 | Amount in satoshis. Min 1. | + +**Response (200):** Transaction ID string + +### POST /regtest/channel/pay + +Pay a Lightning invoice on regtest. + +**Request body:** + +| Field | Type | Required | Default | Description | +|-------|------|----------|---------|-------------| +| `invoice` | string | Yes | — | Valid regtest bolt11 invoice. | +| `amountSat` | integer | No | null | Amount for 0-amount invoices. Min 1. | + +**Response (200):** Payment ID string (UUID) + +### GET /regtest/channel/pay/:id + +Get payment status by ID. + +**Path params:** `id` — UUID (payment ID from POST /regtest/channel/pay) + +**Response (200):** Payment object with invoice state + +### POST /regtest/channel/close + +Force close a channel. + +**Request body:** + +| Field | Type | Required | Default | Description | +|-------|------|----------|---------|-------------| +| `fundingTxId` | string | Yes | — | Funding transaction ID from `channel.fundingTx.id`. | +| `vout` | integer | Yes | — | Output index from `channel.fundingTx.vout`. Min 0. | +| `forceCloseAfterSec` | integer | No | 86400 | Seconds before force close. Use 0 for immediate force close. | + +**Response (200):** Closing transaction ID string + +--- + +## Response Schemas + +### Order + +```json +{ + "id": "fa7e6d29-a04d-47ea-8db4-ec05f6b8601c", + "state": "open", + "state2": "executed", + "orderExiresAt": "2023-07-06T07:58:39.588Z", + "feeSat": 19021, + "lspBalanceSat": 3000000, + "clientBalanceSat": 0, + "channelExpiryWeeks": 12, + "channelExpiresAt": "2023-10-06T07:58:39.588Z", + "couponCode": "", + "zeroConf": false, + "zeroReserve": false, + "discountPercent": 0, + "lspNode": { + "alias": "Blocktank", + "pubkey": "0296b2db...", + "connectionStrings": ["0296b2db...@172.19.0.2:9735"] + }, + "channel": { + "state": "open", + "lspNodePubkey": "0296b2db...", + "clientNodePubkey": "0386b2db...", + "announceChannel": false, + "shortChannelId": "792906x599x1", + "fundingTx": { + "id": "fa205519e0eb80f84a6c234b1c7f5a2cc6995eb4d84b6345fab214097d79b38d", + "vout": 1 + }, + "closingTxId": null, + "close": null + }, + "payment": { + "state": "paid", + "state2": "paid", + "paidSat": 19021, + "bolt11Invoice": { + "request": "lntb1u1pwz5w78pp5...", + "state": "paid", + "amountSat": 19021, + "expiresAt": "2023-07-06T07:58:39.588Z", + "updatedAt": "2023-07-06T07:58:39.588Z" + }, + "onchain": { + "requiredConfirmations": 1, + "address": "bcrt1q66jwcerttp8jcu43mlvz0y93v8pf6lxh5xztq3", + "confirmedSat": 19021, + "transactions": [ + { + "amountSat": 19021, + "txId": "fa205519...", + "vout": 0, + "blockHeight": 600100, + "blockConfirmations": 3, + "feeRateSatPerVbyte": 21.1, + "confirmed": true + } + ] + } + }, + "updatedAt": "2023-07-06T07:58:39.588Z", + "createdAt": "2023-07-06T07:58:39.588Z" +} +``` + +**Order states (`state2`):** `created`, `paid`, `executed`, `expired` + +**Payment states (`payment.state2`):** `created`, `paid`, `refunded`, `refundAvailable`, `canceled` + +**Channel states (`channel.state`):** `opening`, `open`, `closed` + +**Channel close types:** `cooporative`, `force`, `breach` + +**Channel close initiator:** `lsp`, `client` + +### CJitEntry + +```json +{ + "id": "fa7e6d29-a04d-47ea-8db4-ec05f6b8601c", + "state": "created", + "feeSat": 19021, + "channelSizeSat": 3000000, + "channelExpiryWeeks": 12, + "channelOpenError": null, + "nodeId": "03775370500b8c8642617bced873e7914eaec4f6a79c9ca99043224a1b28677082", + "invoice": { + "request": "lntb1u1pwz5w78pp5...", + "state": "pending", + "amountSat": 2000000, + "expiresAt": "2023-07-06T07:58:39.588Z", + "updatedAt": "2023-07-06T07:58:39.588Z" + }, + "channel": { + "state": "opening", + "lspNodePubkey": "0296b2db...", + "clientNodePubkey": "03775370...", + "announceChannel": false, + "fundingTx": null, + "close": null + }, + "lspNode": { + "alias": "Blocktank", + "pubkey": "0296b2db...", + "connectionStrings": ["0296b2db...@172.19.0.2:9735"] + }, + "couponCode": "", + "expiresAt": "2023-07-06T07:58:39.588Z", + "updatedAt": "2023-07-06T07:58:39.588Z", + "createdAt": "2023-07-06T07:58:39.588Z" +} +``` + +**CJIT states:** `created`, `completed`, `expired`, `failed` + +### ChannelOpenError + +Returned as HTTP 412 from POST /channels/:id/open: + +```json +{ + "message": "Channel has been rejected by the client.", + "code": "CHANNEL_REJECTED_BY_DESTINATION", + "details": {}, + "name": "ChannelOpenError" +} +``` + +**Error codes:** `WRONG_ORDER_STATE`, `PEER_NOT_REACHABLE`, `CHANNEL_REJECTED_BY_DESTINATION`, `CHANNEL_REJECTED_BY_LSP`, `BLOCKTANK_NOT_READY`, `UNKNOWN_ERROR` diff --git a/.claude/plugins/blocktank-api/skills/blocktank-api/scripts/lsp.sh b/.claude/plugins/blocktank-api/skills/blocktank-api/scripts/lsp.sh new file mode 100755 index 000000000..8b231198f --- /dev/null +++ b/.claude/plugins/blocktank-api/skills/blocktank-api/scripts/lsp.sh @@ -0,0 +1,60 @@ +#!/usr/bin/env bash +set -euo pipefail + +# Blocktank LSP API caller +# Usage: lsp.sh [json_body] +# +# Examples: +# lsp.sh GET /info +# lsp.sh GET /channels/abc-123 +# lsp.sh POST /channels '{"lspBalanceSat":100000,"channelExpiryWeeks":12}' +# lsp.sh POST /regtest/chain/mine '{"count":6}' +# lsp.sh POST /regtest/chain/deposit '{"address":"bcrt1q...","amountSat":500000}' +# +# Environment: +# BLOCKTANK_API_URL Override base URL (default: staging) + +BASE_URL="${BLOCKTANK_API_URL:-https://api.stag0.blocktank.to/blocktank/api/v2}" +METHOD="${1:?Usage: lsp.sh [json_body]}" +API_PATH="${2:?Usage: lsp.sh [json_body]}" +BODY="${3:-}" + +URL="${BASE_URL}${API_PATH}" + +call_api() { + local http_code + local response + local tmpfile + tmpfile=$(mktemp) + + if [ "$METHOD" = "GET" ]; then + http_code=$(curl -s -o "$tmpfile" -w "%{http_code}" "$URL") + elif [ "$METHOD" = "POST" ]; then + if [ -n "$BODY" ]; then + http_code=$(curl -s -o "$tmpfile" -w "%{http_code}" -X POST "$URL" \ + -H "Content-Type: application/json" \ + -d "$BODY") + else + http_code=$(curl -s -o "$tmpfile" -w "%{http_code}" -X POST "$URL" \ + -H "Content-Type: application/json" \ + -d '{}') + fi + else + echo "Error: Method must be GET or POST, got '$METHOD'" >&2 + rm -f "$tmpfile" + exit 1 + fi + + response=$(cat "$tmpfile") + rm -f "$tmpfile" + + if [ "$http_code" -ge 400 ]; then + echo "HTTP $http_code $METHOD $API_PATH" >&2 + echo "$response" + exit 1 + fi + + echo "$response" +} + +call_api diff --git a/README.md b/README.md index 35c28326f..15eace9fc 100644 --- a/README.md +++ b/README.md @@ -223,7 +223,7 @@ Please focus on: - Thread safety in coroutines ``` -#### Local Development Setup (YOLO Mode) +### Local Development Setup (YOLO Mode) To enable auto-approved permissions for Claude Code during local development: @@ -234,6 +234,12 @@ cp .claude/settings.local.template.json .claude/settings.local.json This reduces confirmation prompts for common operations (Bash, Read, Edit, Write, etc.). Destructive operations like `rm -rf`, `git commit`, and `git push` still require confirmation. +### AI Dev Plugins + +Claude Code plugins provide specialized skills for development workflows. See [`.claude/plugins/`](.claude/plugins/) for available plugins. + +- [blocktank-api](.claude/plugins/blocktank-api/README.md) — Blocktank LSP API for LN testing on regtest + ## License This project is licensed under the MIT License. From a8e454ee5a9c1201ca3394750b228f99d05f96f8 Mon Sep 17 00:00:00 2001 From: Ovi Trif Date: Mon, 23 Mar 2026 01:28:40 +0100 Subject: [PATCH 2/2] ai: lsp rest api agent skill --- .claude/plugins/blocktank-api/README.md | 2 +- .../skills/{blocktank-api => lsp}/SKILL.md | 38 +++++-- .../references/api-reference.md | 0 .../{blocktank-api => lsp}/scripts/lsp.sh | 16 +-- .../skills/lsp/scripts/pay-invoices.sh | 102 ++++++++++++++++++ .../java/to/bitkit/viewmodels/AppViewModel.kt | 29 +++++ lsp | 2 + 7 files changed, 170 insertions(+), 19 deletions(-) rename .claude/plugins/blocktank-api/skills/{blocktank-api => lsp}/SKILL.md (82%) rename .claude/plugins/blocktank-api/skills/{blocktank-api => lsp}/references/api-reference.md (100%) rename .claude/plugins/blocktank-api/skills/{blocktank-api => lsp}/scripts/lsp.sh (74%) create mode 100755 .claude/plugins/blocktank-api/skills/lsp/scripts/pay-invoices.sh create mode 100755 lsp diff --git a/.claude/plugins/blocktank-api/README.md b/.claude/plugins/blocktank-api/README.md index f47d7469d..e7ec6691e 100644 --- a/.claude/plugins/blocktank-api/README.md +++ b/.claude/plugins/blocktank-api/README.md @@ -8,7 +8,7 @@ Once installed, the skill auto-triggers when you mention things like: - "mine blocks", "deposit sats", "pay invoice", "force close" - "channel order", "CJIT", "blocktank", "LSP" -Claude will use the `lsp.sh` script to make API calls directly. +Claude will use the `./lsp` wrapper at the repo root to make API calls directly. ## Configuration diff --git a/.claude/plugins/blocktank-api/skills/blocktank-api/SKILL.md b/.claude/plugins/blocktank-api/skills/lsp/SKILL.md similarity index 82% rename from .claude/plugins/blocktank-api/skills/blocktank-api/SKILL.md rename to .claude/plugins/blocktank-api/skills/lsp/SKILL.md index 2c811a9fe..0e7a842b3 100644 --- a/.claude/plugins/blocktank-api/skills/blocktank-api/SKILL.md +++ b/.claude/plugins/blocktank-api/skills/lsp/SKILL.md @@ -1,5 +1,5 @@ --- -name: blocktank-api +name: lsp description: > This skill should be used when the user asks to interact with the Blocktank LSP API, "mine blocks", "deposit sats", "pay invoice", "force close a channel", "create a channel order", @@ -23,29 +23,26 @@ Override with the `BLOCKTANK_API_URL` environment variable: ## API Script -Call any endpoint using the bundled script: +Call any endpoint using the `./lsp` wrapper at the repo root: ```bash -bash "${CLAUDE_PLUGIN_ROOT}/skills/blocktank-api/scripts/lsp.sh" [json_body] +./lsp [json_body] ``` Examples: ```bash # Get service info -bash "${CLAUDE_PLUGIN_ROOT}/skills/blocktank-api/scripts/lsp.sh" GET /info +./lsp GET /info # Create a channel order -bash "${CLAUDE_PLUGIN_ROOT}/skills/blocktank-api/scripts/lsp.sh" POST /channels \ - '{"lspBalanceSat":100000,"channelExpiryWeeks":12}' +./lsp POST /channels '{"lspBalanceSat":100000,"channelExpiryWeeks":12}' # Mine 6 blocks -bash "${CLAUDE_PLUGIN_ROOT}/skills/blocktank-api/scripts/lsp.sh" POST /regtest/chain/mine \ - '{"count":6}' +./lsp POST /regtest/chain/mine '{"count":6}' # Deposit to an address -bash "${CLAUDE_PLUGIN_ROOT}/skills/blocktank-api/scripts/lsp.sh" POST /regtest/chain/deposit \ - '{"address":"bcrt1q...","amountSat":500000}' +./lsp POST /regtest/chain/deposit '{"address":"bcrt1q...","amountSat":500000}' ``` The script outputs raw JSON. Pipe to `jq` for formatting if needed. @@ -124,6 +121,27 @@ On HTTP errors (4xx/5xx), the script prints the status code to stderr and the er 2. **Close channel** — `POST /regtest/channel/close` with `fundingTxId`, `vout`, and `forceCloseAfterSec: 0` for immediate close 3. **Mine blocks** — `POST /regtest/chain/mine` with `count: 6` to finalize the closure +### Workflow D: Automated Invoice Payments + +Bulk-create and pay invoices to populate the app with payment activity. + +**Prerequisites:** Dev debug build installed, wallet set up, LDK node running, open channel with inbound capacity, ADB connected. + +**Run with defaults** (21 invoices of 1..21 sats, mine 150 blocks in batches of 10): + +```bash +"${CLAUDE_PLUGIN_ROOT}/skills/lsp/scripts/pay-invoices.sh" +``` + +**Custom parameters** via env vars: + +```bash +INVOICE_COUNT=10 MINE_TOTAL=60 MINE_BATCH=10 \ + "${CLAUDE_PLUGIN_ROOT}/skills/lsp/scripts/pay-invoices.sh" +``` + +The script uses `bitkit://dev/create-invoice` deep links (dev builds only) to create invoices on the app's LDK node, then pays each via the LSP's `POST /regtest/channel/pay` endpoint. + ## State Machines ### Order States (`state2`) diff --git a/.claude/plugins/blocktank-api/skills/blocktank-api/references/api-reference.md b/.claude/plugins/blocktank-api/skills/lsp/references/api-reference.md similarity index 100% rename from .claude/plugins/blocktank-api/skills/blocktank-api/references/api-reference.md rename to .claude/plugins/blocktank-api/skills/lsp/references/api-reference.md diff --git a/.claude/plugins/blocktank-api/skills/blocktank-api/scripts/lsp.sh b/.claude/plugins/blocktank-api/skills/lsp/scripts/lsp.sh similarity index 74% rename from .claude/plugins/blocktank-api/skills/blocktank-api/scripts/lsp.sh rename to .claude/plugins/blocktank-api/skills/lsp/scripts/lsp.sh index 8b231198f..2a595fb26 100755 --- a/.claude/plugins/blocktank-api/skills/blocktank-api/scripts/lsp.sh +++ b/.claude/plugins/blocktank-api/skills/lsp/scripts/lsp.sh @@ -2,21 +2,21 @@ set -euo pipefail # Blocktank LSP API caller -# Usage: lsp.sh [json_body] +# Usage: ./lsp [json_body] # # Examples: -# lsp.sh GET /info -# lsp.sh GET /channels/abc-123 -# lsp.sh POST /channels '{"lspBalanceSat":100000,"channelExpiryWeeks":12}' -# lsp.sh POST /regtest/chain/mine '{"count":6}' -# lsp.sh POST /regtest/chain/deposit '{"address":"bcrt1q...","amountSat":500000}' +# ./lsp GET /info +# ./lsp GET /channels/abc-123 +# ./lsp POST /channels '{"lspBalanceSat":100000,"channelExpiryWeeks":12}' +# ./lsp POST /regtest/chain/mine '{"count":6}' +# ./lsp POST /regtest/chain/deposit '{"address":"bcrt1q...","amountSat":500000}' # # Environment: # BLOCKTANK_API_URL Override base URL (default: staging) BASE_URL="${BLOCKTANK_API_URL:-https://api.stag0.blocktank.to/blocktank/api/v2}" -METHOD="${1:?Usage: lsp.sh [json_body]}" -API_PATH="${2:?Usage: lsp.sh [json_body]}" +METHOD="${1:?Usage: ./lsp [json_body]}" +API_PATH="${2:?Usage: ./lsp [json_body]}" BODY="${3:-}" URL="${BASE_URL}${API_PATH}" diff --git a/.claude/plugins/blocktank-api/skills/lsp/scripts/pay-invoices.sh b/.claude/plugins/blocktank-api/skills/lsp/scripts/pay-invoices.sh new file mode 100755 index 000000000..a8f24ef56 --- /dev/null +++ b/.claude/plugins/blocktank-api/skills/lsp/scripts/pay-invoices.sh @@ -0,0 +1,102 @@ +#!/usr/bin/env bash +set -euo pipefail + +# Automated LN invoice creation + payment via Blocktank LSP +# +# Prerequisites: +# - Dev debug build installed on device/emulator with wallet set up and LDK node running +# - ADB connected to the device +# - An open Lightning channel with sufficient inbound capacity +# +# Usage: +# pay-invoices.sh +# INVOICE_COUNT=10 pay-invoices.sh +# +# Each invoice amount equals its index (1 sat, 2 sats, ... N sats). +# +# Environment: +# APP_ID App package (default: to.bitkit.dev) +# INVOICE_COUNT Number of invoices to create and pay (default: 21) +# MINE_TOTAL Total blocks to mine after payments (default: 150) +# MINE_BATCH Blocks per mining call (default: 10) +# WAIT_TIMEOUT Seconds to wait for invoice file (default: 15) +# PAY_DELAY Seconds between payments (default: 3) + +APP_ID="${APP_ID:-to.bitkit.dev}" +INVOICE_COUNT="${INVOICE_COUNT:-21}" +MINE_TOTAL="${MINE_TOTAL:-150}" +MINE_BATCH="${MINE_BATCH:-10}" +WAIT_TIMEOUT="${WAIT_TIMEOUT:-15}" +PAY_DELAY="${PAY_DELAY:-3}" +INVOICE_FILE="files/dev/invoice.txt" + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +LSP="$SCRIPT_DIR/lsp.sh" + +create_invoice() { + local amount="$1" + local index="$2" + adb shell am start -a android.intent.action.VIEW \ + -d "bitkit://dev/create-invoice?amount=${amount}\&description=dev-payment-${index}" \ + "$APP_ID" > /dev/null +} + +read_invoice() { + adb shell "run-as $APP_ID cat $INVOICE_FILE" 2>/dev/null || true +} + +wait_for_invoice() { + local previous="$1" + local elapsed=0 + while [ $elapsed -lt "$WAIT_TIMEOUT" ]; do + local current + current=$(read_invoice) + if [ -n "$current" ] && [ "$current" != "$previous" ]; then + echo "$current" + return 0 + fi + sleep 1 + elapsed=$((elapsed + 1)) + done + echo "ERROR: Timed out waiting for new invoice after ${WAIT_TIMEOUT}s" >&2 + return 1 +} + +# Phase 1: Create and pay invoices +echo "=== Creating and paying $INVOICE_COUNT invoices (1..$INVOICE_COUNT sats) ===" +previous_invoice="" +for i in $(seq 1 "$INVOICE_COUNT"); do + echo "" + echo "--- Invoice $i/$INVOICE_COUNT ($i sats) ---" + + echo " Creating invoice..." + create_invoice "$i" "$i" + + invoice=$(wait_for_invoice "$previous_invoice") + echo " Invoice: ${invoice:0:30}..." + previous_invoice="$invoice" + + echo " Paying via LSP..." + "$LSP" POST /regtest/channel/pay "{\"invoice\":\"$invoice\"}" > /dev/null + echo " Paid." + + sleep "$PAY_DELAY" +done + +echo "" +echo "=== $INVOICE_COUNT invoices paid ===" + +# Phase 2: Mine blocks +batches=$((MINE_TOTAL / MINE_BATCH)) +if [ "$batches" -gt 0 ]; then + echo "" + echo "=== Mining $MINE_TOTAL blocks in $batches batches of $MINE_BATCH ===" + for i in $(seq 1 "$batches"); do + echo " Batch $i/$batches ($MINE_BATCH blocks)..." + "$LSP" POST /regtest/chain/mine "{\"count\":$MINE_BATCH}" > /dev/null + sleep 1 + done +fi + +echo "" +echo "=== Done: $INVOICE_COUNT invoices paid, $MINE_TOTAL blocks mined ===" diff --git a/app/src/main/java/to/bitkit/viewmodels/AppViewModel.kt b/app/src/main/java/to/bitkit/viewmodels/AppViewModel.kt index 986b6aa6c..d7eebaa03 100644 --- a/app/src/main/java/to/bitkit/viewmodels/AppViewModel.kt +++ b/app/src/main/java/to/bitkit/viewmodels/AppViewModel.kt @@ -69,6 +69,7 @@ import to.bitkit.ext.amountOnClose import to.bitkit.ext.amountSats import to.bitkit.ext.channelId import to.bitkit.ext.claimableAtHeight +import to.bitkit.ext.ensureDir import to.bitkit.ext.getClipboardText import to.bitkit.ext.getSatsPerVByteFor import to.bitkit.ext.maxSendableSat @@ -132,6 +133,7 @@ import to.bitkit.utils.timedsheets.sheets.BackupTimedSheet import to.bitkit.utils.timedsheets.sheets.HighBalanceTimedSheet import to.bitkit.utils.timedsheets.sheets.NotificationsTimedSheet import to.bitkit.utils.timedsheets.sheets.QuickPayTimedSheet +import java.io.File import java.math.BigDecimal import javax.inject.Inject import kotlin.coroutines.cancellation.CancellationException @@ -2379,11 +2381,38 @@ class AppViewModel @Inject constructor( return@launch } + if (uri.host == "dev" && Env.isDebug) { + handleDevDeeplink(uri) + return@launch + } + if (!walletRepo.walletExists()) return@launch launchScan(source = ScanSource.DEEPLINK, data = uri.toString(), startDelay = SCREEN_TRANSITION_DELAY) } + private fun handleDevDeeplink(uri: Uri) = viewModelScope.launch(bgDispatcher) { + when (uri.pathSegments.firstOrNull()) { + "create-invoice" -> { + if (!lightningRepo.canReceive()) { + Logger.error("Dev invoice creation failed: no ready LN channel", context = TAG) + return@launch + } + val amountSats = uri.getQueryParameter("amount")?.toULongOrNull() + val description = uri.getQueryParameter("description") ?: "dev-invoice" + lightningRepo.createInvoice(amountSats, description) + .onSuccess { + val file = File(context.filesDir, "dev").ensureDir().resolve("invoice.txt") + file.writeText(it) + Logger.info("Dev invoice written to '${file.path}'", context = TAG) + } + .onFailure { + Logger.error("Dev invoice creation failed", it, context = TAG) + } + } + } + } + // TODO Temporary fix while these schemes can't be decoded https://github.com/synonymdev/bitkit-core/issues/70 private fun String.removeLightningSchemes(): String = LIGHTNING_SCHEME_PATTERNS.fold(this) { acc, regex -> acc.replace(regex, "") diff --git a/lsp b/lsp new file mode 100755 index 000000000..5412a9314 --- /dev/null +++ b/lsp @@ -0,0 +1,2 @@ +#!/usr/bin/env bash +exec "$(dirname "$0")/.claude/plugins/blocktank-api/skills/lsp/scripts/lsp.sh" "$@"