Add ability to sweep funds using xpub or a list of addresses

Implement multiple sweep destination modes (static, address list, xpub) with state management and update configuration scripts.

Replit-Commit-Author: Agent
Replit-Commit-Session-Id: 418bf6f8-212b-4bb0-a7a5-8231a061da4e
Replit-Commit-Checkpoint-Type: full_checkpoint
Replit-Commit-Event-Id: 8df121fd-c189-4c73-a76b-d9a3e07de783
Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/9f85e954-647c-46a5-90a7-396e495a805a/418bf6f8-212b-4bb0-a7a5-8231a061da4e/sPDHkg8
Replit-Helium-Checkpoint-Created: true
This commit is contained in:
alexpaynex
2026-03-18 18:38:16 +00:00
parent e5f78e1eb9
commit 5dd80ee81a
3 changed files with 215 additions and 31 deletions

View File

@@ -157,7 +157,9 @@ if [[ -n "$COLD_ADDRESS" ]]; then
# Timmy Node — Auto-sweep configuration
# Change any value then run: bash ops.sh configure-sweep
COLD_ADDRESS=$COLD_ADDRESS
SWEEP_MODE="static"
COLD_ADDRESS="$COLD_ADDRESS"
XPUB=""
KEEP_SATS=300000
MIN_SWEEP=50000
SWEEP_CRON="0 3 * * *"

View File

