bank: demo withdraw/credit helpers (container; no public landing yet)

This commit is contained in:
Hernâni Marques 2026-07-02 15:29:03 +02:00
parent cd3ffadcde
commit 96f0943319
No known key found for this signature in database
4 changed files with 365 additions and 0 deletions

View file

@ -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' </var/www/bank-landing/withdraw.uri)"
fi
# optional list file of UUIDs
if [ -f /var/www/bank-landing/withdraw-watch.ids ]; then
cat /var/www/bank-landing/withdraw-watch.ids
fi
}
confirm_one() {
local wid="$1"
local tok="$2"
local info conf
info=$(curl -sS -m 10 "${BANK}/withdrawals/${wid}" 2>/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

View file

@ -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' </root/bank-admin-password.txt)
fi
[ -n "$ADMIN_PASS" ] || { echo "Need BANK_ADMIN_PASS or /root/bank-admin-password.txt" >&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'))
"

View file

@ -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' </root/bank-explorer-password.txt)
elif [ -f /tmp/bank-caddy-wire.log ]; then
PASS=$(grep -E '^explorer=' /tmp/bank-caddy-wire.log | tail -1 | cut -d= -f2-)
fi
fi
if [ -z "$PASS" ]; then
echo "Set BANK_PASS or put password in /root/bank-explorer-password.txt" >&2
exit 1
fi
WORKDIR=$(mktemp -d)
trap 'rm -rf "$WORKDIR"' EXIT
echo "{\"scope\":\"readwrite\",\"refreshable\":true}" >"$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'<?xml version="1.0"?><svg xmlns="http://www.w3.org/2000/svg" width="8" height="8">'
f'<!-- QR_FALLBACK uri={uri} --></svg>\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

View file

@ -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