Files
timmy-tower/scripts/bitcoin-ln-node/status.sh
alexpaynex ca94c0a9e5 Add Bitcoin/LND/LNbits local node setup scripts and node diagnostics endpoint
- scripts/bitcoin-ln-node/setup.sh: one-shot installer for Bitcoin Core (pruned mainnet), LND, and LNbits on Apple Silicon Mac. Generates secrets, writes configs, installs launchd plists for auto-start.
- scripts/bitcoin-ln-node/start.sh: start all services via launchctl; waits for RPC readiness and auto-unlocks LND wallet.
- scripts/bitcoin-ln-node/stop.sh: graceful shutdown (lncli stop → bitcoin-cli stop).
- scripts/bitcoin-ln-node/status.sh: full health check (Bitcoin sync %, LND channels/balance, LNbits HTTP, bore tunnel). Supports --json mode for machine consumption.
- scripts/bitcoin-ln-node/expose.sh: opens bore tunnel from LNbits port 5000 to bore.pub for Replit access.
- scripts/bitcoin-ln-node/get-lnbits-key.sh: fetches LNbits admin API key and prints Replit secret values.
- artifacts/api-server/src/routes/node-diagnostics.ts: GET /api/admin/node-status (JSON) and /api/admin/node-status/html — Timmy self-diagnoses its LNbits/LND connectivity and reports issues.
2026-03-18 21:58:41 +00:00

190 lines
9.4 KiB
Bash
Executable File

