From 12db06cc26745b72b0e5ffdef894aeb1c7ed36e1 Mon Sep 17 00:00:00 2001 From: alexpaynex <55271826-alexpaynex@users.noreply.replit.com> Date: Wed, 18 Mar 2026 18:30:28 +0000 Subject: [PATCH] Add auto-sweep hot wallet to cold storage (Task #4) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- infrastructure/lnd-init.sh | 38 ++++++++++++++++++ infrastructure/ops.sh | 33 ++++++++++++++++ infrastructure/setup.sh | 20 +++++++++- infrastructure/sweep.sh | 81 ++++++++++++++++++++++++++++++++++++++ 4 files changed, 170 insertions(+), 2 deletions(-) create mode 100755 infrastructure/sweep.sh diff --git a/infrastructure/lnd-init.sh b/infrastructure/lnd-init.sh index 82d7281..428a0e2 100755 --- a/infrastructure/lnd-init.sh +++ b/infrastructure/lnd-init.sh @@ -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" <> $INFRA_DIR/sweep.conf" +fi + # ── Final output ───────────────────────────────────────────── echo "" echo -e "${GREEN}════════════════════════════════════════════════${NC}" diff --git a/infrastructure/ops.sh b/infrastructure/ops.sh index ce70a54..0b0c528 100755 --- a/infrastructure/ops.sh +++ b/infrastructure/ops.sh @@ -109,6 +109,37 @@ case "${1:-help}" in echo " scp root@:$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 "" ;; diff --git a/infrastructure/setup.sh b/infrastructure/setup.sh index c3d6036..7b8a9c7 100755 --- a/infrastructure/setup.sh +++ b/infrastructure/setup.sh @@ -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" </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