feat(v2): record fee_mismatch_sats per settlement, Phase 1 (#38 4/5)
Phase-1 observability per coord-log §2026-06-01T07:00Z (option A locked: always record, no enforce_fee_match gate): fee_mismatch_sats = bitspire_fee_sats - (platform_fee_sats + operator_fee_sats) Positive = bitspire over-reported; negative = under-reported; zero = exact match. Recorded unconditionally on every settlement; WARN- logged via loguru only when |delta| > tolerance, where tolerance = max(1, int(principal_sats * 0.001)) — 1-sat floor with 0.1% relative ceiling. bitspire.py:parse_settlement: - Computes the delta after split_principal_based returns. - WARN log line carries bitspire_fee_sats / expected / delta / tolerance / principal / both fractions / tx_type / machine-npub prefix for triage queries. - Always stamps fee_mismatch_sats onto CreateDcaSettlementData. - Comment explains the pre-Layer-3 expectation: large deltas are expected while the ATM hardcodes 7.77% cash-out (aiolabs/lamassu- next#57); the data here will quiet once Layer 3 ships. crud.py:create_settlement_idempotent: extends the INSERT to persist the new column. Tests: - tests/conftest.py: `loguru_capture` fixture — loguru routes to a pre-bound stderr sink that pytest's caplog (stdlib only) misses and capsys can't see; the fixture adds a list-sink for the test's duration. Reusable for future log-behavior tests. - tests/test_fee_mismatch_recording.py: 8 cases covering exact-match zero delta, bitspire over- and under-reporting, the pre-Layer-3 large-delta scenario, within-tolerance silence, over-tolerance warning, diagnostic-fields presence in the WARN line, and the 1-sat floor on tiny-principal settlements. 164/164 tests green. Phase 2 (reject on out-of-tolerance) lands as a follow-up once observability data justifies the tighter posture. Refs: aiolabs/satmachineadmin#38 (Layer 1), coord-log §2026-06-01T07:00Z (lnbits advisory + option A lock). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
parent
1babdfbf06
commit
d9e8a04b8b
4 changed files with 236 additions and 2 deletions
22
bitspire.py
22
bitspire.py
|
|
@ -17,6 +17,8 @@ from __future__ import annotations
|
|||
import json
|
||||
from typing import Any, Optional
|
||||
|
||||
from loguru import logger
|
||||
|
||||
from .calculations import split_principal_based
|
||||
from .models import CreateDcaSettlementData, Machine, SuperConfig
|
||||
|
||||
|
|
@ -275,6 +277,25 @@ def parse_settlement(
|
|||
platform_fee_sats, operator_fee_sats = split_principal_based(
|
||||
principal_sats, super_frac, operator_frac
|
||||
)
|
||||
# Phase-1 observability per aiolabs/satmachineadmin#38 + coord-log
|
||||
# §2026-06-01T07:00Z (option A locked): compare bitspire's reported
|
||||
# fee_sats against satmachineadmin's recompute, log on out-of-
|
||||
# tolerance drift, record the delta unconditionally for triage.
|
||||
# Phase 2 (settlement-reject) lands after observability data.
|
||||
fee_mismatch_sats = fee_sats - (platform_fee_sats + operator_fee_sats)
|
||||
tolerance = max(1, int(principal_sats * 0.001))
|
||||
if abs(fee_mismatch_sats) > tolerance:
|
||||
logger.warning(
|
||||
f"bitspire fee mismatch on payment {payment_hash[:12]}...: "
|
||||
f"bitspire_fee_sats={fee_sats} expected={platform_fee_sats + operator_fee_sats} "
|
||||
f"delta={fee_mismatch_sats} tolerance={tolerance} "
|
||||
f"principal={principal_sats} super_frac={super_frac:.4f} "
|
||||
f"operator_frac={operator_frac:.4f} tx_type={tx_type} "
|
||||
f"machine={machine.machine_npub[:12]}... — "
|
||||
"Phase 1 observability only, no behavior change. Pre-Layer-3 "
|
||||
"(lamassu-next#57) the ATM still hardcodes fee fractions, so "
|
||||
"large deltas here are expected until that ships."
|
||||
)
|
||||
exchange_rate = _coerce_float(extra.get("exchange_rate"))
|
||||
if exchange_rate is None or exchange_rate <= 0:
|
||||
# Without exchange rate we can't compute fiat. Use 1.0 as a stand-in
|
||||
|
|
@ -301,6 +322,7 @@ def parse_settlement(
|
|||
fee_sats=fee_sats,
|
||||
platform_fee_sats=platform_fee_sats,
|
||||
operator_fee_sats=operator_fee_sats,
|
||||
fee_mismatch_sats=fee_mismatch_sats,
|
||||
tx_type=tx_type,
|
||||
bills_json=_json_dumps(extra.get("bills")),
|
||||
cassettes_json=_json_dumps(extra.get("cassettes")),
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue