Scripts

From EAWF Document Repository
Jump to navigation Jump to search
#!/usr/bin/env bash
#
# btc — Bitcoin Core + ckpool control utility
# DESC: Bitcoin Core, Mining, and Wallet Manager
# Description:
#   Unified command-line control tool for operating a local Bitcoin Core node
#   and ckpool solo mining setup. Provides commands for:
#       • Core start / stop / restart
#       • Node and mining status dashboard
#       • ckpool control and payout address rotation
#       • Wallet management and archival
#
# Usage:
#   btc <command>
#
# Configuration:
#   Reads configuration from the first readable file:
#       /media/bob/BTC/.btc.conf
#       /etc/bitcoin/.btc.conf
#       ~/.config/bitcoin/.btc.conf
#
# Requirements:
#   • Bitcoin Core (bitcoind + bitcoin-cli)
#   • ckpool (optional but expected for mining functions)
#   • Bash shell
#
# Authors:
#   Bob Holden (operator / design)
#   ChatGPT (OpenAI) — development assistance
#
# License:
#   Intended for personal and educational use. May be freely modified
#   and redistributed.
#
VERSION="2.4.0"
PROG="$(basename -- "$0")"
SCRIPT_PATH="$(readlink -f "$0")"
BTCPATH="/home/bob/bitcoin"
BTCBIN="${BTCPATH}/bin"
FDATE="$(stat -c %Y "$SCRIPT_PATH" | xargs -I{} date -d @{} '+%Y%m%d:%H%M')"
BITAXE_URL="http://192.168.123.68"

# ---- Built-in ANSI colors ----
if [ -t 1 ]; then
  RCol='\033[0m'
  BRed='\033[1;31m'
  BIGre='\033[1;32m'
  BIYel='\033[1;33m'
  BIBlu='\033[1;34m'
  BWhi='\033[1;37m'
else
  RCol=""; BRed=""; BIGre=""; BIYel=""; BIBlu=""; BWhi=""
fi

die(){ echo -e "${BRed}ERROR: $*${RCol}" >&2; exit 1; }

spinner() {
  local pid="$1"
  local msg="$2"
  local spin='|/-\'
  local i=0

  while kill -0 "$pid" 2>/dev/null; do
    printf "%s %c\r" "$msg" "${spin:i++%4:1}"
    sleep 0.1
  done

  printf '%-100s\r' ""
}

wait_core() {
  local msg="${1:-Waiting for Core RPC}"
  local spin='|/-\'
  local i=0

  while ! cli getblockchaininfo >/dev/null 2>&1; do
    printf "%s %c\r" "$msg" "${spin:i++%4:1}"
    sleep 0.2
  done

  printf '%-100s\r' ""
}

wait_ibd() {
  local spin='|/-\'
  local i=0

  while cli getblockchaininfo 2>/dev/null | grep -q '"initialblockdownload"[[:space:]]*:[[:space:]]*true'; do
    printf "Core: syncing blocks %c\r" "${spin:i++%4:1}"
    sleep 1
  done

  printf '%-100s\r' ""
}

wait_txindex() {
  local spin='|/-\'
  local i=0
  local idx synced

  while :; do
    idx="$(cli getindexinfo 2>/dev/null)"
    [ -z "$idx" ] && break
    ! echo "$idx" | grep -q '"txindex"' && break

    synced="$(echo "$idx" | grep -A3 '"txindex"' | grep -oE '"synced"[[:space:]]*:[[:space:]]*(true|false)' | head -1 | grep -oE '(true|false)')"
    [ "$synced" = "true" ] && break

    printf "Core: indexing txindex %c\r" "${spin:i++%4:1}"
    sleep 1
  done

  printf '%-100s\r' ""
}

wait_ckpool() {
  local msg="${1:-Waiting for ckpool}"
  local spin='|/-\'
  local i=0

  while ! ck_running; do
    printf "%s %c\r" "$msg" "${spin:i++%4:1}"
    sleep 0.2
  done

  printf '%-100s\r' ""
}

