feat(status-api): initial push
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
commit
011cc09e3a
1 changed files with 148 additions and 0 deletions
148
bot.js
Normal file
148
bot.js
Normal file
|
|
@ -0,0 +1,148 @@
|
|||
#!/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}`));
|
||||
Loading…
Add table
Reference in a new issue