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}`); });