GMGN 前排数据统计

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

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

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

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址