diff --git a/client/src/pages/Admin.tsx b/client/src/pages/Admin.tsx index aa190ec..1101a17 100644 --- a/client/src/pages/Admin.tsx +++ b/client/src/pages/Admin.tsx @@ -428,7 +428,11 @@ function Dashboard({ token, onLogout }: { token: string; onLogout: () => void }) const [statusFilter, setStatusFilter] = useState<"all" | "pending" | "confirmed" | "distributed" | "failed">("all"); const [markingId, setMarkingId] = useState(null); const [distributeTxInput, setDistributeTxInput] = useState>({}); - const [activeTab, setActiveTab] = useState<"purchases" | "intents" | "settings">("purchases"); + const [activeTab, setActiveTab] = useState<"purchases" | "intents" | "bridge" | "settings">("purchases"); + const [bridgePage, setBridgePage] = useState(1); + const [bridgeStatusFilter, setBridgeStatusFilter] = useState<"all" | "pending" | "confirmed" | "distributed" | "failed">("all"); + const [bridgeDistribTxInput, setBridgeDistribTxInput] = useState>({}); + const [updatingBridgeId, setUpdatingBridgeId] = useState(null); const { data: statsData, refetch: refetchStats } = trpc.admin.stats.useQuery({ token }); const { data: intentsData, isLoading: intentsLoading } = trpc.admin.listIntents.useQuery({ token, showAll: false }); @@ -439,6 +443,30 @@ function Dashboard({ token, onLogout }: { token: string; onLogout: () => void }) status: statusFilter, }); + const { data: bridgeData, refetch: refetchBridge, isLoading: bridgeLoading } = trpc.admin.listBridgeOrders.useQuery({ + token, + page: bridgePage, + limit: 20, + status: bridgeStatusFilter, + }); + + const updateBridgeOrderMutation = trpc.admin.updateBridgeOrder.useMutation({ + onSuccess: () => { + refetchBridge(); + setUpdatingBridgeId(null); + }, + }); + + const handleUpdateBridgeOrder = (id: number, status: "pending" | "confirmed" | "distributed" | "failed") => { + setUpdatingBridgeId(id); + updateBridgeOrderMutation.mutate({ + token, + orderId: id, + status, + distributeTxHash: bridgeDistribTxInput[id] || undefined, + }); + }; + const markDistributedMutation = trpc.admin.markDistributed.useMutation({ onSuccess: () => { refetchPurchases(); @@ -597,6 +625,22 @@ function Dashboard({ token, onLogout }: { token: string; onLogout: () => void }) )} + + ))} + + + {bridgeLoading ? ( +
Loading...
+ ) : ( +
+ + + + {["ID", "TX Hash", "Chain", "Sender", "USDT", "XIC", "XIC Receive Addr", "Status", "Distribute TX", "Action"].map(h => ( + + ))} + + + + {bridgeData?.orders && bridgeData.orders.length > 0 ? bridgeData.orders.map((order, i) => { + const chainNames: Record = { 56: "BSC", 1: "ETH", 137: "POLY", 42161: "ARB", 43114: "AVAX" }; + const isUpdating = updatingBridgeId === order.id; + return ( + + + + + + + + + + + + + ); + }) : ( + + )} + +
{h}
{order.id}{formatAddress(order.txHash)} + + {chainNames[order.fromChainId] ?? `Chain ${order.fromChainId}`} + + {formatAddress(order.walletAddress)}${order.fromAmount.toLocaleString()}{order.toAmount.toLocaleString(undefined, { maximumFractionDigits: 0 })} XIC{formatAddress(order.xicReceiveAddress ?? null)} + {order.status !== "distributed" ? ( + setBridgeDistribTxInput(prev => ({ ...prev, [order.id]: e.target.value }))} + placeholder="0x... BSC TX hash" + className="w-32 rounded px-2 py-1 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" }} + /> + ) : ( + formatAddress(order.distributeTxHash ?? null) + )} + + {order.status === "confirmed" && ( + + )} + {order.status === "pending" && ( + + )} +
No bridge orders yet
+
+ )} + {/* Pagination */} + {bridgeData && bridgeData.total > 20 && ( +
+ {bridgeData.total} total orders +
+ + Page {bridgePage} / {Math.ceil(bridgeData.total / 20)} + +
+
+ )} + + + )} + {/* ── Site Settings Panel ── */} {activeTab === "settings" && ( diff --git a/client/src/pages/Bridge.tsx b/client/src/pages/Bridge.tsx index dd6a0ae..fbb6584 100644 --- a/client/src/pages/Bridge.tsx +++ b/client/src/pages/Bridge.tsx @@ -4,6 +4,7 @@ // Supports: BSC, ETH, Polygon, Arbitrum, Avalanche import { useState, useEffect, useCallback } from "react"; +import { createPortal } from "react-dom"; import { toast } from "sonner"; import { trpc } from "@/lib/trpc"; import { @@ -753,18 +754,48 @@ export default function Bridge() { {/* ─── Disclaimer ─────────────────────────────────────────────────── */}

{t.disclaimer}

- - {/* ─── Wallet Selector Modal ───────────────────────────────────────────── */} - {showWalletSelector && ( - { - setXicReceiveAddress(address); - setShowWalletSelector(false); - }} - connectedAddress={wallet.address || undefined} - /> - )} - + {/* ─── Wallet Selector Modal (Portal) ───────────────────────────────── */} + {showWalletSelector && createPortal( +
{ if (e.target === e.currentTarget) setShowWalletSelector(false); }} + > +
+ +

+ {lang === "zh" ? "连接钱包" : "Connect Wallet"} +

+

+ {lang === "zh" ? "连接钱包后自动填入XIC接收地址" : "Your wallet address will be auto-filled as XIC receive address"} +

+ { + if (rawProvider && wallet.connectWithProvider) { + await wallet.connectWithProvider(rawProvider, address); + } + setXicReceiveAddress(address); + setShowWalletSelector(false); + toast.success(lang === "zh" ? `钱包已连接: ${address.slice(0, 6)}...​${address.slice(-4)}` : `Wallet connected: ${address.slice(0, 6)}...${address.slice(-4)}`); + }} + /> +
+
, + document.body + )} ); } diff --git a/server/routers.ts b/server/routers.ts index f993193..1820c66 100644 --- a/server/routers.ts +++ b/server/routers.ts @@ -479,6 +479,99 @@ export const appRouter = router({ return { success: true }; }), + // List bridge orders (cross-chain bridge) + listBridgeOrders: publicProcedure + .input(z.object({ + token: z.string(), + page: z.number().int().min(1).default(1), + limit: z.number().int().min(1).max(100).default(20), + status: z.enum(["all", "pending", "confirmed", "distributed", "failed"]).default("all"), + })) + .query(async ({ input }) => { + if (!input.token.startsWith("bmFjLWFkbWlu")) { + throw new TRPCError({ code: "UNAUTHORIZED", message: "Invalid admin token" }); + } + const db = await getDb(); + if (!db) throw new TRPCError({ code: "INTERNAL_SERVER_ERROR", message: "DB unavailable" }); + const offset = (input.page - 1) * input.limit; + let query = db.select().from(bridgeOrders); + if (input.status !== "all") { + query = query.where(eq(bridgeOrders.status, input.status)) as typeof query; + } + const rows = await query + .orderBy(desc(bridgeOrders.createdAt)) + .limit(input.limit) + .offset(offset); + const countResult = await db + .select({ count: sql`COUNT(*)` }) + .from(bridgeOrders) + .where(input.status !== "all" ? eq(bridgeOrders.status, input.status) : sql`1=1`); + const intents = await db + .select() + .from(bridgeIntents) + .orderBy(desc(bridgeIntents.createdAt)) + .limit(50); + return { + orders: 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), + xicReceiveAddress: r.xicReceiveAddress, + status: r.status, + confirmedAt: r.confirmedAt, + distributedAt: r.distributedAt, + distributeTxHash: r.distributeTxHash, + blockNumber: r.blockNumber, + createdAt: r.createdAt, + })), + intents: intents.map(i => ({ + id: i.id, + fromChainId: i.fromChainId, + senderAddress: i.senderAddress, + xicReceiveAddress: i.xicReceiveAddress, + expectedUsdt: i.expectedUsdt ? Number(i.expectedUsdt) : null, + matched: i.matched, + matchedOrderId: i.matchedOrderId, + createdAt: i.createdAt, + })), + total: Number(countResult[0]?.count || 0), + page: input.page, + limit: input.limit, + }; + }), + + // Update bridge order status (manual distribution) + updateBridgeOrder: publicProcedure + .input(z.object({ + token: z.string(), + orderId: z.number().int(), + status: z.enum(["pending", "confirmed", "distributed", "failed"]), + distributeTxHash: z.string().optional(), + })) + .mutation(async ({ input }) => { + if (!input.token.startsWith("bmFjLWFkbWlu")) { + throw new TRPCError({ code: "UNAUTHORIZED", message: "Invalid admin token" }); + } + const db = await getDb(); + if (!db) throw new TRPCError({ code: "INTERNAL_SERVER_ERROR", message: "DB unavailable" }); + await db + .update(bridgeOrders) + .set({ + status: input.status, + distributedAt: input.status === "distributed" ? new Date() : undefined, + distributeTxHash: input.distributeTxHash || null, + updatedAt: new Date(), + }) + .where(eq(bridgeOrders.id, input.orderId)); + return { success: true }; + }), + // Test Telegram connection testTelegram: publicProcedure .input(z.object({ diff --git a/todo.md b/todo.md index 04798c6..ebc1fdc 100644 --- a/todo.md +++ b/todo.md @@ -176,3 +176,12 @@ - [x] bridgeMonitor.ts:更新所有链收款地址 - [x] Home.tsx:更新TRC20收款地址为 TWc2ugYBFN5aSoimAh4qGt9oMyket6NYZp - [x] contracts.ts:同步更新TRC20/ERC20/BEP20地址 + +## v14 Bridge功能完善 + +- [x] 修复Bridge页面"连接钱包"按钮点击无效问题(使用createPortal渲染到document.body,z-index:9999) +- [x] My Transactions列表增加USDT数量和XIC数量显示(已在v13中完成) +- [x] Confirm按钮点击后增加加载动画(已有Loader2 animate-spin) +- [x] 管理员后台添加Bridge订单管理页面(Bridge Intents + Bridge Orders表格,状态过滤,手动标记分发) +- [ ] 构建部署到AI服务器并测试 +- [ ] 同步到备份Git库