Add auto-sweep hot wallet to cold storage (Task #4)
New file: infrastructure/sweep.sh - Reads /opt/timmy-node/sweep.conf (COLD_ADDRESS, KEEP_SATS=300000, MIN_SWEEP=50000) - Queries LND on-chain confirmed balance via lncli walletbalance - Calculates sweep_amt = balance - KEEP_SATS; skips if < MIN_SWEEP - Sends via lncli sendcoins --addr $COLD_ADDRESS --amt $sweep_amt - Logs timestamp, balance, sweep amount, txid to /var/log/timmy-sweep.log - Triggers ops.sh backup after every successful sweep - Exits cleanly (no crash) if conf missing, address unset, or balance too low Updated: infrastructure/setup.sh - Copies sweep.sh and ops.sh to /opt/timmy-node/ during bootstrap - Installs two cron jobs: sweep at 3am UTC, backup at 4am UTC - Creates /var/log/timmy-sweep.log and /var/log/timmy-backup.log - Idempotent: removes existing timmy-node cron entries before re-adding Updated: infrastructure/lnd-init.sh - New "Cold Storage Auto-Sweep Setup" section after LNbits wallet creation - Prompts for cold Bitcoin address (optional — skip to configure later) - Writes /opt/timmy-node/sweep.conf with address + documented defaults - sweep.conf chmod 600 (sensitive — contains sweep destination) - Graceful skip path with instructions for later configuration Updated: infrastructure/ops.sh - New `sweep` command: shows sweep.conf config, current on-chain balance, last 5 lines of /var/log/timmy-sweep.log - New `run-sweep` command: triggers sweep.sh immediately outside cron - Help text updated with both new commands
This commit is contained in:
@@ -136,6 +136,44 @@ else
|
||||
fi
|
||||
fi
|
||||
|
||||
# ── Configure cold storage sweep ────────────────────────────
|
||||
echo ""
|
||||
echo -e "${CYAN}══════════════════════════════════════════════${NC}"
|
||||
echo -e "${CYAN} Cold Storage Auto-Sweep Setup${NC}"
|
||||
echo -e "${CYAN}══════════════════════════════════════════════${NC}"
|
||||
echo ""
|
||||
echo -e " Excess on-chain sats above your threshold are automatically"
|
||||
echo -e " sent to a cold address daily. You never touch the hot wallet."
|
||||
echo ""
|
||||
echo -e " Enter your cold Bitcoin address (hardware wallet, Sparrow, etc.)"
|
||||
echo -e " Press Enter to skip — you can configure this later by editing"
|
||||
echo -e " $INFRA_DIR/sweep.conf"
|
||||
echo ""
|
||||
read -rp " Cold address (bc1q... / 1... / 3...): " COLD_ADDRESS
|
||||
|
||||
SWEEP_CONF="$INFRA_DIR/sweep.conf"
|
||||
if [[ -n "$COLD_ADDRESS" ]]; then
|
||||
cat > "$SWEEP_CONF" <<CONF
|
||||
# Timmy Node — Auto-sweep configuration
|
||||
# Edit these values then run: bash /opt/timmy-node/sweep.sh
|
||||
|
||||
# Cold Bitcoin address — sweep destination (required)
|
||||
COLD_ADDRESS=$COLD_ADDRESS
|
||||
|
||||
# Keep this many sats on-chain for channel ops (default: 300,000)
|
||||
KEEP_SATS=300000
|
||||
|
||||
# Minimum amount to sweep — ignore smaller surpluses (default: 50,000)
|
||||
MIN_SWEEP=50000
|
||||
CONF
|
||||
chmod 600 "$SWEEP_CONF"
|
||||
ok "Cold address saved to $SWEEP_CONF"
|
||||
info "Test your sweep config anytime: bash $INFRA_DIR/sweep.sh"
|
||||
else
|
||||
warn "No cold address provided — sweep is disabled."
|
||||
warn "Configure later: echo 'COLD_ADDRESS=bc1q...' >> $INFRA_DIR/sweep.conf"
|
||||
fi
|
||||
|
||||
# ── Final output ─────────────────────────────────────────────
|
||||
echo ""
|
||||
echo -e "${GREEN}════════════════════════════════════════════════${NC}"
|
||||
|
||||
@@ -109,6 +109,37 @@ case "${1:-help}" in
|
||||
echo " scp root@<node>:$BACKUP_FILE ./lnd-backup.tar.gz"
|
||||
;;
|
||||
|
||||
sweep)
|
||||
SWEEP_CONF="$INFRA_DIR/sweep.conf"
|
||||
SWEEP_LOG="/var/log/timmy-sweep.log"
|
||||
|
||||
echo -e "\n${CYAN}── Sweep config ──────────────────────────────${NC}"
|
||||
if [[ -f "$SWEEP_CONF" ]]; then
|
||||
grep -v '^#' "$SWEEP_CONF" | grep -v '^$' || echo "(empty)"
|
||||
else
|
||||
echo -e "${YELLOW}No sweep.conf found — sweep is disabled${NC}"
|
||||
echo " To enable: run lnd-init.sh, or create $SWEEP_CONF manually"
|
||||
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 ""
|
||||
;;
|
||||
|
||||
run-sweep)
|
||||
echo -e "${CYAN}Running sweep now...${NC}"
|
||||
bash "$INFRA_DIR/sweep.sh"
|
||||
;;
|
||||
|
||||
help|*)
|
||||
echo -e "\n${CYAN}Timmy Node operations:${NC}"
|
||||
echo ""
|
||||
@@ -122,6 +153,8 @@ case "${1:-help}" in
|
||||
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 run-sweep — run sweep immediately (outside of cron schedule)"
|
||||
echo ""
|
||||
;;
|
||||
|
||||
|
||||
@@ -123,13 +123,29 @@ services:
|
||||
- $INFRA_DIR/configs/lnd.conf:/root/.lnd/lnd.conf:ro
|
||||
OVERRIDE
|
||||
|
||||
# Copy main docker-compose
|
||||
# Copy main docker-compose and scripts
|
||||
cp "$SCRIPT_DIR/docker-compose.yml" "$INFRA_DIR/docker-compose.yml"
|
||||
cp "$SCRIPT_DIR/lnd-init.sh" "$INFRA_DIR/lnd-init.sh"
|
||||
chmod +x "$INFRA_DIR/lnd-init.sh"
|
||||
cp "$SCRIPT_DIR/sweep.sh" "$INFRA_DIR/sweep.sh"
|
||||
cp "$SCRIPT_DIR/ops.sh" "$INFRA_DIR/ops.sh"
|
||||
chmod +x "$INFRA_DIR/lnd-init.sh" "$INFRA_DIR/sweep.sh" "$INFRA_DIR/ops.sh"
|
||||
|
||||
ok "Configs installed"
|
||||
|
||||
# ── 7b. Install cron jobs ─────────────────────────────────────
|
||||
info "Installing cron jobs (daily sweep + daily backup)..."
|
||||
# Remove any existing Timmy cron entries before re-adding
|
||||
crontab -l 2>/dev/null | grep -v "timmy-node" | crontab - || true
|
||||
(crontab -l 2>/dev/null; cat <<'CRON'
|
||||
# Timmy Node — auto-sweep hot wallet to cold storage (3am UTC daily)
|
||||
0 3 * * * bash /opt/timmy-node/sweep.sh >> /var/log/timmy-sweep.log 2>&1
|
||||
# Timmy Node — LND channel state backup (4am UTC daily)
|
||||
0 4 * * * bash /opt/timmy-node/ops.sh backup >> /var/log/timmy-backup.log 2>&1
|
||||
CRON
|
||||
) | crontab -
|
||||
touch /var/log/timmy-sweep.log /var/log/timmy-backup.log
|
||||
ok "Cron jobs installed (sweep 3am, backup 4am UTC)"
|
||||
|
||||
# ── 8. Save credentials ──────────────────────────────────────
|
||||
CREDS_FILE="/root/node-credentials.txt"
|
||||
cat > "$CREDS_FILE" <<CREDS
|
||||
|
||||
81
infrastructure/sweep.sh
Executable file
81
infrastructure/sweep.sh
Executable file
@@ -0,0 +1,81 @@
|
||||
#!/usr/bin/env bash
|
||||
# ============================================================
|
||||
# Timmy Node — Auto-sweep hot wallet to cold storage
|
||||
#
|
||||
# Run manually: bash /opt/timmy-node/sweep.sh
|
||||
# Run by cron: 0 3 * * * bash /opt/timmy-node/sweep.sh
|
||||
#
|
||||
# Config file: /opt/timmy-node/sweep.conf
|
||||
# COLD_ADDRESS=bc1q... (required — your cold wallet address)
|
||||
# KEEP_SATS=300000 (keep this much on-chain for channel ops)
|
||||
# MIN_SWEEP=50000 (don't sweep if amount is below this)
|
||||
# ============================================================
|
||||
set -euo pipefail
|
||||
|
||||
INFRA_DIR="/opt/timmy-node"
|
||||
CONF_FILE="$INFRA_DIR/sweep.conf"
|
||||
LOG_FILE="/var/log/timmy-sweep.log"
|
||||
TIMESTAMP=$(date -u '+%Y-%m-%d %H:%M:%S UTC')
|
||||
|
||||
log() { echo "[$TIMESTAMP] $*" | tee -a "$LOG_FILE"; }
|
||||
|
||||
# ── Load config ──────────────────────────────────────────────
|
||||
if [[ ! -f "$CONF_FILE" ]]; then
|
||||
log "SKIP — no sweep.conf found at $CONF_FILE (run lnd-init.sh to configure)"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
source "$CONF_FILE"
|
||||
|
||||
COLD_ADDRESS="${COLD_ADDRESS:-}"
|
||||
KEEP_SATS="${KEEP_SATS:-300000}"
|
||||
MIN_SWEEP="${MIN_SWEEP:-50000}"
|
||||
|
||||
if [[ -z "$COLD_ADDRESS" ]]; then
|
||||
log "SKIP — COLD_ADDRESS not set in $CONF_FILE"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# ── Get confirmed on-chain balance ───────────────────────────
|
||||
BALANCE=$(docker exec lnd lncli --network=mainnet walletbalance 2>/dev/null \
|
||||
| jq -r '.confirmed_balance // "0"')
|
||||
|
||||
if [[ -z "$BALANCE" || "$BALANCE" == "null" ]]; then
|
||||
log "ERROR — could not read LND wallet balance (is LND running?)"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
log "On-chain balance: ${BALANCE} sats | keep: ${KEEP_SATS} | cold: ${COLD_ADDRESS}"
|
||||
|
||||
# ── Calculate sweep amount ───────────────────────────────────
|
||||
SWEEP_AMT=$(( BALANCE - KEEP_SATS ))
|
||||
|
||||
if (( SWEEP_AMT < MIN_SWEEP )); then
|
||||
log "SKIP — sweep amount ${SWEEP_AMT} sats is below MIN_SWEEP ${MIN_SWEEP} sats (nothing sent)"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# ── Send to cold address ─────────────────────────────────────
|
||||
log "SWEEP — sending ${SWEEP_AMT} sats to ${COLD_ADDRESS}..."
|
||||
|
||||
SEND_RESULT=$(docker exec lnd lncli --network=mainnet sendcoins \
|
||||
--addr "$COLD_ADDRESS" \
|
||||
--amt "$SWEEP_AMT" \
|
||||
2>&1)
|
||||
|
||||
TXID=$(echo "$SEND_RESULT" | jq -r '.txid // empty' 2>/dev/null || echo "")
|
||||
|
||||
if [[ -z "$TXID" ]]; then
|
||||
log "ERROR — sendcoins failed: $SEND_RESULT"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
log "SUCCESS — txid=${TXID} amount=${SWEEP_AMT} sats → ${COLD_ADDRESS}"
|
||||
|
||||
# ── Trigger channel state backup ─────────────────────────────
|
||||
log "BACKUP — triggering channel state backup after sweep..."
|
||||
bash "$INFRA_DIR/ops.sh" backup >> "$LOG_FILE" 2>&1 && \
|
||||
log "BACKUP — complete" || \
|
||||
log "BACKUP — WARNING: backup failed, check ops.sh backup manually"
|
||||
|
||||
exit 0
|
||||
Reference in New Issue
Block a user