Checkpoint: v11: Bridge页面增强 - Gas费估算显示(含原生代币说明)、交易历史复制哈希+区块浏览器快捷按钮、交易成功弹窗(含到账时间、复制哈希、查看详情)
This commit is contained in:
parent
2eff084785
commit
4bdb118cb2
|
|
@ -5,7 +5,7 @@
|
||||||
import { useState, useEffect, useCallback, useMemo } from "react";
|
import { useState, useEffect, useCallback, useMemo } from "react";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import { trpc } from "@/lib/trpc";
|
import { trpc } from "@/lib/trpc";
|
||||||
import { ArrowDown, ArrowLeft, ExternalLink, Loader2, RefreshCw, Zap, History, ChevronDown, ChevronUp } from "lucide-react";
|
import { ArrowDown, ArrowLeft, ExternalLink, Loader2, RefreshCw, Zap, History, ChevronDown, ChevronUp, Copy, CheckCheck, X } from "lucide-react";
|
||||||
import { Link } from "wouter";
|
import { Link } from "wouter";
|
||||||
import { WalletSelector } from "@/components/WalletSelector";
|
import { WalletSelector } from "@/components/WalletSelector";
|
||||||
import { useWallet } from "@/hooks/useWallet";
|
import { useWallet } from "@/hooks/useWallet";
|
||||||
|
|
@ -68,6 +68,18 @@ const T = {
|
||||||
approve: "授权",
|
approve: "授权",
|
||||||
executeSwap: "执行跨链交换",
|
executeSwap: "执行跨链交换",
|
||||||
lang: "EN",
|
lang: "EN",
|
||||||
|
estGasNative: "Gas费(用{symbol}支付)",
|
||||||
|
estTime: "预计到账时间",
|
||||||
|
gasNote: "Gas 费用{symbol}({chain}原生代币)支付,请确保钱包中有足够的{symbol}",
|
||||||
|
copyHash: "复制哈希",
|
||||||
|
copied: "已复制!",
|
||||||
|
viewExplorer: "在区块浏览器中查看",
|
||||||
|
txSuccessModal: "交易成功",
|
||||||
|
txSuccessDesc: "您的跨链交易已提交,XIC 代币将在以下时间内到账:",
|
||||||
|
viewDetails: "查看交易详情",
|
||||||
|
close: "关闭",
|
||||||
|
estimatedArrival: "预计到账",
|
||||||
|
gasPayWith: "Gas 支付方式",
|
||||||
},
|
},
|
||||||
en: {
|
en: {
|
||||||
title: "Buy XIC from Any Chain",
|
title: "Buy XIC from Any Chain",
|
||||||
|
|
@ -121,6 +133,18 @@ const T = {
|
||||||
approve: "Approve",
|
approve: "Approve",
|
||||||
executeSwap: "Execute Cross-Chain Swap",
|
executeSwap: "Execute Cross-Chain Swap",
|
||||||
lang: "中文",
|
lang: "中文",
|
||||||
|
estGasNative: "Gas Fee (paid in {symbol})",
|
||||||
|
estTime: "Estimated Arrival",
|
||||||
|
gasNote: "Gas fee is paid in {symbol} ({chain} native token). Ensure you have enough {symbol} in your wallet.",
|
||||||
|
copyHash: "Copy Hash",
|
||||||
|
copied: "Copied!",
|
||||||
|
viewExplorer: "View in Explorer",
|
||||||
|
txSuccessModal: "Transaction Submitted!",
|
||||||
|
txSuccessDesc: "Your cross-chain transaction has been submitted. XIC tokens will arrive within:",
|
||||||
|
viewDetails: "View Transaction Details",
|
||||||
|
close: "Close",
|
||||||
|
estimatedArrival: "Est. Arrival",
|
||||||
|
gasPayWith: "Gas Payment",
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -173,8 +197,16 @@ interface RouteQuote {
|
||||||
type: string;
|
type: string;
|
||||||
tool: string;
|
tool: string;
|
||||||
toolDetails?: { name: string; logoURI?: string };
|
toolDetails?: { name: string; logoURI?: string };
|
||||||
|
estimate?: {
|
||||||
|
executionDuration?: number; // seconds
|
||||||
|
gasCosts?: Array<{ amountUSD?: string; amount?: string; token?: { symbol: string; decimals: number } }>;
|
||||||
|
};
|
||||||
}>;
|
}>;
|
||||||
gasCostUSD?: string;
|
gasCostUSD?: string;
|
||||||
|
estimate?: {
|
||||||
|
executionDuration?: number; // total seconds
|
||||||
|
gasCosts?: Array<{ amountUSD?: string; amount?: string; token?: { symbol: string; decimals: number } }>;
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// ─── Li.Fi API helpers ────────────────────────────────────────────────────────
|
// ─── Li.Fi API helpers ────────────────────────────────────────────────────────
|
||||||
|
|
@ -221,6 +253,38 @@ function getTxUrl(chainId: number, txHash: string): string {
|
||||||
return `${chain?.explorerUrl ?? "https://bscscan.com"}/tx/${txHash}`;
|
return `${chain?.explorerUrl ?? "https://bscscan.com"}/tx/${txHash}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ─── Format duration (seconds) to human-readable ─────────────────────────────
|
||||||
|
function formatDuration(seconds: number): string {
|
||||||
|
if (seconds < 60) return `~${seconds}s`;
|
||||||
|
const mins = Math.ceil(seconds / 60);
|
||||||
|
if (mins < 60) return `~${mins} min`;
|
||||||
|
const hrs = Math.ceil(mins / 60);
|
||||||
|
return `~${hrs} hr`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── Get total estimated duration from quote ─────────────────────────────────
|
||||||
|
function getEstimatedDuration(quote: RouteQuote): number {
|
||||||
|
if (quote.estimate?.executionDuration) return quote.estimate.executionDuration;
|
||||||
|
// Sum up step durations
|
||||||
|
return quote.steps.reduce((acc, step) => acc + (step.estimate?.executionDuration ?? 0), 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── Get gas cost info from quote ────────────────────────────────────────────
|
||||||
|
function getGasCostInfo(quote: RouteQuote): { usd: string; nativeSymbol: string; nativeAmount: string } | null {
|
||||||
|
// Try top-level gasCostUSD first
|
||||||
|
const usd = quote.gasCostUSD;
|
||||||
|
// Try to find native token info from steps
|
||||||
|
const firstGasCost = quote.estimate?.gasCosts?.[0] ?? quote.steps[0]?.estimate?.gasCosts?.[0];
|
||||||
|
if (!usd && !firstGasCost) return null;
|
||||||
|
return {
|
||||||
|
usd: usd ? `$${parseFloat(usd).toFixed(3)}` : "N/A",
|
||||||
|
nativeSymbol: firstGasCost?.token?.symbol ?? "",
|
||||||
|
nativeAmount: firstGasCost?.amount && firstGasCost?.token
|
||||||
|
? (parseFloat(firstGasCost.amount) / Math.pow(10, firstGasCost.token.decimals)).toFixed(6)
|
||||||
|
: "",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
// ─── Main Bridge Page ─────────────────────────────────────────────────────────
|
// ─── Main Bridge Page ─────────────────────────────────────────────────────────
|
||||||
export default function Bridge() {
|
export default function Bridge() {
|
||||||
// ── Language ──
|
// ── Language ──
|
||||||
|
|
@ -244,6 +308,8 @@ export default function Bridge() {
|
||||||
const [txHash, setTxHash] = useState<string | null>(null);
|
const [txHash, setTxHash] = useState<string | null>(null);
|
||||||
const [completedTxs, setCompletedTxs] = useState(0);
|
const [completedTxs, setCompletedTxs] = useState(0);
|
||||||
const [showHistory, setShowHistory] = useState(false);
|
const [showHistory, setShowHistory] = useState(false);
|
||||||
|
const [showSuccessModal, setShowSuccessModal] = useState(false);
|
||||||
|
const [copiedHash, setCopiedHash] = useState<string | null>(null);
|
||||||
|
|
||||||
// ── tRPC mutations/queries ──
|
// ── tRPC mutations/queries ──
|
||||||
const recordOrder = trpc.bridge.recordOrder.useMutation();
|
const recordOrder = trpc.bridge.recordOrder.useMutation();
|
||||||
|
|
@ -374,7 +440,7 @@ export default function Bridge() {
|
||||||
setTxHash(hash);
|
setTxHash(hash);
|
||||||
setExecStep("done");
|
setExecStep("done");
|
||||||
setCompletedTxs(c => c + 1);
|
setCompletedTxs(c => c + 1);
|
||||||
toast.success(t.txSuccess);
|
setShowSuccessModal(true);
|
||||||
|
|
||||||
// Record in DB
|
// Record in DB
|
||||||
const xicReceived = (parseFloat(quote.toAmount) / 1e18).toFixed(6);
|
const xicReceived = (parseFloat(quote.toAmount) / 1e18).toFixed(6);
|
||||||
|
|
@ -401,7 +467,7 @@ export default function Bridge() {
|
||||||
setTxHash(hash);
|
setTxHash(hash);
|
||||||
setExecStep("done");
|
setExecStep("done");
|
||||||
setCompletedTxs(c => c + 1);
|
setCompletedTxs(c => c + 1);
|
||||||
toast.success(t.txSuccess);
|
setShowSuccessModal(true);
|
||||||
|
|
||||||
// Record in DB
|
// Record in DB
|
||||||
const xicReceived = (parseFloat(quote.toAmount) / 1e18).toFixed(6);
|
const xicReceived = (parseFloat(quote.toAmount) / 1e18).toFixed(6);
|
||||||
|
|
@ -449,6 +515,25 @@ export default function Bridge() {
|
||||||
return `${t.buyXIC} ${xicAmount} XIC`;
|
return `${t.buyXIC} ${xicAmount} XIC`;
|
||||||
}, [execStep, t, xicAmount]);
|
}, [execStep, t, xicAmount]);
|
||||||
|
|
||||||
|
// ── Copy hash to clipboard ──
|
||||||
|
const copyHashToClipboard = useCallback(async (hash: string) => {
|
||||||
|
try {
|
||||||
|
await navigator.clipboard.writeText(hash);
|
||||||
|
setCopiedHash(hash);
|
||||||
|
setTimeout(() => setCopiedHash(null), 2000);
|
||||||
|
} catch {
|
||||||
|
// fallback
|
||||||
|
const el = document.createElement("textarea");
|
||||||
|
el.value = hash;
|
||||||
|
document.body.appendChild(el);
|
||||||
|
el.select();
|
||||||
|
document.execCommand("copy");
|
||||||
|
document.body.removeChild(el);
|
||||||
|
setCopiedHash(hash);
|
||||||
|
setTimeout(() => setCopiedHash(null), 2000);
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className="min-h-screen"
|
className="min-h-screen"
|
||||||
|
|
@ -651,34 +736,72 @@ export default function Bridge() {
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Route info */}
|
{/* Route info + Gas fee + Estimated time */}
|
||||||
{quote && !quoting && (
|
{quote && !quoting && (() => {
|
||||||
<div
|
const gasCostInfo = getGasCostInfo(quote);
|
||||||
className="rounded-xl p-3 space-y-1.5 text-xs"
|
const duration = getEstimatedDuration(quote);
|
||||||
style={{ background: "rgba(0,212,255,0.04)", border: "1px solid rgba(0,212,255,0.1)" }}
|
const nativeSymbol = fromChain.symbol; // BNB/ETH/MATIC/AVAX
|
||||||
>
|
return (
|
||||||
<div className="flex justify-between">
|
<div
|
||||||
<span className="text-white/40">{t.route}</span>
|
className="rounded-xl p-3 space-y-1.5 text-xs"
|
||||||
<span className="text-white/70">{fromChain.name} USDT → BSC XIC</span>
|
style={{ background: "rgba(0,212,255,0.04)", border: "1px solid rgba(0,212,255,0.1)" }}
|
||||||
</div>
|
>
|
||||||
<div className="flex justify-between">
|
|
||||||
<span className="text-white/40">{t.protocol}</span>
|
|
||||||
<span className="text-white/70">
|
|
||||||
{quote.steps.map((s) => s.toolDetails?.name ?? s.tool).join(" → ")}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
{quote.gasCostUSD && (
|
|
||||||
<div className="flex justify-between">
|
<div className="flex justify-between">
|
||||||
<span className="text-white/40">{t.estGas}</span>
|
<span className="text-white/40">{t.route}</span>
|
||||||
<span className="text-white/70">${quote.gasCostUSD}</span>
|
<span className="text-white/70">{fromChain.name} USDT → BSC XIC</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex justify-between">
|
||||||
|
<span className="text-white/40">{t.protocol}</span>
|
||||||
|
<span className="text-white/70">
|
||||||
|
{quote.steps.map((s) => s.toolDetails?.name ?? s.tool).join(" → ")}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Gas fee row with native token info */}
|
||||||
|
<div className="flex justify-between items-start">
|
||||||
|
<span className="text-white/40">{t.gasPayWith}</span>
|
||||||
|
<div className="text-right">
|
||||||
|
<span className="text-amber-400/80 font-semibold">{nativeSymbol}</span>
|
||||||
|
{gasCostInfo && (
|
||||||
|
<span className="text-white/50 ml-1">
|
||||||
|
({gasCostInfo.usd}
|
||||||
|
{gasCostInfo.nativeAmount && gasCostInfo.nativeSymbol
|
||||||
|
? ` ≈ ${gasCostInfo.nativeAmount} ${gasCostInfo.nativeSymbol}`
|
||||||
|
: ""}
|
||||||
|
)
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Gas note: explain which token is used */}
|
||||||
|
<div
|
||||||
|
className="rounded-lg px-2 py-1.5 text-xs"
|
||||||
|
style={{ background: "rgba(240,180,41,0.06)", border: "1px solid rgba(240,180,41,0.15)" }}
|
||||||
|
>
|
||||||
|
<span className="text-amber-400/60">⚠️ </span>
|
||||||
|
<span className="text-white/40">
|
||||||
|
{t.gasNote
|
||||||
|
.replace(/\{symbol\}/g, nativeSymbol)
|
||||||
|
.replace(/\{chain\}/g, fromChain.name)}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Estimated arrival time */}
|
||||||
|
{duration > 0 && (
|
||||||
|
<div className="flex justify-between">
|
||||||
|
<span className="text-white/40">{t.estimatedArrival}</span>
|
||||||
|
<span className="text-green-400/80 font-semibold">{formatDuration(duration)}</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="flex justify-between">
|
||||||
|
<span className="text-white/40">{t.slippage}</span>
|
||||||
|
<span className="text-white/70">3%</span>
|
||||||
</div>
|
</div>
|
||||||
)}
|
|
||||||
<div className="flex justify-between">
|
|
||||||
<span className="text-white/40">{t.slippage}</span>
|
|
||||||
<span className="text-white/70">3%</span>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
);
|
||||||
)}
|
})()}
|
||||||
|
|
||||||
{/* ── Wallet Selector (inline, not a popup) ── */}
|
{/* ── Wallet Selector (inline, not a popup) ── */}
|
||||||
{!wallet.address && (
|
{!wallet.address && (
|
||||||
|
|
@ -901,14 +1024,26 @@ export default function Bridge() {
|
||||||
<span className="text-sm font-semibold text-white">
|
<span className="text-sm font-semibold text-white">
|
||||||
{order.fromAmount} USDT → {Number(order.toAmount).toLocaleString(undefined, { maximumFractionDigits: 2 })} XIC
|
{order.fromAmount} USDT → {Number(order.toAmount).toLocaleString(undefined, { maximumFractionDigits: 2 })} XIC
|
||||||
</span>
|
</span>
|
||||||
<a
|
<div className="flex items-center gap-1">
|
||||||
href={getTxUrl(order.fromChainId, order.txHash)}
|
<button
|
||||||
target="_blank"
|
onClick={() => copyHashToClipboard(order.txHash)}
|
||||||
rel="noopener noreferrer"
|
className="p-1 rounded transition-colors hover:bg-white/10"
|
||||||
className="text-blue-400/60 hover:text-blue-400 transition-colors"
|
title={t.copyHash}
|
||||||
>
|
>
|
||||||
<ExternalLink size={12} />
|
{copiedHash === order.txHash
|
||||||
</a>
|
? <CheckCheck size={12} className="text-green-400" />
|
||||||
|
: <Copy size={12} className="text-white/30 hover:text-white/60" />}
|
||||||
|
</button>
|
||||||
|
<a
|
||||||
|
href={getTxUrl(order.fromChainId, order.txHash)}
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
className="p-1 rounded transition-colors hover:bg-white/10"
|
||||||
|
title={t.viewExplorer}
|
||||||
|
>
|
||||||
|
<ExternalLink size={12} className="text-blue-400/60 hover:text-blue-400" />
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="text-xs text-white/20">
|
<div className="text-xs text-white/20">
|
||||||
{new Date(order.createdAt).toLocaleString()}
|
{new Date(order.createdAt).toLocaleString()}
|
||||||
|
|
@ -950,6 +1085,113 @@ export default function Bridge() {
|
||||||
{t.disclaimer}
|
{t.disclaimer}
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
|
{/* ── Transaction Success Modal ── */}
|
||||||
|
{showSuccessModal && txHash && (
|
||||||
|
<div
|
||||||
|
className="fixed inset-0 z-50 flex items-center justify-center p-4"
|
||||||
|
style={{ background: "rgba(0,0,0,0.8)", backdropFilter: "blur(8px)" }}
|
||||||
|
onClick={(e) => { if (e.target === e.currentTarget) setShowSuccessModal(false); }}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className="w-full max-w-sm rounded-2xl p-6 space-y-4"
|
||||||
|
style={{
|
||||||
|
background: "linear-gradient(135deg, #0d1117 0%, #0a0a0f 100%)",
|
||||||
|
border: "1px solid rgba(0,230,118,0.3)",
|
||||||
|
boxShadow: "0 0 60px rgba(0,230,118,0.15)",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{/* Close button */}
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<div
|
||||||
|
className="w-8 h-8 rounded-full flex items-center justify-center"
|
||||||
|
style={{ background: "rgba(0,230,118,0.15)", border: "1px solid rgba(0,230,118,0.3)" }}
|
||||||
|
>
|
||||||
|
<CheckCheck size={16} className="text-green-400" />
|
||||||
|
</div>
|
||||||
|
<span className="font-bold text-white" style={{ fontFamily: "'Space Grotesk', sans-serif" }}>
|
||||||
|
{t.txSuccessModal}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
onClick={() => setShowSuccessModal(false)}
|
||||||
|
className="text-white/30 hover:text-white/60 transition-colors"
|
||||||
|
>
|
||||||
|
<X size={18} />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Description */}
|
||||||
|
<p className="text-sm text-white/50">{t.txSuccessDesc}</p>
|
||||||
|
|
||||||
|
{/* Estimated arrival */}
|
||||||
|
{quote && getEstimatedDuration(quote) > 0 && (
|
||||||
|
<div
|
||||||
|
className="rounded-xl p-3 flex items-center justify-between"
|
||||||
|
style={{ background: "rgba(0,230,118,0.06)", border: "1px solid rgba(0,230,118,0.15)" }}
|
||||||
|
>
|
||||||
|
<span className="text-xs text-white/40">{t.estimatedArrival}</span>
|
||||||
|
<span className="text-green-400 font-bold text-sm">{formatDuration(getEstimatedDuration(quote))}</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* TX Hash */}
|
||||||
|
<div
|
||||||
|
className="rounded-xl p-3 space-y-2"
|
||||||
|
style={{ background: "rgba(255,255,255,0.03)", border: "1px solid rgba(255,255,255,0.08)" }}
|
||||||
|
>
|
||||||
|
<div className="text-xs text-white/30">{t.txHash}</div>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<span className="flex-1 text-xs font-mono text-white/60 truncate">
|
||||||
|
{txHash}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
{/* Action buttons */}
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<button
|
||||||
|
onClick={() => copyHashToClipboard(txHash)}
|
||||||
|
className="flex-1 flex items-center justify-center gap-1.5 py-2 rounded-lg text-xs font-semibold transition-all"
|
||||||
|
style={{
|
||||||
|
background: copiedHash === txHash ? "rgba(0,230,118,0.12)" : "rgba(255,255,255,0.06)",
|
||||||
|
border: copiedHash === txHash ? "1px solid rgba(0,230,118,0.3)" : "1px solid rgba(255,255,255,0.1)",
|
||||||
|
color: copiedHash === txHash ? "#00e676" : "rgba(255,255,255,0.5)",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{copiedHash === txHash ? <CheckCheck size={12} /> : <Copy size={12} />}
|
||||||
|
{copiedHash === txHash ? t.copied : t.copyHash}
|
||||||
|
</button>
|
||||||
|
<a
|
||||||
|
href={getTxUrl(fromChain.id, txHash)}
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
className="flex-1 flex items-center justify-center gap-1.5 py-2 rounded-lg text-xs font-semibold transition-all"
|
||||||
|
style={{
|
||||||
|
background: "rgba(0,212,255,0.08)",
|
||||||
|
border: "1px solid rgba(0,212,255,0.2)",
|
||||||
|
color: "#00d4ff",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<ExternalLink size={12} />
|
||||||
|
{t.viewDetails}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Close */}
|
||||||
|
<button
|
||||||
|
onClick={() => setShowSuccessModal(false)}
|
||||||
|
className="w-full py-3 rounded-xl text-sm font-semibold transition-all"
|
||||||
|
style={{
|
||||||
|
background: "linear-gradient(135deg, #f0b429, #e09820)",
|
||||||
|
color: "#0a0a0f",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{t.close}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
18
todo.md
18
todo.md
|
|
@ -123,3 +123,21 @@
|
||||||
- [ ] 错误提示"Wallet connection cancelled"改为中英文双语
|
- [ ] 错误提示"Wallet connection cancelled"改为中英文双语
|
||||||
- [ ] Bridge 页面添加中英文语言切换支持(与主页同步)
|
- [ ] Bridge 页面添加中英文语言切换支持(与主页同步)
|
||||||
- [ ] 信息卡片"5岁以上"应为"5条以上"(支持链数量)
|
- [ ] 信息卡片"5岁以上"应为"5条以上"(支持链数量)
|
||||||
|
|
||||||
|
## v11 Bridge增强功能
|
||||||
|
|
||||||
|
- [ ] Gas费估算显示:在"YOU RECEIVE"区域下方显示预估Gas费(源链原生代币)和预计到账时间
|
||||||
|
- [ ] Gas费说明文案:说明Gas用源链原生代币支付(BSC用BNB,ETH用ETH,Polygon用MATIC等)
|
||||||
|
- [ ] 交易历史"复制交易哈希"快捷按钮
|
||||||
|
- [ ] 交易历史"在区块浏览器中查看"快捷按钮
|
||||||
|
- [ ] 交易成功弹窗提示(附查看交易详情链接)
|
||||||
|
- [ ] 浏览器全流程测试
|
||||||
|
- [ ] 构建并部署到AI服务器
|
||||||
|
- [ ] 记录部署日志
|
||||||
|
|
||||||
|
## v11 钱包连接卡死修复(来自用户反馈)
|
||||||
|
|
||||||
|
- [ ] 修复WalletSelector连接卡死:连接超时30s自动重置状态
|
||||||
|
- [ ] 修复用户取消钱包弹窗后状态不重置(error code 4001/4100处理)
|
||||||
|
- [ ] 修复连接成功后回调不触发(accounts事件监听改为直接返回值处理)
|
||||||
|
- [ ] 确保每次点击钱包按钮都能重新触发钱包弹窗
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue