From ee99f0de9938c2968eb457c83c54de9f59121977 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hern=C3=A2ni=20Marques?= Date: Sun, 30 Aug 2026 17:33:11 +0200 Subject: [PATCH] release 1.24.0: zero-pay phase (stage.my default, TESTPAYSAN templates) --- CLI-AUTOMATION-NOTES.md | 20 ++ README.md | 12 + TESTS.md | 1 + VERSION | 2 +- VERSIONS.md | 1 + check_zero_pay.sh | 472 +++++++++++++++++++++++++++++++++++++++ lib.sh | 1 + pay_talersticker_zero.sh | 11 + taler-monitoring.sh | 8 +- 9 files changed, 526 insertions(+), 2 deletions(-) create mode 100755 check_zero_pay.sh create mode 100755 pay_talersticker_zero.sh diff --git a/CLI-AUTOMATION-NOTES.md b/CLI-AUTOMATION-NOTES.md index b4823b1..5529c87 100644 --- a/CLI-AUTOMATION-NOTES.md +++ b/CLI-AUTOMATION-NOTES.md @@ -208,6 +208,26 @@ Sources: `https://bank.hacktivism.ch/intro/stats.json`, exchange/merchant intro | **Workaround** | Monitoring builds URI externally then `handle-uri --yes`. | | **Wanted** | Documented recipe only, or `wallet-cli testing pay-template --base=… --instance=… --id=…` for demos. | +### 9a. TESTPAYSAN public template POST `{}` → HTTP 451 (demo exchange) + +| | | +|--|--| +| **Seen** | 2026-08-30 magikoopa; `/monitoring_err` e2e.pay; `zero-pay` | +| **Area** | **ops** | +| **Problem** | `POST …/instances/jardin-du-creux/templates/oeufs-6` with `{}` → **451** code **2513** hard legal limits. `exchange_rejections` include **https://exchange.demo.taler.net/** currency mismatch. Merchant still trusts demo KUDOS, not `stage.exchange.lefrancpaysan.ch`. Sending `amount`/`summary` → **409** 2860/2861 (template already has contract). | +| **Workaround** | Empty POST is correct (same as shop-ui). Fix merchant: enable `[merchant-exchange-testpaysan]`, disable demo exchanges. Bank auto-confirm is a different hop (withdraw). | +| **Wanted** | Stage merchant only TESTPAYSAN exchange. | + +### 9b. stage.my has no `stickers` template + +| | | +|--|--| +| **Seen** | 2026-08-30 `zero-pay` `-d stage.taler-ops.ch` instance `admin` | +| **Area** | **ops** | +| **Problem** | Public POST `/templates/stickers` → **2018** template not found. Production `my.taler-ops.ch` instance `talersticker` works. Stage instance `tops` exists without that template. | +| **Workaround** | Create CHF:0 `stickers` template on stage.my (merchant token). Until then phase ERROR is correct. | +| **Wanted** | Same public template as prod on stage. | + --- ## 10. Amount / currency parsing edge cases diff --git a/README.md b/README.md index 2c08f4e..d472e79 100644 --- a/README.md +++ b/README.md @@ -144,6 +144,18 @@ Each check prints **global** and **grouped** ids: # (bank-admin-password.txt under /mnt/data/stagepaysan/bank/secrets/) # Override: INSIDE_SSH=… INSIDE_PROFILE=stage-lfp +# Stage TOPS (default) · public-template CHF:0 — phase zero-pay +# (same HTTP path as e2e_one_pay_public_template; no merchant token) +./taler-monitoring.sh -d stage.taler-ops.ch zero-pay +COUNT=100 ./pay_talersticker_zero.sh +# TESTPAYSAN shop (empty POST {}; bank auto-confirm on stagepaysan) +./taler-monitoring.sh -d stage.lefrancpaysan.ch zero-pay +COUNT=100 GAP_S=0.5 ./pay_talersticker_zero.sh # 0.5s idle between pays/blocks +COUNT=100 PARALLEL=4 ./pay_talersticker_zero.sh # 4 wallets, 4 pays per block +COUNT=100 PARALLEL=10 GAP_S=0.5 ./pay_talersticker_zero.sh +COUNT=100 PARALLEL=all ./pay_talersticker_zero.sh # one block, COUNT wallets +# OUTDIR=/tmp/sticker-zero COUNT=3 ./pay_talersticker_zero.sh # smoke + # Other domains — public HTTPS only (pass e2e/inside explicitly) ./taler-monitoring.sh -d taler.net ./taler-monitoring.sh --domain taler-ops.ch diff --git a/TESTS.md b/TESTS.md index 78c3c11..f7b199e 100644 --- a/TESTS.md +++ b/TESTS.md @@ -122,6 +122,7 @@ When filing an issue, quote the full id + label, e.g. |------------|---------| | **versions.outside-** | deb.taler.net suite index | | **versions.host-** | host-podman: packages via local `podman exec` (`INSIDE_PODMAN=1`). Stage-lfp uses `stage-lfp-*` names even in this mode (not GOA `taler-hacktivism*`) | +| **zero-pay.** | public-template pays (`check_zero_pay.sh`); COUNT=1 in phase | | **versions.ssh-** | ssh: packages via `KOOPA_SSH` / `INSIDE_SSH` from laptop/remote | | **versions.compare-** | installed vs suite | | **aptdeploy.trixie-fresh-** | fresh rebuild container `…-apt-src-trixie` | diff --git a/VERSION b/VERSION index 63f23d2..53cc1a6 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -1.23.9 +1.24.0 diff --git a/VERSIONS.md b/VERSIONS.md index 3090309..0bfac91 100644 --- a/VERSIONS.md +++ b/VERSIONS.md @@ -17,6 +17,7 @@ Git tags: `vMAJOR.FEATURE.FIX` (e.g. `v1.8.0`). File `VERSION` omits the `v` pre | Tag | Date (UTC) | Notes | |-----|------------|--------| +| **v1.24.0** | 2026-08-30 | **Feature / zero-pay:** public-template pays as phase `zero-pay` (`check_zero_pay.sh`). Default merchant **stage.my.taler-ops.ch** (CHF:0 stickers). Wrapper `pay_talersticker_zero.sh` COUNT=100. TESTPAYSAN: empty POST `{}` + auto-confirm of bank wires (host `auto-confirm-withdrawals`). Not in default 4h PHASES (use COUNT=1). | | **v1.23.9** | 2026-08-30 | **Bugfix / versions:** `INSIDE_PODMAN=1` on stagepaysan still used GOA container names → empty `dpkg-query` → **versions.host-04** ERROR on `/monitoring_err`. Stage-lfp names apply in host-podman too; `run-fp-stage-monitoring.sh` exports `INSIDE_*_CTR`. | | **v1.23.8** | 2026-08-29 | **Bugfix / ladder:** harness-style wallet daemon — `ladder/lib_wallet_serve.sh` (`advanced serve` + `--wallet-connection`, default `LADDER_WALLET_SERVE=1`). Mint sends `exchange_url`; settle/pay-wait skip one-shot `run-pending` when serve is up. Aligns mon ladder with taler-harness WalletService + e2e serve path (no run-until-done). CLI-AUTOMATION-NOTES §1a. | | **v1.23.7** | 2026-07-22 | **FP stage host-agent:** ship `taler-monitoring-fp-stage.{service,timer}`; `install-host-agent.sh --fp-stage` (stagepaysan) enables FP mon only, disables GOA timers on that host, enforces suite tip knobs. Same sticky/publish pipeline as GOA. | diff --git a/check_zero_pay.sh b/check_zero_pay.sh new file mode 100755 index 0000000..2c99066 --- /dev/null +++ b/check_zero_pay.sh @@ -0,0 +1,472 @@ +#!/usr/bin/env bash +# Phase zero-pay: public-template pays (CHF:0 stickers / TESTPAYSAN shop). +# Same path as e2e_one_pay_public_template. Never run-until-done. +# Wrapper: pay_talersticker_zero.sh (COUNT default 100). +# +# Env: COUNT (phase default 1) GAP_S PARALLEL MERCHANT INST TEMPLATE AMOUNT +# EXCHANGE ZERO_PAY_POST=amount|empty HANDLE_SECS SETTLE_TRIES SETTLE_SLEEP +# +set -euo pipefail + +ROOT=$(cd "$(dirname "$0")" && pwd) +# shellcheck source=lib.sh +source "$ROOT/lib.sh" +# shellcheck source=metrics.sh +source "$ROOT/metrics.sh" +set_area zero-pay + +# Homebrew python3 (>=3.11) must beat /usr/bin/python3 3.9 — taler-helper-sqlite3. +export PATH="/opt/homebrew/bin:${HOME}/.local/bin:/usr/local/bin:/usr/bin:/bin:${PATH:-}" + +die() { echo "ERROR: $*" >&2; fail "zero-pay" "$*" 2>/dev/null || true; exit 1; } + +COUNT="${COUNT:-${ZERO_PAY_COUNT:-1}}" +GAP_S="${GAP_S:-0}" +PARALLEL="${PARALLEL:-1}" +MERCHANT="${MERCHANT:-${MERCHANT_PUBLIC:-https://stage.my.taler-ops.ch}}" +MERCHANT="${MERCHANT%/}" +CUR="${EXPECT_CURRENCY:-${CUR:-}}" +if [ "$CUR" = "TESTPAYSAN" ]; then + INST="${INST:-${ZERO_PAY_INST:-jardin-du-creux}}" + TEMPLATE="${TEMPLATE:-${ZERO_PAY_TEMPLATE:-oeufs-6}}" + AMOUNT="${AMOUNT:-${ZERO_PAY_AMOUNT:-TESTPAYSAN:5}}" + EXCHANGE="${EXCHANGE:-${EXCHANGE_PUBLIC:-https://stage.exchange.lefrancpaysan.ch/}}" + ZERO_PAY_POST="${ZERO_PAY_POST:-empty}" +else + INST="${INST:-${ZERO_PAY_INST:-admin}}" + TEMPLATE="${TEMPLATE:-${ZERO_PAY_TEMPLATE:-stickers}}" + AMOUNT="${AMOUNT:-${ZERO_PAY_AMOUNT:-CHF:0}}" + EXCHANGE="${EXCHANGE:-${EXCHANGE_PUBLIC:-https://exchange.stage.taler-ops.ch/}}" + ZERO_PAY_POST="${ZERO_PAY_POST:-amount}" +fi +EXCHANGE="${EXCHANGE%/}/" +HANDLE_SECS="${HANDLE_SECS:-25}" +SETTLE_TRIES="${SETTLE_TRIES:-40}" +SETTLE_SLEEP="${SETTLE_SLEEP:-0.15}" +case "$GAP_S" in + ''|*[!0-9.]*|*.*.*) die "GAP_S must be a non-negative number (got $GAP_S)" ;; +esac +case "$COUNT" in + ''|*[!0-9]*) die "COUNT must be a positive integer (got $COUNT)" ;; +esac +[ "$COUNT" -ge 1 ] || die "COUNT must be >= 1" +case "$PARALLEL" in + all|ALL) PARALLEL="$COUNT" ;; + ''|*[!0-9]*) die "PARALLEL must be a positive integer or all (got $PARALLEL)" ;; +esac +[ "$PARALLEL" -ge 1 ] || die "PARALLEL must be >= 1" +if [ "$PARALLEL" -gt "$COUNT" ]; then + PARALLEL="$COUNT" +fi + +OUTDIR="${OUTDIR:-/tmp/talersticker-zero-$(date +%Y%m%d-%H%M%S)}" +mkdir -p "$OUTDIR" +TSV="$OUTDIR/results.tsv" +LOG="$OUTDIR/run.log" +SCRATCH="$OUTDIR/scratch" +mkdir -p "$SCRATCH" +SERVE_PIDFILE="$OUTDIR/serve.pids" + +if [ -z "${TALER_MON_STATE:-}" ]; then + exec > >(tee -a "$LOG") 2>&1 +fi + +now_ms() { python3 -c 'import time; print(int(time.time()*1000))'; } + +slot_db() { echo "$OUTDIR/wallet-$1.sqlite3"; } +slot_sock() { echo "$OUTDIR/wallet-$1.sock"; } +slot_log() { echo "$OUTDIR/wallet-serve-$1.log"; } + +stop_wallet_serve() { + if [ -f "$SERVE_PIDFILE" ]; then + while read -r pid; do + [ -n "$pid" ] || continue + kill "$pid" 2>/dev/null || true + wait "$pid" 2>/dev/null || true + done <"$SERVE_PIDFILE" + rm -f "$SERVE_PIDFILE" + fi + rm -f "$OUTDIR"/wallet-*.sock 2>/dev/null || true +} +trap 'stop_wallet_serve' EXIT + +_wcli_exec() { + local cap="$1" + shift + local -a cmd + cmd=( "$NODE_BIN" "$WALLET_CLI" "$@" ) + if command -v timeout >/dev/null 2>&1; then + timeout -k 2 "$cap" "${cmd[@]}" + elif command -v gtimeout >/dev/null 2>&1; then + gtimeout --kill-after=2 "$cap" "${cmd[@]}" + else + perl -e 'alarm shift; exec @ARGV' "$cap" "${cmd[@]}" + fi +} + +wcli_slot() { + local slot="$1" + local cap="$2" + shift 2 + local sock db + sock=$(slot_sock "$slot") + db=$(slot_db "$slot") + if [ -S "$sock" ]; then + _wcli_exec "$cap" --wallet-connection="$sock" --no-throttle "$@" + else + _wcli_exec "$cap" --wallet-db="$db" --no-throttle --skip-defaults "$@" + fi +} + +start_wallet_slot() { + local slot="$1" + local sock db log + sock=$(slot_sock "$slot") + db=$(slot_db "$slot") + log=$(slot_log "$slot") + rm -f "$sock" + "$NODE_BIN" "$WALLET_CLI" --wallet-db="$db" --no-throttle --skip-defaults \ + advanced serve --unix-path "$sock" \ + >"$log" 2>&1 & + local pid=$! + echo "$pid" >>"$SERVE_PIDFILE" + local i + for i in $(seq 1 50); do + if [ -S "$sock" ] && kill -0 "$pid" 2>/dev/null; then + info "wallet serve slot=$slot pid=$pid" + return 0 + fi + sleep 0.1 + done + echo "WARN: wallet serve slot=$slot socket not ready" + return 1 +} + +find_wallet_cli() { + if [ -n "${WALLET_CLI:-}" ] && [ -f "$WALLET_CLI" ]; then + printf '%s\n' "$WALLET_CLI" + return 0 + fi + local c + for c in \ + "${HOME}/src/llm-qwythos/taler/taler-typescript-core/packages/taler-wallet-cli/bin/taler-wallet-cli.mjs" \ + "${HOME}/src/taler/taler-typescript-core/packages/taler-wallet-cli/bin/taler-wallet-cli.mjs" \ + "${HOME}/.local/bin/taler-wallet-cli.mjs" \ + "${HOME}/src/llm-qwythos/taler/taler-typescript-core/packages/taler-wallet-cli/dist/taler-wallet-cli-bundled.cjs" + do + [ -f "$c" ] && { printf '%s\n' "$c"; return 0; } + done + return 1 +} + +NODE_BIN=$(command -v node) || die "node not found" +python3 - <<'PY' || die "python3 >= 3.11 required (taler-helper-sqlite3)" +import sys +raise SystemExit(0 if sys.version_info >= (3, 11) else 1) +PY +command -v taler-helper-sqlite3 >/dev/null || die "taler-helper-sqlite3 not on PATH" +WALLET_CLI=$(find_wallet_cli) || die "taler-wallet-cli .mjs not found (set WALLET_CLI=)" + +json_get() { + python3 -c 'import json,sys +p,k=sys.argv[1],sys.argv[2] +try: + d=json.load(open(p)) + v=d + for part in k.split("."): + if isinstance(v, dict): + v=v.get(part) + else: + v=None + break + print("" if v is None else v) +except Exception: + print("") +' "$@" +} + +create_template_order() { + local summary="$1" out="$2" + local body url + if [ "${ZERO_PAY_POST}" = "empty" ]; then + body='{}' + else + body=$(python3 -c 'import json,sys; print(json.dumps({"amount":sys.argv[1],"summary":sys.argv[2]}))' "$AMOUNT" "$summary") + fi + url="${MERCHANT}/instances/${INST}/templates/${TEMPLATE}" + if [ "$INST" = "admin" ] || [ "$INST" = "default" ] || [ -z "$INST" ]; then + url="${MERCHANT}/templates/${TEMPLATE}" + fi + curl -sS -m 20 -X POST -H "Content-Type: application/json" -d "$body" "$url" >"$out" +} + +order_public() { + local oid="$1" tok="$2" out="$3" + local qoid qtok base + qoid=$(python3 -c 'import urllib.parse,sys; print(urllib.parse.quote(sys.argv[1], safe=""))' "$oid") + qtok=$(python3 -c 'import urllib.parse,sys; print(urllib.parse.quote(sys.argv[1], safe=""))' "$tok") + if [ "$INST" = "admin" ] || [ "$INST" = "default" ] || [ -z "$INST" ]; then + base="${MERCHANT}/orders/${qoid}?token=${qtok}" + else + base="${MERCHANT}/instances/${INST}/orders/${qoid}?token=${qtok}" + fi + curl -sS -m 12 "$base" >"$out" +} + +order_is_paid() { + python3 -c 'import json,sys +try: + d=json.load(open(sys.argv[1])) +except Exception: + raise SystemExit(1) +if d.get("paid") is True or str(d.get("order_status","")).lower()=="paid": + raise SystemExit(0) +if "refunded" in d or "refund_amount" in d: + raise SystemExit(0) +raise SystemExit(1) +' "$1" +} + +wallet_handle_done() { + grep -q "Finished with status .done." "$1" 2>/dev/null +} + +write_row() { + # n oid create handle settle total status payuri + printf '%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\n' "$@" +} + +# One payment. Args: n slot. Always writes scratch/row.N.tsv and scratch/msg.N.txt. +do_one_pay() { + local n="$1" slot="$2" + local tag + tag=$(printf '%03d' "$n") + local nonce summary create_f status_f pay_f row_f msg_f + nonce="${n}-$(date +%s)-$RANDOM" + summary="taler-monitoring zero-pay #${n} ${nonce}" + create_f="$SCRATCH/ord-$tag.json" + status_f="$SCRATCH/st-$tag.json" + pay_f="$SCRATCH/pay-$tag.out" + row_f="$SCRATCH/row.$n.tsv" + msg_f="$SCRATCH/msg.$n.txt" + + local t0 t1 t_create t_h0 t_h1 t_s0 t_s1 + local create_ms handle_ms settle_ms total_ms + local oid tok payuri hint hc status + local settled merchant_paid i fail_tail + + t0=$(now_ms) + if ! create_template_order "$summary" "$create_f"; then + t1=$(now_ms) + create_ms=$((t1-t0)) + write_row "$n" "" "$create_ms" "" "" "$create_ms" "CREATE_HTTP_FAIL" "" >"$row_f" + echo "[$tag] CREATE_HTTP_FAIL ${create_ms}ms slot=$slot" >"$msg_f" + return 0 + fi + oid=$(json_get "$create_f" order_id) + tok=$(json_get "$create_f" token) + t_create=$(now_ms) + create_ms=$((t_create-t0)) + if [ -z "$oid" ] || [ -z "$tok" ]; then + hint=$(json_get "$create_f" hint) + write_row "$n" "" "$create_ms" "" "" "$create_ms" "CREATE_FAIL" "" >"$row_f" + echo "[$tag] CREATE_FAIL ${create_ms}ms hint=${hint} slot=$slot" >"$msg_f" + return 0 + fi + + order_public "$oid" "$tok" "$status_f" || true + payuri=$(json_get "$status_f" taler_pay_uri) + if [ -z "$payuri" ]; then + local mh + mh=$(python3 -c 'from urllib.parse import urlparse; print(urlparse("'"$MERCHANT"'").hostname or "")') + payuri="taler://pay/${mh}/instances/${INST}/${oid}/?c=${tok}" + fi + payuri=$(printf '%s' "$payuri" | sed 's/:443\//\//g; s/:443?/?/g') + + t_h0=$(now_ms) + set +e + wcli_slot "$slot" "$HANDLE_SECS" handle-uri --yes --non-interactive "$payuri" >"$pay_f" 2>&1 + hc=$? + set -e + t_h1=$(now_ms) + handle_ms=$((t_h1-t_h0)) + if grep -q "Payment failed" "$pay_f" 2>/dev/null; then + hc=1 + fi + + settled=0 + merchant_paid=0 + t_s0=$(now_ms) + i=1 + while [ "$i" -le "$SETTLE_TRIES" ]; do + order_public "$oid" "$tok" "$status_f" || true + if order_is_paid "$status_f"; then + merchant_paid=1 + settled=1 + break + fi + if wallet_handle_done "$pay_f"; then + settled=1 + fi + if [ "$settled" = "1" ] && [ "$i" -ge 3 ]; then + break + fi + sleep "$SETTLE_SLEEP" + i=$((i+1)) + done + t_s1=$(now_ms) + settle_ms=$((t_s1-t_s0)) + total_ms=$((t_s1-t0)) + + if [ "$merchant_paid" = "1" ] || [ "$settled" = "1" ]; then + status=PAID + elif [ "$hc" = "0" ]; then + status=HANDLE_OK_UNSETTLED + else + status=HANDLE_FAIL + fi + write_row "$n" "$oid" "$create_ms" "$handle_ms" "$settle_ms" "$total_ms" "$status" "$payuri" >"$row_f" + echo "[$tag] $status oid=$oid create=${create_ms}ms handle=${handle_ms}ms settle=${settle_ms}ms total=${total_ms}ms slot=$slot" >"$msg_f" + if [ "$status" != "PAID" ]; then + fail_tail=$(tail -c 180 "$pay_f" | tr '\n' ' ') + echo " handle-exit=${hc} tail=${fail_tail}" >>"$msg_f" + fi +} + +prep_slot_exchange() { + local slot="$1" + wcli_slot "$slot" 40 exchanges add "$EXCHANGE" >"$OUTDIR/exchanges-add-$slot.out" 2>&1 || true + wcli_slot "$slot" 40 exchanges tos "$EXCHANGE" >"$OUTDIR/exchanges-tos-$slot.out" 2>&1 || true + wcli_slot "$slot" 40 exchanges accept-tos "$EXCHANGE" >"$OUTDIR/exchanges-accept-tos-$slot.out" 2>&1 || true +} + +section "zero-pay · ${AMOUNT} x ${COUNT} · ${INST}/${TEMPLATE}" +info "merchant $MERCHANT" +info "instance $INST" +info "template $TEMPLATE amount=$AMOUNT" +info "wallet $WALLET_CLI" +info "python $(command -v python3) $(python3 -c 'import sys; print(".".join(map(str,sys.version_info[:3])))')" +info "helper $(command -v taler-helper-sqlite3)" +info "outdir $OUTDIR" +info "gap ${GAP_S}s between blocks (not in total_ms)" +info "parallel $PARALLEL per block (own wallet serve per slot)" +if [ "$PARALLEL" -gt 20 ]; then + echo "WARN: PARALLEL=$PARALLEL starts that many wallet-cli node processes" +fi + +wcli_slot 0 25 version >"$OUTDIR/wallet-version.txt" 2>&1 || true +info "wallet-cli $(head -1 "$OUTDIR/wallet-version.txt" 2>/dev/null || echo '?')" + +: >"$SERVE_PIDFILE" +slot=0 +while [ "$slot" -lt "$PARALLEL" ]; do + start_wallet_slot "$slot" || true + prep_slot_exchange "$slot" + slot=$((slot+1)) +done + +wcli_slot 0 20 balance >"$OUTDIR/balance-before.txt" 2>&1 || true +info "balance before: $(tr '\n' ' ' < "$OUTDIR/balance-before.txt" | tail -c 200)" + +printf 'n\torder_id\tcreate_ms\thandle_ms\tsettle_ms\ttotal_ms\tstatus\tpay_uri\n' >"$TSV" + +ok_n=0 +fail_n=0 +RUN_START_MS=$(now_ms) + +n=1 +while [ "$n" -le "$COUNT" ]; do + block_end=$((n + PARALLEL - 1)) + if [ "$block_end" -gt "$COUNT" ]; then + block_end="$COUNT" + fi + slot=0 + k="$n" + pay_pids="" + while [ "$k" -le "$block_end" ]; do + do_one_pay "$k" "$slot" & + pay_pids="$pay_pids $!" + slot=$((slot+1)) + k=$((k+1)) + done + set +e + for pid in $pay_pids; do + wait "$pid" + done + set -e + k="$n" + while [ "$k" -le "$block_end" ]; do + if [ -f "$SCRATCH/msg.$k.txt" ]; then + cat "$SCRATCH/msg.$k.txt" + else + echo "[$(printf '%03d' "$k")] MISSING (worker died)" + fi + if [ -f "$SCRATCH/row.$k.tsv" ]; then + cat "$SCRATCH/row.$k.tsv" >>"$TSV" + else + write_row "$k" "" "" "" "" "" "WORKER_DIED" "" >>"$TSV" + fi + k=$((k+1)) + done + n=$((block_end + 1)) + if [ "$n" -le "$COUNT" ]; then + case "$GAP_S" in + 0|0.0|0.00) ;; + *) sleep "$GAP_S" ;; + esac + fi +done + +RUN_END_MS=$(now_ms) +wall_ms=$((RUN_END_MS-RUN_START_MS)) + +ok_n=$(awk -F'\t' 'NR>1 && $7=="PAID" {c++} END {print c+0}' "$TSV") +fail_n=$((COUNT - ok_n)) + +section "zero-pay · summary" +info "ok=$ok_n fail=$fail_n count=$COUNT parallel=$PARALLEL gap_s=$GAP_S wall_ms=$wall_ms out=$TSV" +python3 - "$TSV" "$ok_n" "$fail_n" "$COUNT" "$wall_ms" "$OUTDIR/summary.txt" "$GAP_S" "$PARALLEL" <<'PY' +import sys +tsv, ok, fail, count, wall, outp, gap_s, parallel = sys.argv[1:] +ok, fail, count, wall = int(ok), int(fail), int(count), int(wall) +rows=[] +with open(tsv) as f: + next(f) + for line in f: + p=line.rstrip('\n').split('\t') + if len(p)<7: continue + rows.append(p) +paid=[r for r in rows if r[6]=='PAID'] +def nums(idx, rs): + xs=[int(r[idx]) for r in rs if r[idx].isdigit()] + return xs +def fmt(xs): + if not xs: return "n/a" + xs=sorted(xs) + med=xs[len(xs)//2] + return "n=%d min=%d med=%d max=%d mean=%.1f" % (len(xs), xs[0], med, xs[-1], sum(xs)/len(xs)) +lines=[] +lines.append("ok=%d fail=%d count=%d parallel=%s wall_ms=%d wall_s=%.1f gap_s=%s" % ( + ok, fail, count, parallel, wall, wall/1000.0, gap_s)) +lines.append("create_ms " + fmt(nums(2, paid))) +lines.append("handle_ms " + fmt(nums(3, paid))) +lines.append("settle_ms " + fmt(nums(4, paid))) +lines.append("total_ms " + fmt(nums(5, paid))) +if paid: + lines.append("first_paid_oid=%s last_paid_oid=%s" % (paid[0][1], paid[-1][1])) +text="\n".join(lines)+"\n" +open(outp,"w").write(text) +print(text) +PY + +wcli_slot 0 20 balance >"$OUTDIR/balance-after.txt" 2>&1 || true +info "balance after: $(tr '\n' ' ' < "$OUTDIR/balance-after.txt" | tail -c 200)" + +if [ "$ok_n" -lt "$COUNT" ]; then + fail "zero-pay" "ok=$ok_n fail=$fail_n count=$COUNT out=$TSV" + summary + exit 1 +fi +ok "zero-pay" "ok=$ok_n count=$COUNT wall_ms=$wall_ms out=$TSV" +summary +exit 0 diff --git a/lib.sh b/lib.sh index 9187afd..0513d82 100755 --- a/lib.sh +++ b/lib.sh @@ -1565,6 +1565,7 @@ find_wallet_cli() { # shellcheck disable=SC2086 for c in ${WALLET_CLI_CANDIDATES:-} \ "${HOME}/.local/bin/taler-wallet-cli.mjs" \ + "${HOME}/src/llm-qwythos/taler/taler-typescript-core/packages/taler-wallet-cli/bin/taler-wallet-cli.mjs" \ "${HOME}/src/taler/taler-typescript-core/packages/taler-wallet-cli/bin/taler-wallet-cli.mjs" \ "${HOME}/src/taler/taler-typescript-core/packages/taler-wallet-cli/dist/taler-wallet-cli.mjs" \ "${HOME}/src/taler/taler-typescript-core/packages/taler-wallet-cli/dist/taler-wallet-cli-local.mjs" \ diff --git a/pay_talersticker_zero.sh b/pay_talersticker_zero.sh new file mode 100755 index 0000000..ae231de --- /dev/null +++ b/pay_talersticker_zero.sh @@ -0,0 +1,11 @@ +#!/usr/bin/env bash +# Thin wrapper → phase zero-pay (COUNT default 100). +# Default merchant: https://stage.my.taler-ops.ch (not production). +# COUNT=3 ./pay_talersticker_zero.sh +# ./pay_talersticker_zero.sh # 100 pays, stage.my stickers CHF:0 +# ./taler-monitoring.sh -d stage.lefrancpaysan.ch zero-pay +set -euo pipefail +ROOT=$(cd "$(dirname "$0")" && pwd) +export COUNT="${COUNT:-100}" +export TALER_DOMAIN="${TALER_DOMAIN:-stage.taler-ops.ch}" +exec "$ROOT/taler-monitoring.sh" -d "$TALER_DOMAIN" zero-pay diff --git a/taler-monitoring.sh b/taler-monitoring.sh index 5379f3f..58a4929 100755 --- a/taler-monitoring.sh +++ b/taler-monitoring.sh @@ -93,6 +93,8 @@ Phases: sanity public + optional server server server-side only (SSH) e2e withdraw + pay (small amounts; remote aborts on login/KYC) + zero-pay public-template pays (stage.my CHF:0 stickers; TESTPAYSAN shop). COUNT=1 in phase; wrapper COUNT=100 + sticker-pay alias of zero-pay ladder amount ladder classic (default 23-rung withdraw + pay; any currency) max-ladder max-search: highest withdrawable + highest payable (≤ available) goa-ladder alias of ladder (legacy phase name; same check_amount_ladder.sh) @@ -267,7 +269,7 @@ while [ $# -gt 0 ]; do CURRENCY_OVERRIDE="$2"; shift 2 ;; --no-probe) NO_PROBE=1; shift ;; - urls|inside|versions|sanity|server|e2e|ladder|goa-ladder|max-ladder|maxladder|auth401|aptdeploy|apt-deploy|apt_src|surface|ecosystem|mattermost|mail|monpages|pages|devtesting|franken|fake-franken|fake_franken|all|full) PHASES+=("$1"); shift ;; + urls|inside|versions|sanity|server|e2e|zero-pay|zeropay|sticker-pay|template-zero|ladder|goa-ladder|max-ladder|maxladder|auth401|aptdeploy|apt-deploy|apt_src|surface|ecosystem|mattermost|mail|monpages|pages|devtesting|franken|fake-franken|fake_franken|all|full) PHASES+=("$1"); shift ;; *) # bare domain shorthand: ./taler-monitoring.sh taler.net if [[ "$1" == *.* && "$1" != *://* && "$1" != -* ]]; then @@ -445,6 +447,7 @@ _progress_estimate_phase() { server) n=20 ;; # e2e: bank withdraw ladder emits many INFO lines (coins before/after each note) e2e) n=240 ;; + zero-pay|zeropay|sticker-pay|template-zero) n=15 ;; ladder|goa-ladder|max-ladder|maxladder) n=120 ;; auth401) n=70 ;; aptdeploy) n=40 ;; @@ -621,6 +624,9 @@ while [ "$_phase_idx" -lt "$_n_phases" ]; do sanity) run_phase sanity "$ROOT/check_sanity.sh" || ec=1 ;; server) run_phase server "$ROOT/check_server.sh" || ec=1 ;; e2e) run_phase e2e "$ROOT/check_e2e.sh" || ec=1 ;; + zero-pay|zeropay|sticker-pay|template-zero) + run_phase zero-pay "$ROOT/check_zero_pay.sh" || ec=1 + ;; ladder|goa-ladder) run_phase ladder "$ROOT/check_amount_ladder.sh" || ec=1 ;;