feat: Introduce account center for user management and crypto payments; add Telegram push utility.

This commit is contained in:
2569718930@qq.com
2026-03-13 13:01:57 +08:00
parent 50a2202930
commit e798a4f33a
2 changed files with 192 additions and 5 deletions
+100 -4
View File
@@ -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);
}
+92 -1
View File
@@ -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(