diff --git a/scripts/taler-bank/auto-confirm-withdrawals.sh b/scripts/taler-bank/auto-confirm-withdrawals.sh new file mode 100755 index 0000000..3b76b4c --- /dev/null +++ b/scripts/taler-bank/auto-confirm-withdrawals.sh @@ -0,0 +1,103 @@ +#!/bin/bash +# Auto-confirm bank withdrawals once the wallet has selected exchange+reserve. +# Required for regional x-taler-bank: confirm wires GOA from user → exchange. +# +# 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) — account that owns the demo withdrawals +# BANK_PASS or /root/bank-explorer-password.txt / /root/bank-${USER}-password.txt +set -euo pipefail + +BANK="${BANK_URL:-http://127.0.0.1:9012}" +USER="${BANK_USER:-explorer}" +LOOP=0 +SLEEP=5 +if [ "${1:-}" = "--loop" ]; then + LOOP=1 + SLEEP="${2:-5}" +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; } + +token() { + echo '{"scope":"readwrite"}' > /tmp/acw-tok-req.json + curl -sS -m 12 -u "${USER}:${PASS}" \ + -H 'Content-Type: application/json' \ + -d @/tmp/acw-tok-req.json \ + "${BANK}/accounts/${USER}/token" +} + +# Extract withdrawal IDs we care about: +# 1) public landing withdraw.uri +# 2) any ID passed as args after --loop secs +known_ids() { + if [ -f /var/www/bank-landing/withdraw.uri ]; then + basename "$(tr -d '\n' /dev/null || true) + [ -n "$info" ] || return 0 + python3 -c "import json,sys; d=json.loads(sys.argv[1]); sys.exit(0 if d.get('status')=='selected' else 1)" "$info" 2>/dev/null || { + st=$(python3 -c "import json,sys; print(json.loads(sys.argv[1]).get('status','?'))" "$info" 2>/dev/null || echo '?') + echo "skip $wid status=$st" + return 0 + } + echo "confirming $wid ..." + 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 $(cat /tmp/acw-conf.out 2>/dev/null | head -c 200)" + # re-check + curl -sS -m 8 "${BANK}/withdrawals/${wid}" | python3 -c "import json,sys; d=json.load(sys.stdin); print(' now', d.get('status'), d.get('amount'))" 2>/dev/null || true +} + +once() { + local tjson tok + tjson=$(token) + tok=$(python3 -c "import json,sys; print(json.loads(sys.argv[1]).get('access_token',''))" "$tjson") + if [ -z "$tok" ]; then + echo "token fail: $tjson" >&2 + return 1 + fi + local ids + ids=$(known_ids | sort -u) + if [ -z "$ids" ]; then + echo "no withdrawal ids to watch" + return 0 + fi + while read -r wid; do + [ -n "$wid" ] || continue + confirm_one "$wid" "$tok" + done <<<"$ids" +} + +if [ "$LOOP" -eq 1 ]; then + echo "auto-confirm loop every ${SLEEP}s for user=$USER" + while true; do + once || true + sleep "$SLEEP" + done +else + once +fi diff --git a/scripts/taler-bank/credit-account.sh b/scripts/taler-bank/credit-account.sh new file mode 100755 index 0000000..fd3ac15 --- /dev/null +++ b/scripts/taler-bank/credit-account.sh @@ -0,0 +1,96 @@ +#!/bin/bash +# Credit a bank user by transferring from admin (creates regional GOA via admin debit). +# Run as root on koopa host (bank on 127.0.0.1:9012). +# +# Usage: +# credit-account.sh [USERNAME] [AMOUNT] +# credit-account.sh explorer GOA:1000 +set -euo pipefail + +BANK="${BANK_URL:-http://127.0.0.1:9012}" +TO_USER="${1:-explorer}" +AMOUNT="${2:-GOA:1000}" +ADMIN_PASS="${BANK_ADMIN_PASS:-}" +if [ -z "$ADMIN_PASS" ] && [ -f /root/bank-admin-password.txt ]; then + ADMIN_PASS=$(tr -d '\n' &2; exit 1; } + +W=$(mktemp -d) +trap 'rm -rf "$W"' EXIT + +echo '{"scope":"readwrite"}' >"$W/tok.json" +curl -sS -m 15 -u "admin:${ADMIN_PASS}" \ + -H 'Content-Type: application/json' \ + -d @"$W/tok.json" \ + "${BANK}/accounts/admin/token" >"$W/tok.out" +TOKEN=$(python3 -c "import json;print(json.load(open('$W/tok.out')).get('access_token',''))") +[ -n "$TOKEN" ] || { echo "admin token failed:"; cat "$W/tok.out"; exit 1; } + +# payto for x-taler-bank regional accounts +PAYTO="payto://x-taler-bank/bank.hacktivism.ch/${TO_USER}?receiver-name=${TO_USER}&message=credit-from-admin" + +python3 - "$AMOUNT" "$PAYTO" "$W/tx.json" <<'PY' +import json, sys, os +amount, payto, out = sys.argv[1], sys.argv[2], sys.argv[3] +# ShortHashCode: Crockford base32 of 32 random bytes (52 chars) +_ALPH = "0123456789ABCDEFGHJKMNPQRSTVWXYZ" +def crockford32(data: bytes) -> str: + n = int.from_bytes(data, "big") + bits = len(data) * 8 + outc = [] + while bits > 0: + bits -= 5 + outc.append(_ALPH[(n >> bits) & 31] if bits >= 0 else _ALPH[(n << (-bits)) & 31]) + if bits <= 0: + break + # pad to full groups + s = "".join(outc) + # simpler bit stream + return None +def crock32(b: bytes) -> str: + bits = 0 + val = 0 + outc = [] + for byte in b: + val = (val << 8) | byte + bits += 8 + while bits >= 5: + bits -= 5 + outc.append(_ALPH[(val >> bits) & 31]) + if bits: + outc.append(_ALPH[(val << (5 - bits)) & 31]) + return "".join(outc) +uid = crock32(os.urandom(32)) +json.dump({"payto_uri": payto, "amount": amount, "request_uid": uid}, open(out, "w")) +print("request_uid", uid, "len", len(uid)) +PY + +echo "POST admin -> $TO_USER amount=$AMOUNT" +curl -sS -m 15 \ + -H "Authorization: Bearer ${TOKEN}" \ + -H 'Content-Type: application/json' \ + -d @"$W/tx.json" \ + "${BANK}/accounts/admin/transactions" | tee "$W/tx.out" +echo + +# show balances +for u in admin "$TO_USER"; do + curl -sS -m 10 -u "admin:${ADMIN_PASS}" \ + -H 'Content-Type: application/json' -d '{"scope":"readonly"}' \ + "${BANK}/accounts/admin/token" >"$W/rtok" 2>/dev/null || true +done + +# re-token readonly and print target balance +echo '{"scope":"readonly"}' >"$W/rtok.json" +# admin can GET any account +curl -sS -m 10 -H "Authorization: Bearer ${TOKEN}" \ + "${BANK}/accounts/${TO_USER}" | tee "$W/acc.out" +echo +python3 -c " +import json +d=json.load(open('$W/acc.out')) +b=d.get('balance') or {} +print('RESULT', '${TO_USER}', 'balance=', b.get('amount'), b.get('credit_debit_indicator')) +print('debit_threshold=', d.get('debit_threshold')) +" diff --git a/scripts/taler-bank/make-demo-withdraw-qr.sh b/scripts/taler-bank/make-demo-withdraw-qr.sh new file mode 100755 index 0000000..f7d96f0 --- /dev/null +++ b/scripts/taler-bank/make-demo-withdraw-qr.sh @@ -0,0 +1,112 @@ +#!/bin/bash +# Create a demo GOA withdrawal for user explorer and write QR assets under LANDING_DIR. +# Run on koopa (host) with bank on 127.0.0.1:9012. +set -euo pipefail + +BANK="${BANK_URL:-http://127.0.0.1:9012}" +USER="${BANK_USER:-explorer}" +PASS="${BANK_PASS:-}" +AMOUNT="${AMOUNT:-GOA:10}" +LANDING_DIR="${LANDING_DIR:-/var/www/bank-landing}" + +if [ -z "$PASS" ]; then + if [ -f /root/bank-explorer-password.txt ]; then + PASS=$(tr -d '\n' "$WORKDIR/tok.json" +curl -sS -m 15 -u "${USER}:${PASS}" \ + -H "Content-Type: application/json" \ + -d @"$WORKDIR/tok.json" \ + "${BANK}/accounts/${USER}/token" >"$WORKDIR/tok.out" + +TOKEN=$(python3 - "$WORKDIR/tok.out" <<'PY' +import json,sys +d=json.load(open(sys.argv[1])) +print(d.get("access_token","")) +PY +) +if [ -z "$TOKEN" ]; then + echo "token failed:" >&2 + cat "$WORKDIR/tok.out" >&2 + exit 1 +fi + +echo "{\"suggested_amount\":\"${AMOUNT}\"}" >"$WORKDIR/wd.json" +curl -sS -m 15 \ + -H "Authorization: Bearer ${TOKEN}" \ + -H "Content-Type: application/json" \ + -d @"$WORKDIR/wd.json" \ + "${BANK}/accounts/${USER}/withdrawals" >"$WORKDIR/wd.out" + +URI=$(python3 - "$WORKDIR/wd.out" <<'PY' +import json,sys +d=json.load(open(sys.argv[1])) +print(d.get("taler_withdraw_uri") or "") +if not d.get("taler_withdraw_uri"): + sys.stderr.write(open(sys.argv[1]).read()+"\n") + sys.exit(1) +print(d.get("withdrawal_id",""), file=sys.stderr) +PY +) + +mkdir -p "$LANDING_DIR" +printf '%s\n' "$URI" >"$LANDING_DIR/withdraw.uri" +printf '%s\n' "$AMOUNT" >"$LANDING_DIR/withdraw.amount" +date -u +%Y-%m-%dT%H:%MZ >"$LANDING_DIR/withdraw.created" +# track id for auto-confirm-withdrawals.sh +WID=$(basename "$URI") +echo "$WID" >>"$LANDING_DIR/withdraw-watch.ids" +sort -u "$LANDING_DIR/withdraw-watch.ids" -o "$LANDING_DIR/withdraw-watch.ids" +echo "watch_id=$WID (auto-confirm when wallet selects)" + +# QR as SVG (no external deps) via python qrcode if present, else pure matrix via segno/qrcode +python3 - "$URI" "$LANDING_DIR/withdraw-qr.svg" <<'PY' +import sys +uri, out = sys.argv[1], sys.argv[2] +try: + import qrcode + import qrcode.image.svg + img = qrcode.make(uri, image_factory=qrcode.image.svg.SvgPathImage) + img.save(out) + print("qrcode lib ok") +except Exception as e: + # minimal fallback: write HTML with data attribute for client-side QR + open(out, "w").write( + f'' + f'\n' + ) + open(out + ".uri", "w").write(uri) + print("fallback:", e) +PY + +# Also PNG if pillow/qrcode available +python3 - "$URI" "$LANDING_DIR/withdraw-qr.png" <<'PY' || true +import sys +uri, out = sys.argv[1], sys.argv[2] +import qrcode +img = qrcode.make(uri, box_size=8, border=2) +img.save(out) +print("png ok", out) +PY + +echo "URI=$URI" +echo "wrote under $LANDING_DIR" +ls -la "$LANDING_DIR"/withdraw* 2>/dev/null || true + +# Refresh public stats.json when landing-stats is available (in-container path) +if [ -x /usr/local/bin/landing-stats.sh ]; then + LANDING_DIR="$LANDING_DIR" /usr/local/bin/landing-stats.sh || true +elif [ -x "$(dirname "$0")/landing-stats.sh" ]; then + LANDING_DIR="$LANDING_DIR" "$(dirname "$0")/landing-stats.sh" || true +fi diff --git a/scripts/taler-bank/refresh-demo-withdraw.sh b/scripts/taler-bank/refresh-demo-withdraw.sh new file mode 100755 index 0000000..7e23454 --- /dev/null +++ b/scripts/taler-bank/refresh-demo-withdraw.sh @@ -0,0 +1,54 @@ +#!/bin/bash +# Create a fresh demo GOA withdraw for landing QR (no python — runs in bank container). +# Writes under LANDING_DIR (default /var/www/bank-landing). +# +# Inside container: +# /usr/local/bin/refresh-demo-withdraw.sh +# Host: +# podman exec taler-hacktivism-bank /usr/local/bin/refresh-demo-withdraw.sh +set -euo pipefail + +BANK="${BANK_URL:-http://127.0.0.1:9012}" +USER="${BANK_USER:-explorer}" +AMOUNT="${AMOUNT:-GOA:10}" +LANDING_DIR="${LANDING_DIR:-/var/www/bank-landing}" +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 "Set BANK_PASS or /root/bank-explorer-password.txt" >&2; exit 1; } + +BANK="${BANK%/}" +TOK=$(curl -sS -m 12 -u "${USER}:${PASS}" \ + -H 'Content-Type: application/json' \ + -d '{"scope":"readwrite","refreshable":true}' \ + "${BANK}/accounts/${USER}/token") +TOKEN=$(printf '%s' "$TOK" | sed -n 's/.*"access_token"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p') +[ -n "$TOKEN" ] || { echo "token fail: $TOK" >&2; exit 1; } + +WD=$(curl -sS -m 15 \ + -H "Authorization: Bearer ${TOKEN}" \ + -H 'Content-Type: application/json' \ + -d "{\"suggested_amount\":\"${AMOUNT}\"}" \ + "${BANK}/accounts/${USER}/withdrawals") +URI=$(printf '%s' "$WD" | sed -n 's/.*"taler_withdraw_uri"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p') +WID=$(printf '%s' "$WD" | sed -n 's/.*"withdrawal_id"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p') +[ -n "$URI" ] || { echo "no URI from: $WD" >&2; exit 1; } +[ -n "$WID" ] || WID=$(basename "$URI") + +mkdir -p "$LANDING_DIR" +printf '%s\n' "$URI" >"$LANDING_DIR/withdraw.uri" +printf '%s\n' "$AMOUNT" >"$LANDING_DIR/withdraw.amount" +date -u +%Y-%m-%dT%H:%MZ >"$LANDING_DIR/withdraw.created" +echo "$WID" >>"$LANDING_DIR/withdraw-watch.ids" +sort -u "$LANDING_DIR/withdraw-watch.ids" -o "$LANDING_DIR/withdraw-watch.ids" 2>/dev/null || true + +echo "URI=$URI" +echo "WID=$WID" + +if [ -x /usr/local/bin/landing-stats.sh ]; then + LANDING_DIR="$LANDING_DIR" /usr/local/bin/landing-stats.sh || true +fi