This repository has been archived on 2026-03-24. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
token-gated-economy/infrastructure/ops.sh
alexpaynex cdd97922d5 task/30: Sovereign Nostr relay infrastructure (strfry)
## Summary
Deploys strfry (C++ Nostr relay) + relay-policy sidecar as a containerised
stack on the VPS, wired to the API server for event-level access control.

## Files created
- `infrastructure/strfry.conf` — strfry config: bind 0.0.0.0:7777, writePolicy
  plugin → /usr/local/bin/relay-policy-plugin, maxEventSize 65536,
  rejectEphemeral false, db /data/strfry-db
- `infrastructure/relay-policy/plugin.sh` — strfry write-policy plugin (stdin/stdout
  bridge). Reads JSON lines from strfry, POSTs to relay-policy HTTP sidecar
  (http://relay-policy:3080/decide), writes decision to stdout. Safe fallback:
  reject on sidecar timeout/failure
- `infrastructure/relay-policy/index.ts` — Node.js HTTP relay-policy sidecar:
  POST /decide receives strfry events, calls API server /api/relay/policy with
  Bearer RELAY_POLICY_SECRET, returns strfry decision JSON
- `infrastructure/relay-policy/package.json + tsconfig.json` — TS build config
- `infrastructure/relay-policy/Dockerfile` — multi-stage: builder (tsc) + runtime
- `infrastructure/relay-policy/.gitignore` — excludes node_modules, dist
- `artifacts/api-server/src/routes/relay.ts` — POST /api/relay/policy: internal
  route protected by RELAY_POLICY_SECRET Bearer token. Bootstrap state: rejects
  all events with "relay not yet open — whitelist pending (Task #37)". Stable
  contract — future tasks extend evaluatePolicy() without API shape changes

## Files modified
- `infrastructure/docker-compose.yml` — adds relay-policy + strfry services on
  node-net; strfry_data volume (bind-mounted at /data/strfry); relay-policy
  healthcheck; strfry depends on relay-policy healthy
- `infrastructure/ops.sh` — adds relay:logs, relay:restart, relay:status commands
- `artifacts/api-server/src/routes/index.ts` — registers relayRouter

## Operator setup required on VPS
  mkdir -p /data/strfry && chmod 700 /data/strfry
  echo "RELAY_API_URL=https://alexanderwhitestone.com" >> /opt/timmy-node/.env
  echo "RELAY_POLICY_SECRET=$(openssl rand -hex 32)" >> /opt/timmy-node/.env
  # Also set RELAY_POLICY_SECRET in Replit secrets for API server

## Notes
- TypeScript: 0 errors (API server + relay-policy sidecar both compile clean)
- POST /api/relay/policy smoke test: correct bootstrap reject response
- strfry image: ghcr.io/hoytech/strfry:latest
2026-03-19 20:02:00 +00:00

368 lines
16 KiB
Bash
Executable File

#!/usr/bin/env bash
# ============================================================
# Timmy Node — Day-to-day operations helper
# Usage: bash ops.sh <command>
# ============================================================
INFRA_DIR="/opt/timmy-node"
cd "$INFRA_DIR" 2>/dev/null || { echo "Run on the droplet, not locally"; exit 1; }
CYAN='\033[0;36m'; GREEN='\033[0;32m'; YELLOW='\033[1;33m'; NC='\033[0m'
case "${1:-help}" in
status)
echo -e "\n${CYAN}── Docker services ──────────────────────────${NC}"
docker compose ps
echo -e "\n${CYAN}── Bitcoin sync ──────────────────────────────${NC}"
docker exec bitcoin bitcoin-cli -datadir=/home/bitcoin/.bitcoin getblockchaininfo 2>/dev/null \
| jq '{blocks, headers, verificationprogress, size_on_disk}' \
|| echo "Bitcoin not ready"
echo -e "\n${CYAN}── LND state ─────────────────────────────────${NC}"
docker exec lnd lncli --network=mainnet getinfo 2>/dev/null \
| jq '{identity_pubkey, alias, num_peers, num_active_channels, synced_to_chain}' \
|| echo "LND not ready"
echo -e "\n${CYAN}── LND wallet balance ────────────────────────${NC}"
docker exec lnd lncli --network=mainnet walletbalance 2>/dev/null \
| jq '{confirmed_balance, unconfirmed_balance}' \
|| true
echo -e "\n${CYAN}── LND channel balance ───────────────────────${NC}"
docker exec lnd lncli --network=mainnet channelbalance 2>/dev/null \
| jq '{balance, pending_open_balance}' \
|| true
;;
sync)
watch -n 10 'docker exec bitcoin bitcoin-cli getblockchaininfo \
| jq "{blocks, verificationprogress, size_on_disk}"'
;;
logs)
SERVICE="${2:-}"
if [[ -z "$SERVICE" ]]; then
docker compose logs -f --tail=50
else
docker compose logs -f --tail=100 "$SERVICE"
fi
;;
restart)
SERVICE="${2:-}"
if [[ -z "$SERVICE" ]]; then
docker compose restart
else
docker compose restart "$SERVICE"
fi
;;
fund)
echo -e "${CYAN}Your on-chain deposit address (send BTC here to fund channels):${NC}"
docker exec lnd lncli --network=mainnet newaddress p2wkh | jq -r .address
;;
channels)
echo -e "\n${CYAN}── Active channels ───────────────────────────${NC}"
docker exec lnd lncli --network=mainnet listchannels 2>/dev/null \
| jq '.channels[] | {remote_pubkey, capacity, local_balance, remote_balance, active}'
echo -e "\n${CYAN}── Pending channels ──────────────────────────${NC}"
docker exec lnd lncli --network=mainnet pendingchannels 2>/dev/null \
| jq '{pending_open_channels: .pending_open_channels | length, pending_closing_channels: .pending_closing_channels | length}'
;;
open-channel)
echo -e "${CYAN}Open a channel — usage:${NC}"
echo " bash ops.sh open-channel <peer_pubkey>@<host>:<port> <amount_sats>"
if [[ -n "${2:-}" && -n "${3:-}" ]]; then
docker exec lnd lncli --network=mainnet connect "$2" 2>/dev/null || true
PUBKEY=$(echo "$2" | cut -d@ -f1)
docker exec lnd lncli --network=mainnet openchannel \
--node_key "$PUBKEY" \
--local_amt "$3" \
--push_amt 0
fi
;;
lnbits-key)
echo -e "${CYAN}LNbits API keys for Timmy wallet:${NC}"
grep "LNBITS" /root/node-credentials.txt 2>/dev/null || \
echo "Check /root/node-credentials.txt — or open LNbits dashboard"
TSHOST=$(tailscale status --json 2>/dev/null | jq -r '.Self.DNSName' | sed 's/\.$//')
echo -e "\nLNbits dashboard: ${GREEN}https://$TSHOST${NC}"
;;
update)
echo -e "${YELLOW}Pulling latest Docker images...${NC}"
docker compose pull
docker compose up -d
ok "Done"
;;
backup)
BACKUP_FILE="/root/lnd-backup-$(date +%Y%m%d-%H%M%S).tar.gz"
echo -e "${CYAN}Backing up LND channel state to $BACKUP_FILE${NC}"
tar -czf "$BACKUP_FILE" -C /data/lnd .
echo -e "${GREEN}Backup saved. Copy this file off the server:${NC}"
echo " scp root@<node>:$BACKUP_FILE ./lnd-backup.tar.gz"
;;
sweep)
SWEEP_CONF="$INFRA_DIR/sweep.conf"
SWEEP_LOG="/var/log/timmy-sweep.log"
STATE_FILE="$INFRA_DIR/sweep-state"
ADDR_LIST_FILE="$INFRA_DIR/sweep-addresses.txt"
echo -e "\n${CYAN}── Sweep config ──────────────────────────────${NC}"
if [[ -f "$SWEEP_CONF" ]]; then
SWEEP_MODE="static"; COLD_ADDRESS=""; XPUB=""
KEEP_SATS=300000; MIN_SWEEP=50000
SWEEP_FREQ_LABEL="daily at 3am UTC"; SWEEP_CRON="0 3 * * *"
source "$SWEEP_CONF"
echo " Mode : ${SWEEP_MODE}"
case "$SWEEP_MODE" in
static)
echo " Destination : ${COLD_ADDRESS:-(not set — sweep disabled)}"
;;
list)
COUNT=$(grep -c '[^[:space:]]' "$ADDR_LIST_FILE" 2>/dev/null || echo 0)
NEXT_INDEX=0; [[ -f "$STATE_FILE" ]] && source "$STATE_FILE"
REMAINING=$(( COUNT - NEXT_INDEX ))
echo " Addresses : ${COUNT} total, ${REMAINING} remaining (next: index ${NEXT_INDEX})"
NEXT_ADDR=$(sed -n "$((NEXT_INDEX + 1))p" "$ADDR_LIST_FILE" 2>/dev/null | tr -d '[:space:]')
[[ -n "$NEXT_ADDR" ]] && echo " Next address : ${NEXT_ADDR}"
;;
xpub)
NEXT_INDEX=0; [[ -f "$STATE_FILE" ]] && source "$STATE_FILE"
echo " xpub : ${XPUB:0:30}..."
echo " Next index : ${NEXT_INDEX}"
;;
esac
echo " Keep on-chain: ${KEEP_SATS} sats"
echo " Min to sweep : ${MIN_SWEEP} sats"
echo " Frequency : ${SWEEP_FREQ_LABEL}"
else
echo -e " ${YELLOW}No sweep.conf found — sweep is disabled${NC}"
echo " To configure: bash ops.sh configure-sweep"
fi
echo -e "\n${CYAN}── Current on-chain balance ──────────────────${NC}"
docker exec lnd lncli --network=mainnet walletbalance 2>/dev/null \
| jq '{confirmed_balance, unconfirmed_balance}' \
|| echo " LND not ready"
echo -e "\n${CYAN}── Last 5 sweep log entries ──────────────────${NC}"
if [[ -f "$SWEEP_LOG" && -s "$SWEEP_LOG" ]]; then
tail -5 "$SWEEP_LOG"
else
echo " No sweep activity yet"
fi
echo ""
;;
configure-sweep)
SWEEP_CONF="$INFRA_DIR/sweep.conf"
STATE_FILE="$INFRA_DIR/sweep-state"
ADDR_LIST_FILE="$INFRA_DIR/sweep-addresses.txt"
# Load current values as defaults
SWEEP_MODE="static"; COLD_ADDRESS=""; XPUB=""
KEEP_SATS=300000; MIN_SWEEP=50000
SWEEP_CRON="0 3 * * *"; SWEEP_FREQ_LABEL="daily at 3am UTC"
[[ -f "$SWEEP_CONF" ]] && source "$SWEEP_CONF"
echo -e "\n${CYAN}══════════════════════════════════════════${NC}"
echo -e "${CYAN} Configure Auto-Sweep (Enter = keep current)${NC}"
echo -e "${CYAN}══════════════════════════════════════════${NC}\n"
# ── Destination mode ──────────────────────────────────────
echo -e " Current destination mode: ${YELLOW}${SWEEP_MODE}${NC}"
echo " Choose a destination mode:"
echo " 1) Single address — same address every sweep"
echo " 2) Address list — rotate through a list of addresses (no reuse)"
echo " 3) xpub — derive a fresh address each sweep (no reuse)"
read -rp " Choice [1-3, Enter to keep]: " MODE_CHOICE
case "$MODE_CHOICE" in
1) SWEEP_MODE="static" ;;
2) SWEEP_MODE="list" ;;
3) SWEEP_MODE="xpub" ;;
esac
# ── Mode-specific inputs ──────────────────────────────────
case "$SWEEP_MODE" in
static)
echo -e "\n Current cold address: ${YELLOW}${COLD_ADDRESS:-(none)}${NC}"
read -rp " New cold address (bc1q... / 1... / 3...): " INPUT
[[ -n "$INPUT" ]] && COLD_ADDRESS="$INPUT"
XPUB=""
;;
list)
if [[ -f "$ADDR_LIST_FILE" ]]; then
COUNT=$(grep -c '[^[:space:]]' "$ADDR_LIST_FILE" || true)
echo -e "\n Current address list: ${YELLOW}${COUNT} address(es) in $ADDR_LIST_FILE${NC}"
else
echo -e "\n No address list found yet."
fi
NEXT_INDEX=0
[[ -f "$STATE_FILE" ]] && source "$STATE_FILE"
echo " Current position: index ${NEXT_INDEX}"
echo ""
echo " Paste cold addresses now (one per line, blank line when done)."
echo " These REPLACE the existing list. Leave blank to keep current list."
echo ""
NEW_ADDRS=()
while IFS= read -rp " > " ADDR_INPUT && [[ -n "$ADDR_INPUT" ]]; do
ADDR_INPUT=$(echo "$ADDR_INPUT" | tr -d '[:space:]')
[[ -n "$ADDR_INPUT" ]] && NEW_ADDRS+=("$ADDR_INPUT")
done
if (( ${#NEW_ADDRS[@]} > 0 )); then
printf '%s\n' "${NEW_ADDRS[@]}" > "$ADDR_LIST_FILE"
chmod 600 "$ADDR_LIST_FILE"
echo "NEXT_INDEX=0" > "$STATE_FILE"
chmod 600 "$STATE_FILE"
echo -e " ${GREEN}Saved ${#NEW_ADDRS[@]} addresses. Index reset to 0.${NC}"
else
echo " Kept existing address list."
fi
COLD_ADDRESS=""; XPUB=""
;;
xpub)
echo -e "\n Current xpub: ${YELLOW}${XPUB:-(none)}${NC}"
echo " Paste your account xpub (from Sparrow, Coldcard, etc. — account-level, not master)."
read -rp " xpub: " INPUT
if [[ -n "$INPUT" ]]; then
XPUB="$INPUT"
echo "NEXT_INDEX=0" > "$STATE_FILE"
chmod 600 "$STATE_FILE"
echo -e " ${GREEN}xpub saved. Derivation index reset to 0.${NC}"
fi
COLD_ADDRESS=""
;;
esac
# ── Keep threshold ────────────────────────────────────────
echo -e "\n Current keep-on-chain threshold: ${YELLOW}${KEEP_SATS} sats${NC}"
read -rp " New keep threshold (sats): " INPUT
[[ "$INPUT" =~ ^[0-9]+$ ]] && KEEP_SATS="$INPUT"
# ── Min sweep floor ───────────────────────────────────────
echo -e "\n Current minimum sweep amount: ${YELLOW}${MIN_SWEEP} sats${NC}"
read -rp " New minimum sweep (sats): " INPUT
[[ "$INPUT" =~ ^[0-9]+$ ]] && MIN_SWEEP="$INPUT"
# ── Frequency ─────────────────────────────────────────────
echo -e "\n Current frequency: ${YELLOW}${SWEEP_FREQ_LABEL}${NC}"
echo " Choose a new frequency (Enter to keep current):"
echo " 1) Hourly"
echo " 2) Every 6 hours"
echo " 3) Daily at 3am UTC"
echo " 4) Weekly (Sunday 3am UTC)"
read -rp " Choice [1-4]: " FREQ_CHOICE
case "$FREQ_CHOICE" in
1) SWEEP_CRON="0 * * * *"; SWEEP_FREQ_LABEL="hourly" ;;
2) SWEEP_CRON="0 */6 * * *"; SWEEP_FREQ_LABEL="every 6 hours" ;;
3) SWEEP_CRON="0 3 * * *"; SWEEP_FREQ_LABEL="daily at 3am UTC" ;;
4) SWEEP_CRON="0 3 * * 0"; SWEEP_FREQ_LABEL="weekly (Sunday 3am UTC)" ;;
esac
# ── Write sweep.conf ──────────────────────────────────────
cat > "$SWEEP_CONF" <<CONF
# Timmy Node — Auto-sweep configuration
# Edit manually or run: bash ops.sh configure-sweep
SWEEP_MODE="$SWEEP_MODE"
COLD_ADDRESS="$COLD_ADDRESS"
XPUB="$XPUB"
KEEP_SATS=$KEEP_SATS
MIN_SWEEP=$MIN_SWEEP
SWEEP_CRON="$SWEEP_CRON"
SWEEP_FREQ_LABEL="$SWEEP_FREQ_LABEL"
CONF
chmod 600 "$SWEEP_CONF"
# ── Reinstall cron ────────────────────────────────────────
crontab -l 2>/dev/null | grep -v "timmy-node.*sweep" | crontab - || true
(crontab -l 2>/dev/null; echo "# Timmy Node — auto-sweep ($SWEEP_FREQ_LABEL)") | crontab -
(crontab -l 2>/dev/null; echo "$SWEEP_CRON bash $INFRA_DIR/sweep.sh > /dev/null 2>&1") | crontab -
echo -e "\n${GREEN}Sweep configured:${NC}"
echo " Mode : ${SWEEP_MODE}"
case "$SWEEP_MODE" in
static) echo " Destination : ${COLD_ADDRESS:-(not set — sweep disabled)}" ;;
list)
COUNT=$(grep -c '[^[:space:]]' "$ADDR_LIST_FILE" 2>/dev/null || echo 0)
NEXT_INDEX=0; [[ -f "$STATE_FILE" ]] && source "$STATE_FILE"
echo " Addresses : ${COUNT} total, starting at index ${NEXT_INDEX}"
;;
xpub)
NEXT_INDEX=0; [[ -f "$STATE_FILE" ]] && source "$STATE_FILE"
echo " xpub : ${XPUB:0:20}..."
echo " Next index : ${NEXT_INDEX}"
;;
esac
echo " Keep on-chain: ${KEEP_SATS} sats"
echo " Min to sweep : ${MIN_SWEEP} sats"
echo " Frequency : ${SWEEP_FREQ_LABEL}"
echo ""
echo -e " Run now to test: ${CYAN}bash ops.sh run-sweep${NC}"
echo ""
;;
run-sweep)
echo -e "${CYAN}Running sweep now...${NC}"
bash "$INFRA_DIR/sweep.sh"
;;
relay:logs)
echo -e "${CYAN}Tailing strfry relay logs (Ctrl-C to stop)...${NC}"
docker compose logs -f --tail=100 strfry relay-policy
;;
relay:restart)
echo -e "${CYAN}Restarting Nostr relay services...${NC}"
docker compose restart relay-policy
sleep 2
docker compose restart strfry
echo -e "${GREEN}Done — relay services restarted.${NC}"
docker compose ps relay-policy strfry
;;
relay:status)
echo -e "\n${CYAN}── Nostr relay ───────────────────────────────${NC}"
docker compose ps relay-policy strfry
echo -e "\n${CYAN}── relay-policy health ───────────────────────${NC}"
docker exec relay-policy wget -qO- http://localhost:3080/health 2>/dev/null \
| (command -v jq >/dev/null 2>&1 && jq . || cat) \
|| echo "relay-policy not ready"
;;
help|*)
echo -e "\n${CYAN}Timmy Node operations:${NC}"
echo ""
echo " bash ops.sh status — overview of all services + balances"
echo " bash ops.sh sync — watch Bitcoin chain sync progress"
echo " bash ops.sh logs [service] — tail logs (bitcoin | lnd | lnbits)"
echo " bash ops.sh restart [svc] — restart a service or all services"
echo " bash ops.sh fund — get on-chain deposit address"
echo " bash ops.sh channels — list open and pending channels"
echo " bash ops.sh open-channel — open a Lightning channel"
echo " bash ops.sh lnbits-key — show LNBITS_URL and API key for Replit"
echo " bash ops.sh update — pull latest Docker images"
echo " bash ops.sh backup — backup LND channel state"
echo " bash ops.sh sweep — show sweep config, balance, and last sweep log"
echo " bash ops.sh configure-sweep — interactively set address, thresholds, frequency"
echo " bash ops.sh run-sweep — run sweep immediately (outside of cron schedule)"
echo ""
echo " bash ops.sh relay:logs — tail strfry + relay-policy logs"
echo " bash ops.sh relay:restart — restart relay-policy then strfry (safe order)"
echo " bash ops.sh relay:status — show relay container status + health"
echo ""
;;
esac