Greasy Fork

Greasy Fork is available in English.

GMGN 净买入追踪器

追踪和计算净买入地址数据

您需要先安装一个扩展,例如 篡改猴Greasemonkey暴力猴,之后才能安装此脚本。

You will need to install an extension such as Tampermonkey to install this script.

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴Userscripts ,之后才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。

您需要先安装用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name         GMGN 净买入追踪器
// @namespace    http://tampermonkey.net/
// @version      1.1
// @description  追踪和计算净买入地址数据
// @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';

    // 全局变量
    let isRecording = false;
    let tradeData = new Map(); // 存储交易数据 {maker: {buyAmount: 0, sellAmount: 0, netBuying: 0}}
    let currentCaAddress = null;
    let totalTradesProcessed = 0; // 总处理交易数量
    
    // 检查是否为有效的代币页面
    function isValidTokenPage() {
        const url = window.location.href;
        const pattern = /^https:\/\/gmgn\.ai\/(sol|base|tron|eth|bsc)\/token\//;
        return pattern.test(url);
    }
    
    // 动态添加CSS样式
    const style = document.createElement('style');
    style.textContent = `
    .net-buying-tracker-buttons {
        display: flex;
        margin-right: 8px;
        border: 1px solid rgb(75 85 99);
        border-radius: 4px;
        overflow: hidden;
    }
    
    .net-buying-btn {
        height: 24px;
        display: flex;
        align-items: center;
        text-sm: true;
        color: rgb(156 163 175);
        cursor: pointer;
        padding: 4px 12px;
        background: transparent;
        border: none;
        font-size: 12px;
        font-weight: 500;
        transition: all 0.2s ease;
        position: relative;
        white-space: nowrap;
        border-right: 1px solid rgb(75 85 99);
    }
    
    .net-buying-btn:last-child {
        border-right: none;
    }
    
    .net-buying-btn:hover:not(:disabled) {
        background: rgb(55 65 81);
        color: rgb(243 244 246);
    }
    
    .net-buying-btn:disabled {
        opacity: 0.5;
        cursor: not-allowed;
    }
    
    .net-buying-btn.active {
        background: rgb(37 99 235);
        color: white;
        border-color: rgb(37 99 235);
    }
    
    .net-buying-btn.recording {
        background: rgb(220 38 38);
        color: white;
        border-color: rgb(220 38 38);
    }
    
    .net-buying-btn .recording-dot {
        width: 6px;
        height: 6px;
        background: white;
        border-radius: 50%;
        margin-left: 4px;
        animation: pulse 1.5s ease-in-out infinite alternate;
    }
    
    @keyframes pulse {
        0% { opacity: 1; }
        100% { opacity: 0.3; }
    }
    
    /* 弹窗样式 */
    .net-buying-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;
    }
    
    .net-buying-modal-content {
        background-color: #1e293b !important;
        border-radius: 8px !important;
        width: 80% !important;
        max-width: 900px !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;
    }
    
    .net-buying-modal-header {
        display: flex !important;
        justify-content: space-between !important;
        align-items: center !important;
        margin-bottom: 16px !important;
        padding: 0 !important;
    }
    
    .net-buying-modal-title {
        font-size: 18px !important;
        font-weight: 600 !important;
        color: white !important;
        margin: 0 !important;
    }
    
    .net-buying-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;
    }
    
    .net-buying-modal-close:hover {
        color: #ff4444 !important;
        background-color: rgba(255, 255, 255, 0.1) !important;
        border-radius: 4px !important;
    }
    
    .net-buying-summary {
        margin-bottom: 16px;
        padding: 12px;
        background-color: #263238;
        border-radius: 6px;
        display: flex;
        justify-content: space-between;
        align-items: center;
    }
    
    .net-buying-stats {
        display: flex;
        gap: 20px;
    }
    
    .net-buying-stat-item {
        display: flex;
        align-items: baseline;
    }
    
    .net-buying-stat-label {
        color: #94a3b8;
        margin-right: 5px;
    }
    
    .net-buying-stat-value {
        font-weight: 600;
        color: #3b82f6;
    }
    
    .net-buying-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;
    }
    
    .net-buying-export-btn:hover {
        background-color: #059669 !important;
        transform: translateY(-1px) !important;
        box-shadow: 0 2px 4px rgba(16, 185, 129, 0.3) !important;
    }
    
    .net-buying-result-item {
        background-color: #334155;
        border-radius: 6px;
        padding: 12px;
        margin-bottom: 12px;
    }
    
    .net-buying-result-header {
        display: flex;
        justify-content: space-between;
        align-items: center;
        margin-bottom: 8px;
        flex-wrap: wrap;
        gap: 8px;
    }
    
    .net-buying-result-rank {
        font-size: 14px;
        color: #94a3b8;
        font-weight: 600;
        min-width: 30px;
    }
    
    .net-buying-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;
    }
    
    .net-buying-result-address:hover {
        background-color: #64748b;
        transform: translateY(-1px);
    }
    
    .net-buying-detail-section {
        margin-bottom: 12px;
    }
    
    .net-buying-section-title {
        font-size: 13px;
        font-weight: 600;
        color: #94a3b8;
        margin-bottom: 8px;
    }
    
    .net-buying-detail-grid {
        display: grid;
        grid-template-columns: 80px 1fr 80px 1fr 80px 1fr;
        gap: 4px 8px;
        align-items: start;
        font-size: 12px;
    }
    
    .net-buying-detail-label {
        color: #94a3b8;
        font-size: 12px;
        padding: 2px 0;
        align-self: start;
    }
    
    .net-buying-detail-value {
        font-size: 12px;
        color: #e2e8f0;
        padding: 2px 0;
        word-break: break-word;
        line-height: 1.4;
    }
    
    .net-buying-value-highlight {
        color: #3b82f6;
        font-weight: 600;
    }
    
    .net-buying-value-positive {
        color: #00ff88 !important;
    }
    
    .net-buying-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;
    }
    
    .net-buying-address-jump-btn:hover {
        background-color: #059669;
        transform: translateY(-1px);
        box-shadow: 0 2px 4px rgba(16, 185, 129, 0.3);
    }
    `;
    
    // 只在有效的代币页面添加样式
    if (isValidTokenPage()) {
        document.head.appendChild(style);
    }
    
    // 数字格式化函数
    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;
    }
    
    // 提取CA地址和网络
    function extractCaAndNetwork(url) {
        const match = url.match(/\/vas\/api\/v1\/token_trades\/([^\/]+)\/([^\/\?]+)/);
        if (match) {
            return {
                network: match[1],
                ca: match[2]
            };
        }
        return null;
    }
    
    // 拦截fetch请求
    const originalFetch = window.fetch;
    window.fetch = function(url, options) {
        if (isRecording && typeof url === 'string' && url.includes('/vas/api/v1/token_trades/')) {
            console.log('[净买入追踪] 拦截到交易请求:', url);
            return originalFetch.apply(this, arguments)
                .then(response => {
                    if (response.ok) {
                        processTradeResponse(response.clone(), url);
                    }
                    return response;
                });
        }
        return originalFetch.apply(this, arguments);
    };
    
    // 拦截XMLHttpRequest
    const originalXHR = window.XMLHttpRequest;
    window.XMLHttpRequest = function() {
        const xhr = new originalXHR();
        const originalOpen = xhr.open;
        xhr.open = function(method, url) {
            if (isRecording && typeof url === 'string' && url.includes('/vas/api/v1/token_trades/')) {
                console.log('[净买入追踪] 拦截到XHR交易请求:', url);
                const originalOnload = xhr.onload;
                xhr.onload = function() {
                    if (xhr.readyState === 4 && xhr.status === 200) {
                        processTradeResponse(xhr.responseText, url);
                    }
                    originalOnload?.apply(this, arguments);
                };
            }
            return originalOpen.apply(this, arguments);
        };
        return xhr;
    };
    
    // 处理交易响应数据
    function processTradeResponse(response, url) {
        try {
            const dataPromise = typeof response === 'string' ?
                Promise.resolve(JSON.parse(response)) :
                response.json();
                
            dataPromise.then(data => {
                if (data.code === 0 && data.data && data.data.history) {
                    // 提取CA地址
                    const urlInfo = extractCaAndNetwork(url);
                    if (urlInfo) {
                        currentCaAddress = urlInfo.ca;
                    }
                    
                    // 处理交易数据
                    data.data.history.forEach(trade => {
                        recordTrade(trade);
                    });
                    
                    console.log('[净买入追踪] 本次处理了', data.data.history.length, '条交易记录');
                    console.log('[净买入追踪] 累计处理交易:', totalTradesProcessed, '条');
                    console.log('[净买入追踪] 唯一地址数量:', tradeData.size, '个');
                }
            }).catch(e => {
                console.error('[净买入追踪] 解析响应失败:', e);
            });
        } catch (e) {
            console.error('[净买入追踪] 处理响应错误:', e);
        }
    }
    
    // 记录交易数据
    function recordTrade(trade) {
        const { maker, event, amount_usd } = trade;
        
        if (!maker || !event || !amount_usd) return;
        
        // 累计总交易数
        totalTradesProcessed++;
        
        if (!tradeData.has(maker)) {
            tradeData.set(maker, {
                buyAmount: 0,
                sellAmount: 0,
                netBuying: 0,
                totalTrades: 0
            });
        }
        
        const userData = tradeData.get(maker);
        userData.totalTrades++;
        
        if (event === 'buy') {
            userData.buyAmount += parseFloat(amount_usd);
        } else if (event === 'sell') {
            userData.sellAmount += parseFloat(amount_usd);
        }
        
        userData.netBuying = userData.buyAmount - userData.sellAmount;
    }
    
    // 计算净买入数据
    function calculateNetBuying() {
        const netBuyingAddresses = [];
        
        tradeData.forEach((data, maker) => {
            if (data.netBuying > 0) {
                netBuyingAddresses.push({
                    address: maker,
                    buyAmount: data.buyAmount,
                    sellAmount: data.sellAmount,
                    netBuying: data.netBuying,
                    totalTrades: data.totalTrades
                });
            }
        });
        
        // 按净买入量降序排列
        netBuyingAddresses.sort((a, b) => b.netBuying - a.netBuying);
        
        return netBuyingAddresses;
    }
    
    // 创建结果弹窗
    function createResultModal(netBuyingData) {
        // 移除已存在的弹窗
        const existingModal = document.querySelector('.net-buying-modal');
        if (existingModal) {
            existingModal.remove();
        }
        
        const modal = document.createElement('div');
        modal.className = 'net-buying-modal';
        
        modal.innerHTML = `
            <div class="net-buying-modal-content">
                <div class="net-buying-modal-header">
                    <div class="net-buying-modal-title">📈 净买入地址分析 (共${netBuyingData.length}个地址)</div>
                    <button class="net-buying-modal-close">&times;</button>
                </div>
                <div class="net-buying-summary">
                    <div class="net-buying-stats">
                        <div class="net-buying-stat-item">
                            <span class="net-buying-stat-label">净买入地址:</span>
                            <span class="net-buying-stat-value">${netBuyingData.length}</span>
                        </div>
                        <div class="net-buying-stat-item">
                            <span class="net-buying-stat-label">总交易数:</span>
                            <span class="net-buying-stat-value">${totalTradesProcessed}</span>
                        </div>
                        <div class="net-buying-stat-item">
                            <span class="net-buying-stat-label">唯一地址:</span>
                            <span class="net-buying-stat-value">${tradeData.size}</span>
                        </div>
                    </div>
                    <button id="net-buying-export-btn" class="net-buying-export-btn" title="导出Excel">📊 导出Excel</button>
                </div>
                <div id="net-buying-results-list"></div>
            </div>
        `;
        
        document.body.appendChild(modal);
        
        // 填充结果列表
        const resultsList = document.getElementById('net-buying-results-list');
        netBuyingData.forEach((item, index) => {
            const resultItem = document.createElement('div');
            resultItem.className = 'net-buying-result-item';
            resultItem.innerHTML = `
                <div class="net-buying-result-header">
                    <div class="net-buying-result-rank">#${index + 1}</div>
                    <div class="net-buying-result-address" title="点击复制地址">${item.address}</div>
                    <a href="https://gmgn.ai/sol/address/${item.address}" target="_blank" class="net-buying-address-jump-btn" title="查看钱包详情">详情</a>
                </div>
                <div class="net-buying-compact-details">
                    <div class="net-buying-detail-section">
                        <div class="net-buying-section-title">交易信息</div>
                        <div class="net-buying-detail-grid">
                            <span class="net-buying-detail-label">买入额:</span>
                            <span class="net-buying-detail-value net-buying-value-positive">$${formatNumber(item.buyAmount)}</span>
                            <span class="net-buying-detail-label">卖出额:</span>
                            <span class="net-buying-detail-value">$${formatNumber(item.sellAmount)}</span>
                            <span class="net-buying-detail-label">净买入:</span>
                            <span class="net-buying-detail-value net-buying-value-highlight">$${formatNumber(item.netBuying)}</span>
                        </div>
                    </div>
                </div>
            `;
            
            // 添加地址复制功能
            const addressElement = resultItem.querySelector('.net-buying-result-address');
            addressElement.addEventListener('click', () => {
                navigator.clipboard.writeText(item.address).then(() => {
                    addressElement.style.backgroundColor = '#16a34a';
                    addressElement.style.color = 'white';
                    setTimeout(() => {
                        addressElement.style.backgroundColor = '';
                        addressElement.style.color = '';
                    }, 1000);
                });
            });
            
            resultsList.appendChild(resultItem);
        });
        
        // ESC键关闭处理函数
        const escKeyHandler = (e) => {
            if (e.key === 'Escape') {
                closeModal();
            }
        };
        document.addEventListener('keydown', escKeyHandler);
        
        // 关闭弹窗函数
        function closeModal() {
            document.body.removeChild(modal);
            document.removeEventListener('keydown', escKeyHandler);
            // 关闭弹窗后重置数据和按钮状态
            resetData();
            updateButtonStates();
        }
        
        // 绑定导出Excel按钮事件
        const exportBtn = modal.querySelector('#net-buying-export-btn');
        if (exportBtn) {
            exportBtn.addEventListener('click', () => {
                exportToExcel(netBuyingData);
            });
        }
        
        // 绑定关闭按钮事件
        modal.querySelector('.net-buying-modal-close').addEventListener('click', closeModal);
        
        // 点击模态框外部关闭
        modal.addEventListener('click', (e) => {
            if (e.target === modal) {
                closeModal();
            }
        });
    }
    
    // Excel导出功能
    function exportToExcel(data) {
        try {
            const worksheetData = [];
            
            // 添加标题行
            worksheetData.push(['排名', '地址', '买入金额(USD)', '卖出金额(USD)', '净买入(USD)', '交易次数']);
            
            // 添加数据行
            data.forEach((item, index) => {
                worksheetData.push([
                    index + 1,
                    item.address,
                    item.buyAmount.toFixed(2),
                    item.sellAmount.toFixed(2),
                    item.netBuying.toFixed(2),
                    item.totalTrades || 0
                ]);
            });
            
            // 创建工作簿
            const wb = XLSX.utils.book_new();
            const ws = XLSX.utils.aoa_to_sheet(worksheetData);
            
            // 设置列宽
            const colWidths = [
                {wch: 6},   // 排名
                {wch: 45},  // 地址
                {wch: 15},  // 买入金额
                {wch: 15},  // 卖出金额
                {wch: 15},  // 净买入
                {wch: 10}   // 交易次数
            ];
            ws['!cols'] = colWidths;
            
            // 添加工作表到工作簿
            XLSX.utils.book_append_sheet(wb, ws, '净买入地址');
            
            // 生成文件名
            const timestamp = new Date().toISOString().slice(0, 19).replace(/:/g, '-');
            const fileName = `净买入地址_${currentCaAddress ? currentCaAddress.slice(0, 8) : 'data'}_${timestamp}.xlsx`;
            
            // 下载文件
            XLSX.writeFile(wb, fileName);
            
            // 显示成功提示
            const exportBtn = document.querySelector('#net-buying-export-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 resetData() {
        tradeData.clear();
        currentCaAddress = null;
        totalTradesProcessed = 0;
        isRecording = false;
        console.log('[净买入追踪] 数据已重置');
    }
    
    // 更新按钮状态
    function updateButtonStates() {
        const recordBtn = document.getElementById('net-buying-record-btn');
        const calculateBtn = document.getElementById('net-buying-calculate-btn');
        const resetBtn = document.getElementById('net-buying-reset-btn');
        
        if (!recordBtn || !calculateBtn || !resetBtn) return;
        
        if (isRecording) {
            recordBtn.textContent = '录入中';
            recordBtn.className = 'net-buying-btn recording';
            recordBtn.innerHTML = '录入中<span class="recording-dot"></span>';
            calculateBtn.disabled = true;
        } else {
            recordBtn.textContent = '录入';
            recordBtn.className = 'net-buying-btn';
            recordBtn.innerHTML = '录入';
            calculateBtn.disabled = tradeData.size === 0;
        }
    }
    
    // 创建按钮组
    function createButtonGroup() {
        const buttonGroup = document.createElement('div');
        buttonGroup.className = 'net-buying-tracker-buttons';
        buttonGroup.innerHTML = `
            <button id="net-buying-record-btn" class="net-buying-btn">录入</button>
            <button id="net-buying-calculate-btn" class="net-buying-btn" disabled>计算</button>
            <button id="net-buying-reset-btn" class="net-buying-btn">重置</button>
        `;
        
        // 绑定事件
        const recordBtn = buttonGroup.querySelector('#net-buying-record-btn');
        const calculateBtn = buttonGroup.querySelector('#net-buying-calculate-btn');
        const resetBtn = buttonGroup.querySelector('#net-buying-reset-btn');
        
        recordBtn.addEventListener('click', () => {
            isRecording = !isRecording;
            updateButtonStates();
            console.log('[净买入追踪] 录入状态:', isRecording ? '开启' : '关闭');
        });
        
        calculateBtn.addEventListener('click', () => {
            if (tradeData.size > 0) {
                isRecording = false;
                updateButtonStates();
                const netBuyingData = calculateNetBuying();
                createResultModal(netBuyingData);
                console.log('[净买入追踪] 计算结果:', netBuyingData.length, '个净买入地址');
            }
        });
        
        resetBtn.addEventListener('click', () => {
            resetData();
            updateButtonStates();
        });
        
        return buttonGroup;
    }
    
    // 监听DOM变化,插入按钮
    const observer = new MutationObserver(() => {
        const targetTablist = document.querySelector('div[role="tablist"][aria-orientation="horizontal"].chakra-tabs__tablist.css-mm231k');
        if (targetTablist && !document.querySelector('.net-buying-tracker-buttons')) {
            const buttonGroup = createButtonGroup();
            const children = targetTablist.children;
            if (children.length >= 2) {
                // 插入到第二个子元素之前
                targetTablist.insertBefore(buttonGroup, children[1]);
            } else {
                // 如果子元素不足两个,就追加到末尾
                targetTablist.appendChild(buttonGroup);
            }
            console.log('[净买入追踪] 按钮组已插入到chakra-tabs__tablist');
        }
    });
    
    // 初始化
    function initialize() {
        // 立即检查一次
        const targetTablist = document.querySelector('div[role="tablist"][aria-orientation="horizontal"].chakra-tabs__tablist.css-mm231k');
        if (targetTablist && !document.querySelector('.net-buying-tracker-buttons')) {
            const buttonGroup = createButtonGroup();
            const children = targetTablist.children;
            if (children.length >= 2) {
                // 插入到第二个子元素之前
                targetTablist.insertBefore(buttonGroup, children[1]);
            } else {
                // 如果子元素不足两个,就追加到末尾
                targetTablist.appendChild(buttonGroup);
            }
            console.log('[净买入追踪] 按钮组已插入到chakra-tabs__tablist');
        }
        
        // 开始监听DOM变化
        observer.observe(document.body, {
            childList: true,
            subtree: true,
            attributes: false
        });
    }
    
    // 启动 - 只在有效的代币页面启动
    if (isValidTokenPage()) {
        if (document.readyState === 'complete') {
            initialize();
        } else {
            window.addEventListener('DOMContentLoaded', initialize);
        }
        console.log('[净买入追踪] 脚本已加载');
    } else {
        console.log('[净买入追踪] 当前页面不是代币页面,脚本未启动');
    }
})();