bitspire/docker/regtest-bootstrap.sh
Patrick Mulligan 0395aab3d1 feat(docker): streamline regtest dev environment
- Fix EXTENSION_SERVICE_URL default to localhost (was hardcoded LAN IP)
- Fix strfry healthcheck (BusyBox nc lacks -z, use /proc/net/tcp)
- Add LP healthcheck + depends_on strfry
- Add regtest-bootstrap.sh: funds LND, opens channels, creates LP app
- Add regtest.sh: single management script (up/down/reset/status/logs/lncli/bitcoin)
- Minimal start uses only 3 containers (bitcoind, lnd-1, lnd-4) instead of 17

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-10 01:31:51 -04:00

273 lines
9 KiB
Bash
Executable file

#!/usr/bin/env bash
# regtest-bootstrap.sh — Bootstrap a working LP+LND regtest environment
#
# Prerequisites:
# 1. Regtest running (minimal — only needs bitcoind + lnd-1 + lnd-4):
# cd ~/dev/local/docker/regtest && docker compose up -d bitcoind lnd-1 lnd-4
# 2. Lamassu services running:
# cd docker && docker compose -f docker-compose.regtest.yml up -d
#
# What this script does:
# 1. Waits for bitcoind and LND nodes to be synced
# 2. Funds LND-1 (1 BTC from bitcoind)
# 3. Opens a 5M sat channel from LND-1 → LND-4 (LP's backend)
# 4. Mines blocks to confirm and announce the channel
# 5. Waits for LP to be healthy
# 6. Creates an LP app and funds it (10k sats via invoice from LND-1)
# 7. Prints summary with pubkeys, app token, and channel status
set -euo pipefail
# -- Colors --
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
CYAN='\033[0;36m'
NC='\033[0m' # No Color
info() { echo -e "${CYAN}[INFO]${NC} $*"; }
ok() { echo -e "${GREEN}[OK]${NC} $*"; }
warn() { echo -e "${YELLOW}[WARN]${NC} $*"; }
err() { echo -e "${RED}[ERROR]${NC} $*"; }
# -- Auto-detect container name prefix --
# The regtest compose uses project name "lnbits" (via start-regtest) or "regtest" (direct).
# Detect which prefix the RUNNING bitcoind container has (filter=running avoids stale containers).
if docker ps -f name=lnbits-bitcoind-1 -f status=running --format '{{.Names}}' | grep -q .; then
PREFIX="lnbits"
elif docker ps -f name=regtest-bitcoind-1 -f status=running --format '{{.Names}}' | grep -q .; then
PREFIX="regtest"
else
err "No running bitcoind container found (tried lnbits-bitcoind-1 and regtest-bitcoind-1)"
err "Start regtest first: cd ~/dev/local/docker/regtest && docker compose up -d bitcoind lnd-1 lnd-4"
exit 1
fi
info "Using container prefix: ${PREFIX}"
# -- Container helpers --
bitcoin_cli() {
docker exec "${PREFIX}-bitcoind-1" bitcoin-cli -regtest "$@"
}
lncli_1() {
docker exec "${PREFIX}-lnd-1-1" lncli --network regtest --rpcserver=lnd-1:10009 "$@"
}
lncli_4() {
docker exec "${PREFIX}-lnd-4-1" lncli --network regtest --rpcserver=lnd-4:10009 "$@"
}
LP_URL="http://localhost:1776"
LP_EXT_URL="http://localhost:1777"
ADMIN_TOKEN="lamassu-dev-admin-token"
CHANNEL_SIZE=5000000 # 5M sats
FUND_AMOUNT=10000 # 10k sats for LP app
# -- Step 1: Wait for bitcoind --
info "Waiting for bitcoind..."
for i in $(seq 1 60); do
if bitcoin_cli getblockchaininfo > /dev/null 2>&1; then
ok "bitcoind ready (block $(bitcoin_cli getblockcount))"
break
fi
if [ "$i" -eq 60 ]; then
err "bitcoind not ready after 60s — is regtest running?"
exit 1
fi
sleep 1
done
# Ensure a wallet exists
bitcoin_cli createwallet default 2>/dev/null \
|| bitcoin_cli loadwallet default 2>/dev/null \
|| true
# Mine initial blocks if needed (coinbase needs 100 confirmations to be spendable)
BLOCKS=$(bitcoin_cli getblockcount)
if [ "$BLOCKS" -lt 101 ]; then
NEEDED=$((101 - BLOCKS))
info "Mining ${NEEDED} initial blocks (coinbase needs 100 confirmations)..."
bitcoin_cli -generate "$NEEDED" > /dev/null
ok "Mined to block $(bitcoin_cli getblockcount)"
fi
# -- Step 2: Wait for LND-1 and LND-4 to sync --
wait_lnd_sync() {
local name=$1
local cli_fn=$2
info "Waiting for ${name} to sync..."
for i in $(seq 1 120); do
if [ "$($cli_fn getinfo 2>/dev/null | jq -r '.synced_to_chain' 2>/dev/null)" = "true" ]; then
ok "${name} synced"
return 0
fi
if [ "$i" -eq 120 ]; then
err "${name} not synced after 120s"
exit 1
fi
sleep 1
done
}
wait_lnd_sync "LND-1" lncli_1
wait_lnd_sync "LND-4" lncli_4
# -- Step 3: Fund LND-1 --
info "Funding LND-1..."
LND1_ADDR=$(lncli_1 newaddress p2wkh | jq -r '.address')
bitcoin_cli -named sendtoaddress address="$LND1_ADDR" amount=1 fee_rate=100 > /dev/null
info "Mining 6 blocks to confirm funding..."
bitcoin_cli -generate 6 > /dev/null
# Wait for LND-1 to see the confirmed balance
for i in $(seq 1 30); do
BALANCE=$(lncli_1 walletbalance | jq -r '.confirmed_balance')
if [ "$BALANCE" != "0" ] && [ -n "$BALANCE" ]; then
ok "LND-1 funded: ${BALANCE} sats"
break
fi
sleep 1
done
# -- Step 4: Connect LND-1 → LND-4 and open channel --
LND4_PUBKEY=$(lncli_4 getinfo | jq -r '.identity_pubkey')
info "Connecting LND-1 → LND-4 (${LND4_PUBKEY:0:16}...)..."
lncli_1 connect "${LND4_PUBKEY}@${PREFIX}-lnd-4-1:9735" > /dev/null 2>&1 || true
# Check if channel already exists
EXISTING=$(lncli_1 listchannels --peer "$LND4_PUBKEY" 2>/dev/null | jq '.channels | length')
if [ "${EXISTING:-0}" -gt 0 ]; then
warn "Channel to LND-4 already exists, skipping open"
else
info "Opening ${CHANNEL_SIZE} sat channel LND-1 → LND-4..."
lncli_1 openchannel "$LND4_PUBKEY" "$CHANNEL_SIZE" > /dev/null
info "Mining 6 blocks to confirm channel..."
bitcoin_cli -generate 6 > /dev/null
# Wait for channel to leave pending
for i in $(seq 1 60); do
PENDING=$(lncli_1 pendingchannels | jq '.pending_open_channels | length')
if [ "$PENDING" = "0" ]; then
break
fi
sleep 1
done
fi
# -- Step 5: Verify channel is active and in graph --
info "Waiting for channel to become active..."
for i in $(seq 1 60); do
ACTIVE=$(lncli_1 listchannels --peer "$LND4_PUBKEY" 2>/dev/null | jq '[.channels[] | select(.active == true)] | length')
if [ "${ACTIVE:-0}" -gt 0 ]; then
ok "Channel active"
break
fi
if [ "$i" -eq 60 ]; then
warn "Channel not active after 60s — may need more blocks"
fi
sleep 1
done
# Mine a few more blocks to ensure graph announcement propagates
bitcoin_cli -generate 3 > /dev/null
info "Checking graph..."
for i in $(seq 1 30); do
GRAPH_NODES=$(lncli_1 describegraph 2>/dev/null | jq '.nodes | length')
GRAPH_EDGES=$(lncli_1 describegraph 2>/dev/null | jq '.edges | length')
if [ "${GRAPH_EDGES:-0}" -gt 0 ]; then
ok "Graph: ${GRAPH_NODES} nodes, ${GRAPH_EDGES} edges"
break
fi
if [ "$i" -eq 30 ]; then
warn "Graph has no edges yet — channel may not be announced"
fi
sleep 2
done
# -- Step 6: Wait for Lightning.Pub to be healthy --
info "Waiting for Lightning.Pub..."
for i in $(seq 1 120); do
if curl -so /dev/null "${LP_URL}" 2>/dev/null; then
ok "Lightning.Pub ready"
break
fi
if [ "$i" -eq 120 ]; then
err "Lightning.Pub not ready after 120s"
err "Check: docker logs lamassu-lightning-pub"
exit 1
fi
sleep 1
done
# Small delay to ensure LP has fully initialized its LND connection
sleep 5
# -- Step 7: Create LP app --
info "Creating LP app 'regtest-atm'..."
APP_RESPONSE=$(curl -sf "${LP_URL}/api/admin/app/add" \
-H "Authorization: Bearer ${ADMIN_TOKEN}" \
-H "Content-Type: application/json" \
-d '{"name": "regtest-atm", "allow_user_creation": true}')
if [ -z "$APP_RESPONSE" ]; then
err "Failed to create LP app — check LP logs"
exit 1
fi
APP_ID=$(echo "$APP_RESPONSE" | jq -r '.app.id')
APP_NPUB=$(echo "$APP_RESPONSE" | jq -r '.app.npub')
APP_TOKEN=$(echo "$APP_RESPONSE" | jq -r '.auth_token')
if [ "$APP_ID" = "null" ] || [ -z "$APP_ID" ]; then
err "App creation returned unexpected response:"
echo "$APP_RESPONSE" | jq .
exit 1
fi
ok "App created: id=${APP_ID}"
# -- Step 8: Fund the app (create invoice via LP, pay from LND-1) --
info "Funding app with ${FUND_AMOUNT} sats..."
INVOICE_RESPONSE=$(curl -sf "${LP_URL}/api/app/add/invoice" \
-H "Authorization: Bearer ${APP_TOKEN}" \
-H "Content-Type: application/json" \
-d "{\"payer_identifier\": \"bootstrap\", \"http_callback_url\": \"\", \"invoice_req\": {\"amountSats\": ${FUND_AMOUNT}, \"memo\": \"regtest bootstrap\"}}")
INVOICE=$(echo "$INVOICE_RESPONSE" | jq -r '.invoice')
if [ "$INVOICE" = "null" ] || [ -z "$INVOICE" ]; then
warn "Failed to create invoice — app has 0 balance"
warn "Response: $INVOICE_RESPONSE"
else
# Pay from LND-1
PAY_RESULT=$(lncli_1 payinvoice --force "$INVOICE" 2>&1) || true
PAY_STATUS=$(echo "$PAY_RESULT" | jq -r '.status' 2>/dev/null || echo "")
if [ "$PAY_STATUS" = "SUCCEEDED" ] || echo "$PAY_RESULT" | grep -q "SUCCEEDED"; then
ok "App funded with ${FUND_AMOUNT} sats"
else
warn "Payment may have failed — check manually"
warn "Result: $(echo "$PAY_RESULT" | tail -3)"
fi
fi
# -- Summary --
# LP uses LND-4 as its backend, so LP pubkey = LND-4 pubkey
echo ""
echo -e "${GREEN}========================================${NC}"
echo -e "${GREEN} Regtest Bootstrap Complete${NC}"
echo -e "${GREEN}========================================${NC}"
echo ""
echo -e " ${CYAN}LND-1 pubkey:${NC} $(lncli_1 getinfo | jq -r '.identity_pubkey')"
echo -e " ${CYAN}LP (LND-4):${NC} ${LND4_PUBKEY}"
echo ""
echo -e " ${CYAN}LP app ID:${NC} ${APP_ID}"
echo -e " ${CYAN}LP app token:${NC} ${APP_TOKEN}"
echo ""
echo -e " ${CYAN}LP URL:${NC} ${LP_URL}"
echo -e " ${CYAN}Extension URL:${NC} ${LP_EXT_URL}"
echo -e " ${CYAN}Relay URL:${NC} ws://localhost:7777"
echo ""
CHAN_INFO=$(lncli_1 listchannels --peer "$LND4_PUBKEY" 2>/dev/null | jq -r '.channels[0] | "\(.local_balance) / \(.capacity) sats (active: \(.active))"' 2>/dev/null || echo "unknown")
echo -e " ${CYAN}Channel:${NC} LND-1 → LND-4: ${CHAN_INFO}"
echo ""