From fd5ac805664bdd52f2dbb7793a14b1a51d2d32f2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hern=C3=A2ni=20Marques?= Date: Sun, 19 Jul 2026 14:34:47 +0200 Subject: [PATCH] fix 1.22.1: pay-wait withdraw board (start order + pendingIncoming) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit UX fix: after bank OK_BANK, available is often still 0 while pendingIncoming ticks up. The silent wait looked hung. Show OK/OK_BANK withdraws in the order they started, match wallet txs, and update pendingIncoming→done with a timer on the same wallet DB. Short run-pending only; default wait 90s. --- CLI-AUTOMATION-NOTES.md | 1 + VERSION | 2 +- VERSIONS.md | 1 + check_amount_ladder.sh | 29 +++++- ladder/README.md | 1 + ladder/lib_pay.sh | 204 +++++++++++++++++++++++++++++++++++++++- 6 files changed, 232 insertions(+), 6 deletions(-) diff --git a/CLI-AUTOMATION-NOTES.md b/CLI-AUTOMATION-NOTES.md index a1657f3..2f25122 100644 --- a/CLI-AUTOMATION-NOTES.md +++ b/CLI-AUTOMATION-NOTES.md @@ -289,6 +289,7 @@ If only a few changes land, these unlock the most automation: | force-select | multi-rpub try; soft-skip confirm; empty scrape → WARN not balance fail | | higher amounts | mint 5110/P0001 common above live ceiling (§3b); classic soft `CEILING_REJECT`; max-ladder bounds hi; report uses alt_unit_names (Tera-GOA …) | | pay vs balance | never order more than wallet **available** (soft `SKIP_BALANCE`); pendingIncoming does not count as spendable | +| pay wait UX | after withdraw, board lists OK/OK_BANK in **start order** with live wallet `pendingIncoming`/`done` + timer (`LADDER_PAY_WAIT_AVAILABLE_S`, default 90s); same wallet DB as withdraw | | wallet-db path | Prefer `*.sqlite3` (never `*.json` as db path) | | helper / python | mon PATH must include helper + python≥3.11 | diff --git a/VERSION b/VERSION index 57807d6..6245bee 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -1.22.0 +1.22.1 diff --git a/VERSIONS.md b/VERSIONS.md index 8ecdba3..d4ec0fa 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.22.1** | 2026-07-19 | **Bugfix (UX):** pay wait after withdraw no longer looks hung — shows **withdraw board in start order** (OK/OK_BANK from TSV) with live wallet status (`pendingIncoming` → `done`) + timer + aggregate available/pendingIncoming on the **same** wallet DB. Short `run-pending` nudge only (no run-until-done). Default wait 90s (`LADDER_PAY_WAIT_AVAILABLE_S`). | | **v1.22.0** | 2026-07-19 | **Feature:** modular amount ladder — `ladder/lib_pay.sh` (pay) + withdraw in `check_amount_ladder.sh`; `ladder/extract_rpubs.py`. **max-ladder** hunts highest **withdrawable** and highest **payable** (≤ wallet available). GOA pays use public free-amount template **`goa-free`** (`LADDER_FREE_TEMPLATE`). Classic still default 23-rung wd+pay. Phase alias `goa-ladder` → amount ladder (shim removed). | | **v1.21.0** | 2026-07-19 | **Feature:** generic **amount ladder** (`check_amount_ladder.sh`; compat shim `check_goa_ladder.sh`). Default **classic** 23-rung withdraw (+ pay when enabled); **max-search** (`max-ladder` / `LADDER_MODE=max`) hunts highest mintable amount (probes default 32, `0`=until timeout). **Pay:** never overspend — soft `SKIP_BALANCE` if amount > wallet available. **Alt units:** report/logs show human names (e.g. `8.15 Tera-GOA (GOA:…)`) + raw. **Fixes bundled:** external extract (no stdin SyntaxError); force-select multi-rpub; soft `CEILING_REJECT` on mint 5110/P0001; crash-proof report. CLI-AUTOMATION-NOTES §3/3a/3b/§15–18. | | **v1.20.1** | 2026-07-19 | **Docs:** CLI-AUTOMATION-NOTES — force-select empty scrape + ladder pins (§3/3a); wallet-db must not be `*.json` (§15); `taler-helper-sqlite3` needs Python ≥3.11 (§16); portable wall-clock (§17); GOA withdraw checklist (§18). No code change. *(Superseded notes folded into **v1.21.0** feature release.)* | diff --git a/check_amount_ladder.sh b/check_amount_ladder.sh index f62c418..3bc13ed 100755 --- a/check_amount_ladder.sh +++ b/check_amount_ladder.sh @@ -87,8 +87,8 @@ elapsed_ms() { : "${LADDER_HIGH_RUNGS:=6}" # optional seed for reproducible max-search (empty = time-based) : "${LADDER_MAX_SEED:=}" -# After withdraw, wait for spendable available>0 before pay (secs total) -: "${LADDER_PAY_WAIT_AVAILABLE_S:=60}" +# After withdraw, wait for spendable available>0 before pay (secs total; shows countdown) +: "${LADDER_PAY_WAIT_AVAILABLE_S:=90}" # Historic libeufin-ish absolute ceiling used as default LADDER_MAX_AMOUNT on GOA LADDER_ABS_CEILING="4503599627370496" @@ -308,6 +308,31 @@ print("0") ' "$CUR" 2>/dev/null || echo "0" } +# pendingIncoming for same currency (same wallet DB as withdraw) — "0" if missing +wallet_pending_in() { + wcli balance 2>/dev/null | python3 -c ' +import json,sys +t=sys.stdin.read() +dec=json.JSONDecoder() +i=t.find("{") +if i<0: + print("0"); raise SystemExit +try: + d,_=dec.raw_decode(t,i) +except Exception: + try: + d=json.loads(t[i:t.rfind("}")+1]) + except Exception: + print("0"); raise SystemExit +cur=sys.argv[1] +for b in d.get("balances") or []: + a=b.get("pendingIncoming") or b.get("pending_incoming") or "" + if a.startswith(cur+":"): + print(a.split(":",1)[1]); raise SystemExit +print("0") +' "$CUR" 2>/dev/null || echo "0" +} + # Build paired pay + withdraw ladders (same step count, shared random shape). # Pay: [0] + log-uniform mids + [max-1] + [max] # Wd: [0] + max(mid, mid×scale) mids + [max-1] + [max] (mids higher → enough to spend) diff --git a/ladder/README.md b/ladder/README.md index 4737bad..3c6e826 100644 --- a/ladder/README.md +++ b/ladder/README.md @@ -29,3 +29,4 @@ There is **no** `check_goa_ladder.sh` — removed; use the phases above. | `LADDER_USE_FREE_TEMPLATE` | `1` | public POST template with `{"amount":…}` | | `LADDER_FREE_TEMPLATE` | `goa-free` | template id (hacktivism) | | `LADDER_FREE_TEMPLATE_INSTANCE` | merchant instance | e.g. `goa-demo-cp4zqk` | +| `LADDER_PAY_WAIT_AVAILABLE_S` | `90` | after withdraw, wait for spendable **available**>0; logs **withdraw board in start order** (OK/OK_BANK) with live `pendingIncoming`→`done` + timer (same wallet DB) | diff --git a/ladder/lib_pay.sh b/ladder/lib_pay.sh index 9e8e157..e8ae15a 100644 --- a/ladder/lib_pay.sh +++ b/ladder/lib_pay.sh @@ -9,7 +9,7 @@ : "${LADDER_FREE_TEMPLATE:=goa-free}" : "${LADDER_FREE_TEMPLATE_INSTANCE:=}" : "${LADDER_USE_FREE_TEMPLATE:=1}" -: "${LADDER_PAY_WAIT_AVAILABLE_S:=60}" +: "${LADDER_PAY_WAIT_AVAILABLE_S:=90}" : "${LADDER_PAY_SETTLE_ROUNDS:=6}" MAX_PAY_BEST_AMT="${MAX_PAY_BEST_AMT:-}" @@ -188,17 +188,213 @@ except Exception: return 0 } +# Print withdraw board: OK/OK_BANK rungs in *start order* + wallet tx state. +# Uses TSV (ladder order) + transactions dump. Call after refreshing tx dump. +# Args: left_secs available_num pendingIncoming_num +ladder_print_wd_board() { + local left="${1:-?}" bal="${2:-0}" pend="${3:-0}" + local tsv="${TSV:-}" txf="${SCRATCH:-/tmp}/tx-wait-pay.out" + local board + board=$( + python3 - "$tsv" "$txf" "${CUR:-GOA}" "$left" "$bal" "$pend" <<'PY' 2>/dev/null || true +import json, sys +from pathlib import Path + +tsv, txf, cur, left, bal, pend = sys.argv[1:7] +dec = json.JSONDecoder() + +def num(a: str) -> str: + a = (a or "").strip() + if ":" in a: + return a.split(":", 1)[1] + return a + +def iter_json(text: str): + i, n = 0, len(text) + while i < n: + if text[i] == "{": + try: + o, end = dec.raw_decode(text, i) + yield o + i = end + continue + except Exception: + pass + i += 1 + +# TSV rows in start order (OK / OK_BANK only — successful bank path) +rows = [] +if tsv and Path(tsv).is_file(): + for ln in open(tsv, errors="replace"): + if not ln.strip() or ln.startswith("rung"): + continue + p = ln.rstrip("\n").split("\t") + if len(p) < 10: + continue + status = p[3] + if status not in ("OK", "OK_BANK"): + continue + rows.append({ + "rung": p[0], + "amt": p[2], + "bank": status, + "wid": (p[9] if len(p) > 9 else "")[:12], + }) + +# Wallet withdrawal txs (chronological if timestamp present) +wds = [] +if txf and Path(txf).is_file(): + try: + text = open(txf, errors="replace").read() + except Exception: + text = "" + for o in iter_json(text): + txs = o.get("transactions") if isinstance(o, dict) else None + if not isinstance(txs, list): + continue + for t in txs: + if not isinstance(t, dict): + continue + typ = str(t.get("type") or t.get("Type") or "").lower() + if "withdraw" not in typ: + continue + amt = t.get("amountRaw") or t.get("amountEffective") or t.get("amount") or "" + st = ( + t.get("txState") + or t.get("status") + or t.get("pendingMajor") + or t.get("txMajorState") + or "" + ) + st = str(st).lower() + det = t.get("withdrawalDetails") or t.get("withdrawal_details") or {} + if not isinstance(det, dict): + det = {} + conf = det.get("confirmed") + if conf is True and not st: + st = "done" + elif conf is False and not st: + st = "pending" + # pending* / dialog / kyc → still incoming; done/abort final + if any(x in st for x in ("done", "abort", "fail", "delete")): + phase = "done" if "done" in st else st + elif st in ("", "none", "null"): + phase = "unknown" + else: + phase = "pendingIncoming" + ts = 0 + for k in ("timestamp", "timestamp_ms", "age"): + v = t.get(k) + if isinstance(v, dict): + v = v.get("t_s") or v.get("t_ms") or v.get("t_usec") + try: + ts = int(v or 0) + except Exception: + ts = 0 + if ts: + break + wds.append({"amt": str(amt), "n": num(str(amt)), "phase": phase, "st": st or "?", "ts": ts}) + +wds.sort(key=lambda x: x["ts"]) +used = [False] * len(wds) + +lines = [ + f"withdraw board (start order) · timer {left}s left · available={cur}:{bal} · pendingIncoming={cur}:{pend}" +] +if not rows: + lines.append(" (no OK/OK_BANK rows in TSV yet)") +else: + for i, r in enumerate(rows, 1): + n = num(r["amt"]) + match = None + for j, w in enumerate(wds): + if used[j]: + continue + if w["n"] == n or w["amt"] == r["amt"] or w["amt"].endswith(":" + n): + used[j] = True + match = w + break + if match is None: + wlabel = "wallet=? (no tx match)" + elif match["phase"] == "pendingIncoming": + wlabel = f"wallet=pendingIncoming ({match['st']})" + elif match["phase"] == "done": + wlabel = f"wallet=done ({match['st']})" + else: + wlabel = f"wallet={match['phase']} ({match['st']})" + wid = r["wid"] or "-" + lines.append( + f" #{i:<3} rung={r['rung']:<4} {r['amt']:<22} bank={r['bank']:<8} {wlabel} wid={wid}" + ) + pending_n = sum(1 for w in wds if w["phase"] == "pendingIncoming") + done_n = sum(1 for w in wds if w["phase"] == "done") + lines.append( + f" summary: tsv_ok={len(rows)} · wallet_wd={len(wds)} (pendingIncoming={pending_n} done={done_n})" + ) + +print("\n".join(lines)) +PY + ) + if [ -n "$board" ]; then + while IFS= read -r line || [ -n "$line" ]; do + [ -n "$line" ] && info pay "$line" + done <<<"$board" + else + info pay "timer ${left}s left · available=${CUR}:${bal} · pendingIncoming=${CUR}:${pend} · same wallet (board n/a)" + fi +} + +# Wait for spendable coins on the *same* wallet DB as withdraw (WDB). +# After OK_BANK, available is often still 0 while pendingIncoming > 0 — pay needs available. +# Shows withdraw board in start order + countdown (never looks hung). ladder_wait_available() { local wait_s="${1:-$LADDER_PAY_WAIT_AVAILABLE_S}" - local end bal + local end now left bal pend tick=0 end=$(( $(date +%s) + wait_s )) bal=$(wallet_avail) + pend=$(wallet_pending_in 2>/dev/null || echo "0") + if python3 -c 'import sys; from decimal import Decimal; sys.exit(0 if Decimal(sys.argv[1])>0 else 1)' "$bal" 2>/dev/null; then + printf '%s' "$bal" + return 0 + fi + info pay "same wallet as withdraw (db=$(basename "${WDB:-wallet}")) · available=${CUR}:${bal} · pendingIncoming=${CUR}:${pend}" + info pay "waiting up to ${wait_s}s for spendable available>0 — board lists OK/OK_BANK withdraws in start order" + # initial board before loop + wcli transactions >"$SCRATCH/tx-wait-pay.out" 2>&1 || true + ladder_print_wd_board "$wait_s" "$bal" "$pend" while python3 -c 'import sys; from decimal import Decimal; sys.exit(0 if Decimal(sys.argv[1])<=0 else 1)' "$bal" 2>/dev/null; do - [ "$(date +%s)" -ge "$end" ] && break + now=$(date +%s) + left=$(( end - now )) + if [ "$left" -le 0 ]; then + wcli transactions >"$SCRATCH/tx-wait-pay.out" 2>&1 || true + ladder_print_wd_board "0" "$bal" "$pend" + info pay "timer 0s left — stop waiting (available still ${CUR}:0)" + break + fi + # nudge wallet (same DB); do not run-until-done (can hang) wcli transactions >"$SCRATCH/tx-wait-pay.out" 2>&1 || true + if command -v timeout >/dev/null 2>&1 || command -v gtimeout >/dev/null 2>&1; then + local _to + _to=$(command -v gtimeout 2>/dev/null || command -v timeout) + "$_to" 8 wcli advanced run-pending >"$SCRATCH/run-pending.out" 2>&1 || true + fi + # progress board every ~5s (tick every 2s → every 3rd) + if [ $((tick % 3)) -eq 0 ]; then + ladder_print_wd_board "$left" "$bal" "$pend" + fi sleep 2 + tick=$((tick + 1)) bal=$(wallet_avail) + pend=$(wallet_pending_in 2>/dev/null || echo "0") done + if python3 -c 'import sys; from decimal import Decimal; sys.exit(0 if Decimal(sys.argv[1])>0 else 1)' "$bal" 2>/dev/null; then + wcli transactions >"$SCRATCH/tx-wait-pay.out" 2>&1 || true + ladder_print_wd_board "ready" "$bal" "$pend" + ok pay "spendable coins ready available=${CUR}:${bal} · $(format_amount_alt "${CUR}:${bal}") (same wallet)" + else + warn pay "no spendable coins after ${wait_s}s" \ + "problem: available still ${CUR}:0 (pendingIncoming=${CUR}:${pend}). Board above shows which withdraws still pendingIncoming. Pay will SKIP_BALANCE / skip max-pay until wirewatch+wallet finish. Same wallet as withdraw." + fi printf '%s' "$bal" } @@ -346,7 +542,9 @@ ladder_run_pay_phase() { info pay "public free template ${LADDER_FREE_TEMPLATE}@${LADDER_FREE_TEMPLATE_INSTANCE} (no merchant token)" fi + # Same cumulative wallet as withdraw (WDB=wallet-main.sqlite3) wallet_prepare "main" + info pay "using same wallet as withdraw · WDB=${WDB:-?}" local bal0 bal0=$(ladder_wait_available "$LADDER_PAY_WAIT_AVAILABLE_S") info "pay budget" "wallet available ${CUR}:${bal0} · $(format_amount_alt "${CUR}:${bal0}") — never order more than this"