feat: Introduce account center for user management and crypto payments; add Telegram push utility.
This commit is contained in:
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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(
|
||||
|
||||
Reference in New Issue
Block a user