)}
- {/* Not-installed wallets — only show when NO wallet is installed (UI fix: don't mix installed+uninstalled) */}
- {!compact && notInstalledWallets.length > 0 && installedWallets.length === 0 && (
+ {/* Not-installed wallets — only show when NO wallet is installed and not connecting */}
+ {!connecting && !compact && notInstalledWallets.length > 0 && installedWallets.length === 0 && (
{lang === "zh" ? "未安装(点击安装)" : "Not installed (click to install)"}
@@ -765,7 +785,7 @@ export function WalletSelector({ lang, onAddressDetected, connectedAddress, comp
)}
{/* In compact mode, show install links inline */}
- {compact && notInstalledWallets.length > 0 && installedWallets.length === 0 && (
+ {!connecting && compact && notInstalledWallets.length > 0 && installedWallets.length === 0 && (
+ )}
);
}
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 ? (
-
{/* ── TX Success ── */}
- {txHash && (
+ {txHash && execStep === "done" && (
)}
+ {/* ── Transaction History ── */}
+
+
setShowHistory(!showHistory)}
+ className="w-full flex items-center justify-between px-5 py-4 text-left hover:bg-white/5 transition-colors"
+ >
+
+
+ {t.history}
+ {wallet.address && myOrdersQuery.data && myOrdersQuery.data.length > 0 && (
+
+ {myOrdersQuery.data.length}
+
+ )}
+
+ {showHistory ? (
+
+ ) : (
+
+ )}
+
+
+ {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条以上"(支持链数量)