#!/usr/bin/env node /** * Query balances from Besu RPC nodes (VMID 2500-2502 by default) * * Note: Only RPC nodes (2500-2502) expose RPC endpoints. * Validators (1000-1004) and sentries (1500-1503) don't have RPC enabled. * * Usage: * RPC_URLS="http://192.168.11.23:8545,http://192.168.11.24:8545,http://192.168.11.25:8545" \ * WETH9_ADDRESS="0x..." \ * WETH10_ADDRESS="0x..." \ * node scripts/besu_balances_106_117.js * * Or use template (defaults to RPC nodes 115-117): * RPC_TEMPLATE="http://192.168.11.{vmid}:8545" \ * node scripts/besu_balances_106_117.js */ import { ethers } from 'ethers'; // Configuration const WALLET_ADDRESS = '0xa55A4B57A91561e9df5a883D4883Bd4b1a7C4882'; // RPC nodes are 2500-2502; validators (1000-1004) and sentries (1500-1503) don't expose RPC const VMID_START = process.env.VMID_START ? parseInt(process.env.VMID_START) : 2500; const VMID_END = process.env.VMID_END ? parseInt(process.env.VMID_END) : 2502; const CONCURRENCY_LIMIT = 4; const REQUEST_TIMEOUT = 15000; // 15 seconds // ERC-20 minimal ABI const ERC20_ABI = [ 'function balanceOf(address owner) view returns (uint256)', 'function decimals() view returns (uint8)', 'function symbol() view returns (string)' ]; // Default token addresses const WETH9_ADDRESS = process.env.WETH9_ADDRESS || '0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2'; const WETH10_ADDRESS = process.env.WETH10_ADDRESS || null; // VMID to IP mapping for better VMID detection const VMID_TO_IP = { '192.168.11.13': 106, '192.168.11.14': 107, '192.168.11.15': 108, '192.168.11.16': 109, '192.168.11.18': 110, '192.168.11.19': 111, '192.168.11.20': 112, '192.168.11.21': 113, '192.168.11.22': 114, '192.168.11.23': 115, '192.168.11.24': 116, '192.168.11.25': 117, }; // Reverse IP to VMID mapping (VMID -> IP) const IP_TO_VMID = { 106: '192.168.11.13', 107: '192.168.11.14', 108: '192.168.11.15', 109: '192.168.11.16', 110: '192.168.11.18', 111: '192.168.11.19', 112: '192.168.11.20', 113: '192.168.11.21', 114: '192.168.11.22', 115: '192.168.11.23', 116: '192.168.11.24', 117: '192.168.11.25', }; // RPC endpoint configuration function getRpcUrls() { if (process.env.RPC_URLS) { return process.env.RPC_URLS.split(',').map(url => url.trim()); } const template = process.env.RPC_TEMPLATE; const urls = []; for (let vmid = VMID_START; vmid <= VMID_END; vmid++) { if (template && template.includes('{vmid}')) { // Use template if provided urls.push(template.replace('{vmid}', vmid.toString())); } else { // Use actual IP from mapping (default behavior) const ip = IP_TO_VMID[vmid]; if (ip) { urls.push(`http://${ip}:8545`); } else { // Fallback to template or direct VMID const fallbackTemplate = template || 'http://192.168.11.{vmid}:8545'; urls.push(fallbackTemplate.replace('{vmid}', vmid.toString())); } } } return urls; } // Get VMID from URL function getVmidFromUrl(url) { // Try IP mapping first const ipMatch = url.match(/(\d+\.\d+\.\d+\.\d+)/); if (ipMatch && VMID_TO_IP[ipMatch[1]]) { return VMID_TO_IP[ipMatch[1]]; } // Fallback to pattern matching const match = url.match(/(?:\.|:)(\d{3})(?::|\/)/); return match ? parseInt(match[1]) : null; } // Format balance with decimals function formatBalance(balance, decimals, symbol) { const formatted = ethers.formatUnits(balance, decimals); return `${formatted} ${symbol}`; } // Query single RPC endpoint async function queryRpc(url, walletAddress) { const vmid = getVmidFromUrl(url) || 'unknown'; const result = { vmid, url, success: false, chainId: null, blockNumber: null, ethBalance: null, ethBalanceWei: null, weth9: null, weth10: null, errors: [] }; try { // Create provider with timeout const provider = new ethers.JsonRpcProvider(url, undefined, { staticNetwork: false, batchMaxCount: 1, batchMaxSize: 250000, staticNetworkCode: false, }); // Set timeout const controller = new AbortController(); const timeoutId = setTimeout(() => controller.abort(), REQUEST_TIMEOUT); try { // Health checks const [chainId, blockNumber] = await Promise.all([ provider.getNetwork().then(n => Number(n.chainId)), provider.getBlockNumber() ]); clearTimeout(timeoutId); result.chainId = chainId; result.blockNumber = blockNumber; // Query ETH balance const ethBalance = await provider.getBalance(walletAddress); result.ethBalanceWei = ethBalance.toString(); result.ethBalance = ethers.formatEther(ethBalance); // Query WETH9 balance try { const weth9Contract = new ethers.Contract(WETH9_ADDRESS, ERC20_ABI, provider); const [balance, decimals, symbol] = await Promise.all([ weth9Contract.balanceOf(walletAddress), weth9Contract.decimals(), weth9Contract.symbol() ]); result.weth9 = { balance: balance.toString(), decimals: Number(decimals), symbol: symbol, formatted: formatBalance(balance, decimals, symbol) }; } catch (err) { result.errors.push(`WETH9: ${err.message}`); } // Query WETH10 balance (if address provided) if (WETH10_ADDRESS) { try { const weth10Contract = new ethers.Contract(WETH10_ADDRESS, ERC20_ABI, provider); const [balance, decimals, symbol] = await Promise.all([ weth10Contract.balanceOf(walletAddress), weth10Contract.decimals(), weth10Contract.symbol() ]); result.weth10 = { balance: balance.toString(), decimals: Number(decimals), symbol: symbol, formatted: formatBalance(balance, decimals, symbol) }; } catch (err) { result.errors.push(`WETH10: ${err.message}`); } } result.success = true; } catch (err) { clearTimeout(timeoutId); throw err; } } catch (err) { result.errors.push(err.message); result.success = false; } return result; } // Process URLs with concurrency limit async function processWithConcurrencyLimit(urls, walletAddress, limit) { const results = []; const executing = []; for (const url of urls) { const promise = queryRpc(url, walletAddress).then(result => { results.push(result); }); executing.push(promise); if (executing.length >= limit) { await Promise.race(executing); executing.splice(executing.findIndex(p => p === promise), 1); } } await Promise.all(executing); return results.sort((a, b) => (a.vmid === 'unknown' ? 999 : a.vmid) - (b.vmid === 'unknown' ? 999 : b.vmid)); } // Format and print results function printResults(results) { let successCount = 0; for (const result of results) { console.log(`VMID: ${result.vmid}`); console.log(`RPC: ${result.url}`); if (result.success) { successCount++; console.log(`chainId: ${result.chainId}`); console.log(`block: ${result.blockNumber}`); console.log(`ETH: ${result.ethBalance} (wei: ${result.ethBalanceWei})`); if (result.weth9) { console.log(`WETH9: ${result.weth9.formatted} (raw: ${result.weth9.balance})`); } else { console.log(`WETH9: error (see errors below)`); } if (WETH10_ADDRESS) { if (result.weth10) { console.log(`WETH10: ${result.weth10.formatted} (raw: ${result.weth10.balance})`); } else { console.log(`WETH10: error (see errors below)`); } } else { console.log(`WETH10: skipped (missing address)`); } if (result.errors.length > 0) { console.log(`Errors: ${result.errors.join('; ')}`); } } else { console.log(`Status: FAILED`); if (result.errors.length > 0) { console.log(`Errors: ${result.errors.join('; ')}`); } } console.log('---'); } return successCount; } // Main execution async function main() { console.log('Querying balances from Besu RPC nodes...'); console.log(`Wallet: ${WALLET_ADDRESS}`); console.log(`WETH9: ${WETH9_ADDRESS}`); console.log(`WETH10: ${WETH10_ADDRESS || 'not set (will skip)'}`); console.log(''); const rpcUrls = getRpcUrls(); console.log(`Checking ${rpcUrls.length} RPC endpoints (concurrency: ${CONCURRENCY_LIMIT})...`); console.log(''); const results = await processWithConcurrencyLimit(rpcUrls, WALLET_ADDRESS, CONCURRENCY_LIMIT); const successCount = printResults(results); console.log(''); console.log(`Summary: ${successCount} out of ${results.length} RPC endpoints succeeded`); // Exit code: 0 if at least one succeeded, 1 if all failed process.exit(successCount > 0 ? 0 : 1); } main().catch(err => { console.error('Fatal error:', err); process.exit(1); });