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:
@@ -7,6 +7,7 @@ import demoRouter from "./demo.js";
|
||||
import devRouter from "./dev.js";
|
||||
import testkitRouter from "./testkit.js";
|
||||
import uiRouter from "./ui.js";
|
||||
import nodeDiagnosticsRouter from "./node-diagnostics.js";
|
||||
|
||||
const router: IRouter = Router();
|
||||
|
||||
@@ -17,6 +18,7 @@ router.use(sessionsRouter);
|
||||
router.use(demoRouter);
|
||||
router.use(testkitRouter);
|
||||
router.use(uiRouter);
|
||||
router.use(nodeDiagnosticsRouter);
|
||||
|
||||
if (process.env.NODE_ENV !== "production") {
|
||||
router.use(devRouter);
|
||||
|
||||
169
artifacts/api-server/src/routes/node-diagnostics.ts
Normal file
169
artifacts/api-server/src/routes/node-diagnostics.ts
Normal file
@@ -0,0 +1,169 @@
|
||||
import { Router, type IRouter } from "express";
|
||||
import { execFile } from "child_process";
|
||||
import { promisify } from "util";
|
||||
|
||||
const execFileAsync = promisify(execFile);
|
||||
const router: IRouter = Router();
|
||||
|
||||
interface NodeStatus {
|
||||
timestamp: string;
|
||||
lnbits: LNbitsStatus;
|
||||
lnd?: LNDStatus;
|
||||
bitcoin?: BitcoinStatus;
|
||||
overall: "ok" | "degraded" | "down";
|
||||
issues: string[];
|
||||
}
|
||||
|
||||
interface LNbitsStatus {
|
||||
reachable: boolean;
|
||||
url: string;
|
||||
walletBalance?: number;
|
||||
error?: string;
|
||||
}
|
||||
|
||||
interface LNDStatus {
|
||||
synced: boolean;
|
||||
blockHeight: number;
|
||||
activeChannels: number;
|
||||
walletBalance: number;
|
||||
channelBalance: number;
|
||||
}
|
||||
|
||||
interface BitcoinStatus {
|
||||
blocks: number;
|
||||
headers: number;
|
||||
syncPct: number;
|
||||
pruned: boolean;
|
||||
peers: number;
|
||||
}
|
||||
|
||||
async function probeLNbits(): Promise<LNbitsStatus> {
|
||||
const url = process.env.LNBITS_URL ?? "";
|
||||
const key = process.env.LNBITS_API_KEY ?? "";
|
||||
|
||||
if (!url || !key) {
|
||||
return { reachable: false, url: url || "(not configured)", error: "LNBITS_URL or LNBITS_API_KEY not set" };
|
||||
}
|
||||
|
||||
try {
|
||||
const res = await fetch(`${url}/api/v1/wallet`, {
|
||||
headers: { "X-Api-Key": key },
|
||||
signal: AbortSignal.timeout(5000),
|
||||
});
|
||||
if (!res.ok) {
|
||||
return { reachable: false, url, error: `HTTP ${res.status}` };
|
||||
}
|
||||
const data = (await res.json()) as { balance: number };
|
||||
return { reachable: true, url, walletBalance: Math.round((data.balance ?? 0) / 1000) };
|
||||
} catch (e) {
|
||||
return { reachable: false, url, error: String(e) };
|
||||
}
|
||||
}
|
||||
|
||||
async function probeLNbitsNodeInfo(): Promise<{ lnd?: LNDStatus; bitcoin?: BitcoinStatus }> {
|
||||
const url = process.env.LNBITS_URL ?? "";
|
||||
const key = process.env.LNBITS_API_KEY ?? "";
|
||||
if (!url || !key) return {};
|
||||
|
||||
try {
|
||||
const res = await fetch(`${url}/api/v1/extension/lndhub/api/v1/getinfo`, {
|
||||
headers: { "X-Api-Key": key },
|
||||
signal: AbortSignal.timeout(5000),
|
||||
});
|
||||
if (!res.ok) return {};
|
||||
const data = (await res.json()) as Record<string, unknown>;
|
||||
|
||||
const lnd: LNDStatus = {
|
||||
synced: Boolean(data["synced_to_chain"]),
|
||||
blockHeight: Number(data["block_height"] ?? 0),
|
||||
activeChannels: Number(data["num_active_channels"] ?? 0),
|
||||
walletBalance: 0,
|
||||
channelBalance: 0,
|
||||
};
|
||||
return { lnd };
|
||||
} catch {
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
router.get("/api/admin/node-status", async (_req, res) => {
|
||||
const issues: string[] = [];
|
||||
|
||||
const [lnbits, nodeInfo] = await Promise.all([
|
||||
probeLNbits(),
|
||||
probeLNbitsNodeInfo(),
|
||||
]);
|
||||
|
||||
if (!lnbits.reachable) {
|
||||
issues.push(`LNbits unreachable: ${lnbits.error ?? "unknown"}`);
|
||||
}
|
||||
if (nodeInfo.lnd && !nodeInfo.lnd.synced) {
|
||||
issues.push("LND not yet synced to chain tip");
|
||||
}
|
||||
|
||||
const overall: NodeStatus["overall"] =
|
||||
!lnbits.reachable ? "down" :
|
||||
issues.length > 0 ? "degraded" :
|
||||
"ok";
|
||||
|
||||
const status: NodeStatus = {
|
||||
timestamp: new Date().toISOString(),
|
||||
lnbits,
|
||||
...nodeInfo,
|
||||
overall,
|
||||
issues,
|
||||
};
|
||||
|
||||
res.json(status);
|
||||
});
|
||||
|
||||
router.get("/api/admin/node-status/html", async (_req, res) => {
|
||||
const issues: string[] = [];
|
||||
const [lnbits, nodeInfo] = await Promise.all([probeLNbits(), probeLNbitsNodeInfo()]);
|
||||
|
||||
if (!lnbits.reachable) issues.push(`LNbits unreachable: ${lnbits.error ?? "unknown"}`);
|
||||
if (nodeInfo.lnd && !nodeInfo.lnd.synced) issues.push("LND not yet synced");
|
||||
|
||||
const overall = !lnbits.reachable ? "down" : issues.length > 0 ? "degraded" : "ok";
|
||||
const color = overall === "ok" ? "#22c55e" : overall === "degraded" ? "#f59e0b" : "#ef4444";
|
||||
|
||||
res.setHeader("Content-Type", "text/html");
|
||||
res.send(`<!DOCTYPE html><html lang="en">
|
||||
<head><meta charset="UTF-8"><title>Timmy — Node Status</title>
|
||||
<style>
|
||||
body { font-family: monospace; background:#0f172a; color:#e2e8f0; padding:2rem; }
|
||||
h1 { color:#f8fafc; } .badge { padding:4px 12px; border-radius:4px; font-weight:bold; }
|
||||
table { border-collapse:collapse; margin-top:1rem; }
|
||||
td,th { padding:6px 16px; border-bottom:1px solid #1e293b; text-align:left; }
|
||||
th { color:#94a3b8; } .ok{color:#22c55e} .warn{color:#f59e0b} .bad{color:#ef4444}
|
||||
pre { background:#1e293b; padding:1rem; border-radius:6px; overflow:auto; }
|
||||
</style></head><body>
|
||||
<h1>⚡ Timmy Node Status</h1>
|
||||
<p>Overall: <span class="badge" style="background:${color}20;color:${color}">${overall.toUpperCase()}</span></p>
|
||||
|
||||
<h2>LNbits</h2>
|
||||
<table>
|
||||
<tr><th>URL</th><td>${lnbits.url}</td></tr>
|
||||
<tr><th>Reachable</th><td class="${lnbits.reachable ? "ok" : "bad"}">${lnbits.reachable ? "✓ yes" : "✗ no"}</td></tr>
|
||||
${lnbits.walletBalance !== undefined ? `<tr><th>Wallet Balance</th><td>${lnbits.walletBalance} sats</td></tr>` : ""}
|
||||
${lnbits.error ? `<tr><th>Error</th><td class="bad">${lnbits.error}</td></tr>` : ""}
|
||||
</table>
|
||||
|
||||
${nodeInfo.lnd ? `<h2>LND</h2>
|
||||
<table>
|
||||
<tr><th>Synced</th><td class="${nodeInfo.lnd.synced ? "ok" : "warn"}">${nodeInfo.lnd.synced ? "✓ yes" : "⏳ syncing"}</td></tr>
|
||||
<tr><th>Block Height</th><td>${nodeInfo.lnd.blockHeight.toLocaleString()}</td></tr>
|
||||
<tr><th>Active Channels</th><td>${nodeInfo.lnd.activeChannels}</td></tr>
|
||||
</table>` : ""}
|
||||
|
||||
${issues.length > 0 ? `<h2>Issues</h2><ul>${issues.map(i => `<li class="bad">${i}</li>`).join("")}</ul>` : ""}
|
||||
|
||||
<p style="color:#475569;margin-top:2rem;font-size:0.8rem">
|
||||
Refreshed at ${new Date().toISOString()} ·
|
||||
<a href="/api/admin/node-status" style="color:#60a5fa">JSON</a> ·
|
||||
<a href="/api/ui" style="color:#60a5fa">Timmy UI</a>
|
||||
</p>
|
||||
</body></html>`);
|
||||
});
|
||||
|
||||
export default router;
|
||||
97
scripts/bitcoin-ln-node/expose.sh
Executable file
97
scripts/bitcoin-ln-node/expose.sh
Executable file
@@ -0,0 +1,97 @@
|
||||
#!/usr/bin/env bash
|
||||
# =============================================================================
|
||||
# Timmy node — expose LNbits to Replit via bore tunnel
|
||||
# Run this after LNbits is up. Keep this terminal open.
|
||||
# =============================================================================
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
LNBITS_LOCAL="http://127.0.0.1:5000"
|
||||
|
||||
GREEN='\033[0;32m'; CYAN='\033[0;36m'; YELLOW='\033[1;33m'; RED='\033[0;31m'; NC='\033[0m'
|
||||
info() { echo -e "${CYAN}[expose]${NC} $*"; }
|
||||
ok() { echo -e "${GREEN}[ok]${NC} $*"; }
|
||||
warn() { echo -e "${YELLOW}[warn]${NC} $*"; }
|
||||
die() { echo -e "${RED}[error]${NC} $*" >&2; exit 1; }
|
||||
|
||||
# ─── Ensure LNbits is up ──────────────────────────────────────────────────────
|
||||
info "Checking LNbits is reachable locally…"
|
||||
if ! curl -sf "$LNBITS_LOCAL/api/v1/health" &>/dev/null; then
|
||||
warn "LNbits not responding at $LNBITS_LOCAL"
|
||||
warn "Waiting up to 30s…"
|
||||
for i in $(seq 1 6); do
|
||||
sleep 5
|
||||
curl -sf "$LNBITS_LOCAL/api/v1/health" &>/dev/null && break
|
||||
[[ $i -eq 6 ]] && die "LNbits still not up. Run 'bash $SCRIPT_DIR/start.sh' first."
|
||||
done
|
||||
fi
|
||||
ok "LNbits is running locally."
|
||||
|
||||
# ─── Ensure bore is installed ─────────────────────────────────────────────────
|
||||
if ! command -v bore &>/dev/null; then
|
||||
info "Installing bore (tunnel tool)…"
|
||||
if command -v cargo &>/dev/null; then
|
||||
cargo install bore-cli
|
||||
elif command -v brew &>/dev/null; then
|
||||
brew install bore-cli 2>/dev/null || {
|
||||
warn "bore not in Homebrew — installing via cargo…"
|
||||
brew install rust 2>/dev/null && cargo install bore-cli
|
||||
}
|
||||
else
|
||||
die "Neither cargo nor brew found. Install bore manually: https://github.com/ekzhang/bore"
|
||||
fi
|
||||
fi
|
||||
|
||||
# Kill any existing bore tunnel on port 5000
|
||||
if EXISTING=$(pgrep -f "bore local.*5000" 2>/dev/null); then
|
||||
info "Killing existing bore tunnel (PID $EXISTING)…"
|
||||
kill "$EXISTING" 2>/dev/null || true
|
||||
sleep 1
|
||||
fi
|
||||
|
||||
# ─── Start bore ──────────────────────────────────────────────────────────────
|
||||
info "Opening bore tunnel: local:5000 → bore.pub…"
|
||||
echo ""
|
||||
|
||||
# bore prints the public address on startup — capture it
|
||||
BORE_OUT=$(mktemp)
|
||||
bore local 5000 --to bore.pub 2>&1 &
|
||||
BORE_PID=$!
|
||||
|
||||
sleep 3
|
||||
|
||||
# Extract the assigned port from bore output
|
||||
if BORE_LINE=$(bore local 5000 --to bore.pub --help 2>&1 | head -1); then true; fi
|
||||
# Check if bore is still running
|
||||
if ! kill -0 $BORE_PID 2>/dev/null; then
|
||||
die "bore exited immediately. Check your internet connection."
|
||||
fi
|
||||
|
||||
# bore prints: "listening at bore.pub:PORT"
|
||||
# We need to find what port was assigned
|
||||
BORE_PORT=$(ps aux | grep "bore local 5000" | grep -v grep | grep -oE '\-\-port [0-9]+' | awk '{print $2}' || true)
|
||||
if [[ -z "$BORE_PORT" ]]; then
|
||||
# Fallback: tail the bore process log
|
||||
sleep 2
|
||||
BORE_PORT=$(bore local 5000 --to bore.pub 2>&1 & sleep 3; pkill -f "bore local" 2>/dev/null || true)
|
||||
fi
|
||||
|
||||
echo -e "${GREEN}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}"
|
||||
echo -e "${GREEN} Bore tunnel is LIVE (PID: $BORE_PID)${NC}"
|
||||
echo -e "${GREEN}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}"
|
||||
echo ""
|
||||
echo " LNbits is now reachable from Replit."
|
||||
echo ""
|
||||
echo " Set these secrets in Replit:"
|
||||
echo " ┌──────────────────────────────────────────────────┐"
|
||||
echo " │ LNBITS_URL = http://bore.pub:<PORT> │"
|
||||
echo " │ LNBITS_API_KEY = <your-lnbits-admin-key> │"
|
||||
echo " └──────────────────────────────────────────────────┘"
|
||||
echo ""
|
||||
echo " Get your API key: bash $SCRIPT_DIR/get-lnbits-key.sh"
|
||||
echo ""
|
||||
echo " Keep this terminal open. Ctrl+C to stop the tunnel."
|
||||
echo ""
|
||||
|
||||
# Wait for bore to exit
|
||||
wait $BORE_PID
|
||||
106
scripts/bitcoin-ln-node/get-lnbits-key.sh
Executable file
106
scripts/bitcoin-ln-node/get-lnbits-key.sh
Executable file
@@ -0,0 +1,106 @@
|
||||
#!/usr/bin/env bash
|
||||
# =============================================================================
|
||||
# Timmy node — fetch LNbits admin API key
|
||||
# Run this after LNbits is up and has been configured.
|
||||
# Prints the LNBITS_API_KEY to add to Replit secrets.
|
||||
# =============================================================================
|
||||
set -euo pipefail
|
||||
|
||||
LNBITS_LOCAL="http://127.0.0.1:5000"
|
||||
LNBITS_DATA_DIR="$HOME/.lnbits-data"
|
||||
|
||||
GREEN='\033[0;32m'; CYAN='\033[0;36m'; YELLOW='\033[1;33m'; RED='\033[0;31m'; NC='\033[0m'
|
||||
info() { echo -e "${CYAN}[keys]${NC} $*"; }
|
||||
ok() { echo -e "${GREEN}[ok]${NC} $*"; }
|
||||
warn() { echo -e "${YELLOW}[warn]${NC} $*"; }
|
||||
die() { echo -e "${RED}[error]${NC} $*" >&2; exit 1; }
|
||||
|
||||
# Check LNbits is up
|
||||
curl -sf "$LNBITS_LOCAL/api/v1/health" &>/dev/null \
|
||||
|| die "LNbits not reachable at $LNBITS_LOCAL. Run 'bash start.sh' first."
|
||||
|
||||
# ─── Try to get super user from env file ─────────────────────────────────────
|
||||
SUPER_USER=""
|
||||
if [[ -f "$LNBITS_DATA_DIR/.env" ]]; then
|
||||
SUPER_USER=$(grep LNBITS_SUPER_USER "$LNBITS_DATA_DIR/.env" | cut -d= -f2 | tr -d '"' || true)
|
||||
fi
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
SECRETS_FILE="$SCRIPT_DIR/.node-secrets"
|
||||
[[ -f "$SECRETS_FILE" ]] && source "$SECRETS_FILE"
|
||||
SUPER_USER="${SUPER_USER:-${LNBITS_SUPER_USER:-}}"
|
||||
|
||||
if [[ -z "$SUPER_USER" ]]; then
|
||||
# LNbits auto-generates a superuser on first run — find it in the SQLite DB
|
||||
DB_FILE=$(find "$LNBITS_DATA_DIR" -name "*.sqlite3" 2>/dev/null | head -1 || true)
|
||||
if [[ -n "$DB_FILE" ]] && command -v sqlite3 &>/dev/null; then
|
||||
SUPER_USER=$(sqlite3 "$DB_FILE" "SELECT id FROM accounts WHERE is_super_user=1 LIMIT 1;" 2>/dev/null || true)
|
||||
fi
|
||||
fi
|
||||
|
||||
if [[ -z "$SUPER_USER" ]]; then
|
||||
# Last resort: check LNbits log for the first-run superuser line
|
||||
LOG_FILE="$HOME/Library/Logs/timmy-node/lnbits.log"
|
||||
if [[ -f "$LOG_FILE" ]]; then
|
||||
SUPER_USER=$(grep -oE "super user id: [a-f0-9]+" "$LOG_FILE" | tail -1 | awk '{print $4}' || true)
|
||||
fi
|
||||
fi
|
||||
|
||||
if [[ -z "$SUPER_USER" ]]; then
|
||||
warn "Could not auto-detect LNbits super user ID."
|
||||
echo ""
|
||||
echo " Visit: $LNBITS_LOCAL"
|
||||
echo " 1. Create a wallet"
|
||||
echo " 2. Go to Wallet → API Info"
|
||||
echo " 3. Copy the Admin key"
|
||||
echo ""
|
||||
echo " Then add to Replit:"
|
||||
echo " LNBITS_URL = http://bore.pub:<PORT>"
|
||||
echo " LNBITS_API_KEY = <admin-key>"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
info "Super user: $SUPER_USER"
|
||||
|
||||
# Create a wallet for Timmy via superuser API
|
||||
WALLET_RESPONSE=$(curl -sf -X POST "$LNBITS_LOCAL/api/v1/wallet" \
|
||||
-H "Content-Type: application/json" \
|
||||
-H "X-Api-Key: $SUPER_USER" \
|
||||
-d '{"name":"Timmy"}' 2>/dev/null || true)
|
||||
|
||||
if [[ -n "$WALLET_RESPONSE" ]]; then
|
||||
ADMIN_KEY=$(echo "$WALLET_RESPONSE" | python3 -c "import sys,json; d=json.load(sys.stdin); print(d.get('adminkey',''))" 2>/dev/null || true)
|
||||
INKEY=$(echo "$WALLET_RESPONSE" | python3 -c "import sys,json; d=json.load(sys.stdin); print(d.get('inkey',''))" 2>/dev/null || true)
|
||||
WALLET_ID=$(echo "$WALLET_RESPONSE" | python3 -c "import sys,json; d=json.load(sys.stdin); print(d.get('id',''))" 2>/dev/null || true)
|
||||
|
||||
if [[ -n "$ADMIN_KEY" ]]; then
|
||||
ok "Timmy wallet created (ID: $WALLET_ID)"
|
||||
echo ""
|
||||
echo -e "${GREEN}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}"
|
||||
echo -e "${GREEN} Add these to Replit secrets:${NC}"
|
||||
echo -e "${GREEN}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}"
|
||||
echo ""
|
||||
echo " LNBITS_URL = http://bore.pub:<PORT> ← from expose.sh"
|
||||
echo " LNBITS_API_KEY = $ADMIN_KEY"
|
||||
echo ""
|
||||
echo " Invoice key (read-only): $INKEY"
|
||||
echo ""
|
||||
# Save to secrets file
|
||||
cat >> "$SECRETS_FILE" <<EOF
|
||||
LNBITS_WALLET_ID="$WALLET_ID"
|
||||
LNBITS_ADMIN_KEY="$ADMIN_KEY"
|
||||
LNBITS_INVOICE_KEY="$INKEY"
|
||||
EOF
|
||||
ok "Keys saved to $SECRETS_FILE"
|
||||
return 0 2>/dev/null || true
|
||||
fi
|
||||
fi
|
||||
|
||||
# Fallback: just print the wallet URL
|
||||
warn "Could not create wallet automatically."
|
||||
echo ""
|
||||
echo " Visit $LNBITS_LOCAL in your browser:"
|
||||
echo " 1. Create an account / wallet named 'Timmy'"
|
||||
echo " 2. Wallet → API Info → copy Admin key"
|
||||
echo " 3. Add to Replit: LNBITS_API_KEY = <admin key>"
|
||||
echo ""
|
||||
333
scripts/bitcoin-ln-node/setup.sh
Executable file
333
scripts/bitcoin-ln-node/setup.sh
Executable file
@@ -0,0 +1,333 @@
|
||||
#!/usr/bin/env bash
|
||||
# =============================================================================
|
||||
# Timmy — Bitcoin Core + LND + LNbits automated setup
|
||||
# Target: macOS, Apple Silicon (M3 Max)
|
||||
# Mode: mainnet, pruned (~20 GB block storage)
|
||||
# =============================================================================
|
||||
set -euo pipefail
|
||||
|
||||
BREW="/opt/homebrew/bin/brew"
|
||||
BITCOIN_DIR="$HOME/Library/Application Support/Bitcoin"
|
||||
LND_DIR="$HOME/.lnd"
|
||||
LNBITS_DIR="$HOME/.lnbits"
|
||||
LAUNCHD_DIR="$HOME/Library/LaunchAgents"
|
||||
LOG_DIR="$HOME/Library/Logs/timmy-node"
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
|
||||
RED='\033[0;31m'; GREEN='\033[0;32m'; YELLOW='\033[1;33m'; CYAN='\033[0;36m'; NC='\033[0m'
|
||||
info() { echo -e "${CYAN}[setup]${NC} $*"; }
|
||||
ok() { echo -e "${GREEN}[ok]${NC} $*"; }
|
||||
warn() { echo -e "${YELLOW}[warn]${NC} $*"; }
|
||||
die() { echo -e "${RED}[error]${NC} $*" >&2; exit 1; }
|
||||
|
||||
# ─── Guards ──────────────────────────────────────────────────────────────────
|
||||
[[ "$(uname -s)" == "Darwin" ]] || die "This script is for macOS only."
|
||||
[[ "$(uname -m)" == "arm64" ]] || warn "Not Apple Silicon — paths may differ."
|
||||
|
||||
command -v "$BREW" &>/dev/null || die "Homebrew not found at $BREW. Install from https://brew.sh"
|
||||
|
||||
# ─── Step 1: Packages ────────────────────────────────────────────────────────
|
||||
info "Installing Bitcoin Core and LND via Homebrew…"
|
||||
"$BREW" update --quiet
|
||||
"$BREW" install bitcoin lnd || true # 'true' so re-runs don't abort on "already installed"
|
||||
|
||||
command -v bitcoind &>/dev/null || die "bitcoind not found after install."
|
||||
command -v lnd &>/dev/null || die "lnd not found after install."
|
||||
ok "Bitcoin Core $("$BREW" info --json bitcoin | python3 -c "import sys,json; d=json.load(sys.stdin); print(d[0]['installed'][0]['version'])" 2>/dev/null || bitcoind --version | head -1) installed."
|
||||
ok "LND $("$BREW" info --json lnd | python3 -c "import sys,json; d=json.load(sys.stdin); print(d[0]['installed'][0]['version'])" 2>/dev/null || lnd --version | head -1) installed."
|
||||
|
||||
# ─── Step 2: LNbits (Python) ─────────────────────────────────────────────────
|
||||
info "Setting up LNbits…"
|
||||
if ! command -v python3 &>/dev/null; then
|
||||
"$BREW" install python3
|
||||
fi
|
||||
|
||||
PYTHON="$(command -v python3)"
|
||||
PIP="$PYTHON -m pip"
|
||||
|
||||
if [[ ! -d "$LNBITS_DIR" ]]; then
|
||||
info "Cloning LNbits…"
|
||||
git clone --depth 1 https://github.com/lnbits/lnbits.git "$LNBITS_DIR"
|
||||
else
|
||||
info "LNbits already cloned at $LNBITS_DIR"
|
||||
fi
|
||||
|
||||
info "Installing LNbits Python deps (this may take a minute)…"
|
||||
cd "$LNBITS_DIR"
|
||||
if command -v uv &>/dev/null; then
|
||||
uv pip install --system poetry 2>/dev/null || $PIP install --quiet poetry
|
||||
else
|
||||
$PIP install --quiet poetry 2>/dev/null || true
|
||||
fi
|
||||
|
||||
if command -v poetry &>/dev/null || [[ -f "$HOME/.local/bin/poetry" ]]; then
|
||||
POETRY="$(command -v poetry 2>/dev/null || echo "$HOME/.local/bin/poetry")"
|
||||
"$POETRY" install --no-root --quiet 2>/dev/null || warn "poetry install had warnings (often OK)"
|
||||
else
|
||||
warn "poetry not found — trying pip install from pyproject.toml…"
|
||||
$PIP install --quiet lnbits 2>/dev/null || warn "direct pip install of lnbits failed — check $LNBITS_DIR manually"
|
||||
fi
|
||||
cd "$SCRIPT_DIR"
|
||||
ok "LNbits ready at $LNBITS_DIR"
|
||||
|
||||
# ─── Step 3: Directories & Logs ──────────────────────────────────────────────
|
||||
info "Creating data and log directories…"
|
||||
mkdir -p "$LOG_DIR" "$LAUNCHD_DIR"
|
||||
|
||||
# ─── Step 4: Generate secrets ────────────────────────────────────────────────
|
||||
SECRETS_FILE="$SCRIPT_DIR/.node-secrets"
|
||||
if [[ -f "$SECRETS_FILE" ]]; then
|
||||
info "Loading existing secrets from $SECRETS_FILE"
|
||||
source "$SECRETS_FILE"
|
||||
else
|
||||
info "Generating fresh secrets…"
|
||||
RPC_USER="timmy"
|
||||
RPC_PASS="$(openssl rand -hex 32)"
|
||||
WALLET_PASS="$(openssl rand -hex 24)"
|
||||
LND_MACAROON_PASS="$(openssl rand -hex 24)"
|
||||
cat > "$SECRETS_FILE" <<EOF
|
||||
RPC_USER="$RPC_USER"
|
||||
RPC_PASS="$RPC_PASS"
|
||||
WALLET_PASS="$WALLET_PASS"
|
||||
LND_MACAROON_PASS="$LND_MACAROON_PASS"
|
||||
EOF
|
||||
chmod 600 "$SECRETS_FILE"
|
||||
ok "Secrets written to $SECRETS_FILE (keep this safe!)"
|
||||
fi
|
||||
|
||||
# ─── Step 5: Bitcoin Core config ─────────────────────────────────────────────
|
||||
info "Writing Bitcoin Core config…"
|
||||
mkdir -p "$BITCOIN_DIR"
|
||||
# Only write if doesn't exist (don't clobber a running node's config)
|
||||
if [[ ! -f "$BITCOIN_DIR/bitcoin.conf" ]]; then
|
||||
cat > "$BITCOIN_DIR/bitcoin.conf" <<EOF
|
||||
# Timmy node — mainnet, pruned
|
||||
mainnet=1
|
||||
server=1
|
||||
prune=20000
|
||||
|
||||
# RPC
|
||||
rpcuser=$RPC_USER
|
||||
rpcpassword=$RPC_PASS
|
||||
rpcallowip=127.0.0.1
|
||||
rpcbind=127.0.0.1
|
||||
rpcport=8332
|
||||
|
||||
# ZMQ (required by LND)
|
||||
zmqpubrawblock=tcp://127.0.0.1:28332
|
||||
zmqpubrawtx=tcp://127.0.0.1:28333
|
||||
|
||||
# Performance
|
||||
dbcache=4096
|
||||
maxmempool=512
|
||||
maxconnections=24
|
||||
listen=1
|
||||
|
||||
# Logging (keep it manageable)
|
||||
debug=0
|
||||
shrinkdebugfile=1
|
||||
EOF
|
||||
ok "bitcoin.conf written."
|
||||
else
|
||||
ok "bitcoin.conf already exists — leaving untouched."
|
||||
fi
|
||||
|
||||
# ─── Step 6: LND config ──────────────────────────────────────────────────────
|
||||
info "Writing LND config…"
|
||||
mkdir -p "$LND_DIR"
|
||||
if [[ ! -f "$LND_DIR/lnd.conf" ]]; then
|
||||
cat > "$LND_DIR/lnd.conf" <<EOF
|
||||
[Application Options]
|
||||
debuglevel=info
|
||||
maxpendingchannels=10
|
||||
# Expose REST API for LNbits
|
||||
restlisten=0.0.0.0:8080
|
||||
rpclisten=0.0.0.0:10009
|
||||
# Wallet password auto-unlock file (written by start.sh)
|
||||
wallet-unlock-password-file=$LND_DIR/.wallet-password
|
||||
|
||||
[Bitcoin]
|
||||
bitcoin.active=1
|
||||
bitcoin.mainnet=1
|
||||
bitcoin.node=bitcoind
|
||||
|
||||
[Bitcoind]
|
||||
bitcoind.rpchost=127.0.0.1:8332
|
||||
bitcoind.rpcuser=$RPC_USER
|
||||
bitcoind.rpcpass=$RPC_PASS
|
||||
bitcoind.zmqpubrawblock=tcp://127.0.0.1:28332
|
||||
bitcoind.zmqpubrawtx=tcp://127.0.0.1:28333
|
||||
bitcoind.estimatemode=ECONOMICAL
|
||||
|
||||
[tor]
|
||||
tor.active=0
|
||||
EOF
|
||||
ok "lnd.conf written."
|
||||
else
|
||||
ok "lnd.conf already exists — leaving untouched."
|
||||
fi
|
||||
|
||||
# Write wallet unlock password file (used by LND auto-unlock)
|
||||
echo -n "$WALLET_PASS" > "$LND_DIR/.wallet-password"
|
||||
chmod 600 "$LND_DIR/.wallet-password"
|
||||
|
||||
# ─── Step 7: LNbits .env ─────────────────────────────────────────────────────
|
||||
info "Writing LNbits environment…"
|
||||
LNBITS_DATA_DIR="$HOME/.lnbits-data"
|
||||
mkdir -p "$LNBITS_DATA_DIR"
|
||||
|
||||
if [[ ! -f "$LNBITS_DATA_DIR/.env" ]]; then
|
||||
# Generate a superuser ID for LNbits
|
||||
LNBITS_SUPER_USER="$(openssl rand -hex 16)"
|
||||
cat > "$LNBITS_DATA_DIR/.env" <<EOF
|
||||
# LNbits config — Timmy node
|
||||
LNBITS_DATA_FOLDER=$LNBITS_DATA_DIR
|
||||
LNBITS_BACKEND_WALLET_CLASS=LndRestWallet
|
||||
LND_REST_ENDPOINT=https://127.0.0.1:8080
|
||||
LND_REST_CERT=$LND_DIR/tls.cert
|
||||
LND_REST_MACAROON=$LND_DIR/data/chain/bitcoin/mainnet/admin.macaroon
|
||||
LNBITS_SITE_TITLE=Timmy Node
|
||||
LNBITS_ALLOWED_IPS=127.0.0.1
|
||||
HOST=127.0.0.1
|
||||
PORT=5000
|
||||
FORWARDED_ALLOW_IPS=*
|
||||
# Uncomment to set a fixed super user (otherwise auto-generated)
|
||||
# LNBITS_SUPER_USER=$LNBITS_SUPER_USER
|
||||
EOF
|
||||
# Append super user to secrets
|
||||
echo "LNBITS_SUPER_USER=\"$LNBITS_SUPER_USER\"" >> "$SECRETS_FILE"
|
||||
ok "LNbits .env written to $LNBITS_DATA_DIR/.env"
|
||||
else
|
||||
ok "LNbits .env already exists — leaving untouched."
|
||||
fi
|
||||
|
||||
# ─── Step 8: LaunchAgent plists ──────────────────────────────────────────────
|
||||
info "Writing LaunchAgent plists (auto-start on login)…"
|
||||
|
||||
# Bitcoin Core
|
||||
cat > "$LAUNCHD_DIR/com.timmy.bitcoind.plist" <<EOF
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>Label</key> <string>com.timmy.bitcoind</string>
|
||||
<key>ProgramArguments</key>
|
||||
<array>
|
||||
<string>/opt/homebrew/bin/bitcoind</string>
|
||||
<string>-conf=$HOME/Library/Application Support/Bitcoin/bitcoin.conf</string>
|
||||
<string>-datadir=$HOME/Library/Application Support/Bitcoin</string>
|
||||
<string>-daemon=0</string>
|
||||
</array>
|
||||
<key>RunAtLoad</key> <true/>
|
||||
<key>KeepAlive</key> <true/>
|
||||
<key>StandardOutPath</key> <string>$LOG_DIR/bitcoind.log</string>
|
||||
<key>StandardErrorPath</key> <string>$LOG_DIR/bitcoind.err</string>
|
||||
<key>EnvironmentVariables</key>
|
||||
<dict>
|
||||
<key>HOME</key><string>$HOME</string>
|
||||
</dict>
|
||||
</dict>
|
||||
</plist>
|
||||
EOF
|
||||
|
||||
# LND — starts after bitcoind (bitcoind takes ~30s to be RPC-ready)
|
||||
cat > "$LAUNCHD_DIR/com.timmy.lnd.plist" <<EOF
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>Label</key> <string>com.timmy.lnd</string>
|
||||
<key>ProgramArguments</key>
|
||||
<array>
|
||||
<string>/opt/homebrew/bin/lnd</string>
|
||||
<string>--configfile=$LND_DIR/lnd.conf</string>
|
||||
<string>--lnddir=$LND_DIR</string>
|
||||
</array>
|
||||
<key>RunAtLoad</key> <true/>
|
||||
<key>KeepAlive</key> <true/>
|
||||
<key>StandardOutPath</key> <string>$LOG_DIR/lnd.log</string>
|
||||
<key>StandardErrorPath</key> <string>$LOG_DIR/lnd.err</string>
|
||||
<key>EnvironmentVariables</key>
|
||||
<dict>
|
||||
<key>HOME</key><string>$HOME</string>
|
||||
</dict>
|
||||
</dict>
|
||||
</plist>
|
||||
EOF
|
||||
|
||||
# LNbits — depends on LND being up
|
||||
cat > "$LAUNCHD_DIR/com.timmy.lnbits.plist" <<EOF
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>Label</key> <string>com.timmy.lnbits</string>
|
||||
<key>ProgramArguments</key>
|
||||
<array>
|
||||
<string>/bin/bash</string>
|
||||
<string>-c</string>
|
||||
<string>cd $LNBITS_DIR && env \$(cat $LNBITS_DATA_DIR/.env | grep -v '^#' | xargs) poetry run lnbits</string>
|
||||
</array>
|
||||
<key>RunAtLoad</key> <true/>
|
||||
<key>KeepAlive</key> <true/>
|
||||
<key>StandardOutPath</key> <string>$LOG_DIR/lnbits.log</string>
|
||||
<key>StandardErrorPath</key> <string>$LOG_DIR/lnbits.err</string>
|
||||
<key>EnvironmentVariables</key>
|
||||
<dict>
|
||||
<key>HOME</key><string>$HOME</string>
|
||||
<key>PATH</key><string>/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin</string>
|
||||
</dict>
|
||||
</dict>
|
||||
</plist>
|
||||
EOF
|
||||
|
||||
ok "LaunchAgent plists written to $LAUNCHD_DIR"
|
||||
|
||||
# ─── Step 9: Load plists ─────────────────────────────────────────────────────
|
||||
info "Loading LaunchAgents…"
|
||||
for plist in com.timmy.bitcoind com.timmy.lnd com.timmy.lnbits; do
|
||||
launchctl unload "$LAUNCHD_DIR/$plist.plist" 2>/dev/null || true
|
||||
launchctl load -w "$LAUNCHD_DIR/$plist.plist"
|
||||
ok "$plist loaded."
|
||||
done
|
||||
|
||||
# ─── Step 10: Create LND wallet if needed ─────────────────────────────────────
|
||||
info "Waiting 15s for LND to start before checking wallet…"
|
||||
sleep 15
|
||||
|
||||
LNCLI="/opt/homebrew/bin/lncli"
|
||||
if ! "$LNCLI" --lnddir="$LND_DIR" getinfo &>/dev/null; then
|
||||
info "LND wallet not yet initialized — creating one now…"
|
||||
echo "Wallet password will be: $WALLET_PASS"
|
||||
echo "---"
|
||||
echo "Run this command in a new terminal:"
|
||||
echo ""
|
||||
echo " lncli --lnddir=$LND_DIR create"
|
||||
echo ""
|
||||
echo "Use password: $WALLET_PASS"
|
||||
echo "Choose 'n' for existing seed (new wallet)"
|
||||
echo "Save the 24-word seed in a safe place!"
|
||||
echo "---"
|
||||
warn "After creating the wallet, run: bash $SCRIPT_DIR/get-lnbits-key.sh"
|
||||
else
|
||||
ok "LND wallet already exists and is accessible."
|
||||
fi
|
||||
|
||||
# ─── Done ────────────────────────────────────────────────────────────────────
|
||||
echo ""
|
||||
echo -e "${GREEN}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}"
|
||||
echo -e "${GREEN} Timmy node setup complete!${NC}"
|
||||
echo -e "${GREEN}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}"
|
||||
echo ""
|
||||
echo " Bitcoin Core is syncing — this takes 1-3 days (pruned)."
|
||||
echo " LND will connect once bitcoind is at chain tip."
|
||||
echo " LNbits will be live at http://127.0.0.1:5000 once LND is ready."
|
||||
echo ""
|
||||
echo " Next steps:"
|
||||
echo " 1. Create your LND wallet: lncli --lnddir=$LND_DIR create"
|
||||
echo " 2. Check sync status: bash $SCRIPT_DIR/status.sh"
|
||||
echo " 3. Once synced, get key: bash $SCRIPT_DIR/get-lnbits-key.sh"
|
||||
echo ""
|
||||
echo " Secrets are in: $SECRETS_FILE"
|
||||
echo " Logs are in: $LOG_DIR/"
|
||||
echo ""
|
||||
101
scripts/bitcoin-ln-node/start.sh
Executable file
101
scripts/bitcoin-ln-node/start.sh
Executable file
@@ -0,0 +1,101 @@
|
||||
#!/usr/bin/env bash
|
||||
# =============================================================================
|
||||
# Timmy node — start all services
|
||||
# Safe to run multiple times; won't double-start a running service.
|
||||
# =============================================================================
|
||||
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"
|
||||
LAUNCHD_DIR="$HOME/Library/LaunchAgents"
|
||||
LNCLI="/opt/homebrew/bin/lncli"
|
||||
|
||||
[[ -f "$SECRETS_FILE" ]] && source "$SECRETS_FILE"
|
||||
mkdir -p "$LOG_DIR"
|
||||
|
||||
GREEN='\033[0;32m'; CYAN='\033[0;36m'; YELLOW='\033[1;33m'; NC='\033[0m'
|
||||
info() { echo -e "${CYAN}[start]${NC} $*"; }
|
||||
ok() { echo -e "${GREEN}[ok]${NC} $*"; }
|
||||
warn() { echo -e "${YELLOW}[warn]${NC} $*"; }
|
||||
|
||||
start_launchagent() {
|
||||
local label="$1"
|
||||
local plist="$LAUNCHD_DIR/$label.plist"
|
||||
if ! launchctl list | grep -q "$label"; then
|
||||
launchctl load -w "$plist" 2>/dev/null && ok "$label started." || warn "Could not load $label."
|
||||
else
|
||||
ok "$label is already loaded."
|
||||
fi
|
||||
}
|
||||
|
||||
# ─── Bitcoin Core ─────────────────────────────────────────────────────────────
|
||||
info "Starting Bitcoin Core…"
|
||||
start_launchagent "com.timmy.bitcoind"
|
||||
|
||||
# Wait for RPC to be ready (up to 60s)
|
||||
info "Waiting for bitcoind RPC (up to 60s)…"
|
||||
BTC_ARGS=()
|
||||
[[ -n "${RPC_USER:-}" ]] && BTC_ARGS+=(-rpcuser="$RPC_USER" -rpcpassword="$RPC_PASS")
|
||||
for i in $(seq 1 12); do
|
||||
if /opt/homebrew/bin/bitcoin-cli "${BTC_ARGS[@]}" getblockchaininfo &>/dev/null 2>&1; then
|
||||
ok "bitcoind RPC ready."
|
||||
break
|
||||
fi
|
||||
[[ $i -eq 12 ]] && warn "bitcoind RPC not ready after 60s — it may still be starting." || sleep 5
|
||||
done
|
||||
|
||||
# ─── LND ──────────────────────────────────────────────────────────────────────
|
||||
info "Starting LND…"
|
||||
start_launchagent "com.timmy.lnd"
|
||||
|
||||
# Wait for LND to be ready (up to 60s)
|
||||
info "Waiting for LND (up to 60s)…"
|
||||
for i in $(seq 1 12); do
|
||||
LND_STATE=$("$LNCLI" --lnddir="$LND_DIR" state 2>/dev/null | python3 -c "import sys,json; d=json.load(sys.stdin); print(d.get('state','?'))" 2>/dev/null || echo "?")
|
||||
case "$LND_STATE" in
|
||||
RPC_ACTIVE|SERVER_ACTIVE)
|
||||
ok "LND is up (state: $LND_STATE)."
|
||||
break
|
||||
;;
|
||||
WALLET_NOT_CREATED)
|
||||
warn "LND wallet needs to be created:"
|
||||
echo " lncli --lnddir=$LND_DIR create"
|
||||
echo " Use password from $SECRETS_FILE"
|
||||
break
|
||||
;;
|
||||
LOCKED)
|
||||
info "LND wallet is locked — unlocking…"
|
||||
if [[ -f "$LND_DIR/.wallet-password" ]]; then
|
||||
"$LNCLI" --lnddir="$LND_DIR" unlock --stdin < "$LND_DIR/.wallet-password" 2>/dev/null && ok "Wallet unlocked." || warn "Auto-unlock failed — run: lncli --lnddir=$LND_DIR unlock"
|
||||
else
|
||||
warn "No wallet-password file found. Run: lncli --lnddir=$LND_DIR unlock"
|
||||
fi
|
||||
break
|
||||
;;
|
||||
*)
|
||||
[[ $i -eq 12 ]] && warn "LND not ready after 60s (state: $LND_STATE)" || sleep 5
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
# ─── LNbits ───────────────────────────────────────────────────────────────────
|
||||
info "Starting LNbits…"
|
||||
start_launchagent "com.timmy.lnbits"
|
||||
|
||||
info "Waiting for LNbits HTTP (up to 30s)…"
|
||||
for i in $(seq 1 6); do
|
||||
if curl -sf "http://127.0.0.1:5000/api/v1/health" &>/dev/null; then
|
||||
ok "LNbits is up at http://127.0.0.1:5000"
|
||||
break
|
||||
fi
|
||||
[[ $i -eq 6 ]] && warn "LNbits not responding after 30s — check $LOG_DIR/lnbits.err" || sleep 5
|
||||
done
|
||||
|
||||
echo ""
|
||||
ok "All services started. Run 'bash $SCRIPT_DIR/status.sh' for a full health check."
|
||||
echo ""
|
||||
echo " To expose LNbits to Replit, run:"
|
||||
echo " bash $SCRIPT_DIR/expose.sh"
|
||||
echo ""
|
||||
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 ""
|
||||
44
scripts/bitcoin-ln-node/stop.sh
Executable file
44
scripts/bitcoin-ln-node/stop.sh
Executable file
@@ -0,0 +1,44 @@
|
||||
#!/usr/bin/env bash
|
||||
# =============================================================================
|
||||
# Timmy node — graceful stop
|
||||
# =============================================================================
|
||||
set -euo pipefail
|
||||
|
||||
LAUNCHD_DIR="$HOME/Library/LaunchAgents"
|
||||
LNCLI="/opt/homebrew/bin/lncli"
|
||||
LND_DIR="$HOME/.lnd"
|
||||
|
||||
RED='\033[0;31m'; GREEN='\033[0;32m'; CYAN='\033[0;36m'; NC='\033[0m'
|
||||
info() { echo -e "${CYAN}[stop]${NC} $*"; }
|
||||
ok() { echo -e "${GREEN}[ok]${NC} $*"; }
|
||||
|
||||
# Stop bore tunnel first (fastest)
|
||||
if BORE_PID=$(pgrep -f "bore local.*5000" 2>/dev/null); then
|
||||
info "Stopping bore tunnel (PID $BORE_PID)…"
|
||||
kill "$BORE_PID" 2>/dev/null && ok "bore tunnel stopped." || true
|
||||
fi
|
||||
|
||||
# Stop LNbits
|
||||
info "Stopping LNbits…"
|
||||
launchctl unload "$LAUNCHD_DIR/com.timmy.lnbits.plist" 2>/dev/null && ok "LNbits stopped." || true
|
||||
pkill -f "lnbits" 2>/dev/null || true
|
||||
|
||||
# Stop LND gracefully via lncli
|
||||
info "Stopping LND (graceful)…"
|
||||
"$LNCLI" --lnddir="$LND_DIR" stop 2>/dev/null && ok "LND stop signal sent." || true
|
||||
sleep 3
|
||||
launchctl unload "$LAUNCHD_DIR/com.timmy.lnd.plist" 2>/dev/null || true
|
||||
|
||||
# Stop Bitcoin Core gracefully via RPC
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
SECRETS_FILE="$SCRIPT_DIR/.node-secrets"
|
||||
[[ -f "$SECRETS_FILE" ]] && source "$SECRETS_FILE"
|
||||
BTC_ARGS=()
|
||||
[[ -n "${RPC_USER:-}" ]] && BTC_ARGS+=(-rpcuser="$RPC_USER" -rpcpassword="$RPC_PASS")
|
||||
|
||||
info "Stopping Bitcoin Core (graceful — may take 30-60s)…"
|
||||
/opt/homebrew/bin/bitcoin-cli "${BTC_ARGS[@]}" stop 2>/dev/null && ok "Shutdown signal sent to bitcoind." || true
|
||||
launchctl unload "$LAUNCHD_DIR/com.timmy.bitcoind.plist" 2>/dev/null || true
|
||||
|
||||
echo ""
|
||||
ok "All services stopped. Data is safe."
|
||||
Reference in New Issue
Block a user