docs: new root as prior history lost (orphan + GC); ~215 commits not recoverable

This commit is contained in:
Hernâni Marques 2026-07-13 10:12:00 +02:00
commit 96961f23f5
268 changed files with 24161 additions and 0 deletions

View file

@ -0,0 +1,71 @@
# taler-bank scripts
Container: **`taler-hacktivism-bank`** (libeufin-bank, GOA, **no IBAN**).
| File | Container path | User |
|------|----------------|------|
| `start_base_services_for_taler_bank.sh` | `/root/` | **root** |
| `start_bank.sh` | `/usr/local/bin/` | **libeufin-bank** |
| `check_bank-health.sh` | `/usr/local/bin/` | libeufin-bank / any |
| `landing-stats.sh` | `/usr/local/bin/` | root (in container) — writes `/var/www/bank-landing/stats.json` |
| `landing-stats-install.sh` | host only | root/podman — copies + runs + optional cron |
| `demo-withdraw-api.py` | `/usr/local/bin/` | root — loopback **:19096** |
| `install-demo-withdraw-api.sh` | host only | installs API + nginx + auto-confirm |
| `auto-confirm-withdrawals.sh` | `/usr/local/bin/` | root — **explorer-only** confirm loop |
| `refresh-demo-withdraw.sh` | `/usr/local/bin/` | refresh static `withdraw.uri` |
| `credit-account.sh` | host/ops | admin → user credit |
## Usage
```bash
# root in container
./start_base_services_for_taler_bank.sh
# then as libeufin-bank in /usr/local/bin:
./start_bank.sh --restart
```
## Landing stats (inside container)
```bash
# on koopa host — copy + run (writes /var/www/bank-landing/stats.json)
./landing-stats-install.sh
./landing-stats-install.sh --cron # every minute inside container (* * * * *)
./landing-stats-install.sh --run-only
```
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):
| 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 |
Install / restart:
```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
```
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).
### Auto-confirm (explorer only)
```bash
# loop inside container — refuses non-explorer unless ALLOW_NON_EXPLORER=1
auto-confirm-withdrawals.sh --loop 4
```
Only confirms withdrawals owned by **`explorer`** when status is `selected`
(community demo path). Does not confirm arbitrary customer withdraws.
## Config
See `configs/taler-hacktivism-bank/` and `configs/bank-landing/`.

View file

