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.
This commit is contained in:
189
scripts/bitcoin-ln-node/status.sh
Executable file
189
scripts/bitcoin-ln-node/status.sh
Executable file
@@ -0,0 +1,189 @@
|
||||
#!/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 ""
|
||||
Reference in New Issue
Block a user