// ==UserScript==
// @name GMGN 前排数据统计
// @namespace http://tampermonkey.net/
// @version 2.4
// @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: transparent;
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(9, 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;
// 新增存储当前CA地址
let currentCaAddress = null;
// 存储首次加载的CA地址
let initialCaAddress = null;
// 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;
};
function isTargetApi(url) {
if (typeof url !== 'string') return false;
const isTarget = /vas\/api\/v1\/token_holders\/sol(\/|$|\?)/i.test(url);
if (isTarget) {
// 从URL中提取CA地址
const match = url.match(/vas\/api\/v1\/token_holders\/sol\/([^/?]+)/i);
console.log('匹配的ca:',match)
if (match && match[1]) {
currentCaAddress = match[1];
}
}
return isTarget;
}
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) {
// 首次加载,记录初始数据和CA地址
initialStats = currentStats;
initialCaAddress = currentCaAddress;
isFirstLoad = false;
updateStatsDisplay(currentStats, true);
} else {
// 非首次加载,比较CA地址
const isSameCa = currentCaAddress === initialCaAddress;
updateStatsDisplay(currentStats, !isSameCa);
// 如果CA地址不同,更新初始数据为当前数据
if (!isSameCa) {
initialStats = currentStats;
initialCaAddress = currentCaAddress;
}
}
}).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 sevenDaysInSeconds = 7 * 24 * 60 * 60; // 7天的秒数
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, // 新增:可疑地址
holdingLessThan7Days: 0 // 新增:持仓小于7天
};
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++;
}
// 新增7天持仓统计
if (holder.start_holding_at &&
(currentTime - holder.start_holding_at) < sevenDaysInSeconds) {
stats.holdingLessThan7Days++;
}
});
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 title="持有代币且未卖出任何数量的地址(排除转移代币卖出的地址)">满仓</span>
<span title="当前持仓价值高于买入成本的地址">盈利</span>
<span title="当前持仓价值低于买入成本的地址">亏损</span>
<span title="过去24小时内有交易活动的地址">活跃</span>
<span title="长期持有且很少卖出的地址">钻石</span>
<span title="新钱包">新址</span>
<span title="持仓时间小于7天的地址">7天</span>
<span title="盈利超过10倍的地址">10X</span>
<span title="标记为可疑或异常行为的地址">可疑</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="holdingLessThan7Days">-</span>
<span id="highProfit">-</span>
<span id="suspicious">-</span>
</div>
`;
container.insertAdjacentElement('afterbegin', statsItem);
}
function updateStatsDisplay(currentStats, forceNoArrows) {
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 === 'profitable' ? '#2E8B57' :
(id === 'losing' || id === 'suspicious' ? '#FF1493' :
id === 'holdingLessThan7Days' ? '#00E5EE' : '#e9ecef')}">${value}</strong>`;
// 只有当不是强制不显示箭头且确实有变化时才显示箭头
if (!forceNoArrows && 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();
}
};
// 更新各个统计指标
// 新增7天持仓统计更新
updateStatElement('holdingLessThan7Days', currentStats.holdingLessThan7Days,
initialStats && currentStats.holdingLessThan7Days !== initialStats.holdingLessThan7Days,
initialStats && currentStats.holdingLessThan7Days > initialStats.holdingLessThan7Days);
updateStatElement('fullPosition', currentStats.fullPosition,
initialStats && currentStats.fullPosition !== initialStats.fullPosition,
initialStats && currentStats.fullPosition > initialStats.fullPosition);
updateStatElement('profitable', currentStats.profitable,
initialStats && currentStats.profitable !== initialStats.profitable,
initialStats && 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
});
}
})();