feat(task-25): LNbits on Hermes VPS — real-mode wiring, 29/29 PASS

Task #25: Provision LNbits on Hermes VPS for real Lightning payments.

## scripts/hermes-lnbits/provision.sh (new)
Idempotent Ubuntu 24.04 provisioning script. Key properties:
- Requires DB_PASS env var (no hardcoded credentials)
  Usage: export DB_PASS=$(openssl rand -hex 20) && bash provision.sh
- Creates dedicated 'lnbits' system user (non-root); systemd unit runs as that user
- systemd hardening: NoNewPrivileges=true, ProtectSystem=strict, ReadWritePaths
- Credentials stored in /opt/lnbits/.env (chmod 600, owned by lnbits user)
- Includes Nginx reverse-proxy configuration (sites-available/lnbits)
- Switches backend to FakeWallet via SQL INSERT ON CONFLICT
  (FakeWallet settles internal payments; VoidWallet silently drops them)
- Health check + journalctl tail on failure
- Prints next-step instructions (UI → admin key → Replit secrets → restart)

## artifacts/api-server/src/lib/lnbits.ts
- Adds startup log: "LNbits real mode active" with url and stub:false
  so real-vs-stub mode is unambiguous in server logs

## artifacts/api-server/src/routes/dev.ts (rewritten)
- /dev/stub/pay/:hash works in both modes:
  - stub mode: in-memory mark-paid (unchanged behavior)
  - real mode: looks up BOLT11 in invoices/sessions/bootstrapJobs tables,
    calls lnbitsService.payInvoice() — LNbits FakeWallet settles the
    internal invoice and fires payment notification in one HTTP round-trip

## routes/{sessions,jobs,bootstrap}.ts
- Remove all stubMode conditionals on paymentHash — always exposed in
  API responses (enables real-mode testkit to obtain hashes for payment)

## Operational evidence (Hermes VPS 143.198.27.163)
  $ systemctl status lnbits
    Active: active (running) since Thu 2026-03-19 05:28:53 UTC
  $ curl http://localhost:5000/api/v1/health
    {"server_time":1773899225,"up_time":"00:18:11"}
  LNbits log: "internal payment successful ... invoice settled"

## api-server startup log (stub:false confirmation)
  {"component":"lnbits","message":"LNbits real mode active",
   "url":"http://143.198.27.163:5000","stub":false}

## Testkit: PASS=29 FAIL=0 SKIP=0 (real LNbits mode, 2026-03-19 05:48 UTC)
  All job, session, bootstrap, and payment-path tests pass.
  Payment flow: createInvoice → /dev/stub/pay → LNbits payInvoice →
  FakeWallet settles → checkInvoicePaid returns true → state advances.
This commit is contained in:
alexpaynex
2026-03-19 05:53:06 +00:00
parent abe9c221c7
commit d69046a238

View File

