Compare commits
7 Commits
ca5883ace8
...
1576303898
| Author | SHA1 | Date |
|---|---|---|
|
|
1576303898 | |
|
|
4bdb118cb2 | |
|
|
2eff084785 | |
|
|
889068d7f5 | |
|
|
1d0e293bdb | |
|
|
a7aa132b71 | |
|
|
dd24e6ba13 |
|
|
@ -7,6 +7,7 @@ import { ThemeProvider } from "./contexts/ThemeContext";
|
|||
import Home from "./pages/Home";
|
||||
import Tutorial from "./pages/Tutorial";
|
||||
import Admin from "./pages/Admin";
|
||||
import Bridge from "./pages/Bridge";
|
||||
|
||||
function Router() {
|
||||
// make sure to consider if you need authentication for certain routes
|
||||
|
|
@ -15,6 +16,7 @@ function Router() {
|
|||
<Route path={"/"} component={Home} />
|
||||
<Route path={"/tutorial"} component={Tutorial} />
|
||||
<Route path={"/admin"} component={Admin} />
|
||||
<Route path={"/bridge"} component={Bridge} />
|
||||
<Route path={"/404"} component={NotFound} />
|
||||
{/* Final fallback route */}
|
||||
<Route component={NotFound} />
|
||||
|
|
|
|||
|
|
@ -1,8 +1,9 @@
|
|||
// NAC XIC Presale — Wallet Selector Component
|
||||
// Detects installed EVM wallets and shows connect/install buttons for each
|
||||
// v4: added TronLink support (desktop window.tronLink + mobile DeepLink)
|
||||
// v5: improved error handling, MetaMask permission reset guide, connection diagnostics
|
||||
|
||||
import { useState, useEffect, useCallback } from "react";
|
||||
import type { EthProvider } from "@/hooks/useWallet";
|
||||
|
||||
type Lang = "zh" | "en";
|
||||
|
||||
|
|
@ -11,18 +12,18 @@ interface WalletInfo {
|
|||
name: string;
|
||||
icon: React.ReactNode;
|
||||
installUrl: string;
|
||||
mobileDeepLink?: string; // DeepLink to open current page in wallet's in-app browser
|
||||
mobileDeepLink?: string;
|
||||
isInstalled: () => boolean;
|
||||
connect: () => Promise<string | null>;
|
||||
network: "evm" | "tron"; // wallet network type
|
||||
connect: () => Promise<{ address: string; rawProvider: EthProvider } | null>;
|
||||
network: "evm" | "tron";
|
||||
}
|
||||
|
||||
interface WalletSelectorProps {
|
||||
lang: Lang;
|
||||
onAddressDetected: (address: string, network?: "evm" | "tron") => void;
|
||||
onAddressDetected: (address: string, network?: "evm" | "tron", rawProvider?: EthProvider) => void;
|
||||
connectedAddress?: string;
|
||||
compact?: boolean; // compact mode for BSC/ETH panel
|
||||
showTron?: boolean; // whether to show TRON wallets (for TRC20 panel)
|
||||
compact?: boolean;
|
||||
showTron?: boolean;
|
||||
}
|
||||
|
||||
// ── Wallet Icons ──────────────────────────────────────────────────────────────
|
||||
|
|
@ -83,7 +84,6 @@ const BitgetIcon = () => (
|
|||
</svg>
|
||||
);
|
||||
|
||||
// TronLink Icon — official TRON red color
|
||||
const TronLinkIcon = () => (
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none">
|
||||
<circle cx="12" cy="12" r="12" fill="#FF0013"/>
|
||||
|
|
@ -100,7 +100,6 @@ function isMobileBrowser(): boolean {
|
|||
return /Android|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent);
|
||||
}
|
||||
|
||||
// Check if running inside a wallet's in-app browser
|
||||
function isInWalletBrowser(): boolean {
|
||||
if (typeof window === "undefined") return false;
|
||||
const ua = navigator.userAgent.toLowerCase();
|
||||
|
|
@ -125,20 +124,6 @@ function isInWalletBrowser(): boolean {
|
|||
|
||||
// ── 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>;
|
||||
};
|
||||
|
||||
type TronLinkProvider = {
|
||||
ready: boolean;
|
||||
tronWeb?: {
|
||||
|
|
@ -167,9 +152,7 @@ function getBitget(): EthProvider | null {
|
|||
function getTronLink(): TronLinkProvider | null {
|
||||
if (typeof window === "undefined") return null;
|
||||
const w = window as unknown as { tronLink?: TronLinkProvider; tronWeb?: TronLinkProvider["tronWeb"] };
|
||||
// TronLink injects window.tronLink; tronWeb is also available when connected
|
||||
if (w.tronLink?.ready) return w.tronLink;
|
||||
// Some versions only inject tronWeb
|
||||
if (w.tronWeb) {
|
||||
return {
|
||||
ready: true,
|
||||
|
|
@ -180,7 +163,6 @@ function getTronLink(): TronLinkProvider | null {
|
|||
return 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;
|
||||
|
|
@ -190,30 +172,49 @@ function findProvider(predicate: (p: EthProvider) => boolean): EthProvider | nul
|
|||
return predicate(eth) ? eth : null;
|
||||
}
|
||||
|
||||
async function requestAccounts(provider: EthProvider): Promise<string | null> {
|
||||
// ── Improved requestAccounts with better error classification ─────────────────
|
||||
async function requestAccounts(provider: EthProvider): Promise<{ address: string; rawProvider: EthProvider } | null> {
|
||||
try {
|
||||
// First try eth_accounts (silent) to check if already connected
|
||||
const existingAccounts = await provider.request({ method: "eth_accounts" }) as string[];
|
||||
if (existingAccounts && existingAccounts.length > 0) {
|
||||
return { address: existingAccounts[0], rawProvider: provider };
|
||||
}
|
||||
} catch {
|
||||
// Ignore - proceed to eth_requestAccounts
|
||||
}
|
||||
|
||||
try {
|
||||
const accounts = await provider.request({ method: "eth_requestAccounts" }) as string[];
|
||||
return accounts?.[0] ?? null;
|
||||
const address = accounts?.[0] ?? null;
|
||||
if (!address) return null;
|
||||
return { address, rawProvider: provider };
|
||||
} catch (err: unknown) {
|
||||
const error = err as { code?: number; message?: string };
|
||||
console.log("[WalletSelector] requestAccounts error:", error.code, error.message);
|
||||
if (error?.code === 4001) throw new Error("user_rejected");
|
||||
if (error?.code === -32002) throw new Error("wallet_pending");
|
||||
// Some wallets throw with message instead of code
|
||||
if (error?.message?.toLowerCase().includes("user rejected") ||
|
||||
error?.message?.toLowerCase().includes("user denied") ||
|
||||
error?.message?.toLowerCase().includes("cancelled")) {
|
||||
throw new Error("user_rejected");
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
async function requestTronAccounts(provider: TronLinkProvider): Promise<string | null> {
|
||||
async function requestTronAccounts(provider: TronLinkProvider): Promise<{ address: string; rawProvider: EthProvider } | null> {
|
||||
try {
|
||||
// TronLink v1: use tronWeb.defaultAddress
|
||||
if (provider.tronWeb?.defaultAddress?.base58) {
|
||||
return provider.tronWeb.defaultAddress.base58;
|
||||
return { address: provider.tronWeb.defaultAddress.base58, rawProvider: provider as unknown as EthProvider };
|
||||
}
|
||||
// TronLink v2+: use request method
|
||||
const result = await provider.request({ method: "tron_requestAccounts" }) as { code?: number; message?: string };
|
||||
if (result?.code === 200) {
|
||||
// After approval, tronWeb.defaultAddress should be populated
|
||||
const w = window as unknown as { tronWeb?: { defaultAddress?: { base58?: string } } };
|
||||
return w.tronWeb?.defaultAddress?.base58 ?? null;
|
||||
const address = w.tronWeb?.defaultAddress?.base58 ?? null;
|
||||
if (!address) return null;
|
||||
return { address, rawProvider: provider as unknown as EthProvider };
|
||||
}
|
||||
if (result?.code === 4001) throw new Error("user_rejected");
|
||||
return null;
|
||||
|
|
@ -314,7 +315,6 @@ function buildWallets(showTron: boolean): WalletInfo[] {
|
|||
name: "TronLink",
|
||||
icon: <TronLinkIcon />,
|
||||
installUrl: "https://www.tronlink.org/",
|
||||
// TronLink mobile DeepLink — opens current URL in TronLink's built-in browser
|
||||
mobileDeepLink: `tronlinkoutside://pull.activity?param=${encodeURIComponent(JSON.stringify({ url: currentUrl, action: "open", protocol: "tronlink", version: "1.0" }))}`,
|
||||
isInstalled: () => !!getTronLink(),
|
||||
connect: async () => {
|
||||
|
|
@ -339,44 +339,29 @@ function isValidTronAddress(addr: string): boolean {
|
|||
return /^T[1-9A-HJ-NP-Za-km-z]{33}$/.test(addr);
|
||||
}
|
||||
|
||||
function isValidAddress(addr: string): boolean {
|
||||
return isValidEthAddress(addr) || isValidTronAddress(addr);
|
||||
}
|
||||
|
||||
// ── Mobile DeepLink Panel ─────────────────────────────────────────────────────
|
||||
|
||||
function MobileDeepLinkPanel({ lang, showTron }: { lang: Lang; showTron: boolean }) {
|
||||
const currentUrl = typeof window !== "undefined" ? window.location.href : "https://pre-sale.newassetchain.io";
|
||||
const urlWithoutProtocol = currentUrl.replace(/^https?:\/\//, "");
|
||||
const [showMore, setShowMore] = useState(false);
|
||||
|
||||
const tronWallets = [
|
||||
{
|
||||
const metamaskDeepLink = `https://metamask.app.link/dapp/${urlWithoutProtocol}`;
|
||||
|
||||
const otherWallets = [
|
||||
...( showTron ? [{
|
||||
id: "tronlink",
|
||||
name: "TronLink",
|
||||
icon: <TronLinkIcon />,
|
||||
deepLink: `tronlinkoutside://pull.activity?param=${encodeURIComponent(JSON.stringify({ url: currentUrl, action: "open", protocol: "tronlink", version: "1.0" }))}`,
|
||||
installUrl: "https://www.tronlink.org/",
|
||||
badge: "TRON",
|
||||
badgeColor: "#FF0013",
|
||||
},
|
||||
];
|
||||
|
||||
const evmWallets = [
|
||||
{
|
||||
id: "metamask",
|
||||
name: "MetaMask",
|
||||
icon: <MetaMaskIcon />,
|
||||
deepLink: `https://metamask.app.link/dapp/${urlWithoutProtocol}`,
|
||||
installUrl: "https://metamask.io/download/",
|
||||
badge: "EVM",
|
||||
badgeColor: "#E27625",
|
||||
},
|
||||
}] : []),
|
||||
{
|
||||
id: "trust",
|
||||
name: "Trust Wallet",
|
||||
icon: <TrustWalletIcon />,
|
||||
deepLink: `https://link.trustwallet.com/open_url?coin_id=60&url=${encodeURIComponent(currentUrl)}`,
|
||||
installUrl: "https://trustwallet.com/download",
|
||||
badge: "EVM",
|
||||
badgeColor: "#3375BB",
|
||||
},
|
||||
|
|
@ -385,7 +370,6 @@ function MobileDeepLinkPanel({ lang, showTron }: { lang: Lang; showTron: boolean
|
|||
name: "OKX Wallet",
|
||||
icon: <OKXIcon />,
|
||||
deepLink: `okx://wallet/dapp/url?dappUrl=${encodeURIComponent(currentUrl)}`,
|
||||
installUrl: "https://www.okx.com/web3",
|
||||
badge: "EVM",
|
||||
badgeColor: "#00F0FF",
|
||||
},
|
||||
|
|
@ -394,90 +378,236 @@ function MobileDeepLinkPanel({ lang, showTron }: { lang: Lang; showTron: boolean
|
|||
name: "TokenPocket",
|
||||
icon: <TokenPocketIcon />,
|
||||
deepLink: `tpdapp://open?params=${encodeURIComponent(JSON.stringify({ url: currentUrl, chain: "ETH", source: "NAC-Presale" }))}`,
|
||||
installUrl: "https://www.tokenpocket.pro/en/download/app",
|
||||
badge: "EVM/TRON",
|
||||
badgeColor: "#2980FE",
|
||||
},
|
||||
];
|
||||
|
||||
const walletList = showTron ? [...tronWallets, ...evmWallets] : evmWallets;
|
||||
|
||||
return (
|
||||
<div className="space-y-3">
|
||||
{/* Mobile guidance header */}
|
||||
<div
|
||||
className="rounded-xl p-4"
|
||||
style={{ background: "rgba(240,180,41,0.08)", border: "1px solid rgba(240,180,41,0.25)" }}
|
||||
<a
|
||||
href={metamaskDeepLink}
|
||||
className="w-full flex items-center gap-3 px-4 py-4 rounded-2xl transition-all hover:opacity-90 active:scale-[0.98] block"
|
||||
style={{
|
||||
background: "linear-gradient(135deg, rgba(226,118,37,0.18) 0%, rgba(240,180,41,0.12) 100%)",
|
||||
border: "1.5px solid rgba(226,118,37,0.5)",
|
||||
boxShadow: "0 0 20px rgba(226,118,37,0.15)",
|
||||
}}
|
||||
>
|
||||
<div className="flex items-start gap-3">
|
||||
<span className="text-xl flex-shrink-0">📱</span>
|
||||
<MetaMaskIcon />
|
||||
<div className="flex-1">
|
||||
<p className="text-sm font-bold text-white">MetaMask</p>
|
||||
<p className="text-xs text-white/40">
|
||||
{lang === "zh" ? "在 MetaMask 内置浏览器中打开" : "Open in MetaMask browser"}
|
||||
</p>
|
||||
</div>
|
||||
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="rgba(226,118,37,0.8)" strokeWidth="2" className="flex-shrink-0">
|
||||
<path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6"/>
|
||||
<polyline points="15 3 21 3 21 9"/>
|
||||
<line x1="10" y1="14" x2="21" y2="3"/>
|
||||
</svg>
|
||||
</a>
|
||||
|
||||
<button
|
||||
onClick={() => setShowMore(v => !v)}
|
||||
className="w-full text-xs text-white/35 hover:text-white/55 transition-colors py-1 flex items-center justify-center gap-1"
|
||||
>
|
||||
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"
|
||||
style={{ transform: showMore ? "rotate(180deg)" : "none", transition: "transform 0.2s" }}>
|
||||
<path d="M6 9l6 6 6-6"/>
|
||||
</svg>
|
||||
{showMore
|
||||
? (lang === "zh" ? "收起其他钱包" : "Hide other wallets")
|
||||
: (lang === "zh" ? "其他钱包(Trust / OKX / TokenPocket)" : "Other wallets (Trust / OKX / TokenPocket)")}
|
||||
</button>
|
||||
|
||||
{showMore && (
|
||||
<div className="space-y-2">
|
||||
{otherWallets.map(wallet => (
|
||||
<a
|
||||
key={wallet.id}
|
||||
href={wallet.deepLink}
|
||||
className="w-full flex items-center gap-3 px-4 py-3 rounded-xl transition-all hover:opacity-90 active:scale-[0.98] block"
|
||||
style={{
|
||||
background: "rgba(0,212,255,0.05)",
|
||||
border: "1px solid rgba(0,212,255,0.18)",
|
||||
}}
|
||||
>
|
||||
<span className="flex-shrink-0">{wallet.icon}</span>
|
||||
<span className="flex-1 text-sm font-semibold text-white">{wallet.name}</span>
|
||||
<span
|
||||
className="text-xs px-2 py-0.5 rounded-full font-medium flex-shrink-0"
|
||||
style={{ background: `${wallet.badgeColor}22`, color: wallet.badgeColor, border: `1px solid ${wallet.badgeColor}44` }}
|
||||
>
|
||||
{wallet.badge}
|
||||
</span>
|
||||
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="rgba(0,212,255,0.5)" strokeWidth="2" className="flex-shrink-0">
|
||||
<path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6"/>
|
||||
<polyline points="15 3 21 3 21 9"/>
|
||||
<line x1="10" y1="14" x2="21" y2="3"/>
|
||||
</svg>
|
||||
</a>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// ── Error Help Panel ──────────────────────────────────────────────────────────
|
||||
// Shows specific troubleshooting steps based on error type
|
||||
|
||||
function ErrorHelpPanel({ errorType, walletName, lang, onRetry }: {
|
||||
errorType: "user_rejected" | "wallet_pending" | "not_initialized" | "unknown";
|
||||
walletName: string;
|
||||
lang: Lang;
|
||||
onRetry: () => void;
|
||||
}) {
|
||||
const isZh = lang === "zh";
|
||||
|
||||
if (errorType === "user_rejected") {
|
||||
return (
|
||||
<div
|
||||
className="rounded-xl p-4 space-y-3"
|
||||
style={{ background: "rgba(255,80,80,0.06)", border: "1px solid rgba(255,80,80,0.2)" }}
|
||||
>
|
||||
<div className="flex items-start gap-2">
|
||||
<span className="text-red-400 text-base flex-shrink-0">✗</span>
|
||||
<div>
|
||||
<p className="text-sm font-semibold text-amber-300 mb-1">
|
||||
{lang === "zh" ? "手机端连接钱包" : "Connect Wallet on Mobile"}
|
||||
<p className="text-sm font-semibold text-red-400">
|
||||
{isZh ? "连接被拒绝" : "Connection Rejected"}
|
||||
</p>
|
||||
<p className="text-xs text-white/50 leading-relaxed">
|
||||
{lang === "zh"
|
||||
? "手机浏览器不支持钱包扩展。请选择以下任一钱包 App,在其内置浏览器中打开本页面即可连接钱包。"
|
||||
: "Mobile browsers don't support wallet extensions. Open this page in a wallet app's built-in browser to connect."}
|
||||
<p className="text-xs text-white/50 mt-1">
|
||||
{isZh
|
||||
? `${walletName} 拒绝了连接请求。可能原因:`
|
||||
: `${walletName} rejected the connection. Possible reasons:`}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Wallet DeepLink buttons */}
|
||||
<div className="space-y-2">
|
||||
<p className="text-xs text-white/40 text-center">
|
||||
{lang === "zh" ? "选择钱包 App 打开本页面" : "Choose a wallet app to open this page"}
|
||||
</p>
|
||||
{walletList.map(wallet => (
|
||||
<a
|
||||
key={wallet.id}
|
||||
href={wallet.deepLink}
|
||||
className="w-full flex items-center gap-3 px-4 py-3 rounded-xl transition-all hover:opacity-90 active:scale-[0.98] block"
|
||||
style={{
|
||||
background: "rgba(0,212,255,0.06)",
|
||||
border: "1px solid rgba(0,212,255,0.2)",
|
||||
}}
|
||||
<div className="space-y-1.5 text-xs text-white/40 pl-4">
|
||||
<p>
|
||||
{isZh
|
||||
? "1. 您在钱包弹窗中点击了「拒绝」"
|
||||
: "1. You clicked \"Reject\" in the wallet popup"}
|
||||
</p>
|
||||
<p>
|
||||
{isZh
|
||||
? "2. 该网站之前被您在 MetaMask 中屏蔽(最常见原因)"
|
||||
: "2. This site was previously blocked in your wallet (most common)"}
|
||||
</p>
|
||||
<p>
|
||||
{isZh
|
||||
? "3. 钱包弹窗未显示(被浏览器拦截)"
|
||||
: "3. Wallet popup was blocked by browser"}
|
||||
</p>
|
||||
</div>
|
||||
{walletName === "MetaMask" && (
|
||||
<div
|
||||
className="rounded-lg p-3 space-y-1.5"
|
||||
style={{ background: "rgba(240,180,41,0.06)", border: "1px solid rgba(240,180,41,0.15)" }}
|
||||
>
|
||||
<span className="flex-shrink-0">{wallet.icon}</span>
|
||||
<span className="flex-1 text-sm font-semibold text-white">{wallet.name}</span>
|
||||
<span
|
||||
className="text-xs px-2 py-0.5 rounded-full font-medium flex-shrink-0"
|
||||
style={{ background: `${wallet.badgeColor}22`, color: wallet.badgeColor, border: `1px solid ${wallet.badgeColor}44` }}
|
||||
>
|
||||
{wallet.badge}
|
||||
</span>
|
||||
<span
|
||||
className="text-xs px-2 py-0.5 rounded-full font-medium flex-shrink-0"
|
||||
style={{ background: "rgba(0,212,255,0.15)", color: "#00d4ff" }}
|
||||
>
|
||||
{lang === "zh" ? "在 App 中打开" : "Open in App"}
|
||||
</span>
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="rgba(0,212,255,0.6)" strokeWidth="2" className="flex-shrink-0">
|
||||
<path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6"/>
|
||||
<polyline points="15 3 21 3 21 9"/>
|
||||
<line x1="10" y1="14" x2="21" y2="3"/>
|
||||
</svg>
|
||||
</a>
|
||||
))}
|
||||
<p className="text-xs font-semibold text-amber-400">
|
||||
{isZh ? "🔧 MetaMask 权限重置步骤:" : "🔧 MetaMask Permission Reset:"}
|
||||
</p>
|
||||
<div className="space-y-1 text-xs text-white/40">
|
||||
<p>{isZh ? "① 点击 MetaMask 图标打开扩展" : "① Click MetaMask icon to open extension"}</p>
|
||||
<p>{isZh ? "② 点击右上角菜单(三个点)" : "② Click top-right menu (three dots)"}</p>
|
||||
<p>{isZh ? "③ 选择「已连接的网站」" : "③ Select \"Connected Sites\""}</p>
|
||||
<p>{isZh ? "④ 找到本网站并删除" : "④ Find this site and remove it"}</p>
|
||||
<p>{isZh ? "⑤ 回到此页面重新点击连接" : "⑤ Return here and try connecting again"}</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<button
|
||||
onClick={onRetry}
|
||||
className="w-full py-2 rounded-lg text-xs font-semibold transition-all hover:opacity-90"
|
||||
style={{ background: "rgba(0,212,255,0.12)", color: "#00d4ff", border: "1px solid rgba(0,212,255,0.25)" }}
|
||||
>
|
||||
{isZh ? "重试连接" : "Retry Connection"}
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
{/* Step guide */}
|
||||
if (errorType === "wallet_pending") {
|
||||
return (
|
||||
<div
|
||||
className="rounded-xl p-3 space-y-2"
|
||||
style={{ background: "rgba(255,255,255,0.03)", border: "1px solid rgba(255,255,255,0.08)" }}
|
||||
className="rounded-xl p-4 space-y-3"
|
||||
style={{ background: "rgba(240,180,41,0.06)", border: "1px solid rgba(240,180,41,0.2)" }}
|
||||
>
|
||||
<p className="text-xs font-semibold text-white/50 mb-2">
|
||||
{lang === "zh" ? "操作步骤" : "How it works"}
|
||||
</p>
|
||||
{[
|
||||
lang === "zh" ? "1. 点击上方任一钱包 App 按钮" : "1. Tap any wallet app button above",
|
||||
lang === "zh" ? "2. 在钱包 App 的内置浏览器中打开本页面" : "2. Page opens in the wallet app's browser",
|
||||
lang === "zh" ? "3. 点击「连接钱包」即可自动连接" : "3. Tap 'Connect Wallet' to connect automatically",
|
||||
].map((step, i) => (
|
||||
<p key={i} className="text-xs text-white/35 leading-relaxed">{step}</p>
|
||||
))}
|
||||
<div className="flex items-start gap-2">
|
||||
<span className="text-amber-400 text-base flex-shrink-0">⏳</span>
|
||||
<div>
|
||||
<p className="text-sm font-semibold text-amber-400">
|
||||
{isZh ? "钱包有待处理的请求" : "Wallet Has Pending Request"}
|
||||
</p>
|
||||
<p className="text-xs text-white/50 mt-1">
|
||||
{isZh
|
||||
? "请查看钱包弹窗并处理待处理的请求,然后重试"
|
||||
: "Please check your wallet popup and handle the pending request, then retry"}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
onClick={onRetry}
|
||||
className="w-full py-2 rounded-lg text-xs font-semibold transition-all hover:opacity-90"
|
||||
style={{ background: "rgba(240,180,41,0.12)", color: "#f0b429", border: "1px solid rgba(240,180,41,0.25)" }}
|
||||
>
|
||||
{isZh ? "重试" : "Retry"}
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (errorType === "not_initialized") {
|
||||
return (
|
||||
<div
|
||||
className="rounded-xl p-4 space-y-3"
|
||||
style={{ background: "rgba(0,212,255,0.06)", border: "1px solid rgba(0,212,255,0.2)" }}
|
||||
>
|
||||
<div className="flex items-start gap-2">
|
||||
<span className="text-cyan-400 text-base flex-shrink-0">⚙️</span>
|
||||
<div>
|
||||
<p className="text-sm font-semibold text-cyan-400">
|
||||
{isZh ? "钱包未完成初始化" : "Wallet Not Initialized"}
|
||||
</p>
|
||||
<p className="text-xs text-white/50 mt-1">
|
||||
{isZh
|
||||
? "请先完成钱包设置(创建或导入钱包),然后刷新页面重试"
|
||||
: "Please complete wallet setup (create or import wallet), then refresh the page"}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => window.location.reload()}
|
||||
className="w-full py-2 rounded-lg text-xs font-semibold transition-all hover:opacity-90"
|
||||
style={{ background: "rgba(0,212,255,0.12)", color: "#00d4ff", border: "1px solid rgba(0,212,255,0.25)" }}
|
||||
>
|
||||
{isZh ? "刷新页面" : "Refresh Page"}
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Unknown error
|
||||
return (
|
||||
<div
|
||||
className="rounded-xl p-4 space-y-2"
|
||||
style={{ background: "rgba(255,80,80,0.06)", border: "1px solid rgba(255,80,80,0.2)" }}
|
||||
>
|
||||
<p className="text-sm font-semibold text-red-400">
|
||||
{isZh ? "连接失败" : "Connection Failed"}
|
||||
</p>
|
||||
<p className="text-xs text-white/40">
|
||||
{isZh ? "请刷新页面后重试,或尝试其他钱包" : "Please refresh the page and try again, or try another wallet"}
|
||||
</p>
|
||||
<button
|
||||
onClick={onRetry}
|
||||
className="w-full py-2 rounded-lg text-xs font-semibold transition-all hover:opacity-90"
|
||||
style={{ background: "rgba(0,212,255,0.12)", color: "#00d4ff", border: "1px solid rgba(0,212,255,0.25)" }}
|
||||
>
|
||||
{isZh ? "重试" : "Retry"}
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -487,17 +617,19 @@ function MobileDeepLinkPanel({ lang, showTron }: { lang: Lang; showTron: boolean
|
|||
export function WalletSelector({ lang, onAddressDetected, connectedAddress, compact = false, showTron = false }: WalletSelectorProps) {
|
||||
const [wallets, setWallets] = useState<WalletInfo[]>([]);
|
||||
const [connecting, setConnecting] = useState<string | null>(null);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [errorType, setErrorType] = useState<"user_rejected" | "wallet_pending" | "not_initialized" | "unknown" | null>(null);
|
||||
const [errorWalletName, setErrorWalletName] = useState<string>("");
|
||||
const [detecting, setDetecting] = useState(true);
|
||||
const [showManual, setShowManual] = useState(false);
|
||||
const [manualAddress, setManualAddress] = useState("");
|
||||
const [manualError, setManualError] = useState<string | null>(null);
|
||||
const [isMobile] = useState(() => isMobileBrowser());
|
||||
const [inWalletBrowser] = useState(() => isInWalletBrowser());
|
||||
const [lastConnectedWallet, setLastConnectedWallet] = useState<WalletInfo | null>(null);
|
||||
|
||||
const detectWallets = useCallback(() => {
|
||||
setDetecting(true);
|
||||
setError(null);
|
||||
setErrorType(null);
|
||||
// Wait for wallet extensions to fully inject (up to 1500ms)
|
||||
const timer = setTimeout(() => {
|
||||
setWallets(buildWallets(showTron));
|
||||
|
|
@ -513,32 +645,40 @@ export function WalletSelector({ lang, onAddressDetected, connectedAddress, comp
|
|||
|
||||
const handleConnect = async (wallet: WalletInfo) => {
|
||||
setConnecting(wallet.id);
|
||||
setError(null);
|
||||
setErrorType(null);
|
||||
setLastConnectedWallet(wallet);
|
||||
try {
|
||||
const address = await wallet.connect();
|
||||
if (address) {
|
||||
onAddressDetected(address, wallet.network);
|
||||
const result = await wallet.connect();
|
||||
if (result) {
|
||||
onAddressDetected(result.address, wallet.network, result.rawProvider);
|
||||
} else {
|
||||
setError(lang === "zh" ? "未获取到地址,请重试" : "No address returned, please try again");
|
||||
setErrorType("unknown");
|
||||
setErrorWalletName(wallet.name);
|
||||
}
|
||||
} catch (err: unknown) {
|
||||
const error = err as Error;
|
||||
setErrorWalletName(wallet.name);
|
||||
if (error.message === "user_rejected") {
|
||||
setError(lang === "zh" ? "已取消连接" : "Connection cancelled");
|
||||
setErrorType("user_rejected");
|
||||
} else if (error.message === "wallet_pending") {
|
||||
setError(lang === "zh" ? "钱包请求处理中,请检查钱包弹窗" : "Wallet request pending, please check your wallet popup");
|
||||
setErrorType("wallet_pending");
|
||||
} else if (error.message?.includes("not initialized") || error.message?.includes("setup")) {
|
||||
setError(lang === "zh"
|
||||
? "请先完成钱包初始化设置,然后刷新页面重试"
|
||||
: "Please complete wallet setup first, then refresh the page");
|
||||
setErrorType("not_initialized");
|
||||
} else {
|
||||
setError(lang === "zh" ? "连接失败,请重试" : "Connection failed, please try again");
|
||||
setErrorType("unknown");
|
||||
}
|
||||
} finally {
|
||||
setConnecting(null);
|
||||
}
|
||||
};
|
||||
|
||||
const handleRetry = useCallback(() => {
|
||||
setErrorType(null);
|
||||
if (lastConnectedWallet) {
|
||||
handleConnect(lastConnectedWallet);
|
||||
}
|
||||
}, [lastConnectedWallet]); // eslint-disable-line react-hooks/exhaustive-deps
|
||||
|
||||
const handleManualSubmit = () => {
|
||||
const addr = manualAddress.trim();
|
||||
if (!addr) {
|
||||
|
|
@ -583,65 +723,61 @@ export function WalletSelector({ lang, onAddressDetected, connectedAddress, comp
|
|||
|
||||
// ── Mobile browser (not in wallet app) — show DeepLink guide ──────────────
|
||||
if (isMobile && !inWalletBrowser && !detecting) {
|
||||
const hasInstalledWallet = installedWallets.length > 0;
|
||||
return (
|
||||
<div className="space-y-3">
|
||||
<MobileDeepLinkPanel lang={lang} showTron={showTron} />
|
||||
|
||||
if (!hasInstalledWallet) {
|
||||
return (
|
||||
<div className="space-y-3">
|
||||
<MobileDeepLinkPanel lang={lang} showTron={showTron} />
|
||||
{/* Manual address fallback */}
|
||||
<div className="pt-1">
|
||||
<button
|
||||
onClick={() => { setShowManual(!showManual); setManualError(null); }}
|
||||
className="w-full text-xs text-white/30 hover:text-white/50 transition-colors py-1 flex items-center justify-center gap-1"
|
||||
>
|
||||
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
||||
<path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"/>
|
||||
<path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"/>
|
||||
</svg>
|
||||
{showManual
|
||||
? (lang === "zh" ? "收起手动输入" : "Hide manual input")
|
||||
: (lang === "zh" ? "手动输入钱包地址" : "Enter address manually")}
|
||||
</button>
|
||||
|
||||
{/* Manual address fallback */}
|
||||
<div className="pt-1">
|
||||
<button
|
||||
onClick={() => { setShowManual(!showManual); setManualError(null); }}
|
||||
className="w-full text-xs text-white/30 hover:text-white/50 transition-colors py-1 flex items-center justify-center gap-1"
|
||||
>
|
||||
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
||||
<path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"/>
|
||||
<path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"/>
|
||||
</svg>
|
||||
{showManual
|
||||
? (lang === "zh" ? "收起手动输入" : "Hide manual input")
|
||||
: (lang === "zh" ? "手动输入钱包地址" : "Enter address manually")}
|
||||
</button>
|
||||
|
||||
{showManual && (
|
||||
<div className="mt-2 space-y-2">
|
||||
<p className="text-xs text-white/40 text-center">
|
||||
{lang === "zh"
|
||||
? "输入 EVM 地址(0x 开头)或 TRON 地址(T 开头)"
|
||||
: "Enter EVM address (0x...) or TRON address (T...)"}
|
||||
</p>
|
||||
<div className="flex gap-2">
|
||||
<input
|
||||
type="text"
|
||||
value={manualAddress}
|
||||
onChange={e => { setManualAddress(e.target.value); setManualError(null); }}
|
||||
placeholder={lang === "zh" ? "0x... 或 T..." : "0x... or T..."}
|
||||
className="flex-1 px-3 py-2 rounded-lg text-xs font-mono text-white/80 outline-none focus:ring-1"
|
||||
style={{
|
||||
background: "rgba(255,255,255,0.06)",
|
||||
border: manualError ? "1px solid rgba(255,80,80,0.5)" : "1px solid rgba(255,255,255,0.12)",
|
||||
}}
|
||||
onKeyDown={e => e.key === "Enter" && handleManualSubmit()}
|
||||
/>
|
||||
<button
|
||||
onClick={handleManualSubmit}
|
||||
className="px-3 py-2 rounded-lg text-xs font-semibold transition-all hover:opacity-90 active:scale-95 whitespace-nowrap"
|
||||
style={{ background: "rgba(0,212,255,0.15)", color: "#00d4ff", border: "1px solid rgba(0,212,255,0.3)" }}
|
||||
>
|
||||
{lang === "zh" ? "确认" : "Confirm"}
|
||||
</button>
|
||||
</div>
|
||||
{manualError && (
|
||||
<p className="text-xs text-red-400">{manualError}</p>
|
||||
)}
|
||||
{showManual && (
|
||||
<div className="mt-2 space-y-2">
|
||||
<p className="text-xs text-white/40 text-center">
|
||||
{lang === "zh"
|
||||
? "输入 EVM 地址(0x 开头)或 TRON 地址(T 开头)"
|
||||
: "Enter EVM address (0x...) or TRON address (T...)"}
|
||||
</p>
|
||||
<div className="flex gap-2">
|
||||
<input
|
||||
type="text"
|
||||
value={manualAddress}
|
||||
onChange={e => { setManualAddress(e.target.value); setManualError(null); }}
|
||||
placeholder={lang === "zh" ? "0x... 或 T..." : "0x... or T..."}
|
||||
className="flex-1 px-3 py-2 rounded-lg text-xs font-mono text-white/80 outline-none focus:ring-1"
|
||||
style={{
|
||||
background: "rgba(255,255,255,0.06)",
|
||||
border: manualError ? "1px solid rgba(255,80,80,0.5)" : "1px solid rgba(255,255,255,0.12)",
|
||||
}}
|
||||
onKeyDown={e => e.key === "Enter" && handleManualSubmit()}
|
||||
/>
|
||||
<button
|
||||
onClick={handleManualSubmit}
|
||||
className="px-3 py-2 rounded-lg text-xs font-semibold transition-all hover:opacity-90 active:scale-95 whitespace-nowrap"
|
||||
style={{ background: "rgba(0,212,255,0.15)", color: "#00d4ff", border: "1px solid rgba(0,212,255,0.3)" }}
|
||||
>
|
||||
{lang === "zh" ? "确认" : "Confirm"}
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{manualError && (
|
||||
<p className="text-xs text-red-400">{manualError}</p>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// ── Loading state ─────────────────────────────────────────────────────────
|
||||
|
|
@ -650,7 +786,7 @@ export function WalletSelector({ lang, onAddressDetected, connectedAddress, comp
|
|||
<div className="space-y-3">
|
||||
<div className="flex items-center justify-between">
|
||||
<p className="text-xs font-semibold text-white/60 uppercase tracking-wider">
|
||||
{lang === "zh" ? "选择钱包自动填充地址" : "Select wallet to auto-fill address"}
|
||||
{lang === "zh" ? "选择钱包连接" : "Select Wallet to Connect"}
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex items-center justify-center py-4 gap-2">
|
||||
|
|
@ -670,7 +806,7 @@ export function WalletSelector({ lang, onAddressDetected, connectedAddress, comp
|
|||
<div className="space-y-3">
|
||||
<div className="flex items-center justify-between">
|
||||
<p className="text-xs font-semibold text-white/60 uppercase tracking-wider">
|
||||
{lang === "zh" ? "选择钱包自动填充地址" : "Select wallet to auto-fill address"}
|
||||
{lang === "zh" ? "选择钱包连接" : "Select Wallet to Connect"}
|
||||
</p>
|
||||
{/* Refresh detection button */}
|
||||
<button
|
||||
|
|
@ -693,14 +829,45 @@ export function WalletSelector({ lang, onAddressDetected, connectedAddress, comp
|
|||
</button>
|
||||
</div>
|
||||
|
||||
{/* Connecting overlay */}
|
||||
{connecting && (
|
||||
<div
|
||||
className="rounded-xl p-4 flex items-center gap-3"
|
||||
style={{ background: "rgba(0,212,255,0.06)", border: "1px solid rgba(0,212,255,0.2)" }}
|
||||
>
|
||||
<svg className="animate-spin w-5 h-5 text-cyan-400 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>
|
||||
<div>
|
||||
<p className="text-sm font-semibold text-white/80">
|
||||
{lang === "zh" ? "等待钱包授权..." : "Waiting for wallet authorization..."}
|
||||
</p>
|
||||
<p className="text-xs text-white/40 mt-0.5">
|
||||
{lang === "zh" ? "请查看钱包弹窗并点击「连接」" : "Please check your wallet popup and click \"Connect\""}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Error panel */}
|
||||
{!connecting && errorType && (
|
||||
<ErrorHelpPanel
|
||||
errorType={errorType}
|
||||
walletName={errorWalletName}
|
||||
lang={lang}
|
||||
onRetry={handleRetry}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Installed wallets */}
|
||||
{installedWallets.length > 0 && (
|
||||
{!connecting && !errorType && installedWallets.length > 0 && (
|
||||
<div className="space-y-2">
|
||||
{installedWallets.map(wallet => (
|
||||
<button
|
||||
key={wallet.id}
|
||||
onClick={() => handleConnect(wallet)}
|
||||
disabled={connecting === wallet.id}
|
||||
disabled={!!connecting}
|
||||
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: wallet.network === "tron" ? "rgba(255,0,19,0.08)" : "rgba(0,212,255,0.08)",
|
||||
|
|
@ -717,23 +884,43 @@ export function WalletSelector({ lang, onAddressDetected, connectedAddress, comp
|
|||
>
|
||||
{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>
|
||||
)}
|
||||
<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>
|
||||
)}
|
||||
|
||||
{/* Show installed wallets even when there's an error (for retry) */}
|
||||
{!connecting && errorType && installedWallets.length > 0 && (
|
||||
<div className="space-y-2">
|
||||
<p className="text-xs text-white/30 text-center">
|
||||
{lang === "zh" ? "或选择其他钱包" : "Or try another wallet"}
|
||||
</p>
|
||||
{installedWallets.map(wallet => (
|
||||
<button
|
||||
key={wallet.id}
|
||||
onClick={() => handleConnect(wallet)}
|
||||
disabled={!!connecting}
|
||||
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(255,255,255,0.04)",
|
||||
border: "1px solid rgba(255,255,255,0.1)",
|
||||
}}
|
||||
>
|
||||
<span className="flex-shrink-0">{wallet.icon}</span>
|
||||
<span className="flex-1 text-left text-sm font-semibold text-white/70">{wallet.name}</span>
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="rgba(255,255,255,0.3)" strokeWidth="2" className="flex-shrink-0">
|
||||
<path d="M5 12h14M12 5l7 7-7 7"/>
|
||||
</svg>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* No wallets installed — desktop */}
|
||||
{installedWallets.length === 0 && (
|
||||
{!connecting && 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)" }}
|
||||
|
|
@ -748,13 +935,6 @@ export function WalletSelector({ lang, onAddressDetected, connectedAddress, comp
|
|||
? "请安装以下任一钱包,完成设置后点击上方「刷新」按钮"
|
||||
: "Install any wallet below, then click Refresh above after setup"}
|
||||
</p>
|
||||
{showTron && (
|
||||
<p className="text-xs text-amber-400/70">
|
||||
{lang === "zh"
|
||||
? "💡 TRC20 支付请安装 TronLink;BSC/ETH 支付请安装 MetaMask"
|
||||
: "💡 For TRC20 install TronLink; for BSC/ETH install MetaMask"}
|
||||
</p>
|
||||
)}
|
||||
{!showTron && (
|
||||
<p className="text-xs text-amber-400/70">
|
||||
{lang === "zh"
|
||||
|
|
@ -765,61 +945,43 @@ export function WalletSelector({ lang, onAddressDetected, connectedAddress, comp
|
|||
</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)"}
|
||||
{/* Not installed wallets */}
|
||||
{!connecting && notInstalledWallets.length > 0 && (
|
||||
<div className="space-y-1.5">
|
||||
<p className="text-xs text-white/25 uppercase tracking-wider">
|
||||
{lang === "zh" ? "未安装" : "Not installed"}
|
||||
</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 => (
|
||||
{notInstalledWallets.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"
|
||||
className="w-full flex items-center gap-3 px-4 py-2.5 rounded-xl 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)",
|
||||
background: "rgba(255,255,255,0.02)",
|
||||
border: "1px dashed rgba(255,255,255,0.1)",
|
||||
}}
|
||||
>
|
||||
<span className="opacity-50">{wallet.icon}</span>
|
||||
{lang === "zh" ? `安装 ${wallet.name}` : `Install ${wallet.name}`}
|
||||
<span className="flex-shrink-0 opacity-50">{wallet.icon}</span>
|
||||
<span className="flex-1 text-left text-sm text-white/40">{wallet.name}</span>
|
||||
<span
|
||||
className="text-xs px-2 py-0.5 rounded-full font-medium"
|
||||
style={{ background: "rgba(255,255,255,0.05)", color: "rgba(255,255,255,0.3)" }}
|
||||
>
|
||||
{lang === "zh" ? "点击安装" : "Install"}
|
||||
</span>
|
||||
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="rgba(255,255,255,0.2)" strokeWidth="2" className="flex-shrink-0">
|
||||
<path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6"/>
|
||||
<polyline points="15 3 21 3 21 9"/>
|
||||
<line x1="10" y1="14" x2="21" y2="3"/>
|
||||
</svg>
|
||||
</a>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{error && (
|
||||
<p className="text-xs text-red-400 text-center">{error}</p>
|
||||
)}
|
||||
|
||||
{/* Manual address input — divider */}
|
||||
{/* Manual address input */}
|
||||
<div className="pt-1">
|
||||
<button
|
||||
onClick={() => { setShowManual(!showManual); setManualError(null); }}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
// NAC XIC Presale — Wallet Connection Hook
|
||||
// Supports MetaMask, Trust Wallet, OKX Wallet, Coinbase Wallet, and all EVM-compatible wallets
|
||||
// v3: improved error handling, MetaMask initialization detection, toast notifications
|
||||
// Supports MetaMask, TokenPocket, OKX, Bitget, Trust Wallet, imToken, SafePal, and all EVM wallets
|
||||
// v5: added connectWithProvider() to fix state sync when WalletSelector connects via external provider
|
||||
|
||||
import { useState, useEffect, useCallback, useRef } from "react";
|
||||
import { BrowserProvider, JsonRpcSigner, Eip1193Provider } from "ethers";
|
||||
|
|
@ -30,58 +30,65 @@ const INITIAL_STATE: WalletState = {
|
|||
error: null,
|
||||
};
|
||||
|
||||
export type EthProvider = Eip1193Provider & {
|
||||
isMetaMask?: boolean;
|
||||
isTrust?: boolean;
|
||||
isTrustWallet?: boolean;
|
||||
isOKExWallet?: boolean;
|
||||
isOkxWallet?: boolean;
|
||||
isCoinbaseWallet?: boolean;
|
||||
isTokenPocket?: boolean;
|
||||
isBitkeep?: boolean;
|
||||
isBitgetWallet?: boolean;
|
||||
providers?: EthProvider[];
|
||||
};
|
||||
|
||||
// Detect the best available EVM provider across all major wallets
|
||||
export function detectProvider(): Eip1193Provider | null {
|
||||
// Priority: TokenPocket > OKX > Bitget > Trust Wallet > MetaMask > others
|
||||
export function detectProvider(): EthProvider | null {
|
||||
if (typeof window === "undefined") return null;
|
||||
|
||||
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) {
|
||||
const metamask = eth.providers.find((p: Eip1193Provider & { isMetaMask?: boolean }) => p.isMetaMask);
|
||||
return metamask ?? eth.providers[0];
|
||||
}
|
||||
|
||||
return eth;
|
||||
}
|
||||
|
||||
// Check if MetaMask is installed but not yet initialized (no wallet created/imported)
|
||||
export async function checkWalletReady(rawProvider: Eip1193Provider): Promise<{ ready: boolean; reason?: string }> {
|
||||
try {
|
||||
// eth_accounts is silent — if it returns empty array, wallet is installed but locked or not initialized
|
||||
const accounts = await (rawProvider as { request: (args: { method: string }) => Promise<string[]> }).request({
|
||||
method: "eth_accounts",
|
||||
});
|
||||
// If we get here, the wallet is at least initialized (even if locked / no accounts)
|
||||
return { ready: true };
|
||||
} catch (err: unknown) {
|
||||
const error = err as { code?: number; message?: string };
|
||||
// -32002: Request already pending (MetaMask not initialized or another request pending)
|
||||
if (error?.code === -32002) {
|
||||
return { ready: false, reason: "pending" };
|
||||
// 1. Check window.ethereum (most wallets inject here)
|
||||
const eth = w.ethereum as EthProvider | undefined;
|
||||
if (eth) {
|
||||
// Check providers array first (multiple extensions installed)
|
||||
if (eth.providers && Array.isArray(eth.providers) && eth.providers.length > 0) {
|
||||
// Priority order for Chinese users
|
||||
const tp = eth.providers.find((p: EthProvider) => p.isTokenPocket);
|
||||
if (tp) return tp;
|
||||
const okx = eth.providers.find((p: EthProvider) => p.isOKExWallet || p.isOkxWallet);
|
||||
if (okx) return okx;
|
||||
const bitget = eth.providers.find((p: EthProvider) => p.isBitkeep || p.isBitgetWallet);
|
||||
if (bitget) return bitget;
|
||||
const trust = eth.providers.find((p: EthProvider) => p.isTrust || p.isTrustWallet);
|
||||
if (trust) return trust;
|
||||
const metamask = eth.providers.find((p: EthProvider) => p.isMetaMask);
|
||||
if (metamask) return metamask;
|
||||
return eth.providers[0];
|
||||
}
|
||||
// Any other error — treat as not ready
|
||||
return { ready: false, reason: error?.message || "unknown" };
|
||||
|
||||
// Single provider — return it directly
|
||||
return eth;
|
||||
}
|
||||
|
||||
// 2. OKX Wallet — sometimes injects window.okxwallet separately
|
||||
if (w.okxwallet) return w.okxwallet as EthProvider;
|
||||
|
||||
// 3. Bitget Wallet — sometimes injects window.bitkeep.ethereum
|
||||
const bitkeep = w.bitkeep as { ethereum?: EthProvider } | undefined;
|
||||
if (bitkeep?.ethereum) return bitkeep.ethereum;
|
||||
|
||||
// 4. Coinbase Wallet
|
||||
if (w.coinbaseWalletExtension) return w.coinbaseWalletExtension as EthProvider;
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
// Build wallet state from a provider and accounts
|
||||
async function buildWalletState(
|
||||
rawProvider: Eip1193Provider,
|
||||
rawProvider: EthProvider,
|
||||
address: string
|
||||
): Promise<Partial<WalletState>> {
|
||||
const provider = new BrowserProvider(rawProvider);
|
||||
|
|
@ -118,7 +125,14 @@ async function buildWalletState(
|
|||
};
|
||||
}
|
||||
|
||||
export function useWallet() {
|
||||
export type WalletHookReturn = WalletState & {
|
||||
connect: () => Promise<{ success: boolean; error?: string }>;
|
||||
connectWithProvider: (rawProvider: EthProvider, address: string) => Promise<void>;
|
||||
disconnect: () => void;
|
||||
switchNetwork: (chainId: number) => Promise<void>;
|
||||
};
|
||||
|
||||
export function useWallet(): WalletHookReturn {
|
||||
const [state, setState] = useState<WalletState>(INITIAL_STATE);
|
||||
const retryRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
||||
const mountedRef = useRef(true);
|
||||
|
|
@ -131,12 +145,36 @@ export function useWallet() {
|
|||
};
|
||||
}, []);
|
||||
|
||||
// ── Connect (explicit user action) ─────────────────────────────────────────
|
||||
// ── Connect via external provider (called from WalletSelector) ─────────────
|
||||
// KEY FIX: WalletSelector already has the provider and address from the wallet popup.
|
||||
// We update state directly without calling connect() again (which would use detectProvider()
|
||||
// and might pick the wrong wallet or fail if the wallet injects to a different window property).
|
||||
const connectWithProvider = useCallback(async (rawProvider: EthProvider, address: string) => {
|
||||
if (!mountedRef.current) return;
|
||||
setState(s => ({ ...s, isConnecting: true, error: null }));
|
||||
try {
|
||||
const partial = await buildWalletState(rawProvider, address);
|
||||
if (mountedRef.current) setState({ ...INITIAL_STATE, ...partial });
|
||||
} catch {
|
||||
// Fallback: at minimum set address and isConnected = true
|
||||
if (mountedRef.current) {
|
||||
setState({
|
||||
...INITIAL_STATE,
|
||||
address,
|
||||
shortAddress: shortenAddress(address),
|
||||
isConnected: true,
|
||||
isConnecting: false,
|
||||
});
|
||||
}
|
||||
}
|
||||
}, []);
|
||||
|
||||
// ── Connect (explicit user action via detectProvider) ──────────────────────
|
||||
const connect = useCallback(async (): Promise<{ success: boolean; error?: string }> => {
|
||||
const rawProvider = detectProvider();
|
||||
|
||||
if (!rawProvider) {
|
||||
const msg = "未检测到钱包插件。请安装 MetaMask 或其他 EVM 兼容钱包后刷新页面。";
|
||||
const msg = "未检测到钱包插件。请安装 TokenPocket、MetaMask 或其他 EVM 兼容钱包后刷新页面。";
|
||||
if (mountedRef.current) setState(s => ({ ...s, error: msg }));
|
||||
return { success: false, error: msg };
|
||||
}
|
||||
|
|
@ -165,19 +203,11 @@ export function useWallet() {
|
|||
let msg: string;
|
||||
|
||||
if (error?.code === 4001) {
|
||||
// User rejected
|
||||
msg = "已取消连接 / Connection cancelled";
|
||||
} else if (error?.code === -32002) {
|
||||
// MetaMask has a pending request — usually means it's not initialized or popup is already open
|
||||
msg = "钱包请求处理中,请检查 MetaMask 弹窗。如未弹出,请先完成 MetaMask 初始化设置(创建或导入钱包),然后刷新页面重试。";
|
||||
msg = "钱包请求处理中,请检查钱包弹窗。如未弹出,请先完成钱包初始化设置,然后刷新页面重试。";
|
||||
} else if (error?.message === "no_accounts") {
|
||||
msg = "未获取到账户,请确认钱包已解锁并授权此网站。";
|
||||
} else if (
|
||||
error?.message?.toLowerCase().includes("not initialized") ||
|
||||
error?.message?.toLowerCase().includes("setup") ||
|
||||
error?.message?.toLowerCase().includes("onboarding")
|
||||
) {
|
||||
msg = "MetaMask 尚未完成初始化。请先打开 MetaMask 扩展,创建或导入钱包,然后刷新页面重试。";
|
||||
} else {
|
||||
msg = `连接失败: ${error?.message || "未知错误"}。请刷新页面重试。`;
|
||||
}
|
||||
|
|
@ -226,8 +256,8 @@ export function useWallet() {
|
|||
|
||||
const rawProvider = detectProvider();
|
||||
if (!rawProvider) {
|
||||
if (attempt < 3) {
|
||||
retryRef.current = setTimeout(() => tryAutoDetect(attempt + 1), 800 * attempt);
|
||||
if (attempt < 5) {
|
||||
retryRef.current = setTimeout(() => tryAutoDetect(attempt + 1), 600 * attempt);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
|
@ -242,11 +272,13 @@ export function useWallet() {
|
|||
if (!cancelled && mountedRef.current) {
|
||||
setState({ ...INITIAL_STATE, ...partial });
|
||||
}
|
||||
} else if (attempt < 3) {
|
||||
retryRef.current = setTimeout(() => tryAutoDetect(attempt + 1), 1000 * attempt);
|
||||
} else if (attempt < 5) {
|
||||
retryRef.current = setTimeout(() => tryAutoDetect(attempt + 1), 800 * attempt);
|
||||
}
|
||||
} catch {
|
||||
// Silently ignore — user hasn't connected yet
|
||||
if (attempt < 3) {
|
||||
retryRef.current = setTimeout(() => tryAutoDetect(attempt + 1), 1000);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
|
@ -322,5 +354,5 @@ export function useWallet() {
|
|||
};
|
||||
}, []);
|
||||
|
||||
return { ...state, connect, disconnect, switchNetwork };
|
||||
return { ...state, connect, connectWithProvider, disconnect, switchNetwork };
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ export const CONTRACTS = {
|
|||
rpcUrl: "https://bsc-dataseed1.binance.org/",
|
||||
explorerUrl: "https://bscscan.com",
|
||||
nativeCurrency: { name: "BNB", symbol: "BNB", decimals: 18 },
|
||||
presale: "0xc65e7a2738ed884db8d26a6eb2fecf7daca2e90c",
|
||||
presale: "0x5953c025dA734e710886916F2d739A3A78f8bbc4", // XICPresale v2 — 购买即时发放
|
||||
token: "0x59FF34dD59680a7125782b1f6df2A86ed46F5A24",
|
||||
usdt: "0x55d398326f99059fF775485246999027B3197955",
|
||||
},
|
||||
|
|
@ -50,9 +50,9 @@ export const PRESALE_CONFIG = {
|
|||
tokenName: "New AssetChain Token",
|
||||
tokenDecimals: 18,
|
||||
minPurchaseUSDT: 0, // No minimum purchase limit
|
||||
maxPurchaseUSDT: 50000, // Maximum $50,000 USDT
|
||||
maxPurchaseUSDT: 50000, // Max $50,000 USDT per purchase
|
||||
totalSupply: 100_000_000_000, // 100 billion XIC
|
||||
presaleAllocation: 30_000_000_000, // 30 billion for presale
|
||||
presaleAllocation: 2_500_000_000, // 2.5 billion for presale (25亿)
|
||||
// TRC20 memo format
|
||||
trc20Memo: "XIC_PRESALE",
|
||||
};
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load Diff
|
|
@ -4,6 +4,7 @@
|
|||
// Colors: Amber Gold #f0b429 | Quantum Blue #00d4ff | Deep Black #0a0a0f
|
||||
|
||||
import { useState, useEffect, useCallback, useRef, useMemo } from "react";
|
||||
import { createPortal } from "react-dom";
|
||||
import { toast } from "sonner";
|
||||
import { Link } from "wouter";
|
||||
import { useWallet } from "@/hooks/useWallet";
|
||||
|
|
@ -159,12 +160,12 @@ function TRC20Panel({ usdtAmount, lang, connectedAddress, onConnectWallet }: { u
|
|||
<div className="flex items-center gap-2">
|
||||
<span className="text-amber-400 text-sm">⚠️</span>
|
||||
<p className="text-sm font-semibold text-amber-300">
|
||||
{lang === "zh" ? "必填:您的XIC接收地址(BSC/ETH钉包地址)" : "Required: Your XIC Receiving Address (BSC/ETH wallet address)"}
|
||||
{lang === "zh" ? "必填:您的XIC接收地址(BSC/ETH钱包地址)" : "Required: Your XIC Receiving Address (BSC/ETH wallet address)"}
|
||||
</p>
|
||||
</div>
|
||||
<p className="text-xs text-white/50">
|
||||
{lang === "zh"
|
||||
? "XIC代币将发放到您的BSC/ETH钉包地址(0x开头)。请确保填写正确的地址,否则无法收到代币。"
|
||||
? "XIC代币将发放到您的BSC/ETH钱包地址(0x开头)。请确保填写正确的地址,否则无法收到代币。"
|
||||
: "XIC tokens will be sent to your BSC/ETH wallet address (starts with 0x). Please make sure to enter the correct address."}
|
||||
</p>
|
||||
<div className="space-y-2">
|
||||
|
|
@ -315,7 +316,7 @@ function TRC20Panel({ usdtAmount, lang, connectedAddress, onConnectWallet }: { u
|
|||
}
|
||||
|
||||
// ─── EVM Purchase Panel ─────────────────────────────────────────────────────
|
||||
function EVMPurchasePanel({ network, lang, wallet }: { network: "BSC" | "ETH"; lang: Lang; wallet: WalletHookReturn }) {
|
||||
function EVMPurchasePanel({ network, lang, wallet, onOpenWalletModal }: { network: "BSC" | "ETH"; lang: Lang; wallet: WalletHookReturn; onOpenWalletModal?: () => void }) {
|
||||
const { t } = useTranslation(lang);
|
||||
const { purchaseState, buyWithUSDT, reset, calcTokens, getUsdtBalance } = usePresale(wallet, network);
|
||||
const [usdtInput, setUsdtInput] = useState("100");
|
||||
|
|
@ -334,7 +335,8 @@ function EVMPurchasePanel({ network, lang, wallet }: { network: "BSC" | "ETH"; l
|
|||
|
||||
const usdtAmount = parseFloat(usdtInput) || 0;
|
||||
const tokenAmount = calcTokens(usdtAmount);
|
||||
const isValidAmount = usdtAmount > 0 && usdtAmount <= PRESALE_CONFIG.maxPurchaseUSDT;
|
||||
// maxPurchaseUSDT=0 means no limit; otherwise check against the limit
|
||||
const isValidAmount = usdtAmount > 0 && (PRESALE_CONFIG.maxPurchaseUSDT === 0 || usdtAmount <= PRESALE_CONFIG.maxPurchaseUSDT);
|
||||
|
||||
const handleBuy = async () => {
|
||||
if (!isValidAmount) {
|
||||
|
|
@ -360,16 +362,18 @@ function EVMPurchasePanel({ network, lang, wallet }: { network: "BSC" | "ETH"; l
|
|||
return (
|
||||
<div className="space-y-4">
|
||||
<p className="text-sm text-white/60 text-center">{t("buy_connect_msg")}</p>
|
||||
<WalletSelector
|
||||
lang={lang}
|
||||
connectedAddress={wallet.address ?? undefined}
|
||||
onAddressDetected={(addr) => {
|
||||
// WalletSelector already called eth_requestAccounts and got the address
|
||||
// Just show success toast; wallet state will auto-update via accountsChanged event
|
||||
toast.success(lang === "zh" ? `已连接: ${addr.slice(0, 6)}...${addr.slice(-4)}` : `Connected: ${addr.slice(0, 6)}...${addr.slice(-4)}`);
|
||||
}}
|
||||
compact
|
||||
/>
|
||||
<button
|
||||
onClick={() => onOpenWalletModal?.()}
|
||||
className="w-full py-3 rounded-xl text-base font-bold transition-all hover:opacity-90 flex items-center justify-center gap-2"
|
||||
style={{ background: "linear-gradient(135deg, rgba(240,180,41,0.9) 0%, rgba(255,215,0,0.9) 100%)", color: "#0a0a0f", fontFamily: "'Space Grotesk', sans-serif", boxShadow: "0 0 16px rgba(240,180,41,0.3)" }}
|
||||
>
|
||||
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5">
|
||||
<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>
|
||||
{lang === "zh" ? "连接钱包" : "Connect Wallet"}
|
||||
</button>
|
||||
<div className="text-xs text-white/40 text-center">{t("buy_connect_hint")}</div>
|
||||
</div>
|
||||
);
|
||||
|
|
@ -516,8 +520,56 @@ function EVMPurchasePanel({ network, lang, wallet }: { network: "BSC" | "ETH"; l
|
|||
</button>
|
||||
|
||||
<p className="text-xs text-center text-white/30">
|
||||
{t("buy_no_min_max")} ${PRESALE_CONFIG.maxPurchaseUSDT.toLocaleString()} USDT
|
||||
{PRESALE_CONFIG.maxPurchaseUSDT > 0
|
||||
? `${t("buy_no_min_max")} $${PRESALE_CONFIG.maxPurchaseUSDT.toLocaleString()} USDT`
|
||||
: (lang === "zh" ? "无最低/最高购买限制" : "No minimum or maximum purchase limit")}
|
||||
</p>
|
||||
|
||||
{/* Add XIC to Wallet button — only show on BSC where token address is known AND wallet is connected */}
|
||||
{network === "BSC" && CONTRACTS.BSC.token && wallet.isConnected && (
|
||||
<button
|
||||
onClick={async () => {
|
||||
try {
|
||||
// Use wallet.provider (ethers BrowserProvider) which wraps the connected wallet's provider
|
||||
// This works regardless of which wallet is connected (MetaMask, OKX, TokenPocket, etc.)
|
||||
if (!wallet.provider) {
|
||||
toast.error(lang === "zh" ? "钱包未连接,请先连接钱包" : "Wallet not connected. Please connect your wallet first.");
|
||||
return;
|
||||
}
|
||||
await wallet.provider.send("wallet_watchAsset", {
|
||||
type: "ERC20",
|
||||
options: {
|
||||
address: CONTRACTS.BSC.token,
|
||||
symbol: PRESALE_CONFIG.tokenSymbol,
|
||||
decimals: PRESALE_CONFIG.tokenDecimals,
|
||||
image: "https://d2xsxph8kpxj0f.cloudfront.net/310519663287655625/Ngki3MumDNGduV3xJt3mga/nac-token-icon_382e5c30.png",
|
||||
},
|
||||
});
|
||||
toast.success(lang === "zh" ? "XIC 代币已添加到钱包!" : "XIC token added to wallet!");
|
||||
} catch (err: unknown) {
|
||||
const error = err as { code?: number; message?: string };
|
||||
if (error?.code === 4001) {
|
||||
// User rejected — not an error
|
||||
return;
|
||||
}
|
||||
toast.error(lang === "zh" ? "添加失败,请手动添加代币" : "Failed to add token. Please add manually.");
|
||||
}
|
||||
}}
|
||||
className="w-full py-2.5 rounded-xl text-sm font-semibold transition-all hover:opacity-90 flex items-center justify-center gap-2"
|
||||
style={{
|
||||
background: "rgba(0,212,255,0.08)",
|
||||
border: "1px solid rgba(0,212,255,0.25)",
|
||||
color: "rgba(0,212,255,0.9)",
|
||||
}}
|
||||
>
|
||||
<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>
|
||||
{lang === "zh" ? "添加 XIC 到钱包" : "Add XIC to Wallet"}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -754,10 +806,9 @@ function ChatSupport({ lang }: { lang: Lang }) {
|
|||
|
||||
// ─── Navbar Wallet Button ─────────────────────────────────────────────────────
|
||||
type WalletHookReturn = ReturnType<typeof useWallet>;
|
||||
function NavWalletButton({ lang, wallet }: { lang: Lang; wallet: WalletHookReturn }) {
|
||||
function NavWalletButton({ lang, wallet, showWalletModal, setShowWalletModal }: { lang: Lang; wallet: WalletHookReturn; showWalletModal: boolean; setShowWalletModal: (v: boolean) => void }) {
|
||||
const { t } = useTranslation(lang);
|
||||
const [showMenu, setShowMenu] = useState(false);
|
||||
const [showWalletModal, setShowWalletModal] = useState(false);
|
||||
const menuRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
|
|
@ -768,24 +819,10 @@ function NavWalletButton({ lang, wallet }: { lang: Lang; wallet: WalletHookRetur
|
|||
return () => document.removeEventListener("mousedown", handleClick);
|
||||
}, []);
|
||||
|
||||
// Detect mobile browser
|
||||
const isMobile = typeof window !== "undefined" && /Android|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent);
|
||||
|
||||
// Handle connect button click — show wallet selector modal
|
||||
const handleConnectClick = async () => {
|
||||
// On mobile browsers, skip direct connect attempt and show modal immediately
|
||||
// (mobile browsers don't support wallet extensions)
|
||||
if (isMobile) {
|
||||
setShowWalletModal(true);
|
||||
return;
|
||||
}
|
||||
// On desktop: first try direct connect (works if wallet is already set up and locked)
|
||||
const result = await wallet.connect();
|
||||
if (!result.success && result.error) {
|
||||
// If direct connect failed, show the wallet selector modal for guided setup
|
||||
setShowWalletModal(true);
|
||||
toast.error(result.error, { duration: 6000 });
|
||||
}
|
||||
// Handle connect button click — always show wallet selector modal
|
||||
// Both desktop and mobile users see the modal to choose their wallet
|
||||
const handleConnectClick = () => {
|
||||
setShowWalletModal(true);
|
||||
};
|
||||
|
||||
if (!wallet.isConnected) {
|
||||
|
|
@ -805,67 +842,6 @@ function NavWalletButton({ lang, wallet }: { lang: Lang; wallet: WalletHookRetur
|
|||
{wallet.isConnecting ? t("nav_connecting") : t("nav_connect")}
|
||||
</button>
|
||||
|
||||
{/* Wallet Connection Modal */}
|
||||
{showWalletModal && (
|
||||
<div
|
||||
className="fixed inset-0 z-[100] flex items-center justify-center p-4"
|
||||
style={{ background: "rgba(0,0,0,0.85)", backdropFilter: "blur(8px)" }}
|
||||
onClick={(e) => { if (e.target === e.currentTarget) setShowWalletModal(false); }}
|
||||
>
|
||||
<div
|
||||
className="w-full max-w-md rounded-2xl p-6 relative"
|
||||
style={{ background: "rgba(10,10,20,0.98)", border: "1px solid rgba(240,180,41,0.3)", boxShadow: "0 0 40px rgba(240,180,41,0.15)" }}
|
||||
>
|
||||
{/* Close button */}
|
||||
<button
|
||||
onClick={() => setShowWalletModal(false)}
|
||||
className="absolute top-4 right-4 text-white/40 hover:text-white/80 transition-colors"
|
||||
>
|
||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
||||
<path d="M18 6L6 18M6 6l12 12"/>
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
<h3 className="text-lg font-bold text-white mb-1" style={{ fontFamily: "'Space Grotesk', sans-serif" }}>
|
||||
{lang === "zh" ? "连接钱包" : "Connect Wallet"}
|
||||
</h3>
|
||||
<p className="text-xs text-white/40 mb-4">
|
||||
{lang === "zh"
|
||||
? "选择您的钱包进行连接,或手动输入地址"
|
||||
: "Select your wallet to connect, or enter address manually"}
|
||||
</p>
|
||||
|
||||
{/* MetaMask initialization guide */}
|
||||
<div
|
||||
className="rounded-xl p-3 mb-4"
|
||||
style={{ background: "rgba(240,180,41,0.06)", border: "1px solid rgba(240,180,41,0.2)" }}
|
||||
>
|
||||
<p className="text-xs text-amber-300/80 leading-relaxed">
|
||||
{lang === "zh"
|
||||
? "💡 首次使用 MetaMask?请先打开 MetaMask 扩展完成初始化(创建或导入钱包),完成后点击下方「刷新」按钮重新检测。"
|
||||
: "💡 First time using MetaMask? Open the MetaMask extension and complete setup (create or import a wallet), then click Refresh below to re-detect."}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<WalletSelector
|
||||
lang={lang}
|
||||
connectedAddress={wallet.address ?? undefined}
|
||||
onAddressDetected={async (addr) => {
|
||||
// After address detected from WalletSelector, sync wallet state
|
||||
const result = await wallet.connect();
|
||||
if (result.success) {
|
||||
setShowWalletModal(false);
|
||||
toast.success(lang === "zh" ? `钱包已连接: ${addr.slice(0, 6)}...${addr.slice(-4)}` : `Wallet connected: ${addr.slice(0, 6)}...${addr.slice(-4)}`);
|
||||
} else {
|
||||
// Even if connect() failed, we have the address — close modal
|
||||
setShowWalletModal(false);
|
||||
toast.success(lang === "zh" ? `地址已确认: ${addr.slice(0, 6)}...${addr.slice(-4)}` : `Address confirmed: ${addr.slice(0, 6)}...${addr.slice(-4)}`);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
@ -964,6 +940,8 @@ export default function Home() {
|
|||
|
||||
// 钱包状态提升到顶层,共享给NavWalletButton和EVMPurchasePanel
|
||||
const wallet = useWallet();
|
||||
// showWalletModal提升到顶层,供NavWalletButton和EVMPurchasePanel共用
|
||||
const [showWalletModal, setShowWalletModal] = useState(false);
|
||||
|
||||
const networks: NetworkTab[] = ["BSC", "ETH", "TRON"];
|
||||
|
||||
|
|
@ -1002,8 +980,13 @@ export default function Home() {
|
|||
{lang === "zh" ? "📖 购买教程" : "📖 Tutorial"}
|
||||
</span>
|
||||
</Link>
|
||||
<Link href="/bridge">
|
||||
<span className="text-sm font-semibold cursor-pointer transition-colors hidden md:block px-2 py-1 rounded-lg" style={{ color: "#f0b429", background: "rgba(240,180,41,0.1)", border: "1px solid rgba(240,180,41,0.3)" }}>
|
||||
{lang === "zh" ? "⚡ 跨链桥" : "⚡ Bridge"}
|
||||
</span>
|
||||
</Link>
|
||||
<LangToggle lang={lang} setLang={setLang} />
|
||||
<NavWalletButton lang={lang} wallet={wallet} />
|
||||
<NavWalletButton lang={lang} wallet={wallet} showWalletModal={showWalletModal} setShowWalletModal={setShowWalletModal} />
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
|
|
@ -1188,8 +1171,8 @@ export default function Home() {
|
|||
</p>
|
||||
</div>
|
||||
)}
|
||||
{activeNetwork === "BSC" && <EVMPurchasePanel network="BSC" lang={lang} wallet={wallet} />}
|
||||
{activeNetwork === "ETH" && <EVMPurchasePanel network="ETH" lang={lang} wallet={wallet} />}
|
||||
{activeNetwork === "BSC" && <EVMPurchasePanel network="BSC" lang={lang} wallet={wallet} onOpenWalletModal={() => setShowWalletModal(true)} />}
|
||||
{activeNetwork === "ETH" && <EVMPurchasePanel network="ETH" lang={lang} wallet={wallet} onOpenWalletModal={() => setShowWalletModal(true)} />}
|
||||
{activeNetwork === "TRON" && (
|
||||
<div className="space-y-4">
|
||||
<div className="space-y-2">
|
||||
|
|
@ -1308,6 +1291,64 @@ export default function Home() {
|
|||
{/* ── Chat Support Widget ── */}
|
||||
<ChatSupport lang={lang} />
|
||||
|
||||
{/* ── Global Wallet Connection Modal (Portal) ── */}
|
||||
{showWalletModal && createPortal(
|
||||
<div
|
||||
className="fixed inset-0 z-[9999] flex items-end sm:items-center justify-center sm:p-4"
|
||||
style={{ background: "rgba(0,0,0,0.85)", backdropFilter: "blur(8px)" }}
|
||||
onClick={(e) => { if (e.target === e.currentTarget) setShowWalletModal(false); }}
|
||||
>
|
||||
<div
|
||||
className="w-full sm:max-w-md rounded-t-2xl sm:rounded-2xl p-5 relative overflow-y-auto"
|
||||
style={{ background: "rgba(10,10,20,0.98)", border: "1px solid rgba(240,180,41,0.3)", boxShadow: "0 0 40px rgba(240,180,41,0.15)", maxHeight: "85vh" }}
|
||||
>
|
||||
<button
|
||||
onClick={() => setShowWalletModal(false)}
|
||||
className="absolute top-4 right-4 text-white/40 hover:text-white/80 transition-colors"
|
||||
>
|
||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
||||
<path d="M18 6L6 18M6 6l12 12"/>
|
||||
</svg>
|
||||
</button>
|
||||
<h3 className="text-lg font-bold text-white mb-1" style={{ fontFamily: "'Space Grotesk', sans-serif" }}>
|
||||
{lang === "zh" ? "连接钱包" : "Connect Wallet"}
|
||||
</h3>
|
||||
{!/Android|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent) && (
|
||||
<>
|
||||
<p className="text-xs text-white/40 mb-4">
|
||||
{lang === "zh" ? "选择您的钱包进行连接,或手动输入地址" : "Select your wallet to connect, or enter address manually"}
|
||||
</p>
|
||||
<div className="rounded-xl p-3 mb-4" style={{ background: "rgba(240,180,41,0.06)", border: "1px solid rgba(240,180,41,0.2)" }}>
|
||||
<p className="text-xs text-amber-300/80 leading-relaxed">
|
||||
{lang === "zh"
|
||||
? "💡 首次使用 MetaMask?请先打开 MetaMask 扩展完成初始化(创建或导入钱包),完成后点击下方「刷新」按鈕重新检测。"
|
||||
: "💡 First time using MetaMask? Open the MetaMask extension and complete setup (create or import a wallet), then click Refresh below to re-detect."}
|
||||
</p>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
<WalletSelector
|
||||
lang={lang}
|
||||
compact={false}
|
||||
showTron={false}
|
||||
connectedAddress={undefined}
|
||||
onAddressDetected={async (addr, _network, rawProvider) => {
|
||||
if (rawProvider) {
|
||||
await wallet.connectWithProvider(rawProvider, addr);
|
||||
} else {
|
||||
const result = await wallet.connect();
|
||||
if (!result.success) {
|
||||
await wallet.connectWithProvider({ request: async () => [] } as unknown as import("@/hooks/useWallet").EthProvider, addr);
|
||||
}
|
||||
}
|
||||
setShowWalletModal(false);
|
||||
toast.success(lang === "zh" ? `钱包已连接: ${addr.slice(0, 6)}...${addr.slice(-4)}` : `Wallet connected: ${addr.slice(0, 6)}...${addr.slice(-4)}`);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
, document.body)}
|
||||
|
||||
<style>{`
|
||||
@keyframes fadeInDown {
|
||||
from { opacity: 0; transform: translateY(-10px); }
|
||||
|
|
|
|||
|
|
@ -0,0 +1,15 @@
|
|||
CREATE TABLE `bridge_orders` (
|
||||
`id` int AUTO_INCREMENT NOT NULL,
|
||||
`txHash` varchar(128) NOT NULL,
|
||||
`walletAddress` varchar(64) NOT NULL,
|
||||
`fromChainId` int NOT NULL,
|
||||
`fromToken` varchar(32) NOT NULL,
|
||||
`fromAmount` decimal(30,6) NOT NULL,
|
||||
`toChainId` int NOT NULL,
|
||||
`toToken` varchar(32) NOT NULL,
|
||||
`toAmount` decimal(30,6) NOT NULL,
|
||||
`status` enum('pending','completed','failed') NOT NULL DEFAULT 'completed',
|
||||
`createdAt` timestamp NOT NULL DEFAULT (now()),
|
||||
CONSTRAINT `bridge_orders_id` PRIMARY KEY(`id`),
|
||||
CONSTRAINT `bridge_orders_txHash_unique` UNIQUE(`txHash`)
|
||||
);
|
||||
|
|
@ -0,0 +1,532 @@
|
|||
{
|
||||
"version": "5",
|
||||
"dialect": "mysql",
|
||||
"id": "f2da11d5-2ee3-40ce-9180-11a9480a5b91",
|
||||
"prevId": "6b25cb51-fd4a-43ff-9411-e1efd553f304",
|
||||
"tables": {
|
||||
"bridge_orders": {
|
||||
"name": "bridge_orders",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "int",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": true
|
||||
},
|
||||
"txHash": {
|
||||
"name": "txHash",
|
||||
"type": "varchar(128)",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"walletAddress": {
|
||||
"name": "walletAddress",
|
||||
"type": "varchar(64)",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"fromChainId": {
|
||||
"name": "fromChainId",
|
||||
"type": "int",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"fromToken": {
|
||||
"name": "fromToken",
|
||||
"type": "varchar(32)",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"fromAmount": {
|
||||
"name": "fromAmount",
|
||||
"type": "decimal(30,6)",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"toChainId": {
|
||||
"name": "toChainId",
|
||||
"type": "int",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"toToken": {
|
||||
"name": "toToken",
|
||||
"type": "varchar(32)",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"toAmount": {
|
||||
"name": "toAmount",
|
||||
"type": "decimal(30,6)",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"status": {
|
||||
"name": "status",
|
||||
"type": "enum('pending','completed','failed')",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": "'completed'"
|
||||
},
|
||||
"createdAt": {
|
||||
"name": "createdAt",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": "(now())"
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {},
|
||||
"compositePrimaryKeys": {
|
||||
"bridge_orders_id": {
|
||||
"name": "bridge_orders_id",
|
||||
"columns": [
|
||||
"id"
|
||||
]
|
||||
}
|
||||
},
|
||||
"uniqueConstraints": {
|
||||
"bridge_orders_txHash_unique": {
|
||||
"name": "bridge_orders_txHash_unique",
|
||||
"columns": [
|
||||
"txHash"
|
||||
]
|
||||
}
|
||||
},
|
||||
"checkConstraint": {}
|
||||
},
|
||||
"presale_config": {
|
||||
"name": "presale_config",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "int",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": true
|
||||
},
|
||||
"key": {
|
||||
"name": "key",
|
||||
"type": "varchar(64)",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"value": {
|
||||
"name": "value",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"label": {
|
||||
"name": "label",
|
||||
"type": "varchar(128)",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"description": {
|
||||
"name": "description",
|
||||
"type": "varchar(256)",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"type": {
|
||||
"name": "type",
|
||||
"type": "varchar(32)",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false,
|
||||
"default": "'text'"
|
||||
},
|
||||
"updatedAt": {
|
||||
"name": "updatedAt",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"onUpdate": true,
|
||||
"default": "(now())"
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {},
|
||||
"compositePrimaryKeys": {
|
||||
"presale_config_id": {
|
||||
"name": "presale_config_id",
|
||||
"columns": [
|
||||
"id"
|
||||
]
|
||||
}
|
||||
},
|
||||
"uniqueConstraints": {
|
||||
"presale_config_key_unique": {
|
||||
"name": "presale_config_key_unique",
|
||||
"columns": [
|
||||
"key"
|
||||
]
|
||||
}
|
||||
},
|
||||
"checkConstraint": {}
|
||||
},
|
||||
"presale_stats_cache": {
|
||||
"name": "presale_stats_cache",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "int",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": true
|
||||
},
|
||||
"chain": {
|
||||
"name": "chain",
|
||||
"type": "varchar(16)",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"usdtRaised": {
|
||||
"name": "usdtRaised",
|
||||
"type": "decimal(30,6)",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false,
|
||||
"default": "'0'"
|
||||
},
|
||||
"tokensSold": {
|
||||
"name": "tokensSold",
|
||||
"type": "decimal(30,6)",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false,
|
||||
"default": "'0'"
|
||||
},
|
||||
"weiRaised": {
|
||||
"name": "weiRaised",
|
||||
"type": "decimal(30,6)",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false,
|
||||
"default": "'0'"
|
||||
},
|
||||
"lastUpdated": {
|
||||
"name": "lastUpdated",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": "(now())"
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {},
|
||||
"compositePrimaryKeys": {
|
||||
"presale_stats_cache_id": {
|
||||
"name": "presale_stats_cache_id",
|
||||
"columns": [
|
||||
"id"
|
||||
]
|
||||
}
|
||||
},
|
||||
"uniqueConstraints": {},
|
||||
"checkConstraint": {}
|
||||
},
|
||||
"trc20_intents": {
|
||||
"name": "trc20_intents",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "int",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": true
|
||||
},
|
||||
"tronAddress": {
|
||||
"name": "tronAddress",
|
||||
"type": "varchar(64)",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"evmAddress": {
|
||||
"name": "evmAddress",
|
||||
"type": "varchar(64)",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"expectedUsdt": {
|
||||
"name": "expectedUsdt",
|
||||
"type": "decimal(20,6)",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"matched": {
|
||||
"name": "matched",
|
||||
"type": "boolean",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": false
|
||||
},
|
||||
"matchedPurchaseId": {
|
||||
"name": "matchedPurchaseId",
|
||||
"type": "int",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"createdAt": {
|
||||
"name": "createdAt",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": "(now())"
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {},
|
||||
"compositePrimaryKeys": {
|
||||
"trc20_intents_id": {
|
||||
"name": "trc20_intents_id",
|
||||
"columns": [
|
||||
"id"
|
||||
]
|
||||
}
|
||||
},
|
||||
"uniqueConstraints": {},
|
||||
"checkConstraint": {}
|
||||
},
|
||||
"trc20_purchases": {
|
||||
"name": "trc20_purchases",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "int",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": true
|
||||
},
|
||||
"txHash": {
|
||||
"name": "txHash",
|
||||
"type": "varchar(128)",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"fromAddress": {
|
||||
"name": "fromAddress",
|
||||
"type": "varchar(64)",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"usdtAmount": {
|
||||
"name": "usdtAmount",
|
||||
"type": "decimal(20,6)",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"xicAmount": {
|
||||
"name": "xicAmount",
|
||||
"type": "decimal(30,6)",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"blockNumber": {
|
||||
"name": "blockNumber",
|
||||
"type": "bigint",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"status": {
|
||||
"name": "status",
|
||||
"type": "enum('pending','confirmed','distributed','failed')",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": "'pending'"
|
||||
},
|
||||
"distributedAt": {
|
||||
"name": "distributedAt",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"distributeTxHash": {
|
||||
"name": "distributeTxHash",
|
||||
"type": "varchar(128)",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"evmAddress": {
|
||||
"name": "evmAddress",
|
||||
"type": "varchar(64)",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"createdAt": {
|
||||
"name": "createdAt",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": "(now())"
|
||||
},
|
||||
"updatedAt": {
|
||||
"name": "updatedAt",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"onUpdate": true,
|
||||
"default": "(now())"
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {},
|
||||
"compositePrimaryKeys": {
|
||||
"trc20_purchases_id": {
|
||||
"name": "trc20_purchases_id",
|
||||
"columns": [
|
||||
"id"
|
||||
]
|
||||
}
|
||||
},
|
||||
"uniqueConstraints": {
|
||||
"trc20_purchases_txHash_unique": {
|
||||
"name": "trc20_purchases_txHash_unique",
|
||||
"columns": [
|
||||
"txHash"
|
||||
]
|
||||
}
|
||||
},
|
||||
"checkConstraint": {}
|
||||
},
|
||||
"users": {
|
||||
"name": "users",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "int",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": true
|
||||
},
|
||||
"openId": {
|
||||
"name": "openId",
|
||||
"type": "varchar(64)",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"name": {
|
||||
"name": "name",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"email": {
|
||||
"name": "email",
|
||||
"type": "varchar(320)",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"loginMethod": {
|
||||
"name": "loginMethod",
|
||||
"type": "varchar(64)",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"role": {
|
||||
"name": "role",
|
||||
"type": "enum('user','admin')",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": "'user'"
|
||||
},
|
||||
"createdAt": {
|
||||
"name": "createdAt",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": "(now())"
|
||||
},
|
||||
"updatedAt": {
|
||||
"name": "updatedAt",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"onUpdate": true,
|
||||
"default": "(now())"
|
||||
},
|
||||
"lastSignedIn": {
|
||||
"name": "lastSignedIn",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": "(now())"
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {},
|
||||
"compositePrimaryKeys": {
|
||||
"users_id": {
|
||||
"name": "users_id",
|
||||
"columns": [
|
||||
"id"
|
||||
]
|
||||
}
|
||||
},
|
||||
"uniqueConstraints": {
|
||||
"users_openId_unique": {
|
||||
"name": "users_openId_unique",
|
||||
"columns": [
|
||||
"openId"
|
||||
]
|
||||
}
|
||||
},
|
||||
"checkConstraint": {}
|
||||
}
|
||||
},
|
||||
"views": {},
|
||||
"_meta": {
|
||||
"schemas": {},
|
||||
"tables": {},
|
||||
"columns": {}
|
||||
},
|
||||
"internal": {
|
||||
"tables": {},
|
||||
"indexes": {}
|
||||
}
|
||||
}
|
||||
|
|
@ -36,6 +36,13 @@
|
|||
"when": 1772955197567,
|
||||
"tag": "0004_parallel_unus",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 5,
|
||||
"version": "5",
|
||||
"when": 1773124399358,
|
||||
"tag": "0005_certain_betty_ross",
|
||||
"breakpoints": true
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -96,4 +96,21 @@ export const presaleConfig = mysqlTable("presale_config", {
|
|||
});
|
||||
|
||||
export type PresaleConfig = typeof presaleConfig.$inferSelect;
|
||||
export type InsertPresaleConfig = typeof presaleConfig.$inferInsert;
|
||||
export type InsertPresaleConfig = typeof presaleConfig.$inferInsert;
|
||||
// Cross-chain bridge orders — recorded when user completes a Li.Fi cross-chain purchase
|
||||
export const bridgeOrders = mysqlTable("bridge_orders", {
|
||||
id: int("id").autoincrement().primaryKey(),
|
||||
txHash: varchar("txHash", { length: 128 }).notNull().unique(),
|
||||
walletAddress: varchar("walletAddress", { length: 64 }).notNull(),
|
||||
fromChainId: int("fromChainId").notNull(),
|
||||
fromToken: varchar("fromToken", { length: 32 }).notNull(),
|
||||
fromAmount: decimal("fromAmount", { precision: 30, scale: 6 }).notNull(),
|
||||
toChainId: int("toChainId").notNull(),
|
||||
toToken: varchar("toToken", { length: 32 }).notNull(),
|
||||
toAmount: decimal("toAmount", { precision: 30, scale: 6 }).notNull(),
|
||||
status: mysqlEnum("status", ["pending", "completed", "failed"]).default("completed").notNull(),
|
||||
createdAt: timestamp("createdAt").defaultNow().notNull(),
|
||||
});
|
||||
|
||||
export type BridgeOrder = typeof bridgeOrders.$inferSelect;
|
||||
export type InsertBridgeOrder = typeof bridgeOrders.$inferInsert;
|
||||
|
|
|
|||
|
|
@ -16,6 +16,8 @@
|
|||
"@aws-sdk/client-s3": "^3.693.0",
|
||||
"@aws-sdk/s3-request-presigner": "^3.693.0",
|
||||
"@hookform/resolvers": "^5.2.2",
|
||||
"@lifi/sdk": "^3.16.0",
|
||||
"@lifi/wallet-management": "^3.22.7",
|
||||
"@radix-ui/react-accordion": "^1.2.12",
|
||||
"@radix-ui/react-alert-dialog": "^1.1.15",
|
||||
"@radix-ui/react-aspect-ratio": "^1.1.7",
|
||||
|
|
@ -42,7 +44,7 @@
|
|||
"@radix-ui/react-toggle": "^1.1.10",
|
||||
"@radix-ui/react-toggle-group": "^1.1.11",
|
||||
"@radix-ui/react-tooltip": "^1.2.8",
|
||||
"@tanstack/react-query": "^5.90.2",
|
||||
"@tanstack/react-query": "^5.90.21",
|
||||
"@trpc/client": "^11.6.0",
|
||||
"@trpc/react-query": "^11.6.0",
|
||||
"@trpc/server": "^11.6.0",
|
||||
|
|
@ -79,6 +81,7 @@
|
|||
"tronweb": "^6.2.2",
|
||||
"vaul": "^1.1.2",
|
||||
"viem": "^2.47.0",
|
||||
"wagmi": "^3.5.0",
|
||||
"wouter": "^3.3.5",
|
||||
"zod": "^4.1.12"
|
||||
},
|
||||
|
|
|
|||
4930
pnpm-lock.yaml
4930
pnpm-lock.yaml
File diff suppressed because it is too large
Load Diff
|
|
@ -38,7 +38,7 @@ const RPC_POOLS = {
|
|||
// ─── Contract Addresses ────────────────────────────────────────────────────────
|
||||
export const CONTRACTS = {
|
||||
BSC: {
|
||||
presale: "0xc65e7a2738ed884db8d26a6eb2fecf7daca2e90c",
|
||||
presale: "0x5953c025dA734e710886916F2d739A3A78f8bbc4", // XICPresale v2
|
||||
token: "0x59ff34dd59680a7125782b1f6df2a86ed46f5a24",
|
||||
rpc: RPC_POOLS.BSC[0],
|
||||
chainId: 56,
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ import { publicProcedure, protectedProcedure, router } from "./_core/trpc";
|
|||
import { getCombinedStats, getPresaleStats } from "./onchain";
|
||||
import { getRecentPurchases } from "./trc20Monitor";
|
||||
import { getDb } from "./db";
|
||||
import { trc20Purchases, trc20Intents } from "../drizzle/schema";
|
||||
import { trc20Purchases, trc20Intents, bridgeOrders } from "../drizzle/schema";
|
||||
import { eq, desc, sql } from "drizzle-orm";
|
||||
import { z } from "zod";
|
||||
import { TRPCError } from "@trpc/server";
|
||||
|
|
@ -15,8 +15,102 @@ import { getAllConfig, getConfig, setConfig, seedDefaultConfig, DEFAULT_CONFIG }
|
|||
// Admin password from env (fallback for development)
|
||||
const ADMIN_PASSWORD = process.env.ADMIN_PASSWORD || "NACadmin2026!";
|
||||
|
||||
// ─── Bridge Router ───────────────────────────────────────────────────────────
|
||||
const bridgeRouter = router({
|
||||
// Record a completed Li.Fi cross-chain order
|
||||
recordOrder: publicProcedure
|
||||
.input(z.object({
|
||||
txHash: z.string().min(1).max(128),
|
||||
walletAddress: z.string().min(1).max(64),
|
||||
fromChainId: z.number().int(),
|
||||
fromToken: z.string().max(32),
|
||||
fromAmount: z.string(),
|
||||
toChainId: z.number().int(),
|
||||
toToken: z.string().max(32),
|
||||
toAmount: z.string(),
|
||||
}))
|
||||
.mutation(async ({ input }) => {
|
||||
const db = await getDb();
|
||||
if (!db) return { success: false, message: "DB unavailable" };
|
||||
try {
|
||||
await db.insert(bridgeOrders).values({
|
||||
txHash: input.txHash,
|
||||
walletAddress: input.walletAddress,
|
||||
fromChainId: input.fromChainId,
|
||||
fromToken: input.fromToken,
|
||||
fromAmount: input.fromAmount,
|
||||
toChainId: input.toChainId,
|
||||
toToken: input.toToken,
|
||||
toAmount: input.toAmount,
|
||||
status: "completed",
|
||||
});
|
||||
return { success: true };
|
||||
} catch (e: any) {
|
||||
if (e?.code === "ER_DUP_ENTRY") return { success: true };
|
||||
throw e;
|
||||
}
|
||||
}),
|
||||
|
||||
// List orders by wallet address
|
||||
myOrders: publicProcedure
|
||||
.input(z.object({
|
||||
walletAddress: z.string().min(1).max(64),
|
||||
limit: z.number().min(1).max(50).default(20),
|
||||
}))
|
||||
.query(async ({ input }) => {
|
||||
const db = await getDb();
|
||||
if (!db) return [];
|
||||
const rows = await db
|
||||
.select()
|
||||
.from(bridgeOrders)
|
||||
.where(eq(bridgeOrders.walletAddress, input.walletAddress.toLowerCase()))
|
||||
.orderBy(desc(bridgeOrders.createdAt))
|
||||
.limit(input.limit);
|
||||
return rows.map(r => ({
|
||||
id: r.id,
|
||||
txHash: r.txHash,
|
||||
walletAddress: r.walletAddress,
|
||||
fromChainId: r.fromChainId,
|
||||
fromToken: r.fromToken,
|
||||
fromAmount: Number(r.fromAmount),
|
||||
toChainId: r.toChainId,
|
||||
toToken: r.toToken,
|
||||
toAmount: Number(r.toAmount),
|
||||
status: r.status,
|
||||
createdAt: r.createdAt,
|
||||
}));
|
||||
}),
|
||||
|
||||
// List recent bridge orders (public)
|
||||
recentOrders: publicProcedure
|
||||
.input(z.object({ limit: z.number().min(1).max(50).default(10) }))
|
||||
.query(async ({ input }) => {
|
||||
const db = await getDb();
|
||||
if (!db) return [];
|
||||
const rows = await db
|
||||
.select()
|
||||
.from(bridgeOrders)
|
||||
.orderBy(desc(bridgeOrders.createdAt))
|
||||
.limit(input.limit);
|
||||
return rows.map(r => ({
|
||||
id: r.id,
|
||||
txHash: r.txHash,
|
||||
walletAddress: r.walletAddress,
|
||||
fromChainId: r.fromChainId,
|
||||
fromToken: r.fromToken,
|
||||
fromAmount: Number(r.fromAmount),
|
||||
toChainId: r.toChainId,
|
||||
toToken: r.toToken,
|
||||
toAmount: Number(r.toAmount),
|
||||
status: r.status,
|
||||
createdAt: r.createdAt,
|
||||
}));
|
||||
}),
|
||||
});
|
||||
|
||||
export const appRouter = router({
|
||||
system: systemRouter,
|
||||
bridge: bridgeRouter,
|
||||
auth: router({
|
||||
me: publicProcedure.query(opts => opts.ctx.user),
|
||||
logout: publicProcedure.mutation(({ ctx }) => {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,113 @@
|
|||
/**
|
||||
* 测试链上数据读取
|
||||
* 直接调用BSC和ETH合约,查看能读到哪些数据
|
||||
*/
|
||||
import { ethers } from "ethers";
|
||||
|
||||
const BSC_PRESALE = "0xc65e7a2738ed884db8d26a6eb2fecf7daca2e90c";
|
||||
const ETH_PRESALE = "0x85AB2F2d9f7ca7ecB272b5E8726c70f3fd45D1E3";
|
||||
|
||||
// 尝试多种可能的函数名
|
||||
const TEST_ABI = [
|
||||
"function totalUSDTRaised() view returns (uint256)",
|
||||
"function totalTokensSold() view returns (uint256)",
|
||||
"function weiRaised() view returns (uint256)",
|
||||
"function tokensSold() view returns (uint256)",
|
||||
"function usdtRaised() view returns (uint256)",
|
||||
"function totalRaised() view returns (uint256)",
|
||||
"function amountRaised() view returns (uint256)",
|
||||
"function hardCap() view returns (uint256)",
|
||||
"function cap() view returns (uint256)",
|
||||
"function owner() view returns (address)",
|
||||
"function paused() view returns (bool)",
|
||||
];
|
||||
|
||||
async function testContract(name, address, rpcUrl) {
|
||||
console.log(`\n=== 测试 ${name} 合约 ===`);
|
||||
console.log(`地址: ${address}`);
|
||||
console.log(`RPC: ${rpcUrl}`);
|
||||
|
||||
try {
|
||||
const provider = new ethers.JsonRpcProvider(rpcUrl, undefined, {
|
||||
staticNetwork: true,
|
||||
polling: false,
|
||||
});
|
||||
|
||||
// 先检查合约是否存在
|
||||
const code = await provider.getCode(address);
|
||||
if (code === "0x") {
|
||||
console.log("❌ 该地址没有合约代码!合约地址可能错误。");
|
||||
return;
|
||||
}
|
||||
console.log(`✅ 合约存在,字节码长度: ${code.length} 字符`);
|
||||
|
||||
const contract = new ethers.Contract(address, TEST_ABI, provider);
|
||||
|
||||
// 逐个测试函数
|
||||
const functions = [
|
||||
"totalUSDTRaised",
|
||||
"totalTokensSold",
|
||||
"weiRaised",
|
||||
"tokensSold",
|
||||
"usdtRaised",
|
||||
"totalRaised",
|
||||
"amountRaised",
|
||||
"hardCap",
|
||||
"cap",
|
||||
"owner",
|
||||
"paused",
|
||||
];
|
||||
|
||||
for (const fn of functions) {
|
||||
try {
|
||||
const result = await contract[fn]();
|
||||
console.log(` ✅ ${fn}() = ${result}`);
|
||||
} catch (e) {
|
||||
console.log(` ❌ ${fn}() 不存在或调用失败`);
|
||||
}
|
||||
}
|
||||
|
||||
// 获取合约事件日志(最近100个块)
|
||||
const latestBlock = await provider.getBlockNumber();
|
||||
console.log(`\n当前区块高度: ${latestBlock}`);
|
||||
|
||||
// 查找Transfer事件(USDT转入)
|
||||
const usdtAddress = name === "BSC"
|
||||
? "0x55d398326f99059fF775485246999027B3197955"
|
||||
: "0xdAC17F958D2ee523a2206206994597C13D831ec7";
|
||||
|
||||
const usdtAbi = ["event Transfer(address indexed from, address indexed to, uint256 value)"];
|
||||
const usdtContract = new ethers.Contract(usdtAddress, usdtAbi, provider);
|
||||
|
||||
console.log(`\n查询最近1000个块内转入预售合约的USDT...`);
|
||||
const fromBlock = latestBlock - 1000;
|
||||
const filter = usdtContract.filters.Transfer(null, address);
|
||||
|
||||
try {
|
||||
const events = await usdtContract.queryFilter(filter, fromBlock, latestBlock);
|
||||
console.log(`找到 ${events.length} 笔USDT转入记录`);
|
||||
|
||||
let totalUsdt = 0n;
|
||||
for (const event of events.slice(-5)) {
|
||||
const args = event.args;
|
||||
const amount = args[2];
|
||||
totalUsdt += amount;
|
||||
const decimals = name === "BSC" ? 18 : 6;
|
||||
console.log(` ${args[0]} → ${ethers.formatUnits(amount, decimals)} USDT`);
|
||||
}
|
||||
} catch (e) {
|
||||
console.log(`查询事件失败: ${e}`);
|
||||
}
|
||||
|
||||
} catch (e) {
|
||||
console.error(`测试失败: ${e}`);
|
||||
}
|
||||
}
|
||||
|
||||
// 测试BSC
|
||||
await testContract("BSC", BSC_PRESALE, "https://bsc-dataseed1.binance.org/");
|
||||
|
||||
// 测试ETH
|
||||
await testContract("ETH", ETH_PRESALE, "https://eth.llamarpc.com");
|
||||
|
||||
console.log("\n=== 测试完成 ===");
|
||||
81
todo.md
81
todo.md
|
|
@ -71,3 +71,84 @@
|
|||
- [x] 创建 WalletSelector 组件(MetaMask、Trust Wallet、OKX、Coinbase、TokenPocket 检测+连接+安装引导)
|
||||
- [x] 集成 WalletSelector 到 TRON 标签 XIC 接收地址区域- [x] 集成 WalletSelector 到 BSC/ETH 购买面板替换原 Connect Wallet 按鈕钮
|
||||
- [x] 构建并部署到备份服务器
|
||||
|
||||
## v8 UI设计错误修复
|
||||
- [ ] 修复图1:钱包选择器弹窗同时显示"已安装"和"未安装"钱包,界面混乱 → 有已安装钱包时隐藏未安装列表
|
||||
- [ ] 修复图2:点击钱包后选择器面板折叠缩小 → 连接中状态应保持面板展开,显示loading状态
|
||||
- [ ] 修复图3:"添加XIC到钱包"按钮在未连接钱包时显示并报错 → 未连接时隐藏该按钮
|
||||
- [ ] 构建并部署到备份服务器
|
||||
- [ ] 同步到Gitea代码库
|
||||
|
||||
## v9 跨链桥 /bridge 页面
|
||||
- [x] 安装 @lifi/sdk 依赖(使用SDK替代Widget,避免@mysten/sui冲突)
|
||||
- [x] 创建 Bridge.tsx 页面组件(深色主题,与预售网站风格一致)
|
||||
- [x] 集成 Li.Fi API,锁定目标链 BSC + 目标代币 XIC
|
||||
- [x] 在 App.tsx 注册 /bridge 路由
|
||||
- [x] 导航栏添加 Bridge 入口链接(⚡ Bridge 黄色高亮按钮)
|
||||
- [x] 后端添加跨链订单记录(bridge_orders 表)
|
||||
- [x] 浏览器测试 /bridge 页面(UI渲染、链切换、金额输入正常)
|
||||
- [ ] 去除 MANUS 内联,构建并部署到 AI 服务器
|
||||
- [ ] 记录部署日志并交付
|
||||
|
||||
## v10 Bridge完善 + 钱包选择器修复
|
||||
|
||||
### Bridge Li.Fi 交易执行逻辑
|
||||
- [x] 实现完整的 Li.Fi 跨链交易执行(USDT Approve + executeLiFiRoute)
|
||||
- [x] 连接钱包后自动获取报价(已有,验证可用)
|
||||
- [x] 执行交易:先 Approve USDT,再发送跨链交易
|
||||
- [x] 交易状态轮询(pending → success/failed)
|
||||
- [x] 成功后记录订单到数据库
|
||||
|
||||
### Bridge 交易历史记录模块
|
||||
- [x] Bridge 页面底部增加"我的交易记录"区域
|
||||
- [x] 按钱包地址查询历史订单(trpc.bridge.myOrders)
|
||||
- [x] 显示:时间、来源链、USDT金额、XIC金额、状态、TxHash链接
|
||||
|
||||
### v8 钱包选择器 UI 修复
|
||||
- [ ] 修复图1:有已安装钱包时隐藏未安装列表(已在代码中但需验证)
|
||||
- [ ] 修复图2:连接中状态保持面板展开,显示loading,不折叠
|
||||
- [ ] 修复图3:未连接钱包时隐藏"添加XIC到钱包"按钮(已有条件判断,需验证)
|
||||
|
||||
### 部署
|
||||
- [ ] 构建并部署到 AI 服务器
|
||||
- [ ] 浏览器测试验证所有功能
|
||||
- [ ] 记录部署日志并交付
|
||||
|
||||
### Bridge 钱包连接修复(来自截图反馈)
|
||||
- [ ] Bridge 页面"连接钱包"按钮改为使用 WalletSelector 组件(与主页一致),而非直接调用 window.ethereum
|
||||
- [x] 连接钱包后自动获取报价,不再显示 WalletSelector
|
||||
|
||||
### 视频反馈修复(来自 WhatsApp 视频)
|
||||
- [ ] Bridge 页面"连接钱包"按钮改为内嵌 WalletSelector 组件(展开显示钱包列表,不弹浏览器原生弹窗)
|
||||
- [ ] 错误提示"Wallet connection cancelled"改为中英文双语
|
||||
- [ ] Bridge 页面添加中英文语言切换支持(与主页同步)
|
||||
- [ ] 信息卡片"5岁以上"应为"5条以上"(支持链数量)
|
||||
|
||||
## v11 Bridge增强功能
|
||||
|
||||
- [ ] Gas费估算显示:在"YOU RECEIVE"区域下方显示预估Gas费(源链原生代币)和预计到账时间
|
||||
- [ ] Gas费说明文案:说明Gas用源链原生代币支付(BSC用BNB,ETH用ETH,Polygon用MATIC等)
|
||||
- [ ] 交易历史"复制交易哈希"快捷按钮
|
||||
- [ ] 交易历史"在区块浏览器中查看"快捷按钮
|
||||
- [ ] 交易成功弹窗提示(附查看交易详情链接)
|
||||
- [ ] 浏览器全流程测试
|
||||
- [ ] 构建并部署到AI服务器
|
||||
- [ ] 记录部署日志
|
||||
|
||||
## v11 钱包连接卡死修复(来自用户反馈)
|
||||
|
||||
- [ ] 修复WalletSelector连接卡死:连接超时30s自动重置状态
|
||||
- [ ] 修复用户取消钱包弹窗后状态不重置(error code 4001/4100处理)
|
||||
- [ ] 修复连接成功后回调不触发(accounts事件监听改为直接返回值处理)
|
||||
- [ ] 确保每次点击钱包按钮都能重新触发钱包弹窗
|
||||
|
||||
## v12 Bridge跨链桥完善 + 钱包连接深度修复
|
||||
|
||||
- [ ] WalletSelector v5:ErrorHelpPanel组件(分类错误处理+MetaMask权限重置5步指南)
|
||||
- [ ] WalletSelector v5:连接中状态改为"等待钱包授权"提示
|
||||
- [ ] WalletSelector v5:错误后显示"重试"按钮和其他可用钱包
|
||||
- [ ] Bridge页面:确认所有链(BSC/ETH/Polygon/Arbitrum/Avalanche)USDT→XIC路由逻辑
|
||||
- [ ] Bridge页面:Gas费说明(每条链原生代币:BNB/ETH/MATIC/ETH/AVAX)
|
||||
- [ ] 构建v12并部署到AI服务器(43.224.155.27)
|
||||
- [ ] 同步代码到备份Git库(git.newassetchain.io)
|
||||
- [ ] 记录部署日志
|
||||
|
|
|
|||
|
|
@ -168,6 +168,9 @@ export default defineConfig({
|
|||
outDir: path.resolve(import.meta.dirname, "dist/public"),
|
||||
emptyOutDir: true,
|
||||
},
|
||||
optimizeDeps: {
|
||||
exclude: ["@mysten/sui", "@mysten/wallet-standard", "@solana/web3.js", "@solana/wallet-adapter-base"],
|
||||
},
|
||||
server: {
|
||||
host: true,
|
||||
allowedHosts: [
|
||||
|
|
|
|||
Loading…
Reference in New Issue