308 lines
9.8 KiB
JavaScript
Executable File
308 lines
9.8 KiB
JavaScript
Executable File
#!/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);
|
|
});
|
|
|