213 lines
6.3 KiB
JavaScript
Executable File
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);
|