Greasy Fork

Greasy Fork is available in English.

GMGN交易者数据导出

监听GMGN.ai交易者数据并提供Excel导出功能

您需要先安装一个扩展,例如 篡改猴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.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();

})();