@@ -113,12 +113,34 @@ case "${1:-help}" in
SWEEP_CONF="$INFRA_DIR/sweep.conf"
SWEEP_LOG="/var/log/timmy-sweep.log"
STATE_FILE="$INFRA_DIR/sweep-state"
ADDR_LIST_FILE="$INFRA_DIR/sweep-addresses.txt"
echo -e "\n${CYAN}── Sweep config ──────────────────────────────${NC}"
if [[ -f "$SWEEP_CONF" ]]; then
COLD_ADDRESS=""; KEEP_SATS=300000; MIN_SWEEP=50000
SWEEP_MODE="static"; COLD_ADDRESS=""; XPUB=""
KEEP_SATS=300000; MIN_SWEEP=50000
SWEEP_FREQ_LABEL="daily at 3am UTC"; SWEEP_CRON="0 3 * * *"
source "$SWEEP_CONF"
echo " Cold address : ${COLD_ADDRESS:-(not set — sweep disabled)}"
echo " Mode : ${SWEEP_MODE}"
case "$SWEEP_MODE" in
static)
echo " Destination : ${COLD_ADDRESS:-(not set — sweep disabled)}"
;;
list)
COUNT=$(grep -c '[^[:space:]]' "$ADDR_LIST_FILE" 2>/dev/null || echo 0)
NEXT_INDEX=0; [[ -f "$STATE_FILE" ]] && source "$STATE_FILE"
REMAINING=$(( COUNT - NEXT_INDEX ))
echo " Addresses : ${COUNT} total, ${REMAINING} remaining (next: index ${NEXT_INDEX})"
NEXT_ADDR=$(sed -n "$((NEXT_INDEX + 1))p" "$ADDR_LIST_FILE" 2>/dev/null | tr -d '[:space:]')
[[ -n "$NEXT_ADDR" ]] && echo " Next address : ${NEXT_ADDR}"
;;
xpub)
NEXT_INDEX=0; [[ -f "$STATE_FILE" ]] && source "$STATE_FILE"
echo " xpub : ${XPUB:0:30}..."
echo " Next index : ${NEXT_INDEX}"
;;
esac
echo " Keep on-chain: ${KEEP_SATS} sats"
echo " Min to sweep : ${MIN_SWEEP} sats"
echo " Frequency : ${SWEEP_FREQ_LABEL}"
@@ -143,9 +165,12 @@ case "${1:-help}" in
configure-sweep)
SWEEP_CONF="$INFRA_DIR/sweep.conf"
STATE_FILE="$INFRA_DIR/sweep-state"
ADDR_LIST_FILE="$INFRA_DIR/sweep-addresses.txt"
# Load current values as defaults
COLD_ADDRESS=""; KEEP_SATS=300000; MIN_SWEEP=50000
SWEEP_MODE="static"; COLD_ADDRESS=""; XPUB=""
KEEP_SATS=300000; MIN_SWEEP=50000
SWEEP_CRON="0 3 * * *"; SWEEP_FREQ_LABEL="daily at 3am UTC"
[[ -f "$SWEEP_CONF" ]] && source "$SWEEP_CONF"
@@ -153,22 +178,84 @@ case "${1:-help}" in
echo -e "${CYAN} Configure Auto-Sweep (Enter = keep current)${NC}"
echo -e "${CYAN}══════════════════════════════════════════${NC}\n"
# Cold address
echo -e " Current cold address: ${YELLOW}${COLD_ADDRESS:-(none)}${NC}"
read -rp " New cold address: " INPUT
[[ -n "$INPUT" ]] && COLD_ADDRESS="$INPUT"
# ── Destination mode ──────────────────────────────────────
echo -e " Current destination mode: ${YELLOW}${SWEEP_MODE}${NC}"
echo " Choose a destination mode:"
echo " 1) Single address — same address every sweep"
echo " 2) Address list — rotate through a list of addresses (no reuse)"
echo " 3) xpub — derive a fresh address each sweep (no reuse)"
read -rp " Choice [1-3, Enter to keep]: " MODE_CHOICE
case "$MODE_CHOICE" in
1) SWEEP_MODE="static" ;;
2) SWEEP_MODE="list" ;;
3) SWEEP_MODE="xpub" ;;
esac
# Keep threshold
# ── Mode-specific inputs ──────────────────────────────────
case "$SWEEP_MODE" in
static)
echo -e "\n Current cold address: ${YELLOW}${COLD_ADDRESS:-(none)}${NC}"
read -rp " New cold address (bc1q... / 1... / 3...): " INPUT
[[ -n "$INPUT" ]] && COLD_ADDRESS="$INPUT"
XPUB=""
;;
list)
if [[ -f "$ADDR_LIST_FILE" ]]; then
COUNT=$(grep -c '[^[:space:]]' "$ADDR_LIST_FILE" || true)
echo -e "\n Current address list: ${YELLOW}${COUNT} address(es) in $ADDR_LIST_FILE${NC}"
else
echo -e "\n No address list found yet."
fi
NEXT_INDEX=0
[[ -f "$STATE_FILE" ]] && source "$STATE_FILE"
echo " Current position: index ${NEXT_INDEX}"
echo ""
echo " Paste cold addresses now (one per line, blank line when done)."
echo " These REPLACE the existing list. Leave blank to keep current list."
echo ""
NEW_ADDRS=()
while IFS= read -rp " > " ADDR_INPUT && [[ -n "$ADDR_INPUT" ]]; do
ADDR_INPUT=$(echo "$ADDR_INPUT" | tr -d '[:space:]')
[[ -n "$ADDR_INPUT" ]] && NEW_ADDRS+=("$ADDR_INPUT")
done
if (( ${#NEW_ADDRS[@]} > 0 )); then
printf '%s\n' "${NEW_ADDRS[@]}" > "$ADDR_LIST_FILE"
chmod 600 "$ADDR_LIST_FILE"
echo "NEXT_INDEX=0" > "$STATE_FILE"
chmod 600 "$STATE_FILE"
echo -e " ${GREEN}Saved ${#NEW_ADDRS[@]} addresses. Index reset to 0.${NC}"
else
echo " Kept existing address list."
fi
COLD_ADDRESS=""; XPUB=""
;;
xpub)
echo -e "\n Current xpub: ${YELLOW}${XPUB:-(none)}${NC}"
echo " Paste your account xpub (from Sparrow, Coldcard, etc. — account-level, not master)."
read -rp " xpub: " INPUT
if [[ -n "$INPUT" ]]; then
XPUB="$INPUT"
echo "NEXT_INDEX=0" > "$STATE_FILE"
chmod 600 "$STATE_FILE"
echo -e " ${GREEN}xpub saved. Derivation index reset to 0.${NC}"
fi
COLD_ADDRESS=""
;;
esac
# ── Keep threshold ────────────────────────────────────────
echo -e "\n Current keep-on-chain threshold: ${YELLOW}${KEEP_SATS} sats${NC}"
read -rp " New keep threshold (sats): " INPUT
[[ "$INPUT" =~ ^[0-9]+$ ]] && KEEP_SATS="$INPUT"
# Min sweep floor
# ── Min sweep floor ───────────────────────────────────────
echo -e "\n Current minimum sweep amount: ${YELLOW}${MIN_SWEEP} sats${NC}"
read -rp " New minimum sweep (sats): " INPUT
[[ "$INPUT" =~ ^[0-9]+$ ]] && MIN_SWEEP="$INPUT"
# Frequency
# ── Frequency ─────────────────────────────────────────────
echo -e "\n Current frequency: ${YELLOW}${SWEEP_FREQ_LABEL}${NC}"
echo " Choose a new frequency (Enter to keep current):"
echo " 1) Hourly"
@@ -183,12 +270,14 @@ case "${1:-help}" in
4) SWEEP_CRON="0 3 * * 0"; SWEEP_FREQ_LABEL="weekly (Sunday 3am UTC)" ;;
esac
# Write sweep.conf — quote values that may contain spaces
# ── Write sweep.conf ──────────────────────────────────────
cat > "$SWEEP_CONF" <<CONF
# Timmy Node — Auto-sweep configuration
# Edit manually or run: bash ops.sh configure-sweep
SWEEP_MODE="$SWEEP_MODE"
COLD_ADDRESS="$COLD_ADDRESS"
XPUB="$XPUB"
KEEP_SATS=$KEEP_SATS
MIN_SWEEP=$MIN_SWEEP
SWEEP_CRON="$SWEEP_CRON"
@@ -196,13 +285,26 @@ SWEEP_FREQ_LABEL="$SWEEP_FREQ_LABEL"
CONF
chmod 600 "$SWEEP_CONF"
# Reinstall sweep cron with updated schedule
# ── Reinstall cron ────────────────────────────────────────
crontab -l 2>/dev/null | grep -v "timmy-node.*sweep" | crontab - || true
(crontab -l 2>/dev/null; echo "# Timmy Node — auto-sweep ($SWEEP_FREQ_LABEL)") | crontab -
(crontab -l 2>/dev/null; echo "$SWEEP_CRON bash $INFRA_DIR/sweep.sh > /dev/null 2>&1") | crontab -
echo -e "\n${GREEN}Sweep configured:${NC}"
echo " Cold address : ${COLD_ADDRESS:-(not set — sweep disabled)}"
echo " Mode : ${SWEEP_MODE}"
case "$SWEEP_MODE" in
static) echo " Destination : ${COLD_ADDRESS:-(not set — sweep disabled)}" ;;
list)
COUNT=$(grep -c '[^[:space:]]' "$ADDR_LIST_FILE" 2>/dev/null || echo 0)
NEXT_INDEX=0; [[ -f "$STATE_FILE" ]] && source "$STATE_FILE"
echo " Addresses : ${COUNT} total, starting at index ${NEXT_INDEX}"
;;
xpub)
NEXT_INDEX=0; [[ -f "$STATE_FILE" ]] && source "$STATE_FILE"
echo " xpub : ${XPUB:0:20}..."
echo " Next index : ${NEXT_INDEX}"
;;
esac
echo " Keep on-chain: ${KEEP_SATS} sats"
echo " Min to sweep : ${MIN_SWEEP} sats"
echo " Frequency : ${SWEEP_FREQ_LABEL}"

