diff --git a/docker/docker-compose.regtest.yml b/docker/docker-compose.regtest.yml index 5cb1236..0ce54cd 100644 --- a/docker/docker-compose.regtest.yml +++ b/docker/docker-compose.regtest.yml @@ -4,16 +4,22 @@ # environment at ~/dev/local/docker/regtest # # Usage: -# 1. Start the regtest environment: -# cd ~/dev/local/docker/regtest && ./start-regtest +# 1. Start regtest (minimal — only what LP needs): +# cd ~/dev/local/docker/regtest && docker compose up -d bitcoind lnd-1 lnd-4 # # 2. Start lamassu services: -# cd lamassu-next/docker && docker compose -f docker-compose.regtest.yml up -d +# cd docker && docker compose -f docker-compose.regtest.yml up -d # -# 3. Configure apps/machine/.env: +# 3. Bootstrap (funds LND, opens channels, creates LP app): +# cd docker && ./regtest-bootstrap.sh +# +# 4. Configure apps/machine/.env: # VITE_RELAY_URL=ws://localhost:7777 # VITE_EXTENSION_API_URL=http://localhost:1777 # +# Phone wallet testing (LNURL callbacks need LAN IP): +# HOST_IP=192.168.1.100 docker compose -f docker-compose.regtest.yml up -d +# # Services: # - strfry: Private Nostr relay (port 7777) # - lightning-pub: Nostr-native Lightning account system (port 1776) @@ -37,10 +43,11 @@ services: soft: 524288 hard: 524288 healthcheck: - test: ['CMD', 'nc', '-z', 'localhost', '7777'] - interval: 10s - timeout: 5s - retries: 5 + # Check port 7777 (0x1E61) is listening via procfs — BusyBox nc lacks -z flag + test: ['CMD-SHELL', 'grep -q ":1E61 " /proc/net/tcp'] + interval: 5s + timeout: 3s + retries: 10 restart: unless-stopped networks: - regtest @@ -76,7 +83,16 @@ services: - ADMIN_TOKEN=lamassu-dev-admin-token # Extension HTTP API URL (for LNURL callbacks from external wallets) # Use HOST_IP env var for your machine's LAN IP (required for phone wallets) - - EXTENSION_SERVICE_URL=http://${HOST_IP:-192.168.1.190}:1777 + - EXTENSION_SERVICE_URL=http://${HOST_IP:-localhost}:1777 + depends_on: + strfry: + condition: service_healthy + healthcheck: + test: ['CMD-SHELL', 'curl -so /dev/null -w "%{http_code}" http://localhost:1776 | grep -q .'] + interval: 10s + timeout: 5s + retries: 30 + start_period: 30s restart: unless-stopped networks: - regtest diff --git a/docker/regtest-bootstrap.sh b/docker/regtest-bootstrap.sh new file mode 100755 index 0000000..f00ace1 --- /dev/null +++ b/docker/regtest-bootstrap.sh @@ -0,0 +1,273 @@ +#!/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 "" diff --git a/docker/regtest.sh b/docker/regtest.sh new file mode 100755 index 0000000..b8ab709 --- /dev/null +++ b/docker/regtest.sh @@ -0,0 +1,142 @@ +#!/usr/bin/env bash +# regtest.sh — Manage the LP+LND regtest development environment +# +# Usage: +# ./regtest.sh up Start everything + bootstrap +# ./regtest.sh down Stop everything +# ./regtest.sh reset Full teardown (volumes + data) and fresh start +# ./regtest.sh status Show container status +# ./regtest.sh logs [svc] Tail logs (lp, relay, miner, lnd1, lnd4, bitcoind) +# ./regtest.sh lncli N Run lncli on LND-N (e.g. ./regtest.sh lncli 1 getinfo) +# ./regtest.sh bitcoin Run bitcoin-cli (e.g. ./regtest.sh bitcoin getblockcount) + +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +REGTEST_DIR="${REGTEST_DIR:-$HOME/dev/local/docker/regtest}" +COMPOSE_FILE="${SCRIPT_DIR}/docker-compose.regtest.yml" + +RED='\033[0;31m' +GREEN='\033[0;32m' +CYAN='\033[0;36m' +NC='\033[0m' + +# -- Detect container prefix (regtest- or lnbits-) -- +detect_prefix() { + if docker ps -f name=regtest-bitcoind-1 -f status=running --format '{{.Names}}' | grep -q .; then + echo "regtest" + elif docker ps -f name=lnbits-bitcoind-1 -f status=running --format '{{.Names}}' | grep -q .; then + echo "lnbits" + else + echo "" + fi +} + +cmd_up() { + echo -e "${CYAN}Starting regtest (bitcoind + lnd-1 + lnd-4)...${NC}" + cd "$REGTEST_DIR" && docker compose up -d bitcoind lnd-1 lnd-4 + + echo -e "${CYAN}Starting lamassu services...${NC}" + docker compose -f "$COMPOSE_FILE" up -d + + echo -e "${CYAN}Running bootstrap...${NC}" + "$SCRIPT_DIR/regtest-bootstrap.sh" +} + +cmd_down() { + echo -e "${CYAN}Stopping lamassu services...${NC}" + docker compose -f "$COMPOSE_FILE" down 2>/dev/null || true + + echo -e "${CYAN}Stopping regtest...${NC}" + cd "$REGTEST_DIR" && docker compose down 2>/dev/null || true + + echo -e "${GREEN}All stopped.${NC}" +} + +cmd_reset() { + echo -e "${CYAN}Full reset: tearing down everything + wiping data...${NC}" + + docker compose -f "$COMPOSE_FILE" down -v 2>/dev/null || true + cd "$REGTEST_DIR" && docker compose down -v 2>/dev/null || true + + # Clean LND data (root-owned from containers) + docker run --rm -v "$REGTEST_DIR/data:/data" alpine sh -c 'rm -rf /data/lnd-1/* /data/lnd-4/*' 2>/dev/null || true + + echo -e "${GREEN}Clean. Starting fresh...${NC}" + cmd_up +} + +cmd_status() { + echo -e "${CYAN}Containers:${NC}" + docker ps --format 'table {{.Names}}\t{{.Status}}\t{{.Ports}}' | grep -E 'regtest|lamassu|NAMES' | sort + + PREFIX=$(detect_prefix) + if [ -n "$PREFIX" ]; then + echo "" + BLOCK=$(docker exec "${PREFIX}-bitcoind-1" bitcoin-cli -regtest getblockcount 2>/dev/null || echo "?") + echo -e " ${CYAN}Block height:${NC} ${BLOCK}" + + for N in 1 4; do + SYNCED=$(docker exec "${PREFIX}-lnd-${N}-1" lncli --network regtest --rpcserver="lnd-${N}:10009" getinfo 2>/dev/null | jq -r '.synced_to_chain' 2>/dev/null || echo "?") + echo -e " ${CYAN}LND-${N} synced:${NC} ${SYNCED}" + done + + LP_STATUS=$(curl -so /dev/null -w "%{http_code}" http://localhost:1776 2>/dev/null || echo "down") + echo -e " ${CYAN}LP status:${NC} ${LP_STATUS}" + + EXT_STATUS=$(curl -so /dev/null -w "%{http_code}" http://localhost:1777 2>/dev/null || echo "down") + echo -e " ${CYAN}Extension:${NC} ${EXT_STATUS}" + fi +} + +cmd_logs() { + local svc="${1:-lp}" + case "$svc" in + lp|lightning-pub) docker logs -f --tail 50 lamassu-lightning-pub ;; + relay|strfry) docker logs -f --tail 50 lamassu-relay ;; + miner) docker logs -f --tail 50 lamassu-miner ;; + lnd1|lnd-1) PREFIX=$(detect_prefix); docker logs -f --tail 50 "${PREFIX}-lnd-1-1" ;; + lnd4|lnd-4) PREFIX=$(detect_prefix); docker logs -f --tail 50 "${PREFIX}-lnd-4-1" ;; + bitcoind) PREFIX=$(detect_prefix); docker logs -f --tail 50 "${PREFIX}-bitcoind-1" ;; + *) echo "Unknown service: $svc (try: lp, relay, miner, lnd1, lnd4, bitcoind)"; exit 1 ;; + esac +} + +cmd_lncli() { + local N="$1"; shift + PREFIX=$(detect_prefix) + if [ -z "$PREFIX" ]; then + echo -e "${RED}No running regtest found${NC}"; exit 1 + fi + docker exec "${PREFIX}-lnd-${N}-1" lncli --network regtest --rpcserver="lnd-${N}:10009" "$@" +} + +cmd_bitcoin() { + PREFIX=$(detect_prefix) + if [ -z "$PREFIX" ]; then + echo -e "${RED}No running regtest found${NC}"; exit 1 + fi + docker exec "${PREFIX}-bitcoind-1" bitcoin-cli -regtest "$@" +} + +# -- Main -- +case "${1:-help}" in + up) cmd_up ;; + down) cmd_down ;; + reset) cmd_reset ;; + status) cmd_status ;; + logs) shift; cmd_logs "${1:-lp}" ;; + lncli) shift; cmd_lncli "$@" ;; + bitcoin) shift; cmd_bitcoin "$@" ;; + *) + echo "Usage: $0 {up|down|reset|status|logs|lncli|bitcoin}" + echo "" + echo " up Start everything + bootstrap" + echo " down Stop everything" + echo " reset Full teardown + fresh start" + echo " status Show container & service status" + echo " logs [s] Tail logs (lp, relay, miner, lnd1, lnd4, bitcoind)" + echo " lncli N Run lncli on LND-N (e.g. $0 lncli 1 getinfo)" + echo " bitcoin Run bitcoin-cli (e.g. $0 bitcoin getblockcount)" + ;; +esac