diff --git a/server/transactions.js b/server/transactions.js new file mode 100644 index 0000000..4bd7935 --- /dev/null +++ b/server/transactions.js @@ -0,0 +1,185 @@ +const net = require('net') + +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 port = Number(parsed.port) + + if (!host || !port) { + resolve({ + ok: false, + result: null, + error: 'Fulcrum URL is missing host or port', + }) + return + } + + const socket = net.createConnection({ + host, + port, + }) + + let response = '' + let settled = false + + const finish = (value) => { + if (settled) { + return + } + + settled = true + socket.destroy() + resolve(value) + } + + socket.setTimeout(fulcrumTimeoutMs) + + socket.on('connect', () => { + const request = + JSON.stringify({ + jsonrpc: '2.0', + id: 1, + method, + params, + }) + '\n' + + socket.write(request) + }) + + socket.on('data', (data) => { + response += data.toString() + + 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) { + 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', () => { + finish({ + ok: false, + result: null, + error: 'Connection timed out', + }) + }) + + socket.on('error', (error) => { + finish({ + ok: false, + result: null, + error: error.message, + }) + }) + + socket.on('close', () => { + if (!settled) { + finish({ + ok: false, + result: null, + error: + 'Connection closed before a response was received', + }) + } + }) + }) +} + +async function getTransaction(txid) { + if (typeof txid !== 'string' || !txid.trim()) { + throw new Error('Transaction ID is required') + } + + const fulcrumUrl = + process.env.MUNIN_FULCRUM_URL || null + + if (!fulcrumUrl) { + throw new Error('Fulcrum URL is not configured') + } + + const result = await queryFulcrum( + fulcrumUrl, + 'blockchain.transaction.get', + [txid.trim(), true], + ) + + if (!result.ok) { + throw new Error( + `Failed to query transaction: ${JSON.stringify(result.error)}`, + ) + } + + return result.result +} + +module.exports = { + getTransaction, +} \ No newline at end of file