feat(monitoring): ladder max-1 pin, pay ladder, cumulative wallet
This commit is contained in:
parent
982432f191
commit
765c528313
1 changed files with 509 additions and 88 deletions
|
|
@ -7,21 +7,21 @@
|
||||||
# 3) wallet-cli accept-uri only (no run-until-done — hangs / developer ban)
|
# 3) wallet-cli accept-uri only (no run-until-done — hangs / developer ban)
|
||||||
# 4) bank confirm when selected; settle = poll balance + bank transfer_done
|
# 4) bank confirm when selected; settle = poll balance + bank transfer_done
|
||||||
#
|
#
|
||||||
# Amounts: random strictly increasing; 0 and max fixed.
|
# Amounts: random strictly increasing; fixed pins 0, max-1, max (23 default).
|
||||||
|
# Phase A: withdraw ladder into ONE cumulative wallet (mids scaled up for pay budget).
|
||||||
|
# Phase B: pay ladder 0 → low → … → max-1 → max (same step count).
|
||||||
# On first hard failure: stop, print timing report, exit 1.
|
# On first hard failure: stop, print timing report, exit 1.
|
||||||
# Soft: GOA:0 / wallet 7006 (no denoms) → WARN and continue.
|
# Soft: GOA:0 / wallet 7006 / absolute max (CEILING_REJECT) → WARN and continue.
|
||||||
#
|
#
|
||||||
# Env:
|
# Env:
|
||||||
# LADDER_STEPS total rungs (default 23) = 0 + (N-2) random + max
|
# LADDER_STEPS total rungs (default 23) = 0 + (N-3) random + max-1 + max
|
||||||
# LADDER_MAX_AMOUNT fixed last pin (libeufin ceiling 4503599627370496)
|
# LADDER_MAX_AMOUNT absolute last pin (libeufin ceiling 4503599627370496)
|
||||||
# LADDER_TIMEOUT_S default 3600
|
# LADDER_WITHDRAW_SCALE mid withdraw amounts × this vs pay mids (default 1.5)
|
||||||
# LADDER_LOAD=0 skip host load snapshots
|
# LADDER_PAY=0 skip payment phase
|
||||||
|
# LADDER_TIMEOUT_S default 3600
|
||||||
|
# LADDER_LOAD=0 skip host load snapshots
|
||||||
# LADDER_SETTLE_ROUNDS / LADDER_SETTLE_SLEEP — balance poll only (no shepherd)
|
# LADDER_SETTLE_ROUNDS / LADDER_SETTLE_SLEEP — balance poll only (no shepherd)
|
||||||
# EXP_PW_FILE, LADDER_REPORT_DIR, …
|
# EXP_PW_FILE, LADDER_REPORT_DIR, MERCHANT_INSTANCE, …
|
||||||
#
|
|
||||||
# Path: always [0] → strictly increasing random (log-uniform) → [max]
|
|
||||||
# Only 0 and max are fixed amounts.
|
|
||||||
# Load: koopa host snapshot BEFORE withdraw ladder and AFTER (loadavg, mem, podman).
|
|
||||||
#
|
#
|
||||||
# Phase: ./taler-monitoring.sh ladder
|
# Phase: ./taler-monitoring.sh ladder
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
@ -46,22 +46,31 @@ elapsed_ms() {
|
||||||
: "${LADDER_MAX_AMOUNT:=4503599627370496}"
|
: "${LADDER_MAX_AMOUNT:=4503599627370496}"
|
||||||
: "${LADDER_STEPS:=23}"
|
: "${LADDER_STEPS:=23}"
|
||||||
: "${LADDER_LOAD:=1}"
|
: "${LADDER_LOAD:=1}"
|
||||||
|
: "${LADDER_PAY:=1}"
|
||||||
|
# Withdraw mids = pay mids × scale (so wallet can afford the pay ladder)
|
||||||
|
: "${LADDER_WITHDRAW_SCALE:=1.5}"
|
||||||
# Floor for random mids (must be ≥ smallest exchange coin; GOA min denom ≈ 0.000001)
|
# Floor for random mids (must be ≥ smallest exchange coin; GOA min denom ≈ 0.000001)
|
||||||
: "${LADDER_MIN_AMOUNT:=0.000001}"
|
: "${LADDER_MIN_AMOUNT:=0.000001}"
|
||||||
: "${LADDER_CONFIRM_POLLS:=40}"
|
: "${LADDER_CONFIRM_POLLS:=40}"
|
||||||
|
: "${LADDER_PAY_SETTLE_ROUNDS:=6}"
|
||||||
|
: "${MERCHANT_INSTANCE:=goa-demo-cp4zqk}"
|
||||||
|
|
||||||
CUR="${EXPECT_CURRENCY:-GOA}"
|
CUR="${EXPECT_CURRENCY:-GOA}"
|
||||||
BANK="${BANK_PUBLIC%/}"
|
BANK="${BANK_PUBLIC%/}"
|
||||||
EX="${EXCHANGE_PUBLIC%/}/"
|
EX="${EXCHANGE_PUBLIC%/}/"
|
||||||
|
MER="${MERCHANT_PUBLIC%/}"
|
||||||
|
INST="${MERCHANT_INSTANCE}"
|
||||||
SCRATCH=$(mktemp -d)
|
SCRATCH=$(mktemp -d)
|
||||||
WDB="$SCRATCH/wallet.sqlite3"
|
WDB="$SCRATCH/wallet.sqlite3"
|
||||||
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"
|
||||||
|
PAY_TSV="$REPORT_DIR/ladder-pay-results.tsv"
|
||||||
JSON="$REPORT_DIR/ladder-report.json"
|
JSON="$REPORT_DIR/ladder-report.json"
|
||||||
LOAD_BEFORE="$REPORT_DIR/load-before.json"
|
LOAD_BEFORE="$REPORT_DIR/load-before.json"
|
||||||
LOAD_AFTER="$REPORT_DIR/load-after.json"
|
LOAD_AFTER="$REPORT_DIR/load-after.json"
|
||||||
echo -e "rung\trange\tamount\tstatus\tms_mint\tms_accept\tms_confirm\tms_settle\tms_total\twid\tnote" >"$TSV"
|
echo -e "rung\trange\tamount\tstatus\tms_mint\tms_accept\tms_confirm\tms_settle\tms_total\twid\tnote" >"$TSV"
|
||||||
|
echo -e "rung\trange\tamount\tstatus\tms_order\tms_handle\tms_settle\tms_total\toid\tnote" >"$PAY_TSV"
|
||||||
|
|
||||||
ladder_over() {
|
ladder_over() {
|
||||||
local now
|
local now
|
||||||
|
|
@ -94,21 +103,30 @@ print("0")
|
||||||
' "$CUR" 2>/dev/null || echo "0"
|
' "$CUR" 2>/dev/null || echo "0"
|
||||||
}
|
}
|
||||||
|
|
||||||
# Build exactly LADDER_STEPS: [0] + (N-2) log-uniform random increasing + [max]
|
# Build paired pay + withdraw ladders (same step count, shared random shape).
|
||||||
# Random mids are always ≥ LADDER_MIN_AMOUNT (smallest viable coin).
|
# Pay: [0] + log-uniform mids + [max-1] + [max]
|
||||||
build_ladder() {
|
# Wd: [0] + max(mid, mid×scale) mids + [max-1] + [max] (mids higher → enough to spend)
|
||||||
python3 - <<'PY' "$CUR" "${LADDER_MAX_AMOUNT}" "${LADDER_STEPS}" "${LADDER_MIN_AMOUNT}"
|
# Writes: $1 = withdraw list file, $2 = pay list file (space-separated CUR:amt lines as one line each)
|
||||||
|
build_ladder_pair() {
|
||||||
|
local wd_out="$1" pay_out="$2"
|
||||||
|
python3 - <<'PY' "$CUR" "${LADDER_MAX_AMOUNT}" "${LADDER_STEPS}" "${LADDER_MIN_AMOUNT}" "${LADDER_WITHDRAW_SCALE}" "$wd_out" "$pay_out"
|
||||||
import math, random, sys
|
import math, random, sys
|
||||||
from decimal import Decimal, ROUND_HALF_UP, ROUND_UP
|
from decimal import Decimal, ROUND_HALF_UP, ROUND_UP
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
cur = sys.argv[1]
|
cur = sys.argv[1]
|
||||||
max_amt = Decimal(sys.argv[2])
|
max_amt = Decimal(sys.argv[2])
|
||||||
steps = max(2, int(sys.argv[3]))
|
steps = max(1, int(sys.argv[3]))
|
||||||
min_amt = Decimal(sys.argv[4])
|
min_amt = Decimal(sys.argv[4])
|
||||||
|
scale = Decimal(sys.argv[5])
|
||||||
|
wd_path, pay_path = Path(sys.argv[6]), Path(sys.argv[7])
|
||||||
if min_amt <= 0:
|
if min_amt <= 0:
|
||||||
min_amt = Decimal("0.000001")
|
min_amt = Decimal("0.000001")
|
||||||
if min_amt >= max_amt:
|
if min_amt >= max_amt:
|
||||||
min_amt = max_amt / Decimal(1000)
|
min_amt = max_amt / Decimal(1000)
|
||||||
|
if scale < 1:
|
||||||
|
scale = Decimal(1)
|
||||||
|
max_m1 = max_amt - Decimal(1) if max_amt > 1 else max_amt
|
||||||
|
|
||||||
def fmt(v: Decimal) -> str:
|
def fmt(v: Decimal) -> str:
|
||||||
q = v.quantize(Decimal("0.00000001"), rounding=ROUND_HALF_UP)
|
q = v.quantize(Decimal("0.00000001"), rounding=ROUND_HALF_UP)
|
||||||
|
|
@ -119,61 +137,134 @@ def fmt(v: Decimal) -> str:
|
||||||
def quant(v: Decimal) -> Decimal:
|
def quant(v: Decimal) -> Decimal:
|
||||||
if v >= 1:
|
if v >= 1:
|
||||||
return v.quantize(Decimal(1), rounding=ROUND_HALF_UP)
|
return v.quantize(Decimal(1), rounding=ROUND_HALF_UP)
|
||||||
# snap up to min_amt grid for sub-unit amounts
|
|
||||||
if v < min_amt:
|
if v < min_amt:
|
||||||
return min_amt
|
return min_amt
|
||||||
return v.quantize(Decimal("0.00000001"), rounding=ROUND_UP)
|
return v.quantize(Decimal("0.00000001"), rounding=ROUND_UP)
|
||||||
|
|
||||||
mid = steps - 2 # between 0 and max
|
def amt(v: Decimal) -> str:
|
||||||
out = ["%s:0" % cur]
|
return "%s:%s" % (cur, fmt(v))
|
||||||
if mid > 0 and max_amt > 0:
|
|
||||||
lo, hi = float(min_amt), float(max_amt) * 0.999999
|
def make_mids(n_mid: int, hi_cap: Decimal):
|
||||||
|
if n_mid <= 0 or hi_cap <= min_amt:
|
||||||
|
return []
|
||||||
|
lo, hi = float(min_amt), float(hi_cap) * 0.999999
|
||||||
if hi <= lo:
|
if hi <= lo:
|
||||||
hi = lo * 10
|
hi = lo * 10
|
||||||
cuts = sorted(math.exp(random.uniform(math.log(lo), math.log(hi))) for _ in range(mid))
|
cuts = sorted(math.exp(random.uniform(math.log(lo), math.log(hi))) for _ in range(n_mid))
|
||||||
prev = Decimal(0)
|
out, prev = [], Decimal(0)
|
||||||
for c in cuts:
|
for c in cuts:
|
||||||
v = quant(Decimal(str(c)))
|
v = quant(Decimal(str(c)))
|
||||||
if v <= prev:
|
if v <= prev:
|
||||||
step = min_amt if prev < 1 else max(prev * Decimal("1e-6"), Decimal(1))
|
step = min_amt if prev < 1 else max(prev * Decimal("1e-6"), Decimal(1))
|
||||||
v = quant(prev + step)
|
v = quant(prev + step)
|
||||||
if v >= max_amt:
|
if v >= hi_cap:
|
||||||
v = max_amt - (Decimal(1) if max_amt > 1 else min_amt)
|
v = quant(hi_cap - (Decimal(1) if hi_cap > 1 else min_amt))
|
||||||
v = quant(v)
|
if v <= prev or v >= hi_cap:
|
||||||
if v <= prev or v >= max_amt:
|
|
||||||
continue
|
continue
|
||||||
out.append("%s:%s" % (cur, fmt(v)))
|
out.append(v)
|
||||||
prev = v
|
prev = v
|
||||||
out.append("%s:%s" % (cur, fmt(max_amt)))
|
return out
|
||||||
while len(out) > steps:
|
|
||||||
out.pop(len(out) // 2)
|
# Build withdraw ladder first (full range), then pay mids = withdraw/scale
|
||||||
while len(out) < steps and len(out) >= 2:
|
# so each pay mid is always cheaper than the matching withdraw mid.
|
||||||
i = len(out) // 2
|
if steps == 1:
|
||||||
a = Decimal(out[i - 1].split(":", 1)[1])
|
wd_vals = [max_amt]
|
||||||
b = Decimal(out[i].split(":", 1)[1])
|
elif steps == 2:
|
||||||
if a <= 0:
|
wd_vals = [Decimal(0), max_amt]
|
||||||
m = max(min_amt, b / 2 if b > 0 else min_amt)
|
else:
|
||||||
else:
|
n_mid = max(0, steps - 3)
|
||||||
m = (a * b).sqrt() if a * b > 0 else (a + b) / 2
|
wd_vals = [Decimal(0)] + make_mids(n_mid, max_m1) + [max_m1, max_amt]
|
||||||
m = quant(m)
|
while len(wd_vals) > steps and len(wd_vals) > 3:
|
||||||
if m <= a or m >= b:
|
wd_vals.pop(len(wd_vals) // 2)
|
||||||
break
|
while len(wd_vals) < steps and len(wd_vals) >= 2:
|
||||||
out.insert(i, "%s:%s" % (cur, fmt(m)))
|
i = max(1, len(wd_vals) - 2)
|
||||||
print(" ".join(out[:steps]))
|
a, b = wd_vals[i - 1], wd_vals[i]
|
||||||
|
if a <= 0:
|
||||||
|
m = max(min_amt, b / 2 if b > 0 else min_amt)
|
||||||
|
else:
|
||||||
|
m = (a * b).sqrt() if a * b > 0 else (a + b) / 2
|
||||||
|
m = quant(m)
|
||||||
|
if m <= a or m >= b:
|
||||||
|
break
|
||||||
|
wd_vals.insert(i, m)
|
||||||
|
wd_vals = wd_vals[:steps]
|
||||||
|
|
||||||
|
pay_vals = []
|
||||||
|
prev_p = Decimal(-1)
|
||||||
|
for i, w in enumerate(wd_vals):
|
||||||
|
is_first = i == 0
|
||||||
|
is_last = i == len(wd_vals) - 1
|
||||||
|
is_m1 = (not is_last) and w == max_m1 and i == len(wd_vals) - 2
|
||||||
|
if is_first and w == 0:
|
||||||
|
pay_vals.append(Decimal(0))
|
||||||
|
prev_p = Decimal(0)
|
||||||
|
continue
|
||||||
|
if is_last:
|
||||||
|
pay_vals.append(max_amt)
|
||||||
|
continue
|
||||||
|
if is_m1 or w == max_m1:
|
||||||
|
pay_vals.append(max_m1)
|
||||||
|
prev_p = max_m1
|
||||||
|
continue
|
||||||
|
# pay mid = withdraw / scale (strictly less funding needed per step)
|
||||||
|
p = quant(w / scale) if scale > 0 else w
|
||||||
|
if p < min_amt and w >= min_amt:
|
||||||
|
p = min_amt
|
||||||
|
if p <= prev_p:
|
||||||
|
p = quant(prev_p + (min_amt if prev_p < 1 else Decimal(1)))
|
||||||
|
if p >= w:
|
||||||
|
# keep pay strictly below this withdraw rung when possible
|
||||||
|
p = quant(w - (Decimal(1) if w > 1 else min_amt)) if w > prev_p else prev_p
|
||||||
|
if p <= prev_p:
|
||||||
|
p = prev_p # flat ok only if stuck; still ≤ w
|
||||||
|
pay_vals.append(p)
|
||||||
|
prev_p = p
|
||||||
|
|
||||||
|
assert len(pay_vals) == len(wd_vals)
|
||||||
|
# sanity: every non-pin pay mid ≤ matching withdraw mid
|
||||||
|
for i, (p, w) in enumerate(zip(pay_vals, wd_vals)):
|
||||||
|
if i == 0 or i >= len(wd_vals) - 2:
|
||||||
|
continue
|
||||||
|
if p > w:
|
||||||
|
pay_vals[i] = w
|
||||||
|
|
||||||
|
wd_path.write_text(" ".join(amt(v) for v in wd_vals) + "\n")
|
||||||
|
pay_path.write_text(" ".join(amt(v) for v in pay_vals) + "\n")
|
||||||
|
print(" ".join(amt(v) for v in wd_vals))
|
||||||
PY
|
PY
|
||||||
}
|
}
|
||||||
|
|
||||||
# shellcheck source=metrics.sh
|
# shellcheck source=metrics.sh
|
||||||
source "$ROOT/metrics.sh"
|
source "$ROOT/metrics.sh"
|
||||||
METRICS_DIR="$REPORT_DIR"
|
METRICS_DIR="$REPORT_DIR"
|
||||||
export METRICS_DIR CUR WDB CLI_JS
|
ALT_UNITS_FILE="${REPORT_DIR}/alt_unit_names.json"
|
||||||
|
export METRICS_DIR CUR WDB CLI_JS ALT_UNITS_FILE
|
||||||
|
# Ladder can disable host load without killing coin metrics
|
||||||
|
if [ "${LADDER_LOAD:-1}" = "0" ]; then
|
||||||
|
METRICS_LOAD=0
|
||||||
|
export METRICS_LOAD
|
||||||
|
fi
|
||||||
|
|
||||||
section "ladder · GOA withdraw (0 → random → max · ${LADDER_STEPS} steps)"
|
section "ladder · GOA withdraw (0 → random → max · ${LADDER_STEPS} steps)"
|
||||||
info "bank" "$BANK"
|
info "bank" "$BANK"
|
||||||
info "exchange" "$EX"
|
info "exchange" "$EX"
|
||||||
info "currency" "$CUR"
|
info "currency" "$CUR"
|
||||||
|
# alt_unit_names for human amounts (Kilo-GOA / Mega-GOA / … + GOA in parentheses)
|
||||||
|
if metrics_load_alt_units "${EX%/}/config"; then
|
||||||
|
info "alt_unit_names" "from ${EX%/}/config → $ALT_UNITS_FILE"
|
||||||
|
else
|
||||||
|
warn "alt_unit_names" "using built-in SI fallback ($ALT_UNITS_FILE)"
|
||||||
|
fi
|
||||||
info "budget" "${LADDER_TIMEOUT_S}s"
|
info "budget" "${LADDER_TIMEOUT_S}s"
|
||||||
info "steps" "${LADDER_STEPS} (0 + $((LADDER_STEPS - 2)) random≥${LADDER_MIN_AMOUNT} + max=${CUR}:${LADDER_MAX_AMOUNT})"
|
_max_m1=$(python3 -c 'import sys; print(int(sys.argv[1])-1)' "${LADDER_MAX_AMOUNT}" 2>/dev/null || echo "${LADDER_MAX_AMOUNT}-1")
|
||||||
|
info "steps" "${LADDER_STEPS} (0 + random≥${LADDER_MIN_AMOUNT} + max-1=${CUR}:${_max_m1} + max=${CUR}:${LADDER_MAX_AMOUNT})"
|
||||||
|
info "withdraw_scale" "${LADDER_WITHDRAW_SCALE}× pay mids (fund pay ladder)"
|
||||||
|
info "pay_phase" "$([ "${LADDER_PAY}" = "1" ] && echo enabled || echo disabled)"
|
||||||
|
|
||||||
|
section "ladder · load snapshot (before withdraws)"
|
||||||
|
metrics_report_load "$LOAD_BEFORE" "ladder-start" || true
|
||||||
|
# Fresh wallet per rung — baseline empty (or last-rung DB if re-used later)
|
||||||
|
metrics_report_coins "ladder-start" || true
|
||||||
|
|
||||||
if [ ! -f "$EXP_PW_FILE" ]; then
|
if [ ! -f "$EXP_PW_FILE" ]; then
|
||||||
err bank "explorer password missing" "$EXP_PW_FILE"
|
err bank "explorer password missing" "$EXP_PW_FILE"
|
||||||
|
|
@ -207,24 +298,28 @@ ms_tok=$(elapsed_ms "$t0")
|
||||||
[ -n "$TOK" ] || { err bank "explorer token failed"; exit 1; }
|
[ -n "$TOK" ] || { err bank "explorer token failed"; exit 1; }
|
||||||
ok "explorer token (${ms_tok}ms)"
|
ok "explorer token (${ms_tok}ms)"
|
||||||
|
|
||||||
# Fresh wallet DB per rung: without run-until-done the same reserve_pub is reused
|
# ONE cumulative wallet for all withdraws + pays (need balance to spend).
|
||||||
# → bank 409 "Reserve pub already used" on later force-selects.
|
# force-select uses *last* reserve_pub from the current accept output.
|
||||||
wallet_prepare() {
|
wallet_prepare() {
|
||||||
local label="${1:-wallet}"
|
local label="${1:-wallet}"
|
||||||
WDB="$SCRATCH/wallet-${label}.sqlite3"
|
WDB="$SCRATCH/wallet-${label}.sqlite3"
|
||||||
export WDB
|
export WDB
|
||||||
rm -f "$WDB"
|
if [ ! -f "$WDB" ]; then
|
||||||
wcli exchanges add "$EX" >"$SCRATCH/ex-add-$label.out" 2>&1 || true
|
wcli exchanges add "$EX" >"$SCRATCH/ex-add-$label.out" 2>&1 || true
|
||||||
wcli exchanges update "$EX" >"$SCRATCH/ex-upd-$label.out" 2>&1 || true
|
wcli exchanges update "$EX" >"$SCRATCH/ex-upd-$label.out" 2>&1 || true
|
||||||
wcli exchanges accept-tos "$EX" >"$SCRATCH/ex-tos-$label.out" 2>&1 || true
|
wcli exchanges accept-tos "$EX" >"$SCRATCH/ex-tos-$label.out" 2>&1 || true
|
||||||
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
t0=$(now_ms)
|
t0=$(now_ms)
|
||||||
wallet_prepare "bootstrap"
|
rm -f "$SCRATCH/wallet-main.sqlite3"
|
||||||
|
wallet_prepare "main"
|
||||||
ms_tos=$(elapsed_ms "$t0")
|
ms_tos=$(elapsed_ms "$t0")
|
||||||
ok "wallet exchange + ToS (${ms_tos}ms) — fresh DB per rung (no run-until-done)"
|
ok "wallet exchange + ToS (${ms_tos}ms) — cumulative DB for withdraw+pay (no run-until-done)"
|
||||||
|
|
||||||
LADDER_LIST=$(build_ladder)
|
build_ladder_pair "$SCRATCH/ladder-wd-plan.txt" "$SCRATCH/ladder-pay-plan.txt"
|
||||||
|
LADDER_LIST=$(tr -d '\n' <"$SCRATCH/ladder-wd-plan.txt")
|
||||||
|
PAY_LIST=$(tr -d '\n' <"$SCRATCH/ladder-pay-plan.txt")
|
||||||
: "${LADDER_MAX_RUNGS:=99}"
|
: "${LADDER_MAX_RUNGS:=99}"
|
||||||
# shellcheck disable=SC2086
|
# shellcheck disable=SC2086
|
||||||
set -- $LADDER_LIST
|
set -- $LADDER_LIST
|
||||||
|
|
@ -233,15 +328,23 @@ if [ "$#" -gt "$LADDER_MAX_RUNGS" ]; then
|
||||||
set -- $(printf '%s\n' "$@" | head -n "$LADDER_MAX_RUNGS")
|
set -- $(printf '%s\n' "$@" | head -n "$LADDER_MAX_RUNGS")
|
||||||
fi
|
fi
|
||||||
LADDER_N=$#
|
LADDER_N=$#
|
||||||
info "ladder plan" "$*"
|
info "withdraw plan" "$*"
|
||||||
|
info "withdraw plan (alt)" "$(format_amount_list_alt "$@")"
|
||||||
|
info "pay plan" "$PAY_LIST"
|
||||||
|
# shellcheck disable=SC2086
|
||||||
|
info "pay plan (alt)" "$(format_amount_list_alt $PAY_LIST)"
|
||||||
printf '%s\n' "$@" >"$SCRATCH/ladder-plan.txt"
|
printf '%s\n' "$@" >"$SCRATCH/ladder-plan.txt"
|
||||||
|
printf '%s\n' $PAY_LIST >"$SCRATCH/ladder-pay-plan-lines.txt" 2>/dev/null || true
|
||||||
|
|
||||||
OK_N=0
|
OK_N=0
|
||||||
FAIL_N_L=0
|
FAIL_N_L=0
|
||||||
|
PAY_OK_N=0
|
||||||
|
PAY_FAIL_N=0
|
||||||
STOP_REASON=""
|
STOP_REASON=""
|
||||||
STOP_AMOUNT=""
|
STOP_AMOUNT=""
|
||||||
declare -a RUNG_JSON=()
|
declare -a RUNG_JSON=()
|
||||||
|
|
||||||
|
section "ladder · phase A · withdraw (${LADDER_N} rungs)"
|
||||||
rung=0
|
rung=0
|
||||||
for AMT in "$@"; do
|
for AMT in "$@"; do
|
||||||
rung=$((rung + 1))
|
rung=$((rung + 1))
|
||||||
|
|
@ -252,13 +355,15 @@ for AMT in "$@"; do
|
||||||
break
|
break
|
||||||
fi
|
fi
|
||||||
|
|
||||||
section "ladder · rung $rung $AMT"
|
section "ladder · rung $rung $AMT · $(format_amount_alt "$AMT")"
|
||||||
tag=$(printf '%s' "$AMT" | tr '.:' '__')
|
tag=$(printf '%s' "$AMT" | tr '.:' '__')
|
||||||
# TSV "range" column: fixed pins at ends, random mids (no legacy LADDER_RANGES)
|
# TSV "range" column: fixed pins at ends, random mids (refined after AMT_NUM)
|
||||||
if [ "$rung" -eq 1 ]; then
|
if [ "$rung" -eq 1 ]; then
|
||||||
range_note="pin:0"
|
range_note="pin:0"
|
||||||
elif [ "$rung" -eq "$LADDER_N" ]; then
|
elif [ "$rung" -eq "$LADDER_N" ]; then
|
||||||
range_note="pin:max"
|
range_note="pin:max"
|
||||||
|
elif [ "$rung" -eq $((LADDER_N - 1)) ] && [ "$LADDER_N" -ge 3 ]; then
|
||||||
|
range_note="pin:max-1"
|
||||||
else
|
else
|
||||||
range_note="random"
|
range_note="random"
|
||||||
fi
|
fi
|
||||||
|
|
@ -269,8 +374,8 @@ for AMT in "$@"; do
|
||||||
WID="-"
|
WID="-"
|
||||||
FORCE_SEL_409_LOGGED=0
|
FORCE_SEL_409_LOGGED=0
|
||||||
|
|
||||||
# New wallet sqlite for this rung → unique reserve_pub (avoids bank 5114)
|
# keep cumulative main wallet
|
||||||
wallet_prepare "r${rung}"
|
wallet_prepare "main"
|
||||||
|
|
||||||
# mint from explorer pool
|
# mint from explorer pool
|
||||||
t0=$(now_ms)
|
t0=$(now_ms)
|
||||||
|
|
@ -282,13 +387,24 @@ for AMT in "$@"; do
|
||||||
ms_mint=$(elapsed_ms "$t0")
|
ms_mint=$(elapsed_ms "$t0")
|
||||||
WID=$(python3 -c 'import json;d=json.load(open("'"$SCRATCH"'/wd-'"$tag"'.json"));print(d.get("withdrawal_id") or "")' 2>/dev/null || true)
|
WID=$(python3 -c 'import json;d=json.load(open("'"$SCRATCH"'/wd-'"$tag"'.json"));print(d.get("withdrawal_id") or "")' 2>/dev/null || true)
|
||||||
URI=$(python3 -c 'import json;u=json.load(open("'"$SCRATCH"'/wd-'"$tag"'.json")).get("taler_withdraw_uri") or "";print(u.replace(":443/","/"))' 2>/dev/null || true)
|
URI=$(python3 -c 'import json;u=json.load(open("'"$SCRATCH"'/wd-'"$tag"'.json")).get("taler_withdraw_uri") or "";print(u.replace(":443/","/"))' 2>/dev/null || true)
|
||||||
# numeric amount (for zero / settle special-cases)
|
# numeric amount (for zero / settle / ceiling special-cases)
|
||||||
AMT_NUM=$(python3 -c 'import sys; print(sys.argv[1].split(":",1)[-1])' "$AMT")
|
AMT_NUM=$(python3 -c 'import sys; print(sys.argv[1].split(":",1)[-1])' "$AMT")
|
||||||
IS_ZERO=0
|
IS_ZERO=0
|
||||||
python3 -c 'import sys; from decimal import Decimal; sys.exit(0 if Decimal(sys.argv[1])==0 else 1)' "$AMT_NUM" 2>/dev/null && IS_ZERO=1
|
python3 -c 'import sys; from decimal import Decimal; sys.exit(0 if Decimal(sys.argv[1])==0 else 1)' "$AMT_NUM" 2>/dev/null && IS_ZERO=1
|
||||||
|
IS_MAX_PIN=0
|
||||||
|
IS_MAX_M1_PIN=0
|
||||||
|
if [ "$AMT_NUM" = "${LADDER_MAX_AMOUNT}" ]; then
|
||||||
|
IS_MAX_PIN=1
|
||||||
|
range_note="pin:max"
|
||||||
|
elif [ "$AMT_NUM" = "$((LADDER_MAX_AMOUNT - 1))" ] 2>/dev/null || \
|
||||||
|
[ "$AMT_NUM" = "$(python3 -c 'print(int("'"$LADDER_MAX_AMOUNT"'")-1)')" ]; then
|
||||||
|
IS_MAX_M1_PIN=1
|
||||||
|
range_note="pin:max-1"
|
||||||
|
fi
|
||||||
|
|
||||||
if [ "$code" != "200" ] && [ "$code" != "201" ] || [ -z "$WID" ] || [ -z "$URI" ]; then
|
if [ "$code" != "200" ] && [ "$code" != "201" ] || [ -z "$WID" ] || [ -z "$URI" ]; then
|
||||||
note="mint HTTP $code $(head -c 100 "$SCRATCH/wd-$tag.json" 2>/dev/null | tr '\n' ' ')"
|
# strip quotes so STOP_REASON never breaks later shell/python argv
|
||||||
|
note="mint HTTP $code $(head -c 100 "$SCRATCH/wd-$tag.json" 2>/dev/null | tr '\n\"' ' ')"
|
||||||
ms_total=$(elapsed_ms "$t_rung")
|
ms_total=$(elapsed_ms "$t_rung")
|
||||||
if [ "$IS_ZERO" = "1" ]; then
|
if [ "$IS_ZERO" = "1" ]; then
|
||||||
# Probe only: bank may reject GOA:0 — record and continue ladder
|
# Probe only: bank may reject GOA:0 — record and continue ladder
|
||||||
|
|
@ -300,6 +416,15 @@ for AMT in "$@"; do
|
||||||
OK_N=$((OK_N + 1))
|
OK_N=$((OK_N + 1))
|
||||||
continue
|
continue
|
||||||
fi
|
fi
|
||||||
|
# Absolute max is a ceiling probe — bank often returns SQL P0001/5110; do not abort.
|
||||||
|
if [ "$IS_MAX_PIN" = "1" ]; then
|
||||||
|
status="CEILING_REJECT"
|
||||||
|
note="absolute max rejected (ceiling probe; max-1 is the hard pin): $note"
|
||||||
|
warn bank "mint $AMT rejected (ceiling)" \
|
||||||
|
"problem: bank rejects absolute LADDER_MAX_AMOUNT (often SQL P0001/5110). max-1 rung is the last expected success. Ladder continues to report. detail: $note"
|
||||||
|
echo -e "${rung}\t${range_note}\t${AMT}\t${status}\t${ms_mint}\t${ms_accept}\t${ms_confirm}\t${ms_settle}\t${ms_total}\t${WID}\t${note}" >>"$TSV"
|
||||||
|
continue
|
||||||
|
fi
|
||||||
err bank "mint $AMT failed" "$note"
|
err bank "mint $AMT failed" "$note"
|
||||||
status="FAIL_MINT"
|
status="FAIL_MINT"
|
||||||
STOP_REASON="$note"
|
STOP_REASON="$note"
|
||||||
|
|
@ -538,12 +663,14 @@ sys.exit(0 if Decimal(sys.argv[1]) > Decimal(sys.argv[2]) else 1)
|
||||||
ok "settle $AMT → ${CUR}:${after} (settle ${ms_settle}ms, rung ${ms_total}ms)"
|
ok "settle $AMT → ${CUR}:${after} (settle ${ms_settle}ms, rung ${ms_total}ms)"
|
||||||
OK_N=$((OK_N + 1))
|
OK_N=$((OK_N + 1))
|
||||||
echo -e "${rung}\t${range_note}\t${AMT}\t${status}\t${ms_mint}\t${ms_accept}\t${ms_confirm}\t${ms_settle}\t${ms_total}\t${WID}\t${note}" >>"$TSV"
|
echo -e "${rung}\t${range_note}\t${AMT}\t${status}\t${ms_mint}\t${ms_accept}\t${ms_confirm}\t${ms_settle}\t${ms_total}\t${WID}\t${note}" >>"$TSV"
|
||||||
|
metrics_report_coins "ladder-r${rung}-after-${tag}" || true
|
||||||
elif echo "$xfer" | grep -qi True; then
|
elif echo "$xfer" | grep -qi True; then
|
||||||
status="OK_BANK"
|
status="OK_BANK"
|
||||||
note="bank transfer_done avail=${after} $xfer (no run-until-done)"
|
note="bank transfer_done avail=${after} $xfer (no run-until-done)"
|
||||||
ok "settle $AMT bank transfer_done (wallet avail=${CUR}:${after}, ${ms_settle}ms)"
|
ok "settle $AMT bank transfer_done (wallet avail=${CUR}:${after}, ${ms_settle}ms)"
|
||||||
OK_N=$((OK_N + 1))
|
OK_N=$((OK_N + 1))
|
||||||
echo -e "${rung}\t${range_note}\t${AMT}\t${status}\t${ms_mint}\t${ms_accept}\t${ms_confirm}\t${ms_settle}\t${ms_total}\t${WID}\t${note}" >>"$TSV"
|
echo -e "${rung}\t${range_note}\t${AMT}\t${status}\t${ms_mint}\t${ms_accept}\t${ms_confirm}\t${ms_settle}\t${ms_total}\t${WID}\t${note}" >>"$TSV"
|
||||||
|
metrics_report_coins "ladder-r${rung}-after-${tag}" || true
|
||||||
else
|
else
|
||||||
note="no coins / no transfer_done avail=${after} $xfer"
|
note="no coins / no transfer_done avail=${after} $xfer"
|
||||||
err wallet "settle $AMT" "$note"
|
err wallet "settle $AMT" "$note"
|
||||||
|
|
@ -552,10 +679,250 @@ sys.exit(0 if Decimal(sys.argv[1]) > Decimal(sys.argv[2]) else 1)
|
||||||
STOP_AMOUNT="$AMT"
|
STOP_AMOUNT="$AMT"
|
||||||
FAIL_N_L=$((FAIL_N_L + 1))
|
FAIL_N_L=$((FAIL_N_L + 1))
|
||||||
echo -e "${rung}\t${range_note}\t${AMT}\t${status}\t${ms_mint}\t${ms_accept}\t${ms_confirm}\t${ms_settle}\t${ms_total}\t${WID}\t${note}" >>"$TSV"
|
echo -e "${rung}\t${range_note}\t${AMT}\t${status}\t${ms_mint}\t${ms_accept}\t${ms_confirm}\t${ms_settle}\t${ms_total}\t${WID}\t${note}" >>"$TSV"
|
||||||
|
metrics_report_coins "ladder-r${rung}-fail-${tag}" || true
|
||||||
break
|
break
|
||||||
fi
|
fi
|
||||||
done
|
done
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Phase B — payment ladder (same shape: 0 → low → … → max-1 → max)
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
PAY_OK_N=0
|
||||||
|
PAY_FAIL_N=0
|
||||||
|
if [ "${LADDER_PAY}" = "1" ] && [ -n "${PAY_LIST:-}" ] && [ "$FAIL_N_L" -eq 0 ]; then
|
||||||
|
section "ladder · phase B · pay"
|
||||||
|
metrics_report_coins "before-pay-ladder" || true
|
||||||
|
# Merchant secret (same as e2e)
|
||||||
|
MPW="${E2E_MERCHANT_TOKEN:-${MERCHANT_TOKEN:-}}"
|
||||||
|
if [ -z "$MPW" ]; then
|
||||||
|
MPW=$(read_secret "taler-merchant/merchant-${INST}-password.txt" 2>/dev/null || true)
|
||||||
|
fi
|
||||||
|
if [ -z "$MPW" ]; then
|
||||||
|
MPW=$(read_secret "taler-merchant/merchant-goa-demo-cp4zqk-password.txt" 2>/dev/null || true)
|
||||||
|
fi
|
||||||
|
if [ -z "$MPW" ]; then
|
||||||
|
warn pay "no merchant token — skip pay ladder (set E2E_MERCHANT_TOKEN or secrets)"
|
||||||
|
else
|
||||||
|
ok "merchant token" "instance ${INST}"
|
||||||
|
AUTH="Authorization: Bearer secret-token:${MPW}"
|
||||||
|
wallet_prepare "main"
|
||||||
|
# shellcheck disable=SC2086
|
||||||
|
set -- $PAY_LIST
|
||||||
|
PAY_N=$#
|
||||||
|
info "pay rungs" "$PAY_N · $*"
|
||||||
|
prung=0
|
||||||
|
for PAMT in "$@"; do
|
||||||
|
prung=$((prung + 1))
|
||||||
|
if ladder_over; then
|
||||||
|
warn pay "time budget exhausted" "after ${PAY_OK_N} ok pays"
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
section "ladder · pay rung $prung $PAMT · $(format_amount_alt "$PAMT")"
|
||||||
|
ptag=$(printf '%s' "$PAMT" | tr '.:' '__')
|
||||||
|
if [ "$prung" -eq 1 ]; then
|
||||||
|
prange="pin:0"
|
||||||
|
elif [ "$prung" -eq "$PAY_N" ]; then
|
||||||
|
prange="pin:max"
|
||||||
|
elif [ "$prung" -eq $((PAY_N - 1)) ] && [ "$PAY_N" -ge 3 ]; then
|
||||||
|
prange="pin:max-1"
|
||||||
|
else
|
||||||
|
prange="random"
|
||||||
|
fi
|
||||||
|
PNUM=$(python3 -c 'import sys; print(sys.argv[1].split(":",1)[-1])' "$PAMT")
|
||||||
|
IS_PZERO=0
|
||||||
|
python3 -c 'import sys; from decimal import Decimal; sys.exit(0 if Decimal(sys.argv[1])==0 else 1)' "$PNUM" 2>/dev/null && IS_PZERO=1
|
||||||
|
IS_PMAX=0
|
||||||
|
[ "$PNUM" = "${LADDER_MAX_AMOUNT}" ] && IS_PMAX=1
|
||||||
|
t_pay=$(now_ms)
|
||||||
|
ms_order=0 ms_handle=0 ms_psettle=0
|
||||||
|
pnote=""
|
||||||
|
pstatus="FAIL"
|
||||||
|
OID="-"
|
||||||
|
|
||||||
|
metrics_report_coins "before-pay-${ptag}" || true
|
||||||
|
bal=$(wallet_avail)
|
||||||
|
|
||||||
|
if [ "$IS_PZERO" = "1" ]; then
|
||||||
|
pstatus="ZERO_SKIP"
|
||||||
|
pnote="zero pay probe skipped"
|
||||||
|
warn pay "pay $PAMT skipped" "problem: zero amount order not useful. Ladder continues."
|
||||||
|
ms_total=$(elapsed_ms "$t_pay")
|
||||||
|
echo -e "${prung}\t${prange}\t${PAMT}\t${pstatus}\t${ms_order}\t${ms_handle}\t${ms_psettle}\t${ms_total}\t${OID}\t${pnote}" >>"$PAY_TSV"
|
||||||
|
PAY_OK_N=$((PAY_OK_N + 1))
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Soft: absolute max pay is a ceiling probe (unlikely affordable / merchant may reject)
|
||||||
|
if [ "$IS_PMAX" = "1" ]; then
|
||||||
|
# try once; on fail CEILING_REJECT
|
||||||
|
:
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Insufficient balance → soft skip for max pin only; hard fail for mid/max-1
|
||||||
|
if ! python3 -c 'import sys; from decimal import Decimal; sys.exit(0 if Decimal(sys.argv[1])>=Decimal(sys.argv[2]) else 1)' "$bal" "$PNUM" 2>/dev/null; then
|
||||||
|
pnote="insufficient balance avail=${CUR}:${bal} need=${PAMT}"
|
||||||
|
ms_total=$(elapsed_ms "$t_pay")
|
||||||
|
if [ "$IS_PMAX" = "1" ]; then
|
||||||
|
pstatus="CEILING_SKIP"
|
||||||
|
warn pay "pay $PAMT skipped (ceiling)" "$pnote"
|
||||||
|
echo -e "${prung}\t${prange}\t${PAMT}\t${pstatus}\t0\t0\t0\t${ms_total}\t-\t${pnote}" >>"$PAY_TSV"
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
err pay "pay $PAMT" "$pnote"
|
||||||
|
pstatus="FAIL_BALANCE"
|
||||||
|
PAY_FAIL_N=$((PAY_FAIL_N + 1))
|
||||||
|
FAIL_N_L=$((FAIL_N_L + 1))
|
||||||
|
STOP_REASON="$pnote"
|
||||||
|
STOP_AMOUNT="$PAMT"
|
||||||
|
echo -e "${prung}\t${prange}\t${PAMT}\t${pstatus}\t0\t0\t0\t${ms_total}\t-\t${pnote}" >>"$PAY_TSV"
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
|
||||||
|
t0=$(now_ms)
|
||||||
|
SUM_JSON=$(python3 -c 'import json,sys; print(json.dumps(sys.argv[1]))' "ladder pay ${PAMT}" 2>/dev/null || echo '"ladder pay"')
|
||||||
|
curl -skS -m 20 -o "$SCRATCH/ord-$ptag.json" -X POST \
|
||||||
|
-H "$AUTH" -H 'Content-Type: application/json' \
|
||||||
|
-d "{\"order\":{\"summary\":${SUM_JSON},\"amount\":\"${PAMT}\",\"fulfillment_message\":\"ok\"},\"create_token\":true}" \
|
||||||
|
"${MER}/instances/${INST}/private/orders" 2>"$SCRATCH/ord-$ptag.err" || true
|
||||||
|
ms_order=$(elapsed_ms "$t0")
|
||||||
|
OID=$(python3 -c 'import json,re,sys;t=open(sys.argv[1]).read()
|
||||||
|
try: print(json.loads(t).get("order_id") or "")
|
||||||
|
except Exception:
|
||||||
|
m=re.search(r"\"order_id\"\s*:\s*\"([^\"]+)\"",t); print(m.group(1) if m else "")
|
||||||
|
' "$SCRATCH/ord-$ptag.json" 2>/dev/null || true)
|
||||||
|
OTOK=$(python3 -c 'import json,re,sys;t=open(sys.argv[1]).read()
|
||||||
|
try: print(json.loads(t).get("token") or "")
|
||||||
|
except Exception:
|
||||||
|
m=re.search(r"\"token\"\s*:\s*\"([^\"]+)\"",t); print(m.group(1) if m else "")
|
||||||
|
' "$SCRATCH/ord-$ptag.json" 2>/dev/null || true)
|
||||||
|
if [ -z "$OID" ]; then
|
||||||
|
pnote="order create failed $(head -c 80 "$SCRATCH/ord-$ptag.json" 2>/dev/null | tr '\n\"' ' ')"
|
||||||
|
ms_total=$(elapsed_ms "$t_pay")
|
||||||
|
if [ "$IS_PMAX" = "1" ]; then
|
||||||
|
pstatus="CEILING_REJECT"
|
||||||
|
warn pay "pay $PAMT rejected (ceiling)" "$pnote"
|
||||||
|
echo -e "${prung}\t${prange}\t${PAMT}\t${pstatus}\t${ms_order}\t0\t0\t${ms_total}\t-\t${pnote}" >>"$PAY_TSV"
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
err pay "order $PAMT" "$pnote"
|
||||||
|
pstatus="FAIL_ORDER"
|
||||||
|
PAY_FAIL_N=$((PAY_FAIL_N + 1))
|
||||||
|
FAIL_N_L=$((FAIL_N_L + 1))
|
||||||
|
STOP_REASON="$pnote"
|
||||||
|
STOP_AMOUNT="$PAMT"
|
||||||
|
echo -e "${prung}\t${prange}\t${PAMT}\t${pstatus}\t${ms_order}\t0\t0\t${ms_total}\t-\t${pnote}" >>"$PAY_TSV"
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
ok "order $OID ($PAMT) ${ms_order}ms"
|
||||||
|
|
||||||
|
curl -skS -m 12 -o "$SCRATCH/ord-det-$ptag.json" -H "$AUTH" \
|
||||||
|
"${MER}/instances/${INST}/private/orders/${OID}" 2>/dev/null || true
|
||||||
|
PAYURI=$(python3 -c 'import json,sys
|
||||||
|
try: print(json.load(open(sys.argv[1])).get("taler_pay_uri") or "")
|
||||||
|
except Exception: print("")
|
||||||
|
' "$SCRATCH/ord-det-$ptag.json" 2>/dev/null || true)
|
||||||
|
if [ -z "$PAYURI" ] && [ -n "$OTOK" ]; then
|
||||||
|
MH=$(python3 -c 'from urllib.parse import urlparse; print(urlparse("'"$MER"'").hostname or "taler.hacktivism.ch")' 2>/dev/null || echo "taler.hacktivism.ch")
|
||||||
|
PAYURI="taler://pay/${MH}/instances/${INST}/${OID}/?c=${OTOK}"
|
||||||
|
fi
|
||||||
|
PAYURI=$(printf '%s' "$PAYURI" | sed 's/:443\//\//g; s/:443?/?/g')
|
||||||
|
if [ -z "$PAYURI" ]; then
|
||||||
|
pnote="no pay URI for $OID"
|
||||||
|
ms_total=$(elapsed_ms "$t_pay")
|
||||||
|
err pay "uri $PAMT" "$pnote"
|
||||||
|
pstatus="FAIL_URI"
|
||||||
|
PAY_FAIL_N=$((PAY_FAIL_N + 1))
|
||||||
|
FAIL_N_L=$((FAIL_N_L + 1))
|
||||||
|
STOP_REASON="$pnote"
|
||||||
|
STOP_AMOUNT="$PAMT"
|
||||||
|
echo -e "${prung}\t${prange}\t${PAMT}\t${pstatus}\t${ms_order}\t0\t0\t${ms_total}\t${OID}\t${pnote}" >>"$PAY_TSV"
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
|
||||||
|
t0=$(now_ms)
|
||||||
|
if ! wcli handle-uri --yes "$PAYURI" >"$SCRATCH/pay-$ptag.out" 2>&1; then
|
||||||
|
ms_handle=$(elapsed_ms "$t0")
|
||||||
|
pnote="handle-uri failed $(tail -c 100 "$SCRATCH/pay-$ptag.out" | tr '\n\"' ' ')"
|
||||||
|
ms_total=$(elapsed_ms "$t_pay")
|
||||||
|
if [ "$IS_PMAX" = "1" ]; then
|
||||||
|
pstatus="CEILING_REJECT"
|
||||||
|
warn pay "pay $PAMT handle failed (ceiling)" "$pnote"
|
||||||
|
echo -e "${prung}\t${prange}\t${PAMT}\t${pstatus}\t${ms_order}\t${ms_handle}\t0\t${ms_total}\t${OID}\t${pnote}" >>"$PAY_TSV"
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
err pay "handle $PAMT" "$pnote"
|
||||||
|
pstatus="FAIL_HANDLE"
|
||||||
|
PAY_FAIL_N=$((PAY_FAIL_N + 1))
|
||||||
|
FAIL_N_L=$((FAIL_N_L + 1))
|
||||||
|
STOP_REASON="$pnote"
|
||||||
|
STOP_AMOUNT="$PAMT"
|
||||||
|
echo -e "${prung}\t${prange}\t${PAMT}\t${pstatus}\t${ms_order}\t${ms_handle}\t0\t${ms_total}\t${OID}\t${pnote}" >>"$PAY_TSV"
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
ms_handle=$(elapsed_ms "$t0")
|
||||||
|
ok "handle-uri $PAMT ${ms_handle}ms"
|
||||||
|
|
||||||
|
# Short settle polls (bounded — no infinite hang)
|
||||||
|
t0=$(now_ms)
|
||||||
|
settled=0
|
||||||
|
r=0
|
||||||
|
while [ "$r" -lt "${LADDER_PAY_SETTLE_ROUNDS}" ]; do
|
||||||
|
r=$((r + 1))
|
||||||
|
if command -v perl >/dev/null 2>&1; then
|
||||||
|
perl -e 'alarm shift; exec @ARGV' 10 \
|
||||||
|
"$CLI_JS" --wallet-db="$WDB" --no-throttle run-until-done \
|
||||||
|
>"$SCRATCH/pay-run-$ptag.out" 2>&1 || true
|
||||||
|
else
|
||||||
|
wcli run-until-done >"$SCRATCH/pay-run-$ptag.out" 2>&1 || true
|
||||||
|
fi
|
||||||
|
wcli transactions >"$SCRATCH/tx-$ptag.out" 2>&1 || true
|
||||||
|
curl -skS -m 8 -o "$SCRATCH/ord-paid-$ptag.json" -H "$AUTH" \
|
||||||
|
"${MER}/instances/${INST}/private/orders/${OID}" 2>/dev/null || true
|
||||||
|
if grep -qiE 'payment|paid|Payment' "$SCRATCH/tx-$ptag.out" 2>/dev/null \
|
||||||
|
|| python3 -c 'import json,sys
|
||||||
|
d=json.load(open(sys.argv[1]))
|
||||||
|
sys.exit(0 if d.get("paid") is True or str(d.get("order_status","")).lower()=="paid" else 1)
|
||||||
|
' "$SCRATCH/ord-paid-$ptag.json" 2>/dev/null; then
|
||||||
|
settled=1
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
ms_psettle=$(elapsed_ms "$t0")
|
||||||
|
ms_total=$(elapsed_ms "$t_pay")
|
||||||
|
after=$(wallet_avail)
|
||||||
|
if [ "$settled" = "1" ]; then
|
||||||
|
pstatus="OK"
|
||||||
|
pnote="avail=${CUR}:${after}"
|
||||||
|
ok "pay settled $PAMT → bal ${CUR}:${after} (total ${ms_total}ms)"
|
||||||
|
PAY_OK_N=$((PAY_OK_N + 1))
|
||||||
|
echo -e "${prung}\t${prange}\t${PAMT}\t${pstatus}\t${ms_order}\t${ms_handle}\t${ms_psettle}\t${ms_total}\t${OID}\t${pnote}" >>"$PAY_TSV"
|
||||||
|
metrics_report_coins "after-pay-${ptag}" || true
|
||||||
|
else
|
||||||
|
pnote="not settled order=$OID avail=${CUR}:${after}"
|
||||||
|
if [ "$IS_PMAX" = "1" ]; then
|
||||||
|
pstatus="CEILING_REJECT"
|
||||||
|
warn pay "pay $PAMT not settled (ceiling)" "$pnote"
|
||||||
|
echo -e "${prung}\t${prange}\t${PAMT}\t${pstatus}\t${ms_order}\t${ms_handle}\t${ms_psettle}\t${ms_total}\t${OID}\t${pnote}" >>"$PAY_TSV"
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
err pay "settle $PAMT" "$pnote"
|
||||||
|
pstatus="FAIL_SETTLE"
|
||||||
|
PAY_FAIL_N=$((PAY_FAIL_N + 1))
|
||||||
|
FAIL_N_L=$((FAIL_N_L + 1))
|
||||||
|
STOP_REASON="$pnote"
|
||||||
|
STOP_AMOUNT="$PAMT"
|
||||||
|
echo -e "${prung}\t${prange}\t${PAMT}\t${pstatus}\t${ms_order}\t${ms_handle}\t${ms_psettle}\t${ms_total}\t${OID}\t${pnote}" >>"$PAY_TSV"
|
||||||
|
metrics_report_coins "after-pay-fail-${ptag}" || true
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
metrics_report_coins "after-pay-ladder" || true
|
||||||
|
info "pay summary" "ok=$PAY_OK_N fail=$PAY_FAIL_N tsv=$PAY_TSV"
|
||||||
|
fi
|
||||||
|
elif [ "${LADDER_PAY}" = "1" ] && [ "$FAIL_N_L" -gt 0 ]; then
|
||||||
|
warn pay "skipped pay ladder" "withdraw phase already failed"
|
||||||
|
fi
|
||||||
|
|
||||||
ms_phase=$(python3 -c 'import sys,time; print(int((time.time()-float(sys.argv[1]))*1000))' "$SECTION_T0")
|
ms_phase=$(python3 -c 'import sys,time; print(int((time.time()-float(sys.argv[1]))*1000))' "$SECTION_T0")
|
||||||
|
|
||||||
# --- report ---
|
# --- report ---
|
||||||
|
|
@ -563,22 +930,39 @@ section "ladder · report"
|
||||||
info "auto-account" "$ACCT_USER"
|
info "auto-account" "$ACCT_USER"
|
||||||
info "ok_rungs" "$OK_N"
|
info "ok_rungs" "$OK_N"
|
||||||
info "fail_rungs" "$FAIL_N_L"
|
info "fail_rungs" "$FAIL_N_L"
|
||||||
|
info "pay_ok" "$PAY_OK_N"
|
||||||
|
info "pay_fail" "$PAY_FAIL_N"
|
||||||
info "phase_ms" "$ms_phase"
|
info "phase_ms" "$ms_phase"
|
||||||
info "tsv" "$TSV"
|
info "tsv" "$TSV"
|
||||||
|
info "pay_tsv" "$PAY_TSV"
|
||||||
|
|
||||||
# speed summary via python
|
# speed summary via python — free-text stop reason via env (JSON " in bank errors
|
||||||
python3 - "$TSV" "$JSON" "$OK_N" "$FAIL_N_L" "${STOP_AMOUNT:-}" "${STOP_REASON:-}" "$ms_phase" "$ACCT_USER" "$CUR" <<'PY'
|
# used to break shell argv quoting → "syntax error near unexpected token '('")
|
||||||
import csv, json, sys, statistics
|
export LADDER_REPORT_STOP_AMOUNT="${STOP_AMOUNT:-}"
|
||||||
tsv, jpath, ok_n, fail_n, stop_amt, stop_reason, phase_ms, acct, cur = sys.argv[1:10]
|
export LADDER_REPORT_STOP_REASON="${STOP_REASON:-}"
|
||||||
rows = []
|
python3 - "$TSV" "$PAY_TSV" "$JSON" "$OK_N" "$FAIL_N_L" "$PAY_OK_N" "$PAY_FAIL_N" "$ms_phase" "$ACCT_USER" "$CUR" <<'PY'
|
||||||
with open(tsv, newline="") as f:
|
import csv, json, os, sys, statistics
|
||||||
r = csv.DictReader(f, delimiter="\t")
|
tsv, pay_tsv, jpath = sys.argv[1:4]
|
||||||
for row in r:
|
ok_n, fail_n, pay_ok, pay_fail, phase_ms, acct, cur = sys.argv[4:11]
|
||||||
rows.append(row)
|
stop_amt = os.environ.get("LADDER_REPORT_STOP_AMOUNT") or None
|
||||||
|
stop_reason = os.environ.get("LADDER_REPORT_STOP_REASON") or None
|
||||||
|
|
||||||
def nums(key):
|
def load_rows(path):
|
||||||
|
rows = []
|
||||||
|
try:
|
||||||
|
with open(path, newline="") as f:
|
||||||
|
for row in csv.DictReader(f, delimiter="\t"):
|
||||||
|
rows.append(row)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
return rows
|
||||||
|
|
||||||
|
rows = load_rows(tsv)
|
||||||
|
prows = load_rows(pay_tsv)
|
||||||
|
|
||||||
|
def nums(rs, key):
|
||||||
out = []
|
out = []
|
||||||
for row in rows:
|
for row in rs:
|
||||||
try:
|
try:
|
||||||
out.append(int(row[key]))
|
out.append(int(row[key]))
|
||||||
except Exception:
|
except Exception:
|
||||||
|
|
@ -601,41 +985,78 @@ report = {
|
||||||
"auto_account": acct,
|
"auto_account": acct,
|
||||||
"ok_rungs": int(ok_n),
|
"ok_rungs": int(ok_n),
|
||||||
"fail_rungs": int(fail_n),
|
"fail_rungs": int(fail_n),
|
||||||
|
"pay_ok": int(pay_ok),
|
||||||
|
"pay_fail": int(pay_fail),
|
||||||
"stopped_at_amount": stop_amt or None,
|
"stopped_at_amount": stop_amt or None,
|
||||||
"stop_reason": stop_reason or None,
|
"stop_reason": stop_reason or None,
|
||||||
"phase_ms": int(phase_ms),
|
"phase_ms": int(phase_ms),
|
||||||
"timing": {
|
"timing": {
|
||||||
"mint": stats(nums("ms_mint")),
|
"mint": stats(nums(rows, "ms_mint")),
|
||||||
"accept": stats(nums("ms_accept")),
|
"accept": stats(nums(rows, "ms_accept")),
|
||||||
"confirm": stats(nums("ms_confirm")),
|
"confirm": stats(nums(rows, "ms_confirm")),
|
||||||
"settle": stats(nums("ms_settle")),
|
"settle": stats(nums(rows, "ms_settle")),
|
||||||
"rung_total": stats(nums("ms_total")),
|
"rung_total": stats(nums(rows, "ms_total")),
|
||||||
|
"pay_order": stats(nums(prows, "ms_order")),
|
||||||
|
"pay_handle": stats(nums(prows, "ms_handle")),
|
||||||
|
"pay_settle": stats(nums(prows, "ms_settle")),
|
||||||
|
"pay_total": stats(nums(prows, "ms_total")),
|
||||||
},
|
},
|
||||||
"rungs": rows,
|
"rungs": rows,
|
||||||
|
"pays": prows,
|
||||||
}
|
}
|
||||||
json.dump(report, open(jpath, "w"), indent=2)
|
json.dump(report, open(jpath, "w"), indent=2)
|
||||||
print("JSON", jpath)
|
print("JSON", jpath)
|
||||||
print("--- speed (ms) ---")
|
print("--- withdraw speed (ms) ---")
|
||||||
for k, v in report["timing"].items():
|
for k in ("mint", "accept", "confirm", "settle", "rung_total"):
|
||||||
|
v = report["timing"][k]
|
||||||
if v.get("n"):
|
if v.get("n"):
|
||||||
print(f" {k:12} n={v['n']} min={v['min_ms']} p50={v['p50_ms']} avg={v['avg_ms']} max={v['max_ms']}")
|
print(" %s n=%s min=%s p50=%s avg=%s max=%s" % (k.ljust(12), v["n"], v["min_ms"], v["p50_ms"], v["avg_ms"], v["max_ms"]))
|
||||||
print("--- rungs ---")
|
print("--- pay speed (ms) ---")
|
||||||
|
for k in ("pay_order", "pay_handle", "pay_settle", "pay_total"):
|
||||||
|
v = report["timing"][k]
|
||||||
|
if v.get("n"):
|
||||||
|
print(" %s n=%s min=%s p50=%s avg=%s max=%s" % (k.ljust(12), v["n"], v["min_ms"], v["p50_ms"], v["avg_ms"], v["max_ms"]))
|
||||||
|
print("--- withdraw rungs ---")
|
||||||
for row in rows:
|
for row in rows:
|
||||||
print(f" {row['rung']:>2} {row['amount']:16} {row['status']:12} total={row['ms_total']}ms mint={row['ms_mint']} accept={row['ms_accept']} conf={row['ms_confirm']} set={row['ms_settle']}")
|
print(" %2s %-16s %-12s total=%sms" % (row.get("rung"), row.get("amount"), row.get("status"), row.get("ms_total")))
|
||||||
|
print("--- pay rungs ---")
|
||||||
|
for row in prows:
|
||||||
|
print(" %2s %-16s %-12s total=%sms oid=%s" % (row.get("rung"), row.get("amount"), row.get("status"), row.get("ms_total"), row.get("oid")))
|
||||||
if stop_amt:
|
if stop_amt:
|
||||||
print(f"STOPPED at {stop_amt}: {stop_reason}")
|
print("STOPPED at %s: %s" % (stop_amt, stop_reason))
|
||||||
else:
|
else:
|
||||||
print("Completed without hard failure (or budget stop without fail).")
|
print("Completed without hard failure (or budget stop without fail).")
|
||||||
PY
|
PY
|
||||||
|
|
||||||
wcli balance 2>&1 | tee "$REPORT_DIR/balance-final.out" | tail -20 || true
|
wcli balance 2>&1 | tee "$REPORT_DIR/balance-final.out" | tail -20 || true
|
||||||
|
|
||||||
|
section "ladder · load snapshot (after withdraws)"
|
||||||
|
metrics_report_load "$LOAD_AFTER" "ladder-end" || true
|
||||||
|
if [ -f "$LOAD_BEFORE" ] && [ -f "$LOAD_AFTER" ]; then
|
||||||
|
section "metrics · ladder load delta"
|
||||||
|
metrics_print_load_delta "$LOAD_BEFORE" "$LOAD_AFTER" || true
|
||||||
|
fi
|
||||||
|
# Speed timings → metrics overall (min/p50/avg/max per phase)
|
||||||
|
if [ -f "$JSON" ]; then
|
||||||
|
python3 - "$JSON" "${METRICS_DIR}/perf-summary.json" <<'PY' 2>/dev/null || true
|
||||||
|
import json, sys
|
||||||
|
rep = json.load(open(sys.argv[1]))
|
||||||
|
out = {}
|
||||||
|
for k, v in (rep.get("timing") or {}).items():
|
||||||
|
if isinstance(v, dict) and v.get("n"):
|
||||||
|
out[k] = v
|
||||||
|
json.dump(out, open(sys.argv[2], "w"), indent=2)
|
||||||
|
PY
|
||||||
|
fi
|
||||||
|
metrics_print_overall "ladder overall" || true
|
||||||
|
|
||||||
# Keep scratch if LADDER_REPORT_DIR set; else copy key files to /tmp
|
# Keep scratch if LADDER_REPORT_DIR set; else copy key files to /tmp
|
||||||
if [ -z "${LADDER_REPORT_DIR:-}" ]; then
|
if [ -z "${LADDER_REPORT_DIR:-}" ]; then
|
||||||
KEEP="/tmp/goa-ladder-report-$(date +%Y%m%d-%H%M%S)"
|
KEEP="/tmp/goa-ladder-report-$(date +%Y%m%d-%H%M%S)"
|
||||||
mkdir -p "$KEEP"
|
mkdir -p "$KEEP"
|
||||||
cp -a "$TSV" "$JSON" "$SCRATCH/auto-account.json" "$SCRATCH/ladder-plan.txt" \
|
cp -a "$TSV" "$PAY_TSV" "$JSON" "$SCRATCH/auto-account.json" "$SCRATCH/ladder-plan.txt" \
|
||||||
"$REPORT_DIR/balance-final.out" "$KEEP/" 2>/dev/null || true
|
"$SCRATCH/ladder-wd-plan.txt" "$SCRATCH/ladder-pay-plan.txt" \
|
||||||
|
"$REPORT_DIR/balance-final.out" "$LOAD_BEFORE" "$LOAD_AFTER" "$KEEP/" 2>/dev/null || true
|
||||||
info "report_dir" "$KEEP"
|
info "report_dir" "$KEEP"
|
||||||
echo "$KEEP" >"$SCRATCH/KEEP_PATH"
|
echo "$KEEP" >"$SCRATCH/KEEP_PATH"
|
||||||
fi
|
fi
|
||||||
|
|
@ -645,8 +1066,8 @@ if [ "$FAIL_N_L" -gt 0 ]; then
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
if [ "$OK_N" -eq 0 ]; then
|
if [ "$OK_N" -eq 0 ]; then
|
||||||
blocker "ladder" "no successful rungs"
|
blocker "ladder" "no successful withdraw rungs"
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
ok "ladder finished ok_rungs=$OK_N phase=${ms_phase}ms"
|
ok "ladder finished withdraw_ok=$OK_N pay_ok=$PAY_OK_N phase=${ms_phase}ms"
|
||||||
exit 0
|
exit 0
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue