feat: Introduce a runtime coordinator for bot loops and add new payment checkout and confirmation modules.

This commit is contained in:
2569718930@qq.com
2026-03-13 22:11:55 +08:00
parent 4bd97cc5a5
commit af2561d019
4 changed files with 210 additions and 1 deletions
+1 -1
View File
@@ -1092,7 +1092,7 @@ def _build_telegram_messages_mispricing(
if anchor_high_c is not None and anchor_settle is not None:
if anchor_model:
lines_zh.append(
f"基准:多模型最高温 {anchor_model} {anchor_high_c:.1f}C(结算参考 {anchor_settle}C"
f"基准:多模型最高温 {anchor_model} {anchor_high_c:.1f}°C (结算参考 {anchor_settle}°C "
)
else:
lines_zh.append(
+28
View File
@@ -87,6 +87,7 @@ class StartupCoordinator:
self._start_polygon_wallet_loop(),
self._start_polymarket_wallet_activity_loop(),
self._start_weekly_reward_loop(),
self._start_payment_confirm_loop(),
]
self._runtime_status = RuntimeStatus(
started_at=datetime.utcnow().strftime("%Y-%m-%d %H:%M:%S UTC"),
@@ -272,6 +273,33 @@ class StartupCoordinator:
),
)
def _start_payment_confirm_loop(self) -> LoopStatus:
enabled = _env_bool("POLYWEATHER_PAYMENT_CONFIRM_LOOP_ENABLED", True)
details = {
"interval_sec": max(
5, _env_int("POLYWEATHER_PAYMENT_CONFIRM_LOOP_INTERVAL_SEC", 20)
),
"batch_size": max(
1, min(200, _env_int("POLYWEATHER_PAYMENT_CONFIRM_LOOP_BATCH_SIZE", 20))
),
"payment_enabled": _env_bool("POLYWEATHER_PAYMENT_ENABLED", False),
"chain_id": _env_int("POLYWEATHER_PAYMENT_CHAIN_ID", 137),
"confirmations": max(1, _env_int("POLYWEATHER_PAYMENT_CONFIRMATIONS", 2)),
}
validation_error = None
if not bool(details["payment_enabled"]):
validation_error = "payment_service_disabled"
return self._start_with_validation(
key="payment_confirm",
label="支付自动补单",
configured_enabled=enabled,
details=details,
validation_error=validation_error,
starter=lambda: import_module(
"src.payments.confirm_loop"
).start_payment_confirm_loop(),
)
def render_runtime_status_html(status: RuntimeStatus) -> str:
lines = [
+138
View File
@@ -0,0 +1,138 @@
from __future__ import annotations
import os
import threading
import time
from typing import Any, Dict
from loguru import logger
from src.payments import PAYMENT_CHECKOUT, PaymentCheckoutError
def _env_bool(name: str, default: bool) -> bool:
raw = os.getenv(name)
if raw is None:
return default
return raw.strip().lower() in {"1", "true", "yes", "on"}
def _env_int(name: str, default: int, min_value: int = 0) -> int:
raw = os.getenv(name)
if raw is None:
return default
try:
value = int(raw)
except Exception:
return default
return max(min_value, value)
def _is_pending_confirm_error(exc: PaymentCheckoutError) -> bool:
detail = str(exc.detail or "").lower()
if exc.status_code in {404, 408}:
return True
if exc.status_code == 409 and (
"confirmations not enough" in detail or "tx indexed partially" in detail
):
return True
return False
def _short_hash(tx_hash: str) -> str:
text = str(tx_hash or "")
if len(text) < 18:
return text
return f"{text[:10]}...{text[-6:]}"
def _runner() -> None:
enabled = _env_bool("POLYWEATHER_PAYMENT_CONFIRM_LOOP_ENABLED", True)
if not enabled:
logger.info("payment confirm loop disabled")
return
if not PAYMENT_CHECKOUT.enabled:
logger.info("payment confirm loop skipped: payment service disabled")
return
interval_sec = _env_int("POLYWEATHER_PAYMENT_CONFIRM_LOOP_INTERVAL_SEC", 20, 5)
batch_size = _env_int("POLYWEATHER_PAYMENT_CONFIRM_LOOP_BATCH_SIZE", 20, 1)
batch_size = min(batch_size, 200)
logger.info(
"payment confirm loop started interval={}s batch={} chain_id={} confirmations={}",
interval_sec,
batch_size,
PAYMENT_CHECKOUT.chain_id,
PAYMENT_CHECKOUT.confirmations,
)
while True:
try:
intents = PAYMENT_CHECKOUT.list_pending_confirm_intents(limit=batch_size)
scanned = len(intents)
confirmed = 0
already_confirmed = 0
pending = 0
failed = 0
for row in intents:
intent_id = str(row.get("intent_id") or "").strip()
user_id = str(row.get("user_id") or "").strip()
tx_hash = str(row.get("tx_hash") or "").strip().lower()
if not intent_id or not user_id or not tx_hash:
continue
try:
result: Dict[str, Any] = PAYMENT_CHECKOUT.confirm_intent_tx(
user_id=user_id,
intent_id=intent_id,
tx_hash=tx_hash,
)
if bool(result.get("already_confirmed")):
already_confirmed += 1
else:
confirmed += 1
logger.info(
"payment auto-confirmed intent={} user={} tx={}",
intent_id,
user_id,
_short_hash(tx_hash),
)
except PaymentCheckoutError as exc:
if _is_pending_confirm_error(exc):
pending += 1
continue
failed += 1
logger.warning(
"payment auto-confirm failed intent={} user={} tx={} status={} detail={}",
intent_id,
user_id,
_short_hash(tx_hash),
exc.status_code,
exc.detail,
)
if scanned and (confirmed or already_confirmed or failed):
logger.info(
"payment confirm cycle scanned={} confirmed={} already={} pending={} failed={}",
scanned,
confirmed,
already_confirmed,
pending,
failed,
)
except Exception as exc:
logger.warning(f"payment confirm cycle failed: {exc}")
time.sleep(interval_sec)
def start_payment_confirm_loop():
thread = threading.Thread(
target=_runner,
daemon=True,
name="payment-confirm-loop",
)
thread.start()
return thread
+43
View File
@@ -1278,6 +1278,49 @@ class PaymentContractCheckoutService:
raise PaymentCheckoutError(404, "payment intent not found")
return self._serialize_intent(rows[0])
def list_pending_confirm_intents(self, limit: int = 20) -> List[Dict[str, Any]]:
"""
List submitted intents that already have tx_hash and need background confirm.
"""
self._ensure_enabled()
safe_limit = max(1, min(int(limit or 20), 200))
rows = self._rest(
"GET",
"payment_intents",
params={
"select": "id,user_id,tx_hash,status,updated_at,created_at,chain_id",
"status": "eq.submitted",
"tx_hash": "not.is.null",
"chain_id": f"eq.{self.chain_id}",
"order": "updated_at.asc",
"limit": str(safe_limit),
},
allowed_status=[200],
)
if not isinstance(rows, list):
return []
out: List[Dict[str, Any]] = []
for row in rows:
if not isinstance(row, dict):
continue
intent_id = str(row.get("id") or "").strip()
user_id = str(row.get("user_id") or "").strip()
tx_hash = str(row.get("tx_hash") or "").strip().lower()
if not intent_id or not user_id or not tx_hash:
continue
out.append(
{
"intent_id": intent_id,
"user_id": user_id,
"tx_hash": tx_hash,
"status": str(row.get("status") or ""),
"updated_at": row.get("updated_at"),
"created_at": row.get("created_at"),
}
)
return out
def submit_intent_tx(
self,
user_id: str,