bank: demo-withdraw API mints one-shot withdrawals from explorer pool
This commit is contained in:
parent
468fdb5fe1
commit
3d4d7251a5
1 changed files with 172 additions and 0 deletions
172
scripts/taler-bank/demo-withdraw-api.py
Executable file
172
scripts/taler-bank/demo-withdraw-api.py
Executable file
|
|
@ -0,0 +1,172 @@
|
|||
#!/usr/bin/env python3
|
||||
"""
|
||||
HTTP helper for bank landing: mint a fresh demo withdrawal from the shared
|
||||
explorer pool so app users get GOA without registering a bank account.
|
||||
|
||||
Listens on 127.0.0.1:19096 (only inside bank container / localhost).
|
||||
Nginx proxies GET /intro/demo-withdraw.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 ssl
|
||||
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("/")
|
||||
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 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]
|
||||
# wallets often prefer no default HTTPS port in host
|
||||
uri_clean = uri.replace(":443/", "/").replace(":443?", "?")
|
||||
LANDING.mkdir(parents=True, exist_ok=True)
|
||||
(LANDING / "withdraw.uri").write_text(uri_clean + "\n")
|
||||
(LANDING / "withdraw.amount").write_text(AMOUNT + "\n")
|
||||
(LANDING / "withdraw.created").write_text(
|
||||
time.strftime("%Y-%m-%dT%H:%MZ", time.gmtime()) + "\n"
|
||||
)
|
||||
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(wid)
|
||||
watch.write_text("\n".join(sorted(ids)) + "\n")
|
||||
return {
|
||||
"ok": True,
|
||||
"taler_withdraw_uri": uri_clean,
|
||||
"withdrawal_id": wid,
|
||||
"amount": AMOUNT,
|
||||
"pool_account": USER,
|
||||
"hint": "Open in GNU Taler Wallet (iOS/Android/desktop). No bank registration.",
|
||||
"created": time.strftime("%Y-%m-%dT%H:%MZ", time.gmtime()),
|
||||
}
|
||||
|
||||
|
||||
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]
|
||||
if path not in ("/", "/demo-withdraw.json", "/intro/demo-withdraw.json"):
|
||||
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
|
||||
try:
|
||||
body = mint_withdraw()
|
||||
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()
|
||||
Loading…
Add table
Add a link
Reference in a new issue