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:
parent
809b6327b8
commit
81c9b2544f
|
|
@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -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);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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" && (
|
||||||
|
|
|
||||||
|
|
@ -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
12
todo.md
|
|
@ -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] 构建并部署到备份服务器
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue