diff --git a/client/src/components/WalletSelector.tsx b/client/src/components/WalletSelector.tsx index df0c859..7f65bc0 100644 --- a/client/src/components/WalletSelector.tsx +++ b/client/src/components/WalletSelector.tsx @@ -665,14 +665,34 @@ export function WalletSelector({ lang, onAddressDetected, connectedAddress, comp - {/* Installed wallets */} - {installedWallets.length > 0 && ( + {/* Connecting overlay — shown when any wallet is connecting */} + {connecting && ( +
+ + + + +
+

+ {lang === "zh" ? "正在连接钱包..." : "Connecting wallet..."} +

+

+ {lang === "zh" ? "请在钱包弹窗中确认连接请求" : "Please confirm the connection request in your wallet popup"} +

+
+
+ )} + {/* Installed wallets — hidden while connecting to prevent accidental double-clicks */} + {!connecting && installedWallets.length > 0 && (
{installedWallets.map(wallet => (
)} + )} ); } diff --git a/client/src/pages/Bridge.tsx b/client/src/pages/Bridge.tsx index 24f74c8..5cc1a98 100644 --- a/client/src/pages/Bridge.tsx +++ b/client/src/pages/Bridge.tsx @@ -1,20 +1,136 @@ // NAC Cross-Chain Bridge — Buy XIC with USDT from any chain -// Uses Li.Fi SDK for cross-chain routing +// v2: Integrated WalletSelector (same as Home page), full Li.Fi execution, TX history, bilingual +// Uses Li.Fi API for cross-chain routing // Supports: BSC, ETH, Polygon, Arbitrum, Avalanche - -import { useState, useEffect, useCallback } from "react"; +import { useState, useEffect, useCallback, useMemo } from "react"; import { toast } from "sonner"; import { trpc } from "@/lib/trpc"; -import { ArrowDown, ArrowLeft, ExternalLink, Loader2, RefreshCw, Zap } from "lucide-react"; +import { ArrowDown, ArrowLeft, ExternalLink, Loader2, RefreshCw, Zap, History, ChevronDown, ChevronUp } from "lucide-react"; import { Link } from "wouter"; +import { WalletSelector } from "@/components/WalletSelector"; +import { useWallet } from "@/hooks/useWallet"; +import type { EthProvider } from "@/hooks/useWallet"; +import { switchToNetwork } from "@/lib/contracts"; + +// ─── Language ───────────────────────────────────────────────────────────────── +type Lang = "zh" | "en"; + +const T = { + zh: { + title: "从任何链购买 XIC", + subtitle: "使用 BSC、ETH、Polygon、Arbitrum 或 Avalanche 上的 USDT 购买 XIC 代币", + fromChain: "来源链", + youPay: "支付金额 (USDT)", + youReceive: "接收数量 (BSC 上的 XIC)", + gettingRoute: "正在获取最优路由...", + route: "路由", + protocol: "协议", + estGas: "预计 Gas", + slippage: "滑点", + connectWallet: "连接钱包以继续", + connecting: "连接中...", + switchNetwork: "切换到", + refreshQuote: "刷新报价", + gettingQuote: "获取报价中...", + enterAmount: "请输入金额以获取报价", + buyXIC: "购买", + executing: "交易执行中...", + txSubmitted: "交易已提交", + txDesc: "您的 XIC 代币即将到达 BSC 链", + viewBscscan: "在 BSCScan 上查看", + connected: "已连接", + supportedChains: "支持链数", + tokenPrice: "代币价格", + listingTarget: "目标上市", + presalePrice: "预售价格", + xPotential: "5倍潜力", + disclaimer: "跨链交易由 Li.Fi 协议支持。确认交易前请务必核实交易详情。最低滑点为 3%。本信息不构成任何财务建议。", + history: "交易历史", + noHistory: "暂无交易记录", + historyDesc: "连接钱包后可查看您的历史交易", + status_completed: "已完成", + status_pending: "处理中", + status_failed: "失败", + txHash: "交易哈希", + backToPresale: "返回预售", + approving: "授权 USDT 中...", + swapping: "执行跨链交换...", + txSuccess: "交易成功!", + txFailed: "交易失败,请重试", + noRoute: "未找到可用路由", + quoteFailed: "获取报价失败,请重试", + minReceive: "最少获得", + via: "通过", + completedSession: "本次会话已完成", + transactions: "笔交易", + approveFirst: "首先授权 USDT", + approveDesc: "需要授权 USDT 合约以允许 Li.Fi 使用您的代币", + approve: "授权", + executeSwap: "执行跨链交换", + lang: "EN", + }, + en: { + title: "Buy XIC from Any Chain", + subtitle: "Use USDT on BSC, ETH, Polygon, Arbitrum or Avalanche to buy XIC tokens", + fromChain: "From Chain", + youPay: "You Pay (USDT)", + youReceive: "You Receive (XIC on BSC)", + gettingRoute: "Getting best route...", + route: "Route", + protocol: "Protocol", + estGas: "Est. Gas", + slippage: "Slippage", + connectWallet: "Connect Wallet to Continue", + connecting: "Connecting...", + switchNetwork: "Switch to", + refreshQuote: "Refresh Quote", + gettingQuote: "Getting Quote...", + enterAmount: "Enter amount to get quote", + buyXIC: "Buy", + executing: "Executing...", + txSubmitted: "Transaction Submitted", + txDesc: "Your XIC tokens will arrive on BSC shortly", + viewBscscan: "View on BSCScan", + connected: "Connected", + supportedChains: "Supported Chains", + tokenPrice: "Token Price", + listingTarget: "Listing Target", + presalePrice: "Presale price", + xPotential: "5x potential", + disclaimer: "Cross-chain swaps are powered by Li.Fi protocol. Always verify transaction details before confirming. Minimum slippage 3% applies. Not financial advice.", + history: "Transaction History", + noHistory: "No transactions yet", + historyDesc: "Connect your wallet to view transaction history", + status_completed: "Completed", + status_pending: "Pending", + status_failed: "Failed", + txHash: "TX Hash", + backToPresale: "Back to Presale", + approving: "Approving USDT...", + swapping: "Executing cross-chain swap...", + txSuccess: "Transaction successful!", + txFailed: "Transaction failed. Please try again.", + noRoute: "No route found for this pair", + quoteFailed: "Failed to get quote. Please try again.", + minReceive: "Min receive", + via: "via", + completedSession: "completed this session", + transactions: "transaction", + approveFirst: "Approve USDT First", + approveDesc: "Allow Li.Fi to use your USDT for the swap", + approve: "Approve", + executeSwap: "Execute Cross-Chain Swap", + lang: "中文", + }, +}; // ─── Chain Config ───────────────────────────────────────────────────────────── const CHAINS = [ - { id: 56, name: "BSC", symbol: "BNB", color: "#F0B90B", icon: "🟡", usdtAddress: "0x55d398326f99059fF775485246999027B3197955" }, - { id: 1, name: "Ethereum", symbol: "ETH", color: "#627EEA", icon: "🔵", usdtAddress: "0xdAC17F958D2ee523a2206206994597C13D831ec7" }, - { id: 137, name: "Polygon", symbol: "MATIC",color: "#8247E5", icon: "🟣", usdtAddress: "0xc2132D05D31c914a87C6611C10748AEb04B58e8F" }, - { id: 42161, name: "Arbitrum", symbol: "ETH", color: "#28A0F0", icon: "🔷", usdtAddress: "0xFd086bC7CD5C481DCC9C85ebE478A1C0b69FCbb9" }, - { id: 43114, name: "Avalanche", symbol: "AVAX", color: "#E84142", icon: "🔴", usdtAddress: "0x9702230A8Ea53601f5cD2dc00fDBc13d4dF4A8c7" }, + { id: 56, name: "BSC", symbol: "BNB", color: "#F0B90B", icon: "🟡", usdtAddress: "0x55d398326f99059fF775485246999027B3197955", rpcUrl: "https://bsc-dataseed1.binance.org/", nativeCurrency: { name: "BNB", symbol: "BNB", decimals: 18 }, explorerUrl: "https://bscscan.com" }, + { id: 1, name: "Ethereum", symbol: "ETH", color: "#627EEA", icon: "🔵", usdtAddress: "0xdAC17F958D2ee523a2206206994597C13D831ec7", rpcUrl: "https://eth.llamarpc.com", nativeCurrency: { name: "Ether", symbol: "ETH", decimals: 18 }, explorerUrl: "https://etherscan.io" }, + { id: 137, name: "Polygon", symbol: "MATIC",color: "#8247E5", icon: "🟣", usdtAddress: "0xc2132D05D31c914a87C6611C10748AEb04B58e8F", rpcUrl: "https://polygon-rpc.com/", nativeCurrency: { name: "MATIC", symbol: "MATIC", decimals: 18 }, explorerUrl: "https://polygonscan.com" }, + { id: 42161, name: "Arbitrum", symbol: "ETH", color: "#28A0F0", icon: "🔷", usdtAddress: "0xFd086bC7CD5C481DCC9C85ebE478A1C0b69FCbb9", rpcUrl: "https://arb1.arbitrum.io/rpc", nativeCurrency: { name: "Ether", symbol: "ETH", decimals: 18 }, explorerUrl: "https://arbiscan.io" }, + { id: 43114, name: "Avalanche", symbol: "AVAX", color: "#E84142", icon: "🔴", usdtAddress: "0x9702230A8Ea53601f5cD2dc00fDBc13d4dF4A8c7", rpcUrl: "https://api.avax.network/ext/bc/C/rpc", nativeCurrency: { name: "AVAX", symbol: "AVAX", decimals: 18 }, explorerUrl: "https://snowtrace.io" }, ]; // XIC Token on BSC (destination) @@ -30,6 +146,15 @@ const XIC_TOKEN = { // Quick amounts const QUICK_AMOUNTS = [100, 500, 1000, 5000]; +// Li.Fi Diamond contract addresses (for USDT approval) +const LIFI_DIAMOND: Record = { + 56: "0x1231DEB6f5749EF6cE6943a275A1D3E7486F4EaE", + 1: "0x1231DEB6f5749EF6cE6943a275A1D3E7486F4EaE", + 137: "0x1231DEB6f5749EF6cE6943a275A1D3E7486F4EaE", + 42161: "0x1231DEB6f5749EF6cE6943a275A1D3E7486F4EaE", + 43114: "0x1231DEB6f5749EF6cE6943a275A1D3E7486F4EaE", +}; + // ─── Types ──────────────────────────────────────────────────────────────────── interface RouteQuote { id: string; @@ -37,6 +162,13 @@ interface RouteQuote { toAmount: string; toAmountMin: string; estimatedGas: string; + transactionRequest?: { + to: string; + data: string; + value: string; + gasLimit?: string; + gasPrice?: string; + }; steps: Array<{ type: string; tool: string; @@ -65,103 +197,82 @@ async function getRoutes( fromAddress, options: JSON.stringify({ slippage: 0.03, order: "RECOMMENDED" }), }); - const res = await fetch(`${LIFI_API}/quote?${params}`); if (!res.ok) throw new Error(`Li.Fi API error: ${res.status}`); const data = await res.json(); return data ? [data] : []; } -// ─── Wallet Hook (window.ethereum) ─────────────────────────────────────────── -function useEVMWallet() { - const [address, setAddress] = useState(null); - const [chainId, setChainId] = useState(null); - const [connecting, setConnecting] = useState(false); +// ERC-20 minimal ABI for allowance + approve +const ERC20_ABI_APPROVE = [ + "function allowance(address owner, address spender) view returns (uint256)", + "function approve(address spender, uint256 amount) returns (bool)", +]; - const connect = useCallback(async () => { - if (!window.ethereum) { - toast.error("Please install MetaMask or another EVM wallet"); - return; - } - setConnecting(true); - try { - const accounts = await window.ethereum.request({ method: "eth_requestAccounts" }) as string[]; - setAddress(accounts[0]); - const cid = await window.ethereum.request({ method: "eth_chainId" }) as string; - setChainId(parseInt(cid, 16)); - } catch { - toast.error("Wallet connection cancelled"); - } finally { - setConnecting(false); - } - }, []); +// ─── Chain name helper ──────────────────────────────────────────────────────── +function getChainName(chainId: number): string { + const chain = CHAINS.find(c => c.id === chainId); + return chain?.name ?? `Chain ${chainId}`; +} - const switchChain = useCallback(async (targetChainId: number) => { - if (!window.ethereum) return; - try { - await window.ethereum.request({ - method: "wallet_switchEthereumChain", - params: [{ chainId: `0x${targetChainId.toString(16)}` }], - }); - } catch (err: unknown) { - // Chain not added, try adding it - const chain = CHAINS.find((c) => c.id === targetChainId); - if (chain && (err as { code?: number })?.code === 4902) { - toast.error(`Please add ${chain.name} network to your wallet manually`); - } - } - }, []); - - useEffect(() => { - if (!window.ethereum) return; - window.ethereum.request({ method: "eth_accounts" }).then((accounts: unknown) => { - const accs = accounts as string[]; - if (accs.length > 0) { - setAddress(accs[0]); - window.ethereum!.request({ method: "eth_chainId" }).then((cid: unknown) => { - setChainId(parseInt(cid as string, 16)); - }); - } - }); - - const handleAccountsChanged = (accounts: unknown) => { - const accs = accounts as string[]; - setAddress(accs.length > 0 ? accs[0] : null); - }; - const handleChainChanged = (cid: unknown) => { - setChainId(parseInt(cid as string, 16)); - }; - - window.ethereum.on("accountsChanged", handleAccountsChanged); - window.ethereum.on("chainChanged", handleChainChanged); - return () => { - window.ethereum?.removeListener("accountsChanged", handleAccountsChanged); - window.ethereum?.removeListener("chainChanged", handleChainChanged); - }; - }, []); - - return { address, chainId, connecting, connect, switchChain }; +// ─── Explorer URL helper ────────────────────────────────────────────────────── +function getTxUrl(chainId: number, txHash: string): string { + const chain = CHAINS.find(c => c.id === chainId); + return `${chain?.explorerUrl ?? "https://bscscan.com"}/tx/${txHash}`; } // ─── Main Bridge Page ───────────────────────────────────────────────────────── export default function Bridge() { - const wallet = useEVMWallet(); - const [fromChain, setFromChain] = useState(CHAINS[0]); // BSC default + // ── Language ── + const [lang, setLang] = useState(() => { + const bl = navigator.language.toLowerCase(); + return bl.startsWith("zh") ? "zh" : "en"; + }); + const t = T[lang]; + + // ── Wallet (shared hook with Home page) ── + const wallet = useWallet(); + const [showWalletSelector, setShowWalletSelector] = useState(false); + + // ── Bridge state ── + const [fromChain, setFromChain] = useState(CHAINS[0]); const [usdtAmount, setUsdtAmount] = useState("100"); const [quote, setQuote] = useState(null); const [quoting, setQuoting] = useState(false); const [executing, setExecuting] = useState(false); + const [execStep, setExecStep] = useState<"idle" | "approving" | "swapping" | "done">("idle"); const [txHash, setTxHash] = useState(null); const [completedTxs, setCompletedTxs] = useState(0); + const [showHistory, setShowHistory] = useState(false); + // ── tRPC mutations/queries ── const recordOrder = trpc.bridge.recordOrder.useMutation(); + const myOrdersQuery = trpc.bridge.myOrders.useQuery( + { walletAddress: wallet.address ?? "", limit: 20 }, + { enabled: !!wallet.address && showHistory } + ); + + // ── Handle wallet connection from WalletSelector ── + const handleAddressDetected = useCallback(async (address: string, _network?: "evm" | "tron", rawProvider?: EthProvider) => { + if (rawProvider) { + await wallet.connectWithProvider(rawProvider, address); + } + setShowWalletSelector(false); + toast.success(lang === "zh" ? `钱包已连接: ${address.slice(0,6)}...${address.slice(-4)}` : `Wallet connected: ${address.slice(0,6)}...${address.slice(-4)}`); + }, [wallet, lang]); + + // ── Auto-hide wallet selector when connected ── + useEffect(() => { + if (wallet.address) { + setShowWalletSelector(false); + } + }, [wallet.address]); // ─── Fetch quote ───────────────────────────────────────────────────────── const fetchQuote = useCallback(async () => { const amount = parseFloat(usdtAmount); if (!amount || amount <= 0) return; if (!wallet.address) return; - setQuoting(true); setQuote(null); try { @@ -177,80 +288,166 @@ export default function Bridge() { if (routes.length > 0) { setQuote(routes[0]); } else { - toast.error("No route found for this pair"); + toast.error(t.noRoute); } } catch (err) { - toast.error("Failed to get quote. Please try again."); + toast.error(t.quoteFailed); console.error(err); } finally { setQuoting(false); } - }, [fromChain, usdtAmount, wallet.address]); + }, [fromChain, usdtAmount, wallet.address, t]); - // Auto-fetch quote when inputs change + // Auto-fetch quote when inputs change (debounced) useEffect(() => { if (!wallet.address || !usdtAmount || parseFloat(usdtAmount) <= 0) return; const timer = setTimeout(fetchQuote, 800); return () => clearTimeout(timer); }, [fetchQuote, wallet.address, usdtAmount]); - // ─── Execute swap ───────────────────────────────────────────────────────── + // Re-fetch quote when chain changes + useEffect(() => { + setQuote(null); + }, [fromChain]); + + // ─── Execute swap (full Li.Fi flow) ────────────────────────────────────── const executeSwap = useCallback(async () => { - if (!quote || !wallet.address) return; - if (!window.ethereum) { - toast.error("No wallet detected"); + if (!quote || !wallet.address || !wallet.signer) { + toast.error(lang === "zh" ? "请先连接钱包" : "Please connect wallet first"); return; } - - // Switch to correct chain if needed if (wallet.chainId !== fromChain.id) { - await wallet.switchChain(fromChain.id); - toast.info(`Please confirm network switch to ${fromChain.name}`); - return; + try { + await switchToNetwork(fromChain.id); + } catch { + toast.error(lang === "zh" ? `请切换到 ${fromChain.name} 网络` : `Please switch to ${fromChain.name} network`); + return; + } } setExecuting(true); + setExecStep("idle"); + try { - // Get the full route with transaction data from Li.Fi - const res = await fetch(`${LIFI_API}/quote`, { - method: "GET", - }); - if (!res.ok) throw new Error("Failed to get transaction data"); + const { ethers } = await import("ethers"); + const usdtContract = new ethers.Contract(fromChain.usdtAddress, ERC20_ABI_APPROVE, wallet.signer); + const lifiDiamond = LIFI_DIAMOND[fromChain.id]; + const amountWei = BigInt(Math.floor(parseFloat(usdtAmount) * 1000000)); - // For now, show the user the transaction details and let them confirm - // In production, this would call the Li.Fi SDK's executeRoute - toast.success("Route confirmed! Redirecting to Li.Fi for execution..."); + // Step 1: Check and approve USDT allowance + setExecStep("approving"); + const allowance: bigint = await usdtContract.allowance(wallet.address, lifiDiamond); + if (allowance < amountWei) { + toast.info(lang === "zh" ? "请在钱包中确认 USDT 授权..." : "Please confirm USDT approval in your wallet..."); + const approveTx = await usdtContract.approve(lifiDiamond, amountWei * BigInt(10)); + await approveTx.wait(); + toast.success(lang === "zh" ? "USDT 授权成功" : "USDT approved successfully"); + } - // Record the order - const hash = `pending-${Date.now()}`; - await recordOrder.mutateAsync({ - txHash: hash, - walletAddress: wallet.address, - fromChainId: fromChain.id, - fromToken: "USDT", - fromAmount: usdtAmount, - toChainId: XIC_TOKEN.chainId, - toToken: "XIC", - toAmount: (parseFloat(quote.toAmount) / 1e18).toFixed(2), - }); + // Step 2: Execute the Li.Fi transaction + setExecStep("swapping"); + toast.info(lang === "zh" ? "请在钱包中确认跨链交易..." : "Please confirm the cross-chain transaction in your wallet..."); - setTxHash(hash); - setCompletedTxs((c) => c + 1); - } catch (err) { - toast.error("Transaction failed. Please try again."); - console.error(err); + if (!quote.transactionRequest) { + // Re-fetch quote to get fresh transactionRequest + const amountWeiStr = amountWei.toString(); + const freshRoutes = await getRoutes( + fromChain.id, + fromChain.usdtAddress, + XIC_TOKEN.chainId, + XIC_TOKEN.address, + amountWeiStr, + wallet.address + ); + if (!freshRoutes[0]?.transactionRequest) { + throw new Error("No transaction data from Li.Fi"); + } + const txReq = freshRoutes[0].transactionRequest; + const tx = await wallet.signer.sendTransaction({ + to: txReq.to, + data: txReq.data, + value: BigInt(txReq.value || "0"), + gasLimit: txReq.gasLimit ? BigInt(txReq.gasLimit) : undefined, + }); + const receipt = await tx.wait(); + const hash = receipt?.hash ?? tx.hash; + setTxHash(hash); + setExecStep("done"); + setCompletedTxs(c => c + 1); + toast.success(t.txSuccess); + + // Record in DB + const xicReceived = (parseFloat(quote.toAmount) / 1e18).toFixed(6); + await recordOrder.mutateAsync({ + txHash: hash, + walletAddress: wallet.address!, + fromChainId: fromChain.id, + fromToken: "USDT", + fromAmount: String(parseFloat(usdtAmount)), + toChainId: XIC_TOKEN.chainId, + toToken: "XIC", + toAmount: xicReceived, + }); + } else { + const txReq = quote.transactionRequest; + const tx = await wallet.signer.sendTransaction({ + to: txReq.to, + data: txReq.data, + value: BigInt(txReq.value || "0"), + gasLimit: txReq.gasLimit ? BigInt(txReq.gasLimit) : undefined, + }); + const receipt = await tx.wait(); + const hash = receipt?.hash ?? tx.hash; + setTxHash(hash); + setExecStep("done"); + setCompletedTxs(c => c + 1); + toast.success(t.txSuccess); + + // Record in DB + const xicReceived = (parseFloat(quote.toAmount) / 1e18).toFixed(6); + await recordOrder.mutateAsync({ + txHash: hash, + walletAddress: wallet.address!, + fromChainId: fromChain.id, + fromToken: "USDT", + fromAmount: String(parseFloat(usdtAmount)), + toChainId: XIC_TOKEN.chainId, + toToken: "XIC", + toAmount: xicReceived, + }); + } + + // Refresh history + if (showHistory) myOrdersQuery.refetch(); + + } catch (err: unknown) { + const error = err as Error; + if (error.message?.includes("user rejected") || error.message?.includes("ACTION_REJECTED")) { + toast.error(lang === "zh" ? "已取消交易" : "Transaction cancelled"); + } else { + toast.error(t.txFailed); + console.error(err); + } + setExecStep("idle"); } finally { setExecuting(false); } - }, [quote, wallet, fromChain, usdtAmount, recordOrder]); + }, [quote, wallet, fromChain, usdtAmount, t, lang, recordOrder, showHistory, myOrdersQuery]); - const xicAmount = quote + const xicAmount = useMemo(() => quote ? (parseFloat(quote.toAmount) / 1e18).toLocaleString(undefined, { maximumFractionDigits: 2 }) - : "—"; + : "—", [quote]); - const xicMin = quote + const xicMin = useMemo(() => quote ? (parseFloat(quote.toAmountMin) / 1e18).toLocaleString(undefined, { maximumFractionDigits: 2 }) - : "—"; + : "—", [quote]); + + // ── Exec step label ── + const execLabel = useMemo(() => { + if (execStep === "approving") return t.approving; + if (execStep === "swapping") return t.swapping; + return `${t.buyXIC} ${xicAmount} XIC`; + }, [execStep, t, xicAmount]); return (
- Back to Presale + {t.backToPresale}
@@ -279,27 +476,35 @@ export default function Bridge() { NAC Cross-Chain Bridge
-
- Powered by Li.Fi +
+ {/* Language toggle */} + +
+ Powered by Li.Fi +
{/* ── Main Content ── */} -
+
{/* Title */} -
+

- Buy XIC from Any Chain + {t.title}

-

- Use USDT on BSC, ETH, Polygon, Arbitrum or Avalanche to buy XIC tokens -

+

{t.subtitle}

{completedTxs > 0 && (
- ✓ {completedTxs} transaction{completedTxs > 1 ? "s" : ""} completed this session + ✓ {completedTxs} {t.transactions}{completedTxs > 1 && lang === "en" ? "s" : ""} {t.completedSession}
)}
@@ -316,7 +521,7 @@ export default function Bridge() { {/* FROM: Chain Selector */}
{CHAINS.map((chain) => ( @@ -344,7 +549,7 @@ export default function Bridge() { {/* FROM: USDT Amount */}
- Getting best route... + {t.gettingRoute}
) : ( - Min: {xicMin} XIC · via {quote.steps[0]?.toolDetails?.name ?? quote.steps[0]?.tool} + {t.minReceive}: {xicMin} XIC · {t.via} {quote.steps[0]?.toolDetails?.name ?? quote.steps[0]?.tool}
)}
@@ -453,54 +658,76 @@ export default function Bridge() { style={{ background: "rgba(0,212,255,0.04)", border: "1px solid rgba(0,212,255,0.1)" }} >
- Route - - {fromChain.name} USDT → BSC XIC - + {t.route} + {fromChain.name} USDT → BSC XIC
- Protocol + {t.protocol} {quote.steps.map((s) => s.toolDetails?.name ?? s.tool).join(" → ")}
{quote.gasCostUSD && (
- Est. Gas + {t.estGas} ${quote.gasCostUSD}
)}
- Slippage + {t.slippage} 3%
)} - {/* Action Button */} - {!wallet.address ? ( - ) : ( - "Connect Wallet to Continue" +
+ + +
)} - - ) : wallet.chainId !== fromChain.id ? ( +
+ )} + + {/* ── Connected: Switch Network or Execute ── */} + {wallet.address && wallet.chainId !== fromChain.id && ( - ) : ( + )} + + {wallet.address && wallet.chainId === fromChain.id && (
@@ -555,41 +784,149 @@ export default function Bridge() { {/* Connected wallet info */} {wallet.address && (
- Connected: - - {wallet.address.slice(0, 6)}...{wallet.address.slice(-4)} - + {t.connected}: +
+ + {wallet.address.slice(0, 6)}...{wallet.address.slice(-4)} + + +
)}
{/* ── TX Success ── */} - {txHash && ( + {txHash && execStep === "done" && (
-
✓ Transaction Submitted
-
- Your XIC tokens will arrive on BSC shortly -
+
✓ {t.txSubmitted}
+
{t.txDesc}
- View on BSCScan + {t.viewBscscan}
)} + {/* ── Transaction History ── */} +
+ + + {showHistory && ( +
+ {!wallet.address ? ( +

{t.historyDesc}

+ ) : myOrdersQuery.isLoading ? ( +
+ + Loading... +
+ ) : !myOrdersQuery.data || myOrdersQuery.data.length === 0 ? ( +

{t.noHistory}

+ ) : ( +
+ {myOrdersQuery.data.map((order) => ( +
+
+ + {getChainName(order.fromChainId)} → BSC + + + {order.status === "completed" ? t.status_completed + : order.status === "pending" ? t.status_pending + : t.status_failed} + +
+
+ + {order.fromAmount} USDT → {Number(order.toAmount).toLocaleString(undefined, { maximumFractionDigits: 2 })} XIC + + + + +
+
+ {new Date(order.createdAt).toLocaleString()} +
+
+ ))} +
+ )} +
+ )} +
+ {/* ── Info Cards ── */} -
+
{[ - { label: "Supported Chains", value: "5+", sub: "BSC, ETH, Polygon..." }, - { label: "Token Price", value: "$0.02", sub: "Presale price" }, - { label: "Listing Target", value: "$0.10", sub: "5x potential" }, + { label: t.supportedChains, value: "5+", sub: "BSC, ETH, Polygon..." }, + { label: t.tokenPrice, value: "$0.02", sub: t.presalePrice }, + { label: t.listingTarget, value: "$0.10", sub: t.xPotential }, ].map((item) => (
- Cross-chain swaps are powered by Li.Fi protocol. Always verify transaction details before confirming. - Minimum slippage 3% applies. Not financial advice. + {t.disclaimer}
diff --git a/server/routers.ts b/server/routers.ts index b5407d5..21e62ae 100644 --- a/server/routers.ts +++ b/server/routers.ts @@ -51,6 +51,36 @@ const bridgeRouter = router({ } }), + // List orders by wallet address + myOrders: publicProcedure + .input(z.object({ + walletAddress: z.string().min(1).max(64), + limit: z.number().min(1).max(50).default(20), + })) + .query(async ({ input }) => { + const db = await getDb(); + if (!db) return []; + const rows = await db + .select() + .from(bridgeOrders) + .where(eq(bridgeOrders.walletAddress, input.walletAddress.toLowerCase())) + .orderBy(desc(bridgeOrders.createdAt)) + .limit(input.limit); + return rows.map(r => ({ + id: r.id, + txHash: r.txHash, + walletAddress: r.walletAddress, + fromChainId: r.fromChainId, + fromToken: r.fromToken, + fromAmount: Number(r.fromAmount), + toChainId: r.toChainId, + toToken: r.toToken, + toAmount: Number(r.toAmount), + status: r.status, + createdAt: r.createdAt, + })); + }), + // List recent bridge orders (public) recentOrders: publicProcedure .input(z.object({ limit: z.number().min(1).max(50).default(10) })) diff --git a/todo.md b/todo.md index e5d6dce..5456be6 100644 --- a/todo.md +++ b/todo.md @@ -89,3 +89,37 @@ - [x] 浏览器测试 /bridge 页面(UI渲染、链切换、金额输入正常) - [ ] 去除 MANUS 内联,构建并部署到 AI 服务器 - [ ] 记录部署日志并交付 + +## v10 Bridge完善 + 钱包选择器修复 + +### Bridge Li.Fi 交易执行逻辑 +- [x] 实现完整的 Li.Fi 跨链交易执行(USDT Approve + executeLiFiRoute) +- [x] 连接钱包后自动获取报价(已有,验证可用) +- [x] 执行交易:先 Approve USDT,再发送跨链交易 +- [x] 交易状态轮询(pending → success/failed) +- [x] 成功后记录订单到数据库 + +### Bridge 交易历史记录模块 +- [x] Bridge 页面底部增加"我的交易记录"区域 +- [x] 按钱包地址查询历史订单(trpc.bridge.myOrders) +- [x] 显示:时间、来源链、USDT金额、XIC金额、状态、TxHash链接 + +### v8 钱包选择器 UI 修复 +- [ ] 修复图1:有已安装钱包时隐藏未安装列表(已在代码中但需验证) +- [ ] 修复图2:连接中状态保持面板展开,显示loading,不折叠 +- [ ] 修复图3:未连接钱包时隐藏"添加XIC到钱包"按钮(已有条件判断,需验证) + +### 部署 +- [ ] 构建并部署到 AI 服务器 +- [ ] 浏览器测试验证所有功能 +- [ ] 记录部署日志并交付 + +### Bridge 钱包连接修复(来自截图反馈) +- [ ] Bridge 页面"连接钱包"按钮改为使用 WalletSelector 组件(与主页一致),而非直接调用 window.ethereum +- [x] 连接钱包后自动获取报价,不再显示 WalletSelector + +### 视频反馈修复(来自 WhatsApp 视频) +- [ ] Bridge 页面"连接钱包"按钮改为内嵌 WalletSelector 组件(展开显示钱包列表,不弹浏览器原生弹窗) +- [ ] 错误提示"Wallet connection cancelled"改为中英文双语 +- [ ] Bridge 页面添加中英文语言切换支持(与主页同步) +- [ ] 信息卡片"5岁以上"应为"5条以上"(支持链数量)