- 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.
190 lines
9.4 KiB
Bash
Executable File
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 ""
|