Compare commits

...

4 commits

17 changed files with 808 additions and 58 deletions

1
.gitignore vendored
View file

@ -1,4 +1,5 @@
secrets.env secrets.env
out/
__pycache__/ __pycache__/
*.pyc *.pyc
android-test/apks/ android-test/apks/

View file

@ -208,6 +208,46 @@ Sources: `https://bank.hacktivism.ch/intro/stats.json`, exchange/merchant intro
| **Workaround** | Monitoring builds URI externally then `handle-uri --yes`. | | **Workaround** | Monitoring builds URI externally then `handle-uri --yes`. |
| **Wanted** | Documented recipe only, or `wallet-cli testing pay-template --base=… --instance=… --id=…` for demos. | | **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. |
### 9c. public GET paid = `StatusPaidResponse` (no `paid` / `order_status`)
| | |
|--|--|
| **Seen** | TESTPAYSAN token-pay 2026-08-30/31; e2e WARN not settled; body `{refunded:false, refund_taken:…}` |
| **Area** | **ops** |
| **Problem** | Suite required `paid` / `order_status` on **public** GET. Spec does not (`get-orders-ORDER_ID.rst` `StatusPaidResponse`: refund_* only). `order_status:"paid"` is **private** (`CheckPaymentPaidResponse`). |
| **Workaround** | `merchant_order_json_is_paid`: refunded/refund_taken, no `taler_pay_uri`. Do not trust `handle-uri` exit 0. |
| **Wanted** | wallet-cli non-zero on Payment expired/failed. |
### 9d. Merchant `/keys` parse fail → 2010 then recover from DB
| | |
|--|--|
| **Seen** | 2026-08-30 TESTPAYSAN `taler-merchant-httpd` after pasta hairpin + public `EXCHANGE_BASE_URL` |
| **Area** | **ops** / **core** |
| **Problem** | First fetch of `https://stage.exchange.lefrancpaysan.ch/keys` from inside the Quadlet failed (`version:0` / json_helper) → merchant **2010**. After keys-change notification, `/keys` loaded from DB; `GET /exchanges` then **http 200 ec 0** for the public URL. Leftover **list** rows still show demo.taler.net, taler-ops.ch, `http://127.0.0.1:9031/` (package kudos/tops + old DB). Config has **one** `[merchant-exchange-testpaysan]` HTTPS URL; kudos/chf `DISABLED=YES` in **overrides**, never `/usr/share`. |
| **Workaround** | Pasta `/etc/hosts``169.254.1.2` for `stage.{exchange,bank,monnaie}.lefrancpaysan.ch`. Persist `/data/etc/hosts-public-https`. Restart **httpd only**, not Quadlet. |
| **Wanted** | Merchant drop stale exchange list rows when `DISABLED=YES`; parse `/keys` error that names URL + HTTP status. |
--- ---
## 10. Amount / currency parsing edge cases ## 10. Amount / currency parsing edge cases
@ -265,11 +305,23 @@ Sources: `https://bank.hacktivism.ch/intro/stats.json`, exchange/merchant intro
| **Seen** | e2e pay settle loops; ladder pay settle rounds | | **Seen** | e2e pay settle loops; ladder pay settle rounds |
| **Area** | **cli** / **core** | | **Area** | **cli** / **core** |
| **Problem** | After `handle-uri` pay, success is “order paid” / balance drop / merchant order status — not one CLI wait. | | **Problem** | After `handle-uri` pay, success is “order paid” / balance drop / merchant order status — not one CLI wait. |
| **Workaround** | Short poll loops; never run-until-done. | | **Workaround** | Short poll loops; never run-until-done. TESTPAYSAN token-pay: private merchant status, see §9c. |
| **Wanted** | Same as §12: bounded wait on transaction id / order id. | | **Wanted** | Same as §12: bounded wait on transaction id / order id. |
--- ---
## 14b. Own sqlite wallet (Android / iOS export)
| | |
|--|--|
| **Seen** | 2026-08-30 magikoopa; real CHF / TESTPAYSAN pay from a phone SQL dump |
| **Area** | **cli** / **ops** |
| **Problem** | Suite e2e/ladder/zero-pay used a fresh temp sqlite, so a funded export could not pay. |
| **Workaround** | `./taler-monitoring.sh --wallet-db PATH` (same flag as **taler-wallet-cli** / **taler-harness**). Native mobile sqlite: `--wallet-migrate``TALER_WALLET_MIGRATE_NATIVE_DB=1`. Not `*.json` (§15). zero-pay: `PARALLEL=1`. |
| **Wanted** | Keep harness flag names when adding wallet CLI to the suite. |
---
## 15. `--wallet-db=*.json` is rejected (“memory backend not supported”) ## 15. `--wallet-db=*.json` is rejected (“memory backend not supported”)
| | | | | |

View file

@ -143,6 +143,20 @@ Each check prints **global** and **grouped** ids:
# Admin credit for e2e: E2E_BANK_ADMIN_PASS=… or stage host secrets # Admin credit for e2e: E2E_BANK_ADMIN_PASS=… or stage host secrets
# (bank-admin-password.txt under /mnt/data/stagepaysan/bank/secrets/) # (bank-admin-password.txt under /mnt/data/stagepaysan/bank/secrets/)
# Override: INSIDE_SSH=… INSIDE_PROFILE=stage-lfp # Override: INSIDE_SSH=… INSIDE_PROFILE=stage-lfp
# Live stack names + mini ladder: README_testpaysan.md (do not duplicate here).
# 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
# Prod vs stage.my, CHF:0, real-CHF mini: README_mytops.md (do not duplicate here).
# Other domains — public HTTPS only (pass e2e/inside explicitly) # Other domains — public HTTPS only (pass e2e/inside explicitly)
./taler-monitoring.sh -d taler.net ./taler-monitoring.sh -d taler.net

