Checkpoint: v14: Bridge页面功能完善 - 1) 修复Connect Wallet按钮:使用createPortal将WalletSelector渲染到document.body,z-index:9999,支持点击遮罩关闭;2) 管理员后台新增Bridge Orders标签页(Bridge Intents表格+Bridge Orders表格,状态过滤器,手动标记分发/确认功能);3) 后端新增admin.listBridgeOrders和admin.updateBridgeOrder路由
This commit is contained in:
parent
0659cd71cb
commit
84dd7d288f
|
|
@ -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<number | null>(null);
|
||||
const [distributeTxInput, setDistributeTxInput] = useState<Record<number, string>>({});
|
||||
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<Record<number, string>>({});
|
||||
const [updatingBridgeId, setUpdatingBridgeId] = useState<number | null>(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 })
|
|||
</span>
|
||||
)}
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setActiveTab("bridge")}
|
||||
className="px-4 py-2 rounded-xl text-sm font-semibold transition-all"
|
||||
style={{
|
||||
background: activeTab === "bridge" ? "rgba(138,71,229,0.15)" : "rgba(255,255,255,0.04)",
|
||||
border: activeTab === "bridge" ? "1px solid rgba(138,71,229,0.4)" : "1px solid rgba(255,255,255,0.08)",
|
||||
color: activeTab === "bridge" ? "#a855f7" : "rgba(255,255,255,0.5)",
|
||||
}}
|
||||
>
|
||||
⚡ Bridge Orders
|
||||
{bridgeData && bridgeData.total > 0 && (
|
||||
<span className="ml-2 px-1.5 py-0.5 rounded-full text-xs" style={{ background: "rgba(138,71,229,0.2)", color: "#a855f7" }}>
|
||||
{bridgeData.total}
|
||||
</span>
|
||||
)}
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setActiveTab("settings")}
|
||||
className="px-4 py-2 rounded-xl text-sm font-semibold transition-all"
|
||||
|
|
@ -833,6 +877,197 @@ function Dashboard({ token, onLogout }: { token: string; onLogout: () => void })
|
|||
</div>
|
||||
)}
|
||||
|
||||
{/* ── Bridge Orders Panel ── */}
|
||||
{activeTab === "bridge" && (
|
||||
<div className="space-y-4">
|
||||
{/* Bridge Intents */}
|
||||
<div className="rounded-2xl overflow-hidden" style={{ background: "rgba(255,255,255,0.02)", border: "1px solid rgba(138,71,229,0.2)" }}>
|
||||
<div className="px-5 py-4" style={{ borderBottom: "1px solid rgba(255,255,255,0.06)" }}>
|
||||
<h3 className="font-semibold text-white/80" style={{ fontFamily: "'Space Grotesk', sans-serif" }}>
|
||||
Bridge Intents
|
||||
<span className="text-white/40 text-sm ml-2">(users who registered intent but transfer not yet detected)</span>
|
||||
</h3>
|
||||
</div>
|
||||
<div className="overflow-x-auto">
|
||||
<table className="w-full text-sm">
|
||||
<thead>
|
||||
<tr style={{ borderBottom: "1px solid rgba(255,255,255,0.06)" }}>
|
||||
{["ID", "Chain", "Sender", "XIC Receive Addr", "Expected USDT", "Matched", "Time"].map(h => (
|
||||
<th key={h} className="px-4 py-3 text-left text-xs font-semibold uppercase tracking-wider text-white/30">{h}</th>
|
||||
))}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{bridgeData?.intents && bridgeData.intents.length > 0 ? bridgeData.intents.map((intent, i) => {
|
||||
const chainNames: Record<number, string> = { 56: "BSC", 1: "ETH", 137: "POLY", 42161: "ARB", 43114: "AVAX" };
|
||||
return (
|
||||
<tr key={intent.id} style={{ borderBottom: i < bridgeData.intents.length - 1 ? "1px solid rgba(255,255,255,0.04)" : "none" }}>
|
||||
<td className="px-4 py-3 text-white/50 text-xs">{intent.id}</td>
|
||||
<td className="px-4 py-3">
|
||||
<span className="text-xs px-2 py-0.5 rounded-full" style={{ background: "rgba(138,71,229,0.15)", color: "#a855f7" }}>
|
||||
{chainNames[intent.fromChainId] ?? `Chain ${intent.fromChainId}`}
|
||||
</span>
|
||||
</td>
|
||||
<td className="px-4 py-3">{formatAddress(intent.senderAddress ?? null)}</td>
|
||||
<td className="px-4 py-3">{formatAddress(intent.xicReceiveAddress)}</td>
|
||||
<td className="px-4 py-3 text-white/80">
|
||||
{intent.expectedUsdt ? `$${intent.expectedUsdt.toLocaleString()}` : <span className="text-white/30">—</span>}
|
||||
</td>
|
||||
<td className="px-4 py-3">
|
||||
<span className={`text-xs px-2 py-0.5 rounded-full ${intent.matched ? "text-green-400" : "text-yellow-400"}`}
|
||||
style={{ background: intent.matched ? "rgba(0,230,118,0.1)" : "rgba(240,180,41,0.1)" }}>
|
||||
{intent.matched ? "Matched" : "Pending"}
|
||||
</span>
|
||||
</td>
|
||||
<td className="px-4 py-3 text-white/30 text-xs">{formatDate(intent.createdAt)}</td>
|
||||
</tr>
|
||||
);
|
||||
}) : (
|
||||
<tr><td colSpan={7} className="px-4 py-8 text-center text-white/30 text-sm">No bridge intents yet</td></tr>
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Bridge Orders */}
|
||||
<div className="rounded-2xl overflow-hidden" style={{ background: "rgba(255,255,255,0.02)", border: "1px solid rgba(255,255,255,0.06)" }}>
|
||||
<div className="px-5 py-4 flex items-center justify-between" style={{ borderBottom: "1px solid rgba(255,255,255,0.06)" }}>
|
||||
<h3 className="font-semibold text-white/80" style={{ fontFamily: "'Space Grotesk', sans-serif" }}>
|
||||
Bridge Orders
|
||||
<span className="text-white/40 text-sm ml-2">(confirmed USDT deposits awaiting XIC distribution)</span>
|
||||
</h3>
|
||||
{/* Status Filter */}
|
||||
<div className="flex gap-2">
|
||||
{(["all", "pending", "confirmed", "distributed", "failed"] as const).map(s => (
|
||||
<button
|
||||
key={s}
|
||||
onClick={() => { setBridgeStatusFilter(s); setBridgePage(1); }}
|
||||
className="px-2 py-1 rounded-lg text-xs font-medium transition-all"
|
||||
style={{
|
||||
background: bridgeStatusFilter === s ? "rgba(138,71,229,0.2)" : "rgba(255,255,255,0.04)",
|
||||
border: bridgeStatusFilter === s ? "1px solid rgba(138,71,229,0.4)" : "1px solid rgba(255,255,255,0.08)",
|
||||
color: bridgeStatusFilter === s ? "#a855f7" : "rgba(255,255,255,0.4)",
|
||||
}}
|
||||
>
|
||||
{s}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
{bridgeLoading ? (
|
||||
<div className="flex justify-center py-12"><span className="text-white/30">Loading...</span></div>
|
||||
) : (
|
||||
<div className="overflow-x-auto">
|
||||
<table className="w-full text-sm">
|
||||
<thead>
|
||||
<tr style={{ borderBottom: "1px solid rgba(255,255,255,0.06)" }}>
|
||||
{["ID", "TX Hash", "Chain", "Sender", "USDT", "XIC", "XIC Receive Addr", "Status", "Distribute TX", "Action"].map(h => (
|
||||
<th key={h} className="px-4 py-3 text-left text-xs font-semibold uppercase tracking-wider text-white/30">{h}</th>
|
||||
))}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{bridgeData?.orders && bridgeData.orders.length > 0 ? bridgeData.orders.map((order, i) => {
|
||||
const chainNames: Record<number, string> = { 56: "BSC", 1: "ETH", 137: "POLY", 42161: "ARB", 43114: "AVAX" };
|
||||
const isUpdating = updatingBridgeId === order.id;
|
||||
return (
|
||||
<tr key={order.id} style={{ borderBottom: i < bridgeData.orders.length - 1 ? "1px solid rgba(255,255,255,0.04)" : "none" }}>
|
||||
<td className="px-4 py-3 text-white/50 text-xs">{order.id}</td>
|
||||
<td className="px-4 py-3">{formatAddress(order.txHash)}</td>
|
||||
<td className="px-4 py-3">
|
||||
<span className="text-xs px-2 py-0.5 rounded-full" style={{ background: "rgba(138,71,229,0.15)", color: "#a855f7" }}>
|
||||
{chainNames[order.fromChainId] ?? `Chain ${order.fromChainId}`}
|
||||
</span>
|
||||
</td>
|
||||
<td className="px-4 py-3">{formatAddress(order.walletAddress)}</td>
|
||||
<td className="px-4 py-3 text-amber-400 font-semibold">${order.fromAmount.toLocaleString()}</td>
|
||||
<td className="px-4 py-3 text-cyan-400 font-semibold">{order.toAmount.toLocaleString(undefined, { maximumFractionDigits: 0 })} XIC</td>
|
||||
<td className="px-4 py-3">{formatAddress(order.xicReceiveAddress ?? null)}</td>
|
||||
<td className="px-4 py-3"><StatusBadge status={order.status as any} /></td>
|
||||
<td className="px-4 py-3">
|
||||
{order.status !== "distributed" ? (
|
||||
<input
|
||||
type="text"
|
||||
value={bridgeDistribTxInput[order.id] || ""}
|
||||
onChange={e => 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)
|
||||
)}
|
||||
</td>
|
||||
<td className="px-4 py-3">
|
||||
{order.status === "confirmed" && (
|
||||
<button
|
||||
onClick={() => handleUpdateBridgeOrder(order.id, "distributed")}
|
||||
disabled={isUpdating}
|
||||
className="px-3 py-1.5 rounded-lg text-xs font-semibold transition-all"
|
||||
style={{
|
||||
background: isUpdating ? "rgba(0,230,118,0.1)" : "rgba(0,230,118,0.2)",
|
||||
border: "1px solid rgba(0,230,118,0.4)",
|
||||
color: "#00e676",
|
||||
cursor: isUpdating ? "not-allowed" : "pointer",
|
||||
}}
|
||||
>
|
||||
{isUpdating ? "..." : "Mark Distributed"}
|
||||
</button>
|
||||
)}
|
||||
{order.status === "pending" && (
|
||||
<button
|
||||
onClick={() => handleUpdateBridgeOrder(order.id, "confirmed")}
|
||||
disabled={isUpdating}
|
||||
className="px-3 py-1.5 rounded-lg text-xs font-semibold transition-all"
|
||||
style={{
|
||||
background: "rgba(0,212,255,0.15)",
|
||||
border: "1px solid rgba(0,212,255,0.3)",
|
||||
color: "#00d4ff",
|
||||
}}
|
||||
>
|
||||
{isUpdating ? "..." : "Confirm"}
|
||||
</button>
|
||||
)}
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
}) : (
|
||||
<tr><td colSpan={10} className="px-4 py-8 text-center text-white/30 text-sm">No bridge orders yet</td></tr>
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
)}
|
||||
{/* Pagination */}
|
||||
{bridgeData && bridgeData.total > 20 && (
|
||||
<div className="flex items-center justify-between px-5 py-4" style={{ borderTop: "1px solid rgba(255,255,255,0.06)" }}>
|
||||
<span className="text-xs text-white/30">{bridgeData.total} total orders</span>
|
||||
<div className="flex gap-2">
|
||||
<button
|
||||
onClick={() => setBridgePage(p => Math.max(1, p - 1))}
|
||||
disabled={bridgePage === 1}
|
||||
className="px-3 py-1.5 rounded-lg text-xs transition-all"
|
||||
style={{ background: "rgba(255,255,255,0.06)", border: "1px solid rgba(255,255,255,0.1)", color: bridgePage === 1 ? "rgba(255,255,255,0.2)" : "rgba(255,255,255,0.6)" }}
|
||||
>
|
||||
← Prev
|
||||
</button>
|
||||
<span className="px-3 py-1.5 text-xs text-white/40">Page {bridgePage} / {Math.ceil(bridgeData.total / 20)}</span>
|
||||
<button
|
||||
onClick={() => setBridgePage(p => p + 1)}
|
||||
disabled={bridgePage >= Math.ceil(bridgeData.total / 20)}
|
||||
className="px-3 py-1.5 rounded-lg text-xs transition-all"
|
||||
style={{ background: "rgba(255,255,255,0.06)", border: "1px solid rgba(255,255,255,0.1)", color: bridgePage >= Math.ceil(bridgeData.total / 20) ? "rgba(255,255,255,0.2)" : "rgba(255,255,255,0.6)" }}
|
||||
>
|
||||
Next →
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* ── Site Settings Panel ── */}
|
||||
{activeTab === "settings" && (
|
||||
<SettingsPanel token={token} />
|
||||
|
|
|
|||
|
|
@ -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 ─────────────────────────────────────────────────── */}
|
||||
<p className="text-xs text-white/25 text-center leading-relaxed">{t.disclaimer}</p>
|
||||
</div>
|
||||
|
||||
{/* ─── Wallet Selector Modal ───────────────────────────────────────────── */}
|
||||
{showWalletSelector && (
|
||||
<WalletSelector
|
||||
lang={lang}
|
||||
onAddressDetected={(address: string) => {
|
||||
setXicReceiveAddress(address);
|
||||
setShowWalletSelector(false);
|
||||
}}
|
||||
connectedAddress={wallet.address || undefined}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
{/* ─── Wallet Selector Modal (Portal) ───────────────────────────────── */}
|
||||
{showWalletSelector && createPortal(
|
||||
<div
|
||||
className="fixed inset-0 z-[9999] flex items-end sm:items-center justify-center sm:p-4"
|
||||
style={{ background: "rgba(0,0,0,0.85)", backdropFilter: "blur(8px)" }}
|
||||
onClick={(e) => { if (e.target === e.currentTarget) setShowWalletSelector(false); }}
|
||||
>
|
||||
<div
|
||||
className="w-full sm:max-w-md rounded-t-2xl sm:rounded-2xl p-5 relative overflow-y-auto"
|
||||
style={{ background: "rgba(10,10,20,0.98)", border: "1px solid rgba(240,180,41,0.3)", boxShadow: "0 0 40px rgba(240,180,41,0.15)", maxHeight: "85vh" }}
|
||||
>
|
||||
<button
|
||||
onClick={() => setShowWalletSelector(false)}
|
||||
className="absolute top-4 right-4 text-white/40 hover:text-white/80 transition-colors"
|
||||
>
|
||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
||||
<path d="M18 6L6 18M6 6l12 12"/>
|
||||
</svg>
|
||||
</button>
|
||||
<h3 className="text-lg font-bold text-white mb-1" style={{ fontFamily: "'Space Grotesk', sans-serif" }}>
|
||||
{lang === "zh" ? "连接钱包" : "Connect Wallet"}
|
||||
</h3>
|
||||
<p className="text-xs text-white/40 mb-4">
|
||||
{lang === "zh" ? "连接钱包后自动填入XIC接收地址" : "Your wallet address will be auto-filled as XIC receive address"}
|
||||
</p>
|
||||
<WalletSelector
|
||||
lang={lang}
|
||||
compact={false}
|
||||
showTron={false}
|
||||
connectedAddress={wallet.address || undefined}
|
||||
onAddressDetected={async (address: string, _network, rawProvider) => {
|
||||
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)}`);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>,
|
||||
document.body
|
||||
)} </div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<number>`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({
|
||||
|
|
|
|||
9
todo.md
9
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库
|
||||
|
|
|
|||
Loading…
Reference in New Issue