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:
Hernâni Marques 2026-07-19 14:34:47 +02:00
parent 35cd86bd43
commit fd5ac80566
No known key found for this signature in database
6 changed files with 232 additions and 6 deletions

View file

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