aboutsummaryrefslogtreecommitdiffstats
path: root/lib/ln.ts
blob: 1565c6512fafe166a4af1aafcf1cd4000cb3af29 (plain) (blame)
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
import config from "$config";

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): any {
  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}`);
    }

    try {
      client = new LightningClient(rpcPath);
      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.lightning);

export const lnb = lightningProxy(config.lightningb);
export { LightningUnavailableError };