您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
统计GMGN任意代币前排地址的数据,让数字来说话!新增首次记录和涨跌提醒功能,所有数字可点击查看详情,弹框显示净流入数据,负数红色显示,点击外部关闭
// ==UserScript== // @name GMGN 前排统计 // @namespace http://tampermonkey.net/ // @version 4.7 // @description 统计GMGN任意代币前排地址的数据,让数字来说话!新增首次记录和涨跌提醒功能,所有数字可点击查看详情,弹框显示净流入数据,负数红色显示,点击外部关闭 // @match https://gmgn.ai/* // @match https://www.gmgn.ai/* // @require https://cdnjs.cloudflare.com/ajax/libs/xlsx/0.18.5/xlsx.full.min.js // @require https://code.jquery.com/jquery-3.6.0.min.js // @grant none // @run-at document-start // @license MIT // ==/UserScript== (function() { 'use strict'; // 动态添加 CSS const style = document.createElement('style'); style.textContent = ` .statistic-gmgn-stats-container { background-color: transparent; border-radius: 4px; font-family: Arial, sans-serif; margin-right: 8px; margin-bottom:8px; border: 1px solid #333; /* 精细的右侧和下侧发光效果 */ box-shadow: 2px 2px 4px rgba(0, 119, 255, 0.6), /* 右下外发光(更小的偏移和模糊) */ 1px 1px 2px rgba(0, 119, 255, 0.4), /* 精细的次级发光 */ inset 0 0 3px rgba(0, 119, 255, 0.2); /* 更细腻的内发光 */ padding: 4px 6px; max-width: fit-content; } .statistic-gmgn-stats-header, .statistic-gmgn-stats-data { display: grid; grid-template-columns: repeat(11, 1fr); text-align: center; gap: 6px; font-weight: normal; font-size: 13px; } .statistic-gmgn-stats-header.sol-network, .statistic-gmgn-stats-data.sol-network { grid-template-columns: repeat(12, minmax(auto, 1fr)); gap: 4px; font-size: 12px; } .statistic-gmgn-stats-header span { color: #ccc; font-weight: normal; padding: 1px 2px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } .statistic-gmgn-stats-header.sol-network span { font-size: 11px; padding: 1px; } .statistic-gmgn-stats-data span { color: #00ff00; font-weight: normal; cursor: default; transition: all 0.2s ease; padding: 1px 3px; border-radius: 2px; min-width: 0; white-space: nowrap; } .statistic-gmgn-stats-data span.clickable { cursor: pointer; } .statistic-gmgn-stats-data span.clickable:hover { background-color: rgba(0, 255, 0, 0.1); border-radius: 3px; transform: scale(1.03); } .statistic-gmgn-stats-data.sol-network span { padding: 1px 2px; font-size: 12px; } .statistic-gmgn-stats-data span .statistic-up-arrow, .statistic-up-arrow { color: green !important; margin-left: 2px; font-weight: bold; } .statistic-gmgn-stats-data span .statistic-down-arrow, .statistic-down-arrow { color: red !important; margin-left: 2px; font-weight: bold; } /* 完整弹框CSS样式 - 从token_holding_temp.js复制并添加statistic前缀 */ .statistic-gmgn-modal { position: fixed; top: 0; left: 0; right: 0; bottom: 0; background-color: rgba(0, 0, 0, 0.5); display: flex; align-items: center; justify-content: center; z-index: 1000; } .statistic-gmgn-modal-content { background-color: #1e293b !important; border-radius: 8px !important; width: 80% !important; max-width: 800px !important; max-height: 80vh !important; overflow-y: auto !important; padding: 20px !important; color: white !important; position: fixed !important; top: 50% !important; left: 50% !important; transform: translate(-50%, -50%) !important; box-shadow: 0 10px 25px rgba(0, 0, 0, 0.5) !important; margin: 0 !important; z-index: 100000 !important; box-sizing: border-box !important; min-height: auto !important; min-width: 300px !important; pointer-events: auto !important; } .statistic-gmgn-modal-header { display: flex !important; justify-content: space-between !important; align-items: center !important; margin-bottom: 16px !important; padding: 0 !important; } .statistic-gmgn-modal-title { font-size: 18px !important; font-weight: 600 !important; color: white !important; margin: 0 !important; } .statistic-gmgn-modal-close { background: none !important; border: none !important; color: #94a3b8 !important; font-size: 20px !important; cursor: pointer !important; padding: 5px !important; line-height: 1 !important; width: auto !important; height: auto !important; min-width: 30px !important; min-height: 30px !important; } .statistic-gmgn-modal-close:hover { color: #ff4444 !important; background-color: rgba(255, 255, 255, 0.1) !important; border-radius: 4px !important; } .statistic-gmgn-result-item { background-color: #334155; border-radius: 6px; padding: 12px; margin-bottom: 12px; } .statistic-gmgn-analysis-summary { margin-bottom: 16px; padding: 12px; background-color: #263238; border-radius: 6px; display: flex; justify-content: space-between; align-items: center; } .statistic-gmgn-summary-stats { display: flex; gap: 20px; } .statistic-gmgn-stat-item { display: flex; align-items: baseline; } .statistic-gmgn-stat-label { color: #94a3b8; margin-right: 5px; } .statistic-gmgn-stat-value { font-weight: 600; color: #3b82f6; } .statistic-gmgn-result-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 8px; flex-wrap: wrap; gap: 8px; } .statistic-gmgn-result-rank { font-size: 14px; color: #94a3b8; font-weight: 600; min-width: 30px; } .statistic-gmgn-result-address { font-weight: 600; word-break: break-all; cursor: pointer; padding: 4px 8px; border-radius: 4px; transition: all 0.2s ease; background-color: #475569; flex: 1; min-width: 200px; color: #00ff88; font-family: monospace; } .statistic-gmgn-result-address:hover { background-color: #64748b; transform: translateY(-1px); } .statistic-gmgn-detail-section { margin-bottom: 12px; } .statistic-gmgn-section-title { font-size: 13px; font-weight: 600; color: #94a3b8; margin-bottom: 8px; } .statistic-gmgn-detail-grid { display: grid; grid-template-columns: 80px 1fr 80px 1fr; gap: 4px 8px; align-items: start; font-size: 12px; } .statistic-gmgn-detail-label { color: #94a3b8; font-size: 12px; padding: 2px 0; align-self: start; } .statistic-gmgn-detail-value { font-size: 12px; color: #e2e8f0; padding: 2px 0; word-break: break-word; line-height: 1.4; } .statistic-gmgn-value-highlight { color: #3b82f6; font-weight: 600; } .statistic-gmgn-compact-details .statistic-gmgn-detail-section { margin-bottom: 8px; } .statistic-gmgn-compact-details .statistic-gmgn-detail-section { margin-left: 10px; } .statistic-gmgn-address-jump-btn { background-color: #10b981; color: white; padding: 4px 8px; border-radius: 6px; font-size: 11px; font-weight: 500; margin-left: 8px; cursor: pointer; transition: all 0.2s ease; text-decoration: none; display: inline-block; border: none; } .statistic-gmgn-address-jump-btn:hover { background-color: #059669; transform: translateY(-1px); box-shadow: 0 2px 4px rgba(16, 185, 129, 0.3); } .statistic-gmgn-profit-positive { color: #00ff88 !important; } .statistic-gmgn-profit-negative { color: #ff4444 !important; } .statistic-gmgn-empty-message { text-align: center; color: #ccc; padding: 20px; margin: 0; } .statistic-gmgn-stats-info { text-align: center !important; margin-bottom: 15px !important; padding: 10px !important; background: rgba(0, 119, 255, 0.1) !important; border-radius: 8px !important; border: 1px solid rgba(0, 119, 255, 0.3) !important; color: #fff !important; font-size: 14px !important; } .statistic-gmgn-export-btn { background-color: #10b981 !important; color: white !important; border: none !important; padding: 8px 16px !important; border-radius: 6px !important; font-size: 12px !important; font-weight: 500 !important; cursor: pointer !important; transition: all 0.2s ease !important; display: flex !important; align-items: center !important; gap: 4px !important; } .statistic-gmgn-export-btn:hover { background-color: #059669 !important; transform: translateY(-1px) !important; box-shadow: 0 2px 4px rgba(16, 185, 129, 0.3) !important; } `; document.head.appendChild(style); // 存储拦截到的数据 let interceptedData = null; // 存储首次加载的数据 let initialStats = null; // 标记是否是首次加载 let isFirstLoad = true; // 新增存储当前CA地址 let currentCaAddress = null; // 存储首次加载的CA地址 let initialCaAddress = null; // 检查当前网络是否为SOL function isSolNetwork() { const url = window.location.href; return url.includes('/sol/') || url.includes('gmgn.ai/sol'); } // 检查是否为交易所地址 function isExchangeAddress(holder) { const exchangeNames = ['coinbase', 'binance', 'bybit', 'bitget', 'okx', 'kraken', 'coinsquare', 'crypto.com', 'robinhood', 'mexc']; // 检查native_transfer中的name if (holder.native_transfer && holder.native_transfer.name) { const name = holder.native_transfer.name.toLowerCase(); if (exchangeNames.some(exchange => name.includes(exchange))) { return true; } } // 检查其他可能的transfer字段 if (holder.transfer && holder.transfer.name) { const name = holder.transfer.name.toLowerCase(); if (exchangeNames.some(exchange => name.includes(exchange))) { return true; } } return false; } // 获取交易所名称 function getExchangeName(holder) { const exchangeNames = ['coinbase', 'binance', 'bybit', 'bitget', 'okx', 'kraken', 'coinsquare', 'crypto.com', 'robinhood', 'mexc']; let sourceName = ''; if (holder.native_transfer && holder.native_transfer.name) { sourceName = holder.native_transfer.name.toLowerCase(); } else if (holder.transfer && holder.transfer.name) { sourceName = holder.transfer.name.toLowerCase(); } for (let exchange of exchangeNames) { if (sourceName.includes(exchange)) { return exchange.charAt(0).toUpperCase() + exchange.slice(1); } } return 'Unknown'; } // 交易所专用弹框 function createExchangeModal(data, caAddress) { // 移除已存在的弹框 const existingModal = document.querySelector('.statistic-gmgn-modal'); if (existingModal) { existingModal.remove(); } // 按交易所分组数据 const exchangeGroups = {}; data.forEach(holder => { const exchangeName = getExchangeName(holder); if (!exchangeGroups[exchangeName]) { exchangeGroups[exchangeName] = []; } exchangeGroups[exchangeName].push(holder); }); // 创建弹框 const modal = document.createElement('div'); modal.className = 'statistic-gmgn-modal'; // 生成交易所统计数据 const exchangeSummary = Object.keys(exchangeGroups).map(exchange => { return { name: exchange, count: exchangeGroups[exchange].length, addresses: exchangeGroups[exchange] }; }).sort((a, b) => b.count - a.count); modal.innerHTML = ` <div class="statistic-gmgn-modal-content"> <div class="statistic-gmgn-modal-header"> <div class="statistic-gmgn-modal-title">🏢 交易所地址分析 (共${data.length}个地址)</div> <button class="statistic-gmgn-modal-close">×</button> </div> <div class="statistic-gmgn-analysis-summary"> <div class="statistic-gmgn-summary-stats"> <div class="statistic-gmgn-stat-item"> <span class="statistic-gmgn-stat-label">CA地址:</span> <span class="statistic-gmgn-stat-value">${caAddress || 'N/A'}</span> </div> <div class="statistic-gmgn-stat-item"> <span class="statistic-gmgn-stat-label">交易所数:</span> <span class="statistic-gmgn-stat-value">${Object.keys(exchangeGroups).length}</span> </div> </div> <button id="statistic-export-exchange-btn" class="statistic-gmgn-export-btn" title="导出Excel">📄 导出Excel</button> </div> <div id="statistic-exchange-summary"> <div class="statistic-gmgn-section-title">📈 交易所统计</div> <div class="statistic-exchange-summary-grid"> ${exchangeSummary.map(item => ` <div class="statistic-exchange-summary-item" data-exchange="${item.name}"> <span class="statistic-exchange-name">${item.name}</span> <span class="statistic-exchange-count">${item.count}个地址</span> </div> `).join('')} </div> </div> <div id="statistic-exchange-details"></div> </div> `; document.body.appendChild(modal); // 添加交易所统计样式 if (!document.getElementById('exchange-summary-styles')) { const summaryStyles = document.createElement('style'); summaryStyles.id = 'exchange-summary-styles'; summaryStyles.textContent = ` .statistic-exchange-summary-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 12px; margin-bottom: 20px; } .statistic-exchange-summary-item { background-color: #475569; border-radius: 8px; padding: 12px 16px; display: flex; justify-content: space-between; align-items: center; cursor: pointer; transition: all 0.3s ease; border: 2px solid transparent; } .statistic-exchange-summary-item:hover { background-color: #64748b; border-color: #3b82f6; transform: translateY(-2px); } .statistic-exchange-summary-item.active { background-color: #3b82f6; border-color: #1d4ed8; } .statistic-exchange-name { font-weight: 600; color: #e2e8f0; font-size: 14px; } .statistic-exchange-count { color: #10b981; font-weight: 600; font-size: 13px; } .statistic-exchange-details-section { margin-bottom: 20px; } .statistic-exchange-section-header { background-color: #1e293b; padding: 12px 16px; border-radius: 8px 8px 0 0; border-left: 4px solid #3b82f6; margin-bottom: 0; } .statistic-exchange-section-title { font-size: 16px; font-weight: 600; color: #3b82f6; margin: 0; } .statistic-exchange-section-count { font-size: 12px; color: #94a3b8; margin-top: 4px; } `; document.head.appendChild(summaryStyles); } // 绑定交易所统计点击事件 exchangeSummary.forEach(item => { const summaryItem = modal.querySelector(`[data-exchange="${item.name}"]`); if (summaryItem) { summaryItem.addEventListener('click', () => { // 移除所有活跃状态 modal.querySelectorAll('.statistic-exchange-summary-item').forEach(el => { el.classList.remove('active'); }); // 添加当前活跃状态 summaryItem.classList.add('active'); // 显示该交易所的详细信息 displayExchangeDetails(item.addresses, item.name, modal); }); } }); // ESC键关闭处理函数 const escKeyHandler = (e) => { if (e.key === 'Escape') { document.body.removeChild(modal); document.removeEventListener('keydown', escKeyHandler); } }; document.addEventListener('keydown', escKeyHandler); // 绑定导出Excel按钮事件 const exportBtn = modal.querySelector('#statistic-export-exchange-btn'); if (exportBtn) { exportBtn.addEventListener('click', () => { exportExchangeToExcel(exchangeGroups, caAddress); }); } // 绑定关闭按钮事件 modal.querySelector('.statistic-gmgn-modal-close').addEventListener('click', () => { document.body.removeChild(modal); document.removeEventListener('keydown', escKeyHandler); }); // 点击模态框外部关闭 modal.addEventListener('click', (e) => { if (e.target === modal) { document.body.removeChild(modal); document.removeEventListener('keydown', escKeyHandler); } }); // 默认显示第一个交易所的详情 if (exchangeSummary.length > 0) { const firstItem = modal.querySelector(`[data-exchange="${exchangeSummary[0].name}"]`); if (firstItem) { firstItem.click(); } } } // 显示交易所详细信息 function displayExchangeDetails(addresses, exchangeName, modal) { const detailsContainer = modal.querySelector('#statistic-exchange-details'); // 创建全局排名映射 - 基于原始完整数据按持仓比例排序 const globalRankMap = new Map(); if (interceptedData?.data?.list) { const allHolders = [...interceptedData.data.list]; allHolders .sort((a, b) => (b.amount_percentage || 0) - (a.amount_percentage || 0)) .forEach((holder, index) => { globalRankMap.set(holder.address, index + 1); }); } // 按持仓比例排序 const sortedAddresses = addresses.sort((a, b) => (b.amount_percentage || 0) - (a.amount_percentage || 0)); detailsContainer.innerHTML = ` <div class="statistic-exchange-details-section"> <div class="statistic-exchange-section-header"> <div class="statistic-exchange-section-title">${exchangeName} 地址详情</div> <div class="statistic-exchange-section-count">共 ${sortedAddresses.length} 个地址</div> </div> ${sortedAddresses.map((holder, index) => { const globalRank = globalRankMap.get(holder.address) || (index + 1); const processedData = { rank: index + 1, rankIndex: globalRank, // 使用全局排名 address: holder.address, balance: formatNumber(holder.balance), usdValue: formatNumber(holder.usd_value), netflowUsd: formatNumber(holder.netflow_usd), netflowClass: (holder.netflow_usd || 0) >= 0 ? 'statistic-gmgn-profit-positive' : 'statistic-gmgn-profit-negative', profit: formatNumber(holder.profit), profitSign: holder.profit >= 0 ? '+' : '', profitClass: holder.profit >= 0 ? 'statistic-gmgn-profit-positive' : 'statistic-gmgn-profit-negative', profitChange: holder.profit_change ? (holder.profit_change * 100).toFixed(1) + '%' : 'N/A', profitChangeClass: (holder.profit_change || 0) >= 0 ? 'statistic-gmgn-profit-positive' : 'statistic-gmgn-profit-negative', exchangeName: getExchangeName(holder), transferName: (holder.native_transfer && holder.native_transfer.name) || (holder.transfer && holder.transfer.name) || 'N/A', amountPercentage: holder.amount_percentage ? (holder.amount_percentage * 100).toFixed(2) + '%' : 'N/A', sellPercentage: holder.sell_amount_percentage ? (holder.sell_amount_percentage * 100).toFixed(2) + '%' : '0.00%' // 筹码已卖 }; return ` <div class="statistic-gmgn-result-item"> <div class="statistic-gmgn-result-header"> <div class="statistic-gmgn-result-rank"> <span style="color: #ff6b35; font-weight: bold; background: rgba(255, 107, 53, 0.15); padding: 2px 6px; border-radius: 12px; border: 1px solid rgba(255, 107, 53, 0.3); font-size: 12px;">榜${processedData.rankIndex}</span> </div> <div class="statistic-gmgn-result-address" title="点击复制地址" onclick="navigator.clipboard.writeText('${processedData.address}'); this.style.backgroundColor='#16a34a'; this.style.color='white'; setTimeout(() => { this.style.backgroundColor=''; this.style.color=''; }, 1000);">${processedData.address}</div> <a href="https://gmgn.ai/sol/address/${processedData.address}" target="_blank" class="statistic-gmgn-address-jump-btn" title="查看钱包详情">详情</a> </div> <div class="statistic-gmgn-compact-details"> <div class="statistic-gmgn-detail-section"> <div class="statistic-gmgn-section-title">基本信息</div> <div class="statistic-gmgn-detail-grid"> <span class="statistic-gmgn-detail-label">持仓:</span> <span class="statistic-gmgn-detail-value">${processedData.balance}</span> <span class="statistic-gmgn-detail-label">持仓占比:</span> <span class="statistic-gmgn-detail-value statistic-gmgn-value-highlight">${processedData.amountPercentage}</span> <span class="statistic-gmgn-detail-label">筹码已卖:</span> <span class="statistic-gmgn-detail-value" style="color: ${processedData.sellPercentage === '0.00%' ? '#00ff88' : '#ff6b35'}; font-weight: bold;">${processedData.sellPercentage}</span> <span class="statistic-gmgn-detail-label">净流入:</span> <span class="statistic-gmgn-detail-value ${processedData.netflowClass}">$${processedData.netflowUsd}</span> <span class="statistic-gmgn-detail-label">盈亏:</span> <span class="statistic-gmgn-detail-value ${processedData.profitClass}">${processedData.profitSign}$${processedData.profit}</span> <span class="statistic-gmgn-detail-label">倍数:</span> <span class="statistic-gmgn-detail-value ${processedData.profitChangeClass}">${processedData.profitChange}</span> <span class="statistic-gmgn-detail-label">交易所:</span> <span class="statistic-gmgn-detail-value statistic-gmgn-value-highlight">${processedData.exchangeName}</span> <span class="statistic-gmgn-detail-label">标签:</span> <span class="statistic-gmgn-detail-value">${processedData.transferName}</span> </div> </div> </div> </div> `; }).join('')} </div> `; } // 交易所数据导出函数 function exportExchangeToExcel(exchangeGroups, caAddress) { try { const worksheetData = []; // 添加标题行 worksheetData.push(['交易所', '排名', '地址', '持仓数量', '持仓比例', '筹码已卖', 'USD价值', '净流入USD', '盈亏USD', '盈亏倍数', '标签名称']); // 按交易所排序添加数据 Object.keys(exchangeGroups).forEach(exchangeName => { const addresses = exchangeGroups[exchangeName].sort((a, b) => (b.amount_percentage || 0) - (a.amount_percentage || 0)); addresses.forEach((holder, index) => { const row = [ exchangeName, index + 1, holder.address, formatNumber(holder.balance), holder.amount_percentage ? (holder.amount_percentage * 100).toFixed(2) + '%' : 'N/A', holder.sell_amount_percentage ? (holder.sell_amount_percentage * 100).toFixed(2) + '%' : '0.00%', formatNumber(holder.usd_value), formatNumber(holder.netflow_usd), (holder.profit >= 0 ? '+' : '') + formatNumber(holder.profit), holder.profit_change ? (holder.profit_change * 100).toFixed(1) + '%' : 'N/A', (holder.native_transfer && holder.native_transfer.name) || (holder.transfer && holder.transfer.name) || 'N/A' ]; worksheetData.push(row); }); }); // 创建工作簿 const wb = XLSX.utils.book_new(); const ws = XLSX.utils.aoa_to_sheet(worksheetData); // 设置列宽 const colWidths = [ {wch: 12}, // 交易所 {wch: 6}, // 排名 {wch: 45}, // 地址 {wch: 15}, // 持仓数量 {wch: 10}, // 持仓比例 {wch: 10}, // 已卖比例 {wch: 15}, // USD价值 {wch: 15}, // 净流入 {wch: 15}, // 盈亏 {wch: 12}, // 倍数 {wch: 25} // 标签名称 ]; ws['!cols'] = colWidths; // 添加工作表到工作簿 XLSX.utils.book_append_sheet(wb, ws, '交易所地址'); // 生成文件名 const timestamp = new Date().toISOString().slice(0, 19).replace(/:/g, '-'); const fileName = `交易所地址_${caAddress ? caAddress.slice(0, 8) : 'data'}_${timestamp}.xlsx`; // 下载文件 XLSX.writeFile(wb, fileName); // 显示成功提示 const exportBtn = document.querySelector('#statistic-export-exchange-btn'); if (exportBtn) { const originalText = exportBtn.textContent; exportBtn.textContent = '✅ 导出成功'; exportBtn.style.backgroundColor = '#059669'; setTimeout(() => { exportBtn.textContent = originalText; exportBtn.style.backgroundColor = ''; }, 2000); } } catch (error) { console.error('Excel导出失败:', error); alert('导出失败,请检查浏览器控制台了解详情'); } } // 优化后的弹框管理函数 function createModal(title, data, caAddress, showSolBalance = false) { // 移除已存在的弹框 const existingModal = document.querySelector('.statistic-gmgn-modal'); if (existingModal) { existingModal.remove(); } // 1. 数据预处理 - 首先获取全局排名 if (!interceptedData?.data?.list) { console.error('无法获取原始数据进行全局排名'); return; } // 创建全局排名映射 - 基于原始完整数据按持仓比例排序 const globalRankMap = new Map(); const allHolders = [...interceptedData.data.list]; allHolders .sort((a, b) => (b.amount_percentage || 0) - (a.amount_percentage || 0)) .forEach((holder, index) => { globalRankMap.set(holder.address, index + 1); }); // 2. 处理筛选后的数据,使用全局排名 const processedData = data .sort((a, b) => (b.amount_percentage || 0) - (a.amount_percentage || 0)) // 按持仓比例排序 .map((holder, index) => { const globalRank = globalRankMap.get(holder.address) || (index + 1); const baseData = { rank: index + 1, // 在当前数据集中的排名(用于显示序号) rankIndex: globalRank, // 在全局数据中的排名(用于显示"榜X") address: holder.address, balance: formatNumber(holder.balance), usdValue: formatNumber(holder.usd_value), netflowUsd: formatNumber(holder.netflow_usd), netflowClass: (holder.netflow_usd || 0) >= 0 ? 'statistic-gmgn-profit-positive' : 'statistic-gmgn-profit-negative', profit: formatNumber(holder.profit), profitSign: holder.profit >= 0 ? '+' : '', profitClass: holder.profit >= 0 ? 'statistic-gmgn-profit-positive' : 'statistic-gmgn-profit-negative', profitChange: holder.profit_change ? (holder.profit_change * 100).toFixed(1) + '%' : 'N/A', profitChangeClass: (holder.profit_change || 0) >= 0 ? 'statistic-gmgn-profit-positive' : 'statistic-gmgn-profit-negative', amountPercentage: holder.amount_percentage ? (holder.amount_percentage * 100).toFixed(2) + '%' : 'N/A', sellPercentage: holder.sell_amount_percentage ? (holder.sell_amount_percentage * 100).toFixed(2) + '%' : '0.00%' // 筹码已卖 }; // 只有在需要显示SOL余额时才添加 if (showSolBalance) { baseData.solBalance = holder.native_balance ? ((holder.native_balance / 1000000000).toFixed(2) + ' SOL') : 'N/A'; } return baseData; }); // 2. 创建弹框基础结构 - 使用token_holding_temp.js的DOM结构 const modal = document.createElement('div'); modal.className = 'statistic-gmgn-modal'; modal.innerHTML = ` <div class="statistic-gmgn-modal-content"> <div class="statistic-gmgn-modal-header"> <div class="statistic-gmgn-modal-title">📊 ${title} (${processedData.length}个地址)</div> <button class="statistic-gmgn-modal-close">×</button> </div> <div class="statistic-gmgn-analysis-summary"> <div class="statistic-gmgn-summary-stats"> <div class="statistic-gmgn-stat-item"> <span class="statistic-gmgn-stat-label">CA地址:</span> <span class="statistic-gmgn-stat-value">${caAddress || 'N/A'}</span> </div> <div class="statistic-gmgn-stat-item"> <span class="statistic-gmgn-stat-label">总数量:</span> <span class="statistic-gmgn-stat-value">${processedData.length}</span> </div> </div> <button id="statistic-export-excel-btn" class="statistic-gmgn-export-btn" title="导出Excel">📊 导出Excel</button> </div> <div id="statistic-gmgn-results-list"></div> </div> `; // 3. 插入DOM document.body.appendChild(modal); // 4. 填充结果列表 - 参考token_holding_temp.js的方式 const resultsList = document.getElementById('statistic-gmgn-results-list'); processedData.forEach((holder, index) => { const item = document.createElement('div'); item.className = 'statistic-gmgn-result-item'; item.innerHTML = ` <div class="statistic-gmgn-result-header"> <div class="statistic-gmgn-result-rank"> <span style="color: #ff6b35; font-weight: bold; background: rgba(255, 107, 53, 0.15); padding: 2px 6px; border-radius: 12px; border: 1px solid rgba(255, 107, 53, 0.3); font-size: 12px;">榜${holder.rankIndex}</span> </div> <div class="statistic-gmgn-result-address" title="点击复制地址">${holder.address}</div> <a href="https://gmgn.ai/sol/address/${holder.address}" target="_blank" class="statistic-gmgn-address-jump-btn" title="查看钱包详情">详情</a> </div> <div class="statistic-gmgn-compact-details"> <div class="statistic-gmgn-detail-section"> <div class="statistic-gmgn-section-title">基本信息</div> <div class="statistic-gmgn-detail-grid"> <span class="statistic-gmgn-detail-label">持仓:</span> <span class="statistic-gmgn-detail-value">${holder.balance}</span> <span class="statistic-gmgn-detail-label">持仓占比:</span> <span class="statistic-gmgn-detail-value statistic-gmgn-value-highlight">${holder.amountPercentage}</span> <span class="statistic-gmgn-detail-label">筹码已卖:</span> <span class="statistic-gmgn-detail-value" style="color: ${holder.sellPercentage === '0.00%' ? '#00ff88' : '#ff6b35'}; font-weight: bold;">${holder.sellPercentage}</span> <span class="statistic-gmgn-detail-label">净流入:</span> <span class="statistic-gmgn-detail-value ${holder.netflowClass}">$${holder.netflowUsd}</span> <span class="statistic-gmgn-detail-label">盈亏:</span> <span class="statistic-gmgn-detail-value ${holder.profitClass}">${holder.profitSign}$${holder.profit}</span> <span class="statistic-gmgn-detail-label">倍数:</span> <span class="statistic-gmgn-detail-value ${holder.profitChangeClass}">${holder.profitChange}</span> ${holder.solBalance ? ` <span class="statistic-gmgn-detail-label">SOL餘額:</span> <span class="statistic-gmgn-detail-value statistic-gmgn-value-highlight">${holder.solBalance}</span> ` : ''} </div> </div> </div> `; // 添加地址复制功能 const addressElement = item.querySelector('.statistic-gmgn-result-address'); addressElement.addEventListener('click', () => { navigator.clipboard.writeText(holder.address).then(() => { addressElement.style.backgroundColor = '#16a34a'; addressElement.style.color = 'white'; setTimeout(() => { addressElement.style.backgroundColor = ''; addressElement.style.color = ''; }, 1000); }); }); resultsList.appendChild(item); }); // ESC键关闭处理函数 const escKeyHandler = (e) => { if (e.key === 'Escape') { document.body.removeChild(modal); document.removeEventListener('keydown', escKeyHandler); } }; document.addEventListener('keydown', escKeyHandler); // 5. 绑定导出Excel按钮事件 const exportBtn = modal.querySelector('#statistic-export-excel-btn'); if (exportBtn) { exportBtn.addEventListener('click', () => { exportToExcel(processedData, title, caAddress, showSolBalance); }); } // 6. 绑定关闭按钮事件 modal.querySelector('.statistic-gmgn-modal-close').addEventListener('click', () => { document.body.removeChild(modal); document.removeEventListener('keydown', escKeyHandler); }); // 点击模态框外部关闭 modal.addEventListener('click', (e) => { if (e.target === modal) { document.body.removeChild(modal); document.removeEventListener('keydown', escKeyHandler); } }); } // 数字格式化函数 function formatNumber(num) { if (num === null || num === undefined) return 'N/A'; // 處理負數:保留負號,對絕對值進行格式化 const isNegative = num < 0; const absNum = Math.abs(num); let formatted; if (absNum >= 1000000000) { formatted = (absNum / 1000000000).toFixed(2) + 'B'; } else if (absNum >= 1000000) { formatted = (absNum / 1000000).toFixed(2) + 'M'; } else if (absNum >= 1000) { formatted = (absNum / 1000).toFixed(2) + 'K'; } else { formatted = absNum.toFixed(2); } return isNegative ? '-' + formatted : formatted; } // Excel导出功能 function exportToExcel(data, title, caAddress, showSolBalance) { try { // 创建工作表数据 const worksheetData = []; // 添加标题行 const headers = ['排名', '地址', '持仓数量', '持仓占比', '筹码已卖', 'USD价值', '净流入USD', '盈亏USD', '盈亏倍数']; if (showSolBalance) { headers.push('SOL餘額'); } worksheetData.push(headers); // 添加数据行 data.forEach((holder, index) => { const row = [ holder.rank, holder.address, holder.balance, holder.amountPercentage, holder.sellPercentage, holder.usdValue, holder.netflowUsd, (holder.profitSign || '') + holder.profit, holder.profitChange ]; if (showSolBalance) { row.push(holder.solBalance || 'N/A'); } worksheetData.push(row); }); // 创建工作簿 const wb = XLSX.utils.book_new(); const ws = XLSX.utils.aoa_to_sheet(worksheetData); // 设置列宽 const colWidths = [ {wch: 6}, // 排名 {wch: 45}, // 地址 {wch: 15}, // 持仓数量 {wch: 10}, // 持仓比例 {wch: 10}, // 已卖比例 {wch: 15}, // USD价值 {wch: 15}, // 净流入 {wch: 15}, // 盈亏 {wch: 12} // 倍数 ]; if (showSolBalance) { colWidths.push({wch: 12}); // SOL餘額 } ws['!cols'] = colWidths; // 添加工作表到工作簿 XLSX.utils.book_append_sheet(wb, ws, title); // 生成文件名 const timestamp = new Date().toISOString().slice(0, 19).replace(/:/g, '-'); const fileName = `${title}_${caAddress ? caAddress.slice(0, 8) : 'data'}_${timestamp}.xlsx`; // 下载文件 XLSX.writeFile(wb, fileName); // 显示成功提示 const exportBtn = document.querySelector('#statistic-export-excel-btn'); if (exportBtn) { const originalText = exportBtn.textContent; exportBtn.textContent = '✅ 导出成功'; exportBtn.style.backgroundColor = '#059669'; setTimeout(() => { exportBtn.textContent = originalText; exportBtn.style.backgroundColor = ''; }, 2000); } } catch (error) { console.error('Excel导出失败:', error); alert('导出失败,请检查浏览器控制台了解详情'); } } // 根据类型获取对应的地址数据 function getAddressByType(type) { if (!interceptedData?.data?.list) return []; const currentTime = Math.floor(Date.now() / 1000); const sevenDaysInSeconds = 7 * 24 * 60 * 60; const holders = interceptedData.data.list; switch(type) { case 'fullPosition': return holders.filter(h => h.sell_amount_percentage === 0 && (!h.token_transfer_out || !h.token_transfer_out.address) ); case 'profitable': return holders.filter(h => h.profit > 0); case 'losing': return holders.filter(h => h.profit < 0); case 'active24h': return holders.filter(h => h.last_active_timestamp > currentTime - 86400); case 'diamondHands': return holders.filter(h => h.maker_token_tags?.includes('diamond_hands')); case 'newAddress': return holders.filter(h => h.tags?.includes('fresh_wallet')); case 'holdingLessThan7Days': return holders.filter(h => h.start_holding_at && (currentTime - h.start_holding_at) < sevenDaysInSeconds ); case 'highProfit': return holders.filter(h => h.profit_change > 5); case 'suspicious': return holders.filter(h => h.is_suspicious || (h.maker_token_tags && ( h.maker_token_tags.includes('rat_trader') || h.maker_token_tags.includes('transfer_in') )) ); case 'lowSolBalance': return holders.filter(h => h.native_balance && (h.native_balance / 1000000000) < 1 ); case 'tokenTransferIn': return holders.filter(h => h.token_transfer_in && h.token_transfer_in.address && h.token_transfer_in.address.trim() !== '' ); case 'exchangeAddresses': return holders.filter(h => isExchangeAddress(h)); default: return []; } } // 获取类型对应的中文标题 function getTypeTitle(type) { const titles = { 'fullPosition': '满仓地址', 'profitable': '盈利地址', 'losing': '亏损地址', 'active24h': '24小时活跃地址', 'diamondHands': '钻石手地址', 'newAddress': '新地址', 'holdingLessThan7Days': '持仓小于7天的地址', 'highProfit': '5倍以上盈利地址', 'suspicious': '可疑地址', 'lowSolBalance': 'SOL餘額不足1的地址', 'tokenTransferIn': '代币转入地址', 'exchangeAddresses': '交易所地址' }; return titles[type] || '未知类型'; } // 1. 拦截 fetch 请求 const originalFetch = window.fetch; window.fetch = function(url, options) { if (isTargetApi(url)) { console.log('[拦截] fetch 请求:', url); return originalFetch.apply(this, arguments) .then(response => { if (response.ok) { processResponse(response.clone()); } return response; }); } return originalFetch.apply(this, arguments); }; // 2. 拦截 XMLHttpRequest const originalXHR = window.XMLHttpRequest; window.XMLHttpRequest = function() { const xhr = new originalXHR(); const originalOpen = xhr.open; xhr.open = function(method, url) { if (isTargetApi(url)) { console.log('[拦截] XHR 请求:', url); const originalOnload = xhr.onload; xhr.onload = function() { if (xhr.readyState === 4 && xhr.status === 200) { processResponse(xhr.responseText); } originalOnload?.apply(this, arguments); }; } return originalOpen.apply(this, arguments); }; return xhr; }; function isTargetApi(url) { if (typeof url !== 'string') return false; // 检查是否是token_holders API且包含limit参数 const isTokenHoldersApi = /vas\/api\/v1\/token_holders\/(sol|eth|base|bsc|tron)(\/|$|\?)/i.test(url); const hasLimitParam = /[?&]limit=/i.test(url); const isTarget = isTokenHoldersApi && hasLimitParam; if (isTarget) { // 从URL中提取CA地址 const match = url.match(/vas\/api\/v1\/token_holders\/sol\/([^/?]+)/i); console.log('匹配的ca:',match) console.log('包含limit参数的URL:', url) if (match && match[1]) { currentCaAddress = match[1]; } } return isTarget; } function processResponse(response) { console.log('开始处理响应数据'); try { const dataPromise = typeof response === 'string' ? Promise.resolve(JSON.parse(response)) : response.json(); dataPromise.then(data => { interceptedData = data; console.log('[成功] 拦截到数据量:', data.data?.list?.length); console.log('[成功] 拦截到数据:',data); const currentStats = calculateStats(); if (isFirstLoad) { // 首次加载,记录初始数据和CA地址 initialStats = currentStats; initialCaAddress = currentCaAddress; isFirstLoad = false; updateStatsDisplay(currentStats, true); } else { // 非首次加载,比较CA地址 const isSameCa = currentCaAddress === initialCaAddress; updateStatsDisplay(currentStats, !isSameCa); // 如果CA地址不同,更新初始数据为当前数据 if (!isSameCa) { initialStats = currentStats; initialCaAddress = currentCaAddress; } } }).catch(e => console.error('解析失败:', e)); } catch (e) { console.error('处理响应错误:', e); } } // 3. 计算所有统计指标 function calculateStats() { if (!interceptedData?.data?.list) return null; const currentTime = Math.floor(Date.now() / 1000); const sevenDaysInSeconds = 7 * 24 * 60 * 60; // 7天的秒数 const holders = interceptedData.data.list; const stats = { fullPosition: 0, // 全仓 profitable: 0, // 盈利 losing: 0, // 亏损 active24h: 0, // 24h活跃 diamondHands: 0, // 钻石手 newAddress: 0, // 新地址 highProfit: 0, // 10x盈利 suspicious: 0, // 新增:可疑地址 holdingLessThan7Days: 0, // 新增:持仓小于7天 lowSolBalance: 0, // 新增:SOL餘額小於1的地址 tokenTransferIn: 0, // 新增:代币转入地址数 exchangeAddresses: 0 // 新增:交易所地址数 }; holders.forEach(holder => { // 满判断条件:1.没有卖出;2.没有出货地址 if (holder.sell_amount_percentage === 0 && (!holder.token_transfer_out || !holder.token_transfer_out.address)) { stats.fullPosition++; } if (holder.profit > 0) stats.profitable++; if (holder.profit < 0) stats.losing++; if (holder.last_active_timestamp > currentTime - 86400) stats.active24h++; if (holder.maker_token_tags?.includes('diamond_hands')) stats.diamondHands++; if (holder.tags?.includes('fresh_wallet')) stats.newAddress++; if (holder.profit_change > 5) stats.highProfit++; // 增强版可疑地址检测 if ( holder.is_suspicious || (holder.maker_token_tags && ( holder.maker_token_tags.includes('rat_trader') || holder.maker_token_tags.includes('transfer_in') )) ) { stats.suspicious++; } // 新增7天持仓统计 if (holder.start_holding_at && (currentTime - holder.start_holding_at) < sevenDaysInSeconds) { stats.holdingLessThan7Days++; } // 新增低SOL餘額統計(小於1 SOL) if (holder.native_balance && (holder.native_balance / 1000000000) < 1) { stats.lowSolBalance++; } // 新增代币转入地址统计 if (holder.token_transfer_in && holder.token_transfer_in.address && holder.token_transfer_in.address.trim() !== '') { stats.tokenTransferIn++; } // 新增交易所地址统计 if (isExchangeAddress(holder)) { stats.exchangeAddresses++; } }); return stats; } // 1. 持久化容器监听 const observer = new MutationObserver(() => { const targetContainer = document.querySelector('.flex.overflow-x-auto.overflow-y-hidden.scroll-smooth.w-full'); if (targetContainer && !targetContainer.querySelector('#statistic-gmgn-stats-item')) { injectStatsItem(targetContainer); } }); function injectStatsItem(container) { if (container.querySelector('#statistic-gmgn-stats-item')) return; const isSol = isSolNetwork(); const statsItem = document.createElement('div'); statsItem.id = 'statistic-gmgn-stats-item'; statsItem.className = 'statistic-gmgn-stats-container'; const headerClass = isSol ? 'statistic-gmgn-stats-header sol-network' : 'statistic-gmgn-stats-header'; const dataClass = isSol ? 'statistic-gmgn-stats-data sol-network' : 'statistic-gmgn-stats-data'; statsItem.innerHTML = ` <div class="${headerClass}"> <span title="持有代币且未卖出任何数量的地址(排除转移代币卖出的地址)">满仓</span> <span title="当前持仓价值高于买入成本的地址">盈利</span> <span title="当前持仓价值低于买入成本的地址">亏损</span> <span title="过去24小时内有交易活动的地址">活跃</span> <span title="长期持有且很少卖出的地址">钻石</span> <span title="新钱包">新址</span> <span title="持仓时间小于7天的地址">7天</span> <span title="盈利超过5倍的地址">5X</span> <span title="标记为可疑或异常行为的地址">可疑</span> <span title="有代币转入记录的地址">转入</span> <span title="与交易所相关的地址">交易所</span> ${isSol ? '<span title="SOL餘額小於1的地址">低SOL</span>' : ''} </div> <div class="${dataClass}"> <span id="fullPosition">-</span> <span id="profitable">-</span> <span id="losing">-</span> <span id="active24h">-</span> <span id="diamondHands">-</span> <span id="newAddress">-</span> <span id="holdingLessThan7Days">-</span> <span id="highProfit">-</span> <span id="suspicious">-</span> <span id="tokenTransferIn">-</span> <span id="exchangeAddresses">-</span> ${isSol ? '<span id="lowSolBalance">-</span>' : ''} </div> `; container.insertAdjacentElement('afterbegin', statsItem); } function updateStatsDisplay(currentStats, forceNoArrows) { if (!currentStats) return; // 确保DOM已存在 if (!document.getElementById('statistic-gmgn-stats-item')) { injectStatsItem(); } const updateStatElement = (id, value, hasChanged, isIncrease) => { const element = document.getElementById(id); if (!element) return; element.innerHTML = `<strong style="color: ${id === 'profitable' ? '#2E8B57' : (id === 'losing' || id === 'suspicious' ? '#FF1493' : id === 'holdingLessThan7Days' ? '#00E5EE' : id === 'lowSolBalance' ? '#FFA500' : '#e9ecef')}">${value}</strong>`; // 只有当不是强制不显示箭头且确实有变化时才显示箭头 if (!forceNoArrows && hasChanged) { const arrow = document.createElement('span'); arrow.className = isIncrease ? 'statistic-up-arrow' : 'statistic-down-arrow'; arrow.textContent = isIncrease ? '▲' : '▼'; // 移除旧的箭头(如果有) const oldArrow = element.querySelector('.statistic-up-arrow, .statistic-down-arrow'); if (oldArrow) oldArrow.remove(); element.appendChild(arrow); } else { // 没有变化或强制不显示箭头,移除箭头(如果有) const oldArrow = element.querySelector('.statistic-up-arrow, .statistic-down-arrow'); if (oldArrow) oldArrow.remove(); } // 为所有统计类型添加点击事件监听器 const baseClickableTypes = ['fullPosition', 'profitable', 'losing', 'active24h', 'diamondHands', 'newAddress', 'holdingLessThan7Days', 'highProfit', 'suspicious', 'tokenTransferIn', 'exchangeAddresses']; const clickableTypes = isSolNetwork() ? [...baseClickableTypes, 'lowSolBalance'] : baseClickableTypes; if (clickableTypes.includes(id)) { element.classList.add('clickable'); element.onclick = (e) => { e.preventDefault(); e.stopPropagation(); const addresses = getAddressByType(id); // 交易所地址使用专用弹框 if (id === 'exchangeAddresses') { createExchangeModal(addresses, currentCaAddress); } else { const title = getTypeTitle(id); const showSolBalance = id === 'lowSolBalance'; createModal(title, addresses, currentCaAddress, showSolBalance); } }; } else { // 其他类型移除点击样式和事件 element.classList.remove('clickable'); element.onclick = null; } }; // 更新各个统计指标 // 新增7天持仓统计更新 updateStatElement('holdingLessThan7Days', currentStats.holdingLessThan7Days, initialStats && currentStats.holdingLessThan7Days !== initialStats.holdingLessThan7Days, initialStats && currentStats.holdingLessThan7Days > initialStats.holdingLessThan7Days); updateStatElement('fullPosition', currentStats.fullPosition, initialStats && currentStats.fullPosition !== initialStats.fullPosition, initialStats && currentStats.fullPosition > initialStats.fullPosition); updateStatElement('profitable', currentStats.profitable, initialStats && currentStats.profitable !== initialStats.profitable, initialStats && currentStats.profitable > initialStats.profitable); updateStatElement('losing', currentStats.losing, currentStats.losing !== initialStats.losing, currentStats.losing > initialStats.losing); updateStatElement('active24h', currentStats.active24h, currentStats.active24h !== initialStats.active24h, currentStats.active24h > initialStats.active24h); updateStatElement('diamondHands', currentStats.diamondHands, currentStats.diamondHands !== initialStats.diamondHands, currentStats.diamondHands > initialStats.diamondHands); updateStatElement('newAddress', currentStats.newAddress, currentStats.newAddress !== initialStats.newAddress, currentStats.newAddress > initialStats.newAddress); updateStatElement('highProfit', currentStats.highProfit, currentStats.highProfit !== initialStats.highProfit, currentStats.highProfit > initialStats.highProfit); updateStatElement('suspicious', currentStats.suspicious, currentStats.suspicious !== initialStats.suspicious, currentStats.suspicious > initialStats.suspicious); updateStatElement('tokenTransferIn', currentStats.tokenTransferIn, initialStats && currentStats.tokenTransferIn !== initialStats.tokenTransferIn, initialStats && currentStats.tokenTransferIn > initialStats.tokenTransferIn); updateStatElement('exchangeAddresses', currentStats.exchangeAddresses, initialStats && currentStats.exchangeAddresses !== initialStats.exchangeAddresses, initialStats && currentStats.exchangeAddresses > initialStats.exchangeAddresses); // 只在SOL网络时更新低SOL余额统计 if (isSolNetwork()) { updateStatElement('lowSolBalance', currentStats.lowSolBalance, initialStats && currentStats.lowSolBalance !== initialStats.lowSolBalance, initialStats && currentStats.lowSolBalance > initialStats.lowSolBalance); } } // 4. 初始化 if (document.readyState === 'complete') { startObserving(); } else { window.addEventListener('DOMContentLoaded', startObserving); } function startObserving() { // 立即检查一次 const initialContainer = document.querySelector('.flex.overflow-x-auto.overflow-y-hidden.scroll-smooth.w-full'); if (initialContainer) injectStatsItem(initialContainer); // 持续监听DOM变化 observer.observe(document.body, { childList: true, subtree: true, attributes: false }); } })();
QingJ © 2025
镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址