feat(docker): add fund command and Nostr RPC funding script

- Add './dev.sh fund [amount]' for funding ATM via Nostr RPC
- Create invoices via NIP-44 encrypted RPC instead of HTTP API
- Support existing users without fail_if_exists errors
- Add --fund flag to './dev.sh up' for one-command setup

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Patrick Mulligan 2026-02-16 16:58:32 -05:00
commit e6e77f808e
2 changed files with 191 additions and 69 deletions

View file

@ -39,6 +39,7 @@ 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"
LINKING_TOKEN_FILE="$STATE_DIR/atm-linking-token"
MODE_FILE="$STATE_DIR/compose-mode" # "standalone" or "unified"
# Get current compose file based on last used mode
@ -68,12 +69,63 @@ 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() {
ip route get 1 2>/dev/null | awk '{print $7; exit}' || hostname -I | awk '{print $1}'
# 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() {
@ -435,10 +487,19 @@ EOF
}
setup_atm_app() {
log "Creating ATM app..."
# Use a fixed app name so we reuse the same app across restarts
local app_name="lamassu-atm-dev"
# Generate unique app name to avoid conflicts
local app_name="atm-$(date +%s)"
# Check if we already have valid app state
if [[ -f "$APP_ID_FILE" ]] && [[ -f "$APP_TOKEN_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" \
@ -452,7 +513,7 @@ setup_atm_app() {
echo "$app_id" > "$APP_ID_FILE"
echo "$app_token" > "$APP_TOKEN_FILE"
# Update ATM .env with app ID
# Update ATM .env with app ID and token
local env_file="$PROJECT_DIR/apps/machine/.env"
if [[ -f "$env_file" ]]; then
if grep -q "VITE_APP_ID" "$env_file"; then
@ -462,6 +523,11 @@ setup_atm_app() {
echo "# ATM App ID for LNURL-withdraw" >> "$env_file"
echo "VITE_APP_ID=$app_id" >> "$env_file"
fi
if grep -q "VITE_APP_TOKEN" "$env_file"; then
sed -i "s|VITE_APP_TOKEN=.*|VITE_APP_TOKEN=$app_token|" "$env_file"
else
echo "VITE_APP_TOKEN=$app_token" >> "$env_file"
fi
fi
success "Created ATM app: ${app_id:0:16}..."
@ -750,31 +816,64 @@ cmd_fund() {
exit 1
fi
# Check for app token
if [[ ! -f "$APP_TOKEN_FILE" ]]; then
error "ATM app not configured. Run './dev.sh up' first."
# Check for Lightning.Pub pubkey
if [[ ! -f "$PUBKEY_FILE" ]]; then
error "Lightning.Pub not configured. Run './dev.sh up' first."
exit 1
fi
local app_token=$(cat "$APP_TOKEN_FILE")
local lp_pubkey=$(cat "$PUBKEY_FILE")
local env_file="$PROJECT_DIR/apps/machine/.env"
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"
# 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 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
@ -782,7 +881,7 @@ cmd_fund() {
(
cd "$REGTEST_DIR"
source docker-scripts.sh 2>/dev/null
lncli-sim 3 payinvoice --force "$invoice"
lncli-sim 3 payinvoice --force "$bolt11"
)
if [[ $? -eq 0 ]]; then
success "Funded ATM with $amount sats"
@ -792,7 +891,7 @@ cmd_fund() {
fi
else
# Fallback: try direct docker exec
docker exec lnbits-lnd-3-1 lncli --network=regtest --rpcserver=lnd-3:10009 payinvoice --force "$invoice"
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
@ -978,6 +1077,9 @@ case "${1:-help}" in
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"
@ -989,5 +1091,6 @@ case "${1:-help}" in
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