# ---- Load config (first readable wins) ----
load_cfg() {
  local cfg
  for cfg in \
    "/media/bob/BTC/.btc.conf" \
    "/etc/bitcoin/.btc.conf" \
    "$HOME/.config/bitcoin/.btc.conf"
  do
    if [ -r "$cfg" ]; then
      # shellcheck disable=SC1090
      source "$cfg"
      CFG_LOADED_FROM="$cfg"
      return 0
    fi
  done
  die "No config found."
}

# ---- Helpers ----
cli() {
  "$BTCBIN/bitcoin-cli" -datadir="$DATADIR" "$@"
}

wcli() {
  local w="$1"
  shift
  "$BTCBIN/bitcoin-cli" -datadir="$DATADIR" -rpcwallet="$w" "$@"
}

usage() {
  clear
  cat <<TXT
Usage: btc <command>

Core                  Bitaxe                       Wallet
----------------      -------------------------    ------------------------
start                 ckstart                      wlist
stop                  ckstop                       wdir
restart               cklog                        wcreate   <wallet>
status                ckaddr                       wload     <wallet>
log                                                wunload   <wallet>
paths                 bitaxe                       winfo     <wallet>
rate(USD/BTC)                                      wbal      <wallet>
version                                            wnew      <wallet> [label]
upgrade ##.#[.#.#]                                 warchive  <wallet>
                                                   warchived
                                                   wwatch    <wallet>
TXT
}

paths() {
  echo "cfg:        $CFG_LOADED_FROM"
  echo "btcbin:     $BTCBIN"
  echo "datadir:    $DATADIR"
  echo "wallets:    $DATADIR/<walletname>"
  echo "core conf:  $DATADIR/bitcoin.conf"
  echo "ck conf:    $CKPOOL_CONF"
}

# ---- Core ----
start() {
  "$BTCBIN/bitcoind" -datadir="$DATADIR" -daemon || die "Failed to start bitcoind"

  wait_core "Waiting for Core RPC"
  wait_ibd
  wait_txindex

  echo "Bitcoin Core running"
  status
}

stop() {
  if cli stop >/dev/null 2>&1; then
    :
  else
    if ! pgrep -x bitcoind >/dev/null 2>&1; then
      echo "Core already stopped"
      status
      return 0
    fi
  fi

  echo "Waiting for bitcoind to stop..."
  while pgrep -x bitcoind >/dev/null 2>&1; do
    sleep 1
  done
  echo "Core stopped"
  status
}

restart() {
  local ck_was_running=0

  if ck_running; then
    ck_was_running=1
    echo "Stopping ckpool..."
    ckstop
  fi

  echo "Restarting Core..."
  stop
  sleep 2
  start

  if [ "$ck_was_running" -eq 1 ]; then
    echo "Restarting ckpool..."
    ckstart
  fi
}

log() {
  tail -f "$DATADIR/debug.log"
}

bitaxe() {
# echo off
  curl -s "$BITAXE_URL/api/system/info" | jq .
}

fmt_hms() {
  local s="${1:-0}"
  printf '%02d:%02d:%02d' $((s/3600)) $(((s%3600)/60)) $((s%60))
}

fmt_commas() {
  printf "%s" "$1" | sed ':a;s/\B[0-9]\{3\}\>/,&/;ta'
}

