The GOA shop e2e path used to walk every product template every run, which was slow and often hit balance or time budgets after the first few pays. Keep the full catalog in E2E_SHOP_PRODUCTS (id|name|amount, synced with the merchant landing). Each e2e run shuffles that list and pays only E2E_SHOP_PICK_N products (default 2). Shop pays pin instance goa-shop for public template POSTs; settlement payto matches the landing shop popup. Docs in README.md and TESTS.md describe the pick behaviour and env overrides.
1220 lines
49 KiB
Bash
Executable file
1220 lines
49 KiB
Bash
Executable file
#!/usr/bin/env bash
|
||
# Full withdraw + pay cycle; clear BLOCKERS when the path cannot finish.
|
||
# Skips remaining steps if overall budget exceeded (E2E_TIMEOUT, default 90s).
|
||
set -euo pipefail
|
||
ROOT=$(cd "$(dirname "$0")" && pwd)
|
||
# shellcheck source=lib.sh
|
||
source "$ROOT/lib.sh"
|
||
|
||
: "${E2E_TIMEOUT:=180}" # ATM ladder + wirewatch lag needs room (was 55 — too tight)
|
||
: "${E2E_PAY_SECS:=22}" # hard reserve for handle-uri + settle (do not starve pay)
|
||
: "${E2E_SETTLE_ROUNDS:=18}"
|
||
: "${E2E_SETTLE_SLEEP:=3}"
|
||
E2E_START=$(date +%s)
|
||
BAL_BEFORE="(not captured)"
|
||
BAL_AFTER="(not captured)"
|
||
E2E_REPORTED=0
|
||
e2e_left() { echo $(( E2E_TIMEOUT - ($(date +%s) - E2E_START) )); }
|
||
e2e_over() { [ "$(e2e_left)" -le 0 ]; }
|
||
|
||
# Fixed short timeout — used even when e2e budget is exhausted / on EXIT
|
||
# stdout only for JSON; logs go to stderr file so balance parse stays clean
|
||
wcli_bal_snap() {
|
||
local out="$1"
|
||
if [ -z "${WALLET_CLI:-}" ] || [ ! -f "${WDB:-}" ]; then
|
||
echo "(no wallet)" >"$out"
|
||
return 1
|
||
fi
|
||
local err="${out}.err"
|
||
if command -v perl >/dev/null 2>&1; then
|
||
perl -e 'alarm shift; exec @ARGV' 10 \
|
||
node "$WALLET_CLI" --wallet-db="$WDB" --no-throttle --skip-defaults balance \
|
||
>"$out" 2>"$err" || true
|
||
else
|
||
node "$WALLET_CLI" --wallet-db="$WDB" --no-throttle --skip-defaults balance \
|
||
>"$out" 2>"$err" || true
|
||
fi
|
||
[ -s "$out" ]
|
||
}
|
||
|
||
fmt_bal() {
|
||
python3 -c '
|
||
import json,re,sys
|
||
raw=open(sys.argv[1]).read() if sys.argv[1] else ""
|
||
if not raw.strip() or raw.strip().startswith("(no"):
|
||
print(raw.strip() or "(empty)"); sys.exit(0)
|
||
# extract first JSON object with "balances"
|
||
d=None
|
||
for m in re.finditer(r"\{", raw):
|
||
try:
|
||
d=json.loads(raw[m.start():])
|
||
if isinstance(d, dict) and "balances" in d:
|
||
break
|
||
except Exception:
|
||
d=None
|
||
if not isinstance(d, dict):
|
||
print(re.sub(r"\s+"," ", raw)[:200]); sys.exit(0)
|
||
parts=[]
|
||
for b in d.get("balances") or []:
|
||
cur=(b.get("scopeInfo") or {}).get("currency") or "?"
|
||
parts.append("%s avail=%s pendingIn=%s" % (cur, b.get("available"), b.get("pendingIncoming")))
|
||
print("; ".join(parts) if parts else "(no balances)")
|
||
' "${1:-/dev/null}" 2>/dev/null || echo "(unreadable)"
|
||
}
|
||
|
||
# Numeric available for currency CUR (from balance file or fresh wallet snap)
|
||
wallet_avail_num() {
|
||
local f="${1:-}"
|
||
if [ -z "$f" ] || [ ! -s "$f" ]; then
|
||
f="$SCRATCH/bal-live.out"
|
||
wcli_bal_snap "$f" || wcli balance >"$f" 2>&1 || true
|
||
fi
|
||
python3 -c '
|
||
import json,re,sys
|
||
cur=sys.argv[2]
|
||
raw=open(sys.argv[1]).read() if sys.argv[1] else ""
|
||
d=None
|
||
for m in re.finditer(r"\{", raw):
|
||
try:
|
||
d=json.loads(raw[m.start():])
|
||
if isinstance(d, dict) and "balances" in d: break
|
||
except Exception:
|
||
d=None
|
||
if isinstance(d, dict):
|
||
for b in d.get("balances") or []:
|
||
c=(b.get("scopeInfo") or {}).get("currency") or ""
|
||
av=b.get("available") or ""
|
||
if c==cur or av.startswith(cur+":"):
|
||
try:
|
||
print(float(av.split(":",1)[1])); sys.exit(0)
|
||
except Exception:
|
||
pass
|
||
# text fallback
|
||
m=re.search(r"%s:([0-9]+(?:\.[0-9]+)?)" % re.escape(cur), raw)
|
||
print(float(m.group(1)) if m else 0.0)
|
||
' "${f:-/dev/null}" "${CUR:-GOA}" 2>/dev/null || echo 0
|
||
}
|
||
|
||
# Wait for spendable balance (settlement / wirewatch lag). Returns 0 if avail > min.
|
||
# Emphasizes timing, not hard failure, while waiting.
|
||
wait_wallet_balance() {
|
||
local min_n="${1:-0}"
|
||
local rounds="${2:-15}"
|
||
local sleep_s="${3:-3}"
|
||
local r av
|
||
info "wallet settle wait" "up to ${rounds}×${sleep_s}s for avail>${min_n} ${CUR} (wirewatch lag is common)"
|
||
for r in $(seq 1 "$rounds"); do
|
||
wcli run-until-done >"$SCRATCH/settle-run.out" 2>&1 || true
|
||
wcli_bal_snap "$SCRATCH/bal-live.out" || wcli balance >"$SCRATCH/bal-live.out" 2>&1 || true
|
||
av=$(wallet_avail_num "$SCRATCH/bal-live.out")
|
||
if python3 -c "import sys; sys.exit(0 if float(sys.argv[1]) > float(sys.argv[2]) else 1)" "$av" "$min_n" 2>/dev/null; then
|
||
ok "wallet balance ready" "avail=${CUR}:${av} after ~$((r * sleep_s))s settle wait"
|
||
info "balance" "$(fmt_bal "$SCRATCH/bal-live.out")"
|
||
return 0
|
||
fi
|
||
if [ "$r" = "1" ] || [ "$((r % 3))" = "0" ]; then
|
||
info "settlement lag" "round $r/${rounds}: avail=${CUR}:${av} (still waiting — not an error yet)"
|
||
fi
|
||
# gentle wirewatch nudge mid-wait (local only)
|
||
if [ "$r" = "4" ] || [ "$r" = "8" ]; then
|
||
if [ "${E2E_REMOTE:-0}" != "1" ] && koopa_ssh_ok; then
|
||
koopa_ssh_run 8 \
|
||
'podman exec taler-hacktivism-exchange-ansible systemctl restart taler-exchange-wirewatch 2>/dev/null || true' \
|
||
>/dev/null 2>&1 || true
|
||
fi
|
||
fi
|
||
sleep "$sleep_s"
|
||
done
|
||
av=$(wallet_avail_num "$SCRATCH/bal-live.out")
|
||
warn "settlement lag" "after ${rounds}×${sleep_s}s still avail=${CUR}:${av} — coins may still be in flight"
|
||
return 1
|
||
}
|
||
|
||
print_balances() {
|
||
section "e2e · wallet balances"
|
||
info "BALANCE before" "$BAL_BEFORE"
|
||
# refresh after if we can
|
||
if [ -n "${SCRATCH:-}" ]; then
|
||
wcli_bal_snap "$SCRATCH/bal-end.out" || true
|
||
if [ -s "$SCRATCH/bal-end.out" ]; then
|
||
BAL_AFTER=$(fmt_bal "$SCRATCH/bal-end.out")
|
||
fi
|
||
fi
|
||
info "BALANCE after " "$BAL_AFTER"
|
||
}
|
||
|
||
e2e_finish() {
|
||
local code="${1:-1}"
|
||
if [ "$E2E_REPORTED" = "1" ]; then
|
||
return "$code"
|
||
fi
|
||
E2E_REPORTED=1
|
||
print_balances
|
||
summary || true
|
||
return "$code"
|
||
}
|
||
|
||
e2e_skip_rest() {
|
||
local why="$1"
|
||
warn "e2e timeout" "budget ${E2E_TIMEOUT}s exceeded — $why"
|
||
blocker "e2e-timeout" "skipped remaining steps after ${E2E_TIMEOUT}s ($why)"
|
||
section "e2e · report"
|
||
info "WITHDRAW" "incomplete (timeout)"
|
||
info "PAY" "SKIPPED (timeout)"
|
||
# If we already started a withdrawal, dig inside for missing coins
|
||
if [ -n "${WID:-}" ]; then
|
||
dig_when_no_coins 2>/dev/null || true
|
||
fi
|
||
e2e_finish 1
|
||
exit 1
|
||
}
|
||
|
||
# Area e2e-### — withdraw + pay cycle
|
||
set_area e2e
|
||
section "e2e · prerequisites"
|
||
info "e2e budget" "${E2E_TIMEOUT}s (set E2E_TIMEOUT= to change)"
|
||
|
||
# Remote domains (not koopa): public path only, tiny amounts, abort on login/KYC
|
||
: "${E2E_REMOTE:=0}"
|
||
if [ "${LOCAL_STACK:-1}" != "1" ]; then
|
||
E2E_REMOTE=1
|
||
SKIP_SSH=1
|
||
E2E_FAKE_INCOMING=0
|
||
fi
|
||
|
||
# Currency for amounts / balance checks
|
||
CUR="${EXPECT_CURRENCY:-}"
|
||
if [ -z "$CUR" ]; then
|
||
CUR=$(curl -skS -m 8 "$BANK_PUBLIC/config" 2>/dev/null | python3 -c 'import sys,json
|
||
try: print(json.load(sys.stdin).get("currency") or "")
|
||
except Exception: print("")' 2>/dev/null || true)
|
||
fi
|
||
[ -z "$CUR" ] && CUR=$(curl -skS -m 8 "$EXCHANGE_PUBLIC/config" 2>/dev/null | python3 -c 'import sys,json
|
||
try: print(json.load(sys.stdin).get("currency") or "")
|
||
except Exception: print("")' 2>/dev/null || true)
|
||
[ -z "$CUR" ] && CUR="GOA"
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# Variable amounts
|
||
# Withdraw = typical ATM notes only (Bankomat): 20 / 50 / 100 / 200 …
|
||
# Pay = several smaller purchase amounts
|
||
# Env:
|
||
# E2E_WITHDRAW_VALUES="20 50 100" # bare numbers → prefixed with CUR
|
||
# E2E_PAY_VALUES="0.01 0.05 0.5 1 2 5"
|
||
# E2E_VARIABLE=0 # single fixed WITHDRAW_AMT / PAY_AMT
|
||
# E2E_ATM_MAX=N # try at most N ATM withdraws (budget)
|
||
# ---------------------------------------------------------------------------
|
||
: "${E2E_VARIABLE:=1}"
|
||
: "${E2E_ATM_MAX:=4}"
|
||
if [ "$E2E_REMOTE" = "1" ]; then
|
||
# remote: still ATM-shaped but smaller notes; keep cheap
|
||
: "${E2E_WITHDRAW_VALUES:=10 20 50}"
|
||
: "${E2E_PAY_VALUES:=0.01 0.05 0.1 1}"
|
||
else
|
||
# local GOA: classic ATM denominations
|
||
: "${E2E_WITHDRAW_VALUES:=20 50 100 200}"
|
||
: "${E2E_PAY_VALUES:=0.01 0.05 0.1 0.5 1 2 5 10}"
|
||
fi
|
||
|
||
# Build CUR:amount lists
|
||
build_amt_list() {
|
||
local cur="$1"; shift
|
||
local v out=""
|
||
for v in "$@"; do
|
||
case "$v" in
|
||
*:*) out="${out}${out:+ }$v" ;;
|
||
*) out="${out}${out:+ }${cur}:${v}" ;;
|
||
esac
|
||
done
|
||
printf '%s' "$out"
|
||
}
|
||
# shellcheck disable=SC2086
|
||
WITHDRAW_LIST=$(build_amt_list "$CUR" $E2E_WITHDRAW_VALUES)
|
||
# shellcheck disable=SC2086
|
||
PAY_LIST=$(build_amt_list "$CUR" $E2E_PAY_VALUES)
|
||
|
||
if [ "$E2E_VARIABLE" != "1" ]; then
|
||
if [ "$E2E_REMOTE" = "1" ]; then
|
||
WITHDRAW_LIST="${WITHDRAW_AMT:-${CUR}:20}"
|
||
PAY_LIST="${PAY_AMT:-${CUR}:0.01}"
|
||
else
|
||
WITHDRAW_LIST="${WITHDRAW_AMT:-${CUR}:20}"
|
||
PAY_LIST="${PAY_AMT:-${CUR}:0.01}"
|
||
fi
|
||
fi
|
||
|
||
# Credit = sum of planned ATM withdraws + small buffer for fees
|
||
CREDIT_AMT=$(python3 -c '
|
||
import sys
|
||
cur=sys.argv[1]
|
||
vals=sys.argv[2].split()
|
||
s=0.0
|
||
for a in vals:
|
||
try: s += float(a.split(":",1)[-1])
|
||
except Exception: pass
|
||
buf = max(10.0, s * 0.05)
|
||
print("%s:%g" % (cur, s + buf))
|
||
' "$CUR" "$WITHDRAW_LIST" 2>/dev/null || echo "${CUR}:100")
|
||
|
||
# First withdraw/pay for legacy single-step labels
|
||
WITHDRAW_AMT=$(printf '%s' "$WITHDRAW_LIST" | awk '{print $1}')
|
||
PAY_AMT=$(printf '%s' "$PAY_LIST" | awk '{print $1}')
|
||
|
||
# More wall time when running ATM ladder
|
||
if [ "$E2E_VARIABLE" = "1" ]; then
|
||
n_w=$(printf '%s' "$WITHDRAW_LIST" | wc -w | tr -d ' ')
|
||
n_p=$(printf '%s' "$PAY_LIST" | wc -w | tr -d ' ')
|
||
need=$(( 40 + n_w * 35 + n_p * 20 ))
|
||
if [ "${E2E_TIMEOUT}" -lt "$need" ]; then
|
||
E2E_TIMEOUT=$need
|
||
fi
|
||
# cap ATM attempts
|
||
WITHDRAW_LIST=$(printf '%s' "$WITHDRAW_LIST" | tr ' ' '\n' | head -n "$E2E_ATM_MAX" | tr '\n' ' ' | sed 's/ *$//')
|
||
fi
|
||
|
||
BANK_HOST=$(python3 -c 'from urllib.parse import urlparse; print(urlparse("'"$BANK_PUBLIC"'").hostname or "bank")' 2>/dev/null || echo bank)
|
||
|
||
SCRATCH=$(mktemp -d)
|
||
WDB="$SCRATCH/wallet.sqlite3"
|
||
export PATH="/opt/homebrew/bin:/usr/local/bin:$PATH"
|
||
# Always dump balances on any exit (timeout, blocker, signal, success)
|
||
trap 'ec=$?; e2e_finish "$ec"; rm -rf "$SCRATCH"; exit "$ec"' EXIT
|
||
|
||
# Abort cleanly on login / KYC / registration barriers (esp. remote domains)
|
||
e2e_abort_auth() {
|
||
local step="$1" msg="$2"
|
||
warn "$step" "$msg"
|
||
blocker "$step" "abort (login/KYC): $msg"
|
||
section "e2e · report"
|
||
info "ABORTED" "login or KYC blocked further e2e — not retrying"
|
||
info "hint" "remote e2e needs open bank registration + credit path; set E2E_BANK_ADMIN_PASS / E2E_MERCHANT_TOKEN if you have them"
|
||
e2e_finish 1
|
||
exit 1
|
||
}
|
||
|
||
is_auth_kyc_body() {
|
||
# stdin or file arg: true if looks like login/KYC barrier
|
||
local f="${1:-}"
|
||
local t
|
||
if [ -n "$f" ] && [ -f "$f" ]; then t=$(head -c 800 "$f" 2>/dev/null || true)
|
||
else t=$(cat 2>/dev/null || true); fi
|
||
printf '%s' "$t" | grep -qiE 'kyc|legitim|login required|unauthorized|forbidden|captcha|challenge|tan_|not.?allowed|registration.?disabled|admin.?only|permission.?denied|401|403'
|
||
}
|
||
|
||
WALLET_CLI=$(find_wallet_cli) || {
|
||
blocker "prereq" "taler-wallet-cli not found (set WALLET_CLI=)"
|
||
exit 1
|
||
}
|
||
ok "wallet-cli ($WALLET_CLI)"
|
||
info "e2e mode" "$([ "$E2E_REMOTE" = "1" ] && echo "remote/public domain (no SSH)" || echo "local koopa stack")"
|
||
info "currency" "$CUR"
|
||
info "ATM withdraw ladder" "$WITHDRAW_LIST (credit $CREDIT_AMT)"
|
||
info "pay ladder" "$PAY_LIST"
|
||
info "e2e budget" "${E2E_TIMEOUT}s"
|
||
|
||
# Local stack: koopa secrets. Remote: only explicit env (never leak local passwords to foreign banks).
|
||
if [ "$E2E_REMOTE" = "1" ]; then
|
||
ADMIN_PASS="${E2E_BANK_ADMIN_PASS:-}"
|
||
MPW="${E2E_MERCHANT_TOKEN:-${MERCHANT_TOKEN:-}}"
|
||
else
|
||
ADMIN_PASS="${E2E_BANK_ADMIN_PASS:-$(read_secret "taler-bank/bank-admin-password.txt" || true)}"
|
||
MPW="${E2E_MERCHANT_TOKEN:-${MERCHANT_TOKEN:-$(read_secret "taler-merchant/merchant-goa-demo-cp4zqk-password.txt" || true)}}"
|
||
fi
|
||
if [ -n "$ADMIN_PASS" ]; then
|
||
ok "bank admin secret"
|
||
elif [ "$E2E_REMOTE" = "1" ]; then
|
||
warn "bank admin secret" "missing — will try public registration only"
|
||
else
|
||
blocker "prereq" "bank admin password missing (SECRETS_ROOT or koopa /root)"
|
||
exit 1
|
||
fi
|
||
if [ -n "$MPW" ]; then
|
||
ok "merchant secret (instance ${MERCHANT_INSTANCE})"
|
||
elif [ "$E2E_REMOTE" = "1" ]; then
|
||
warn "merchant secret" "missing — order create may fail (set E2E_MERCHANT_TOKEN)"
|
||
else
|
||
blocker "prereq" "merchant instance password missing"
|
||
exit 1
|
||
fi
|
||
|
||
# Public reachability gates (remote: soft-skip if bank/merchant absent)
|
||
for pair in \
|
||
"bank|$BANK_PUBLIC/config" \
|
||
"exchange|$EXCHANGE_PUBLIC/config" \
|
||
"exchange-keys|$EXCHANGE_PUBLIC/keys" \
|
||
"bank-integration|$BANK_PUBLIC/taler-integration/config" \
|
||
"merchant|$MERCHANT_PUBLIC/config"
|
||
do
|
||
IFS='|' read -r name url <<<"$pair"
|
||
code=$(http_code "$url")
|
||
if [ "$code" = "200" ]; then
|
||
ok "reachable $name"
|
||
else
|
||
if [ "$E2E_REMOTE" = "1" ] && [[ "$name" == bank* || "$name" == merchant ]]; then
|
||
warn "reachable $name" "HTTP $code — e2e may abort"
|
||
else
|
||
blocker "prereq" "$name HTTP $code ($url) — cannot start payment cycle"
|
||
fi
|
||
fi
|
||
done
|
||
# Remote: need at least bank+exchange+merchant for a full cycle
|
||
if [ "$E2E_REMOTE" = "1" ]; then
|
||
bc=$(http_code "$BANK_PUBLIC/config")
|
||
ec=$(http_code "$EXCHANGE_PUBLIC/config")
|
||
mc=$(http_code "$MERCHANT_PUBLIC/config")
|
||
if [ "$bc" != "200" ] || [ "$ec" != "200" ]; then
|
||
e2e_abort_auth "prereq" "bank/exchange not publicly usable (bank=$bc exchange=$ec) — skip e2e"
|
||
fi
|
||
if [ "$mc" != "200" ]; then
|
||
warn "prereq" "merchant HTTP $mc — will try withdraw only if possible"
|
||
fi
|
||
fi
|
||
if [ "${#BLOCKERS[@]}" -gt 0 ]; then
|
||
exit 1
|
||
fi
|
||
|
||
USER="mon$(date +%s | tail -c 6)"
|
||
USER_PW=$(python3 -c 'import secrets;print(secrets.token_urlsafe(10))')
|
||
info "e2e user" "$USER withdraw=$WITHDRAW_AMT pay=$PAY_AMT"
|
||
BANK="$BANK_PUBLIC"
|
||
INST="$MERCHANT_INSTANCE"
|
||
|
||
# $1 = max seconds for this call (optional); rest = wallet-cli args
|
||
wcli() {
|
||
local maxc=12
|
||
if [[ "${1:-}" =~ ^[0-9]+$ ]]; then
|
||
maxc=$1
|
||
shift
|
||
fi
|
||
e2e_over && return 124
|
||
local cap
|
||
cap=$(e2e_left)
|
||
[ "$cap" -gt "$maxc" ] && cap=$maxc
|
||
[ "$cap" -lt 3 ] && return 124
|
||
if command -v perl >/dev/null 2>&1; then
|
||
perl -e 'alarm shift; exec @ARGV' "$cap" \
|
||
node "$WALLET_CLI" --wallet-db="$WDB" --no-throttle --skip-defaults "$@"
|
||
else
|
||
node "$WALLET_CLI" --wallet-db="$WDB" --no-throttle --skip-defaults "$@"
|
||
fi
|
||
}
|
||
|
||
# Pay must not be starved: use reserved seconds even if global budget is tight
|
||
wcli_pay() {
|
||
local cap=$E2E_PAY_SECS
|
||
local left
|
||
left=$(e2e_left)
|
||
if [ "$left" -gt "$cap" ]; then
|
||
cap=$E2E_PAY_SECS
|
||
elif [ "$left" -ge 8 ]; then
|
||
cap=$left
|
||
else
|
||
# still try once with floor 12s so Alarm clock is not the blocker
|
||
cap=12
|
||
fi
|
||
info "pay wallet cap" "${cap}s"
|
||
if command -v perl >/dev/null 2>&1; then
|
||
perl -e 'alarm shift; exec @ARGV' "$cap" \
|
||
node "$WALLET_CLI" --wallet-db="$WDB" --no-throttle --skip-defaults "$@"
|
||
else
|
||
node "$WALLET_CLI" --wallet-db="$WDB" --no-throttle --skip-defaults "$@"
|
||
fi
|
||
}
|
||
|
||
# When coins never become available: run inside + targeted bank/exchange dig
|
||
dig_when_no_coins() {
|
||
section "e2e · coins missing — inside dig"
|
||
info "reason" "wallet empty after withdraw path (often wirewatch timing) — probing bank + exchange"
|
||
if [ -n "${WID:-}" ]; then
|
||
curl -sS -m 6 -o "$SCRATCH/wd-dig.json" \
|
||
"$BANK_PUBLIC/taler-integration/withdrawal-operation/${WID}" 2>/dev/null || true
|
||
info "backend withdrawal-operation" "$(python3 -c '
|
||
import json
|
||
try:
|
||
d=json.load(open("'"$SCRATCH"'/wd-dig.json"))
|
||
print("status=%s transfer_done=%s selection_done=%s amount=%s reserve=%s" % (
|
||
d.get("status"), d.get("transfer_done"), d.get("selection_done"),
|
||
d.get("amount"), (d.get("selected_reserve_pub") or "-")[:24]))
|
||
except Exception as e:
|
||
print("unreadable", e)
|
||
' 2>/dev/null)"
|
||
fi
|
||
# Full component inside report (capped SSH; ignore its exit)
|
||
if [ "${SKIP_SSH}" != "1" ] && [ -x "$ROOT/check_inside.sh" ]; then
|
||
info "inside" "running check_inside.sh …"
|
||
# subshell so its PASS_N/FAIL_N/summary don't poison ours; still print to stdout
|
||
(
|
||
# shellcheck disable=SC2030
|
||
"$ROOT/check_inside.sh" || true
|
||
) || true
|
||
else
|
||
warn "inside" "skipped (SKIP_SSH=1 or check_inside.sh missing)"
|
||
fi
|
||
# Extra targeted: wirewatch + recent journal + bank→exchange path
|
||
if koopa_ssh_ok; then
|
||
section "e2e · coins missing — exchange/bank focus"
|
||
DIG=$(koopa_ssh_bash 15 <<'REMOTE' || true
|
||
set +e
|
||
emit() { printf 'D|%s|%s\n' "$1" "$(printf '%s' "$2" | tr '\n' ' ' | head -c 220)"; }
|
||
EX=$(podman ps --format '{{.Names}}' | grep -i exchange | head -1)
|
||
BANK=$(podman ps --format '{{.Names}}' | grep -iE 'hacktivism-bank|taler-bank' | head -1)
|
||
[ -z "$BANK" ] && BANK=$(podman ps --format '{{.Names}}' | grep -i bank | head -1)
|
||
if [ -n "$EX" ]; then
|
||
podman exec "$EX" pgrep -af taler-exchange-wirewatch 2>/dev/null | head -1 | \
|
||
{ read -r l; [ -n "$l" ] && emit OK "wirewatch: $l" || emit ERROR "wirewatch not running"; }
|
||
podman exec "$EX" pgrep -af taler-exchange-transfer 2>/dev/null | head -1 | \
|
||
{ read -r l; [ -n "$l" ] && emit OK "transfer: $l" || emit WARN "transfer not running"; }
|
||
podman exec "$EX" pgrep -af taler-exchange-aggregator 2>/dev/null | head -1 | \
|
||
{ read -r l; [ -n "$l" ] && emit OK "aggregator: $l" || emit WARN "aggregator not running"; }
|
||
# last wirewatch log lines if journal available
|
||
j=$(podman exec "$EX" bash -c 'journalctl -u "taler-exchange-wirewatch*" -n 8 --no-pager 2>/dev/null | tail -5' 2>/dev/null | tr '\n' ';' | head -c 280)
|
||
[ -n "$j" ] && emit INFO "wirewatch journal: $j"
|
||
# reserve lookup via exchange if tools exist (best-effort)
|
||
c=$(curl -sS -m 3 -o /dev/null -w '%{http_code}' http://127.0.0.1:9011/keys 2>/dev/null || echo 000)
|
||
emit INFO "exchange /keys HTTP $c"
|
||
else
|
||
emit ERROR "no exchange container"
|
||
fi
|
||
if [ -n "$BANK" ]; then
|
||
podman exec "$BANK" pgrep -af 'MainKt serve|libeufin-bank serve' 2>/dev/null | head -1 | \
|
||
{ read -r l; [ -n "$l" ] && emit OK "libeufin: $l" || emit ERROR "libeufin not running"; }
|
||
c=$(curl -sS -m 3 -o /dev/null -w '%{http_code}' http://127.0.0.1:9012/taler-integration/config 2>/dev/null || echo 000)
|
||
emit INFO "bank integration /config HTTP $c"
|
||
else
|
||
emit ERROR "no bank container"
|
||
fi
|
||
REMOTE
|
||
)
|
||
while IFS= read -r line; do
|
||
case "$line" in
|
||
D\|*)
|
||
IFS="|" read -r _ lvl msg <<<"$line"
|
||
case "$lvl" in
|
||
OK) ok "dig $msg" ;;
|
||
ERROR) err "dig" "$msg" ;;
|
||
WARN) warn "dig" "$msg" ;;
|
||
INFO) info "dig" "$msg" ;;
|
||
esac
|
||
;;
|
||
esac
|
||
done <<<"$DIG"
|
||
fi
|
||
}
|
||
|
||
# Baseline wallet balance (empty DB) — always shown even if we abort later
|
||
wcli_bal_snap "$SCRATCH/bal-before.out" || true
|
||
BAL_BEFORE=$(fmt_bal "$SCRATCH/bal-before.out")
|
||
info "BALANCE before" "$BAL_BEFORE"
|
||
|
||
# ---------------------------------------------------------------------------
|
||
section "e2e · bank (account · credit · withdraw)"
|
||
# ---------------------------------------------------------------------------
|
||
AT=""
|
||
if [ -n "${ADMIN_PASS:-}" ]; then
|
||
AT=$(curl -sS -m 15 -u "admin:${ADMIN_PASS}" -H 'Content-Type: application/json' \
|
||
-d '{"scope":"readwrite"}' \
|
||
"$BANK/accounts/admin/token" 2>"$SCRATCH/at.err" | python3 -c 'import sys,json;print(json.load(sys.stdin).get("access_token",""))' 2>/dev/null || true)
|
||
fi
|
||
if [ -n "$AT" ]; then
|
||
ok "admin token"
|
||
elif [ "$E2E_REMOTE" = "1" ]; then
|
||
warn "admin token" "unavailable — trying open registration"
|
||
else
|
||
if is_auth_kyc_body "$SCRATCH/at.err"; then
|
||
e2e_abort_auth "bank-auth" "admin login blocked (KYC/auth) — $(head -c 80 "$SCRATCH/at.err" 2>/dev/null)"
|
||
fi
|
||
blocker "bank-auth" "admin token failed — check admin password / bank up ($(head -c 80 "$SCRATCH/at.err" 2>/dev/null))"
|
||
exit 1
|
||
fi
|
||
|
||
# Create account: with admin bearer if we have it, else unauthenticated registration (demo banks)
|
||
if [ -n "$AT" ]; then
|
||
code=$(curl -sS -m 15 -o "$SCRATCH/acc.json" -w '%{http_code}' -X POST \
|
||
-H "Authorization: Bearer $AT" -H 'Content-Type: application/json' \
|
||
-d "{\"username\":\"${USER}\",\"password\":\"${USER_PW}\",\"name\":\"Monitoring\",\"is_public\":false,\"is_taler_exchange\":false,\"debit_threshold\":\"${CUR}:1000\"}" \
|
||
"$BANK/accounts")
|
||
else
|
||
code=$(curl -sS -m 15 -o "$SCRATCH/acc.json" -w '%{http_code}' -X POST \
|
||
-H 'Content-Type: application/json' \
|
||
-d "{\"username\":\"${USER}\",\"password\":\"${USER_PW}\",\"name\":\"Monitoring\",\"is_public\":false,\"is_taler_exchange\":false,\"debit_threshold\":\"${CUR}:1000\"}" \
|
||
"$BANK/accounts")
|
||
fi
|
||
if [ "$code" = "200" ] || [ "$code" = "201" ]; then
|
||
ok "create account $USER"
|
||
elif [ "$code" = "401" ] || [ "$code" = "403" ] || is_auth_kyc_body "$SCRATCH/acc.json"; then
|
||
e2e_abort_auth "bank-account" "registration/login blocked HTTP $code — $(head -c 100 "$SCRATCH/acc.json" | tr '\n' ' ')"
|
||
else
|
||
if [ "$E2E_REMOTE" = "1" ]; then
|
||
e2e_abort_auth "bank-account" "POST /accounts HTTP $code — cannot open account on this domain"
|
||
fi
|
||
blocker "bank-account" "POST /accounts HTTP $code — $(head -c 100 "$SCRATCH/acc.json" | tr '\n' ' ')"
|
||
exit 1
|
||
fi
|
||
|
||
if [ -n "$AT" ]; then
|
||
export USER_NAME="$USER" CREDIT_AMT="$CREDIT_AMT" OUT="$SCRATCH/credit.json" BANK_HOST="$BANK_HOST"
|
||
python3 - <<'PY'
|
||
import json, os
|
||
ALPH = "0123456789ABCDEFGHJKMNPQRSTVWXYZ"
|
||
def crock32(b):
|
||
bits = val = 0; o = []
|
||
for byte in b:
|
||
val = (val << 8) | byte; bits += 8
|
||
while bits >= 5:
|
||
bits -= 5; o.append(ALPH[(val >> bits) & 31])
|
||
if bits: o.append(ALPH[(val << (5 - bits)) & 31])
|
||
return "".join(o)
|
||
uid = crock32(os.urandom(32))
|
||
user = os.environ["USER_NAME"]
|
||
host = os.environ.get("BANK_HOST") or "bank"
|
||
json.dump({
|
||
"payto_uri": f"payto://x-taler-bank/{host}/{user}?receiver-name={user}&message=mon",
|
||
"amount": os.environ["CREDIT_AMT"],
|
||
"request_uid": uid,
|
||
}, open(os.environ["OUT"], "w"))
|
||
PY
|
||
curl -sS -m 15 -o "$SCRATCH/credit.out" -H "Authorization: Bearer $AT" \
|
||
-H 'Content-Type: application/json' -d @"$SCRATCH/credit.json" \
|
||
"$BANK/accounts/admin/transactions" >/dev/null || true
|
||
if grep -q row_id "$SCRATCH/credit.out" 2>/dev/null; then
|
||
ok "credit $CREDIT_AMT → $USER"
|
||
else
|
||
if is_auth_kyc_body "$SCRATCH/credit.out"; then
|
||
e2e_abort_auth "bank-credit" "admin credit blocked (KYC/auth)"
|
||
fi
|
||
if [ "$E2E_REMOTE" = "1" ]; then
|
||
e2e_abort_auth "bank-credit" "admin transfer failed — no credit path on remote ($(head -c 80 "$SCRATCH/credit.out" | tr '\n' ' '))"
|
||
fi
|
||
blocker "bank-credit" "admin transfer failed — $(head -c 100 "$SCRATCH/credit.out" | tr '\n' ' ')"
|
||
exit 1
|
||
fi
|
||
else
|
||
# No admin: remote demo may start funded or require cash-in — try withdraw later; warn
|
||
warn "bank-credit" "skipped (no admin) — withdraw may fail without balance"
|
||
fi
|
||
|
||
UT=$(curl -sS -m 15 -u "${USER}:${USER_PW}" -H 'Content-Type: application/json' \
|
||
-d '{"scope":"readwrite"}' \
|
||
"$BANK/accounts/${USER}/token" 2>"$SCRATCH/ut.err" | python3 -c 'import sys,json;print(json.load(sys.stdin).get("access_token",""))' 2>/dev/null || true)
|
||
if [ -n "$UT" ]; then
|
||
ok "user token"
|
||
elif is_auth_kyc_body "$SCRATCH/ut.err" || is_auth_kyc_body /dev/null; then
|
||
e2e_abort_auth "bank-auth" "user token failed (login/KYC) for $USER"
|
||
else
|
||
e2e_abort_auth "bank-auth" "user token failed for $USER — $(head -c 80 "$SCRATCH/ut.err" 2>/dev/null)"
|
||
fi
|
||
|
||
# ---------------------------------------------------------------------------
|
||
section "e2e · wallet setup (exchange + ToS once)"
|
||
# ---------------------------------------------------------------------------
|
||
if ! wcli exchanges add "${EXCHANGE_PUBLIC}/" >"$SCRATCH/ex-add.out" 2>&1; then
|
||
if ! grep -qiE 'already|ok' "$SCRATCH/ex-add.out"; then
|
||
blocker "wallet-exchange" "exchanges add failed — $(tail -c 160 "$SCRATCH/ex-add.out" | tr '\n' ' ')"
|
||
else
|
||
ok "wallet exchange known"
|
||
fi
|
||
else
|
||
ok "wallet add exchange"
|
||
fi
|
||
wcli exchanges update "${EXCHANGE_PUBLIC}/" >"$SCRATCH/ex-up.out" 2>&1 || true
|
||
if wcli exchanges accept-tos "${EXCHANGE_PUBLIC}/" >"$SCRATCH/ex-tos.out" 2>&1; then
|
||
ok "wallet accept ToS"
|
||
else
|
||
if grep -qiE 'pending|tos|terms' "$SCRATCH/ex-tos.out"; then
|
||
blocker "wallet-tos" "accept-tos failed — wallet may refuse withdraw ($(tail -c 120 "$SCRATCH/ex-tos.out" | tr '\n' ' '))"
|
||
else
|
||
warn "wallet accept ToS" "$(tail -c 80 "$SCRATCH/ex-tos.out" | tr '\n' ' ')"
|
||
fi
|
||
fi
|
||
|
||
wd_status() {
|
||
curl -sS -m 8 -o "$SCRATCH/wd-st.json" -w '' \
|
||
"$BANK/taler-integration/withdrawal-operation/${WID}" 2>/dev/null || true
|
||
python3 -c 'import json;d=json.load(open("'"$SCRATCH"'/wd-st.json"));print(d.get("status",""))' 2>/dev/null || true
|
||
}
|
||
wd_status_detail() {
|
||
python3 -c 'import json;d=json.load(open("'"$SCRATCH"'/wd-st.json"))
|
||
print("status=%s reserve=%s exchange=%s" % (
|
||
d.get("status"),
|
||
(d.get("selected_reserve_pub") or "-")[:20],
|
||
(d.get("selected_exchange") or d.get("selected_exchange_account") or "-")[:60],
|
||
))' 2>/dev/null || echo "(no status body)"
|
||
}
|
||
|
||
fake_incoming_speedup() {
|
||
[ "${E2E_FAKE_INCOMING}" = "1" ] || return 0
|
||
st=$(wd_status)
|
||
RPUB=$(python3 -c 'import json;d=json.load(open("'"$SCRATCH"'/wd-st.json"));print(d.get("selected_reserve_pub") or "")' 2>/dev/null || true)
|
||
AMT=$(python3 -c 'import json;d=json.load(open("'"$SCRATCH"'/wd-st.json"));print(d.get("amount") or "'"$WITHDRAW_AMT"'")' 2>/dev/null || echo "$WITHDRAW_AMT")
|
||
DEBIT=$(python3 -c 'import json;d=json.load(open("'"$SCRATCH"'/wd-st.json"));print(d.get("sender_wire") or "")' 2>/dev/null || true)
|
||
[ -z "$DEBIT" ] && DEBIT="payto://x-taler-bank/${BANK_HOST}/${USER}?receiver-name=${USER}"
|
||
[ -z "$RPUB" ] && return 0
|
||
[ -z "${AT:-}" ] && return 0
|
||
WG="${BANK}/accounts/exchange/taler-wire-gateway/admin/add-incoming"
|
||
curl -sS -m 10 -o "$SCRATCH/fake-in.json" -w '%{http_code}' -X POST \
|
||
-H "Authorization: Bearer $AT" -H 'Content-Type: application/json' \
|
||
-d "{\"amount\":\"${AMT}\",\"reserve_pub\":\"${RPUB}\",\"debit_account\":\"${DEBIT}\"}" \
|
||
"$WG" >/dev/null || true
|
||
if [ "$E2E_REMOTE" != "1" ] && koopa_ssh_ok; then
|
||
koopa_ssh_run 22 \
|
||
'podman exec taler-hacktivism-exchange-ansible bash -c "runuser -u taler-exchange-wire -- timeout 15 taler-exchange-wirewatch -c /etc/taler-exchange/taler-exchange.conf -t -L INFO 2>&1 | tail -5"' \
|
||
>/dev/null 2>&1 || true
|
||
fi
|
||
}
|
||
|
||
# One ATM-style withdraw: create → accept → select → confirm → settle
|
||
# returns 0 on wallet balance increase, 1 on soft fail (ladder continues)
|
||
e2e_one_withdraw() {
|
||
WITHDRAW_AMT="$1"
|
||
local tag
|
||
tag=$(printf '%s' "$WITHDRAW_AMT" | tr '.:' '__')
|
||
section "e2e · ATM withdraw $WITHDRAW_AMT"
|
||
e2e_over && { warn "ATM withdraw" "budget exhausted — skip $WITHDRAW_AMT"; return 1; }
|
||
|
||
curl -sS -m 15 -o "$SCRATCH/wd-$tag.json" -H "Authorization: Bearer $UT" \
|
||
-H 'Content-Type: application/json' \
|
||
-d "{\"amount\":\"${WITHDRAW_AMT}\",\"exchange_url\":\"${EXCHANGE_PUBLIC}/\"}" \
|
||
"$BANK/accounts/${USER}/withdrawals"
|
||
WID=$(python3 -c 'import json;d=json.load(open("'"$SCRATCH"'/wd-'"$tag"'.json"));print(d.get("withdrawal_id") or d.get("id") or "")' 2>/dev/null || true)
|
||
URI=$(python3 -c 'import json;d=json.load(open("'"$SCRATCH"'/wd-'"$tag"'.json"));print(d.get("taler_withdraw_uri") or "")' 2>/dev/null || true)
|
||
URI_CLEAN=$(python3 -c 'import json;u=json.load(open("'"$SCRATCH"'/wd-'"$tag"'.json")).get("taler_withdraw_uri") or "";print(u.replace(":443/","/"))' 2>/dev/null || true)
|
||
if [ -z "$WID" ] || [ -z "$URI_CLEAN" ]; then
|
||
warn "ATM withdraw $WITHDRAW_AMT" "create failed — $(head -c 80 "$SCRATCH/wd-$tag.json" | tr '\n' ' ')"
|
||
return 1
|
||
fi
|
||
ok "ATM withdrawal created $WITHDRAW_AMT ($WID)"
|
||
cp "$SCRATCH/wd-$tag.json" "$SCRATCH/wd.json"
|
||
|
||
USE_URI="$URI"
|
||
[ -z "$USE_URI" ] && USE_URI="$URI_CLEAN"
|
||
if wcli withdraw accept-uri --exchange "${EXCHANGE_PUBLIC}/" "$USE_URI" >"$SCRATCH/accept-$tag.out" 2>&1; then
|
||
ok "wallet accept $WITHDRAW_AMT"
|
||
elif [ "$USE_URI" != "$URI_CLEAN" ] && [ -n "$URI_CLEAN" ] \
|
||
&& wcli withdraw accept-uri --exchange "${EXCHANGE_PUBLIC}/" "$URI_CLEAN" >"$SCRATCH/accept-$tag.out" 2>&1; then
|
||
ok "wallet accept $WITHDRAW_AMT (no :443)"
|
||
else
|
||
warn "ATM withdraw $WITHDRAW_AMT" "accept-uri failed — $(tail -c 100 "$SCRATCH/accept-$tag.out" | tr '\n' ' ')"
|
||
return 1
|
||
fi
|
||
cp "$SCRATCH/accept-$tag.out" "$SCRATCH/accept.out"
|
||
|
||
st=""
|
||
for i in 1 2 3 4; do
|
||
e2e_over && break
|
||
wcli run-until-done >"$SCRATCH/sel-$tag-$i.out" 2>&1 || true
|
||
st=$(wd_status)
|
||
case "$st" in selected|confirmed|aborted) break ;; esac
|
||
done
|
||
if [ "$st" != "selected" ] && [ "$st" != "confirmed" ]; then
|
||
wcli transactions >"$SCRATCH/tx-pre.json" 2>&1 || true
|
||
RPUB=$(python3 -c '
|
||
import re
|
||
t=open("'"$SCRATCH"'/accept.out").read()+open("'"$SCRATCH"'/tx-pre.json").read()
|
||
m=re.search(r"reserve[_ ]?pub[\"=: ]+([A-Z0-9]{40,})", t, re.I)
|
||
if not m: m=re.search(r"\"reservePub\"\s*:\s*\"([^\"]+)\"", t)
|
||
print(m.group(1) if m else "")
|
||
' 2>/dev/null || true)
|
||
EPAYTO=$(curl -sS -m 12 "$EXCHANGE_PUBLIC/keys" 2>/dev/null | python3 -c '
|
||
import json,sys
|
||
d=json.load(sys.stdin)
|
||
acc=d.get("accounts") or []
|
||
for a in acc:
|
||
p=a.get("payto_uri") or a.get("payto_address") or ""
|
||
if "x-taler-bank" in p or "exchange" in p:
|
||
print(p); break
|
||
else:
|
||
if acc: print(acc[0].get("payto_uri") or "")
|
||
' 2>/dev/null || true)
|
||
if [ -n "$RPUB" ] && [ -n "$EPAYTO" ]; then
|
||
curl -sS -m 12 -o "$SCRATCH/sel.json" -X POST \
|
||
-H 'Content-Type: application/json' \
|
||
-d "{\"reserve_pub\":\"$RPUB\",\"selected_exchange\":\"$EPAYTO\"}" \
|
||
"$BANK/taler-integration/withdrawal-operation/${WID}" >/dev/null || true
|
||
st=$(wd_status)
|
||
fi
|
||
fi
|
||
if [ "$st" != "selected" ] && [ "$st" != "confirmed" ]; then
|
||
warn "ATM withdraw $WITHDRAW_AMT" "not selected (status=${st:-?})"
|
||
return 1
|
||
fi
|
||
if [ "$st" != "confirmed" ]; then
|
||
code=$(curl -sS -m 15 -o "$SCRATCH/conf-$tag.json" -w '%{http_code}' -X POST \
|
||
-H "Authorization: Bearer $UT" -H 'Content-Type: application/json' -d '{}' \
|
||
"$BANK/accounts/${USER}/withdrawals/${WID}/confirm")
|
||
case "$code" in
|
||
200|204) ok "bank confirm $WITHDRAW_AMT" ;;
|
||
*) warn "ATM withdraw $WITHDRAW_AMT" "confirm HTTP $code"; return 1 ;;
|
||
esac
|
||
fi
|
||
fake_incoming_speedup
|
||
|
||
# Short per-ATM poll; full settle wait happens after the ladder (avoids false FAIL)
|
||
local ok_bal=0 r av
|
||
for r in 1 2 3 4 5 6; do
|
||
wcli run-until-done >"$SCRATCH/run-$tag-$r.out" 2>&1 || true
|
||
wcli_bal_snap "$SCRATCH/bal-$tag.out" || wcli balance >"$SCRATCH/bal-$tag.out" 2>&1 || true
|
||
av=$(wallet_avail_num "$SCRATCH/bal-$tag.out")
|
||
if python3 -c "import sys; sys.exit(0 if float(sys.argv[1]) > 0 else 1)" "$av" 2>/dev/null; then
|
||
ok_bal=1
|
||
ok "wallet funded after ATM $WITHDRAW_AMT" "avail=${CUR}:${av}"
|
||
info "balance" "$(fmt_bal "$SCRATCH/bal-$tag.out")"
|
||
break
|
||
fi
|
||
sleep 2
|
||
done
|
||
if [ "$ok_bal" = "1" ]; then
|
||
return 0
|
||
fi
|
||
# Bank side often already confirmed — treat as timing lag, not hard fail
|
||
st=$(wd_status)
|
||
if [ "$st" = "confirmed" ]; then
|
||
warn "ATM withdraw $WITHDRAW_AMT" "bank confirmed; wallet not funded yet (settlement timing — will recheck later)"
|
||
return 2
|
||
fi
|
||
warn "ATM withdraw $WITHDRAW_AMT" "no wallet balance yet (status=${st:-?})"
|
||
return 1
|
||
}
|
||
|
||
# One payment: e2e_one_pay AMOUNT [SUMMARY] [TAG]
|
||
# SUMMARY defaults to "monitoring pay $AMOUNT"; TAG defaults from amount.
|
||
e2e_one_pay() {
|
||
PAY_AMT="$1"
|
||
local PAY_SUM="${2:-monitoring pay ${PAY_AMT}}"
|
||
local tag="${3:-}"
|
||
if [ -z "$tag" ]; then
|
||
tag=$(printf '%s' "$PAY_AMT" | tr '.:' '__')
|
||
fi
|
||
# shell-safe tag for files
|
||
tag=$(printf '%s' "$tag" | tr -c 'A-Za-z0-9._-' '_')
|
||
section "e2e · pay $PAY_AMT · $PAY_SUM"
|
||
e2e_over && { warn "pay" "budget exhausted — skip $PAY_AMT ($PAY_SUM)"; return 1; }
|
||
if [ -z "${MPW:-}" ]; then
|
||
warn "pay $PAY_AMT" "no merchant token"
|
||
return 1
|
||
fi
|
||
AUTH="Authorization: Bearer secret-token:${MPW}"
|
||
# JSON-escape summary for curl -d
|
||
local SUM_JSON
|
||
SUM_JSON=$(python3 -c 'import json,sys; print(json.dumps(sys.argv[1]))' "$PAY_SUM" 2>/dev/null || printf '"%s"' "$PAY_SUM")
|
||
curl -skS -m 15 -o "$SCRATCH/ord-$tag.json" -X POST \
|
||
-H "$AUTH" -H 'Content-Type: application/json' \
|
||
-d "{\"order\":{\"summary\":${SUM_JSON},\"amount\":\"${PAY_AMT}\",\"fulfillment_message\":\"ok\"},\"create_token\":true}" \
|
||
"${MERCHANT_PUBLIC}/instances/${INST}/private/orders" 2>"$SCRATCH/ord-$tag.err" || true
|
||
if ! grep -q order_id "$SCRATCH/ord-$tag.json" 2>/dev/null; then
|
||
if [ "$E2E_REMOTE" != "1" ] && koopa_ssh_ok; then
|
||
koopa_ssh_run 20 \
|
||
"curl -skS -m 12 -X POST -H 'Authorization: Bearer secret-token:${MPW}' -H 'Content-Type: application/json' -d '{\"order\":{\"summary\":${SUM_JSON},\"amount\":\"${PAY_AMT}\",\"fulfillment_message\":\"ok\"},\"create_token\":true}' 'https://127.0.0.1:9010/instances/${INST}/private/orders'" \
|
||
>"$SCRATCH/ord-$tag.json" 2>"$SCRATCH/ord-$tag.err" || true
|
||
fi
|
||
fi
|
||
OID=$(python3 -c 'import json,re,sys;t=open(sys.argv[1]).read()
|
||
try: print(json.loads(t).get("order_id") or "")
|
||
except Exception:
|
||
m=re.search(r"\"order_id\"\s*:\s*\"([^\"]+)\"",t); print(m.group(1) if m else "")
|
||
' "$SCRATCH/ord-$tag.json" 2>/dev/null || true)
|
||
OTOK=$(python3 -c 'import json,re,sys;t=open(sys.argv[1]).read()
|
||
try: print(json.loads(t).get("token") or "")
|
||
except Exception:
|
||
m=re.search(r"\"token\"\s*:\s*\"([^\"]+)\"",t); print(m.group(1) if m else "")
|
||
' "$SCRATCH/ord-$tag.json" 2>/dev/null || true)
|
||
if [ -z "$OID" ]; then
|
||
if is_auth_kyc_body "$SCRATCH/ord-$tag.json"; then
|
||
e2e_abort_auth "merchant-order" "merchant auth/KYC on pay $PAY_AMT ($PAY_SUM)"
|
||
fi
|
||
warn "pay $PAY_AMT ($PAY_SUM)" "order create failed — $(head -c 80 "$SCRATCH/ord-$tag.json" | tr '\n' ' ')"
|
||
return 1
|
||
fi
|
||
ok "merchant order $OID ($PAY_AMT · $PAY_SUM)"
|
||
curl -skS -m 12 -o "$SCRATCH/ord-det-$tag.json" -H "$AUTH" \
|
||
"${MERCHANT_PUBLIC}/instances/${INST}/private/orders/${OID}" 2>/dev/null || true
|
||
PAYURI=$(python3 -c 'import json,sys
|
||
try: print(json.load(open(sys.argv[1])).get("taler_pay_uri") or "")
|
||
except Exception: print("")
|
||
' "$SCRATCH/ord-det-$tag.json" 2>/dev/null || true)
|
||
if [ -z "$PAYURI" ] && [ -n "$OTOK" ]; then
|
||
MH=$(python3 -c 'from urllib.parse import urlparse; print(urlparse("'"$MERCHANT_PUBLIC"'").hostname or "taler.hacktivism.ch")' 2>/dev/null || echo "taler.hacktivism.ch")
|
||
PAYURI="taler://pay/${MH}/instances/${INST}/${OID}/?c=${OTOK}"
|
||
fi
|
||
# normalize default HTTPS port (wallets accept both)
|
||
PAYURI=$(printf '%s' "$PAYURI" | sed 's/:443\//\//g; s/:443?/?/g')
|
||
if [ -z "$PAYURI" ]; then
|
||
warn "pay $PAY_AMT ($PAY_SUM)" "no pay URI for $OID"
|
||
return 1
|
||
fi
|
||
ok "taler_pay_uri ready ($tag)"
|
||
if ! wcli_pay handle-uri --yes "$PAYURI" >"$SCRATCH/pay-$tag.out" 2>&1; then
|
||
warn "pay $PAY_AMT ($PAY_SUM)" "handle-uri failed — $(tail -c 100 "$SCRATCH/pay-$tag.out" | tr '\n' ' ')"
|
||
return 1
|
||
fi
|
||
ok "wallet handle pay $PAY_AMT ($PAY_SUM)"
|
||
local r
|
||
for r in 1 2 3; do
|
||
wcli_pay run-until-done >"$SCRATCH/pay-run-$tag.out" 2>&1 || true
|
||
wcli_pay transactions >"$SCRATCH/tx-$tag.out" 2>&1 || true
|
||
curl -skS -m 8 -o "$SCRATCH/ord-paid-$tag.json" -H "$AUTH" \
|
||
"${MERCHANT_PUBLIC}/instances/${INST}/private/orders/${OID}" 2>/dev/null || true
|
||
if grep -qiE 'payment|paid|Payment' "$SCRATCH/tx-$tag.out" 2>/dev/null \
|
||
|| grep -qiE 'done|paid|success|Payment' "$SCRATCH/pay-$tag.out" 2>/dev/null \
|
||
|| python3 -c 'import json,sys
|
||
d=json.load(open(sys.argv[1]))
|
||
sys.exit(0 if d.get("paid") is True or str(d.get("order_status","")).lower()=="paid" else 1)
|
||
' "$SCRATCH/ord-paid-$tag.json" 2>/dev/null; then
|
||
ok "payment settled $PAY_AMT ($PAY_SUM · order $OID)"
|
||
return 0
|
||
fi
|
||
done
|
||
warn "pay $PAY_AMT ($PAY_SUM)" "not settled for order $OID"
|
||
return 1
|
||
}
|
||
|
||
# GOA shop catalog (full list) — keep in sync with configs/merchant-landing/index.html
|
||
# id|product_name|amount
|
||
# Landing QR = taler://pay-template/…/{id}; popup = public POST templates/{id}.
|
||
# E2E does not pay every product every run: shuffle + pick E2E_SHOP_PICK_N (default 2).
|
||
: "${E2E_SHOP_PRODUCTS:=orbit-sticker|Orbit sticker pack|GOA:2
|
||
nebula-coffee|Nebula coffee|GOA:5
|
||
voidwave-playlist|Voidwave playlist|GOA:8
|
||
comet-cap|Comet cap|GOA:15
|
||
shuttle-pass|Shuttle day pass|GOA:25
|
||
beacon-badge|Beacon badge|GOA:3
|
||
relay-pin|Relay pin|GOA:4
|
||
eclipse-shades|Eclipse shades|GOA:12
|
||
star-chart|Star chart print|GOA:7
|
||
rainbow-pill|Rainbow pill (sample/joke)|GOA:4.2
|
||
blue-or-red-pill|Blue or red pill (sample/joke)|GOA:1.5}"
|
||
: "${E2E_SHOP_PICK_N:=2}"
|
||
# Public templates live on goa-shop (landing shop-pay.js); not the e2e default demo instance.
|
||
: "${E2E_SHOP_INSTANCE:=goa-shop}"
|
||
|
||
# Settlement payto shown on landing shop popup (static)
|
||
: "${E2E_SHOP_PAYTO:=payto://x-taler-bank/bank.hacktivism.ch/goa-shop?receiver-name=GOA%20Shop}"
|
||
|
||
# Pay one shop product the way the public landing does:
|
||
# POST /instances/{inst}/templates/{id} body {} → order_id+token → taler_pay_uri
|
||
# Also checks pay-template URI shape (what qrencode PNGs encode).
|
||
# Usage: e2e_one_pay_public_template PRODUCT_ID PRODUCT_NAME AMOUNT
|
||
e2e_one_pay_public_template() {
|
||
local pid="$1"
|
||
local pname="${2:-$1}"
|
||
local pamt="$3"
|
||
local tag
|
||
tag=$(printf 'shop_%s' "$pid" | tr -c 'A-Za-z0-9._-' '_')
|
||
section "e2e · shop product · $pname ($pid) · $pamt"
|
||
e2e_over && { warn "shop $pname" "budget exhausted — skip"; return 1; }
|
||
|
||
local MH
|
||
MH=$(python3 -c 'from urllib.parse import urlparse; print(urlparse("'"$MERCHANT_PUBLIC"'").hostname or "taler.hacktivism.ch")' 2>/dev/null || echo "taler.hacktivism.ch")
|
||
local TPL_URI="taler://pay-template/${MH}/instances/${INST}/${pid}"
|
||
local TPL_HTTPS="${MERCHANT_PUBLIC}/instances/${INST}/templates/$(python3 -c 'import urllib.parse,sys; print(urllib.parse.quote(sys.argv[1], safe=""))' "$pid")"
|
||
info "shop $pname" "pay-template URI $TPL_URI"
|
||
info "shop $pname" "public template POST $TPL_HTTPS"
|
||
info "shop $pname" "settlement payto $E2E_SHOP_PAYTO"
|
||
|
||
# Public create (no merchant secret) — same as shop-pay.js
|
||
curl -skS -m 15 -o "$SCRATCH/tpl-$tag.json" -X POST \
|
||
-H 'Content-Type: application/json' \
|
||
-d '{}' \
|
||
"$TPL_HTTPS" 2>"$SCRATCH/tpl-$tag.err" || true
|
||
if ! grep -q order_id "$SCRATCH/tpl-$tag.json" 2>/dev/null; then
|
||
if [ "$E2E_REMOTE" != "1" ] && koopa_ssh_ok; then
|
||
koopa_ssh_run 20 \
|
||
"curl -skS -m 12 -X POST -H 'Content-Type: application/json' -d '{}' 'https://127.0.0.1:9010/instances/${INST}/templates/${pid}'" \
|
||
>"$SCRATCH/tpl-$tag.json" 2>"$SCRATCH/tpl-$tag.err" || true
|
||
fi
|
||
fi
|
||
OID=$(python3 -c 'import json,re,sys;t=open(sys.argv[1]).read()
|
||
try: print(json.loads(t).get("order_id") or "")
|
||
except Exception:
|
||
m=re.search(r"\"order_id\"\s*:\s*\"([^\"]+)\"",t); print(m.group(1) if m else "")
|
||
' "$SCRATCH/tpl-$tag.json" 2>/dev/null || true)
|
||
OTOK=$(python3 -c 'import json,re,sys;t=open(sys.argv[1]).read()
|
||
try: print(json.loads(t).get("token") or "")
|
||
except Exception:
|
||
m=re.search(r"\"token\"\s*:\s*\"([^\"]+)\"",t); print(m.group(1) if m else "")
|
||
' "$SCRATCH/tpl-$tag.json" 2>/dev/null || true)
|
||
if [ -z "$OID" ] || [ -z "$OTOK" ]; then
|
||
warn "shop $pname ($pid)" "public template POST failed — $(head -c 100 "$SCRATCH/tpl-$tag.json" 2>/dev/null | tr '\n' ' ')"
|
||
return 1
|
||
fi
|
||
ok "shop $pname" "public order $OID (template $pid)"
|
||
|
||
local statusUrl="${MERCHANT_PUBLIC}/instances/${INST}/orders/$(python3 -c 'import urllib.parse,sys; print(urllib.parse.quote(sys.argv[1], safe=""))' "$OID")?token=$(python3 -c 'import urllib.parse,sys; print(urllib.parse.quote(sys.argv[1], safe=""))' "$OTOK")"
|
||
curl -skS -m 12 -o "$SCRATCH/tpl-st-$tag.json" "$statusUrl" 2>/dev/null || true
|
||
PAYURI=$(python3 -c 'import json,sys
|
||
try:
|
||
d=json.load(open(sys.argv[1]))
|
||
print(d.get("taler_pay_uri") or "")
|
||
except Exception: print("")
|
||
' "$SCRATCH/tpl-st-$tag.json" 2>/dev/null || true)
|
||
if [ -z "$PAYURI" ]; then
|
||
PAYURI="taler://pay/${MH}/instances/${INST}/${OID}/?c=${OTOK}"
|
||
fi
|
||
PAYURI=$(printf '%s' "$PAYURI" | sed 's/:443\//\//g; s/:443?/?/g')
|
||
if ! printf '%s' "$PAYURI" | grep -q '^taler://pay/'; then
|
||
warn "shop $pname ($pid)" "bad pay URI: $PAYURI"
|
||
return 1
|
||
fi
|
||
ok "shop $pname" "taler_pay_uri $PAYURI"
|
||
|
||
if ! wcli_pay handle-uri --yes "$PAYURI" >"$SCRATCH/pay-$tag.out" 2>&1; then
|
||
warn "shop $pname ($pid)" "handle-uri failed — $(tail -c 120 "$SCRATCH/pay-$tag.out" | tr '\n' ' ')"
|
||
return 1
|
||
fi
|
||
ok "shop $pname" "wallet accepted pay URI"
|
||
|
||
local r AUTH=""
|
||
if [ -n "${MPW:-}" ]; then
|
||
AUTH="Authorization: Bearer secret-token:${MPW}"
|
||
fi
|
||
for r in 1 2 3; do
|
||
wcli_pay run-until-done >"$SCRATCH/pay-run-$tag.out" 2>&1 || true
|
||
wcli_pay transactions >"$SCRATCH/tx-$tag.out" 2>&1 || true
|
||
if [ -n "$AUTH" ]; then
|
||
curl -skS -m 8 -o "$SCRATCH/ord-paid-$tag.json" -H "$AUTH" \
|
||
"${MERCHANT_PUBLIC}/instances/${INST}/private/orders/${OID}" 2>/dev/null || true
|
||
if python3 -c 'import json,sys
|
||
d=json.load(open(sys.argv[1]))
|
||
sys.exit(0 if d.get("paid") is True or str(d.get("order_status","")).lower()=="paid" else 1)
|
||
' "$SCRATCH/ord-paid-$tag.json" 2>/dev/null; then
|
||
ok "shop $pname" "payment settled ($pamt · order $OID)"
|
||
return 0
|
||
fi
|
||
fi
|
||
if grep -qiE 'payment|paid|Payment' "$SCRATCH/tx-$tag.out" 2>/dev/null \
|
||
|| grep -qiE 'done|paid|success|Payment' "$SCRATCH/pay-$tag.out" 2>/dev/null; then
|
||
ok "shop $pname" "payment settled via wallet tx ($pamt · order $OID)"
|
||
return 0
|
||
fi
|
||
done
|
||
warn "shop $pname ($pid)" "not settled for order $OID"
|
||
return 1
|
||
}
|
||
|
||
# ---------------------------------------------------------------------------
|
||
section "e2e · ATM withdraw ladder"
|
||
# ---------------------------------------------------------------------------
|
||
WITHDRAW_OK=0
|
||
WITHDRAW_OK_N=0
|
||
WITHDRAW_LAG_N=0
|
||
WITHDRAW_FAIL_N=0
|
||
WITHDRAW_REPORT=""
|
||
for WITHDRAW_AMT in $WITHDRAW_LIST; do
|
||
e2e_over && { warn "ATM ladder" "time budget low — stopping more ATM withdraws (not a protocol error)"; break; }
|
||
set +e
|
||
e2e_one_withdraw "$WITHDRAW_AMT"
|
||
wc=$?
|
||
set -e
|
||
case "$wc" in
|
||
0)
|
||
WITHDRAW_OK=1
|
||
WITHDRAW_OK_N=$((WITHDRAW_OK_N + 1))
|
||
WITHDRAW_REPORT="${WITHDRAW_REPORT}${WITHDRAW_REPORT:+ }${WITHDRAW_AMT}=OK"
|
||
;;
|
||
2)
|
||
WITHDRAW_LAG_N=$((WITHDRAW_LAG_N + 1))
|
||
WITHDRAW_REPORT="${WITHDRAW_REPORT}${WITHDRAW_REPORT:+ }${WITHDRAW_AMT}=LAG"
|
||
;;
|
||
*)
|
||
WITHDRAW_FAIL_N=$((WITHDRAW_FAIL_N + 1))
|
||
WITHDRAW_REPORT="${WITHDRAW_REPORT}${WITHDRAW_REPORT:+ }${WITHDRAW_AMT}=FAIL"
|
||
;;
|
||
esac
|
||
done
|
||
info "ATM withdraw summary" "$WITHDRAW_REPORT (ok=$WITHDRAW_OK_N lag=$WITHDRAW_LAG_N fail=$WITHDRAW_FAIL_N)"
|
||
|
||
# Settlement catch-up: bank may have confirmed while wallet was still empty
|
||
section "e2e · wallet settlement (timing)"
|
||
if ! wait_wallet_balance 0 "${E2E_SETTLE_ROUNDS}" "${E2E_SETTLE_SLEEP}"; then
|
||
if [ "$WITHDRAW_LAG_N" -gt 0 ] || [ "$WITHDRAW_OK_N" -gt 0 ]; then
|
||
warn "settlement timing" "ATM path reached bank confirm (lag=$WITHDRAW_LAG_N ok=$WITHDRAW_OK_N) but wallet empty after wait — TIME lag, not protocol error"
|
||
fi
|
||
fi
|
||
av_now=$(wallet_avail_num)
|
||
if python3 -c "import sys; sys.exit(0 if float(sys.argv[1]) > 0 else 1)" "$av_now" 2>/dev/null; then
|
||
WITHDRAW_OK=1
|
||
if [ "$WITHDRAW_OK_N" = "0" ]; then
|
||
warn "settlement timing" "coins arrived after ATM ladder (${CUR}:${av_now}) — earlier 'FAIL' was timing lag"
|
||
WITHDRAW_REPORT="${WITHDRAW_REPORT} → late-OK avail=${CUR}:${av_now}"
|
||
fi
|
||
ok "spendable balance for payments" "${CUR}:${av_now}"
|
||
else
|
||
if [ "$WITHDRAW_OK" != "1" ]; then
|
||
warn "withdraw" "still no spendable ${CUR} after settle wait"
|
||
# diagnostic dig only — do not treat as hard stop if we can still see bank state
|
||
if [ "$E2E_REMOTE" != "1" ]; then
|
||
dig_when_no_coins || true
|
||
fi
|
||
# one last balance after dig (dig takes wall time — often enough for wirewatch)
|
||
wait_wallet_balance 0 8 3 || true
|
||
av_now=$(wallet_avail_num)
|
||
if python3 -c "import sys; sys.exit(0 if float(sys.argv[1]) > 0 else 1)" "$av_now" 2>/dev/null; then
|
||
WITHDRAW_OK=1
|
||
warn "settlement timing" "coins present after dig wait (${CUR}:${av_now}) — timing, not blocker"
|
||
else
|
||
blocker "withdraw-settle" "no spendable ${CUR} after ATM ladder + settle wait ($WITHDRAW_LIST)"
|
||
section "e2e · report"
|
||
info "WITHDRAW" "$WITHDRAW_REPORT — no coins after extended wait"
|
||
info "PAY" "SKIPPED (no spendable balance after settle wait)"
|
||
exit 1
|
||
fi
|
||
fi
|
||
fi
|
||
|
||
# ---------------------------------------------------------------------------
|
||
section "e2e · variable payments (if balance allows)"
|
||
# ---------------------------------------------------------------------------
|
||
PAY_OK=0
|
||
PAY_OK_N=0
|
||
PAY_FAIL_N=0
|
||
PAY_SKIP_N=0
|
||
PAY_REPORT=""
|
||
if [ -z "${MPW:-}" ]; then
|
||
if [ "$E2E_REMOTE" = "1" ]; then
|
||
e2e_abort_auth "merchant-order" "no merchant token — set E2E_MERCHANT_TOKEN"
|
||
fi
|
||
blocker "merchant-order" "no merchant token"
|
||
exit 1
|
||
fi
|
||
|
||
for PAY_AMT in $PAY_LIST; do
|
||
e2e_over && { warn "pay ladder" "time budget low — stopping more payments"; break; }
|
||
av_now=$(wallet_avail_num)
|
||
pay_n=$(python3 -c 'import sys; print(float(sys.argv[1].split(":",1)[-1]))' "$PAY_AMT" 2>/dev/null || echo 0)
|
||
if ! python3 -c "import sys; sys.exit(0 if float(sys.argv[1]) + 1e-12 >= float(sys.argv[2]) else 1)" "$av_now" "$pay_n" 2>/dev/null; then
|
||
warn "pay $PAY_AMT" "skip — insufficient avail ${CUR}:${av_now} (need ${pay_n})"
|
||
PAY_SKIP_N=$((PAY_SKIP_N + 1))
|
||
PAY_REPORT="${PAY_REPORT}${PAY_REPORT:+ }${PAY_AMT}=SKIP(bal)"
|
||
continue
|
||
fi
|
||
set +e
|
||
e2e_one_pay "$PAY_AMT"
|
||
pc=$?
|
||
set -e
|
||
if [ "$pc" = "0" ]; then
|
||
PAY_OK=1
|
||
PAY_OK_N=$((PAY_OK_N + 1))
|
||
PAY_REPORT="${PAY_REPORT}${PAY_REPORT:+ }${PAY_AMT}=OK"
|
||
else
|
||
PAY_FAIL_N=$((PAY_FAIL_N + 1))
|
||
PAY_REPORT="${PAY_REPORT}${PAY_REPORT:+ }${PAY_AMT}=FAIL"
|
||
fi
|
||
done
|
||
info "pay summary" "$PAY_REPORT (ok=$PAY_OK_N fail=$PAY_FAIL_N skip=$PAY_SKIP_N)"
|
||
if [ "$PAY_OK" != "1" ]; then
|
||
av_now=$(wallet_avail_num)
|
||
if python3 -c "import sys; sys.exit(0 if float(sys.argv[1]) > 0 else 1)" "$av_now" 2>/dev/null; then
|
||
warn "pay" "balance ${CUR}:${av_now} but no pay amount completed — check merchant/timing (not necessarily empty wallet)"
|
||
else
|
||
blocker "pay-settle" "no payment succeeded and no spendable balance ($PAY_LIST)"
|
||
fi
|
||
fi
|
||
|
||
# ---------------------------------------------------------------------------
|
||
section "e2e · GOA shop products (merchant landing catalog)"
|
||
# ---------------------------------------------------------------------------
|
||
# Public template POST + wallet pay — full catalog list, random pick of N products.
|
||
SHOP_OK=0
|
||
SHOP_OK_N=0
|
||
SHOP_FAIL_N=0
|
||
SHOP_SKIP_N=0
|
||
SHOP_REPORT=""
|
||
if [ "$E2E_REMOTE" = "1" ] || [ "${CUR:-}" != "GOA" ]; then
|
||
info "goa-shop" "SKIPPED (remote or non-GOA currency)"
|
||
else
|
||
# Build catalog array from E2E_SHOP_PRODUCTS (id|name|amount lines)
|
||
SHOP_CATALOG=()
|
||
while IFS= read -r line; do
|
||
[ -z "$line" ] && continue
|
||
case "$line" in \#*) continue ;; esac
|
||
SHOP_CATALOG+=("$line")
|
||
done <<EOF
|
||
$E2E_SHOP_PRODUCTS
|
||
EOF
|
||
SHOP_CAT_N=${#SHOP_CATALOG[@]}
|
||
PICK_N="${E2E_SHOP_PICK_N:-2}"
|
||
case "$PICK_N" in ''|*[!0-9]*) PICK_N=2 ;; esac
|
||
if [ "$PICK_N" -lt 1 ]; then PICK_N=1; fi
|
||
if [ "$SHOP_CAT_N" -eq 0 ]; then
|
||
warn "goa-shop" "empty E2E_SHOP_PRODUCTS catalog"
|
||
else
|
||
if [ "$PICK_N" -gt "$SHOP_CAT_N" ]; then PICK_N=$SHOP_CAT_N; fi
|
||
# Shuffle catalog, take first PICK_N (portable: python)
|
||
SHOP_PICKED=$(
|
||
printf '%s\n' "${SHOP_CATALOG[@]}" | python3 -c "
|
||
import random, sys
|
||
lines = [ln.strip() for ln in sys.stdin if ln.strip()]
|
||
n = min(int(sys.argv[1]), len(lines))
|
||
random.shuffle(lines)
|
||
print('\n'.join(lines[:n]))
|
||
" "$PICK_N"
|
||
)
|
||
info "goa-shop" "catalog ${SHOP_CAT_N} products · random pick ${PICK_N} · instance ${E2E_SHOP_INSTANCE}"
|
||
info "goa-shop payto" "$E2E_SHOP_PAYTO"
|
||
info "goa-shop pick" "$(printf '%s\n' "$SHOP_PICKED" | cut -d'|' -f1,2 | tr '\n' '; ' | sed 's/; $//')"
|
||
# e2e_one_pay_public_template uses global INST — temporarily pin shop instance
|
||
_E2E_INST_SAVE="$INST"
|
||
INST="${E2E_SHOP_INSTANCE}"
|
||
while IFS= read -r line; do
|
||
[ -z "$line" ] && continue
|
||
e2e_over && { warn "goa-shop" "time budget low — stopping product pays"; break; }
|
||
pid=$(printf '%s' "$line" | cut -d'|' -f1)
|
||
pname=$(printf '%s' "$line" | cut -d'|' -f2)
|
||
pamt=$(printf '%s' "$line" | cut -d'|' -f3)
|
||
[ -z "$pid" ] || [ -z "$pamt" ] && continue
|
||
[ -z "$pname" ] && pname="$pid"
|
||
av_now=$(wallet_avail_num)
|
||
pay_n=$(python3 -c 'import sys; print(float(sys.argv[1].split(":",1)[-1]))' "$pamt" 2>/dev/null || echo 0)
|
||
if ! python3 -c "import sys; sys.exit(0 if float(sys.argv[1]) + 1e-12 >= float(sys.argv[2]) else 1)" "$av_now" "$pay_n" 2>/dev/null; then
|
||
warn "goa-shop $pname ($pid)" "skip — insufficient avail ${CUR}:${av_now} (need ${pay_n})"
|
||
SHOP_SKIP_N=$((SHOP_SKIP_N + 1))
|
||
SHOP_REPORT="${SHOP_REPORT}${SHOP_REPORT:+ }${pname}=SKIP(bal)"
|
||
continue
|
||
fi
|
||
set +e
|
||
e2e_one_pay_public_template "$pid" "$pname" "$pamt"
|
||
pc=$?
|
||
set -e
|
||
if [ "$pc" = "0" ]; then
|
||
SHOP_OK=1
|
||
SHOP_OK_N=$((SHOP_OK_N + 1))
|
||
PAY_OK=1
|
||
PAY_OK_N=$((PAY_OK_N + 1))
|
||
SHOP_REPORT="${SHOP_REPORT}${SHOP_REPORT:+ }${pname}=OK"
|
||
else
|
||
SHOP_FAIL_N=$((SHOP_FAIL_N + 1))
|
||
SHOP_REPORT="${SHOP_REPORT}${SHOP_REPORT:+ }${pname}=FAIL"
|
||
fi
|
||
done <<EOF
|
||
$SHOP_PICKED
|
||
EOF
|
||
INST="$_E2E_INST_SAVE"
|
||
unset _E2E_INST_SAVE
|
||
info "goa-shop summary" "$SHOP_REPORT (ok=$SHOP_OK_N fail=$SHOP_FAIL_N skip=$SHOP_SKIP_N · pick $PICK_N/$SHOP_CAT_N)"
|
||
if [ "$SHOP_OK" != "1" ] && [ "$SHOP_FAIL_N" -gt 0 ]; then
|
||
warn "goa-shop" "no sample product payment succeeded ($SHOP_REPORT)"
|
||
fi
|
||
fi
|
||
fi
|
||
|
||
section "e2e · report"
|
||
info "user" "$USER"
|
||
info "ATM withdraws" "$WITHDRAW_REPORT"
|
||
info "payments" "$PAY_REPORT"
|
||
info "goa-shop" "${SHOP_REPORT:-(n/a)}"
|
||
info "final balance" "$(fmt_bal "$SCRATCH/bal-live.out" 2>/dev/null || echo "(n/a)")"
|
||
info "scratch" "$SCRATCH"
|
||
# Success if we had coins and at least one pay, OR coins + only pay skips (nothing affordable)
|
||
if [ "${#BLOCKERS[@]}" -eq 0 ]; then
|
||
if [ "$WITHDRAW_OK" = "1" ] && [ "$PAY_OK" = "1" ]; then
|
||
exit 0
|
||
fi
|
||
if [ "$WITHDRAW_OK" = "1" ] && [ "$PAY_OK_N" = "0" ] && [ "$PAY_FAIL_N" = "0" ]; then
|
||
warn "pay" "no payment tried/completed — withdraw OK"
|
||
exit 0
|
||
fi
|
||
if [ "$WITHDRAW_OK" = "1" ] && [ "$PAY_OK" != "1" ]; then
|
||
# money there, pays failed → soft exit 1 without inventing blockers if already warned
|
||
exit 1
|
||
fi
|
||
fi
|
||
exit 1
|