diff --git a/.manus/db/db-query-1773136418178.json b/.manus/db/db-query-1773136418178.json new file mode 100644 index 0000000..8e31d76 --- /dev/null +++ b/.manus/db/db-query-1773136418178.json @@ -0,0 +1,74 @@ +{ + "query": "ALTER TABLE bridge_intents MODIFY COLUMN senderAddress VARCHAR(64) NULL;\nDESCRIBE bridge_intents;", + "command": "mysql --batch --raw --column-names --default-character-set=utf8mb4 --host gateway03.us-east-1.prod.aws.tidbcloud.com --port 4000 --user 3Bq4cgN2KNKQqNu.8160cd2033e0 --database Ngki3MumDNGduV3xJt3mga --execute ALTER TABLE bridge_intents MODIFY COLUMN senderAddress VARCHAR(64) NULL;\nDESCRIBE bridge_intents;", + "rows": [ + { + "Field": "id", + "Type": "int", + "Null": "NO", + "Key": "PRI", + "Default": "NULL", + "Extra": "auto_increment" + }, + { + "Field": "fromChainId", + "Type": "int", + "Null": "NO", + "Key": "", + "Default": "NULL", + "Extra": "" + }, + { + "Field": "senderAddress", + "Type": "varchar(64)", + "Null": "YES", + "Key": "", + "Default": "NULL", + "Extra": "" + }, + { + "Field": "xicReceiveAddress", + "Type": "varchar(64)", + "Null": "NO", + "Key": "", + "Default": "NULL", + "Extra": "" + }, + { + "Field": "expectedUsdt", + "Type": "decimal(20,6)", + "Null": "YES", + "Key": "", + "Default": "NULL", + "Extra": "" + }, + { + "Field": "matched", + "Type": "tinyint(1)", + "Null": "NO", + "Key": "", + "Default": "0", + "Extra": "" + }, + { + "Field": "matchedOrderId", + "Type": "int", + "Null": "YES", + "Key": "", + "Default": "NULL", + "Extra": "" + }, + { + "Field": "createdAt", + "Type": "timestamp", + "Null": "NO", + "Key": "", + "Default": "CURRENT_TIMESTAMP", + "Extra": "" + } + ], + "messages": [], + "stdout": "Field\tType\tNull\tKey\tDefault\tExtra\nid\tint\tNO\tPRI\tNULL\tauto_increment\nfromChainId\tint\tNO\t\tNULL\t\nsenderAddress\tvarchar(64)\tYES\t\tNULL\t\nxicReceiveAddress\tvarchar(64)\tNO\t\tNULL\t\nexpectedUsdt\tdecimal(20,6)\tYES\t\tNULL\t\nmatched\ttinyint(1)\tNO\t\t0\t\nmatchedOrderId\tint\tYES\t\tNULL\t\ncreatedAt\ttimestamp\tNO\t\tCURRENT_TIMESTAMP\t\n", + "stderr": "", + "execution_time_ms": 1992 +} \ No newline at end of file diff --git a/client/src/lib/contracts.ts b/client/src/lib/contracts.ts index bffda2a..c88e0c6 100644 --- a/client/src/lib/contracts.ts +++ b/client/src/lib/contracts.ts @@ -24,7 +24,7 @@ export const CONTRACTS = { rpcUrl: "https://eth.llamarpc.com", explorerUrl: "https://etherscan.io", nativeCurrency: { name: "Ether", symbol: "ETH", decimals: 18 }, - presale: "0x85AB2F2d9f7ca7ecB272b5E8726c70f3fd45D1E3", + presale: "0x43DAb577f3279e11D311E7d628C6201d893A9Aa3", token: "", // XIC not yet on ETH usdt: "0xdAC17F958D2ee523a2206206994597C13D831ec7", }, @@ -37,7 +37,7 @@ export const CONTRACTS = { token: "", usdt: "TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t", // Receiving wallet for TRC20 USDT - receivingWallet: "TYASr5UV6HEcXatwdFyffSGZszd6Gkjkvb", + receivingWallet: "TWc2ugYBFN5aSoimAh4qGt9oMyket6NYZp", }, } as const; diff --git a/client/src/pages/Bridge.tsx b/client/src/pages/Bridge.tsx index 3987b5d..dd6a0ae 100644 --- a/client/src/pages/Bridge.tsx +++ b/client/src/pages/Bridge.tsx @@ -1,678 +1,440 @@ -// NAC Cross-Chain Bridge — Buy XIC with USDT from any chain -// v2: Integrated WalletSelector (same as Home page), full Li.Fi execution, TX history, bilingual -// Uses Li.Fi API for cross-chain routing +// NAC Cross-Chain Bridge — Self-Developed +// v3: NAC native bridge, no third-party protocols +// User sends USDT on any chain to our receiving address → backend monitors → XIC distributed // Supports: BSC, ETH, Polygon, Arbitrum, Avalanche -import { useState, useEffect, useCallback, useMemo } from "react"; + +import { useState, useEffect, useCallback } from "react"; import { toast } from "sonner"; import { trpc } from "@/lib/trpc"; -import { ArrowDown, ArrowLeft, ExternalLink, Loader2, RefreshCw, Zap, History, ChevronDown, ChevronUp, Copy, CheckCheck, X } from "lucide-react"; +import { + ArrowDown, ArrowLeft, Copy, CheckCheck, ExternalLink, + Loader2, RefreshCw, History, ChevronDown, ChevronUp, + Wallet, AlertCircle, CheckCircle2, Clock, XCircle +} 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", + title: "从任意链购买 XIC", subtitle: "使用 BSC、ETH、Polygon、Arbitrum 或 Avalanche 上的 USDT 购买 XIC 代币", - fromChain: "来源链", + 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 上查看", + youReceive: "您将获得 (XIC)", + xicReceiveAddr: "XIC 接收地址(BSC 链)", + xicReceiveAddrHint: "输入您的 BSC 钱包地址,XIC 代币将发送到此地址", + xicReceiveAddrPlaceholder: "0x... BSC 地址", + sendTo: "发送 USDT 到以下地址", + sendToHint: "请将 USDT 发送到以下地址,我们将在确认后向您的 XIC 接收地址分发代币", + copyAddress: "复制地址", + copied: "已复制!", + confirmSend: "我已发送 USDT", + confirmSendHint: "点击确认后,我们将监控您的转账并在确认后分发 XIC", + registering: "注册中...", + registered: "已登记!我们正在监控您的转账", + registeredDesc: "转账确认后(通常 1-5 分钟),XIC 代币将自动分发到您的接收地址", + connectWallet: "连接钱包", connected: "已连接", - supportedChains: "支持链数", + walletConnected: "钱包已连接,地址已自动填入", + history: "我的交易记录", + noHistory: "暂无交易记录", + historyDesc: "输入钱包地址后可查看历史记录", + status_pending: "等待转账", + status_confirmed: "已确认", + status_distributed: "已分发", + status_failed: "失败", + backToPresale: "返回预售", tokenPrice: "代币价格", listingTarget: "目标上市", + supportedChains: "支持链数", 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: "执行跨链交换", + disclaimer: "本跨链桥为 NAC 自研系统。请确保发送正确金额的 USDT 到指定地址。最小转账金额:$10 USDT。", + gasNote: "Gas 费用由 {symbol}({chain} 原生代币)支付,请确保钱包中有足够的 {symbol}", + minAmount: "最小转账金额:$10 USDT", + calcXic: "按 $0.02/XIC 计算", + step1: "第一步:选择来源链并输入金额", + step2: "第二步:填写 XIC 接收地址", + step3: "第三步:发送 USDT 到指定地址", + step4: "第四步:等待确认和分发", lang: "EN", - estGasNative: "Gas费(用{symbol}支付)", - estTime: "预计到账时间", - gasNote: "Gas 费用{symbol}({chain}原生代币)支付,请确保钱包中有足够的{symbol}", + queryHistory: "查询记录", + enterAddrToQuery: "输入钱包地址查询", + refreshHistory: "刷新", + viewExplorer: "查看区块浏览器", copyHash: "复制哈希", - copied: "已复制!", - viewExplorer: "在区块浏览器中查看", - txSuccessModal: "交易成功", - txSuccessDesc: "您的跨链交易已提交,XIC 代币将在以下时间内到账:", - viewDetails: "查看交易详情", - close: "关闭", - estimatedArrival: "预计到账", - gasPayWith: "Gas 支付方式", + txHash: "交易哈希", + chainLabel: "来源链", + amountLabel: "金额", + timeLabel: "时间", + addressRequired: "请填写 XIC 接收地址", + invalidAddress: "XIC 接收地址格式不正确(需要 0x 开头的 BSC 地址)", + amountRequired: "请输入 USDT 金额", + amountMin: "最小金额为 $10 USDT", + intentSuccess: "转账意图已登记!请立即发送 USDT", + intentFailed: "登记失败,请重试", }, en: { title: "Buy XIC from Any Chain", subtitle: "Use USDT on BSC, ETH, Polygon, Arbitrum or Avalanche to buy XIC tokens", - fromChain: "From Chain", + fromChain: "Select Source 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", + youReceive: "You Receive (XIC)", + xicReceiveAddr: "XIC Receive Address (BSC Chain)", + xicReceiveAddrHint: "Enter your BSC wallet address. XIC tokens will be sent here.", + xicReceiveAddrPlaceholder: "0x... BSC address", + sendTo: "Send USDT to This Address", + sendToHint: "Send USDT to the address below. We will distribute XIC tokens to your receive address after confirmation.", + copyAddress: "Copy Address", + copied: "Copied!", + confirmSend: "I Have Sent USDT", + confirmSendHint: "After clicking, we will monitor your transfer and distribute XIC upon confirmation", + registering: "Registering...", + registered: "Registered! We are monitoring your transfer", + registeredDesc: "After transfer confirmation (usually 1-5 minutes), XIC tokens will be automatically distributed to your receive address", + connectWallet: "Connect Wallet", connected: "Connected", - supportedChains: "Supported Chains", + walletConnected: "Wallet connected, address auto-filled", + history: "My Transactions", + noHistory: "No transactions yet", + historyDesc: "Enter your wallet address to view history", + status_pending: "Awaiting Transfer", + status_confirmed: "Confirmed", + status_distributed: "Distributed", + status_failed: "Failed", + backToPresale: "Back to Presale", tokenPrice: "Token Price", listingTarget: "Listing Target", + supportedChains: "Supported Chains", 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: "中文", - estGasNative: "Gas Fee (paid in {symbol})", - estTime: "Estimated Arrival", + disclaimer: "This bridge is developed by NAC. Please ensure you send the correct USDT amount to the specified address. Minimum transfer: $10 USDT.", gasNote: "Gas fee is paid in {symbol} ({chain} native token). Ensure you have enough {symbol} in your wallet.", + minAmount: "Minimum: $10 USDT", + calcXic: "Calculated at $0.02/XIC", + step1: "Step 1: Select chain and enter amount", + step2: "Step 2: Enter XIC receive address", + step3: "Step 3: Send USDT to the address", + step4: "Step 4: Wait for confirmation", + lang: "中文", + queryHistory: "Query", + enterAddrToQuery: "Enter wallet address to query", + refreshHistory: "Refresh", + viewExplorer: "View Explorer", 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", + txHash: "TX Hash", + chainLabel: "Chain", + amountLabel: "Amount", + timeLabel: "Time", + addressRequired: "Please enter XIC receive address", + invalidAddress: "Invalid XIC receive address (must be a 0x BSC address)", + amountRequired: "Please enter USDT amount", + amountMin: "Minimum amount is $10 USDT", + intentSuccess: "Intent registered! Please send USDT now", + intentFailed: "Registration failed, please try again", }, }; // ─── Chain Config ───────────────────────────────────────────────────────────── const CHAINS = [ - { 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" }, + { + chainId: 56, + name: "BSC", + shortName: "BSC", + symbol: "BNB", + icon: "🟡", + color: "#F0B90B", + receivingAddress: "0x43DAb577f3279e11D311E7d628C6201d893A9Aa3", + explorerUrl: "https://bscscan.com", + usdtDecimals: 18, + gasNote: "BNB", + }, + { + chainId: 1, + name: "Ethereum", + shortName: "ETH", + symbol: "ETH", + icon: "🔵", + color: "#627EEA", + receivingAddress: "0x43DAb577f3279e11D311E7d628C6201d893A9Aa3", + explorerUrl: "https://etherscan.io", + usdtDecimals: 6, + gasNote: "ETH", + }, + { + chainId: 137, + name: "Polygon", + shortName: "POLY", + symbol: "MATIC", + icon: "🟣", + color: "#8247E5", + receivingAddress: "0x43DAb577f3279e11D311E7d628C6201d893A9Aa3", + explorerUrl: "https://polygonscan.com", + usdtDecimals: 6, + gasNote: "MATIC", + }, + { + chainId: 42161, + name: "Arbitrum", + shortName: "ARB", + symbol: "ETH", + icon: "🔷", + color: "#28A0F0", + receivingAddress: "0x43DAb577f3279e11D311E7d628C6201d893A9Aa3", + explorerUrl: "https://arbiscan.io", + usdtDecimals: 6, + gasNote: "ETH", + }, + { + chainId: 43114, + name: "Avalanche", + shortName: "AVAX", + symbol: "AVAX", + icon: "🔴", + color: "#E84142", + receivingAddress: "0x43DAb577f3279e11D311E7d628C6201d893A9Aa3", + explorerUrl: "https://snowtrace.io", + usdtDecimals: 6, + gasNote: "AVAX", + }, ]; -// XIC Token on BSC (destination) -const XIC_TOKEN = { - chainId: 56, - address: "0x59FF34dD59680a7125782b1f6df2A86ed46F5A24", - symbol: "XIC", - name: "New AssetChain", - decimals: 18, - logoURI: "https://d2xsxph8kpxj0f.cloudfront.net/310519663287655625/Ngki3MumDNGduV3xJt3mga/nac-token-icon_382e5c30.png", -}; - -// Quick amounts +const XIC_PRICE = 0.02; // $0.02 per XIC 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; - fromAmount: string; - toAmount: string; - toAmountMin: string; - estimatedGas: string; - transactionRequest?: { - to: string; - data: string; - value: string; - gasLimit?: string; - gasPrice?: string; - }; - steps: Array<{ - 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 ──────────────────────────────────────────────────────── -const LIFI_API = "https://li.quest/v1"; - -async function getRoutes( - fromChainId: number, - fromTokenAddress: string, - toChainId: number, - toTokenAddress: string, - fromAmount: string, - fromAddress: string -): Promise { - const params = new URLSearchParams({ - fromChainId: String(fromChainId), - fromTokenAddress, - toChainId: String(toChainId), - toTokenAddress, - fromAmount, - 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] : []; -} - -// 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)", -]; - -// ─── Chain name helper ──────────────────────────────────────────────────────── -function getChainName(chainId: number): string { - const chain = CHAINS.find(c => c.id === chainId); - return chain?.name ?? `Chain ${chainId}`; -} - -// ─── 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}`; -} - -// ─── 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 Component ─────────────────────────────────────────────────────────── export default function Bridge() { - // ── Language ── - const [lang, setLang] = useState(() => { - const bl = navigator.language.toLowerCase(); - return bl.startsWith("zh") ? "zh" : "en"; - }); + const [lang, setLang] = useState("en"); const t = T[lang]; - // ── Wallet (shared hook with Home page) ── + const [selectedChainIdx, setSelectedChainIdx] = useState(0); + const selectedChain = CHAINS[selectedChainIdx]; + + const [usdtAmount, setUsdtAmount] = useState("100"); + const [xicReceiveAddress, setXicReceiveAddress] = useState(""); + const [copied, setCopied] = useState(false); + const [registered, setRegistered] = useState(false); + const [registering, setRegistering] = useState(false); + + // History + const [showHistory, setShowHistory] = useState(false); + const [historyAddress, setHistoryAddress] = useState(""); + const [queryAddress, setQueryAddress] = useState(""); + const [copiedHash, setCopiedHash] = useState(null); + + // Wallet 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); - const [showSuccessModal, setShowSuccessModal] = useState(false); - const [copiedHash, setCopiedHash] = useState(null); - - // ── 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 ── + // Auto-fill XIC receive address from connected wallet useEffect(() => { - if (wallet.address) { - setShowWalletSelector(false); + if (wallet.address && !xicReceiveAddress) { + setXicReceiveAddress(wallet.address); } }, [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 { - const amountWei = BigInt(Math.floor(amount * 1e6)).toString(); // USDT 6 decimals - const routes = await getRoutes( - fromChain.id, - fromChain.usdtAddress, - XIC_TOKEN.chainId, - XIC_TOKEN.address, - amountWei, - wallet.address - ); - if (routes.length > 0) { - setQuote(routes[0]); - } else { - toast.error(t.noRoute); - } - } catch (err) { - toast.error(t.quoteFailed); - console.error(err); - } finally { - setQuoting(false); - } - }, [fromChain, usdtAmount, wallet.address, t]); + // tRPC: register bridge intent + const registerIntent = trpc.bridge.registerIntent.useMutation(); - // 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]); + // tRPC: query orders by wallet address + const { data: orders, isLoading: ordersLoading, refetch: refetchOrders } = trpc.bridge.myOrders.useQuery( + { walletAddress: queryAddress }, + { enabled: !!queryAddress && queryAddress.length > 10 } + ); - // Re-fetch quote when chain changes - useEffect(() => { - setQuote(null); - }, [fromChain]); + // Calculated XIC amount + const xicAmount = usdtAmount && !isNaN(Number(usdtAmount)) && Number(usdtAmount) > 0 + ? (Number(usdtAmount) / XIC_PRICE).toLocaleString(undefined, { maximumFractionDigits: 0 }) + : "—"; - // ─── Execute swap (full Li.Fi flow) ────────────────────────────────────── - const executeSwap = useCallback(async () => { - if (!quote || !wallet.address || !wallet.signer) { - toast.error(lang === "zh" ? "请先连接钱包" : "Please connect wallet first"); + // Copy receiving address + const copyReceivingAddress = useCallback(() => { + navigator.clipboard.writeText(selectedChain.receivingAddress); + setCopied(true); + toast.success(t.copied); + setTimeout(() => setCopied(false), 2000); + }, [selectedChain.receivingAddress, t.copied]); + + // Copy tx hash + const copyHash = useCallback((hash: string) => { + navigator.clipboard.writeText(hash); + setCopiedHash(hash); + setTimeout(() => setCopiedHash(null), 2000); + }, []); + + // Validate and register intent + const handleConfirmSend = async () => { + const amount = Number(usdtAmount); + if (!usdtAmount || isNaN(amount) || amount <= 0) { + toast.error(t.amountRequired); return; } - if (wallet.chainId !== fromChain.id) { - try { - await switchToNetwork(fromChain.id); - } catch { - toast.error(lang === "zh" ? `请切换到 ${fromChain.name} 网络` : `Please switch to ${fromChain.name} network`); - return; - } + if (amount < 10) { + toast.error(t.amountMin); + return; + } + if (!xicReceiveAddress) { + toast.error(t.addressRequired); + return; + } + if (!/^0x[0-9a-fA-F]{40}$/.test(xicReceiveAddress)) { + toast.error(t.invalidAddress); + return; } - setExecuting(true); - setExecStep("idle"); - + setRegistering(true); try { - 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)); - - // 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"); - } - - // Step 2: Execute the Li.Fi transaction - setExecStep("swapping"); - toast.info(lang === "zh" ? "请在钱包中确认跨链交易..." : "Please confirm the cross-chain transaction in your wallet..."); - - 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); - setShowSuccessModal(true); - - // 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); - setShowSuccessModal(true); - - // 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"); + await registerIntent.mutateAsync({ + fromChainId: selectedChain.chainId, + senderAddress: wallet.address || undefined, // only set if wallet connected + xicReceiveAddress, + expectedUsdt: amount, + }); + setRegistered(true); + toast.success(t.intentSuccess); + // Auto-query history + setQueryAddress(xicReceiveAddress); + setHistoryAddress(xicReceiveAddress); + } catch (err: any) { + toast.error(t.intentFailed + ": " + (err?.message || "")); } finally { - setExecuting(false); + setRegistering(false); } - }, [quote, wallet, fromChain, usdtAmount, t, lang, recordOrder, showHistory, myOrdersQuery]); + }; - const xicAmount = useMemo(() => quote - ? (parseFloat(quote.toAmount) / 1e18).toLocaleString(undefined, { maximumFractionDigits: 2 }) - : "—", [quote]); + // Query history + const handleQueryHistory = () => { + if (!historyAddress || historyAddress.length < 10) return; + setQueryAddress(historyAddress); + setShowHistory(true); + }; - 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]); - - // ── 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); - } - }, []); + // Status badge + const StatusBadge = ({ status }: { status: string }) => { + const configs: Record = { + pending: { bg: "rgba(240,180,41,0.12)", color: "#f0b429", icon: , label: t.status_pending }, + confirmed: { bg: "rgba(0,212,255,0.12)", color: "#00d4ff", icon: , label: t.status_confirmed }, + distributed: { bg: "rgba(0,230,118,0.12)", color: "#00e676", icon: , label: t.status_distributed }, + failed: { bg: "rgba(255,80,80,0.12)", color: "#ff5050", icon: , label: t.status_failed }, + }; + const cfg = configs[status] || configs.pending; + return ( + + {cfg.icon} + {cfg.label} + + ); + }; return (
- {/* ── Header ── */} + {/* ─── Header ─────────────────────────────────────────────────────────── */}
- - - {t.backToPresale} + + + + {t.backToPresale} +
- - - NAC Cross-Chain Bridge - -
-
- {/* Language toggle */} - -
- Powered by Li.Fi -
+ ⚡ NAC Cross-Chain Bridge
+
- {/* ── Main Content ── */} -
- {/* Title */} -
-

+
+ {/* ─── Title ──────────────────────────────────────────────────────── */} +
+

{t.title}

{t.subtitle}

- {completedTxs > 0 && ( -
- ✓ {completedTxs} {t.transactions}{completedTxs > 1 && lang === "en" ? "s" : ""} {t.completedSession} -
- )}
- {/* ── Bridge Card ── */} + {/* ─── Main Card ──────────────────────────────────────────────────── */}
- {/* FROM: Chain Selector */} -
- -
- {CHAINS.map((chain) => ( + {/* Step 1: Select chain */} +
+ +
+ {CHAINS.map((chain, idx) => ( ))}
+ {/* Gas note */} +

+ {t.gasNote + .replace("{symbol}", selectedChain.gasNote) + .replace("{chain}", selectedChain.name) + .replace("{symbol}", selectedChain.gasNote)} +

- {/* FROM: USDT Amount */} -
- + {/* Step 2: Amount */} +
+
setUsdtAmount(e.target.value)} - placeholder="0.00" - min="1" + onChange={e => { setUsdtAmount(e.target.value); setRegistered(false); }} className="flex-1 bg-transparent text-xl font-bold text-white outline-none" + placeholder="100" style={{ fontFamily: "'JetBrains Mono', monospace" }} /> -
-
- ₮ -
- USDT -
+ USDT
{/* Quick amounts */} -
- {QUICK_AMOUNTS.map((amt) => ( +
+ {QUICK_AMOUNTS.map(amt => ( ))}
+

{t.minAmount}

{/* Arrow */} -
+
- +
- {/* TO: XIC */} -
- + {/* You receive */} +
+
-
- {quoting ? ( -
- - {t.gettingRoute} -
- ) : ( - - {xicAmount} - - )} - {quote && !quoting && ( -
- {t.minReceive}: {xicMin} XIC · {t.via} {quote.steps[0]?.toolDetails?.name ?? quote.steps[0]?.tool} -
- )} -
-
- XIC { (e.target as HTMLImageElement).style.display = "none"; }} - /> - XIC -
-
-
- - {/* 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.route} - {fromChain.name} USDT → BSC XIC -
-
- {t.protocol} - - {quote.steps.map((s) => s.toolDetails?.name ?? s.tool).join(" → ")} - -
+ {xicAmount} + + XIC +
+

{t.calcXic}

+
- {/* 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% -
-
- ); - })()} - - {/* ── Wallet Selector (inline, not a popup) ── */} - {!wallet.address && ( -
- {!showWalletSelector ? ( + {/* Step 3: XIC receive address */} +
+
+ + {!wallet.address && ( - ) : ( -
- - -
+ )} + {wallet.address && ( + + + {t.walletConnected} + )}
- )} - - {/* ── Connected: Switch Network or Execute ── */} - {wallet.address && wallet.chainId !== fromChain.id && ( - - )} - - {wallet.address && wallet.chainId === fromChain.id && ( -
- - -
- )} - - {/* Connected wallet info */} - {wallet.address && ( -
- {t.connected}: -
- - {wallet.address.slice(0, 6)}...{wallet.address.slice(-4)} - - -
-
- )} -
- - {/* ── TX Success ── */} - {txHash && execStep === "done" && ( -
-
✓ {t.txSubmitted}
-
{t.txDesc}
- - {t.viewBscscan} - + /> +

{t.xicReceiveAddrHint}

- )} - {/* ── Transaction History ── */} -
- +

{t.sendToHint}

+
+ + {selectedChain.receivingAddress} + + +
+
- {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()} -
-
- ))} -
- )} + {/* Confirm button */} + {!registered ? ( + + ) : ( +
+
+ + {t.registered} +
+

{t.registeredDesc}

)} + +

{t.confirmSendHint}

- {/* ── Info Cards ── */} + {/* ─── Info Cards ─────────────────────────────────────────────────── */}
{[ { 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) => ( + ].map((card, i) => (
-
- {item.value} +
+ {card.value}
-
{item.label}
-
{item.sub}
+
{card.label}
+
{card.sub}
))}
- {/* ── Disclaimer ── */} + {/* ─── Transaction History ─────────────────────────────────────────── */}
- {t.disclaimer} -
-

- - {/* ── Transaction Success Modal ── */} - {showSuccessModal && txHash && ( -
{ if (e.target === e.currentTarget) setShowSuccessModal(false); }} - > -
setShowHistory(!showHistory)} + className="w-full flex items-center justify-between px-5 py-4 transition-colors hover:bg-white/5" + style={{ background: "rgba(255,255,255,0.03)" }} > - {/* Close button */} -
-
-
- -
- - {t.txSuccessModal} - -
- +
+ + {t.history}
+ {showHistory ? : } + - {/* Description */} -

{t.txSuccessDesc}

- - {/* Estimated arrival */} - {quote && getEstimatedDuration(quote) > 0 && ( -
- {t.estimatedArrival} - {formatDuration(getEstimatedDuration(quote))} -
- )} - - {/* TX Hash */} -
-
{t.txHash}
-
- - {txHash} - -
- {/* Action buttons */} + {showHistory && ( +
+ {/* Query input */}
+ setHistoryAddress(e.target.value)} + placeholder={t.enterAddrToQuery} + className="flex-1 rounded-lg px-3 py-2 text-xs text-white outline-none" + style={{ + background: "rgba(255,255,255,0.06)", + border: "1px solid rgba(255,255,255,0.12)", + fontFamily: "'JetBrains Mono', monospace", + }} + /> - - - {t.viewDetails} - + {queryAddress && ( + + )}
-
- {/* Close */} - -
+ {/* Orders list */} + {!queryAddress && ( +

{t.historyDesc}

+ )} + {queryAddress && ordersLoading && ( +
+ +
+ )} + {queryAddress && !ordersLoading && orders && orders.length === 0 && ( +

{t.noHistory}

+ )} + {queryAddress && !ordersLoading && orders && orders.length > 0 && ( +
+ {orders.map(order => { + const chain = CHAINS.find(c => c.chainId === order.fromChainId); + const isIntent = order.type === 'intent'; + // Safe field access for both intent and order types + const usdtAmt = isIntent + ? (order as { type: 'intent'; expectedUsdt: number | null }).expectedUsdt + : (order as { type: 'order'; fromAmount: number }).fromAmount; + const xicAmt = usdtAmt ? Number(usdtAmt) / XIC_PRICE : null; + const txHash = !isIntent + ? (order as { type: 'order'; txHash?: string }).txHash + : undefined; + return ( +
+
+ + {chain?.icon} {chain?.name ?? `Chain ${order.fromChainId}`} → BSC + + +
+
+ + {usdtAmt ? `${Number(usdtAmt).toLocaleString()} USDT` : 'Pending USDT'} + + + {xicAmt ? xicAmt.toLocaleString(undefined, { maximumFractionDigits: 0 }) : '—'} XIC + + + + {new Date(order.createdAt!).toLocaleDateString()} + +
+ {txHash && ( +
+ + {txHash.slice(0, 20)}...{txHash.slice(-8)} + + + {chain && ( + + + + )} +
+ )} + {isIntent && ( +

+ ⏳ Monitoring your transfer on {chain?.name ?? `Chain ${order.fromChainId}`}... +

+ )} +
+ ); + })} +
+ )} +
+ )}
+ + {/* ─── Disclaimer ─────────────────────────────────────────────────── */} +

{t.disclaimer}

+
+ + {/* ─── Wallet Selector Modal ───────────────────────────────────────────── */} + {showWalletSelector && ( + { + setXicReceiveAddress(address); + setShowWalletSelector(false); + }} + connectedAddress={wallet.address || undefined} + /> )}
); diff --git a/deploy-logs/v13-deploy-log.md b/deploy-logs/v13-deploy-log.md new file mode 100644 index 0000000..1ef8b54 --- /dev/null +++ b/deploy-logs/v13-deploy-log.md @@ -0,0 +1,95 @@ +# v13 部署日志 + +**部署时间:** 2026-03-10 +**部署人员:** Manus AI +**部署服务器:** AI服务器 43.224.155.27:22000 +**部署目录:** /www/wwwroot/nac-presale-test/ +**PM2进程:** nac-presale-test (id=8) +**服务端口:** 3100 +**域名:** https://pre-sale.newassetchain.io + +--- + +## 本次更新内容 + +### 1. 自研跨链桥(完全移除Li.Fi第三方协议) + +**背景:** 之前Bridge页面使用Li.Fi协议,由于XIC代币尚未在任何DEX上市,Li.Fi无法获取报价,Bridge功能完全无法使用。 + +**解决方案:** 完全移除Li.Fi,改为NAC自研跨链桥: +- 用户选择源链(BSC/ETH/Polygon/Arbitrum/Avalanche) +- 系统显示对应链的官方USDT收款地址 +- 用户发送USDT后点击"I Have Sent USDT"注册意向 +- 后端监听器自动监控各链的USDT转入 +- 确认收款后按$0.02/XIC预售价分发XIC代币 + +### 2. 官方收款地址更新 + +| 网络 | 地址 | +|------|------| +| TRC20/USDT (TRON) | TWc2ugYBFN5aSoimAh4qGt9oMyket6NYZp | +| ERC20/USDT (Ethereum) | 0x43DAb577f3279e11D311E7d628C6201d893A9Aa3 | +| BEP20/USDT (BSC) | 0x43DAb577f3279e11D311E7d628C6201d893A9Aa3 | +| Polygon USDT | 0x43DAb577f3279e11D311E7d628C6201d893A9Aa3 | +| Arbitrum USDT | 0x43DAb577f3279e11D311E7d628C6201d893A9Aa3 | +| Avalanche USDT | 0x43DAb577f3279e11D311E7d628C6201d893A9Aa3 | + +### 3. 新增文件 + +- `server/bridgeMonitor.ts` - 多链USDT转入监听器(BSC/ETH/Polygon/Arbitrum/Avalanche) +- `server/_core/index.ts` - 添加Bridge监听器启动入口 + +### 4. 数据库变更 + +新增表: +- `bridge_intents` - 用户购买意向记录(注册后等待确认) +- `bridge_orders` - 已确认的跨链购买订单 + +修改表: +- `bridge_orders.status` - 添加 `distributed` 枚举值 + +--- + +## 测试结果 + +| 功能 | 状态 | 备注 | +|------|------|------| +| Bridge页面加载 | ✅ 正常 | | +| 5条链选择(BSC/ETH/POLY/ARB/AVAX) | ✅ 正常 | | +| Gas费说明随链切换 | ✅ 正常 | | +| 收款地址显示 | ✅ 正常 | 官方地址 | +| USDT金额输入 | ✅ 正常 | | +| XIC计算($0.02/XIC) | ✅ 正常 | 100 USDT = 5,000 XIC | +| 复制收款地址 | ✅ 正常 | | +| "I Have Sent USDT"提交 | ✅ 正常 | 注册意向到数据库 | +| 成功提示显示 | ✅ 正常 | 绿色成功框 | +| My Transactions查询 | ✅ 正常 | 显示待处理订单 | +| 主页TRC20收款地址 | ✅ 正常 | TWc2ugYBFN5aSoimAh4qGt9oMyket6NYZp | +| 主页链上数据 | ✅ 正常 | $9,900 Raised, 495K Tokens Sold | + +--- + +## 后台管理员信息 + +- **后台管理员账号:** 通过Manus OAuth登录 +- **数据库用户:** nac_presale / NACpresale2026! +- **数据库名:** nac_presale +- **MySQL连接:** 127.0.0.1:3306(仅本地) +- **宝塔面板:** http://43.224.155.27:12/btwest(cproot/vajngkvf) +- **PM2进程名:** nac-presale-test(id=8) + +--- + +## 待优化事项 + +1. Polygon/Arbitrum/Avalanche 链的 USDT 合约地址需要配置(目前使用通用ERC20地址) +2. 钱包连接功能(MetaMask)需要在有MetaMask扩展的浏览器中验证 +3. Bridge监听器的RPC节点可考虑使用付费节点提高稳定性 + +--- + +## Git同步 + +- **Gitea仓库:** https://git.newassetchain.io/nacadmin/xic-presale +- **分支:** main +- **提交信息:** v13: 自研跨链桥,移除Li.Fi,更新官方收款地址 diff --git a/drizzle/0006_colossal_unicorn.sql b/drizzle/0006_colossal_unicorn.sql new file mode 100644 index 0000000..215949d --- /dev/null +++ b/drizzle/0006_colossal_unicorn.sql @@ -0,0 +1,21 @@ +CREATE TABLE `bridge_intents` ( + `id` int AUTO_INCREMENT NOT NULL, + `fromChainId` int NOT NULL, + `senderAddress` varchar(64) NOT NULL, + `xicReceiveAddress` varchar(64) NOT NULL, + `expectedUsdt` decimal(20,6), + `matched` boolean NOT NULL DEFAULT false, + `matchedOrderId` int, + `createdAt` timestamp NOT NULL DEFAULT (now()), + CONSTRAINT `bridge_intents_id` PRIMARY KEY(`id`) +); +--> statement-breakpoint +ALTER TABLE `bridge_orders` MODIFY COLUMN `toChainId` int NOT NULL DEFAULT 56;--> statement-breakpoint +ALTER TABLE `bridge_orders` MODIFY COLUMN `toToken` varchar(32) NOT NULL DEFAULT 'XIC';--> statement-breakpoint +ALTER TABLE `bridge_orders` MODIFY COLUMN `status` enum('pending','confirmed','distributed','failed') NOT NULL DEFAULT 'pending';--> statement-breakpoint +ALTER TABLE `bridge_orders` ADD `xicReceiveAddress` varchar(64);--> statement-breakpoint +ALTER TABLE `bridge_orders` ADD `confirmedAt` timestamp;--> statement-breakpoint +ALTER TABLE `bridge_orders` ADD `distributedAt` timestamp;--> statement-breakpoint +ALTER TABLE `bridge_orders` ADD `distributeTxHash` varchar(128);--> statement-breakpoint +ALTER TABLE `bridge_orders` ADD `blockNumber` bigint;--> statement-breakpoint +ALTER TABLE `bridge_orders` ADD `updatedAt` timestamp DEFAULT (now()) NOT NULL ON UPDATE CURRENT_TIMESTAMP; \ No newline at end of file diff --git a/drizzle/0007_wide_menace.sql b/drizzle/0007_wide_menace.sql new file mode 100644 index 0000000..12baf16 --- /dev/null +++ b/drizzle/0007_wide_menace.sql @@ -0,0 +1 @@ +ALTER TABLE `bridge_intents` MODIFY COLUMN `senderAddress` varchar(64); \ No newline at end of file diff --git a/drizzle/meta/0006_snapshot.json b/drizzle/meta/0006_snapshot.json new file mode 100644 index 0000000..3856a0a --- /dev/null +++ b/drizzle/meta/0006_snapshot.json @@ -0,0 +1,653 @@ +{ + "version": "5", + "dialect": "mysql", + "id": "b1822efc-d652-4957-970f-f71b733359b6", + "prevId": "f2da11d5-2ee3-40ce-9180-11a9480a5b91", + "tables": { + "bridge_intents": { + "name": "bridge_intents", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "fromChainId": { + "name": "fromChainId", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "senderAddress": { + "name": "senderAddress", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "xicReceiveAddress": { + "name": "xicReceiveAddress", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "expectedUsdt": { + "name": "expectedUsdt", + "type": "decimal(20,6)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "matched": { + "name": "matched", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "matchedOrderId": { + "name": "matchedOrderId", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(now())" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": { + "bridge_intents_id": { + "name": "bridge_intents_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "bridge_orders": { + "name": "bridge_orders", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "txHash": { + "name": "txHash", + "type": "varchar(128)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "walletAddress": { + "name": "walletAddress", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "fromChainId": { + "name": "fromChainId", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "fromToken": { + "name": "fromToken", + "type": "varchar(32)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "fromAmount": { + "name": "fromAmount", + "type": "decimal(30,6)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "toChainId": { + "name": "toChainId", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 56 + }, + "toToken": { + "name": "toToken", + "type": "varchar(32)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'XIC'" + }, + "toAmount": { + "name": "toAmount", + "type": "decimal(30,6)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "xicReceiveAddress": { + "name": "xicReceiveAddress", + "type": "varchar(64)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "enum('pending','confirmed','distributed','failed')", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'pending'" + }, + "confirmedAt": { + "name": "confirmedAt", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "distributedAt": { + "name": "distributedAt", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "distributeTxHash": { + "name": "distributeTxHash", + "type": "varchar(128)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "blockNumber": { + "name": "blockNumber", + "type": "bigint", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(now())" + }, + "updatedAt": { + "name": "updatedAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "onUpdate": true, + "default": "(now())" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": { + "bridge_orders_id": { + "name": "bridge_orders_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": { + "bridge_orders_txHash_unique": { + "name": "bridge_orders_txHash_unique", + "columns": [ + "txHash" + ] + } + }, + "checkConstraint": {} + }, + "presale_config": { + "name": "presale_config", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "key": { + "name": "key", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "label": { + "name": "label", + "type": "varchar(128)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "varchar(256)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "type": { + "name": "type", + "type": "varchar(32)", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "'text'" + }, + "updatedAt": { + "name": "updatedAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "onUpdate": true, + "default": "(now())" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": { + "presale_config_id": { + "name": "presale_config_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": { + "presale_config_key_unique": { + "name": "presale_config_key_unique", + "columns": [ + "key" + ] + } + }, + "checkConstraint": {} + }, + "presale_stats_cache": { + "name": "presale_stats_cache", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "chain": { + "name": "chain", + "type": "varchar(16)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "usdtRaised": { + "name": "usdtRaised", + "type": "decimal(30,6)", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "'0'" + }, + "tokensSold": { + "name": "tokensSold", + "type": "decimal(30,6)", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "'0'" + }, + "weiRaised": { + "name": "weiRaised", + "type": "decimal(30,6)", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "'0'" + }, + "lastUpdated": { + "name": "lastUpdated", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(now())" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": { + "presale_stats_cache_id": { + "name": "presale_stats_cache_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "trc20_intents": { + "name": "trc20_intents", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "tronAddress": { + "name": "tronAddress", + "type": "varchar(64)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "evmAddress": { + "name": "evmAddress", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "expectedUsdt": { + "name": "expectedUsdt", + "type": "decimal(20,6)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "matched": { + "name": "matched", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "matchedPurchaseId": { + "name": "matchedPurchaseId", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(now())" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": { + "trc20_intents_id": { + "name": "trc20_intents_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "trc20_purchases": { + "name": "trc20_purchases", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "txHash": { + "name": "txHash", + "type": "varchar(128)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "fromAddress": { + "name": "fromAddress", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "usdtAmount": { + "name": "usdtAmount", + "type": "decimal(20,6)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "xicAmount": { + "name": "xicAmount", + "type": "decimal(30,6)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "blockNumber": { + "name": "blockNumber", + "type": "bigint", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "enum('pending','confirmed','distributed','failed')", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'pending'" + }, + "distributedAt": { + "name": "distributedAt", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "distributeTxHash": { + "name": "distributeTxHash", + "type": "varchar(128)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "evmAddress": { + "name": "evmAddress", + "type": "varchar(64)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(now())" + }, + "updatedAt": { + "name": "updatedAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "onUpdate": true, + "default": "(now())" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": { + "trc20_purchases_id": { + "name": "trc20_purchases_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": { + "trc20_purchases_txHash_unique": { + "name": "trc20_purchases_txHash_unique", + "columns": [ + "txHash" + ] + } + }, + "checkConstraint": {} + }, + "users": { + "name": "users", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "openId": { + "name": "openId", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "email": { + "name": "email", + "type": "varchar(320)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "loginMethod": { + "name": "loginMethod", + "type": "varchar(64)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "role": { + "name": "role", + "type": "enum('user','admin')", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'user'" + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(now())" + }, + "updatedAt": { + "name": "updatedAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "onUpdate": true, + "default": "(now())" + }, + "lastSignedIn": { + "name": "lastSignedIn", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(now())" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": { + "users_id": { + "name": "users_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": { + "users_openId_unique": { + "name": "users_openId_unique", + "columns": [ + "openId" + ] + } + }, + "checkConstraint": {} + } + }, + "views": {}, + "_meta": { + "schemas": {}, + "tables": {}, + "columns": {} + }, + "internal": { + "tables": {}, + "indexes": {} + } +} \ No newline at end of file diff --git a/drizzle/meta/0007_snapshot.json b/drizzle/meta/0007_snapshot.json new file mode 100644 index 0000000..bab745e --- /dev/null +++ b/drizzle/meta/0007_snapshot.json @@ -0,0 +1,653 @@ +{ + "version": "5", + "dialect": "mysql", + "id": "7e9d948e-d569-4194-bb75-6219b837045e", + "prevId": "b1822efc-d652-4957-970f-f71b733359b6", + "tables": { + "bridge_intents": { + "name": "bridge_intents", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "fromChainId": { + "name": "fromChainId", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "senderAddress": { + "name": "senderAddress", + "type": "varchar(64)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "xicReceiveAddress": { + "name": "xicReceiveAddress", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "expectedUsdt": { + "name": "expectedUsdt", + "type": "decimal(20,6)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "matched": { + "name": "matched", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "matchedOrderId": { + "name": "matchedOrderId", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(now())" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": { + "bridge_intents_id": { + "name": "bridge_intents_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "bridge_orders": { + "name": "bridge_orders", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "txHash": { + "name": "txHash", + "type": "varchar(128)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "walletAddress": { + "name": "walletAddress", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "fromChainId": { + "name": "fromChainId", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "fromToken": { + "name": "fromToken", + "type": "varchar(32)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "fromAmount": { + "name": "fromAmount", + "type": "decimal(30,6)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "toChainId": { + "name": "toChainId", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 56 + }, + "toToken": { + "name": "toToken", + "type": "varchar(32)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'XIC'" + }, + "toAmount": { + "name": "toAmount", + "type": "decimal(30,6)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "xicReceiveAddress": { + "name": "xicReceiveAddress", + "type": "varchar(64)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "enum('pending','confirmed','distributed','failed')", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'pending'" + }, + "confirmedAt": { + "name": "confirmedAt", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "distributedAt": { + "name": "distributedAt", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "distributeTxHash": { + "name": "distributeTxHash", + "type": "varchar(128)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "blockNumber": { + "name": "blockNumber", + "type": "bigint", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(now())" + }, + "updatedAt": { + "name": "updatedAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "onUpdate": true, + "default": "(now())" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": { + "bridge_orders_id": { + "name": "bridge_orders_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": { + "bridge_orders_txHash_unique": { + "name": "bridge_orders_txHash_unique", + "columns": [ + "txHash" + ] + } + }, + "checkConstraint": {} + }, + "presale_config": { + "name": "presale_config", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "key": { + "name": "key", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "label": { + "name": "label", + "type": "varchar(128)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "varchar(256)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "type": { + "name": "type", + "type": "varchar(32)", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "'text'" + }, + "updatedAt": { + "name": "updatedAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "onUpdate": true, + "default": "(now())" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": { + "presale_config_id": { + "name": "presale_config_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": { + "presale_config_key_unique": { + "name": "presale_config_key_unique", + "columns": [ + "key" + ] + } + }, + "checkConstraint": {} + }, + "presale_stats_cache": { + "name": "presale_stats_cache", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "chain": { + "name": "chain", + "type": "varchar(16)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "usdtRaised": { + "name": "usdtRaised", + "type": "decimal(30,6)", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "'0'" + }, + "tokensSold": { + "name": "tokensSold", + "type": "decimal(30,6)", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "'0'" + }, + "weiRaised": { + "name": "weiRaised", + "type": "decimal(30,6)", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "'0'" + }, + "lastUpdated": { + "name": "lastUpdated", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(now())" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": { + "presale_stats_cache_id": { + "name": "presale_stats_cache_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "trc20_intents": { + "name": "trc20_intents", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "tronAddress": { + "name": "tronAddress", + "type": "varchar(64)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "evmAddress": { + "name": "evmAddress", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "expectedUsdt": { + "name": "expectedUsdt", + "type": "decimal(20,6)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "matched": { + "name": "matched", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "matchedPurchaseId": { + "name": "matchedPurchaseId", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(now())" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": { + "trc20_intents_id": { + "name": "trc20_intents_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "trc20_purchases": { + "name": "trc20_purchases", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "txHash": { + "name": "txHash", + "type": "varchar(128)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "fromAddress": { + "name": "fromAddress", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "usdtAmount": { + "name": "usdtAmount", + "type": "decimal(20,6)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "xicAmount": { + "name": "xicAmount", + "type": "decimal(30,6)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "blockNumber": { + "name": "blockNumber", + "type": "bigint", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "enum('pending','confirmed','distributed','failed')", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'pending'" + }, + "distributedAt": { + "name": "distributedAt", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "distributeTxHash": { + "name": "distributeTxHash", + "type": "varchar(128)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "evmAddress": { + "name": "evmAddress", + "type": "varchar(64)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(now())" + }, + "updatedAt": { + "name": "updatedAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "onUpdate": true, + "default": "(now())" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": { + "trc20_purchases_id": { + "name": "trc20_purchases_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": { + "trc20_purchases_txHash_unique": { + "name": "trc20_purchases_txHash_unique", + "columns": [ + "txHash" + ] + } + }, + "checkConstraint": {} + }, + "users": { + "name": "users", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "openId": { + "name": "openId", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "email": { + "name": "email", + "type": "varchar(320)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "loginMethod": { + "name": "loginMethod", + "type": "varchar(64)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "role": { + "name": "role", + "type": "enum('user','admin')", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'user'" + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(now())" + }, + "updatedAt": { + "name": "updatedAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "onUpdate": true, + "default": "(now())" + }, + "lastSignedIn": { + "name": "lastSignedIn", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(now())" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": { + "users_id": { + "name": "users_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": { + "users_openId_unique": { + "name": "users_openId_unique", + "columns": [ + "openId" + ] + } + }, + "checkConstraint": {} + } + }, + "views": {}, + "_meta": { + "schemas": {}, + "tables": {}, + "columns": {} + }, + "internal": { + "tables": {}, + "indexes": {} + } +} \ No newline at end of file diff --git a/drizzle/meta/_journal.json b/drizzle/meta/_journal.json index 2e36797..ea4eee9 100644 --- a/drizzle/meta/_journal.json +++ b/drizzle/meta/_journal.json @@ -43,6 +43,20 @@ "when": 1773124399358, "tag": "0005_certain_betty_ross", "breakpoints": true + }, + { + "idx": 6, + "version": "5", + "when": 1773135614044, + "tag": "0006_colossal_unicorn", + "breakpoints": true + }, + { + "idx": 7, + "version": "5", + "when": 1773136228889, + "tag": "0007_wide_menace", + "breakpoints": true } ] } \ No newline at end of file diff --git a/drizzle/schema.ts b/drizzle/schema.ts index 965dbd7..efb7474 100644 --- a/drizzle/schema.ts +++ b/drizzle/schema.ts @@ -97,20 +97,44 @@ export const presaleConfig = mysqlTable("presale_config", { export type PresaleConfig = typeof presaleConfig.$inferSelect; export type InsertPresaleConfig = typeof presaleConfig.$inferInsert; -// Cross-chain bridge orders — recorded when user completes a Li.Fi cross-chain purchase +// Cross-chain bridge orders — NAC self-developed cross-chain bridge +// User sends USDT on any supported chain to our receiving address +// Backend monitors and records confirmed transfers, then distributes XIC export const bridgeOrders = mysqlTable("bridge_orders", { id: int("id").autoincrement().primaryKey(), txHash: varchar("txHash", { length: 128 }).notNull().unique(), - walletAddress: varchar("walletAddress", { length: 64 }).notNull(), + walletAddress: varchar("walletAddress", { length: 64 }).notNull(), // sender wallet on source chain fromChainId: int("fromChainId").notNull(), fromToken: varchar("fromToken", { length: 32 }).notNull(), - fromAmount: decimal("fromAmount", { precision: 30, scale: 6 }).notNull(), - toChainId: int("toChainId").notNull(), - toToken: varchar("toToken", { length: 32 }).notNull(), - toAmount: decimal("toAmount", { precision: 30, scale: 6 }).notNull(), - status: mysqlEnum("status", ["pending", "completed", "failed"]).default("completed").notNull(), + fromAmount: decimal("fromAmount", { precision: 30, scale: 6 }).notNull(), // USDT amount sent + toChainId: int("toChainId").notNull().default(56), // always BSC for XIC + toToken: varchar("toToken", { length: 32 }).notNull().default("XIC"), + toAmount: decimal("toAmount", { precision: 30, scale: 6 }).notNull(), // XIC amount to distribute + xicReceiveAddress: varchar("xicReceiveAddress", { length: 64 }), // BSC address to receive XIC + status: mysqlEnum("status", ["pending", "confirmed", "distributed", "failed"]).default("pending").notNull(), + confirmedAt: timestamp("confirmedAt"), + distributedAt: timestamp("distributedAt"), + distributeTxHash: varchar("distributeTxHash", { length: 128 }), + blockNumber: bigint("blockNumber", { mode: "number" }), createdAt: timestamp("createdAt").defaultNow().notNull(), + updatedAt: timestamp("updatedAt").defaultNow().onUpdateNow().notNull(), }); export type BridgeOrder = typeof bridgeOrders.$inferSelect; export type InsertBridgeOrder = typeof bridgeOrders.$inferInsert; + +// Bridge deposit intents — user pre-registers before sending USDT +// Helps backend match incoming transfers to the correct XIC receive address +export const bridgeIntents = mysqlTable("bridge_intents", { + id: int("id").autoincrement().primaryKey(), + fromChainId: int("fromChainId").notNull(), + senderAddress: varchar("senderAddress", { length: 64 }), // sender on source chain (optional, filled when wallet connected) + xicReceiveAddress: varchar("xicReceiveAddress", { length: 64 }).notNull(), // BSC address to receive XIC + expectedUsdt: decimal("expectedUsdt", { precision: 20, scale: 6 }), // expected USDT amount + matched: boolean("matched").default(false).notNull(), + matchedOrderId: int("matchedOrderId"), + createdAt: timestamp("createdAt").defaultNow().notNull(), +}); + +export type BridgeIntent = typeof bridgeIntents.$inferSelect; +export type InsertBridgeIntent = typeof bridgeIntents.$inferInsert; diff --git a/server/_core/index.ts b/server/_core/index.ts index 5ae46b3..69a80e6 100644 --- a/server/_core/index.ts +++ b/server/_core/index.ts @@ -8,6 +8,7 @@ import { appRouter } from "../routers"; import { createContext } from "./context"; import { serveStatic, setupVite } from "./vite"; import { startTRC20Monitor } from "../trc20Monitor"; +import { startBridgeMonitor } from "../bridgeMonitor"; function isPortAvailable(port: number): Promise { return new Promise(resolve => { @@ -64,6 +65,9 @@ async function startServer() { // Start TRC20 monitor in background startTRC20Monitor().catch(e => console.error("[TRC20Monitor] Start error:", e)); + + // Start Bridge monitor (multi-chain USDT deposit listener) + startBridgeMonitor(); } startServer().catch(console.error); diff --git a/server/bridgeMonitor.ts b/server/bridgeMonitor.ts new file mode 100644 index 0000000..82df195 --- /dev/null +++ b/server/bridgeMonitor.ts @@ -0,0 +1,285 @@ +/** + * NAC Cross-Chain Bridge Monitor + * Self-developed bridge: monitors USDT transfers on BSC/ETH/Polygon/Arbitrum/Avalanche + * When user sends USDT to our receiving address, we record the order and distribute XIC + */ + +import { getDb } from "./db"; +import { bridgeOrders, bridgeIntents } from "../drizzle/schema"; +import { eq, and, desc } from "drizzle-orm"; + +// ─── Presale Config ─────────────────────────────────────────────────────────── +export const XIC_PRICE_USDT = 0.02; // $0.02 per XIC + +// ─── Chain Configs ──────────────────────────────────────────────────────────── +export interface ChainConfig { + chainId: number; + name: string; + symbol: string; // native token symbol (BNB, ETH, MATIC, AVAX) + icon: string; + color: string; + usdtAddress: string; // USDT contract on this chain + receivingAddress: string; // Our USDT receiving address on this chain + rpcUrl: string; + explorerUrl: string; + explorerTxPath: string; // e.g. /tx/ + decimals: number; // USDT decimals (6 for most, 18 for BSC) +} + +export const BRIDGE_CHAINS: ChainConfig[] = [ + { + chainId: 56, + name: "BSC", + symbol: "BNB", + icon: "🟡", + color: "#F0B90B", + usdtAddress: "0x55d398326f99059fF775485246999027B3197955", + receivingAddress: process.env.BRIDGE_BSC_ADDRESS || "0x43DAb577f3279e11D311E7d628C6201d893A9Aa3", + rpcUrl: "https://bsc-dataseed1.binance.org/", + explorerUrl: "https://bscscan.com", + explorerTxPath: "/tx/", + decimals: 18, // BSC USDT is 18 decimals + }, + { + chainId: 1, + name: "Ethereum", + symbol: "ETH", + icon: "🔵", + color: "#627EEA", + usdtAddress: "0xdAC17F958D2ee523a2206206994597C13D831ec7", + receivingAddress: process.env.BRIDGE_ETH_ADDRESS || "0x43DAb577f3279e11D311E7d628C6201d893A9Aa3", + rpcUrl: "https://ethereum.publicnode.com", + explorerUrl: "https://etherscan.io", + explorerTxPath: "/tx/", + decimals: 6, // ETH USDT is 6 decimals + }, + { + chainId: 137, + name: "Polygon", + symbol: "MATIC", + icon: "🟣", + color: "#8247E5", + usdtAddress: "0xc2132D05D31c914a87C6611C10748AEb04B58e8F", + receivingAddress: process.env.BRIDGE_POLYGON_ADDRESS || "0x43DAb577f3279e11D311E7d628C6201d893A9Aa3", + rpcUrl: "https://polygon-rpc.com/", + explorerUrl: "https://polygonscan.com", + explorerTxPath: "/tx/", + decimals: 6, + }, + { + chainId: 42161, + name: "Arbitrum", + symbol: "ETH", + icon: "🔷", + color: "#28A0F0", + usdtAddress: "0xFd086bC7CD5C481DCC9C85ebE478A1C0b69FCbb9", + receivingAddress: process.env.BRIDGE_ARB_ADDRESS || "0x43DAb577f3279e11D311E7d628C6201d893A9Aa3", + rpcUrl: "https://arb1.arbitrum.io/rpc", + explorerUrl: "https://arbiscan.io", + explorerTxPath: "/tx/", + decimals: 6, + }, + { + chainId: 43114, + name: "Avalanche", + symbol: "AVAX", + icon: "🔴", + color: "#E84142", + usdtAddress: "0x9702230A8Ea53601f5cD2dc00fDBc13d4dF4A8c7", + receivingAddress: process.env.BRIDGE_AVAX_ADDRESS || "0x43DAb577f3279e11D311E7d628C6201d893A9Aa3", + rpcUrl: "https://api.avax.network/ext/bc/C/rpc", + explorerUrl: "https://snowtrace.io", + explorerTxPath: "/tx/", + decimals: 6, + }, +]; + +// ─── ERC-20 Transfer event ABI ──────────────────────────────────────────────── +// Transfer(address indexed from, address indexed to, uint256 value) +const TRANSFER_TOPIC = "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef"; + +// ─── Fetch USDT transfers to our address via eth_getLogs ────────────────────── +async function fetchUsdtTransfers(chain: ChainConfig, fromBlock: string = "latest"): Promise> { + try { + // Pad address to 32 bytes for topic matching + const paddedTo = "0x000000000000000000000000" + chain.receivingAddress.slice(2).toLowerCase(); + + const payload = { + jsonrpc: "2.0", + id: 1, + method: "eth_getLogs", + params: [{ + fromBlock: fromBlock === "latest" ? "latest" : fromBlock, + toBlock: "latest", + address: chain.usdtAddress, + topics: [ + TRANSFER_TOPIC, + null, // any sender + paddedTo, // to our receiving address + ], + }], + }; + + const res = await fetch(chain.rpcUrl, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(payload), + signal: AbortSignal.timeout(10000), + }); + + const data = await res.json(); + if (!data.result || !Array.isArray(data.result)) return []; + + return data.result.map((log: any) => { + const fromAddress = "0x" + log.topics[1].slice(26); + const toAddress = "0x" + log.topics[2].slice(26); + const rawAmount = BigInt(log.data); + const divisor = BigInt(10 ** chain.decimals); + const amount = Number(rawAmount) / Number(divisor); + const blockNumber = parseInt(log.blockNumber, 16); + + return { + txHash: log.transactionHash, + fromAddress: fromAddress.toLowerCase(), + toAddress: toAddress.toLowerCase(), + amount, + blockNumber, + }; + }); + } catch (err) { + console.error(`[BridgeMonitor] Error fetching transfers on ${chain.name}:`, err); + return []; + } +} + +// ─── Get latest block number ────────────────────────────────────────────────── +async function getLatestBlock(chain: ChainConfig): Promise { + try { + const res = await fetch(chain.rpcUrl, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ jsonrpc: "2.0", id: 1, method: "eth_blockNumber", params: [] }), + signal: AbortSignal.timeout(8000), + }); + const data = await res.json(); + return parseInt(data.result, 16); + } catch { + return 0; + } +} + +// ─── Process new transfers and save to DB ──────────────────────────────────── +async function processTransfers(chain: ChainConfig): Promise { + const db = await getDb(); + if (!db) return; + + // Get recent blocks (last ~100 blocks ≈ ~5 min on BSC, ~20 min on ETH) + const latestBlock = await getLatestBlock(chain); + if (!latestBlock) return; + + const lookbackBlocks = chain.chainId === 1 ? 50 : 200; // ETH slower, BSC faster + const fromBlock = "0x" + Math.max(0, latestBlock - lookbackBlocks).toString(16); + + const transfers = await fetchUsdtTransfers(chain, fromBlock); + + for (const transfer of transfers) { + if (transfer.amount < 0.01) continue; // ignore dust + + // Check if already recorded + try { + const existing = await db + .select({ id: bridgeOrders.id }) + .from(bridgeOrders) + .where(eq(bridgeOrders.txHash, transfer.txHash)) + .limit(1); + + if (existing.length > 0) continue; // already recorded + + // Calculate XIC amount + const xicAmount = transfer.amount / XIC_PRICE_USDT; + + // Try to find matching intent (user pre-registered their XIC receive address) + const intent = await db + .select() + .from(bridgeIntents) + .where(and( + eq(bridgeIntents.fromChainId, chain.chainId), + eq(bridgeIntents.senderAddress, transfer.fromAddress), + eq(bridgeIntents.matched, false), + )) + .orderBy(desc(bridgeIntents.createdAt)) + .limit(1); + + const xicReceiveAddress = intent.length > 0 ? intent[0].xicReceiveAddress : null; + + // Record the order + await db.insert(bridgeOrders).values({ + txHash: transfer.txHash, + walletAddress: transfer.fromAddress, + fromChainId: chain.chainId, + fromToken: "USDT", + fromAmount: String(transfer.amount), + toChainId: 56, + toToken: "XIC", + toAmount: String(xicAmount), + xicReceiveAddress, + status: "confirmed", + confirmedAt: new Date(), + blockNumber: transfer.blockNumber, + }); + + // Mark intent as matched + if (intent.length > 0) { + await db + .update(bridgeIntents) + .set({ matched: true, matchedOrderId: undefined }) + .where(eq(bridgeIntents.id, intent[0].id)); + } + + console.log(`[BridgeMonitor] New ${chain.name} deposit: ${transfer.amount} USDT from ${transfer.fromAddress} → ${xicAmount} XIC`); + } catch (err: any) { + if (err?.code === "ER_DUP_ENTRY") continue; + console.error(`[BridgeMonitor] Error recording transfer ${transfer.txHash}:`, err); + } + } +} + +// ─── Start monitoring all chains ────────────────────────────────────────────── +let monitorInterval: NodeJS.Timeout | null = null; + +export function startBridgeMonitor(): void { + if (monitorInterval) return; + + console.log("[BridgeMonitor] Starting multi-chain USDT deposit monitor..."); + + const run = async () => { + for (const chain of BRIDGE_CHAINS) { + await processTransfers(chain).catch(err => + console.error(`[BridgeMonitor] Error on ${chain.name}:`, err) + ); + } + }; + + // Run immediately, then every 30 seconds + run(); + monitorInterval = setInterval(run, 30_000); +} + +export function stopBridgeMonitor(): void { + if (monitorInterval) { + clearInterval(monitorInterval); + monitorInterval = null; + console.log("[BridgeMonitor] Stopped."); + } +} + +// ─── Export chain config for use in routes ─────────────────────────────────── +export function getChainConfig(chainId: number): ChainConfig | undefined { + return BRIDGE_CHAINS.find(c => c.chainId === chainId); +} diff --git a/server/onchain.ts b/server/onchain.ts index d3730d3..4756e62 100644 --- a/server/onchain.ts +++ b/server/onchain.ts @@ -25,11 +25,11 @@ const RPC_POOLS = { "https://rpc.ankr.com/bsc", ], ETH: [ - "https://eth.llamarpc.com", - "https://ethereum.publicnode.com", + "https://ethereum.publicnode.com", // China-accessible "https://rpc.ankr.com/eth", - "https://1rpc.io/eth", "https://eth.drpc.org", + "https://1rpc.io/eth", + "https://eth.llamarpc.com", "https://cloudflare-eth.com", "https://rpc.payload.de", ], diff --git a/server/routers.ts b/server/routers.ts index 21e62ae..f993193 100644 --- a/server/routers.ts +++ b/server/routers.ts @@ -5,7 +5,7 @@ import { publicProcedure, protectedProcedure, router } from "./_core/trpc"; import { getCombinedStats, getPresaleStats } from "./onchain"; import { getRecentPurchases } from "./trc20Monitor"; import { getDb } from "./db"; -import { trc20Purchases, trc20Intents, bridgeOrders } from "../drizzle/schema"; +import { trc20Purchases, trc20Intents, bridgeOrders, bridgeIntents } from "../drizzle/schema"; import { eq, desc, sql } from "drizzle-orm"; import { z } from "zod"; import { TRPCError } from "@trpc/server"; @@ -42,7 +42,9 @@ const bridgeRouter = router({ toChainId: input.toChainId, toToken: input.toToken, toAmount: input.toAmount, - status: "completed", + status: "confirmed" as const, + xicReceiveAddress: null, + confirmedAt: new Date(), }); return { success: true }; } catch (e: any) { @@ -51,7 +53,7 @@ const bridgeRouter = router({ } }), - // List orders by wallet address + // List orders by wallet address (includes pending intents + confirmed orders) myOrders: publicProcedure .input(z.object({ walletAddress: z.string().min(1).max(64), @@ -60,25 +62,70 @@ const bridgeRouter = router({ .query(async ({ input }) => { const db = await getDb(); if (!db) return []; - const rows = await db + const addr = input.walletAddress.toLowerCase(); + // Query confirmed bridge orders + const confirmedRows = await db .select() .from(bridgeOrders) - .where(eq(bridgeOrders.walletAddress, input.walletAddress.toLowerCase())) + .where(eq(bridgeOrders.walletAddress, addr)) .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, - })); + // Query pending bridge intents (by xicReceiveAddress) + const intentRows = await db + .select() + .from(bridgeIntents) + .where(eq(bridgeIntents.xicReceiveAddress, addr)) + .orderBy(desc(bridgeIntents.createdAt)) + .limit(input.limit); + // Merge: intents first (pending), then confirmed orders + const result = [ + ...intentRows.map(r => ({ + id: r.id, + type: 'intent' as const, + fromChainId: r.fromChainId, + xicReceiveAddress: r.xicReceiveAddress, + expectedUsdt: r.expectedUsdt ? Number(r.expectedUsdt) : null, + matched: r.matched, + status: r.matched ? 'confirmed' as const : 'pending' as const, + createdAt: r.createdAt, + })), + ...confirmedRows.map(r => ({ + id: r.id, + type: 'order' as const, + 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, + })), + ]; + return result.sort((a, b) => new Date(b.createdAt!).getTime() - new Date(a.createdAt!).getTime()).slice(0, input.limit); + }), + + // Register a bridge intent — user pre-registers before sending USDT + registerIntent: publicProcedure + .input(z.object({ + fromChainId: z.number().int(), + senderAddress: z.string().max(64).or(z.literal("")).transform(v => v === "" ? undefined : v).optional(), // optional — filled when wallet connected + xicReceiveAddress: z.string().regex(/^0x[0-9a-fA-F]{40}$/, "Invalid EVM address"), + expectedUsdt: z.number().min(0.01).optional(), + })) + .mutation(async ({ input }) => { + const db = await getDb(); + if (!db) throw new TRPCError({ code: "INTERNAL_SERVER_ERROR", message: "DB unavailable" }); + await db.insert(bridgeIntents).values({ + fromChainId: input.fromChainId, + senderAddress: input.senderAddress ? input.senderAddress.toLowerCase() : null, + xicReceiveAddress: input.xicReceiveAddress, + expectedUsdt: input.expectedUsdt ? String(input.expectedUsdt) : null, + matched: false, + }); + return { success: true, message: "Intent registered. Please send USDT now." }; }), // List recent bridge orders (public) diff --git a/todo.md b/todo.md index 4ad51ee..04798c6 100644 --- a/todo.md +++ b/todo.md @@ -144,11 +144,35 @@ ## v12 Bridge跨链桥完善 + 钱包连接深度修复 -- [ ] WalletSelector v5:ErrorHelpPanel组件(分类错误处理+MetaMask权限重置5步指南) -- [ ] WalletSelector v5:连接中状态改为"等待钱包授权"提示 -- [ ] WalletSelector v5:错误后显示"重试"按钮和其他可用钱包 -- [ ] Bridge页面:确认所有链(BSC/ETH/Polygon/Arbitrum/Avalanche)USDT→XIC路由逻辑 -- [ ] Bridge页面:Gas费说明(每条链原生代币:BNB/ETH/MATIC/ETH/AVAX) -- [ ] 构建v12并部署到AI服务器(43.224.155.27) -- [ ] 同步代码到备份Git库(git.newassetchain.io) -- [ ] 记录部署日志 +- [x] WalletSelector v5:ErrorHelpPanel组件(分类错误处理+MetaMask权限重置5步指南) +- [x] WalletSelector v5:连接中状态改为"等待钱包授权"提示 +- [x] WalletSelector v5:错误后显示"重试"按钮和其他可用钱包 +- [x] Bridge页面:确认所有链(BSC/ETH/Polygon/Arbitrum/Avalanche)USDT→XIC路由逻辑 +- [x] Bridge页面:Gas费说明(每条链原生代币:BNB/ETH/MATIC/ETH/AVAX) +- [x] 构建v12并部署到AI服务器(43.224.155.27) +- [x] 同步代码到备份Git库(git.newassetchain.io) +- [x] 记录部署日志 + +## v13 自研跨链桥(完全移除Li.Fi) + +- [x] 数据库:新增 bridge_deposits 表(多链USDT转入记录) +- [x] 后端:多链USDT收款地址配置(BSC/ETH/Polygon/Arbitrum/Avalanche) +- [x] 后端:链上USDT转入监听(每30秒轮询各链收款地址) +- [x] 后端:tRPC接口:提交转账意图(walletAddress + fromChain + usdtAmount + xicReceiveAddress) +- [x] 后端:tRPC接口:查询订单状态(by walletAddress) +- [x] 前端:完全移除Li.Fi依赖和代码 +- [x] 前端:选链 → 显示对应链USDT收款地址 → 用户转账 → 实时状态跟踪 +- [x] 前端:连接钱包后自动填写XIC接收地址 +- [x] 前端:中英文双语支持 +- [x] 构建并部署到AI服务器 +- [x] 浏览器完整测试 +- [x] 同步到备份Git库 +- [x] 记录部署日志 + +## v13 收款地址更新(官方地址) + +- [x] Bridge.tsx:更新BSC收款地址为 0x43DAb577f3279e11D311E7d628C6201d893A9Aa3 +- [x] Bridge.tsx:ETH/Polygon/Arbitrum 使用同一EVM地址 0x43DAb577f3279e11D311E7d628C6201d893A9Aa3 +- [x] bridgeMonitor.ts:更新所有链收款地址 +- [x] Home.tsx:更新TRC20收款地址为 TWc2ugYBFN5aSoimAh4qGt9oMyket6NYZp +- [x] contracts.ts:同步更新TRC20/ERC20/BEP20地址