您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
监听GMGN.ai交易者数据并提供Excel导出功能
// ==UserScript== // @name GMGN交易者数据导出 // @namespace http://tampermonkey.net/ // @version 1.2 // @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/* // @run-at document-start // @grant none // @license MIT // ==/UserScript== (function() { 'use strict'; let tradersData = []; let currentCA = ''; let currentChain = ''; // 直接在最外层拦截 XMLHttpRequest const originalXHR = window.XMLHttpRequest; window.XMLHttpRequest = function() { const xhr = new originalXHR(); const originalOpen = xhr.open; xhr.open = function(method, url) { if (isTargetTraderApi(url)) { console.log('[GMGN交易者] 拦截请求:', url); const originalOnload = xhr.onload; xhr.onload = function() { if (xhr.readyState === 4 && xhr.status === 200) { try { const data = JSON.parse(xhr.responseText); if (data.code === 0 && data.data?.list) { processTraderData(data.data.list); } } catch (e) { console.warn('[GMGN交易者] 处理失败:', e); } } originalOnload?.apply(this, arguments); }; } return originalOpen.apply(this, arguments); }; return xhr; }; function isTargetTraderApi(url) { if (typeof url !== 'string') return false; return url.includes('/vas/api/v1/token_traders/'); } // 处理交易者数据的函数 function processTraderData(newData) { const existingAddresses = new Set(tradersData.map(trader => trader.address)); // 添加新的交易者数据 newData.forEach(trader => { if (trader.address && !existingAddresses.has(trader.address)) { tradersData.push(trader); } else if (trader.address && existingAddresses.has(trader.address)) { // 更新现有交易者数据 const existingIndex = tradersData.findIndex(t => t.address === trader.address); if (existingIndex !== -1) { tradersData[existingIndex] = trader; } } }); console.log(`GMGN本次获取 ${newData.length} 条数据,总计 ${tradersData.length} 条交易者数据`); updateDownloadButton(); } // 从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 = []; const urlInfo = extractCAFromURL(); if (urlInfo) { currentCA = urlInfo.ca; currentChain = urlInfo.chain; console.log(`开始监听新的CA: ${currentChain}/${currentCA}`); } } // 格式化金额为$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'; // 按总利润降序排序 const sortedTradersData = [...tradersData].sort((a, b) => (b.profit || 0) - (a.profit || 0)); sortedTradersData.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 = ` <svg width="12px" height="12px" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg" fill="currentColor"> <path d="M8.35343 15.677L13.881 7.19356C14.098 6.86024 14.01 6.40976 13.684 6.1877C13.5677 6.10833 13.431 6.066 13.2912 6.06606H8.94507V0.725344C8.94507 0.324837 8.62782 0 8.23639 0C7.99931 0 7.77814 0.121166 7.64658 0.322951L2.11903 8.80644C1.902 9.13976 1.99001 9.59 2.31579 9.8123C2.43216 9.89175 2.56893 9.93416 2.70883 9.93417H7.05494V15.2747C7.05494 15.6752 7.37219 16 7.76362 16C8.0007 16 8.2221 15.8788 8.35343 15.677Z"></path> </svg> 导出交易者数据 `; 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.style.pointerEvents = 'auto'; button.title = `点击导出 ${tradersData.length} 条交易者数据`; console.log(`按钮状态已更新,数据量: ${tradersData.length}`); } else { button.style.opacity = '0.6'; button.style.pointerEvents = 'none'; button.title = '没有数据,请切换到交易者tab页'; } } else { // 如果按钮还不存在,但有数据了,尝试创建按钮 if (tradersData.length > 0) { console.log('数据已获取但按钮不存在,尝试插入按钮'); setTimeout(() => { insertDownloadButton(); }, 500); } } } // 插入下载按钮到页面 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); console.log('下载按钮已插入'); // 插入后立即更新按钮状态 updateDownloadButton(); } else if (!targetDiv) { console.log('目标容器不存在,无法插入按钮'); } else if (existingButton) { console.log('按钮已存在,更新状态'); updateDownloadButton(); } } // 监听页面变化 function observePageChanges() { let lastUrl = location.href; let lastCA = currentCA; // 使用MutationObserver监听DOM变化 const observer = new MutationObserver(function(mutations) { const currentUrl = location.href; const urlInfo = extractCAFromURL(); const newCA = urlInfo ? urlInfo.ca : ''; // URL变化或CA变化时重置数据 if (currentUrl !== lastUrl || newCA !== lastCA) { lastUrl = currentUrl; lastCA = newCA; console.log(`页面变化检测 - URL: ${currentUrl}, CA: ${newCA}`); console.log('重置交易者数据'); resetData(); // 延迟插入按钮,等待页面加载 setTimeout(() => { insertDownloadButton(); }, 2000); } // 检查是否需要重新插入按钮 if (!document.getElementById('gmgn-export-button')) { setTimeout(() => { insertDownloadButton(); }, 1000); } }); observer.observe(document.body, { childList: true, subtree: true }); // 同时监听浏览器历史变化(前进/后退按钮) window.addEventListener('popstate', function() { const urlInfo = extractCAFromURL(); const newCA = urlInfo ? urlInfo.ca : ''; if (newCA !== currentCA) { console.log(`浏览器历史变化 - 新CA: ${newCA}, 旧CA: ${currentCA}`); console.log('重置交易者数据'); resetData(); setTimeout(() => { insertDownloadButton(); }, 2000); } }); } // 初始化函数 - 在document-start阶段执行 function init() { console.log('GMGN交易者数据导出插件已在document-start阶段启动'); // 立即设置当前页面的CA和链信息 resetData(); // 等待DOM完全加载后执行DOM相关操作 function waitForDOM() { if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', () => { setTimeout(() => { insertDownloadButton(); observePageChanges(); }, 2000); }); } else { setTimeout(() => { insertDownloadButton(); observePageChanges(); }, 2000); } } // 如果DOM已经存在,立即执行;否则等待 if (document.documentElement) { waitForDOM(); } else { // 极早阶段,连documentElement都不存在,使用更底层的监听 const observer = new MutationObserver((mutations, obs) => { if (document.documentElement) { obs.disconnect(); waitForDOM(); } }); observer.observe(document, { childList: true, subtree: true }); } } // 在document-start阶段立即执行 init(); })();