@ -0,0 +1,143 @@
#!/bin/bash
# Auto-confirm bank withdrawals for the community demo pool ONLY.
#
# Only account: explorer (override only if you really mean another pool user
# via BANK_USER, but still confirms with that user's token only — never
# confirms other customers' withdrawals).
#
# Run once: auto-confirm-withdrawals.sh
# Loop: auto-confirm-withdrawals.sh --loop [SECS]
#
# Env:
# BANK_URL (default http://127.0.0.1:9012)
# BANK_USER (default explorer) — must be the pool account
# BANK_PASS or /root/bank-explorer-password.txt
# LANDING_DIR (default /var/www/bank-landing)
# ALLOW_NON_EXPLORER=1 — allow BANK_USER other than explorer (off by default)
set -euo pipefail
BANK="${BANK_URL:-http://127.0.0.1:9012}"
BANK="${BANK%/}"
USER="${BANK_USER:-explorer}"
LANDING_DIR="${LANDING_DIR:-/var/www/bank-landing}"
LOOP=0
SLEEP=5
if [ "${1:-}" = "--loop" ]; then
LOOP=1
SLEEP="${2:-5}"
fi
# Safety: only the shared community account unless explicitly overridden
if [ "$USER" != "explorer" ] && [ "${ALLOW_NON_EXPLORER:-0}" != "1" ]; then
echo "refusing BANK_USER=$USER — auto-confirm is for explorer only (set ALLOW_NON_EXPLORER=1 to override)" >&2
exit 1
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; }
# JSON field extract without python (bank container may lack python3)
json_str() {
# json_str FIELD < json-text-or-file
local field="$1"
local data
if [ -f "${2:-}" ]; then data=$(cat "$2"); else data=$(cat); fi
printf '%s' "$data" | sed -n "s/.*\"${field}\"[[:space:]]*:[[:space:]]*\"\\([^\"]*\\)\".*/\\1/p" | head -1
}
token() {
curl -sS -m 12 -u "${USER}:${PASS}" \
-H 'Content-Type: application/json' \
-d '{"scope":"readwrite","refreshable":true}' \
"${BANK}/accounts/${USER}/token"
}
# IDs created for the community pool (landing + watch list only)
known_ids() {
if [ -f "${LANDING_DIR}/withdraw.uri" ]; then
basename "$(tr -d '\n' <"${LANDING_DIR}/withdraw.uri")"
fi
if [ -f "${LANDING_DIR}/withdraw-watch.ids" ]; then
# strip empty / comments
grep -E '^[0-9a-fA-F-]{36}$' "${LANDING_DIR}/withdraw-watch.ids" || true
fi
}
confirm_one() {
local wid="$1"
local tok="$2"
local info st uname conf
# Public status — must belong to explorer (pool), not another customer
info=$(curl -sS -m 10 "${BANK}/withdrawals/${wid}" 2>/dev/null || true)
[ -n "$info" ] || return 0
st=$(printf '%s' "$info" | sed -n 's/.*"status"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' | head -1)
uname=$(printf '%s' "$info" | sed -n 's/.*"username"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' | head -1)
# Only confirm withdrawals owned by the pool account
if [ -n "$uname" ] && [ "$uname" != "$USER" ]; then
echo "skip $wid owner=$uname (only confirm $USER)"
return 0
fi
# If API omits username, still only confirm via explorer token (cannot confirm others)
if [ -z "$uname" ]; then
# require sender_wire / payto to mention explorer when present
case "$info" in
*explorer*) ;;
*)
# still try only if status selected — confirm endpoint is under explorer account
;;
esac
fi
if [ "$st" != "selected" ]; then
echo "skip $wid status=${st:-?} owner=${uname:-?}"
return 0
fi
echo "confirming $wid as $USER (community pool) ..."
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 $(head -c 200 /tmp/acw-conf.out 2>/dev/null || true)"
curl -sS -m 8 "${BANK}/withdrawals/${wid}" 2>/dev/null \
| sed -n 's/.*"status"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/ now status=\1/p' | head -1 || true
}
once() {
local tjson tok ids
tjson=$(token)
tok=$(printf '%s' "$tjson" | sed -n 's/.*"access_token"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' | head -1)
if [ -z "$tok" ]; then
echo "token fail for $USER: $tjson" >&2
return 1
fi
ids=$(known_ids | sort -u)
if [ -z "$ids" ]; then
echo "no withdrawal ids to watch (landing withdraw.uri / withdraw-watch.ids)"
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 pool user=$USER only"
while true; do
once || true
sleep "$SLEEP"
done
else
once
fi

View file

@ -0,0 +1,48 @@
#!/bin/bash
# Health check for manual libeufin-bank (same style as merchant/exchange health).
# Container: taler-hacktivism-bank
CONF=/etc/libeufin/libeufin-bank.conf
PORT=$(grep -E '^\s*PORT\s*=' /etc/libeufin/bank-overrides.conf 2>/dev/null | tail -1 | awk -F= '{gsub(/ /,"",$2); print $2}')
PORT=${PORT:-9012}
green() { echo -e "\e[32m$1\e[0m"; }
red() { echo -e "\e[31m$1\e[0m"; }
yellow() { echo -e "\e[33m$1\e[0m"; }
fail=0
ok() { green "[OK] $1"; }
bad() { red "[FAIL] $1"; fail=1; }
echo "=== Taler Bank (libeufin-bank) Health Check ==="
if pgrep -f 'libeufin-bank serve|MainKt serve|tech.libeufin.bank.MainKt' >/dev/null 2>&1; then
ok "process libeufin-bank serve"
else
bad "process libeufin-bank serve is NOT running"
fi
if curl -sf -m 3 "http://127.0.0.1:${PORT}/config" >/dev/null 2>&1; then
ok "HTTP /config on 127.0.0.1:${PORT}"
else
bad "no HTTP response on 127.0.0.1:${PORT}/config"
fi
if curl -sf -m 3 "http://127.0.0.1:${PORT}/taler-integration/config" >/dev/null 2>&1; then
ok "HTTP /taler-integration/config on 127.0.0.1:${PORT}"
else
yellow "[WARN] no HTTP response on /taler-integration/config"
fi
if pg_isready >/dev/null 2>&1; then
ok "postgresql accepting connections"
else
yellow "[WARN] postgresql not ready (or pg_isready missing)"
fi
if [ "$fail" -eq 0 ]; then
green "=== ALL CRITICAL CHECKS PASSED ==="
exit 0
fi
red "=== SOME CHECKS FAILED ==="
exit 1

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,316 @@
#!/usr/bin/env python3
"""
HTTP helper for bank landing:
- GET /demo-withdraw.json mint shared-pool (explorer) demo withdraw
- GET /auto-account.json create a personal bank account (balance 0)
and return one-time credentials for the user to copy
Listens on 127.0.0.1:19096 (only inside bank container / localhost).
Nginx proxies /intro/*.json this service.
Env:
BANK_URL default http://127.0.0.1:9012
BANK_USER default explorer
BANK_PASS or /root/bank-explorer-password.txt
AMOUNT default GOA:10
LANDING_DIR default /var/www/bank-landing
"""
from __future__ import annotations
import json
import os
import re
import secrets
import ssl
import string
import time
import urllib.error
import urllib.request
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
from pathlib import Path
BANK = os.environ.get("BANK_URL", "http://127.0.0.1:9012").rstrip("/")
# Public HTTPS base (Caddy) — absolute webui / login links
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")
LANDING = Path(os.environ.get("LANDING_DIR", "/var/www/bank-landing"))
LISTEN = ("127.0.0.1", int(os.environ.get("DEMO_WITHDRAW_PORT", "19096")))
def public_webui_url() -> str:
return f"{BANK_PUBLIC}/webui/"
def load_pass() -> str:
p = os.environ.get("BANK_PASS", "").strip()
if p:
return p
for f in (
Path(f"/root/bank-{USER}-password.txt"),
Path("/root/bank-explorer-password.txt"),
):
if f.is_file():
return f.read_text().strip()
raise RuntimeError("no BANK_PASS / explorer password file")
def http_json(method: str, url: str, body=None, headers=None, auth=None):
data = None if body is None else json.dumps(body).encode()
h = dict(headers or {})
if body is not None:
h["Content-Type"] = "application/json"
if auth:
import base64
token = base64.b64encode(f"{auth[0]}:{auth[1]}".encode()).decode()
h["Authorization"] = f"Basic {token}"
req = urllib.request.Request(url, data=data, method=method, headers=h)
ctx = ssl.create_default_context()
try:
with urllib.request.urlopen(req, context=ctx, timeout=20) as r:
raw = r.read().decode()
return r.status, json.loads(raw) if raw.strip() else {}
except urllib.error.HTTPError as e:
raw = e.read().decode()
try:
return e.code, json.loads(raw)
except Exception:
return e.code, {"raw": raw[:500]}
def mint_withdraw() -> dict:
pw = load_pass()
code, tok = http_json(
"POST",
f"{BANK}/accounts/{USER}/token",
{"scope": "readwrite", "refreshable": True},
auth=(USER, pw),
)
if code != 200 or not tok.get("access_token"):
raise RuntimeError(f"token failed HTTP {code}: {tok}")
access = tok["access_token"]
code, wd = http_json(
"POST",
f"{BANK}/accounts/{USER}/withdrawals",
{"suggested_amount": AMOUNT},
headers={"Authorization": f"Bearer {access}"},
)
if code not in (200, 201):
raise RuntimeError(f"withdrawal create HTTP {code}: {wd}")
uri = wd.get("taler_withdraw_uri") or ""
wid = wd.get("withdrawal_id") or ""
if not uri:
raise RuntimeError(f"no taler_withdraw_uri: {wd}")
if not wid:
wid = uri.rstrip("/").split("/")[-1]
# Keep host:port from libeufin (e.g. bank.hacktivism.ch:443). Stripping :443
# breaks taler-integration withdraw links / main landing QR on HTTPS banks.
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.created").write_text(
time.strftime("%Y-%m-%dT%H:%MZ", time.gmtime()) + "\n"
)
watch_withdrawal(wid)
return {
"ok": True,
"taler_withdraw_uri": uri,
"withdrawal_id": wid,
"amount": AMOUNT,
"pool_account": USER,
"taler_integration_base": f"{BANK_PUBLIC}/taler-integration/",
"hint": "Open in GNU Taler Wallet (iOS/Android/desktop). No bank registration.",
"created": time.strftime("%Y-%m-%dT%H:%MZ", time.gmtime()),
}
def watch_withdrawal(wid: str) -> None:
"""Queue withdrawal_id for auto-confirm loop (shared pool + personal)."""
if not wid:
return
LANDING.mkdir(parents=True, exist_ok=True)
watch = LANDING / "withdraw-watch.ids"
ids = set()
if watch.is_file():
ids = {ln.strip() for ln in watch.read_text().splitlines() if ln.strip()}
ids.add(str(wid).strip())
watch.write_text("\n".join(sorted(ids)) + "\n")
# Funny stems for usernames (bank-safe [a-z0-9-]).
# Source: hand-curated in this file only — not scraped from the web.
# Keep culture-neutral: light space / physics wordplay, no animals, foods,
# body parts, religion, politics, or slang that can offend.
_FUNNY_STEMS = (
"nebula-nudge",
"orbit-echo",
"voidwave-vibe",
"comet-crumb",
"quark-pulse",
"plasma-spark",
"astro-glint",
"lunar-loop",
"warp-ripple",
"photon-bloom",
"galaxy-drift",
"rocket-ribbon",
"satellite-swirl",
"meteor-mint",
"stardust-swirl",
"hyperdrive-hum",
"cosmic-coral",
"space-spark",
"nova-nibble",
"aurora-arc",
"solar-swish",
"pulsar-pop",
"comet-cloud",
"orbit-opal",
"zenith-zip",
"eclipse-echo",
"horizon-hum",
"starlight-step",
)
def _rand_username_and_name() -> tuple[str, str]:
# Shown as goa-account-<funnypiece>-<tag> (e.g. goa-account-space-potato-k3m9x)
stem = secrets.choice(_FUNNY_STEMS)
alphabet = string.ascii_lowercase + string.digits
tag = "".join(secrets.choice(alphabet) for _ in range(5))
username = f"goa-account-{stem}-{tag}"
name = username
return username, name
def _rand_password() -> str:
# Embed "pleasechangeme" with random material before and after.
alphabet = "ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz23456789"
prefix = "".join(secrets.choice(alphabet) for _ in range(4))
suffix = "".join(secrets.choice(alphabet) for _ in range(6))
return prefix + "pleasechangeme" + suffix
def create_personal_account() -> dict:
"""Public registration: auto username/password, balance starts at 0."""
username, name = _rand_username_and_name()
password = _rand_password()
code, body = http_json(
"POST",
f"{BANK}/accounts",
{
"username": username,
"password": password,
"name": name,
},
)
if code not in (200, 201, 204):
# retry once on conflict
if code in (409, 400):
username, name = _rand_username_and_name()
code, body = http_json(
"POST",
f"{BANK}/accounts",
{
"username": username,
"password": password,
"name": name,
},
)
if code not in (200, 201, 204):
raise RuntimeError(f"register failed HTTP {code}: {body}")
webui = public_webui_url()
# Same shared-pool withdraw as step 2 (explorer + auto-confirm)
wd = mint_withdraw()
withdraw_uri = wd["taler_withdraw_uri"]
return {
"ok": True,
"created_for_you": True,
"username": username,
"password": password,
"name": name,
"display_name": name,
"balance": "GOA:0",
"balance_note": "Starts at zero — not the shared community pool.",
"taler_withdraw_uri": withdraw_uri,
"withdrawal_id": wd["withdrawal_id"],
"withdraw_amount": wd.get("amount") or AMOUNT,
"pool_account": wd.get("pool_account") or USER,
"qr_payload": withdraw_uri,
"webui": webui,
"account_url": webui,
"login_url": webui,
"hint": (
f"Login at {webui} with username {username} and the password shown "
"(not stored for recovery). Wallet QR is taler://withdraw/… from the "
f"shared pool ({USER}), same as step 2."
),
"created": time.strftime("%Y-%m-%dT%H:%MZ", time.gmtime()),
"created_human": time.strftime("%Y-%m-%d %H:%M %Z", time.localtime()),
}
class Handler(BaseHTTPRequestHandler):
def log_message(self, fmt, *args):
sys_stderr = __import__("sys").stderr
sys_stderr.write("%s - %s\n" % (self.address_string(), fmt % args))
def _cors(self):
self.send_header("Access-Control-Allow-Origin", "*")
self.send_header("Access-Control-Allow-Methods", "GET, OPTIONS")
self.send_header("Cache-Control", "no-store")
def do_OPTIONS(self):
self.send_response(204)
self._cors()
self.end_headers()
def do_GET(self):
path = self.path.split("?", 1)[0]
try:
if path in (
"/",
"/demo-withdraw.json",
"/intro/demo-withdraw.json",
):
body = mint_withdraw()
elif path in (
"/auto-account.json",
"/intro/auto-account.json",
):
body = create_personal_account()
else:
self.send_response(404)
self._cors()
self.send_header("Content-Type", "application/json")
self.end_headers()
self.wfile.write(b'{"ok":false,"error":"not found"}')
return
raw = json.dumps(body).encode()
self.send_response(200)
self._cors()
self.send_header("Content-Type", "application/json")
self.send_header("Content-Length", str(len(raw)))
self.end_headers()
self.wfile.write(raw)
except Exception as e:
raw = json.dumps({"ok": False, "error": str(e)}).encode()
self.send_response(500)
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 main():
httpd = ThreadingHTTPServer(LISTEN, Handler)
print(f"demo-withdraw-api on http://{LISTEN[0]}:{LISTEN[1]}/", flush=True)
httpd.serve_forever()
if __name__ == "__main__":
main()

