bitspire/docker/dev.sh
Patrick Mulligan f37555565e feat(machine): add SQLite persistence and remove npub linking
Add crash-safe persistence for cassette inventory, cashbox state, and
transaction history using better-sqlite3 in the Electron main process.
The state machine now loads inventory from the database at runtime
instead of using hardcoded values, and transactions are automatically
persisted on completion.

Remove the unnecessary npub linking code — Lightning.Pub auto-creates
and associates Nostr users when appId is included in RPC requests,
making the HTTP-based user creation and token linking redundant.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-19 11:45:09 -05:00

1087 lines
38 KiB
Bash
Executable file

#!/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 (uses shared regtest)
# ./dev.sh up --standalone # Start self-contained (own bitcoind/lnd)
# ./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 --machine # Start and launch ATM app
# ./dev.sh up --fund --machine # Start, fund, and launch ATM
# ./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_ID_FILE="$STATE_DIR/atm-app-id"
MODE_FILE="$STATE_DIR/compose-mode" # "standalone" or "unified"
# Get current compose file based on last used mode
get_compose_file() {
local mode=$(cat "$MODE_FILE" 2>/dev/null || echo "unified")
if [[ "$mode" == "standalone" ]]; then
echo "docker-compose.dev.yml"
else
echo "docker-compose.regtest.yml"
fi
}
# 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"
# Get ATM pubkey from its private key
get_atm_pubkey() {
local env_file="$PROJECT_DIR/apps/machine/.env"
if [[ ! -f "$env_file" ]]; then
return 1
fi
local privkey=$(grep "VITE_ATM_PRIVATE_KEY=" "$env_file" | cut -d= -f2)
if [[ -z "$privkey" ]]; then
return 1
fi
# Use node with @noble/curves to derive pubkey from privkey
cd "$PROJECT_DIR" && node -e "
const { secp256k1 } = require('./node_modules/.pnpm/@noble+curves@2.0.1/node_modules/@noble/curves/secp256k1.js');
const privkeyHex = '$privkey';
const privkey = Uint8Array.from(Buffer.from(privkeyHex, 'hex'));
const pubkeyFull = secp256k1.getPublicKey(privkey, true);
const pubkey = Buffer.from(pubkeyFull.slice(1)).toString('hex');
console.log(pubkey);
" 2>/dev/null
}
#############################################################################
# Helper Functions
#############################################################################
get_local_ip() {
# Allow explicit override via env var
if [[ -n "${LAMASSU_HOST_IP:-}" ]]; then
echo "$LAMASSU_HOST_IP"
return
fi
# Linux: use ip route
if command -v ip &>/dev/null; then
local ip=$(ip route get 1 2>/dev/null | awk '{print $7; exit}')
if [[ -n "$ip" ]]; then
echo "$ip"
return
fi
fi
# macOS: use route + ifconfig
if [[ "$(uname)" == "Darwin" ]]; then
local iface=$(route get default 2>/dev/null | awk '/interface:/ {print $2}')
if [[ -n "$iface" ]]; then
local ip=$(ifconfig "$iface" 2>/dev/null | awk '/inet / {print $2}')
if [[ -n "$ip" ]]; then
echo "$ip"
return
fi
fi
fi
# Fallback: hostname -I (Linux) or hostname (macOS)
hostname -I 2>/dev/null | awk '{print $1}' || hostname 2>/dev/null
}
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
}
lnd4_has_channels() {
local channel_count=$(docker exec lnbits-lnd-4-1 lncli --network=regtest --rpcserver=lnd-4:10009 listchannels 2>/dev/null | grep -c '"chan_id"' || echo "0")
[[ "$channel_count" -gt 0 ]]
}
lnd4_has_active_channels() {
docker exec lnbits-lnd-4-1 lncli --network=regtest --rpcserver=lnd-4:10009 listchannels 2>/dev/null | grep -q '"active": true'
}
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 --rpcserver=lnd-4:10009 walletbalance 2>/dev/null | grep -oP '"total_balance":\s*"\K[0-9]+' || echo "0"
}
get_lnd4_channel_balance() {
docker exec lnbits-lnd-4-1 lncli --network=regtest --rpcserver=lnd-4:10009 channelbalance 2>/dev/null | grep -oP '"local_balance"[^}]*"sat":\s*"\K[0-9]+' || echo "0"
}
#############################################################################
# Regtest Management
#############################################################################
is_lnd1_ready() {
# Check if lnd-1 is fully ready (can make RPC calls)
docker exec lnbits-lnd-1-1 lncli --network=regtest --rpcserver=lnd-1:10009 getinfo &>/dev/null 2>&1
}
is_lnd1_wallet_ready() {
# Check if lnd-1 wallet is ready (can query balance - needed for channel operations)
docker exec lnbits-lnd-1-1 lncli --network=regtest --rpcserver=lnd-1:10009 walletbalance &>/dev/null 2>&1
}
wait_for_lnd1_wallet() {
local attempts=0
while ! is_lnd1_wallet_ready && [[ $attempts -lt 30 ]]; do
sleep 2
attempts=$((attempts + 1))
done
is_lnd1_wallet_ready
}
wait_for_lnd1() {
log "Waiting for lnd-1 to be ready..."
local attempts=0
while ! is_lnd1_ready && [[ $attempts -lt 30 ]]; do
sleep 2
attempts=$((attempts + 1))
done
is_lnd1_ready
}
ensure_lnd4_channel() {
# Ensure lnd-4 has a channel so it can create invoices
# Also ensure lnd-3 has a channel so it can pay invoices (for funding)
# Opens channels from lnd-1 (hub) to both lnd-3 and lnd-4
if lnd4_has_active_channels; then
log "lnd-4 has active channels"
return 0
fi
log "Setting up Lightning channels for ATM..."
# Wait for lnd-1 to be ready (it's the funding hub)
if ! wait_for_lnd1; then
error "lnd-1 failed to start"
return 1
fi
success "lnd-1 is ready"
# Wait for wallet to be fully initialized (needed for channel operations)
log "Waiting for lnd-1 wallet..."
if ! wait_for_lnd1_wallet; then
error "lnd-1 wallet failed to initialize"
return 1
fi
success "lnd-1 wallet is ready"
# From a clean slate, bitcoind may have 0 blocks. Mine some for LND to sync.
local block_count=$(docker exec lnbits-bitcoind-1 bitcoin-cli -regtest getblockcount 2>/dev/null || echo "0")
if [[ "$block_count" -lt 101 ]]; then
log "Mining initial blocks for chain sync..."
local init_addr=$(docker exec lnbits-lnd-1-1 lncli --network=regtest --rpcserver=lnd-1:10009 newaddress p2wkh 2>/dev/null | grep -oP '"address":\s*"\K[^"]+')
if [[ -n "$init_addr" ]]; then
docker exec lnbits-bitcoind-1 bitcoin-cli -regtest generatetoaddress 110 "$init_addr" &>/dev/null
sleep 3
success "Initial blocks mined"
fi
fi
# Wait for LND to sync to chain
log "Waiting for lnd-1 to sync..."
local sync_attempts=0
while [[ $sync_attempts -lt 30 ]]; do
local synced=$(docker exec lnbits-lnd-1-1 lncli --network=regtest --rpcserver=lnd-1:10009 getinfo 2>/dev/null | grep -oP '"synced_to_chain":\s*\K(true|false)')
if [[ "$synced" == "true" ]]; then
break
fi
sleep 2
sync_attempts=$((sync_attempts + 1))
done
success "lnd-1 synced to chain"
# Check if lnd-1 has funds, if not fund it
local lnd1_balance=$(docker exec lnbits-lnd-1-1 lncli --network=regtest --rpcserver=lnd-1:10009 walletbalance 2>/dev/null | grep -oP '"confirmed_balance":\s*"\K[0-9]+' | head -1 || echo "0")
lnd1_balance="${lnd1_balance:-0}"
if [[ "$lnd1_balance" -lt 100000000 ]]; then
log "Funding lnd-1 with mining rewards..."
local lnd1_addr=$(docker exec lnbits-lnd-1-1 lncli --network=regtest --rpcserver=lnd-1:10009 newaddress p2wkh 2>/dev/null | grep -oP '"address":\s*"\K[^"]+')
docker exec lnbits-bitcoind-1 bitcoin-cli -regtest generatetoaddress 110 "$lnd1_addr" &>/dev/null
sleep 3
success "lnd-1 funded"
fi
# Get pubkeys
local lnd4_pubkey=$(docker exec lnbits-lnd-4-1 lncli --network=regtest --rpcserver=lnd-4:10009 getinfo 2>/dev/null | grep -oP '"identity_pubkey":\s*"\K[^"]+')
local lnd3_pubkey=$(docker exec lnbits-lnd-3-1 lncli --network=regtest --rpcserver=lnd-3:10009 getinfo 2>/dev/null | grep -oP '"identity_pubkey":\s*"\K[^"]+')
if [[ -z "$lnd4_pubkey" ]] || [[ -z "$lnd3_pubkey" ]]; then
error "Could not get LND pubkeys"
return 1
fi
# Connect lnd-1 to lnd-3 and lnd-4
log "Connecting nodes..."
docker exec lnbits-lnd-1-1 lncli --network=regtest --rpcserver=lnd-1:10009 \
connect "${lnd3_pubkey}@lnd-3:9735" &>/dev/null || true
docker exec lnbits-lnd-1-1 lncli --network=regtest --rpcserver=lnd-1:10009 \
connect "${lnd4_pubkey}@lnd-4:9735" &>/dev/null || true
# Retry loop for opening channels (server may still be initializing)
# From a clean slate, LND needs ~30-60s after getinfo works to fully initialize
local result3="" result4="" attempts=0
while [[ $attempts -lt 10 ]]; do
# Open channel to lnd-3: 10M sats, push 5M so lnd-3 has sending capacity
if [[ -z "$result3" ]] || echo "$result3" | grep -q "still in the process of starting"; then
result3=$(docker exec lnbits-lnd-1-1 lncli --network=regtest --rpcserver=lnd-1:10009 \
openchannel --node_key="$lnd3_pubkey" --local_amt=10000000 --push_amt=5000000 2>&1)
fi
# Open channel to lnd-4: 10M sats, push 5M so lnd-4 has receiving capacity
if [[ -z "$result4" ]] || echo "$result4" | grep -q "still in the process of starting"; then
result4=$(docker exec lnbits-lnd-1-1 lncli --network=regtest --rpcserver=lnd-1:10009 \
openchannel --node_key="$lnd4_pubkey" --local_amt=10000000 --push_amt=5000000 2>&1)
fi
# Check if both succeeded
if echo "$result3" | grep -q "funding_txid" && echo "$result4" | grep -q "funding_txid"; then
break
fi
attempts=$((attempts + 1))
if [[ $attempts -lt 10 ]]; then
log "Waiting for LND to be fully ready... (attempt $attempts/10)"
sleep 5
fi
done
log "Opening channel lnd-1 -> lnd-3..."
log "Opening channel lnd-1 -> lnd-4..."
local channels_opened=false
if echo "$result3" | grep -q "funding_txid" || echo "$result4" | grep -q "funding_txid"; then
channels_opened=true
fi
if [[ "$channels_opened" == "true" ]]; then
log "Mining blocks to confirm channels..."
# From clean slate, bitcoind may need wallet. Ensure it has one.
docker exec lnbits-bitcoind-1 bitcoin-cli -regtest createwallet "regtest" &>/dev/null 2>&1 || true
# Mine 10 blocks to confirm channels
local addr=$(docker exec lnbits-bitcoind-1 bitcoin-cli -regtest getnewaddress 2>/dev/null)
docker exec lnbits-bitcoind-1 bitcoin-cli -regtest generatetoaddress 10 "$addr" &>/dev/null
# Wait for channels to become active (may take up to 60s)
local attempts=0
while ! lnd4_has_active_channels && [[ $attempts -lt 30 ]]; do
sleep 2
attempts=$((attempts + 1))
done
if lnd4_has_active_channels; then
# Wait for graph propagation (critical for routing payments)
log "Waiting for network graph to sync..."
# Mine more blocks to trigger channel announcements
docker exec lnbits-bitcoind-1 bitcoin-cli -regtest generatetoaddress 6 "$addr" &>/dev/null
sleep 5
# Verify graph has propagated by checking if lnd-3 can route to lnd-4
local route_attempts=0
while [[ $route_attempts -lt 15 ]]; do
local route=$(docker exec lnbits-lnd-3-1 lncli --network=regtest --rpcserver=lnd-3:10009 \
queryroutes --dest "$lnd4_pubkey" --amt 10000 2>&1)
if echo "$route" | grep -q '"total_amt"'; then
# Route found - wait a bit more for full sync before payments
sleep 10
break
fi
sleep 5
route_attempts=$((route_attempts + 1))
done
success "Channels active - lnd-3 and lnd-4 ready"
return 0
else
warn "Channels opened but not yet active, may need more time"
return 0
fi
else
error "Failed to open channels"
echo "lnd-3 result: $result3"
echo "lnd-4 result: $result4"
return 1
fi
}
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
# Ensure lnd-4 has a channel for invoices
ensure_lnd4_channel || warn "Could not setup lnd-4 channel (funding may fail)"
}
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() {
# Use a fixed app name so we reuse the same app across restarts
local app_name="lamassu-atm-dev"
# Check if we already have valid app state
if [[ -f "$APP_ID_FILE" ]]; then
local existing_app_id=$(cat "$APP_ID_FILE")
if [[ -n "$existing_app_id" ]]; then
log "Using existing ATM app: ${existing_app_id:0:16}..."
return 0
fi
fi
log "Creating ATM app..."
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[^"]+')
echo "$app_id" > "$APP_ID_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" >> "$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)"
if [[ -f "$APP_ID_FILE" ]]; then
log "Using existing app ID from state"
return 0
fi
return 1
fi
}
#############################################################################
# Zeus Connection
#############################################################################
generate_lndconnect() {
local local_ip=$(get_local_ip)
local rest_port=8081 # lnd-3 REST API port
# Read certs directly from lnd-3 container (host files are root-owned)
local cert_b64=$(docker exec lnbits-lnd-3-1 base64 -w0 /root/.lnd/tls.cert 2>/dev/null | tr '+/' '-_' | tr -d '=')
local mac_b64=$(docker exec lnbits-lnd-3-1 base64 -w0 /root/.lnd/data/chain/bitcoin/regtest/admin.macaroon 2>/dev/null | tr '+/' '-_' | tr -d '=')
if [[ -n "$cert_b64" ]] && [[ -n "$mac_b64" ]]; then
echo "lndconnect://${local_ip}:${rest_port}?cert=${cert_b64}&macaroon=${mac_b64}"
return 0
fi
return 1
}
#############################################################################
# Display Functions
#############################################################################
get_atm_balance() {
local app_id=$(cat "$APP_ID_FILE" 2>/dev/null)
if [[ -z "$app_id" ]]; then
echo "not configured"
return
fi
echo "configured (use './dev.sh fund' to add sats)"
}
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 ""
local lnd4_channel_balance=$(get_lnd4_channel_balance)
echo -e "${CYAN}lnd-4 (Lightning.Pub backend)${NC}"
echo " Channel Balance: ${lnd4_channel_balance} sats"
echo " Wallet 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:0:60}...${NC}"
echo -e " ${DIM}To display QR: ./dev.sh zeus${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"
local launch_machine=false
local standalone=false
local compose_file="docker-compose.regtest.yml"
# 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 ;;
--standalone) standalone=true; shift ;;
--fund) auto_fund=true; shift ;;
--fund=*) auto_fund=true; fund_amount="${1#*=}"; shift ;;
--machine|--atm) launch_machine=true; shift ;;
*) shift ;;
esac
done
# Determine compose file
if [[ "$standalone" == "true" ]]; then
compose_file="docker-compose.dev.yml"
skip_regtest=true # Standalone mode has its own bitcoind/lnd
log "Starting in STANDALONE mode (self-contained bitcoind/lnd)"
else
log "Starting in UNIFIED mode (shared regtest)"
fi
echo ""
log "Starting Lamassu development environment..."
echo ""
# 1. Start shared regtest if needed (not in standalone mode)
if [[ "$standalone" != "true" ]] && [[ "$skip_regtest" != "true" ]]; then
start_regtest || exit 1
elif [[ "$standalone" != "true" ]] && ! is_regtest_running; then
error "Shared regtest not running."
echo " Start with: regtest-start"
echo " Or use: ./dev.sh up --standalone"
exit 1
elif [[ "$standalone" != "true" ]]; then
# Regtest already running, but ensure lnd-4 has channels
ensure_lnd4_channel || warn "Could not setup lnd-4 channel (funding may fail)"
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)
# Save mode for other commands
if [[ "$standalone" == "true" ]]; then
echo "standalone" > "$MODE_FILE"
else
echo "unified" > "$MODE_FILE"
fi
log "Starting lamassu services..."
docker compose -f "$SCRIPT_DIR/$compose_file" 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
# 11. Launch ATM if requested (in background so status stays visible)
if [[ "$launch_machine" == "true" ]]; then
echo ""
cmd_atm --background
fi
}
cmd_down() {
local compose_file=$(get_compose_file)
log "Stopping lamassu services..."
docker compose -f "$SCRIPT_DIR/$compose_file" 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
local compose_file=$(get_compose_file)
log "Stopping services..."
docker compose -f "$SCRIPT_DIR/$compose_file" 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() {
local compose_file=$(get_compose_file)
docker compose -f "$SCRIPT_DIR/$compose_file" 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 Lightning.Pub pubkey
if [[ ! -f "$PUBKEY_FILE" ]]; then
error "Lightning.Pub not configured. Run './dev.sh up' first."
exit 1
fi
local lp_pubkey=$(cat "$PUBKEY_FILE")
local env_file="$PROJECT_DIR/apps/machine/.env"
# Get ATM private key from .env
if [[ ! -f "$env_file" ]]; then
error "ATM not configured. Run './dev.sh up' first."
exit 1
fi
local atm_privkey=$(grep "VITE_ATM_PRIVATE_KEY=" "$env_file" | cut -d= -f2)
if [[ -z "$atm_privkey" ]]; then
error "VITE_ATM_PRIVATE_KEY not found in .env"
exit 1
fi
# Get app ID - required to ensure funds go to the same user as LNURL-withdraw
local app_id=""
if [[ -f "$APP_ID_FILE" ]]; then
app_id=$(cat "$APP_ID_FILE")
else
app_id=$(grep "VITE_APP_ID=" "$env_file" | cut -d= -f2)
fi
if [[ -z "$app_id" ]]; then
error "No app ID found. Run './dev.sh up' first to create the ATM app."
exit 1
fi
log "Creating invoice via Nostr RPC for $amount sats..."
log "Using app ID: ${app_id:0:16}..."
# Use the Nostr-based funding script (the Lightning.Pub way)
# This creates an invoice for the ATM's Nostr user under the correct app,
# ensuring balance is shared with LNURL-withdraw (Extension API)
local invoice=$(cd "$PROJECT_DIR/packages/nostr-client" && \
VITE_ATM_PRIVATE_KEY="$atm_privkey" \
VITE_LIGHTNING_PUB_PUBKEY="$lp_pubkey" \
VITE_RELAY_URL="ws://localhost:7777" \
VITE_APP_ID="$app_id" \
node dev/fund-dev.mjs "$amount" 2>&1)
# Extract just the invoice (last line, starts with lnbc)
local bolt11=$(echo "$invoice" | grep -E "^lnbc")
if [[ -z "$bolt11" ]]; then
error "Failed to create invoice via Nostr"
echo "Output: $invoice"
exit 1
fi
log "Got invoice: ${bolt11:0:30}..."
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 "$bolt11"
)
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 "$bolt11"
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_zeus() {
if ! is_regtest_running; then
error "Regtest not running. Start with: ./dev.sh up"
exit 1
fi
local lndconnect=$(generate_lndconnect 2>/dev/null)
if [[ -z "$lndconnect" ]]; then
error "Could not generate lndconnect string"
exit 1
fi
echo ""
echo -e "${CYAN}Zeus Wallet Connection (lnd-3)${NC}"
echo ""
if command -v qrencode &> /dev/null; then
local qr_file="$STATE_DIR/zeus-lndconnect.png"
qrencode -o "$qr_file" -s 6 -l L "$lndconnect"
echo -e "QR code saved to: ${GREEN}$qr_file${NC}"
echo ""
# Try to open the image
if command -v xdg-open &> /dev/null; then
xdg-open "$qr_file" 2>/dev/null &
echo -e "${DIM}Opening QR code image...${NC}"
elif command -v open &> /dev/null; then
open "$qr_file" 2>/dev/null &
echo -e "${DIM}Opening QR code image...${NC}"
else
echo -e "${DIM}Open the PNG file to scan with Zeus${NC}"
fi
else
echo -e "${DIM}$lndconnect${NC}"
echo ""
echo -e "${YELLOW}Install qrencode for QR display: nix-shell -p qrencode${NC}"
fi
echo ""
}
cmd_atm() {
local atm_dir="$PROJECT_DIR/apps/machine"
local background=false
# Parse arguments
while [[ $# -gt 0 ]]; do
case "$1" in
--background|-b) background=true; shift ;;
*) shift ;;
esac
done
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
# Auto-generate lndconnect URI for Zeus QR in mock UI
local lndconnect=$(generate_lndconnect 2>/dev/null || echo "")
if [[ -n "$lndconnect" ]]; then
export VITE_LNDCONNECT_URL="$lndconnect"
log "Zeus lndconnect injected into ATM UI"
fi
if [[ "$background" == "true" ]]; then
local log_file="$STATE_DIR/atm.log"
log "Starting ATM application in background..."
log "Logs: $log_file"
(cd "$atm_dir" && nohup pnpm dev > "$log_file" 2>&1 &)
sleep 2
if pgrep -f "electron.*machine" > /dev/null; then
success "ATM app started (PID: $(pgrep -f 'electron.*machine' | head -1))"
else
warn "ATM may still be starting. Check logs: tail -f $log_file"
fi
else
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"
if [[ -n "$lndconnect" ]]; then
echo -e " - ${GREEN}Zeus QR available on idle screen${NC}"
fi
echo ""
cd "$atm_dir" && pnpm dev
fi
}
#############################################################################
# Entry Point
#############################################################################
case "${1:-help}" in
up|start)
shift
cmd_up "$@"
;;
down|stop)
shift
cmd_down "$@"
;;
reset)
cmd_reset
;;
logs)
shift
cmd_logs "$@"
;;
# Internal command for external orchestration (called by regtest-start --with-lamassu)
_setup_after_start)
if ! is_lightning_pub_ready; then
wait_for_lightning_pub || exit 1
fi
local pubkey=$(extract_lightning_pub_info)
update_atm_env "$pubkey"
setup_atm_app || true
echo "ATM setup complete"
;;
status|info)
cmd_status
;;
fund)
shift
cmd_fund "$@"
;;
mine)
shift
cmd_mine "$@"
;;
atm)
cmd_atm
;;
zeus)
cmd_zeus
;;
*)
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 " zeus Show Zeus wallet connection QR code"
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 " --standalone Use self-contained mode (own bitcoind/lnd)"
echo " --worktree <path> Build Lightning.Pub from git worktree"
echo " --image <name> Use specific Docker image"
echo " --skip-regtest Don't auto-start shared regtest"
echo " --fund Auto-fund ATM with 100k sats after startup"
echo " --fund=<sats> Auto-fund ATM with specific amount"
echo " --machine Launch ATM app after startup"
echo ""
echo "Environment variables:"
echo " LAMASSU_HOST_IP Override auto-detected LAN IP (for VPN/multi-NIC)"
echo ""
echo "Examples:"
echo " $0 up # Start (uses shared regtest)"
echo " $0 up --standalone # Start self-contained"
echo " $0 up --fund # Start and fund ATM"
echo " $0 up --fund --machine # Start, fund, and launch 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"
echo " LAMASSU_HOST_IP=192.168.1.50 $0 up # Override LAN IP"
;;
esac