Add Rich List scanner and API endpoints

This commit is contained in:
Krystie
2026-03-24 10:24:44 +01:00
commit 30d22c6d61
795 changed files with 105653 additions and 0 deletions
+358
View File
@@ -0,0 +1,358 @@
const express = require('express');
const cors = require('cors');
const axios = require('axios');
const app = express();
const PORT = process.env.PORT || 3051;
const RPC_URL = 'http://127.0.0.1:19112';
const RPC_USER = 'trianglesrpc';
const RPC_PASS = 'rpc$(openssl rand -hex 16)';
// Cache for expensive RPC calls
const cache = {};
const CACHE_TTL = 15000; // 15 seconds
const inflight = {}; // dedup concurrent requests
function getCached(key) {
const entry = cache[key];
if (entry && (Date.now() - entry.timestamp) < CACHE_TTL) return entry.data;
return null;
}
function setCache(key, data) {
cache[key] = { data, timestamp: Date.now() };
}
// Dedup wrapper: concurrent identical requests share one RPC call
async function cachedRpc(key, fn) {
const cached = getCached(key);
if (cached) return cached;
if (inflight[key]) return inflight[key];
inflight[key] = fn().then(data => {
setCache(key, data);
delete inflight[key];
return data;
}).catch(err => {
delete inflight[key];
throw err;
});
return inflight[key];
}
app.use(cors());
app.use(express.json());
// RPC helper
async function rpc(method, params = []) {
try {
const response = await axios.post(RPC_URL, {
jsonrpc: '1.0',
id: 'triangles-api',
method,
params
}, {
auth: { username: RPC_USER, password: RPC_PASS }
});
return response.data.result;
} catch (error) {
console.error(`RPC error (${method}):`, error.response?.data || error.message);
throw new Error(error.response?.data?.error?.message || error.message);
}
}
// REST endpoint: /rest/chaininfo (with cache for expensive gettxoutsetinfo)
app.get('/rest/chaininfo', async (req, res) => {
try {
const now = Date.now();
// Check if cached supply is still valid
let supply;
if (cache.supply && cache.supply.data && (now - cache.supply.timestamp) < CACHE_TTL) {
supply = cache.supply.data;
} else {
supply = await rpc('gettxoutsetinfo');
if (!cache.supply) cache.supply = {};
cache.supply.data = supply;
cache.supply.timestamp = now;
}
const [info, difficulty, bestblockhash] = await Promise.all([
rpc('getinfo'),
rpc('getdifficulty'),
rpc('getbestblockhash')
]);
res.json({
chain: info.testnet ? 'test' : 'main',
blocks: info.blocks,
bestblockhash,
difficulty: typeof difficulty === 'object' ? difficulty : { 'proof-of-work': difficulty, 'proof-of-stake': 0 },
moneysupply: supply.total_amount || 0
});
} catch (error) {
res.status(500).json({ error: error.message });
}
});
// REST endpoint: /rest/block/:hash
app.get('/rest/block/:hash', async (req, res) => {
try {
const block = await rpc('getblock', [req.params.hash, true]); // true = verbose with tx data
res.json(block);
} catch (error) {
res.status(404).json({ error: 'Block not found' });
}
});
// REST endpoint: /rest/blockbyheight/:height
app.get('/rest/blockbyheight/:height', async (req, res) => {
try {
const hash = await rpc('getblockhash', [parseInt(req.params.height)]);
const block = await rpc('getblock', [hash, true]); // true = verbose
res.json(block);
} catch (error) {
res.status(404).json({ error: 'Block not found' });
}
});
// REST endpoint: /rest/blockhashbyheight/:height
app.get('/rest/blockhashbyheight/:height', async (req, res) => {
try {
const hash = await rpc('getblockhash', [parseInt(req.params.height)]);
res.json({ blockhash: hash });
} catch (error) {
res.status(404).json({ error: 'Block not found' });
}
});
// REST endpoint: /rest/blockheader/:hash
app.get('/rest/blockheader/:hash', async (req, res) => {
try {
const header = await rpc('getblockheader', [req.params.hash]);
res.json(header);
} catch (error) {
res.status(404).json({ error: 'Block header not found' });
}
});
// REST endpoint: /rest/tx/:txid
app.get('/rest/tx/:txid', async (req, res) => {
try {
const tx = await rpc('getrawtransaction', [req.params.txid, 1]);
res.json(tx);
} catch (error) {
res.status(404).json({ error: 'Transaction not found' });
}
});
// REST endpoint: /rest/mempool
app.get('/rest/mempool', async (req, res) => {
try {
const mempool = await rpc('getrawmempool');
res.json(mempool);
} catch (error) {
res.status(500).json({ error: error.message });
}
});
// REST endpoint: /rest/staking
app.get('/rest/staking', async (req, res) => {
try {
const staking = await rpc('getstakinginfo');
res.json(staking);
} catch (error) {
res.status(500).json({ error: error.message });
}
});
// REST endpoint: /rest/mining
app.get('/rest/mining', async (req, res) => {
try {
const mining = await rpc('getmininginfo');
res.json(mining);
} catch (error) {
res.status(500).json({ error: error.message });
}
});
// REST endpoint: /rest/network
app.get('/rest/network', async (req, res) => {
try {
const info = await rpc('getnetworkinfo');
res.json(info);
} catch (error) {
res.status(500).json({ error: error.message });
}
});
// REST endpoint: /rest/peers
app.get('/rest/peers', async (req, res) => {
try {
const peers = await rpc('getpeerinfo');
res.json(peers);
} catch (error) {
res.status(500).json({ error: error.message });
}
});
// REST endpoint: /rest/difficulty
app.get('/rest/difficulty', async (req, res) => {
try {
const difficulty = await rpc('getdifficulty');
res.json(typeof difficulty === 'object' ? difficulty : { 'proof-of-work': difficulty });
} catch (error) {
res.status(500).json({ error: error.message });
}
});
// REST endpoint: /rest/supply (with cache)
app.get('/rest/supply', async (req, res) => {
try {
const now = Date.now();
let supply;
if (cache.supply.data && (now - cache.supply.timestamp) < CACHE_TTL) {
supply = cache.supply.data;
} else {
supply = await rpc('gettxoutsetinfo');
cache.supply.data = supply;
cache.supply.timestamp = now;
}
res.json({
total: supply.total_amount || 0,
height: supply.height || 0
});
} catch (error) {
res.status(500).json({ error: error.message });
}
});
// REST endpoint: /rest/subsidy
app.get('/rest/subsidy', async (req, res) => {
try {
const mining = await rpc('getmininginfo');
res.json(mining.blockvalue || 0);
} catch (error) {
res.status(500).json({ error: error.message });
}
});
// REST endpoint: /rest/estimatefee
app.get('/rest/estimatefee', async (req, res) => {
try {
const fee = await rpc('estimatefee', [6]); // 6 blocks
res.json(fee);
} catch (error) {
res.status(500).json({ error: error.message });
}
});
// Address endpoints (requires addressindex=1)
app.get('/rest/address/:addr/balance', async (req, res) => {
try {
const balance = await rpc('getaddressbalance', [{ addresses: [req.params.addr] }]);
res.json(balance);
} catch (error) {
res.status(500).json({ error: 'Address index not enabled' });
}
});
app.get('/rest/address/:addr/utxos', async (req, res) => {
try {
const utxos = await rpc('getaddressutxos', [{ addresses: [req.params.addr] }]);
res.json(utxos);
} catch (error) {
res.status(500).json({ error: 'Address index not enabled' });
}
});
app.get('/rest/address/:addr/txids', async (req, res) => {
try {
const start = req.query.start ? parseInt(req.query.start) : undefined;
const end = req.query.end ? parseInt(req.query.end) : undefined;
const params = { addresses: [req.params.addr] };
if (start !== undefined) params.start = start;
if (end !== undefined) params.end = end;
const txids = await rpc('getaddresstxids', [params]);
res.json(txids);
} catch (error) {
res.status(500).json({ error: 'Address index not enabled' });
}
});
// REST endpoint: /rest/validate/:addr
app.get('/rest/validate/:addr', async (req, res) => {
try {
const validation = await rpc('validateaddress', [req.params.addr]);
res.json(validation);
} catch (error) {
res.status(500).json({ error: error.message });
}
});
// REST endpoint: /rest/richlist
const fs = require('fs');
const path = require('path');
app.get('/rest/richlist', async (req, res) => {
try {
const limit = parseInt(req.query.limit) || 100;
const richlistPath = path.join(__dirname, 'data', 'richlist.json');
if (!fs.existsSync(richlistPath)) {
return res.json({
status: 'scanning',
message: 'Rich list scanner is still processing blockchain data',
addresses: []
});
}
const richlist = JSON.parse(fs.readFileSync(richlistPath, 'utf8'));
res.json({
status: 'ready',
total: richlist.length,
addresses: richlist.slice(0, limit)
});
} catch (error) {
res.status(500).json({ error: error.message });
}
});
// REST endpoint: /rest/richlist/status
app.get('/rest/richlist/status', async (req, res) => {
try {
const statePath = path.join(__dirname, 'data', 'scanner-state.json');
if (!fs.existsSync(statePath)) {
return res.json({
status: 'not_started',
message: 'Rich list scanner has not been started yet'
});
}
const state = JSON.parse(fs.readFileSync(statePath, 'utf8'));
const progress = state.chainHeight > 0 ? (state.lastProcessedBlock / state.chainHeight * 100).toFixed(2) : 0;
res.json({
status: state.lastProcessedBlock >= state.chainHeight ? 'synced' : 'scanning',
lastProcessedBlock: state.lastProcessedBlock,
chainHeight: state.chainHeight,
progress: parseFloat(progress),
totalAddresses: state.totalAddresses,
lastUpdate: state.lastUpdate
});
} catch (error) {
res.status(500).json({ error: error.message });
}
});
app.listen(PORT, () => {
console.log(`Triangles REST API running on http://localhost:${PORT}`);
console.log(`Proxying to Triangles RPC at ${RPC_URL}`);
});