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)}`;
|
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 ---
|
// --- Main Component ---
|
||||||
|
|
||||||
export function AccountCenter() {
|
export function AccountCenter() {
|
||||||
@@ -618,7 +696,7 @@ export function AccountCenter() {
|
|||||||
try {
|
try {
|
||||||
accessToken = await getValidAccessToken();
|
accessToken = await getValidAccessToken();
|
||||||
} catch (tokenErr) {
|
} catch (tokenErr) {
|
||||||
setPaymentError(String(tokenErr));
|
setPaymentError(normalizePaymentError(tokenErr).message);
|
||||||
setPaymentBusy(false);
|
setPaymentBusy(false);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -677,7 +755,8 @@ export function AccountCenter() {
|
|||||||
setPaymentInfo(`${walletLabel} 绑定成功: ${shortAddress(address)}`);
|
setPaymentInfo(`${walletLabel} 绑定成功: ${shortAddress(address)}`);
|
||||||
await loadPaymentSnapshot();
|
await loadPaymentSnapshot();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
setPaymentError(String(error));
|
setPaymentInfo("");
|
||||||
|
setPaymentError(normalizePaymentError(error).message);
|
||||||
} finally {
|
} finally {
|
||||||
setPaymentBusy(false);
|
setPaymentBusy(false);
|
||||||
}
|
}
|
||||||
@@ -712,13 +791,14 @@ export function AccountCenter() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
setPaymentBusy(true);
|
setPaymentBusy(true);
|
||||||
|
let approvedInThisRun = false;
|
||||||
try {
|
try {
|
||||||
// Ensure we have a valid token BEFORE switching chain / sending tx.
|
// Ensure we have a valid token BEFORE switching chain / sending tx.
|
||||||
let accessToken: string;
|
let accessToken: string;
|
||||||
try {
|
try {
|
||||||
accessToken = await getValidAccessToken();
|
accessToken = await getValidAccessToken();
|
||||||
} catch (tokenErr) {
|
} catch (tokenErr) {
|
||||||
setPaymentError(String(tokenErr));
|
setPaymentError(normalizePaymentError(tokenErr).message);
|
||||||
setPaymentBusy(false);
|
setPaymentBusy(false);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -786,6 +866,7 @@ export function AccountCenter() {
|
|||||||
],
|
],
|
||||||
})) as string;
|
})) as string;
|
||||||
await waitForReceipt(String(approveHash || ""));
|
await waitForReceipt(String(approveHash || ""));
|
||||||
|
approvedInThisRun = true;
|
||||||
setPaymentInfo("USDC 授权成功,正在发起支付...");
|
setPaymentInfo("USDC 授权成功,正在发起支付...");
|
||||||
} else {
|
} else {
|
||||||
setPaymentInfo("授权额度充足,正在发起支付...");
|
setPaymentInfo("授权额度充足,正在发起支付...");
|
||||||
@@ -839,7 +920,22 @@ export function AccountCenter() {
|
|||||||
await loadSnapshot();
|
await loadSnapshot();
|
||||||
await loadPaymentSnapshot();
|
await loadPaymentSnapshot();
|
||||||
} catch (error) {
|
} 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 {
|
} finally {
|
||||||
setPaymentBusy(false);
|
setPaymentBusy(false);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,10 +1,11 @@
|
|||||||
import hashlib
|
import hashlib
|
||||||
import json
|
import json
|
||||||
import os
|
import os
|
||||||
|
import re
|
||||||
import threading
|
import threading
|
||||||
import time
|
import time
|
||||||
from datetime import datetime, timezone
|
from datetime import datetime, timezone
|
||||||
from typing import Any, Dict, List, Optional
|
from typing import Any, Dict, List, Optional, Tuple
|
||||||
|
|
||||||
from loguru import logger
|
from loguru import logger
|
||||||
|
|
||||||
@@ -58,6 +59,79 @@ def _norm_prob(v: Any) -> Optional[float]:
|
|||||||
return max(0.0, min(1.0, n))
|
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]:
|
def _optional_bool(value: Any) -> Optional[bool]:
|
||||||
if value is None:
|
if value is None:
|
||||||
return None
|
return None
|
||||||
@@ -254,6 +328,23 @@ def _market_price_cap_ok(
|
|||||||
yes_buy = _norm_prob(forecast_bucket.get("yes_buy"))
|
yes_buy = _norm_prob(forecast_bucket.get("yes_buy"))
|
||||||
bucket_label = str(forecast_bucket.get("label") or "").strip() or None
|
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:
|
if yes_buy is None or yes_buy <= 0.0:
|
||||||
logger.info(
|
logger.info(
|
||||||
"trade alert skipped: no actionable mapped bucket quote city={} bucket={} anchor_model={} anchor_settle={}".format(
|
"trade alert skipped: no actionable mapped bucket quote city={} bucket={} anchor_model={} anchor_settle={}".format(
|
||||||
|
|||||||
Reference in New Issue
Block a user