#!/usr/bin/env bash # ============================================================ # Timmy Node — Day-to-day operations helper # Usage: bash ops.sh # ============================================================ 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 @: " 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@:$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" </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