1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
|
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 };
|