release 1.22.0: modular ladder withdraw + pay max-search
Split pay into ladder/lib_pay.sh; force-select extract in ladder/extract_rpubs.py. check_goa_ladder.sh is only a compat shim. max-ladder now hunts highest withdrawable and highest payable (never above wallet available), using public free-amount template goa-free on hacktivism. Classic remains default 23-rung wd+pay. Pay phase always runs when LADDER_PAY=1 (visible SKIP_BALANCE when no spendable coins). Report includes best_pay / alt names.
This commit is contained in:
parent
32ffb6cdb1
commit
76a253613a
9 changed files with 449 additions and 274 deletions
163
ladder/extract_rpubs.py
Executable file
163
ladder/extract_rpubs.py
Executable file
|
|
@ -0,0 +1,163 @@
|
|||
#!/usr/bin/env python3
|
||||
"""Extract reserve_pub candidates from wallet accept + transactions dumps.
|
||||
|
||||
Used by check_amount_ladder.sh force-select. Kept as a file (not a bash
|
||||
heredoc) so shell refactors cannot turn Python ``continue`` into ``return``.
|
||||
|
||||
Usage:
|
||||
ladder_extract_rpubs.py WID AMT accept.out tx.json used-rpubs.txt
|
||||
Prints one reserve pub per line, preferred first.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import re
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def main(argv: list[str]) -> int:
|
||||
if len(argv) < 6:
|
||||
print(
|
||||
"usage: ladder_extract_rpubs.py WID AMT accept.out tx.json used-rpubs.txt",
|
||||
file=sys.stderr,
|
||||
)
|
||||
return 2
|
||||
|
||||
wid = argv[1]
|
||||
amt = argv[2]
|
||||
paths = argv[3:5]
|
||||
used_path = argv[5]
|
||||
used: set[str] = set()
|
||||
if Path(used_path).is_file():
|
||||
used = {ln.strip() for ln in open(used_path) if ln.strip()}
|
||||
|
||||
amt_num = amt.split(":", 1)[-1] if amt else ""
|
||||
dec = json.JSONDecoder()
|
||||
|
||||
def walk_collect(o, bag, ctx=None):
|
||||
ctx = dict(ctx or {})
|
||||
if isinstance(o, dict):
|
||||
for k, v in o.items():
|
||||
if not isinstance(v, str):
|
||||
continue
|
||||
kl = str(k).lower().replace("-", "_")
|
||||
if kl in (
|
||||
"withdrawal_id",
|
||||
"withdraw_id",
|
||||
"wopid",
|
||||
"id",
|
||||
"transactionid",
|
||||
"transaction_id",
|
||||
):
|
||||
ctx["id"] = v
|
||||
elif kl in (
|
||||
"amount",
|
||||
"rawamount",
|
||||
"instructedamount",
|
||||
"amounteffective",
|
||||
"amountraw",
|
||||
"transferamount",
|
||||
):
|
||||
ctx["amount"] = v
|
||||
elif kl in (
|
||||
"taler_withdraw_uri",
|
||||
"talerwithdrawuri",
|
||||
"uri",
|
||||
"bankconfirmationurl",
|
||||
"confirmtransferurl",
|
||||
):
|
||||
ctx["uri"] = v
|
||||
for k, v in o.items():
|
||||
kl = str(k).lower().replace("-", "_")
|
||||
if kl in ("reserve_pub", "reservepub") and isinstance(v, str) and len(v) >= 40:
|
||||
bag.append((v, dict(ctx)))
|
||||
elif not isinstance(v, (str, int, float, bool)) and v is not None:
|
||||
walk_collect(v, bag, ctx)
|
||||
elif isinstance(o, list):
|
||||
for i in o:
|
||||
walk_collect(i, bag, ctx)
|
||||
|
||||
def score_ctx(pub, ctx, window=""):
|
||||
score = 0
|
||||
cid = str(ctx.get("id") or "")
|
||||
camt = str(ctx.get("amount") or "")
|
||||
curi = str(ctx.get("uri") or "")
|
||||
if wid and wid in curi:
|
||||
score += 120
|
||||
if wid and wid in cid:
|
||||
score += 100
|
||||
if wid and wid in window:
|
||||
score += 80
|
||||
if amt and (camt == amt or camt.endswith(amt_num)):
|
||||
score += 50
|
||||
if amt and amt in window:
|
||||
score += 30
|
||||
if pub in used:
|
||||
score -= 200
|
||||
return score
|
||||
|
||||
def iter_json_objects(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
|
||||
|
||||
raw_pubs: list[str] = []
|
||||
scored: list[tuple[int, str]] = []
|
||||
|
||||
for p in paths:
|
||||
try:
|
||||
t = open(p, errors="replace").read()
|
||||
except Exception:
|
||||
continue
|
||||
for o in iter_json_objects(t):
|
||||
bag = []
|
||||
walk_collect(o, bag)
|
||||
for pub, ctx in bag:
|
||||
raw_pubs.append(pub)
|
||||
scored.append((score_ctx(pub, ctx), pub))
|
||||
|
||||
rpub_pats = (
|
||||
r"\"reservePub\"\s*:\s*\"([A-Z0-9]{40,})\"",
|
||||
r"\"reserve_pub\"\s*:\s*\"([A-Z0-9]{40,})\"",
|
||||
r"reserve[_ ]?pub[\"\s:=]+([A-Z0-9]{40,})",
|
||||
)
|
||||
for p in paths:
|
||||
try:
|
||||
blob = open(p, errors="replace").read()
|
||||
except Exception:
|
||||
continue
|
||||
for pat in rpub_pats:
|
||||
for m in re.finditer(pat, blob, re.I):
|
||||
pub = m.group(1)
|
||||
raw_pubs.append(pub)
|
||||
window = blob[max(0, m.start() - 600) : m.end() + 600]
|
||||
scored.append((score_ctx(pub, {}, window) + 10, pub))
|
||||
|
||||
order: list[str] = []
|
||||
seen: set[str] = set()
|
||||
for score, pub in sorted(scored, key=lambda x: x[0], reverse=True):
|
||||
if pub in seen or pub in used:
|
||||
continue
|
||||
seen.add(pub)
|
||||
order.append(pub)
|
||||
for pub in reversed(raw_pubs):
|
||||
if pub in seen or pub in used:
|
||||
continue
|
||||
seen.add(pub)
|
||||
order.append(pub)
|
||||
for pub in order:
|
||||
print(pub)
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main(sys.argv))
|
||||
Loading…
Add table
Add a link
Reference in a new issue