#!/usr/bin/env node /** * Rich List Scanner for Triangles Blockchain * Scans all blocks, tracks address balances, generates rich list */ const axios = require('axios'); const fs = require('fs'); const path = require('path'); const RPC_URL = 'http://127.0.0.1:19112'; const RPC_USER = 'trianglesrpc'; const RPC_PASS = '2KVK2FvLZBW9Hxv4a2Uj3dMRDAXdh4ei6S5tdZ3z2Mme'; const DATA_DIR = path.join(__dirname, 'data'); const STATE_FILE = path.join(DATA_DIR, 'scanner-state.json'); const RICHLIST_FILE = path.join(DATA_DIR, 'richlist.json'); const BALANCES_FILE = path.join(DATA_DIR, 'balances.json'); const BATCH_SIZE = 100; // Process 100 blocks per batch const SAVE_INTERVAL = 500; // Save state every 500 blocks // Ensure data directory exists if (!fs.existsSync(DATA_DIR)) { fs.mkdirSync(DATA_DIR, { recursive: true }); } // RPC helper async function rpc(method, params = []) { try { const response = await axios.post(RPC_URL, { jsonrpc: '1.0', id: 'richlist-scanner', method, params }, { auth: { username: RPC_USER, password: RPC_PASS }, timeout: 30000 }); return response.data.result; } catch (error) { console.error(`RPC error (${method}):`, error.response?.data || error.message); throw error; } } // Load scanner state function loadState() { if (fs.existsSync(STATE_FILE)) { return JSON.parse(fs.readFileSync(STATE_FILE, 'utf8')); } return { lastProcessedBlock: 0, chainHeight: 0, totalAddresses: 0, lastUpdate: new Date().toISOString() }; } // Save scanner state function saveState(state) { fs.writeFileSync(STATE_FILE, JSON.stringify(state, null, 2)); } // Load balances function loadBalances() { if (fs.existsSync(BALANCES_FILE)) { return new Map(Object.entries(JSON.parse(fs.readFileSync(BALANCES_FILE, 'utf8')))); } return new Map(); } // Save balances function saveBalances(balances) { const obj = Object.fromEntries(balances); fs.writeFileSync(BALANCES_FILE, JSON.stringify(obj, null, 2)); } // Generate rich list from balances function generateRichList(balances) { const list = Array.from(balances.entries()) .map(([address, balance]) => ({ address, balance })) .filter(item => item.balance > 0) .sort((a, b) => b.balance - a.balance) .slice(0, 1000); // Top 1000 fs.writeFileSync(RICHLIST_FILE, JSON.stringify(list, null, 2)); console.log(`Rich list generated: ${list.length} addresses`); return list; } // Process a single block async function processBlock(height, balances) { try { const blockHash = await rpc('getblockhash', [height]); const block = await rpc('getblock', [blockHash, true]); for (const txid of block.tx) { // txid might be an object in some RPC versions, extract string if needed const txidStr = typeof txid === 'string' ? txid : txid.txid || txid; const tx = await rpc('getrawtransaction', [txidStr, 1]); // Subtract inputs (spent) if (tx.vin) { for (const input of tx.vin) { if (input.txid && input.vout !== undefined) { try { const prevTx = await rpc('getrawtransaction', [input.txid, 1]); const prevOut = prevTx.vout[input.vout]; if (prevOut && prevOut.scriptPubKey && prevOut.scriptPubKey.addresses) { for (const addr of prevOut.scriptPubKey.addresses) { const current = balances.get(addr) || 0; balances.set(addr, current - prevOut.value); } } } catch (e) { // Coinbase or missing tx, skip } } } } // Add outputs (received) if (tx.vout) { for (const output of tx.vout) { if (output.scriptPubKey && output.scriptPubKey.addresses) { for (const addr of output.scriptPubKey.addresses) { const current = balances.get(addr) || 0; balances.set(addr, current + output.value); } } } } } return true; } catch (error) { console.error(`Error processing block ${height}:`, error.message); return false; } } // Main scanning loop async function scan() { console.log('Starting rich list scanner...'); const state = loadState(); const balances = loadBalances(); console.log(`Resuming from block ${state.lastProcessedBlock}`); console.log(`Current addresses tracked: ${balances.size}`); while (true) { try { // Get current chain height const info = await rpc('getinfo'); state.chainHeight = info.blocks; if (state.lastProcessedBlock >= state.chainHeight) { console.log('Caught up! Waiting for new blocks...'); await new Promise(resolve => setTimeout(resolve, 60000)); // Wait 1 minute continue; } const targetBlock = Math.min(state.lastProcessedBlock + BATCH_SIZE, state.chainHeight); console.log(`Processing blocks ${state.lastProcessedBlock + 1} to ${targetBlock} (${state.chainHeight} total)`); for (let height = state.lastProcessedBlock + 1; height <= targetBlock; height++) { const success = await processBlock(height, balances); if (success) { state.lastProcessedBlock = height; state.totalAddresses = balances.size; state.lastUpdate = new Date().toISOString(); if (height % SAVE_INTERVAL === 0 || height === state.chainHeight) { saveState(state); saveBalances(balances); generateRichList(balances); console.log(`Progress: ${height}/${state.chainHeight} (${(height/state.chainHeight*100).toFixed(2)}%)`); } } else { console.log(`Retrying block ${height} in 5 seconds...`); await new Promise(resolve => setTimeout(resolve, 5000)); break; } } // Small delay between batches await new Promise(resolve => setTimeout(resolve, 1000)); } catch (error) { console.error('Scanner error:', error); await new Promise(resolve => setTimeout(resolve, 10000)); // Wait 10 seconds on error } } } // Handle graceful shutdown process.on('SIGINT', () => { console.log('Shutting down scanner...'); process.exit(0); }); process.on('SIGTERM', () => { console.log('Shutting down scanner...'); process.exit(0); }); // Start scanning scan().catch(console.error);