bank: default auto-confirm poll interval to 2 seconds.

Faster explorer pool confirmation for demo withdrawals.
This commit is contained in:
Hernâni Marques 2026-07-16 20:49:53 +02:00
parent ffe8ad89db
commit 97b2fcce46
3 changed files with 186 additions and 66 deletions

View file

@ -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 | | `landing-stats-install.sh` | host only | root/podman — copies + runs + optional cron |
| `demo-withdraw-api.py` | `/usr/local/bin/` | root — loopback **:19096** | | `demo-withdraw-api.py` | `/usr/local/bin/` | root — loopback **:19096** |
| `install-demo-withdraw-api.sh` | host only | installs API + nginx + auto-confirm | | `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 | | `auto-confirm-withdrawals.sh` | `/usr/local/bin/` | root — **explorer-only** confirm loop |
| `refresh-demo-withdraw.sh` | `/usr/local/bin/` | refresh static `withdraw.uri` | | `refresh-demo-withdraw.sh` | `/usr/local/bin/` | refresh static `withdraw.uri` |
| `credit-account.sh` | host/ops | admin → user credit | | `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 ```bash
# loop inside container — refuses non-explorer unless ALLOW_NON_EXPLORER=1 # 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` Only confirms withdrawals owned by **`explorer`** when status is `selected`
(community demo path). Does not confirm arbitrary customer withdraws. (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 ## Config
See `configs/taler-hacktivism-bank/` and `configs/bank-landing/`. See `configs/taler-hacktivism-bank/` and `configs/bank-landing/`.

View file

@ -1,33 +1,39 @@
#!/bin/bash #!/bin/bash
# Auto-confirm bank withdrawals for the community demo pool ONLY. # Auto-confirm bank withdrawals for the community demo pool ONLY (explorer).
#
# 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).
# #
# Run once: auto-confirm-withdrawals.sh # Run once: auto-confirm-withdrawals.sh
# Loop: auto-confirm-withdrawals.sh --loop [SECS] # Loop: auto-confirm-withdrawals.sh --loop [SECS]
# #
# Env: # Env:
# BANK_URL (default http://127.0.0.1:9012) # 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 # BANK_PASS or /root/bank-explorer-password.txt
# LANDING_DIR (default /var/www/bank-landing) # 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 set -euo pipefail
BANK="${BANK_URL:-http://127.0.0.1:9012}" BANK="${BANK_URL:-http://127.0.0.1:9012}"
BANK="${BANK%/}" BANK="${BANK%/}"
USER="${BANK_USER:-explorer}" USER="${BANK_USER:-explorer}"
LANDING_DIR="${LANDING_DIR:-/var/www/bank-landing}" 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 LOOP=0
SLEEP=5 SLEEP=2
if [ "${1:-}" = "--loop" ]; then if [ "${1:-}" = "--loop" ]; then
LOOP=1 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 fi
# Safety: only the shared community account unless explicitly overridden
if [ "$USER" != "explorer" ] && [ "${ALLOW_NON_EXPLORER:-0}" != "1" ]; then 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 echo "refusing BANK_USER=$USER — auto-confirm is for explorer only (set ALLOW_NON_EXPLORER=1 to override)" >&2
exit 1 exit 1
@ -41,14 +47,20 @@ if [ -z "$PASS" ]; then
fi fi
[ -n "$PASS" ] || { echo "no password for $USER" >&2; exit 1; } [ -n "$PASS" ] || { echo "no password for $USER" >&2; exit 1; }
# JSON field extract without python (bank container may lack python3) # Single instance (loop mode)
json_str() { if [ "$LOOP" -eq 1 ]; then
# json_str FIELD < json-text-or-file mkdir -p "$(dirname "$LOCK_FILE")" 2>/dev/null || true
local field="$1" exec 9>"$LOCK_FILE"
local data if command -v flock >/dev/null 2>&1; then
if [ -f "${2:-}" ]; then data=$(cat "$2"); else data=$(cat); fi if ! flock -n 9; then
printf '%s' "$data" | sed -n "s/.*\"${field}\"[[:space:]]*:[[:space:]]*\"\\([^\"]*\\)\".*/\\1/p" | head -1 echo "auto-confirm already running (lock $LOCK_FILE) — exit"
} exit 0
fi
fi
fi
log() { printf '%s\n' "$*"; }
qlog() { [ "${QUIET}" = "1" ] || log "$@"; }
token() { token() {
curl -sS -m 12 -u "${USER}:${PASS}" \ curl -sS -m 12 -u "${USER}:${PASS}" \
@ -57,83 +69,169 @@ token() {
"${BANK}/accounts/${USER}/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() { known_ids() {
if [ -f "${LANDING_DIR}/withdraw.uri" ]; then if [ -f "$URI_FILE" ]; then
basename "$(tr -d '\n' <"${LANDING_DIR}/withdraw.uri")" basename "$(tr -d '\n' <"$URI_FILE")"
fi fi
if [ -f "${LANDING_DIR}/withdraw-watch.ids" ]; then if [ -f "$WATCH_FILE" ]; then
# strip empty / comments grep -E '^[0-9a-fA-F-]{36}$' "$WATCH_FILE" || true
grep -E '^[0-9a-fA-F-]{36}$' "${LANDING_DIR}/withdraw-watch.ids" || true
fi 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() { confirm_one() {
local wid="$1" local wid="$1"
local tok="$2" local tok="$2"
local info st uname conf local info st uname conf
# Public status — must belong to explorer (pool), not another customer info=$(status_json "$wid")
info=$(curl -sS -m 10 "${BANK}/withdrawals/${wid}" 2>/dev/null || true)
[ -n "$info" ] || return 0 [ -n "$info" ] || return 0
st=$(printf '%s' "$info" | sed -n 's/.*"status"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' | head -1) st=$(field status "$info")
uname=$(printf '%s' "$info" | sed -n 's/.*"username"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' | head -1) uname=$(field username "$info")
# Only confirm withdrawals owned by the pool account
if [ -n "$uname" ] && [ "$uname" != "$USER" ]; then if [ -n "$uname" ] && [ "$uname" != "$USER" ]; then
echo "skip $wid owner=$uname (only confirm $USER)" qlog "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:-?}"
return 0 return 0
fi fi
echo "confirming $wid as $USER (community pool) ..." case "$st" in
selected)
log "confirming $wid as $USER (status=selected) ..."
conf=$(curl -sS -m 15 -o /tmp/acw-conf.out -w '%{http_code}' \ conf=$(curl -sS -m 15 -o /tmp/acw-conf.out -w '%{http_code}' \
-X POST \ -X POST \
-H "Authorization: Bearer ${tok}" \ -H "Authorization: Bearer ${tok}" \
-H 'Content-Type: application/json' \ -H 'Content-Type: application/json' \
-d '{}' \ -d '{}' \
"${BANK}/accounts/${USER}/withdrawals/${wid}/confirm") "${BANK}/accounts/${USER}/withdrawals/${wid}/confirm")
echo " HTTP $conf $(head -c 200 /tmp/acw-conf.out 2>/dev/null || true)" log " HTTP $conf $(head -c 160 /tmp/acw-conf.out 2>/dev/null || true)"
curl -sS -m 8 "${BANK}/withdrawals/${wid}" 2>/dev/null \ # remember for prune keep
| sed -n 's/.*"status"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/ now status=\1/p' | head -1 || true 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() { 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) tjson=$(token)
tok=$(printf '%s' "$tjson" | sed -n 's/.*"access_token"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' | head -1) tok=$(printf '%s' "$tjson" | sed -n 's/.*"access_token"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' | head -1)
if [ -z "$tok" ]; then if [ -z "$tok" ]; then
echo "token fail for $USER: $tjson" >&2 echo "token fail for $USER: $tjson" >&2
return 1 return 1
fi 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 if [ -z "$ids" ]; then
echo "no withdrawal ids to watch (landing withdraw.uri / withdraw-watch.ids)" qlog "no withdrawal ids to watch"
return 0 return 0
fi fi
: >"${LANDING_DIR}/.auto-confirm-selected.tmp"
while read -r wid; do while read -r wid; do
[ -n "$wid" ] || continue [ -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" 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" 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 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 while true; do
once || true once || true
sleep "$SLEEP" sleep "$SLEEP"

View file

@ -61,18 +61,31 @@ else
fi fi
' '
# start/restart API # start/restart API + single auto-confirm loop (flock inside script)
podman exec -u root "$CTR" bash -lc ' 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 \ nohup python3 /usr/local/bin/demo-withdraw-api.py \
>>/var/log/demo-withdraw-api.log 2>&1 </dev/null & >>/var/log/demo-withdraw-api.log 2>&1 </dev/null &
echo "api pid $!" echo "api pid $!"
# auto-confirm loop # stop auto-confirm by pid
pkill -f "auto-confirm-withdrawals.sh --loop" 2>/dev/null || true ps -eo pid=,args= | awk "/auto-confirm-withdrawals\\.sh --loop/ && !/awk/ {print \$1}" | while read p; do kill \$p 2>/dev/null || true; done
nohup /usr/local/bin/auto-confirm-withdrawals.sh --loop 4 \ sleep 0.5
if [ -f /var/log/auto-confirm-withdrawals.log ]; then
sz=$(wc -c </var/log/auto-confirm-withdrawals.log)
if [ "${sz:-0}" -gt 5000000 ]; then
tail -100 /var/log/auto-confirm-withdrawals.log > /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 </dev/null & >>/var/log/auto-confirm-withdrawals.log 2>&1 </dev/null &
echo "auto-confirm pid $!" echo "auto-confirm pid $!"
sleep 1 sleep 1
ps -eo pid=,args= | awk "/auto-confirm-withdrawals\\.sh --loop/ && !/awk/"
curl -sS -m 8 http://127.0.0.1:19096/demo-withdraw.json | head -c 300; echo curl -sS -m 8 http://127.0.0.1:19096/demo-withdraw.json | head -c 300; echo
' '