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:
@@ -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 * * *"
|
||||
|
||||
@@ -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}"
|
||||
|
||||
@@ -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" || \
|
||||
|
||||
Reference in New Issue
Block a user