diff --git a/scripts/taler-bank/goa-withdraw-ladder.sh b/scripts/taler-bank/goa-withdraw-ladder.sh new file mode 100755 index 0000000..f66925c --- /dev/null +++ b/scripts/taler-bank/goa-withdraw-ladder.sh @@ -0,0 +1,305 @@ +#!/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