Add Rich List scanner and API endpoints
This commit is contained in:
@@ -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}`);
|
||||
});
|
||||
Reference in New Issue
Block a user