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); } } // --- Block address enrichment ------------------------------------------------- // Attach the distinct addresses appearing in a block's transaction outputs. // Triangles has a transparent ledger (addressindex/txindex enabled), so these // are public. Privacy in Triangles is network-level (Tor) + encrypted SMSG // messaging, not shielded transactions, so surfacing per-block addresses is fine. async function attachAddresses(block) { if (!block || !Array.isArray(block.tx)) return block; const addrSet = new Set(); for (const entry of block.tx.slice(0, 50)) { // getblock(verbose) gives full tx objects here; fall back to a lookup if // we only got a txid string. let tx = entry; if (typeof entry === 'string') { try { tx = await rpc('getrawtransaction', [entry, 1]); } catch (e) { continue; } } for (const vout of ((tx && tx.vout) || [])) { const addrs = vout.scriptPubKey && vout.scriptPubKey.addresses; if (Array.isArray(addrs)) addrs.forEach((a) => addrSet.add(a)); } } block.addresses = Array.from(addrSet); return block; } // getblock + address enrichment, cached by hash (blocks are immutable). async function getEnrichedBlock(hash) { const key = 'eblock:' + hash; const cached = getCached(key); if (cached) return cached; const block = await rpc('getblock', [hash, true]); await attachAddresses(block); setCache(key, block); return block; } // 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 getEnrichedBlock(req.params.hash); 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 getEnrichedBlock(hash); 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