830 lines
31 KiB
JavaScript
830 lines
31 KiB
JavaScript
/**
|
||
* XIC Multi-Wallet Connector v2.1
|
||
* 支持:MetaMask、TronLink、OKX Wallet、Trust Wallet、TokenPocket、Bitget Wallet、Phantom
|
||
* 修复:MetaMask 连接失败、缓存问题、移动端兼容
|
||
*/
|
||
(function () {
|
||
'use strict';
|
||
|
||
// ─── 样式 ─────────────────────────────────────────────────────────────────────
|
||
const STYLES = `
|
||
@keyframes xicFadeIn { from { opacity:0; transform:translateY(-10px) scale(0.97); } to { opacity:1; transform:translateY(0) scale(1); } }
|
||
@keyframes xicOverlayIn { from { opacity:0; } to { opacity:1; } }
|
||
@keyframes xicToastIn { from { opacity:0; transform:translateX(-50%) translateY(20px); } to { opacity:1; transform:translateX(-50%) translateY(0); } }
|
||
@keyframes xicSpin { from { transform:rotate(0deg); } to { transform:rotate(360deg); } }
|
||
|
||
#xic-wallet-overlay {
|
||
display: none;
|
||
position: fixed;
|
||
inset: 0;
|
||
background: rgba(0,0,0,0.75);
|
||
backdrop-filter: blur(6px);
|
||
z-index: 999998;
|
||
align-items: center;
|
||
justify-content: center;
|
||
animation: xicOverlayIn 0.25s ease;
|
||
}
|
||
#xic-wallet-overlay.open { display: flex; }
|
||
|
||
#xic-wallet-modal {
|
||
background: linear-gradient(145deg, #1a1b2e, #16213e);
|
||
border: 1px solid rgba(99,102,241,0.3);
|
||
border-radius: 20px;
|
||
padding: 28px;
|
||
width: 420px;
|
||
max-width: calc(100vw - 32px);
|
||
max-height: 90vh;
|
||
overflow-y: auto;
|
||
box-shadow: 0 25px 60px rgba(0,0,0,0.6), 0 0 0 1px rgba(255,255,255,0.05);
|
||
animation: xicFadeIn 0.3s cubic-bezier(0.34,1.56,0.64,1);
|
||
font-family: 'Plus Jakarta Sans', 'Inter', -apple-system, sans-serif;
|
||
}
|
||
|
||
#xic-wallet-modal::-webkit-scrollbar { width: 4px; }
|
||
#xic-wallet-modal::-webkit-scrollbar-track { background: transparent; }
|
||
#xic-wallet-modal::-webkit-scrollbar-thumb { background: rgba(99,102,241,0.4); border-radius: 2px; }
|
||
|
||
.xic-modal-header {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
margin-bottom: 6px;
|
||
}
|
||
.xic-modal-title {
|
||
font-size: 20px;
|
||
font-weight: 700;
|
||
color: #fff;
|
||
letter-spacing: -0.3px;
|
||
}
|
||
.xic-modal-subtitle {
|
||
font-size: 13px;
|
||
color: rgba(255,255,255,0.5);
|
||
margin-bottom: 20px;
|
||
}
|
||
#xic-close-btn {
|
||
background: rgba(255,255,255,0.08);
|
||
border: none;
|
||
color: rgba(255,255,255,0.7);
|
||
width: 32px;
|
||
height: 32px;
|
||
border-radius: 8px;
|
||
cursor: pointer;
|
||
font-size: 16px;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
transition: all 0.2s;
|
||
flex-shrink: 0;
|
||
}
|
||
#xic-close-btn:hover { background: rgba(255,255,255,0.15); color: #fff; }
|
||
|
||
.xic-wallet-grid {
|
||
display: grid;
|
||
grid-template-columns: 1fr 1fr;
|
||
gap: 10px;
|
||
margin-bottom: 16px;
|
||
}
|
||
|
||
.xic-wallet-btn {
|
||
background: rgba(255,255,255,0.05);
|
||
border: 1px solid rgba(255,255,255,0.08);
|
||
border-radius: 14px;
|
||
padding: 14px 12px;
|
||
cursor: pointer;
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
gap: 8px;
|
||
transition: all 0.2s;
|
||
position: relative;
|
||
text-align: center;
|
||
}
|
||
.xic-wallet-btn:hover {
|
||
background: rgba(99,102,241,0.15);
|
||
border-color: rgba(99,102,241,0.4);
|
||
transform: translateY(-2px);
|
||
box-shadow: 0 8px 24px rgba(99,102,241,0.2);
|
||
}
|
||
.xic-wallet-btn:active { transform: translateY(0); }
|
||
.xic-wallet-btn.loading { pointer-events: none; opacity: 0.7; }
|
||
.xic-wallet-btn.detected {
|
||
border-color: rgba(16,185,129,0.4);
|
||
background: rgba(16,185,129,0.06);
|
||
}
|
||
|
||
.xic-wallet-icon {
|
||
width: 44px;
|
||
height: 44px;
|
||
border-radius: 12px;
|
||
overflow: hidden;
|
||
flex-shrink: 0;
|
||
}
|
||
.xic-wallet-icon svg, .xic-wallet-icon img { width: 100%; height: 100%; }
|
||
|
||
.xic-wallet-name {
|
||
font-size: 13px;
|
||
font-weight: 600;
|
||
color: #fff;
|
||
line-height: 1.2;
|
||
}
|
||
.xic-wallet-desc {
|
||
font-size: 11px;
|
||
color: rgba(255,255,255,0.45);
|
||
line-height: 1.3;
|
||
}
|
||
.xic-wallet-network {
|
||
font-size: 10px;
|
||
font-weight: 600;
|
||
padding: 2px 8px;
|
||
border-radius: 20px;
|
||
background: rgba(99,102,241,0.2);
|
||
color: rgba(99,102,241,0.9);
|
||
letter-spacing: 0.3px;
|
||
}
|
||
.xic-detected-dot {
|
||
position: absolute;
|
||
top: 10px;
|
||
right: 10px;
|
||
width: 8px;
|
||
height: 8px;
|
||
background: #10b981;
|
||
border-radius: 50%;
|
||
box-shadow: 0 0 6px #10b981;
|
||
}
|
||
.xic-spinner {
|
||
width: 20px;
|
||
height: 20px;
|
||
border: 2px solid rgba(255,255,255,0.2);
|
||
border-top-color: #fff;
|
||
border-radius: 50%;
|
||
animation: xicSpin 0.8s linear infinite;
|
||
margin: 0 auto;
|
||
}
|
||
|
||
.xic-modal-footer {
|
||
text-align: center;
|
||
font-size: 12px;
|
||
color: rgba(255,255,255,0.35);
|
||
padding-top: 12px;
|
||
border-top: 1px solid rgba(255,255,255,0.06);
|
||
}
|
||
.xic-modal-footer a {
|
||
color: rgba(99,102,241,0.8);
|
||
text-decoration: none;
|
||
}
|
||
.xic-modal-footer a:hover { color: #818cf8; text-decoration: underline; }
|
||
|
||
.xic-install-hint {
|
||
background: rgba(245,158,11,0.1);
|
||
border: 1px solid rgba(245,158,11,0.25);
|
||
border-radius: 12px;
|
||
padding: 12px 14px;
|
||
margin-top: 12px;
|
||
font-size: 12px;
|
||
color: rgba(255,255,255,0.7);
|
||
display: none;
|
||
}
|
||
.xic-install-hint.show { display: block; }
|
||
.xic-install-hint strong { color: #fbbf24; }
|
||
.xic-install-hint a {
|
||
color: #fbbf24;
|
||
font-weight: 600;
|
||
text-decoration: none;
|
||
}
|
||
.xic-install-hint a:hover { text-decoration: underline; }
|
||
|
||
.xic-connected-banner {
|
||
background: linear-gradient(135deg, rgba(16,185,129,0.15), rgba(5,150,105,0.1));
|
||
border: 1px solid rgba(16,185,129,0.3);
|
||
border-radius: 12px;
|
||
padding: 12px 16px;
|
||
margin-bottom: 16px;
|
||
display: none;
|
||
align-items: center;
|
||
gap: 10px;
|
||
}
|
||
.xic-connected-banner.show { display: flex; }
|
||
.xic-connected-dot {
|
||
width: 10px;
|
||
height: 10px;
|
||
background: #10b981;
|
||
border-radius: 50%;
|
||
box-shadow: 0 0 8px #10b981;
|
||
flex-shrink: 0;
|
||
}
|
||
.xic-connected-text { flex: 1; }
|
||
.xic-connected-label { font-size: 11px; color: rgba(255,255,255,0.5); }
|
||
.xic-connected-addr { font-size: 13px; font-weight: 600; color: #10b981; font-family: monospace; }
|
||
.xic-disconnect-btn {
|
||
background: rgba(239,68,68,0.15);
|
||
border: 1px solid rgba(239,68,68,0.3);
|
||
color: #f87171;
|
||
border-radius: 8px;
|
||
padding: 4px 10px;
|
||
font-size: 11px;
|
||
cursor: pointer;
|
||
transition: all 0.2s;
|
||
}
|
||
.xic-disconnect-btn:hover { background: rgba(239,68,68,0.25); }
|
||
`;
|
||
|
||
// ─── 钱包配置 ─────────────────────────────────────────────────────────────────
|
||
const WALLETS = [
|
||
{
|
||
id: 'metamask',
|
||
name: 'MetaMask',
|
||
desc: '最流行的以太坊钱包',
|
||
network: 'ETH/BSC',
|
||
installUrl: 'https://metamask.io/download/',
|
||
icon: `<svg viewBox="0 0 40 40" fill="none" xmlns="http://www.w3.org/2000/svg"><rect width="40" height="40" rx="10" fill="#F6851B"/><path d="M32 10L22.5 17L24.3 13L32 10Z" fill="#E2761B"/><path d="M8 10L17.4 17.1L15.7 13L8 10Z" fill="#E4761B"/><path d="M28.7 26.5L26.1 30.5L31.4 32L33 26.6L28.7 26.5Z" fill="#E4761B"/><path d="M7 26.6L8.6 32L13.9 30.5L11.3 26.5L7 26.6Z" fill="#E4761B"/><path d="M13.6 21.5L12 24L17.2 24.3L17 18.8L13.6 21.5Z" fill="#E4761B"/><path d="M26.4 21.5L22.9 18.7L22.8 24.3L28 24L26.4 21.5Z" fill="#E4761B"/><path d="M13.9 30.5L16.8 29L14.3 26.6L13.9 30.5Z" fill="#E4761B"/><path d="M23.2 29L26.1 30.5L25.7 26.6L23.2 29Z" fill="#E4761B"/></svg>`,
|
||
detect: () => {
|
||
// 检测 MetaMask:window.ethereum.isMetaMask 或 window.ethereum providers 数组
|
||
if (window.ethereum) {
|
||
if (window.ethereum.isMetaMask) return true;
|
||
if (window.ethereum.providers) {
|
||
return window.ethereum.providers.some(p => p.isMetaMask);
|
||
}
|
||
}
|
||
return false;
|
||
},
|
||
connect: connectMetaMask,
|
||
},
|
||
{
|
||
id: 'tronlink',
|
||
name: 'TronLink',
|
||
desc: 'TRON 官方钱包',
|
||
network: 'TRON',
|
||
installUrl: 'https://www.tronlink.org/',
|
||
icon: `<svg viewBox="0 0 40 40" fill="none" xmlns="http://www.w3.org/2000/svg"><rect width="40" height="40" rx="10" fill="#EF0027"/><path d="M20 8L32 15V25L20 32L8 25V15L20 8Z" fill="#FF3355" opacity="0.3"/><path d="M20 10L30 16.5V23.5L20 30L10 23.5V16.5L20 10Z" fill="white" opacity="0.9"/><path d="M20 14L26 17.5V24.5L20 28L14 24.5V17.5L20 14Z" fill="#EF0027"/></svg>`,
|
||
detect: () => !!(window.tronLink || (window.tronWeb && window.tronWeb.ready)),
|
||
connect: connectTronLink,
|
||
},
|
||
{
|
||
id: 'okx',
|
||
name: 'OKX Wallet',
|
||
desc: 'OKX 多链钱包',
|
||
network: 'TRON/ETH',
|
||
installUrl: 'https://www.okx.com/web3',
|
||
icon: `<svg viewBox="0 0 40 40" fill="none" xmlns="http://www.w3.org/2000/svg"><rect width="40" height="40" rx="10" fill="#000"/><rect x="10" y="10" width="8" height="8" rx="1" fill="white"/><rect x="22" y="10" width="8" height="8" rx="1" fill="white"/><rect x="16" y="16" width="8" height="8" rx="1" fill="white"/><rect x="10" y="22" width="8" height="8" rx="1" fill="white"/><rect x="22" y="22" width="8" height="8" rx="1" fill="white"/></svg>`,
|
||
detect: () => !!(window.okxwallet || (window.ethereum && window.ethereum.isOkxWallet)),
|
||
connect: connectOKX,
|
||
},
|
||
{
|
||
id: 'trust',
|
||
name: 'Trust Wallet',
|
||
desc: '币安官方钱包',
|
||
network: 'TRON/ETH',
|
||
installUrl: 'https://trustwallet.com/download',
|
||
icon: `<svg viewBox="0 0 40 40" fill="none" xmlns="http://www.w3.org/2000/svg"><rect width="40" height="40" rx="10" fill="#3375BB"/><path d="M20 8L30 12V20C30 25.5 25.5 30.5 20 32C14.5 30.5 10 25.5 10 20V12L20 8Z" fill="white"/><path d="M16 20L18.5 22.5L24 17" stroke="#3375BB" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"/></svg>`,
|
||
detect: () => !!(window.trustwallet || (window.ethereum && window.ethereum.isTrust)),
|
||
connect: connectTrust,
|
||
},
|
||
{
|
||
id: 'tokenpocket',
|
||
name: 'TokenPocket',
|
||
desc: '多链 DeFi 钱包',
|
||
network: 'TRON/ETH',
|
||
installUrl: 'https://www.tokenpocket.pro/',
|
||
icon: `<svg viewBox="0 0 40 40" fill="none" xmlns="http://www.w3.org/2000/svg"><rect width="40" height="40" rx="10" fill="#2980FE"/><rect x="10" y="12" width="20" height="4" rx="2" fill="white"/><rect x="10" y="18" width="14" height="4" rx="2" fill="white"/><rect x="10" y="24" width="17" height="4" rx="2" fill="white"/></svg>`,
|
||
detect: () => !!(window.tokenpocket || (window.ethereum && window.ethereum.isTokenPocket)),
|
||
connect: connectTokenPocket,
|
||
},
|
||
{
|
||
id: 'bitget',
|
||
name: 'Bitget Wallet',
|
||
desc: 'Bitget 交易所钱包',
|
||
network: 'TRON/ETH',
|
||
installUrl: 'https://web3.bitget.com/zh-CN/wallet-download',
|
||
icon: `<svg viewBox="0 0 40 40" fill="none" xmlns="http://www.w3.org/2000/svg"><rect width="40" height="40" rx="10" fill="#00F0FF"/><path d="M12 20C12 15.6 15.6 12 20 12C22.2 12 24.2 12.9 25.7 14.3L28 12C26 10.1 23.1 9 20 9C13.9 9 9 13.9 9 20C9 26.1 13.9 31 20 31C23.1 31 26 29.9 28 28L25.7 25.7C24.2 27.1 22.2 28 20 28C15.6 28 12 24.4 12 20Z" fill="#1A1A2E"/></svg>`,
|
||
detect: () => !!(window.bitkeep || (window.ethereum && window.ethereum.isBitKeep)),
|
||
connect: connectBitget,
|
||
},
|
||
{
|
||
id: 'phantom',
|
||
name: 'Phantom',
|
||
desc: 'Solana & 多链钱包',
|
||
network: 'SOL/ETH',
|
||
installUrl: 'https://phantom.app/download',
|
||
icon: `<svg viewBox="0 0 40 40" fill="none" xmlns="http://www.w3.org/2000/svg"><rect width="40" height="40" rx="10" fill="#AB9FF2"/><path d="M20 9C14 9 9 14 9 20C9 26 14 31 20 31C26 31 31 26 31 20C31 14 26 9 20 9ZM20 27C16.1 27 13 23.9 13 20C13 16.1 16.1 13 20 13C23.9 13 27 16.1 27 20C27 23.9 23.9 27 20 27Z" fill="white"/><circle cx="17" cy="19" r="2" fill="white"/><circle cx="23" cy="19" r="2" fill="white"/></svg>`,
|
||
detect: () => !!(window.phantom),
|
||
connect: connectPhantom,
|
||
},
|
||
];
|
||
|
||
// ─── 状态 ─────────────────────────────────────────────────────────────────────
|
||
let currentWallet = null;
|
||
let currentAddress = null;
|
||
let modalEl = null;
|
||
let overlayEl = null;
|
||
let isInjected = false;
|
||
let installHintEl = null;
|
||
let connectedBannerEl = null;
|
||
|
||
// ─── 工具函数 ─────────────────────────────────────────────────────────────────
|
||
function shortenAddr(addr) {
|
||
if (!addr) return '';
|
||
if (addr.length > 20) return addr.slice(0, 8) + '...' + addr.slice(-6);
|
||
return addr;
|
||
}
|
||
|
||
function showToast(msg, type = 'info') {
|
||
const colors = { info: '#6366f1', success: '#10b981', error: '#ef4444', warn: '#f59e0b' };
|
||
// 移除已有 toast
|
||
document.querySelectorAll('.xic-toast').forEach(t => t.remove());
|
||
const toast = document.createElement('div');
|
||
toast.className = 'xic-toast';
|
||
toast.style.cssText = `
|
||
position:fixed; bottom:28px; left:50%; transform:translateX(-50%);
|
||
background:${colors[type] || colors.info}; color:#fff;
|
||
padding:12px 24px; border-radius:12px; font-size:14px; font-weight:600;
|
||
z-index:9999999; box-shadow:0 8px 32px rgba(0,0,0,0.4);
|
||
font-family:'Plus Jakarta Sans','Inter',sans-serif;
|
||
animation: xicToastIn 0.3s ease;
|
||
pointer-events:none; white-space:nowrap; max-width:90vw;
|
||
`;
|
||
toast.textContent = msg;
|
||
document.body.appendChild(toast);
|
||
setTimeout(() => toast.remove(), 4000);
|
||
}
|
||
|
||
function getEthProvider() {
|
||
// 获取 MetaMask provider(处理多钱包共存的情况)
|
||
if (!window.ethereum) return null;
|
||
if (window.ethereum.isMetaMask) return window.ethereum;
|
||
// 多钱包共存时,从 providers 数组中找 MetaMask
|
||
if (window.ethereum.providers) {
|
||
const mm = window.ethereum.providers.find(p => p.isMetaMask);
|
||
if (mm) return mm;
|
||
}
|
||
return null;
|
||
}
|
||
|
||
// ─── 钱包连接函数 ─────────────────────────────────────────────────────────────
|
||
async function connectMetaMask() {
|
||
const provider = getEthProvider();
|
||
|
||
if (!provider) {
|
||
// 检测是否是移动端
|
||
const isMobile = /Android|iPhone|iPad|iPod/i.test(navigator.userAgent);
|
||
if (isMobile) {
|
||
// 移动端:尝试通过 MetaMask 深链接打开
|
||
|
||
console.log("[XIC] MetaMask mobile deeplink blocked");
|
||
throw new Error('正在跳转到 MetaMask App...');
|
||
}
|
||
// 桌面端:显示安装提示
|
||
showInstallHint('metamask');
|
||
throw new Error('MetaMask 未安装');
|
||
}
|
||
|
||
try {
|
||
// 请求账户授权
|
||
const accounts = await provider.request({ method: 'eth_requestAccounts' });
|
||
if (!accounts || accounts.length === 0) {
|
||
throw new Error('用户拒绝了连接请求');
|
||
}
|
||
|
||
const address = accounts[0];
|
||
|
||
// 获取当前链信息
|
||
let chainId = null;
|
||
try {
|
||
chainId = await provider.request({ method: 'eth_chainId' });
|
||
} catch (e) {}
|
||
|
||
console.log('[XIC Wallet] MetaMask 连接成功:', address, 'chainId:', chainId);
|
||
return { address, type: 'eth', chainId };
|
||
} catch (err) {
|
||
if (err.code === 4001) {
|
||
throw new Error('用户取消了连接');
|
||
}
|
||
if (err.code === -32002) {
|
||
throw new Error('MetaMask 正在等待确认,请打开 MetaMask 扩展');
|
||
}
|
||
throw err;
|
||
}
|
||
}
|
||
|
||
async function connectTronLink() {
|
||
// 方式1:新版 TronLink API
|
||
if (window.tronLink) {
|
||
try {
|
||
const res = await window.tronLink.request({ method: 'tron_requestAccounts' });
|
||
if (res && (res.code === 200 || res.code === 4000)) {
|
||
await new Promise(r => setTimeout(r, 500));
|
||
const addr = window.tronWeb?.defaultAddress?.base58;
|
||
if (addr) return { address: addr, type: 'tron' };
|
||
}
|
||
} catch (e) {
|
||
console.warn('[XIC Wallet] TronLink new API failed:', e);
|
||
}
|
||
}
|
||
// 方式2:旧版 tronWeb
|
||
if (window.tronWeb && window.tronWeb.ready) {
|
||
const addr = window.tronWeb.defaultAddress?.base58;
|
||
if (addr) return { address: addr, type: 'tron' };
|
||
}
|
||
// 未安装
|
||
showInstallHint('tronlink');
|
||
throw new Error('请先安装 TronLink 钱包');
|
||
}
|
||
|
||
async function connectOKX() {
|
||
// OKX TRON
|
||
if (window.okxwallet?.tronLink) {
|
||
try {
|
||
const res = await window.okxwallet.tronLink.request({ method: 'tron_requestAccounts' });
|
||
if (res?.code === 200) {
|
||
const addr = window.okxwallet.tronLink.tronWeb?.defaultAddress?.base58;
|
||
if (addr) return { address: addr, type: 'tron' };
|
||
}
|
||
} catch (e) {}
|
||
}
|
||
// OKX ETH
|
||
if (window.okxwallet) {
|
||
try {
|
||
const accounts = await window.okxwallet.request({ method: 'eth_requestAccounts' });
|
||
if (accounts?.[0]) return { address: accounts[0], type: 'eth' };
|
||
} catch (e) {}
|
||
}
|
||
// 通用 ethereum(OKX 注入)
|
||
if (window.ethereum?.isOkxWallet) {
|
||
const accounts = await window.ethereum.request({ method: 'eth_requestAccounts' });
|
||
if (accounts?.[0]) return { address: accounts[0], type: 'eth' };
|
||
}
|
||
showInstallHint('okx');
|
||
throw new Error('请先安装 OKX Wallet');
|
||
}
|
||
|
||
async function connectTrust() {
|
||
if (window.trustwallet) {
|
||
try {
|
||
const accounts = await window.trustwallet.request({ method: 'eth_requestAccounts' });
|
||
if (accounts?.[0]) return { address: accounts[0], type: 'eth' };
|
||
} catch (e) {}
|
||
}
|
||
if (window.ethereum?.isTrust) {
|
||
const accounts = await window.ethereum.request({ method: 'eth_requestAccounts' });
|
||
if (accounts?.[0]) return { address: accounts[0], type: 'eth' };
|
||
}
|
||
// 移动端深链接
|
||
const isMobile = /Android|iPhone|iPad|iPod/i.test(navigator.userAgent);
|
||
if (isMobile) {
|
||
console.log("[XIC] Trust Wallet deeplink blocked");
|
||
throw new Error('正在跳转到 Trust Wallet...');
|
||
}
|
||
showInstallHint('trust');
|
||
throw new Error('请先安装 Trust Wallet');
|
||
}
|
||
|
||
async function connectTokenPocket() {
|
||
if (window.tokenpocket) {
|
||
try {
|
||
const accounts = await window.tokenpocket.request({ method: 'eth_requestAccounts' });
|
||
if (accounts?.[0]) return { address: accounts[0], type: 'eth' };
|
||
} catch (e) {}
|
||
}
|
||
if (window.ethereum?.isTokenPocket) {
|
||
const accounts = await window.ethereum.request({ method: 'eth_requestAccounts' });
|
||
if (accounts?.[0]) return { address: accounts[0], type: 'eth' };
|
||
}
|
||
showInstallHint('tokenpocket');
|
||
throw new Error('请先安装 TokenPocket');
|
||
}
|
||
|
||
async function connectBitget() {
|
||
if (window.bitkeep?.ethereum) {
|
||
try {
|
||
const accounts = await window.bitkeep.ethereum.request({ method: 'eth_requestAccounts' });
|
||
if (accounts?.[0]) return { address: accounts[0], type: 'eth' };
|
||
} catch (e) {}
|
||
}
|
||
if (window.ethereum?.isBitKeep) {
|
||
const accounts = await window.ethereum.request({ method: 'eth_requestAccounts' });
|
||
if (accounts?.[0]) return { address: accounts[0], type: 'eth' };
|
||
}
|
||
showInstallHint('bitget');
|
||
throw new Error('请先安装 Bitget Wallet');
|
||
}
|
||
|
||
async function connectPhantom() {
|
||
if (window.phantom?.ethereum) {
|
||
try {
|
||
const accounts = await window.phantom.ethereum.request({ method: 'eth_requestAccounts' });
|
||
if (accounts?.[0]) return { address: accounts[0], type: 'eth' };
|
||
} catch (e) {}
|
||
}
|
||
if (window.phantom?.solana) {
|
||
try {
|
||
const resp = await window.phantom.solana.connect();
|
||
if (resp?.publicKey) return { address: resp.publicKey.toString(), type: 'sol' };
|
||
} catch (e) {}
|
||
}
|
||
showInstallHint('phantom');
|
||
throw new Error('请先安装 Phantom 钱包');
|
||
}
|
||
|
||
// ─── 安装提示 ─────────────────────────────────────────────────────────────────
|
||
function showInstallHint(walletId) {
|
||
const wallet = WALLETS.find(w => w.id === walletId);
|
||
if (!wallet || !installHintEl) return;
|
||
installHintEl.innerHTML = `
|
||
<strong>⚠️ ${wallet.name} 未安装</strong><br>
|
||
请先安装 ${wallet.name} 钱包扩展,然后刷新页面重试。<br>
|
||
<a href="${wallet.installUrl}" target="_blank" rel="noopener">👉 点击下载 ${wallet.name}</a>
|
||
`;
|
||
installHintEl.classList.add('show');
|
||
}
|
||
|
||
// ─── 连接成功处理 ─────────────────────────────────────────────────────────────
|
||
function onConnected(address, wallet) {
|
||
currentAddress = address;
|
||
currentWallet = wallet;
|
||
|
||
// 更新 modal 内的已连接横幅
|
||
if (connectedBannerEl) {
|
||
connectedBannerEl.querySelector('.xic-connected-addr').textContent = shortenAddr(address);
|
||
connectedBannerEl.querySelector('.xic-connected-label').textContent = `已连接 ${wallet.name}`;
|
||
connectedBannerEl.classList.add('show');
|
||
}
|
||
|
||
// 更新导航栏按钮
|
||
updateNavbarBtn(address);
|
||
|
||
// 关闭弹窗
|
||
setTimeout(() => closeModal(), 800);
|
||
|
||
// 显示成功提示
|
||
showToast(`✅ ${wallet.name} 连接成功:${shortenAddr(address)}`, 'success');
|
||
|
||
// 监听账户变更
|
||
if (wallet.id === 'metamask') {
|
||
const provider = getEthProvider();
|
||
if (provider) {
|
||
provider.on('accountsChanged', (accounts) => {
|
||
if (accounts.length === 0) {
|
||
onDisconnected();
|
||
} else {
|
||
currentAddress = accounts[0];
|
||
updateNavbarBtn(accounts[0]);
|
||
}
|
||
});
|
||
}
|
||
}
|
||
}
|
||
|
||
function onDisconnected() {
|
||
currentAddress = null;
|
||
currentWallet = null;
|
||
if (connectedBannerEl) connectedBannerEl.classList.remove('show');
|
||
updateNavbarBtn(null);
|
||
showToast('钱包已断开连接', 'warn');
|
||
}
|
||
|
||
function updateNavbarBtn(address) {
|
||
const btn = findConnectBtn();
|
||
if (!btn) return;
|
||
if (address) {
|
||
btn.textContent = shortenAddr(address);
|
||
btn.style.cssText = `
|
||
background: linear-gradient(135deg, #10b981, #059669) !important;
|
||
color: #fff !important;
|
||
border: none !important;
|
||
border-radius: 10px !important;
|
||
padding: 10px 18px !important;
|
||
font-weight: 700 !important;
|
||
cursor: pointer !important;
|
||
font-size: 13px !important;
|
||
`;
|
||
} else {
|
||
btn.textContent = 'Connect Wallet';
|
||
btn.style.cssText = '';
|
||
}
|
||
}
|
||
|
||
// ─── 渲染钱包列表 ─────────────────────────────────────────────────────────────
|
||
function renderWalletGrid() {
|
||
const grid = modalEl.querySelector('.xic-wallet-grid');
|
||
if (!grid) return;
|
||
grid.innerHTML = '';
|
||
|
||
WALLETS.forEach(wallet => {
|
||
const detected = wallet.detect();
|
||
const btn = document.createElement('button');
|
||
btn.className = 'xic-wallet-btn' + (detected ? ' detected' : '');
|
||
btn.innerHTML = `
|
||
${detected ? '<span class="xic-detected-dot"></span>' : ''}
|
||
<div class="xic-wallet-icon">${wallet.icon}</div>
|
||
<div class="xic-wallet-name">${wallet.name}</div>
|
||
<div class="xic-wallet-desc">${wallet.desc}</div>
|
||
<div class="xic-wallet-network">${wallet.network}</div>
|
||
`;
|
||
btn.addEventListener('click', () => handleWalletClick(wallet, btn));
|
||
grid.appendChild(btn);
|
||
});
|
||
}
|
||
|
||
async function handleWalletClick(wallet, btn) {
|
||
// 隐藏安装提示
|
||
if (installHintEl) installHintEl.classList.remove('show');
|
||
|
||
// 显示加载状态
|
||
const originalHTML = btn.innerHTML;
|
||
btn.classList.add('loading');
|
||
btn.innerHTML = `<div class="xic-spinner"></div><div class="xic-wallet-name" style="margin-top:8px">连接中...</div>`;
|
||
|
||
try {
|
||
const result = await wallet.connect();
|
||
onConnected(result.address, wallet);
|
||
} catch (err) {
|
||
console.error('[XIC Wallet] 连接失败:', err);
|
||
// 恢复按钮状态
|
||
btn.classList.remove('loading');
|
||
btn.innerHTML = originalHTML;
|
||
|
||
const msg = err.message || '连接失败';
|
||
if (!msg.includes('未安装') && !msg.includes('Not installed')) {
|
||
showToast('❌ ' + msg, 'error');
|
||
}
|
||
}
|
||
}
|
||
|
||
// ─── 创建弹窗 ─────────────────────────────────────────────────────────────────
|
||
function createModal() {
|
||
// 注入样式
|
||
if (!document.getElementById('xic-wallet-styles')) {
|
||
const style = document.createElement('style');
|
||
style.id = 'xic-wallet-styles';
|
||
style.textContent = STYLES;
|
||
document.head.appendChild(style);
|
||
}
|
||
|
||
// 创建遮罩层
|
||
overlayEl = document.createElement('div');
|
||
overlayEl.id = 'xic-wallet-overlay';
|
||
|
||
// 创建弹窗
|
||
modalEl = document.createElement('div');
|
||
modalEl.id = 'xic-wallet-modal';
|
||
modalEl.innerHTML = `
|
||
<div class="xic-modal-header">
|
||
<div class="xic-modal-title">连接钱包</div>
|
||
<button id="xic-close-btn">✕</button>
|
||
</div>
|
||
<div class="xic-modal-subtitle">选择您的钱包以继续购买 XIC</div>
|
||
|
||
<div class="xic-connected-banner">
|
||
<div class="xic-connected-dot"></div>
|
||
<div class="xic-connected-text">
|
||
<div class="xic-connected-label">已连接</div>
|
||
<div class="xic-connected-addr"></div>
|
||
</div>
|
||
<button class="xic-disconnect-btn">断开</button>
|
||
</div>
|
||
|
||
<div class="xic-wallet-grid"></div>
|
||
|
||
<div class="xic-install-hint"></div>
|
||
|
||
<div class="xic-modal-footer">
|
||
没有钱包?
|
||
<a href="https://metamask.io/download/" target="_blank" rel="noopener">下载 MetaMask</a>
|
||
或
|
||
<a href="https://www.tronlink.org/" target="_blank" rel="noopener">下载 TronLink</a>
|
||
</div>
|
||
`;
|
||
|
||
overlayEl.appendChild(modalEl);
|
||
document.body.appendChild(overlayEl);
|
||
|
||
// 获取引用
|
||
connectedBannerEl = modalEl.querySelector('.xic-connected-banner');
|
||
installHintEl = modalEl.querySelector('.xic-install-hint');
|
||
|
||
// 关闭按钮
|
||
modalEl.querySelector('#xic-close-btn').addEventListener('click', closeModal);
|
||
|
||
// 断开连接按钮
|
||
modalEl.querySelector('.xic-disconnect-btn').addEventListener('click', () => {
|
||
onDisconnected();
|
||
closeModal();
|
||
});
|
||
|
||
// 点击遮罩关闭
|
||
overlayEl.addEventListener('click', (e) => {
|
||
if (e.target === overlayEl) closeModal();
|
||
});
|
||
|
||
// ESC 关闭
|
||
document.addEventListener('keydown', (e) => {
|
||
if (e.key === 'Escape') closeModal();
|
||
});
|
||
|
||
// 渲染钱包列表
|
||
renderWalletGrid();
|
||
}
|
||
|
||
// ─── 弹窗控制 ─────────────────────────────────────────────────────────────────
|
||
function openModal() {
|
||
if (!overlayEl) createModal();
|
||
overlayEl.classList.add('open');
|
||
// 刷新检测状态
|
||
renderWalletGrid();
|
||
// 如果已连接,显示已连接状态
|
||
if (currentAddress && connectedBannerEl) {
|
||
connectedBannerEl.querySelector('.xic-connected-addr').textContent = shortenAddr(currentAddress);
|
||
connectedBannerEl.querySelector('.xic-connected-label').textContent = `已连接 ${currentWallet?.name || ''}`;
|
||
connectedBannerEl.classList.add('show');
|
||
}
|
||
if (installHintEl) installHintEl.classList.remove('show');
|
||
}
|
||
|
||
function closeModal() {
|
||
if (overlayEl) overlayEl.classList.remove('open');
|
||
}
|
||
|
||
// ─── 找到连接按钮 ─────────────────────────────────────────────────────────────
|
||
function findConnectBtn() {
|
||
const selectors = [
|
||
'button[class*="wallet"]',
|
||
'button[class*="connect"]',
|
||
'button[class*="Connect"]',
|
||
];
|
||
// 先用文本匹配
|
||
const allBtns = document.querySelectorAll('button');
|
||
for (const btn of allBtns) {
|
||
const text = (btn.textContent || '').trim();
|
||
if (
|
||
text === 'Select Wallet' ||
|
||
text === 'Connect Wallet' ||
|
||
text === 'Connect' ||
|
||
text.startsWith('Connecting') ||
|
||
text === 'Change wallet'
|
||
) {
|
||
return btn;
|
||
}
|
||
}
|
||
// 再用 class 匹配
|
||
for (const sel of selectors) {
|
||
const el = document.querySelector(sel);
|
||
if (el) return el;
|
||
}
|
||
return null;
|
||
}
|
||
|
||
// ─── 注入拦截器 ───────────────────────────────────────────────────────────────
|
||
function injectInterceptor() {
|
||
const btn = findConnectBtn();
|
||
if (btn && !btn.dataset.xicInjected) {
|
||
// 克隆按钮移除原有监听器
|
||
const newBtn = btn.cloneNode(true);
|
||
newBtn.dataset.xicInjected = 'true';
|
||
newBtn.addEventListener('click', (e) => {
|
||
e.stopPropagation();
|
||
e.preventDefault();
|
||
openModal();
|
||
});
|
||
btn.parentNode.replaceChild(newBtn, btn);
|
||
isInjected = true;
|
||
console.log('[XIC Wallet] v2.1 多钱包连接器已注入');
|
||
}
|
||
}
|
||
|
||
// ─── 暴露全局 API ─────────────────────────────────────────────────────────────
|
||
window.XICWallet = {
|
||
open: openModal,
|
||
close: closeModal,
|
||
getAddress: () => currentAddress,
|
||
getWallet: () => currentWallet,
|
||
isConnected: () => !!currentAddress,
|
||
disconnect: onDisconnected,
|
||
};
|
||
|
||
// ─── 初始化 ───────────────────────────────────────────────────────────────────
|
||
function init() {
|
||
// 使用 MutationObserver 持续监听 DOM
|
||
const observer = new MutationObserver(() => {
|
||
if (!isInjected) injectInterceptor();
|
||
});
|
||
|
||
if (document.body) {
|
||
observer.observe(document.body, { childList: true, subtree: true });
|
||
injectInterceptor();
|
||
} else {
|
||
document.addEventListener('DOMContentLoaded', () => {
|
||
observer.observe(document.body, { childList: true, subtree: true });
|
||
injectInterceptor();
|
||
});
|
||
}
|
||
|
||
// 多次重试确保注入成功(React 渲染后)
|
||
[500, 1000, 1500, 2500, 4000].forEach(delay => {
|
||
setTimeout(() => {
|
||
if (!isInjected) injectInterceptor();
|
||
}, delay);
|
||
});
|
||
}
|
||
|
||
init();
|
||
})();
|