ops: GOA vanilla landings stats wdGet + collect greening
This commit is contained in:
parent
0d881cd726
commit
337d253691
24 changed files with 1074 additions and 107 deletions
|
|
@ -38,24 +38,48 @@ Details + JSON schema: `configs/bank-landing/README.md`.
|
|||
|
||||
## Demo withdraw + auto-account API
|
||||
|
||||
`demo-withdraw-api.py` listens on **127.0.0.1:19096** (proxied by nginx on the landing):
|
||||
**FP reference SoT:** `$HOME/git/admin-logs/taler/francpaysan-admin-log`
|
||||
(`files/testpaysan/demo-withdraw-api.py`, `files/caddy/Caddyfile` `@wdGet`).
|
||||
GOA ports that pattern: mint `amount`+`exchange_url`, and GET Integration
|
||||
injects `suggested_exchange` / `required_exchange` (Android/iOS).
|
||||
|
||||
`demo-withdraw-api.py` listens on **127.0.0.1:19096** (in bank CTR; nginx on :9013):
|
||||
|
||||
| Path | Behaviour |
|
||||
|------|-----------|
|
||||
| `GET /demo-withdraw.json` | Mint one-shot withdraw from shared **`explorer`** pool; write `withdraw.uri` + watch ids |
|
||||
| `GET /auto-account.json` | Public `POST /accounts` with generated **`goa-account-<random>`** user + password containing **pleasechangeme**; **balance GOA:0**; return credentials once |
|
||||
| `GET /taler-integration/withdrawal-operation/*` | Proxy libeufin + inject `suggested_exchange`/`required_exchange` |
|
||||
|
||||
Install / restart:
|
||||
**Public path (Android):** Caddy `@wdGet` (GET only) → landing **:9013** → nginx → **:19096**.
|
||||
POST / other Integration stays Caddy → **:9012** (raw libeufin).
|
||||
FP stage proxies `@wdGet` straight to host `:19096`; GOA keeps 19096 CTR-only
|
||||
and routes via published 9013 instead.
|
||||
|
||||
Install / restart (host, podman → `taler-hacktivism-bank`):
|
||||
|
||||
```bash
|
||||
./install-demo-withdraw-api.sh
|
||||
# Public checks:
|
||||
curl -sS https://bank.hacktivism.ch/intro/demo-withdraw.json | head
|
||||
curl -sS https://bank.hacktivism.ch/intro/auto-account.json | head # creates a real account
|
||||
# After Caddy @wdGet + nginx Integration are live:
|
||||
WID=… # from demo-withdraw.json
|
||||
curl -sS "https://bank.hacktivism.ch/taler-integration/withdrawal-operation/$WID" \
|
||||
| python3 -c 'import sys,json; d=json.load(sys.stdin); print(d.get("suggested_exchange"), d.get("required_exchange"))'
|
||||
```
|
||||
|
||||
Requires **python3** in the bank container. Env: `BANK_URL`, `BANK_USER`/`BANK_PASS`
|
||||
(or `/root/bank-explorer-password.txt`), `AMOUNT` (default `GOA:10` for shared withdraws).
|
||||
(or `/root/bank-explorer-password.txt`), `AMOUNT` (default `GOA:10`),
|
||||
`EXCHANGE_URL` (default `https://exchange.hacktivism.ch/`).
|
||||
|
||||
**Caddy (root on koopa — you apply):** insert `@wdGet` before the bank catch-all
|
||||
`reverse_proxy 127.0.0.1:9012` in `~/koopa-caddy/Caddyfile` (SoT:
|
||||
`configs/caddy/Caddyfile` / `host/caddy/Caddyfile`; paste-ready snippet:
|
||||
`scripts/caddy/wdGet-bank-snippet.caddy`, also copied live as
|
||||
`~/koopa-caddy/wdGet-bank-snippet.caddy`), then `sudo ~/bin/caddy-apply`.
|
||||
Caddy alone is not enough — demo-api + nginx Integration must be installed
|
||||
via `./install-demo-withdraw-api.sh` first.
|
||||
|
||||
### Auto-confirm (explorer only)
|
||||
|
||||
|
|
|
|||
|
|
@ -34,6 +34,10 @@ BANK = os.environ.get("BANK_URL", "http://127.0.0.1:9012").rstrip("/")
|
|||
BANK_PUBLIC = os.environ.get("BANK_PUBLIC", "https://bank.hacktivism.ch").rstrip("/")
|
||||
USER = os.environ.get("BANK_USER", "explorer")
|
||||
AMOUNT = os.environ.get("AMOUNT", "GOA:10")
|
||||
EXCHANGE = (
|
||||
os.environ.get("EXCHANGE_URL", "https://exchange.hacktivism.ch/").rstrip("/")
|
||||
+ "/"
|
||||
)
|
||||
LANDING = Path(os.environ.get("LANDING_DIR", "/var/www/bank-landing"))
|
||||
LISTEN = ("127.0.0.1", int(os.environ.get("DEMO_WITHDRAW_PORT", "19096")))
|
||||
|
||||
|
|
@ -112,7 +116,7 @@ def http_json(method: str, url: str, body=None, headers=None, auth=None):
|
|||
return e.code, {"raw": raw[:500]}
|
||||
|
||||
|
||||
def mint_withdraw() -> dict:
|
||||
def mint_withdraw(amount: str | None = None) -> dict:
|
||||
pw = load_pass()
|
||||
code, tok = http_json(
|
||||
"POST",
|
||||
|
|
@ -123,10 +127,14 @@ def mint_withdraw() -> dict:
|
|||
if code != 200 or not tok.get("access_token"):
|
||||
raise RuntimeError(f"token failed HTTP {code}: {tok}")
|
||||
access = tok["access_token"]
|
||||
amt = amount or AMOUNT
|
||||
if ":" not in str(amt):
|
||||
amt = f"GOA:{amt}"
|
||||
# FP pattern: amount + exchange_url (not suggested_amount alone)
|
||||
code, wd = http_json(
|
||||
"POST",
|
||||
f"{BANK}/accounts/{USER}/withdrawals",
|
||||
{"suggested_amount": AMOUNT},
|
||||
{"amount": amt, "exchange_url": EXCHANGE},
|
||||
headers={"Authorization": f"Bearer {access}"},
|
||||
)
|
||||
if code not in (200, 201):
|
||||
|
|
@ -142,7 +150,7 @@ def mint_withdraw() -> dict:
|
|||
uri = normalize_taler_withdraw_uri(str(uri).strip())
|
||||
LANDING.mkdir(parents=True, exist_ok=True)
|
||||
(LANDING / "withdraw.uri").write_text(uri + "\n")
|
||||
(LANDING / "withdraw.amount").write_text(AMOUNT + "\n")
|
||||
(LANDING / "withdraw.amount").write_text(amt + "\n")
|
||||
(LANDING / "withdraw.created").write_text(
|
||||
time.strftime("%Y-%m-%dT%H:%MZ", time.gmtime()) + "\n"
|
||||
)
|
||||
|
|
@ -151,7 +159,8 @@ def mint_withdraw() -> dict:
|
|||
"ok": True,
|
||||
"taler_withdraw_uri": uri,
|
||||
"withdrawal_id": wid,
|
||||
"amount": AMOUNT,
|
||||
"amount": amt,
|
||||
"exchange_url": EXCHANGE,
|
||||
"pool_account": USER,
|
||||
"taler_integration_base": f"{BANK_PUBLIC}/taler-integration/",
|
||||
"hint": "Open in GNU Taler Wallet (iOS/Android/desktop). No bank registration.",
|
||||
|
|
@ -301,15 +310,54 @@ class Handler(BaseHTTPRequestHandler):
|
|||
self._cors()
|
||||
self.end_headers()
|
||||
|
||||
def do_GET(self):
|
||||
path = self.path.split("?", 1)[0]
|
||||
def _proxy_wd(self, parsed):
|
||||
"""GET withdrawal-operation: inject suggested/required exchange (Android/iOS)."""
|
||||
q = ("?" + parsed.query) if parsed.query else ""
|
||||
url = f"{BANK}{parsed.path}{q}"
|
||||
ctx = ssl.create_default_context()
|
||||
req = urllib.request.Request(url, method="GET")
|
||||
try:
|
||||
with urllib.request.urlopen(req, context=ctx, timeout=70) as r:
|
||||
raw = r.read()
|
||||
status = r.status
|
||||
except urllib.error.HTTPError as e:
|
||||
raw = e.read()
|
||||
status = e.code
|
||||
except Exception as e:
|
||||
raw = json.dumps({"ok": False, "error": str(e)}).encode()
|
||||
status = 502
|
||||
try:
|
||||
body = json.loads(raw.decode() or "{}")
|
||||
if isinstance(body, dict):
|
||||
body.setdefault("suggested_exchange", EXCHANGE)
|
||||
body.setdefault("required_exchange", EXCHANGE)
|
||||
raw = json.dumps(body).encode()
|
||||
except Exception:
|
||||
pass
|
||||
self.send_response(status)
|
||||
self._cors()
|
||||
self.send_header("Content-Type", "application/json")
|
||||
self.send_header("Content-Length", str(len(raw)))
|
||||
self.end_headers()
|
||||
self.wfile.write(raw)
|
||||
|
||||
def do_GET(self):
|
||||
from urllib.parse import parse_qs, urlparse
|
||||
|
||||
parsed = urlparse(self.path)
|
||||
path = parsed.path
|
||||
qs = parse_qs(parsed.query)
|
||||
try:
|
||||
if path.startswith("/taler-integration/withdrawal-operation/"):
|
||||
self._proxy_wd(parsed)
|
||||
return
|
||||
if path in (
|
||||
"/",
|
||||
"/demo-withdraw.json",
|
||||
"/intro/demo-withdraw.json",
|
||||
):
|
||||
body = mint_withdraw()
|
||||
amt = (qs.get("amount") or qs.get("n") or [None])[0]
|
||||
body = mint_withdraw(amt)
|
||||
elif path in (
|
||||
"/auto-account.json",
|
||||
"/intro/auto-account.json",
|
||||
|
|
|
|||
|
|
@ -14,61 +14,86 @@ podman exec -u root "$CTR" chmod 755 \
|
|||
/usr/local/bin/auto-confirm-withdrawals.sh \
|
||||
/usr/local/bin/refresh-demo-withdraw.sh
|
||||
|
||||
# nginx: proxy demo-withdraw.json
|
||||
NGX=/etc/nginx/sites-available/bank-landing
|
||||
# nginx: ensure demo-withdraw / auto-account / Integration → :19096
|
||||
# FP pattern: Caddy @wdGet GET → :9013 → this location → demo-api
|
||||
# (single in-CTR python; no nested podman)
|
||||
podman exec -u root "$CTR" bash -lc '
|
||||
set -e
|
||||
f=/etc/nginx/sites-available/bank-landing
|
||||
if ! grep -q demo-withdraw.json "$f"; then
|
||||
# insert before location /intro/
|
||||
python3 - <<PY
|
||||
python3 - <<PY
|
||||
from pathlib import Path
|
||||
p=Path("/etc/nginx/sites-available/bank-landing")
|
||||
t=p.read_text()
|
||||
block=""" location = /intro/demo-withdraw.json {
|
||||
p = Path("/etc/nginx/sites-available/bank-landing")
|
||||
t = p.read_text()
|
||||
changed = False
|
||||
anchor = " location /intro/ {"
|
||||
blocks = []
|
||||
if "demo-withdraw.json" not in t:
|
||||
blocks.append(""" location = /intro/demo-withdraw.json {
|
||||
proxy_pass http://127.0.0.1:19096/demo-withdraw.json;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Host \$host;
|
||||
add_header Cache-Control "no-store" always;
|
||||
add_header Access-Control-Allow-Origin * always;
|
||||
}
|
||||
"""
|
||||
if "demo-withdraw.json" not in t:
|
||||
t=t.replace(" location /intro/ {", block+" location /intro/ {", 1)
|
||||
p.write_text(t)
|
||||
print("nginx location added")
|
||||
""")
|
||||
print("nginx demo-withdraw location added")
|
||||
else:
|
||||
print("nginx already has demo-withdraw")
|
||||
if "auto-account.json" not in t:
|
||||
t=p.read_text()
|
||||
block2=""" location = /intro/auto-account.json {
|
||||
blocks.append(""" location = /intro/auto-account.json {
|
||||
proxy_pass http://127.0.0.1:19096/auto-account.json;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Host \$host;
|
||||
add_header Cache-Control "no-store" always;
|
||||
add_header Access-Control-Allow-Origin * always;
|
||||
}
|
||||
"""
|
||||
t=t.replace(" location /intro/ {", block2+" location /intro/ {", 1)
|
||||
p.write_text(t)
|
||||
""")
|
||||
print("nginx auto-account location added")
|
||||
else:
|
||||
print("nginx already has auto-account")
|
||||
if "taler-integration/withdrawal-operation" not in t:
|
||||
blocks.append(""" # FP pattern: GET Integration via demo-api (suggested/required_exchange)
|
||||
# Caddy @wdGet → :9013 → here → :19096; POST/other stays on :9012
|
||||
location /taler-integration/withdrawal-operation/ {
|
||||
proxy_pass http://127.0.0.1:19096;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Host \$host;
|
||||
proxy_read_timeout 70s;
|
||||
add_header Cache-Control "no-store" always;
|
||||
add_header Access-Control-Allow-Origin * always;
|
||||
}
|
||||
""")
|
||||
print("nginx Integration withdrawal-operation location added")
|
||||
else:
|
||||
print("nginx already has Integration withdrawal-operation")
|
||||
if blocks:
|
||||
if anchor not in t:
|
||||
raise SystemExit("nginx anchor location /intro/ not found")
|
||||
t = t.replace(anchor, "".join(blocks) + anchor, 1)
|
||||
p.write_text(t)
|
||||
changed = True
|
||||
if not changed:
|
||||
print("nginx locations already complete")
|
||||
PY
|
||||
nginx -t && nginx -s reload || true
|
||||
else
|
||||
echo "nginx already configured"
|
||||
fi
|
||||
nginx -t && nginx -s reload || true
|
||||
'
|
||||
|
||||
# start/restart API + single auto-confirm loop (flock inside script)
|
||||
podman exec -u root "$CTR" bash -lc '
|
||||
# stop demo-withdraw by pid (avoid pkill -f self-match)
|
||||
# stop demo-withdraw by pid (avoid pkill -f self-match); wait for :19096
|
||||
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
|
||||
sleep 1
|
||||
ps -eo pid=,args= | awk "/demo-withdraw-api\\.py/ && !/awk/ {print \$1}" | while read p; do kill -9 \$p 2>/dev/null || true; done
|
||||
sleep 0.5
|
||||
nohup python3 /usr/local/bin/demo-withdraw-api.py \
|
||||
>>/var/log/demo-withdraw-api.log 2>&1 </dev/null &
|
||||
echo "api pid $!"
|
||||
# fail loud if bind lost to a leftover listener
|
||||
sleep 0.5
|
||||
ps -eo pid=,args= | awk "/demo-withdraw-api\\.py/ && !/awk/" || {
|
||||
echo "ERROR: demo-withdraw-api failed to stay up (port busy?)" >&2
|
||||
tail -20 /var/log/demo-withdraw-api.log >&2 || true
|
||||
exit 1
|
||||
}
|
||||
# stop auto-confirm by pid
|
||||
ps -eo pid=,args= | awk "/auto-confirm-withdrawals\\.sh --loop/ && !/awk/ {print \$1}" | while read p; do kill \$p 2>/dev/null || true; done
|
||||
sleep 0.5
|
||||
|
|
|
|||
|
|
@ -203,8 +203,11 @@ if [ -n "$ADMIN_TOKEN" ]; then
|
|||
u=$(printf '%s' "$line" | sed -n 's/.*"username"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p')
|
||||
[ -n "$u" ] && echo "$u"
|
||||
done >"$WORKDIR/usernames.txt" || true
|
||||
ACCOUNTS_N=$(grep -cve '^\s*$' "$WORKDIR/usernames.txt" 2>/dev/null || echo 0)
|
||||
ACCOUNTS_USERS=$(grep -Eve '^(admin|exchange)$' "$WORKDIR/usernames.txt" 2>/dev/null | grep -cve '^\s*$' || echo 0)
|
||||
# grep -c exits 1 on zero matches; do not `|| echo 0` (would print 0\n0 and break JSON)
|
||||
ACCOUNTS_N=$(grep -cve '^\s*$' "$WORKDIR/usernames.txt" 2>/dev/null || true)
|
||||
ACCOUNTS_N=${ACCOUNTS_N:-0}
|
||||
ACCOUNTS_USERS=$(grep -Eve '^(admin|exchange)$' "$WORKDIR/usernames.txt" 2>/dev/null | grep -cve '^\s*$' || true)
|
||||
ACCOUNTS_USERS=${ACCOUNTS_USERS:-0}
|
||||
else
|
||||
echo "$BANK_USER" >"$WORKDIR/usernames.txt"
|
||||
ACCOUNTS_N=1
|
||||
|
|
@ -323,9 +326,11 @@ sort -t$'\t' -k2,2nr "$WORKDIR/all-in.tsv" -o "$WORKDIR/all-in-sorted.tsv" 2>/de
|
|||
|| cp "$WORKDIR/all-in.tsv" "$WORKDIR/all-in-sorted.tsv"
|
||||
|
||||
# Unique reserves = individual wallet withdraws (each wallet reserve_pub)
|
||||
WALLETS_N=$(awk -F'\t' '$5!=""{print $5}' "$WORKDIR/all-wd-sorted.tsv" | sort -u | grep -cve '^\s*$' || echo 0)
|
||||
WALLETS_N=$(awk -F'\t' '$5!=""{print $5}' "$WORKDIR/all-wd-sorted.tsv" | sort -u | grep -cve '^\s*$' || true)
|
||||
WALLETS_N=${WALLETS_N:-0}
|
||||
# Accounts that funded at least one withdraw
|
||||
ACCOUNTS_WITH_WD=$(awk -F'\t' '$4!=""{print $4}' "$WORKDIR/all-wd-sorted.tsv" | sort -u | grep -cve '^\s*$' || echo 0)
|
||||
ACCOUNTS_WITH_WD=$(awk -F'\t' '$4!=""{print $4}' "$WORKDIR/all-wd-sorted.tsv" | sort -u | grep -cve '^\s*$' || true)
|
||||
ACCOUNTS_WITH_WD=${ACCOUNTS_WITH_WD:-0}
|
||||
|
||||
# Aggregates
|
||||
NOW=$(now_unix)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue