Files
triangles-api/rich-list-scanner.js
T

213 lines
6.3 KiB
JavaScript
Executable File

#!/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);