status() {
  clear

  local now j blocks peers mempool txindex addr pool
  local ax_result ax_state ax_uptime ax_hash ax_acc ax_rej ax_stale ax_temp ax_vrtemp ax_fan ax_wifi ax_bestdiff
  local uptime_hms stale_pct acc_fmt stale_fmt
  local spin='|/-\'
  local i=0

  now="$(date '+%Y-%m-%d %H:%M')"

  # ---- Bitaxe Parsing Block
   exec 3< <(
    ax="$(curl -fsS --connect-timeout 1 --max-time 1 "${BITAXE_URL}/api/system/info" 2>/dev/null)"
    uptime="$(echo "$ax"   | grep -oE '"uptimeSeconds"[[:space:]]*:[[:space:]]*[0-9]+'    | head -1 | grep -oE '[0-9]+')"
    hash1m="$(echo "$ax" | grep -oE '"hashRate_1m"[[:space:]]*:[[:space:]]*[0-9.]+' | head -1 | sed -E 's/.*:[[:space:]]*//')"
    accepted="$(echo "$ax" | grep -oE '"sharesAccepted"[[:space:]]*:[[:space:]]*[0-9]+'    | head -1 | grep -oE '[0-9]+')"
    rejected="$(echo "$ax" | grep -oE '"sharesRejected"[[:space:]]*:[[:space:]]*[0-9]+'    | head -1 | grep -oE '[0-9]+')"
    temp="$(echo "$ax"     | grep -oE '"temp"[[:space:]]*:[[:space:]]*-?[0-9]+'            | head -1 | grep -oE -- '-?[0-9]+')"
    vrtemp="$(echo "$ax"   | grep -oE '"vrTemp"[[:space:]]*:[[:space:]]*-?[0-9]+'          | head -1 | grep -oE -- '-?[0-9]+')"
    fan="$(echo "$ax"      | grep -oE '"fanrpm"[[:space:]]*:[[:space:]]*[0-9]+'            | head -1 | grep -oE '[0-9]+')"
    wifi="$(echo "$ax"     | grep -oE '"wifiRSSI"[[:space:]]*:[[:space:]]*-?[0-9]+'        | head -1 | grep -oE -- '-?[0-9]+')"
    bestdiff="$(echo "$ax" | grep -oE '"bestDiff"[[:space:]]*:[[:space:]]*[0-9]+'          | head -1 | grep -oE '[0-9]+')"

    stale="$(echo "$ax" | awk '
      /"message"[[:space:]]*:[[:space:]]*"Stale"/ { want=1; next }
      want && /"count"[[:space:]]*:/ {
        match($0, /[0-9]+/)
        print substr($0, RSTART, RLENGTH)
        exit
      }')"

    [ -n "$uptime" ]   || uptime=0
    [ -n "$hash1m" ]   || hash1m=0
    [ -n "$accepted" ] || accepted=0
    [ -n "$rejected" ] || rejected=0
    [ -n "$stale" ]    || stale=0
    [ -n "$temp" ]     || temp="?"
    [ -n "$vrtemp" ]   || vrtemp="?"
    [ -n "$fan" ]      || fan="?"
    [ -n "$wifi" ]     || wifi="?"


#======== hash_disp conversion from hash1m

    s=${hash1m//$'\n'/}; s=${s//$'\r'/}
    upper=${s%%.*}; lower=${s#*.}; [ "$upper" = "$s" ] && lower=""
    unit="GH/s"; whole=$upper; dec=$lower
    (( ${#upper} > 3 )) && { unit="TH/s"; whole=${upper:0:${#upper}-3}; dec="${upper:${#upper}-3}$lower"; }
    hash_disp="$whole.${dec:0:3} $unit"

#========

    total=$((accepted + rejected))
    if [ "$total" -gt 0 ]; then
      stale_pct="$(awk -v s="$stale" -v t="$total" 'BEGIN { printf "%.2f", (s/t)*100 }')"
    else
      stale_pct="0.00"
    fi

    if [ -z "$ax" ]; then
      printf 'STOPPED|0|0|0|0.00|?|?|?|?|0\n'
      exit 0
    fi

    printf 'RUNNING|%s|%s|%s|%s|%s|%s|%s|%s|%s|%s\n' \
      "$uptime" "$hash_disp" "$accepted" "$stale" "$stale_pct" "$temp" "$vrtemp" "$fan" "$wifi" "$bestdiff"
  )

  while ! read -t 0.1 -u 3 ax_result; do
    printf "Bitaxe: checking %c\r" "${spin:i++%4:1}"
  done
  printf '%-100s\r' ""

  IFS='|' read -r ax_state ax_uptime ax_hash ax_acc ax_stale stale_pct ax_temp ax_vrtemp ax_fan ax_wifi ax_bestdiff <<< "$ax_result"

  uptime_hms="$(fmt_hms "$ax_uptime")"
  acc_fmt="$(fmt_commas "$ax_acc")"
  stale_fmt="$(fmt_commas "$ax_stale")"
  ax_bestdiff="$(fmt_commas "$ax_bestdiff")"

  echo -e "Bitcoin Node Status: ${BIGre}${now}${RCol}    Uptime: ${BIGre}${uptime_hms}${RCol}"

  # ---- Core ----
  if ! cli getblockchaininfo >/dev/null 2>&1; then
    echo -e "Core:   ${BRed}STOPPED${RCol}"
  else
    j="$(cli getblockchaininfo)"
    blocks="$(echo "$j" | grep -oE '"blocks"[[:space:]]*:[[:space:]]*[0-9]+' | head -1 | grep -oE '[0-9]+')"
    peers="$(cli getconnectioncount 2>/dev/null)"
    mempool="$(cli getmempoolinfo 2>/dev/null | grep -oE '"size"[[:space:]]*:[[:space:]]*[0-9]+' | grep -oE '[0-9]+')"
    txindex="$(cli getindexinfo 2>/dev/null | grep -A3 '"txindex"' | grep '"best_block_height"' | grep -o '[0-9]\+' | head -1)"

    echo -e "Core:   ${BIGre}RUNNING${RCol}  block: ${BIGre}${blocks:-?}${RCol}  txindex: ${BIGre}${txindex:-?}${RCol}  mempool: ${BIGre}${mempool:-0}${RCol} txs  peers: ${BIGre}${peers:-?}${RCol}"

   # echo -e "Core:  ${BIGre}RUNNING${RCol}  block: ${BIGre}${blocks:-?}${RCol}  peers: ${BIGre}${peers:-?}${RCol}  mempool: ${BIGre}${mempool:-0}Txs${RCol}  txindex: ${BIGre}${txindex:-?}${RCol}"
  fi

  # ---- Pool / payout ----
  addr="$(grep -oE '"btcaddress"[[:space:]]*:[[:space:]]*"[^"]+"' "$CKPOOL_CONF" \
        | head -1 \
        | sed -E 's/.*"btcaddress"[[:space:]]*:[[:space:]]*"([^"]+)".*/\1/')"

  if ck_running; then
    pool="${BIGre}RUNNING${RCol}"
  else
    pool="${BRed}STOPPED${RCol}"
  fi

  echo -e "Pool:   $pool"
  if [ "$ax_state" = "STOPPED" ]; then
    echo -e "Bitaxe: ${BRed}STOPPED${RCol}"
  else
    echo -e "Bitaxe: hash: ${BIGre}${ax_hash}${RCol}  shares: ${BIGre}${acc_fmt}${RCol}  stale: ${BIGre}${stale_fmt}${RCol}  stale%: ${BIGre}${stale_pct}${RCol}"
    echo -e "Temp:   ${BIGre}${ax_temp}${RCol}C  VRTemp: ${BIGre}${ax_vrtemp}${RCol}C  Fan: ${BIGre}${ax_fan}${RCol} RPM  WiFi: ${BIGre}${ax_wifi}${RCol} dBm"
    echo -e "Best:   ${BIGre}${ax_bestdiff}${RCol}"
  fi
  echo -e "PayTo:  ${BIGre}${addr:-?}${RCol}"
  echo
}

# Bitcoin Exchange Rate
rate() {
  API="https://www.bitstamp.net/api/v2/ticker/btcusd/"
  json="$(curl -fsS "$API")"

  # Parse with jq (requires: sudo apt install jq)
  timestamp="$(jq -r '.timestamp' <<<"$json")"
  open_24="$(jq -r '.open_24'  <<<"$json")"
  open="$(jq -r '.open'        <<<"$json")"
  high="$(jq -r '.high'        <<<"$json")"
  low="$(jq -r '.low'          <<<"$json")"
  last="$(jq -r '.last'        <<<"$json")"
  vwap="$(jq -r '.vwap'        <<<"$json")"
  volume="$(jq -r '.volume'    <<<"$json")"
  change="$(jq -r '.percent_change_24' <<<"$json")"

  ts="$(date -d @"$timestamp" +'%Y-%m-%d %H:%M:%S')"
  now="$(date +%s)"
  elapsed=$(( now - timestamp ))

  # Grouping with thousands separators (ensure this locale exists)
  export LC_NUMERIC=en_US.UTF-8
  format_btc="%10s(\$): %'12.0f"
  format_vol="%8s(BTC): %'21.8f"
  format_chg="%13s: %'15.2f%%"

  echo "USD/BTC@bitstamp.net $ts [${elapsed}s ago]:"
  printf "$format_btc\n" "Open24" "$open_24"
  printf "$format_btc\n" "Open"    "$open"
  printf "$format_btc\n" "High"    "$high"
  printf "$format_btc\n" "Low"     "$low"
  printf "$format_btc\n" "Last"    "$last"
  printf "$format_btc\n" "VWAP"    "$vwap"
  printf "$format_vol\n" "Volume"  "$volume"
  printf "$format_chg\n" "Change24" "$change"
}

version() {
  echo -e "${PROG} v${VERSION} (${FDATE})"

  echo -n "Bitcoin Core: "

  "${BTCBIN}/bitcoin-cli" --version 2>/dev/null \
    | head -1 \
    | sed 's/Bitcoin Core RPC client version v//'
}

upgrade() {
    VER="$1"

    if [ -z "$VER" ]; then
        echo "Usage: btc upgrade VERSION"
        echo "Example: btc upgrade 31.0"
        return 1
    fi

    if "${BTCBIN}/bitcoin-cli" getblockchaininfo >/dev/null 2>&1; then
        echo "ERROR: Bitcoin Core appears to be running."
        echo "Stop it before upgrading."
        return 1
    fi

    TARGET="${BTCPATH}/Bitcoin-${VER}/bin"

    if [ ! -d "$TARGET" ]; then
        echo "Missing directory:"
        echo "  $TARGET"
        return 1
    fi

    echo "Switching Bitcoin Core to version ${VER}..."
    echo

    for f in \
        bitcoin-cli \
        bitcoind \
        bitcoin-qt \
        bitcoin-wallet \
        bitcoin-util \
        bitcoin-tx \
        bitcoin-gui
    do
        if [ -f "$TARGET/$f" ]; then
            ln -sfn "$TARGET/$f" "${BTCBIN}/$f"
            echo "Linked $f -> Bitcoin-${VER}"
        fi
    done

    echo
    echo "Now using Bitcoin Core:"
    "${BTCBIN}/bitcoin-cli" --version | head -1

    echo
    echo "Executable:"
    readlink -f "${BTCBIN}/bitcoind"
}

# ---- ckpool ----
ck_running() {
  pgrep -f "$CKPOOL_BIN" >/dev/null 2>&1
}

ckstart() {
  if ck_running; then
    echo "ckpool already running"
    status
    return 0
  fi

  if ! cli getblockchaininfo >/dev/null 2>&1; then
    die "Core is not running. Start Core first."
  fi

  echo "Starting ckpool..."
  nohup "$CKPOOL_BIN" -c "$CKPOOL_CONF" >> "$CKPOOL_LOG" 2>&1 &
  echo $! > "$CKPOOL_PID"

  wait_ckpool "Waiting for ckpool"

  echo "ckpool started"
  status
}

ckstop() {
  if [ -f "$CKPOOL_PID" ]; then
    kill "$(cat "$CKPOOL_PID")" 2>/dev/null || true
    rm -f "$CKPOOL_PID"
    echo "ckpool stopped"
    status
  else
    local pid
    pid="$(pgrep -f "$CKPOOL_BIN" | head -1)"
    if [ -n "$pid" ]; then
      kill "$pid" 2>/dev/null || true
      echo "ckpool stopped"
      status
    else
      echo "ckpool not running"
      status
    fi
  fi
}

cklog() {
  tail -f "$CKPOOL_LOG"
}

ckaddr() {
  local cur
  cur="$(grep -oE '"btcaddress"[[:space:]]*:[[:space:]]*"[^"]+"' "$CKPOOL_CONF" \
        | head -1 \
        | sed -E 's/.*"btcaddress"[[:space:]]*:[[:space:]]*"([^"]+)".*/\1/')"
  [ -n "$cur" ] || die "Could not find btcaddress in $CKPOOL_CONF"
  echo -e "$cur\n"
}

# ---- Wallets ----
wlist() {
  cli listwallets
}

wdir() {
  cli listwalletdir
}

winfo() {
  local w="$1"
  [ -n "$w" ] || die "Usage: btc winfo <wallet>"
  wcli "$w" getwalletinfo
}

wbal() {
  local w="$1"
  [ -n "$w" ] || die "Usage: btc wbal <wallet>"
  wcli "$w" getbalances
}

wload() {
  local w="$1"
  [ -n "$w" ] || die "Usage: btc wload <wallet>"
  cli loadwallet "$w"
}

wunload() {
  local w="$1"
  [ -n "$w" ] || die "Usage: btc wunload <wallet>"
  cli unloadwallet "$w"
}

wcreate() {
  local w="$1"
  [ -n "$w" ] || die "Usage: btc wcreate <wallet>"
  cli createwallet "$w" false false "" false true
}

wnew() {
  local w="$1"
  local label="${2:-}"
  [ -n "$w" ] || die "Usage: btc wnew <wallet> [label]"
  wcli "$w" getnewaddress "$label" bech32m
}

warchive() {
  local w="$1"
  [ -n "$w" ] || die "Usage: btc warchive <wallet>"

  local src="$DATADIR/$w"
  local dst="$DATADIR/warchive/$w"

  [ -e "$src" ] || die "Wallet not found: $src"
  [ ! -e "$dst" ] || die "Archive target already exists: $dst"

  cli unloadwallet "$w" >/dev/null 2>&1 || true
  mkdir -p "$DATADIR/warchive"
  mv "$src" "$dst" || die "Failed to archive wallet"

  echo "Archived wallet:"
  echo "  from: $src"
  echo "  to:   $dst"
}

warchived() {
  ls -1 "$DATADIR/warchive" 2>/dev/null
}

wwatch() {
  local w="$1"
  [ -n "$w" ] || die "Usage: btc wwatch <wallet>"

  local live="$DATADIR/$w"
  local arch="$DATADIR/warchive/$w"
  local staged=0
  local was_loaded=0

  if [ -e "$live" ]; then
    :
  elif [ -e "$arch" ]; then
    mv "$arch" "$live" || die "Failed to stage archived wallet: $w"
    staged=1
  else
    die "Wallet not found: $w"
  fi

  if cli listwallets | grep -q "\"$w\""; then
    was_loaded=1
  else
    cli loadwallet "$w" >/dev/null 2>&1 || die "Failed to load wallet: $w"
  fi

  echo "wallet: $w"
  wcli "$w" getbalances

  if [ "$was_loaded" -eq 0 ]; then
    cli unloadwallet "$w" >/dev/null 2>&1 || true
  fi

  if [ "$staged" -eq 1 ]; then
    mv "$live" "$arch" || die "Failed to return wallet to warchive: $w"
  fi
}

# ---- Init ----
load_cfg

[ -n "${BTCBIN:-}" ]  || die "BTCBIN not set in config"
[ -n "${DATADIR:-}" ] || die "DATADIR not set in config"
[ -x "$BTCBIN/bitcoind" ]    || die "bitcoind not found at $BTCBIN/bitcoind"
[ -x "$BTCBIN/bitcoin-cli" ] || die "bitcoin-cli not found at $BTCBIN/bitcoin-cli"
[ -d "$DATADIR" ]            || die "Datadir not found: $DATADIR"

[ -n "${CKPOOL_BIN:-}" ]  || die "CKPOOL_BIN not set in config"
[ -n "${CKPOOL_CONF:-}" ] || die "CKPOOL_CONF not set in config"
[ -n "${CKPOOL_LOG:-}" ]  || die "CKPOOL_LOG not set in config"
[ -n "${CKPOOL_PID:-}" ]  || die "CKPOOL_PID not set in config"

# ---- Command dispatcher ----
case "${1:-}" in
  "")                 usage ;;

  start)              start ;;
  stop)               stop ;;
  restart)            restart ;;
  status)             status ;;
  log)                log ;;
  bitaxe)             bitaxe ;;
  paths)              paths ;;
  rate)               rate ;;
  version)            version ;;
  upgrade)            upgrade "$2";;
  ckstart)            ckstart ;;
  ckstop)             ckstop ;;
  cklog)              cklog ;;
  ckaddr)             ckaddr ;;
  wlist)              wlist ;;
  wdir)               wdir ;;
  winfo)              shift; winfo "$@" ;;
  wbal)               shift; wbal "$@" ;;
  wload)              shift; wload "$@" ;;
  wunload)            shift; wunload "$@" ;;
  wcreate)            shift; wcreate "$@" ;;
  wnew)               shift; wnew "$@" ;;
  warchive)           shift; warchive "$@" ;;
  warchived)          warchived ;;
  wwatch)             shift; wwatch "$@" ;;
  *)                  die "Unknown command: $1" ;;