View File

@@ -3,17 +3,18 @@
# 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
# Run by cron: configured by ops.sh configure-sweep
#
# 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)
# Config: /opt/timmy-node/sweep.conf
# State: /opt/timmy-node/sweep-state (address index, auto-managed)
# Addresses: /opt/timmy-node/sweep-addresses.txt (for list mode)
# ============================================================
set -euo pipefail
INFRA_DIR="/opt/timmy-node"
CONF_FILE="$INFRA_DIR/sweep.conf"
STATE_FILE="$INFRA_DIR/sweep-state"
ADDR_LIST_FILE="$INFRA_DIR/sweep-addresses.txt"
LOG_FILE="/var/log/timmy-sweep.log"
TIMESTAMP=$(date -u '+%Y-%m-%d %H:%M:%S UTC')
@@ -21,20 +22,89 @@ 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)"
log "SKIP — no sweep.conf found at $CONF_FILE (run: bash ops.sh configure-sweep)"
exit 0
fi
source "$CONF_FILE"
SWEEP_MODE="${SWEEP_MODE:-static}"
COLD_ADDRESS="${COLD_ADDRESS:-}"
XPUB="${XPUB:-}"
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
# ── Resolve destination address ──────────────────────────────
# Sets SWEEP_TO or exits. Logs the resolved address and remaining capacity.
resolve_address() {
case "$SWEEP_MODE" in
xpub)
if [[ -z "$XPUB" ]]; then
log "ERROR — SWEEP_MODE=xpub but XPUB not set in $CONF_FILE"
exit 1
fi
NEXT_INDEX=0
[[ -f "$STATE_FILE" ]] && source "$STATE_FILE"
DESC="wpkh(${XPUB}/0/${NEXT_INDEX})"
CHECKSUM_DESC=$(docker exec bitcoin bitcoin-cli \
-datadir=/home/bitcoin/.bitcoin \
getdescriptorinfo "$DESC" 2>&1 | jq -r '.descriptor // empty')
if [[ -z "$CHECKSUM_DESC" ]]; then
log "ERROR — could not get descriptor info for index $NEXT_INDEX (is Bitcoin Core synced?)"
exit 1
fi
DERIVED=$(docker exec bitcoin bitcoin-cli \
-datadir=/home/bitcoin/.bitcoin \
deriveaddresses "$CHECKSUM_DESC" 2>&1 | jq -r '.[0] // empty')
if [[ -z "$DERIVED" || "$DERIVED" == "null" ]]; then
log "ERROR — could not derive address at xpub index $NEXT_INDEX"
exit 1
fi
SWEEP_TO="$DERIVED"
log "ADDRESS — xpub index ${NEXT_INDEX}${SWEEP_TO}"
;;
list)
if [[ ! -f "$ADDR_LIST_FILE" ]]; then
log "ERROR — SWEEP_MODE=list but $ADDR_LIST_FILE not found (run: bash ops.sh configure-sweep)"
exit 1
fi
NEXT_INDEX=0
[[ -f "$STATE_FILE" ]] && source "$STATE_FILE"
ADDR=$(sed -n "$((NEXT_INDEX + 1))p" "$ADDR_LIST_FILE" | tr -d '[:space:]')
if [[ -z "$ADDR" ]]; then
log "ERROR — address list exhausted at index ${NEXT_INDEX} — add more addresses via ops.sh configure-sweep"
exit 1
fi
TOTAL=$(grep -c '[^[:space:]]' "$ADDR_LIST_FILE" || true)
REMAINING=$(( TOTAL - NEXT_INDEX ))
if (( REMAINING <= 5 )); then
log "WARNING — only ${REMAINING} address(es) remaining in list, add more soon"
fi
SWEEP_TO="$ADDR"
log "ADDRESS — list index ${NEXT_INDEX} (${REMAINING} remaining) → ${SWEEP_TO}"
;;
static|*)
if [[ -z "$COLD_ADDRESS" ]]; then
log "SKIP — COLD_ADDRESS not set in $CONF_FILE (run: bash ops.sh configure-sweep)"
exit 0
fi
SWEEP_TO="$COLD_ADDRESS"
;;
esac
}
resolve_address
# ── Get confirmed on-chain balance ───────────────────────────
BALANCE=$(docker exec lnd lncli --network=mainnet walletbalance 2>/dev/null \
@@ -45,9 +115,9 @@ if [[ -z "$BALANCE" || "$BALANCE" == "null" ]]; then
exit 1
fi
log "On-chain balance: ${BALANCE} sats | keep: ${KEEP_SATS} | cold: ${COLD_ADDRESS}"
log "Balance: ${BALANCE} sats | keep: ${KEEP_SATS} | destination: ${SWEEP_TO}"
# ── Calculate sweep amount ───────────────────────────────────
# ── Calculate sweep amount ───────────────────────────────────
SWEEP_AMT=$(( BALANCE - KEEP_SATS ))
if (( SWEEP_AMT < MIN_SWEEP )); then
@@ -55,11 +125,11 @@ if (( SWEEP_AMT < MIN_SWEEP )); then
exit 0
fi
# ── Send to cold address ─────────────────────────────────────
log "SWEEP — sending ${SWEEP_AMT} sats to ${COLD_ADDRESS}..."
# ── Send to cold address ─────────────────────────────────────
log "SWEEP — sending ${SWEEP_AMT} sats to ${SWEEP_TO}..."
if ! SEND_RESULT=$(docker exec lnd lncli --network=mainnet sendcoins \
--addr "$COLD_ADDRESS" \
--addr "$SWEEP_TO" \
--amt "$SWEEP_AMT" \
2>&1)
then
@@ -74,9 +144,19 @@ if [[ -z "$TXID" ]]; then
exit 1
fi
log "SUCCESS — txid=${TXID} amount=${SWEEP_AMT} sats → ${COLD_ADDRESS}"
log "SUCCESS — txid=${TXID} amount=${SWEEP_AMT} sats → ${SWEEP_TO}"
# ── Trigger channel state backup ─────────────────────────────
# ── Advance address index (xpub / list modes) ─────────────────
if [[ "$SWEEP_MODE" == "xpub" || "$SWEEP_MODE" == "list" ]]; then
NEXT_INDEX=0
[[ -f "$STATE_FILE" ]] && source "$STATE_FILE"
NEW_INDEX=$(( NEXT_INDEX + 1 ))
echo "NEXT_INDEX=$NEW_INDEX" > "$STATE_FILE"
chmod 600 "$STATE_FILE"
log "INDEX — advanced to ${NEW_INDEX} for next sweep"
fi
# ── 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" || \