@fiber-pay/cli 0.1.0-rc.4 → 0.1.0-rc.5
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/dist/cli.js +1054 -270
- package/dist/cli.js.map +1 -1
- package/error-codes.json +10 -0
- package/package.json +4 -4
package/dist/cli.js
CHANGED
|
@@ -1,17 +1,168 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// src/index.ts
|
|
4
|
-
import { join as
|
|
4
|
+
import { join as join9 } from "path";
|
|
5
5
|
import { Command as Command13 } from "commander";
|
|
6
6
|
|
|
7
7
|
// src/commands/binary.ts
|
|
8
|
-
import {
|
|
9
|
-
DEFAULT_FIBER_VERSION,
|
|
10
|
-
downloadFiberBinary,
|
|
11
|
-
getFiberBinaryInfo
|
|
12
|
-
} from "@fiber-pay/node";
|
|
8
|
+
import { DEFAULT_FIBER_VERSION, downloadFiberBinary } from "@fiber-pay/node";
|
|
13
9
|
import { Command } from "commander";
|
|
14
10
|
|
|
11
|
+
// src/lib/binary-path.ts
|
|
12
|
+
import { dirname, join } from "path";
|
|
13
|
+
import { BinaryManager, getFiberBinaryInfo } from "@fiber-pay/node";
|
|
14
|
+
|
|
15
|
+
// src/lib/node-runtime-daemon.ts
|
|
16
|
+
import { spawnSync } from "child_process";
|
|
17
|
+
import { existsSync } from "fs";
|
|
18
|
+
function getCustomBinaryState(binaryPath) {
|
|
19
|
+
const exists = existsSync(binaryPath);
|
|
20
|
+
if (!exists) {
|
|
21
|
+
return { path: binaryPath, ready: false, version: "unknown" };
|
|
22
|
+
}
|
|
23
|
+
try {
|
|
24
|
+
const result = spawnSync(binaryPath, ["--version"], { encoding: "utf-8" });
|
|
25
|
+
if (result.status !== 0) {
|
|
26
|
+
return { path: binaryPath, ready: false, version: "unknown" };
|
|
27
|
+
}
|
|
28
|
+
const output = `${result.stdout ?? ""}${result.stderr ?? ""}`.trim();
|
|
29
|
+
const firstLine = output.split("\n").find((line) => line.trim().length > 0) ?? "unknown";
|
|
30
|
+
return { path: binaryPath, ready: true, version: firstLine.trim() };
|
|
31
|
+
} catch {
|
|
32
|
+
return { path: binaryPath, ready: false, version: "unknown" };
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
function getBinaryVersion(binaryPath) {
|
|
36
|
+
try {
|
|
37
|
+
const result = spawnSync(binaryPath, ["--version"], { encoding: "utf-8" });
|
|
38
|
+
if (result.status !== 0) {
|
|
39
|
+
return "unknown";
|
|
40
|
+
}
|
|
41
|
+
const output = `${result.stdout ?? ""}${result.stderr ?? ""}`.trim();
|
|
42
|
+
if (!output) {
|
|
43
|
+
return "unknown";
|
|
44
|
+
}
|
|
45
|
+
const firstLine = output.split("\n").find((line) => line.trim().length > 0);
|
|
46
|
+
return firstLine?.trim() ?? "unknown";
|
|
47
|
+
} catch {
|
|
48
|
+
return "unknown";
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
function getCliEntrypoint() {
|
|
52
|
+
const entrypoint = process.argv[1];
|
|
53
|
+
if (!entrypoint) {
|
|
54
|
+
throw new Error("Unable to resolve CLI entrypoint path");
|
|
55
|
+
}
|
|
56
|
+
return entrypoint;
|
|
57
|
+
}
|
|
58
|
+
function startRuntimeDaemonFromNode(params) {
|
|
59
|
+
const cliEntrypoint = getCliEntrypoint();
|
|
60
|
+
const result = spawnSync(
|
|
61
|
+
process.execPath,
|
|
62
|
+
[
|
|
63
|
+
cliEntrypoint,
|
|
64
|
+
"--data-dir",
|
|
65
|
+
params.dataDir,
|
|
66
|
+
"--rpc-url",
|
|
67
|
+
params.rpcUrl,
|
|
68
|
+
"runtime",
|
|
69
|
+
"start",
|
|
70
|
+
"--daemon",
|
|
71
|
+
"--fiber-rpc-url",
|
|
72
|
+
params.rpcUrl,
|
|
73
|
+
"--proxy-listen",
|
|
74
|
+
params.proxyListen,
|
|
75
|
+
"--state-file",
|
|
76
|
+
params.stateFilePath,
|
|
77
|
+
"--alert-logs-base-dir",
|
|
78
|
+
params.alertLogsBaseDir,
|
|
79
|
+
"--json"
|
|
80
|
+
],
|
|
81
|
+
{ encoding: "utf-8" }
|
|
82
|
+
);
|
|
83
|
+
if (result.status === 0) {
|
|
84
|
+
return { ok: true };
|
|
85
|
+
}
|
|
86
|
+
const stderr = (result.stderr ?? "").trim();
|
|
87
|
+
const stdout = (result.stdout ?? "").trim();
|
|
88
|
+
const details = stderr || stdout || `exit code ${result.status ?? "unknown"}`;
|
|
89
|
+
return { ok: false, message: details };
|
|
90
|
+
}
|
|
91
|
+
function stopRuntimeDaemonFromNode(params) {
|
|
92
|
+
const cliEntrypoint = getCliEntrypoint();
|
|
93
|
+
spawnSync(
|
|
94
|
+
process.execPath,
|
|
95
|
+
[
|
|
96
|
+
cliEntrypoint,
|
|
97
|
+
"--data-dir",
|
|
98
|
+
params.dataDir,
|
|
99
|
+
"--rpc-url",
|
|
100
|
+
params.rpcUrl,
|
|
101
|
+
"runtime",
|
|
102
|
+
"stop",
|
|
103
|
+
"--json"
|
|
104
|
+
],
|
|
105
|
+
{ encoding: "utf-8" }
|
|
106
|
+
);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// src/lib/binary-path.ts
|
|
110
|
+
function getProfileBinaryInstallDir(dataDir) {
|
|
111
|
+
return join(dataDir, "bin");
|
|
112
|
+
}
|
|
113
|
+
function getProfileManagedBinaryPath(dataDir) {
|
|
114
|
+
return new BinaryManager(getProfileBinaryInstallDir(dataDir)).getBinaryPath();
|
|
115
|
+
}
|
|
116
|
+
function validateConfiguredBinaryPath(binaryPath) {
|
|
117
|
+
const value = binaryPath.trim();
|
|
118
|
+
if (!value) {
|
|
119
|
+
throw new Error("Configured binaryPath cannot be empty");
|
|
120
|
+
}
|
|
121
|
+
if (value.includes("\0")) {
|
|
122
|
+
throw new Error("Configured binaryPath contains an invalid null byte");
|
|
123
|
+
}
|
|
124
|
+
return value;
|
|
125
|
+
}
|
|
126
|
+
function resolveBinaryPath(config) {
|
|
127
|
+
const managedPath = getProfileManagedBinaryPath(config.dataDir);
|
|
128
|
+
if (config.binaryPath) {
|
|
129
|
+
const binaryPath2 = validateConfiguredBinaryPath(config.binaryPath);
|
|
130
|
+
const installDir2 = dirname(binaryPath2);
|
|
131
|
+
const expectedPath = new BinaryManager(installDir2).getBinaryPath();
|
|
132
|
+
const managedByBinaryManager = expectedPath === binaryPath2;
|
|
133
|
+
const source = binaryPath2 === managedPath ? "profile-managed" : "configured-path";
|
|
134
|
+
return {
|
|
135
|
+
binaryPath: binaryPath2,
|
|
136
|
+
installDir: managedByBinaryManager ? installDir2 : null,
|
|
137
|
+
managedPath,
|
|
138
|
+
managedByBinaryManager,
|
|
139
|
+
source
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
const installDir = getProfileBinaryInstallDir(config.dataDir);
|
|
143
|
+
const binaryPath = managedPath;
|
|
144
|
+
return {
|
|
145
|
+
binaryPath,
|
|
146
|
+
installDir,
|
|
147
|
+
managedPath,
|
|
148
|
+
managedByBinaryManager: true,
|
|
149
|
+
source: "profile-managed"
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
function getBinaryManagerInstallDirOrThrow(resolvedBinary) {
|
|
153
|
+
if (resolvedBinary.installDir) {
|
|
154
|
+
return resolvedBinary.installDir;
|
|
155
|
+
}
|
|
156
|
+
throw new Error(
|
|
157
|
+
`Configured binaryPath "${resolvedBinary.binaryPath}" is incompatible with BinaryManager-managed path naming. BinaryManager expects "${new BinaryManager(dirname(resolvedBinary.binaryPath)).getBinaryPath()}". Set binaryPath to a standard managed name (fnn/fnn.exe) in the target directory, or unset binaryPath to use the profile-managed binary.`
|
|
158
|
+
);
|
|
159
|
+
}
|
|
160
|
+
async function getBinaryDetails(config) {
|
|
161
|
+
const resolvedBinary = resolveBinaryPath(config);
|
|
162
|
+
const info = resolvedBinary.installDir ? await getFiberBinaryInfo(resolvedBinary.installDir) : getCustomBinaryState(resolvedBinary.binaryPath);
|
|
163
|
+
return { resolvedBinary, info };
|
|
164
|
+
}
|
|
165
|
+
|
|
15
166
|
// src/lib/format.ts
|
|
16
167
|
import {
|
|
17
168
|
ChannelState,
|
|
@@ -284,21 +435,6 @@ function printPeerListHuman(peers) {
|
|
|
284
435
|
console.log(`${peerId} ${pubkey} ${peer.address}`);
|
|
285
436
|
}
|
|
286
437
|
}
|
|
287
|
-
function printNodeInfoHuman(data) {
|
|
288
|
-
console.log("Node Info");
|
|
289
|
-
console.log(` Node ID: ${data.nodeId}`);
|
|
290
|
-
console.log(` Version: ${data.version}`);
|
|
291
|
-
console.log(` Chain Hash: ${data.chainHash}`);
|
|
292
|
-
console.log(` Funding Address: ${data.fundingAddress}`);
|
|
293
|
-
console.log(` Channels: ${data.channelCount} (${data.pendingChannelCount} pending)`);
|
|
294
|
-
console.log(` Peers: ${data.peersCount}`);
|
|
295
|
-
if (data.addresses.length > 0) {
|
|
296
|
-
console.log(" Addresses:");
|
|
297
|
-
for (const addr of data.addresses) {
|
|
298
|
-
console.log(` - ${addr}`);
|
|
299
|
-
}
|
|
300
|
-
}
|
|
301
|
-
}
|
|
302
438
|
|
|
303
439
|
// src/commands/binary.ts
|
|
304
440
|
function showProgress(progress) {
|
|
@@ -311,14 +447,36 @@ function showProgress(progress) {
|
|
|
311
447
|
function createBinaryCommand(config) {
|
|
312
448
|
const binary = new Command("binary").description("Fiber binary management");
|
|
313
449
|
binary.command("download").option("--version <version>", "Fiber binary version", DEFAULT_FIBER_VERSION).option("--force", "Force re-download").option("--json").action(async (options) => {
|
|
450
|
+
const resolvedBinary = resolveBinaryPath(config);
|
|
451
|
+
let installDir;
|
|
452
|
+
try {
|
|
453
|
+
installDir = getBinaryManagerInstallDirOrThrow(resolvedBinary);
|
|
454
|
+
} catch (error) {
|
|
455
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
456
|
+
if (options.json) {
|
|
457
|
+
printJsonError({
|
|
458
|
+
code: "BINARY_PATH_INCOMPATIBLE",
|
|
459
|
+
message,
|
|
460
|
+
recoverable: true,
|
|
461
|
+
suggestion: "Use `fiber-pay config profile unset binaryPath` or set binaryPath to a standard fnn filename in the target directory."
|
|
462
|
+
});
|
|
463
|
+
} else {
|
|
464
|
+
console.error(`\u274C ${message}`);
|
|
465
|
+
}
|
|
466
|
+
process.exit(1);
|
|
467
|
+
}
|
|
314
468
|
const info = await downloadFiberBinary({
|
|
315
|
-
installDir
|
|
469
|
+
installDir,
|
|
316
470
|
version: options.version,
|
|
317
471
|
force: Boolean(options.force),
|
|
318
472
|
onProgress: options.json ? void 0 : showProgress
|
|
319
473
|
});
|
|
320
474
|
if (options.json) {
|
|
321
|
-
printJsonSuccess(
|
|
475
|
+
printJsonSuccess({
|
|
476
|
+
...info,
|
|
477
|
+
source: resolvedBinary.source,
|
|
478
|
+
resolvedPath: resolvedBinary.binaryPath
|
|
479
|
+
});
|
|
322
480
|
} else {
|
|
323
481
|
console.log("\n\u2705 Binary installed successfully!");
|
|
324
482
|
console.log(` Path: ${info.path}`);
|
|
@@ -327,9 +485,13 @@ function createBinaryCommand(config) {
|
|
|
327
485
|
}
|
|
328
486
|
});
|
|
329
487
|
binary.command("info").option("--json").action(async (options) => {
|
|
330
|
-
const info = await
|
|
488
|
+
const { resolvedBinary, info } = await getBinaryDetails(config);
|
|
331
489
|
if (options.json) {
|
|
332
|
-
printJsonSuccess(
|
|
490
|
+
printJsonSuccess({
|
|
491
|
+
...info,
|
|
492
|
+
source: resolvedBinary.source,
|
|
493
|
+
resolvedPath: resolvedBinary.binaryPath
|
|
494
|
+
});
|
|
333
495
|
} else {
|
|
334
496
|
console.log(info.ready ? "\u2705 Binary is ready" : "\u274C Binary not found or not executable");
|
|
335
497
|
console.log(` Path: ${info.path}`);
|
|
@@ -341,7 +503,7 @@ function createBinaryCommand(config) {
|
|
|
341
503
|
|
|
342
504
|
// src/commands/channel.ts
|
|
343
505
|
import { randomUUID } from "crypto";
|
|
344
|
-
import { ckbToShannons } from "@fiber-pay/sdk";
|
|
506
|
+
import { ckbToShannons as ckbToShannons2 } from "@fiber-pay/sdk";
|
|
345
507
|
import { Command as Command2 } from "commander";
|
|
346
508
|
|
|
347
509
|
// src/lib/async.ts
|
|
@@ -353,17 +515,17 @@ function sleep(ms) {
|
|
|
353
515
|
import { FiberRpcClient } from "@fiber-pay/sdk";
|
|
354
516
|
|
|
355
517
|
// src/lib/pid.ts
|
|
356
|
-
import { existsSync, readFileSync, unlinkSync, writeFileSync } from "fs";
|
|
357
|
-
import { join } from "path";
|
|
518
|
+
import { existsSync as existsSync2, readFileSync, unlinkSync, writeFileSync } from "fs";
|
|
519
|
+
import { join as join2 } from "path";
|
|
358
520
|
function getPidFilePath(dataDir) {
|
|
359
|
-
return
|
|
521
|
+
return join2(dataDir, "fiber.pid");
|
|
360
522
|
}
|
|
361
523
|
function writePidFile(dataDir, pid) {
|
|
362
524
|
writeFileSync(getPidFilePath(dataDir), String(pid));
|
|
363
525
|
}
|
|
364
526
|
function readPidFile(dataDir) {
|
|
365
527
|
const pidPath = getPidFilePath(dataDir);
|
|
366
|
-
if (!
|
|
528
|
+
if (!existsSync2(pidPath)) return null;
|
|
367
529
|
try {
|
|
368
530
|
return parseInt(readFileSync(pidPath, "utf-8").trim(), 10);
|
|
369
531
|
} catch {
|
|
@@ -372,7 +534,7 @@ function readPidFile(dataDir) {
|
|
|
372
534
|
}
|
|
373
535
|
function removePidFile(dataDir) {
|
|
374
536
|
const pidPath = getPidFilePath(dataDir);
|
|
375
|
-
if (
|
|
537
|
+
if (existsSync2(pidPath)) {
|
|
376
538
|
unlinkSync(pidPath);
|
|
377
539
|
}
|
|
378
540
|
}
|
|
@@ -386,20 +548,20 @@ function isProcessRunning(pid) {
|
|
|
386
548
|
}
|
|
387
549
|
|
|
388
550
|
// src/lib/runtime-meta.ts
|
|
389
|
-
import { existsSync as
|
|
390
|
-
import { join as
|
|
551
|
+
import { existsSync as existsSync3, readFileSync as readFileSync2, unlinkSync as unlinkSync2, writeFileSync as writeFileSync2 } from "fs";
|
|
552
|
+
import { join as join3 } from "path";
|
|
391
553
|
function getRuntimePidFilePath(dataDir) {
|
|
392
|
-
return
|
|
554
|
+
return join3(dataDir, "runtime.pid");
|
|
393
555
|
}
|
|
394
556
|
function getRuntimeMetaFilePath(dataDir) {
|
|
395
|
-
return
|
|
557
|
+
return join3(dataDir, "runtime.meta.json");
|
|
396
558
|
}
|
|
397
559
|
function writeRuntimePid(dataDir, pid) {
|
|
398
560
|
writeFileSync2(getRuntimePidFilePath(dataDir), String(pid));
|
|
399
561
|
}
|
|
400
562
|
function readRuntimePid(dataDir) {
|
|
401
563
|
const pidPath = getRuntimePidFilePath(dataDir);
|
|
402
|
-
if (!
|
|
564
|
+
if (!existsSync3(pidPath)) return null;
|
|
403
565
|
try {
|
|
404
566
|
return Number.parseInt(readFileSync2(pidPath, "utf-8").trim(), 10);
|
|
405
567
|
} catch {
|
|
@@ -411,7 +573,7 @@ function writeRuntimeMeta(dataDir, meta) {
|
|
|
411
573
|
}
|
|
412
574
|
function readRuntimeMeta(dataDir) {
|
|
413
575
|
const metaPath = getRuntimeMetaFilePath(dataDir);
|
|
414
|
-
if (!
|
|
576
|
+
if (!existsSync3(metaPath)) return null;
|
|
415
577
|
try {
|
|
416
578
|
return JSON.parse(readFileSync2(metaPath, "utf-8"));
|
|
417
579
|
} catch {
|
|
@@ -421,10 +583,10 @@ function readRuntimeMeta(dataDir) {
|
|
|
421
583
|
function removeRuntimeFiles(dataDir) {
|
|
422
584
|
const pidPath = getRuntimePidFilePath(dataDir);
|
|
423
585
|
const metaPath = getRuntimeMetaFilePath(dataDir);
|
|
424
|
-
if (
|
|
586
|
+
if (existsSync3(pidPath)) {
|
|
425
587
|
unlinkSync2(pidPath);
|
|
426
588
|
}
|
|
427
|
-
if (
|
|
589
|
+
if (existsSync3(metaPath)) {
|
|
428
590
|
unlinkSync2(metaPath);
|
|
429
591
|
}
|
|
430
592
|
}
|
|
@@ -457,7 +619,10 @@ function resolveRuntimeProxyUrl(config) {
|
|
|
457
619
|
}
|
|
458
620
|
function createRpcClient(config) {
|
|
459
621
|
const resolved = resolveRpcEndpoint(config);
|
|
460
|
-
return new FiberRpcClient({
|
|
622
|
+
return new FiberRpcClient({
|
|
623
|
+
url: resolved.url,
|
|
624
|
+
biscuitToken: config.rpcBiscuitToken
|
|
625
|
+
});
|
|
461
626
|
}
|
|
462
627
|
function resolveRpcEndpoint(config) {
|
|
463
628
|
const runtimeProxyUrl = resolveRuntimeProxyUrl(config);
|
|
@@ -534,6 +699,235 @@ async function waitForRuntimeJobTerminal(runtimeUrl, jobId, timeoutSeconds) {
|
|
|
534
699
|
throw new Error(`Timed out waiting for runtime job ${jobId}`);
|
|
535
700
|
}
|
|
536
701
|
|
|
702
|
+
// src/commands/rebalance.ts
|
|
703
|
+
import { ckbToShannons, shannonsToCkb as shannonsToCkb2 } from "@fiber-pay/sdk";
|
|
704
|
+
async function executeRebalance(config, params) {
|
|
705
|
+
const rpc = await createReadyRpcClient(config);
|
|
706
|
+
const amountCkb = parseFloat(params.amountInput);
|
|
707
|
+
const maxFeeCkb = params.maxFeeInput !== void 0 ? parseFloat(params.maxFeeInput) : void 0;
|
|
708
|
+
const manualHops = params.hops ?? [];
|
|
709
|
+
if (!Number.isFinite(amountCkb) || amountCkb <= 0) {
|
|
710
|
+
const message = "Invalid --amount value. Expected a positive CKB amount.";
|
|
711
|
+
if (params.json) {
|
|
712
|
+
printJsonError({
|
|
713
|
+
code: params.errorCode,
|
|
714
|
+
message,
|
|
715
|
+
recoverable: true,
|
|
716
|
+
suggestion: "Provide a positive number, e.g. `--amount 10`.",
|
|
717
|
+
details: { amount: params.amountInput }
|
|
718
|
+
});
|
|
719
|
+
} else {
|
|
720
|
+
console.error(`Error: ${message}`);
|
|
721
|
+
}
|
|
722
|
+
process.exit(1);
|
|
723
|
+
}
|
|
724
|
+
if (maxFeeCkb !== void 0 && (!Number.isFinite(maxFeeCkb) || maxFeeCkb < 0 || manualHops.length > 0)) {
|
|
725
|
+
const message = manualHops.length > 0 ? "--max-fee is only supported in auto rebalance mode (without manual hops)." : "Invalid --max-fee value. Expected a non-negative CKB amount.";
|
|
726
|
+
if (params.json) {
|
|
727
|
+
printJsonError({
|
|
728
|
+
code: params.errorCode,
|
|
729
|
+
message,
|
|
730
|
+
recoverable: true,
|
|
731
|
+
suggestion: manualHops.length > 0 ? "Remove `--max-fee` or run auto mode without manual hops." : "Provide a non-negative number, e.g. `--max-fee 0.01`.",
|
|
732
|
+
details: { maxFee: params.maxFeeInput, hasManualHops: manualHops.length > 0 }
|
|
733
|
+
});
|
|
734
|
+
} else {
|
|
735
|
+
console.error(`Error: ${message}`);
|
|
736
|
+
}
|
|
737
|
+
process.exit(1);
|
|
738
|
+
}
|
|
739
|
+
const selfPubkey = (await rpc.nodeInfo()).node_id;
|
|
740
|
+
const amount = ckbToShannons(amountCkb);
|
|
741
|
+
const isManual = manualHops.length > 0;
|
|
742
|
+
let routeHopCount;
|
|
743
|
+
const result = isManual ? await (async () => {
|
|
744
|
+
const hopsInfo = [
|
|
745
|
+
...manualHops.map((pubkey) => ({ pubkey })),
|
|
746
|
+
...manualHops[manualHops.length - 1] === selfPubkey ? [] : [{ pubkey: selfPubkey }]
|
|
747
|
+
];
|
|
748
|
+
const route = await rpc.buildRouter({
|
|
749
|
+
amount,
|
|
750
|
+
hops_info: hopsInfo
|
|
751
|
+
});
|
|
752
|
+
routeHopCount = route.router_hops.length;
|
|
753
|
+
return rpc.sendPaymentWithRouter({
|
|
754
|
+
router: route.router_hops,
|
|
755
|
+
keysend: true,
|
|
756
|
+
allow_self_payment: true,
|
|
757
|
+
dry_run: params.dryRun ? true : void 0
|
|
758
|
+
});
|
|
759
|
+
})() : await rpc.sendPayment({
|
|
760
|
+
target_pubkey: selfPubkey,
|
|
761
|
+
amount,
|
|
762
|
+
keysend: true,
|
|
763
|
+
allow_self_payment: true,
|
|
764
|
+
max_fee_amount: maxFeeCkb !== void 0 ? ckbToShannons(maxFeeCkb) : void 0,
|
|
765
|
+
dry_run: params.dryRun ? true : void 0
|
|
766
|
+
});
|
|
767
|
+
const payload = {
|
|
768
|
+
mode: isManual ? "manual" : "auto",
|
|
769
|
+
selfPubkey,
|
|
770
|
+
amountCkb,
|
|
771
|
+
maxFeeCkb: isManual ? void 0 : maxFeeCkb,
|
|
772
|
+
routeHopCount,
|
|
773
|
+
paymentHash: result.payment_hash,
|
|
774
|
+
status: result.status === "Success" ? "success" : result.status === "Failed" ? "failed" : "pending",
|
|
775
|
+
feeCkb: shannonsToCkb2(result.fee),
|
|
776
|
+
failureReason: result.failed_error,
|
|
777
|
+
dryRun: params.dryRun
|
|
778
|
+
};
|
|
779
|
+
if (params.json) {
|
|
780
|
+
printJsonSuccess(payload);
|
|
781
|
+
} else {
|
|
782
|
+
console.log(
|
|
783
|
+
payload.dryRun ? `Rebalance dry-run complete (${payload.mode} route)` : `Rebalance sent (${payload.mode} route)`
|
|
784
|
+
);
|
|
785
|
+
console.log(` Self: ${payload.selfPubkey}`);
|
|
786
|
+
console.log(` Amount: ${payload.amountCkb} CKB`);
|
|
787
|
+
if (payload.mode === "manual" && payload.routeHopCount !== void 0) {
|
|
788
|
+
console.log(` Hops: ${payload.routeHopCount}`);
|
|
789
|
+
}
|
|
790
|
+
console.log(` Hash: ${payload.paymentHash}`);
|
|
791
|
+
console.log(` Status: ${payload.status}`);
|
|
792
|
+
console.log(` Fee: ${payload.feeCkb} CKB`);
|
|
793
|
+
if (payload.mode === "auto" && payload.maxFeeCkb !== void 0) {
|
|
794
|
+
console.log(` MaxFee: ${payload.maxFeeCkb} CKB`);
|
|
795
|
+
}
|
|
796
|
+
if (payload.failureReason) {
|
|
797
|
+
console.log(` Error: ${payload.failureReason}`);
|
|
798
|
+
}
|
|
799
|
+
}
|
|
800
|
+
}
|
|
801
|
+
function registerPaymentRebalanceCommand(parent, config) {
|
|
802
|
+
parent.command("rebalance").description("Technical rebalance command mapped to payment-layer circular self-payment").requiredOption("--amount <ckb>", "Amount in CKB to rebalance").option("--max-fee <ckb>", "Maximum fee in CKB (auto mode only)").option(
|
|
803
|
+
"--hops <pubkeys>",
|
|
804
|
+
"Comma-separated peer pubkeys for manual route mode (self pubkey appended automatically)"
|
|
805
|
+
).option("--dry-run", "Simulate route/payment and return estimated result").option("--json").action(async (options) => {
|
|
806
|
+
const hasHopsOption = typeof options.hops === "string";
|
|
807
|
+
const manualHops = hasHopsOption ? options.hops.split(",").map((item) => item.trim()).filter(Boolean) : [];
|
|
808
|
+
if (hasHopsOption && manualHops.length === 0) {
|
|
809
|
+
const message = "Invalid --hops value. Expected a non-empty comma-separated list of pubkeys.";
|
|
810
|
+
if (options.json) {
|
|
811
|
+
printJsonError({
|
|
812
|
+
code: "PAYMENT_REBALANCE_INPUT_INVALID",
|
|
813
|
+
message,
|
|
814
|
+
recoverable: true,
|
|
815
|
+
suggestion: "Provide pubkeys like `--hops 0xabc...,0xdef...`.",
|
|
816
|
+
details: { hops: options.hops }
|
|
817
|
+
});
|
|
818
|
+
} else {
|
|
819
|
+
console.error(`Error: ${message}`);
|
|
820
|
+
}
|
|
821
|
+
process.exit(1);
|
|
822
|
+
}
|
|
823
|
+
await executeRebalance(config, {
|
|
824
|
+
amountInput: options.amount,
|
|
825
|
+
maxFeeInput: options.maxFee,
|
|
826
|
+
hops: manualHops,
|
|
827
|
+
dryRun: Boolean(options.dryRun),
|
|
828
|
+
json: Boolean(options.json),
|
|
829
|
+
errorCode: "PAYMENT_REBALANCE_INPUT_INVALID"
|
|
830
|
+
});
|
|
831
|
+
});
|
|
832
|
+
}
|
|
833
|
+
function registerChannelRebalanceCommand(parent, config) {
|
|
834
|
+
parent.command("rebalance").description("High-level channel rebalance wrapper using payment-layer orchestration").requiredOption("--amount <ckb>", "Amount in CKB to rebalance").option("--from-channel <channelId>", "Source-biased channel id (optional)").option("--to-channel <channelId>", "Destination-biased channel id (optional)").option("--max-fee <ckb>", "Maximum fee in CKB (auto mode only)").option("--dry-run", "Simulate route/payment and return estimated result").option("--json").action(async (options) => {
|
|
835
|
+
const json = Boolean(options.json);
|
|
836
|
+
const fromChannelId = options.fromChannel;
|
|
837
|
+
const toChannelId = options.toChannel;
|
|
838
|
+
if (fromChannelId && !toChannelId || !fromChannelId && toChannelId) {
|
|
839
|
+
const message = "Both --from-channel and --to-channel must be provided together for guided channel rebalance.";
|
|
840
|
+
if (json) {
|
|
841
|
+
printJsonError({
|
|
842
|
+
code: "CHANNEL_REBALANCE_INPUT_INVALID",
|
|
843
|
+
message,
|
|
844
|
+
recoverable: true,
|
|
845
|
+
suggestion: "Provide both channel ids, or provide neither to run auto mode.",
|
|
846
|
+
details: { fromChannel: fromChannelId, toChannel: toChannelId }
|
|
847
|
+
});
|
|
848
|
+
} else {
|
|
849
|
+
console.error(`Error: ${message}`);
|
|
850
|
+
}
|
|
851
|
+
process.exit(1);
|
|
852
|
+
}
|
|
853
|
+
let guidedHops;
|
|
854
|
+
if (fromChannelId && toChannelId) {
|
|
855
|
+
const rpc = await createReadyRpcClient(config);
|
|
856
|
+
const channels = (await rpc.listChannels({ include_closed: true })).channels;
|
|
857
|
+
const fromChannel = channels.find((item) => item.channel_id === fromChannelId);
|
|
858
|
+
const toChannel = channels.find((item) => item.channel_id === toChannelId);
|
|
859
|
+
if (!fromChannel || !toChannel) {
|
|
860
|
+
const message = "Invalid channel selection: source/target channel id not found.";
|
|
861
|
+
if (json) {
|
|
862
|
+
printJsonError({
|
|
863
|
+
code: "CHANNEL_REBALANCE_INPUT_INVALID",
|
|
864
|
+
message,
|
|
865
|
+
recoverable: true,
|
|
866
|
+
suggestion: "Run `channel list --json` and retry with valid channel ids.",
|
|
867
|
+
details: { fromChannel: fromChannelId, toChannel: toChannelId }
|
|
868
|
+
});
|
|
869
|
+
} else {
|
|
870
|
+
console.error(`Error: ${message}`);
|
|
871
|
+
}
|
|
872
|
+
process.exit(1);
|
|
873
|
+
}
|
|
874
|
+
if (fromChannel.peer_id === toChannel.peer_id) {
|
|
875
|
+
const message = "Source and target channels point to the same peer; choose two different channel peers.";
|
|
876
|
+
if (json) {
|
|
877
|
+
printJsonError({
|
|
878
|
+
code: "CHANNEL_REBALANCE_INPUT_INVALID",
|
|
879
|
+
message,
|
|
880
|
+
recoverable: true,
|
|
881
|
+
suggestion: "Select channels with different peer ids for guided rebalance.",
|
|
882
|
+
details: {
|
|
883
|
+
fromChannel: fromChannelId,
|
|
884
|
+
toChannel: toChannelId,
|
|
885
|
+
peerId: fromChannel.peer_id
|
|
886
|
+
}
|
|
887
|
+
});
|
|
888
|
+
} else {
|
|
889
|
+
console.error(`Error: ${message}`);
|
|
890
|
+
}
|
|
891
|
+
process.exit(1);
|
|
892
|
+
}
|
|
893
|
+
const peers = (await rpc.listPeers()).peers;
|
|
894
|
+
const pubkeyByPeerId = new Map(peers.map((peer) => [peer.peer_id, peer.pubkey]));
|
|
895
|
+
const fromPubkey = pubkeyByPeerId.get(fromChannel.peer_id);
|
|
896
|
+
const toPubkey = pubkeyByPeerId.get(toChannel.peer_id);
|
|
897
|
+
if (!fromPubkey || !toPubkey) {
|
|
898
|
+
const message = "Unable to resolve selected channel peer_id to pubkey for guided rebalance route.";
|
|
899
|
+
if (json) {
|
|
900
|
+
printJsonError({
|
|
901
|
+
code: "CHANNEL_REBALANCE_INPUT_INVALID",
|
|
902
|
+
message,
|
|
903
|
+
recoverable: true,
|
|
904
|
+
suggestion: "Ensure both peers are connected (`peer list --json`) and retry guided mode, or use `payment rebalance --hops`.",
|
|
905
|
+
details: {
|
|
906
|
+
fromChannel: fromChannelId,
|
|
907
|
+
toChannel: toChannelId,
|
|
908
|
+
fromPeerId: fromChannel.peer_id,
|
|
909
|
+
toPeerId: toChannel.peer_id,
|
|
910
|
+
resolvedPeers: peers.length
|
|
911
|
+
}
|
|
912
|
+
});
|
|
913
|
+
} else {
|
|
914
|
+
console.error(`Error: ${message}`);
|
|
915
|
+
}
|
|
916
|
+
process.exit(1);
|
|
917
|
+
}
|
|
918
|
+
guidedHops = [fromPubkey, toPubkey];
|
|
919
|
+
}
|
|
920
|
+
await executeRebalance(config, {
|
|
921
|
+
amountInput: options.amount,
|
|
922
|
+
maxFeeInput: options.maxFee,
|
|
923
|
+
hops: guidedHops,
|
|
924
|
+
dryRun: Boolean(options.dryRun),
|
|
925
|
+
json,
|
|
926
|
+
errorCode: "CHANNEL_REBALANCE_INPUT_INVALID"
|
|
927
|
+
});
|
|
928
|
+
});
|
|
929
|
+
}
|
|
930
|
+
|
|
537
931
|
// src/commands/channel.ts
|
|
538
932
|
function createChannelCommand(config) {
|
|
539
933
|
const channel = new Command2("channel").description("Channel lifecycle and status commands");
|
|
@@ -708,7 +1102,7 @@ function createChannelCommand(config) {
|
|
|
708
1102
|
peerId,
|
|
709
1103
|
openChannelParams: {
|
|
710
1104
|
peer_id: peerId,
|
|
711
|
-
funding_amount:
|
|
1105
|
+
funding_amount: ckbToShannons2(fundingCkb),
|
|
712
1106
|
public: !options.private
|
|
713
1107
|
},
|
|
714
1108
|
waitForReady: false
|
|
@@ -741,7 +1135,7 @@ function createChannelCommand(config) {
|
|
|
741
1135
|
}
|
|
742
1136
|
const result = await rpc.openChannel({
|
|
743
1137
|
peer_id: peerId,
|
|
744
|
-
funding_amount:
|
|
1138
|
+
funding_amount: ckbToShannons2(fundingCkb),
|
|
745
1139
|
public: !options.private
|
|
746
1140
|
});
|
|
747
1141
|
const payload = { temporaryChannelId: result.temporary_channel_id, peer: peerId, fundingCkb };
|
|
@@ -765,7 +1159,7 @@ function createChannelCommand(config) {
|
|
|
765
1159
|
action: "accept",
|
|
766
1160
|
acceptChannelParams: {
|
|
767
1161
|
temporary_channel_id: temporaryChannelId,
|
|
768
|
-
funding_amount:
|
|
1162
|
+
funding_amount: ckbToShannons2(fundingCkb)
|
|
769
1163
|
}
|
|
770
1164
|
},
|
|
771
1165
|
options: {
|
|
@@ -793,7 +1187,7 @@ function createChannelCommand(config) {
|
|
|
793
1187
|
}
|
|
794
1188
|
const result = await rpc.acceptChannel({
|
|
795
1189
|
temporary_channel_id: temporaryChannelId,
|
|
796
|
-
funding_amount:
|
|
1190
|
+
funding_amount: ckbToShannons2(fundingCkb)
|
|
797
1191
|
});
|
|
798
1192
|
const payload = { channelId: result.channel_id, temporaryChannelId, fundingCkb };
|
|
799
1193
|
if (json) {
|
|
@@ -818,7 +1212,7 @@ function createChannelCommand(config) {
|
|
|
818
1212
|
channel_id: channelId,
|
|
819
1213
|
force: Boolean(options.force)
|
|
820
1214
|
},
|
|
821
|
-
waitForClosed:
|
|
1215
|
+
waitForClosed: Boolean(options.force)
|
|
822
1216
|
},
|
|
823
1217
|
options: {
|
|
824
1218
|
idempotencyKey: `shutdown:channel:${channelId}`
|
|
@@ -903,6 +1297,7 @@ function createChannelCommand(config) {
|
|
|
903
1297
|
console.log(` Channel ID: ${payload.channelId}`);
|
|
904
1298
|
}
|
|
905
1299
|
});
|
|
1300
|
+
registerChannelRebalanceCommand(channel, config);
|
|
906
1301
|
channel.command("update").argument("<channelId>").option("--enabled <enabled>").option("--tlc-expiry-delta <ms>").option("--tlc-minimum-value <shannonsHex>").option("--tlc-fee-proportional-millionths <value>").option("--json").action(async (channelId, options) => {
|
|
907
1302
|
const rpc = await createReadyRpcClient(config);
|
|
908
1303
|
const json = Boolean(options.json);
|
|
@@ -956,13 +1351,13 @@ function createChannelCommand(config) {
|
|
|
956
1351
|
}
|
|
957
1352
|
|
|
958
1353
|
// src/commands/config.ts
|
|
959
|
-
import { existsSync as
|
|
1354
|
+
import { existsSync as existsSync5, readFileSync as readFileSync4, writeFileSync as writeFileSync4 } from "fs";
|
|
960
1355
|
import { Command as Command3 } from "commander";
|
|
961
1356
|
import { parseDocument, stringify as yamlStringify } from "yaml";
|
|
962
1357
|
|
|
963
1358
|
// src/lib/config.ts
|
|
964
|
-
import { existsSync as
|
|
965
|
-
import { join as
|
|
1359
|
+
import { existsSync as existsSync4, mkdirSync, readFileSync as readFileSync3, writeFileSync as writeFileSync3 } from "fs";
|
|
1360
|
+
import { join as join4 } from "path";
|
|
966
1361
|
|
|
967
1362
|
// src/lib/config-templates.ts
|
|
968
1363
|
var TESTNET_CONFIG_TEMPLATE_V071 = `# This configuration file only contains the necessary configurations for the testnet deployment.
|
|
@@ -1121,7 +1516,7 @@ var DEFAULT_DATA_DIR = `${process.env.HOME}/.fiber-pay`;
|
|
|
1121
1516
|
var DEFAULT_RPC_URL = "http://127.0.0.1:8227";
|
|
1122
1517
|
var DEFAULT_NETWORK = "testnet";
|
|
1123
1518
|
function getConfigPath(dataDir) {
|
|
1124
|
-
return
|
|
1519
|
+
return join4(dataDir, "config.yml");
|
|
1125
1520
|
}
|
|
1126
1521
|
function parseNetworkFromConfig(configContent) {
|
|
1127
1522
|
const match = configContent.match(/^\s*chain:\s*(testnet|mainnet)\s*$/m);
|
|
@@ -1153,11 +1548,11 @@ function parseCkbRpcUrlFromConfig(configContent) {
|
|
|
1153
1548
|
return match?.[1]?.trim() || void 0;
|
|
1154
1549
|
}
|
|
1155
1550
|
function getProfilePath(dataDir) {
|
|
1156
|
-
return
|
|
1551
|
+
return join4(dataDir, "profile.json");
|
|
1157
1552
|
}
|
|
1158
1553
|
function loadProfileConfig(dataDir) {
|
|
1159
1554
|
const profilePath = getProfilePath(dataDir);
|
|
1160
|
-
if (!
|
|
1555
|
+
if (!existsSync4(profilePath)) return void 0;
|
|
1161
1556
|
try {
|
|
1162
1557
|
const raw = readFileSync3(profilePath, "utf-8");
|
|
1163
1558
|
return JSON.parse(raw);
|
|
@@ -1166,7 +1561,7 @@ function loadProfileConfig(dataDir) {
|
|
|
1166
1561
|
}
|
|
1167
1562
|
}
|
|
1168
1563
|
function saveProfileConfig(dataDir, profile) {
|
|
1169
|
-
if (!
|
|
1564
|
+
if (!existsSync4(dataDir)) {
|
|
1170
1565
|
mkdirSync(dataDir, { recursive: true });
|
|
1171
1566
|
}
|
|
1172
1567
|
const profilePath = getProfilePath(dataDir);
|
|
@@ -1175,11 +1570,11 @@ function saveProfileConfig(dataDir, profile) {
|
|
|
1175
1570
|
}
|
|
1176
1571
|
function writeNetworkConfigFile(dataDir, network, options = {}) {
|
|
1177
1572
|
const configPath = getConfigPath(dataDir);
|
|
1178
|
-
const alreadyExists =
|
|
1573
|
+
const alreadyExists = existsSync4(configPath);
|
|
1179
1574
|
if (alreadyExists && !options.force) {
|
|
1180
1575
|
return { path: configPath, created: false, overwritten: false };
|
|
1181
1576
|
}
|
|
1182
|
-
if (!
|
|
1577
|
+
if (!existsSync4(dataDir)) {
|
|
1183
1578
|
mkdirSync(dataDir, { recursive: true });
|
|
1184
1579
|
}
|
|
1185
1580
|
let content = getConfigTemplate(network);
|
|
@@ -1205,7 +1600,7 @@ function writeNetworkConfigFile(dataDir, network, options = {}) {
|
|
|
1205
1600
|
}
|
|
1206
1601
|
function ensureNodeConfigFile(dataDir, network) {
|
|
1207
1602
|
const configPath = getConfigPath(dataDir);
|
|
1208
|
-
if (!
|
|
1603
|
+
if (!existsSync4(configPath)) {
|
|
1209
1604
|
writeNetworkConfigFile(dataDir, network);
|
|
1210
1605
|
}
|
|
1211
1606
|
return configPath;
|
|
@@ -1214,7 +1609,7 @@ function getEffectiveConfig(explicitFlags2) {
|
|
|
1214
1609
|
const dataDir = process.env.FIBER_DATA_DIR || DEFAULT_DATA_DIR;
|
|
1215
1610
|
const dataDirSource = explicitFlags2?.has("dataDir") ? "cli" : process.env.FIBER_DATA_DIR ? "env" : "default";
|
|
1216
1611
|
const configPath = getConfigPath(dataDir);
|
|
1217
|
-
const configExists =
|
|
1612
|
+
const configExists = existsSync4(configPath);
|
|
1218
1613
|
const configContent = configExists ? readFileSync3(configPath, "utf-8") : void 0;
|
|
1219
1614
|
const profile = loadProfileConfig(dataDir);
|
|
1220
1615
|
const cliNetwork = explicitFlags2?.has("network") ? process.env.FIBER_NETWORK : void 0;
|
|
@@ -1227,6 +1622,10 @@ function getEffectiveConfig(explicitFlags2) {
|
|
|
1227
1622
|
const fileRpcUrl = configContent ? parseRpcUrlFromConfig(configContent) : void 0;
|
|
1228
1623
|
const rpcUrl = cliRpcUrl || envRpcUrl || fileRpcUrl || DEFAULT_RPC_URL;
|
|
1229
1624
|
const rpcUrlSource = cliRpcUrl ? "cli" : envRpcUrl ? "env" : fileRpcUrl ? "config" : "default";
|
|
1625
|
+
const cliRpcBiscuitToken = explicitFlags2?.has("rpcBiscuitToken") ? process.env.FIBER_RPC_BISCUIT_TOKEN : void 0;
|
|
1626
|
+
const envRpcBiscuitToken = !explicitFlags2?.has("rpcBiscuitToken") ? process.env.FIBER_RPC_BISCUIT_TOKEN : void 0;
|
|
1627
|
+
const rpcBiscuitToken = cliRpcBiscuitToken || envRpcBiscuitToken || void 0;
|
|
1628
|
+
const rpcBiscuitTokenSource = cliRpcBiscuitToken ? "cli" : envRpcBiscuitToken ? "env" : "unset";
|
|
1230
1629
|
const cliBinaryPath = explicitFlags2?.has("binaryPath") ? process.env.FIBER_BINARY_PATH : void 0;
|
|
1231
1630
|
const profileBinaryPath = profile?.binaryPath;
|
|
1232
1631
|
const envBinaryPath = !explicitFlags2?.has("binaryPath") ? process.env.FIBER_BINARY_PATH : void 0;
|
|
@@ -1253,6 +1652,7 @@ function getEffectiveConfig(explicitFlags2) {
|
|
|
1253
1652
|
configPath,
|
|
1254
1653
|
network,
|
|
1255
1654
|
rpcUrl,
|
|
1655
|
+
rpcBiscuitToken,
|
|
1256
1656
|
keyPassword,
|
|
1257
1657
|
ckbRpcUrl,
|
|
1258
1658
|
runtimeProxyListen
|
|
@@ -1262,6 +1662,7 @@ function getEffectiveConfig(explicitFlags2) {
|
|
|
1262
1662
|
configPath: "derived",
|
|
1263
1663
|
network: networkSource,
|
|
1264
1664
|
rpcUrl: rpcUrlSource,
|
|
1665
|
+
rpcBiscuitToken: rpcBiscuitTokenSource,
|
|
1265
1666
|
ckbRpcUrl: ckbRpcUrlSource,
|
|
1266
1667
|
runtimeProxyListen: runtimeProxyListenSource
|
|
1267
1668
|
}
|
|
@@ -1356,7 +1757,7 @@ function parseTypedValue(raw, valueType) {
|
|
|
1356
1757
|
return raw;
|
|
1357
1758
|
}
|
|
1358
1759
|
function ensureConfigFileOrExit(configPath, json) {
|
|
1359
|
-
if (!
|
|
1760
|
+
if (!existsSync5(configPath)) {
|
|
1360
1761
|
const msg = `Config file not found: ${configPath}. Run \`fiber-pay config init\` first.`;
|
|
1361
1762
|
if (json) {
|
|
1362
1763
|
printJsonError({
|
|
@@ -1728,7 +2129,7 @@ function createConfigCommand(_config) {
|
|
|
1728
2129
|
}
|
|
1729
2130
|
|
|
1730
2131
|
// src/commands/graph.ts
|
|
1731
|
-
import { shannonsToCkb as
|
|
2132
|
+
import { shannonsToCkb as shannonsToCkb3, toHex as toHex2 } from "@fiber-pay/sdk";
|
|
1732
2133
|
import { Command as Command4 } from "commander";
|
|
1733
2134
|
function printGraphNodeListHuman(nodes) {
|
|
1734
2135
|
if (nodes.length === 0) {
|
|
@@ -1743,7 +2144,7 @@ function printGraphNodeListHuman(nodes) {
|
|
|
1743
2144
|
const nodeId = truncateMiddle(node.node_id, 10, 8).padEnd(22, " ");
|
|
1744
2145
|
const alias = (node.node_name || "(unnamed)").slice(0, 20).padEnd(20, " ");
|
|
1745
2146
|
const version = (node.version || "?").slice(0, 10).padEnd(10, " ");
|
|
1746
|
-
const minFunding =
|
|
2147
|
+
const minFunding = shannonsToCkb3(node.auto_accept_min_ckb_funding_amount).toString().padStart(12, " ");
|
|
1747
2148
|
const age = formatAge(parseHexTimestampMs(node.timestamp));
|
|
1748
2149
|
console.log(`${nodeId} ${alias} ${version} ${minFunding} ${age}`);
|
|
1749
2150
|
}
|
|
@@ -1765,7 +2166,7 @@ function printGraphChannelListHuman(channels) {
|
|
|
1765
2166
|
const outpoint = ch.channel_outpoint ? truncateMiddle(`${ch.channel_outpoint.tx_hash}:${ch.channel_outpoint.index}`, 10, 8) : "n/a";
|
|
1766
2167
|
const n1 = truncateMiddle(ch.node1, 10, 8).padEnd(22, " ");
|
|
1767
2168
|
const n2 = truncateMiddle(ch.node2, 10, 8).padEnd(22, " ");
|
|
1768
|
-
const capacity = `${
|
|
2169
|
+
const capacity = `${shannonsToCkb3(ch.capacity)} CKB`.padStart(12, " ");
|
|
1769
2170
|
const age = formatAge(parseHexTimestampMs(ch.created_timestamp));
|
|
1770
2171
|
console.log(`${outpoint.padEnd(22, " ")} ${n1} ${n2} ${capacity} ${age}`);
|
|
1771
2172
|
}
|
|
@@ -1810,7 +2211,7 @@ Next cursor: ${result.last_cursor}`);
|
|
|
1810
2211
|
}
|
|
1811
2212
|
|
|
1812
2213
|
// src/commands/invoice.ts
|
|
1813
|
-
import { ckbToShannons as
|
|
2214
|
+
import { ckbToShannons as ckbToShannons3, randomBytes32, shannonsToCkb as shannonsToCkb4, toHex as toHex3 } from "@fiber-pay/sdk";
|
|
1814
2215
|
import { Command as Command5 } from "commander";
|
|
1815
2216
|
function createInvoiceCommand(config) {
|
|
1816
2217
|
const invoice = new Command5("invoice").description("Invoice lifecycle and status commands");
|
|
@@ -1839,7 +2240,7 @@ function createInvoiceCommand(config) {
|
|
|
1839
2240
|
params: {
|
|
1840
2241
|
action: "create",
|
|
1841
2242
|
newInvoiceParams: {
|
|
1842
|
-
amount:
|
|
2243
|
+
amount: ckbToShannons3(amountCkb),
|
|
1843
2244
|
currency,
|
|
1844
2245
|
description: options.description,
|
|
1845
2246
|
expiry: toHex3(expirySeconds),
|
|
@@ -1876,7 +2277,7 @@ function createInvoiceCommand(config) {
|
|
|
1876
2277
|
}
|
|
1877
2278
|
}
|
|
1878
2279
|
const result = await rpc.newInvoice({
|
|
1879
|
-
amount:
|
|
2280
|
+
amount: ckbToShannons3(amountCkb),
|
|
1880
2281
|
currency,
|
|
1881
2282
|
description: options.description,
|
|
1882
2283
|
expiry: toHex3(expirySeconds),
|
|
@@ -1908,7 +2309,7 @@ function createInvoiceCommand(config) {
|
|
|
1908
2309
|
paymentHash,
|
|
1909
2310
|
status: result.status,
|
|
1910
2311
|
invoice: result.invoice_address,
|
|
1911
|
-
amountCkb: result.invoice.amount ?
|
|
2312
|
+
amountCkb: result.invoice.amount ? shannonsToCkb4(result.invoice.amount) : void 0,
|
|
1912
2313
|
currency: result.invoice.currency,
|
|
1913
2314
|
description: metadata.description,
|
|
1914
2315
|
createdAt: createdAtMs ? new Date(createdAtMs).toISOString() : result.invoice.data.timestamp,
|
|
@@ -2028,19 +2429,104 @@ function createInvoiceCommand(config) {
|
|
|
2028
2429
|
}
|
|
2029
2430
|
|
|
2030
2431
|
// src/commands/job.ts
|
|
2031
|
-
import { existsSync as
|
|
2432
|
+
import { existsSync as existsSync7 } from "fs";
|
|
2032
2433
|
import { Command as Command6 } from "commander";
|
|
2033
2434
|
|
|
2034
2435
|
// src/lib/log-files.ts
|
|
2035
|
-
import {
|
|
2036
|
-
|
|
2037
|
-
|
|
2436
|
+
import {
|
|
2437
|
+
appendFileSync,
|
|
2438
|
+
closeSync,
|
|
2439
|
+
createReadStream,
|
|
2440
|
+
existsSync as existsSync6,
|
|
2441
|
+
mkdirSync as mkdirSync2,
|
|
2442
|
+
openSync,
|
|
2443
|
+
readdirSync,
|
|
2444
|
+
readSync,
|
|
2445
|
+
statSync
|
|
2446
|
+
} from "fs";
|
|
2447
|
+
import { join as join5 } from "path";
|
|
2448
|
+
var DATE_DIR_PATTERN = /^\d{4}-\d{2}-\d{2}$/;
|
|
2449
|
+
function todayDateString() {
|
|
2450
|
+
const now = /* @__PURE__ */ new Date();
|
|
2451
|
+
const y = now.getUTCFullYear();
|
|
2452
|
+
const m = String(now.getUTCMonth() + 1).padStart(2, "0");
|
|
2453
|
+
const d = String(now.getUTCDate()).padStart(2, "0");
|
|
2454
|
+
return `${y}-${m}-${d}`;
|
|
2455
|
+
}
|
|
2456
|
+
function validateLogDate(date) {
|
|
2457
|
+
const value = date.trim();
|
|
2458
|
+
if (!DATE_DIR_PATTERN.test(value)) {
|
|
2459
|
+
throw new Error(`Invalid date '${value}'. Expected format YYYY-MM-DD.`);
|
|
2460
|
+
}
|
|
2461
|
+
if (value.includes("/") || value.includes("\\") || value.includes("..")) {
|
|
2462
|
+
throw new Error(`Invalid date '${value}'. Path separators or '..' are not allowed.`);
|
|
2463
|
+
}
|
|
2464
|
+
return value;
|
|
2465
|
+
}
|
|
2466
|
+
function resolveLogDirForDate(dataDir, date) {
|
|
2467
|
+
return resolveLogDirForDateWithOptions(dataDir, date, {});
|
|
2468
|
+
}
|
|
2469
|
+
function resolveLogDirForDateWithOptions(dataDir, date, options) {
|
|
2470
|
+
const dateStr = date ?? todayDateString();
|
|
2471
|
+
const logsBaseDir = options.logsBaseDir ?? join5(dataDir, "logs");
|
|
2472
|
+
if (date !== void 0) {
|
|
2473
|
+
validateLogDate(dateStr);
|
|
2474
|
+
}
|
|
2475
|
+
const dir = join5(logsBaseDir, dateStr);
|
|
2476
|
+
const ensureExists = options.ensureExists ?? true;
|
|
2477
|
+
if (ensureExists) {
|
|
2478
|
+
mkdirSync2(dir, { recursive: true });
|
|
2479
|
+
}
|
|
2480
|
+
return dir;
|
|
2481
|
+
}
|
|
2482
|
+
function resolvePersistedLogPaths(dataDir, meta, date) {
|
|
2483
|
+
const logsBaseDir = meta?.logsBaseDir ?? join5(dataDir, "logs");
|
|
2484
|
+
if (date) {
|
|
2485
|
+
const dir2 = resolveLogDirForDateWithOptions(dataDir, date, {
|
|
2486
|
+
logsBaseDir,
|
|
2487
|
+
ensureExists: false
|
|
2488
|
+
});
|
|
2489
|
+
return {
|
|
2490
|
+
runtimeAlerts: join5(dir2, "runtime.alerts.jsonl"),
|
|
2491
|
+
fnnStdout: join5(dir2, "fnn.stdout.log"),
|
|
2492
|
+
fnnStderr: join5(dir2, "fnn.stderr.log")
|
|
2493
|
+
};
|
|
2494
|
+
}
|
|
2495
|
+
if (meta?.alertLogFilePath || meta?.fnnStdoutLogPath || meta?.fnnStderrLogPath) {
|
|
2496
|
+
const defaultDir = resolveLogDirForDateWithOptions(dataDir, void 0, {
|
|
2497
|
+
logsBaseDir,
|
|
2498
|
+
ensureExists: false
|
|
2499
|
+
});
|
|
2500
|
+
return {
|
|
2501
|
+
runtimeAlerts: meta.alertLogFilePath ?? join5(defaultDir, "runtime.alerts.jsonl"),
|
|
2502
|
+
fnnStdout: meta.fnnStdoutLogPath ?? join5(defaultDir, "fnn.stdout.log"),
|
|
2503
|
+
fnnStderr: meta.fnnStderrLogPath ?? join5(defaultDir, "fnn.stderr.log")
|
|
2504
|
+
};
|
|
2505
|
+
}
|
|
2506
|
+
const dir = resolveLogDirForDateWithOptions(dataDir, void 0, {
|
|
2507
|
+
logsBaseDir,
|
|
2508
|
+
ensureExists: false
|
|
2509
|
+
});
|
|
2038
2510
|
return {
|
|
2039
|
-
runtimeAlerts:
|
|
2040
|
-
fnnStdout:
|
|
2041
|
-
fnnStderr:
|
|
2511
|
+
runtimeAlerts: join5(dir, "runtime.alerts.jsonl"),
|
|
2512
|
+
fnnStdout: join5(dir, "fnn.stdout.log"),
|
|
2513
|
+
fnnStderr: join5(dir, "fnn.stderr.log")
|
|
2042
2514
|
};
|
|
2043
2515
|
}
|
|
2516
|
+
function listLogDates(dataDir, logsBaseDir) {
|
|
2517
|
+
const logsDir = logsBaseDir ?? join5(dataDir, "logs");
|
|
2518
|
+
if (!existsSync6(logsDir)) {
|
|
2519
|
+
return [];
|
|
2520
|
+
}
|
|
2521
|
+
const entries = readdirSync(logsDir, { withFileTypes: true });
|
|
2522
|
+
const dates = entries.filter((entry) => entry.isDirectory() && DATE_DIR_PATTERN.test(entry.name)).map((entry) => entry.name);
|
|
2523
|
+
dates.sort((a, b) => a > b ? -1 : a < b ? 1 : 0);
|
|
2524
|
+
return dates;
|
|
2525
|
+
}
|
|
2526
|
+
function appendToTodayLog(dataDir, filename, text) {
|
|
2527
|
+
const dir = resolveLogDirForDate(dataDir);
|
|
2528
|
+
appendFileSync(join5(dir, filename), text, "utf-8");
|
|
2529
|
+
}
|
|
2044
2530
|
function resolvePersistedLogTargets(paths, source) {
|
|
2045
2531
|
const all = [
|
|
2046
2532
|
{
|
|
@@ -2065,7 +2551,7 @@ function resolvePersistedLogTargets(paths, source) {
|
|
|
2065
2551
|
return all.filter((target) => target.source === source);
|
|
2066
2552
|
}
|
|
2067
2553
|
function readLastLines(filePath, maxLines) {
|
|
2068
|
-
if (!
|
|
2554
|
+
if (!existsSync6(filePath)) {
|
|
2069
2555
|
return [];
|
|
2070
2556
|
}
|
|
2071
2557
|
if (!Number.isFinite(maxLines) || maxLines <= 0) {
|
|
@@ -2109,7 +2595,7 @@ function readLastLines(filePath, maxLines) {
|
|
|
2109
2595
|
}
|
|
2110
2596
|
}
|
|
2111
2597
|
async function readAppendedLines(filePath, offset, remainder = "") {
|
|
2112
|
-
if (!
|
|
2598
|
+
if (!existsSync6(filePath)) {
|
|
2113
2599
|
return { lines: [], nextOffset: 0, remainder: "" };
|
|
2114
2600
|
}
|
|
2115
2601
|
const size = statSync(filePath).size;
|
|
@@ -2209,10 +2695,11 @@ function createJobCommand(config) {
|
|
|
2209
2695
|
console.log(` ${JSON.stringify(payload.result)}`);
|
|
2210
2696
|
}
|
|
2211
2697
|
});
|
|
2212
|
-
job.command("trace").argument("<jobId>").option("--tail <n>", "Max lines to inspect per log file", "400").option("--json").action(async (jobId, options) => {
|
|
2698
|
+
job.command("trace").argument("<jobId>").option("--tail <n>", "Max lines to inspect per log file", "400").option("--date <YYYY-MM-DD>", "Date of log directory to search (default: today UTC)").option("--json").action(async (jobId, options) => {
|
|
2213
2699
|
const json = Boolean(options.json);
|
|
2214
2700
|
const tailInput = Number.parseInt(String(options.tail ?? "400"), 10);
|
|
2215
2701
|
const tail = Number.isFinite(tailInput) && tailInput > 0 ? tailInput : 400;
|
|
2702
|
+
const date = options.date ? String(options.date).trim() : void 0;
|
|
2216
2703
|
const runtimeUrl = getRuntimeUrlOrExit(config, json);
|
|
2217
2704
|
const jobResponse = await fetch(`${runtimeUrl}/jobs/${jobId}`);
|
|
2218
2705
|
if (!jobResponse.ok) {
|
|
@@ -2226,7 +2713,24 @@ function createJobCommand(config) {
|
|
|
2226
2713
|
const eventsPayload = await eventsResponse.json();
|
|
2227
2714
|
const tokens = collectTraceTokens(jobRecord, eventsPayload.events);
|
|
2228
2715
|
const meta = readRuntimeMeta(config.dataDir);
|
|
2229
|
-
|
|
2716
|
+
let logPaths;
|
|
2717
|
+
try {
|
|
2718
|
+
logPaths = resolvePersistedLogPaths(config.dataDir, meta, date);
|
|
2719
|
+
} catch (error) {
|
|
2720
|
+
const message = error instanceof Error ? error.message : "Invalid --date value.";
|
|
2721
|
+
if (json) {
|
|
2722
|
+
printJsonError({
|
|
2723
|
+
code: "JOB_TRACE_DATE_INVALID",
|
|
2724
|
+
message,
|
|
2725
|
+
recoverable: true,
|
|
2726
|
+
suggestion: "Retry with --date in YYYY-MM-DD format.",
|
|
2727
|
+
details: { date }
|
|
2728
|
+
});
|
|
2729
|
+
} else {
|
|
2730
|
+
console.error(`Error: ${message}`);
|
|
2731
|
+
}
|
|
2732
|
+
process.exit(1);
|
|
2733
|
+
}
|
|
2230
2734
|
const runtimeAlertMatches = collectRelatedLines(logPaths.runtimeAlerts, tokens, tail);
|
|
2231
2735
|
const fnnStdoutMatches = collectRelatedLines(logPaths.fnnStdout, tokens, tail);
|
|
2232
2736
|
const fnnStderrMatches = collectRelatedLines(logPaths.fnnStderr, tokens, tail);
|
|
@@ -2429,7 +2933,7 @@ function collectStructuredTokens(set, input, depth = 0) {
|
|
|
2429
2933
|
}
|
|
2430
2934
|
}
|
|
2431
2935
|
function collectRelatedLines(filePath, tokens, tail) {
|
|
2432
|
-
if (!
|
|
2936
|
+
if (!existsSync7(filePath)) {
|
|
2433
2937
|
return [];
|
|
2434
2938
|
}
|
|
2435
2939
|
const lines = readLastLines(filePath, tail);
|
|
@@ -2445,7 +2949,7 @@ function collectRelatedLines(filePath, tokens, tail) {
|
|
|
2445
2949
|
function printTraceSection(title, filePath, lines) {
|
|
2446
2950
|
console.log(`
|
|
2447
2951
|
${title}: ${filePath}`);
|
|
2448
|
-
if (!
|
|
2952
|
+
if (!existsSync7(filePath)) {
|
|
2449
2953
|
console.log(" (file not found)");
|
|
2450
2954
|
return;
|
|
2451
2955
|
}
|
|
@@ -2459,7 +2963,8 @@ ${title}: ${filePath}`);
|
|
|
2459
2963
|
}
|
|
2460
2964
|
|
|
2461
2965
|
// src/commands/logs.ts
|
|
2462
|
-
import { existsSync as
|
|
2966
|
+
import { existsSync as existsSync8, statSync as statSync2 } from "fs";
|
|
2967
|
+
import { join as join6 } from "path";
|
|
2463
2968
|
import { formatRuntimeAlert } from "@fiber-pay/runtime";
|
|
2464
2969
|
import { Command as Command7 } from "commander";
|
|
2465
2970
|
var ALLOWED_SOURCES = /* @__PURE__ */ new Set([
|
|
@@ -2468,6 +2973,7 @@ var ALLOWED_SOURCES = /* @__PURE__ */ new Set([
|
|
|
2468
2973
|
"fnn-stdout",
|
|
2469
2974
|
"fnn-stderr"
|
|
2470
2975
|
]);
|
|
2976
|
+
var DATE_DIR_PATTERN2 = /^\d{4}-\d{2}-\d{2}$/;
|
|
2471
2977
|
function parseRuntimeAlertLine(line) {
|
|
2472
2978
|
try {
|
|
2473
2979
|
const parsed = JSON.parse(line);
|
|
@@ -2493,9 +2999,45 @@ function coerceJsonLineForOutput(source, line) {
|
|
|
2493
2999
|
return parseRuntimeAlertLine(line) ?? line;
|
|
2494
3000
|
}
|
|
2495
3001
|
function createLogsCommand(config) {
|
|
2496
|
-
return new Command7("logs").alias("log").description("View persisted runtime/fnn logs").option("--source <source>", "Log source: all|runtime|fnn-stdout|fnn-stderr", "all").option("--tail <n>", "Number of recent lines per source", "80").option("--follow", "Keep streaming appended log lines (human output mode only)").option("--interval-ms <ms>", "Polling interval for --follow mode", "1000").option("--json").action(async (options) => {
|
|
3002
|
+
return new Command7("logs").alias("log").description("View persisted runtime/fnn logs").option("--source <source>", "Log source: all|runtime|fnn-stdout|fnn-stderr", "all").option("--tail <n>", "Number of recent lines per source", "80").option("--date <YYYY-MM-DD>", "Date of log directory to read (default: today UTC)").option("--list-dates", "List available log dates and exit").option("--follow", "Keep streaming appended log lines (human output mode only)").option("--interval-ms <ms>", "Polling interval for --follow mode", "1000").option("--json").action(async (options) => {
|
|
2497
3003
|
const json = Boolean(options.json);
|
|
2498
3004
|
const follow = Boolean(options.follow);
|
|
3005
|
+
const listDates = Boolean(options.listDates);
|
|
3006
|
+
const date = options.date ? String(options.date).trim() : void 0;
|
|
3007
|
+
const meta = readRuntimeMeta(config.dataDir);
|
|
3008
|
+
if (follow && date) {
|
|
3009
|
+
const message = "--follow cannot be used with --date. --follow only streams today's logs.";
|
|
3010
|
+
if (json) {
|
|
3011
|
+
printJsonError({
|
|
3012
|
+
code: "LOG_FOLLOW_DATE_UNSUPPORTED",
|
|
3013
|
+
message,
|
|
3014
|
+
recoverable: true,
|
|
3015
|
+
suggestion: "Remove --date or remove --follow and retry."
|
|
3016
|
+
});
|
|
3017
|
+
} else {
|
|
3018
|
+
console.error(`Error: ${message}`);
|
|
3019
|
+
}
|
|
3020
|
+
process.exit(1);
|
|
3021
|
+
}
|
|
3022
|
+
if (listDates) {
|
|
3023
|
+
const logsDir = meta?.logsBaseDir ?? join6(config.dataDir, "logs");
|
|
3024
|
+
const dates = listLogDates(config.dataDir, logsDir);
|
|
3025
|
+
if (json) {
|
|
3026
|
+
printJsonSuccess({ dates, logsDir });
|
|
3027
|
+
} else {
|
|
3028
|
+
if (dates.length === 0) {
|
|
3029
|
+
console.log("No log dates found.");
|
|
3030
|
+
} else {
|
|
3031
|
+
console.log(`Log dates (${dates.length}):`);
|
|
3032
|
+
for (const date2 of dates) {
|
|
3033
|
+
console.log(` ${date2}`);
|
|
3034
|
+
}
|
|
3035
|
+
console.log(`
|
|
3036
|
+
Logs directory: ${logsDir}`);
|
|
3037
|
+
}
|
|
3038
|
+
}
|
|
3039
|
+
return;
|
|
3040
|
+
}
|
|
2499
3041
|
const sourceInput = String(options.source ?? "all").trim().toLowerCase();
|
|
2500
3042
|
if (json && follow) {
|
|
2501
3043
|
const message = "--follow is not supported with --json. Use human mode for streaming logs.";
|
|
@@ -2527,18 +3069,36 @@ function createLogsCommand(config) {
|
|
|
2527
3069
|
const tail = Number.isFinite(tailInput) && tailInput > 0 ? tailInput : 80;
|
|
2528
3070
|
const intervalInput = Number.parseInt(String(options.intervalMs ?? "1000"), 10);
|
|
2529
3071
|
const intervalMs = Number.isFinite(intervalInput) && intervalInput > 0 ? intervalInput : 1e3;
|
|
2530
|
-
|
|
2531
|
-
|
|
3072
|
+
let paths;
|
|
3073
|
+
try {
|
|
3074
|
+
paths = resolvePersistedLogPaths(config.dataDir, meta, date);
|
|
3075
|
+
} catch (error) {
|
|
3076
|
+
const message = error instanceof Error ? error.message : "Invalid --date value.";
|
|
3077
|
+
if (json) {
|
|
3078
|
+
printJsonError({
|
|
3079
|
+
code: "LOG_DATE_INVALID",
|
|
3080
|
+
message,
|
|
3081
|
+
recoverable: true,
|
|
3082
|
+
suggestion: "Retry with --date in YYYY-MM-DD format.",
|
|
3083
|
+
details: { date }
|
|
3084
|
+
});
|
|
3085
|
+
} else {
|
|
3086
|
+
console.error(`Error: ${message}`);
|
|
3087
|
+
}
|
|
3088
|
+
process.exit(1);
|
|
3089
|
+
}
|
|
2532
3090
|
const targets = resolvePersistedLogTargets(paths, source);
|
|
2533
|
-
|
|
2534
|
-
|
|
3091
|
+
const displayDate = date ?? inferDateFromPaths(paths);
|
|
3092
|
+
if (source !== "all" && targets.length === 1 && !existsSync8(targets[0].path)) {
|
|
3093
|
+
const dateLabel = displayDate ? ` on ${displayDate}` : "";
|
|
3094
|
+
const message = `Log file not found for source ${source}${dateLabel}: ${targets[0].path}`;
|
|
2535
3095
|
if (json) {
|
|
2536
3096
|
printJsonError({
|
|
2537
3097
|
code: "LOG_FILE_NOT_FOUND",
|
|
2538
3098
|
message,
|
|
2539
3099
|
recoverable: true,
|
|
2540
|
-
suggestion: "Start node/runtime or generate activity, then retry logs command.",
|
|
2541
|
-
details: { source, path: targets[0].path }
|
|
3100
|
+
suggestion: "Start node/runtime or generate activity, then retry logs command. Use --list-dates to see available dates.",
|
|
3101
|
+
details: { source, date: displayDate ?? null, path: targets[0].path }
|
|
2542
3102
|
});
|
|
2543
3103
|
} else {
|
|
2544
3104
|
console.error(`Error: ${message}`);
|
|
@@ -2547,7 +3107,7 @@ function createLogsCommand(config) {
|
|
|
2547
3107
|
}
|
|
2548
3108
|
const entries = [];
|
|
2549
3109
|
for (const target of targets) {
|
|
2550
|
-
const exists =
|
|
3110
|
+
const exists = existsSync8(target.path);
|
|
2551
3111
|
let lines = [];
|
|
2552
3112
|
if (exists) {
|
|
2553
3113
|
try {
|
|
@@ -2582,6 +3142,7 @@ function createLogsCommand(config) {
|
|
|
2582
3142
|
printJsonSuccess({
|
|
2583
3143
|
source,
|
|
2584
3144
|
tail,
|
|
3145
|
+
date: displayDate ?? null,
|
|
2585
3146
|
entries: entries.map((entry) => ({
|
|
2586
3147
|
source: entry.source,
|
|
2587
3148
|
title: entry.title,
|
|
@@ -2593,7 +3154,8 @@ function createLogsCommand(config) {
|
|
|
2593
3154
|
});
|
|
2594
3155
|
return;
|
|
2595
3156
|
}
|
|
2596
|
-
|
|
3157
|
+
const headerDate = displayDate ? `, date: ${displayDate}` : "";
|
|
3158
|
+
console.log(`Logs (source: ${source}${headerDate}, tail: ${tail})`);
|
|
2597
3159
|
for (const entry of entries) {
|
|
2598
3160
|
console.log(`
|
|
2599
3161
|
${entry.title}: ${entry.path}`);
|
|
@@ -2647,7 +3209,7 @@ Following logs (interval: ${intervalMs}ms). Press Ctrl+C to stop.`);
|
|
|
2647
3209
|
for (const target of targets) {
|
|
2648
3210
|
const state = states.get(target.source);
|
|
2649
3211
|
if (!state) continue;
|
|
2650
|
-
if (!
|
|
3212
|
+
if (!existsSync8(state.path)) {
|
|
2651
3213
|
state.offset = 0;
|
|
2652
3214
|
state.remainder = "";
|
|
2653
3215
|
continue;
|
|
@@ -2678,28 +3240,38 @@ Following logs (interval: ${intervalMs}ms). Press Ctrl+C to stop.`);
|
|
|
2678
3240
|
});
|
|
2679
3241
|
});
|
|
2680
3242
|
}
|
|
3243
|
+
function inferDateFromPaths(paths) {
|
|
3244
|
+
const candidate = paths.runtimeAlerts.split("/").at(-2);
|
|
3245
|
+
if (!candidate || !DATE_DIR_PATTERN2.test(candidate)) {
|
|
3246
|
+
return void 0;
|
|
3247
|
+
}
|
|
3248
|
+
const stdoutDate = paths.fnnStdout.split("/").at(-2);
|
|
3249
|
+
const stderrDate = paths.fnnStderr.split("/").at(-2);
|
|
3250
|
+
if (stdoutDate !== candidate || stderrDate !== candidate) {
|
|
3251
|
+
return void 0;
|
|
3252
|
+
}
|
|
3253
|
+
return candidate;
|
|
3254
|
+
}
|
|
2681
3255
|
|
|
2682
3256
|
// src/commands/node.ts
|
|
2683
|
-
import { nodeIdToPeerId as nodeIdToPeerId2, scriptToAddress as scriptToAddress2 } from "@fiber-pay/sdk";
|
|
2684
3257
|
import { Command as Command8 } from "commander";
|
|
2685
3258
|
|
|
2686
3259
|
// src/lib/node-start.ts
|
|
2687
3260
|
import { spawn } from "child_process";
|
|
2688
|
-
import {
|
|
2689
|
-
import { join as
|
|
3261
|
+
import { mkdirSync as mkdirSync3 } from "fs";
|
|
3262
|
+
import { join as join7 } from "path";
|
|
2690
3263
|
import {
|
|
2691
3264
|
createKeyManager,
|
|
2692
3265
|
ensureFiberBinary,
|
|
2693
|
-
getDefaultBinaryPath,
|
|
2694
3266
|
ProcessManager
|
|
2695
3267
|
} from "@fiber-pay/node";
|
|
2696
3268
|
import { startRuntimeService } from "@fiber-pay/runtime";
|
|
2697
3269
|
|
|
2698
3270
|
// src/lib/bootnode.ts
|
|
2699
|
-
import { existsSync as
|
|
3271
|
+
import { existsSync as existsSync9, readFileSync as readFileSync5 } from "fs";
|
|
2700
3272
|
import { parse as parseYaml } from "yaml";
|
|
2701
3273
|
function extractBootnodeAddrs(configFilePath) {
|
|
2702
|
-
if (!
|
|
3274
|
+
if (!existsSync9(configFilePath)) return [];
|
|
2703
3275
|
try {
|
|
2704
3276
|
const content = readFileSync5(configFilePath, "utf-8");
|
|
2705
3277
|
const doc = parseYaml(content);
|
|
@@ -2730,8 +3302,8 @@ async function autoConnectBootnodes(rpc, bootnodes) {
|
|
|
2730
3302
|
}
|
|
2731
3303
|
|
|
2732
3304
|
// src/lib/node-migration.ts
|
|
2733
|
-
import { dirname } from "path";
|
|
2734
|
-
import { BinaryManager, MigrationManager } from "@fiber-pay/node";
|
|
3305
|
+
import { dirname as dirname2 } from "path";
|
|
3306
|
+
import { BinaryManager as BinaryManager2, MigrationManager } from "@fiber-pay/node";
|
|
2735
3307
|
|
|
2736
3308
|
// src/lib/migration-utils.ts
|
|
2737
3309
|
function replaceRawMigrateHint(message) {
|
|
@@ -2754,8 +3326,8 @@ async function runMigrationGuard(opts) {
|
|
|
2754
3326
|
return { checked: false, skippedReason: "store does not exist" };
|
|
2755
3327
|
}
|
|
2756
3328
|
const storePath = MigrationManager.resolveStorePath(dataDir);
|
|
2757
|
-
const binaryDir =
|
|
2758
|
-
const bm = new
|
|
3329
|
+
const binaryDir = dirname2(binaryPath);
|
|
3330
|
+
const bm = new BinaryManager2(binaryDir);
|
|
2759
3331
|
const migrateBinPath = bm.getMigrateBinaryPath();
|
|
2760
3332
|
let migrationCheck;
|
|
2761
3333
|
try {
|
|
@@ -2794,98 +3366,115 @@ async function runMigrationGuard(opts) {
|
|
|
2794
3366
|
return { checked: true, migrationCheck };
|
|
2795
3367
|
}
|
|
2796
3368
|
|
|
2797
|
-
// src/lib/
|
|
2798
|
-
import { spawnSync } from "child_process";
|
|
2799
|
-
|
|
2800
|
-
|
|
2801
|
-
|
|
2802
|
-
|
|
2803
|
-
return { path: binaryPath, ready: false, version: "unknown" };
|
|
3369
|
+
// src/lib/runtime-port.ts
|
|
3370
|
+
import { spawnSync as spawnSync2 } from "child_process";
|
|
3371
|
+
function parsePortFromListen(listen) {
|
|
3372
|
+
const value = listen.trim();
|
|
3373
|
+
if (!value) {
|
|
3374
|
+
return void 0;
|
|
2804
3375
|
}
|
|
2805
|
-
|
|
2806
|
-
|
|
2807
|
-
|
|
2808
|
-
|
|
2809
|
-
|
|
2810
|
-
|
|
2811
|
-
|
|
2812
|
-
return { path: binaryPath, ready: true, version: firstLine.trim() };
|
|
2813
|
-
} catch {
|
|
2814
|
-
return { path: binaryPath, ready: false, version: "unknown" };
|
|
3376
|
+
const lastColon = value.lastIndexOf(":");
|
|
3377
|
+
if (lastColon < 0 || lastColon === value.length - 1) {
|
|
3378
|
+
return void 0;
|
|
3379
|
+
}
|
|
3380
|
+
const port = Number(value.slice(lastColon + 1));
|
|
3381
|
+
if (!Number.isInteger(port) || port < 1 || port > 65535) {
|
|
3382
|
+
return void 0;
|
|
2815
3383
|
}
|
|
3384
|
+
return port;
|
|
2816
3385
|
}
|
|
2817
|
-
function
|
|
2818
|
-
|
|
2819
|
-
const
|
|
2820
|
-
if (
|
|
2821
|
-
|
|
3386
|
+
function extractFirstPidFromLsofOutput(output) {
|
|
3387
|
+
for (const line of output.split("\n")) {
|
|
3388
|
+
const trimmed = line.trim();
|
|
3389
|
+
if (!trimmed.startsWith("p") || trimmed.length < 2) {
|
|
3390
|
+
continue;
|
|
2822
3391
|
}
|
|
2823
|
-
const
|
|
2824
|
-
if (
|
|
2825
|
-
return
|
|
3392
|
+
const pid = Number(trimmed.slice(1));
|
|
3393
|
+
if (Number.isInteger(pid) && pid > 0) {
|
|
3394
|
+
return pid;
|
|
2826
3395
|
}
|
|
2827
|
-
const firstLine = output.split("\n").find((line) => line.trim().length > 0);
|
|
2828
|
-
return firstLine?.trim() ?? "unknown";
|
|
2829
|
-
} catch {
|
|
2830
|
-
return "unknown";
|
|
2831
3396
|
}
|
|
3397
|
+
return void 0;
|
|
2832
3398
|
}
|
|
2833
|
-
function
|
|
2834
|
-
const
|
|
2835
|
-
|
|
2836
|
-
|
|
3399
|
+
function readProcessCommand(pid) {
|
|
3400
|
+
const result = spawnSync2("ps", ["-p", String(pid), "-o", "command="], {
|
|
3401
|
+
encoding: "utf-8"
|
|
3402
|
+
});
|
|
3403
|
+
if (result.error || result.status !== 0) {
|
|
3404
|
+
return void 0;
|
|
2837
3405
|
}
|
|
2838
|
-
|
|
3406
|
+
const command = (result.stdout ?? "").trim();
|
|
3407
|
+
return command.length > 0 ? command : void 0;
|
|
2839
3408
|
}
|
|
2840
|
-
function
|
|
2841
|
-
const
|
|
2842
|
-
|
|
2843
|
-
|
|
2844
|
-
[
|
|
2845
|
-
cliEntrypoint,
|
|
2846
|
-
"--data-dir",
|
|
2847
|
-
params.dataDir,
|
|
2848
|
-
"--rpc-url",
|
|
2849
|
-
params.rpcUrl,
|
|
2850
|
-
"runtime",
|
|
2851
|
-
"start",
|
|
2852
|
-
"--daemon",
|
|
2853
|
-
"--fiber-rpc-url",
|
|
2854
|
-
params.rpcUrl,
|
|
2855
|
-
"--proxy-listen",
|
|
2856
|
-
params.proxyListen,
|
|
2857
|
-
"--state-file",
|
|
2858
|
-
params.stateFilePath,
|
|
2859
|
-
"--alert-log-file",
|
|
2860
|
-
params.alertLogFile,
|
|
2861
|
-
"--json"
|
|
2862
|
-
],
|
|
2863
|
-
{ encoding: "utf-8" }
|
|
2864
|
-
);
|
|
2865
|
-
if (result.status === 0) {
|
|
2866
|
-
return { ok: true };
|
|
3409
|
+
function findListeningProcessByPort(listen) {
|
|
3410
|
+
const port = parsePortFromListen(listen);
|
|
3411
|
+
if (!port) {
|
|
3412
|
+
return void 0;
|
|
2867
3413
|
}
|
|
2868
|
-
const
|
|
2869
|
-
|
|
2870
|
-
|
|
2871
|
-
|
|
3414
|
+
const result = spawnSync2("lsof", ["-nP", `-iTCP:${port}`, "-sTCP:LISTEN", "-Fp"], {
|
|
3415
|
+
encoding: "utf-8"
|
|
3416
|
+
});
|
|
3417
|
+
if (result.error || result.status !== 0) {
|
|
3418
|
+
return void 0;
|
|
3419
|
+
}
|
|
3420
|
+
const pid = extractFirstPidFromLsofOutput(result.stdout ?? "");
|
|
3421
|
+
if (!pid) {
|
|
3422
|
+
return void 0;
|
|
3423
|
+
}
|
|
3424
|
+
return {
|
|
3425
|
+
pid,
|
|
3426
|
+
command: readProcessCommand(pid)
|
|
3427
|
+
};
|
|
2872
3428
|
}
|
|
2873
|
-
function
|
|
2874
|
-
|
|
2875
|
-
|
|
2876
|
-
|
|
2877
|
-
|
|
2878
|
-
|
|
2879
|
-
|
|
2880
|
-
|
|
2881
|
-
|
|
2882
|
-
|
|
2883
|
-
|
|
2884
|
-
|
|
2885
|
-
|
|
2886
|
-
|
|
2887
|
-
|
|
2888
|
-
|
|
3429
|
+
function isFiberRuntimeCommand(command) {
|
|
3430
|
+
if (!command) {
|
|
3431
|
+
return false;
|
|
3432
|
+
}
|
|
3433
|
+
const normalized = command.toLowerCase();
|
|
3434
|
+
const hasFiberIdentifier = normalized.includes("fiber-pay") || normalized.includes("@fiber-pay/cli") || normalized.includes("/packages/cli/dist/cli.js") || normalized.includes("\\packages\\cli\\dist\\cli.js") || normalized.includes("/dist/cli.js") || normalized.includes("\\dist\\cli.js");
|
|
3435
|
+
if (!hasFiberIdentifier) {
|
|
3436
|
+
return false;
|
|
3437
|
+
}
|
|
3438
|
+
return normalized.includes("runtime") && normalized.includes("start");
|
|
3439
|
+
}
|
|
3440
|
+
async function terminateProcess(pid, timeoutMs = 5e3) {
|
|
3441
|
+
if (!isProcessRunning(pid)) {
|
|
3442
|
+
return true;
|
|
3443
|
+
}
|
|
3444
|
+
try {
|
|
3445
|
+
process.kill(pid, "SIGTERM");
|
|
3446
|
+
} catch (error) {
|
|
3447
|
+
if (error.code === "ESRCH") {
|
|
3448
|
+
return true;
|
|
3449
|
+
}
|
|
3450
|
+
return false;
|
|
3451
|
+
}
|
|
3452
|
+
const deadline = Date.now() + timeoutMs;
|
|
3453
|
+
while (Date.now() < deadline) {
|
|
3454
|
+
if (!isProcessRunning(pid)) {
|
|
3455
|
+
return true;
|
|
3456
|
+
}
|
|
3457
|
+
await new Promise((resolve2) => setTimeout(resolve2, 100));
|
|
3458
|
+
}
|
|
3459
|
+
if (!isProcessRunning(pid)) {
|
|
3460
|
+
return true;
|
|
3461
|
+
}
|
|
3462
|
+
try {
|
|
3463
|
+
process.kill(pid, "SIGKILL");
|
|
3464
|
+
} catch (error) {
|
|
3465
|
+
if (error.code === "ESRCH") {
|
|
3466
|
+
return true;
|
|
3467
|
+
}
|
|
3468
|
+
return false;
|
|
3469
|
+
}
|
|
3470
|
+
const killDeadline = Date.now() + 1e3;
|
|
3471
|
+
while (Date.now() < killDeadline) {
|
|
3472
|
+
if (!isProcessRunning(pid)) {
|
|
3473
|
+
return true;
|
|
3474
|
+
}
|
|
3475
|
+
await new Promise((resolve2) => setTimeout(resolve2, 50));
|
|
3476
|
+
}
|
|
3477
|
+
return !isProcessRunning(pid);
|
|
2889
3478
|
}
|
|
2890
3479
|
|
|
2891
3480
|
// src/lib/node-start.ts
|
|
@@ -2997,19 +3586,22 @@ async function runNodeStartCommand(config, options) {
|
|
|
2997
3586
|
options.runtimeProxyListen ?? config.runtimeProxyListen ?? "127.0.0.1:8229"
|
|
2998
3587
|
);
|
|
2999
3588
|
const proxyListenSource = options.runtimeProxyListen ? "cli" : config.runtimeProxyListen ? "profile" : "default";
|
|
3000
|
-
const runtimeStateFilePath =
|
|
3001
|
-
const
|
|
3002
|
-
|
|
3003
|
-
|
|
3004
|
-
const
|
|
3005
|
-
|
|
3006
|
-
|
|
3007
|
-
|
|
3589
|
+
const runtimeStateFilePath = join7(config.dataDir, "runtime-state.json");
|
|
3590
|
+
const logsBaseDir = join7(config.dataDir, "logs");
|
|
3591
|
+
mkdirSync3(logsBaseDir, { recursive: true });
|
|
3592
|
+
resolveLogDirForDate(config.dataDir);
|
|
3593
|
+
const resolvedBinary = resolveBinaryPath(config);
|
|
3594
|
+
const binaryPath = resolvedBinary.binaryPath;
|
|
3595
|
+
if (resolvedBinary.source === "profile-managed") {
|
|
3596
|
+
const installDir = getBinaryManagerInstallDirOrThrow(resolvedBinary);
|
|
3597
|
+
await ensureFiberBinary({ installDir });
|
|
3598
|
+
}
|
|
3008
3599
|
const binaryVersion = getBinaryVersion(binaryPath);
|
|
3009
3600
|
const configFilePath = ensureNodeConfigFile(config.dataDir, config.network);
|
|
3010
3601
|
emitStage("binary_resolved", "ok", {
|
|
3011
3602
|
binaryPath,
|
|
3012
3603
|
binaryVersion,
|
|
3604
|
+
binarySource: resolvedBinary.source,
|
|
3013
3605
|
configFilePath
|
|
3014
3606
|
});
|
|
3015
3607
|
if (!json) {
|
|
@@ -3070,11 +3662,11 @@ async function runNodeStartCommand(config, options) {
|
|
|
3070
3662
|
removePidFile(config.dataDir);
|
|
3071
3663
|
});
|
|
3072
3664
|
processManager.on("stdout", (text) => {
|
|
3073
|
-
|
|
3665
|
+
appendToTodayLog(config.dataDir, "fnn.stdout.log", text);
|
|
3074
3666
|
emitFnnLog("stdout", text);
|
|
3075
3667
|
});
|
|
3076
3668
|
processManager.on("stderr", (text) => {
|
|
3077
|
-
|
|
3669
|
+
appendToTodayLog(config.dataDir, "fnn.stderr.log", text);
|
|
3078
3670
|
emitFnnLog("stderr", text);
|
|
3079
3671
|
});
|
|
3080
3672
|
await processManager.start();
|
|
@@ -3119,13 +3711,38 @@ async function runNodeStartCommand(config, options) {
|
|
|
3119
3711
|
process.exit(1);
|
|
3120
3712
|
}
|
|
3121
3713
|
try {
|
|
3714
|
+
const runtimePortProcess = findListeningProcessByPort(runtimeProxyListen);
|
|
3715
|
+
if (runtimePortProcess) {
|
|
3716
|
+
if (isFiberRuntimeCommand(runtimePortProcess.command)) {
|
|
3717
|
+
const terminated = await terminateProcess(runtimePortProcess.pid);
|
|
3718
|
+
if (!terminated) {
|
|
3719
|
+
throw new Error(
|
|
3720
|
+
`Runtime proxy ${runtimeProxyListen} is occupied by stale fiber-pay runtime PID ${runtimePortProcess.pid}, but termination failed`
|
|
3721
|
+
);
|
|
3722
|
+
}
|
|
3723
|
+
removeRuntimeFiles(config.dataDir);
|
|
3724
|
+
emitStage("runtime_preflight", "ok", {
|
|
3725
|
+
proxyListen: runtimeProxyListen,
|
|
3726
|
+
cleanedStaleProcessPid: runtimePortProcess.pid
|
|
3727
|
+
});
|
|
3728
|
+
} else if (runtimePortProcess.command) {
|
|
3729
|
+
const details = runtimePortProcess.command ? `PID ${runtimePortProcess.pid} (${runtimePortProcess.command})` : `PID ${runtimePortProcess.pid}`;
|
|
3730
|
+
throw new Error(
|
|
3731
|
+
`Runtime proxy ${runtimeProxyListen} is already in use by non-fiber-pay process: ${details}`
|
|
3732
|
+
);
|
|
3733
|
+
} else {
|
|
3734
|
+
throw new Error(
|
|
3735
|
+
`Runtime proxy ${runtimeProxyListen} is already in use by process PID ${runtimePortProcess.pid}. Unable to determine command owner; inspect this PID manually before retrying.`
|
|
3736
|
+
);
|
|
3737
|
+
}
|
|
3738
|
+
}
|
|
3122
3739
|
if (runtimeDaemon) {
|
|
3123
3740
|
const daemonStart = startRuntimeDaemonFromNode({
|
|
3124
3741
|
dataDir: config.dataDir,
|
|
3125
3742
|
rpcUrl: config.rpcUrl,
|
|
3126
3743
|
proxyListen: runtimeProxyListen,
|
|
3127
3744
|
stateFilePath: runtimeStateFilePath,
|
|
3128
|
-
|
|
3745
|
+
alertLogsBaseDir: logsBaseDir
|
|
3129
3746
|
});
|
|
3130
3747
|
if (!daemonStart.ok) {
|
|
3131
3748
|
throw new Error(daemonStart.message);
|
|
@@ -3140,23 +3757,25 @@ async function runNodeStartCommand(config, options) {
|
|
|
3140
3757
|
storage: {
|
|
3141
3758
|
stateFilePath: runtimeStateFilePath
|
|
3142
3759
|
},
|
|
3143
|
-
alerts: [{ type: "stdout" }, { type: "file",
|
|
3760
|
+
alerts: [{ type: "stdout" }, { type: "daily-file", baseLogsDir: logsBaseDir }],
|
|
3144
3761
|
jobs: {
|
|
3145
3762
|
enabled: true,
|
|
3146
|
-
dbPath:
|
|
3763
|
+
dbPath: join7(config.dataDir, "runtime-jobs.db")
|
|
3147
3764
|
}
|
|
3148
3765
|
});
|
|
3149
3766
|
const runtimeStatus = runtime.service.getStatus();
|
|
3150
3767
|
writeRuntimePid(config.dataDir, process.pid);
|
|
3768
|
+
const todayLogDir = resolveLogDirForDate(config.dataDir);
|
|
3151
3769
|
writeRuntimeMeta(config.dataDir, {
|
|
3152
3770
|
pid: process.pid,
|
|
3153
3771
|
startedAt: runtimeStatus.startedAt,
|
|
3154
3772
|
fiberRpcUrl: runtimeStatus.targetUrl,
|
|
3155
3773
|
proxyListen: runtimeStatus.proxyListen,
|
|
3156
3774
|
stateFilePath: runtimeStateFilePath,
|
|
3157
|
-
alertLogFilePath:
|
|
3158
|
-
fnnStdoutLogPath,
|
|
3159
|
-
fnnStderrLogPath,
|
|
3775
|
+
alertLogFilePath: join7(todayLogDir, "runtime.alerts.jsonl"),
|
|
3776
|
+
fnnStdoutLogPath: join7(todayLogDir, "fnn.stdout.log"),
|
|
3777
|
+
fnnStderrLogPath: join7(todayLogDir, "fnn.stderr.log"),
|
|
3778
|
+
logsBaseDir,
|
|
3160
3779
|
daemon: false
|
|
3161
3780
|
});
|
|
3162
3781
|
}
|
|
@@ -3226,7 +3845,7 @@ async function runNodeStartCommand(config, options) {
|
|
|
3226
3845
|
process.exit(1);
|
|
3227
3846
|
}
|
|
3228
3847
|
emitStage("rpc_ready", "ok", { rpcUrl: config.rpcUrl });
|
|
3229
|
-
const bootnodes = nodeConfig.configFilePath ? extractBootnodeAddrs(nodeConfig.configFilePath) : extractBootnodeAddrs(
|
|
3848
|
+
const bootnodes = nodeConfig.configFilePath ? extractBootnodeAddrs(nodeConfig.configFilePath) : extractBootnodeAddrs(join7(config.dataDir, "config.yml"));
|
|
3230
3849
|
if (bootnodes.length > 0) {
|
|
3231
3850
|
await autoConnectBootnodes(rpc, bootnodes);
|
|
3232
3851
|
}
|
|
@@ -3267,9 +3886,8 @@ async function runNodeStartCommand(config, options) {
|
|
|
3267
3886
|
proxyUrl: `http://${runtimeProxyListen}`,
|
|
3268
3887
|
proxyListenSource,
|
|
3269
3888
|
logs: {
|
|
3270
|
-
|
|
3271
|
-
|
|
3272
|
-
runtimeAlerts: runtimeAlertLogPath
|
|
3889
|
+
baseDir: logsBaseDir,
|
|
3890
|
+
todayDir: resolveLogDirForDate(config.dataDir)
|
|
3273
3891
|
}
|
|
3274
3892
|
});
|
|
3275
3893
|
} else {
|
|
@@ -3279,7 +3897,7 @@ async function runNodeStartCommand(config, options) {
|
|
|
3279
3897
|
` Runtime proxy: http://${runtimeProxyListen} (browser-safe endpoint + monitoring)`
|
|
3280
3898
|
);
|
|
3281
3899
|
console.log(` Runtime mode: ${runtimeDaemon ? "daemon" : "embedded"}`);
|
|
3282
|
-
console.log(` Log files: ${
|
|
3900
|
+
console.log(` Log files: ${logsBaseDir}`);
|
|
3283
3901
|
console.log(" Press Ctrl+C to stop.");
|
|
3284
3902
|
}
|
|
3285
3903
|
let shutdownRequested = false;
|
|
@@ -3347,8 +3965,6 @@ async function runNodeStartCommand(config, options) {
|
|
|
3347
3965
|
|
|
3348
3966
|
// src/lib/node-status.ts
|
|
3349
3967
|
import { existsSync as existsSync10 } from "fs";
|
|
3350
|
-
import { join as join6 } from "path";
|
|
3351
|
-
import { getFiberBinaryInfo as getFiberBinaryInfo2 } from "@fiber-pay/node";
|
|
3352
3968
|
import {
|
|
3353
3969
|
buildMultiaddrFromNodeId,
|
|
3354
3970
|
buildMultiaddrFromRpcUrl,
|
|
@@ -3515,24 +4131,29 @@ async function runNodeStatusCommand(config, options) {
|
|
|
3515
4131
|
const json = Boolean(options.json);
|
|
3516
4132
|
const pid = readPidFile(config.dataDir);
|
|
3517
4133
|
const resolvedRpc = resolveRpcEndpoint(config);
|
|
3518
|
-
const
|
|
3519
|
-
const binaryInfo = config.binaryPath ? getCustomBinaryState(config.binaryPath) : await getFiberBinaryInfo2(join6(config.dataDir, "bin"));
|
|
4134
|
+
const { resolvedBinary, info: binaryInfo } = await getBinaryDetails(config);
|
|
3520
4135
|
const configExists = existsSync10(config.configPath);
|
|
3521
4136
|
const nodeRunning = Boolean(pid && isProcessRunning(pid));
|
|
3522
4137
|
let rpcResponsive = false;
|
|
3523
4138
|
let nodeId = null;
|
|
4139
|
+
let addresses = [];
|
|
4140
|
+
let chainHash = null;
|
|
4141
|
+
let version = null;
|
|
3524
4142
|
let peerId = null;
|
|
4143
|
+
let peersCount = 0;
|
|
3525
4144
|
let peerIdError = null;
|
|
3526
4145
|
let multiaddr = null;
|
|
3527
4146
|
let multiaddrError = null;
|
|
3528
4147
|
let multiaddrInferred = false;
|
|
3529
4148
|
let channelsTotal = 0;
|
|
3530
4149
|
let channelsReady = 0;
|
|
4150
|
+
let pendingChannelCount = 0;
|
|
3531
4151
|
let canSend = false;
|
|
3532
4152
|
let canReceive = false;
|
|
3533
4153
|
let localCkb = 0;
|
|
3534
4154
|
let remoteCkb = 0;
|
|
3535
4155
|
let fundingAddress = null;
|
|
4156
|
+
let fundingLockScript = null;
|
|
3536
4157
|
let fundingCkb = 0;
|
|
3537
4158
|
let fundingBalanceError = null;
|
|
3538
4159
|
if (nodeRunning) {
|
|
@@ -3542,6 +4163,10 @@ async function runNodeStatusCommand(config, options) {
|
|
|
3542
4163
|
const channels = await rpc.listChannels({ include_closed: false });
|
|
3543
4164
|
rpcResponsive = true;
|
|
3544
4165
|
nodeId = nodeInfo.node_id;
|
|
4166
|
+
addresses = nodeInfo.addresses;
|
|
4167
|
+
chainHash = nodeInfo.chain_hash;
|
|
4168
|
+
version = nodeInfo.version;
|
|
4169
|
+
peersCount = parseInt(nodeInfo.peers_count, 16);
|
|
3545
4170
|
try {
|
|
3546
4171
|
peerId = await nodeIdToPeerId(nodeInfo.node_id);
|
|
3547
4172
|
} catch (error) {
|
|
@@ -3568,18 +4193,17 @@ async function runNodeStatusCommand(config, options) {
|
|
|
3568
4193
|
(channel) => channel.state?.state_name === ChannelState2.ChannelReady
|
|
3569
4194
|
);
|
|
3570
4195
|
channelsReady = readyChannels.length;
|
|
4196
|
+
pendingChannelCount = Math.max(channelsTotal - channelsReady, 0);
|
|
3571
4197
|
const liquidity = summarizeChannelLiquidity(readyChannels);
|
|
3572
4198
|
canSend = liquidity.canSend;
|
|
3573
4199
|
canReceive = liquidity.canReceive;
|
|
3574
4200
|
localCkb = liquidity.localCkb;
|
|
3575
4201
|
remoteCkb = liquidity.remoteCkb;
|
|
3576
4202
|
fundingAddress = scriptToAddress(nodeInfo.default_funding_lock_script, config.network);
|
|
4203
|
+
fundingLockScript = nodeInfo.default_funding_lock_script;
|
|
3577
4204
|
if (config.ckbRpcUrl) {
|
|
3578
4205
|
try {
|
|
3579
|
-
const fundingBalance = await getLockBalanceShannons(
|
|
3580
|
-
config.ckbRpcUrl,
|
|
3581
|
-
nodeInfo.default_funding_lock_script
|
|
3582
|
-
);
|
|
4206
|
+
const fundingBalance = await getLockBalanceShannons(config.ckbRpcUrl, fundingLockScript);
|
|
3583
4207
|
fundingCkb = Number(fundingBalance) / 1e8;
|
|
3584
4208
|
} catch (error) {
|
|
3585
4209
|
fundingBalanceError = error instanceof Error ? error.message : "Failed to query CKB balance for funding address";
|
|
@@ -3610,18 +4234,24 @@ async function runNodeStatusCommand(config, options) {
|
|
|
3610
4234
|
rpcTarget: resolvedRpc.target,
|
|
3611
4235
|
resolvedRpcUrl: resolvedRpc.url,
|
|
3612
4236
|
nodeId,
|
|
4237
|
+
addresses,
|
|
4238
|
+
chainHash,
|
|
4239
|
+
version,
|
|
3613
4240
|
peerId,
|
|
4241
|
+
peersCount,
|
|
3614
4242
|
peerIdError,
|
|
3615
4243
|
multiaddr,
|
|
3616
4244
|
multiaddrError,
|
|
3617
4245
|
multiaddrInferred,
|
|
4246
|
+
fundingLockScript,
|
|
3618
4247
|
checks: {
|
|
3619
4248
|
binary: {
|
|
3620
4249
|
path: binaryInfo.path,
|
|
3621
4250
|
ready: binaryInfo.ready,
|
|
3622
4251
|
version: binaryInfo.version,
|
|
3623
|
-
source:
|
|
3624
|
-
managedPath:
|
|
4252
|
+
source: resolvedBinary.source,
|
|
4253
|
+
managedPath: resolvedBinary.managedPath,
|
|
4254
|
+
resolvedPath: resolvedBinary.binaryPath
|
|
3625
4255
|
},
|
|
3626
4256
|
config: {
|
|
3627
4257
|
path: config.configPath,
|
|
@@ -3639,6 +4269,7 @@ async function runNodeStatusCommand(config, options) {
|
|
|
3639
4269
|
channels: {
|
|
3640
4270
|
total: channelsTotal,
|
|
3641
4271
|
ready: channelsReady,
|
|
4272
|
+
pending: pendingChannelCount,
|
|
3642
4273
|
canSend,
|
|
3643
4274
|
canReceive
|
|
3644
4275
|
}
|
|
@@ -3665,6 +4296,12 @@ async function runNodeStatusCommand(config, options) {
|
|
|
3665
4296
|
console.log(`\u2705 Node is running (PID: ${output.pid})`);
|
|
3666
4297
|
if (output.rpcResponsive) {
|
|
3667
4298
|
console.log(` Node ID: ${String(output.nodeId)}`);
|
|
4299
|
+
if (output.version) {
|
|
4300
|
+
console.log(` Version: ${String(output.version)}`);
|
|
4301
|
+
}
|
|
4302
|
+
if (output.chainHash) {
|
|
4303
|
+
console.log(` Chain Hash: ${String(output.chainHash)}`);
|
|
4304
|
+
}
|
|
3668
4305
|
if (output.peerId) {
|
|
3669
4306
|
console.log(` Peer ID: ${String(output.peerId)}`);
|
|
3670
4307
|
} else if (output.peerIdError) {
|
|
@@ -3680,6 +4317,12 @@ async function runNodeStatusCommand(config, options) {
|
|
|
3680
4317
|
} else {
|
|
3681
4318
|
console.log(" Multiaddr: unavailable");
|
|
3682
4319
|
}
|
|
4320
|
+
if (output.addresses.length > 0) {
|
|
4321
|
+
console.log(" Addresses:");
|
|
4322
|
+
for (const address of output.addresses) {
|
|
4323
|
+
console.log(` - ${address}`);
|
|
4324
|
+
}
|
|
4325
|
+
}
|
|
3683
4326
|
} else {
|
|
3684
4327
|
console.log(" \u26A0\uFE0F RPC not responding");
|
|
3685
4328
|
}
|
|
@@ -3691,11 +4334,17 @@ async function runNodeStatusCommand(config, options) {
|
|
|
3691
4334
|
console.log("");
|
|
3692
4335
|
console.log("Diagnostics");
|
|
3693
4336
|
console.log(` Binary: ${output.checks.binary.ready ? "ready" : "missing"}`);
|
|
4337
|
+
console.log(` Binary Path: ${output.checks.binary.resolvedPath}`);
|
|
3694
4338
|
console.log(` Config: ${output.checks.config.exists ? "present" : "missing"}`);
|
|
3695
4339
|
console.log(` RPC: ${output.checks.node.rpcReachable ? "reachable" : "unreachable"}`);
|
|
3696
4340
|
console.log(
|
|
3697
|
-
` Channels: ${output.checks.channels.ready}/${output.checks.channels.total} ready/total`
|
|
4341
|
+
` Channels: ${output.checks.channels.ready}/${output.checks.channels.pending}/${output.checks.channels.total} ready/pending/total`
|
|
3698
4342
|
);
|
|
4343
|
+
if (output.rpcResponsive) {
|
|
4344
|
+
console.log(` Peers: ${output.peersCount}`);
|
|
4345
|
+
} else {
|
|
4346
|
+
console.log(" Peers: unavailable");
|
|
4347
|
+
}
|
|
3699
4348
|
console.log(` Can Send: ${output.checks.channels.canSend ? "yes" : "no"}`);
|
|
3700
4349
|
console.log(` Can Receive: ${output.checks.channels.canReceive ? "yes" : "no"}`);
|
|
3701
4350
|
console.log(` Recommendation:${output.recommendation}`);
|
|
@@ -3858,11 +4507,28 @@ async function runNodeStopCommand(config, options) {
|
|
|
3858
4507
|
}
|
|
3859
4508
|
|
|
3860
4509
|
// src/lib/node-upgrade.ts
|
|
3861
|
-
import { BinaryManager as
|
|
4510
|
+
import { BinaryManager as BinaryManager3, MigrationManager as MigrationManager2 } from "@fiber-pay/node";
|
|
3862
4511
|
async function runNodeUpgradeCommand(config, options) {
|
|
3863
4512
|
const json = Boolean(options.json);
|
|
3864
|
-
const
|
|
3865
|
-
|
|
4513
|
+
const resolvedBinary = resolveBinaryPath(config);
|
|
4514
|
+
let installDir;
|
|
4515
|
+
try {
|
|
4516
|
+
installDir = getBinaryManagerInstallDirOrThrow(resolvedBinary);
|
|
4517
|
+
} catch (error) {
|
|
4518
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
4519
|
+
if (json) {
|
|
4520
|
+
printJsonError({
|
|
4521
|
+
code: "BINARY_PATH_INCOMPATIBLE",
|
|
4522
|
+
message,
|
|
4523
|
+
recoverable: true,
|
|
4524
|
+
suggestion: "Use `fiber-pay config profile unset binaryPath` or set binaryPath to a standard fnn filename in the target directory."
|
|
4525
|
+
});
|
|
4526
|
+
} else {
|
|
4527
|
+
console.error(`\u274C ${message}`);
|
|
4528
|
+
}
|
|
4529
|
+
process.exit(1);
|
|
4530
|
+
}
|
|
4531
|
+
const binaryManager = new BinaryManager3(installDir);
|
|
3866
4532
|
const pid = readPidFile(config.dataDir);
|
|
3867
4533
|
if (pid && isProcessRunning(pid)) {
|
|
3868
4534
|
const msg = "The Fiber node is currently running. Stop it before upgrading.";
|
|
@@ -4134,29 +4800,6 @@ function createNodeCommand(config) {
|
|
|
4134
4800
|
node.command("ready").description("Agent-oriented readiness summary for automation").option("--json").action(async (options) => {
|
|
4135
4801
|
await runNodeReadyCommand(config, options);
|
|
4136
4802
|
});
|
|
4137
|
-
node.command("info").option("--json").action(async (options) => {
|
|
4138
|
-
const rpc = await createReadyRpcClient(config);
|
|
4139
|
-
const nodeInfo = await rpc.nodeInfo();
|
|
4140
|
-
const fundingAddress = scriptToAddress2(nodeInfo.default_funding_lock_script, config.network);
|
|
4141
|
-
const peerId = await nodeIdToPeerId2(nodeInfo.node_id);
|
|
4142
|
-
const output = {
|
|
4143
|
-
nodeId: nodeInfo.node_id,
|
|
4144
|
-
peerId,
|
|
4145
|
-
addresses: nodeInfo.addresses,
|
|
4146
|
-
chainHash: nodeInfo.chain_hash,
|
|
4147
|
-
fundingAddress,
|
|
4148
|
-
fundingLockScript: nodeInfo.default_funding_lock_script,
|
|
4149
|
-
version: nodeInfo.version,
|
|
4150
|
-
channelCount: parseInt(nodeInfo.channel_count, 16),
|
|
4151
|
-
pendingChannelCount: parseInt(nodeInfo.pending_channel_count, 16),
|
|
4152
|
-
peersCount: parseInt(nodeInfo.peers_count, 16)
|
|
4153
|
-
};
|
|
4154
|
-
if (options.json) {
|
|
4155
|
-
printJsonSuccess(output);
|
|
4156
|
-
} else {
|
|
4157
|
-
printNodeInfoHuman(output);
|
|
4158
|
-
}
|
|
4159
|
-
});
|
|
4160
4803
|
node.command("upgrade").description("Upgrade the Fiber node binary and migrate the database if needed").option("--version <version>", "Target Fiber version (default: latest)").option("--no-backup", "Skip creating a store backup before migration").option("--check-only", "Only check if migration is needed, do not migrate").option(
|
|
4161
4804
|
"--force-migrate",
|
|
4162
4805
|
"Force migration attempt even when compatibility check reports incompatible data"
|
|
@@ -4167,7 +4810,7 @@ function createNodeCommand(config) {
|
|
|
4167
4810
|
}
|
|
4168
4811
|
|
|
4169
4812
|
// src/commands/payment.ts
|
|
4170
|
-
import { ckbToShannons as
|
|
4813
|
+
import { ckbToShannons as ckbToShannons4, shannonsToCkb as shannonsToCkb5 } from "@fiber-pay/sdk";
|
|
4171
4814
|
import { Command as Command9 } from "commander";
|
|
4172
4815
|
function createPaymentCommand(config) {
|
|
4173
4816
|
const payment = new Command9("payment").description("Payment lifecycle and status commands");
|
|
@@ -4209,9 +4852,9 @@ function createPaymentCommand(config) {
|
|
|
4209
4852
|
const paymentParams = {
|
|
4210
4853
|
invoice,
|
|
4211
4854
|
target_pubkey: recipientNodeId,
|
|
4212
|
-
amount: amountCkb ?
|
|
4855
|
+
amount: amountCkb ? ckbToShannons4(amountCkb) : void 0,
|
|
4213
4856
|
keysend: recipientNodeId ? true : void 0,
|
|
4214
|
-
max_fee_amount: maxFeeCkb ?
|
|
4857
|
+
max_fee_amount: maxFeeCkb ? ckbToShannons4(maxFeeCkb) : void 0
|
|
4215
4858
|
};
|
|
4216
4859
|
const endpoint = resolveRpcEndpoint(config);
|
|
4217
4860
|
if (endpoint.target === "runtime-proxy") {
|
|
@@ -4253,7 +4896,7 @@ function createPaymentCommand(config) {
|
|
|
4253
4896
|
const payload = {
|
|
4254
4897
|
paymentHash: result.payment_hash,
|
|
4255
4898
|
status: result.status === "Success" ? "success" : result.status === "Failed" ? "failed" : "pending",
|
|
4256
|
-
feeCkb:
|
|
4899
|
+
feeCkb: shannonsToCkb5(result.fee),
|
|
4257
4900
|
failureReason: result.failed_error
|
|
4258
4901
|
};
|
|
4259
4902
|
if (json) {
|
|
@@ -4268,6 +4911,7 @@ function createPaymentCommand(config) {
|
|
|
4268
4911
|
}
|
|
4269
4912
|
}
|
|
4270
4913
|
});
|
|
4914
|
+
registerPaymentRebalanceCommand(payment, config);
|
|
4271
4915
|
payment.command("get").argument("<paymentHash>").option("--json").action(async (paymentHash, options) => {
|
|
4272
4916
|
const rpc = await createReadyRpcClient(config);
|
|
4273
4917
|
const result = await rpc.getPayment({ payment_hash: paymentHash });
|
|
@@ -4416,7 +5060,7 @@ function createPaymentCommand(config) {
|
|
|
4416
5060
|
process.exit(1);
|
|
4417
5061
|
}
|
|
4418
5062
|
const hopsInfo = pubkeys.map((pubkey) => ({ pubkey }));
|
|
4419
|
-
const amount = options.amount ?
|
|
5063
|
+
const amount = options.amount ? ckbToShannons4(parseFloat(options.amount)) : void 0;
|
|
4420
5064
|
const result = await rpc.buildRouter({
|
|
4421
5065
|
hops_info: hopsInfo,
|
|
4422
5066
|
amount
|
|
@@ -4432,7 +5076,7 @@ function createPaymentCommand(config) {
|
|
|
4432
5076
|
console.log(
|
|
4433
5077
|
` Outpoint: ${hop.channel_outpoint.tx_hash}:${hop.channel_outpoint.index}`
|
|
4434
5078
|
);
|
|
4435
|
-
console.log(` Amount: ${
|
|
5079
|
+
console.log(` Amount: ${shannonsToCkb5(hop.amount_received)} CKB`);
|
|
4436
5080
|
console.log(` Expiry: ${hop.incoming_tlc_expiry}`);
|
|
4437
5081
|
}
|
|
4438
5082
|
}
|
|
@@ -4440,7 +5084,7 @@ function createPaymentCommand(config) {
|
|
|
4440
5084
|
payment.command("send-route").description("Send a payment using a pre-built route from `payment route`").requiredOption(
|
|
4441
5085
|
"--router <json>",
|
|
4442
5086
|
"JSON array of router hops (output of `payment route --json`)"
|
|
4443
|
-
).option("--invoice <invoice>", "Invoice to pay").option("--payment-hash <hash>", "Payment hash (for keysend)").option("--keysend", "Keysend mode").option("--dry-run", "Simulate\u2014do not actually send").option("--json").action(async (options) => {
|
|
5087
|
+
).option("--invoice <invoice>", "Invoice to pay").option("--payment-hash <hash>", "Payment hash (for keysend)").option("--keysend", "Keysend mode").option("--allow-self-payment", "Allow self-payment for circular route rebalancing").option("--dry-run", "Simulate\u2014do not actually send").option("--json").action(async (options) => {
|
|
4444
5088
|
const rpc = await createReadyRpcClient(config);
|
|
4445
5089
|
const json = Boolean(options.json);
|
|
4446
5090
|
let router;
|
|
@@ -4465,12 +5109,13 @@ function createPaymentCommand(config) {
|
|
|
4465
5109
|
invoice: options.invoice,
|
|
4466
5110
|
payment_hash: options.paymentHash,
|
|
4467
5111
|
keysend: options.keysend ? true : void 0,
|
|
5112
|
+
allow_self_payment: options.allowSelfPayment ? true : void 0,
|
|
4468
5113
|
dry_run: options.dryRun ? true : void 0
|
|
4469
5114
|
});
|
|
4470
5115
|
const payload = {
|
|
4471
5116
|
paymentHash: result.payment_hash,
|
|
4472
5117
|
status: result.status === "Success" ? "success" : result.status === "Failed" ? "failed" : "pending",
|
|
4473
|
-
feeCkb:
|
|
5118
|
+
feeCkb: shannonsToCkb5(result.fee),
|
|
4474
5119
|
failureReason: result.failed_error,
|
|
4475
5120
|
dryRun: Boolean(options.dryRun)
|
|
4476
5121
|
};
|
|
@@ -4494,7 +5139,7 @@ function getJobPaymentHash(job) {
|
|
|
4494
5139
|
}
|
|
4495
5140
|
function getJobFeeCkb(job) {
|
|
4496
5141
|
const result = job.result;
|
|
4497
|
-
return result?.fee ?
|
|
5142
|
+
return result?.fee ? shannonsToCkb5(result.fee) : 0;
|
|
4498
5143
|
}
|
|
4499
5144
|
function getJobFailure(job) {
|
|
4500
5145
|
const result = job.result;
|
|
@@ -4566,7 +5211,7 @@ function createPeerCommand(config) {
|
|
|
4566
5211
|
|
|
4567
5212
|
// src/commands/runtime.ts
|
|
4568
5213
|
import { spawn as spawn2 } from "child_process";
|
|
4569
|
-
import { resolve } from "path";
|
|
5214
|
+
import { join as join8, resolve } from "path";
|
|
4570
5215
|
import {
|
|
4571
5216
|
alertPriorityOrder,
|
|
4572
5217
|
formatRuntimeAlert as formatRuntimeAlert2,
|
|
@@ -4640,15 +5285,22 @@ function shouldPrintAlert(alert, filter) {
|
|
|
4640
5285
|
}
|
|
4641
5286
|
return true;
|
|
4642
5287
|
}
|
|
5288
|
+
function resolveRuntimeRecoveryListen(config) {
|
|
5289
|
+
const meta = readRuntimeMeta(config.dataDir);
|
|
5290
|
+
return meta?.proxyListen ?? config.runtimeProxyListen ?? "127.0.0.1:8229";
|
|
5291
|
+
}
|
|
4643
5292
|
function createRuntimeCommand(config) {
|
|
4644
5293
|
const runtime = new Command11("runtime").description("Polling monitor and alert runtime service");
|
|
4645
|
-
runtime.command("start").description("Start runtime monitor service in foreground").option("--daemon", "Start runtime monitor in detached background mode").option("--fiber-rpc-url <url>", "Target fiber rpc URL (defaults to --rpc-url/global config)").option("--proxy-listen <host:port>", "Monitor proxy listen address").option("--channel-poll-ms <ms>", "Channel polling interval in milliseconds").option("--invoice-poll-ms <ms>", "Invoice polling interval in milliseconds").option("--payment-poll-ms <ms>", "Payment polling interval in milliseconds").option("--peer-poll-ms <ms>", "Peer polling interval in milliseconds").option("--health-poll-ms <ms>", "RPC health polling interval in milliseconds").option("--include-closed <bool>", "Monitor closed channels (true|false)").option("--completed-ttl-seconds <seconds>", "TTL for completed invoices/payments in tracker").option("--state-file <path>", "State file path for snapshots and history").option("--alert-log-file <path>", "Path to runtime alert JSONL log file").option("--flush-ms <ms>", "State flush interval in milliseconds").option("--webhook <url>", "Webhook URL to receive alert POST payloads").option("--websocket <host:port>", "WebSocket alert broadcast listen address").option(
|
|
5294
|
+
runtime.command("start").description("Start runtime monitor service in foreground").option("--daemon", "Start runtime monitor in detached background mode").option("--fiber-rpc-url <url>", "Target fiber rpc URL (defaults to --rpc-url/global config)").option("--proxy-listen <host:port>", "Monitor proxy listen address").option("--channel-poll-ms <ms>", "Channel polling interval in milliseconds").option("--invoice-poll-ms <ms>", "Invoice polling interval in milliseconds").option("--payment-poll-ms <ms>", "Payment polling interval in milliseconds").option("--peer-poll-ms <ms>", "Peer polling interval in milliseconds").option("--health-poll-ms <ms>", "RPC health polling interval in milliseconds").option("--include-closed <bool>", "Monitor closed channels (true|false)").option("--completed-ttl-seconds <seconds>", "TTL for completed invoices/payments in tracker").option("--state-file <path>", "State file path for snapshots and history").option("--alert-log-file <path>", "Path to runtime alert JSONL log file (legacy static path)").option("--alert-logs-base-dir <dir>", "Base logs directory for daily-rotated alert files").option("--flush-ms <ms>", "State flush interval in milliseconds").option("--webhook <url>", "Webhook URL to receive alert POST payloads").option("--websocket <host:port>", "WebSocket alert broadcast listen address").option(
|
|
4646
5295
|
"--log-min-priority <priority>",
|
|
4647
5296
|
"Minimum runtime log priority (critical|high|medium|low)"
|
|
4648
5297
|
).option("--log-type <types>", "Comma-separated runtime alert types to print").option("--json").action(async (options) => {
|
|
4649
5298
|
const asJson = Boolean(options.json);
|
|
4650
5299
|
const daemon = Boolean(options.daemon);
|
|
4651
5300
|
const isRuntimeChild = process.env.FIBER_RUNTIME_CHILD === "1";
|
|
5301
|
+
const runtimeListen = String(
|
|
5302
|
+
options.proxyListen ?? config.runtimeProxyListen ?? "127.0.0.1:8229"
|
|
5303
|
+
);
|
|
4652
5304
|
try {
|
|
4653
5305
|
const existingPid = readRuntimePid(config.dataDir);
|
|
4654
5306
|
if (existingPid && isProcessRunning(existingPid) && (!isRuntimeChild || existingPid !== process.pid)) {
|
|
@@ -4668,6 +5320,32 @@ function createRuntimeCommand(config) {
|
|
|
4668
5320
|
if (existingPid && !isProcessRunning(existingPid)) {
|
|
4669
5321
|
removeRuntimeFiles(config.dataDir);
|
|
4670
5322
|
}
|
|
5323
|
+
const discovered = findListeningProcessByPort(runtimeListen);
|
|
5324
|
+
if (discovered && (!existingPid || discovered.pid !== existingPid)) {
|
|
5325
|
+
if (isFiberRuntimeCommand(discovered.command)) {
|
|
5326
|
+
const terminated = await terminateProcess(discovered.pid);
|
|
5327
|
+
if (!terminated) {
|
|
5328
|
+
throw new Error(
|
|
5329
|
+
`Runtime port ${runtimeListen} is occupied by stale fiber-pay runtime PID ${discovered.pid} but termination failed.`
|
|
5330
|
+
);
|
|
5331
|
+
}
|
|
5332
|
+
removeRuntimeFiles(config.dataDir);
|
|
5333
|
+
if (!asJson) {
|
|
5334
|
+
console.log(
|
|
5335
|
+
`Recovered stale runtime process on ${runtimeListen} (PID: ${discovered.pid}).`
|
|
5336
|
+
);
|
|
5337
|
+
}
|
|
5338
|
+
} else if (discovered.command) {
|
|
5339
|
+
const details = discovered.command ? `PID ${discovered.pid} (${discovered.command})` : `PID ${discovered.pid}`;
|
|
5340
|
+
throw new Error(
|
|
5341
|
+
`Runtime proxy listen ${runtimeListen} is already in use by non-fiber-pay process: ${details}`
|
|
5342
|
+
);
|
|
5343
|
+
} else {
|
|
5344
|
+
throw new Error(
|
|
5345
|
+
`Runtime proxy listen ${runtimeListen} is already in use by process PID ${discovered.pid}. Unable to determine the owning command; inspect this PID manually before retrying.`
|
|
5346
|
+
);
|
|
5347
|
+
}
|
|
5348
|
+
}
|
|
4671
5349
|
if (daemon && !isRuntimeChild) {
|
|
4672
5350
|
const childArgv = process.argv.filter((arg) => arg !== "--daemon");
|
|
4673
5351
|
const child = spawn2(process.execPath, childArgv.slice(1), {
|
|
@@ -4710,7 +5388,7 @@ function createRuntimeCommand(config) {
|
|
|
4710
5388
|
),
|
|
4711
5389
|
proxy: {
|
|
4712
5390
|
enabled: true,
|
|
4713
|
-
listen:
|
|
5391
|
+
listen: runtimeListen
|
|
4714
5392
|
},
|
|
4715
5393
|
storage: {
|
|
4716
5394
|
stateFilePath: options.stateFile ? resolve(String(options.stateFile)) : resolve(config.dataDir, "runtime-state.json"),
|
|
@@ -4721,9 +5399,21 @@ function createRuntimeCommand(config) {
|
|
|
4721
5399
|
dbPath: resolve(config.dataDir, "runtime-jobs.db")
|
|
4722
5400
|
}
|
|
4723
5401
|
};
|
|
4724
|
-
|
|
5402
|
+
let alertLogsBaseDir;
|
|
5403
|
+
let alertLogFile;
|
|
5404
|
+
if (options.alertLogsBaseDir) {
|
|
5405
|
+
alertLogsBaseDir = resolve(String(options.alertLogsBaseDir));
|
|
5406
|
+
} else if (options.alertLogFile) {
|
|
5407
|
+
alertLogFile = resolve(String(options.alertLogFile));
|
|
5408
|
+
} else {
|
|
5409
|
+
alertLogsBaseDir = resolve(config.dataDir, "logs");
|
|
5410
|
+
}
|
|
4725
5411
|
const alerts = [{ type: "stdout" }];
|
|
4726
|
-
|
|
5412
|
+
if (alertLogsBaseDir) {
|
|
5413
|
+
alerts.push({ type: "daily-file", baseLogsDir: alertLogsBaseDir });
|
|
5414
|
+
} else if (alertLogFile) {
|
|
5415
|
+
alerts.push({ type: "file", path: alertLogFile });
|
|
5416
|
+
}
|
|
4727
5417
|
if (options.webhook) {
|
|
4728
5418
|
alerts.push({ type: "webhook", url: String(options.webhook) });
|
|
4729
5419
|
}
|
|
@@ -4745,6 +5435,12 @@ function createRuntimeCommand(config) {
|
|
|
4745
5435
|
}
|
|
4746
5436
|
const runtime2 = await startRuntimeService2(runtimeConfig);
|
|
4747
5437
|
const status = runtime2.service.getStatus();
|
|
5438
|
+
const logsBaseDir = alertLogsBaseDir ?? resolve(config.dataDir, "logs");
|
|
5439
|
+
const todayLogDir = resolveLogDirForDateWithOptions(config.dataDir, void 0, {
|
|
5440
|
+
logsBaseDir,
|
|
5441
|
+
ensureExists: false
|
|
5442
|
+
});
|
|
5443
|
+
const effectiveAlertLogPath = alertLogsBaseDir ? join8(todayLogDir, "runtime.alerts.jsonl") : alertLogFile ?? join8(todayLogDir, "runtime.alerts.jsonl");
|
|
4748
5444
|
writeRuntimePid(config.dataDir, process.pid);
|
|
4749
5445
|
writeRuntimeMeta(config.dataDir, {
|
|
4750
5446
|
pid: process.pid,
|
|
@@ -4752,7 +5448,10 @@ function createRuntimeCommand(config) {
|
|
|
4752
5448
|
fiberRpcUrl: status.targetUrl,
|
|
4753
5449
|
proxyListen: status.proxyListen,
|
|
4754
5450
|
stateFilePath: runtimeConfig.storage?.stateFilePath,
|
|
4755
|
-
alertLogFilePath:
|
|
5451
|
+
alertLogFilePath: effectiveAlertLogPath,
|
|
5452
|
+
fnnStdoutLogPath: join8(todayLogDir, "fnn.stdout.log"),
|
|
5453
|
+
fnnStderrLogPath: join8(todayLogDir, "fnn.stderr.log"),
|
|
5454
|
+
logsBaseDir,
|
|
4756
5455
|
daemon: daemon || isRuntimeChild
|
|
4757
5456
|
});
|
|
4758
5457
|
runtime2.service.on("alert", (alert) => {
|
|
@@ -4771,14 +5470,16 @@ function createRuntimeCommand(config) {
|
|
|
4771
5470
|
fiberRpcUrl: status.targetUrl,
|
|
4772
5471
|
proxyListen: status.proxyListen,
|
|
4773
5472
|
stateFilePath: runtimeConfig.storage?.stateFilePath,
|
|
4774
|
-
alertLogFile
|
|
5473
|
+
alertLogFile: effectiveAlertLogPath,
|
|
5474
|
+
logsBaseDir
|
|
4775
5475
|
});
|
|
4776
5476
|
printJsonEvent("runtime_started", status);
|
|
4777
5477
|
} else {
|
|
4778
5478
|
console.log(`Fiber RPC: ${status.targetUrl}`);
|
|
4779
5479
|
console.log(`Proxy listen: ${status.proxyListen}`);
|
|
4780
5480
|
console.log(`State file: ${runtimeConfig.storage?.stateFilePath}`);
|
|
4781
|
-
console.log(`
|
|
5481
|
+
console.log(`Logs dir: ${logsBaseDir}`);
|
|
5482
|
+
console.log(`Alert log: ${effectiveAlertLogPath}`);
|
|
4782
5483
|
console.log("Runtime monitor is running. Press Ctrl+C to stop.");
|
|
4783
5484
|
}
|
|
4784
5485
|
const signal = await runtime2.waitForShutdownSignal();
|
|
@@ -4812,8 +5513,42 @@ function createRuntimeCommand(config) {
|
|
|
4812
5513
|
});
|
|
4813
5514
|
runtime.command("status").description("Show runtime process and health status").option("--json").action(async (options) => {
|
|
4814
5515
|
const asJson = Boolean(options.json);
|
|
4815
|
-
|
|
5516
|
+
let pid = readRuntimePid(config.dataDir);
|
|
4816
5517
|
const meta = readRuntimeMeta(config.dataDir);
|
|
5518
|
+
const recoveryListen = resolveRuntimeRecoveryListen(config);
|
|
5519
|
+
if (!pid) {
|
|
5520
|
+
const fallback = findListeningProcessByPort(recoveryListen);
|
|
5521
|
+
if (fallback && isFiberRuntimeCommand(fallback.command)) {
|
|
5522
|
+
pid = fallback.pid;
|
|
5523
|
+
writeRuntimePid(config.dataDir, pid);
|
|
5524
|
+
} else if (fallback && fallback.command) {
|
|
5525
|
+
const details = fallback.command ? `PID ${fallback.pid} (${fallback.command})` : `PID ${fallback.pid}`;
|
|
5526
|
+
if (asJson) {
|
|
5527
|
+
printJsonError({
|
|
5528
|
+
code: "RUNTIME_PORT_IN_USE",
|
|
5529
|
+
message: `Runtime proxy port is in use by non-fiber-pay process: ${details}`,
|
|
5530
|
+
recoverable: true,
|
|
5531
|
+
suggestion: "Stop that process or use a different --proxy-listen port."
|
|
5532
|
+
});
|
|
5533
|
+
} else {
|
|
5534
|
+
console.log(`Runtime proxy port is in use by non-fiber-pay process: ${details}`);
|
|
5535
|
+
}
|
|
5536
|
+
process.exit(1);
|
|
5537
|
+
} else if (fallback) {
|
|
5538
|
+
const message = `Runtime proxy port is in use by process PID ${fallback.pid}. The owning command could not be determined; inspect this PID manually.`;
|
|
5539
|
+
if (asJson) {
|
|
5540
|
+
printJsonError({
|
|
5541
|
+
code: "RUNTIME_PORT_IN_USE",
|
|
5542
|
+
message,
|
|
5543
|
+
recoverable: true,
|
|
5544
|
+
suggestion: "Inspect the PID owner manually or use a different --proxy-listen port."
|
|
5545
|
+
});
|
|
5546
|
+
} else {
|
|
5547
|
+
console.log(message);
|
|
5548
|
+
}
|
|
5549
|
+
process.exit(1);
|
|
5550
|
+
}
|
|
5551
|
+
}
|
|
4817
5552
|
if (!pid) {
|
|
4818
5553
|
if (asJson) {
|
|
4819
5554
|
printJsonError({
|
|
@@ -4844,9 +5579,11 @@ function createRuntimeCommand(config) {
|
|
|
4844
5579
|
process.exit(1);
|
|
4845
5580
|
}
|
|
4846
5581
|
let rpcStatus;
|
|
4847
|
-
if (meta?.proxyListen) {
|
|
5582
|
+
if (meta?.proxyListen ?? recoveryListen) {
|
|
4848
5583
|
try {
|
|
4849
|
-
const response = await fetch(
|
|
5584
|
+
const response = await fetch(
|
|
5585
|
+
`http://${meta?.proxyListen ?? recoveryListen}/monitor/status`
|
|
5586
|
+
);
|
|
4850
5587
|
if (response.ok) {
|
|
4851
5588
|
rpcStatus = await response.json();
|
|
4852
5589
|
}
|
|
@@ -4874,7 +5611,41 @@ function createRuntimeCommand(config) {
|
|
|
4874
5611
|
});
|
|
4875
5612
|
runtime.command("stop").description("Stop runtime process by PID").option("--json").action(async (options) => {
|
|
4876
5613
|
const asJson = Boolean(options.json);
|
|
4877
|
-
|
|
5614
|
+
let pid = readRuntimePid(config.dataDir);
|
|
5615
|
+
const recoveryListen = resolveRuntimeRecoveryListen(config);
|
|
5616
|
+
if (!pid) {
|
|
5617
|
+
const fallback = findListeningProcessByPort(recoveryListen);
|
|
5618
|
+
if (fallback && isFiberRuntimeCommand(fallback.command)) {
|
|
5619
|
+
pid = fallback.pid;
|
|
5620
|
+
writeRuntimePid(config.dataDir, pid);
|
|
5621
|
+
} else if (fallback && fallback.command) {
|
|
5622
|
+
const details = fallback.command ? `PID ${fallback.pid} (${fallback.command})` : `PID ${fallback.pid}`;
|
|
5623
|
+
if (asJson) {
|
|
5624
|
+
printJsonError({
|
|
5625
|
+
code: "RUNTIME_PORT_IN_USE",
|
|
5626
|
+
message: `Runtime proxy port is in use by non-fiber-pay process: ${details}`,
|
|
5627
|
+
recoverable: true,
|
|
5628
|
+
suggestion: "Stop that process manually; it is not managed by fiber-pay runtime PID files."
|
|
5629
|
+
});
|
|
5630
|
+
} else {
|
|
5631
|
+
console.log(`Runtime proxy port is in use by non-fiber-pay process: ${details}`);
|
|
5632
|
+
}
|
|
5633
|
+
process.exit(1);
|
|
5634
|
+
} else if (fallback) {
|
|
5635
|
+
const message = `Runtime proxy port is in use by process PID ${fallback.pid}. The owning command could not be determined; inspect this PID manually.`;
|
|
5636
|
+
if (asJson) {
|
|
5637
|
+
printJsonError({
|
|
5638
|
+
code: "RUNTIME_PORT_IN_USE",
|
|
5639
|
+
message,
|
|
5640
|
+
recoverable: true,
|
|
5641
|
+
suggestion: "Inspect the PID owner manually; it may not be managed by fiber-pay runtime PID files."
|
|
5642
|
+
});
|
|
5643
|
+
} else {
|
|
5644
|
+
console.log(message);
|
|
5645
|
+
}
|
|
5646
|
+
process.exit(1);
|
|
5647
|
+
}
|
|
5648
|
+
}
|
|
4878
5649
|
if (!pid) {
|
|
4879
5650
|
if (asJson) {
|
|
4880
5651
|
printJsonError({
|
|
@@ -4903,14 +5674,19 @@ function createRuntimeCommand(config) {
|
|
|
4903
5674
|
}
|
|
4904
5675
|
process.exit(1);
|
|
4905
5676
|
}
|
|
4906
|
-
|
|
4907
|
-
|
|
4908
|
-
|
|
4909
|
-
|
|
4910
|
-
|
|
4911
|
-
|
|
4912
|
-
|
|
4913
|
-
|
|
5677
|
+
const terminated = await terminateProcess(pid);
|
|
5678
|
+
if (!terminated) {
|
|
5679
|
+
if (asJson) {
|
|
5680
|
+
printJsonError({
|
|
5681
|
+
code: "RUNTIME_STOP_FAILED",
|
|
5682
|
+
message: `Failed to terminate runtime process ${pid}.`,
|
|
5683
|
+
recoverable: true,
|
|
5684
|
+
suggestion: `Try stopping PID ${pid} manually and rerun runtime stop.`
|
|
5685
|
+
});
|
|
5686
|
+
} else {
|
|
5687
|
+
console.log(`Failed to terminate runtime process ${pid}.`);
|
|
5688
|
+
}
|
|
5689
|
+
process.exit(1);
|
|
4914
5690
|
}
|
|
4915
5691
|
removeRuntimeFiles(config.dataDir);
|
|
4916
5692
|
if (asJson) {
|
|
@@ -4926,8 +5702,8 @@ function createRuntimeCommand(config) {
|
|
|
4926
5702
|
import { Command as Command12 } from "commander";
|
|
4927
5703
|
|
|
4928
5704
|
// src/lib/build-info.ts
|
|
4929
|
-
var CLI_VERSION = "0.1.0-rc.
|
|
4930
|
-
var CLI_COMMIT = "
|
|
5705
|
+
var CLI_VERSION = "0.1.0-rc.5";
|
|
5706
|
+
var CLI_COMMIT = "28cee07226e004d775ed747bbcbf61474f93c492";
|
|
4931
5707
|
|
|
4932
5708
|
// src/commands/version.ts
|
|
4933
5709
|
function createVersionCommand() {
|
|
@@ -5035,6 +5811,14 @@ function applyGlobalOverrides(argv) {
|
|
|
5035
5811
|
}
|
|
5036
5812
|
break;
|
|
5037
5813
|
}
|
|
5814
|
+
case "--rpc-biscuit-token": {
|
|
5815
|
+
const value = getFlagValue(argv, index);
|
|
5816
|
+
if (value) {
|
|
5817
|
+
process.env.FIBER_RPC_BISCUIT_TOKEN = value;
|
|
5818
|
+
explicitFlags.add("rpcBiscuitToken");
|
|
5819
|
+
}
|
|
5820
|
+
break;
|
|
5821
|
+
}
|
|
5038
5822
|
case "--network": {
|
|
5039
5823
|
const value = getFlagValue(argv, index);
|
|
5040
5824
|
if (value) {
|
|
@@ -5074,7 +5858,7 @@ function applyGlobalOverrides(argv) {
|
|
|
5074
5858
|
}
|
|
5075
5859
|
if (!explicitDataDir && profileName) {
|
|
5076
5860
|
const homeDir = process.env.HOME ?? process.cwd();
|
|
5077
|
-
process.env.FIBER_DATA_DIR =
|
|
5861
|
+
process.env.FIBER_DATA_DIR = join9(homeDir, ".fiber-pay", "profiles", profileName);
|
|
5078
5862
|
}
|
|
5079
5863
|
}
|
|
5080
5864
|
function printFatal(error) {
|
|
@@ -5103,7 +5887,7 @@ async function main() {
|
|
|
5103
5887
|
applyGlobalOverrides(argv);
|
|
5104
5888
|
const config = getEffectiveConfig(explicitFlags).config;
|
|
5105
5889
|
const program = new Command13();
|
|
5106
|
-
program.name("fiber-pay").description("AI Agent Payment SDK for CKB Lightning Network").option("--profile <name>", "Use profile at ~/.fiber-pay/profiles/<name>").option("--data-dir <path>", "Override data directory for all commands").option("--rpc-url <url>", "Override RPC URL for all commands").option("--network <network>", "Override network for all commands (testnet|mainnet)").option("--key-password <password>", "Override key password for all commands").option("--binary-path <path>", "Override fiber binary path for all commands").showHelpAfterError().showSuggestionAfterError();
|
|
5890
|
+
program.name("fiber-pay").description("AI Agent Payment SDK for CKB Lightning Network").option("--profile <name>", "Use profile at ~/.fiber-pay/profiles/<name>").option("--data-dir <path>", "Override data directory for all commands").option("--rpc-url <url>", "Override RPC URL for all commands").option("--rpc-biscuit-token <token>", "Set RPC Authorization Bearer token for all commands").option("--network <network>", "Override network for all commands (testnet|mainnet)").option("--key-password <password>", "Override key password for all commands").option("--binary-path <path>", "Override fiber binary path for all commands").showHelpAfterError().showSuggestionAfterError();
|
|
5107
5891
|
program.exitOverride();
|
|
5108
5892
|
program.configureOutput({
|
|
5109
5893
|
writeOut: (str) => process.stdout.write(str),
|