#!/usr/bin/env bash
# =============================================================================
# Timmy node — status / diagnostics
# Run this any time to get a full health snapshot.
# Also machine-readable via: bash status.sh --json
# =============================================================================
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
SECRETS_FILE="$SCRIPT_DIR/.node-secrets"
LND_DIR="$HOME/.lnd"
LOG_DIR="$HOME/Library/Logs/timmy-node"
LNCLI="/opt/homebrew/bin/lncli"
BCLI="/opt/homebrew/bin/bitcoin-cli"
JSON_MODE=false
[[ "${1:-}" == "--json" ]] && JSON_MODE=true
[[ -f "$SECRETS_FILE" ]] && source "$SECRETS_FILE"
RED='\033[0;31m'; GREEN='\033[0;32m'; YELLOW='\033[1;33m'; CYAN='\033[0;36m'; BOLD='\033[1m'; NC='\033[0m'
section() { $JSON_MODE || echo -e "\n${BOLD}${CYAN}── $* ──${NC}"; }
pass() { $JSON_MODE || echo -e " ${GREEN}${NC} $*"; }
fail() { $JSON_MODE || echo -e " ${RED}${NC} $*"; }
warn() { $JSON_MODE || echo -e " ${YELLOW}!${NC} $*"; }
declare -A STATUS
# ─── Bitcoin Core ────────────────────────────────────────────────────────────
section "Bitcoin Core"
BTC_ARGS=()
[[ -n "${RPC_USER:-}" ]] && BTC_ARGS+=(-rpcuser="$RPC_USER" -rpcpassword="$RPC_PASS")
if ! pgrep -x bitcoind &>/dev/null; then
fail "bitcoind process not running"
STATUS[bitcoind]="down"
else
pass "bitcoind is running (PID: $(pgrep -x bitcoind))"
STATUS[bitcoind]="running"
if BTC_INFO=$("$BCLI" "${BTC_ARGS[@]}" getblockchaininfo 2>/dev/null); then
CHAIN=$(echo "$BTC_INFO" | python3 -c "import sys,json; d=json.load(sys.stdin); print(d['chain'])")
BLOCKS=$(echo "$BTC_INFO" | python3 -c "import sys,json; d=json.load(sys.stdin); print(d['blocks'])")
HEADERS=$(echo "$BTC_INFO" | python3 -c "import sys,json; d=json.load(sys.stdin); print(d['headers'])")
PROGRESS=$(echo "$BTC_INFO" | python3 -c "import sys,json; d=json.load(sys.stdin); print(f\"{d['verificationprogress']*100:.2f}%\")")
PRUNED=$(echo "$BTC_INFO" | python3 -c "import sys,json; d=json.load(sys.stdin); print(d.get('pruned', False))")
PRUNEHEIGHT=$(echo "$BTC_INFO" | python3 -c "import sys,json; d=json.load(sys.stdin); print(d.get('pruneheight', 'n/a'))")
SIZE_ON_DISK=$(echo "$BTC_INFO" | python3 -c "import sys,json; d=json.load(sys.stdin); print(f\"{d.get('size_on_disk',0)/1e9:.1f} GB\")")
pass "Chain: $CHAIN | Blocks: $BLOCKS / $HEADERS | Sync: $PROGRESS"
pass "Pruned: $PRUNED (prune height: $PRUNEHEIGHT) | Disk: $SIZE_ON_DISK"
STATUS[bitcoin_sync]="$PROGRESS"
STATUS[bitcoin_blocks]="$BLOCKS"
if MEMPOOL=$("$BCLI" "${BTC_ARGS[@]}" getmempoolinfo 2>/dev/null); then
TXCOUNT=$(echo "$MEMPOOL" | python3 -c "import sys,json; d=json.load(sys.stdin); print(d['size'])")
pass "Mempool: $TXCOUNT txs"
fi
NETINFO=$("$BCLI" "${BTC_ARGS[@]}" getnetworkinfo 2>/dev/null || echo "{}")
PEERS=$(echo "$NETINFO" | python3 -c "import sys,json; d=json.load(sys.stdin); print(d.get('connections', '?'))")
pass "Peers: $PEERS"
STATUS[bitcoin_peers]="$PEERS"
else
warn "bitcoind is running but RPC not ready yet (starting up?)"
STATUS[bitcoin_rpc]="not ready"
fi
fi
# ─── LND ─────────────────────────────────────────────────────────────────────
section "LND"
if ! pgrep -x lnd &>/dev/null; then
fail "lnd process not running"
STATUS[lnd]="down"
else
pass "lnd is running (PID: $(pgrep -x lnd))"
STATUS[lnd]="running"
if LND_INFO=$("$LNCLI" --lnddir="$LND_DIR" getinfo 2>/dev/null); then
LND_VERSION=$(echo "$LND_INFO" | python3 -c "import sys,json; d=json.load(sys.stdin); print(d['version'])" 2>/dev/null || echo "?")
LND_ALIAS=$(echo "$LND_INFO" | python3 -c "import sys,json; d=json.load(sys.stdin); print(d['alias'])" 2>/dev/null || echo "?")
LND_SYNCED=$(echo "$LND_INFO" | python3 -c "import sys,json; d=json.load(sys.stdin); print(d['synced_to_chain'])" 2>/dev/null || echo "?")
LND_PEERS=$(echo "$LND_INFO" | python3 -c "import sys,json; d=json.load(sys.stdin); print(d['num_peers'])" 2>/dev/null || echo "?")
LND_CHANS=$(echo "$LND_INFO" | python3 -c "import sys,json; d=json.load(sys.stdin); print(d['num_active_channels'])" 2>/dev/null || echo "?")
LND_BLOCK=$(echo "$LND_INFO" | python3 -c "import sys,json; d=json.load(sys.stdin); print(d['block_height'])" 2>/dev/null || echo "?")
pass "Version: $LND_VERSION | Alias: $LND_ALIAS"
pass "Synced to chain: $LND_SYNCED | Block: $LND_BLOCK"
pass "Peers: $LND_PEERS | Active channels: $LND_CHANS"
STATUS[lnd_synced]="$LND_SYNCED"
STATUS[lnd_channels]="$LND_CHANS"
if BAL=$("$LNCLI" --lnddir="$LND_DIR" walletbalance 2>/dev/null); then
CONFIRMED=$(echo "$BAL" | python3 -c "import sys,json; d=json.load(sys.stdin); print(d['confirmed_balance'])" 2>/dev/null || echo "?")
UNCONFIRMED=$(echo "$BAL" | python3 -c "import sys,json; d=json.load(sys.stdin); print(d['unconfirmed_balance'])" 2>/dev/null || echo "?")
pass "On-chain balance: ${CONFIRMED} sats confirmed, ${UNCONFIRMED} unconfirmed"
STATUS[wallet_confirmed]="$CONFIRMED"
fi
if CHANBAL=$("$LNCLI" --lnddir="$LND_DIR" channelbalance 2>/dev/null); then
LOCAL=$(echo "$CHANBAL" | python3 -c "import sys,json; d=json.load(sys.stdin); print(d['local_balance']['sat'])" 2>/dev/null || echo "?")
REMOTE=$(echo "$CHANBAL" | python3 -c "import sys,json; d=json.load(sys.stdin); print(d['remote_balance']['sat'])" 2>/dev/null || echo "?")
pass "Channel balance: ${LOCAL} sats local, ${REMOTE} sats remote"
STATUS[channel_local]="$LOCAL"
fi
elif "$LNCLI" --lnddir="$LND_DIR" state 2>/dev/null | grep -q "WAITING_TO_START\|LOCKED"; then
warn "LND wallet is LOCKED — run: lncli --lnddir=$LND_DIR unlock"
STATUS[lnd_state]="locked"
else
warn "LND running but wallet not ready (still starting or needs creation)"
STATUS[lnd_state]="not ready"
fi
fi
# ─── LNbits ──────────────────────────────────────────────────────────────────
section "LNbits"
LNBITS_URL_LOCAL="http://127.0.0.1:5000"
if ! pgrep -f "lnbits" &>/dev/null; then
fail "lnbits process not running"
STATUS[lnbits]="down"
else
pass "lnbits process is running"
STATUS[lnbits]="running"
if curl -sf "$LNBITS_URL_LOCAL/api/v1/health" &>/dev/null 2>&1; then
pass "LNbits HTTP reachable at $LNBITS_URL_LOCAL"
STATUS[lnbits_http]="ok"
else
warn "LNbits process is up but HTTP not responding yet (starting?)"
STATUS[lnbits_http]="starting"
fi
fi
# ─── Bore tunnel (if running) ─────────────────────────────────────────────────
section "Bore tunnel (external access)"
if BORE_PID=$(pgrep -f "bore local.*5000" 2>/dev/null); then
BORE_CMD=$(ps -p "$BORE_PID" -o args= 2>/dev/null || echo "?")
pass "bore tunnel active (PID $BORE_PID): $BORE_CMD"
STATUS[bore]="active"
else
warn "No bore tunnel running. Timmy (Replit) cannot reach LNbits."
echo " Start with: bash $SCRIPT_DIR/expose.sh"
STATUS[bore]="down"
fi
# ─── Recent errors ────────────────────────────────────────────────────────────
section "Recent log errors (last 20 lines)"
for svc in bitcoind lnd lnbits; do
ERR_FILE="$LOG_DIR/$svc.err"
if [[ -f "$ERR_FILE" ]] && [[ -s "$ERR_FILE" ]]; then
ERRS=$(tail -5 "$ERR_FILE" | grep -iE "error|fatal|panic|crit" || true)
if [[ -n "$ERRS" ]]; then
fail "$svc recent errors:"
echo "$ERRS" | sed 's/^/ /'
STATUS["${svc}_errors"]="yes"
else
pass "$svc error log: clean"
fi
fi
done
# ─── JSON output ─────────────────────────────────────────────────────────────
if $JSON_MODE; then
echo "{"
i=0; count=${#STATUS[@]}
for k in "${!STATUS[@]}"; do
i=$((i+1))
comma=$([[ $i -lt $count ]] && echo "," || echo "")
echo " \"$k\": \"${STATUS[$k]}\"$comma"
done
echo "}"
exit 0
fi
# ─── Summary ─────────────────────────────────────────────────────────────────
section "Summary"
ALL_OK=true
[[ "${STATUS[bitcoind]:-down}" != "running" ]] && { fail "bitcoind is down"; ALL_OK=false; }
[[ "${STATUS[lnd]:-down}" != "running" ]] && { fail "lnd is down"; ALL_OK=false; }
[[ "${STATUS[lnbits]:-down}" != "running" ]] && { fail "lnbits is down"; ALL_OK=false; }
[[ "${STATUS[bore]:-down}" != "active" ]] && { warn "bore tunnel is down — Timmy can't reach LNbits"; }
$ALL_OK && echo -e "\n ${GREEN}${BOLD}All core services are running.${NC}\n" || \
echo -e "\n ${RED}${BOLD}Some services need attention — see above.${NC}\n"
echo " Run 'bash $SCRIPT_DIR/start.sh' to start stopped services."
echo " Run 'bash $SCRIPT_DIR/expose.sh' to open the bore tunnel for Replit."
echo ""