View file

@ -0,0 +1,80 @@
#!/bin/bash
# Install + start demo-withdraw API + auto-confirm loop inside bank container.
# Host:
# ./scripts/taler-bank/install-demo-withdraw-api.sh
set -euo pipefail
ROOT=$(cd "$(dirname "$0")" && pwd)
CTR="${BANK_CONTAINER:-taler-hacktivism-bank}"
podman cp "$ROOT/demo-withdraw-api.py" "$CTR:/usr/local/bin/demo-withdraw-api.py"
podman cp "$ROOT/auto-confirm-withdrawals.sh" "$CTR:/usr/local/bin/auto-confirm-withdrawals.sh"
podman cp "$ROOT/refresh-demo-withdraw.sh" "$CTR:/usr/local/bin/refresh-demo-withdraw.sh"
podman exec -u root "$CTR" chmod 755 \
/usr/local/bin/demo-withdraw-api.py \
/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
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
from pathlib import Path
p=Path("/etc/nginx/sites-available/bank-landing")
t=p.read_text()
block=""" 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")
else:
print("nginx already has demo-withdraw")
if "auto-account.json" not in t:
t=p.read_text()
block2=""" 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")
PY
nginx -t && nginx -s reload || true
else
echo "nginx already configured"
fi
'
# start/restart API
podman exec -u root "$CTR" bash -lc '
pkill -f "demo-withdraw-api.py" 2>/dev/null || true
nohup python3 /usr/local/bin/demo-withdraw-api.py \
>>/var/log/demo-withdraw-api.log 2>&1 </dev/null &
echo "api pid $!"
# auto-confirm loop
pkill -f "auto-confirm-withdrawals.sh --loop" 2>/dev/null || true
nohup /usr/local/bin/auto-confirm-withdrawals.sh --loop 4 \
>>/var/log/auto-confirm-withdrawals.log 2>&1 </dev/null &
echo "auto-confirm pid $!"
sleep 1
curl -sS -m 8 http://127.0.0.1:19096/demo-withdraw.json | head -c 300; echo
'
echo "OK: demo-withdraw API + auto-confirm in $CTR"
echo "Public: https://bank.hacktivism.ch/intro/demo-withdraw.json"

View file

@ -0,0 +1,78 @@
#!/bin/bash
# Install + run landing-stats.sh *inside* the bank container, and optionally
# install a cron entry there. Documented in configs/bank-landing/README.md.
#
# Run on koopa host (root or user in podman group):
# ./landing-stats-install.sh # copy + one-shot run
# ./landing-stats-install.sh --cron # also install *minutely* cron inside container
# ./landing-stats-install.sh --run-only # only exec existing script
set -euo pipefail
DO_CRON=0
RUN_ONLY=0
# cron schedule (default: every minute)
: "${LANDING_STATS_CRON:=* * * * *}"
for a in "$@"; do
case "$a" in
--cron) DO_CRON=1 ;;
--run-only) RUN_ONLY=1 ;;
-h|--help)
sed -n '2,12p' "$0" | sed 's/^# \?//'
exit 0
;;
esac
done
ROOT=$(cd "$(dirname "$0")" && pwd)
SRC="$ROOT/landing-stats.sh"
[ -f "$SRC" ] || { echo "missing $SRC" >&2; exit 1; }
# Prefer the known names from ops docs
C=""
for name in taler-hacktivism-bank taler-bank-hacktivism; do
if podman ps --format '{{.Names}}' 2>/dev/null | grep -qx "$name"; then
C=$name
break
fi
done
if [ -z "$C" ]; then
C=$(podman ps --format '{{.Names}}' 2>/dev/null | grep -i bank | head -1 || true)
fi
[ -n "$C" ] || { echo "no bank container running" >&2; exit 1; }
echo "container=$C"
# Host password → container /root (if not already there)
if [ -f /root/bank-explorer-password.txt ]; then
podman cp /root/bank-explorer-password.txt "$C:/root/bank-explorer-password.txt" 2>/dev/null || true
fi
if [ "$RUN_ONLY" != "1" ]; then
podman exec "$C" mkdir -p /usr/local/bin /var/www/bank-landing
podman cp "$SRC" "$C:/usr/local/bin/landing-stats.sh"
podman exec "$C" chmod 755 /usr/local/bin/landing-stats.sh
echo "installed /usr/local/bin/landing-stats.sh"
fi
# Ensure landing dir exists for nginx intro
podman exec "$C" mkdir -p /var/www/bank-landing
echo "running landing-stats.sh inside $C"
podman exec \
-e LANDING_DIR=/var/www/bank-landing \
-e BANK_USER=explorer \
"$C" /usr/local/bin/landing-stats.sh
if [ "$DO_CRON" = "1" ]; then
# default: every minute (* * * * *); override with LANDING_STATS_CRON='*/5 * * * *'
podman exec -e LANDING_STATS_CRON="$LANDING_STATS_CRON" "$C" bash -c '
sched="${LANDING_STATS_CRON:-* * * * *}"
line="$sched TZ=Europe/Zurich LANDING_DIR=/var/www/bank-landing /usr/local/bin/landing-stats.sh >>/var/log/landing-stats.log 2>&1"
(crontab -l 2>/dev/null | grep -v landing-stats.sh; echo "$line") | crontab -
echo "cron installed:"; crontab -l | grep landing-stats
'
fi
echo "public check: curl -sS https://bank.hacktivism.ch/intro/stats.json | head"
# If host maps 9013 → container landing, show local file
podman exec "$C" head -c 400 /var/www/bank-landing/stats.json 2>/dev/null || true
echo

View file

