fix 1.22.1: pay-wait withdraw board (start order + pendingIncoming)
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.
This commit is contained in:
parent
35cd86bd43
commit
fd5ac80566
6 changed files with 232 additions and 6 deletions
|
|
@ -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 |
|
||||
|
||||
|
|
|
|||
2
VERSION
2
VERSION
|
|
@ -1 +1 @@
|
|||
1.22.0
|
||||
1.22.1
|
||||
|
|
|
|||
|
|
@ -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.)* |
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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) |
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue