diff --git a/scripts/taler-bank/README.md b/scripts/taler-bank/README.md index df29e4b..bf18099 100644 --- a/scripts/taler-bank/README.md +++ b/scripts/taler-bank/README.md @@ -11,6 +11,7 @@ Container: **`taler-hacktivism-bank`** (libeufin-bank, GOA, **no IBAN**). | `landing-stats-install.sh` | host only | root/podman — copies + runs + optional cron | | `demo-withdraw-api.py` | `/usr/local/bin/` | root — loopback **:19096** | | `install-demo-withdraw-api.sh` | host only | installs API + nginx + auto-confirm | +| `maintenance/raise-debit-limits.sh` | host or bank ctr | raise all accounts’ `debit_threshold` (dry-run; `--no-dry`) | | `auto-confirm-withdrawals.sh` | `/usr/local/bin/` | root — **explorer-only** confirm loop | | `refresh-demo-withdraw.sh` | `/usr/local/bin/` | refresh static `withdraw.uri` | | `credit-account.sh` | host/ops | admin → user credit | @@ -60,12 +61,20 @@ Requires **python3** in the bank container. Env: `BANK_URL`, `BANK_USER`/`BANK_P ```bash # loop inside container — refuses non-explorer unless ALLOW_NON_EXPLORER=1 -auto-confirm-withdrawals.sh --loop 4 +auto-confirm-withdrawals.sh --loop 2 ``` Only confirms withdrawals owned by **`explorer`** when status is `selected` (community demo path). Does not confirm arbitrary customer withdraws. +**Stable ops (via `koopa-external` if LAN `koopa` is down):** + +- One process only (`flock` on `/var/run/auto-confirm-withdrawals.lock`) +- `QUIET=1` + summary `tick checked=… selected=…` each loop +- Watch list capped (`WATCH_MAX=80`, prune of bloated `withdraw-watch.ids`) +- Status via `taler-integration/withdrawal-operation/{id}` +- Reinstall: `./install-demo-withdraw-api.sh` on host with podman access to `taler-hacktivism-bank` + ## Config See `configs/taler-hacktivism-bank/` and `configs/bank-landing/`. diff --git a/scripts/taler-bank/auto-confirm-withdrawals.sh b/scripts/taler-bank/auto-confirm-withdrawals.sh index 3b9dc85..a46c523 100755 --- a/scripts/taler-bank/auto-confirm-withdrawals.sh +++ b/scripts/taler-bank/auto-confirm-withdrawals.sh @@ -1,33 +1,39 @@ #!/bin/bash -# Auto-confirm bank withdrawals for the community demo pool ONLY. -# -# Only account: explorer (override only if you really mean another pool user -# via BANK_USER, but still confirms with that user's token only — never -# confirms other customers' withdrawals). +# 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) — must be the pool account +# BANK_USER (default explorer) # BANK_PASS or /root/bank-explorer-password.txt # LANDING_DIR (default /var/www/bank-landing) -# ALLOW_NON_EXPLORER=1 — allow BANK_USER other than explorer (off by default) +# 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=5 +SLEEP=2 if [ "${1:-}" = "--loop" ]; then LOOP=1 - SLEEP="${2:-5}" + 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 -# Safety: only the shared community account unless explicitly overridden 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 @@ -41,14 +47,20 @@ if [ -z "$PASS" ]; then fi [ -n "$PASS" ] || { echo "no password for $USER" >&2; exit 1; } -# JSON field extract without python (bank container may lack python3) -json_str() { - # json_str FIELD < json-text-or-file - local field="$1" - local data - if [ -f "${2:-}" ]; then data=$(cat "$2"); else data=$(cat); fi - printf '%s' "$data" | sed -n "s/.*\"${field}\"[[:space:]]*:[[:space:]]*\"\\([^\"]*\\)\".*/\\1/p" | head -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}" \ @@ -57,83 +69,169 @@ token() { "${BANK}/accounts/${USER}/token" } -# IDs created for the community pool (landing + watch list only) +# 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 "${LANDING_DIR}/withdraw.uri" ]; then - basename "$(tr -d '\n' <"${LANDING_DIR}/withdraw.uri")" + if [ -f "$URI_FILE" ]; then + basename "$(tr -d '\n' <"$URI_FILE")" fi - if [ -f "${LANDING_DIR}/withdraw-watch.ids" ]; then - # strip empty / comments - grep -E '^[0-9a-fA-F-]{36}$' "${LANDING_DIR}/withdraw-watch.ids" || true + 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 - # Public status — must belong to explorer (pool), not another customer - info=$(curl -sS -m 10 "${BANK}/withdrawals/${wid}" 2>/dev/null || true) + info=$(status_json "$wid") [ -n "$info" ] || return 0 - st=$(printf '%s' "$info" | sed -n 's/.*"status"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' | head -1) - uname=$(printf '%s' "$info" | sed -n 's/.*"username"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' | head -1) + st=$(field status "$info") + uname=$(field username "$info") - # Only confirm withdrawals owned by the pool account if [ -n "$uname" ] && [ "$uname" != "$USER" ]; then - echo "skip $wid owner=$uname (only confirm $USER)" - return 0 - fi - # If API omits username, still only confirm via explorer token (cannot confirm others) - if [ -z "$uname" ]; then - # require sender_wire / payto to mention explorer when present - case "$info" in - *explorer*) ;; - *) - # still try only if status selected — confirm endpoint is under explorer account - ;; - esac - fi - - if [ "$st" != "selected" ]; then - echo "skip $wid status=${st:-?} owner=${uname:-?}" + qlog "skip $wid owner=$uname (only confirm $USER)" return 0 fi - echo "confirming $wid as $USER (community pool) ..." - 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") - echo " HTTP $conf $(head -c 200 /tmp/acw-conf.out 2>/dev/null || true)" - curl -sS -m 8 "${BANK}/withdrawals/${wid}" 2>/dev/null \ - | sed -n 's/.*"status"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/ now status=\1/p' | head -1 || true + 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 + 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 - ids=$(known_ids | sort -u) + + # 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 - echo "no withdrawal ids to watch (landing withdraw.uri / withdraw-watch.ids)" + qlog "no withdrawal ids to watch" return 0 fi + + : >"${LANDING_DIR}/.auto-confirm-selected.tmp" while read -r wid; do [ -n "$wid" ] || continue - confirm_one "$wid" "$tok" + 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 - echo "auto-confirm loop every ${SLEEP}s for pool user=$USER only" + 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" diff --git a/scripts/taler-bank/install-demo-withdraw-api.sh b/scripts/taler-bank/install-demo-withdraw-api.sh index 5366486..c4cd2e8 100755 --- a/scripts/taler-bank/install-demo-withdraw-api.sh +++ b/scripts/taler-bank/install-demo-withdraw-api.sh @@ -61,18 +61,31 @@ else fi ' -# start/restart API +# start/restart API + single auto-confirm loop (flock inside script) podman exec -u root "$CTR" bash -lc ' -pkill -f "demo-withdraw-api.py" 2>/dev/null || true +# stop demo-withdraw by pid (avoid pkill -f self-match) +ps -eo pid=,args= | awk "/demo-withdraw-api\\.py/ && !/awk/ {print \$1}" | while read p; do kill \$p 2>/dev/null || true; done +sleep 0.3 nohup python3 /usr/local/bin/demo-withdraw-api.py \ >>/var/log/demo-withdraw-api.log 2>&1 /dev/null || true -nohup /usr/local/bin/auto-confirm-withdrawals.sh --loop 4 \ +# stop auto-confirm by pid +ps -eo pid=,args= | awk "/auto-confirm-withdrawals\\.sh --loop/ && !/awk/ {print \$1}" | while read p; do kill \$p 2>/dev/null || true; done +sleep 0.5 +if [ -f /var/log/auto-confirm-withdrawals.log ]; then + sz=$(wc -c /tmp/ac-keep.log + : > /var/log/auto-confirm-withdrawals.log + cat /tmp/ac-keep.log >> /var/log/auto-confirm-withdrawals.log + fi +fi +nohup env QUIET=1 WATCH_MAX=80 \ + /usr/local/bin/auto-confirm-withdrawals.sh --loop 2 \ >>/var/log/auto-confirm-withdrawals.log 2>&1