const http = require('http'); const net = require('net'); const port = 8080; const fulcrumTimeoutMs = 3000; function queryFulcrum(fulcrumUrl, method, params = []) { return new Promise((resolve) => { if (!fulcrumUrl) { resolve({ ok: false, result: null, error: 'Fulcrum URL is not configured', }); return; } let parsed; try { parsed = new URL(fulcrumUrl); } catch { resolve({ ok: false, result: null, error: 'Invalid Fulcrum URL', }); return; } if (parsed.protocol !== 'tcp:') { resolve({ ok: false, result: null, error: 'Fulcrum URL must use tcp://', }); return; } const host = parsed.hostname; const fulcrumPort = Number(parsed.port); if (!host || !fulcrumPort) { resolve({ ok: false, result: null, error: 'Fulcrum URL is missing host or port', }); return; } const socket = net.createConnection({ host, port: fulcrumPort, }); let response = ''; let settled = false; const finish = (value) => { if (settled) return; settled = true; socket.destroy(); resolve(value); }; socket.setTimeout(fulcrumTimeoutMs); socket.on('connect', () => { console.log( `Fulcrum connected for ${method} (${host}:${fulcrumPort})`, ); const request = JSON.stringify({ jsonrpc: '2.0', id: 1, method, params, }) + '\n'; console.log(`Sending Fulcrum request: ${request.trim()}`); socket.write(request); }); socket.on('data', (data) => { const chunk = data.toString(); console.log(`Fulcrum response data: ${chunk.trim()}`); response += chunk; const lines = response.split('\n'); for (const line of lines) { if (!line.trim()) continue; let message; try { message = JSON.parse(line); } catch { continue; } if (message.error) { console.error( `Fulcrum returned an error for ${method}:`, JSON.stringify(message.error), ); finish({ ok: false, result: null, error: message.error, }); return; } if (Object.prototype.hasOwnProperty.call(message, 'result')) { finish({ ok: true, result: message.result, error: null, }); return; } } }); socket.on('timeout', () => { console.error( `Fulcrum request timed out after ${fulcrumTimeoutMs}ms: ${method}`, ); finish({ ok: false, result: null, error: 'Connection timed out', }); }); socket.on('error', (error) => { console.error( `Fulcrum socket error for ${method}:`, error.message, ); finish({ ok: false, result: null, error: error.message, }); }); socket.on('close', (hadError) => { console.log( `Fulcrum socket closed for ${method}; hadError=${hadError}`, ); finish({ ok: false, result: null, error: 'Connection closed before a response was received', }); }); }); } async function getFulcrumStatus(fulcrumUrl) { const version = await queryFulcrum( fulcrumUrl, 'server.version', ['Munin Bitcoin', '1.4'], ); if (!version.ok) { return { configured: Boolean(fulcrumUrl), connected: false, url: fulcrumUrl, serverVersion: null, blockchainHeight: null, error: version.error, }; } const headers = await queryFulcrum( fulcrumUrl, 'blockchain.headers.subscribe', [], ); return { configured: Boolean(fulcrumUrl), connected: true, url: fulcrumUrl, serverVersion: version.result, blockchainHeight: headers.ok && headers.result ? headers.result.height ?? null : null, error: headers.ok ? null : headers.error, }; } async function getStatus() { const fulcrumUrl = process.env.MUNIN_FULCRUM_URL || null; const fulcrum = await getFulcrumStatus(fulcrumUrl); return { service: 'running', fulcrum, wallet: 'not configured', labels: 0, transactions: 0, lastScan: 'never', }; } const server = http.createServer(async (req, res) => { if (req.url === '/api/status') { try { const status = await getStatus(); res.writeHead(200, { 'Content-Type': 'application/json', }); res.end(JSON.stringify(status)); } catch (error) { console.error('Failed to get service status:', error); res.writeHead(500, { 'Content-Type': 'application/json', }); res.end( JSON.stringify({ service: 'running', error: 'Failed to determine service status', }), ); } return; } res.writeHead(404); res.end('Not found'); }); server.listen(port, () => { console.log(`Status API listening on port ${port}`); });