149 lines
5.8 KiB
JavaScript
149 lines
5.8 KiB
JavaScript
|
|
#!/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}`));
|