feat(docker): add dev.sh with auto-funding and ATM app setup

- Add dev.sh script for managing regtest development environment
- Implement cmd_fund to fund ATM app owner via Lightning.Pub API
- Add --fund flag to cmd_up for automatic funding on startup
- Update setup_atm_app to write VITE_APP_ID to machine .env
- Fix Electron IPC to pass appId and extensionApiUrl to renderer
- Restructure repo from nested lamassu-next/ to root

The dev.sh script now supports:
- ./dev.sh up --fund  # Start regtest and auto-fund ATM
- ./dev.sh fund       # Fund existing ATM app
- ./dev.sh status     # Show environment status
- ./dev.sh reset      # Clean restart

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Patrick Mulligan 2026-02-15 14:19:16 -05:00
commit c98f126ba7
180 changed files with 2695 additions and 9587 deletions

655
docker/dev.sh Executable file
View file

@ -0,0 +1,655 @@
#!/bin/bash
#
# Lamassu Next Development Environment
#
# Single command to manage the complete development stack:
# - Regtest Bitcoin/Lightning network (from ~/dev/local/docker/regtest)
# - Lightning.Pub with configurable image/worktree
# - Auto-configured ATM application
#
# Usage:
# ./dev.sh up # Start everything
# ./dev.sh up --fund # Start and auto-fund ATM with 100k sats
# ./dev.sh up --fund=50000 # Start and auto-fund with specific amount
# ./dev.sh up --worktree ~/path/to/lp # Build Lightning.Pub from worktree
# ./dev.sh up --image myimage:tag # Use specific Docker image
# ./dev.sh down # Stop everything
# ./dev.sh atm # Launch ATM application
# ./dev.sh status # Show connection info
# ./dev.sh logs [service] # Follow logs
# ./dev.sh fund [amount] # Fund ATM (default: 100000 sats)
# ./dev.sh mine [blocks] # Mine blocks manually
# ./dev.sh reset # Reset all state (fresh start)
#
set -e
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
PROJECT_DIR="$(dirname "$SCRIPT_DIR")"
REGTEST_DIR="${REGTEST_DIR:-$HOME/dev/local/docker/regtest}"
DEFAULT_IMAGE="lightning-pub-withdraw:latest"
DEFAULT_FUNDING_SATS=100000
# State files
STATE_DIR="$SCRIPT_DIR/.state"
PUBKEY_FILE="$STATE_DIR/lightning-pub-pubkey"
NPROFILE_FILE="$STATE_DIR/lightning-pub-nprofile"
APP_TOKEN_FILE="$STATE_DIR/atm-app-token"
APP_ID_FILE="$STATE_DIR/atm-app-id"
# Colors
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
RED='\033[0;31m'
CYAN='\033[0;36m'
BOLD='\033[1m'
DIM='\033[2m'
NC='\033[0m'
log() { echo -e "${GREEN}►${NC} $1"; }
warn() { echo -e "${YELLOW}⚠${NC} $1"; }
error() { echo -e "${RED}✗${NC} $1"; }
success() { echo -e "${GREEN}✓${NC} $1"; }
# Ensure state directory exists
mkdir -p "$STATE_DIR"
#############################################################################
# Helper Functions
#############################################################################
get_local_ip() {
ip route get 1 2>/dev/null | awk '{print $7; exit}' || hostname -I | awk '{print $1}'
}
is_regtest_running() {
# Check if lnd-4 container is actually running (not just network exists)
docker ps --filter "name=lnbits-lnd-4-1" --format "{{.Names}}" 2>/dev/null | grep -q lnbits-lnd-4-1
}
is_lnd4_ready() {
# Use lnd-4 hostname (not localhost) since lnd binds to container IP
docker exec lnbits-lnd-4-1 lncli --network=regtest --rpcserver=lnd-4:10009 getinfo &>/dev/null 2>&1
}
is_lightning_pub_ready() {
docker logs lamassu-lightning-pub 2>&1 | grep -q "LightningPub listening"
}
get_lnd4_balance() {
docker exec lnbits-lnd-4-1 lncli --network=regtest walletbalance 2>/dev/null | grep -oP '"total_balance":\s*"\K[0-9]+' || echo "0"
}
#############################################################################
# Regtest Management
#############################################################################
start_regtest() {
if is_regtest_running; then
log "Regtest network already running"
return 0
fi
if [[ ! -d "$REGTEST_DIR" ]]; then
error "Regtest directory not found: $REGTEST_DIR"
echo " Clone it from: https://github.com/your-org/regtest-env"
exit 1
fi
log "Starting regtest environment..."
# Use project name "lnbits" to match the expected network name (lnbits_default)
(cd "$REGTEST_DIR" && docker compose -p lnbits up -d)
# Wait for lnd-4 to be ready
log "Waiting for lnd-4 to be ready..."
local attempts=0
while ! is_lnd4_ready && [[ $attempts -lt 30 ]]; do
sleep 2
attempts=$((attempts + 1))
done
if is_lnd4_ready; then
success "lnd-4 is ready"
else
error "lnd-4 failed to start"
return 1
fi
}
stop_regtest() {
if [[ -d "$REGTEST_DIR" ]]; then
log "Stopping regtest environment..."
(cd "$REGTEST_DIR" && docker compose down)
fi
}
#############################################################################
# Lightning.Pub Management
#############################################################################
build_from_worktree() {
local worktree="$1"
local image_name="lightning-pub-dev:latest"
if [[ ! -d "$worktree" ]]; then
error "Worktree not found: $worktree"
exit 1
fi
log "Building Lightning.Pub from $worktree..."
docker build -t "$image_name" "$worktree"
echo "$image_name"
}
wait_for_lightning_pub() {
log "Waiting for Lightning.Pub..."
local attempts=0
while ! is_lightning_pub_ready && [[ $attempts -lt 45 ]]; do
# Check for errors
if docker logs lamassu-lightning-pub 2>&1 | grep -q "Error:"; then
local err=$(docker logs lamassu-lightning-pub 2>&1 | grep "Error:" | tail -1)
error "Lightning.Pub error: $err"
return 1
fi
sleep 2
attempts=$((attempts + 1))
done
if is_lightning_pub_ready; then
sleep 2 # Extra time for Nostr middleware
success "Lightning.Pub is ready"
return 0
else
error "Lightning.Pub failed to start (timeout)"
docker logs lamassu-lightning-pub 2>&1 | tail -10
return 1
fi
}
extract_lightning_pub_info() {
local pubkey=$(docker logs lamassu-lightning-pub 2>&1 | grep -oP 'pubkey:\s*\K[a-f0-9]+' | tail -1)
local nprofile=$(docker logs lamassu-lightning-pub 2>&1 | grep -oP 'nprofile:\s*\K\S+' | tail -1)
echo "$pubkey" > "$PUBKEY_FILE"
echo "$nprofile" > "$NPROFILE_FILE"
echo "$pubkey"
}
#############################################################################
# ATM Configuration
#############################################################################
update_atm_env() {
local pubkey="$1"
local env_file="$PROJECT_DIR/apps/machine/.env"
if [[ ! -f "$env_file" ]]; then
warn "ATM .env not found, creating..."
cat > "$env_file" << EOF
# Lightning.Pub connection
VITE_RELAY_URL=ws://localhost:7777
VITE_LIGHTNING_PUB_PUBKEY=$pubkey
VITE_LIGHTNING_PUB_API_URL=http://localhost:1776
VITE_ADMIN_TOKEN=lamassu-dev-admin-token
VITE_ATM_PRIVATE_KEY=f391a2c3fc734f443b0f685688a0441b5fb9805853c0023f570c5a3c6412b136
# Extension API (for LNURL-withdraw)
VITE_EXTENSION_API_URL=http://localhost:1777
EOF
else
# Update existing pubkey
if grep -q "VITE_LIGHTNING_PUB_PUBKEY" "$env_file"; then
sed -i "s/VITE_LIGHTNING_PUB_PUBKEY=.*/VITE_LIGHTNING_PUB_PUBKEY=$pubkey/" "$env_file"
else
echo "VITE_LIGHTNING_PUB_PUBKEY=$pubkey" >> "$env_file"
fi
fi
success "Updated ATM .env with pubkey"
}
setup_atm_app() {
log "Creating ATM app..."
# Generate unique app name to avoid conflicts
local app_name="atm-$(date +%s)"
local response=$(curl -s -X POST http://localhost:1776/api/admin/app/add \
-H "Content-Type: application/json" \
-H "Authorization: Bearer lamassu-dev-admin-token" \
-d "{\"name\":\"$app_name\",\"allow_user_creation\":true}" 2>/dev/null)
if echo "$response" | grep -q '"status":"OK"'; then
local app_id=$(echo "$response" | grep -oP '"id":"\K[^"]+')
local app_token=$(echo "$response" | grep -oP '"auth_token":"\K[^"]+')
echo "$app_id" > "$APP_ID_FILE"
echo "$app_token" > "$APP_TOKEN_FILE"
# Update ATM .env with app ID
local env_file="$PROJECT_DIR/apps/machine/.env"
if [[ -f "$env_file" ]]; then
if grep -q "VITE_APP_ID" "$env_file"; then
sed -i "s/VITE_APP_ID=.*/VITE_APP_ID=$app_id/" "$env_file"
else
echo "" >> "$env_file"
echo "# ATM App ID for LNURL-withdraw" >> "$env_file"
echo "VITE_APP_ID=$app_id" >> "$env_file"
fi
fi
success "Created ATM app: ${app_id:0:16}..."
return 0
else
warn "Failed to create ATM app (may already exist)"
# Try to use existing app from state file
if [[ -f "$APP_ID_FILE" ]]; then
log "Using existing app ID from state"
return 0
fi
return 1
fi
}
#############################################################################
# Zeus Connection
#############################################################################
generate_lndconnect() {
local lnd_data="$REGTEST_DIR/data/lnd-3"
local local_ip=$(get_local_ip)
local rest_port=8082
local cert_path="$lnd_data/tls.cert"
local mac_path="$lnd_data/data/chain/bitcoin/regtest/admin.macaroon"
if [[ ! -f "$cert_path" ]] || [[ ! -f "$mac_path" ]]; then
return 1
fi
local cert_b64=$(base64 -w0 "$cert_path" | tr '+/' '-_' | tr -d '=')
local mac_b64=$(base64 -w0 "$mac_path" | tr '+/' '-_' | tr -d '=')
echo "lndconnect://${local_ip}:${rest_port}?cert=${cert_b64}&macaroon=${mac_b64}"
}
#############################################################################
# Display Functions
#############################################################################
get_atm_balance() {
# Get ATM app owner balance from Lightning.Pub logs
local app_id=$(cat "$APP_ID_FILE" 2>/dev/null)
if [[ -z "$app_id" ]]; then
echo "not configured"
return
fi
# Query the app balance via admin API (if available)
# For now, just indicate it's configured
local app_token=$(cat "$APP_TOKEN_FILE" 2>/dev/null)
if [[ -n "$app_token" ]]; then
echo "configured (use './dev.sh fund' to add sats)"
else
echo "not configured"
fi
}
show_status() {
local pubkey=$(cat "$PUBKEY_FILE" 2>/dev/null || docker logs lamassu-lightning-pub 2>&1 | grep -oP 'pubkey:\s*\K[a-f0-9]+' | tail -1)
local nprofile=$(cat "$NPROFILE_FILE" 2>/dev/null || docker logs lamassu-lightning-pub 2>&1 | grep -oP 'nprofile:\s*\K\S+' | tail -1)
local local_ip=$(get_local_ip)
local lndconnect=$(generate_lndconnect 2>/dev/null || echo "")
local lnd4_balance=$(get_lnd4_balance)
local app_id=$(cat "$APP_ID_FILE" 2>/dev/null || echo "not set")
echo ""
echo -e "${BOLD}╔═══════════════════════════════════════════════════════════════════╗${NC}"
echo -e "${BOLD}║ LAMASSU NEXT - DEVELOPMENT ENVIRONMENT ║${NC}"
echo -e "${BOLD}╚═══════════════════════════════════════════════════════════════════╝${NC}"
echo ""
echo -e "${CYAN}Lightning.Pub${NC}"
echo -e " Pubkey: ${GREEN}$pubkey${NC}"
echo -e " nprofile: ${GREEN}$nprofile${NC}"
echo ""
echo -e "${CYAN}Service URLs (local)${NC}"
echo " Nostr Relay: ws://localhost:7777"
echo " Lightning.Pub: http://localhost:1776"
echo " Withdraw API: http://localhost:1777"
echo ""
echo -e "${CYAN}External Access (LAN: $local_ip)${NC}"
echo " Nostr Relay: ws://${local_ip}:7777"
echo " Withdraw API: http://${local_ip}:1777"
echo ""
echo -e "${CYAN}lnd-4 (Lightning.Pub backend)${NC}"
echo " Balance: ${lnd4_balance} sats"
echo ""
echo -e "${CYAN}ATM App${NC}"
if [[ "$app_id" != "not set" ]]; then
echo " App ID: ${app_id:0:16}..."
echo " Status: $(get_atm_balance)"
else
echo " Status: not configured"
fi
echo ""
if [[ -n "$lndconnect" ]]; then
echo -e "${CYAN}Zeus Wallet (connect to lnd-3 for testing)${NC}"
echo -e " ${DIM}$lndconnect${NC}"
echo ""
fi
echo -e "${CYAN}Quick Commands${NC}"
echo " ./dev.sh logs lightning-pub # View Lightning.Pub logs"
echo " ./dev.sh fund 100000 # Fund ATM with 100k sats"
echo " ./dev.sh status # Show this info"
echo ""
echo -e "${BOLD}═══════════════════════════════════════════════════════════════════${NC}"
}
#############################################################################
# Main Commands
#############################################################################
cmd_up() {
local image="$DEFAULT_IMAGE"
local worktree=""
local skip_regtest=false
local auto_fund=false
local fund_amount="$DEFAULT_FUNDING_SATS"
# Parse arguments
while [[ $# -gt 0 ]]; do
case "$1" in
--image) image="$2"; shift 2 ;;
--worktree) worktree="$2"; shift 2 ;;
--skip-regtest) skip_regtest=true; shift ;;
--fund) auto_fund=true; shift ;;
--fund=*) auto_fund=true; fund_amount="${1#*=}"; shift ;;
*) shift ;;
esac
done
echo ""
log "Starting Lamassu development environment..."
echo ""
# 1. Start regtest if needed
if [[ "$skip_regtest" != "true" ]]; then
start_regtest || exit 1
else
if ! is_regtest_running; then
error "Regtest not running. Remove --skip-regtest or start it manually."
exit 1
fi
fi
# 2. Build from worktree if specified
if [[ -n "$worktree" ]]; then
image=$(build_from_worktree "$worktree")
fi
# 3. Check image exists
if ! docker image inspect "$image" &>/dev/null; then
error "Docker image not found: $image"
echo ""
echo "Options:"
echo " 1. Build from worktree: ./dev.sh up --worktree ~/path/to/lightning-pub"
echo " 2. Build manually: docker build -t $image ~/path/to/lightning-pub"
exit 1
fi
log "Using Lightning.Pub image: $image"
# 4. Start lamassu services
export REGTEST_DATA_DIR="$REGTEST_DIR/data"
export LIGHTNING_PUB_IMAGE="$image"
export HOST_IP=$(get_local_ip)
log "Starting lamassu services..."
docker compose -f "$SCRIPT_DIR/docker-compose.regtest.yml" up -d
# 5. Wait for Lightning.Pub
if ! wait_for_lightning_pub; then
error "Failed to start. Check: ./dev.sh logs lightning-pub"
exit 1
fi
# 6. Extract and save Lightning.Pub info
local pubkey=$(extract_lightning_pub_info)
# 7. Update ATM .env
update_atm_env "$pubkey"
# 8. Setup ATM app
setup_atm_app || true
# 9. Auto-fund if requested
if [[ "$auto_fund" == "true" ]]; then
echo ""
log "Auto-funding ATM with $fund_amount sats..."
sleep 2 # Give Lightning.Pub a moment to settle
cmd_fund "$fund_amount" || warn "Auto-funding failed. Run './dev.sh fund' manually."
fi
# 10. Show status
show_status
}
cmd_down() {
log "Stopping lamassu services..."
docker compose -f "$SCRIPT_DIR/docker-compose.regtest.yml" down 2>/dev/null || true
if [[ "$1" == "--all" ]]; then
stop_regtest
fi
success "Services stopped"
}
cmd_reset() {
warn "This will delete all lamassu state (Lightning.Pub identity, ATM app, etc.)"
read -p "Continue? [y/N] " -n 1 -r
echo
if [[ ! $REPLY =~ ^[Yy]$ ]]; then
echo "Aborted"
exit 0
fi
log "Stopping services..."
docker compose -f "$SCRIPT_DIR/docker-compose.regtest.yml" down -v 2>/dev/null || true
log "Removing state files..."
rm -rf "$STATE_DIR"
mkdir -p "$STATE_DIR"
success "Reset complete. Run './dev.sh up' for fresh start."
}
cmd_logs() {
docker compose -f "$SCRIPT_DIR/docker-compose.regtest.yml" logs -f "$@"
}
cmd_status() {
if ! docker ps --format '{{.Names}}' | grep -q lamassu-lightning-pub; then
error "Services not running. Start with: ./dev.sh up"
exit 1
fi
show_status
}
cmd_fund() {
local amount="${1:-$DEFAULT_FUNDING_SATS}"
if ! is_regtest_running; then
error "Regtest not running"
exit 1
fi
# Check for app token
if [[ ! -f "$APP_TOKEN_FILE" ]]; then
error "ATM app not configured. Run './dev.sh up' first."
exit 1
fi
local app_token=$(cat "$APP_TOKEN_FILE")
log "Creating invoice for $amount sats..."
# Create invoice for app owner (using unique payer_identifier)
local payer_id="funder-$(date +%s)"
local response=$(curl -s -X POST "http://localhost:1776/api/app/add/invoice" \
-H "Authorization: Bearer $app_token" \
-H "Content-Type: application/json" \
-d "{\"payer_identifier\": \"$payer_id\", \"http_callback_url\": \"\", \"invoice_req\": {\"amountSats\": $amount, \"memo\": \"ATM funding\"}}")
local invoice=$(echo "$response" | grep -oP '"invoice":"\K[^"]+')
if [[ -z "$invoice" ]]; then
error "Failed to create invoice"
echo "Response: $response"
exit 1
fi
log "Paying invoice from lnd-3..."
# Source regtest helpers and pay
if [[ -f "$REGTEST_DIR/docker-scripts.sh" ]]; then
(
cd "$REGTEST_DIR"
source docker-scripts.sh 2>/dev/null
lncli-sim 3 payinvoice --force "$invoice"
)
if [[ $? -eq 0 ]]; then
success "Funded ATM with $amount sats"
else
error "Payment failed"
exit 1
fi
else
# Fallback: try direct docker exec
docker exec lnbits-lnd-3-1 lncli --network=regtest --rpcserver=lnd-3:10009 payinvoice --force "$invoice"
if [[ $? -eq 0 ]]; then
success "Funded ATM with $amount sats"
else
error "Payment failed. Make sure lnd-3 has funds and channels."
exit 1
fi
fi
}
cmd_mine() {
local blocks="${1:-1}"
if ! is_regtest_running; then
error "Regtest not running"
exit 1
fi
log "Mining $blocks block(s)..."
docker exec lnbits-bitcoind-1 bitcoin-cli -regtest -generate "$blocks" > /dev/null
local height=$(docker exec lnbits-bitcoind-1 bitcoin-cli -regtest getblockcount)
success "Mined $blocks block(s) (height: $height)"
}
cmd_atm() {
local atm_dir="$PROJECT_DIR/apps/machine"
if [[ ! -d "$atm_dir" ]]; then
error "ATM app not found at $atm_dir"
exit 1
fi
# Check if services are running
if ! docker ps --format '{{.Names}}' | grep -q lamassu-lightning-pub; then
warn "Services not running. Start with: ./dev.sh up"
read -p "Start services first? [Y/n] " -n 1 -r
echo
if [[ ! $REPLY =~ ^[Nn]$ ]]; then
cmd_up
fi
fi
log "Starting ATM application..."
echo ""
echo -e "${CYAN}ATM Mock Mode:${NC}"
echo " - Press 'b' to insert a bill (simulates cash insertion)"
echo " - Use the UI to complete transactions"
echo ""
cd "$atm_dir" && pnpm dev
}
#############################################################################
# Entry Point
#############################################################################
case "${1:-help}" in
up|start)
shift
cmd_up "$@"
;;
down|stop)
shift
cmd_down "$@"
;;
reset)
cmd_reset
;;
logs)
shift
cmd_logs "$@"
;;
status|info)
cmd_status
;;
fund)
shift
cmd_fund "$@"
;;
mine)
shift
cmd_mine "$@"
;;
atm)
cmd_atm
;;
*)
echo "Lamassu Next Development Environment"
echo ""
echo "Usage: $0 <command> [options]"
echo ""
echo "Commands:"
echo " up [options] Start development environment"
echo " down [--all] Stop services (--all includes regtest)"
echo " atm Launch ATM application (Electron)"
echo " status Show connection info"
echo " logs [service] Follow service logs"
echo " fund [sats] Fund ATM account"
echo " mine [blocks] Mine blocks manually (default: 1)"
echo " reset Delete all state and start fresh"
echo ""
echo "Note: Auto-miner runs in background (1 block/2min). Check with:"
echo " ./dev.sh logs miner"
echo ""
echo "Options for 'up':"
echo " --worktree <path> Build Lightning.Pub from git worktree"
echo " --image <name> Use specific Docker image"
echo " --skip-regtest Don't auto-start regtest"
echo " --fund Auto-fund ATM with 100k sats after startup"
echo " --fund=<sats> Auto-fund ATM with specific amount"
echo ""
echo "Examples:"
echo " $0 up # Start with default image"
echo " $0 up --fund # Start and fund ATM"
echo " $0 up --fund=50000 # Start and fund with 50k sats"
echo " $0 up --worktree ~/dev/lightning-pub/withdraw"
echo " $0 atm # Launch ATM app"
echo " $0 fund 200000 # Add 200k more sats"
echo " $0 logs lightning-pub"
echo " $0 reset && $0 up --fund # Fresh start with funding"
;;
esac