From 84dd7d288fd34591d6c1af9c25c405150d333b08 Mon Sep 17 00:00:00 2001 From: Manus Date: Tue, 10 Mar 2026 06:34:14 -0400 Subject: [PATCH] =?UTF-8?q?Checkpoint:=20v14:=20Bridge=E9=A1=B5=E9=9D=A2?= =?UTF-8?q?=E5=8A=9F=E8=83=BD=E5=AE=8C=E5=96=84=20-=201)=20=E4=BF=AE?= =?UTF-8?q?=E5=A4=8DConnect=20Wallet=E6=8C=89=E9=92=AE=EF=BC=9A=E4=BD=BF?= =?UTF-8?q?=E7=94=A8createPortal=E5=B0=86WalletSelector=E6=B8=B2=E6=9F=93?= =?UTF-8?q?=E5=88=B0document.body=EF=BC=8Cz-index:9999=EF=BC=8C=E6=94=AF?= =?UTF-8?q?=E6=8C=81=E7=82=B9=E5=87=BB=E9=81=AE=E7=BD=A9=E5=85=B3=E9=97=AD?= =?UTF-8?q?=EF=BC=9B2)=20=E7=AE=A1=E7=90=86=E5=91=98=E5=90=8E=E5=8F=B0?= =?UTF-8?q?=E6=96=B0=E5=A2=9EBridge=20Orders=E6=A0=87=E7=AD=BE=E9=A1=B5?= =?UTF-8?q?=EF=BC=88Bridge=20Intents=E8=A1=A8=E6=A0=BC+Bridge=20Orders?= =?UTF-8?q?=E8=A1=A8=E6=A0=BC=EF=BC=8C=E7=8A=B6=E6=80=81=E8=BF=87=E6=BB=A4?= =?UTF-8?q?=E5=99=A8=EF=BC=8C=E6=89=8B=E5=8A=A8=E6=A0=87=E8=AE=B0=E5=88=86?= =?UTF-8?q?=E5=8F=91/=E7=A1=AE=E8=AE=A4=E5=8A=9F=E8=83=BD=EF=BC=89?= =?UTF-8?q?=EF=BC=9B3)=20=E5=90=8E=E7=AB=AF=E6=96=B0=E5=A2=9Eadmin.listBri?= =?UTF-8?q?dgeOrders=E5=92=8Cadmin.updateBridgeOrder=E8=B7=AF=E7=94=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- client/src/pages/Admin.tsx | 237 +++++++++++++++++++++++++++++++++++- client/src/pages/Bridge.tsx | 57 +++++++-- server/routers.ts | 93 ++++++++++++++ todo.md | 9 ++ 4 files changed, 382 insertions(+), 14 deletions(-) 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库