esac
exit


ckrotate)           shift; ckrotate "$@" ;;

ckrotate() {
  local w="$1"
  local label="${2:-}"

  [ -n "$w" ] || die "Usage: btc ckrotate <wallet> [label]"

  if ck_running; then
    die "ckpool is running. Stop it first: btc ckstop"
  fi

  local key="btcaddress"
  local old new ts bak
  local was_loaded=0

  old="$(grep -oE "\"$key\"[[:space:]]*:[[:space:]]*\"[^\"]+\"" "$CKPOOL_CONF" \
        | head -1 \
        | sed -E "s/.*\"$key\"[[:space:]]*:[[:space:]]*\"([^\"]+)\".*/\1/")"

  [ -n "$old" ] || die "Could not find \"$key\" in $CKPOOL_CONF"

  if cli listwallets | grep -q "\"$w\""; then
    was_loaded=1
  else
    cli loadwallet "$w" >/dev/null 2>&1 || die "Failed to load wallet: $w"
  fi

  new="$(wcli "$w" getnewaddress "$label" bech32m)" || die "Failed to get new address"

  echo
  echo "ckrotate preview"
  echo "wallet: $w"
  echo "old:    $old"
  echo "new:    $new"
  echo

  echo -n "Type YES to continue: "
  read -r ans
  [ "$ans" = "YES" ] || die "Aborted."

  ts="$(date +%Y%m%d-%H%M%S)"
  bak="${CKPOOL_CONF}.bak.${ts}"

  cp "$CKPOOL_CONF" "$bak" || die "Backup failed"

  sed -i -E "s/(\"$key\"[[:space:]]*:[[:space:]]*\")([^\"]+)(\")/\1${new}\3/" "$CKPOOL_CONF" \
    || die "Failed to update ckpool.conf"

  if [ "$was_loaded" -eq 0 ]; then
    cli unloadwallet "$w" >/dev/null 2>&1 || true
  fi

  echo
  echo "Address rotated."
  echo "backup: $bak"
  echo
  echo "Start ckpool again with:"
  echo "btc ckstart"
}