From 4bdb118cb2a74456661d879c9537198b977a8dad Mon Sep 17 00:00:00 2001 From: Manus Date: Tue, 10 Mar 2026 04:53:41 -0400 Subject: [PATCH] =?UTF-8?q?Checkpoint:=20v11:=20Bridge=E9=A1=B5=E9=9D=A2?= =?UTF-8?q?=E5=A2=9E=E5=BC=BA=20-=20Gas=E8=B4=B9=E4=BC=B0=E7=AE=97?= =?UTF-8?q?=E6=98=BE=E7=A4=BA=EF=BC=88=E5=90=AB=E5=8E=9F=E7=94=9F=E4=BB=A3?= =?UTF-8?q?=E5=B8=81=E8=AF=B4=E6=98=8E=EF=BC=89=E3=80=81=E4=BA=A4=E6=98=93?= =?UTF-8?q?=E5=8E=86=E5=8F=B2=E5=A4=8D=E5=88=B6=E5=93=88=E5=B8=8C+?= =?UTF-8?q?=E5=8C=BA=E5=9D=97=E6=B5=8F=E8=A7=88=E5=99=A8=E5=BF=AB=E6=8D=B7?= =?UTF-8?q?=E6=8C=89=E9=92=AE=E3=80=81=E4=BA=A4=E6=98=93=E6=88=90=E5=8A=9F?= =?UTF-8?q?=E5=BC=B9=E7=AA=97=EF=BC=88=E5=90=AB=E5=88=B0=E8=B4=A6=E6=97=B6?= =?UTF-8?q?=E9=97=B4=E3=80=81=E5=A4=8D=E5=88=B6=E5=93=88=E5=B8=8C=E3=80=81?= =?UTF-8?q?=E6=9F=A5=E7=9C=8B=E8=AF=A6=E6=83=85=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- client/src/pages/Bridge.tsx | 314 +++++++++++++++++++++++++++++++----- todo.md | 18 +++ 2 files changed, 296 insertions(+), 36 deletions(-) diff --git a/client/src/pages/Bridge.tsx b/client/src/pages/Bridge.tsx index 5cc1a98..3987b5d 100644 --- a/client/src/pages/Bridge.tsx +++ b/client/src/pages/Bridge.tsx @@ -5,7 +5,7 @@ import { useState, useEffect, useCallback, useMemo } from "react"; import { toast } from "sonner"; 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 { WalletSelector } from "@/components/WalletSelector"; import { useWallet } from "@/hooks/useWallet"; @@ -68,6 +68,18 @@ const T = { approve: "授权", executeSwap: "执行跨链交换", lang: "EN", + estGasNative: "Gas费(用{symbol}支付)", + estTime: "预计到账时间", + gasNote: "Gas 费用{symbol}({chain}原生代币)支付,请确保钱包中有足够的{symbol}", + copyHash: "复制哈希", + copied: "已复制!", + viewExplorer: "在区块浏览器中查看", + txSuccessModal: "交易成功", + txSuccessDesc: "您的跨链交易已提交,XIC 代币将在以下时间内到账:", + viewDetails: "查看交易详情", + close: "关闭", + estimatedArrival: "预计到账", + gasPayWith: "Gas 支付方式", }, en: { title: "Buy XIC from Any Chain", @@ -121,6 +133,18 @@ const T = { approve: "Approve", executeSwap: "Execute Cross-Chain Swap", 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; tool: string; toolDetails?: { name: string; logoURI?: string }; + estimate?: { + executionDuration?: number; // seconds + gasCosts?: Array<{ amountUSD?: string; amount?: string; token?: { symbol: string; decimals: number } }>; + }; }>; gasCostUSD?: string; + estimate?: { + executionDuration?: number; // total seconds + gasCosts?: Array<{ amountUSD?: string; amount?: string; token?: { symbol: string; decimals: number } }>; + }; } // ─── Li.Fi API helpers ──────────────────────────────────────────────────────── @@ -221,6 +253,38 @@ function getTxUrl(chainId: number, txHash: string): string { 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 ───────────────────────────────────────────────────────── export default function Bridge() { // ── Language ── @@ -244,6 +308,8 @@ export default function Bridge() { const [txHash, setTxHash] = useState(null); const [completedTxs, setCompletedTxs] = useState(0); const [showHistory, setShowHistory] = useState(false); + const [showSuccessModal, setShowSuccessModal] = useState(false); + const [copiedHash, setCopiedHash] = useState(null); // ── tRPC mutations/queries ── const recordOrder = trpc.bridge.recordOrder.useMutation(); @@ -374,7 +440,7 @@ export default function Bridge() { setTxHash(hash); setExecStep("done"); setCompletedTxs(c => c + 1); - toast.success(t.txSuccess); + setShowSuccessModal(true); // Record in DB const xicReceived = (parseFloat(quote.toAmount) / 1e18).toFixed(6); @@ -401,7 +467,7 @@ export default function Bridge() { setTxHash(hash); setExecStep("done"); setCompletedTxs(c => c + 1); - toast.success(t.txSuccess); + setShowSuccessModal(true); // Record in DB const xicReceived = (parseFloat(quote.toAmount) / 1e18).toFixed(6); @@ -449,6 +515,25 @@ export default function Bridge() { return `${t.buyXIC} ${xicAmount} XIC`; }, [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 (
- {/* Route info */} - {quote && !quoting && ( -
-
- {t.route} - {fromChain.name} USDT → BSC XIC -
-
- {t.protocol} - - {quote.steps.map((s) => s.toolDetails?.name ?? s.tool).join(" → ")} - -
- {quote.gasCostUSD && ( + {/* Route info + Gas fee + Estimated time */} + {quote && !quoting && (() => { + const gasCostInfo = getGasCostInfo(quote); + const duration = getEstimatedDuration(quote); + const nativeSymbol = fromChain.symbol; // BNB/ETH/MATIC/AVAX + return ( +
- {t.estGas} - ${quote.gasCostUSD} + {t.route} + {fromChain.name} USDT → BSC XIC +
+
+ {t.protocol} + + {quote.steps.map((s) => s.toolDetails?.name ?? s.tool).join(" → ")} + +
+ + {/* Gas fee row with native token info */} +
+ {t.gasPayWith} +
+ {nativeSymbol} + {gasCostInfo && ( + + ({gasCostInfo.usd} + {gasCostInfo.nativeAmount && gasCostInfo.nativeSymbol + ? ` ≈ ${gasCostInfo.nativeAmount} ${gasCostInfo.nativeSymbol}` + : ""} + ) + + )} +
+
+ + {/* Gas note: explain which token is used */} +
+ ⚠️ + + {t.gasNote + .replace(/\{symbol\}/g, nativeSymbol) + .replace(/\{chain\}/g, fromChain.name)} + +
+ + {/* Estimated arrival time */} + {duration > 0 && ( +
+ {t.estimatedArrival} + {formatDuration(duration)} +
+ )} + +
+ {t.slippage} + 3%
- )} -
- {t.slippage} - 3%
-
- )} + ); + })()} {/* ── Wallet Selector (inline, not a popup) ── */} {!wallet.address && ( @@ -901,14 +1024,26 @@ export default function Bridge() { {order.fromAmount} USDT → {Number(order.toAmount).toLocaleString(undefined, { maximumFractionDigits: 2 })} XIC - - - +
+ + + + +
{new Date(order.createdAt).toLocaleString()} @@ -950,6 +1085,113 @@ export default function Bridge() { {t.disclaimer}
+ + {/* ── Transaction Success Modal ── */} + {showSuccessModal && txHash && ( +
{ if (e.target === e.currentTarget) setShowSuccessModal(false); }} + > +
+ {/* Close button */} +
+
+
+ +
+ + {t.txSuccessModal} + +
+ +
+ + {/* Description */} +

{t.txSuccessDesc}

+ + {/* Estimated arrival */} + {quote && getEstimatedDuration(quote) > 0 && ( +
+ {t.estimatedArrival} + {formatDuration(getEstimatedDuration(quote))} +
+ )} + + {/* TX Hash */} +
+
{t.txHash}
+
+ + {txHash} + +
+ {/* Action buttons */} +
+ + + + {t.viewDetails} + +
+
+ + {/* Close */} + +
+
+ )} ); } diff --git a/todo.md b/todo.md index 5456be6..2b9c3cf 100644 --- a/todo.md +++ b/todo.md @@ -123,3 +123,21 @@ - [ ] 错误提示"Wallet connection cancelled"改为中英文双语 - [ ] Bridge 页面添加中英文语言切换支持(与主页同步) - [ ] 信息卡片"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事件监听改为直接返回值处理) +- [ ] 确保每次点击钱包按钮都能重新触发钱包弹窗