status-api/bot.js

149 lines
5.8 KiB
JavaScript
Raw Permalink Normal View History

#!/usr/bin/env node
/**
* Lethean Chain Status Bot
*
* Lightweight HTTP API that returns chain status in formats suitable for
* embedding in Telegram/Discord bots, status pages, or shell scripts.
*
* Endpoints:
* GET / human-readable status
* GET /json full JSON status
* GET /height just the height (plain text)
* GET /badge shields.io compatible badge JSON
* GET /check/:h check if a given height has been reached
*
* Usage:
* node bot.js
* curl http://localhost:8101/height → "10969"
* curl http://localhost:8101/json → {"height":10969,"hf":[0,1,2,3],...}
* curl http://localhost:8101/check/11000 → {"reached":false,"current":10969,"remaining":31}
* curl http://localhost:8101/badge → shields.io badge JSON
*/
const http = require('http');
const PORT = process.env.PORT || 8101;
const DAEMON = process.env.DAEMON_URL || 'http://127.0.0.1:46941';
async function getChainInfo() {
try {
const resp = await fetch(`${DAEMON}/json_rpc`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ jsonrpc: '2.0', id: '0', method: 'getinfo' }),
});
const data = await resp.json();
const r = data.result;
return {
height: r.height,
hf: r.is_hardfok_active.map((v, i) => v ? i : null).filter(v => v !== null),
difficulty: r.pow_difficulty,
aliases: r.alias_count,
txPool: r.tx_pool_size,
posAllowed: r.pos_allowed,
synced: r.daemon_network_state === 2,
};
} catch {
return null;
}
}
const server = http.createServer(async (req, res) => {
const url = new URL(req.url, `http://localhost:${PORT}`);
res.setHeader('Access-Control-Allow-Origin', '*');
const info = await getChainInfo();
if (!info) {
res.writeHead(503, { 'Content-Type': 'application/json' });
return res.end('{"error":"daemon unreachable"}');
}
// Plain height
if (url.pathname === '/height') {
res.writeHead(200, { 'Content-Type': 'text/plain' });
return res.end(String(info.height));
}
// Full JSON
if (url.pathname === '/json') {
res.writeHead(200, { 'Content-Type': 'application/json' });
return res.end(JSON.stringify(info));
}
// Height check
const checkMatch = url.pathname.match(/^\/check\/(\d+)$/);
if (checkMatch) {
const target = parseInt(checkMatch[1]);
res.writeHead(200, { 'Content-Type': 'application/json' });
return res.end(JSON.stringify({
reached: info.height >= target,
current: info.height,
target,
remaining: Math.max(0, target - info.height),
eta: info.height < target ? `~${(target - info.height) * 2} min` : 'now',
}));
}
// Shields.io badge
if (url.pathname === '/badge') {
const color = info.height >= 11000 ? 'brightgreen' : info.height >= 10500 ? 'yellow' : 'blue';
res.writeHead(200, { 'Content-Type': 'application/json' });
return res.end(JSON.stringify({
schemaVersion: 1,
label: 'Lethean Testnet',
message: `height ${info.height} | HF${info.hf[info.hf.length - 1]}`,
color,
}));
}
// HTML dashboard
if (url.pathname === '/dashboard') {
const hf5eta = info.height >= 11500 ? 'ACTIVE' : `${11500 - info.height} blocks (~${Math.round((11500 - info.height) * 2 / 60)}h)`;
res.writeHead(200, { 'Content-Type': 'text/html' });
return res.end(`<!DOCTYPE html><html><head><title>Lethean Status</title>
<meta http-equiv="refresh" content="30">
<style>body{background:#111;color:#eee;font-family:monospace;padding:20px;max-width:800px;margin:0 auto}
h1{color:#00d4aa}table{width:100%;border-collapse:collapse}td,th{padding:8px;text-align:left;border-bottom:1px solid #333}
th{color:#00d4aa}.ok{color:#0f0}.warn{color:#ff0}.err{color:#f00}a{color:#00d4aa}</style></head>
<body><h1>Lethean Network Status</h1>
<table>
<tr><th>Chain</th><td>Height <b>${info.height}</b> | HF${info.hf[info.hf.length-1]} | ${info.aliases} aliases | Pool: ${info.txPool} tx</td></tr>
<tr><th>HF5 (ITNS)</th><td>${hf5eta}</td></tr>
<tr><th>Synced</th><td class="${info.synced?'ok':'err'}">${info.synced?'YES':'NO'}</td></tr>
</table>
<h2>Services</h2>
<table><tr><th>Service</th><th>URL</th></tr>
<tr><td>Explorer</td><td><a href="https://explorer.lthn.io">explorer.lthn.io</a></td></tr>
<tr><td>Trade DEX</td><td><a href="https://trade.lthn.io">trade.lthn.io</a></td></tr>
<tr><td>Docs</td><td><a href="https://testnet-docs.lthn.io">testnet-docs.lthn.io</a></td></tr>
<tr><td>Downloads</td><td><a href="https://downloads.lthn.io">downloads.lthn.io</a></td></tr>
<tr><td>VPN Config</td><td><a href="https://vpn.lthn.io">vpn.lthn.io</a></td></tr>
<tr><td>SWAP</td><td><a href="https://swap.lthn.io">swap.lthn.io</a></td></tr>
<tr><td>LNS DNS</td><td><code>dig @10.69.69.165 charon.lthn A</code></td></tr>
<tr><td>LNS API</td><td><a href="https://lns.lthn.io/health">lns.lthn.io/health</a></td></tr>
</table>
<h2>Quick Start</h2>
<pre>curl -sL https://downloads.lthn.io/setup.sh | bash</pre>
<p style="color:#666;margin-top:30px">Auto-refreshes every 30s | <a href="/json">JSON API</a> | <a href="/badge">Badge</a></p>
</body></html>`);
}
// Human-readable
res.writeHead(200, { 'Content-Type': 'text/plain' });
res.end([
`Lethean Testnet Status`,
` Height: ${info.height}`,
` HF Active: ${info.hf.join(', ')}`,
` Difficulty: ${info.difficulty}`,
` Aliases: ${info.aliases}`,
` TX Pool: ${info.txPool}`,
` PoS: ${info.posAllowed ? 'enabled' : 'disabled'}`,
` Synced: ${info.synced}`,
``,
` HF4 (11000): ${info.height >= 11000 ? 'ACTIVE' : `in ${11000 - info.height} blocks (~${(11000 - info.height) * 2} min)`}`,
` HF5 (11500): ${info.height >= 11500 ? 'ACTIVE' : `in ${11500 - info.height} blocks`}`,
].join('\n'));
});
server.listen(PORT, () => console.log(`Chain status bot on :${PORT}`));