633 lines
21 KiB
Bash
Executable file
633 lines
21 KiB
Bash
Executable file
# shellcheck shell=bash
|
|
# Shared helpers for taler-monitoring (laptop or koopa).
|
|
|
|
# Default stack = GOA / hacktivism (overridden by TALER_DOMAIN / --domain)
|
|
: "${TALER_DOMAIN:=hacktivism.ch}"
|
|
: "${BANK_PUBLIC:=https://bank.hacktivism.ch}"
|
|
: "${EXCHANGE_PUBLIC:=https://exchange.hacktivism.ch}"
|
|
: "${MERCHANT_PUBLIC:=https://taler.hacktivism.ch}"
|
|
: "${BANK_LOCAL:=http://127.0.0.1:9012}"
|
|
: "${EXCHANGE_LOCAL:=http://127.0.0.1:9011}"
|
|
: "${MERCHANT_LOCAL:=https://127.0.0.1:9010}"
|
|
: "${LANDING_LOCAL:=http://127.0.0.1:9013}"
|
|
: "${KOOPA_SSH:=koopa}"
|
|
: "${MERCHANT_INSTANCE:=goa-demo-cp4zqk}"
|
|
: "${WITHDRAW_AMT:=GOA:20}" # single-shot fallback; e2e ladder uses ATM notes
|
|
: "${PAY_AMT:=GOA:0.01}"
|
|
: "${CREDIT_AMT:=GOA:4700}" # covers ATM ladder 20+50+100+200+4200 (paivana)
|
|
: "${TIMEOUT:=12}"
|
|
: "${E2E_TIMEOUT:=55}" # whole e2e budget; skip rest when exceeded (e2e raises as needed)
|
|
: "${E2E_PAY_SECS:=22}" # dedicated seconds for pay handle-uri (avoid Alarm clock)
|
|
# Local GOA: public paivana paywall (https://paivana.hacktivism.ch · template GOA:4200)
|
|
: "${PAIVANA_PUBLIC:=https://paivana.hacktivism.ch}"
|
|
: "${E2E_PAIVANA:=1}" # 0 = skip paivana section in e2e
|
|
# Devtest: inject reserve credit via wire-gateway admin/add-incoming (optional).
|
|
# Default off once wirewatch DNS works; set E2E_FAKE_INCOMING=1 to force.
|
|
: "${E2E_FAKE_INCOMING:=0}"
|
|
# SSH must never hang the monitoring run
|
|
: "${SSH_CONNECT_TIMEOUT:=3}"
|
|
: "${SSH_CMD_TIMEOUT:=12}" # hard cap for whole remote script (seconds)
|
|
: "${SKIP_SSH:=0}"
|
|
# Expected currency for public /config checks (empty = report only, don't fail)
|
|
: "${EXPECT_CURRENCY:=GOA}"
|
|
# 1 = this is the local koopa/hacktivism stack (inside/e2e/SSH make sense)
|
|
: "${LOCAL_STACK:=1}"
|
|
# Probe merchant host candidates when applying a generic domain (0=off)
|
|
: "${TALER_DOMAIN_PROBE:=1}"
|
|
|
|
BANK_PUBLIC=${BANK_PUBLIC%/}
|
|
EXCHANGE_PUBLIC=${EXCHANGE_PUBLIC%/}
|
|
MERCHANT_PUBLIC=${MERCHANT_PUBLIC%/}
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Domain profiles → bank / exchange / merchant base URLs
|
|
#
|
|
# Single place to declare a stack: scripts/taler-monitoring/domains.conf
|
|
# (or TALER_DOMAINS_CONF). Each profile names the three public endpoints.
|
|
#
|
|
# CLI still wins after profile load:
|
|
# --bank URL --exchange URL --merchant URL --currency CODE
|
|
# Env: BANK_PUBLIC EXCHANGE_PUBLIC MERCHANT_PUBLIC EXPECT_CURRENCY
|
|
#
|
|
# Unknown domains fall back to heuristics (see apply_taler_domain).
|
|
# ---------------------------------------------------------------------------
|
|
|
|
_MONITOR_LIB_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)
|
|
: "${TALER_DOMAINS_CONF:=${_MONITOR_LIB_DIR}/domains.conf}"
|
|
|
|
# host_or_url → https://… (no trailing slash)
|
|
_to_https_base() {
|
|
local x="${1%/}"
|
|
case "$x" in
|
|
https://*|http://*) printf '%s' "$x" ;;
|
|
"") printf '' ;;
|
|
*) printf 'https://%s' "$x" ;;
|
|
esac
|
|
}
|
|
|
|
# 1 = check GOA-style /intro landing pages (assets, link crawl, demo-withdraw).
|
|
# 0 = skip (e.g. taler-ops.ch / mytops — no public landings).
|
|
: "${CHECK_LANDING:=1}"
|
|
|
|
# Declare bank + exchange + merchant (+ currency, local stack, landing, domain id).
|
|
# Usage:
|
|
# set_taler_stack BANK EXCHANGE MERCHANT [CURRENCY] [LOCAL 0|1] [LANDING 0|1] [TALER_DOMAIN]
|
|
# BANK/EXCHANGE/MERCHANT: hostname or full URL
|
|
set_taler_stack() {
|
|
local bank="${1:-}" exchange="${2:-}" merchant="${3:-}"
|
|
local currency="${4:-}" local_stack="${5:-0}" landing="${6:-}" domain_id="${7:-}"
|
|
|
|
BANK_PUBLIC=$(_to_https_base "$bank")
|
|
EXCHANGE_PUBLIC=$(_to_https_base "$exchange")
|
|
MERCHANT_PUBLIC=$(_to_https_base "$merchant")
|
|
[ -n "$currency" ] && EXPECT_CURRENCY="$currency"
|
|
LOCAL_STACK="$local_stack"
|
|
if [ "$LOCAL_STACK" = "1" ]; then
|
|
SKIP_SSH=0
|
|
else
|
|
SKIP_SSH=1
|
|
fi
|
|
# default: landings only matter on local GOA stack
|
|
if [ -n "$landing" ]; then
|
|
CHECK_LANDING="$landing"
|
|
elif [ "$LOCAL_STACK" = "1" ]; then
|
|
CHECK_LANDING=1
|
|
else
|
|
CHECK_LANDING=0
|
|
fi
|
|
[ -n "$domain_id" ] && TALER_DOMAIN="$domain_id"
|
|
|
|
BANK_PUBLIC=${BANK_PUBLIC%/}
|
|
EXCHANGE_PUBLIC=${EXCHANGE_PUBLIC%/}
|
|
MERCHANT_PUBLIC=${MERCHANT_PUBLIC%/}
|
|
}
|
|
|
|
# Load first matching profile from domains.conf.
|
|
# Fields (whitespace-separated; # comments; blank lines ignored):
|
|
# name bank exchange merchant currency local[0|1] landing[0|1] [canonical_domain]
|
|
# Returns 0 if found, 1 if not.
|
|
load_domain_profile() {
|
|
local want="$1" conf="${TALER_DOMAINS_CONF:-}"
|
|
local line name bank exchange merchant currency local_stack landing canon
|
|
[ -n "$want" ] || return 1
|
|
[ -n "$conf" ] && [ -f "$conf" ] || return 1
|
|
|
|
while IFS= read -r line || [ -n "$line" ]; do
|
|
line="${line%%#*}"
|
|
# shellcheck disable=SC2086
|
|
set -- $line
|
|
[ $# -ge 5 ] || continue
|
|
name="$1"
|
|
[ "$name" = "$want" ] || continue
|
|
bank="$2"
|
|
exchange="$3"
|
|
merchant="$4"
|
|
currency="$5"
|
|
local_stack="${6:-0}"
|
|
landing="${7:-}"
|
|
canon="${8:-$name}"
|
|
# Backward compat: old 7th field was canonical domain (contains a dot)
|
|
if [ -n "$landing" ] && [[ "$landing" == *.* && "$landing" != "0" && "$landing" != "1" ]]; then
|
|
canon="$landing"
|
|
landing=""
|
|
fi
|
|
set_taler_stack "$bank" "$exchange" "$merchant" "$currency" "$local_stack" "$landing" "$canon"
|
|
return 0
|
|
done <"$conf"
|
|
return 1
|
|
}
|
|
|
|
_normalize_domain() {
|
|
local d="$1"
|
|
d="${d#https://}"
|
|
d="${d#http://}"
|
|
d="${d%%/*}"
|
|
d="${d%%:*}"
|
|
# Strip service host prefix only if a real domain remains (has a dot).
|
|
# e.g. bank.demo.taler.net → demo.taler.net, taler.hacktivism.ch → hacktivism.ch
|
|
# but NOT taler.net → net
|
|
case "$d" in
|
|
bank.*|exchange.*|taler.*|backend.*|merchant.*|shop.*|libeufin.*|my.*|map.*)
|
|
rest="${d#*.}"
|
|
if [[ "$rest" == *.* ]]; then
|
|
d="$rest"
|
|
fi
|
|
;;
|
|
esac
|
|
printf '%s' "$d"
|
|
}
|
|
|
|
_probe_https_config() {
|
|
# 0 if https://$1/config returns 200
|
|
local host="$1" code
|
|
code=$(curl -skS --max-redirs 0 -m 4 -o /dev/null -w '%{http_code}' "https://${host}/config" 2>/dev/null || echo 000)
|
|
[ "$code" = "200" ]
|
|
}
|
|
|
|
apply_taler_domain() {
|
|
local raw="${1:-}"
|
|
local d
|
|
[ -n "$raw" ] || return 0
|
|
d=$(_normalize_domain "$raw")
|
|
TALER_DOMAIN="$d"
|
|
|
|
# 1) Explicit profile (domains.conf) — preferred way to add stacks
|
|
if load_domain_profile "$d" || load_domain_profile "$raw"; then
|
|
# Optional tiny defaults for e2e ladders by currency
|
|
case "${EXPECT_CURRENCY:-}" in
|
|
GOA)
|
|
: "${WITHDRAW_AMT:=GOA:20}"
|
|
: "${PAY_AMT:=GOA:0.01}"
|
|
: "${CREDIT_AMT:=GOA:400}"
|
|
;;
|
|
KUDOS|TESTKUDOS)
|
|
MERCHANT_INSTANCE="${MERCHANT_INSTANCE:-sandbox}"
|
|
WITHDRAW_AMT="${WITHDRAW_AMT:-${EXPECT_CURRENCY}:20}"
|
|
PAY_AMT="${PAY_AMT:-${EXPECT_CURRENCY}:0.01}"
|
|
CREDIT_AMT="${CREDIT_AMT:-${EXPECT_CURRENCY}:100}"
|
|
;;
|
|
CHF)
|
|
WITHDRAW_AMT="${WITHDRAW_AMT:-CHF:20}"
|
|
PAY_AMT="${PAY_AMT:-CHF:0.01}"
|
|
CREDIT_AMT="${CREDIT_AMT:-CHF:100}"
|
|
;;
|
|
esac
|
|
else
|
|
# 2) Unknown domain — heuristics only (prefer profile in domains.conf)
|
|
# Override: --bank / --exchange / --merchant / --currency
|
|
# Do not keep default GOA: empty currency = report only, no hard fail.
|
|
BANK_PUBLIC="https://bank.${d}"
|
|
EXCHANGE_PUBLIC="https://exchange.${d}"
|
|
# TOPS-style multi-tenant merchant first, then legacy names
|
|
MERCHANT_PUBLIC="https://my.${d}"
|
|
EXPECT_CURRENCY=""
|
|
LOCAL_STACK=0
|
|
SKIP_SSH=1
|
|
CHECK_LANDING=0
|
|
if [ "${TALER_DOMAIN_PROBE}" = "1" ]; then
|
|
local h
|
|
for h in "backend.${d}" "my.${d}" "taler.${d}" "merchant.${d}" "shop.${d}"; do
|
|
_probe_https_config "$h" && { MERCHANT_PUBLIC="https://$h"; break; }
|
|
done
|
|
for h in "bank.${d}" "libeufin.${d}"; do
|
|
_probe_https_config "$h" && { BANK_PUBLIC="https://$h"; break; }
|
|
done
|
|
_probe_https_config "exchange.${d}" || true
|
|
fi
|
|
fi
|
|
|
|
BANK_PUBLIC=${BANK_PUBLIC%/}
|
|
EXCHANGE_PUBLIC=${EXCHANGE_PUBLIC%/}
|
|
MERCHANT_PUBLIC=${MERCHANT_PUBLIC%/}
|
|
|
|
# Hard rule: only the local koopa/hacktivism stack may use SSH
|
|
if [ "${LOCAL_STACK}" != "1" ]; then
|
|
SKIP_SSH=1
|
|
fi
|
|
}
|
|
|
|
# Apply TALER_DOMAIN from env once (CLI exports TALER_DOMAIN_APPLIED=1 after overrides).
|
|
if [ "${TALER_DOMAIN_APPLIED:-0}" != "1" ] \
|
|
&& [ -n "${TALER_DOMAIN:-}" ] && [ "${TALER_DOMAIN}" != "hacktivism.ch" ]; then
|
|
apply_taler_domain "$TALER_DOMAIN"
|
|
TALER_DOMAIN_APPLIED=1
|
|
fi
|
|
|
|
# Safe SSH: publickey only, short connect, overall alarm so we never block forever.
|
|
SSH_BASE_OPTS=(
|
|
-o BatchMode=yes
|
|
-o ConnectTimeout="${SSH_CONNECT_TIMEOUT}"
|
|
-o ConnectionAttempts=1
|
|
-o ServerAliveInterval=3
|
|
-o ServerAliveCountMax=2
|
|
-o StrictHostKeyChecking=accept-new
|
|
-o PreferredAuthentications=publickey
|
|
-o PasswordAuthentication=no
|
|
-o KbdInteractiveAuthentication=no
|
|
-o GSSAPIAuthentication=no
|
|
-o NumberOfPasswordPrompts=0
|
|
)
|
|
|
|
# Hard wall-clock timeout so ssh/curl never block the monitoring run forever.
|
|
with_timeout() {
|
|
local secs="$1"; shift
|
|
if command -v gtimeout >/dev/null 2>&1; then
|
|
gtimeout --kill-after=2 "$secs" "$@"
|
|
return $?
|
|
fi
|
|
if command -v timeout >/dev/null 2>&1; then
|
|
timeout -k 2 "$secs" "$@" 2>/dev/null || timeout --kill-after=2 "$secs" "$@"
|
|
return $?
|
|
fi
|
|
# Portable: perl alarm + process group kill
|
|
perl -e '
|
|
use strict; use warnings;
|
|
my $secs = shift @ARGV;
|
|
my $pid = fork();
|
|
die "fork: $!" unless defined $pid;
|
|
if ($pid == 0) {
|
|
setpgrp(0, 0);
|
|
exec @ARGV;
|
|
exit 127;
|
|
}
|
|
$SIG{ALRM} = sub {
|
|
kill "TERM", -$pid;
|
|
select(undef, undef, undef, 1.0);
|
|
kill "KILL", -$pid;
|
|
exit 124;
|
|
};
|
|
alarm $secs;
|
|
waitpid($pid, 0);
|
|
my $code = $? >> 8;
|
|
alarm 0;
|
|
exit $code;
|
|
' "$secs" "$@"
|
|
}
|
|
|
|
# Probe: 0 if koopa SSH works quickly
|
|
koopa_ssh_ok() {
|
|
[ "${SKIP_SSH}" = "1" ] && return 1
|
|
with_timeout $((SSH_CONNECT_TIMEOUT + 3)) \
|
|
ssh "${SSH_BASE_OPTS[@]}" "${KOOPA_SSH}" 'echo ok' >/dev/null 2>&1
|
|
}
|
|
|
|
# Run remote bash -s with optional stdin script; hard-capped
|
|
# usage: koopa_ssh_bash [timeout_secs] <<'EOF' ... EOF
|
|
# or: koopa_ssh_run timeout_secs 'remote command'
|
|
koopa_ssh_run() {
|
|
local t="${1:-$SSH_CMD_TIMEOUT}"
|
|
shift
|
|
with_timeout "$t" ssh "${SSH_BASE_OPTS[@]}" "${KOOPA_SSH}" "$@"
|
|
}
|
|
|
|
koopa_ssh_bash() {
|
|
local t="${1:-$SSH_CMD_TIMEOUT}"
|
|
with_timeout "$t" ssh "${SSH_BASE_OPTS[@]}" "${KOOPA_SSH}" 'bash -s'
|
|
}
|
|
|
|
# ANSI colours for tags (green OK / yellow WARN / red ERROR·BLOCKER / cyan INFO).
|
|
# Default: always on so pasted ```bash``` logs keep visible tags when the terminal
|
|
# supports colour. Disable: NO_COLOR=1. Force off for dumb pipes: CLICOLOR=0.
|
|
if [ "${NO_COLOR:-0}" = "1" ] || [ "${CLICOLOR:-1}" = "0" ]; then
|
|
G= R= Y= C= N= B=
|
|
else
|
|
G=$'\e[1;32m' # bold green [OK]
|
|
R=$'\e[1;31m' # bold red [ERROR] [BLOCKER]
|
|
Y=$'\e[1;33m' # bold yellow [WARN]
|
|
C=$'\e[1;36m' # bold cyan [INFO]
|
|
B=$'\e[1m'
|
|
N=$'\e[0m'
|
|
fi
|
|
|
|
PASS_N=0
|
|
FAIL_N=0
|
|
WARN_N=0
|
|
INFO_N=0
|
|
BLOCKERS=() # human-readable payment/withdraw blockers
|
|
ERRORS=() # all ERROR lines (component scope)
|
|
|
|
# Test IDs by area: www-001, e2e-001, inside-001, …
|
|
# Usage: set_area www then each ok/fail/warn/info/blocker/err auto-numbers.
|
|
TEST_AREA=""
|
|
TEST_N=0
|
|
# Last issued id (www-001); set by _take_tid — not via $(…) so TEST_N persists.
|
|
LAST_TID=""
|
|
set_area() {
|
|
TEST_AREA="$1"
|
|
TEST_N=0
|
|
LAST_TID=""
|
|
}
|
|
# Assign next id into LAST_TID (must not run in a subshell).
|
|
_take_tid() {
|
|
LAST_TID=""
|
|
if [ -z "${TEST_AREA:-}" ]; then
|
|
return
|
|
fi
|
|
TEST_N=$((TEST_N + 1))
|
|
LAST_TID=$(printf '%s-%03d' "$TEST_AREA" "$TEST_N")
|
|
}
|
|
_fmt_tid() {
|
|
# prefix "www-001 " or empty
|
|
if [ -n "${LAST_TID:-}" ]; then
|
|
printf '%s ' "$LAST_TID"
|
|
fi
|
|
}
|
|
|
|
ok() {
|
|
local label="$1" detail="${2:-}"
|
|
_take_tid
|
|
if [ -n "$detail" ]; then
|
|
printf '%s[OK]%s %s%s — %s\n' "$G" "$N" "$(_fmt_tid)" "$label" "$detail"
|
|
else
|
|
printf '%s[OK]%s %s%s\n' "$G" "$N" "$(_fmt_tid)" "$label"
|
|
fi
|
|
PASS_N=$((PASS_N + 1))
|
|
}
|
|
# component-scoped error: err bank "libeufin down" "detail"
|
|
err() {
|
|
local comp="$1" msg="$2" detail="${3:-}"
|
|
_take_tid
|
|
printf '%s[ERROR]%s %s%s: %s%s\n' "$R" "$N" "$(_fmt_tid)" "$comp" "$msg" "${detail:+ — $detail}"
|
|
FAIL_N=$((FAIL_N + 1))
|
|
ERRORS+=("${LAST_TID:+$LAST_TID }[$comp] $msg${detail:+ — $detail}")
|
|
}
|
|
# legacy fail label ...
|
|
fail() {
|
|
local label="$1" detail="${2:-}"
|
|
_take_tid
|
|
printf '%s[ERROR]%s %s%s%s\n' "$R" "$N" "$(_fmt_tid)" "$label" "${detail:+ — $detail}"
|
|
FAIL_N=$((FAIL_N + 1))
|
|
ERRORS+=("${LAST_TID:+$LAST_TID }$label${detail:+ — $detail}")
|
|
}
|
|
warn() {
|
|
local label="$1" detail="${2:-}"
|
|
_take_tid
|
|
printf '%s[WARN]%s %s%s%s\n' "$Y" "$N" "$(_fmt_tid)" "$label" "${detail:+ — $detail}"
|
|
WARN_N=$((WARN_N + 1))
|
|
}
|
|
info() {
|
|
local label="$1" detail="${2:-}"
|
|
_take_tid
|
|
if [ -n "$detail" ]; then
|
|
printf '%s[INFO]%s %s%s — %s\n' "$C" "$N" "$(_fmt_tid)" "$label" "$detail"
|
|
else
|
|
printf '%s[INFO]%s %s%s\n' "$C" "$N" "$(_fmt_tid)" "$label"
|
|
fi
|
|
INFO_N=$((INFO_N + 1))
|
|
}
|
|
blocker() {
|
|
# Payment/withdraw path cannot proceed because of this
|
|
local step="$1" msg="$2"
|
|
_take_tid
|
|
printf '%s[BLOCKER]%s %s%s: %s\n' "$R$B" "$N" "$(_fmt_tid)" "$step" "$msg"
|
|
BLOCKERS+=("${LAST_TID:+$LAST_TID }[$step] $msg")
|
|
FAIL_N=$((FAIL_N + 1))
|
|
ERRORS+=("BLOCKER ${LAST_TID:+$LAST_TID }[$step] $msg")
|
|
}
|
|
section() { printf '\n%s== %s ==%s\n' "$B" "$*" "$N"; }
|
|
|
|
summary() {
|
|
echo ""
|
|
if [ "${#BLOCKERS[@]}" -gt 0 ]; then
|
|
printf '%s--- BLOCKERS (fix/withdraw path) ---%s\n' "$R$B" "$N"
|
|
local b
|
|
for b in "${BLOCKERS[@]}"; do
|
|
printf '%s • %s%s\n' "$R" "$b" "$N"
|
|
done
|
|
fi
|
|
if [ "${#ERRORS[@]}" -gt 0 ] && [ "${#BLOCKERS[@]}" -lt "${#ERRORS[@]}" ]; then
|
|
printf '%s--- ERRORS ---%s\n' "$R" "$N"
|
|
local e
|
|
for e in "${ERRORS[@]}"; do
|
|
case "$e" in BLOCKER*) continue ;; esac
|
|
printf '%s • %s%s\n' "$R" "$e" "$N"
|
|
done
|
|
fi
|
|
printf 'totals: %s%d OK%s' "$G" "$PASS_N" "$N"
|
|
[ "$FAIL_N" -gt 0 ] && printf ', %s%d ERROR%s' "$R" "$FAIL_N" "$N"
|
|
[ "$WARN_N" -gt 0 ] && printf ', %s%d WARN%s' "$Y" "$WARN_N" "$N"
|
|
[ "$INFO_N" -gt 0 ] && printf ', %d INFO' "$INFO_N"
|
|
[ "${#BLOCKERS[@]}" -gt 0 ] && printf ', %s%d BLOCKER%s' "$R" "${#BLOCKERS[@]}" "$N"
|
|
printf '\n'
|
|
[ "$FAIL_N" -eq 0 ]
|
|
}
|
|
|
|
http_code() {
|
|
local url="$1"; shift
|
|
curl -skS --max-redirs 0 -m "${TIMEOUT}" -o /dev/null -w '%{http_code}' "$@" "$url" 2>/dev/null || echo 000
|
|
}
|
|
|
|
http_body() {
|
|
local url="$1" out="$2"; shift 2
|
|
curl -skS --max-redirs 0 -m "${TIMEOUT}" -o "$out" -w '%{http_code}' "$@" "$url" 2>/dev/null || echo 000
|
|
}
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Currency unit map checks (wallet codec: alt_unit_names must include "0")
|
|
# ---------------------------------------------------------------------------
|
|
# Returns 0 if JSON body at $1 has usable alt_unit_names.
|
|
# Supports:
|
|
# - exchange/bank: currency_specification.alt_unit_names
|
|
# - merchant: currencies.<CODE>.alt_unit_names for each code
|
|
# Optional $2 = required currency code for currency_specification.currency
|
|
json_has_alt_unit_names() {
|
|
local file="$1" want_cur="${2:-}"
|
|
python3 - "$file" "$want_cur" <<'PY'
|
|
import json, sys
|
|
path, want = sys.argv[1], sys.argv[2]
|
|
try:
|
|
d = json.load(open(path))
|
|
except Exception as e:
|
|
print(f"json-error: {e}")
|
|
sys.exit(2)
|
|
|
|
def check_au(au, label):
|
|
if not isinstance(au, dict) or not au:
|
|
print(f"{label}: missing/empty alt_unit_names")
|
|
return False
|
|
if "0" not in au or not str(au.get("0") or "").strip():
|
|
print(f"{label}: alt_unit_names missing non-empty key \"0\" (have {sorted(au.keys())})")
|
|
return False
|
|
print(f"{label}: alt_unit_names ok (0={au.get('0')!r}, n={len(au)})")
|
|
return True
|
|
|
|
ok = True
|
|
cs = d.get("currency_specification")
|
|
if isinstance(cs, dict):
|
|
if want and cs.get("currency") and cs.get("currency") != want:
|
|
print(f"currency_specification.currency={cs.get('currency')!r} want {want!r}")
|
|
ok = False
|
|
if not check_au(cs.get("alt_unit_names"), "currency_specification"):
|
|
ok = False
|
|
elif "currency_specification" in d:
|
|
print("currency_specification: not an object")
|
|
ok = False
|
|
|
|
curs = d.get("currencies")
|
|
if isinstance(curs, dict) and curs:
|
|
for code, spec in curs.items():
|
|
if not isinstance(spec, dict):
|
|
print(f"currencies.{code}: not an object")
|
|
ok = False
|
|
continue
|
|
if not check_au(spec.get("alt_unit_names"), f"currencies.{code}"):
|
|
ok = False
|
|
|
|
if not isinstance(cs, dict) and not (isinstance(curs, dict) and curs):
|
|
# neither shape — fail
|
|
print("no currency_specification or currencies map")
|
|
ok = False
|
|
|
|
sys.exit(0 if ok else 1)
|
|
PY
|
|
}
|
|
|
|
# Check one exchange base URL's /config for alt_unit_names.
|
|
# $1=label $2=base_url $3=expected currency (optional) $4=strict(1) or soft(0)
|
|
check_exchange_alt_units() {
|
|
local label="$1" base="$2" want_cur="${3:-}" strict="${4:-1}"
|
|
local f code
|
|
base="${base%/}"
|
|
f=$(mktemp)
|
|
code=$(http_body "${base}/config" "$f")
|
|
if [ "$code" != "200" ]; then
|
|
rm -f "$f"
|
|
if [ "$strict" = "1" ]; then
|
|
fail "$label /config" "HTTP $code ($base)"
|
|
else
|
|
warn "$label /config" "HTTP $code ($base)"
|
|
fi
|
|
return
|
|
fi
|
|
local out ec
|
|
set +e
|
|
out=$(json_has_alt_unit_names "$f" "$want_cur" 2>&1)
|
|
ec=$?
|
|
set -e
|
|
rm -f "$f"
|
|
if [ "$ec" -eq 0 ]; then
|
|
ok "$label alt_unit_names" "$(echo "$out" | tr '\n' '; ' | sed 's/; $//')"
|
|
else
|
|
if [ "$strict" = "1" ]; then
|
|
fail "$label alt_unit_names" "$(echo "$out" | tr '\n' '; ' | sed 's/; $//')"
|
|
else
|
|
warn "$label alt_unit_names" "$(echo "$out" | tr '\n' '; ' | sed 's/; $//')"
|
|
fi
|
|
fi
|
|
}
|
|
|
|
# From merchant /config JSON file: walk exchanges[] and check each base_url/config.
|
|
# Local stack hosts (hacktivism.ch or $EXCHANGE_PUBLIC host) are strict; others soft.
|
|
check_merchant_listed_exchanges_alt_units() {
|
|
local mer_json="$1"
|
|
local list
|
|
list=$(python3 - "$mer_json" <<'PY'
|
|
import json, sys
|
|
d = json.load(open(sys.argv[1]))
|
|
for e in d.get("exchanges") or []:
|
|
if not isinstance(e, dict):
|
|
continue
|
|
u = (e.get("base_url") or e.get("url") or "").rstrip("/")
|
|
c = e.get("currency") or ""
|
|
if u:
|
|
print(f"{c}\t{u}")
|
|
PY
|
|
)
|
|
if [ -z "$list" ]; then
|
|
fail "merchant exchanges[]" "empty — no exchanges to check for alt_unit_names"
|
|
return
|
|
fi
|
|
local line cur url strict host
|
|
while IFS=$'\t' read -r cur url; do
|
|
[ -n "$url" ] || continue
|
|
host="${url#https://}"; host="${host#http://}"; host="${host%%/*}"
|
|
strict=1
|
|
case "$host" in
|
|
*hacktivism.ch) strict=1 ;;
|
|
*)
|
|
# foreign exchange (e.g. taler-ops) — soft unless it is our configured EXCHANGE_PUBLIC
|
|
if [ "$url" = "${EXCHANGE_PUBLIC}" ] || [ "$url" = "${EXCHANGE_PUBLIC}/" ]; then
|
|
strict=1
|
|
else
|
|
strict=0
|
|
fi
|
|
;;
|
|
esac
|
|
check_exchange_alt_units "exchange ${cur:-?} ${host}" "$url" "$cur" "$strict"
|
|
done <<<"$list"
|
|
}
|
|
|
|
SECRETS_ROOT="${SECRETS_ROOT:-}"
|
|
if [ -z "$SECRETS_ROOT" ]; then
|
|
for d in \
|
|
"$(cd "$(dirname "${BASH_SOURCE[0]}")/../../.." && pwd)/koopa-admin-secrets/koopa/host-root" \
|
|
"/Users/newkamek/src/koopa/koopa-admin-secrets/koopa/host-root" \
|
|
"$HOME/src/koopa/koopa-admin-secrets/koopa/host-root"
|
|
do
|
|
if [ -d "$d/taler-bank" ]; then SECRETS_ROOT=$d; break; fi
|
|
done
|
|
fi
|
|
|
|
read_secret() {
|
|
local rel="$1"
|
|
if [ -n "${SECRETS_ROOT:-}" ] && [ -f "${SECRETS_ROOT}/${rel}" ]; then
|
|
tr -d '\n' <"${SECRETS_ROOT}/${rel}"
|
|
return 0
|
|
fi
|
|
koopa_ssh_ok || return 1
|
|
koopa_ssh_run 10 "tr -d '\\n' </root/$(basename "$rel") 2>/dev/null || true" 2>/dev/null
|
|
}
|
|
|
|
find_wallet_cli() {
|
|
# Must return a path suitable for: node "$WALLET_CLI" …
|
|
# Prefer *.mjs / bundled entry; plain /usr/bin/taler-wallet-cli is often a
|
|
# shell wrapper and cannot be passed to node.
|
|
if [ -n "${WALLET_CLI:-}" ] && [ -f "$WALLET_CLI" ]; then
|
|
echo "$WALLET_CLI"; return 0
|
|
fi
|
|
local c cand
|
|
for c in \
|
|
"${HOME}/.local/bin/taler-wallet-cli.mjs" \
|
|
"${HOME}/taler/opt/taler-wallet-cli/usr/lib/taler-wallet-cli/node_modules/taler-wallet-cli/bin/taler-wallet-cli.mjs" \
|
|
/home/hernani/taler/opt/taler-wallet-cli/usr/lib/taler-wallet-cli/node_modules/taler-wallet-cli/bin/taler-wallet-cli.mjs \
|
|
/Users/newkamek/src/taler/taler-typescript-core/packages/taler-wallet-cli/bin/taler-wallet-cli.mjs \
|
|
/usr/lib/taler-wallet-cli/node_modules/taler-wallet-cli/bin/taler-wallet-cli.mjs \
|
|
/usr/share/taler-wallet-cli/bin/taler-wallet-cli.mjs
|
|
do
|
|
[ -n "$c" ] && [ -f "$c" ] && { echo "$c"; return 0; }
|
|
done
|
|
# Debian package wrapper → resolve real .mjs
|
|
cand=$(command -v taler-wallet-cli 2>/dev/null || true)
|
|
if [ -n "$cand" ] && [ -f "$cand" ]; then
|
|
if head -1 "$cand" 2>/dev/null | grep -q 'node\|mjs'; then
|
|
# shebang node script or .mjs
|
|
echo "$cand"; return 0
|
|
fi
|
|
# follow symlink into package tree
|
|
if [ -L "$cand" ]; then
|
|
c=$(readlink -f "$cand" 2>/dev/null || true)
|
|
[ -n "$c" ] && [ -f "$c" ] && { echo "$c"; return 0; }
|
|
fi
|
|
fi
|
|
return 1
|
|
}
|