NOTICE: By continued use of this site you understand and agree to the binding Terms of Service and Privacy Policy.
// ==UserScript==
// @name pummmmPro Gmgn
// @namespace http://tampermonkey.net/
// @version 0.2
// @description PUMMMMMPro高级工具提示增强
// @author You
// @match https://gmgn.ai/?*
// @match https://gmgn.ai/sol/token/*
// @grant GM_xmlhttpRequest
// @grant GM_addStyle
// @grant GM_setValue
// @grant GM_getValue
// @grant GM_registerMenuCommand
// @connect pummmmpro.com
// @license MIT
// ==/UserScript==
(function() {
'use strict';
GM_addStyle(`
.custom-tooltip {
display: none;
position: absolute;
left: 100%;
top: 100%;
margin-left: -50px;
margin-top: -20px;
background-color: #1e1e2d;
border: 1px solid #2d2d3d;
border-radius: 12px;
box-shadow: 0 10px 25px rgba(0,0,0,0.3);
z-index: 1000;
width: 1400px;
max-height: 600px;
overflow: auto;
font-family: 'Segoe UI', 'PingFang SC', 'Microsoft YaHei', sans-serif;
color: #e0e0e0;
padding: 20px;
transition: opacity 0.2s ease;
}
.custom-tooltip table {
width: 100%;
border-collapse: separate;
border-spacing: 0;
font-size: 13px;
}
.custom-tooltip tr {
line-height: 0.3;
}
.custom-tooltip th {
background-color: #2a2a3a;
color: #a0a0c0;
padding: 12px 15px;
text-align: left;
font-weight: 500;
top: 0;
z-index: 10;
}
.custom-tooltip td {
padding: 10px 15px;
border-bottom: 1px solid #2d2d3d;
}
.custom-tooltip tr:hover td {
background-color: #2a2a3a;
}
.custom-tooltip .address-link {
color: #4a8cff;
text-decoration: none;
transition: color 0.2s;
font-family: 'Consolas', monospace;
}
.custom-tooltip .address-link:hover {
color: #6ba2ff;
text-decoration: underline;
}
.custom-tooltip .percentage-cell {
color: #4caf50;
font-weight: 500;
}
.custom-tooltip .hide-red {
color: #ff6b6b;
}
.custom-tooltip .hide-green {
color: #4caf50;
}
.custom-tooltip .tooltip-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 15px;
padding-bottom: 10px;
border-bottom: 1px solid #2d2d3d;
}
.custom-tooltip .tooltip-title {
font-size: 16px;
font-weight: 600;
color: #ffffff;
}
.custom-tooltip .tooltip-time {
font-size: 12px;
color: #a0a0c0;
}
@keyframes spin {
to { transform: rotate(360deg); }
}
.tooltip-loading {
display: flex;
justify-content: center;
align-items: center;
height: 100px;
}
.spinner {
width: 40px;
height: 40px;
border: 4px solid rgba(255,255,255,0.1);
border-radius: 50%;
border-top-color: #4a6bff;
animation: spin 1s linear infinite;
}
.token-input {
width: 100%;
padding: 10px;
margin: 10px 0;
background-color: #2a2a3a;
border: 1px solid #3d3d4d;
border-radius: 6px;
color: #ffffff;
font-family: 'Segoe UI', sans-serif;
}
.token-dialog {
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
background-color: #1e1e2d;
border: 1px solid #2d2d3d;
border-radius: 12px;
padding: 20px;
z-index: 9999;
width: 400px;
box-shadow: 0 10px 25px rgba(0,0,0,0.3);
}
.token-dialog h3 {
margin-top: 0;
color: #ffffff;
}
.token-input {
width: 150px;
height:30px;
padding: 10px;
margin: 10px 0;
background-color: #2a2a3a;
border: 1px solid #3d3d4d;
border-radius: 6px;
color: #ffffff;
font-family: 'Segoe UI', sans-serif;
}
.token-submit {
background-color: #4a6bff;
color: white;
border: none;
padding: 10px 15px;
border-radius: 6px;
cursor: pointer;
font-weight: 500;
height:44px;
transition: background-color 0.2s;
}
.token-submit:hover {
background-color: #5a7bff;
}
`);
let USER_TOKEN = GM_getValue('pummmmpro_token', null);
function showTokenDialog() {
const dialog = document.createElement('div');
dialog.className = 'token-dialog';
dialog.innerHTML = `
<h3>PUMMMMPro</h3>
<input type="text" class="token-input" placeholder="Enter your Token">
<button class="token-submit">Save</button>
`;
document.body.appendChild(dialog);
const input = dialog.querySelector('.token-input');
const submitBtn = dialog.querySelector('.token-submit');
submitBtn.addEventListener('click', () => {
const token = input.value.trim();
if (!token) {
dialog.remove();
return;
}
if (token.length < 8) {
dialog.remove();
return;
}
USER_TOKEN = token;
dialog.remove();
});
}
// 初始路由检测
checkAndExecute();
// 监听后续路由变化(SPA 跳转)
window.addEventListener('popstate', checkAndExecute);
const observer = new MutationObserver(checkAndExecute);
observer.observe(document.body, { childList: true, subtree: true });
function checkAndExecute() {
if (window.location.href.match(/https:\/\/gmgn\.ai\/sol\/token\/.+/)) {
executeTokenPageScript();
} else if (window.location.href.match(/https:\/\/gmgn\.ai\/\?.*/)) {
executeScript();
}
}
function executeScript() {
const parentSelector = '.transition-colors.flex-shrink-0';
const parents = document.querySelectorAll(parentSelector);
parents.forEach(parent => {
const textElements = parent.querySelectorAll('.text-\\[14px\\]');
textElements.forEach(el => {
if (el.nextElementSibling?.classList.contains('new-span')) {
return;
}
const newSpan = document.createElement('span');
newSpan.className = 'new-span';
Object.assign(newSpan.style, {
position: 'absolute',
color: '#e96061',
marginTop: '0px',
marginLeft: '200px'
});
newSpan.textContent = 'Query';
el.parentNode.insertBefore(newSpan, el.nextSibling);
addClickTooltip(newSpan, el.textContent);
});
});
}
function executeTokenPageScript() {
let el1 = document.querySelector('.flex.items-center.cursor-pointer.gap-x-4px.text-text-300.text-sm.font-normal.group');
if (!el1) {
el1 = document.querySelector('.flex.items-center.cursor-pointer.gap-x-4px.text-text-300.text-sm.font-normal.group');
if(!el1){
return;
}
}
const el = document.querySelector('.new-buttonTn');
if (el) {
return;
}
const dragButton = document.createElement('button');
dragButton.className = 'new-buttonTn';
dragButton.textContent = 'Query';
const savedPosition = JSON.parse(GM_getValue('dragButtonPosition', null)) || {
left: '50px',
top: '50px'
};
Object.assign(dragButton.style, {
position: 'fixed',
left: savedPosition.left,
top: savedPosition.top,
padding: '10px 20px',
backgroundColor: '#4CAF50',
color: 'white',
border: 'none',
borderRadius: '5px',
cursor: 'move',
zIndex: '9999',
userSelect: 'none'
});
el1.appendChild(dragButton);
let isDragging = false;
let offsetX, offsetY;
dragButton.addEventListener('mousedown', (e) => {
isDragging = true;
offsetX = e.clientX - dragButton.getBoundingClientRect().left;
offsetY = e.clientY - dragButton.getBoundingClientRect().top;
e.preventDefault();
});
document.addEventListener('mousemove', (e) => {
if (!isDragging) return;
let newX = e.clientX - offsetX;
let newY = e.clientY - offsetY;
// 限制按钮不超出视口
newX = Math.max(0, Math.min(newX, window.innerWidth - dragButton.offsetWidth));
newY = Math.max(0, Math.min(newY, window.innerHeight - dragButton.offsetHeight));
dragButton.style.left = `${newX}px`;
dragButton.style.top = `${newY}px`;
});
document.addEventListener('mouseup', () => {
if (isDragging) {
isDragging = false;
dragButton.style.cursor = 'move';
// 保存位置到localStorage
GM_setValue('dragButtonPosition', JSON.stringify({
left: dragButton.style.left,
top: dragButton.style.top
}));
}
});
// 在这里调用你想要触发的函数
addClickTooltip(dragButton, el1.textContent.trim());
}
function addClickTooltip(trigger, spanText) {
const tooltip = document.createElement('div');
tooltip.className = 'custom-tooltip';
document.body.appendChild(tooltip);
trigger.addEventListener('click', (e) => {
e.preventDefault();
e.stopPropagation();
if (!USER_TOKEN || typeof USER_TOKEN !== 'string' || USER_TOKEN.trim() === "") {
showTokenDialog();
return;
}else{
GM_setValue('pummmmpro_token', USER_TOKEN);
}
e.stopPropagation();
tooltip.style.display = 'block';tooltip.style.position = 'fixed';tooltip.style.left = '50%';
tooltip.style.top = '50%';tooltip.style.transform = 'translate(-50%, -50%)';tooltip.style.opacity = '0';
setTimeout(() => { tooltip.style.opacity = '1'; }, 10);
tooltip.innerHTML = `<div class="tooltip-loading"><div class="spinner"></div></div> `;
GM_xmlhttpRequest({
method: "GET",
url: `https://pummmmpro.com/api/analysisVip/${USER_TOKEN}/${spanText}`,
headers: {
"Accept": "application/json"
},
onload: function(response) {
if(response.status === 200){
const responseJson = JSON.parse(response.responseText);
const data = responseJson.tableData;
const ca = responseJson.ca;
tooltip.innerHTML = `
<div class="tooltip-header">
<div class="tooltip-title">PummmmmPro</div>
<div class="tooltip-title"><a href="https://x.com/search?q=${ca}&f=live" target="_blank" class="address-link" title="Hash"> 推特 </a></div>
<div class="tooltip-title"><a href="https://x3.pro/trending-tweets/coin-search?address=${ca}" target="_blank" class="address-link" title="Hash"> X3推特 </a></div>
<div class="tooltip-title"><button class="exit-button">退出账户</button></div>
</div>
<table>
<thead>
<tr>
<th>地址</th>
<th>余额</th>
<th>钱包时长</th>
<th>占比</th>
<th>来源</th>
<th>盈利</th>
<th>买/卖</th>
<th>买入时差</th>
<th>操作时差</th>
<th>持币顺序</th>
<th>币均时</th>
<th>时币数</th>
<th>GasTips</th>
<th>筹码类型</th>
<th>Hash</th>
</tr>
</thead>
<tbody>
${data.map(item => {
const fullAddress = item.owner || 'N/A';
const displayAddress = fullAddress.length > 8 ? `${fullAddress.substring(0, 6)}...${fullAddress.substring(fullAddress.length - 4)}` : fullAddress;
const fullSour = item.solSour || '';
const displaySour = fullSour.length > 20 ? `${fullSour.substring(0, 3)}...${fullSour.substring(fullSour.length - 2)}` : fullSour;
const profitChangeInt = item.profitChange > 0 ? `${item.profitChange }%` : '';
const walletAgeDays = item.initTime ? Math.floor((Date.now() - item.initTime * 1000) / (1000 * 60 * 60 * 24)): '0';
const tokenOrderClass = item.tokenOrderHide === 1 ? 'hide-red' : '';
const tipsClass = item.tipsHide === 1 ? 'hide-red' : '';
const activityTimeClass = item.activityTimeHide === 1 ? 'hide-red' : '';
const initDayClass = walletAgeDays < 2 ? 'hide-red' : '';
const outsideClass = item.outside ? 'hide-green' : '';
const activityTimeFormat = item.activityTime ? new Date(item.activityTime * 1000).toLocaleTimeString('en-US', { hour12: false, hour: '2-digit',minute: '2-digit',second: '2-digit'}) : '-';
//const lastActivityTimeFormat = item.lastActivityTime ? new Date(item.lastActivityTime * 1000).toLocaleTimeString('en-US', { hour12: false, hour: '2-digit',minute: '2-digit',second: '2-digit'}) : '-';
const currentTime = Date.now();
const activityTime = item.activityTime ? item.activityTime * 1000 : 0;
const timeDiffMinutes = activityTime ? Math.floor((currentTime - activityTime) / (1000 * 60)) : '-';
const lastActivityTime = item.lastActivityTime ? item.lastActivityTime * 1000 : 0;
const lastTimeDiffMinutes = lastActivityTime ? Math.floor((currentTime - lastActivityTime) / (1000 * 60)) : '-';
return `
<tr>
<td><a href="https://gmgn.ai/sol/address/${fullAddress}" target="_blank" class="address-link" title="${fullAddress}"> ${displayAddress}</a></td>
<td>${item.amount || '0'}</td>
<td class="${initDayClass}">${walletAgeDays}</td>
<td class="percentage-cell">${item.ratio || '-'}%</td>
<td>${fullSour.length > 10 ? `<a href="https://gmgn.ai/sol/address/${fullSour}" target="_blank" class="address-link" title="${fullSour}">${displaySour}</a>`: displaySour }</td>
<td class="percentage-cell">${profitChangeInt}</td>
<td>${item.buyTxCountCur}/${item.sellTxCountCur}</td>
<td class="${activityTimeClass}">${activityTimeFormat} (${timeDiffMinutes})</td>
<td>${lastTimeDiffMinutes}</td>
<td class="${tokenOrderClass}">${item.tokenOrder || '-'}</td>
<td>${item.holdTime || '-'}</td>
<td>${item.tokenNum}</td>
<td class="${tipsClass}">${item.tips || '-'}</td>
<td><span>${item.within || ''}</span><span class="${outsideClass}">${item.outside || ''}</span></td>
<td> <a href="https://solscan.io/tx/${item.hash}" target="_blank" class="address-link" title="Hash"> Hash </a></td>
</tr>
`;
}).join('')}
</tbody>
</table>
`;
}else{
tooltip.innerHTML = `
<div class="tooltip-header">
<div class="tooltip-title">PummmmmPro</div>
<div class="tooltip-title"><button class="exit-button">退出账户</button></div>
</div>
<div style=" color: #ff6b6b;padding: 20px;text-align: center;font-size: 14px;">
<div style="font-size: 24px; margin-bottom: 10px;">⚠️</div>
<div style="font-weight: 500; margin-bottom: 5px;">Data loading failed</div>
<div style="color: #a0a0c0; font-size: 12px;">使用次数已用完,请联系管理员。The usage has been exhausted. Please contact the administrator.</div>
</div>
`;
}
},
onerror: function(error) {
tooltip.innerHTML = `
<div class="tooltip-header">
<div class="tooltip-title">PummmmmPro</div>
<div class="tooltip-title"><button class="exit-button">退出账户</button></div>
</div>
<div style=" color: #ff6b6b;padding: 20px;text-align: center;font-size: 14px;">
<div style="font-size: 24px; margin-bottom: 10px;">⚠️</div>
<div style="font-weight: 500; margin-bottom: 5px;">Data loading failed</div>
<div style="color: #a0a0c0; font-size: 12px;">${error.message}</div>
</div>
`;
}
});
});
const hideTooltip = () => {
tooltip.style.opacity = '0';
setTimeout(() => { tooltip.style.display = 'none'; }, 300);
};
const exit = () => {
tooltip.style.display = "none";
USER_TOKEN = "";
GM_setValue('pummmmpro_token', "");
};
document.addEventListener('click', (e) => {
if (!tooltip.contains(e.target)) {
hideTooltip();
return;
}
if (e.target.classList.contains('exit-button')) {
exit();
}
});
}
})();