23
README_mytops.md Normal file
View file

@ -0,0 +1,23 @@
# mytops / TOPS (CHF) — deltas
Generic CLI: [README.md](./README.md). **cwd = this tree.** Instance/template only via env (`INST` `TEMPLATE` `MERCHANT`) — not baked as suite defaults here.
| `-d` | Money |
|------|--------|
| `stage.my.taler-ops.ch` / `stage.taler-ops.ch` | stage CHF |
| `my.taler-ops.ch` | **prod, real CHF** — on purpose only |
```bash
./taler-monitoring.sh -d my.taler-ops.ch urls
./taler-monitoring.sh -d stage.my.taler-ops.ch urls
# CHF:0 / mini ladder: set INST TEMPLATE MERCHANT; LADDER_STEPS=5; cap LADDER_MAX_AMOUNT
# own sqlite: --wallet-db PATH [--wallet-migrate] (same flag as taler-wallet-cli / harness)
# Prod CHF:0 stickers instance is the one that exists on my.taler-ops.ch
# stage.my: that template is missing (HTTP 2018). Wrapper default COUNT=100 + stage.my → expect fail until template exists.
```
| | |
|--|--|
| last100tx | `paid=true` without deposit often omitted; amount-sort hides CHF:0 |
| Betel | SSH `taler-betel`, stage only |
| Lifeline | no login without Freigabe |

25
README_testpaysan.md Normal file
View file

@ -0,0 +1,25 @@
# TESTPAYSAN (FP stage) — deltas
Generic CLI: [README.md](./README.md). **cwd = this tree.** Instance/template via env, not in this file.
| | |
|--|--|
| `-d` | `stage.lefrancpaysan.ch` (same as `stage.monnaie…`) |
| Currency | TESTPAYSAN |
| Public | `https://stage.{bank,exchange,monnaie}.lefrancpaysan.ch` |
| Live UNIX | **`testpaysan`** · Quadlet **`stage-testpaysan`** · ports **90309032** · data **`/mnt/data/testpaysan`** |
| SSH (this laptop) | Host **`francpaysan`**, then `sudo -n -u testpaysan` |
| Stale | `stagepaysan` / `stage-lfp-*` / `francpaysan-stage-user` + `id_ed` — harness `-d` still prints those INSIDE_* flags; public URLs are correct |
| HALT | no Quadlet recreate of `stage-testpaysan`; no prod `*.lefrancpaysan.ch` without `stage.` |
| Config | persist under `/data/etc/…-overrides.conf`, never `/usr/share` |
| Pasta | guest `/etc/hosts``169.254.1.2` for those FQDNs |
```bash
./taler-monitoring.sh -d stage.lefrancpaysan.ch urls
E2E_WALLET_SERVE=1 E2E_TIMEOUT=480 ./taler-monitoring.sh -d stage.lefrancpaysan.ch e2e
# mini ladder: LADDER_STEPS=5 (not 3). Pay instance = env LADDER_PAY_TEMPLATE_INSTANCE
COUNT=1 ./taler-monitoring.sh -d stage.lefrancpaysan.ch zero-pay # needs a funded wallet
# own sqlite (Android/iOS export): --wallet-db PATH [--wallet-migrate]
```
**Smoke 2026-08-30:** `urls` HTTPS OK; **3 ERROR** `stats.json` stuck **2026-08-29 13:36 CEST**. `zero-pay` COUNT=1 **FAIL** insufficient balance without prior `e2e` withdraw.

View file

