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

1
docker/.state/atm-app-id Normal file
View file

@ -0,0 +1 @@
02f5340554b29537f4b9c3bb6c131f69c08044b2114939849d3bec8c4df456e7

View file

@ -0,0 +1 @@
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhcHBJZCI6IjAyZjUzNDA1NTRiMjk1MzdmNGI5YzNiYjZjMTMxZjY5YzA4MDQ0YjIxMTQ5Mzk4NDlkM2JlYzhjNGRmNDU2ZTciLCJpYXQiOjE3NzExODIwMTZ9.uE473GYJdB946daNCZ-5PqPe1JGihSM6KnL66n1AN7k

View file

@ -0,0 +1 @@
nprofile1qyg8wue69uhhxarjvee8jw3hxumnwqpq35fnkv4nguyhrutu4tspkjlfaqs95pnnegzqkg24h040vn3852jshankcx

View file

@ -0,0 +1 @@
8d133b32b3470971f17caae01b4be9e8205a0673ca040b2155bbeaf64e27a2a5

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

View file

@ -0,0 +1,226 @@
# Lamassu Next Development Infrastructure
# Usage: docker compose -f docker-compose.dev.yml up -d
services:
# Private Nostr relay for ATM communication
strfry:
image: ghcr.io/hoytech/strfry:latest
container_name: lamassu-relay
ports:
- '7777:7777'
volumes:
- ./strfry.conf:/etc/strfry.conf:ro
- strfry-data:/app/strfry-db
ulimits:
nofile:
soft: 524288
hard: 524288
healthcheck:
test: ['CMD', 'nc', '-z', 'localhost', '7777']
interval: 10s
timeout: 5s
retries: 5
restart: unless-stopped
# Bitcoin Core in regtest mode
bitcoind:
image: lncm/bitcoind:v27.0
container_name: lamassu-bitcoind
volumes:
- bitcoind-data:/data/.bitcoin
environment:
BITCOIN_NETWORK: regtest
command:
- -regtest
- -server
- -rpcuser=lamassu
- -rpcpassword=lamassu
- -rpcallowip=0.0.0.0/0
- -rpcbind=0.0.0.0
- -zmqpubrawblock=tcp://0.0.0.0:28332
- -zmqpubrawtx=tcp://0.0.0.0:28333
- -fallbackfee=0.00001
- -txindex=1
ports:
- '18443:18443' # RPC
- '28332:28332' # ZMQ blocks
- '28333:28333' # ZMQ tx
healthcheck:
test:
[
'CMD',
'bitcoin-cli',
'-regtest',
'-rpcuser=lamassu',
'-rpcpassword=lamassu',
'getblockchaininfo',
]
interval: 10s
timeout: 5s
retries: 10
restart: unless-stopped
# LND Lightning node
lnd:
image: lightninglabs/lnd:v0.18.0-beta
container_name: lamassu-lnd
depends_on:
bitcoind:
condition: service_healthy
volumes:
- lnd-data:/root/.lnd
environment:
- NETWORK=regtest
command:
- --bitcoin.active
- --bitcoin.regtest
- --bitcoin.node=bitcoind
- --bitcoind.rpchost=bitcoind:18443
- --bitcoind.rpcuser=lamassu
- --bitcoind.rpcpass=lamassu
- --bitcoind.zmqpubrawblock=tcp://bitcoind:28332
- --bitcoind.zmqpubrawtx=tcp://bitcoind:28333
- --rpclisten=0.0.0.0:10009
- --restlisten=0.0.0.0:8080
- --tlsextradomain=lnd
- --tlsextraip=0.0.0.0
- --noseedbackup
- --accept-keysend
- --accept-amp
ports:
- '10009:10009' # gRPC
- '8080:8080' # REST
- '9735:9735' # P2P
healthcheck:
test: ['CMD', 'lncli', '--network=regtest', 'getinfo']
interval: 10s
timeout: 10s
retries: 30
start_period: 30s
restart: unless-stopped
# Lightning.Pub - Nostr-native Lightning account system
# Note: Lightning.Pub connects to LND for actual Lightning operations
# Using patched image with Kind 0 profile publishing support
lightning-pub:
image: lightning-pub-patched:latest
container_name: lamassu-lightning-pub
depends_on:
lnd:
condition: service_healthy
extra_hosts:
- 'host.docker.internal:host-gateway' # Enable host.docker.internal on Linux
ports:
- '1776:1776'
volumes:
- lightning-pub-data:/root/lightning_pub
- lnd-data:/root/.lnd:ro # Read-only access to LND data for macaroons/certs
environment:
- NETWORK=regtest
- LND_ADDRESS=lnd:10009
- LND_CERT_PATH=/root/.lnd/tls.cert
- LND_MACAROON_PATH=/root/.lnd/data/chain/bitcoin/regtest/admin.macaroon
# Relay URLs (space-separated). Docker-internal relay FIRST for publishing,
# then backup. Mock ATM rewrites ndebit relay for browser access.
- NOSTR_RELAYS=ws://strfry:7777
# Disable external liquidity provider for regtest
- DISABLE_LIQUIDITY_PROVIDER=true
# Admin token for HTTP API access (development only)
- ADMIN_TOKEN=lamassu-dev-admin-token
restart: unless-stopped
# Second LND node (Alice) for payment testing
# This node can pay invoices to Lightning.Pub's LND
lnd-alice:
image: lightninglabs/lnd:v0.18.0-beta
container_name: lamassu-lnd-alice
depends_on:
bitcoind:
condition: service_healthy
volumes:
- lnd-alice-data:/root/.lnd
environment:
- NETWORK=regtest
command:
- --bitcoin.active
- --bitcoin.regtest
- --bitcoin.node=bitcoind
- --bitcoind.rpchost=bitcoind:18443
- --bitcoind.rpcuser=lamassu
- --bitcoind.rpcpass=lamassu
- --bitcoind.zmqpubrawblock=tcp://bitcoind:28332
- --bitcoind.zmqpubrawtx=tcp://bitcoind:28333
- --rpclisten=0.0.0.0:10009
- --restlisten=0.0.0.0:8080
- --tlsextradomain=lnd-alice
- --tlsextraip=0.0.0.0
- --noseedbackup
- --accept-keysend
- --accept-amp
- --alias=alice
ports:
- '10010:10009' # gRPC (different host port)
- '8081:8080' # REST (different host port)
- '9736:9735' # P2P (different host port)
healthcheck:
test: ['CMD', 'lncli', '--network=regtest', 'getinfo']
interval: 10s
timeout: 10s
retries: 30
start_period: 30s
restart: unless-stopped
# Automatic block miner for regtest (keeps LND synced)
# Mines 1 block every 30 seconds
miner:
image: alpine:latest
container_name: lamassu-miner
depends_on:
bitcoind:
condition: service_healthy
entrypoint: /bin/sh
command:
- -c
- |
apk add --no-cache curl jq
echo "Starting auto-miner (1 block every 30 seconds)..."
while true; do
# Create wallet if not exists
curl -s --user lamassu:lamassu --data-binary '{"jsonrpc":"1.0","method":"createwallet","params":["miner"]}' http://bitcoind:18443/ > /dev/null 2>&1
# Mine a block
ADDR=$$(curl -s --user lamassu:lamassu --data-binary '{"jsonrpc":"1.0","method":"getnewaddress","params":[]}' http://bitcoind:18443/ | jq -r '.result // empty')
if [ -n "$$ADDR" ]; then
curl -s --user lamassu:lamassu --data-binary "{\"jsonrpc\":\"1.0\",\"method\":\"generatetoaddress\",\"params\":[1,\"$$ADDR\"]}" http://bitcoind:18443/ > /dev/null
fi
sleep 30
done
restart: unless-stopped
profiles:
- mining # Only starts with: docker compose --profile mining up -d
# PostgreSQL for optional server-side state
postgres:
image: postgres:16-alpine
container_name: lamassu-postgres
ports:
- '5432:5432'
environment:
POSTGRES_DB: lamassu_dev
POSTGRES_USER: lamassu
POSTGRES_PASSWORD: lamassu_dev_password
volumes:
- postgres-data:/var/lib/postgresql/data
healthcheck:
test: ['CMD-SHELL', 'pg_isready -U lamassu -d lamassu_dev']
interval: 10s
timeout: 5s
retries: 5
restart: unless-stopped
volumes:
strfry-data:
bitcoind-data:
lnd-data:
lnd-alice-data:
lightning-pub-data:
postgres-data:

View file

@ -0,0 +1,148 @@
# Lamassu Next - Regtest Integration
#
# This overlay connects lamassu-next services to the comprehensive regtest
# environment at ~/dev/local/docker/regtest
#
# Usage:
# 1. Start the regtest environment:
# cd ~/dev/local/docker/regtest && ./start-regtest
#
# 2. Start lamassu services:
# cd lamassu-next/docker && docker compose -f docker-compose.regtest.yml up -d
#
# 3. Configure apps/machine/.env:
# VITE_RELAY_URL=ws://localhost:7777
# VITE_EXTENSION_API_URL=http://localhost:1777
#
# Services:
# - strfry: Private Nostr relay (port 7777)
# - lightning-pub: Nostr-native Lightning account system (port 1776)
# - Uses lnd-4 from regtest as backend
# - Withdraw extension on port 1777
# - miner: Auto-mines blocks to keep Lightning channels active
#
services:
# Private Nostr relay for ATM communication
strfry:
image: ghcr.io/hoytech/strfry:latest
container_name: lamassu-relay
ports:
- '7777:7777'
volumes:
- ./strfry.conf:/etc/strfry.conf:ro
- strfry-data:/app/strfry-db
ulimits:
nofile:
soft: 524288
hard: 524288
healthcheck:
test: ['CMD', 'nc', '-z', 'localhost', '7777']
interval: 10s
timeout: 5s
retries: 5
restart: unless-stopped
networks:
- regtest
# Lightning.Pub - Nostr-native Lightning account system
# Connects to lnd-4 from the regtest environment
# Use LIGHTNING_PUB_IMAGE env var to specify image (default: lightning-pub-withdraw)
lightning-pub:
image: ${LIGHTNING_PUB_IMAGE:-lightning-pub-withdraw:latest}
container_name: lamassu-lightning-pub
extra_hosts:
- 'host.docker.internal:host-gateway'
ports:
- '1776:1776'
- '1777:1777' # Withdraw extension HTTP API
volumes:
- lightning-pub-data:/root/lightning_pub
# Override Dockerfile's anonymous /app/data volume with named volume
- lightning-pub-appdata:/app/data
# Mount lnd-4 data from regtest for macaroons/certs
- ${REGTEST_DATA_DIR:-/home/padreug/dev/local/docker/regtest/data}/lnd-4:/root/.lnd:ro
environment:
- NETWORK=regtest
# lnd-4 is accessible via Docker network
- LND_ADDRESS=lnd-4:10009
- LND_CERT_PATH=/root/.lnd/tls.cert
- LND_MACAROON_PATH=/root/.lnd/data/chain/bitcoin/regtest/admin.macaroon
# Use strfry from this compose
- NOSTR_RELAYS=ws://strfry:7777
# Disable external liquidity provider for regtest
- DISABLE_LIQUIDITY_PROVIDER=true
# Admin token for HTTP API access (development only)
- 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
restart: unless-stopped
networks:
- regtest
# Auto-miner for regtest (mines 1 block every MINE_INTERVAL seconds)
# Keeps Lightning channels active during development
miner:
image: boltz/bitcoin-core:25.0
container_name: lamassu-miner
entrypoint: /bin/sh
command:
- -c
- |
echo "Auto-miner started (interval: $${MINE_INTERVAL}s)"
# Wait for bitcoind to be ready
while ! bitcoin-cli -regtest -rpcconnect=bitcoind getblockchaininfo > /dev/null 2>&1; do
echo "Waiting for bitcoind..."
sleep 5
done
echo "bitcoind ready, starting mining loop"
while true; do
bitcoin-cli -regtest -rpcconnect=bitcoind -generate 1 > /dev/null 2>&1 && echo "Mined block $(bitcoin-cli -regtest -rpcconnect=bitcoind getblockcount)"
sleep $${MINE_INTERVAL}
done
environment:
- MINE_INTERVAL=${MINE_INTERVAL:-120}
volumes:
- bitcoin-data:/root/.bitcoin
restart: unless-stopped
networks:
- regtest
# PostgreSQL for optional server-side state
postgres:
image: postgres:16-alpine
container_name: lamassu-postgres
ports:
- '5432:5432'
environment:
POSTGRES_DB: lamassu_dev
POSTGRES_USER: lamassu
POSTGRES_PASSWORD: lamassu_dev_password
volumes:
- postgres-data:/var/lib/postgresql/data
healthcheck:
test: ['CMD-SHELL', 'pg_isready -U lamassu -d lamassu_dev']
interval: 10s
timeout: 5s
retries: 5
restart: unless-stopped
networks:
- regtest
volumes:
strfry-data:
lightning-pub-data:
lightning-pub-appdata:
postgres-data:
# Mount the bitcoin-data volume from the regtest environment
bitcoin-data:
external: true
name: lnbits_bitcoin-data
networks:
regtest:
# The regtest environment uses 'lnbits_default' as its Docker network
# (named after the original LNbits regtest project)
name: lnbits_default
external: true

335
docker/start-with-regtest.sh Executable file
View file

@ -0,0 +1,335 @@
#!/bin/bash
#
# Start lamassu-next development environment with comprehensive regtest
#
# This script:
# 1. Connects to the regtest environment at ~/dev/local/docker/regtest
# 2. Starts Lightning.Pub with the specified image/worktree
# 3. Creates and funds an ATM account
# 4. Displays connection info for Zeus wallet and Lightning.Pub nprofile
#
# Prerequisites:
# ~/dev/local/docker/regtest must be running:
# cd ~/dev/local/docker/regtest && ./start-regtest
#
# Usage:
# ./start-with-regtest.sh # Start with default image
# ./start-with-regtest.sh --image myimage # Use specific Docker image
# ./start-with-regtest.sh --worktree ~/path # Build from worktree
# ./start-with-regtest.sh down # Stop services
# ./start-with-regtest.sh logs # Follow logs
# ./start-with-regtest.sh status # Show connection info
#
set -e
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
REGTEST_DIR="${REGTEST_DIR:-$HOME/dev/local/docker/regtest}"
DEFAULT_IMAGE="lightning-pub-withdraw:latest"
ATM_FUNDING_SATS=100000
# Colors
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
RED='\033[0;31m'
BLUE='\033[0;34m'
CYAN='\033[0;36m'
BOLD='\033[1m'
NC='\033[0m'
log() { echo -e "${GREEN}[lamassu]${NC} $1"; }
warn() { echo -e "${YELLOW}[lamassu]${NC} $1"; }
error() { echo -e "${RED}[lamassu]${NC} $1"; }
info() { echo -e "${CYAN}[lamassu]${NC} $1"; }
# Get local IP for external wallet access
get_local_ip() {
ip route get 1 2>/dev/null | awk '{print $7; exit}' || hostname -I | awk '{print $1}'
}
# Check if regtest is running
check_regtest() {
if ! docker network inspect lnbits_default &>/dev/null; then
error "Regtest network not found!"
echo ""
echo "Start the regtest environment first:"
echo " cd $REGTEST_DIR && ./start-regtest"
exit 1
fi
# Check if lnd-4 is running (Lightning.Pub's backend)
if ! docker exec lnbits-lnd-4-1 lncli --network=regtest getinfo &>/dev/null 2>&1; then
warn "lnd-4 not responding. Waiting..."
sleep 5
fi
}
# Build Lightning.Pub from worktree
build_from_worktree() {
local worktree="$1"
local image_name="lightning-pub-custom: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 to be ready and get nprofile
wait_for_lightning_pub() {
log "Waiting for Lightning.Pub to start..."
local max_attempts=30
local attempt=0
while [[ $attempt -lt $max_attempts ]]; do
if docker logs lamassu-lightning-pub 2>&1 | grep -q "LightningPub listening"; then
sleep 2 # Extra time for Nostr middleware
return 0
fi
attempt=$((attempt + 1))
sleep 2
done
error "Lightning.Pub failed to start within 60 seconds"
docker logs lamassu-lightning-pub 2>&1 | tail -20
return 1
}
# Get Lightning.Pub nprofile from logs
get_nprofile() {
docker logs lamassu-lightning-pub 2>&1 | grep -oP 'nprofile:\s*\K\S+' | tail -1
}
# Get Lightning.Pub pubkey from logs
get_pubkey() {
docker logs lamassu-lightning-pub 2>&1 | grep -oP 'pubkey:\s*\K[a-f0-9]+' | tail -1
}
# Generate lndconnect URL for Zeus (using lnd-3 which has REST exposed)
generate_lndconnect() {
local lnd_container="lnbits-lnd-3-1"
local lnd_data="$REGTEST_DIR/data/lnd-3"
local local_ip=$(get_local_ip)
local rest_port=8082 # lnd-3's REST port
# Get cert and macaroon
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
warn "lnd-3 credentials not found. Zeus connection string unavailable."
return 1
fi
# Base64url encode (replace + with -, / with _, remove =)
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}"
}
# Create and fund ATM account
setup_atm_account() {
log "Setting up ATM account..."
# Create app
local app_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":"atm-app","allow_user_creation":true}' 2>/dev/null)
if echo "$app_response" | grep -q '"status":"OK"'; then
local app_id=$(echo "$app_response" | grep -oP '"id":"\K[^"]+')
local app_token=$(echo "$app_response" | grep -oP '"auth_token":"\K[^"]+')
log "Created ATM app: $app_id"
# Store app token for later use
echo "$app_token" > "$SCRIPT_DIR/.atm-app-token"
echo "$app_id" > "$SCRIPT_DIR/.atm-app-id"
# TODO: Fund the app by creating an invoice and paying from lnd-3
# This requires the app to support invoice creation via HTTP API
# For now, the app starts with 0 balance
return 0
else
warn "Failed to create ATM app: $app_response"
return 1
fi
}
# Display connection information
show_connection_info() {
local nprofile=$(get_nprofile)
local pubkey=$(get_pubkey)
local local_ip=$(get_local_ip)
local lndconnect=$(generate_lndconnect 2>/dev/null || echo "")
echo ""
echo -e "${BOLD}═══════════════════════════════════════════════════════════════${NC}"
echo -e "${BOLD} LAMASSU REGTEST 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:${NC}"
echo " Nostr Relay: ws://localhost:7777"
echo " Lightning.Pub: http://localhost:1776"
echo " Withdraw API: http://localhost:1777"
echo " PostgreSQL: localhost:5432"
echo ""
echo -e "${CYAN}External Access (from phone/other devices):${NC}"
echo " Nostr Relay: ws://${local_ip}:7777"
echo " Withdraw API: http://${local_ip}:1777"
echo ""
if [[ -n "$lndconnect" ]]; then
echo -e "${CYAN}Zeus Wallet Connection (lnd-3):${NC}"
echo -e " ${GREEN}$lndconnect${NC}"
echo ""
echo " Scan this with Zeus to connect to lnd-3 for testing payments."
echo ""
fi
echo -e "${CYAN}ATM Configuration (apps/machine/.env):${NC}"
echo " VITE_RELAY_URL=ws://localhost:7777"
echo " VITE_LIGHTNING_PUB_PUBKEY=$pubkey"
echo " VITE_EXTENSION_API_URL=http://localhost:1777"
echo ""
echo -e "${CYAN}CLI Helpers:${NC}"
echo " source $REGTEST_DIR/docker-scripts.sh"
echo " bitcoin-cli-sim -generate 1 # Mine blocks"
echo " lncli-sim 4 getinfo # lnd-4 (Lightning.Pub)"
echo " lncli-sim 3 getinfo # lnd-3 (Zeus wallet)"
echo ""
echo -e "${BOLD}═══════════════════════════════════════════════════════════════${NC}"
}
# Start services
start() {
local image="$DEFAULT_IMAGE"
local worktree=""
# Parse arguments
while [[ $# -gt 0 ]]; do
case "$1" in
--image)
image="$2"
shift 2
;;
--worktree)
worktree="$2"
shift 2
;;
*)
shift
;;
esac
done
log "Checking prerequisites..."
check_regtest
# Build from worktree if specified
if [[ -n "$worktree" ]]; then
image=$(build_from_worktree "$worktree")
fi
# Check if image exists
if ! docker image inspect "$image" &>/dev/null; then
error "Docker image not found: $image"
echo ""
echo "Either:"
echo " 1. Build the image: docker build -t $image <path>"
echo " 2. Use --worktree: ./start-with-regtest.sh --worktree ~/path/to/lightning-pub"
exit 1
fi
log "Using Lightning.Pub image: $image"
# Export environment variables
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
# Wait for Lightning.Pub and setup
if wait_for_lightning_pub; then
setup_atm_account || true
show_connection_info
else
error "Failed to start Lightning.Pub. Check logs with: $0 logs lightning-pub"
exit 1
fi
}
# Stop services
stop() {
log "Stopping lamassu services..."
docker compose -f "$SCRIPT_DIR/docker-compose.regtest.yml" down
rm -f "$SCRIPT_DIR/.atm-app-token" "$SCRIPT_DIR/.atm-app-id"
log "Services stopped."
}
# Show logs
logs() {
docker compose -f "$SCRIPT_DIR/docker-compose.regtest.yml" logs -f "$@"
}
# Show status/connection info
status() {
if ! docker ps --format '{{.Names}}' | grep -q lamassu-lightning-pub; then
error "Services not running. Start with: $0 start"
exit 1
fi
show_connection_info
}
# Main
case "${1:-start}" in
start)
shift || true
start "$@"
;;
stop|down)
stop
;;
logs)
shift
logs "$@"
;;
status|info)
status
;;
*)
echo "Usage: $0 [command] [options]"
echo ""
echo "Commands:"
echo " start Start services (default)"
echo " stop, down Stop services"
echo " logs [service] Follow logs"
echo " status, info Show connection info"
echo ""
echo "Options for 'start':"
echo " --image <name> Use specific Docker image"
echo " --worktree <path> Build from Lightning.Pub worktree"
echo ""
echo "Examples:"
echo " $0 # Start with default image"
echo " $0 --worktree ~/dev/lightning-pub/withdraw # Build and use worktree"
echo " $0 --image lightning-pub-custom:v1 # Use specific image"
exit 1
;;
esac

