koopa-admin-log/scripts/taler-bank/goa-withdraw-ladder.sh
Hernâni Marques 4dbede51b4 bank: standalone GOA withdraw ladder helper script.
Fixed zero and max endpoints with random intermediate amounts for local tests.
2026-07-16 23:10:02 +02:00

305 lines
10 KiB
Bash
Executable file

#!/usr/bin/env bash
# Systematic GOA withdraw ladder on bank.hacktivism.ch
#
# Per landing / intro docs:
# 1) GET /intro/auto-account.json → personal goa-account-* (+ first pool URI)
# 2) Shared pool withdrawals (explorer) with server auto-confirm
# 3) wallet-cli: ToS + accept-uri + run-until-done each rung
# 4) amounts from atomic-GOA up until bank/wallet fails
#
# Prefer the monitoring phase (random increasing ranges + timings + report):
# ../taler-monitoring/taler-monitoring.sh ladder
# LADDER_MAX_RUNGS=10 ../taler-monitoring/check_goa_ladder.sh
#
# Usage (standalone, fixed ladder):
# ./goa-withdraw-ladder.sh
# MAX_RUNGS=12 ./goa-withdraw-ladder.sh
# AMOUNTS='GOA:0.000001 GOA:0.01 GOA:1 GOA:10 GOA:100' ./goa-withdraw-ladder.sh
#
# Env:
# BANK default https://bank.hacktivism.ch
# EXCHANGE default https://exchange.hacktivism.ch/
# EXP_USER default explorer
# EXP_PW_FILE explorer password file (for mint + confirm)
# WALLET_CLI path to taler-wallet-cli.mjs or binary
# WDB wallet sqlite path
# SHOTDIR result directory
set -euo pipefail
export PATH="/tmp/py313bin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin:${PATH}"
BANK="${BANK:-https://bank.hacktivism.ch}"
BANK="${BANK%/}"
EXCHANGE="${EXCHANGE:-https://exchange.hacktivism.ch/}"
EXP_USER="${EXP_USER:-explorer}"
EXP_PW_FILE="${EXP_PW_FILE:-/Users/newkamek/src/koopa/koopa-admin-secrets/koopa/host-root/taler-bank/bank-explorer-password.txt}"
SHOTDIR="${SHOTDIR:-/tmp/goa-ladder-$(date +%Y%m%d-%H%M%S)}"
WDB="${WDB:-$SHOTDIR/wallet.db}"
MAX_RUNGS="${MAX_RUNGS:-24}"
CLI_JS="${WALLET_CLI_JS:-/Users/newkamek/src/taler-typescript-core/packages/taler-wallet-cli/bin/taler-wallet-cli.mjs}"
mkdir -p "$SHOTDIR"
LOG="$SHOTDIR/ladder.log"
RESULTS="$SHOTDIR/results.tsv"
echo -e "rung\tamount\tstatus\twid\tnote" >"$RESULTS"
log() { printf '%s\n' "$*" | tee -a "$LOG"; }
die() { log "ERROR: $*"; exit 1; }
wcli() {
if [ -f "$CLI_JS" ]; then
node "$CLI_JS" --wallet-db="$WDB" --no-throttle "$@"
else
taler-wallet-cli --wallet-db="$WDB" --no-throttle "$@"
fi
}
# Default ladder via Python: fixed 0 + random low picks + random high ranges + fixed max.
# Prefer taler-monitoring check_goa_ladder.sh for full control (LADDER_* env).
default_amounts() {
python3 - <<'PY'
import math, random
from decimal import Decimal, ROUND_HALF_UP
MAX = Decimal("4503599627370496")
HIGH_FROM = Decimal("1000000")
HIGH_RUNGS = 12
CUR = "GOA"
def fmt(v: Decimal) -> str:
q = v.quantize(Decimal("0.00000001"), rounding=ROUND_HALF_UP)
if q == q.to_integral():
return "%s:%s" % (CUR, format(int(q), "d"))
return "%s:%s" % (CUR, format(q, "f").rstrip("0").rstrip("."))
def logu(lo, hi):
lo = max(lo, 1e-12)
if hi <= lo:
return hi
return math.exp(random.uniform(math.log(lo), math.log(hi)))
out = [fmt(Decimal(0))]
prev = Decimal(0)
# low bands (same idea as monitoring LADDER_RANGES without 0:0)
bands = [
(1e-6, 9e-6), (1e-5, 9e-5), (1e-4, 9e-4), (1e-3, 9e-3), (0.01, 0.09),
(0.1, 0.9), (1, 9), (10, 49), (50, 99), (100, 499), (500, 999),
(1000, 4999), (5000, 9999), (1e4, 5e4), (5e4, 1e5), (1e5, 5e5), (5e5, 2e6),
]
for lo, hi in bands:
lo2 = max(lo, float(prev) * 1.0000001 if prev > 0 else lo)
if lo2 > hi:
continue
v = Decimal(str(logu(lo2, hi))).quantize(Decimal("0.00000001"))
if v <= prev:
continue
if v >= MAX:
continue
out.append(fmt(v))
prev = v
band_lo = max(float(HIGH_FROM), float(prev) * 1.0000001)
band_hi = float(MAX - 1)
if band_lo < band_hi and HIGH_RUNGS > 0:
cuts = [band_lo, band_hi]
for _ in range(HIGH_RUNGS - 1):
cuts.append(logu(band_lo, band_hi))
cuts = sorted(set(cuts))
while len(cuts) < HIGH_RUNGS + 1:
cuts.append(logu(band_lo, band_hi))
cuts = sorted(set(cuts))
segs = [(cuts[i], cuts[i + 1]) for i in range(len(cuts) - 1) if cuts[i + 1] > cuts[i] * 1.001]
segs.sort(key=lambda t: t[0])
for r_lo, r_hi in segs[:HIGH_RUNGS]:
floor = max(r_lo, float(prev) * 1.0000001)
if floor >= r_hi:
continue
v = Decimal(str(logu(floor, r_hi))).quantize(Decimal(1))
if v <= prev or v >= MAX:
continue
out.append(fmt(v))
prev = v
out.append(fmt(MAX))
print("\n".join(out))
PY
}
if [ -n "${AMOUNTS:-}" ]; then
# shellcheck disable=SC2206
LADDER=($AMOUNTS)
else
LADDER=()
while IFS= read -r line; do
[ -n "$line" ] || continue
LADDER+=("$line")
[ "${#LADDER[@]}" -ge "$MAX_RUNGS" ] && break
done < <(default_amounts)
fi
[ -f "$EXP_PW_FILE" ] || die "missing explorer password: $EXP_PW_FILE"
EXP_PW="$(tr -d '\n' <"$EXP_PW_FILE")"
explorer_token() {
curl -sS -m 20 -u "${EXP_USER}:${EXP_PW}" \
-H 'Content-Type: application/json' \
-d '{"scope":"readwrite","duration":{"d_us":3600000000}}' \
"${BANK}/accounts/${EXP_USER}/token" \
| python3 -c 'import json,sys; print(json.load(sys.stdin)["access_token"])'
}
confirm_when_selected() {
local wid="$1" tok="$2"
local i st
for i in $(seq 1 30); do
st=$(curl -sS -m 12 "${BANK}/taler-integration/withdrawal-operation/${wid}" \
| python3 -c 'import json,sys; print(json.load(sys.stdin).get("status",""))' 2>/dev/null || true)
case "$st" in
selected)
code=$(curl -sS -m 20 -o "$SHOTDIR/conf-${wid}.json" -w '%{http_code}' -X POST \
-H "Authorization: Bearer ${tok}" \
-H 'Content-Type: application/json' -d '{}' \
"${BANK}/accounts/${EXP_USER}/withdrawals/${wid}/confirm")
log " auto-confirm HTTP $code (status was selected)"
return 0
;;
confirmed)
log " already confirmed"
return 0
;;
aborted)
log " aborted"
return 1
;;
esac
sleep 1
done
log " timeout waiting for selected (last=$st)"
return 1
}
mint_withdraw() {
local amount="$1"
curl -sS -m 30 -H "Authorization: Bearer ${TOK}" \
-H 'Content-Type: application/json' \
-d "{\"amount\":\"${amount}\"}" \
"${BANK}/accounts/${EXP_USER}/withdrawals"
}
wallet_avail() {
wcli balance 2>/dev/null | python3 -c '
import json,sys,re
t=sys.stdin.read()
i=t.find("{")
if i<0: print("0"); raise SystemExit
d=json.loads(t[i:t.rfind("}")+1])
for b in d.get("balances") or []:
a=b.get("available") or ""
if a.startswith("GOA:"):
print(a.split(":",1)[1]); raise SystemExit
print("0")
' 2>/dev/null || echo "0"
}
# --- main ---
log "SHOTDIR=$SHOTDIR"
log "BANK=$BANK EXCHANGE=$EXCHANGE"
log "ladder (${#LADDER[@]} rungs): ${LADDER[*]}"
log "=== 1) auto-account (bank.hacktivism.ch/intro) ==="
curl -sS -m 30 "${BANK}/intro/auto-account.json" | tee "$SHOTDIR/auto-account.json" | python3 -m json.tool | tee -a "$LOG" | head -40
ACCT_USER=$(python3 -c 'import json; print(json.load(open("'"$SHOTDIR"'/auto-account.json"))["username"])')
ACCT_PASS=$(python3 -c 'import json; print(json.load(open("'"$SHOTDIR"'/auto-account.json"))["password"])')
log "personal account: $ACCT_USER (balance GOA:0 — pool withdraws use explorer + auto-confirm)"
echo "$ACCT_USER" >"$SHOTDIR/account-user.txt"
echo "$ACCT_PASS" >"$SHOTDIR/account-pass.txt"
log "=== 2) explorer token + wallet init ==="
TOK=$(explorer_token)
log "explorer token ok"
wcli exchanges add "$EXCHANGE" 2>&1 | tee "$SHOTDIR/ex-add.out" | tail -5 || true
wcli exchanges update "$EXCHANGE" 2>&1 | tee "$SHOTDIR/ex-upd.out" | tail -5 || true
wcli exchanges accept-tos "$EXCHANGE" 2>&1 | tee "$SHOTDIR/ex-tos.out" | tail -5 || true
n=0
fail_rung=""
for amt in "${LADDER[@]}"; do
n=$((n + 1))
log ""
log "======== rung $n / ${#LADDER[@]} amount=$amt ========"
WD=$(mint_withdraw "$amt" 2>&1) || true
echo "$WD" | tee "$SHOTDIR/wd-${n}.json" >/dev/null
if ! echo "$WD" | python3 -c 'import json,sys; json.load(sys.stdin)' 2>/dev/null; then
log "FAIL mint: $WD"
echo -e "${n}\t${amt}\tFAIL_MINT\t-\t$(echo "$WD" | tr '\n' ' ' | head -c 200)" >>"$RESULTS"
fail_rung="$amt"
break
fi
WID=$(echo "$WD" | python3 -c 'import json,sys; print(json.load(sys.stdin).get("withdrawal_id",""))')
URI=$(echo "$WD" | python3 -c 'import json,sys; u=json.load(sys.stdin).get("taler_withdraw_uri",""); print(u.replace(":443/","/"))')
[ -n "$WID" ] && [ -n "$URI" ] || {
log "FAIL parse mint: $WD"
echo -e "${n}\t${amt}\tFAIL_PARSE\t-\t" >>"$RESULTS"
fail_rung="$amt"
break
}
log " WID=$WID"
log " URI=$URI"
before=$(wallet_avail)
if ! wcli withdraw accept-uri --exchange "$EXCHANGE" "$URI" 2>&1 | tee "$SHOTDIR/accept-${n}.out" | tail -20; then
# still try confirm if selected
true
fi
if ! confirm_when_selected "$WID" "$TOK"; then
log "FAIL confirm $amt"
echo -e "${n}\t${amt}\tFAIL_CONFIRM\t${WID}\t" >>"$RESULTS"
fail_rung="$amt"
# keep going? user asked until it no longer works — stop
break
fi
settled=0
for r in $(seq 1 20); do
wcli run-until-done 2>&1 | tee -a "$SHOTDIR/rud-${n}.out" >/dev/null || true
after=$(wallet_avail)
# progress if available increased (float-safe string compare via python)
if python3 -c "import sys; sys.exit(0 if float(sys.argv[1])>float(sys.argv[2]) else 1)" "$after" "$before" 2>/dev/null; then
settled=1
log " settled avail GOA:$after (was $before)"
break
fi
sleep 1
done
after=$(wallet_avail)
if [ "$settled" = "1" ]; then
log "OK $amt → wallet GOA:$after"
echo -e "${n}\t${amt}\tOK\t${WID}\tavail=${after}" >>"$RESULTS"
else
# bank may be confirmed but wire lag — check transfer_done
xfer=$(curl -sS -m 10 "${BANK}/taler-integration/withdrawal-operation/${WID}" \
| python3 -c 'import json,sys; d=json.load(sys.stdin); print(d.get("transfer_done"), d.get("status"))')
log " not settled yet transfer_done/status=$xfer avail=$after"
if echo "$xfer" | grep -q True; then
echo -e "${n}\t${amt}\tOK_BANK_LAG\t${WID}\tavail=${after}" >>"$RESULTS"
log "OK bank confirmed (wallet lag) $amt"
else
echo -e "${n}\t${amt}\tFAIL_SETTLE\t${WID}\tavail=${after} $xfer" >>"$RESULTS"
fail_rung="$amt"
break
fi
fi
done
log ""
log "=== final balance ==="
wcli balance 2>&1 | tee "$SHOTDIR/balance-final.out"
log ""
log "=== results ==="
column -t -s $'\t' "$RESULTS" 2>/dev/null || cat "$RESULTS"
log "SHOTDIR=$SHOTDIR"
if [ -n "$fail_rung" ]; then
log "STOPPED at first failure: $fail_rung"
exit 2
fi
log "All rungs OK"
exit 0