#!/bin/bash # Auto-confirm bank withdrawals for the community demo pool ONLY (explorer). # # Run once: auto-confirm-withdrawals.sh # Loop: auto-confirm-withdrawals.sh --loop [SECS] # # Env: # BANK_URL (default http://127.0.0.1:9012) # BANK_USER (default explorer) # BANK_PASS or /root/bank-explorer-password.txt # LANDING_DIR (default /var/www/bank-landing) # ALLOW_NON_EXPLORER=1 # WATCH_MAX max IDs kept in withdraw-watch.ids after prune (default 80) # QUIET=1 less skip noise (default 1 in --loop) # LOCK_FILE default /var/run/auto-confirm-withdrawals.lock set -euo pipefail BANK="${BANK_URL:-http://127.0.0.1:9012}" BANK="${BANK%/}" USER="${BANK_USER:-explorer}" LANDING_DIR="${LANDING_DIR:-/var/www/bank-landing}" WATCH_FILE="${LANDING_DIR}/withdraw-watch.ids" URI_FILE="${LANDING_DIR}/withdraw.uri" WATCH_MAX="${WATCH_MAX:-80}" LOCK_FILE="${LOCK_FILE:-/var/run/auto-confirm-withdrawals.lock}" LOOP=0 SLEEP=2 if [ "${1:-}" = "--loop" ]; then LOOP=1 SLEEP="${2:-2}" fi # Quiet by default in loop mode if [ -z "${QUIET+x}" ]; then if [ "$LOOP" -eq 1 ]; then QUIET=1; else QUIET=0; fi fi if [ "$USER" != "explorer" ] && [ "${ALLOW_NON_EXPLORER:-0}" != "1" ]; then echo "refusing BANK_USER=$USER — auto-confirm is for explorer only (set ALLOW_NON_EXPLORER=1 to override)" >&2 exit 1 fi PASS="${BANK_PASS:-}" if [ -z "$PASS" ]; then for f in "/root/bank-${USER}-password.txt" /root/bank-explorer-password.txt; do if [ -f "$f" ]; then PASS=$(tr -d '\n' <"$f"); break; fi done fi [ -n "$PASS" ] || { echo "no password for $USER" >&2; exit 1; } # Single instance (loop mode) if [ "$LOOP" -eq 1 ]; then mkdir -p "$(dirname "$LOCK_FILE")" 2>/dev/null || true exec 9>"$LOCK_FILE" if command -v flock >/dev/null 2>&1; then if ! flock -n 9; then echo "auto-confirm already running (lock $LOCK_FILE) — exit" exit 0 fi fi fi log() { printf '%s\n' "$*"; } qlog() { [ "${QUIET}" = "1" ] || log "$@"; } token() { curl -sS -m 12 -u "${USER}:${PASS}" \ -H 'Content-Type: application/json' \ -d '{"scope":"readwrite","refreshable":true}' \ "${BANK}/accounts/${USER}/token" } # Prefer taler-integration status (has selection_done / transfer_done) status_json() { local wid="$1" local j j=$(curl -sS -m 8 "${BANK}/taler-integration/withdrawal-operation/${wid}" 2>/dev/null || true) if [ -z "$j" ] || ! printf '%s' "$j" | grep -q '"status"'; then j=$(curl -sS -m 8 "${BANK}/withdrawals/${wid}" 2>/dev/null || true) fi printf '%s' "$j" } field() { # field name from json on stdin/arg local name="$1" data="${2:-}" if [ -z "$data" ]; then data=$(cat); fi printf '%s' "$data" | sed -n "s/.*\"${name}\"[[:space:]]*:[[:space:]]*\"\\([^\"]*\\)\".*/\\1/p" | head -1 } known_ids() { if [ -f "$URI_FILE" ]; then basename "$(tr -d '\n' <"$URI_FILE")" fi if [ -f "$WATCH_FILE" ]; then grep -E '^[0-9a-fA-F-]{36}$' "$WATCH_FILE" || true fi } # Keep file small: drop confirmed/aborted; keep pending/selected + tail prune_watch() { [ -f "$WATCH_FILE" ] || return 0 local tmp keep=0 tmp=$(mktemp) # Prefer newest IDs; re-check status for tail of file only would be slow — # keep last WATCH_MAX unique lines and re-append selected ones found this round. if [ -f "${LANDING_DIR}/.auto-confirm-selected" ]; then cat "${LANDING_DIR}/.auto-confirm-selected" >>"$tmp" 2>/dev/null || true fi tail -n "$((WATCH_MAX * 3))" "$WATCH_FILE" 2>/dev/null \ | grep -E '^[0-9a-fA-F-]{36}$' \ | awk 'NF && !seen[$0]++' \ | tail -n "$WATCH_MAX" >>"$tmp" || true # unique preserve order awk 'NF && !seen[$0]++' "$tmp" >"${tmp}.2" mv "${tmp}.2" "$WATCH_FILE" rm -f "$tmp" keep=$(wc -l <"$WATCH_FILE" | tr -d ' ') qlog "prune watch list → ${keep} ids" } confirm_one() { local wid="$1" local tok="$2" local info st uname conf info=$(status_json "$wid") [ -n "$info" ] || return 0 st=$(field status "$info") uname=$(field username "$info") if [ -n "$uname" ] && [ "$uname" != "$USER" ]; then qlog "skip $wid owner=$uname (only confirm $USER)" return 0 fi case "$st" in selected) log "confirming $wid as $USER (status=selected) ..." conf=$(curl -sS -m 15 -o /tmp/acw-conf.out -w '%{http_code}' \ -X POST \ -H "Authorization: Bearer ${tok}" \ -H 'Content-Type: application/json' \ -d '{}' \ "${BANK}/accounts/${USER}/withdrawals/${wid}/confirm") log " HTTP $conf $(head -c 160 /tmp/acw-conf.out 2>/dev/null || true)" # remember for prune keep echo "$wid" >>"${LANDING_DIR}/.auto-confirm-selected" # verify info=$(status_json "$wid") st=$(field status "$info") log " now status=${st:-?}" ;; confirmed|aborted) qlog "skip $wid status=$st" ;; pending|"") qlog "skip $wid status=${st:-?} (waiting wallet select)" ;; *) qlog "skip $wid status=${st:-?}" ;; esac } once() { local tjson tok ids n_sel=0 n_conf=0 n_pend=0 n_other=0 n=0 tjson=$(token) tok=$(printf '%s' "$tjson" | sed -n 's/.*"access_token"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' | head -1) if [ -z "$tok" ]; then echo "token fail for $USER: $tjson" >&2 return 1 fi # Cap work per loop: last WATCH_MAX ids only (newest at end of file) ids=$( { [ -f "$URI_FILE" ] && basename "$(tr -d '\n' <"$URI_FILE")" if [ -f "$WATCH_FILE" ]; then grep -E '^[0-9a-fA-F-]{36}$' "$WATCH_FILE" | awk 'NF && !seen[$0]++' | tail -n "$WATCH_MAX" fi } | awk 'NF && !seen[$0]++' ) if [ -z "$ids" ]; then qlog "no withdrawal ids to watch" return 0 fi : >"${LANDING_DIR}/.auto-confirm-selected.tmp" while read -r wid; do [ -n "$wid" ] || continue n=$((n + 1)) info=$(status_json "$wid") st=$(field status "$info") case "$st" in selected) n_sel=$((n_sel + 1)) echo "$wid" >>"${LANDING_DIR}/.auto-confirm-selected.tmp" confirm_one "$wid" "$tok" ;; confirmed) n_conf=$((n_conf + 1)); qlog "skip $wid status=confirmed" ;; pending) n_pend=$((n_pend + 1)); qlog "skip $wid status=pending" ;; aborted) n_other=$((n_other + 1)); qlog "skip $wid status=aborted" ;; *) n_other=$((n_other + 1)); qlog "skip $wid status=${st:-?}" ;; esac done <<<"$ids" if [ -s "${LANDING_DIR}/.auto-confirm-selected.tmp" ]; then mv "${LANDING_DIR}/.auto-confirm-selected.tmp" "${LANDING_DIR}/.auto-confirm-selected" else rm -f "${LANDING_DIR}/.auto-confirm-selected.tmp" fi # Always one summary line per loop (monitoring-friendly) log "tick checked=$n selected=$n_sel confirmed=$n_conf pending=$n_pend other=$n_other" # Periodic prune (every loop is ok now that we only scan tail) if [ "$n" -gt "$((WATCH_MAX / 2))" ] || [ -f "$WATCH_FILE" ]; then wc=$(wc -l <"$WATCH_FILE" 2>/dev/null | tr -d ' ' || echo 0) if [ "${wc:-0}" -gt "$((WATCH_MAX * 2))" ]; then prune_watch fi fi } if [ "$LOOP" -eq 1 ]; then log "auto-confirm loop every ${SLEEP}s user=$USER watch_max=$WATCH_MAX quiet=$QUIET" # initial prune if bloated if [ -f "$WATCH_FILE" ]; then wc=$(wc -l <"$WATCH_FILE" | tr -d ' ') if [ "$wc" -gt "$((WATCH_MAX * 2))" ]; then log "watch list bloated ($wc) — pruning" prune_watch fi fi while true; do once || true sleep "$SLEEP" done else once fi