@ -122,6 +122,7 @@ When filing an issue, quote the full id + label, e.g.
|------------|---------| |------------|---------|
| **versions.outside-** | deb.taler.net suite index | | **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*`) | | **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.ssh-** | ssh: packages via `KOOPA_SSH` / `INSIDE_SSH` from laptop/remote |
| **versions.compare-** | installed vs suite | | **versions.compare-** | installed vs suite |
| **aptdeploy.trixie-fresh-** | fresh rebuild container `…-apt-src-trixie` | | **aptdeploy.trixie-fresh-** | fresh rebuild container `…-apt-src-trixie` |

View file

@ -1 +1 @@
1.23.9 1.26.1

View file

@ -17,6 +17,9 @@ Git tags: `vMAJOR.FEATURE.FIX` (e.g. `v1.8.0`). File `VERSION` omits the `v` pre
| Tag | Date (UTC) | Notes | | Tag | Date (UTC) | Notes |
|-----|------------|--------| |-----|------------|--------|
| **v1.26.1** | 2026-08-31 | **Bugfix:** public order settle accepts `StatusPaidResponse` (`refunded`/`refund_taken`); `paid`/`order_status` not on public GET. |
| **v1.26.0** | 2026-08-30 | **Feature:** `--wallet-db PATH` / `--wallet-migrate` (taler-wallet-cli / taler-harness names) for e2e, ladder, zero-pay. TESTPAYSAN: one Quadlet `stage-testpaysan`; e2e token-pay via env `E2E_TOKEN_*` / `--choice-index`. Stack READMEs `README_testpaysan.md` / `README_mytops.md`. |
| **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.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.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. | | **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. |

View file

@ -210,7 +210,7 @@ export LADDER_MODE LADDER_MAX_PROBES LADDER_MAX_TOL_REL LADDER_HIGH_RUNGS LADDER
export LADDER_FREE_TEMPLATE LADDER_FREE_TEMPLATE_INSTANCE LADDER_USE_FREE_TEMPLATE LADDER_PAY_WAIT_AVAILABLE_S export LADDER_FREE_TEMPLATE LADDER_FREE_TEMPLATE_INSTANCE LADDER_USE_FREE_TEMPLATE LADDER_PAY_WAIT_AVAILABLE_S
SCRATCH=$(mktemp -d) SCRATCH=$(mktemp -d)
WDB="$SCRATCH/wallet.sqlite3" WDB=$(taler_mon_resolve_wallet_db "$SCRATCH/wallet.sqlite3") || exit 2
REPORT_DIR="${LADDER_REPORT_DIR:-$SCRATCH}" REPORT_DIR="${LADDER_REPORT_DIR:-$SCRATCH}"
mkdir -p "$REPORT_DIR" mkdir -p "$REPORT_DIR"
TSV="$REPORT_DIR/ladder-results.tsv" TSV="$REPORT_DIR/ladder-results.tsv"

View file

@ -8,8 +8,8 @@ source "$ROOT/lib.sh"
# shellcheck source=metrics.sh # shellcheck source=metrics.sh
source "$ROOT/metrics.sh" source "$ROOT/metrics.sh"
: "${E2E_TIMEOUT:=180}" # Bank withdraw ladder + wirewatch lag needs room (was 55 — too tight) : "${E2E_TIMEOUT:=360}" # Bank withdraw + refresh/pay + shop sample + token-pay
: "${E2E_PAY_SECS:=22}" # hard reserve for handle-uri + settle (do not starve pay) : "${E2E_PAY_SECS:=60}" # handle-uri + coin refresh; macOS perl SIGALRM kills at this cap
: "${E2E_SETTLE_ROUNDS:=18}" : "${E2E_SETTLE_ROUNDS:=18}"
: "${E2E_SETTLE_SLEEP:=3}" : "${E2E_SETTLE_SLEEP:=3}"
E2E_START=$(date +%s) E2E_START=$(date +%s)
@ -307,7 +307,7 @@ PAY_AMT=$(printf '%s' "$PAY_LIST" | awk '{print $1}')
if [ "$E2E_VARIABLE" = "1" ]; then if [ "$E2E_VARIABLE" = "1" ]; then
n_w=$(printf '%s' "$WITHDRAW_LIST" | wc -w | tr -d ' ') n_w=$(printf '%s' "$WITHDRAW_LIST" | wc -w | tr -d ' ')
n_p=$(printf '%s' "$PAY_LIST" | wc -w | tr -d ' ') n_p=$(printf '%s' "$PAY_LIST" | wc -w | tr -d ' ')
need=$(( 50 + n_w * 40 + n_p * 20 + 60 )) # + room for paivana pay need=$(( 80 + n_w * 50 + n_p * 50 + 180 )) # shop sample + token-pay + refresh room
if [ "${E2E_TIMEOUT}" -lt "$need" ]; then if [ "${E2E_TIMEOUT}" -lt "$need" ]; then
E2E_TIMEOUT=$need E2E_TIMEOUT=$need
fi fi
@ -330,8 +330,9 @@ fi
BANK_HOST=$(python3 -c 'from urllib.parse import urlparse; print(urlparse("'"$BANK_PUBLIC"'").hostname or "bank")' 2>/dev/null || echo bank) BANK_HOST=$(python3 -c 'from urllib.parse import urlparse; print(urlparse("'"$BANK_PUBLIC"'").hostname or "bank")' 2>/dev/null || echo bank)
SCRATCH=$(mktemp -d) SCRATCH=$(mktemp -d)
WDB="$SCRATCH/wallet.sqlite3" WDB=$(taler_mon_resolve_wallet_db "$SCRATCH/wallet.sqlite3") || exit 2
WSOCK="$SCRATCH/wallet.sock" # macOS AF_UNIX path cap ~104 bytes. Do not use $TMPDIR (/var/folders/...).
WSOCK="/tmp/e2e-wcli-$$.sock"
WSERVE_PID="" WSERVE_PID=""
# Client/server wallet (advanced serve): long-lived shepherd without run-until-done. # Client/server wallet (advanced serve): long-lived shepherd without run-until-done.
# Default on — wallet-cli only completes withdraw/pay while a serve process is up. # Default on — wallet-cli only completes withdraw/pay while a serve process is up.
@ -598,20 +599,26 @@ start_wallet_serve() {
[ -n "${WALLET_CLI:-}" ] && [ -f "${WALLET_CLI}" ] || return 1 [ -n "${WALLET_CLI:-}" ] && [ -f "${WALLET_CLI}" ] || return 1
stop_wallet_serve stop_wallet_serve
rm -f "$WSOCK" rm -f "$WSOCK"
# helper shebang is /usr/bin/env python3 — macOS /usr/bin/python3 is 3.9 (FATAL).
export PATH="/opt/homebrew/bin:/usr/local/bin:${HOME}/.local/bin:${PATH}"
export PYTHON="${PYTHON:-$(command -v python3)}"
# serve holds the DB open; clients use --wallet-connection only # serve holds the DB open; clients use --wallet-connection only
"$NODE_BIN" "$WALLET_CLI" --wallet-db="$WDB" --no-throttle --skip-defaults \ "$NODE_BIN" "$WALLET_CLI" --wallet-db="$WDB" --no-throttle --skip-defaults \
advanced serve --unix-path "$WSOCK" \ advanced serve --unix-path "$WSOCK" \
>"$SCRATCH/wallet-serve.log" 2>&1 & >"$SCRATCH/wallet-serve.log" 2>&1 &
WSERVE_PID=$! WSERVE_PID=$!
local i local i
for i in $(seq 1 40); do for i in $(seq 1 80); do
if [ -S "$WSOCK" ] && kill -0 "$WSERVE_PID" 2>/dev/null; then if kill -0 "$WSERVE_PID" 2>/dev/null && { [ -S "$WSOCK" ] || grep -q "serving at" "$SCRATCH/wallet-serve.log" 2>/dev/null; }; then
ok "wallet serve" "pid=$WSERVE_PID sock=$WSOCK (no run-until-done)" ok "wallet serve" "pid=$WSERVE_PID sock=$WSOCK (no run-until-done)"
return 0 return 0
fi fi
if ! kill -0 "$WSERVE_PID" 2>/dev/null; then
break
fi
sleep 0.15 sleep 0.15
done done
warn "wallet serve" "socket not ready — falling back to one-shot CLI (coins may stay pending)" warn "wallet serve" "socket not ready — $(head -c 180 "$SCRATCH/wallet-serve.log" 2>/dev/null | tr '\n' ' ')"
stop_wallet_serve stop_wallet_serve
return 1 return 1
} }
@ -658,20 +665,10 @@ wcli() {
fi fi
} }
# Pay must not be starved: use reserved seconds even if global budget is tight # Pay must not be starved: always keep the reserved handle-uri cap
# (leftover e2e budget used to shrink this to 1214s → SIGALRM mid-pay).
wcli_pay() { wcli_pay() {
local cap=$E2E_PAY_SECS local cap=$E2E_PAY_SECS
local left
left=$(e2e_left)
left=$(printf '%s' "$left" | awk '/^[0-9]+$/{print; exit} {for(i=1;i<=NF;i++) if($i+0==$i){print $i; exit}}')
if [ -n "$left" ] && [ "$left" -gt "$cap" ] 2>/dev/null; then
cap=$E2E_PAY_SECS
elif [ -n "$left" ] && [ "$left" -ge 8 ] 2>/dev/null; then
cap=$left
else
# still try once with floor 12s so Alarm clock is not the blocker
cap=12
fi
info "pay wallet cap" "${cap}s" info "pay wallet cap" "${cap}s"
if [ -n "${WSERVE_PID:-}" ] && [ -S "${WSOCK:-}" ]; then if [ -n "${WSERVE_PID:-}" ] && [ -S "${WSOCK:-}" ]; then
_wcli_exec "$cap" --wallet-connection="$WSOCK" --no-throttle "$@" _wcli_exec "$cap" --wallet-connection="$WSOCK" --no-throttle "$@"
@ -1203,16 +1200,7 @@ except Exception: print("")
fi fi
if grep -qiE 'payment|paid|Payment' "$SCRATCH/tx-$tag.out" 2>/dev/null \ if grep -qiE 'payment|paid|Payment' "$SCRATCH/tx-$tag.out" 2>/dev/null \
|| grep -qiE 'done|paid|success|Payment' "$SCRATCH/pay-$tag.out" 2>/dev/null \ || grep -qiE 'done|paid|success|Payment' "$SCRATCH/pay-$tag.out" 2>/dev/null \
|| python3 -c 'import json,sys || merchant_order_json_is_paid "$SCRATCH/ord-paid-$tag.json" "$SCRATCH/ord-pub-$tag.json" 2>/dev/null; then
for p in sys.argv[1:]:
try:
d=json.load(open(p))
if d.get("paid") is True or str(d.get("order_status","")).lower()=="paid":
sys.exit(0)
except Exception:
pass
sys.exit(1)
' "$SCRATCH/ord-paid-$tag.json" "$SCRATCH/ord-pub-$tag.json" 2>/dev/null; then
ok "payment settled $PAY_AMT ($PAY_SUM · order $OID)" ok "payment settled $PAY_AMT ($PAY_SUM · order $OID)"
metrics_report_coins "after-pay-${tag}" || true metrics_report_coins "after-pay-${tag}" || true
metrics_record_flow spent "$PAY_AMT" || true metrics_record_flow spent "$PAY_AMT" || true
@ -1343,16 +1331,7 @@ except Exception: print("")
"${MERCHANT_PUBLIC}/instances/${INST}/private/orders/${OID}" 2>/dev/null || true "${MERCHANT_PUBLIC}/instances/${INST}/private/orders/${OID}" 2>/dev/null || true
fi fi
curl -skS -m 8 -o "$SCRATCH/ord-pub-$tag.json" "$statusUrl" 2>/dev/null || true curl -skS -m 8 -o "$SCRATCH/ord-pub-$tag.json" "$statusUrl" 2>/dev/null || true
if python3 -c 'import json,sys if merchant_order_json_is_paid "$SCRATCH/ord-paid-$tag.json" "$SCRATCH/ord-pub-$tag.json" 2>/dev/null \
for p in sys.argv[1:]:
try:
d=json.load(open(p))
if d.get("paid") is True or str(d.get("order_status","")).lower()=="paid":
sys.exit(0)
except Exception:
pass
sys.exit(1)
' "$SCRATCH/ord-paid-$tag.json" "$SCRATCH/ord-pub-$tag.json" 2>/dev/null \
|| grep -qiE 'payment|paid|Payment' "$SCRATCH/tx-$tag.out" 2>/dev/null \ || grep -qiE 'payment|paid|Payment' "$SCRATCH/tx-$tag.out" 2>/dev/null \
|| grep -qiE 'done|paid|success|Payment' "$SCRATCH/pay-$tag.out" 2>/dev/null; then || grep -qiE 'done|paid|success|Payment' "$SCRATCH/pay-$tag.out" 2>/dev/null; then
ok "shop $pname" "payment settled ($pamt · order $OID)" ok "shop $pname" "payment settled ($pamt · order $OID)"
@ -1717,12 +1696,120 @@ else
info "paivana summary" "${PAIVANA_REPORT:-?} · instance ${E2E_PAIVANA_INSTANCE}" info "paivana summary" "${PAIVANA_REPORT:-?} · instance ${E2E_PAIVANA_INSTANCE}"
fi fi
# ---------------------------------------------------------------------------
# Loyalty-token pays (generic). TESTPAYSAN default = stage token-order API.
# Override: E2E_TOKEN_ORDER_URL E2E_TOKEN_PRODUCT E2E_TOKEN_PAID_N E2E_TOKEN_FREE_N
# ---------------------------------------------------------------------------
if [ "${CUR:-}" = "TESTPAYSAN" ] && [ -z "${E2E_TOKEN_ENABLE:-}" ]; then
E2E_TOKEN_ENABLE=1
fi
: "${E2E_TOKEN_ORDER_URL:=https://stage.monnaie.lefrancpaysan.ch/shops/api/kiosque-order}"
: "${E2E_TOKEN_PRODUCT:=prod_banana}"
: "${E2E_TOKEN_PAID_N:=2}"
: "${E2E_TOKEN_FREE_N:=1}"
TOKEN_OK=0
TOKEN_OK_N=0
TOKEN_FAIL_N=0
TOKEN_REPORT=""
e2e_one_token_order() {
local product="$1" choice="$2" tag="$3"
local body create_f payuri oid otok inst
body=$(python3 -c 'import json,sys; print(json.dumps({"product_id":sys.argv[1]}))' "$product")
create_f="$SCRATCH/token-$tag.json"
curl -skS -m 20 -o "$create_f" -X POST \
-H 'Content-Type: application/json' -d "$body" \
"$E2E_TOKEN_ORDER_URL" 2>"$SCRATCH/token-$tag.err" || true
oid=$(python3 -c 'import json,sys
try:
d=json.load(open(sys.argv[1])); print(d.get("order_id") or "")
except Exception: print("")' "$create_f" 2>/dev/null || true)
otok=$(python3 -c 'import json,sys
try:
d=json.load(open(sys.argv[1])); print(d.get("token") or "")
except Exception: print("")' "$create_f" 2>/dev/null || true)
payuri=$(python3 -c 'import json,sys
try:
d=json.load(open(sys.argv[1])); print((d.get("taler_pay_uri") or "").replace(":443",""))
except Exception: print("")' "$create_f" 2>/dev/null || true)
if [ -z "$oid" ] || [ -z "$payuri" ]; then
warn "token-pay $tag" "order POST failed — $(head -c 120 "$create_f" 2>/dev/null | tr '\n' ' ')"
return 1
fi
info "token-pay $tag" "order $oid choice=$choice product=$product"
if ! wcli_pay handle-uri --yes --non-interactive --choice-index="$choice" "$payuri" \
>"$SCRATCH/token-pay-$tag.out" 2>&1; then
warn "token-pay $tag" "handle-uri failed — $(tail -c 140 "$SCRATCH/token-pay-$tag.out" | tr '\n' ' ')"
return 1
fi
# wallet-cli can print Payment expired/failed and still exit 0
if grep -qiE 'Payment expired|Payment failed|Payment aborted|insufficient balance' \
"$SCRATCH/token-pay-$tag.out" 2>/dev/null; then
warn "token-pay $tag" "handle-uri not paid — $(tail -c 140 "$SCRATCH/token-pay-$tag.out" | tr '\n' ' ')"
return 1
fi
inst="${E2E_TOKEN_INSTANCE:-kiosque-banane}"
local r
for r in 1 2 3 4 5 6 8 10 12 15 18 20; do
if [ -n "$otok" ]; then
curl -skS -m 8 -o "$SCRATCH/token-st-$tag.json" \
"${MERCHANT_PUBLIC}/instances/${inst}/orders/$(python3 -c 'import urllib.parse,sys; print(urllib.parse.quote(sys.argv[1], safe=""))' "$oid")?token=$(python3 -c 'import urllib.parse,sys; print(urllib.parse.quote(sys.argv[1], safe=""))' "$otok")" \
2>/dev/null || true
if merchant_order_json_is_paid "$SCRATCH/token-st-$tag.json" 2>/dev/null; then
ok "token-pay $tag" "paid choice=$choice oid=$oid"
return 0
fi
fi
sleep 2
done
warn "token-pay $tag" "not settled for order $oid$(head -c 80 "$SCRATCH/token-st-$tag.json" 2>/dev/null | tr '\n' ' ')"
return 1
}
if [ "${E2E_TOKEN_ENABLE}" = "1" ]; then
set_group token
section "e2e · token pays (public order API)"
info "token-pay" "POST $E2E_TOKEN_ORDER_URL product=$E2E_TOKEN_PRODUCT paid_n=$E2E_TOKEN_PAID_N free_n=$E2E_TOKEN_FREE_N"
_ti=1
while [ "$_ti" -le "$E2E_TOKEN_PAID_N" ]; do
if e2e_one_token_order "$E2E_TOKEN_PRODUCT" 0 "paid${_ti}"; then
TOKEN_OK_N=$((TOKEN_OK_N + 1))
TOKEN_REPORT="${TOKEN_REPORT:+$TOKEN_REPORT }paid${_ti}=OK"
else
TOKEN_FAIL_N=$((TOKEN_FAIL_N + 1))
TOKEN_REPORT="${TOKEN_REPORT:+$TOKEN_REPORT }paid${_ti}=FAIL"
fi
_ti=$((_ti + 1))
done
_ti=1
while [ "$_ti" -le "$E2E_TOKEN_FREE_N" ]; do
if e2e_one_token_order "$E2E_TOKEN_PRODUCT" 1 "free${_ti}"; then
TOKEN_OK_N=$((TOKEN_OK_N + 1))
TOKEN_REPORT="${TOKEN_REPORT:+$TOKEN_REPORT }free${_ti}=OK"
else
TOKEN_FAIL_N=$((TOKEN_FAIL_N + 1))
TOKEN_REPORT="${TOKEN_REPORT:+$TOKEN_REPORT }free${_ti}=FAIL"
fi
_ti=$((_ti + 1))
done
unset _ti
if [ "$TOKEN_FAIL_N" = "0" ] && [ "$TOKEN_OK_N" -gt 0 ]; then
TOKEN_OK=1
PAY_OK=1
PAY_OK_N=$((PAY_OK_N + TOKEN_OK_N))
ok "token-pay summary" "$TOKEN_REPORT"
else
warn "token-pay summary" "$TOKEN_REPORT"
fi
fi
set_group report set_group report
section "e2e · report" section "e2e · report"
info "user" "$USER" info "user" "$USER"
info "Bank withdraws" "$WITHDRAW_REPORT" info "Bank withdraws" "$WITHDRAW_REPORT"
info "payments" "$PAY_REPORT" info "payments" "$PAY_REPORT"
info "shop" "${SHOP_REPORT:-(n/a)}" info "shop" "${SHOP_REPORT:-(n/a)}"
info "token-pay" "${TOKEN_REPORT:-(n/a)}"
info "paivana" "${PAIVANA_REPORT:-(n/a)}" info "paivana" "${PAIVANA_REPORT:-(n/a)}"
info "final balance" "$(fmt_bal "$SCRATCH/bal-live.out" 2>/dev/null || echo "(n/a)")" info "final balance" "$(fmt_bal "$SCRATCH/bal-live.out" 2>/dev/null || echo "(n/a)")"
info "scratch" "$SCRATCH" info "scratch" "$SCRATCH"

View file

@ -58,9 +58,9 @@ info "flags" "INSIDE_ACCESS=${INSIDE_ACCESS} INSIDE_PODMAN=${INSIDE_PODMAN:-0} I
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
if [ "$PROFILE" = "stage-lfp" ]; then if [ "$PROFILE" = "stage-lfp" ]; then
SSH_HOST="${INSIDE_SSH:-}" SSH_HOST="${INSIDE_SSH:-}"
BANK_CTR="${INSIDE_BANK_CTR:-stage-lfp-bank}" BANK_CTR="${INSIDE_BANK_CTR:-stage-testpaysan}"
EX_CTR="${INSIDE_EXCHANGE_CTR:-stage-lfp-exchange-ansible}" EX_CTR="${INSIDE_EXCHANGE_CTR:-stage-testpaysan}"
MER_CTR="${INSIDE_MERCHANT_CTR:-stage-lfp-merchant}" MER_CTR="${INSIDE_MERCHANT_CTR:-stage-testpaysan}"
BANK_PORT="${INSIDE_BANK_PORT:-9032}" BANK_PORT="${INSIDE_BANK_PORT:-9032}"
EX_PORT="${INSIDE_EXCHANGE_PORT:-9031}" EX_PORT="${INSIDE_EXCHANGE_PORT:-9031}"
MER_PORT="${INSIDE_MERCHANT_PORT:-9030}" MER_PORT="${INSIDE_MERCHANT_PORT:-9030}"
@ -101,9 +101,9 @@ hasp() { podman exec "$1" pgrep -f "$2" >/dev/null 2>&1; }
BANK="${BANK_CTR}" BANK="${BANK_CTR}"
EX="${EX_CTR}" EX="${EX_CTR}"
MER="${MER_CTR}" MER="${MER_CTR}"
podman ps --format '{{.Names}}' 2>/dev/null | grep -qx "$BANK" || BANK=$(podman ps --format '{{.Names}}' 2>/dev/null | grep -iE 'stage.*bank|lfp-bank' | head -1) podman ps --format '{{.Names}}' 2>/dev/null | grep -qx "$BANK" || BANK=$(podman ps --format '{{.Names}}' 2>/dev/null | grep -iE 'stage-testpaysan|stage.*bank|lfp-bank' | head -1)
podman ps --format '{{.Names}}' 2>/dev/null | grep -qx "$EX" || EX=$(podman ps --format '{{.Names}}' 2>/dev/null | grep -iE 'stage.*exchange|lfp-exchange' | head -1) podman ps --format '{{.Names}}' 2>/dev/null | grep -qx "$EX" || EX=$(podman ps --format '{{.Names}}' 2>/dev/null | grep -iE 'stage-testpaysan|stage.*exchange|lfp-exchange' | head -1)
podman ps --format '{{.Names}}' 2>/dev/null | grep -qx "$MER" || MER=$(podman ps --format '{{.Names}}' 2>/dev/null | grep -iE 'stage.*merchant|lfp-merchant' | head -1) podman ps --format '{{.Names}}' 2>/dev/null | grep -qx "$MER" || MER=$(podman ps --format '{{.Names}}' 2>/dev/null | grep -iE 'stage-testpaysan|stage.*merchant|lfp-merchant' | head -1)
check_dns() { check_dns() {
local comp="$1" ctr="$2" host="$3" local comp="$1" ctr="$2" host="$3"

View file

@ -187,9 +187,9 @@ fi
# GOA names → empty dpkg list → versions.host-04 ERROR (public /monitoring_err). # GOA names → empty dpkg list → versions.host-04 ERROR (public /monitoring_err).
if [ "${INSIDE_PROFILE:-}" = "stage-lfp" ] \ if [ "${INSIDE_PROFILE:-}" = "stage-lfp" ] \
|| [ "${EXPECT_CURRENCY:-}" = "TESTPAYSAN" ]; then || [ "${EXPECT_CURRENCY:-}" = "TESTPAYSAN" ]; then
_VERS_BANK="${INSIDE_BANK_CTR:-stage-lfp-bank}" _VERS_BANK="${INSIDE_BANK_CTR:-stage-testpaysan}"
_VERS_EX="${INSIDE_EXCHANGE_CTR:-stage-lfp-exchange-ansible}" _VERS_EX="${INSIDE_EXCHANGE_CTR:-stage-testpaysan}"
_VERS_MER="${INSIDE_MERCHANT_CTR:-stage-lfp-merchant}" _VERS_MER="${INSIDE_MERCHANT_CTR:-stage-testpaysan}"
fi fi
if [ "$_VERS_LOCAL" = "1" ]; then if [ "$_VERS_LOCAL" = "1" ]; then

471
check_zero_pay.sh Executable file
View file

@ -0,0 +1,471 @@
#!/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))'; }
if [ -n "${WALLET_DB:-}" ] && [ "$PARALLEL" -gt 1 ]; then
die "--wallet-db / WALLET_DB needs PARALLEL=1"
fi
slot_db() {
if [ -n "${WALLET_DB:-}" ] && [ "$1" = "0" ]; then
taler_mon_resolve_wallet_db "$OUTDIR/wallet-0.sqlite3" || exit 2
else
echo "$OUTDIR/wallet-$1.sqlite3"
fi
}
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() {
merchant_order_json_is_paid "$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

View file

@ -14,9 +14,10 @@ export TALER_DOMAIN="${TALER_DOMAIN:-stage.lefrancpaysan.ch}"
export INSIDE_PODMAN="${INSIDE_PODMAN:-1}" export INSIDE_PODMAN="${INSIDE_PODMAN:-1}"
export INSIDE_MODE="${INSIDE_MODE:-local-podman}" export INSIDE_MODE="${INSIDE_MODE:-local-podman}"
export INSIDE_PROFILE="${INSIDE_PROFILE:-stage-lfp}" export INSIDE_PROFILE="${INSIDE_PROFILE:-stage-lfp}"
export INSIDE_BANK_CTR="${INSIDE_BANK_CTR:-stage-lfp-bank}" # One Quadlet stack on testpaysan (not three stage-lfp-* names).
export INSIDE_EXCHANGE_CTR="${INSIDE_EXCHANGE_CTR:-stage-lfp-exchange-ansible}" export INSIDE_BANK_CTR="${INSIDE_BANK_CTR:-stage-testpaysan}"
export INSIDE_MERCHANT_CTR="${INSIDE_MERCHANT_CTR:-stage-lfp-merchant}" export INSIDE_EXCHANGE_CTR="${INSIDE_EXCHANGE_CTR:-stage-testpaysan}"
export INSIDE_MERCHANT_CTR="${INSIDE_MERCHANT_CTR:-stage-testpaysan}"
export LOCAL_STACK="${LOCAL_STACK:-0}" export LOCAL_STACK="${LOCAL_STACK:-0}"
export CONTINUE_ON_ERROR="${CONTINUE_ON_ERROR:-1}" export CONTINUE_ON_ERROR="${CONTINUE_ON_ERROR:-1}"
# monpages obligatory (ERROR): only FP stage mon hosts below — never GOA inventory # monpages obligatory (ERROR): only FP stage mon hosts below — never GOA inventory

45
lib.sh
View file

@ -4,6 +4,46 @@
# Machine paths/SSH hosts: only from env (~/.config/taler-monitoring/env or # Machine paths/SSH hosts: only from env (~/.config/taler-monitoring/env or
# TALER_MONITORING_ENV=…), installed via taler-monitoring-env — never hardcode. # TALER_MONITORING_ENV=…), installed via taler-monitoring-env — never hardcode.
# Wallet sqlite path (same flag as taler-wallet-cli / taler-harness: --wallet-db).
# Android/iOS SQL export or any existing sqlite. Not *.json (memory backend).
taler_mon_resolve_wallet_db() {
local fallback="${1:-}"
local p="${WALLET_DB:-}"
if [ -z "$p" ]; then
printf '%s\n' "$fallback"
return 0
fi
case "$p" in
*.json|*.JSON)
echo "ERROR: --wallet-db must be sqlite (.sqlite3 / .db), not .json" >&2
return 2
;;
esac
printf '%s\n' "$p"
}
# Merchant order JSON is paid? Public GET: StatusPaidResponse (refunded / refund_taken).
# Spec does not put paid / order_status there (those are private CheckPaymentPaidResponse).
merchant_order_json_is_paid() {
python3 -c '
import json, sys
for p in sys.argv[1:]:
try:
d = json.load(open(p))
except Exception:
continue
if not isinstance(d, dict):
continue
if d.get("paid") is True or str(d.get("order_status") or "").lower() == "paid":
raise SystemExit(0)
if d.get("taler_pay_uri"):
continue
if "refunded" in d or "refund_amount" in d or "refund_taken" in d:
raise SystemExit(0)
raise SystemExit(1)
' "$@"
}
# Load host env once (before any := defaults) # Load host env once (before any := defaults)
if [ "${_TALER_MON_ENV_LOADED:-0}" != "1" ]; then if [ "${_TALER_MON_ENV_LOADED:-0}" != "1" ]; then
_mon_cfg="${TALER_MONITORING_ENV:-${XDG_CONFIG_HOME:-$HOME/.config}/taler-monitoring/env}" _mon_cfg="${TALER_MONITORING_ENV:-${XDG_CONFIG_HOME:-$HOME/.config}/taler-monitoring/env}"
@ -66,8 +106,8 @@ MERCHANT_INSTANCE=$(printf '%s' "${MERCHANT_INSTANCE}" | tr '[:upper:]' '[:lower
: "${PAY_AMT:=GOA:0.01}" : "${PAY_AMT:=GOA:0.01}"
: "${CREDIT_AMT:=GOA:4700}" # covers bank withdraw ladder 20+50+100+200+4200 (paivana) : "${CREDIT_AMT:=GOA:4700}" # covers bank withdraw ladder 20+50+100+200+4200 (paivana)
: "${TIMEOUT:=12}" : "${TIMEOUT:=12}"
: "${E2E_TIMEOUT:=55}" # whole e2e budget; skip rest when exceeded (e2e raises as needed) : "${E2E_TIMEOUT:=360}" # whole e2e budget; skip rest when exceeded (e2e raises as needed)
: "${E2E_PAY_SECS:=22}" # dedicated seconds for pay handle-uri (avoid Alarm clock) : "${E2E_PAY_SECS:=60}" # dedicated seconds for pay handle-uri (avoid SIGALRM)
# Whole-run wall clock for taler-monitoring.sh (seconds). 0 = unlimited. # Whole-run wall clock for taler-monitoring.sh (seconds). 0 = unlimited.
# Host-agent default phases (urls/inside/versions) should finish under this. # Host-agent default phases (urls/inside/versions) should finish under this.
: "${RUN_TIMEOUT:=600}" : "${RUN_TIMEOUT:=600}"
@ -1565,6 +1605,7 @@ find_wallet_cli() {
# shellcheck disable=SC2086 # shellcheck disable=SC2086
for c in ${WALLET_CLI_CANDIDATES:-} \ for c in ${WALLET_CLI_CANDIDATES:-} \
"${HOME}/.local/bin/taler-wallet-cli.mjs" \ "${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/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.mjs" \
"${HOME}/src/taler/taler-typescript-core/packages/taler-wallet-cli/dist/taler-wallet-cli-local.mjs" \ "${HOME}/src/taler/taler-typescript-core/packages/taler-wallet-cli/dist/taler-wallet-cli-local.mjs" \

11
pay_talersticker_zero.sh Executable file
View file

@ -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

View file

@ -93,6 +93,8 @@ Phases:
sanity public + optional server sanity public + optional server
server server-side only (SSH) server server-side only (SSH)
e2e withdraw + pay (small amounts; remote aborts on login/KYC) 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) ladder amount ladder classic (default 23-rung withdraw + pay; any currency)
max-ladder max-search: highest withdrawable + highest payable (≤ available) max-ladder max-search: highest withdrawable + highest payable (≤ available)
goa-ladder alias of ladder (legacy phase name; same check_amount_ladder.sh) goa-ladder alias of ladder (legacy phase name; same check_amount_ladder.sh)
@ -123,6 +125,10 @@ Options:
--exchange URL exchange base — overrides profile --exchange URL exchange base — overrides profile
--merchant URL merchant-backend base — overrides profile --merchant URL merchant-backend base — overrides profile
--currency CODE expected currency (GOA, KUDOS, CHF, …); empty = report only --currency CODE expected currency (GOA, KUDOS, CHF, …); empty = report only
--wallet-db PATH sqlite wallet (taler-wallet-cli / taler-harness same flag).
Android/iOS SQL export or a test DB. Not *.json.
Env WALLET_DB=PATH. e2e / ladder / zero-pay (PARALLEL=1).
--wallet-migrate TALER_WALLET_MIGRATE_NATIVE_DB=1 (native mobile sqlite)
--no-probe do not probe alternate hosts (unknown domains only) --no-probe do not probe alternate hosts (unknown domains only)
--lang en|fr|de UI/console language (sticky bar + badges; de available, not a profile default) --lang en|fr|de UI/console language (sticky bar + badges; de available, not a profile default)
overrides auto (FrancPaysan→fr, else en) overrides auto (FrancPaysan→fr, else en)
@ -266,8 +272,18 @@ while [ $# -gt 0 ]; do
[ $# -ge 2 ] || { echo "missing arg for $1" >&2; exit 2; } [ $# -ge 2 ] || { echo "missing arg for $1" >&2; exit 2; }
CURRENCY_OVERRIDE="$2"; shift 2 CURRENCY_OVERRIDE="$2"; shift 2
;; ;;
--wallet-db)
[ $# -ge 2 ] || { echo "missing arg for $1" >&2; exit 2; }
WALLET_DB="$2"
export WALLET_DB
shift 2
;;
--wallet-migrate)
export TALER_WALLET_MIGRATE_NATIVE_DB=1
shift
;;
--no-probe) NO_PROBE=1; shift ;; --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 # bare domain shorthand: ./taler-monitoring.sh taler.net
if [[ "$1" == *.* && "$1" != *://* && "$1" != -* ]]; then if [[ "$1" == *.* && "$1" != *://* && "$1" != -* ]]; then
@ -445,6 +461,7 @@ _progress_estimate_phase() {
server) n=20 ;; server) n=20 ;;
# e2e: bank withdraw ladder emits many INFO lines (coins before/after each note) # e2e: bank withdraw ladder emits many INFO lines (coins before/after each note)
e2e) n=240 ;; e2e) n=240 ;;
zero-pay|zeropay|sticker-pay|template-zero) n=15 ;;
ladder|goa-ladder|max-ladder|maxladder) n=120 ;; ladder|goa-ladder|max-ladder|maxladder) n=120 ;;
auth401) n=70 ;; auth401) n=70 ;;
aptdeploy) n=40 ;; aptdeploy) n=40 ;;
@ -621,6 +638,9 @@ while [ "$_phase_idx" -lt "$_n_phases" ]; do
sanity) run_phase sanity "$ROOT/check_sanity.sh" || ec=1 ;; sanity) run_phase sanity "$ROOT/check_sanity.sh" || ec=1 ;;
server) run_phase server "$ROOT/check_server.sh" || ec=1 ;; server) run_phase server "$ROOT/check_server.sh" || ec=1 ;;
e2e) run_phase e2e "$ROOT/check_e2e.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) ladder|goa-ladder)
run_phase ladder "$ROOT/check_amount_ladder.sh" || ec=1 run_phase ladder "$ROOT/check_amount_ladder.sh" || ec=1
;; ;;