Files
triangles-api/server.js
T

425 lines
12 KiB
JavaScript

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 = '2KVK2FvLZBW9Hxv4a2Uj3dMRDAXdh4ei6S5tdZ3z2Mme';
// 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': difficulty },
moneysupply: info.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 });
}
});
const server = app.listen(PORT, () => {
console.log(`Triangles REST API running on http://localhost:${PORT}`);
console.log(`Proxying to Triangles RPC at ${RPC_URL}`);
});
// WebSocket support for real-time updates
const { Server } = require('socket.io');
const io = new Server(server, {
cors: { origin: '*' }
});
let lastBlockHash = null;
let connectedClients = 0;
io.on('connection', (socket) => {
connectedClients++;
console.log(`WebSocket client connected (${connectedClients} total)`);
socket.on('disconnect', () => {
connectedClients--;
console.log(`WebSocket client disconnected (${connectedClients} total)`);
});
});
// Poll for new blocks and network stats every 5 seconds
let lastNetworkInfo = null;
async function checkForNewBlocks() {
try {
const info = await rpc('getinfo');
const currentHash = await rpc('getbestblockhash');
if (lastBlockHash && currentHash !== lastBlockHash) {
const block = await rpc('getblock', [currentHash, true]);
console.log(`New block detected: ${block.height} (${currentHash.slice(0, 16)}...)`);
// Get block type
const isPoS = block.flags && block.flags.includes('proof-of-stake');
io.emit('newBlock', {
height: block.height,
hash: currentHash,
time: block.time,
tx: block.tx.length,
size: block.size,
type: isPoS ? 'PoS' : 'PoW',
difficulty: block.difficulty
});
}
lastBlockHash = currentHash;
// Network stats updates (only if changed significantly)
const currentPeers = info.connections;
if (!lastNetworkInfo || Math.abs(currentPeers - lastNetworkInfo.peers) > 0) {
io.emit('networkUpdate', {
peers: currentPeers,
blockHeight: info.blocks,
difficulty: info.difficulty
});
lastNetworkInfo = { peers: currentPeers };
}
} catch (error) {
console.error('Block polling error:', error.message);
}
}
// Start polling (every 5 seconds for faster updates)
setInterval(checkForNewBlocks, 5000);
checkForNewBlocks(); // Initial check