如题
// ==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();
})();
这是一个专为移动设备优化的页面(即为了让你能够在 Google 搜索结果里秒开这个页面),如果你希望参与 V2EX 社区的讨论,你可以继续到 V2EX 上打开本讨论主题的完整版本。
V2EX 是创意工作者们的社区,是一个分享自己正在做的有趣事物、交流想法,可以遇见新朋友甚至新机会的地方。
V2EX is a community of developers, designers and creative people.