你可以在 V2EX 设置中绑定 Solana 地址,然后就可以用 Phantom / Glow 登录 V2EX。
Get V2EX Coin9raUVuzeWUk53co63M4WXLWPWE4Xc6Lpn7RS9dnkpump
如题
// ==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();
})();
![]() |
1
MyBules 50 天前
支持
|
![]() |
2
Oah1zO OP 唔,增加了几个节点和夜间模式的支持..
``` // ==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(); })(); ``` |
![]() |
3
Oah1zO OP |