// ==UserScript== // @name GMGN.ai 前排标注查询工具 // @namespace http://tampermonkey.net/ // @version 1.3 // @description 获取GMGN.ai前100持仓者的MemeRadar标注信息 // @author 专业油猴脚本开发者 // @match https://gmgn.ai/* // @run-at document-start // @grant GM_xmlhttpRequest // @grant GM_getValue // @grant GM_setValue // @grant GM_addStyle // @connect plugin.chaininsight.vip // @require https://cdnjs.cloudflare.com/ajax/libs/xlsx/0.18.5/xlsx.full.min.js // @license MIT // @downloadURL none // ==/UserScript== (function() { 'use strict'; console.log('[标注查询] MemeRadar标注查询工具已启动'); // 全局变量 let currentCA = ''; // 当前代币合约地址 let currentChain = ''; // 当前链网络 let topHolders = []; // 前排持仓者地址列表 let tagData = []; // 标注数据 let isDataReady = false; // 数据是否就绪 let isFetchingTags = false; // 是否正在获取标注 let hasInterceptedTags = false; // 是否已拦截到标注数据 let hasInterceptedHolders = false; // 是否已拦截到持仓者数据 let interceptedCA = ''; // 已拦截的CA地址 // 链网络映射 const chainMapping = { 'sol': 'Solana', 'eth': 'Ethereum', 'base': 'Base', 'bsc': 'bsc', // tron 不支持 }; // 立即设置XHR拦截 setupXhrInterception(); // DOM加载完成后初始化UI if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', initializeUI); } else { setTimeout(initializeUI, 100); } /** * 设置XHR请求拦截 */ function setupXhrInterception() { console.log('[请求拦截] 开始设置token_holders请求拦截'); // 避免重复设置 if (window._memeradarInterceptionSetup) { console.log('[请求拦截] 检测到已存在拦截设置,跳过重复设置'); return; } const originalOpen = XMLHttpRequest.prototype.open; const originalSend = XMLHttpRequest.prototype.send; XMLHttpRequest.prototype.open = function(method, url) { this._url = url; this._method = method; return originalOpen.apply(this, arguments); }; XMLHttpRequest.prototype.send = function(body) { const url = this._url; // 监听token_holders请求 if (url && url.includes('/vas/api/v1/token_holders/')) { // 解析链网络和CA地址 const urlMatch = url.match(/\/token_holders\/([^\/]+)\/([^?]+)/); if (!urlMatch) { console.warn('[请求拦截] ⚠️无法解析token_holders URL:', url); return originalSend.apply(this, arguments); } const chain = urlMatch[1]; const ca = urlMatch[2]; // 检查是否已经拦截过这个CA if (hasInterceptedHolders && interceptedCA === ca) { console.log(`[请求拦截] 📋已拦截过CA ${ca} 的持仓者数据,跳过重复拦截`); return originalSend.apply(this, arguments); } console.log('[请求拦截] 🎯捕获到token_holders请求:', url); console.log(`[数据解析] 链网络: ${chain}, CA地址: ${ca}`); // 检查CA是否变化 if (currentCA && currentCA !== ca) { console.log('[数据重置] 检测到CA地址变化,清除所有数据'); resetAllData(); } currentChain = chain; currentCA = ca; console.log('currentChain', currentChain); console.log('currentCA', currentCA); this.addEventListener('load', function() { if (this.status === 200) { console.log('[请求拦截] ✅token_holders请求成功'); try { const response = JSON.parse(this.responseText); if (response.code === 0 && response.data && response.data.list) { processTokenHolders(response.data.list); // 标记已拦截成功 hasInterceptedHolders = true; interceptedCA = ca; console.log(`[拦截完成] ✅已完成CA ${ca} 的持仓者数据拦截,后续请求将被跳过`); } else { console.warn('[数据处理] ⚠️token_holders返回数据格式异常:', response); } } catch (error) { console.error('[数据处理] ❌解析token_holders响应失败:', error); } } else { console.error('[请求拦截] ❌token_holders请求失败,状态码:', this.status); } }); } // 监听wallet_tags_v2请求 if (url && url.includes('/api/v0/util/query/wallet_tags_v2')) { // 检查是否已经拦截过标注数据(针对当前CA) if (hasInterceptedTags && currentCA) { console.log(`[请求拦截] 📋已拦截过CA ${currentCA} 的标注数据,跳过重复拦截`); return originalSend.apply(this, arguments); } console.log('[请求拦截] 🎯捕获到wallet_tags_v2请求:', url); this.addEventListener('load', function() { if (this.status === 200) { console.log('[请求拦截] ✅wallet_tags_v2请求成功'); try { const response = JSON.parse(this.responseText); console.log('[标注拦截] wallet_tags_v2响应数据:', response); if (response.code === 0 && response.data) { console.log('[标注拦截] ✅成功拦截到标注数据,开始处理'); // 确保有持仓者数据才处理 if (topHolders && topHolders.length > 0) { processInterceptedTagData(response.data); hasInterceptedTags = true; updateButtonState(); console.log('[标注拦截] ✅标注数据处理完成,已更新按钮状态'); } else { console.warn('[标注拦截] ⚠️持仓者数据尚未准备,延迟处理标注数据'); // 保存标注数据,等待持仓者数据准备完成 window._pendingTagData = response.data; } } else { console.warn('[数据处理] ⚠️wallet_tags_v2返回数据格式异常:', response); console.log('[数据处理] 响应码:', response.code, '消息:', response.msg); } } catch (error) { console.error('[数据处理] ❌解析wallet_tags_v2响应失败:', error); } } else { console.error('[请求拦截] ❌wallet_tags_v2请求失败,状态码:', this.status); } }); this.addEventListener('error', function(error) { console.error('[请求拦截] ❌wallet_tags_v2网络请求错误:', error); }); } return originalSend.apply(this, arguments); }; window._memeradarInterceptionSetup = true; console.log('[请求拦截] ✅XHR拦截设置完成'); } /** * 处理持仓者数据 */ function processTokenHolders(holdersList) { console.log(`[数据处理] 开始处理持仓者列表,总数量: ${holdersList.length}`); // 提取前100个地址 topHolders = holdersList.slice(0, 100).map(holder => holder.address); isDataReady = true; console.log(`[数据处理] ✅已提取前${topHolders.length}个持仓者地址`); console.log('[数据处理] 前5个地址示例:', topHolders.slice(0, 5)); // 检查是否有待处理的标注数据 if (window._pendingTagData) { console.log('[数据处理] 🔄发现待处理的标注数据,开始处理'); processInterceptedTagData(window._pendingTagData); hasInterceptedTags = true; window._pendingTagData = null; // 清除待处理数据 console.log('[数据处理] ✅待处理标注数据处理完成'); } // 更新按钮状态 updateButtonState(); } /** * 处理拦截到的标注数据 */ function processInterceptedTagData(responseData) { console.log('[拦截数据] 开始处理拦截到的标注数据'); if (!responseData || !responseData.walletTags) { console.warn('[拦截数据] 响应数据格式异常'); return; } // 创建地址到标注的映射 const tagMap = {}; responseData.walletTags.forEach(wallet => { tagMap[wallet.address] = { count: wallet.count || 0, tags: wallet.tags ? wallet.tags.map(tag => tag.tagName) : [], expertTags: wallet.expertTags ? wallet.expertTags.map(tag => tag.tagName) : [] }; }); // 为所有地址创建完整的标注数据 tagData = topHolders.map(address => { const walletTags = tagMap[address] || { count: 0, tags: [], expertTags: [] }; return { address: address, tagCount: walletTags.count, tags: walletTags.tags, expertTags: walletTags.expertTags }; }); // 按标注数量降序排序 tagData.sort((a, b) => b.tagCount - a.tagCount); console.log(`[拦截数据] ✅处理完成,共${tagData.length}个地址,有标注的地址:${tagData.filter(w => w.tagCount > 0).length}个`); } /** * 重置所有数据 */ function resetAllData() { console.log('[数据重置] 🔄开始重置所有数据和拦截状态'); currentCA = ''; currentChain = ''; topHolders = []; tagData = []; isDataReady = false; isFetchingTags = false; hasInterceptedTags = false; hasInterceptedHolders = false; interceptedCA = ''; // 清除待处理的标注数据 if (window._pendingTagData) { window._pendingTagData = null; } console.log('[数据重置] ✅所有数据和拦截状态已重置,可开始新一轮拦截'); } /** * 初始化UI界面 */ function initializeUI() { console.log('[UI初始化] 开始初始化用户界面'); addStyles(); setupUI(); console.log('[UI初始化] ✅用户界面初始化完成'); } /** * 添加CSS样式 */ function addStyles() { GM_addStyle(` .memeradar-btn { background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white; border: none; border-radius: 6px; padding: 6px 12px; font-size: 12px; font-weight: 600; cursor: pointer; transition: all 0.3s ease; margin-right: 8px; min-width: 80px; height: 32px; display: flex; align-items: center; justify-content: center; box-shadow: 0 2px 4px rgba(102, 126, 234, 0.3); } .memeradar-btn:hover { transform: translateY(-1px); box-shadow: 0 4px 8px rgba(102, 126, 234, 0.4); } .memeradar-btn:disabled { background: #94a3b8; cursor: not-allowed; transform: none; box-shadow: none; } .memeradar-btn.fetching { background: linear-gradient(135deg, #f59e0b 0%, #d97706 100%); animation: pulse 2s infinite; } .memeradar-btn.ready { background: linear-gradient(135deg, #10b981 0%, #059669 100%); } @keyframes pulse { 0%, 100% { opacity: 1; } 50% { opacity: 0.7; } } .memeradar-modal { position: fixed; top: 0; left: 0; right: 0; bottom: 0; background: rgba(0, 0, 0, 0.75); display: flex; align-items: center; justify-content: center; z-index: 10000; backdrop-filter: blur(5px); } .memeradar-modal-content { background: linear-gradient(135deg, #1e293b 0%, #334155 100%); border-radius: 12px; width: 90%; max-width: 900px; max-height: 85vh; overflow: hidden; box-shadow: 0 20px 40px rgba(0, 0, 0, 0.3); border: 1px solid rgba(255, 255, 255, 0.1); } .memeradar-modal-header { background: linear-gradient(135deg, #3b82f6 0%, #2563eb 100%); padding: 16px 20px; display: flex; justify-content: space-between; align-items: center; border-radius: 12px 12px 0 0; } .memeradar-modal-title { color: white; font-size: 18px; font-weight: 700; margin: 0; } .memeradar-modal-close { background: rgba(255, 255, 255, 0.2); border: none; color: white; font-size: 18px; width: 32px; height: 32px; border-radius: 50%; cursor: pointer; transition: background 0.2s ease; } .memeradar-modal-close:hover { background: rgba(255, 255, 255, 0.3); } .memeradar-modal-body { padding: 20px; overflow-y: auto; max-height: calc(85vh - 140px); scrollbar-width: auto; scrollbar-color: #3b82f6 rgba(30, 41, 59, 0.6); } .memeradar-modal-body::-webkit-scrollbar { width: 12px; display: block !important; } .memeradar-modal-body::-webkit-scrollbar-track { background: rgba(30, 41, 59, 0.6); border-radius: 6px; margin: 4px; } .memeradar-modal-body::-webkit-scrollbar-thumb { background: linear-gradient(135deg, #3b82f6 0%, #2563eb 100%); border-radius: 6px; border: 2px solid rgba(30, 41, 59, 0.1); transition: all 0.2s ease; } .memeradar-modal-body::-webkit-scrollbar-thumb:hover { background: linear-gradient(135deg, #2563eb 0%, #1d4ed8 100%); transform: scale(1.1); } .memeradar-modal-body::-webkit-scrollbar-corner { background: transparent; } .memeradar-stats { background: rgba(59, 130, 246, 0.1); border: 1px solid rgba(59, 130, 246, 0.2); border-radius: 8px; padding: 16px; margin-bottom: 20px; display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 16px; } .memeradar-stat-item { text-align: center; } .memeradar-stat-label { color: #94a3b8; font-size: 12px; margin-bottom: 4px; } .memeradar-stat-value { color: #3b82f6; font-size: 20px; font-weight: 700; } .memeradar-export-btn { background: linear-gradient(135deg, #10b981 0%, #059669 100%); color: white; border: none; border-radius: 6px; padding: 8px 16px; font-size: 14px; font-weight: 600; cursor: pointer; transition: all 0.2s ease; display: flex; align-items: center; gap: 6px; margin-bottom: 16px; } .memeradar-export-btn:hover { transform: translateY(-1px); box-shadow: 0 4px 8px rgba(16, 185, 129, 0.3); } .memeradar-wallet-item { background: linear-gradient(135deg, rgba(15, 23, 42, 0.9) 0%, rgba(30, 41, 59, 0.9) 100%); border: 1px solid rgba(100, 116, 139, 0.3); border-radius: 12px; padding: 18px; margin-bottom: 14px; transition: all 0.3s ease; box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); } .memeradar-wallet-item:hover { background: linear-gradient(135deg, rgba(30, 41, 59, 1) 0%, rgba(51, 65, 85, 1) 100%); border-color: rgba(59, 130, 246, 0.5); transform: translateY(-2px); box-shadow: 0 4px 16px rgba(59, 130, 246, 0.15); } .memeradar-wallet-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 12px; } .memeradar-wallet-address { font-family: 'Courier New', monospace; color: #e2e8f0; font-size: 14px; cursor: pointer; padding: 4px 8px; background: rgba(15, 23, 42, 0.8); border-radius: 4px; transition: all 0.2s ease; flex: 1; word-break: break-all; } .memeradar-wallet-address:hover { background: rgba(59, 130, 246, 0.2); color: #3b82f6; } .memeradar-tag-count { background: linear-gradient(135deg, #f59e0b 0%, #d97706 100%); color: white; padding: 6px 14px; border-radius: 20px; font-size: 12px; font-weight: 700; margin-left: 12px; box-shadow: 0 2px 4px rgba(245, 158, 11, 0.3); } .memeradar-tags-container { display: flex; flex-wrap: wrap; gap: 6px; } .memeradar-tag { background: linear-gradient(135deg, #c084fc 0%, #a855f7 100%); color: white; padding: 5px 10px; border-radius: 14px; font-size: 11px; font-weight: 600; white-space: nowrap; box-shadow: 0 1px 3px rgba(192, 132, 252, 0.3); transition: all 0.2s ease; } .memeradar-tag:hover { transform: translateY(-1px); box-shadow: 0 2px 6px rgba(192, 132, 252, 0.4); } .memeradar-expert-tag { background: linear-gradient(135deg, #3b82f6 0%, #2563eb 100%); color: white; padding: 5px 10px; border-radius: 14px; font-size: 11px; font-weight: 600; white-space: nowrap; box-shadow: 0 1px 3px rgba(59, 130, 246, 0.3); transition: all 0.2s ease; position: relative; } .memeradar-expert-tag:hover { transform: translateY(-1px); box-shadow: 0 2px 6px rgba(59, 130, 246, 0.4); } .memeradar-expert-tag::before { content: "⭐"; margin-right: 4px; } .memeradar-no-tags { color: #94a3b8; font-style: italic; font-size: 12px; } .memeradar-loading { text-align: center; padding: 40px; color: #94a3b8; } .memeradar-error { background: rgba(239, 68, 68, 0.1); border: 1px solid rgba(239, 68, 68, 0.2); color: #ef4444; padding: 12px; border-radius: 6px; margin-bottom: 16px; font-size: 14px; } `); } /** * 设置UI界面 */ function setupUI() { const observer = new MutationObserver(() => { const targetContainer = document.querySelector('.flex.overflow-x-auto.overflow-y-hidden.scroll-smooth.w-full'); if (targetContainer && !targetContainer.querySelector('#memeradar-btn')) { injectButton(targetContainer); } }); observer.observe(document.body, { childList: true, subtree: true }); } /** * 注入按钮到页面 */ function injectButton(container) { const button = document.createElement('button'); button.id = 'memeradar-btn'; button.className = 'memeradar-btn'; button.textContent = '获取前排标注'; container.insertAdjacentElement('afterbegin', button); button.addEventListener('click', handleButtonClick); console.log('[UI注入] ✅前排标注按钮已注入'); } /** * 处理按钮点击事件 */ async function handleButtonClick() { const button = document.getElementById('memeradar-btn'); if (isFetchingTags) { console.log('[按钮点击] 正在获取标注中,忽略点击'); return; } // 检查数据是否就绪 if (!isDataReady || !topHolders.length) { showErrorModal('数据尚未就绪', '请等待页面加载完成,或刷新页面重试。\n\n可能原因:\n1. 页面数据还在加载中\n2. 网络请求被拦截失败\n3. 当前页面不是代币详情页'); return; } // 检查链网络是否支持 if (!chainMapping[currentChain]) { showErrorModal('不支持的链网络', `当前链网络 "${currentChain}" 暂不支持标注查询。\n\n支持的链网络:\n• Solana (sol)\n• Ethereum (eth)\n• Base (base)\n• BSC (bsc)`); return; } // 如果已有标注数据(无论是拦截的还是API获取的),直接显示 if (tagData.length > 0) { showTagsModal(); return; } // 调试信息:显示当前状态 console.log('[按钮点击] 当前数据状态检查:'); console.log(' hasInterceptedTags:', hasInterceptedTags); console.log(' tagData.length:', tagData.length); console.log(' topHolders.length:', topHolders.length); console.log(' window._pendingTagData:', !!window._pendingTagData); // 如果已拦截到标注数据但还没处理完成,提示用户稍等 if (hasInterceptedTags && tagData.length === 0) { showErrorModal('数据处理中', '已检测到标注数据,正在处理中,请稍候...'); return; } // 开始通过API获取标注数据 isFetchingTags = true; button.className = 'memeradar-btn fetching'; button.textContent = '获取中...'; try { console.log(`[API获取] 开始通过API获取${topHolders.length}个地址的标注信息`); await fetchWalletTags(); button.className = 'memeradar-btn ready'; button.textContent = '查看标注'; console.log('[API获取] ✅标注数据获取完成'); showTagsModal(); } catch (error) { console.error('[API获取] ❌获取标注数据失败:', error); showErrorModal('获取失败', `标注数据获取失败:${error.message}\n\n请检查网络连接或稍后重试。`); button.className = 'memeradar-btn'; button.textContent = '获取前排标注'; } finally { isFetchingTags = false; } } /** * 获取钱包标注数据 */ async function fetchWalletTags() { const chainName = chainMapping[currentChain]; const requestData = { walletAddresses: topHolders, chain: chainName }; console.log(`[API请求] 发送标注查询请求,链网络: ${chainName}, 地址数量: ${topHolders.length}`); return new Promise((resolve, reject) => { GM_xmlhttpRequest({ method: 'POST', url: 'https://plugin.chaininsight.vip/api/v0/util/query/wallet_tags_v2', headers: { 'Content-Type': 'application/json', 'Accept': 'application/json' }, data: JSON.stringify(requestData), timeout: 30000, onload: function(response) { console.log(`[API响应] 状态码: ${response.status}`); if (response.status !== 200) { reject(new Error(`HTTP ${response.status}: ${response.statusText}`)); return; } try { const data = JSON.parse(response.responseText); console.log('[API响应] 响应数据:', data); if (data.code !== 0) { reject(new Error(data.msg || `API错误码: ${data.code}`)); return; } processTagData(data.data); resolve(); } catch (error) { console.error('[API响应] JSON解析失败:', error); reject(new Error('响应数据解析失败')); } }, onerror: function(error) { console.error('[API请求] 网络请求失败:', error); reject(new Error('网络请求失败')); }, ontimeout: function() { console.warn('[API请求] 请求超时'); reject(new Error('请求超时')); } }); }); } /** * 处理标注数据 */ function processTagData(responseData) { console.log('[数据处理] 开始处理标注数据'); if (!responseData || !responseData.walletTags) { console.warn('[数据处理] 响应数据格式异常'); tagData = []; return; } // 创建地址到标注的映射 const tagMap = {}; responseData.walletTags.forEach(wallet => { tagMap[wallet.address] = { count: wallet.count || 0, tags: wallet.tags ? wallet.tags.map(tag => tag.tagName) : [], expertTags: wallet.expertTags ? wallet.expertTags.map(tag => tag.tagName) : [] }; }); // 为所有地址创建完整的标注数据 tagData = topHolders.map(address => { const walletTags = tagMap[address] || { count: 0, tags: [], expertTags: [] }; return { address: address, tagCount: walletTags.count, tags: walletTags.tags, expertTags: walletTags.expertTags }; }); // 按标注数量降序排序 tagData.sort((a, b) => b.tagCount - a.tagCount); console.log(`[数据处理] ✅处理完成,共${tagData.length}个地址,有标注的地址:${tagData.filter(w => w.tagCount > 0).length}个`); } /** * 更新按钮状态 */ function updateButtonState() { const button = document.getElementById('memeradar-btn'); if (!button) return; if (!isDataReady) { button.disabled = true; button.textContent = '等待数据...'; button.className = 'memeradar-btn'; } else if (hasInterceptedTags && tagData.length > 0) { // 已拦截到标注数据,可直接查看 button.disabled = false; button.textContent = '查看标注'; button.className = 'memeradar-btn ready'; } else if (tagData.length > 0) { // 已获取标注数据,可查看 button.disabled = false; button.textContent = '查看标注'; button.className = 'memeradar-btn ready'; } else { // 需要获取标注数据 button.disabled = false; button.textContent = '获取前排标注'; button.className = 'memeradar-btn'; } } /** * 显示错误弹窗 */ function showErrorModal(title, message) { const modal = document.createElement('div'); modal.className = 'memeradar-modal'; modal.innerHTML = `
`; document.body.appendChild(modal); // 绑定关闭事件 modal.querySelector('.memeradar-modal-close').addEventListener('click', () => { document.body.removeChild(modal); }); modal.addEventListener('click', (e) => { if (e.target === modal) { document.body.removeChild(modal); } }); } /** * 显示标注数据弹窗 */ function showTagsModal() { console.log('[界面显示] 显示标注数据弹窗'); // 只显示有标注的钱包(包括有专业玩家标注的) const walletsWithTags = tagData.filter(w => w.tagCount > 0 || (w.expertTags && w.expertTags.length > 0)); const hasTagsCount = walletsWithTags.length; const totalTags = walletsWithTags.reduce((sum, w) => sum + w.tagCount, 0); const expertTaggedAddressCount = walletsWithTags.filter(w => w.expertTags && w.expertTags.length > 0).length; const modal = document.createElement('div'); modal.className = 'memeradar-modal'; modal.innerHTML = ` `; document.body.appendChild(modal); // 填充钱包列表 - 只显示有标注的钱包 const walletsList = modal.querySelector('#wallets-list'); walletsWithTags.forEach((wallet, index) => { const walletItem = document.createElement('div'); walletItem.className = 'memeradar-wallet-item'; // 生成标签HTML const regularTags = wallet.tags.map(tag => `${tag}`).join(''); const expertTagsHtml = (wallet.expertTags && wallet.expertTags.length > 0) ? wallet.expertTags.map(tag => `${tag}`).join('') : ''; walletItem.innerHTML = `