ai 写了个油猴脚本.在个人页中,把地址、sol、v2ex 余额显示出来了..

2025 年 8 月 4 日
 Oah1zO

如题

// ==UserScript==
// @name         V2EX Solana Balance Checker (Table Style)
// @namespace    http://tampermonkey.net/
// @version      0.5
// @description  Automatically finds the Solana address on a V2EX user's profile and displays balances in a table below the user info.
// @author       Gemini
// @match        https://www.v2ex.com/member/*
// @grant        GM_xmlhttpRequest
// @grant        GM_addStyle
// @connect      api.mainnet-beta.solana.com
// ==/UserScript==

(function() {
    'use strict';

    // 1. The Mint Address for the V2EX token
    const v2exTokenMintAddress = '9raUVuzeWUk53co63M4WXLWPWE4Xc6Lpn7RS9dnkpump';

    // 2. Solana RPC Endpoint
    const solanaRpcEndpoint = 'https://api.mainnet-beta.solana.com';

    /**
     * @function findAddressOnPage
     * @description Scans <script> tags on the page to find and extract the Solana address.
     * @returns {string|null} The found address or null if not found.
     */
    function findAddressOnPage() {
        const scripts = document.querySelectorAll('script');
        for (const script of scripts) {
            if (script.textContent.includes('const address =')) {
                const match = script.textContent.match(/const address = "([1-9A-HJ-NP-Za-km-z]{32,44})";/);
                if (match && match[1]) {
                    console.log('Successfully found SOL address on page:', match[1]);
                    return match[1];
                }
            }
        }
        return null;
    }

    // 3. Automatically extract the SOL address from the page
    const userSolanaAddress = findAddressOnPage();

    if (!userSolanaAddress) {
        console.log('V2EX Solana Balance Checker: Could not find a Solana address on this page.');
        return;
    }

    // 4. Define table styles
    GM_addStyle(`
        .solana-balance-box {
            border-bottom: 1px solid #e2e2e2;
            margin-bottom: 20px;
        }
        .solana-balance-table {
            width: 100%;
            border-collapse: collapse;
            table-layout: fixed;
            margin-bottom: -1px; /* Fix for overlapping borders */
        }
        .solana-balance-table th, .solana-balance-table td {
            padding: 12px;
            text-align: left;
            border-top: 1px solid #e2e2e2;
            font-size: 14px;
            line-height: 1.6;
        }
        .solana-balance-table th {
            font-weight: bold;
            background-color: #f9f9f9;
            color: #555;
        }
        /* --- Column Widths --- */
        .solana-balance-table th:nth-child(1) { width: 60%; } /* Address column */
        .solana-balance-table th:nth-child(2) { width: 20%; } /* SOL Balance column */
        .solana-balance-table th:nth-child(3) { width: 20%; } /* Token Balance column */

        .solana-balance-table td {
            font-family: 'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, Courier, monospace;
            word-wrap: break-word;
            color: #000;
        }
        .solana-balance-table td .loader {
            font-weight: bold;
            color: #999;
        }
    `);

    // 5. Create and insert DOM elements
    // Create a container with the 'box' class
    const container = document.createElement('div');
    container.className = 'box solana-balance-box';

    // Create the table
    const table = document.createElement('table');
    table.className = 'solana-balance-table';

    // Create the header row (Row 1)
    const headerRow = table.insertRow();
    headerRow.innerHTML = '<th>Address</th><th>SOL</th><th>$V2EX</th>';

    // Create the data row (Row 2)
    const dataRow = table.insertRow();
    const addressCell = dataRow.insertCell();
    addressCell.textContent = userSolanaAddress;

    const solBalanceCell = dataRow.insertCell();
    solBalanceCell.innerHTML = '<span class="loader">Loading...</span>';

    const tokenBalanceCell = dataRow.insertCell();
    tokenBalanceCell.innerHTML = '<span class="loader">Loading...</span>';

    container.appendChild(table);

    // Find the main user info box
    const mainInfoBox = document.querySelector('#Main .box');
    if (mainInfoBox) {
        // Insert the new container right after the main info box
        mainInfoBox.parentNode.insertBefore(container, mainInfoBox.nextSibling);
    }


    // 6. Fetch data from the Solana RPC
    function getSolBalance() {
        GM_xmlhttpRequest({
            method: 'POST',
            url: solanaRpcEndpoint,
            headers: { 'Content-Type': 'application/json' },
            data: JSON.stringify({ jsonrpc: '2.0', id: 1, method: 'getBalance', params: [userSolanaAddress] }),
            onload: function(response) {
                try {
                    const data = JSON.parse(response.responseText);
                    if (data.result) {
                        const solBalance = data.result.value / 1_000_000_000; // Lamports to SOL
                        solBalanceCell.textContent = `${solBalance.toFixed(6)} SOL`;
                    } else {
                        solBalanceCell.textContent = 'Failed to fetch';
                        console.error('Failed to fetch SOL balance:', data.error);
                    }
                } catch (e) {
                    solBalanceCell.textContent = 'Parse error';
                }
            },
            onerror: function() {
                solBalanceCell.textContent = 'Request error';
            }
        });
    }

    function getTokenBalance() {
        GM_xmlhttpRequest({
            method: 'POST',
            url: solanaRpcEndpoint,
            headers: { 'Content-Type': 'application/json' },
            data: JSON.stringify({
                jsonrpc: '2.0', id: 1, method: 'getTokenAccountsByOwner',
                params: [userSolanaAddress, { mint: v2exTokenMintAddress }, { encoding: 'jsonParsed' }]
            }),
            onload: function(response) {
                try {
                    const data = JSON.parse(response.responseText);
                    if (data.result && data.result.value.length > 0) {
                        const tokenBalance = data.result.value[0].account.data.parsed.info.tokenAmount.uiAmountString;
                        tokenBalanceCell.textContent = tokenBalance;
                    } else if (data.result) {
                        tokenBalanceCell.textContent = '0';
                    } else {
                        tokenBalanceCell.textContent = 'Failed to fetch';
                        console.error('Failed to fetch token balance:', data.error || 'Token account not found');
                    }
                } catch(e) {
                    tokenBalanceCell.textContent = 'Parse error';
                }
            },
            onerror: function() {
                tokenBalanceCell.textContent = 'Request error';
            }
        });
    }

    // Execute the fetch functions
    getSolBalance();
    getTokenBalance();

})();
945 次点击
所在节点    Solana
3 条回复
FakeJstar
2025 年 8 月 4 日
支持
Oah1zO
2025 年 8 月 4 日
唔,增加了几个节点和夜间模式的支持..
```
// ==UserScript==
// @name V2EX Solana Balance Checker 0.7
// @namespace http://tampermonkey.net/
// @version 0.7
// @description Uses JavaScript to read and apply V2EX's native theme colors for perfect integration. Includes auto RPC-node failover.
// @author Gemini
// @match https://www.v2ex.com/member/*
// @match https://v2ex.com/member/*
// @match https://*.v2ex.com/member/*
// @grant GM_xmlhttpRequest
// @grant GM_addStyle
// @connect api.mainnet-beta.solana.com
// @connect rpc.ankr.com
// @connect solana-mainnet.rpc.extrnode.com
// ==/UserScript==

(function() {
'use strict';

// 1. Configuration
const v2exTokenMintAddress = '9raUVuzeWUk53co63M4WXLWPWE4Xc6Lpn7RS9dnkpump';
const RPC_ENDPOINTS = [ 'https://rpc.ankr.com/solana', 'https://api.mainnet-beta.solana.com', 'https://solana-mainnet.rpc.extrnode.com' ];

function findAddressOnPage() {
const scripts = document.querySelectorAll('script');
for (const script of scripts) {
if (script.textContent.includes('const address =')) {
const match = script.textContent.match(/const address = "([1-9A-HJ-NP-Za-km-z]{32,44})";/);
if (match && match[1]) { return match[1]; }
}
}
return null;
}

const userSolanaAddress = findAddressOnPage();
if (!userSolanaAddress) { return; }

// 2. Add base styles for LAYOUT ONLY.
GM_addStyle(`
.solana-balance-box { background-color: var(--box-background-color); border-bottom: 1px solid var(--box-border-color); margin-bottom: 20px; }
.solana-balance-table { width: 100%; border-collapse: collapse; table-layout: fixed; margin-bottom: -1px; }
.solana-balance-table th, .solana-balance-table td { padding: 12px; text-align: left; border-top: 1px solid var(--box-border-color); font-size: 14px; line-height: 1.6; }
.solana-balance-table th { background-color: var(--box-header-background-color); font-weight: bold; }
.solana-balance-table td { font-family: var(--mono-font); word-wrap: break-word; }
.solana-balance-table th:nth-child(1) { width: 60%; } .solana-balance-table th:nth-child(2) { width: 20%; } .solana-balance-table th:nth-child(3) { width: 20%; }
`);

// 3. Create and insert DOM elements
const container = document.createElement('div');
container.className = 'solana-balance-box';
const table = document.createElement('table');
table.className = 'solana-balance-table';
const headerRow = table.insertRow();
headerRow.innerHTML = '<th>Address</th><th>SOL</th><th>$V2EX</th>';
const dataRow = table.insertRow();
const addressCell = dataRow.insertCell();
const solBalanceCell = dataRow.insertCell();
const tokenBalanceCell = dataRow.insertCell();
addressCell.textContent = userSolanaAddress;
solBalanceCell.textContent = 'Loading...';
tokenBalanceCell.textContent = 'Loading...';
container.appendChild(table);
const mainInfoBox = document.querySelector('#Main .box');
if (mainInfoBox) {
mainInfoBox.parentNode.insertBefore(container, mainInfoBox.nextSibling);
}

// 4. JavaScript function to READ and APPLY native text colors for BOTH headers and data
function updateTextColorsForTheme() {
// Read the actual color values V2EX is currently using
const nativeHeaderTextColor = getComputedStyle(document.body).getPropertyValue('--box-header-text-color').trim();
const nativeTextColor = getComputedStyle(document.body).getPropertyValue('--box-foreground-color').trim();
const nativeFadeColor = getComputedStyle(document.body).getPropertyValue('--color-fade').trim();

// --- FIX: Apply color to table headers (th) ---
const headers = table.querySelectorAll('th');
for (const header of headers) {
// By changing this to nativeTextColor, the header color will match the data cell color.
header.style.setProperty('color', nativeTextColor, 'important');
}

// --- Apply color to table data (td) ---
const cells = [addressCell, solBalanceCell, tokenBalanceCell];
for (const cell of cells) {
if (cell.textContent === 'Loading...' || cell.textContent === 'Error') {
cell.style.setProperty('color', nativeFadeColor, 'important');
} else {
cell.style.setProperty('color', nativeTextColor, 'important');
}
}
}

// 5. Observer to detect theme changes in real-time
const themeObserver = new MutationObserver(() => updateTextColorsForTheme());
themeObserver.observe(document.documentElement, { attributes: true, attributeFilter: ['class'] });
updateTextColorsForTheme();

// 6. Fetch data
function makeRpcRequest(requestPayload, onSuccess, onFailure) {
let endpointIndex = 0;
function tryNextEndpoint() {
if (endpointIndex >= RPC_ENDPOINTS.length) { if (onFailure) onFailure(); return; }
const currentEndpoint = RPC_ENDPOINTS[endpointIndex++];
GM_xmlhttpRequest({
method: 'POST', url: currentEndpoint, headers: { 'Content-Type': 'application/json' },
data: JSON.stringify(requestPayload), timeout: 8000,
onload: function(response) {
try { const data = JSON.parse(response.responseText); if (data.error) { tryNextEndpoint(); } else { onSuccess(data); } }
catch (e) { tryNextEndpoint(); }
},
onerror: tryNextEndpoint, ontimeout: tryNextEndpoint
});
}
tryNextEndpoint();
}

function getSolBalance() {
makeRpcRequest({ jsonrpc: '2.0', id: 1, method: 'getBalance', params: [userSolanaAddress] },
(data) => { solBalanceCell.textContent = `${(data.result.value / 1e9).toFixed(6)}`; updateTextColorsForTheme(); },
() => { solBalanceCell.textContent = 'Error'; updateTextColorsForTheme(); }
);
}
function getTokenBalance() {
makeRpcRequest({ jsonrpc: '2.0', id: 1, method: 'getTokenAccountsByOwner', params: [userSolanaAddress, { mint: v2exTokenMintAddress }, { encoding: 'jsonParsed' }] },
(data) => { tokenBalanceCell.textContent = data.result.value.length > 0 ? data.result.value[0].account.data.parsed.info.tokenAmount.uiAmountString : '0'; updateTextColorsForTheme(); },
() => { tokenBalanceCell.textContent = 'Error'; updateTextColorsForTheme(); }
);
}

getSolBalance();
getTokenBalance();

})();
```
Oah1zO
2025 年 8 月 9 日
又更新了一个版本..效果如下图吧..

安装: https://greasyfork.org/zh-CN/scripts/545123-v2ex-solana-balance-checker

这是一个专为移动设备优化的页面(即为了让你能够在 Google 搜索结果里秒开这个页面),如果你希望参与 V2EX 社区的讨论,你可以继续到 V2EX 上打开本讨论主题的完整版本。

https://www.v2ex.com/t/1149724

V2EX 是创意工作者们的社区,是一个分享自己正在做的有趣事物、交流想法,可以遇见新朋友甚至新机会的地方。

V2EX is a community of developers, designers and creative people.

© 2021 V2EX