#!/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(`
Lethean Status
Lethean Network Status
| Chain | Height ${info.height} | HF${info.hf[info.hf.length-1]} | ${info.aliases} aliases | Pool: ${info.txPool} tx |
| HF5 (ITNS) | ${hf5eta} |
| Synced | ${info.synced?'YES':'NO'} |
Services
Quick Start
curl -sL https://downloads.lthn.io/setup.sh | bash
Auto-refreshes every 30s | JSON API | Badge
`);
}
// 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}`));