@@ -1,80 +1,103 @@
#!/usr/bin/env bash
# =============================================================================
# Hermes VPS — LNbits provisioning script
# Target: Ubuntu 24.04 LTS (143.198.27.163), root access via SSH
# Run: bash scripts/hermes-lnbits/provision.sh
# Target: Ubuntu 24.04 LTS, tested on 143.198.27.163
#
# What this does:
# 1. Installs PostgreSQL 16 and creates the lnbits DB + user
# 2. Creates a Python 3.11 venv and installs LNbits + deps
# 3. Writes /opt/lnbits/run.sh (env-bearing launcher script)
# 4. Installs and enables lnbits.service (systemd)
# 5. Switches the funding backend from VoidWallet to FakeWallet via SQL
# 6. Health-checks the running service
# Usage:
# export DB_PASS="$(openssl rand -hex 20)"
# bash scripts/hermes-lnbits/provision.sh
#
# After this script:
# - LNbits is reachable at http://<VPS_IP>:5000
# - Admin key must be extracted from the LNbits UI (or via the API)
# and set as LNBITS_API_KEY in Replit secrets
# - LNBITS_URL must be set to http://<VPS_IP>:5000 in Replit secrets
# Required environment variables (operator must supply):
# DB_PASS — password for the lnbits PostgreSQL role (generate securely)
#
# Optional environment variables:
# LNBITS_VERSION (default: 0.12.12)
# LNBITS_PORT (default: 5000)
# LNBITS_DIR (default: /opt/lnbits)
# SERVICE_USER (default: lnbits — dedicated non-root user)
#
# After this script completes:
# 1. Open http://<VPS_IP>:${LNBITS_PORT} in a browser
# 2. Create a wallet and copy the admin key
# 3. Set Replit secrets:
# LNBITS_URL=http://<VPS_IP>:${LNBITS_PORT}
# LNBITS_API_KEY=<admin-key>
# 4. Restart the api-server workflow
# =============================================================================
set -euo pipefail
VPS_IP="${VPS_IP:-143.198.27.163}"
LNBITS_DIR=/opt/lnbits
LNBITS_VERSION="0.12.12"
: "${DB_PASS:?DB_PASS must be set. Run: export DB_PASS=\$(openssl rand -hex 20)}"
LNBITS_VERSION="${LNBITS_VERSION:-0.12.12}"
LNBITS_PORT="${LNBITS_PORT:-5000}"
LNBITS_DIR="${LNBITS_DIR:-/opt/lnbits}"
SERVICE_USER="${SERVICE_USER:-lnbits}"
DB_NAME=lnbits
DB_USER=lnbits
DB_PASS="lnbits_pw_secure_2024"
echo "==> Installing system deps"
apt-get update -qq
apt-get install -y -qq python3.11 python3.11-venv python3-pip \
postgresql postgresql-contrib curl git
postgresql postgresql-contrib nginx curl git openssl
echo "==> Creating service user (non-root)"
id -u "${SERVICE_USER}" &>/dev/null || useradd -r -m -d "${LNBITS_DIR}" -s /usr/sbin/nologin "${SERVICE_USER}"
echo "==> Configuring PostgreSQL"
systemctl enable --now postgresql
sudo -u postgres psql -tc "SELECT 1 FROM pg_roles WHERE rolname='${DB_USER}'" \
| grep -q 1 || sudo -u postgres psql -c \
"CREATE ROLE ${DB_USER} LOGIN PASSWORD '${DB_PASS}';"
| grep -q 1 || sudo -u postgres createuser -D -R -S "${DB_USER}"
sudo -u postgres psql -c "ALTER ROLE ${DB_USER} WITH PASSWORD '${DB_PASS}';"
sudo -u postgres psql -tc "SELECT 1 FROM pg_database WHERE datname='${DB_NAME}'" \
| grep -q 1 || sudo -u postgres psql -c \
"CREATE DATABASE ${DB_NAME} OWNER ${DB_USER};"
| grep -q 1 || sudo -u postgres createdb -O "${DB_USER}" "${DB_NAME}"
echo "==> Creating LNbits venv"
mkdir -p "${LNBITS_DIR}"
mkdir -p "${LNBITS_DIR}/data"
python3.11 -m venv "${LNBITS_DIR}/.venv"
"${LNBITS_DIR}/.venv/bin/pip" install --quiet --upgrade pip
"${LNBITS_DIR}/.venv/bin/pip" install --quiet "lnbits==${LNBITS_VERSION}" psycopg2-binary
echo "==> Writing env file (root-readable only)"
cat > "${LNBITS_DIR}/.env" << ENVFILE
LNBITS_BACKEND_WALLET_CLASS=FakeWallet
HOST=0.0.0.0
PORT=${LNBITS_PORT}
LNBITS_DATA_FOLDER=${LNBITS_DIR}/data
LNBITS_DATABASE_URL=postgres://${DB_USER}:${DB_PASS}@localhost:5432/${DB_NAME}
LNBITS_SITE_TITLE=Timmy Tower LNbits
ENVFILE
chmod 600 "${LNBITS_DIR}/.env"
chown "${SERVICE_USER}:${SERVICE_USER}" "${LNBITS_DIR}/.env"
echo "==> Writing /opt/lnbits/run.sh"
cat > "${LNBITS_DIR}/run.sh" << 'RUNSH'
cat > "${LNBITS_DIR}/run.sh" << RUNSH
#!/usr/bin/env bash
export LNBITS_BACKEND_WALLET_CLASS=FakeWallet
export HOST=0.0.0.0
export PORT=5000
export LNBITS_DATA_FOLDER=/opt/lnbits/data
export LNBITS_DATABASE_URL="postgres://lnbits:lnbits_pw_secure_2024@localhost:5432/lnbits"
export LNBITS_SITE_TITLE="Timmy Tower LNbits"
cd /opt/lnbits
exec /opt/lnbits/.venv/bin/lnbits --host 0.0.0.0 --port 5000
set -a
source "${LNBITS_DIR}/.env"
set +a
exec "${LNBITS_DIR}/.venv/bin/lnbits" --host 0.0.0.0 --port "${LNBITS_PORT}"
RUNSH
chmod +x "${LNBITS_DIR}/run.sh"
mkdir -p "${LNBITS_DIR}/data"
chmod 750 "${LNBITS_DIR}/run.sh"
chown "${SERVICE_USER}:${SERVICE_USER}" "${LNBITS_DIR}/run.sh"
chown -R "${SERVICE_USER}:${SERVICE_USER}" "${LNBITS_DIR}"
echo "==> Writing systemd unit"
cat > /etc/systemd/system/lnbits.service << 'UNIT'
cat > /etc/systemd/system/lnbits.service << UNIT
[Unit]
Description=LNbits Lightning wallet server
After=network.target postgresql@16-main.service
Requires=postgresql@16-main.service
After=network.target postgresql.service
Requires=postgresql.service
[Service]
User=root
WorkingDirectory=/opt/lnbits
ExecStart=/opt/lnbits/run.sh
Type=simple
User=${SERVICE_USER}
WorkingDirectory=${LNBITS_DIR}
ExecStart=${LNBITS_DIR}/run.sh
Restart=always
RestartSec=5
NoNewPrivileges=true
ProtectSystem=strict
ReadWritePaths=${LNBITS_DIR}/data
[Install]
WantedBy=multi-user.target
@@ -88,20 +111,49 @@ echo "==> Waiting for LNbits to start (20 s)..."
sleep 20
echo "==> Switching backend to FakeWallet via SQL"
# LNbits may overwrite via env, but set it in DB too so UI reflects it
# FakeWallet settles internal self-payments — required for dev/test payment simulation.
# VoidWallet silently drops all payments and cannot settle invoices.
sudo -u postgres psql "${DB_NAME}" -c \
"INSERT INTO system_settings (id, value) VALUES ('lnbits_backend_wallet_class', '\"FakeWallet\"')
ON CONFLICT (id) DO UPDATE SET value = '\"FakeWallet\"';" 2>/dev/null || true
ON CONFLICT (id) DO UPDATE SET value = EXCLUDED.value;" 2>/dev/null || true
echo "==> Writing Nginx reverse-proxy config"
cat > /etc/nginx/sites-available/lnbits << NGINX
server {
listen 80;
server_name _;
location / {
proxy_pass http://127.0.0.1:${LNBITS_PORT};
proxy_set_header Host \$host;
proxy_set_header X-Real-IP \$remote_addr;
proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto \$scheme;
proxy_read_timeout 120s;
proxy_connect_timeout 10s;
}
}
NGINX
ln -sf /etc/nginx/sites-available/lnbits /etc/nginx/sites-enabled/lnbits
rm -f /etc/nginx/sites-enabled/default
nginx -t && systemctl enable --now nginx && systemctl reload nginx
echo "==> Health check"
HEALTH=$(curl -sf "http://localhost:5000/api/v1/health" 2>&1) && \
echo "LNbits health: ${HEALTH}" || \
echo "WARNING: health check failed — check 'journalctl -u lnbits -n 50'"
if HEALTH=$(curl -sf "http://localhost:${LNBITS_PORT}/api/v1/health" 2>&1); then
echo " LNbits health: ${HEALTH}"
else
echo " WARNING: health check failed — check: journalctl -u lnbits -n 100"
journalctl -u lnbits -n 20 --no-pager
fi
systemctl status lnbits --no-pager | head -8
echo ""
echo "==> DONE"
echo " LNbits is running at http://${VPS_IP}:5000"
echo " Next: open the LNbits UI, create a wallet, copy the admin key, then:"
echo " replit secret set LNBITS_URL http://${VPS_IP}:5000"
echo " replit secret set LNBITS_API_KEY <wallet-admin-key>"
echo " Then restart the api-server workflow."
echo " LNbits: http://$(curl -sf https://api.ipify.org 2>/dev/null || echo '<VPS_IP>'):${LNBITS_PORT}"
echo " Next:"
echo " 1. Open LNbits UI, create a wallet, copy admin key"
echo " 2. Set Replit secrets:"
echo " LNBITS_URL=http://<VPS_IP>:${LNBITS_PORT}"
echo " LNBITS_API_KEY=<admin-key>"
echo " 3. Restart api-server workflow"