@ -0,0 +1,612 @@
#!/bin/bash
# Generate public landing stats JSON for bank.hacktivism.ch intro page.
#
# *** Run INSIDE the bank container *** (taler-hacktivism-bank).
# Writes: $LANDING_DIR/stats.json → https://bank.hacktivism.ch/intro/stats.json
#
# Pure bash + curl + awk (no python). Times in Europe/Zurich (CET/CEST).
#
# Documented: configs/bank-landing/README.md
set -euo pipefail
LANDING_DIR="${LANDING_DIR:-/var/www/bank-landing}"
# Demo funding account (for recent withdraw list / flow)
BANK_USER="${BANK_USER:-explorer}"
# Admin lists all accounts + can read other accounts' txs
ADMIN_USER="${ADMIN_USER:-admin}"
# Per-account transaction window (libeufin delta). Was -100 → systematically
# undercounted credits/withdraws on active accounts (broken public stats).
TX_DELTA="${TX_DELTA:--50000}"
# Max accounts to list + scan (was 80 → missed later accounts; bank has 100+).
MAX_SCAN_ACCOUNTS="${MAX_SCAN_ACCOUNTS:-500}"
# Account-list page size for GET /accounts?delta=… (must cover all users)
ACCOUNTS_DELTA="${ACCOUNTS_DELTA:--500}"
# curl timeout per account (deeper history needs more headroom)
TX_CURL_TIMEOUT="${TX_CURL_TIMEOUT:-25}"
export TZ="${TZ:-Europe/Zurich}"
PASS="${BANK_PASS:-}"
ADMIN_PASS="${BANK_ADMIN_PASS:-}"
detect_bank() {
if [ -n "${BANK_URL:-}" ]; then
echo "${BANK_URL%/}"
return
fi
local port="" conf u code
for conf in /etc/libeufin/bank-overrides.conf /etc/libeufin/libeufin-bank.conf; do
if [ -f "$conf" ]; then
port=$(grep -E '^\s*PORT\s*=' "$conf" 2>/dev/null | tail -1 | awk -F= '{gsub(/ /,"",$2); print $2}')
[ -n "$port" ] && break
fi
done
port="${port:-9012}"
for u in "http://127.0.0.1:${port}" "http://127.0.0.1:9012" "http://127.0.0.1:8080"; do
code=$(curl -sS -m 2 -o /dev/null -w '%{http_code}' "$u/config" 2>/dev/null || echo 000)
if [ "$code" = "200" ]; then
echo "$u"
return
fi
done
echo "http://127.0.0.1:${port}"
}
BANK="$(detect_bank)"
BANK="${BANK%/}"
read_pass_file() {
local f
for f in "$@"; do
if [ -f "$f" ] && [ -r "$f" ]; then
tr -d '\n' <"$f"
return 0
fi
done
return 1
}
if [ -z "$PASS" ]; then
PASS=$(read_pass_file \
"/root/bank-${BANK_USER}-password.txt" \
/root/bank-explorer-password.txt \
"/etc/libeufin/secrets/bank-${BANK_USER}-password.txt" || true)
fi
if [ -z "$ADMIN_PASS" ]; then
ADMIN_PASS=$(read_pass_file \
/root/bank-admin-password.txt \
/etc/libeufin/secrets/bank-admin-password.txt || true)
fi
WORKDIR=$(mktemp -d)
trap 'rm -rf "$WORKDIR"' EXIT
OUT="$LANDING_DIR/stats.json"
RUN="$LANDING_DIR/stats-run.json"
TMP="${OUT}.tmp.$$"
mkdir -p "$LANDING_DIR"
now_iso() { date +%Y-%m-%dT%H:%M%z | sed -E 's/([+-][0-9]{2})([0-9]{2})$/\1:\2/'; }
now_unix() { date +%s; }
now_human() { date +"%Y-%m-%d %H:%M %Z"; }
iso_from_unix() {
local u="$1"
date -d "@${u}" +%Y-%m-%dT%H:%M%z 2>/dev/null | sed -E 's/([+-][0-9]{2})([0-9]{2})$/\1:\2/' \
|| date -r "${u}" +%Y-%m-%dT%H:%M%z 2>/dev/null | sed -E 's/([+-][0-9]{2})([0-9]{2})$/\1:\2/' \
|| echo ""
}
# Human CEST/CET label for display: "2026-07-09 20:49 CEST"
human_from_unix() {
local u="$1"
date -d "@${u}" +"%Y-%m-%d %H:%M %Z" 2>/dev/null \
|| date -r "${u}" +"%Y-%m-%d %H:%M %Z" 2>/dev/null \
|| echo ""
}
json_str() {
printf '"%s"' "$(printf '%s' "$1" | sed 's/\\/\\\\/g; s/"/\\"/g; s/ /\\t/g' | tr '\n' ' ')"
}
# Public run status for intro page. Never wipe stats.json on failure.
write_run() {
local ok_json="$1" msg="${2:-}"
cat >"$RUN" <<EOF
{
"ok": ${ok_json},
"at": $(json_str "$(now_iso)"),
"at_human": $(json_str "$(now_human)"),
"error": $( [ -n "$msg" ] && json_str "$msg" || echo null )
}
EOF
}
write_err() {
local msg="$1"
write_run false "$msg"
rm -f "$TMP" 2>/dev/null || true
echo "error: $msg (stats.json left unchanged; stats-run.json updated)" >&2
}
token_for() {
local user="$1" pass="$2" out="$3"
echo '{"scope":"readonly"}' >"$WORKDIR/tok-req.json"
curl -sS -m 12 -u "${user}:${pass}" \
-H 'Content-Type: application/json' \
-d @"$WORKDIR/tok-req.json" \
"${BANK}/accounts/${user}/token" >"$out" || true
awk -F'"' '/access_token/ {
for (i=1;i<=NF;i++) if ($i=="access_token") { print $(i+2); exit }
}' "$out" 2>/dev/null || true
}
extract_field() {
# extract "key":"value" or "key": number from a JSON blob (first hit)
local key="$1" file="$2"
awk -v key="$key" '
function between(s, k, p, rest, q2, r) {
p = index(s, "\"" k "\"")
if (p == 0) return ""
rest = substr(s, p + length(k) + 2)
while (rest ~ /^[[:space:]:]/) rest = substr(rest, 2)
if (substr(rest, 1, 1) == "\"") {
rest = substr(rest, 2)
q2 = index(rest, "\"")
if (q2 == 0) return ""
return substr(rest, 1, q2 - 1)
}
r = ""
while (rest ~ /^[0-9]/) {
r = r substr(rest, 1, 1)
rest = substr(rest, 2)
}
return r
}
{ print between($0, key); exit }
' "$file" 2>/dev/null
}
# --- tokens ---
if [ -z "$PASS" ] && [ -z "$ADMIN_PASS" ]; then
write_err "no bank password (explorer or admin) for stats"
exit 1
fi
ADMIN_TOKEN=""
if [ -n "$ADMIN_PASS" ]; then
ADMIN_TOKEN=$(token_for "$ADMIN_USER" "$ADMIN_PASS" "$WORKDIR/admin-tok.json")
fi
USER_TOKEN=""
if [ -n "$PASS" ]; then
USER_TOKEN=$(token_for "$BANK_USER" "$PASS" "$WORKDIR/user-tok.json")
fi
# Prefer admin for everything when available
AUTH_TOKEN="${ADMIN_TOKEN:-$USER_TOKEN}"
if [ -z "$AUTH_TOKEN" ]; then
write_err "token failed (admin/explorer)"
exit 1
fi
# --- account list ---
ACCOUNTS_N=0
ACCOUNTS_USERS=0
: >"$WORKDIR/usernames.txt"
if [ -n "$ADMIN_TOKEN" ]; then
# Use a large negative delta so we list *all* accounts (delta=-80 truncated
# the roster and undercounted bank_accounts + flow).
curl -sS -m 30 -H "Authorization: Bearer ${ADMIN_TOKEN}" \
"${BANK}/accounts?delta=${ACCOUNTS_DELTA}" >"$WORKDIR/accounts.json" || true
# usernames from "username":"..."
tr -d '\n' <"$WORKDIR/accounts.json" \
| sed 's/},{/}\n{/g' \
| while IFS= read -r line; do
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)
else
echo "$BANK_USER" >"$WORKDIR/usernames.txt"
ACCOUNTS_N=1
ACCOUNTS_USERS=1
fi
# --- scan txs: distinguish incoming (credit) vs withdraw (Taler debit) ---
# all-wd.tsv: amount \t t_s \t subject \t username \t reserve
# all-in.tsv: amount \t t_s \t subject \t username
# flow.tsv: kind \t amount_num kind = incoming | withdraw | other_out
: >"$WORKDIR/all-wd.tsv"
: >"$WORKDIR/all-in.tsv"
: >"$WORKDIR/flow.tsv"
SCAN_OK=0
SCAN_EMPTY=0
SCAN_FAIL=0
# Scan customer accounts only (not exchange/admin — exchange credits would double-count)
{
echo "$BANK_USER"
grep -Eve "^(admin|exchange|${BANK_USER})$" "$WORKDIR/usernames.txt" 2>/dev/null || true
} | awk 'NF && !seen[$0]++' | head -n "$MAX_SCAN_ACCOUNTS" >"$WORKDIR/scan-users.txt"
while IFS= read -r uname; do
[ -n "$uname" ] || continue
case "$uname" in admin|exchange) continue ;; esac
# Safe filename (usernames are mostly [A-Za-z0-9_-])
safe=$(printf '%s' "$uname" | tr -c 'A-Za-z0-9._-' '_')
code=$(curl -sS -m "${TX_CURL_TIMEOUT}" -o "$WORKDIR/tx-${safe}.json" -w '%{http_code}' \
-H "Authorization: Bearer ${AUTH_TOKEN}" \
"${BANK}/accounts/${uname}/transactions?delta=${TX_DELTA}" 2>/dev/null || echo 000)
# 204 / empty body: account with no transactions — normal, not an error
if [ "$code" = "204" ] || [ ! -s "$WORKDIR/tx-${safe}.json" ]; then
SCAN_EMPTY=$((SCAN_EMPTY + 1))
continue
fi
if [ "$code" != "200" ]; then
SCAN_FAIL=$((SCAN_FAIL + 1))
continue
fi
SCAN_OK=$((SCAN_OK + 1))
tr -d '\n' <"$WORKDIR/tx-${safe}.json" \
| sed 's/},{/}\n{/g' \
>"$WORKDIR/tx-lines.txt" 2>/dev/null || continue
awk -v uname="$uname" -v flowf="$WORKDIR/flow.tsv" -v inf="$WORKDIR/all-in.tsv" '
function between(s, key, p, rest, q2, r) {
p = index(s, "\"" key "\"")
if (p == 0) return ""
rest = substr(s, p + length(key) + 2)
while (rest ~ /^[[:space:]:]/) rest = substr(rest, 2)
if (substr(rest, 1, 1) == "\"") {
rest = substr(rest, 2)
q2 = index(rest, "\"")
if (q2 == 0) return ""
return substr(rest, 1, q2 - 1)
}
r = ""
while (rest ~ /^[0-9]/) {
r = r substr(rest, 1, 1)
rest = substr(rest, 2)
}
return r
}
function amt_n(a, p) {
p = index(a, ":")
if (p == 0) return 0
return substr(a, p + 1) + 0
}
{
line = $0
dir = between(line, "direction")
amt = between(line, "amount")
ts = between(line, "t_s")
subj = between(line, "subject")
low = tolower(subj)
if (amt == "") next
n = amt_n(amt)
if (dir == "credit") {
# Incoming: bank credits into customer accounts (admin top-up, transfers in)
print "incoming\t" n >> flowf
printf "%s\t%s\t%s\t%s\n", amt, ts, subj, uname >> inf
} else if (dir == "debit" && index(low, "withdraw") > 0) {
# Withdraw: Taler withdrawal debit → exchange → wallet coins
print "withdraw\t" n >> flowf
res = subj
sub(/^.*[Ww]ithdrawal[ ]+/, "", res)
gsub(/[^A-Za-z0-9]/, "", res)
printf "%s\t%s\t%s\t%s\t%s\n", amt, ts, subj, uname, res
} else if (dir == "debit") {
# Other debits (non-withdraw transfers)
print "other_out\t" n >> flowf
}
}
' "$WORKDIR/tx-lines.txt" >>"$WORKDIR/all-wd.tsv" 2>/dev/null || true
done <"$WORKDIR/scan-users.txt"
# Sum by kind
TOTAL_IN_N=$(awk -F'\t' '$1=="incoming"{s+=$2} END{printf "%.8f", s+0}' "$WORKDIR/flow.tsv" 2>/dev/null || echo 0)
TOTAL_WD_FLOW_N=$(awk -F'\t' '$1=="withdraw"{s+=$2} END{printf "%.8f", s+0}' "$WORKDIR/flow.tsv" 2>/dev/null || echo 0)
TOTAL_OTHER_OUT_N=$(awk -F'\t' '$1=="other_out"{s+=$2} END{printf "%.8f", s+0}' "$WORKDIR/flow.tsv" 2>/dev/null || echo 0)
N_INCOMING=$(awk -F'\t' '$1=="incoming"{c++} END{print c+0}' "$WORKDIR/flow.tsv" 2>/dev/null || echo 0)
# total out = withdraw + other debits
TOTAL_OUT_N=$(awk -v a="${TOTAL_WD_FLOW_N:-0}" -v b="${TOTAL_OTHER_OUT_N:-0}" 'BEGIN{printf "%.8f", a+b}')
# Sort by t_s descending
sort -t$'\t' -k2,2nr "$WORKDIR/all-wd.tsv" -o "$WORKDIR/all-wd-sorted.tsv" 2>/dev/null \
|| cp "$WORKDIR/all-wd.tsv" "$WORKDIR/all-wd-sorted.tsv"
sort -t$'\t' -k2,2nr "$WORKDIR/all-in.tsv" -o "$WORKDIR/all-in-sorted.tsv" 2>/dev/null \
|| 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)
# 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)
# Aggregates
NOW=$(now_unix)
GEN_ISO=$(now_iso)
GEN_HUMAN=$(date +"%Y-%m-%d %H:%M %Z")
# Build recent withdraws JSON array (up to 10) with CEST times — landing shows all 10
RECENT_WD_N="${RECENT_WD_N:-10}"
: >"$WORKDIR/recent.jsonl"
N_WD=0
TOTAL_WD=0
W24=0; N24=0; W7=0; N7=0
DAY=$((NOW - 86400))
WEEK=$((NOW - 7 * 86400))
LAST_AMT=""; LAST_TS=""; LAST_SUBJ=""
while IFS=$'\t' read -r amt ts subj uname res; do
[ -n "$amt" ] || continue
n=$(printf '%s' "$amt" | awk -F: '{print $2+0}')
N_WD=$((N_WD + 1))
TOTAL_WD=$(awk -v a="$TOTAL_WD" -v b="$n" 'BEGIN{printf "%.8f", a+b}')
ts_n=${ts:-0}
if [ "$ts_n" -ge "$DAY" ] 2>/dev/null; then
W24=$(awk -v a="$W24" -v b="$n" 'BEGIN{printf "%.8f", a+b}')
N24=$((N24 + 1))
fi
if [ "$ts_n" -ge "$WEEK" ] 2>/dev/null; then
W7=$(awk -v a="$W7" -v b="$n" 'BEGIN{printf "%.8f", a+b}')
N7=$((N7 + 1))
fi
if [ -z "$LAST_AMT" ]; then
LAST_AMT=$amt
LAST_TS=$ts_n
LAST_SUBJ=$subj
fi
if [ "$N_WD" -le "$RECENT_WD_N" ]; then
at_h=""; at_iso=""
if [ -n "$ts_n" ] && [ "$ts_n" != "0" ]; then
at_h=$(human_from_unix "$ts_n")
at_iso=$(iso_from_unix "$ts_n")
fi
printf '%s\t%s\t%s\t%s\t%s\t%s\n' "$amt" "$ts_n" "$at_h" "$at_iso" "$uname" "$res" >>"$WORKDIR/recent.jsonl"
fi
done <"$WORKDIR/all-wd-sorted.tsv"
fmt_goa() {
awk -v n="$1" 'BEGIN{
if (n+0 == int(n+0)) printf "GOA:%d", int(n+0);
else printf "GOA:%.8g", n+0;
}'
}
TOTAL_AMT=$(fmt_goa "$TOTAL_WD")
TOTAL_IN_AMT=$(fmt_goa "${TOTAL_IN_N:-0}")
TOTAL_OUT_AMT=$(fmt_goa "${TOTAL_OUT_N:-0}")
TOTAL_WD_FLOW_AMT=$(fmt_goa "${TOTAL_WD_FLOW_N:-0}")
TOTAL_OTHER_OUT_AMT=$(fmt_goa "${TOTAL_OTHER_OUT_N:-0}")
W24_AMT=$(fmt_goa "$W24")
W7_AMT=$(fmt_goa "$W7")
LAST_AT_H=""; LAST_AT_ISO=""
if [ -n "${LAST_TS:-}" ] && [ "$LAST_TS" != "0" ]; then
LAST_AT_H=$(human_from_unix "$LAST_TS")
LAST_AT_ISO=$(iso_from_unix "$LAST_TS")
fi
# recent_withdraws JSON
RECENT_JSON="["
first=1
while IFS=$'\t' read -r amt ts_n at_h at_iso uname res; do
[ -n "$amt" ] || continue
if [ "$first" = 1 ]; then first=0; else RECENT_JSON="${RECENT_JSON},"; fi
RECENT_JSON="${RECENT_JSON}
{
\"kind\": \"withdraw\",
\"amount\": $(json_str "$amt"),
\"at\": $(json_str "$at_h"),
\"at_iso\": $(json_str "$at_iso"),
\"at_unix\": ${ts_n:-null},
\"account\": $(json_str "$uname"),
\"reserve\": $(json_str "$res")
}"
done <"$WORKDIR/recent.jsonl"
RECENT_JSON="${RECENT_JSON}
]"
# recent incoming (up to 5)
: >"$WORKDIR/recent-in.jsonl"
N_IN_LIST=0
while IFS=$'\t' read -r amt ts subj uname; do
[ -n "$amt" ] || continue
N_IN_LIST=$((N_IN_LIST + 1))
[ "$N_IN_LIST" -le 5 ] || break
ts_n=${ts:-0}
at_h=""; at_iso=""
if [ -n "$ts_n" ] && [ "$ts_n" != "0" ]; then
at_h=$(human_from_unix "$ts_n")
at_iso=$(iso_from_unix "$ts_n")
fi
printf '%s\t%s\t%s\t%s\t%s\n' "$amt" "$ts_n" "$at_h" "$at_iso" "$uname" >>"$WORKDIR/recent-in.jsonl"
done <"$WORKDIR/all-in-sorted.tsv"
RECENT_IN_JSON="["
first=1
while IFS=$'\t' read -r amt ts_n at_h at_iso uname; do
[ -n "$amt" ] || continue
if [ "$first" = 1 ]; then first=0; else RECENT_IN_JSON="${RECENT_IN_JSON},"; fi
RECENT_IN_JSON="${RECENT_IN_JSON}
{
\"kind\": \"incoming\",
\"amount\": $(json_str "$amt"),
\"at\": $(json_str "$at_h"),
\"at_iso\": $(json_str "$at_iso"),
\"at_unix\": ${ts_n:-null},
\"account\": $(json_str "$uname")
}"
done <"$WORKDIR/recent-in.jsonl"
RECENT_IN_JSON="${RECENT_IN_JSON}
]"
# Balance of explorer (optional display not required in footer)
BALANCE="GOA:0"
if [ -n "${USER_TOKEN:-$AUTH_TOKEN}" ]; then
curl -sS -m 10 -H "Authorization: Bearer ${USER_TOKEN:-$AUTH_TOKEN}" \
"${BANK}/accounts/${BANK_USER}" >"$WORKDIR/acct.json" || true
BALANCE=$(awk -F'"' '/"amount"/ {
for (i=1;i<=NF;i++) if ($i=="amount") { print $(i+2); exit }
}' "$WORKDIR/acct.json" 2>/dev/null || echo "GOA:0")
fi
# Demo block kept for page QR logic only (not shown in footer)
DEMO_URI=""; DEMO_AMT=""; DEMO_CREATED=""; DEMO_WID=""; DEMO_STATUS=""
[ -f "$LANDING_DIR/withdraw.uri" ] && DEMO_URI=$(tr -d '\n' <"$LANDING_DIR/withdraw.uri")
[ -f "$LANDING_DIR/withdraw.amount" ] && DEMO_AMT=$(tr -d '\n' <"$LANDING_DIR/withdraw.amount")
[ -f "$LANDING_DIR/withdraw.created" ] && DEMO_CREATED=$(tr -d '\n' <"$LANDING_DIR/withdraw.created")
if [ -n "$DEMO_URI" ]; then
DEMO_WID=$(basename "$DEMO_URI")
curl -sS -m 8 \
"${BANK}/taler-integration/withdrawal-operation/${DEMO_WID}" \
>"$WORKDIR/demo-wd.json" 2>/dev/null || true
DEMO_STATUS=$(awk -F'"' '/"status"/ {
for (i=1;i<=NF;i++) if ($i=="status") { print $(i+2); exit }
}' "$WORKDIR/demo-wd.json" 2>/dev/null || true)
fi
DEMO_READY=false
case "$DEMO_STATUS" in
pending|selected) DEMO_READY=true ;;
"") [ -n "$DEMO_URI" ] && DEMO_READY=true ;;
esac
[ "$DEMO_STATUS" = "confirmed" ] && DEMO_READY=false
[ "$DEMO_STATUS" = "aborted" ] && DEMO_READY=false
# Sanity: never publish an empty-looking success if admin scan should have accounts
if [ -n "$ADMIN_TOKEN" ] && [ "${ACCOUNTS_N:-0}" = "0" ]; then
write_err "accounts list empty after admin scan"
exit 1
fi
# Live performance probes (same idea as exchange landing-stats)
# Milliseconds as integer, rounded (not truncated) so sub-ms loopback does not
# always show 0 when we fall back to in-container URLs.
measure_ms() {
local url="$1" t
t=$(curl -sS -o /dev/null -m 8 -w '%{time_total}' "$url" 2>/dev/null || echo "")
[ -z "$t" ] && { echo "null"; return; }
awk -v t="$t" 'BEGIN{
ms = (t+0)*1000
if (ms > 0 && ms < 1) ms = 1
printf "%d", int(ms + 0.5)
}'
}
num_or_null() { case "${1:-}" in ''|null) echo null ;; *) echo "$1" ;; esac; }
# Prefer public URL for real client latency (Caddy → bank); fall back to loopback.
# Integration used to probe only $BANK (127.0.0.1:9012) → ~01 ms and not comparable
# to /config which already used the public host.
BANK_PUBLIC_BASE="${BANK_PUBLIC_URL:-https://bank.hacktivism.ch}"
CONFIG_MS=$(measure_ms "${BANK_PUBLIC_BASE}/config")
CONFIG_HTTP=$(curl -sS -o /dev/null -m 8 -w '%{http_code}' "${BANK_PUBLIC_BASE}/config" 2>/dev/null || echo "000")
if [ "$CONFIG_HTTP" != "200" ]; then
CONFIG_MS=$(measure_ms "${BANK}/config")
CONFIG_HTTP=$(curl -sS -o /dev/null -m 8 -w '%{http_code}' "${BANK}/config" 2>/dev/null || echo "000")
fi
INT_MS=$(measure_ms "${BANK_PUBLIC_BASE}/taler-integration/config")
INT_HTTP=$(curl -sS -o /dev/null -m 8 -w '%{http_code}' "${BANK_PUBLIC_BASE}/taler-integration/config" 2>/dev/null || echo "000")
if [ "$INT_HTTP" != "200" ]; then
INT_MS=$(measure_ms "${BANK}/taler-integration/config")
INT_HTTP=$(curl -sS -o /dev/null -m 8 -w '%{http_code}' "${BANK}/taler-integration/config" 2>/dev/null || echo "000")
fi
WEBUI_MS=$(measure_ms "${BANK_PUBLIC_BASE}/webui/")
WEBUI_HTTP=$(curl -sS -o /dev/null -m 8 -w '%{http_code}' "${BANK_PUBLIC_BASE}/webui/" 2>/dev/null || echo "000")
LOADAVG=""
[ -r /proc/loadavg ] && LOADAVG=$(awk '{print $1","$2","$3}' /proc/loadavg)
MEM_JSON='"container_rss_human": "—"'
MEM_HELPER="${MEM_HELPER:-/usr/local/lib/landing-mem-snapshot.sh}"
if [ -f "$MEM_HELPER" ]; then
# shellcheck disable=SC1090
. "$MEM_HELPER"
mem_snapshot_json || true
fi
cat >"$TMP" <<EOF
{
"ok": true,
"currency": "GOA",
"timezone": $(json_str "$TZ"),
"generated_at": $(json_str "$GEN_ISO"),
"generated_at_human": $(json_str "$GEN_HUMAN"),
"generated_at_unix": $NOW,
"source": "in-container landing-stats.sh",
"bank_url": $(json_str "$BANK"),
"scan": {
"tx_delta": $(json_str "$TX_DELTA"),
"accounts_delta": $(json_str "$ACCOUNTS_DELTA"),
"max_scan_accounts": ${MAX_SCAN_ACCOUNTS:-0},
"accounts_listed": ${ACCOUNTS_N:-0},
"accounts_scanned_ok": ${SCAN_OK:-0},
"accounts_empty_tx": ${SCAN_EMPTY:-0},
"accounts_scan_fail": ${SCAN_FAIL:-0},
"note": "empty_tx includes HTTP 204 (no ledger rows) — normal for new auto-accounts"
},
"bank_accounts": {
"total": ${ACCOUNTS_N:-0},
"users": ${ACCOUNTS_USERS:-0},
"with_withdraws": ${ACCOUNTS_WITH_WD:-0}
},
"wallets": {
"unique_reserves": ${WALLETS_N:-0},
"note": "unique reserve pubs from Taler withdrawals (one per wallet withdraw)"
},
"balance_explorer": $(json_str "$BALANCE"),
"flow": {
"incoming": {
"label": "Incoming bank credits",
"count": ${N_INCOMING:-0},
"amount": $(json_str "$TOTAL_IN_AMT"),
"value": ${TOTAL_IN_N:-0}
},
"withdraw": {
"label": "Taler withdrawals to wallets",
"count": ${N_WD:-0},
"amount": $(json_str "$TOTAL_WD_FLOW_AMT"),
"value": ${TOTAL_WD_FLOW_N:-0}
},
"other_out": {
"label": "Other debits (non-withdraw)",
"amount": $(json_str "$TOTAL_OTHER_OUT_AMT"),
"value": ${TOTAL_OTHER_OUT_N:-0}
},
"total_in": $(json_str "$TOTAL_IN_AMT"),
"total_in_value": ${TOTAL_IN_N:-0},
"total_out": $(json_str "$TOTAL_OUT_AMT"),
"total_out_value": ${TOTAL_OUT_N:-0},
"note": "incoming=credits; withdraw=Taler withdrawal debits; excl. admin+exchange accounts"
},
"withdraws": {
"count": ${N_WD:-0},
"total_amount": $(json_str "$TOTAL_AMT"),
"total_value": ${TOTAL_WD:-0},
"last_amount": $( [ -n "$LAST_AMT" ] && json_str "$LAST_AMT" || echo null ),
"last_at": $( [ -n "$LAST_AT_H" ] && json_str "$LAST_AT_H" || echo null ),
"last_at_iso": $( [ -n "$LAST_AT_ISO" ] && json_str "$LAST_AT_ISO" || echo null ),
"last_at_unix": ${LAST_TS:-null},
"last_subject": $( [ -n "$LAST_SUBJ" ] && json_str "$LAST_SUBJ" || echo null ),
"last_24h": { "count": ${N24:-0}, "amount": $(json_str "$W24_AMT"), "value": ${W24:-0} },
"last_7d": { "count": ${N7:-0}, "amount": $(json_str "$W7_AMT"), "value": ${W7:-0} }
},
"recent_withdraws": $RECENT_JSON,
"recent_incoming": $RECENT_IN_JSON,
"demo": {
"uri": $( [ -n "$DEMO_URI" ] && json_str "$DEMO_URI" || echo null ),
"amount": $( [ -n "$DEMO_AMT" ] && json_str "$DEMO_AMT" || echo null ),
"created": $( [ -n "$DEMO_CREATED" ] && json_str "$DEMO_CREATED" || echo null ),
"withdrawal_id": $( [ -n "$DEMO_WID" ] && json_str "$DEMO_WID" || echo null ),
"status": $( [ -n "$DEMO_STATUS" ] && json_str "$DEMO_STATUS" || echo null ),
"ready": $DEMO_READY
},
"performance": {
"config_http": $(json_str "$CONFIG_HTTP"),
"config_ms": $(num_or_null "$CONFIG_MS"),
"integration_http": $(json_str "$INT_HTTP"),
"integration_ms": $(num_or_null "$INT_MS"),
"webui_http": $(json_str "$WEBUI_HTTP"),
"webui_ms": $(num_or_null "$WEBUI_MS"),
"loadavg": $(json_str "${LOADAVG:-}"),
"memory": {
${MEM_JSON}
}
}
}
EOF
grep -q '"ok": true' "$TMP" || { write_err "tmp json missing ok:true"; exit 1; }
mv -f "$TMP" "$OUT"
write_run true
echo "ok accounts=${ACCOUNTS_N} wallets=${WALLETS_N} withdraws=${N_WD} total=${TOTAL_AMT} tz=${TZ} -> $OUT"

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