65
docker/strfry.conf Normal file
View file

@ -0,0 +1,65 @@
##
## strfry configuration for Lamassu ATM development
##
relay {
bind = "0.0.0.0"
port = 7777
# Set to 0 to skip configuring nofiles limit (avoids container ulimit issues)
nofiles = 0
info {
name = "Lamassu Dev Relay"
description = "Private Nostr relay for ATM development and testing"
pubkey = ""
contact = ""
}
# Maximum message size in bytes
maxWebsocketPayloadSize = 131072
# Connection limits
maxWebsockets = 100
maxConnections = 1000
}
# Event policies
events {
# Maximum event size
maxEventSize = 65536
# Rate limiting
rejectEventsNewerThanSeconds = 900
rejectEventsOlderThanSeconds = 94608000
# Event kinds we care about
# 14: Private DMs (NIP-17)
# 21000: Lightning.Pub RPC (generic request/response)
# 21001-21003: CLINK events (Offer, Debit, Manage)
# 22242: NIP-42 auth
# 30078: Service Beacon (replaceable, service discovery)
# 30079: Transaction records (replaceable)
# TODO: Review if these ephemeral event settings actually do anything useful.
# CLINK events (21000-21003) are in the ephemeral range but need to persist
# long enough for request/response flows. These settings may not be recognized
# by strfry - verify against strfry documentation.
# Allow ephemeral events up to 5 minutes old to be accepted
rejectEphemeralEventsOlderThanSeconds = 300
# Keep ephemeral events for 5 minutes before deletion
ephemeralEventsLifetimeSeconds = 300
}
# Negentropy sync (for relay federation)
negentropy {
enabled = true
syncOnConnect = false
}
# Plugins (for NIP-42 auth in production)
# plugins {
# authRequired = true
# writePolicy = "accept"
# readPolicy = "accept"
# }