feat: Introduce a runtime coordinator for bot loops and add new payment checkout and confirmation modules.
This commit is contained in:
@@ -1092,7 +1092,7 @@ def _build_telegram_messages_mispricing(
|
|||||||
if anchor_high_c is not None and anchor_settle is not None:
|
if anchor_high_c is not None and anchor_settle is not None:
|
||||||
if anchor_model:
|
if anchor_model:
|
||||||
lines_zh.append(
|
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:
|
else:
|
||||||
lines_zh.append(
|
lines_zh.append(
|
||||||
|
|||||||
@@ -87,6 +87,7 @@ class StartupCoordinator:
|
|||||||
self._start_polygon_wallet_loop(),
|
self._start_polygon_wallet_loop(),
|
||||||
self._start_polymarket_wallet_activity_loop(),
|
self._start_polymarket_wallet_activity_loop(),
|
||||||
self._start_weekly_reward_loop(),
|
self._start_weekly_reward_loop(),
|
||||||
|
self._start_payment_confirm_loop(),
|
||||||
]
|
]
|
||||||
self._runtime_status = RuntimeStatus(
|
self._runtime_status = RuntimeStatus(
|
||||||
started_at=datetime.utcnow().strftime("%Y-%m-%d %H:%M:%S UTC"),
|
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:
|
def render_runtime_status_html(status: RuntimeStatus) -> str:
|
||||||
lines = [
|
lines = [
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
@@ -1278,6 +1278,49 @@ class PaymentContractCheckoutService:
|
|||||||
raise PaymentCheckoutError(404, "payment intent not found")
|
raise PaymentCheckoutError(404, "payment intent not found")
|
||||||
return self._serialize_intent(rows[0])
|
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(
|
def submit_intent_tx(
|
||||||
self,
|
self,
|
||||||
user_id: str,
|
user_id: str,
|
||||||
|
|||||||
Reference in New Issue
Block a user