Checkpoint: v7: 多钱包列表选择器(MetaMask/Trust Wallet/OKX/Coinbase/TokenPocket 检测+连接+安装引导),集成到 BSC/ETH 面板和 TRON 面板;重写 useWallet hook 支持所有主流 EVM 钱包自动识别;文案"EVM 地址"→"XIC 接收地址";后台一键开启/关闭预售功能;已部署到 pre-sale.newassetchain.io

This commit is contained in:
Manus 2026-03-09 01:34:56 -04:00
parent 809b6327b8
commit 81c9b2544f
6 changed files with 736 additions and 144 deletions

View File

@ -0,0 +1,372 @@
// NAC XIC Presale — WalletSelector Component
// Detects installed EVM wallets and shows connect/install buttons for each
import { useState, useEffect } from "react";
type Lang = "zh" | "en";
interface WalletInfo {
id: string;
name: string;
icon: React.ReactNode;
installUrl: string;
isInstalled: () => boolean;
connect: () => Promise<string | null>;
}
interface WalletSelectorProps {
lang: Lang;
onAddressDetected: (address: string) => void;
connectedAddress?: string;
compact?: boolean; // compact mode for BSC/ETH panel
}
// ── Wallet Icons ──────────────────────────────────────────────────────────────
const MetaMaskIcon = () => (
<svg width="24" height="24" viewBox="0 0 35 33" fill="none">
<path d="M32.96 1L19.4 10.7l2.5-5.9L32.96 1z" fill="#E17726" stroke="#E17726" strokeWidth="0.25" strokeLinecap="round" strokeLinejoin="round"/>
<path d="M2.04 1l13.46 9.8-2.38-5.99L2.04 1z" fill="#E27625" stroke="#E27625" strokeWidth="0.25" strokeLinecap="round" strokeLinejoin="round"/>
<path d="M28.22 23.53l-3.61 5.53 7.73 2.13 2.22-7.54-6.34-.12z" fill="#E27625" stroke="#E27625" strokeWidth="0.25" strokeLinecap="round" strokeLinejoin="round"/>
<path d="M.44 23.65l2.2 7.54 7.72-2.13-3.6-5.53-6.32.12z" fill="#E27625" stroke="#E27625" strokeWidth="0.25" strokeLinecap="round" strokeLinejoin="round"/>
<path d="M9.97 14.46l-2.16 3.26 7.69.35-.26-8.27-5.27 4.66z" fill="#E27625" stroke="#E27625" strokeWidth="0.25" strokeLinecap="round" strokeLinejoin="round"/>
<path d="M25.03 14.46l-5.35-4.75-.17 8.36 7.68-.35-2.16-3.26z" fill="#E27625" stroke="#E27625" strokeWidth="0.25" strokeLinecap="round" strokeLinejoin="round"/>
<path d="M10.36 29.06l4.63-2.24-3.99-3.11-.64 5.35z" fill="#E27625" stroke="#E27625" strokeWidth="0.25" strokeLinecap="round" strokeLinejoin="round"/>
<path d="M20.01 26.82l4.63 2.24-.64-5.35-3.99 3.11z" fill="#E27625" stroke="#E27625" strokeWidth="0.25" strokeLinecap="round" strokeLinejoin="round"/>
</svg>
);
const TrustWalletIcon = () => (
<svg width="24" height="24" viewBox="0 0 24 24" fill="none">
<circle cx="12" cy="12" r="12" fill="#3375BB"/>
<path d="M12 4.5L6 7.5v5c0 3.31 2.57 6.41 6 7.5 3.43-1.09 6-4.19 6-7.5v-5L12 4.5z" fill="white" fillOpacity="0.9"/>
<path d="M10.5 12.5l1.5 1.5 3-3" stroke="#3375BB" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round"/>
</svg>
);
const OKXIcon = () => (
<svg width="24" height="24" viewBox="0 0 24 24" fill="none">
<rect width="24" height="24" rx="6" fill="#000"/>
<rect x="4" y="4" width="6" height="6" rx="1" fill="white"/>
<rect x="14" y="4" width="6" height="6" rx="1" fill="white"/>
<rect x="4" y="14" width="6" height="6" rx="1" fill="white"/>
<rect x="14" y="14" width="6" height="6" rx="1" fill="white"/>
<rect x="9" y="9" width="6" height="6" rx="1" fill="white"/>
</svg>
);
const CoinbaseIcon = () => (
<svg width="24" height="24" viewBox="0 0 24 24" fill="none">
<circle cx="12" cy="12" r="12" fill="#0052FF"/>
<circle cx="12" cy="12" r="7" fill="white"/>
<rect x="9" y="10.5" width="6" height="3" rx="1.5" fill="#0052FF"/>
</svg>
);
const TokenPocketIcon = () => (
<svg width="24" height="24" viewBox="0 0 24 24" fill="none">
<rect width="24" height="24" rx="6" fill="#2980FE"/>
<path d="M7 8h5a3 3 0 0 1 0 6H7V8z" fill="white"/>
<rect x="7" y="15" width="2.5" height="3" rx="1" fill="white"/>
</svg>
);
const BitgetIcon = () => (
<svg width="24" height="24" viewBox="0 0 24 24" fill="none">
<rect width="24" height="24" rx="6" fill="#00F0FF"/>
<path d="M7 8h5a3 3 0 0 1 0 6H7V8z" fill="#000"/>
<path d="M12 14h2a3 3 0 0 1 0 6h-2v-6z" fill="#000"/>
</svg>
);
// ── Provider detection helpers ────────────────────────────────────────────────
type EthProvider = {
isMetaMask?: boolean;
isTrust?: boolean;
isTrustWallet?: boolean;
isOKExWallet?: boolean;
isOkxWallet?: boolean;
isCoinbaseWallet?: boolean;
isTokenPocket?: boolean;
isBitkeep?: boolean;
isBitgetWallet?: boolean;
providers?: EthProvider[];
request: (args: { method: string; params?: unknown[] }) => Promise<unknown>;
};
function getEth(): EthProvider | null {
if (typeof window === "undefined") return null;
return (window as unknown as { ethereum?: EthProvider }).ethereum ?? null;
}
function getOKX(): EthProvider | null {
if (typeof window === "undefined") return null;
return (window as unknown as { okxwallet?: EthProvider }).okxwallet ?? null;
}
function getBitget(): EthProvider | null {
if (typeof window === "undefined") return null;
const w = window as unknown as { bitkeep?: { ethereum?: EthProvider } };
return w.bitkeep?.ethereum ?? null;
}
// Find a specific provider from the providers array or direct injection
function findProvider(predicate: (p: EthProvider) => boolean): EthProvider | null {
const eth = getEth();
if (!eth) return null;
if (eth.providers && Array.isArray(eth.providers)) {
return eth.providers.find(predicate) ?? null;
}
return predicate(eth) ? eth : null;
}
async function requestAccounts(provider: EthProvider): Promise<string | null> {
try {
const accounts = await provider.request({ method: "eth_requestAccounts" }) as string[];
return accounts?.[0] ?? null;
} catch {
return null;
}
}
// ── Wallet definitions ────────────────────────────────────────────────────────
function buildWallets(): WalletInfo[] {
return [
{
id: "metamask",
name: "MetaMask",
icon: <MetaMaskIcon />,
installUrl: "https://metamask.io/download/",
isInstalled: () => !!findProvider(p => !!p.isMetaMask),
connect: async () => {
const p = findProvider(p => !!p.isMetaMask) ?? getEth();
return p ? requestAccounts(p) : null;
},
},
{
id: "trust",
name: "Trust Wallet",
icon: <TrustWalletIcon />,
installUrl: "https://trustwallet.com/download",
isInstalled: () => !!findProvider(p => !!(p.isTrust || p.isTrustWallet)),
connect: async () => {
const p = findProvider(p => !!(p.isTrust || p.isTrustWallet)) ?? getEth();
return p ? requestAccounts(p) : null;
},
},
{
id: "okx",
name: "OKX Wallet",
icon: <OKXIcon />,
installUrl: "https://www.okx.com/web3",
isInstalled: () => !!(getOKX() || findProvider(p => !!(p.isOKExWallet || p.isOkxWallet))),
connect: async () => {
const p = getOKX() ?? findProvider(p => !!(p.isOKExWallet || p.isOkxWallet));
return p ? requestAccounts(p) : null;
},
},
{
id: "coinbase",
name: "Coinbase Wallet",
icon: <CoinbaseIcon />,
installUrl: "https://www.coinbase.com/wallet/downloads",
isInstalled: () => !!findProvider(p => !!p.isCoinbaseWallet),
connect: async () => {
const p = findProvider(p => !!p.isCoinbaseWallet) ?? getEth();
return p ? requestAccounts(p) : null;
},
},
{
id: "tokenpocket",
name: "TokenPocket",
icon: <TokenPocketIcon />,
installUrl: "https://www.tokenpocket.pro/en/download/app",
isInstalled: () => !!findProvider(p => !!p.isTokenPocket),
connect: async () => {
const p = findProvider(p => !!p.isTokenPocket) ?? getEth();
return p ? requestAccounts(p) : null;
},
},
{
id: "bitget",
name: "Bitget Wallet",
icon: <BitgetIcon />,
installUrl: "https://web3.bitget.com/en/wallet-download",
isInstalled: () => !!(getBitget() || findProvider(p => !!(p.isBitkeep || p.isBitgetWallet))),
connect: async () => {
const p = getBitget() ?? findProvider(p => !!(p.isBitkeep || p.isBitgetWallet));
return p ? requestAccounts(p) : null;
},
},
];
}
// ── WalletSelector Component ──────────────────────────────────────────────────
export function WalletSelector({ lang, onAddressDetected, connectedAddress, compact = false }: WalletSelectorProps) {
const [wallets, setWallets] = useState<WalletInfo[]>([]);
const [connecting, setConnecting] = useState<string | null>(null);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
// Build wallet list after a short delay to allow extensions to inject
const timer = setTimeout(() => {
setWallets(buildWallets());
}, 400);
return () => clearTimeout(timer);
}, []);
const handleConnect = async (wallet: WalletInfo) => {
setConnecting(wallet.id);
setError(null);
try {
const address = await wallet.connect();
if (address) {
onAddressDetected(address);
} else {
setError(lang === "zh" ? "未获取到地址,请重试" : "No address returned, please try again");
}
} catch {
setError(lang === "zh" ? "连接失败,请重试" : "Connection failed, please try again");
} finally {
setConnecting(null);
}
};
const installedWallets = wallets.filter(w => w.isInstalled());
const notInstalledWallets = wallets.filter(w => !w.isInstalled());
// If connected address is already set, show compact confirmation
if (connectedAddress) {
return (
<div
className="rounded-xl p-3 flex items-center gap-3"
style={{ background: "rgba(0,230,118,0.08)", border: "1px solid rgba(0,230,118,0.25)" }}
>
<div className="w-2 h-2 rounded-full bg-green-400 flex-shrink-0" />
<div className="flex-1 min-w-0">
<p className="text-xs text-green-400 font-semibold">
{lang === "zh" ? "钱包已连接" : "Wallet Connected"}
</p>
<p className="text-xs text-white/50 font-mono truncate">{connectedAddress}</p>
</div>
</div>
);
}
return (
<div className="space-y-3">
<p className="text-xs font-semibold text-white/60 uppercase tracking-wider">
{lang === "zh" ? "选择钱包自动填充地址" : "Select wallet to auto-fill address"}
</p>
{/* Installed wallets */}
{installedWallets.length > 0 && (
<div className="space-y-2">
{installedWallets.map(wallet => (
<button
key={wallet.id}
onClick={() => handleConnect(wallet)}
disabled={connecting === wallet.id}
className="w-full flex items-center gap-3 px-4 py-3 rounded-xl transition-all hover:opacity-90 active:scale-[0.98]"
style={{
background: "rgba(0,212,255,0.08)",
border: "1px solid rgba(0,212,255,0.3)",
}}
>
<span className="flex-shrink-0">{wallet.icon}</span>
<span className="flex-1 text-left text-sm font-semibold text-white">{wallet.name}</span>
<span
className="text-xs px-2 py-0.5 rounded-full font-medium"
style={{ background: "rgba(0,212,255,0.15)", color: "#00d4ff" }}
>
{lang === "zh" ? "已安装" : "Installed"}
</span>
{connecting === wallet.id ? (
<svg className="animate-spin w-4 h-4 text-white/60 flex-shrink-0" fill="none" viewBox="0 0 24 24">
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"/>
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z"/>
</svg>
) : (
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="rgba(0,212,255,0.7)" strokeWidth="2" className="flex-shrink-0">
<path d="M5 12h14M12 5l7 7-7 7"/>
</svg>
)}
</button>
))}
</div>
)}
{/* No wallets installed */}
{installedWallets.length === 0 && (
<div
className="rounded-xl p-4 text-center"
style={{ background: "rgba(255,255,255,0.04)", border: "1px dashed rgba(255,255,255,0.15)" }}
>
<p className="text-sm text-white/50 mb-1">
{lang === "zh" ? "未检测到 EVM 钱包" : "No EVM wallet detected"}
</p>
<p className="text-xs text-white/30">
{lang === "zh" ? "请安装以下任一钱包后刷新页面" : "Install any wallet below and refresh the page"}
</p>
</div>
)}
{/* Not-installed wallets — show install links */}
{!compact && notInstalledWallets.length > 0 && (
<div className="space-y-1">
<p className="text-xs text-white/30 mt-2">
{lang === "zh" ? "未安装(点击安装)" : "Not installed (click to install)"}
</p>
<div className="grid grid-cols-3 gap-2">
{notInstalledWallets.map(wallet => (
<a
key={wallet.id}
href={wallet.installUrl}
target="_blank"
rel="noopener noreferrer"
className="flex flex-col items-center gap-1.5 p-2.5 rounded-xl transition-all hover:opacity-80"
style={{
background: "rgba(255,255,255,0.04)",
border: "1px solid rgba(255,255,255,0.08)",
}}
>
<span className="opacity-40">{wallet.icon}</span>
<span className="text-xs text-white/30 text-center leading-tight">{wallet.name}</span>
</a>
))}
</div>
</div>
)}
{/* In compact mode, show install links inline */}
{compact && notInstalledWallets.length > 0 && installedWallets.length === 0 && (
<div className="flex flex-wrap gap-2">
{notInstalledWallets.slice(0, 4).map(wallet => (
<a
key={wallet.id}
href={wallet.installUrl}
target="_blank"
rel="noopener noreferrer"
className="flex items-center gap-1.5 px-3 py-1.5 rounded-lg text-xs transition-all hover:opacity-80"
style={{
background: "rgba(255,255,255,0.05)",
border: "1px solid rgba(255,255,255,0.1)",
color: "rgba(255,255,255,0.4)",
}}
>
<span className="opacity-50">{wallet.icon}</span>
{lang === "zh" ? `安装 ${wallet.name}` : `Install ${wallet.name}`}
</a>
))}
</div>
)}
{error && (
<p className="text-xs text-red-400 text-center">{error}</p>
)}
</div>
);
}

