// ==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">×</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('[净买入追踪] 当前页面不是代币页面,脚本未启动');
}
})();