// ==UserScript== // @name GMGN交易者数据导出 // @namespace http://tampermonkey.net/ // @version 1.0 // @description 监听GMGN.ai交易者数据并提供Excel导出功能 // @author You // @match https://gmgn.ai/sol/token/* // @match https://gmgn.ai/eth/token/* // @match https://gmgn.ai/bsc/token/* // @match https://gmgn.ai/base/token/* // @match https://gmgn.ai/arb/token/* // @match https://gmgn.ai/op/token/* // @grant none // @license MIT // @downloadURL none // ==/UserScript== (function() { 'use strict'; let tradersData = []; let currentCA = ''; let currentChain = ''; let isListening = false; // 从URL中提取CA地址和链网络 function extractCAFromURL() { const url = window.location.pathname; const match = url.match(/\/(\w+)\/token\/(?:\w+_)?([A-Za-z0-9]+)$/); if (match) { const chain = match[1]; const ca = match[2]; return { chain, ca }; } return null; } // 清空数据并重新开始监听 function resetData() { tradersData = []; isListening = false; const urlInfo = extractCAFromURL(); if (urlInfo) { currentCA = urlInfo.ca; currentChain = urlInfo.chain; console.log(`开始监听新的CA: ${currentChain}/${currentCA}`); startXHRInterception(); } } // 拦截XHR请求 function startXHRInterception() { if (isListening) return; isListening = true; const originalXHROpen = XMLHttpRequest.prototype.open; const originalXHRSend = XMLHttpRequest.prototype.send; XMLHttpRequest.prototype.open = function(method, url, ...args) { this._url = url; return originalXHROpen.apply(this, [method, url, ...args]); }; XMLHttpRequest.prototype.send = function(...args) { const xhr = this; // 检查是否为交易者数据请求 if (xhr._url && xhr._url.includes(`/vas/api/v1/token_traders/${currentChain}/${currentCA}`)) { console.log('检测到交易者数据请求:', xhr._url); const originalOnReadyStateChange = xhr.onreadystatechange; xhr.onreadystatechange = function() { if (xhr.readyState === 4 && xhr.status === 200) { try { const responseData = JSON.parse(xhr.responseText); if (responseData.code === 0 && responseData.data && responseData.data.list) { tradersData = responseData.data.list; console.log(`获取到 ${tradersData.length} 条交易者数据`); updateDownloadButton(); } } catch (e) { console.error('解析交易者数据失败:', e); } } if (originalOnReadyStateChange) { originalOnReadyStateChange.apply(xhr, arguments); } }; } return originalXHRSend.apply(this, args); }; } // 格式化金额为$xxxK/M/B格式 function formatCurrency(value) { if (!value || value === 0) return '$0'; const absValue = Math.abs(value); let formattedValue; let suffix; if (absValue >= 1000000000) { formattedValue = (value / 1000000000).toFixed(1); suffix = 'B'; } else if (absValue >= 1000000) { formattedValue = (value / 1000000).toFixed(1); suffix = 'M'; } else if (absValue >= 1000) { formattedValue = (value / 1000).toFixed(1); suffix = 'K'; } else { formattedValue = value.toFixed(2); suffix = ''; } // 移除不必要的.0 if (formattedValue.endsWith('.0')) { formattedValue = formattedValue.slice(0, -2); } return `$${formattedValue}${suffix}`; } // 格式化时间戳 function formatTimestamp(timestamp) { if (!timestamp) return '-'; const date = new Date(timestamp * 1000); return date.toLocaleDateString('zh-CN'); } // 计算持仓时间(小时) function calculateHoldingTime(startTime, endTime) { if (!startTime || !endTime) return '-'; const hours = Math.round((endTime - startTime) / 3600); return hours > 0 ? `${hours}小时` : '-'; } // 导出Excel数据 function exportToExcel() { if (tradersData.length === 0) { alert('没有获取到交易者数据,请切换到【交易者】tab页或重新刷新网页'); return; } const headers = ['交易者地址', 'SOL余额', '总买入', '总卖出', '平均买价', '平均卖价', '总利润', '利润率', '持仓时间', '最后活跃']; let csvContent = "data:text/csv;charset=utf-8,\uFEFF" + headers.join(',') + '\n'; tradersData.forEach(trader => { const row = [ trader.address || '-', (parseFloat(trader.native_balance) / 1000000000).toFixed(2) || '0.00', formatCurrency(trader.buy_volume_cur || 0), formatCurrency(trader.sell_volume_cur || 0), trader.avg_cost?.toFixed(8) || '0', trader.avg_sold?.toFixed(8) || '0', formatCurrency(trader.profit || 0), ((trader.profit_change || 0) * 100).toFixed(2) + '%', calculateHoldingTime(trader.start_holding_at, trader.end_holding_at), formatTimestamp(trader.last_active_timestamp) ]; csvContent += row.join(',') + '\n'; }); const encodedUri = encodeURI(csvContent); const link = document.createElement("a"); link.setAttribute("href", encodedUri); link.setAttribute("download", `gmgn_traders_${currentChain}_${currentCA}_${new Date().getTime()}.csv`); document.body.appendChild(link); link.click(); document.body.removeChild(link); console.log(`成功导出 ${tradersData.length} 条交易者数据`); } // 创建下载按钮 function createDownloadButton() { const button = document.createElement('div'); button.className = 'h-[28px] flex items-center text-[12px] font-medium cursor-pointer bg-btn-secondary p-6px rounded-6px gap-2px text-text-200 hover:text-text-100'; button.id = 'gmgn-export-button'; button.innerHTML = ` 导出交易者数据 `; button.addEventListener('click', exportToExcel); return button; } // 更新下载按钮状态 function updateDownloadButton() { const button = document.getElementById('gmgn-export-button'); if (button) { if (tradersData.length > 0) { button.style.opacity = '1'; button.title = `点击导出 ${tradersData.length} 条交易者数据`; } else { button.style.opacity = '0.6'; button.title = '没有数据,请切换到交易者tab页'; } } } // 插入下载按钮到页面 function insertDownloadButton() { const targetDiv = document.querySelector('.flex.absolute.top-0.right-0.gap-8px.pl-4px'); const existingButton = document.getElementById('gmgn-export-button'); if (targetDiv && !existingButton) { const downloadButton = createDownloadButton(); targetDiv.insertBefore(downloadButton, targetDiv.firstChild); updateDownloadButton(); console.log('下载按钮已插入'); } } // 监听页面变化 function observePageChanges() { let lastUrl = location.href; // 使用MutationObserver监听DOM变化 const observer = new MutationObserver(function(mutations) { if (location.href !== lastUrl) { lastUrl = location.href; console.log('URL变化,重置数据:', lastUrl); resetData(); // 延迟插入按钮,等待页面加载 setTimeout(() => { insertDownloadButton(); }, 2000); } // 检查是否需要重新插入按钮 if (!document.getElementById('gmgn-export-button')) { setTimeout(() => { insertDownloadButton(); }, 1000); } }); observer.observe(document.body, { childList: true, subtree: true }); } // 初始化 function init() { console.log('GMGN交易者数据导出插件已启动'); resetData(); // 等待页面加载完成后插入按钮 if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', () => { setTimeout(() => { insertDownloadButton(); observePageChanges(); }, 2000); }); } else { setTimeout(() => { insertDownloadButton(); observePageChanges(); }, 2000); } } init(); })();