From e798a4f33a5934c424ffd52a3b9016494088fcdb Mon Sep 17 00:00:00 2001 From: "2569718930@qq.com" <2569718930@qq.com> Date: Fri, 13 Mar 2026 13:01:57 +0800 Subject: [PATCH] feat: Introduce account center for user management and crypto payments; add Telegram push utility. --- frontend/components/account/AccountCenter.tsx | 104 +++++++++++++++++- src/utils/telegram_push.py | 93 +++++++++++++++- 2 files changed, 192 insertions(+), 5 deletions(-) diff --git a/frontend/components/account/AccountCenter.tsx b/frontend/components/account/AccountCenter.tsx index 9cbec801..b06e513d 100644 --- a/frontend/components/account/AccountCenter.tsx +++ b/frontend/components/account/AccountCenter.tsx @@ -217,6 +217,84 @@ function buildApproveCalldata(spender: string, amount: bigint) { return `0x095ea7b3${toPaddedAddress(spender)}${toPaddedHex(amount)}`; } +type NormalizedPaymentError = { + message: string; + pending: boolean; + userRejected: boolean; +}; + +function normalizePaymentError(error: unknown): NormalizedPaymentError { + const source = error as any; + const code = Number(source?.code ?? source?.error?.code ?? NaN); + const messageCandidates = [ + source?.shortMessage, + source?.message, + source?.reason, + source?.data?.message, + source?.error?.message, + error instanceof Error ? error.message : "", + typeof error === "string" ? error : "", + ]; + const rawMessage = messageCandidates + .find((item) => typeof item === "string" && item.trim()) + ?.trim(); + const lower = String(rawMessage || "").toLowerCase(); + + if (lower.includes("confirm pending")) { + return { + message: "链上交易已提交,正在确认中,请稍后刷新查看状态。", + pending: true, + userRejected: false, + }; + } + + const userRejected = + code === 4001 || + /user rejected|user denied|rejected request|cancelled|canceled|拒绝|取消/.test( + lower, + ); + if (userRejected) { + return { + message: "你已取消钱包操作。", + pending: false, + userRejected: true, + }; + } + + const insufficientGas = + (code === -32000 && /insufficient funds|gas/.test(lower)) || + /not enough pol|network fee|网络费|手续费/.test(lower); + if (insufficientGas) { + return { + message: "钱包 POL 不足,无法支付链上手续费,请先充值少量 POL 后重试。", + pending: false, + userRejected: false, + }; + } + + if (rawMessage) { + return { + message: rawMessage, + pending: false, + userRejected: false, + }; + } + + try { + return { + message: JSON.stringify(error), + pending: false, + userRejected: false, + }; + } catch { + return { + message: "发生未知错误,请稍后重试。", + pending: false, + userRejected: false, + }; + } +} + // --- Main Component --- export function AccountCenter() { @@ -618,7 +696,7 @@ export function AccountCenter() { try { accessToken = await getValidAccessToken(); } catch (tokenErr) { - setPaymentError(String(tokenErr)); + setPaymentError(normalizePaymentError(tokenErr).message); setPaymentBusy(false); return; } @@ -677,7 +755,8 @@ export function AccountCenter() { setPaymentInfo(`${walletLabel} 绑定成功: ${shortAddress(address)}`); await loadPaymentSnapshot(); } catch (error) { - setPaymentError(String(error)); + setPaymentInfo(""); + setPaymentError(normalizePaymentError(error).message); } finally { setPaymentBusy(false); } @@ -712,13 +791,14 @@ export function AccountCenter() { } setPaymentBusy(true); + let approvedInThisRun = false; try { // Ensure we have a valid token BEFORE switching chain / sending tx. let accessToken: string; try { accessToken = await getValidAccessToken(); } catch (tokenErr) { - setPaymentError(String(tokenErr)); + setPaymentError(normalizePaymentError(tokenErr).message); setPaymentBusy(false); return; } @@ -786,6 +866,7 @@ export function AccountCenter() { ], })) as string; await waitForReceipt(String(approveHash || "")); + approvedInThisRun = true; setPaymentInfo("USDC 授权成功,正在发起支付..."); } else { setPaymentInfo("授权额度充足,正在发起支付..."); @@ -839,7 +920,22 @@ export function AccountCenter() { await loadSnapshot(); await loadPaymentSnapshot(); } catch (error) { - setPaymentError(String(error)); + const normalized = normalizePaymentError(error); + if (normalized.pending) { + setPaymentError(normalized.message); + } else if (normalized.userRejected) { + setPaymentInfo( + approvedInThisRun + ? "USDC 授权已完成,本次支付已取消,可直接再次点击支付。" + : "", + ); + setPaymentError(normalized.message); + } else { + setPaymentInfo( + approvedInThisRun ? "USDC 授权已完成,但支付未完成,请重试。" : "", + ); + setPaymentError(normalized.message); + } } finally { setPaymentBusy(false); } diff --git a/src/utils/telegram_push.py b/src/utils/telegram_push.py index 255f6755..f581761a 100644 --- a/src/utils/telegram_push.py +++ b/src/utils/telegram_push.py @@ -1,10 +1,11 @@ import hashlib import json import os +import re import threading import time from datetime import datetime, timezone -from typing import Any, Dict, List, Optional +from typing import Any, Dict, List, Optional, Tuple from loguru import logger @@ -58,6 +59,79 @@ def _norm_prob(v: Any) -> Optional[float]: return max(0.0, min(1.0, n)) +def _safe_float(v: Any) -> Optional[float]: + if v is None: + return None + try: + return float(v) + except Exception: + return None + + +def _bucket_value(row: Dict[str, Any]) -> Optional[float]: + if not isinstance(row, dict): + return None + for key in ("value", "temp"): + n = _safe_float(row.get(key)) + if n is not None: + return n + label = str(row.get("label") or "").strip() + m = re.search(r"(-?\d+(?:\.\d+)?)", label) + if not m: + return None + return _safe_float(m.group(1)) + + +def _bucket_bounds(row: Dict[str, Any]) -> Optional[Tuple[Optional[float], Optional[float]]]: + value = _bucket_value(row) + if value is None: + return None + label = str(row.get("label") or "").strip().lower() + is_upper_tail = any(key in label for key in ("+", "or higher", "or above", "and above")) + is_lower_tail = any(key in label for key in ("<=", "or lower", "or below", "and below")) + if is_upper_tail and not is_lower_tail: + return value, None + if is_lower_tail and not is_upper_tail: + return None, value + return value, value + + +def _observed_settlement_floor(alert_payload: Dict[str, Any]) -> Optional[float]: + evidence = alert_payload.get("evidence") or {} + if not isinstance(evidence, dict): + evidence = {} + inputs = evidence.get("inputs") or {} + if not isinstance(inputs, dict): + inputs = {} + + suppression = alert_payload.get("suppression") or {} + if not isinstance(suppression, dict): + suppression = {} + + rules = alert_payload.get("rules") or {} + if not isinstance(rules, dict): + rules = {} + breakthrough = rules.get("forecast_breakthrough") or {} + if not isinstance(breakthrough, dict): + breakthrough = {} + + floor_candidates: List[float] = [] + for raw in ( + inputs.get("wu_settle"), + suppression.get("max_so_far"), + inputs.get("current_temp"), + suppression.get("current_temp"), + breakthrough.get("current_temp"), + ): + n = _safe_float(raw) + if n is not None: + floor_candidates.append(n) + + if not floor_candidates: + return None + return max(floor_candidates) + + def _optional_bool(value: Any) -> Optional[bool]: if value is None: return None @@ -254,6 +328,23 @@ def _market_price_cap_ok( yes_buy = _norm_prob(forecast_bucket.get("yes_buy")) bucket_label = str(forecast_bucket.get("label") or "").strip() or None + observed_floor = _observed_settlement_floor(alert_payload) + bucket_bounds = _bucket_bounds(forecast_bucket) if isinstance(forecast_bucket, dict) else None + if observed_floor is not None and bucket_bounds is not None: + _lower, upper = bucket_bounds + if upper is not None and observed_floor > upper + 1e-9: + logger.info( + "trade alert skipped: mapped bucket invalidated by observed high city={} bucket={} observed_floor={} upper_bound={} anchor_model={} anchor_settle={}".format( + alert_payload.get("city"), + bucket_label or "--", + round(observed_floor, 2), + round(upper, 2), + anchor_model, + settle_ref, + ) + ) + return False + if yes_buy is None or yes_buy <= 0.0: logger.info( "trade alert skipped: no actionable mapped bucket quote city={} bucket={} anchor_model={} anchor_settle={}".format(