View File

@ -1,7 +1,8 @@
// NAC XIC Presale — Wallet Connection Hook // NAC XIC Presale — Wallet Connection Hook
// Supports MetaMask / any EVM-compatible wallet (BSC + ETH) // Supports MetaMask, Trust Wallet, OKX Wallet, Coinbase Wallet, and all EVM-compatible wallets
// Robust auto-detect with retry, multi-provider support, and graceful fallback
import { useState, useEffect, useCallback } from "react"; import { useState, useEffect, useCallback, useRef } from "react";
import { BrowserProvider, JsonRpcSigner, Eip1193Provider } from "ethers"; import { BrowserProvider, JsonRpcSigner, Eip1193Provider } from "ethers";
import { shortenAddress, switchToNetwork } from "@/lib/contracts"; import { shortenAddress, switchToNetwork } from "@/lib/contracts";
@ -29,118 +30,246 @@ const INITIAL_STATE: WalletState = {
error: null, error: null,
}; };
// Detect the best available EVM provider across all major wallets
function detectProvider(): Eip1193Provider | null {
if (typeof window === "undefined") return null;
// Check for multiple injected providers (e.g., MetaMask + Coinbase both installed)
const w = window as unknown as Record<string, unknown>;
const eth = w.ethereum as (Eip1193Provider & { providers?: Eip1193Provider[]; isMetaMask?: boolean; isTrust?: boolean; isOKExWallet?: boolean; isCoinbaseWallet?: boolean }) | undefined;
if (!eth) {
// Fallback: check wallet-specific globals
if (w.okxwallet) return w.okxwallet as Eip1193Provider;
if (w.coinbaseWalletExtension) return w.coinbaseWalletExtension as Eip1193Provider;
return null;
}
// If multiple providers are injected (common when multiple extensions installed)
if (eth.providers && Array.isArray(eth.providers) && eth.providers.length > 0) {
// Prefer MetaMask if available, otherwise use first provider
const metamask = eth.providers.find((p: Eip1193Provider & { isMetaMask?: boolean }) => p.isMetaMask);
return metamask ?? eth.providers[0];
}
return eth;
}
// Build wallet state from a provider and accounts
async function buildWalletState(
rawProvider: Eip1193Provider,
address: string
): Promise<Partial<WalletState>> {
const provider = new BrowserProvider(rawProvider);
let chainId: number | null = null;
let signer: JsonRpcSigner | null = null;
try {
const network = await provider.getNetwork();
chainId = Number(network.chainId);
} catch {
// Some wallets don't support getNetwork immediately — try eth_chainId directly
try {
const chainHex = await (rawProvider as { request: (args: { method: string }) => Promise<string> }).request({ method: "eth_chainId" });
chainId = parseInt(chainHex, 16);
} catch {
chainId = null;
}
}
try {
signer = await provider.getSigner();
} catch {
// getSigner may fail on some wallets before full connection — that's OK
signer = null;
}
return {
address,
shortAddress: shortenAddress(address),
isConnected: true,
chainId,
provider,
signer,
isConnecting: false,
error: null,
};
}
export function useWallet() { export function useWallet() {
const [state, setState] = useState<WalletState>(INITIAL_STATE); const [state, setState] = useState<WalletState>(INITIAL_STATE);
const retryRef = useRef<ReturnType<typeof setTimeout> | null>(null);
const mountedRef = useRef(true);
useEffect(() => {
mountedRef.current = true;
return () => {
mountedRef.current = false;
if (retryRef.current) clearTimeout(retryRef.current);
};
}, []);
// ── Connect (explicit user action) ─────────────────────────────────────────
const connect = useCallback(async () => { const connect = useCallback(async () => {
if (!window.ethereum) { const rawProvider = detectProvider();
setState(s => ({ ...s, error: "Please install MetaMask or a compatible wallet." })); if (!rawProvider) {
setState(s => ({ ...s, error: "请安装 MetaMask 或其他 EVM 兼容钱包 / Please install MetaMask or a compatible wallet." }));
return; return;
} }
setState(s => ({ ...s, isConnecting: true, error: null })); setState(s => ({ ...s, isConnecting: true, error: null }));
try { try {
const provider = new BrowserProvider(window.ethereum as Eip1193Provider); // Request accounts — this triggers the wallet popup
const accounts = await provider.send("eth_requestAccounts", []); const accounts = await (rawProvider as { request: (args: { method: string; params?: unknown[] }) => Promise<string[]> }).request({
const network = await provider.getNetwork(); method: "eth_requestAccounts",
const signer = await provider.getSigner(); params: [],
const address = accounts[0] as string;
setState({
address,
shortAddress: shortenAddress(address),
isConnected: true,
chainId: Number(network.chainId),
provider,
signer,
isConnecting: false,
error: null,
}); });
if (!accounts || accounts.length === 0) throw new Error("No accounts returned");
const partial = await buildWalletState(rawProvider, accounts[0]);
if (mountedRef.current) setState({ ...INITIAL_STATE, ...partial });
} catch (err: unknown) { } catch (err: unknown) {
setState(s => ({ const msg = (err as Error).message || "Failed to connect wallet";
...s, if (mountedRef.current) setState(s => ({ ...s, isConnecting: false, error: msg }));
isConnecting: false,
error: (err as Error).message || "Failed to connect wallet",
}));
} }
}, []); }, []);
// ── Disconnect ──────────────────────────────────────────────────────────────
const disconnect = useCallback(() => { const disconnect = useCallback(() => {
setState(INITIAL_STATE); setState(INITIAL_STATE);
}, []); }, []);
// ── Switch Network ──────────────────────────────────────────────────────────
const switchNetwork = useCallback(async (chainId: number) => { const switchNetwork = useCallback(async (chainId: number) => {
try { try {
await switchToNetwork(chainId); await switchToNetwork(chainId);
if (window.ethereum) { const rawProvider = detectProvider();
const provider = new BrowserProvider(window.ethereum as Eip1193Provider); if (rawProvider) {
const provider = new BrowserProvider(rawProvider);
const network = await provider.getNetwork(); const network = await provider.getNetwork();
const signer = await provider.getSigner(); let signer: JsonRpcSigner | null = null;
setState(s => ({ try { signer = await provider.getSigner(); } catch { /* ignore */ }
...s, if (mountedRef.current) {
chainId: Number(network.chainId), setState(s => ({
provider, ...s,
signer,
error: null,
}));
}
} catch (err: unknown) {
setState(s => ({ ...s, error: (err as Error).message }));
}
}, []);
// Auto-detect already-connected wallet on page load (silent, no popup)
useEffect(() => {
const autoDetect = async () => {
if (!window.ethereum) return;
try {
const provider = new BrowserProvider(window.ethereum as Eip1193Provider);
// Use eth_accounts (not eth_requestAccounts) — silent, no popup
const accounts = await provider.send("eth_accounts", []);
if (accounts && accounts.length > 0) {
const network = await provider.getNetwork();
const signer = await provider.getSigner();
const address = accounts[0] as string;
setState({
address,
shortAddress: shortenAddress(address),
isConnected: true,
chainId: Number(network.chainId), chainId: Number(network.chainId),
provider, provider,
signer, signer,
isConnecting: false,
error: null, error: null,
}); }));
}
}
} catch (err: unknown) {
if (mountedRef.current) setState(s => ({ ...s, error: (err as Error).message }));
}
}, []);
// ── Auto-detect on page load (silent, no popup) ─────────────────────────────
useEffect(() => {
let cancelled = false;
const tryAutoDetect = async (attempt: number) => {
if (cancelled) return;
const rawProvider = detectProvider();
if (!rawProvider) {
// Wallet extension may not be injected yet — retry up to 3 times
if (attempt < 3) {
retryRef.current = setTimeout(() => tryAutoDetect(attempt + 1), 800 * attempt);
}
return;
}
try {
const accounts = await (rawProvider as { request: (args: { method: string }) => Promise<string[]> }).request({
method: "eth_accounts", // Silent — no popup
});
if (cancelled) return;
if (accounts && accounts.length > 0) {
const partial = await buildWalletState(rawProvider, accounts[0]);
if (!cancelled && mountedRef.current) {
setState({ ...INITIAL_STATE, ...partial });
}
} else if (attempt < 3) {
// Accounts empty — wallet might not have finished loading, retry
retryRef.current = setTimeout(() => tryAutoDetect(attempt + 1), 1000 * attempt);
} }
} catch { } catch {
// Silently ignore — user hasn't connected yet // Silently ignore — user hasn't connected yet
} }
}; };
autoDetect();
// Small initial delay to let wallet extensions inject themselves
retryRef.current = setTimeout(() => tryAutoDetect(1), 300);
return () => {
cancelled = true;
if (retryRef.current) clearTimeout(retryRef.current);
};
}, []); }, []);
// Listen for account/chain changes // ── Listen for account / chain changes ─────────────────────────────────────
useEffect(() => { useEffect(() => {
if (!window.ethereum) return; const rawProvider = detectProvider();
const handleAccountsChanged = (accounts: unknown) => { if (!rawProvider) return;
const eth = rawProvider as {
on?: (event: string, handler: (data: unknown) => void) => void;
removeListener?: (event: string, handler: (data: unknown) => void) => void;
};
if (!eth.on) return;
const handleAccountsChanged = async (accounts: unknown) => {
const accs = accounts as string[]; const accs = accounts as string[];
if (accs.length === 0) { if (!mountedRef.current) return;
if (!accs || accs.length === 0) {
setState(INITIAL_STATE); setState(INITIAL_STATE);
} else { } else {
setState(s => ({ // Re-build full state with new address
...s, try {
address: accs[0], const partial = await buildWalletState(rawProvider, accs[0]);
shortAddress: shortenAddress(accs[0]), if (mountedRef.current) setState({ ...INITIAL_STATE, ...partial });
isConnected: true, } catch {
})); if (mountedRef.current) {
setState(s => ({
...s,
address: accs[0],
shortAddress: shortenAddress(accs[0]),
isConnected: true,
}));
}
}
} }
}; };
const handleChainChanged = () => {
window.location.reload(); const handleChainChanged = async () => {
if (!mountedRef.current) return;
// Re-fetch network info instead of reloading the page
try {
const provider = new BrowserProvider(rawProvider);
const network = await provider.getNetwork();
let signer: JsonRpcSigner | null = null;
try { signer = await provider.getSigner(); } catch { /* ignore */ }
if (mountedRef.current) {
setState(s => ({
...s,
chainId: Number(network.chainId),
provider,
signer,
}));
}
} catch {
// If we can't get network info, reload as last resort
window.location.reload();
}
}; };
window.ethereum.on("accountsChanged", handleAccountsChanged);
window.ethereum.on("chainChanged", handleChainChanged); eth.on("accountsChanged", handleAccountsChanged);
eth.on("chainChanged", handleChainChanged);
return () => { return () => {
window.ethereum?.removeListener("accountsChanged", handleAccountsChanged); if (eth.removeListener) {
window.ethereum?.removeListener("chainChanged", handleChainChanged); eth.removeListener("accountsChanged", handleAccountsChanged);
eth.removeListener("chainChanged", handleChainChanged);
}
}; };
}, []); }, []);

View File

@ -130,19 +130,31 @@ function SettingsPanel({ token }: { token: string }) {
const [telegramStatus, setTelegramStatus] = useState<"idle" | "testing" | "success" | "error">("idle"); const [telegramStatus, setTelegramStatus] = useState<"idle" | "testing" | "success" | "error">("idle");
const [telegramError, setTelegramError] = useState(""); const [telegramError, setTelegramError] = useState("");
// ── Presale Active/Paused Toggle ──────────────────────────────────────────
const isPresaleLive = (configData?.find(c => c.key === "presaleStatus")?.value ?? "live") === "live";
const [togglingPresale, setTogglingPresale] = useState(false);
const setConfigMutation = trpc.admin.setConfig.useMutation({ const setConfigMutation = trpc.admin.setConfig.useMutation({
onSuccess: (_, vars) => { onSuccess: (_, vars) => {
setSavedKeys(prev => { const s = new Set(Array.from(prev)); s.add(vars.key); return s; }); setSavedKeys(prev => { const s = new Set(Array.from(prev)); s.add(vars.key); return s; });
setSavingKey(null); setSavingKey(null);
setTogglingPresale(false);
refetchConfig(); refetchConfig();
setTimeout(() => setSavedKeys(prev => { const n = new Set(Array.from(prev)); n.delete(vars.key); return n; }), 2000); setTimeout(() => setSavedKeys(prev => { const n = new Set(Array.from(prev)); n.delete(vars.key); return n; }), 2000);
}, },
onError: (err) => { onError: (err) => {
setSavingKey(null); setSavingKey(null);
setTogglingPresale(false);
alert(`Save failed: ${err.message}`); alert(`Save failed: ${err.message}`);
}, },
}); });
const handleTogglePresale = () => {
const newStatus = isPresaleLive ? "paused" : "live";
setTogglingPresale(true);
setConfigMutation.mutate({ token, key: "presaleStatus", value: newStatus });
};
const testTelegramMutation = trpc.admin.testTelegram.useMutation({ const testTelegramMutation = trpc.admin.testTelegram.useMutation({
onSuccess: () => { onSuccess: () => {
setTelegramStatus("success"); setTelegramStatus("success");
@ -269,6 +281,66 @@ function SettingsPanel({ token }: { token: string }) {
return ( return (
<div className="space-y-6"> <div className="space-y-6">
{/* ── Presale Active Toggle ── */}
<div
className="rounded-2xl p-6"
style={{
background: isPresaleLive ? "rgba(0,230,118,0.06)" : "rgba(255,60,60,0.06)",
border: isPresaleLive ? "2px solid rgba(0,230,118,0.45)" : "2px solid rgba(255,60,60,0.45)",
}}
>
<div className="flex items-center justify-between gap-4 flex-wrap">
<div>
<div className="flex items-center gap-3 mb-1">
<div
className="w-3 h-3 rounded-full"
style={{
background: isPresaleLive ? "#00e676" : "#ff4444",
boxShadow: isPresaleLive ? "0 0 8px rgba(0,230,118,0.8)" : "0 0 8px rgba(255,68,68,0.8)",
animation: isPresaleLive ? "pulse 2s infinite" : "none",
}}
/>
<h3
className="text-lg font-bold"
style={{
color: isPresaleLive ? "#00e676" : "#ff6060",
fontFamily: "'Space Grotesk', sans-serif",
}}
>
{isPresaleLive ? "预售进行中 PRESALE LIVE" : "预售已暂停 PRESALE PAUSED"}
</h3>
</div>
<p className="text-xs ml-6" style={{ color: "rgba(255,255,255,0.45)" }}>
{isPresaleLive
? "用户当前可正常购买 XIC 代币。点击《暂停预售》可立即封禁所有购买入口。"
: "预售已暂停,首页购买按钮已禁用。点击《开启预售》可重新开放购买。"}
</p>
</div>
<button
onClick={handleTogglePresale}
disabled={togglingPresale}
className="flex-shrink-0 px-8 py-3 rounded-xl font-bold text-base transition-all"
style={{
background: isPresaleLive
? "linear-gradient(135deg, rgba(255,60,60,0.25) 0%, rgba(255,60,60,0.15) 100%)"
: "linear-gradient(135deg, rgba(0,230,118,0.25) 0%, rgba(0,230,118,0.15) 100%)",
border: isPresaleLive ? "1.5px solid rgba(255,60,60,0.6)" : "1.5px solid rgba(0,230,118,0.6)",
color: isPresaleLive ? "#ff6060" : "#00e676",
fontFamily: "'Space Grotesk', sans-serif",
opacity: togglingPresale ? 0.6 : 1,
cursor: togglingPresale ? "not-allowed" : "pointer",
letterSpacing: "0.03em",
}}
>
{togglingPresale
? "处理中..."
: isPresaleLive
? "⏸ 暂停预售"
: "▶ 开启预售"}
</button>
</div>
</div>
{/* Presale Parameters */} {/* Presale Parameters */}
<div className="rounded-2xl p-5" style={{ background: "rgba(255,255,255,0.02)", border: "1px solid rgba(240,180,41,0.15)" }}> <div className="rounded-2xl p-5" style={{ background: "rgba(255,255,255,0.02)", border: "1px solid rgba(240,180,41,0.15)" }}>
<h3 className="text-sm font-semibold mb-4" style={{ color: "#f0b429" }}>Presale Parameters </h3> <h3 className="text-sm font-semibold mb-4" style={{ color: "#f0b429" }}>Presale Parameters </h3>

View File

@ -11,6 +11,7 @@ import { usePresale } from "@/hooks/usePresale";
import { CONTRACTS, PRESALE_CONFIG, formatNumber, shortenAddress } from "@/lib/contracts"; import { CONTRACTS, PRESALE_CONFIG, formatNumber, shortenAddress } from "@/lib/contracts";
import { trpc } from "@/lib/trpc"; import { trpc } from "@/lib/trpc";
import { type Lang, useTranslation } from "@/lib/i18n"; import { type Lang, useTranslation } from "@/lib/i18n";
import { WalletSelector } from "@/components/WalletSelector";
// ─── Network Tab Types ──────────────────────────────────────────────────────── // ─── Network Tab Types ────────────────────────────────────────────────────────
type NetworkTab = "BSC" | "ETH" | "TRON"; type NetworkTab = "BSC" | "ETH" | "TRON";
@ -111,9 +112,6 @@ function TRC20Panel({ usdtAmount, lang, connectedAddress, onConnectWallet }: { u
const [evmAddress, setEvmAddress] = useState(connectedAddress || ""); const [evmAddress, setEvmAddress] = useState(connectedAddress || "");
const [evmAddrError, setEvmAddrError] = useState(""); const [evmAddrError, setEvmAddrError] = useState("");
const [submitted, setSubmitted] = useState(false); const [submitted, setSubmitted] = useState(false);
const [isAutoConnecting, setIsAutoConnecting] = useState(false);
const hasEthereum = typeof window !== "undefined" && !!window.ethereum;
// TronLink detection state // TronLink detection state
const [tronAddress, setTronAddress] = useState<string | null>(null); const [tronAddress, setTronAddress] = useState<string | null>(null);
const [isTronConnecting, setIsTronConnecting] = useState(false); const [isTronConnecting, setIsTronConnecting] = useState(false);
@ -164,30 +162,10 @@ function TRC20Panel({ usdtAmount, lang, connectedAddress, onConnectWallet }: { u
} }
}, [connectedAddress, submitted]); }, [connectedAddress, submitted]);
// Auto-connect EVM wallet from within TRC20 panel
const handleAutoConnectEVM = async () => {
if (!window.ethereum) return;
setIsAutoConnecting(true);
try {
const accounts = await window.ethereum.request({ method: "eth_requestAccounts" }) as string[];
if (accounts && accounts.length > 0 && !submitted) {
setEvmAddress(accounts[0]);
setEvmAddrError("");
toast.success(lang === "zh" ? "EVM地址已自动填充" : "EVM address auto-filled!");
}
// Also notify parent to update wallet state
if (onConnectWallet) onConnectWallet();
} catch {
// User rejected or error — silently ignore
} finally {
setIsAutoConnecting(false);
}
};
const submitTrc20Mutation = trpc.presale.registerTrc20Intent.useMutation({ const submitTrc20Mutation = trpc.presale.registerTrc20Intent.useMutation({
onSuccess: () => { onSuccess: () => {
setSubmitted(true); setSubmitted(true);
toast.success(lang === "zh" ? "EVM地址已保存" : "EVM address saved!"); toast.success(lang === "zh" ? "XIC接收地址已保存" : "XIC receiving address saved!");
}, },
onError: (err: { message: string }) => { onError: (err: { message: string }) => {
toast.error(err.message); toast.error(err.message);
@ -202,8 +180,8 @@ function TRC20Panel({ usdtAmount, lang, connectedAddress, onConnectWallet }: { u
}; };
const validateEvmAddress = (addr: string) => { const validateEvmAddress = (addr: string) => {
if (!addr) return lang === "zh" ? "请输入您的EVM地址" : "Please enter your EVM address"; if (!addr) return lang === "zh" ? "请输入您的XIC接收地址" : "Please enter your XIC receiving address";
if (!/^0x[0-9a-fA-F]{40}$/.test(addr)) return lang === "zh" ? "无效的EVM地址格式应以0x开头共42位" : "Invalid EVM address format (must start with 0x, 42 chars)"; if (!/^0x[0-9a-fA-F]{40}$/.test(addr)) return lang === "zh" ? "无效的XIC接收地址格式应以0x开头入42位" : "Invalid XIC receiving address format (must start with 0x, 42 chars)";
return ""; return "";
}; };
@ -221,42 +199,33 @@ function TRC20Panel({ usdtAmount, lang, connectedAddress, onConnectWallet }: { u
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<span className="text-amber-400 text-sm"></span> <span className="text-amber-400 text-sm"></span>
<p className="text-sm font-semibold text-amber-300"> <p className="text-sm font-semibold text-amber-300">
{lang === "zh" ? "必填:您的EVM钱包地址用于接收XIC代币" : "Required: Your EVM Wallet Address (to receive XIC tokens)"} {lang === "zh" ? "必填:您的XIC接收地址BSC/ETH钉包地址" : "Required: Your XIC Receiving Address (BSC/ETH wallet address)"}
</p> </p>
</div> </div>
<p className="text-xs text-white/50"> <p className="text-xs text-white/50">
{lang === "zh" {lang === "zh"
? "XIC代币在BSC网络上发放。请提供您的BSC/ETH地址0x开头以便我们将代币发送给您。" ? "XIC代币将发放到您的BSC/ETH钉包地址0x开头。请确保填写正确的地址否则无法收到代币。"
: "XIC tokens are distributed on BSC. Please provide your BSC/ETH address (starts with 0x) so we can send your tokens."} : "XIC tokens will be sent to your BSC/ETH wallet address (starts with 0x). Please make sure to enter the correct address."}
</p> </p>
<div className="space-y-2"> <div className="space-y-2">
{/* Auto-connect button — shown when MetaMask is available but address not yet filled */} {/* WalletSelector — shown when address not yet filled */}
{hasEthereum && !connectedAddress && !evmAddress && ( {!evmAddress && !submitted && (
<button <WalletSelector
onClick={handleAutoConnectEVM} lang={lang}
disabled={isAutoConnecting} connectedAddress={connectedAddress}
className="w-full py-2.5 rounded-xl text-sm font-semibold flex items-center justify-center gap-2 transition-all hover:opacity-90" onAddressDetected={(addr) => {
style={{ setEvmAddress(addr);
background: "rgba(0,212,255,0.12)", setEvmAddrError("");
border: "1px solid rgba(0,212,255,0.35)", toast.success(lang === "zh" ? "XIC接收地址已自动填充" : "XIC receiving address auto-filled!");
color: "#00d4ff", if (onConnectWallet) onConnectWallet();
}} }}
> />
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
<path d="M21 12V7H5a2 2 0 0 1 0-4h14v4"/>
<path d="M3 5v14a2 2 0 0 0 2 2h16v-5"/>
<path d="M18 12a2 2 0 0 0 0 4h4v-4z"/>
</svg>
{isAutoConnecting
? (lang === "zh" ? "连接中..." : "Connecting...")
: (lang === "zh" ? "连接MetaMask自动填充地址" : "Connect MetaMask to auto-fill")}
</button>
)} )}
<input <input
type="text" type="text"
value={evmAddress} value={evmAddress}
onChange={e => { setEvmAddress(e.target.value); setEvmAddrError(""); setSubmitted(false); }} onChange={e => { setEvmAddress(e.target.value); setEvmAddrError(""); setSubmitted(false); }}
placeholder={lang === "zh" ? "0x... (您的BSC/ETH地址)" : "0x... (your BSC/ETH address)"} placeholder={lang === "zh" ? "0x... (您的XIC接收地址)" : "0x... (your XIC receiving address)"}
className="w-full px-4 py-3 rounded-xl text-sm font-mono" className="w-full px-4 py-3 rounded-xl text-sm font-mono"
style={{ style={{
background: "rgba(255,255,255,0.05)", background: "rgba(255,255,255,0.05)",
@ -266,7 +235,7 @@ function TRC20Panel({ usdtAmount, lang, connectedAddress, onConnectWallet }: { u
}} }}
/> />
{evmAddrError && <p className="text-xs text-red-400">{evmAddrError}</p>} {evmAddrError && <p className="text-xs text-red-400">{evmAddrError}</p>}
{submitted && <p className="text-xs text-green-400"> {lang === "zh" ? "EVM地址已保存" : "EVM address saved"}</p>} {submitted && <p className="text-xs text-green-400"> {lang === "zh" ? "XIC接收地址已保存" : "XIC receiving address saved"}</p>}
<button <button
onClick={handleEvmSubmit} onClick={handleEvmSubmit}
disabled={submitTrc20Mutation.isPending || submitted || !evmAddress} disabled={submitTrc20Mutation.isPending || submitted || !evmAddress}
@ -278,7 +247,7 @@ function TRC20Panel({ usdtAmount, lang, connectedAddress, onConnectWallet }: { u
opacity: !evmAddress ? 0.5 : 1, opacity: !evmAddress ? 0.5 : 1,
}} }}
> >
{submitTrc20Mutation.isPending ? (lang === "zh" ? "保存中..." : "Saving...") : submitted ? (lang === "zh" ? "✓ 已保存" : "✓ Saved") : (lang === "zh" ? "保存EVM地址" : "Save EVM Address")} {submitTrc20Mutation.isPending ? (lang === "zh" ? "保存中..." : "Saving...") : submitted ? (lang === "zh" ? "✓ 已保存" : "✓ Saved") : (lang === "zh" ? "保存XIC接收地址" : "Save XIC Receiving Address")}
</button> </button>
</div> </div>
</div> </div>
@ -308,8 +277,8 @@ function TRC20Panel({ usdtAmount, lang, connectedAddress, onConnectWallet }: { u
</div> </div>
<p className="text-xs text-white/40"> <p className="text-xs text-white/40">
{lang === "zh" {lang === "zh"
? "您的 TronLink 已连接。请在上方填写 EVM 地址以接收 XIC 代币,然后向下方地址发送 USDT。" ? "您的 TronLink 已连接。请在上方填写 XIC 接收地址,然后向下方地址发送 USDT。"
: "TronLink connected. Please fill your EVM address above to receive XIC tokens, then send USDT to the address below."} : "TronLink connected. Please fill your XIC receiving address above, then send USDT to the address below."}
</p> </p>
</div> </div>
) : ( ) : (
@ -378,8 +347,8 @@ function TRC20Panel({ usdtAmount, lang, connectedAddress, onConnectWallet }: { u
<div className="space-y-2"> <div className="space-y-2">
<StepBadge num={1} text={ <StepBadge num={1} text={
lang === "zh" lang === "zh"
? `在上方填写您的EVM地址并保存` ? `在上方填写您的XIC接收地址并保存`
: `Enter and save your EVM address above` : `Enter and save your XIC receiving address above`
} /> } />
<StepBadge num={2} text={ <StepBadge num={2} text={
lang === "zh" lang === "zh"
@ -447,18 +416,17 @@ function EVMPurchasePanel({ network, lang, wallet }: { network: "BSC" | "ETH"; l
if (!wallet.isConnected) { if (!wallet.isConnected) {
return ( return (
<div className="space-y-4"> <div className="space-y-4">
<div className="text-center py-6"> <p className="text-sm text-white/60 text-center">{t("buy_connect_msg")}</p>
<div className="text-4xl mb-3">🔗</div> <WalletSelector
<p className="text-white/60 text-sm mb-4">{t("buy_connect_msg")}</p> lang={lang}
<button connectedAddress={wallet.address ?? undefined}
onClick={wallet.connect} onAddressDetected={(addr) => {
disabled={wallet.isConnecting} // Address detected — wallet is now connected, trigger wallet.connect to sync state
className="btn-primary-nac w-full py-3 rounded-xl text-base font-bold pulse-amber" wallet.connect();
style={{ fontFamily: "'Space Grotesk', sans-serif" }} toast.success(lang === "zh" ? `已连接: ${addr.slice(0, 6)}...${addr.slice(-4)}` : `Connected: ${addr.slice(0, 6)}...${addr.slice(-4)}`);
> }}
{wallet.isConnecting ? t("nav_connecting") : t("buy_connect_btn")} compact
</button> />
</div>
<div className="text-xs text-white/40 text-center">{t("buy_connect_hint")}</div> <div className="text-xs text-white/40 text-center">{t("buy_connect_hint")}</div>
</div> </div>
); );
@ -963,6 +931,8 @@ export default function Home() {
const stats = onChainStats || FALLBACK_STATS; const stats = onChainStats || FALLBACK_STATS;
const progressPct = stats.progressPct || 0; const progressPct = stats.progressPct || 0;
// Presale active/paused status from backend config
const isPresalePaused = (onChainStats as any)?.presaleStatus === "paused";
// 钱包状态提升到顶层共享给NavWalletButton和EVMPurchasePanel // 钱包状态提升到顶层共享给NavWalletButton和EVMPurchasePanel
const wallet = useWallet(); const wallet = useWallet();
@ -971,6 +941,21 @@ export default function Home() {
return ( return (
<div className="min-h-screen" style={{ background: "#0a0a0f" }}> <div className="min-h-screen" style={{ background: "#0a0a0f" }}>
{/* ── Presale Paused Banner ── */}
{isPresalePaused && (
<div
className="fixed top-0 left-0 right-0 z-[60] flex items-center justify-center gap-3 py-3 px-4 text-sm font-bold"
style={{
background: "linear-gradient(90deg, rgba(255,60,60,0.95) 0%, rgba(200,0,0,0.95) 100%)",
color: "white",
letterSpacing: "0.05em",
}}
>
<span className="text-lg"></span>
<span>{lang === "zh" ? "预售活动已暂停,暂时无法购买。请关注官方渠道获取最新公告。" : "Presale is currently paused. Please follow our official channels for updates."}</span>
<span className="text-lg"></span>
</div>
)}
{/* ── Navigation ── */} {/* ── Navigation ── */}
<nav className="fixed top-0 left-0 right-0 z-50 flex items-center justify-between px-6 py-4" style={{ background: "rgba(10,10,15,0.9)", borderBottom: "1px solid rgba(240,180,41,0.1)", backdropFilter: "blur(12px)" }}> <nav className="fixed top-0 left-0 right-0 z-50 flex items-center justify-between px-6 py-4" style={{ background: "rgba(10,10,15,0.9)", borderBottom: "1px solid rgba(240,180,41,0.1)", backdropFilter: "blur(12px)" }}>
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
@ -1155,7 +1140,26 @@ export default function Home() {
</div> </div>
{/* Purchase Area */} {/* Purchase Area */}
<div> <div className="relative">
{/* Presale Paused Overlay */}
{isPresalePaused && (
<div
className="absolute inset-0 z-10 flex flex-col items-center justify-center rounded-2xl gap-3"
style={{
background: "rgba(10,10,15,0.88)",
border: "1.5px solid rgba(255,60,60,0.4)",
backdropFilter: "blur(4px)",
}}
>
<div className="text-4xl"></div>
<p className="text-base font-bold" style={{ color: "#ff6060", fontFamily: "'Space Grotesk', sans-serif" }}>
{lang === "zh" ? "预售已暂停" : "Presale Paused"}
</p>
<p className="text-xs text-white/50 text-center px-4">
{lang === "zh" ? "请关注官方 Telegram / Twitter 获取最新公告" : "Follow our official Telegram / Twitter for updates"}
</p>
</div>
)}
{activeNetwork === "BSC" && <EVMPurchasePanel network="BSC" lang={lang} wallet={wallet} />} {activeNetwork === "BSC" && <EVMPurchasePanel network="BSC" lang={lang} wallet={wallet} />}
{activeNetwork === "ETH" && <EVMPurchasePanel network="ETH" lang={lang} wallet={wallet} />} {activeNetwork === "ETH" && <EVMPurchasePanel network="ETH" lang={lang} wallet={wallet} />}
{activeNetwork === "TRON" && ( {activeNetwork === "TRON" && (

View File

@ -30,7 +30,10 @@ export const appRouter = router({
presale: router({ presale: router({
// Combined stats from BSC + ETH + TRC20 // Combined stats from BSC + ETH + TRC20
stats: publicProcedure.query(async () => { stats: publicProcedure.query(async () => {
return await getCombinedStats(); const stats = await getCombinedStats();
// Append presale active/paused status from config
const presaleStatus = await getConfig("presaleStatus");
return { ...stats, presaleStatus: presaleStatus ?? "live" };
}), }),
// Single chain stats // Single chain stats

12
todo.md
View File

@ -59,3 +59,15 @@
- [x] 更新 XIC 代币合约地址为 0x59ff34dd59680a7125782b1f6df2a86ed46f5a24 - [x] 更新 XIC 代币合约地址为 0x59ff34dd59680a7125782b1f6df2a86ed46f5a24
- [x] 为 TRON 标签添加 TronLink 钱包检测并自动填充 TRON 接收地址 - [x] 为 TRON 标签添加 TronLink 钱包检测并自动填充 TRON 接收地址
- [x] 构建并部署到备份服务器 pre-sale.newassetchain.io - [x] 构建并部署到备份服务器 pre-sale.newassetchain.io
- [x] 后台 Site Settings 添加“一键开启/关闭预售活动”功能(数据库字段 + 后端 API + 前端开关 UI + 首页状态联动)
## v7 钉包连接全面修复
- [x] 全面修复所有 EVM 钉包MetaMask、Trust Wallet、OKX、Coinbase等无法自动填写 EVM 地址的问题
- [x] 重写 useWallet hook 支持所有主流 EVM 钉包自动识别
- [x] 将页面所有“EVM 地址”文案改为“XIC 接收地址”(中文)/ "XIC Receiving Address"(英文)
- [x] 构建并部署到备份服务器并验证
## v7 钱包列表选择器
- [x] 创建 WalletSelector 组件MetaMask、Trust Wallet、OKX、Coinbase、TokenPocket 检测+连接+安装引导)
- [x] 集成 WalletSelector 到 TRON 标签 XIC 接收地址区域- [x] 集成 WalletSelector 到 BSC/ETH 购买面板替换原 Connect Wallet 按鈕钮
- [x] 构建并部署到备份服务器