import config from "$config"; import { existsSync } from "fs"; import net from "net"; // Patch net.Socket.prototype.connect to prevent Bun from crashing when // connecting to a non-existent unix socket. Bun's net.createConnection // throws ENOENT in a way that bypasses all JS error handlers. // Instead, we check if the socket file exists and emit a proper error event. const origConnect = net.Socket.prototype.connect; net.Socket.prototype.connect = function (...args: any[]) { const sockPath = typeof args[0] === "string" ? args[0] : args[0]?.path; if (typeof sockPath === "string" && sockPath.startsWith("/") && !existsSync(sockPath)) { process.nextTick(() => { this.emit( "error", Object.assign(new Error(`connect ENOENT ${sockPath}`), { code: "ENOENT", errno: -2, syscall: "connect", address: sockPath, }), ); }); return this; } return origConnect.apply(this, args); }; const mod = (await import("@asoltys/clightning-client")).default; const { LightningClient } = mod as { LightningClient: any }; class LightningUnavailableError extends Error { code = "LIGHTNING_UNAVAILABLE" as const; constructor(msg: string) { super(msg); this.name = "LightningUnavailableError"; } } function isUnavailable(e: any) { const code = e?.code ?? e?.errno; return code === "ENOENT" || code === 2 || code === "ECONNREFUSED"; } function isSocketDied(e: any) { const code = e?.code ?? e?.errno; return code === "EPIPE" || code === "ECONNRESET" || code === "ENOENT" || code === 2; } function lightningProxy(rpcPath: string) { let client: any = null; let nextTryAt = 0; let backoff = 250; const maxBackoff = 5000; function ensure() { if (client) return client; const now = Date.now(); if (now < nextTryAt) { throw new LightningUnavailableError(`Lightning not ready at ${rpcPath}`); } if (!existsSync(rpcPath)) { nextTryAt = Date.now() + backoff; backoff = Math.min(maxBackoff, Math.floor(backoff * 1.8)); throw new LightningUnavailableError( `Lightning RPC socket not found at ${rpcPath}` ); } try { client = new LightningClient(rpcPath); // Prevent unhandled 'error' events from crashing the process client.on("error", (e: any) => { if (isSocketDied(e)) client = null; }); backoff = 250; nextTryAt = 0; return client; } catch (e: any) { nextTryAt = Date.now() + backoff; backoff = Math.min(maxBackoff, Math.floor(backoff * 1.8)); if (isUnavailable(e)) { throw new LightningUnavailableError( `Lightning RPC unavailable at ${rpcPath} (${e?.code ?? e?.errno ?? "error"})` ); } throw e; } } return new Proxy( {}, { get(_t, prop: string | symbol) { if (prop === "toString") return () => "[LightningProxy]"; if (prop === Symbol.toStringTag) return "LightningProxy"; return async (...args: any[]) => { const c = ensure(); const v = c[prop as any]; if (typeof v !== "function") return v; try { return await v.apply(c, args); } catch (e: any) { if (isSocketDied(e)) client = null; throw e; } }; }, } ); } // Main proxy for short-lived RPC calls (getinfo, invoice, decode, etc.) const ln = lightningProxy(config.lightning); export default ln; // Dedicated proxy for long-polling (waitanyinvoice) — uses its own socket // so it doesn't block the main connection export const lnListen = lightningProxy(config.lightningListen || config.lightning); export const lnb = lightningProxy(config.lightningb); export { LightningUnavailableError };