109
scripts/taler-bank/start_bank.sh Executable file
View file

@ -0,0 +1,109 @@
#!/bin/bash
# Start / restart libeufin-bank serve (manual, no systemd).
# Run as: libeufin-bank
# Same role as start_merchant.sh / start_exchange.sh.
#
# Container: taler-hacktivism-bank
# Prerequisite: /root/start_base_services_for_taler_bank.sh (as root) for postgres.
#
# Usage:
# start_bank.sh
# start_bank.sh --restart | -r
# start_bank.sh --help
set -u
usage() {
cat <<'EOF'
Usage: start_bank.sh [--restart|-r] [--help|-h]
(default) Start libeufin-bank serve if not already running.
--restart Stop live serve process, then start cleanly.
Does not touch postgres (use /root/start_base_services_for_taler_bank.sh).
EOF
}
DO_RESTART=0
for arg in "$@"; do
case "$arg" in
--restart|-r) DO_RESTART=1 ;;
--help|-h) usage; exit 0 ;;
*) echo "Unknown option: $arg" >&2; usage >&2; exit 2 ;;
esac
done
if [ "$(id -un)" != "libeufin-bank" ]; then
echo "This script must be run as user libeufin-bank" >&2
exit 1
fi
CONF=/etc/libeufin/libeufin-bank.conf
LOG_DIR=/var/log/libeufin-bank
PORT=$(grep -E '^\s*PORT\s*=' /etc/libeufin/bank-overrides.conf 2>/dev/null | tail -1 | awk -F= '{gsub(/ /,"",$2); print $2}')
PORT=${PORT:-9012}
# Match both wrapper name and the Java MainKt process that actually listens.
list_serve_pids() {
ps -eo pid=,stat=,args= 2>/dev/null | while read -r pid stat args; do
case "$stat" in Z*) continue ;; esac
case "$args" in
*start_bank.sh*) continue ;;
*check_bank-health*) continue ;;
esac
case "$args" in
*libeufin-bank\ serve*|/usr/bin/libeufin-bank\ serve*|*MainKt\ serve*|*tech.libeufin.bank.MainKt*)
echo "$pid"
;;
esac
done | sort -u
}
kill_serve() {
local pids
pids=$(list_serve_pids | tr '\n' ' ')
if [ -z "${pids// }" ]; then
echo "No live libeufin-bank serve to stop."
return 0
fi
echo "Stopping PIDs: $pids"
# shellcheck disable=SC2086
kill -TERM $pids 2>/dev/null || true
sleep 2
local left
left=$(list_serve_pids | tr '\n' ' ')
if [ -n "${left// }" ]; then
echo "SIGKILL remaining: $left"
# shellcheck disable=SC2086
kill -KILL $left 2>/dev/null || true
sleep 1
fi
echo "libeufin-bank serve stopped."
}
if [ "$DO_RESTART" -eq 1 ]; then
echo "=== restart: kill libeufin-bank serve ==="
kill_serve
fi
echo "Start libeufin-bank serve (port $PORT):"
LOG_FILE="$LOG_DIR/libeufin-bank-$(date +%Y-%m-%d).log"
mkdir -p "$LOG_DIR"
touch "$LOG_FILE" 2>/dev/null || true
if [ "$DO_RESTART" -eq 0 ] && [ -n "$(list_serve_pids)" ]; then
echo "libeufin-bank serve already running"
else
nohup libeufin-bank serve -c "$CONF" >>"$LOG_FILE" 2>&1 &
disown 2>/dev/null || true
sleep 2
fi
echo "Live processes:"
ps -eo pid,stat,args 2>/dev/null | grep -E 'libeufin-bank serve|MainKt serve' | grep -v grep | grep -v ' Z ' || true
if [ -x /usr/local/bin/check_bank-health.sh ]; then
/usr/local/bin/check_bank-health.sh || exit 1
elif [ -x ./check_bank-health.sh ]; then
./check_bank-health.sh || exit 1
fi
exit 0

