@fiber-pay/runtime 0.1.0-rc.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +110 -0
- package/dist/index.d.ts +523 -0
- package/dist/index.js +3389 -0
- package/dist/index.js.map +1 -0
- package/package.json +46 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,3389 @@
|
|
|
1
|
+
// src/service.ts
|
|
2
|
+
import { EventEmitter as EventEmitter2 } from "events";
|
|
3
|
+
import { FiberRpcClient as FiberRpcClient2 } from "@fiber-pay/sdk";
|
|
4
|
+
|
|
5
|
+
// src/alerts/alert-manager.ts
|
|
6
|
+
import { randomUUID } from "crypto";
|
|
7
|
+
var AlertManager = class {
|
|
8
|
+
backends;
|
|
9
|
+
store;
|
|
10
|
+
listeners = [];
|
|
11
|
+
constructor(options) {
|
|
12
|
+
this.backends = options.backends;
|
|
13
|
+
this.store = options.store;
|
|
14
|
+
}
|
|
15
|
+
/** Register a listener that is called after every emitted alert. */
|
|
16
|
+
onEmit(listener) {
|
|
17
|
+
this.listeners.push(listener);
|
|
18
|
+
}
|
|
19
|
+
async start() {
|
|
20
|
+
for (const backend of this.backends) {
|
|
21
|
+
if (backend.start) {
|
|
22
|
+
await backend.start();
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
async stop() {
|
|
27
|
+
for (const backend of this.backends) {
|
|
28
|
+
if (backend.stop) {
|
|
29
|
+
await backend.stop();
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
async emit(input) {
|
|
34
|
+
const alert = {
|
|
35
|
+
id: randomUUID(),
|
|
36
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
37
|
+
type: input.type,
|
|
38
|
+
priority: input.priority,
|
|
39
|
+
source: input.source,
|
|
40
|
+
data: input.data
|
|
41
|
+
};
|
|
42
|
+
this.store.addAlert(alert);
|
|
43
|
+
await Promise.allSettled(this.backends.map((backend) => backend.send(alert)));
|
|
44
|
+
for (const listener of this.listeners) {
|
|
45
|
+
listener(alert);
|
|
46
|
+
}
|
|
47
|
+
return alert;
|
|
48
|
+
}
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
// src/alerts/backends/file-jsonl.ts
|
|
52
|
+
import { appendFileSync, mkdirSync } from "fs";
|
|
53
|
+
import { dirname } from "path";
|
|
54
|
+
var JsonlFileAlertBackend = class {
|
|
55
|
+
path;
|
|
56
|
+
constructor(path) {
|
|
57
|
+
this.path = path;
|
|
58
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
59
|
+
}
|
|
60
|
+
async send(alert) {
|
|
61
|
+
appendFileSync(this.path, `${JSON.stringify(alert)}
|
|
62
|
+
`, "utf-8");
|
|
63
|
+
}
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
// src/alerts/format.ts
|
|
67
|
+
var ANSI_RESET = "\x1B[0m";
|
|
68
|
+
var ANSI_BOLD = "\x1B[1m";
|
|
69
|
+
var ANSI_DIM = "\x1B[2m";
|
|
70
|
+
var ANSI_RED = "\x1B[31m";
|
|
71
|
+
var ANSI_GREEN = "\x1B[32m";
|
|
72
|
+
var ANSI_YELLOW = "\x1B[33m";
|
|
73
|
+
var ANSI_BLUE = "\x1B[34m";
|
|
74
|
+
var ANSI_MAGENTA = "\x1B[35m";
|
|
75
|
+
var ANSI_CYAN = "\x1B[36m";
|
|
76
|
+
function clip(value, max = 120) {
|
|
77
|
+
if (value.length <= max) {
|
|
78
|
+
return value;
|
|
79
|
+
}
|
|
80
|
+
return `${value.slice(0, max - 1)}\u2026`;
|
|
81
|
+
}
|
|
82
|
+
function readStringField(record, key) {
|
|
83
|
+
const value = record[key];
|
|
84
|
+
return typeof value === "string" && value.length > 0 ? value : void 0;
|
|
85
|
+
}
|
|
86
|
+
function toRecord(value) {
|
|
87
|
+
if (!value || typeof value !== "object") {
|
|
88
|
+
return void 0;
|
|
89
|
+
}
|
|
90
|
+
return value;
|
|
91
|
+
}
|
|
92
|
+
function summarizeAlertData(data, withColor) {
|
|
93
|
+
if (data === null || data === void 0) {
|
|
94
|
+
return "{}";
|
|
95
|
+
}
|
|
96
|
+
if (typeof data !== "object") {
|
|
97
|
+
return clip(String(data), 160);
|
|
98
|
+
}
|
|
99
|
+
const record = data;
|
|
100
|
+
const parts = [];
|
|
101
|
+
const eventType = readStringField(record, "type");
|
|
102
|
+
if (eventType) {
|
|
103
|
+
parts.push(`type=${eventType}`);
|
|
104
|
+
}
|
|
105
|
+
const channel = toRecord(record.channel);
|
|
106
|
+
const previousChannel = toRecord(record.previousChannel);
|
|
107
|
+
const channelId = readStringField(record, "channelId") ?? readStringField(channel ?? {}, "channel_id") ?? readStringField(previousChannel ?? {}, "channel_id");
|
|
108
|
+
if (channelId) {
|
|
109
|
+
parts.push(`channelId=${channelId}`);
|
|
110
|
+
}
|
|
111
|
+
const paymentHash = readStringField(record, "paymentHash");
|
|
112
|
+
if (paymentHash) {
|
|
113
|
+
parts.push(`paymentHash=${paymentHash}`);
|
|
114
|
+
}
|
|
115
|
+
const invoicePaymentHash = readStringField(record, "invoicePaymentHash");
|
|
116
|
+
if (invoicePaymentHash) {
|
|
117
|
+
parts.push(`invoicePaymentHash=${invoicePaymentHash}`);
|
|
118
|
+
}
|
|
119
|
+
const peerId = readStringField(record, "peerId") ?? readStringField(channel ?? {}, "peer_id");
|
|
120
|
+
if (peerId) {
|
|
121
|
+
parts.push(`peerId=${peerId}`);
|
|
122
|
+
}
|
|
123
|
+
const jobId = readStringField(record, "jobId");
|
|
124
|
+
if (jobId) {
|
|
125
|
+
parts.push(`jobId=${jobId}`);
|
|
126
|
+
}
|
|
127
|
+
const previousState = readStringField(record, "previousState");
|
|
128
|
+
const currentState = readStringField(record, "currentState");
|
|
129
|
+
if (previousState && currentState) {
|
|
130
|
+
parts.push(
|
|
131
|
+
colorize(`state=${previousState}->${currentState}`, `${ANSI_BOLD}${ANSI_YELLOW}`, withColor)
|
|
132
|
+
);
|
|
133
|
+
}
|
|
134
|
+
const channelState = toRecord(channel?.state);
|
|
135
|
+
const previousChannelState = toRecord(previousChannel?.state);
|
|
136
|
+
const currentStateName = readStringField(channelState ?? {}, "state_name");
|
|
137
|
+
const previousStateName = readStringField(previousChannelState ?? {}, "state_name");
|
|
138
|
+
if (!previousState && !currentState && previousStateName && currentStateName) {
|
|
139
|
+
parts.push(
|
|
140
|
+
colorize(`state=${previousStateName}->${currentStateName}`, `${ANSI_BOLD}${ANSI_YELLOW}`, withColor)
|
|
141
|
+
);
|
|
142
|
+
} else if (!previousState && !currentState && currentStateName) {
|
|
143
|
+
parts.push(colorize(`state=${currentStateName}`, `${ANSI_BOLD}${ANSI_YELLOW}`, withColor));
|
|
144
|
+
}
|
|
145
|
+
const reason = readStringField(record, "reason") ?? readStringField(record, "message");
|
|
146
|
+
if (reason) {
|
|
147
|
+
parts.push(`reason=${clip(reason, 80)}`);
|
|
148
|
+
}
|
|
149
|
+
const error = readStringField(record, "error");
|
|
150
|
+
if (error) {
|
|
151
|
+
parts.push(`error=${clip(error, 80)}`);
|
|
152
|
+
}
|
|
153
|
+
if (parts.length > 0) {
|
|
154
|
+
return parts.join(" ");
|
|
155
|
+
}
|
|
156
|
+
return clip(JSON.stringify(record), 160);
|
|
157
|
+
}
|
|
158
|
+
function shouldUseColor(mode) {
|
|
159
|
+
if (mode === "always") {
|
|
160
|
+
return true;
|
|
161
|
+
}
|
|
162
|
+
if (mode === "never") {
|
|
163
|
+
return false;
|
|
164
|
+
}
|
|
165
|
+
return process.env.NO_COLOR === void 0 && process.stdout.isTTY !== false;
|
|
166
|
+
}
|
|
167
|
+
function colorize(text, color, enabled) {
|
|
168
|
+
if (!enabled) {
|
|
169
|
+
return text;
|
|
170
|
+
}
|
|
171
|
+
return `${color}${text}${ANSI_RESET}`;
|
|
172
|
+
}
|
|
173
|
+
function formatRuntimeAlert(alert, options = {}) {
|
|
174
|
+
const colorMode = options.color ?? "auto";
|
|
175
|
+
const withColor = shouldUseColor(colorMode);
|
|
176
|
+
const priorityLabel = alert.priority.toUpperCase().padEnd(8, " ");
|
|
177
|
+
const priorityColor = alert.priority === "critical" ? ANSI_RED : alert.priority === "high" ? ANSI_YELLOW : alert.priority === "medium" ? ANSI_CYAN : ANSI_GREEN;
|
|
178
|
+
const typeColor = alert.type.startsWith("channel_") ? ANSI_BLUE : alert.type.startsWith("payment_") || alert.type.startsWith("incoming_") || alert.type.startsWith("outgoing_") ? ANSI_MAGENTA : ANSI_CYAN;
|
|
179
|
+
const prefixLabel = options.prefix ?? "[fiber-runtime]";
|
|
180
|
+
const prefix = colorize(prefixLabel, `${ANSI_BOLD}${ANSI_CYAN}`, withColor);
|
|
181
|
+
const ts = colorize(alert.timestamp, ANSI_DIM, withColor);
|
|
182
|
+
const priority = colorize(priorityLabel, `${ANSI_BOLD}${priorityColor}`, withColor);
|
|
183
|
+
const type = colorize(alert.type, `${ANSI_BOLD}${typeColor}`, withColor);
|
|
184
|
+
const data = colorize(summarizeAlertData(alert.data, withColor), ANSI_DIM, withColor);
|
|
185
|
+
return `${prefix} ${ts} ${priority} ${type} ${data}`;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// src/alerts/backends/stdout.ts
|
|
189
|
+
var StdoutAlertBackend = class {
|
|
190
|
+
async send(alert) {
|
|
191
|
+
console.log(formatRuntimeAlert(alert));
|
|
192
|
+
}
|
|
193
|
+
};
|
|
194
|
+
|
|
195
|
+
// src/utils/async.ts
|
|
196
|
+
function sleep(ms, signal) {
|
|
197
|
+
return new Promise((resolve2, reject) => {
|
|
198
|
+
if (signal?.aborted) {
|
|
199
|
+
reject(new DOMException("Aborted", "AbortError"));
|
|
200
|
+
return;
|
|
201
|
+
}
|
|
202
|
+
const timer = setTimeout(resolve2, ms);
|
|
203
|
+
signal?.addEventListener(
|
|
204
|
+
"abort",
|
|
205
|
+
() => {
|
|
206
|
+
clearTimeout(timer);
|
|
207
|
+
reject(new DOMException("Aborted", "AbortError"));
|
|
208
|
+
},
|
|
209
|
+
{ once: true }
|
|
210
|
+
);
|
|
211
|
+
});
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// src/alerts/backends/webhook.ts
|
|
215
|
+
var WebhookAlertBackend = class {
|
|
216
|
+
config;
|
|
217
|
+
constructor(config) {
|
|
218
|
+
this.config = {
|
|
219
|
+
timeoutMs: 5e3,
|
|
220
|
+
headers: {},
|
|
221
|
+
...config
|
|
222
|
+
};
|
|
223
|
+
}
|
|
224
|
+
async send(alert) {
|
|
225
|
+
let lastError;
|
|
226
|
+
for (let attempt = 0; attempt < 3; attempt += 1) {
|
|
227
|
+
const controller = new AbortController();
|
|
228
|
+
const timer = setTimeout(() => controller.abort(), this.config.timeoutMs);
|
|
229
|
+
try {
|
|
230
|
+
const response = await fetch(this.config.url, {
|
|
231
|
+
method: "POST",
|
|
232
|
+
headers: {
|
|
233
|
+
"content-type": "application/json",
|
|
234
|
+
...this.config.headers
|
|
235
|
+
},
|
|
236
|
+
body: JSON.stringify(alert),
|
|
237
|
+
signal: controller.signal
|
|
238
|
+
});
|
|
239
|
+
clearTimeout(timer);
|
|
240
|
+
if (response.ok) {
|
|
241
|
+
return;
|
|
242
|
+
}
|
|
243
|
+
lastError = new Error(`Webhook response status: ${response.status}`);
|
|
244
|
+
} catch (error) {
|
|
245
|
+
clearTimeout(timer);
|
|
246
|
+
lastError = error;
|
|
247
|
+
}
|
|
248
|
+
await sleep(100 * 2 ** attempt);
|
|
249
|
+
}
|
|
250
|
+
throw new Error(`Failed to deliver webhook alert: ${String(lastError)}`);
|
|
251
|
+
}
|
|
252
|
+
};
|
|
253
|
+
|
|
254
|
+
// src/alerts/backends/websocket.ts
|
|
255
|
+
import { createHash } from "crypto";
|
|
256
|
+
import http from "http";
|
|
257
|
+
var WebsocketAlertBackend = class {
|
|
258
|
+
config;
|
|
259
|
+
clients = /* @__PURE__ */ new Set();
|
|
260
|
+
server;
|
|
261
|
+
constructor(config) {
|
|
262
|
+
this.config = config;
|
|
263
|
+
}
|
|
264
|
+
async start() {
|
|
265
|
+
if (this.server) {
|
|
266
|
+
return;
|
|
267
|
+
}
|
|
268
|
+
this.server = http.createServer((req, res) => {
|
|
269
|
+
if (req.url === "/health") {
|
|
270
|
+
res.writeHead(200, { "content-type": "application/json" });
|
|
271
|
+
res.end(JSON.stringify({ ok: true }));
|
|
272
|
+
return;
|
|
273
|
+
}
|
|
274
|
+
res.writeHead(404, { "content-type": "application/json" });
|
|
275
|
+
res.end(JSON.stringify({ error: "Not found" }));
|
|
276
|
+
});
|
|
277
|
+
this.server.on("upgrade", (request, socket) => {
|
|
278
|
+
const key = request.headers["sec-websocket-key"];
|
|
279
|
+
if (!key || typeof key !== "string") {
|
|
280
|
+
socket.destroy();
|
|
281
|
+
return;
|
|
282
|
+
}
|
|
283
|
+
const accept = createHash("sha1").update(`${key}258EAFA5-E914-47DA-95CA-C5AB0DC85B11`).digest("base64");
|
|
284
|
+
const headers = [
|
|
285
|
+
"HTTP/1.1 101 Switching Protocols",
|
|
286
|
+
"Upgrade: websocket",
|
|
287
|
+
"Connection: Upgrade",
|
|
288
|
+
`Sec-WebSocket-Accept: ${accept}`
|
|
289
|
+
];
|
|
290
|
+
socket.write(`${headers.join("\r\n")}\r
|
|
291
|
+
\r
|
|
292
|
+
`);
|
|
293
|
+
this.clients.add(socket);
|
|
294
|
+
socket.on("close", () => this.clients.delete(socket));
|
|
295
|
+
socket.on("end", () => this.clients.delete(socket));
|
|
296
|
+
socket.on("error", () => this.clients.delete(socket));
|
|
297
|
+
});
|
|
298
|
+
await new Promise((resolve2, reject) => {
|
|
299
|
+
this.server?.once("error", reject);
|
|
300
|
+
this.server?.listen(this.config.port, this.config.host, () => {
|
|
301
|
+
this.server?.off("error", reject);
|
|
302
|
+
resolve2();
|
|
303
|
+
});
|
|
304
|
+
});
|
|
305
|
+
}
|
|
306
|
+
async send(alert) {
|
|
307
|
+
const payload = Buffer.from(JSON.stringify(alert), "utf8");
|
|
308
|
+
const frame = buildWebSocketFrame(payload);
|
|
309
|
+
for (const client of this.clients) {
|
|
310
|
+
try {
|
|
311
|
+
client.write(frame);
|
|
312
|
+
} catch {
|
|
313
|
+
this.clients.delete(client);
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
async stop() {
|
|
318
|
+
for (const client of this.clients) {
|
|
319
|
+
client.destroy();
|
|
320
|
+
}
|
|
321
|
+
this.clients.clear();
|
|
322
|
+
if (!this.server) {
|
|
323
|
+
return;
|
|
324
|
+
}
|
|
325
|
+
const server = this.server;
|
|
326
|
+
this.server = void 0;
|
|
327
|
+
await new Promise((resolve2) => {
|
|
328
|
+
server.close(() => resolve2());
|
|
329
|
+
});
|
|
330
|
+
}
|
|
331
|
+
};
|
|
332
|
+
function buildWebSocketFrame(payload) {
|
|
333
|
+
const payloadLength = payload.length;
|
|
334
|
+
if (payloadLength < 126) {
|
|
335
|
+
return Buffer.concat([Buffer.from([129, payloadLength]), payload]);
|
|
336
|
+
}
|
|
337
|
+
if (payloadLength < 65536) {
|
|
338
|
+
const header2 = Buffer.alloc(4);
|
|
339
|
+
header2[0] = 129;
|
|
340
|
+
header2[1] = 126;
|
|
341
|
+
header2.writeUInt16BE(payloadLength, 2);
|
|
342
|
+
return Buffer.concat([header2, payload]);
|
|
343
|
+
}
|
|
344
|
+
const header = Buffer.alloc(10);
|
|
345
|
+
header[0] = 129;
|
|
346
|
+
header[1] = 127;
|
|
347
|
+
header.writeUInt32BE(0, 2);
|
|
348
|
+
header.writeUInt32BE(payloadLength, 6);
|
|
349
|
+
return Buffer.concat([header, payload]);
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
// src/config.ts
|
|
353
|
+
import { resolve } from "path";
|
|
354
|
+
|
|
355
|
+
// src/jobs/retry-policy.ts
|
|
356
|
+
var defaultPaymentRetryPolicy = {
|
|
357
|
+
maxRetries: 3,
|
|
358
|
+
baseDelayMs: 2e3,
|
|
359
|
+
maxDelayMs: 3e4,
|
|
360
|
+
backoffMultiplier: 2,
|
|
361
|
+
jitterMs: 500
|
|
362
|
+
};
|
|
363
|
+
function shouldRetry(error, retryCount, policy) {
|
|
364
|
+
if (!error.retryable) return false;
|
|
365
|
+
if (retryCount >= policy.maxRetries) return false;
|
|
366
|
+
return true;
|
|
367
|
+
}
|
|
368
|
+
function computeRetryDelay(retryCount, policy) {
|
|
369
|
+
const base = policy.baseDelayMs * policy.backoffMultiplier ** retryCount;
|
|
370
|
+
const capped = Math.min(base, policy.maxDelayMs);
|
|
371
|
+
const jitter = Math.floor(Math.random() * policy.jitterMs);
|
|
372
|
+
return capped + jitter;
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
// src/config.ts
|
|
376
|
+
var defaultRuntimeConfig = {
|
|
377
|
+
fiberRpcUrl: "http://127.0.0.1:8227",
|
|
378
|
+
channelPollIntervalMs: 3e3,
|
|
379
|
+
invoicePollIntervalMs: 2e3,
|
|
380
|
+
paymentPollIntervalMs: 1e3,
|
|
381
|
+
peerPollIntervalMs: 1e4,
|
|
382
|
+
healthPollIntervalMs: 5e3,
|
|
383
|
+
includeClosedChannels: true,
|
|
384
|
+
completedItemTtlSeconds: 86400,
|
|
385
|
+
requestTimeoutMs: 1e4,
|
|
386
|
+
alerts: [{ type: "stdout" }],
|
|
387
|
+
proxy: {
|
|
388
|
+
enabled: true,
|
|
389
|
+
listen: "127.0.0.1:8229"
|
|
390
|
+
},
|
|
391
|
+
storage: {
|
|
392
|
+
stateFilePath: resolve(process.cwd(), ".fiber-pay-runtime-state.json"),
|
|
393
|
+
flushIntervalMs: 3e4,
|
|
394
|
+
maxAlertHistory: 5e3
|
|
395
|
+
},
|
|
396
|
+
jobs: {
|
|
397
|
+
enabled: true,
|
|
398
|
+
dbPath: resolve(process.cwd(), ".fiber-pay-jobs.db"),
|
|
399
|
+
maxConcurrentJobs: 5,
|
|
400
|
+
schedulerIntervalMs: 500,
|
|
401
|
+
retryPolicy: defaultPaymentRetryPolicy
|
|
402
|
+
}
|
|
403
|
+
};
|
|
404
|
+
function createRuntimeConfig(input = {}) {
|
|
405
|
+
const config = {
|
|
406
|
+
...defaultRuntimeConfig,
|
|
407
|
+
...input,
|
|
408
|
+
proxy: {
|
|
409
|
+
...defaultRuntimeConfig.proxy,
|
|
410
|
+
...input.proxy
|
|
411
|
+
},
|
|
412
|
+
storage: {
|
|
413
|
+
...defaultRuntimeConfig.storage,
|
|
414
|
+
...input.storage
|
|
415
|
+
},
|
|
416
|
+
jobs: {
|
|
417
|
+
...defaultRuntimeConfig.jobs,
|
|
418
|
+
...input.jobs,
|
|
419
|
+
retryPolicy: {
|
|
420
|
+
...defaultRuntimeConfig.jobs.retryPolicy,
|
|
421
|
+
...input.jobs?.retryPolicy
|
|
422
|
+
}
|
|
423
|
+
},
|
|
424
|
+
alerts: input.alerts ?? defaultRuntimeConfig.alerts
|
|
425
|
+
};
|
|
426
|
+
if (!config.fiberRpcUrl) {
|
|
427
|
+
throw new Error("Runtime config requires fiberRpcUrl");
|
|
428
|
+
}
|
|
429
|
+
if (!config.proxy.listen) {
|
|
430
|
+
throw new Error("Runtime config requires proxy.listen");
|
|
431
|
+
}
|
|
432
|
+
if (config.channelPollIntervalMs <= 0 || config.invoicePollIntervalMs <= 0) {
|
|
433
|
+
throw new Error("Polling intervals must be > 0");
|
|
434
|
+
}
|
|
435
|
+
if (config.paymentPollIntervalMs <= 0 || config.peerPollIntervalMs <= 0) {
|
|
436
|
+
throw new Error("Polling intervals must be > 0");
|
|
437
|
+
}
|
|
438
|
+
if (config.healthPollIntervalMs <= 0) {
|
|
439
|
+
throw new Error("healthPollIntervalMs must be > 0");
|
|
440
|
+
}
|
|
441
|
+
if (config.completedItemTtlSeconds < 0) {
|
|
442
|
+
throw new Error("completedItemTtlSeconds must be >= 0");
|
|
443
|
+
}
|
|
444
|
+
if (config.storage.flushIntervalMs <= 0) {
|
|
445
|
+
throw new Error("storage.flushIntervalMs must be > 0");
|
|
446
|
+
}
|
|
447
|
+
if (config.storage.maxAlertHistory <= 0) {
|
|
448
|
+
throw new Error("storage.maxAlertHistory must be > 0");
|
|
449
|
+
}
|
|
450
|
+
if (!config.jobs.dbPath) {
|
|
451
|
+
throw new Error("jobs.dbPath is required");
|
|
452
|
+
}
|
|
453
|
+
if (config.jobs.maxConcurrentJobs <= 0) {
|
|
454
|
+
throw new Error("jobs.maxConcurrentJobs must be > 0");
|
|
455
|
+
}
|
|
456
|
+
if (config.jobs.schedulerIntervalMs <= 0) {
|
|
457
|
+
throw new Error("jobs.schedulerIntervalMs must be > 0");
|
|
458
|
+
}
|
|
459
|
+
if (config.jobs.retryPolicy.maxRetries < 0) {
|
|
460
|
+
throw new Error("jobs.retryPolicy.maxRetries must be >= 0");
|
|
461
|
+
}
|
|
462
|
+
if (config.jobs.retryPolicy.baseDelayMs < 0) {
|
|
463
|
+
throw new Error("jobs.retryPolicy.baseDelayMs must be >= 0");
|
|
464
|
+
}
|
|
465
|
+
if (config.jobs.retryPolicy.maxDelayMs < 0) {
|
|
466
|
+
throw new Error("jobs.retryPolicy.maxDelayMs must be >= 0");
|
|
467
|
+
}
|
|
468
|
+
return config;
|
|
469
|
+
}
|
|
470
|
+
function parseListenAddress(listen) {
|
|
471
|
+
const [host, portText] = listen.split(":");
|
|
472
|
+
const port = Number(portText);
|
|
473
|
+
if (!host || Number.isNaN(port) || port <= 0) {
|
|
474
|
+
throw new Error(`Invalid listen address: ${listen}`);
|
|
475
|
+
}
|
|
476
|
+
return { host, port };
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
// src/monitors/channel-monitor.ts
|
|
480
|
+
import { ChannelState } from "@fiber-pay/sdk";
|
|
481
|
+
|
|
482
|
+
// src/diff/channel-diff.ts
|
|
483
|
+
function diffChannels(previous, current) {
|
|
484
|
+
const events = [];
|
|
485
|
+
const prevById = new Map(previous.map((channel) => [channel.channel_id, channel]));
|
|
486
|
+
const currById = new Map(current.map((channel) => [channel.channel_id, channel]));
|
|
487
|
+
for (const [channelId, channel] of currById.entries()) {
|
|
488
|
+
const previousChannel = prevById.get(channelId);
|
|
489
|
+
if (!previousChannel) {
|
|
490
|
+
events.push({ type: "channel_new", channel });
|
|
491
|
+
continue;
|
|
492
|
+
}
|
|
493
|
+
if (channel.state.state_name !== previousChannel.state.state_name) {
|
|
494
|
+
events.push({
|
|
495
|
+
type: "channel_state_changed",
|
|
496
|
+
channel,
|
|
497
|
+
previousState: previousChannel.state.state_name,
|
|
498
|
+
currentState: channel.state.state_name
|
|
499
|
+
});
|
|
500
|
+
}
|
|
501
|
+
if (channel.local_balance !== previousChannel.local_balance || channel.remote_balance !== previousChannel.remote_balance) {
|
|
502
|
+
events.push({
|
|
503
|
+
type: "channel_balance_changed",
|
|
504
|
+
channel,
|
|
505
|
+
localBalanceBefore: previousChannel.local_balance,
|
|
506
|
+
localBalanceAfter: channel.local_balance,
|
|
507
|
+
remoteBalanceBefore: previousChannel.remote_balance,
|
|
508
|
+
remoteBalanceAfter: channel.remote_balance
|
|
509
|
+
});
|
|
510
|
+
}
|
|
511
|
+
const previousPending = new Set(previousChannel.pending_tlcs.map((tlc) => tlc.id));
|
|
512
|
+
const newTlcCount = channel.pending_tlcs.filter((tlc) => !previousPending.has(tlc.id)).length;
|
|
513
|
+
if (newTlcCount > 0) {
|
|
514
|
+
events.push({
|
|
515
|
+
type: "channel_pending_tlc_added",
|
|
516
|
+
channel,
|
|
517
|
+
previousPendingTlcCount: previousChannel.pending_tlcs.length,
|
|
518
|
+
newPendingTlcCount: channel.pending_tlcs.length
|
|
519
|
+
});
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
for (const [channelId, previousChannel] of prevById.entries()) {
|
|
523
|
+
if (!currById.has(channelId)) {
|
|
524
|
+
events.push({
|
|
525
|
+
type: "channel_disappeared",
|
|
526
|
+
channelId,
|
|
527
|
+
previousChannel
|
|
528
|
+
});
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
return events;
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
// src/monitors/base-monitor.ts
|
|
535
|
+
var BaseMonitor = class {
|
|
536
|
+
intervalMs;
|
|
537
|
+
hooks;
|
|
538
|
+
timer;
|
|
539
|
+
running = false;
|
|
540
|
+
constructor(intervalMs, hooks = {}) {
|
|
541
|
+
this.intervalMs = intervalMs;
|
|
542
|
+
this.hooks = hooks;
|
|
543
|
+
}
|
|
544
|
+
start() {
|
|
545
|
+
this.stop();
|
|
546
|
+
void this.runOnce();
|
|
547
|
+
this.timer = setInterval(() => {
|
|
548
|
+
void this.runOnce();
|
|
549
|
+
}, this.intervalMs);
|
|
550
|
+
}
|
|
551
|
+
stop() {
|
|
552
|
+
if (this.timer) {
|
|
553
|
+
clearInterval(this.timer);
|
|
554
|
+
this.timer = void 0;
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
async runOnce() {
|
|
558
|
+
if (this.running) {
|
|
559
|
+
return;
|
|
560
|
+
}
|
|
561
|
+
this.running = true;
|
|
562
|
+
try {
|
|
563
|
+
await this.poll();
|
|
564
|
+
await this.hooks.onCycleSuccess?.(this.name);
|
|
565
|
+
} catch (error) {
|
|
566
|
+
await this.hooks.onCycleError?.(error, this.name);
|
|
567
|
+
} finally {
|
|
568
|
+
this.running = false;
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
};
|
|
572
|
+
|
|
573
|
+
// src/monitors/channel-monitor.ts
|
|
574
|
+
var ChannelMonitor = class extends BaseMonitor {
|
|
575
|
+
get name() {
|
|
576
|
+
return "channel-monitor";
|
|
577
|
+
}
|
|
578
|
+
client;
|
|
579
|
+
store;
|
|
580
|
+
alerts;
|
|
581
|
+
config;
|
|
582
|
+
constructor(options) {
|
|
583
|
+
super(options.config.intervalMs, options.hooks);
|
|
584
|
+
this.client = options.client;
|
|
585
|
+
this.store = options.store;
|
|
586
|
+
this.alerts = options.alerts;
|
|
587
|
+
this.config = options.config;
|
|
588
|
+
}
|
|
589
|
+
async poll() {
|
|
590
|
+
const previous = this.store.getChannelSnapshot();
|
|
591
|
+
const result = await this.client.listChannels({
|
|
592
|
+
include_closed: this.config.includeClosedChannels
|
|
593
|
+
});
|
|
594
|
+
const current = result.channels;
|
|
595
|
+
const changes = diffChannels(previous, current);
|
|
596
|
+
for (const change of changes) {
|
|
597
|
+
if (change.type === "channel_new" && change.channel.state.state_name === ChannelState.NegotiatingFunding) {
|
|
598
|
+
await this.alerts.emit({
|
|
599
|
+
type: "new_inbound_channel_request",
|
|
600
|
+
priority: "high",
|
|
601
|
+
source: this.name,
|
|
602
|
+
data: { channelId: change.channel.channel_id, channel: change.channel }
|
|
603
|
+
});
|
|
604
|
+
}
|
|
605
|
+
if (change.type === "channel_state_changed") {
|
|
606
|
+
await this.alerts.emit({
|
|
607
|
+
type: "channel_state_changed",
|
|
608
|
+
priority: getChannelStatePriority(change.currentState),
|
|
609
|
+
source: this.name,
|
|
610
|
+
data: {
|
|
611
|
+
channelId: change.channel.channel_id,
|
|
612
|
+
previousState: change.previousState,
|
|
613
|
+
currentState: change.currentState,
|
|
614
|
+
channel: change.channel
|
|
615
|
+
}
|
|
616
|
+
});
|
|
617
|
+
}
|
|
618
|
+
if (change.type === "channel_state_changed" && change.currentState === ChannelState.ChannelReady) {
|
|
619
|
+
await this.alerts.emit({
|
|
620
|
+
type: "channel_became_ready",
|
|
621
|
+
priority: "medium",
|
|
622
|
+
source: this.name,
|
|
623
|
+
data: change
|
|
624
|
+
});
|
|
625
|
+
}
|
|
626
|
+
if (change.type === "channel_state_changed" && (change.currentState === ChannelState.ShuttingDown || change.currentState === ChannelState.Closed)) {
|
|
627
|
+
await this.alerts.emit({
|
|
628
|
+
type: "channel_closing",
|
|
629
|
+
priority: "high",
|
|
630
|
+
source: this.name,
|
|
631
|
+
data: change
|
|
632
|
+
});
|
|
633
|
+
}
|
|
634
|
+
if (change.type === "channel_disappeared") {
|
|
635
|
+
await this.alerts.emit({
|
|
636
|
+
type: "channel_state_changed",
|
|
637
|
+
priority: "high",
|
|
638
|
+
source: this.name,
|
|
639
|
+
data: {
|
|
640
|
+
channelId: change.channelId,
|
|
641
|
+
previousState: change.previousChannel.state.state_name,
|
|
642
|
+
currentState: "DISAPPEARED",
|
|
643
|
+
channel: change.previousChannel
|
|
644
|
+
}
|
|
645
|
+
});
|
|
646
|
+
await this.alerts.emit({
|
|
647
|
+
type: "channel_closing",
|
|
648
|
+
priority: "high",
|
|
649
|
+
source: this.name,
|
|
650
|
+
data: change
|
|
651
|
+
});
|
|
652
|
+
}
|
|
653
|
+
if (change.type === "channel_balance_changed") {
|
|
654
|
+
await this.alerts.emit({
|
|
655
|
+
type: "channel_balance_changed",
|
|
656
|
+
priority: "low",
|
|
657
|
+
source: this.name,
|
|
658
|
+
data: change
|
|
659
|
+
});
|
|
660
|
+
}
|
|
661
|
+
if (change.type === "channel_pending_tlc_added") {
|
|
662
|
+
await this.alerts.emit({
|
|
663
|
+
type: "new_pending_tlc",
|
|
664
|
+
priority: "medium",
|
|
665
|
+
source: this.name,
|
|
666
|
+
data: change
|
|
667
|
+
});
|
|
668
|
+
}
|
|
669
|
+
}
|
|
670
|
+
this.store.setChannelSnapshot(current);
|
|
671
|
+
}
|
|
672
|
+
};
|
|
673
|
+
function getChannelStatePriority(stateName) {
|
|
674
|
+
const normalized = stateName.toUpperCase();
|
|
675
|
+
const closedState = String(ChannelState.Closed).toUpperCase();
|
|
676
|
+
const shuttingDownState = String(ChannelState.ShuttingDown).toUpperCase();
|
|
677
|
+
const readyState = String(ChannelState.ChannelReady).toUpperCase();
|
|
678
|
+
if (normalized === closedState || normalized === "CLOSED" || normalized === shuttingDownState || normalized === "SHUTTING_DOWN") {
|
|
679
|
+
return "high";
|
|
680
|
+
}
|
|
681
|
+
if (normalized === readyState || normalized === "CHANNEL_READY") {
|
|
682
|
+
return "medium";
|
|
683
|
+
}
|
|
684
|
+
return "low";
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
// src/monitors/health-monitor.ts
|
|
688
|
+
var HealthMonitor = class extends BaseMonitor {
|
|
689
|
+
get name() {
|
|
690
|
+
return "health-monitor";
|
|
691
|
+
}
|
|
692
|
+
client;
|
|
693
|
+
alerts;
|
|
694
|
+
isOffline = false;
|
|
695
|
+
constructor(options) {
|
|
696
|
+
super(options.config.intervalMs, options.hooks);
|
|
697
|
+
this.client = options.client;
|
|
698
|
+
this.alerts = options.alerts;
|
|
699
|
+
}
|
|
700
|
+
async poll() {
|
|
701
|
+
let isHealthy = false;
|
|
702
|
+
let failureReason;
|
|
703
|
+
try {
|
|
704
|
+
isHealthy = await this.client.ping();
|
|
705
|
+
} catch (error) {
|
|
706
|
+
failureReason = error instanceof Error ? error.message : String(error);
|
|
707
|
+
isHealthy = false;
|
|
708
|
+
}
|
|
709
|
+
if (isHealthy && this.isOffline) {
|
|
710
|
+
this.isOffline = false;
|
|
711
|
+
await this.alerts.emit({
|
|
712
|
+
type: "node_online",
|
|
713
|
+
priority: "low",
|
|
714
|
+
source: this.name,
|
|
715
|
+
data: { message: "Fiber node RPC recovered" }
|
|
716
|
+
});
|
|
717
|
+
return;
|
|
718
|
+
}
|
|
719
|
+
if (!isHealthy && !this.isOffline) {
|
|
720
|
+
this.isOffline = true;
|
|
721
|
+
await this.alerts.emit({
|
|
722
|
+
type: "node_offline",
|
|
723
|
+
priority: "critical",
|
|
724
|
+
source: this.name,
|
|
725
|
+
data: {
|
|
726
|
+
message: failureReason ? `Fiber node ping failed: ${failureReason}` : "Fiber node ping returned false"
|
|
727
|
+
}
|
|
728
|
+
});
|
|
729
|
+
}
|
|
730
|
+
}
|
|
731
|
+
};
|
|
732
|
+
|
|
733
|
+
// src/monitors/tracker-utils.ts
|
|
734
|
+
function isExpectedTrackerError(error) {
|
|
735
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
736
|
+
return /not found|does not exist|no such|temporarily unavailable|connection refused|timed out|timeout/i.test(
|
|
737
|
+
message
|
|
738
|
+
);
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
// src/monitors/invoice-tracker.ts
|
|
742
|
+
var InvoiceTracker = class extends BaseMonitor {
|
|
743
|
+
get name() {
|
|
744
|
+
return "invoice-tracker";
|
|
745
|
+
}
|
|
746
|
+
client;
|
|
747
|
+
store;
|
|
748
|
+
alerts;
|
|
749
|
+
config;
|
|
750
|
+
constructor(options) {
|
|
751
|
+
super(options.config.intervalMs, options.hooks);
|
|
752
|
+
this.client = options.client;
|
|
753
|
+
this.store = options.store;
|
|
754
|
+
this.alerts = options.alerts;
|
|
755
|
+
this.config = options.config;
|
|
756
|
+
}
|
|
757
|
+
async poll() {
|
|
758
|
+
const tracked = this.store.listTrackedInvoices();
|
|
759
|
+
for (const invoice of tracked) {
|
|
760
|
+
try {
|
|
761
|
+
const next = await this.client.getInvoice({ payment_hash: invoice.paymentHash });
|
|
762
|
+
const previousStatus = invoice.status;
|
|
763
|
+
const currentStatus = next.status;
|
|
764
|
+
if (currentStatus !== previousStatus) {
|
|
765
|
+
this.store.updateTrackedInvoice(invoice.paymentHash, currentStatus);
|
|
766
|
+
if (currentStatus === "Received" || currentStatus === "Paid") {
|
|
767
|
+
await this.alerts.emit({
|
|
768
|
+
type: "incoming_payment_received",
|
|
769
|
+
priority: "high",
|
|
770
|
+
source: this.name,
|
|
771
|
+
data: {
|
|
772
|
+
paymentHash: invoice.paymentHash,
|
|
773
|
+
previousStatus,
|
|
774
|
+
currentStatus,
|
|
775
|
+
invoice: next
|
|
776
|
+
}
|
|
777
|
+
});
|
|
778
|
+
}
|
|
779
|
+
if (currentStatus === "Expired") {
|
|
780
|
+
await this.alerts.emit({
|
|
781
|
+
type: "invoice_expired",
|
|
782
|
+
priority: "medium",
|
|
783
|
+
source: this.name,
|
|
784
|
+
data: {
|
|
785
|
+
paymentHash: invoice.paymentHash,
|
|
786
|
+
previousStatus,
|
|
787
|
+
currentStatus,
|
|
788
|
+
invoice: next
|
|
789
|
+
}
|
|
790
|
+
});
|
|
791
|
+
}
|
|
792
|
+
if (currentStatus === "Cancelled") {
|
|
793
|
+
await this.alerts.emit({
|
|
794
|
+
type: "invoice_cancelled",
|
|
795
|
+
priority: "medium",
|
|
796
|
+
source: this.name,
|
|
797
|
+
data: {
|
|
798
|
+
paymentHash: invoice.paymentHash,
|
|
799
|
+
previousStatus,
|
|
800
|
+
currentStatus,
|
|
801
|
+
invoice: next
|
|
802
|
+
}
|
|
803
|
+
});
|
|
804
|
+
}
|
|
805
|
+
}
|
|
806
|
+
} catch (error) {
|
|
807
|
+
if (isExpectedTrackerError(error)) {
|
|
808
|
+
continue;
|
|
809
|
+
}
|
|
810
|
+
throw error;
|
|
811
|
+
}
|
|
812
|
+
}
|
|
813
|
+
this.store.pruneCompleted(this.config.completedItemTtlSeconds * 1e3);
|
|
814
|
+
}
|
|
815
|
+
};
|
|
816
|
+
|
|
817
|
+
// src/monitors/payment-tracker.ts
|
|
818
|
+
var PaymentTracker = class extends BaseMonitor {
|
|
819
|
+
get name() {
|
|
820
|
+
return "payment-tracker";
|
|
821
|
+
}
|
|
822
|
+
client;
|
|
823
|
+
store;
|
|
824
|
+
alerts;
|
|
825
|
+
config;
|
|
826
|
+
constructor(options) {
|
|
827
|
+
super(options.config.intervalMs, options.hooks);
|
|
828
|
+
this.client = options.client;
|
|
829
|
+
this.store = options.store;
|
|
830
|
+
this.alerts = options.alerts;
|
|
831
|
+
this.config = options.config;
|
|
832
|
+
}
|
|
833
|
+
async poll() {
|
|
834
|
+
const tracked = this.store.listTrackedPayments();
|
|
835
|
+
for (const payment of tracked) {
|
|
836
|
+
try {
|
|
837
|
+
const next = await this.client.getPayment({ payment_hash: payment.paymentHash });
|
|
838
|
+
const previousStatus = payment.status;
|
|
839
|
+
const currentStatus = next.status;
|
|
840
|
+
if (currentStatus !== previousStatus) {
|
|
841
|
+
this.store.updateTrackedPayment(payment.paymentHash, currentStatus);
|
|
842
|
+
if (currentStatus === "Success") {
|
|
843
|
+
await this.alerts.emit({
|
|
844
|
+
type: "outgoing_payment_completed",
|
|
845
|
+
priority: "medium",
|
|
846
|
+
source: this.name,
|
|
847
|
+
data: {
|
|
848
|
+
paymentHash: payment.paymentHash,
|
|
849
|
+
previousStatus,
|
|
850
|
+
currentStatus,
|
|
851
|
+
payment: next
|
|
852
|
+
}
|
|
853
|
+
});
|
|
854
|
+
}
|
|
855
|
+
if (currentStatus === "Failed") {
|
|
856
|
+
await this.alerts.emit({
|
|
857
|
+
type: "outgoing_payment_failed",
|
|
858
|
+
priority: "high",
|
|
859
|
+
source: this.name,
|
|
860
|
+
data: {
|
|
861
|
+
paymentHash: payment.paymentHash,
|
|
862
|
+
previousStatus,
|
|
863
|
+
currentStatus,
|
|
864
|
+
payment: next
|
|
865
|
+
}
|
|
866
|
+
});
|
|
867
|
+
}
|
|
868
|
+
}
|
|
869
|
+
} catch (error) {
|
|
870
|
+
if (isExpectedTrackerError(error)) {
|
|
871
|
+
continue;
|
|
872
|
+
}
|
|
873
|
+
throw error;
|
|
874
|
+
}
|
|
875
|
+
}
|
|
876
|
+
this.store.pruneCompleted(this.config.completedItemTtlSeconds * 1e3);
|
|
877
|
+
}
|
|
878
|
+
};
|
|
879
|
+
|
|
880
|
+
// src/diff/peer-diff.ts
|
|
881
|
+
function diffPeers(previous, current) {
|
|
882
|
+
const events = [];
|
|
883
|
+
const prevById = new Map(previous.map((peer) => [peer.peer_id, peer]));
|
|
884
|
+
const currById = new Map(current.map((peer) => [peer.peer_id, peer]));
|
|
885
|
+
for (const [peerId, peer] of currById.entries()) {
|
|
886
|
+
if (!prevById.has(peerId)) {
|
|
887
|
+
events.push({ type: "peer_connected", peer });
|
|
888
|
+
}
|
|
889
|
+
}
|
|
890
|
+
for (const [peerId, peer] of prevById.entries()) {
|
|
891
|
+
if (!currById.has(peerId)) {
|
|
892
|
+
events.push({ type: "peer_disconnected", peer });
|
|
893
|
+
}
|
|
894
|
+
}
|
|
895
|
+
return events;
|
|
896
|
+
}
|
|
897
|
+
|
|
898
|
+
// src/monitors/peer-monitor.ts
|
|
899
|
+
var PeerMonitor = class extends BaseMonitor {
|
|
900
|
+
get name() {
|
|
901
|
+
return "peer-monitor";
|
|
902
|
+
}
|
|
903
|
+
client;
|
|
904
|
+
store;
|
|
905
|
+
alerts;
|
|
906
|
+
constructor(options) {
|
|
907
|
+
super(options.config.intervalMs, options.hooks);
|
|
908
|
+
this.client = options.client;
|
|
909
|
+
this.store = options.store;
|
|
910
|
+
this.alerts = options.alerts;
|
|
911
|
+
}
|
|
912
|
+
async poll() {
|
|
913
|
+
const previous = this.store.getPeerSnapshot();
|
|
914
|
+
const result = await this.client.listPeers();
|
|
915
|
+
const current = result.peers;
|
|
916
|
+
const changes = diffPeers(previous, current);
|
|
917
|
+
for (const change of changes) {
|
|
918
|
+
if (change.type === "peer_connected") {
|
|
919
|
+
await this.alerts.emit({
|
|
920
|
+
type: "peer_connected",
|
|
921
|
+
priority: "low",
|
|
922
|
+
source: this.name,
|
|
923
|
+
data: { peer: change.peer }
|
|
924
|
+
});
|
|
925
|
+
}
|
|
926
|
+
if (change.type === "peer_disconnected") {
|
|
927
|
+
await this.alerts.emit({
|
|
928
|
+
type: "peer_disconnected",
|
|
929
|
+
priority: "low",
|
|
930
|
+
source: this.name,
|
|
931
|
+
data: { peer: change.peer }
|
|
932
|
+
});
|
|
933
|
+
}
|
|
934
|
+
}
|
|
935
|
+
this.store.setPeerSnapshot(current);
|
|
936
|
+
}
|
|
937
|
+
};
|
|
938
|
+
|
|
939
|
+
// src/proxy/rpc-proxy.ts
|
|
940
|
+
import http2 from "http";
|
|
941
|
+
|
|
942
|
+
// src/proxy/body.ts
|
|
943
|
+
var MAX_REQUEST_BODY_BYTES = 1024 * 1024;
|
|
944
|
+
async function readRawBody(req) {
|
|
945
|
+
const chunks = [];
|
|
946
|
+
let totalBytes = 0;
|
|
947
|
+
return await new Promise((resolve2, reject) => {
|
|
948
|
+
req.on("data", (chunk) => {
|
|
949
|
+
totalBytes += chunk.length;
|
|
950
|
+
if (totalBytes > MAX_REQUEST_BODY_BYTES) {
|
|
951
|
+
req.destroy();
|
|
952
|
+
reject(new PayloadTooLargeError());
|
|
953
|
+
return;
|
|
954
|
+
}
|
|
955
|
+
chunks.push(chunk);
|
|
956
|
+
});
|
|
957
|
+
req.on("end", () => resolve2(Buffer.concat(chunks)));
|
|
958
|
+
req.on("error", reject);
|
|
959
|
+
});
|
|
960
|
+
}
|
|
961
|
+
var PayloadTooLargeError = class extends Error {
|
|
962
|
+
constructor() {
|
|
963
|
+
super("Payload too large");
|
|
964
|
+
this.name = "PayloadTooLargeError";
|
|
965
|
+
}
|
|
966
|
+
};
|
|
967
|
+
function isPayloadTooLargeError(error) {
|
|
968
|
+
return error instanceof PayloadTooLargeError;
|
|
969
|
+
}
|
|
970
|
+
|
|
971
|
+
// src/proxy/http-utils.ts
|
|
972
|
+
var CORS_HEADERS = {
|
|
973
|
+
"access-control-allow-origin": "*",
|
|
974
|
+
"access-control-allow-methods": "GET,POST,DELETE,OPTIONS",
|
|
975
|
+
"access-control-allow-headers": "Content-Type, Authorization"
|
|
976
|
+
};
|
|
977
|
+
var CORS_PREFLIGHT_HEADERS = {
|
|
978
|
+
...CORS_HEADERS,
|
|
979
|
+
"access-control-max-age": "86400"
|
|
980
|
+
};
|
|
981
|
+
function writeJson(res, status, value) {
|
|
982
|
+
res.writeHead(status, {
|
|
983
|
+
"content-type": "application/json",
|
|
984
|
+
...CORS_HEADERS
|
|
985
|
+
});
|
|
986
|
+
res.end(JSON.stringify(value));
|
|
987
|
+
}
|
|
988
|
+
function parseOptionalPositiveInteger(value) {
|
|
989
|
+
if (!value) {
|
|
990
|
+
return void 0;
|
|
991
|
+
}
|
|
992
|
+
const parsed = Number(value);
|
|
993
|
+
if (!Number.isInteger(parsed) || parsed <= 0) {
|
|
994
|
+
return void 0;
|
|
995
|
+
}
|
|
996
|
+
return parsed;
|
|
997
|
+
}
|
|
998
|
+
|
|
999
|
+
// src/proxy/json.ts
|
|
1000
|
+
function tryParseJson(text) {
|
|
1001
|
+
try {
|
|
1002
|
+
return JSON.parse(text);
|
|
1003
|
+
} catch {
|
|
1004
|
+
return void 0;
|
|
1005
|
+
}
|
|
1006
|
+
}
|
|
1007
|
+
function isObject(value) {
|
|
1008
|
+
return typeof value === "object" && value !== null;
|
|
1009
|
+
}
|
|
1010
|
+
|
|
1011
|
+
// src/proxy/jsonrpc-tracking.ts
|
|
1012
|
+
function collectJsonRpcMethods(requestBody) {
|
|
1013
|
+
const methods = /* @__PURE__ */ new Map();
|
|
1014
|
+
for (const item of normalizeJsonRpcRequest(requestBody)) {
|
|
1015
|
+
if (item.id !== void 0 && typeof item.method === "string") {
|
|
1016
|
+
methods.set(item.id, item.method);
|
|
1017
|
+
}
|
|
1018
|
+
}
|
|
1019
|
+
return methods;
|
|
1020
|
+
}
|
|
1021
|
+
function captureTrackedHashes(methodById, responseBody, handlers) {
|
|
1022
|
+
const responses = normalizeJsonRpcResponse(responseBody);
|
|
1023
|
+
for (const message of responses) {
|
|
1024
|
+
if (message.error || message.id === void 0) {
|
|
1025
|
+
continue;
|
|
1026
|
+
}
|
|
1027
|
+
const method = methodById.get(message.id);
|
|
1028
|
+
if (!method) {
|
|
1029
|
+
continue;
|
|
1030
|
+
}
|
|
1031
|
+
if (method === "new_invoice") {
|
|
1032
|
+
const paymentHash = extractInvoicePaymentHash(message.result);
|
|
1033
|
+
if (paymentHash) {
|
|
1034
|
+
handlers.onInvoiceTracked(paymentHash);
|
|
1035
|
+
}
|
|
1036
|
+
}
|
|
1037
|
+
if (method === "send_payment") {
|
|
1038
|
+
const paymentHash = extractPaymentHash(message.result);
|
|
1039
|
+
if (paymentHash) {
|
|
1040
|
+
handlers.onPaymentTracked(paymentHash);
|
|
1041
|
+
}
|
|
1042
|
+
}
|
|
1043
|
+
}
|
|
1044
|
+
}
|
|
1045
|
+
function normalizeJsonRpcRequest(body) {
|
|
1046
|
+
if (!body) {
|
|
1047
|
+
return [];
|
|
1048
|
+
}
|
|
1049
|
+
if (Array.isArray(body)) {
|
|
1050
|
+
return body.filter(isObject);
|
|
1051
|
+
}
|
|
1052
|
+
if (isObject(body)) {
|
|
1053
|
+
return [body];
|
|
1054
|
+
}
|
|
1055
|
+
return [];
|
|
1056
|
+
}
|
|
1057
|
+
function normalizeJsonRpcResponse(body) {
|
|
1058
|
+
if (!body) {
|
|
1059
|
+
return [];
|
|
1060
|
+
}
|
|
1061
|
+
if (Array.isArray(body)) {
|
|
1062
|
+
return body.filter(isObject);
|
|
1063
|
+
}
|
|
1064
|
+
if (isObject(body)) {
|
|
1065
|
+
return [body];
|
|
1066
|
+
}
|
|
1067
|
+
return [];
|
|
1068
|
+
}
|
|
1069
|
+
function extractInvoicePaymentHash(result) {
|
|
1070
|
+
if (!isObject(result)) {
|
|
1071
|
+
return void 0;
|
|
1072
|
+
}
|
|
1073
|
+
const invoice = result.invoice;
|
|
1074
|
+
if (!isObject(invoice)) {
|
|
1075
|
+
return void 0;
|
|
1076
|
+
}
|
|
1077
|
+
const data = invoice.data;
|
|
1078
|
+
if (!isObject(data)) {
|
|
1079
|
+
return void 0;
|
|
1080
|
+
}
|
|
1081
|
+
return typeof data.payment_hash === "string" ? data.payment_hash : void 0;
|
|
1082
|
+
}
|
|
1083
|
+
function extractPaymentHash(result) {
|
|
1084
|
+
if (!isObject(result)) {
|
|
1085
|
+
return void 0;
|
|
1086
|
+
}
|
|
1087
|
+
return typeof result.payment_hash === "string" ? result.payment_hash : void 0;
|
|
1088
|
+
}
|
|
1089
|
+
|
|
1090
|
+
// src/proxy/job-routes.ts
|
|
1091
|
+
async function handleJobPostEndpoint(pathname, req, res, deps) {
|
|
1092
|
+
let rawBody;
|
|
1093
|
+
try {
|
|
1094
|
+
rawBody = await readRawBody(req);
|
|
1095
|
+
} catch (error) {
|
|
1096
|
+
if (isPayloadTooLargeError(error)) {
|
|
1097
|
+
writeJson(res, 413, { error: "Request body too large" });
|
|
1098
|
+
return;
|
|
1099
|
+
}
|
|
1100
|
+
writeJson(res, 400, { error: "Failed to read request body" });
|
|
1101
|
+
return;
|
|
1102
|
+
}
|
|
1103
|
+
const body = tryParseJson(rawBody.toString("utf-8"));
|
|
1104
|
+
if (!isObject(body)) {
|
|
1105
|
+
writeJson(res, 400, { error: "Invalid JSON body" });
|
|
1106
|
+
return;
|
|
1107
|
+
}
|
|
1108
|
+
if (pathname === "/jobs/payment") {
|
|
1109
|
+
if (!deps.createPaymentJob) {
|
|
1110
|
+
writeJson(res, 404, { error: "Jobs are not enabled in runtime config" });
|
|
1111
|
+
return;
|
|
1112
|
+
}
|
|
1113
|
+
const params = body.params;
|
|
1114
|
+
if (!params) {
|
|
1115
|
+
writeJson(res, 400, { error: "Missing params for payment job" });
|
|
1116
|
+
return;
|
|
1117
|
+
}
|
|
1118
|
+
const options = body.options;
|
|
1119
|
+
const job = await deps.createPaymentJob(params, options);
|
|
1120
|
+
writeJson(res, 200, job);
|
|
1121
|
+
return;
|
|
1122
|
+
}
|
|
1123
|
+
if (pathname === "/jobs/invoice") {
|
|
1124
|
+
if (!deps.createInvoiceJob) {
|
|
1125
|
+
writeJson(res, 404, { error: "Jobs are not enabled in runtime config" });
|
|
1126
|
+
return;
|
|
1127
|
+
}
|
|
1128
|
+
const params = body.params;
|
|
1129
|
+
if (!params) {
|
|
1130
|
+
writeJson(res, 400, { error: "Missing params for invoice job" });
|
|
1131
|
+
return;
|
|
1132
|
+
}
|
|
1133
|
+
const options = body.options;
|
|
1134
|
+
const job = await deps.createInvoiceJob(params, options);
|
|
1135
|
+
writeJson(res, 200, job);
|
|
1136
|
+
return;
|
|
1137
|
+
}
|
|
1138
|
+
if (pathname === "/jobs/channel") {
|
|
1139
|
+
if (!deps.createChannelJob) {
|
|
1140
|
+
writeJson(res, 404, { error: "Jobs are not enabled in runtime config" });
|
|
1141
|
+
return;
|
|
1142
|
+
}
|
|
1143
|
+
const params = body.params;
|
|
1144
|
+
if (!params) {
|
|
1145
|
+
writeJson(res, 400, { error: "Missing params for channel job" });
|
|
1146
|
+
return;
|
|
1147
|
+
}
|
|
1148
|
+
const options = body.options;
|
|
1149
|
+
const job = await deps.createChannelJob(params, options);
|
|
1150
|
+
writeJson(res, 200, job);
|
|
1151
|
+
return;
|
|
1152
|
+
}
|
|
1153
|
+
writeJson(res, 404, { error: "Unknown jobs endpoint" });
|
|
1154
|
+
}
|
|
1155
|
+
async function handleDeleteEndpoint(req, res, deps) {
|
|
1156
|
+
const url = new URL(req.url ?? "/", "http://127.0.0.1");
|
|
1157
|
+
if (!url.pathname.startsWith("/jobs/")) {
|
|
1158
|
+
writeJson(res, 405, { error: "Method not allowed" });
|
|
1159
|
+
return;
|
|
1160
|
+
}
|
|
1161
|
+
if (!deps.cancelJob) {
|
|
1162
|
+
writeJson(res, 404, { error: "Jobs are not enabled in runtime config" });
|
|
1163
|
+
return;
|
|
1164
|
+
}
|
|
1165
|
+
const segments = url.pathname.split("/").filter(Boolean);
|
|
1166
|
+
const [, id] = segments;
|
|
1167
|
+
if (!id) {
|
|
1168
|
+
writeJson(res, 400, { error: "Missing job id" });
|
|
1169
|
+
return;
|
|
1170
|
+
}
|
|
1171
|
+
try {
|
|
1172
|
+
deps.cancelJob(id);
|
|
1173
|
+
writeJson(res, 204, {});
|
|
1174
|
+
} catch (error) {
|
|
1175
|
+
writeJson(res, 404, { error: String(error) });
|
|
1176
|
+
}
|
|
1177
|
+
}
|
|
1178
|
+
|
|
1179
|
+
// src/alerts/types.ts
|
|
1180
|
+
var alertTypeValues = [
|
|
1181
|
+
"channel_state_changed",
|
|
1182
|
+
"new_inbound_channel_request",
|
|
1183
|
+
"channel_became_ready",
|
|
1184
|
+
"channel_closing",
|
|
1185
|
+
"incoming_payment_received",
|
|
1186
|
+
"invoice_expired",
|
|
1187
|
+
"invoice_cancelled",
|
|
1188
|
+
"outgoing_payment_completed",
|
|
1189
|
+
"outgoing_payment_failed",
|
|
1190
|
+
"channel_balance_changed",
|
|
1191
|
+
"new_pending_tlc",
|
|
1192
|
+
"peer_connected",
|
|
1193
|
+
"peer_disconnected",
|
|
1194
|
+
"node_offline",
|
|
1195
|
+
"node_online",
|
|
1196
|
+
"payment_job_started",
|
|
1197
|
+
"payment_job_retrying",
|
|
1198
|
+
"payment_job_succeeded",
|
|
1199
|
+
"payment_job_failed",
|
|
1200
|
+
"invoice_job_started",
|
|
1201
|
+
"invoice_job_retrying",
|
|
1202
|
+
"invoice_job_succeeded",
|
|
1203
|
+
"invoice_job_failed",
|
|
1204
|
+
"channel_job_started",
|
|
1205
|
+
"channel_job_retrying",
|
|
1206
|
+
"channel_job_succeeded",
|
|
1207
|
+
"channel_job_failed"
|
|
1208
|
+
];
|
|
1209
|
+
var alertPriorityOrder = {
|
|
1210
|
+
low: 1,
|
|
1211
|
+
medium: 2,
|
|
1212
|
+
high: 3,
|
|
1213
|
+
critical: 4
|
|
1214
|
+
};
|
|
1215
|
+
function isAlertPriority(value) {
|
|
1216
|
+
return value === "critical" || value === "high" || value === "medium" || value === "low";
|
|
1217
|
+
}
|
|
1218
|
+
function isAlertType(value) {
|
|
1219
|
+
return alertTypeValues.includes(value);
|
|
1220
|
+
}
|
|
1221
|
+
|
|
1222
|
+
// src/proxy/monitor-routes.ts
|
|
1223
|
+
function handleMonitorEndpoint(req, res, deps) {
|
|
1224
|
+
const url = new URL(req.url ?? "/", "http://127.0.0.1");
|
|
1225
|
+
if (url.pathname === "/jobs") {
|
|
1226
|
+
if (!deps.listJobs) {
|
|
1227
|
+
writeJson(res, 404, { error: "Jobs are not enabled in runtime config" });
|
|
1228
|
+
return;
|
|
1229
|
+
}
|
|
1230
|
+
const state = url.searchParams.get("state") ?? void 0;
|
|
1231
|
+
const type = url.searchParams.get("type");
|
|
1232
|
+
const limit = parseOptionalPositiveInteger(url.searchParams.get("limit"));
|
|
1233
|
+
const offset = parseOptionalPositiveInteger(url.searchParams.get("offset"));
|
|
1234
|
+
writeJson(res, 200, {
|
|
1235
|
+
jobs: deps.listJobs({
|
|
1236
|
+
state,
|
|
1237
|
+
type: type ?? void 0,
|
|
1238
|
+
limit,
|
|
1239
|
+
offset
|
|
1240
|
+
})
|
|
1241
|
+
});
|
|
1242
|
+
return;
|
|
1243
|
+
}
|
|
1244
|
+
if (url.pathname.startsWith("/jobs/")) {
|
|
1245
|
+
if (!deps.getJob) {
|
|
1246
|
+
writeJson(res, 404, { error: "Jobs are not enabled in runtime config" });
|
|
1247
|
+
return;
|
|
1248
|
+
}
|
|
1249
|
+
const segments = url.pathname.split("/").filter(Boolean);
|
|
1250
|
+
const [, id, sub] = segments;
|
|
1251
|
+
if (!id) {
|
|
1252
|
+
writeJson(res, 400, { error: "Missing job id" });
|
|
1253
|
+
return;
|
|
1254
|
+
}
|
|
1255
|
+
if (sub === "events") {
|
|
1256
|
+
if (!deps.listJobEvents) {
|
|
1257
|
+
writeJson(res, 404, { error: "Job events not available" });
|
|
1258
|
+
return;
|
|
1259
|
+
}
|
|
1260
|
+
writeJson(res, 200, { events: deps.listJobEvents(id) });
|
|
1261
|
+
return;
|
|
1262
|
+
}
|
|
1263
|
+
const job = deps.getJob(id);
|
|
1264
|
+
if (!job) {
|
|
1265
|
+
writeJson(res, 404, { error: "Job not found" });
|
|
1266
|
+
return;
|
|
1267
|
+
}
|
|
1268
|
+
writeJson(res, 200, job);
|
|
1269
|
+
return;
|
|
1270
|
+
}
|
|
1271
|
+
if (url.pathname === "/monitor/list_tracked_invoices") {
|
|
1272
|
+
writeJson(res, 200, { invoices: deps.listTrackedInvoices() });
|
|
1273
|
+
return;
|
|
1274
|
+
}
|
|
1275
|
+
if (url.pathname === "/monitor/list_tracked_payments") {
|
|
1276
|
+
writeJson(res, 200, { payments: deps.listTrackedPayments() });
|
|
1277
|
+
return;
|
|
1278
|
+
}
|
|
1279
|
+
if (url.pathname === "/monitor/list_alerts") {
|
|
1280
|
+
const limitRaw = url.searchParams.get("limit");
|
|
1281
|
+
const minPriorityRaw = url.searchParams.get("min_priority");
|
|
1282
|
+
const typeRaw = url.searchParams.get("type");
|
|
1283
|
+
const sourceRaw = url.searchParams.get("source");
|
|
1284
|
+
const limit = parseOptionalPositiveInteger(limitRaw);
|
|
1285
|
+
if (limitRaw !== null && limit === void 0) {
|
|
1286
|
+
writeJson(res, 400, {
|
|
1287
|
+
error: "Invalid query parameter: limit must be a positive integer"
|
|
1288
|
+
});
|
|
1289
|
+
return;
|
|
1290
|
+
}
|
|
1291
|
+
if (minPriorityRaw && !isAlertPriority(minPriorityRaw)) {
|
|
1292
|
+
writeJson(res, 400, {
|
|
1293
|
+
error: "Invalid query parameter: min_priority must be one of critical|high|medium|low"
|
|
1294
|
+
});
|
|
1295
|
+
return;
|
|
1296
|
+
}
|
|
1297
|
+
if (typeRaw && !isAlertType(typeRaw)) {
|
|
1298
|
+
writeJson(res, 400, {
|
|
1299
|
+
error: "Invalid query parameter: type is not a known alert type"
|
|
1300
|
+
});
|
|
1301
|
+
return;
|
|
1302
|
+
}
|
|
1303
|
+
const minPriority = minPriorityRaw && isAlertPriority(minPriorityRaw) ? minPriorityRaw : void 0;
|
|
1304
|
+
const type = typeRaw && isAlertType(typeRaw) ? typeRaw : void 0;
|
|
1305
|
+
writeJson(res, 200, {
|
|
1306
|
+
alerts: deps.listAlerts({
|
|
1307
|
+
limit,
|
|
1308
|
+
minPriority,
|
|
1309
|
+
type,
|
|
1310
|
+
source: sourceRaw ?? void 0
|
|
1311
|
+
})
|
|
1312
|
+
});
|
|
1313
|
+
return;
|
|
1314
|
+
}
|
|
1315
|
+
if (url.pathname === "/monitor/status") {
|
|
1316
|
+
writeJson(res, 200, deps.getStatus());
|
|
1317
|
+
return;
|
|
1318
|
+
}
|
|
1319
|
+
writeJson(res, 404, { error: "Not found" });
|
|
1320
|
+
}
|
|
1321
|
+
|
|
1322
|
+
// src/proxy/rpc-proxy.ts
|
|
1323
|
+
var RpcMonitorProxy = class {
|
|
1324
|
+
config;
|
|
1325
|
+
deps;
|
|
1326
|
+
server;
|
|
1327
|
+
constructor(config, deps) {
|
|
1328
|
+
this.config = config;
|
|
1329
|
+
this.deps = deps;
|
|
1330
|
+
}
|
|
1331
|
+
async start() {
|
|
1332
|
+
if (this.server) {
|
|
1333
|
+
return;
|
|
1334
|
+
}
|
|
1335
|
+
this.server = http2.createServer((req, res) => {
|
|
1336
|
+
void this.handleRequest(req, res);
|
|
1337
|
+
});
|
|
1338
|
+
const { host, port } = parseListenAddress(this.config.listen);
|
|
1339
|
+
await new Promise((resolve2, reject) => {
|
|
1340
|
+
this.server?.once("error", reject);
|
|
1341
|
+
this.server?.listen(port, host, () => {
|
|
1342
|
+
this.server?.off("error", reject);
|
|
1343
|
+
resolve2();
|
|
1344
|
+
});
|
|
1345
|
+
});
|
|
1346
|
+
}
|
|
1347
|
+
async stop() {
|
|
1348
|
+
if (!this.server) {
|
|
1349
|
+
return;
|
|
1350
|
+
}
|
|
1351
|
+
const server = this.server;
|
|
1352
|
+
this.server = void 0;
|
|
1353
|
+
await new Promise((resolve2) => {
|
|
1354
|
+
server.close(() => resolve2());
|
|
1355
|
+
});
|
|
1356
|
+
}
|
|
1357
|
+
async handleRequest(req, res) {
|
|
1358
|
+
if (req.method === "OPTIONS") {
|
|
1359
|
+
res.writeHead(204, CORS_PREFLIGHT_HEADERS);
|
|
1360
|
+
res.end();
|
|
1361
|
+
return;
|
|
1362
|
+
}
|
|
1363
|
+
if (req.method === "GET") {
|
|
1364
|
+
handleMonitorEndpoint(req, res, this.deps);
|
|
1365
|
+
return;
|
|
1366
|
+
}
|
|
1367
|
+
if (req.method === "DELETE") {
|
|
1368
|
+
await handleDeleteEndpoint(req, res, this.deps);
|
|
1369
|
+
return;
|
|
1370
|
+
}
|
|
1371
|
+
if (req.method !== "POST") {
|
|
1372
|
+
writeJson(res, 405, { error: "Method not allowed" });
|
|
1373
|
+
return;
|
|
1374
|
+
}
|
|
1375
|
+
const url = new URL(req.url ?? "/", "http://127.0.0.1");
|
|
1376
|
+
if (url.pathname.startsWith("/jobs/")) {
|
|
1377
|
+
await handleJobPostEndpoint(url.pathname, req, res, this.deps);
|
|
1378
|
+
return;
|
|
1379
|
+
}
|
|
1380
|
+
let requestBody;
|
|
1381
|
+
try {
|
|
1382
|
+
requestBody = await readRawBody(req);
|
|
1383
|
+
} catch (error) {
|
|
1384
|
+
if (isPayloadTooLargeError(error)) {
|
|
1385
|
+
writeJson(res, 413, { error: "Request body too large" });
|
|
1386
|
+
return;
|
|
1387
|
+
}
|
|
1388
|
+
writeJson(res, 400, { error: "Failed to read request body" });
|
|
1389
|
+
return;
|
|
1390
|
+
}
|
|
1391
|
+
const requestJson = tryParseJson(requestBody.toString("utf-8"));
|
|
1392
|
+
const methodById = collectJsonRpcMethods(requestJson);
|
|
1393
|
+
let responseText = "";
|
|
1394
|
+
let responseStatus = 500;
|
|
1395
|
+
let responseHeaders = new Headers();
|
|
1396
|
+
try {
|
|
1397
|
+
const response = await fetch(this.config.targetUrl, {
|
|
1398
|
+
method: "POST",
|
|
1399
|
+
headers: {
|
|
1400
|
+
"content-type": req.headers["content-type"] ?? "application/json",
|
|
1401
|
+
...req.headers.authorization ? { authorization: req.headers.authorization } : {}
|
|
1402
|
+
},
|
|
1403
|
+
body: requestBody
|
|
1404
|
+
});
|
|
1405
|
+
responseStatus = response.status;
|
|
1406
|
+
responseHeaders = response.headers;
|
|
1407
|
+
responseText = await response.text();
|
|
1408
|
+
const responseJson = tryParseJson(responseText);
|
|
1409
|
+
captureTrackedHashes(methodById, responseJson, {
|
|
1410
|
+
onInvoiceTracked: this.deps.onInvoiceTracked,
|
|
1411
|
+
onPaymentTracked: this.deps.onPaymentTracked
|
|
1412
|
+
});
|
|
1413
|
+
} catch (error) {
|
|
1414
|
+
writeJson(res, 502, { error: `Proxy request failed: ${String(error)}` });
|
|
1415
|
+
return;
|
|
1416
|
+
}
|
|
1417
|
+
const contentType = responseHeaders.get("content-type") ?? "application/json";
|
|
1418
|
+
res.writeHead(responseStatus, {
|
|
1419
|
+
"content-type": contentType,
|
|
1420
|
+
...CORS_HEADERS
|
|
1421
|
+
});
|
|
1422
|
+
res.end(responseText);
|
|
1423
|
+
}
|
|
1424
|
+
};
|
|
1425
|
+
|
|
1426
|
+
// src/storage/memory-store.ts
|
|
1427
|
+
import { mkdir, readFile, writeFile } from "fs/promises";
|
|
1428
|
+
import { dirname as dirname2 } from "path";
|
|
1429
|
+
function nowMs() {
|
|
1430
|
+
return Date.now();
|
|
1431
|
+
}
|
|
1432
|
+
function toRecord2(items) {
|
|
1433
|
+
return Object.fromEntries(items.entries());
|
|
1434
|
+
}
|
|
1435
|
+
var MemoryStore = class {
|
|
1436
|
+
config;
|
|
1437
|
+
channelSnapshot = [];
|
|
1438
|
+
peerSnapshot = [];
|
|
1439
|
+
trackedInvoices = /* @__PURE__ */ new Map();
|
|
1440
|
+
trackedPayments = /* @__PURE__ */ new Map();
|
|
1441
|
+
alerts = [];
|
|
1442
|
+
flushTimer;
|
|
1443
|
+
constructor(config) {
|
|
1444
|
+
this.config = config;
|
|
1445
|
+
}
|
|
1446
|
+
async load() {
|
|
1447
|
+
try {
|
|
1448
|
+
const raw = await readFile(this.config.stateFilePath, "utf-8");
|
|
1449
|
+
const state = JSON.parse(raw);
|
|
1450
|
+
this.channelSnapshot = state.channels ?? [];
|
|
1451
|
+
this.peerSnapshot = state.peers ?? [];
|
|
1452
|
+
this.trackedInvoices = new Map(Object.entries(state.trackedInvoices ?? {}));
|
|
1453
|
+
this.trackedPayments = new Map(Object.entries(state.trackedPayments ?? {}));
|
|
1454
|
+
this.alerts = state.alerts ?? [];
|
|
1455
|
+
} catch {
|
|
1456
|
+
this.channelSnapshot = [];
|
|
1457
|
+
this.peerSnapshot = [];
|
|
1458
|
+
this.trackedInvoices.clear();
|
|
1459
|
+
this.trackedPayments.clear();
|
|
1460
|
+
this.alerts = [];
|
|
1461
|
+
}
|
|
1462
|
+
}
|
|
1463
|
+
async flush() {
|
|
1464
|
+
const state = {
|
|
1465
|
+
channels: this.channelSnapshot,
|
|
1466
|
+
peers: this.peerSnapshot,
|
|
1467
|
+
trackedInvoices: toRecord2(this.trackedInvoices),
|
|
1468
|
+
trackedPayments: toRecord2(this.trackedPayments),
|
|
1469
|
+
alerts: this.alerts
|
|
1470
|
+
};
|
|
1471
|
+
await mkdir(dirname2(this.config.stateFilePath), { recursive: true });
|
|
1472
|
+
await writeFile(this.config.stateFilePath, JSON.stringify(state, null, 2), "utf-8");
|
|
1473
|
+
}
|
|
1474
|
+
startAutoFlush() {
|
|
1475
|
+
this.stopAutoFlush();
|
|
1476
|
+
this.flushTimer = setInterval(() => {
|
|
1477
|
+
void this.flush();
|
|
1478
|
+
}, this.config.flushIntervalMs);
|
|
1479
|
+
}
|
|
1480
|
+
stopAutoFlush() {
|
|
1481
|
+
if (this.flushTimer) {
|
|
1482
|
+
clearInterval(this.flushTimer);
|
|
1483
|
+
this.flushTimer = void 0;
|
|
1484
|
+
}
|
|
1485
|
+
}
|
|
1486
|
+
pruneCompleted(ttlMs) {
|
|
1487
|
+
const now = nowMs();
|
|
1488
|
+
for (const [hash, entry] of this.trackedInvoices.entries()) {
|
|
1489
|
+
if (entry.completedAt && now - entry.completedAt >= ttlMs) {
|
|
1490
|
+
this.trackedInvoices.delete(hash);
|
|
1491
|
+
}
|
|
1492
|
+
}
|
|
1493
|
+
for (const [hash, entry] of this.trackedPayments.entries()) {
|
|
1494
|
+
if (entry.completedAt && now - entry.completedAt >= ttlMs) {
|
|
1495
|
+
this.trackedPayments.delete(hash);
|
|
1496
|
+
}
|
|
1497
|
+
}
|
|
1498
|
+
}
|
|
1499
|
+
getChannelSnapshot() {
|
|
1500
|
+
return this.channelSnapshot;
|
|
1501
|
+
}
|
|
1502
|
+
setChannelSnapshot(channels) {
|
|
1503
|
+
this.channelSnapshot = channels;
|
|
1504
|
+
}
|
|
1505
|
+
getPeerSnapshot() {
|
|
1506
|
+
return this.peerSnapshot;
|
|
1507
|
+
}
|
|
1508
|
+
setPeerSnapshot(peers) {
|
|
1509
|
+
this.peerSnapshot = peers;
|
|
1510
|
+
}
|
|
1511
|
+
addTrackedInvoice(hash, status = "Open") {
|
|
1512
|
+
const existing = this.trackedInvoices.get(hash);
|
|
1513
|
+
if (existing) return;
|
|
1514
|
+
const now = nowMs();
|
|
1515
|
+
this.trackedInvoices.set(hash, {
|
|
1516
|
+
paymentHash: hash,
|
|
1517
|
+
status,
|
|
1518
|
+
trackedAt: now,
|
|
1519
|
+
updatedAt: now
|
|
1520
|
+
});
|
|
1521
|
+
}
|
|
1522
|
+
listTrackedInvoices() {
|
|
1523
|
+
return [...this.trackedInvoices.values()];
|
|
1524
|
+
}
|
|
1525
|
+
getTrackedInvoice(hash) {
|
|
1526
|
+
return this.trackedInvoices.get(hash);
|
|
1527
|
+
}
|
|
1528
|
+
updateTrackedInvoice(hash, status) {
|
|
1529
|
+
const now = nowMs();
|
|
1530
|
+
const existing = this.trackedInvoices.get(hash);
|
|
1531
|
+
const next = existing ? {
|
|
1532
|
+
...existing,
|
|
1533
|
+
status,
|
|
1534
|
+
updatedAt: now,
|
|
1535
|
+
completedAt: isTerminalInvoiceStatus(status) ? existing.completedAt ?? now : void 0
|
|
1536
|
+
} : {
|
|
1537
|
+
paymentHash: hash,
|
|
1538
|
+
status,
|
|
1539
|
+
trackedAt: now,
|
|
1540
|
+
updatedAt: now,
|
|
1541
|
+
completedAt: isTerminalInvoiceStatus(status) ? now : void 0
|
|
1542
|
+
};
|
|
1543
|
+
this.trackedInvoices.set(hash, next);
|
|
1544
|
+
}
|
|
1545
|
+
addTrackedPayment(hash, status = "Created") {
|
|
1546
|
+
const existing = this.trackedPayments.get(hash);
|
|
1547
|
+
if (existing) return;
|
|
1548
|
+
const now = nowMs();
|
|
1549
|
+
this.trackedPayments.set(hash, {
|
|
1550
|
+
paymentHash: hash,
|
|
1551
|
+
status,
|
|
1552
|
+
trackedAt: now,
|
|
1553
|
+
updatedAt: now
|
|
1554
|
+
});
|
|
1555
|
+
}
|
|
1556
|
+
listTrackedPayments() {
|
|
1557
|
+
return [...this.trackedPayments.values()];
|
|
1558
|
+
}
|
|
1559
|
+
getTrackedPayment(hash) {
|
|
1560
|
+
return this.trackedPayments.get(hash);
|
|
1561
|
+
}
|
|
1562
|
+
updateTrackedPayment(hash, status) {
|
|
1563
|
+
const now = nowMs();
|
|
1564
|
+
const existing = this.trackedPayments.get(hash);
|
|
1565
|
+
const next = existing ? {
|
|
1566
|
+
...existing,
|
|
1567
|
+
status,
|
|
1568
|
+
updatedAt: now,
|
|
1569
|
+
completedAt: isTerminalPaymentStatus(status) ? existing.completedAt ?? now : void 0
|
|
1570
|
+
} : {
|
|
1571
|
+
paymentHash: hash,
|
|
1572
|
+
status,
|
|
1573
|
+
trackedAt: now,
|
|
1574
|
+
updatedAt: now,
|
|
1575
|
+
completedAt: isTerminalPaymentStatus(status) ? now : void 0
|
|
1576
|
+
};
|
|
1577
|
+
this.trackedPayments.set(hash, next);
|
|
1578
|
+
}
|
|
1579
|
+
addAlert(alert) {
|
|
1580
|
+
this.alerts.push(alert);
|
|
1581
|
+
if (this.alerts.length > this.config.maxAlertHistory) {
|
|
1582
|
+
this.alerts.splice(0, this.alerts.length - this.config.maxAlertHistory);
|
|
1583
|
+
}
|
|
1584
|
+
}
|
|
1585
|
+
listAlerts(filters) {
|
|
1586
|
+
const minPriority = filters?.minPriority;
|
|
1587
|
+
const type = filters?.type;
|
|
1588
|
+
const source = filters?.source;
|
|
1589
|
+
const limit = filters?.limit;
|
|
1590
|
+
let alerts = this.alerts;
|
|
1591
|
+
if (minPriority) {
|
|
1592
|
+
const minRank = alertPriorityOrder[minPriority];
|
|
1593
|
+
alerts = alerts.filter((alert) => alertPriorityOrder[alert.priority] >= minRank);
|
|
1594
|
+
}
|
|
1595
|
+
if (type) {
|
|
1596
|
+
alerts = alerts.filter((alert) => alert.type === type);
|
|
1597
|
+
}
|
|
1598
|
+
if (source) {
|
|
1599
|
+
alerts = alerts.filter((alert) => alert.source === source);
|
|
1600
|
+
}
|
|
1601
|
+
if (!limit || limit <= 0) {
|
|
1602
|
+
return [...alerts];
|
|
1603
|
+
}
|
|
1604
|
+
return alerts.slice(-limit);
|
|
1605
|
+
}
|
|
1606
|
+
};
|
|
1607
|
+
function isTerminalInvoiceStatus(status) {
|
|
1608
|
+
return status === "Paid" || status === "Cancelled" || status === "Expired";
|
|
1609
|
+
}
|
|
1610
|
+
function isTerminalPaymentStatus(status) {
|
|
1611
|
+
return status === "Success" || status === "Failed";
|
|
1612
|
+
}
|
|
1613
|
+
|
|
1614
|
+
// src/storage/sqlite-store.ts
|
|
1615
|
+
import Database from "better-sqlite3";
|
|
1616
|
+
import { randomUUID as randomUUID2 } from "crypto";
|
|
1617
|
+
import { mkdirSync as mkdirSync2 } from "fs";
|
|
1618
|
+
import { dirname as dirname3 } from "path";
|
|
1619
|
+
var MIGRATIONS = [
|
|
1620
|
+
{
|
|
1621
|
+
version: 1,
|
|
1622
|
+
sql: `
|
|
1623
|
+
CREATE TABLE IF NOT EXISTS jobs (
|
|
1624
|
+
id TEXT PRIMARY KEY,
|
|
1625
|
+
type TEXT NOT NULL,
|
|
1626
|
+
state TEXT NOT NULL,
|
|
1627
|
+
params TEXT NOT NULL,
|
|
1628
|
+
result TEXT,
|
|
1629
|
+
error TEXT,
|
|
1630
|
+
retry_count INTEGER NOT NULL DEFAULT 0,
|
|
1631
|
+
max_retries INTEGER NOT NULL DEFAULT 3,
|
|
1632
|
+
next_retry_at INTEGER,
|
|
1633
|
+
idempotency_key TEXT NOT NULL UNIQUE,
|
|
1634
|
+
created_at INTEGER NOT NULL,
|
|
1635
|
+
updated_at INTEGER NOT NULL,
|
|
1636
|
+
completed_at INTEGER
|
|
1637
|
+
);
|
|
1638
|
+
|
|
1639
|
+
CREATE INDEX IF NOT EXISTS idx_jobs_state ON jobs(state);
|
|
1640
|
+
CREATE INDEX IF NOT EXISTS idx_jobs_type ON jobs(type);
|
|
1641
|
+
CREATE INDEX IF NOT EXISTS idx_jobs_next_retry_at ON jobs(next_retry_at);
|
|
1642
|
+
|
|
1643
|
+
CREATE TABLE IF NOT EXISTS job_events (
|
|
1644
|
+
id TEXT PRIMARY KEY,
|
|
1645
|
+
job_id TEXT NOT NULL REFERENCES jobs(id) ON DELETE CASCADE,
|
|
1646
|
+
event_type TEXT NOT NULL,
|
|
1647
|
+
from_state TEXT,
|
|
1648
|
+
to_state TEXT,
|
|
1649
|
+
data TEXT,
|
|
1650
|
+
created_at INTEGER NOT NULL
|
|
1651
|
+
);
|
|
1652
|
+
|
|
1653
|
+
CREATE INDEX IF NOT EXISTS idx_job_events_job_id ON job_events(job_id);
|
|
1654
|
+
`
|
|
1655
|
+
}
|
|
1656
|
+
];
|
|
1657
|
+
var SqliteJobStore = class {
|
|
1658
|
+
db;
|
|
1659
|
+
constructor(dbPath) {
|
|
1660
|
+
mkdirSync2(dirname3(dbPath), { recursive: true });
|
|
1661
|
+
this.db = new Database(dbPath);
|
|
1662
|
+
this.db.pragma("journal_mode = WAL");
|
|
1663
|
+
this.db.pragma("foreign_keys = ON");
|
|
1664
|
+
this.runMigrations();
|
|
1665
|
+
}
|
|
1666
|
+
// ─── Migrations ─────────────────────────────────────────────────────────────
|
|
1667
|
+
runMigrations() {
|
|
1668
|
+
this.db.exec(`
|
|
1669
|
+
CREATE TABLE IF NOT EXISTS schema_migrations (
|
|
1670
|
+
version INTEGER PRIMARY KEY,
|
|
1671
|
+
applied_at INTEGER NOT NULL
|
|
1672
|
+
);
|
|
1673
|
+
`);
|
|
1674
|
+
const applied = new Set(
|
|
1675
|
+
this.db.prepare("SELECT version FROM schema_migrations").all().map(
|
|
1676
|
+
(r) => r.version
|
|
1677
|
+
)
|
|
1678
|
+
);
|
|
1679
|
+
for (const migration of MIGRATIONS) {
|
|
1680
|
+
if (!applied.has(migration.version)) {
|
|
1681
|
+
this.db.transaction(() => {
|
|
1682
|
+
this.db.exec(migration.sql);
|
|
1683
|
+
this.db.prepare("INSERT INTO schema_migrations (version, applied_at) VALUES (?, ?)").run(
|
|
1684
|
+
migration.version,
|
|
1685
|
+
Date.now()
|
|
1686
|
+
);
|
|
1687
|
+
})();
|
|
1688
|
+
}
|
|
1689
|
+
}
|
|
1690
|
+
}
|
|
1691
|
+
// ─── Job CRUD ────────────────────────────────────────────────────────────────
|
|
1692
|
+
createJob(job) {
|
|
1693
|
+
const now = Date.now();
|
|
1694
|
+
const full = {
|
|
1695
|
+
...job,
|
|
1696
|
+
id: randomUUID2(),
|
|
1697
|
+
createdAt: now,
|
|
1698
|
+
updatedAt: now
|
|
1699
|
+
};
|
|
1700
|
+
this.db.prepare(
|
|
1701
|
+
`INSERT INTO jobs
|
|
1702
|
+
(id, type, state, params, result, error, retry_count, max_retries,
|
|
1703
|
+
next_retry_at, idempotency_key, created_at, updated_at, completed_at)
|
|
1704
|
+
VALUES
|
|
1705
|
+
(@id, @type, @state, @params, @result, @error, @retry_count, @max_retries,
|
|
1706
|
+
@next_retry_at, @idempotency_key, @created_at, @updated_at, @completed_at)`
|
|
1707
|
+
).run({
|
|
1708
|
+
id: full.id,
|
|
1709
|
+
type: full.type,
|
|
1710
|
+
state: full.state,
|
|
1711
|
+
params: JSON.stringify(full.params),
|
|
1712
|
+
result: full.result !== void 0 ? JSON.stringify(full.result) : null,
|
|
1713
|
+
error: full.error !== void 0 ? JSON.stringify(full.error) : null,
|
|
1714
|
+
retry_count: full.retryCount,
|
|
1715
|
+
max_retries: full.maxRetries,
|
|
1716
|
+
next_retry_at: full.nextRetryAt ?? null,
|
|
1717
|
+
idempotency_key: full.idempotencyKey,
|
|
1718
|
+
created_at: full.createdAt,
|
|
1719
|
+
updated_at: full.updatedAt,
|
|
1720
|
+
completed_at: full.completedAt ?? null
|
|
1721
|
+
});
|
|
1722
|
+
return full;
|
|
1723
|
+
}
|
|
1724
|
+
updateJob(id, updates) {
|
|
1725
|
+
const existing = this.getJob(id);
|
|
1726
|
+
if (!existing) throw new Error(`Job not found: ${id}`);
|
|
1727
|
+
const now = Date.now();
|
|
1728
|
+
const merged = { ...existing, ...updates, updatedAt: now };
|
|
1729
|
+
this.db.prepare(
|
|
1730
|
+
`UPDATE jobs SET
|
|
1731
|
+
state = @state,
|
|
1732
|
+
params = @params,
|
|
1733
|
+
result = @result,
|
|
1734
|
+
error = @error,
|
|
1735
|
+
retry_count = @retry_count,
|
|
1736
|
+
next_retry_at = @next_retry_at,
|
|
1737
|
+
updated_at = @updated_at,
|
|
1738
|
+
completed_at = @completed_at
|
|
1739
|
+
WHERE id = @id`
|
|
1740
|
+
).run({
|
|
1741
|
+
id: merged.id,
|
|
1742
|
+
state: merged.state,
|
|
1743
|
+
params: JSON.stringify(merged.params),
|
|
1744
|
+
result: merged.result !== void 0 ? JSON.stringify(merged.result) : null,
|
|
1745
|
+
error: merged.error !== void 0 ? JSON.stringify(merged.error) : null,
|
|
1746
|
+
retry_count: merged.retryCount,
|
|
1747
|
+
next_retry_at: merged.nextRetryAt ?? null,
|
|
1748
|
+
updated_at: merged.updatedAt,
|
|
1749
|
+
completed_at: merged.completedAt ?? null
|
|
1750
|
+
});
|
|
1751
|
+
return merged;
|
|
1752
|
+
}
|
|
1753
|
+
getJob(id) {
|
|
1754
|
+
const row = this.db.prepare("SELECT * FROM jobs WHERE id = ?").get(id);
|
|
1755
|
+
return row ? this.rowToJob(row) : void 0;
|
|
1756
|
+
}
|
|
1757
|
+
getJobByIdempotencyKey(key) {
|
|
1758
|
+
const row = this.db.prepare("SELECT * FROM jobs WHERE idempotency_key = ?").get(key);
|
|
1759
|
+
return row ? this.rowToJob(row) : void 0;
|
|
1760
|
+
}
|
|
1761
|
+
listJobs(filter = {}) {
|
|
1762
|
+
const conditions = [];
|
|
1763
|
+
const params = {};
|
|
1764
|
+
if (filter.type) {
|
|
1765
|
+
conditions.push("type = @type");
|
|
1766
|
+
params.type = filter.type;
|
|
1767
|
+
}
|
|
1768
|
+
if (filter.state) {
|
|
1769
|
+
const states = Array.isArray(filter.state) ? filter.state : [filter.state];
|
|
1770
|
+
const placeholders = states.map((_, i) => `@state_${i}`).join(", ");
|
|
1771
|
+
conditions.push(`state IN (${placeholders})`);
|
|
1772
|
+
for (const [i, s] of states.entries()) {
|
|
1773
|
+
params[`state_${i}`] = s;
|
|
1774
|
+
}
|
|
1775
|
+
}
|
|
1776
|
+
const where = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
|
|
1777
|
+
const limit = filter.limit ? `LIMIT @limit` : "";
|
|
1778
|
+
const offset = filter.offset ? `OFFSET @offset` : "";
|
|
1779
|
+
if (filter.limit) params.limit = filter.limit;
|
|
1780
|
+
if (filter.offset) params.offset = filter.offset;
|
|
1781
|
+
const rows = this.db.prepare(`SELECT * FROM jobs ${where} ORDER BY created_at DESC ${limit} ${offset}`).all(params);
|
|
1782
|
+
return rows.map((r) => this.rowToJob(r));
|
|
1783
|
+
}
|
|
1784
|
+
deleteJob(id) {
|
|
1785
|
+
this.db.prepare("DELETE FROM jobs WHERE id = ?").run(id);
|
|
1786
|
+
}
|
|
1787
|
+
/** Return jobs that are ready to be retried right now. */
|
|
1788
|
+
getRetryableJobs(now = Date.now()) {
|
|
1789
|
+
const rows = this.db.prepare(
|
|
1790
|
+
`SELECT * FROM jobs
|
|
1791
|
+
WHERE state = 'waiting_retry' AND next_retry_at <= @now
|
|
1792
|
+
ORDER BY next_retry_at ASC`
|
|
1793
|
+
).all({ now });
|
|
1794
|
+
return rows.map((r) => this.rowToJob(r));
|
|
1795
|
+
}
|
|
1796
|
+
/** Return jobs in non-terminal states (for recovery after daemon restart). */
|
|
1797
|
+
getInProgressJobs() {
|
|
1798
|
+
const rows = this.db.prepare(
|
|
1799
|
+
`SELECT * FROM jobs
|
|
1800
|
+
WHERE state IN (
|
|
1801
|
+
'queued',
|
|
1802
|
+
'executing',
|
|
1803
|
+
'inflight',
|
|
1804
|
+
'waiting_retry',
|
|
1805
|
+
'invoice_created',
|
|
1806
|
+
'invoice_active',
|
|
1807
|
+
'invoice_received',
|
|
1808
|
+
'channel_opening',
|
|
1809
|
+
'channel_accepting',
|
|
1810
|
+
'channel_abandoning',
|
|
1811
|
+
'channel_updating',
|
|
1812
|
+
'channel_awaiting_ready',
|
|
1813
|
+
'channel_closing'
|
|
1814
|
+
)
|
|
1815
|
+
ORDER BY created_at ASC`
|
|
1816
|
+
).all();
|
|
1817
|
+
return rows.map((r) => this.rowToJob(r));
|
|
1818
|
+
}
|
|
1819
|
+
// ─── Job Events ──────────────────────────────────────────────────────────────
|
|
1820
|
+
addJobEvent(jobId, eventType, fromState, toState, data) {
|
|
1821
|
+
const event = {
|
|
1822
|
+
id: randomUUID2(),
|
|
1823
|
+
jobId,
|
|
1824
|
+
eventType,
|
|
1825
|
+
fromState,
|
|
1826
|
+
toState,
|
|
1827
|
+
data,
|
|
1828
|
+
createdAt: Date.now()
|
|
1829
|
+
};
|
|
1830
|
+
this.db.prepare(
|
|
1831
|
+
`INSERT INTO job_events (id, job_id, event_type, from_state, to_state, data, created_at)
|
|
1832
|
+
VALUES (@id, @job_id, @event_type, @from_state, @to_state, @data, @created_at)`
|
|
1833
|
+
).run({
|
|
1834
|
+
id: event.id,
|
|
1835
|
+
job_id: event.jobId,
|
|
1836
|
+
event_type: event.eventType,
|
|
1837
|
+
from_state: event.fromState ?? null,
|
|
1838
|
+
to_state: event.toState ?? null,
|
|
1839
|
+
data: event.data !== void 0 ? JSON.stringify(event.data) : null,
|
|
1840
|
+
created_at: event.createdAt
|
|
1841
|
+
});
|
|
1842
|
+
return event;
|
|
1843
|
+
}
|
|
1844
|
+
listJobEvents(jobId) {
|
|
1845
|
+
const rows = this.db.prepare("SELECT * FROM job_events WHERE job_id = ? ORDER BY created_at ASC").all(jobId);
|
|
1846
|
+
return rows.map((r) => ({
|
|
1847
|
+
id: r.id,
|
|
1848
|
+
jobId: r.job_id,
|
|
1849
|
+
eventType: r.event_type,
|
|
1850
|
+
fromState: r.from_state ?? void 0,
|
|
1851
|
+
toState: r.to_state ?? void 0,
|
|
1852
|
+
data: r.data ? JSON.parse(r.data) : void 0,
|
|
1853
|
+
createdAt: r.created_at
|
|
1854
|
+
}));
|
|
1855
|
+
}
|
|
1856
|
+
// ─── Lifecycle ───────────────────────────────────────────────────────────────
|
|
1857
|
+
close() {
|
|
1858
|
+
this.db.close();
|
|
1859
|
+
}
|
|
1860
|
+
// ─── Private Helpers ─────────────────────────────────────────────────────────
|
|
1861
|
+
rowToJob(row) {
|
|
1862
|
+
return {
|
|
1863
|
+
id: row.id,
|
|
1864
|
+
type: row.type,
|
|
1865
|
+
state: row.state,
|
|
1866
|
+
params: JSON.parse(row.params),
|
|
1867
|
+
result: row.result ? JSON.parse(row.result) : void 0,
|
|
1868
|
+
error: row.error ? JSON.parse(row.error) : void 0,
|
|
1869
|
+
retryCount: row.retry_count,
|
|
1870
|
+
maxRetries: row.max_retries,
|
|
1871
|
+
nextRetryAt: row.next_retry_at ?? void 0,
|
|
1872
|
+
idempotencyKey: row.idempotency_key,
|
|
1873
|
+
createdAt: row.created_at,
|
|
1874
|
+
updatedAt: row.updated_at,
|
|
1875
|
+
completedAt: row.completed_at ?? void 0
|
|
1876
|
+
};
|
|
1877
|
+
}
|
|
1878
|
+
};
|
|
1879
|
+
|
|
1880
|
+
// src/jobs/job-manager.ts
|
|
1881
|
+
import { EventEmitter } from "events";
|
|
1882
|
+
import { randomUUID as randomUUID3 } from "crypto";
|
|
1883
|
+
|
|
1884
|
+
// src/jobs/types.ts
|
|
1885
|
+
var TERMINAL_JOB_STATES = /* @__PURE__ */ new Set(["succeeded", "failed", "cancelled"]);
|
|
1886
|
+
|
|
1887
|
+
// src/jobs/error-classifier.ts
|
|
1888
|
+
var PATTERNS = [
|
|
1889
|
+
// Routing / topology
|
|
1890
|
+
{ pattern: /no path found|no route|route not found/i, category: "no_route", retryable: true },
|
|
1891
|
+
{ pattern: /no outgoing channel|no available channel/i, category: "no_route", retryable: true },
|
|
1892
|
+
// Liquidity
|
|
1893
|
+
{ pattern: /insufficient (balance|capacity|funds)/i, category: "insufficient_balance", retryable: false },
|
|
1894
|
+
{ pattern: /amount.*too large|exceeds.*capacity/i, category: "amount_too_large", retryable: false },
|
|
1895
|
+
// Invoice lifecycle
|
|
1896
|
+
{ pattern: /invoice.*expir|expir.*invoice/i, category: "invoice_expired", retryable: false },
|
|
1897
|
+
{ pattern: /invoice.*cancel|cancel.*invoice/i, category: "invoice_cancelled", retryable: false },
|
|
1898
|
+
{ pattern: /payment hash.*exist|payment_hash.*exist|duplicated payment hash/i, category: "invalid_payment", retryable: false },
|
|
1899
|
+
// Peer / connectivity
|
|
1900
|
+
{ pattern: /peer.*offline|peer.*unreachable|peer.*disconnect/i, category: "peer_offline", retryable: true },
|
|
1901
|
+
{ pattern: /connection.*refused|connection.*reset/i, category: "peer_offline", retryable: true },
|
|
1902
|
+
{ pattern: /peer.*feature not found|waiting for peer to send init message/i, category: "peer_offline", retryable: true },
|
|
1903
|
+
{ pattern: /channel.*already.*exist|duplicat(e|ed).*channel/i, category: "temporary_failure", retryable: true },
|
|
1904
|
+
// Timeout
|
|
1905
|
+
{ pattern: /timeout|timed out/i, category: "timeout", retryable: true },
|
|
1906
|
+
// Payment validity
|
|
1907
|
+
{ pattern: /invalid.*invoice|malformed.*invoice/i, category: "invalid_payment", retryable: false },
|
|
1908
|
+
{ pattern: /payment.*hash.*mismatch|preimage.*invalid/i, category: "invalid_payment", retryable: false },
|
|
1909
|
+
// Generic temporary
|
|
1910
|
+
{ pattern: /temporary.*failure|try again|retry/i, category: "temporary_failure", retryable: true }
|
|
1911
|
+
];
|
|
1912
|
+
function classifyRpcError(error, failedError) {
|
|
1913
|
+
const raw = failedError ?? (error instanceof Error ? error.message : String(error));
|
|
1914
|
+
for (const { pattern, category, retryable } of PATTERNS) {
|
|
1915
|
+
if (pattern.test(raw)) {
|
|
1916
|
+
return { category, retryable, message: raw, rawError: raw };
|
|
1917
|
+
}
|
|
1918
|
+
}
|
|
1919
|
+
return {
|
|
1920
|
+
category: "unknown",
|
|
1921
|
+
retryable: false,
|
|
1922
|
+
// safe default: don't retry unknowns
|
|
1923
|
+
message: raw,
|
|
1924
|
+
rawError: raw
|
|
1925
|
+
};
|
|
1926
|
+
}
|
|
1927
|
+
|
|
1928
|
+
// src/jobs/state-machine.ts
|
|
1929
|
+
var PAYMENT_TRANSITIONS = [
|
|
1930
|
+
{ from: "queued", event: "send_issued", to: "executing" },
|
|
1931
|
+
{ from: "executing", event: "payment_inflight", to: "inflight" },
|
|
1932
|
+
{ from: "executing", event: "payment_success", to: "succeeded" },
|
|
1933
|
+
{ from: "executing", event: "payment_failed_retryable", to: "waiting_retry" },
|
|
1934
|
+
{ from: "executing", event: "payment_failed_permanent", to: "failed" },
|
|
1935
|
+
{ from: "inflight", event: "payment_success", to: "succeeded" },
|
|
1936
|
+
{ from: "inflight", event: "payment_failed_retryable", to: "waiting_retry" },
|
|
1937
|
+
{ from: "inflight", event: "payment_failed_permanent", to: "failed" },
|
|
1938
|
+
{ from: "waiting_retry", event: "retry_delay_elapsed", to: "executing" },
|
|
1939
|
+
{
|
|
1940
|
+
from: ["queued", "executing", "inflight", "waiting_retry"],
|
|
1941
|
+
event: "cancel",
|
|
1942
|
+
to: "cancelled"
|
|
1943
|
+
}
|
|
1944
|
+
];
|
|
1945
|
+
var INVOICE_TRANSITIONS = [
|
|
1946
|
+
{ from: "queued", event: "send_issued", to: "executing" },
|
|
1947
|
+
{ from: "executing", event: "invoice_created", to: "invoice_created" },
|
|
1948
|
+
{ from: "invoice_created", event: "payment_success", to: "succeeded" },
|
|
1949
|
+
{ from: "executing", event: "payment_failed_retryable", to: "waiting_retry" },
|
|
1950
|
+
{ from: "invoice_created", event: "payment_failed_retryable", to: "waiting_retry" },
|
|
1951
|
+
{ from: "invoice_active", event: "payment_failed_retryable", to: "waiting_retry" },
|
|
1952
|
+
{ from: "invoice_received", event: "payment_failed_retryable", to: "waiting_retry" },
|
|
1953
|
+
{ from: "waiting_retry", event: "retry_delay_elapsed", to: "executing" },
|
|
1954
|
+
{ from: "invoice_created", event: "invoice_received", to: "invoice_received" },
|
|
1955
|
+
{ from: "invoice_created", event: "invoice_settled", to: "invoice_settled" },
|
|
1956
|
+
{ from: "invoice_active", event: "invoice_received", to: "invoice_received" },
|
|
1957
|
+
{ from: "invoice_active", event: "invoice_settled", to: "invoice_settled" },
|
|
1958
|
+
{ from: "invoice_active", event: "invoice_expired", to: "invoice_expired" },
|
|
1959
|
+
{ from: "invoice_active", event: "invoice_cancelled", to: "invoice_cancelled" },
|
|
1960
|
+
{ from: "invoice_received", event: "invoice_settled", to: "invoice_settled" },
|
|
1961
|
+
{ from: ["invoice_created", "invoice_received"], event: "invoice_expired", to: "invoice_expired" },
|
|
1962
|
+
{ from: ["invoice_created", "invoice_received"], event: "invoice_cancelled", to: "invoice_cancelled" },
|
|
1963
|
+
{ from: ["invoice_settled", "invoice_expired", "invoice_cancelled"], event: "payment_success", to: "succeeded" },
|
|
1964
|
+
{ from: ["invoice_expired", "invoice_cancelled"], event: "payment_failed_permanent", to: "failed" },
|
|
1965
|
+
{ from: ["executing", "invoice_created", "invoice_active", "invoice_received"], event: "payment_failed_permanent", to: "failed" },
|
|
1966
|
+
{
|
|
1967
|
+
from: ["queued", "executing", "waiting_retry", "invoice_created", "invoice_active", "invoice_received"],
|
|
1968
|
+
event: "cancel",
|
|
1969
|
+
to: "cancelled"
|
|
1970
|
+
}
|
|
1971
|
+
];
|
|
1972
|
+
var CHANNEL_TRANSITIONS = [
|
|
1973
|
+
{ from: "queued", event: "send_issued", to: "executing" },
|
|
1974
|
+
{ from: "executing", event: "channel_opening", to: "channel_opening" },
|
|
1975
|
+
{ from: "executing", event: "channel_accepting", to: "channel_accepting" },
|
|
1976
|
+
{ from: "executing", event: "channel_abandoning", to: "channel_abandoning" },
|
|
1977
|
+
{ from: "executing", event: "channel_updating", to: "channel_updating" },
|
|
1978
|
+
{ from: "channel_opening", event: "payment_success", to: "succeeded" },
|
|
1979
|
+
{ from: "channel_accepting", event: "payment_success", to: "succeeded" },
|
|
1980
|
+
{ from: "channel_abandoning", event: "payment_success", to: "succeeded" },
|
|
1981
|
+
{ from: "channel_updating", event: "payment_success", to: "succeeded" },
|
|
1982
|
+
{ from: "executing", event: "payment_failed_retryable", to: "waiting_retry" },
|
|
1983
|
+
{ from: "channel_opening", event: "payment_failed_retryable", to: "waiting_retry" },
|
|
1984
|
+
{ from: "channel_accepting", event: "payment_failed_retryable", to: "waiting_retry" },
|
|
1985
|
+
{ from: "channel_abandoning", event: "payment_failed_retryable", to: "waiting_retry" },
|
|
1986
|
+
{ from: "channel_updating", event: "payment_failed_retryable", to: "waiting_retry" },
|
|
1987
|
+
{ from: "channel_awaiting_ready", event: "payment_failed_retryable", to: "waiting_retry" },
|
|
1988
|
+
{ from: "channel_closing", event: "payment_failed_retryable", to: "waiting_retry" },
|
|
1989
|
+
{ from: "executing", event: "payment_failed_permanent", to: "failed" },
|
|
1990
|
+
{ from: "channel_opening", event: "payment_failed_permanent", to: "failed" },
|
|
1991
|
+
{ from: "channel_accepting", event: "payment_failed_permanent", to: "failed" },
|
|
1992
|
+
{ from: "channel_abandoning", event: "payment_failed_permanent", to: "failed" },
|
|
1993
|
+
{ from: "channel_updating", event: "payment_failed_permanent", to: "failed" },
|
|
1994
|
+
{ from: "channel_awaiting_ready", event: "payment_failed_permanent", to: "failed" },
|
|
1995
|
+
{ from: "channel_closing", event: "payment_failed_permanent", to: "failed" },
|
|
1996
|
+
{ from: ["executing", "channel_ready"], event: "channel_closing", to: "channel_closing" },
|
|
1997
|
+
{ from: "waiting_retry", event: "retry_delay_elapsed", to: "executing" },
|
|
1998
|
+
{ from: "channel_opening", event: "channel_opening", to: "channel_awaiting_ready" },
|
|
1999
|
+
{ from: "channel_awaiting_ready", event: "channel_opening", to: "channel_awaiting_ready" },
|
|
2000
|
+
{ from: "channel_opening", event: "channel_ready", to: "channel_ready" },
|
|
2001
|
+
{ from: "channel_awaiting_ready", event: "channel_ready", to: "channel_ready" },
|
|
2002
|
+
{ from: ["channel_opening", "channel_ready"], event: "channel_closed", to: "channel_closed" },
|
|
2003
|
+
{ from: "channel_awaiting_ready", event: "channel_closed", to: "channel_closed" },
|
|
2004
|
+
{ from: "channel_closing", event: "channel_closed", to: "channel_closed" },
|
|
2005
|
+
{ from: "channel_closing", event: "payment_success", to: "succeeded" },
|
|
2006
|
+
{ from: "channel_closed", event: "payment_failed_permanent", to: "failed" },
|
|
2007
|
+
{ from: ["channel_ready", "channel_opening"], event: "channel_failed", to: "failed" },
|
|
2008
|
+
{ from: ["channel_ready", "channel_closed"], event: "payment_success", to: "succeeded" },
|
|
2009
|
+
{
|
|
2010
|
+
from: ["queued", "executing", "waiting_retry", "channel_opening", "channel_accepting", "channel_abandoning", "channel_updating", "channel_awaiting_ready", "channel_ready", "channel_closing"],
|
|
2011
|
+
event: "cancel",
|
|
2012
|
+
to: "cancelled"
|
|
2013
|
+
}
|
|
2014
|
+
];
|
|
2015
|
+
var JobStateMachine = class {
|
|
2016
|
+
table;
|
|
2017
|
+
constructor(transitions) {
|
|
2018
|
+
this.table = /* @__PURE__ */ new Map();
|
|
2019
|
+
for (const t of transitions) {
|
|
2020
|
+
const froms = Array.isArray(t.from) ? t.from : [t.from];
|
|
2021
|
+
for (const from of froms) {
|
|
2022
|
+
this.table.set(`${from}:${t.event}`, t.to);
|
|
2023
|
+
}
|
|
2024
|
+
}
|
|
2025
|
+
}
|
|
2026
|
+
transition(current, event) {
|
|
2027
|
+
return this.table.get(`${current}:${event}`) ?? null;
|
|
2028
|
+
}
|
|
2029
|
+
isTerminal(state) {
|
|
2030
|
+
return state === "succeeded" || state === "failed" || state === "cancelled";
|
|
2031
|
+
}
|
|
2032
|
+
};
|
|
2033
|
+
var paymentStateMachine = new JobStateMachine(PAYMENT_TRANSITIONS);
|
|
2034
|
+
var invoiceStateMachine = new JobStateMachine(INVOICE_TRANSITIONS);
|
|
2035
|
+
var channelStateMachine = new JobStateMachine(CHANNEL_TRANSITIONS);
|
|
2036
|
+
|
|
2037
|
+
// src/jobs/executor-utils.ts
|
|
2038
|
+
function transitionJobState(job, machine, event, options) {
|
|
2039
|
+
const nextState = machine.transition(job.state, event);
|
|
2040
|
+
if (!nextState) {
|
|
2041
|
+
throw new Error(`Invalid state transition: ${job.state} --${event}--> ?`);
|
|
2042
|
+
}
|
|
2043
|
+
const now = options?.now ?? Date.now();
|
|
2044
|
+
return {
|
|
2045
|
+
...job,
|
|
2046
|
+
...options?.patch ?? {},
|
|
2047
|
+
state: nextState,
|
|
2048
|
+
updatedAt: now,
|
|
2049
|
+
completedAt: machine.isTerminal(nextState) ? now : job.completedAt
|
|
2050
|
+
};
|
|
2051
|
+
}
|
|
2052
|
+
function applyRetryOrFail(job, classifiedError, policy, options) {
|
|
2053
|
+
const now = options?.now ?? Date.now();
|
|
2054
|
+
if (shouldRetry(classifiedError, job.retryCount, policy)) {
|
|
2055
|
+
const delay = computeRetryDelay(job.retryCount, policy);
|
|
2056
|
+
const retryTransition = options?.machine && options.retryEvent ? transitionJobState(job, options.machine, options.retryEvent, { now }) : { ...job, state: "waiting_retry", updatedAt: now };
|
|
2057
|
+
return {
|
|
2058
|
+
...retryTransition,
|
|
2059
|
+
error: classifiedError,
|
|
2060
|
+
retryCount: job.retryCount + 1,
|
|
2061
|
+
nextRetryAt: now + delay
|
|
2062
|
+
};
|
|
2063
|
+
}
|
|
2064
|
+
const failTransition = options?.machine && options.failEvent ? transitionJobState(job, options.machine, options.failEvent, { now }) : { ...job, state: "failed", completedAt: now, updatedAt: now };
|
|
2065
|
+
return {
|
|
2066
|
+
...failTransition,
|
|
2067
|
+
error: classifiedError,
|
|
2068
|
+
...options?.failedPatch ?? {}
|
|
2069
|
+
};
|
|
2070
|
+
}
|
|
2071
|
+
|
|
2072
|
+
// src/jobs/executors/payment-executor.ts
|
|
2073
|
+
async function* runPaymentJob(job, rpc, policy, signal) {
|
|
2074
|
+
let current = { ...job };
|
|
2075
|
+
while (!paymentStateMachine.isTerminal(current.state)) {
|
|
2076
|
+
if (signal.aborted) {
|
|
2077
|
+
current = transitionJobState(current, paymentStateMachine, "cancel");
|
|
2078
|
+
yield current;
|
|
2079
|
+
return;
|
|
2080
|
+
}
|
|
2081
|
+
if (current.state === "queued") {
|
|
2082
|
+
current = transitionJobState(current, paymentStateMachine, "send_issued");
|
|
2083
|
+
yield current;
|
|
2084
|
+
continue;
|
|
2085
|
+
}
|
|
2086
|
+
if (current.state === "waiting_retry") {
|
|
2087
|
+
const delay = current.nextRetryAt ? Math.max(0, current.nextRetryAt - Date.now()) : 0;
|
|
2088
|
+
if (delay > 0) {
|
|
2089
|
+
await sleep(delay, signal);
|
|
2090
|
+
if (signal.aborted) {
|
|
2091
|
+
current = transitionJobState(current, paymentStateMachine, "cancel");
|
|
2092
|
+
yield current;
|
|
2093
|
+
return;
|
|
2094
|
+
}
|
|
2095
|
+
}
|
|
2096
|
+
current = transitionJobState(current, paymentStateMachine, "retry_delay_elapsed", {
|
|
2097
|
+
patch: { nextRetryAt: void 0 }
|
|
2098
|
+
});
|
|
2099
|
+
yield current;
|
|
2100
|
+
continue;
|
|
2101
|
+
}
|
|
2102
|
+
if (current.state === "executing") {
|
|
2103
|
+
let paymentHash;
|
|
2104
|
+
try {
|
|
2105
|
+
const sendResult = await rpc.sendPayment(current.params.sendPaymentParams);
|
|
2106
|
+
paymentHash = sendResult.payment_hash;
|
|
2107
|
+
if (sendResult.status === "Success") {
|
|
2108
|
+
current = transitionJobState(current, paymentStateMachine, "payment_success", {
|
|
2109
|
+
patch: {
|
|
2110
|
+
result: {
|
|
2111
|
+
paymentHash: sendResult.payment_hash,
|
|
2112
|
+
status: sendResult.status,
|
|
2113
|
+
fee: sendResult.fee,
|
|
2114
|
+
failedError: sendResult.failed_error
|
|
2115
|
+
}
|
|
2116
|
+
}
|
|
2117
|
+
});
|
|
2118
|
+
yield current;
|
|
2119
|
+
return;
|
|
2120
|
+
}
|
|
2121
|
+
if (sendResult.status === "Failed") {
|
|
2122
|
+
const classified = classifyRpcError(
|
|
2123
|
+
new Error(sendResult.failed_error ?? "Payment failed"),
|
|
2124
|
+
sendResult.failed_error
|
|
2125
|
+
);
|
|
2126
|
+
current = applyRetryOrFail(current, classified, policy, {
|
|
2127
|
+
failedPatch: {
|
|
2128
|
+
result: {
|
|
2129
|
+
paymentHash: sendResult.payment_hash,
|
|
2130
|
+
status: sendResult.status,
|
|
2131
|
+
fee: sendResult.fee,
|
|
2132
|
+
failedError: sendResult.failed_error
|
|
2133
|
+
}
|
|
2134
|
+
},
|
|
2135
|
+
machine: paymentStateMachine,
|
|
2136
|
+
retryEvent: "payment_failed_retryable",
|
|
2137
|
+
failEvent: "payment_failed_permanent"
|
|
2138
|
+
});
|
|
2139
|
+
yield current;
|
|
2140
|
+
continue;
|
|
2141
|
+
}
|
|
2142
|
+
current = transitionJobState(current, paymentStateMachine, "payment_inflight");
|
|
2143
|
+
if (paymentHash) {
|
|
2144
|
+
current = { ...current, params: { ...current.params, sendPaymentParams: { ...current.params.sendPaymentParams, payment_hash: paymentHash } } };
|
|
2145
|
+
}
|
|
2146
|
+
yield current;
|
|
2147
|
+
continue;
|
|
2148
|
+
} catch (err) {
|
|
2149
|
+
const classified = classifyRpcError(err);
|
|
2150
|
+
current = applyRetryOrFail(current, classified, policy, {
|
|
2151
|
+
machine: paymentStateMachine,
|
|
2152
|
+
retryEvent: "payment_failed_retryable",
|
|
2153
|
+
failEvent: "payment_failed_permanent"
|
|
2154
|
+
});
|
|
2155
|
+
yield current;
|
|
2156
|
+
continue;
|
|
2157
|
+
}
|
|
2158
|
+
}
|
|
2159
|
+
if (current.state === "inflight") {
|
|
2160
|
+
const hash = current.params.sendPaymentParams.payment_hash;
|
|
2161
|
+
if (!hash) {
|
|
2162
|
+
current = transitionJobState(current, paymentStateMachine, "payment_failed_permanent", {
|
|
2163
|
+
patch: {
|
|
2164
|
+
error: { category: "unknown", retryable: false, message: "No payment_hash in inflight job" }
|
|
2165
|
+
}
|
|
2166
|
+
});
|
|
2167
|
+
yield current;
|
|
2168
|
+
continue;
|
|
2169
|
+
}
|
|
2170
|
+
try {
|
|
2171
|
+
const pollResult = await rpc.getPayment({ payment_hash: hash });
|
|
2172
|
+
if (pollResult.status === "Success") {
|
|
2173
|
+
current = transitionJobState(current, paymentStateMachine, "payment_success", {
|
|
2174
|
+
patch: {
|
|
2175
|
+
result: {
|
|
2176
|
+
paymentHash: pollResult.payment_hash,
|
|
2177
|
+
status: pollResult.status,
|
|
2178
|
+
fee: pollResult.fee,
|
|
2179
|
+
failedError: pollResult.failed_error
|
|
2180
|
+
}
|
|
2181
|
+
}
|
|
2182
|
+
});
|
|
2183
|
+
yield current;
|
|
2184
|
+
return;
|
|
2185
|
+
}
|
|
2186
|
+
if (pollResult.status === "Failed") {
|
|
2187
|
+
const classified = classifyRpcError(
|
|
2188
|
+
new Error(pollResult.failed_error ?? "Payment failed"),
|
|
2189
|
+
pollResult.failed_error
|
|
2190
|
+
);
|
|
2191
|
+
current = applyRetryOrFail(current, classified, policy, {
|
|
2192
|
+
failedPatch: {
|
|
2193
|
+
result: {
|
|
2194
|
+
paymentHash: pollResult.payment_hash,
|
|
2195
|
+
status: pollResult.status,
|
|
2196
|
+
fee: pollResult.fee,
|
|
2197
|
+
failedError: pollResult.failed_error
|
|
2198
|
+
}
|
|
2199
|
+
},
|
|
2200
|
+
machine: paymentStateMachine,
|
|
2201
|
+
retryEvent: "payment_failed_retryable",
|
|
2202
|
+
failEvent: "payment_failed_permanent"
|
|
2203
|
+
});
|
|
2204
|
+
yield current;
|
|
2205
|
+
continue;
|
|
2206
|
+
}
|
|
2207
|
+
await sleep(POLL_INTERVAL_MS, signal);
|
|
2208
|
+
if (signal.aborted) {
|
|
2209
|
+
current = transitionJobState(current, paymentStateMachine, "cancel");
|
|
2210
|
+
yield current;
|
|
2211
|
+
return;
|
|
2212
|
+
}
|
|
2213
|
+
current = { ...current, updatedAt: Date.now() };
|
|
2214
|
+
continue;
|
|
2215
|
+
} catch (err) {
|
|
2216
|
+
const classified = classifyRpcError(err);
|
|
2217
|
+
current = applyRetryOrFail(current, classified, policy, {
|
|
2218
|
+
machine: paymentStateMachine,
|
|
2219
|
+
retryEvent: "payment_failed_retryable",
|
|
2220
|
+
failEvent: "payment_failed_permanent"
|
|
2221
|
+
});
|
|
2222
|
+
yield current;
|
|
2223
|
+
continue;
|
|
2224
|
+
}
|
|
2225
|
+
}
|
|
2226
|
+
break;
|
|
2227
|
+
}
|
|
2228
|
+
}
|
|
2229
|
+
var POLL_INTERVAL_MS = 1500;
|
|
2230
|
+
|
|
2231
|
+
// src/jobs/executors/invoice-executor.ts
|
|
2232
|
+
var DEFAULT_POLL_INTERVAL = 1500;
|
|
2233
|
+
async function* runInvoiceJob(job, rpc, policy, signal) {
|
|
2234
|
+
let current = { ...job };
|
|
2235
|
+
if (current.state === "queued") {
|
|
2236
|
+
current = transitionJobState(current, invoiceStateMachine, "send_issued");
|
|
2237
|
+
yield current;
|
|
2238
|
+
}
|
|
2239
|
+
if (current.state === "waiting_retry") {
|
|
2240
|
+
current = transitionJobState(current, invoiceStateMachine, "retry_delay_elapsed", {
|
|
2241
|
+
patch: { nextRetryAt: void 0 }
|
|
2242
|
+
});
|
|
2243
|
+
yield current;
|
|
2244
|
+
}
|
|
2245
|
+
try {
|
|
2246
|
+
const pollIntervalMs = current.params.pollIntervalMs ?? DEFAULT_POLL_INTERVAL;
|
|
2247
|
+
if (current.params.action === "create") {
|
|
2248
|
+
if (!current.params.newInvoiceParams) {
|
|
2249
|
+
throw new Error("Invoice create job requires newInvoiceParams");
|
|
2250
|
+
}
|
|
2251
|
+
const created = await rpc.newInvoice(current.params.newInvoiceParams);
|
|
2252
|
+
const paymentHash = created.invoice.data.payment_hash;
|
|
2253
|
+
const createdStatus = "Open";
|
|
2254
|
+
current = {
|
|
2255
|
+
...current,
|
|
2256
|
+
state: "invoice_created",
|
|
2257
|
+
result: {
|
|
2258
|
+
paymentHash,
|
|
2259
|
+
invoiceAddress: created.invoice_address,
|
|
2260
|
+
status: createdStatus,
|
|
2261
|
+
invoice: created.invoice
|
|
2262
|
+
},
|
|
2263
|
+
updatedAt: Date.now()
|
|
2264
|
+
};
|
|
2265
|
+
yield current;
|
|
2266
|
+
if (!current.params.waitForTerminal) {
|
|
2267
|
+
current = transitionJobState(current, invoiceStateMachine, "payment_success");
|
|
2268
|
+
yield current;
|
|
2269
|
+
return;
|
|
2270
|
+
}
|
|
2271
|
+
while (true) {
|
|
2272
|
+
if (signal.aborted) {
|
|
2273
|
+
current = transitionJobState(current, invoiceStateMachine, "cancel");
|
|
2274
|
+
yield current;
|
|
2275
|
+
return;
|
|
2276
|
+
}
|
|
2277
|
+
const invoice = await rpc.getInvoice({ payment_hash: paymentHash });
|
|
2278
|
+
if (invoice.status === "Paid") {
|
|
2279
|
+
current = {
|
|
2280
|
+
...current,
|
|
2281
|
+
state: "invoice_settled",
|
|
2282
|
+
result: {
|
|
2283
|
+
paymentHash,
|
|
2284
|
+
invoiceAddress: invoice.invoice_address,
|
|
2285
|
+
status: invoice.status,
|
|
2286
|
+
invoice: invoice.invoice
|
|
2287
|
+
},
|
|
2288
|
+
updatedAt: Date.now()
|
|
2289
|
+
};
|
|
2290
|
+
yield current;
|
|
2291
|
+
current = transitionJobState(current, invoiceStateMachine, "payment_success");
|
|
2292
|
+
yield current;
|
|
2293
|
+
return;
|
|
2294
|
+
}
|
|
2295
|
+
if (invoice.status === "Received") {
|
|
2296
|
+
current = {
|
|
2297
|
+
...current,
|
|
2298
|
+
state: "invoice_received",
|
|
2299
|
+
result: {
|
|
2300
|
+
paymentHash,
|
|
2301
|
+
invoiceAddress: invoice.invoice_address,
|
|
2302
|
+
status: invoice.status,
|
|
2303
|
+
invoice: invoice.invoice
|
|
2304
|
+
},
|
|
2305
|
+
updatedAt: Date.now()
|
|
2306
|
+
};
|
|
2307
|
+
yield current;
|
|
2308
|
+
} else if (invoice.status === "Cancelled") {
|
|
2309
|
+
current = {
|
|
2310
|
+
...current,
|
|
2311
|
+
state: "invoice_cancelled",
|
|
2312
|
+
result: {
|
|
2313
|
+
paymentHash,
|
|
2314
|
+
invoiceAddress: invoice.invoice_address,
|
|
2315
|
+
status: invoice.status,
|
|
2316
|
+
invoice: invoice.invoice
|
|
2317
|
+
},
|
|
2318
|
+
completedAt: Date.now(),
|
|
2319
|
+
updatedAt: Date.now()
|
|
2320
|
+
};
|
|
2321
|
+
yield current;
|
|
2322
|
+
current = transitionJobState(current, invoiceStateMachine, "payment_failed_permanent");
|
|
2323
|
+
yield current;
|
|
2324
|
+
return;
|
|
2325
|
+
} else if (invoice.status === "Expired") {
|
|
2326
|
+
current = {
|
|
2327
|
+
...current,
|
|
2328
|
+
state: "invoice_expired",
|
|
2329
|
+
result: {
|
|
2330
|
+
paymentHash,
|
|
2331
|
+
invoiceAddress: invoice.invoice_address,
|
|
2332
|
+
status: invoice.status,
|
|
2333
|
+
invoice: invoice.invoice
|
|
2334
|
+
},
|
|
2335
|
+
completedAt: Date.now(),
|
|
2336
|
+
updatedAt: Date.now()
|
|
2337
|
+
};
|
|
2338
|
+
yield current;
|
|
2339
|
+
current = transitionJobState(current, invoiceStateMachine, "payment_failed_permanent");
|
|
2340
|
+
yield current;
|
|
2341
|
+
return;
|
|
2342
|
+
}
|
|
2343
|
+
await sleep(pollIntervalMs, signal);
|
|
2344
|
+
}
|
|
2345
|
+
}
|
|
2346
|
+
if (current.params.action === "watch") {
|
|
2347
|
+
if (!current.params.getInvoicePaymentHash) {
|
|
2348
|
+
throw new Error("Invoice watch job requires getInvoicePaymentHash");
|
|
2349
|
+
}
|
|
2350
|
+
const paymentHash = current.params.getInvoicePaymentHash;
|
|
2351
|
+
while (true) {
|
|
2352
|
+
if (signal.aborted) {
|
|
2353
|
+
current = transitionJobState(current, invoiceStateMachine, "cancel");
|
|
2354
|
+
yield current;
|
|
2355
|
+
return;
|
|
2356
|
+
}
|
|
2357
|
+
const invoice = await rpc.getInvoice({ payment_hash: paymentHash });
|
|
2358
|
+
current = {
|
|
2359
|
+
...current,
|
|
2360
|
+
state: invoice.status === "Paid" ? "invoice_settled" : invoice.status === "Received" ? "invoice_received" : invoice.status === "Cancelled" ? "invoice_cancelled" : invoice.status === "Expired" ? "invoice_expired" : "invoice_active",
|
|
2361
|
+
result: {
|
|
2362
|
+
paymentHash,
|
|
2363
|
+
invoiceAddress: invoice.invoice_address,
|
|
2364
|
+
status: invoice.status,
|
|
2365
|
+
invoice: invoice.invoice
|
|
2366
|
+
},
|
|
2367
|
+
updatedAt: Date.now()
|
|
2368
|
+
};
|
|
2369
|
+
yield current;
|
|
2370
|
+
if (invoice.status === "Paid") {
|
|
2371
|
+
current = transitionJobState(current, invoiceStateMachine, "payment_success");
|
|
2372
|
+
yield current;
|
|
2373
|
+
return;
|
|
2374
|
+
}
|
|
2375
|
+
if (invoice.status === "Cancelled" || invoice.status === "Expired") {
|
|
2376
|
+
current = transitionJobState(current, invoiceStateMachine, "payment_failed_permanent");
|
|
2377
|
+
yield current;
|
|
2378
|
+
return;
|
|
2379
|
+
}
|
|
2380
|
+
await sleep(pollIntervalMs, signal);
|
|
2381
|
+
}
|
|
2382
|
+
}
|
|
2383
|
+
if (current.params.action === "cancel") {
|
|
2384
|
+
if (!current.params.cancelInvoiceParams) {
|
|
2385
|
+
throw new Error("Invoice cancel job requires cancelInvoiceParams");
|
|
2386
|
+
}
|
|
2387
|
+
const cancelled = await rpc.cancelInvoice(current.params.cancelInvoiceParams);
|
|
2388
|
+
current = {
|
|
2389
|
+
...current,
|
|
2390
|
+
state: "invoice_cancelled",
|
|
2391
|
+
result: {
|
|
2392
|
+
paymentHash: current.params.cancelInvoiceParams.payment_hash,
|
|
2393
|
+
invoiceAddress: cancelled.invoice_address,
|
|
2394
|
+
status: cancelled.status,
|
|
2395
|
+
invoice: cancelled.invoice
|
|
2396
|
+
},
|
|
2397
|
+
updatedAt: Date.now()
|
|
2398
|
+
};
|
|
2399
|
+
yield current;
|
|
2400
|
+
current = transitionJobState(current, invoiceStateMachine, "payment_success");
|
|
2401
|
+
yield current;
|
|
2402
|
+
return;
|
|
2403
|
+
}
|
|
2404
|
+
if (current.params.action === "settle") {
|
|
2405
|
+
if (!current.params.settleInvoiceParams) {
|
|
2406
|
+
throw new Error("Invoice settle job requires settleInvoiceParams");
|
|
2407
|
+
}
|
|
2408
|
+
await rpc.settleInvoice(current.params.settleInvoiceParams);
|
|
2409
|
+
current = {
|
|
2410
|
+
...current,
|
|
2411
|
+
state: "invoice_settled",
|
|
2412
|
+
result: {
|
|
2413
|
+
paymentHash: current.params.settleInvoiceParams.payment_hash,
|
|
2414
|
+
status: "Paid"
|
|
2415
|
+
},
|
|
2416
|
+
updatedAt: Date.now()
|
|
2417
|
+
};
|
|
2418
|
+
yield current;
|
|
2419
|
+
current = transitionJobState(current, invoiceStateMachine, "payment_success");
|
|
2420
|
+
yield current;
|
|
2421
|
+
return;
|
|
2422
|
+
}
|
|
2423
|
+
throw new Error(`Unsupported invoice action: ${current.params.action}`);
|
|
2424
|
+
} catch (error) {
|
|
2425
|
+
const classified = classifyRpcError(error);
|
|
2426
|
+
current = applyRetryOrFail(current, classified, policy, {
|
|
2427
|
+
machine: invoiceStateMachine,
|
|
2428
|
+
retryEvent: "payment_failed_retryable",
|
|
2429
|
+
failEvent: "payment_failed_permanent"
|
|
2430
|
+
});
|
|
2431
|
+
yield current;
|
|
2432
|
+
}
|
|
2433
|
+
}
|
|
2434
|
+
|
|
2435
|
+
// src/jobs/executors/channel-executor.ts
|
|
2436
|
+
import { ChannelState as ChannelState2 } from "@fiber-pay/sdk";
|
|
2437
|
+
var DEFAULT_POLL_INTERVAL2 = 2e3;
|
|
2438
|
+
async function* runChannelJob(job, rpc, policy, signal) {
|
|
2439
|
+
let current = { ...job };
|
|
2440
|
+
const resumedFromRetry = job.state === "waiting_retry";
|
|
2441
|
+
if (current.state === "queued") {
|
|
2442
|
+
current = transitionJobState(current, channelStateMachine, "send_issued");
|
|
2443
|
+
yield current;
|
|
2444
|
+
}
|
|
2445
|
+
if (current.state === "waiting_retry") {
|
|
2446
|
+
current = transitionJobState(current, channelStateMachine, "retry_delay_elapsed", {
|
|
2447
|
+
patch: { nextRetryAt: void 0 }
|
|
2448
|
+
});
|
|
2449
|
+
yield current;
|
|
2450
|
+
}
|
|
2451
|
+
try {
|
|
2452
|
+
const pollIntervalMs = current.params.pollIntervalMs ?? DEFAULT_POLL_INTERVAL2;
|
|
2453
|
+
if (current.params.action === "open") {
|
|
2454
|
+
if (!current.params.openChannelParams) {
|
|
2455
|
+
throw new Error("Channel open job requires openChannelParams");
|
|
2456
|
+
}
|
|
2457
|
+
const targetPeerId = current.params.peerId ?? current.params.openChannelParams.peer_id;
|
|
2458
|
+
let temporaryChannelId = current.result?.temporaryChannelId;
|
|
2459
|
+
if (resumedFromRetry) {
|
|
2460
|
+
const existing = await findTargetChannel(rpc, targetPeerId, current.params.channelId);
|
|
2461
|
+
if (existing && !isClosed(existing.state.state_name)) {
|
|
2462
|
+
current = transitionJobState(current, channelStateMachine, "channel_opening", {
|
|
2463
|
+
patch: {
|
|
2464
|
+
result: {
|
|
2465
|
+
temporaryChannelId,
|
|
2466
|
+
channelId: existing.channel_id,
|
|
2467
|
+
state: existing.state.state_name
|
|
2468
|
+
}
|
|
2469
|
+
}
|
|
2470
|
+
});
|
|
2471
|
+
yield current;
|
|
2472
|
+
} else {
|
|
2473
|
+
const opened = await rpc.openChannel(current.params.openChannelParams);
|
|
2474
|
+
temporaryChannelId = opened.temporary_channel_id;
|
|
2475
|
+
current = transitionJobState(current, channelStateMachine, "channel_opening", {
|
|
2476
|
+
patch: {
|
|
2477
|
+
result: {
|
|
2478
|
+
temporaryChannelId,
|
|
2479
|
+
state: "OPENING"
|
|
2480
|
+
}
|
|
2481
|
+
}
|
|
2482
|
+
});
|
|
2483
|
+
yield current;
|
|
2484
|
+
}
|
|
2485
|
+
} else {
|
|
2486
|
+
const opened = await rpc.openChannel(current.params.openChannelParams);
|
|
2487
|
+
temporaryChannelId = opened.temporary_channel_id;
|
|
2488
|
+
current = transitionJobState(current, channelStateMachine, "channel_opening", {
|
|
2489
|
+
patch: {
|
|
2490
|
+
result: {
|
|
2491
|
+
temporaryChannelId,
|
|
2492
|
+
state: "OPENING"
|
|
2493
|
+
}
|
|
2494
|
+
}
|
|
2495
|
+
});
|
|
2496
|
+
yield current;
|
|
2497
|
+
}
|
|
2498
|
+
if (!current.params.waitForReady) {
|
|
2499
|
+
current = transitionJobState(current, channelStateMachine, "payment_success");
|
|
2500
|
+
yield current;
|
|
2501
|
+
return;
|
|
2502
|
+
}
|
|
2503
|
+
while (true) {
|
|
2504
|
+
if (signal.aborted) {
|
|
2505
|
+
current = transitionJobState(current, channelStateMachine, "cancel");
|
|
2506
|
+
yield current;
|
|
2507
|
+
return;
|
|
2508
|
+
}
|
|
2509
|
+
const channels = await rpc.listChannels({
|
|
2510
|
+
peer_id: targetPeerId,
|
|
2511
|
+
include_closed: true
|
|
2512
|
+
});
|
|
2513
|
+
const candidates = channels.channels.filter((channel) => {
|
|
2514
|
+
if (current.params.channelId && channel.channel_id !== current.params.channelId) return false;
|
|
2515
|
+
return channel.peer_id === targetPeerId;
|
|
2516
|
+
});
|
|
2517
|
+
const readyMatch = candidates.find(
|
|
2518
|
+
(channel) => String(channel.state.state_name).toUpperCase() === "CHANNEL_READY"
|
|
2519
|
+
);
|
|
2520
|
+
const activeMatch = candidates.find((channel) => !isClosed(channel.state.state_name));
|
|
2521
|
+
const closedMatch = current.params.channelId && !activeMatch && candidates.length > 0 ? candidates.find((channel) => isTerminalClosed(channel.state.state_name)) : void 0;
|
|
2522
|
+
if (readyMatch) {
|
|
2523
|
+
current = transitionJobState(current, channelStateMachine, "channel_ready", {
|
|
2524
|
+
patch: {
|
|
2525
|
+
result: {
|
|
2526
|
+
temporaryChannelId,
|
|
2527
|
+
channelId: readyMatch.channel_id,
|
|
2528
|
+
state: readyMatch.state.state_name
|
|
2529
|
+
}
|
|
2530
|
+
}
|
|
2531
|
+
});
|
|
2532
|
+
yield current;
|
|
2533
|
+
current = transitionJobState(current, channelStateMachine, "payment_success");
|
|
2534
|
+
yield current;
|
|
2535
|
+
return;
|
|
2536
|
+
}
|
|
2537
|
+
if (closedMatch) {
|
|
2538
|
+
current = transitionJobState(current, channelStateMachine, "channel_closed", {
|
|
2539
|
+
patch: {
|
|
2540
|
+
result: {
|
|
2541
|
+
temporaryChannelId,
|
|
2542
|
+
channelId: closedMatch.channel_id,
|
|
2543
|
+
state: closedMatch.state.state_name
|
|
2544
|
+
}
|
|
2545
|
+
}
|
|
2546
|
+
});
|
|
2547
|
+
yield current;
|
|
2548
|
+
current = transitionJobState(current, channelStateMachine, "payment_failed_permanent");
|
|
2549
|
+
yield current;
|
|
2550
|
+
return;
|
|
2551
|
+
}
|
|
2552
|
+
current = transitionJobState(current, channelStateMachine, "channel_opening");
|
|
2553
|
+
yield current;
|
|
2554
|
+
await sleep(pollIntervalMs, signal);
|
|
2555
|
+
}
|
|
2556
|
+
}
|
|
2557
|
+
if (current.params.action === "shutdown") {
|
|
2558
|
+
const shutdownParams = current.params.shutdownChannelParams;
|
|
2559
|
+
if (!shutdownParams) {
|
|
2560
|
+
throw new Error("Channel shutdown job requires shutdownChannelParams");
|
|
2561
|
+
}
|
|
2562
|
+
await rpc.shutdownChannel(shutdownParams);
|
|
2563
|
+
current = transitionJobState(current, channelStateMachine, "channel_closing", {
|
|
2564
|
+
patch: {
|
|
2565
|
+
result: {
|
|
2566
|
+
channelId: shutdownParams.channel_id,
|
|
2567
|
+
state: "SHUTTING_DOWN"
|
|
2568
|
+
}
|
|
2569
|
+
}
|
|
2570
|
+
});
|
|
2571
|
+
yield current;
|
|
2572
|
+
if (!current.params.waitForClosed) {
|
|
2573
|
+
current = transitionJobState(current, channelStateMachine, "payment_success");
|
|
2574
|
+
yield current;
|
|
2575
|
+
return;
|
|
2576
|
+
}
|
|
2577
|
+
while (true) {
|
|
2578
|
+
if (signal.aborted) {
|
|
2579
|
+
current = transitionJobState(current, channelStateMachine, "cancel");
|
|
2580
|
+
yield current;
|
|
2581
|
+
return;
|
|
2582
|
+
}
|
|
2583
|
+
const channels = await rpc.listChannels({ include_closed: true });
|
|
2584
|
+
const match = channels.channels.find((channel) => channel.channel_id === shutdownParams.channel_id);
|
|
2585
|
+
if (!match || isTerminalClosed(String(match.state.state_name))) {
|
|
2586
|
+
current = transitionJobState(current, channelStateMachine, "channel_closed", {
|
|
2587
|
+
patch: {
|
|
2588
|
+
result: {
|
|
2589
|
+
channelId: shutdownParams.channel_id,
|
|
2590
|
+
state: "CLOSED"
|
|
2591
|
+
}
|
|
2592
|
+
}
|
|
2593
|
+
});
|
|
2594
|
+
yield current;
|
|
2595
|
+
current = transitionJobState(current, channelStateMachine, "payment_success");
|
|
2596
|
+
yield current;
|
|
2597
|
+
return;
|
|
2598
|
+
}
|
|
2599
|
+
await sleep(pollIntervalMs, signal);
|
|
2600
|
+
}
|
|
2601
|
+
}
|
|
2602
|
+
if (current.params.action === "accept") {
|
|
2603
|
+
if (!current.params.acceptChannelParams) {
|
|
2604
|
+
throw new Error("Channel accept job requires acceptChannelParams");
|
|
2605
|
+
}
|
|
2606
|
+
const accepted = await rpc.acceptChannel(current.params.acceptChannelParams);
|
|
2607
|
+
current = transitionJobState(current, channelStateMachine, "channel_accepting", {
|
|
2608
|
+
patch: {
|
|
2609
|
+
result: {
|
|
2610
|
+
acceptedChannelId: accepted.channel_id,
|
|
2611
|
+
channelId: accepted.channel_id,
|
|
2612
|
+
state: "ACCEPTED"
|
|
2613
|
+
}
|
|
2614
|
+
}
|
|
2615
|
+
});
|
|
2616
|
+
yield current;
|
|
2617
|
+
current = transitionJobState(current, channelStateMachine, "payment_success");
|
|
2618
|
+
yield current;
|
|
2619
|
+
return;
|
|
2620
|
+
}
|
|
2621
|
+
if (current.params.action === "abandon") {
|
|
2622
|
+
if (!current.params.abandonChannelParams) {
|
|
2623
|
+
throw new Error("Channel abandon job requires abandonChannelParams");
|
|
2624
|
+
}
|
|
2625
|
+
await rpc.abandonChannel(current.params.abandonChannelParams);
|
|
2626
|
+
current = transitionJobState(current, channelStateMachine, "channel_abandoning", {
|
|
2627
|
+
patch: {
|
|
2628
|
+
result: {
|
|
2629
|
+
channelId: current.params.abandonChannelParams.channel_id,
|
|
2630
|
+
state: "ABANDONED"
|
|
2631
|
+
}
|
|
2632
|
+
}
|
|
2633
|
+
});
|
|
2634
|
+
yield current;
|
|
2635
|
+
current = transitionJobState(current, channelStateMachine, "payment_success");
|
|
2636
|
+
yield current;
|
|
2637
|
+
return;
|
|
2638
|
+
}
|
|
2639
|
+
if (current.params.action === "update") {
|
|
2640
|
+
if (!current.params.updateChannelParams) {
|
|
2641
|
+
throw new Error("Channel update job requires updateChannelParams");
|
|
2642
|
+
}
|
|
2643
|
+
await rpc.updateChannel(current.params.updateChannelParams);
|
|
2644
|
+
current = transitionJobState(current, channelStateMachine, "channel_updating", {
|
|
2645
|
+
patch: {
|
|
2646
|
+
result: {
|
|
2647
|
+
channelId: current.params.updateChannelParams.channel_id,
|
|
2648
|
+
state: "UPDATED"
|
|
2649
|
+
}
|
|
2650
|
+
}
|
|
2651
|
+
});
|
|
2652
|
+
yield current;
|
|
2653
|
+
current = transitionJobState(current, channelStateMachine, "payment_success");
|
|
2654
|
+
yield current;
|
|
2655
|
+
return;
|
|
2656
|
+
}
|
|
2657
|
+
throw new Error(`Unsupported channel action: ${current.params.action}`);
|
|
2658
|
+
} catch (error) {
|
|
2659
|
+
const classified = classifyRpcError(error);
|
|
2660
|
+
current = applyRetryOrFail(current, classified, policy, {
|
|
2661
|
+
machine: channelStateMachine,
|
|
2662
|
+
retryEvent: "payment_failed_retryable",
|
|
2663
|
+
failEvent: "payment_failed_permanent"
|
|
2664
|
+
});
|
|
2665
|
+
yield current;
|
|
2666
|
+
}
|
|
2667
|
+
}
|
|
2668
|
+
async function findTargetChannel(rpc, peerId, channelId) {
|
|
2669
|
+
const channels = await rpc.listChannels({
|
|
2670
|
+
peer_id: peerId,
|
|
2671
|
+
include_closed: true
|
|
2672
|
+
});
|
|
2673
|
+
return channels.channels.find((channel) => {
|
|
2674
|
+
if (channelId && channel.channel_id !== channelId) return false;
|
|
2675
|
+
return channel.peer_id === peerId;
|
|
2676
|
+
});
|
|
2677
|
+
}
|
|
2678
|
+
function isClosed(stateName) {
|
|
2679
|
+
return stateName === ChannelState2.Closed || stateName === "CLOSED" || stateName === ChannelState2.ShuttingDown || stateName === "SHUTTING_DOWN";
|
|
2680
|
+
}
|
|
2681
|
+
function isTerminalClosed(stateName) {
|
|
2682
|
+
return stateName === ChannelState2.Closed || stateName === "CLOSED";
|
|
2683
|
+
}
|
|
2684
|
+
|
|
2685
|
+
// src/jobs/job-manager.ts
|
|
2686
|
+
function stableStringify(value) {
|
|
2687
|
+
if (value === null || typeof value !== "object") {
|
|
2688
|
+
return JSON.stringify(value);
|
|
2689
|
+
}
|
|
2690
|
+
if (Array.isArray(value)) {
|
|
2691
|
+
return `[${value.map((item) => stableStringify(item)).join(",")}]`;
|
|
2692
|
+
}
|
|
2693
|
+
const entries = Object.entries(value).sort(([left], [right]) => left.localeCompare(right)).map(([key, nested]) => `${JSON.stringify(key)}:${stableStringify(nested)}`);
|
|
2694
|
+
return `{${entries.join(",")}}`;
|
|
2695
|
+
}
|
|
2696
|
+
function haveSameParams(left, right) {
|
|
2697
|
+
return stableStringify(left) === stableStringify(right);
|
|
2698
|
+
}
|
|
2699
|
+
var JobManager = class extends EventEmitter {
|
|
2700
|
+
rpc;
|
|
2701
|
+
store;
|
|
2702
|
+
retryPolicy;
|
|
2703
|
+
schedulerIntervalMs;
|
|
2704
|
+
maxConcurrentJobs;
|
|
2705
|
+
running = false;
|
|
2706
|
+
schedulerTimer;
|
|
2707
|
+
active = /* @__PURE__ */ new Map();
|
|
2708
|
+
constructor(rpc, store, config = {}) {
|
|
2709
|
+
super();
|
|
2710
|
+
this.rpc = rpc;
|
|
2711
|
+
this.store = store;
|
|
2712
|
+
this.retryPolicy = config.retryPolicy ?? defaultPaymentRetryPolicy;
|
|
2713
|
+
this.schedulerIntervalMs = config.schedulerIntervalMs ?? 500;
|
|
2714
|
+
this.maxConcurrentJobs = config.maxConcurrentJobs ?? 5;
|
|
2715
|
+
}
|
|
2716
|
+
async ensurePayment(params, options = {}) {
|
|
2717
|
+
const idempotencyKey = options.idempotencyKey ?? params.sendPaymentParams.payment_hash ?? randomUUID3();
|
|
2718
|
+
const existing = this.store.getJobByIdempotencyKey(idempotencyKey);
|
|
2719
|
+
if (existing?.type === "payment") {
|
|
2720
|
+
if (!haveSameParams(existing.params, params)) {
|
|
2721
|
+
throw new Error(
|
|
2722
|
+
`Idempotency key collision with different payment params: ${idempotencyKey}. Use a new idempotency key for a new payment intent.`
|
|
2723
|
+
);
|
|
2724
|
+
}
|
|
2725
|
+
if (existing.state === "succeeded" || existing.state === "cancelled") {
|
|
2726
|
+
return existing;
|
|
2727
|
+
}
|
|
2728
|
+
if (!TERMINAL_JOB_STATES.has(existing.state) || existing.state === "failed") {
|
|
2729
|
+
if (existing.state === "failed" && !this.active.has(existing.id)) {
|
|
2730
|
+
const reset = this.store.updateJob(existing.id, {
|
|
2731
|
+
state: "queued",
|
|
2732
|
+
params,
|
|
2733
|
+
result: void 0,
|
|
2734
|
+
error: void 0,
|
|
2735
|
+
retryCount: 0,
|
|
2736
|
+
nextRetryAt: void 0,
|
|
2737
|
+
completedAt: void 0
|
|
2738
|
+
});
|
|
2739
|
+
this.schedule(reset);
|
|
2740
|
+
return reset;
|
|
2741
|
+
}
|
|
2742
|
+
return existing;
|
|
2743
|
+
}
|
|
2744
|
+
}
|
|
2745
|
+
const job = this.store.createJob({
|
|
2746
|
+
type: "payment",
|
|
2747
|
+
state: "queued",
|
|
2748
|
+
params,
|
|
2749
|
+
retryCount: 0,
|
|
2750
|
+
maxRetries: options.maxRetries ?? this.retryPolicy.maxRetries,
|
|
2751
|
+
idempotencyKey
|
|
2752
|
+
});
|
|
2753
|
+
this.store.addJobEvent(job.id, "created", void 0, "queued", this.buildEventData(job));
|
|
2754
|
+
this.emit("job:created", job);
|
|
2755
|
+
this.schedule(job);
|
|
2756
|
+
return job;
|
|
2757
|
+
}
|
|
2758
|
+
async manageInvoice(params, options = {}) {
|
|
2759
|
+
const idempotencyKey = options.idempotencyKey ?? deriveInvoiceKey(params) ?? randomUUID3();
|
|
2760
|
+
return this.createOrReuseJob("invoice", params, idempotencyKey, options.maxRetries, options.reuseTerminal);
|
|
2761
|
+
}
|
|
2762
|
+
async manageChannel(params, options = {}) {
|
|
2763
|
+
const idempotencyKey = options.idempotencyKey ?? deriveChannelKey(params) ?? randomUUID3();
|
|
2764
|
+
return this.createOrReuseJob("channel", params, idempotencyKey, options.maxRetries, options.reuseTerminal);
|
|
2765
|
+
}
|
|
2766
|
+
getJob(id) {
|
|
2767
|
+
return this.store.getJob(id);
|
|
2768
|
+
}
|
|
2769
|
+
listJobs(filter = {}) {
|
|
2770
|
+
return this.store.listJobs(filter);
|
|
2771
|
+
}
|
|
2772
|
+
cancelJob(id) {
|
|
2773
|
+
const job = this.getJob(id);
|
|
2774
|
+
if (!job) throw new Error(`Job not found: ${id}`);
|
|
2775
|
+
if (TERMINAL_JOB_STATES.has(job.state)) return;
|
|
2776
|
+
this.active.get(id)?.abortController.abort();
|
|
2777
|
+
if (!this.active.has(id)) {
|
|
2778
|
+
const updated = this.store.updateJob(id, {
|
|
2779
|
+
state: "cancelled",
|
|
2780
|
+
completedAt: Date.now()
|
|
2781
|
+
});
|
|
2782
|
+
this.store.addJobEvent(id, "cancelled", job.state, "cancelled", this.buildEventData(updated));
|
|
2783
|
+
this.emit("job:cancelled", updated);
|
|
2784
|
+
}
|
|
2785
|
+
}
|
|
2786
|
+
start() {
|
|
2787
|
+
if (this.running) return;
|
|
2788
|
+
this.running = true;
|
|
2789
|
+
this.recover();
|
|
2790
|
+
this.schedulerTimer = setInterval(() => this.tick(), this.schedulerIntervalMs);
|
|
2791
|
+
}
|
|
2792
|
+
async stop() {
|
|
2793
|
+
this.running = false;
|
|
2794
|
+
if (this.schedulerTimer) {
|
|
2795
|
+
clearInterval(this.schedulerTimer);
|
|
2796
|
+
this.schedulerTimer = void 0;
|
|
2797
|
+
}
|
|
2798
|
+
for (const [, exec] of this.active) {
|
|
2799
|
+
exec.abortController.abort();
|
|
2800
|
+
}
|
|
2801
|
+
await Promise.allSettled(Array.from(this.active.values()).map((e) => e.promise));
|
|
2802
|
+
}
|
|
2803
|
+
async createOrReuseJob(type, params, idempotencyKey, maxRetries, reuseTerminal) {
|
|
2804
|
+
const existing = this.store.getJobByIdempotencyKey(idempotencyKey);
|
|
2805
|
+
if (existing?.type === type) {
|
|
2806
|
+
if (!haveSameParams(existing.params, params)) {
|
|
2807
|
+
throw new Error(
|
|
2808
|
+
`Idempotency key collision with different ${type} params: ${idempotencyKey}. Use a new idempotency key for a new ${type} intent.`
|
|
2809
|
+
);
|
|
2810
|
+
}
|
|
2811
|
+
if (existing.state === "succeeded" || existing.state === "cancelled") {
|
|
2812
|
+
if (reuseTerminal === false) {
|
|
2813
|
+
const reset = this.store.updateJob(existing.id, {
|
|
2814
|
+
state: "queued",
|
|
2815
|
+
params,
|
|
2816
|
+
result: void 0,
|
|
2817
|
+
error: void 0,
|
|
2818
|
+
retryCount: 0,
|
|
2819
|
+
nextRetryAt: void 0,
|
|
2820
|
+
completedAt: void 0
|
|
2821
|
+
});
|
|
2822
|
+
this.store.addJobEvent(existing.id, "created", existing.state, "queued", this.buildEventData(reset));
|
|
2823
|
+
this.emit("job:created", reset);
|
|
2824
|
+
this.schedule(reset);
|
|
2825
|
+
return reset;
|
|
2826
|
+
}
|
|
2827
|
+
return existing;
|
|
2828
|
+
}
|
|
2829
|
+
if (!TERMINAL_JOB_STATES.has(existing.state) || existing.state === "failed") {
|
|
2830
|
+
if (existing.state === "failed" && !this.active.has(existing.id)) {
|
|
2831
|
+
const reset = this.store.updateJob(existing.id, {
|
|
2832
|
+
state: "queued",
|
|
2833
|
+
params,
|
|
2834
|
+
result: void 0,
|
|
2835
|
+
error: void 0,
|
|
2836
|
+
retryCount: 0,
|
|
2837
|
+
nextRetryAt: void 0,
|
|
2838
|
+
completedAt: void 0
|
|
2839
|
+
});
|
|
2840
|
+
this.schedule(reset);
|
|
2841
|
+
return reset;
|
|
2842
|
+
}
|
|
2843
|
+
return existing;
|
|
2844
|
+
}
|
|
2845
|
+
}
|
|
2846
|
+
const job = this.store.createJob({
|
|
2847
|
+
type,
|
|
2848
|
+
state: "queued",
|
|
2849
|
+
params,
|
|
2850
|
+
retryCount: 0,
|
|
2851
|
+
maxRetries: maxRetries ?? this.retryPolicy.maxRetries,
|
|
2852
|
+
idempotencyKey
|
|
2853
|
+
});
|
|
2854
|
+
this.store.addJobEvent(job.id, "created", void 0, "queued", this.buildEventData(job));
|
|
2855
|
+
this.emit("job:created", job);
|
|
2856
|
+
this.schedule(job);
|
|
2857
|
+
return job;
|
|
2858
|
+
}
|
|
2859
|
+
tick() {
|
|
2860
|
+
if (!this.running) return;
|
|
2861
|
+
if (this.active.size >= this.maxConcurrentJobs) return;
|
|
2862
|
+
const queued = this.store.listJobs({
|
|
2863
|
+
state: "queued",
|
|
2864
|
+
limit: this.maxConcurrentJobs - this.active.size
|
|
2865
|
+
});
|
|
2866
|
+
for (const job of queued) {
|
|
2867
|
+
if (this.active.size >= this.maxConcurrentJobs) break;
|
|
2868
|
+
if (!this.active.has(job.id)) {
|
|
2869
|
+
this.execute(job);
|
|
2870
|
+
}
|
|
2871
|
+
}
|
|
2872
|
+
if (this.active.size < this.maxConcurrentJobs) {
|
|
2873
|
+
const retryable = this.store.getRetryableJobs();
|
|
2874
|
+
for (const job of retryable) {
|
|
2875
|
+
if (this.active.size >= this.maxConcurrentJobs) break;
|
|
2876
|
+
if (!this.active.has(job.id)) {
|
|
2877
|
+
this.execute(job);
|
|
2878
|
+
}
|
|
2879
|
+
}
|
|
2880
|
+
}
|
|
2881
|
+
}
|
|
2882
|
+
schedule(job) {
|
|
2883
|
+
if (this.running && this.active.size < this.maxConcurrentJobs && !this.active.has(job.id)) {
|
|
2884
|
+
this.execute(job);
|
|
2885
|
+
}
|
|
2886
|
+
}
|
|
2887
|
+
recover() {
|
|
2888
|
+
const inProgress = this.store.getInProgressJobs();
|
|
2889
|
+
for (const job of inProgress) {
|
|
2890
|
+
if (this.active.size >= this.maxConcurrentJobs) break;
|
|
2891
|
+
let recoveredJob = job;
|
|
2892
|
+
if (job.type === "payment" && (job.state === "executing" || job.state === "inflight")) {
|
|
2893
|
+
recoveredJob = this.store.updateJob(job.id, { state: "inflight" });
|
|
2894
|
+
}
|
|
2895
|
+
this.execute(recoveredJob);
|
|
2896
|
+
}
|
|
2897
|
+
}
|
|
2898
|
+
execute(job) {
|
|
2899
|
+
const abortController = new AbortController();
|
|
2900
|
+
const promise = (async () => {
|
|
2901
|
+
try {
|
|
2902
|
+
const generator = job.type === "payment" ? runPaymentJob(job, this.rpc, this.retryPolicy, abortController.signal) : job.type === "invoice" ? runInvoiceJob(job, this.rpc, this.retryPolicy, abortController.signal) : runChannelJob(job, this.rpc, this.retryPolicy, abortController.signal);
|
|
2903
|
+
for await (const updated of generator) {
|
|
2904
|
+
const prev = this.getJob(updated.id);
|
|
2905
|
+
const fromState = prev?.state ?? job.state;
|
|
2906
|
+
this.store.updateJob(updated.id, updated);
|
|
2907
|
+
this.store.addJobEvent(
|
|
2908
|
+
updated.id,
|
|
2909
|
+
stateToEvent(updated.state),
|
|
2910
|
+
fromState,
|
|
2911
|
+
updated.state,
|
|
2912
|
+
this.buildEventData(updated)
|
|
2913
|
+
);
|
|
2914
|
+
this.emit("job:state_changed", updated, fromState);
|
|
2915
|
+
if (updated.state === "succeeded") this.emit("job:succeeded", updated);
|
|
2916
|
+
if (updated.state === "failed") this.emit("job:failed", updated);
|
|
2917
|
+
if (updated.state === "cancelled") this.emit("job:cancelled", updated);
|
|
2918
|
+
}
|
|
2919
|
+
} finally {
|
|
2920
|
+
this.active.delete(job.id);
|
|
2921
|
+
}
|
|
2922
|
+
})();
|
|
2923
|
+
this.active.set(job.id, { abortController, promise });
|
|
2924
|
+
}
|
|
2925
|
+
buildEventData(job) {
|
|
2926
|
+
const base = {
|
|
2927
|
+
type: job.type,
|
|
2928
|
+
idempotencyKey: job.idempotencyKey,
|
|
2929
|
+
retryCount: job.retryCount,
|
|
2930
|
+
maxRetries: job.maxRetries,
|
|
2931
|
+
nextRetryAt: job.nextRetryAt
|
|
2932
|
+
};
|
|
2933
|
+
if (job.error) {
|
|
2934
|
+
base.error = {
|
|
2935
|
+
category: job.error.category,
|
|
2936
|
+
retryable: job.error.retryable,
|
|
2937
|
+
message: job.error.message
|
|
2938
|
+
};
|
|
2939
|
+
}
|
|
2940
|
+
if (job.type === "payment") {
|
|
2941
|
+
const paymentJob = job;
|
|
2942
|
+
return {
|
|
2943
|
+
...base,
|
|
2944
|
+
invoice: paymentJob.params.invoice,
|
|
2945
|
+
paymentHash: paymentJob.result?.paymentHash,
|
|
2946
|
+
paymentStatus: paymentJob.result?.status
|
|
2947
|
+
};
|
|
2948
|
+
}
|
|
2949
|
+
if (job.type === "invoice") {
|
|
2950
|
+
const invoiceJob = job;
|
|
2951
|
+
return {
|
|
2952
|
+
...base,
|
|
2953
|
+
action: invoiceJob.params.action,
|
|
2954
|
+
paymentHash: invoiceJob.result?.paymentHash ?? invoiceJob.params.getInvoicePaymentHash,
|
|
2955
|
+
invoiceStatus: invoiceJob.result?.status
|
|
2956
|
+
};
|
|
2957
|
+
}
|
|
2958
|
+
const channelJob = job;
|
|
2959
|
+
return {
|
|
2960
|
+
...base,
|
|
2961
|
+
action: channelJob.params.action,
|
|
2962
|
+
channelId: channelJob.params.channelId ?? channelJob.params.shutdownChannelParams?.channel_id ?? channelJob.result?.channelId,
|
|
2963
|
+
peerId: channelJob.params.peerId ?? channelJob.params.openChannelParams?.peer_id,
|
|
2964
|
+
channelState: channelJob.result?.state
|
|
2965
|
+
};
|
|
2966
|
+
}
|
|
2967
|
+
};
|
|
2968
|
+
function deriveInvoiceKey(params) {
|
|
2969
|
+
if (params.action === "watch" && params.getInvoicePaymentHash) {
|
|
2970
|
+
return `invoice:watch:${params.getInvoicePaymentHash}`;
|
|
2971
|
+
}
|
|
2972
|
+
if (params.action === "cancel" && params.cancelInvoiceParams?.payment_hash) {
|
|
2973
|
+
return `invoice:cancel:${params.cancelInvoiceParams.payment_hash}`;
|
|
2974
|
+
}
|
|
2975
|
+
if (params.action === "settle" && params.settleInvoiceParams?.payment_hash) {
|
|
2976
|
+
return `invoice:settle:${params.settleInvoiceParams.payment_hash}`;
|
|
2977
|
+
}
|
|
2978
|
+
if (params.action === "create" && params.newInvoiceParams?.payment_hash) {
|
|
2979
|
+
return `invoice:create:${params.newInvoiceParams.payment_hash}`;
|
|
2980
|
+
}
|
|
2981
|
+
return void 0;
|
|
2982
|
+
}
|
|
2983
|
+
function deriveChannelKey(params) {
|
|
2984
|
+
if (params.action === "open") return void 0;
|
|
2985
|
+
if (params.acceptChannelParams?.temporary_channel_id) {
|
|
2986
|
+
return `channel:accept:${params.acceptChannelParams.temporary_channel_id}`;
|
|
2987
|
+
}
|
|
2988
|
+
if (params.action === "shutdown" && params.shutdownChannelParams?.channel_id) {
|
|
2989
|
+
return `channel:shutdown:${params.shutdownChannelParams.channel_id}`;
|
|
2990
|
+
}
|
|
2991
|
+
if (params.action === "abandon" && params.abandonChannelParams?.channel_id) {
|
|
2992
|
+
return `channel:abandon:${params.abandonChannelParams.channel_id}`;
|
|
2993
|
+
}
|
|
2994
|
+
if (params.action === "update" && params.updateChannelParams?.channel_id) {
|
|
2995
|
+
const fingerprint = stableStringify(params.updateChannelParams);
|
|
2996
|
+
return `channel:update:${params.updateChannelParams.channel_id}:${fingerprint}`;
|
|
2997
|
+
}
|
|
2998
|
+
if (params.channelId) return `channel:${params.action}:${params.channelId}`;
|
|
2999
|
+
return void 0;
|
|
3000
|
+
}
|
|
3001
|
+
function stateToEvent(state) {
|
|
3002
|
+
switch (state) {
|
|
3003
|
+
case "executing":
|
|
3004
|
+
return "executing";
|
|
3005
|
+
case "inflight":
|
|
3006
|
+
return "inflight";
|
|
3007
|
+
case "invoice_created":
|
|
3008
|
+
return "invoice_created";
|
|
3009
|
+
case "invoice_received":
|
|
3010
|
+
return "invoice_received";
|
|
3011
|
+
case "invoice_settled":
|
|
3012
|
+
return "invoice_settled";
|
|
3013
|
+
case "invoice_expired":
|
|
3014
|
+
return "invoice_expired";
|
|
3015
|
+
case "invoice_cancelled":
|
|
3016
|
+
return "invoice_cancelled";
|
|
3017
|
+
case "channel_opening":
|
|
3018
|
+
return "channel_opening";
|
|
3019
|
+
case "channel_accepting":
|
|
3020
|
+
return "channel_accepting";
|
|
3021
|
+
case "channel_abandoning":
|
|
3022
|
+
return "channel_abandoning";
|
|
3023
|
+
case "channel_updating":
|
|
3024
|
+
return "channel_updating";
|
|
3025
|
+
case "channel_awaiting_ready":
|
|
3026
|
+
return "channel_awaiting_ready";
|
|
3027
|
+
case "channel_ready":
|
|
3028
|
+
return "channel_ready";
|
|
3029
|
+
case "channel_closing":
|
|
3030
|
+
return "channel_closing";
|
|
3031
|
+
case "channel_closed":
|
|
3032
|
+
return "channel_closed";
|
|
3033
|
+
case "waiting_retry":
|
|
3034
|
+
return "retry_scheduled";
|
|
3035
|
+
case "succeeded":
|
|
3036
|
+
return "succeeded";
|
|
3037
|
+
case "failed":
|
|
3038
|
+
return "failed";
|
|
3039
|
+
case "cancelled":
|
|
3040
|
+
return "cancelled";
|
|
3041
|
+
default:
|
|
3042
|
+
return "executing";
|
|
3043
|
+
}
|
|
3044
|
+
}
|
|
3045
|
+
|
|
3046
|
+
// src/service.ts
|
|
3047
|
+
var FiberMonitorService = class extends EventEmitter2 {
|
|
3048
|
+
config;
|
|
3049
|
+
startedAt;
|
|
3050
|
+
client;
|
|
3051
|
+
store;
|
|
3052
|
+
alerts;
|
|
3053
|
+
monitors;
|
|
3054
|
+
proxy;
|
|
3055
|
+
jobStore;
|
|
3056
|
+
jobManager;
|
|
3057
|
+
running = false;
|
|
3058
|
+
constructor(configInput = {}) {
|
|
3059
|
+
super();
|
|
3060
|
+
this.config = createRuntimeConfig(configInput);
|
|
3061
|
+
this.startedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
3062
|
+
this.client = new FiberRpcClient2({
|
|
3063
|
+
url: this.config.fiberRpcUrl,
|
|
3064
|
+
timeout: this.config.requestTimeoutMs
|
|
3065
|
+
});
|
|
3066
|
+
this.store = new MemoryStore({
|
|
3067
|
+
stateFilePath: this.config.storage.stateFilePath,
|
|
3068
|
+
flushIntervalMs: this.config.storage.flushIntervalMs,
|
|
3069
|
+
maxAlertHistory: this.config.storage.maxAlertHistory
|
|
3070
|
+
});
|
|
3071
|
+
this.alerts = new AlertManager({
|
|
3072
|
+
backends: this.createAlertBackends(this.config),
|
|
3073
|
+
store: this.store
|
|
3074
|
+
});
|
|
3075
|
+
this.jobStore = this.config.jobs.enabled ? new SqliteJobStore(this.config.jobs.dbPath) : null;
|
|
3076
|
+
this.jobManager = this.jobStore ? new JobManager(this.client, this.jobStore, {
|
|
3077
|
+
maxConcurrentJobs: this.config.jobs.maxConcurrentJobs,
|
|
3078
|
+
schedulerIntervalMs: this.config.jobs.schedulerIntervalMs,
|
|
3079
|
+
retryPolicy: this.config.jobs.retryPolicy
|
|
3080
|
+
}) : null;
|
|
3081
|
+
this.wireJobAlerts();
|
|
3082
|
+
const hooks = {};
|
|
3083
|
+
this.monitors = [
|
|
3084
|
+
new ChannelMonitor({
|
|
3085
|
+
client: this.client,
|
|
3086
|
+
store: this.store,
|
|
3087
|
+
alerts: this.alerts,
|
|
3088
|
+
config: {
|
|
3089
|
+
intervalMs: this.config.channelPollIntervalMs,
|
|
3090
|
+
includeClosedChannels: this.config.includeClosedChannels
|
|
3091
|
+
},
|
|
3092
|
+
hooks
|
|
3093
|
+
}),
|
|
3094
|
+
new InvoiceTracker({
|
|
3095
|
+
client: this.client,
|
|
3096
|
+
store: this.store,
|
|
3097
|
+
alerts: this.alerts,
|
|
3098
|
+
config: {
|
|
3099
|
+
intervalMs: this.config.invoicePollIntervalMs,
|
|
3100
|
+
completedItemTtlSeconds: this.config.completedItemTtlSeconds
|
|
3101
|
+
},
|
|
3102
|
+
hooks
|
|
3103
|
+
}),
|
|
3104
|
+
new PaymentTracker({
|
|
3105
|
+
client: this.client,
|
|
3106
|
+
store: this.store,
|
|
3107
|
+
alerts: this.alerts,
|
|
3108
|
+
config: {
|
|
3109
|
+
intervalMs: this.config.paymentPollIntervalMs,
|
|
3110
|
+
completedItemTtlSeconds: this.config.completedItemTtlSeconds
|
|
3111
|
+
},
|
|
3112
|
+
hooks
|
|
3113
|
+
}),
|
|
3114
|
+
new PeerMonitor({
|
|
3115
|
+
client: this.client,
|
|
3116
|
+
store: this.store,
|
|
3117
|
+
alerts: this.alerts,
|
|
3118
|
+
config: { intervalMs: this.config.peerPollIntervalMs },
|
|
3119
|
+
hooks
|
|
3120
|
+
}),
|
|
3121
|
+
new HealthMonitor({
|
|
3122
|
+
client: this.client,
|
|
3123
|
+
alerts: this.alerts,
|
|
3124
|
+
config: { intervalMs: this.config.healthPollIntervalMs }
|
|
3125
|
+
})
|
|
3126
|
+
];
|
|
3127
|
+
this.alerts.onEmit((alert) => {
|
|
3128
|
+
this.emit("alert", alert);
|
|
3129
|
+
});
|
|
3130
|
+
this.proxy = new RpcMonitorProxy(
|
|
3131
|
+
{
|
|
3132
|
+
listen: this.config.proxy.listen,
|
|
3133
|
+
targetUrl: this.config.fiberRpcUrl
|
|
3134
|
+
},
|
|
3135
|
+
{
|
|
3136
|
+
onInvoiceTracked: (paymentHash) => {
|
|
3137
|
+
this.store.addTrackedInvoice(paymentHash);
|
|
3138
|
+
},
|
|
3139
|
+
onPaymentTracked: (paymentHash) => {
|
|
3140
|
+
this.store.addTrackedPayment(paymentHash);
|
|
3141
|
+
},
|
|
3142
|
+
listTrackedInvoices: () => this.store.listTrackedInvoices(),
|
|
3143
|
+
listTrackedPayments: () => this.store.listTrackedPayments(),
|
|
3144
|
+
listAlerts: (filters) => this.store.listAlerts(filters),
|
|
3145
|
+
getStatus: () => this.getStatus(),
|
|
3146
|
+
createPaymentJob: this.jobManager ? (params, options) => this.jobManager.ensurePayment(params, options) : void 0,
|
|
3147
|
+
createInvoiceJob: this.jobManager ? (params, options) => this.jobManager.manageInvoice(params, options) : void 0,
|
|
3148
|
+
createChannelJob: this.jobManager ? (params, options) => this.jobManager.manageChannel(params, options) : void 0,
|
|
3149
|
+
getJob: this.jobManager ? (id) => this.jobManager.getJob(id) : void 0,
|
|
3150
|
+
listJobs: this.jobManager ? (filter) => this.jobManager.listJobs(filter) : void 0,
|
|
3151
|
+
cancelJob: this.jobManager ? (id) => this.jobManager.cancelJob(id) : void 0,
|
|
3152
|
+
listJobEvents: this.jobStore ? (jobId) => this.jobStore.listJobEvents(jobId) : void 0
|
|
3153
|
+
}
|
|
3154
|
+
);
|
|
3155
|
+
}
|
|
3156
|
+
async start() {
|
|
3157
|
+
if (this.running) {
|
|
3158
|
+
return;
|
|
3159
|
+
}
|
|
3160
|
+
await this.store.load();
|
|
3161
|
+
this.store.startAutoFlush();
|
|
3162
|
+
await this.alerts.start();
|
|
3163
|
+
for (const monitor of this.monitors) {
|
|
3164
|
+
monitor.start();
|
|
3165
|
+
}
|
|
3166
|
+
this.jobManager?.start();
|
|
3167
|
+
if (this.config.proxy.enabled) {
|
|
3168
|
+
await this.proxy.start();
|
|
3169
|
+
}
|
|
3170
|
+
this.running = true;
|
|
3171
|
+
}
|
|
3172
|
+
async stop() {
|
|
3173
|
+
if (!this.running) {
|
|
3174
|
+
return;
|
|
3175
|
+
}
|
|
3176
|
+
for (const monitor of this.monitors) {
|
|
3177
|
+
monitor.stop();
|
|
3178
|
+
}
|
|
3179
|
+
await this.jobManager?.stop();
|
|
3180
|
+
if (this.config.proxy.enabled) {
|
|
3181
|
+
await this.proxy.stop();
|
|
3182
|
+
}
|
|
3183
|
+
this.store.stopAutoFlush();
|
|
3184
|
+
await this.store.flush();
|
|
3185
|
+
await this.alerts.stop();
|
|
3186
|
+
this.jobStore?.close();
|
|
3187
|
+
this.running = false;
|
|
3188
|
+
}
|
|
3189
|
+
getStatus() {
|
|
3190
|
+
return {
|
|
3191
|
+
startedAt: this.startedAt,
|
|
3192
|
+
proxyListen: this.config.proxy.listen,
|
|
3193
|
+
targetUrl: this.config.fiberRpcUrl,
|
|
3194
|
+
running: this.running
|
|
3195
|
+
};
|
|
3196
|
+
}
|
|
3197
|
+
listAlerts(filters) {
|
|
3198
|
+
return this.store.listAlerts(filters);
|
|
3199
|
+
}
|
|
3200
|
+
listTrackedInvoices() {
|
|
3201
|
+
return this.store.listTrackedInvoices();
|
|
3202
|
+
}
|
|
3203
|
+
listTrackedPayments() {
|
|
3204
|
+
return this.store.listTrackedPayments();
|
|
3205
|
+
}
|
|
3206
|
+
trackInvoice(paymentHash) {
|
|
3207
|
+
this.store.addTrackedInvoice(paymentHash);
|
|
3208
|
+
}
|
|
3209
|
+
trackPayment(paymentHash) {
|
|
3210
|
+
this.store.addTrackedPayment(paymentHash);
|
|
3211
|
+
}
|
|
3212
|
+
createAlertBackends(config) {
|
|
3213
|
+
return config.alerts.map((alertConfig) => {
|
|
3214
|
+
if (alertConfig.type === "stdout") {
|
|
3215
|
+
return new StdoutAlertBackend();
|
|
3216
|
+
}
|
|
3217
|
+
if (alertConfig.type === "webhook") {
|
|
3218
|
+
return new WebhookAlertBackend({
|
|
3219
|
+
url: alertConfig.url,
|
|
3220
|
+
timeoutMs: alertConfig.timeoutMs,
|
|
3221
|
+
headers: alertConfig.headers
|
|
3222
|
+
});
|
|
3223
|
+
}
|
|
3224
|
+
if (alertConfig.type === "file") {
|
|
3225
|
+
return new JsonlFileAlertBackend(alertConfig.path);
|
|
3226
|
+
}
|
|
3227
|
+
const [host, portText] = alertConfig.listen.split(":");
|
|
3228
|
+
return new WebsocketAlertBackend({
|
|
3229
|
+
host,
|
|
3230
|
+
port: Number(portText)
|
|
3231
|
+
});
|
|
3232
|
+
});
|
|
3233
|
+
}
|
|
3234
|
+
wireJobAlerts() {
|
|
3235
|
+
if (!this.jobManager) {
|
|
3236
|
+
return;
|
|
3237
|
+
}
|
|
3238
|
+
this.jobManager.on("job:created", (job) => {
|
|
3239
|
+
this.emitJobAlert(job, "started", "low");
|
|
3240
|
+
this.trackJobArtifacts(job);
|
|
3241
|
+
});
|
|
3242
|
+
this.jobManager.on("job:state_changed", (job) => {
|
|
3243
|
+
this.trackJobArtifacts(job);
|
|
3244
|
+
if (job.state === "waiting_retry") {
|
|
3245
|
+
this.emitJobAlert(job, "retrying", "medium");
|
|
3246
|
+
}
|
|
3247
|
+
});
|
|
3248
|
+
this.jobManager.on("job:succeeded", (job) => {
|
|
3249
|
+
this.trackJobArtifacts(job);
|
|
3250
|
+
this.emitJobAlert(job, "succeeded", "medium");
|
|
3251
|
+
});
|
|
3252
|
+
this.jobManager.on("job:failed", (job) => {
|
|
3253
|
+
this.trackJobArtifacts(job);
|
|
3254
|
+
this.emitJobAlert(job, "failed", "high");
|
|
3255
|
+
});
|
|
3256
|
+
}
|
|
3257
|
+
emitJobAlert(job, lifecycle, priority) {
|
|
3258
|
+
const type = this.toJobAlertType(job.type, lifecycle);
|
|
3259
|
+
const data = this.toJobAlertData(job);
|
|
3260
|
+
void this.alerts.emit({
|
|
3261
|
+
type,
|
|
3262
|
+
priority,
|
|
3263
|
+
source: "job-manager",
|
|
3264
|
+
data
|
|
3265
|
+
});
|
|
3266
|
+
}
|
|
3267
|
+
toJobAlertType(jobType, lifecycle) {
|
|
3268
|
+
return `${jobType}_job_${lifecycle}`;
|
|
3269
|
+
}
|
|
3270
|
+
toJobAlertData(job) {
|
|
3271
|
+
const error = job.error?.message;
|
|
3272
|
+
if (job.type === "payment") {
|
|
3273
|
+
const paymentJob = job;
|
|
3274
|
+
return {
|
|
3275
|
+
jobId: paymentJob.id,
|
|
3276
|
+
idempotencyKey: paymentJob.idempotencyKey,
|
|
3277
|
+
retryCount: paymentJob.retryCount,
|
|
3278
|
+
error,
|
|
3279
|
+
fee: paymentJob.result?.fee
|
|
3280
|
+
};
|
|
3281
|
+
}
|
|
3282
|
+
if (job.type === "invoice") {
|
|
3283
|
+
const invoiceJob = job;
|
|
3284
|
+
return {
|
|
3285
|
+
jobId: invoiceJob.id,
|
|
3286
|
+
idempotencyKey: invoiceJob.idempotencyKey,
|
|
3287
|
+
retryCount: invoiceJob.retryCount,
|
|
3288
|
+
action: invoiceJob.params.action,
|
|
3289
|
+
status: invoiceJob.result?.status,
|
|
3290
|
+
paymentHash: this.extractInvoiceHash(invoiceJob),
|
|
3291
|
+
error
|
|
3292
|
+
};
|
|
3293
|
+
}
|
|
3294
|
+
const channelJob = job;
|
|
3295
|
+
return {
|
|
3296
|
+
jobId: channelJob.id,
|
|
3297
|
+
idempotencyKey: channelJob.idempotencyKey,
|
|
3298
|
+
retryCount: channelJob.retryCount,
|
|
3299
|
+
action: channelJob.params.action,
|
|
3300
|
+
channelId: this.extractChannelId(channelJob),
|
|
3301
|
+
error
|
|
3302
|
+
};
|
|
3303
|
+
}
|
|
3304
|
+
trackJobArtifacts(job) {
|
|
3305
|
+
if (job.type === "payment") {
|
|
3306
|
+
const paymentHash = this.extractPaymentHash(job);
|
|
3307
|
+
if (paymentHash) {
|
|
3308
|
+
this.store.addTrackedPayment(paymentHash);
|
|
3309
|
+
}
|
|
3310
|
+
return;
|
|
3311
|
+
}
|
|
3312
|
+
if (job.type === "invoice") {
|
|
3313
|
+
const paymentHash = this.extractInvoiceHash(job);
|
|
3314
|
+
if (paymentHash) {
|
|
3315
|
+
this.store.addTrackedInvoice(paymentHash);
|
|
3316
|
+
}
|
|
3317
|
+
}
|
|
3318
|
+
}
|
|
3319
|
+
extractPaymentHash(job) {
|
|
3320
|
+
return this.normalizeHash(job.result?.paymentHash ?? job.params.sendPaymentParams.payment_hash);
|
|
3321
|
+
}
|
|
3322
|
+
extractInvoiceHash(job) {
|
|
3323
|
+
return this.normalizeHash(
|
|
3324
|
+
job.result?.paymentHash ?? job.params.getInvoicePaymentHash ?? job.params.cancelInvoiceParams?.payment_hash ?? job.params.settleInvoiceParams?.payment_hash ?? job.params.newInvoiceParams?.payment_hash
|
|
3325
|
+
);
|
|
3326
|
+
}
|
|
3327
|
+
extractChannelId(job) {
|
|
3328
|
+
return this.normalizeHash(
|
|
3329
|
+
job.result?.channelId ?? job.result?.acceptedChannelId ?? job.params.channelId ?? job.params.shutdownChannelParams?.channel_id
|
|
3330
|
+
);
|
|
3331
|
+
}
|
|
3332
|
+
normalizeHash(value) {
|
|
3333
|
+
if (!value || !value.startsWith("0x")) {
|
|
3334
|
+
return void 0;
|
|
3335
|
+
}
|
|
3336
|
+
return value;
|
|
3337
|
+
}
|
|
3338
|
+
};
|
|
3339
|
+
|
|
3340
|
+
// src/bootstrap.ts
|
|
3341
|
+
async function startRuntimeService(configInput = {}) {
|
|
3342
|
+
const service = new FiberMonitorService(configInput);
|
|
3343
|
+
await service.start();
|
|
3344
|
+
let stopping = false;
|
|
3345
|
+
const stop = async () => {
|
|
3346
|
+
if (stopping) {
|
|
3347
|
+
return;
|
|
3348
|
+
}
|
|
3349
|
+
stopping = true;
|
|
3350
|
+
await service.stop();
|
|
3351
|
+
};
|
|
3352
|
+
const waitForShutdownSignal = () => {
|
|
3353
|
+
return new Promise((resolve2) => {
|
|
3354
|
+
const onSignal = (signal) => {
|
|
3355
|
+
process.off("SIGINT", onSignal);
|
|
3356
|
+
process.off("SIGTERM", onSignal);
|
|
3357
|
+
resolve2(signal);
|
|
3358
|
+
};
|
|
3359
|
+
process.on("SIGINT", onSignal);
|
|
3360
|
+
process.on("SIGTERM", onSignal);
|
|
3361
|
+
});
|
|
3362
|
+
};
|
|
3363
|
+
return {
|
|
3364
|
+
service,
|
|
3365
|
+
stop,
|
|
3366
|
+
waitForShutdownSignal
|
|
3367
|
+
};
|
|
3368
|
+
}
|
|
3369
|
+
export {
|
|
3370
|
+
FiberMonitorService,
|
|
3371
|
+
JobManager,
|
|
3372
|
+
MemoryStore,
|
|
3373
|
+
RpcMonitorProxy,
|
|
3374
|
+
SqliteJobStore,
|
|
3375
|
+
alertPriorityOrder,
|
|
3376
|
+
alertTypeValues,
|
|
3377
|
+
classifyRpcError,
|
|
3378
|
+
computeRetryDelay,
|
|
3379
|
+
createRuntimeConfig,
|
|
3380
|
+
defaultPaymentRetryPolicy,
|
|
3381
|
+
defaultRuntimeConfig,
|
|
3382
|
+
formatRuntimeAlert,
|
|
3383
|
+
isAlertPriority,
|
|
3384
|
+
isAlertType,
|
|
3385
|
+
paymentStateMachine,
|
|
3386
|
+
shouldRetry,
|
|
3387
|
+
startRuntimeService
|
|
3388
|
+
};
|
|
3389
|
+
//# sourceMappingURL=index.js.map
|