GMGN 前排数据统计

统计GMGN任意代币前排地址的数据,让数字来说话!新增首次记录和涨跌提醒功能

当前为 2025-07-20 提交的版本,查看 最新版本

您需要先安装一个扩展,例如 篡改猴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      2.0
// @description  统计GMGN任意代币前排地址的数据,让数字来说话!新增首次记录和涨跌提醒功能
// @match        https://gmgn.ai/*
// @match        https://www.gmgn.ai/*
// @grant        none
// @run-at       document-start
// @license MIT
// ==/UserScript==

(function() {
    'use strict';

    // 动态添加 CSS
    const style = document.createElement('style');
    style.textContent = `
    .gmgn-stats-container {
        background-color: #000;
        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: 6px;
    }
    .gmgn-stats-header, .gmgn-stats-data {
        display: grid;
        grid-template-columns: repeat(8, 1fr);
        text-align: center;
        gap: 8px;
        font-weight: normal;
    }
    .gmgn-stats-header span {
        color: #ccc;
        font-weight: normal;
    }
    .gmgn-stats-data span {
        color: #00ff00;
        font-weight: normal;
    }
    .gmgn-stats-data span .up-arrow,
    .up-arrow {
        color: green !important;
        margin-left: 2px;
        font-weight: bold;
    }
    .gmgn-stats-data span .down-arrow,
    .down-arrow {
        color: red !important;
        margin-left: 2px;
        font-weight: bold;
    }
`;
    document.head.appendChild(style);

    // 存储拦截到的数据
    let interceptedData = null;
    // 存储首次加载的数据
    let initialStats = null;
    // 标记是否是首次加载
    let isFirstLoad = true;

    // 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;
    };

    // 检测目标 API
    function isTargetApi(url) {
        return typeof url === 'string' &&
            /vas\/api\/v1\/token_holders\/sol(\/|$|\?)/i.test(url);
    }

    // 处理响应数据
    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) {
                    // 首次加载,记录初始数据
                    initialStats = currentStats;
                    isFirstLoad = false;
                    updateStatsDisplay(currentStats, true);
                } else {
                    // 非首次加载,比较变化
                    updateStatsDisplay(currentStats, false);
                }
            }).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 holders = interceptedData.data.list;
        const stats = {
            fullPosition: 0,    // 全仓
            profitable: 0,      // 盈利
            losing: 0,         // 亏损
            active24h: 0,      // 24h活跃
            diamondHands: 0,   // 钻石手
            newAddress: 0,     // 新地址
            highProfit: 0,     // 10x盈利
            suspicious: 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.is_new) stats.newAddress++;
            if (holder.profit_change > 10) 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++;
            }
        });
        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('#gmgn-stats-item')) {
            injectStatsItem(targetContainer);
        }
    });

    function injectStatsItem(container) {
        if (container.querySelector('#gmgn-stats-item')) return;

        const statsItem = document.createElement('div');
        statsItem.id = 'gmgn-stats-item';
        statsItem.className = 'gmgn-stats-container';
        statsItem.innerHTML = `
        <div class="gmgn-stats-header">
            <span>满仓</span>
            <span>盈利</span>
            <span>亏损</span>
            <span>活跃</span>
            <span>钻石</span>
            <span>新址</span>
            <span>10X</span>
            <span>可疑</span>
        </div>
        <div class="gmgn-stats-data">
            <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="highProfit">-</span>
            <span id="suspicious">-</span>
        </div>
    `;
        container.insertAdjacentElement('afterbegin', statsItem);
    }

    // 5. 更新统计显示
    function updateStatsDisplay(currentStats, isInitial) {
        if (!currentStats) return;

        // 确保DOM已存在
        if (!document.getElementById('gmgn-stats-item')) {
            injectStatsItem();
        }

        // 更新所有指标
        const updateStatElement = (id, value, hasChanged, isIncrease) => {
            const element = document.getElementById(id);
            if (!element) return;

            // element.innerHTML = `<strong style="color: ${id === 'losing' || id === 'suspicious' ? '#ff5252' : '#f8f9fa'}">${value}</strong>`;
            element.innerHTML = `<strong style="color: ${id === 'profitable' ? '#2E8B57' :
    (id === 'losing' || id === 'suspicious' ? '#FF1493' : '#f8f9fa')}">${value}</strong>`;

            if (!isInitial && hasChanged) {
                // 非首次加载且有变化,添加涨跌箭头
                const arrow = document.createElement('span');
                arrow.className = isIncrease ? 'up-arrow' : 'down-arrow';
                arrow.textContent = isIncrease ? '▲' : '▼';

                // 移除旧的箭头(如果有)
                const oldArrow = element.querySelector('.up-arrow, .down-arrow');
                if (oldArrow) oldArrow.remove();

                element.appendChild(arrow);
            } else {
                // 没有变化,移除箭头(如果有)
                const oldArrow = element.querySelector('.up-arrow, .down-arrow');
                if (oldArrow) oldArrow.remove();
            }
        };

        if (isInitial) {
            // 首次加载,直接显示数据
            updateStatElement('fullPosition', currentStats.fullPosition);
            updateStatElement('profitable', currentStats.profitable);
            updateStatElement('losing', currentStats.losing);
            updateStatElement('active24h', currentStats.active24h);
            updateStatElement('diamondHands', currentStats.diamondHands);
            updateStatElement('newAddress', currentStats.newAddress);
            updateStatElement('highProfit', currentStats.highProfit);
            updateStatElement('suspicious', currentStats.suspicious);
        } else {
            // 非首次加载,比较变化
            updateStatElement('fullPosition', currentStats.fullPosition,
                currentStats.fullPosition !== initialStats.fullPosition,
                currentStats.fullPosition > initialStats.fullPosition);

            updateStatElement('profitable', currentStats.profitable,
                currentStats.profitable !== initialStats.profitable,
                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);
        }
    }
        /*
// 白色系
'#ffffff'  // 纯白 (white)
'#f8f9fa'  // 浅白 (light white)
'#e9ecef'  // 灰白 (off-white)

// 绿色系(按亮度排序)
'#00ff00'  // 荧光绿 (图片同款)
'#00cc00'  // 亮绿
'#28a745'  // Bootstrap成功绿
'#228b22'  // 森林绿

// 红色系
'#ff0000'  // 纯红 (图片同款)
'#dc3545'  // Bootstrap危险红
'#c00000'  // 深红
'#ff4500'  // 橙红

// 其他常用数据颜色
'#ffa500'  // 橙色 (警告)
'#ffff00'  // 黄色 (注意)
'#007bff'  // 蓝色 (信息)
'#6f42c1'  // 紫色 (特殊标识)

*/

    // 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
        });
    }
})();