View file

@ -0,0 +1,148 @@
#!/bin/bash
# Root: base services for manual libeufin-bank (like merchant/exchange start_base).
# Then interactive shell as libeufin-bank → run start_bank.sh there.
#
# Container: taler-hacktivism-bank
#
# Usage:
# /root/start_base_services_for_taler_bank.sh
# /root/start_base_services_for_taler_bank.sh --no-shell
# /root/start_base_services_for_taler_bank.sh --no-shell --start-bank
# /root/start_base_services_for_taler_bank.sh --no-shell --start-bank --restart
set -e
CONF=/etc/libeufin/libeufin-bank.conf
LOG_DIR=/var/log/libeufin-bank
PWD_BIN=/usr/local/bin
BANK_STARTER=start_bank.sh
BANK_USER=libeufin-bank
if [ "$(id -u)" -ne 0 ]; then
echo "Run as root" >&2
exit 1
fi
NO_SHELL=0
START_BANK=0
BANK_RESTART=0
for arg in "$@"; do
case "$arg" in
--no-shell|-n) NO_SHELL=1 ;;
--start-bank) START_BANK=1; NO_SHELL=1 ;;
--restart|-r) BANK_RESTART=1 ;;
--help|-h)
cat <<'EOF'
Usage: start_base_services_for_taler_bank.sh [options]
(default) Start postgres + dirs, then interactive shell as libeufin-bank
--no-shell Start base services only, do not open a shell
--start-bank After base services, run /usr/local/bin/start_bank.sh as libeufin-bank
(implies --no-shell)
--restart With --start-bank: pass --restart to start_bank.sh
EOF
exit 0
;;
*)
echo "Unknown option: $arg" >&2
exit 2
;;
esac
done
# --- Debian postgresql defaults (pg_createcluster layout) ---
ensure_postgresql() {
echo " Debian perms on /etc/postgresql + data/log/run..."
if [ -d /etc/postgresql ]; then
chown -R root:postgres /etc/postgresql
find /etc/postgresql -type d -exec chmod 755 {} \;
find /etc/postgresql -type f -name '*.conf' -exec chmod 640 {} \;
fi
chown -R postgres:postgres /var/lib/postgresql /var/log/postgresql 2>/dev/null || true
mkdir -p /var/run/postgresql
chown postgres:postgres /var/run/postgresql
chmod 2775 /var/run/postgresql 2>/dev/null || chmod 775 /var/run/postgresql
# Snakeoil TLS key: postgres fails if it cannot read the key (common in rootless images).
# Prefer fixing perms; if still broken, force ssl=off for local-only DB.
if [ -f /etc/ssl/private/ssl-cert-snakeoil.key ]; then
chown root:ssl-cert /etc/ssl/private/ssl-cert-snakeoil.key 2>/dev/null || true
chmod 640 /etc/ssl/private/ssl-cert-snakeoil.key 2>/dev/null || true
fi
for pgconf in /etc/postgresql/*/main/postgresql.conf; do
[ -f "$pgconf" ] || continue
if grep -qE '^\s*ssl\s*=' "$pgconf"; then
sed -i 's/^\s*ssl\s*=.*/ssl = off/' "$pgconf"
else
echo "ssl = off" >>"$pgconf"
fi
done
if pg_isready -q 2>/dev/null; then
echo " already accepting connections"
pg_isready || true
return 0
fi
rm -f /var/run/postgresql/.s.PGSQL.*.lock 2>/dev/null || true
if ! pgrep -u postgres -x postgres >/dev/null 2>&1; then
rm -f /var/lib/postgresql/*/main/postmaster.pid 2>/dev/null || true
fi
if command -v pg_ctlcluster >/dev/null 2>&1 && command -v pg_lsclusters >/dev/null 2>&1; then
while read -r ver name _rest; do
[ -n "$ver" ] || continue
echo " pg_ctlcluster $ver $name start"
pg_ctlcluster "$ver" "$name" start 2>/dev/null || true
done < <(pg_lsclusters --no-header 2>/dev/null || true)
fi
if ! pg_isready -q 2>/dev/null; then
if [ -x /etc/init.d/postgresql ]; then
/etc/init.d/postgresql start || true
else
service postgresql start || true
fi
fi
sleep 1
pg_isready || true
}
echo "Create log + data dirs... giving permission to ${BANK_USER}:"
mkdir -p "$LOG_DIR" /var/lib/libeufin-bank
chown "${BANK_USER}:${BANK_USER}" "$LOG_DIR" /var/lib/libeufin-bank
chmod 755 "$LOG_DIR" /var/lib/libeufin-bank
echo "Start base services needed for GOA Exploration Bank."
echo ""
echo "1. postgresql:"
ensure_postgresql
# ensure role + DB (postgres:///libeufin) — Debian createuser/createdb as postgres
su -s /bin/bash postgres -c "psql -tc \"SELECT 1 FROM pg_roles WHERE rolname='libeufin-bank'\" | grep -q 1 || createuser -s libeufin-bank" || true
su -s /bin/bash postgres -c "psql -tc \"SELECT 1 FROM pg_database WHERE datname='libeufin'\" | grep -q 1 || createdb -O libeufin-bank libeufin" || true
pg_isready || true
if [ "$START_BANK" -eq 1 ]; then
echo ""
echo "2. start_bank.sh as ${BANK_USER}:"
if [ ! -x "$PWD_BIN/$BANK_STARTER" ]; then
echo "missing $PWD_BIN/$BANK_STARTER" >&2
exit 1
fi
if [ "$BANK_RESTART" -eq 1 ]; then
runuser -u "$BANK_USER" -- "$PWD_BIN/$BANK_STARTER" --restart
else
runuser -u "$BANK_USER" -- "$PWD_BIN/$BANK_STARTER"
fi
exit $?
fi
if [ "$NO_SHELL" -eq 1 ]; then
echo "Base services started (--no-shell). Next: runuser -u ${BANK_USER} -- $PWD_BIN/$BANK_STARTER [--restart]"
exit 0
fi
echo "2. Switching now to user ${BANK_USER}, in $PWD_BIN; find executable $BANK_STARTER there!"
echo ""
cd "$PWD_BIN"
# util-linux: -u and -s are mutually exclusive (same as exchange/merchant)
exec runuser -u "$BANK_USER" -- bash