@fiber-pay/cli 0.1.0-rc.3 → 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/README.md +1 -1
- package/dist/cli.js +1521 -332
- package/dist/cli.js.map +1 -1
- package/error-codes.json +10 -0
- package/package.json +6 -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,22 +1351,22 @@ 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
|
-
var
|
|
1363
|
+
var TESTNET_CONFIG_TEMPLATE_V071 = `# This configuration file only contains the necessary configurations for the testnet deployment.
|
|
969
1364
|
# All options' descriptions can be found via \`fnn --help\` and be overridden by command line arguments or environment variables.
|
|
970
1365
|
fiber:
|
|
971
|
-
listening_addr: "/ip4/
|
|
1366
|
+
listening_addr: "/ip4/0.0.0.0/tcp/8228"
|
|
972
1367
|
bootnode_addrs:
|
|
973
1368
|
- "/ip4/54.179.226.154/tcp/8228/p2p/Qmes1EBD4yNo9Ywkfe6eRw9tG1nVNGLDmMud1xJMsoYFKy"
|
|
974
|
-
- "/ip4/
|
|
1369
|
+
- "/ip4/16.163.7.105/tcp/8228/p2p/QmdyQWjPtbK4NWWsvy8s69NGJaQULwgeQDT5ZpNDrTNaeV"
|
|
975
1370
|
announce_listening_addr: true
|
|
976
1371
|
announced_addrs:
|
|
977
1372
|
# If you want to announce your fiber node public address to the network, you need to add the address here, please change the ip to your public ip accordingly.
|
|
@@ -1037,7 +1432,7 @@ services:
|
|
|
1037
1432
|
- rpc
|
|
1038
1433
|
- ckb
|
|
1039
1434
|
`;
|
|
1040
|
-
var
|
|
1435
|
+
var MAINNET_CONFIG_TEMPLATE_V071 = `# This configuration file only contains the necessary configurations for the mainnet deployment.
|
|
1041
1436
|
# All options' descriptions can be found via \`fnn --help\` and be overridden by command line arguments or environment variables.
|
|
1042
1437
|
fiber:
|
|
1043
1438
|
listening_addr: "/ip4/0.0.0.0/tcp/8228"
|
|
@@ -1113,7 +1508,7 @@ services:
|
|
|
1113
1508
|
- ckb
|
|
1114
1509
|
`;
|
|
1115
1510
|
function getConfigTemplate(network) {
|
|
1116
|
-
return network === "mainnet" ?
|
|
1511
|
+
return network === "mainnet" ? MAINNET_CONFIG_TEMPLATE_V071 : TESTNET_CONFIG_TEMPLATE_V071;
|
|
1117
1512
|
}
|
|
1118
1513
|
|
|
1119
1514
|
// src/lib/config.ts
|
|
@@ -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,116 +3240,32 @@ Following logs (interval: ${intervalMs}ms). Press Ctrl+C to stop.`);
|
|
|
2678
3240
|
});
|
|
2679
3241
|
});
|
|
2680
3242
|
}
|
|
2681
|
-
|
|
2682
|
-
|
|
2683
|
-
|
|
2684
|
-
|
|
2685
|
-
|
|
2686
|
-
// src/lib/node-runtime-daemon.ts
|
|
2687
|
-
import { spawnSync } from "child_process";
|
|
2688
|
-
import { existsSync as existsSync8 } from "fs";
|
|
2689
|
-
function getCustomBinaryState(binaryPath) {
|
|
2690
|
-
const exists = existsSync8(binaryPath);
|
|
2691
|
-
if (!exists) {
|
|
2692
|
-
return { path: binaryPath, ready: false, version: "unknown" };
|
|
2693
|
-
}
|
|
2694
|
-
try {
|
|
2695
|
-
const result = spawnSync(binaryPath, ["--version"], { encoding: "utf-8" });
|
|
2696
|
-
if (result.status !== 0) {
|
|
2697
|
-
return { path: binaryPath, ready: false, version: "unknown" };
|
|
2698
|
-
}
|
|
2699
|
-
const output = `${result.stdout ?? ""}${result.stderr ?? ""}`.trim();
|
|
2700
|
-
const firstLine = output.split("\n").find((line) => line.trim().length > 0) ?? "unknown";
|
|
2701
|
-
return { path: binaryPath, ready: true, version: firstLine.trim() };
|
|
2702
|
-
} catch {
|
|
2703
|
-
return { path: binaryPath, ready: false, version: "unknown" };
|
|
2704
|
-
}
|
|
2705
|
-
}
|
|
2706
|
-
function getBinaryVersion(binaryPath) {
|
|
2707
|
-
try {
|
|
2708
|
-
const result = spawnSync(binaryPath, ["--version"], { encoding: "utf-8" });
|
|
2709
|
-
if (result.status !== 0) {
|
|
2710
|
-
return "unknown";
|
|
2711
|
-
}
|
|
2712
|
-
const output = `${result.stdout ?? ""}${result.stderr ?? ""}`.trim();
|
|
2713
|
-
if (!output) {
|
|
2714
|
-
return "unknown";
|
|
2715
|
-
}
|
|
2716
|
-
const firstLine = output.split("\n").find((line) => line.trim().length > 0);
|
|
2717
|
-
return firstLine?.trim() ?? "unknown";
|
|
2718
|
-
} catch {
|
|
2719
|
-
return "unknown";
|
|
2720
|
-
}
|
|
2721
|
-
}
|
|
2722
|
-
function getCliEntrypoint() {
|
|
2723
|
-
const entrypoint = process.argv[1];
|
|
2724
|
-
if (!entrypoint) {
|
|
2725
|
-
throw new Error("Unable to resolve CLI entrypoint path");
|
|
3243
|
+
function inferDateFromPaths(paths) {
|
|
3244
|
+
const candidate = paths.runtimeAlerts.split("/").at(-2);
|
|
3245
|
+
if (!candidate || !DATE_DIR_PATTERN2.test(candidate)) {
|
|
3246
|
+
return void 0;
|
|
2726
3247
|
}
|
|
2727
|
-
|
|
2728
|
-
|
|
2729
|
-
|
|
2730
|
-
|
|
2731
|
-
const result = spawnSync(
|
|
2732
|
-
process.execPath,
|
|
2733
|
-
[
|
|
2734
|
-
cliEntrypoint,
|
|
2735
|
-
"--data-dir",
|
|
2736
|
-
params.dataDir,
|
|
2737
|
-
"--rpc-url",
|
|
2738
|
-
params.rpcUrl,
|
|
2739
|
-
"runtime",
|
|
2740
|
-
"start",
|
|
2741
|
-
"--daemon",
|
|
2742
|
-
"--fiber-rpc-url",
|
|
2743
|
-
params.rpcUrl,
|
|
2744
|
-
"--proxy-listen",
|
|
2745
|
-
params.proxyListen,
|
|
2746
|
-
"--state-file",
|
|
2747
|
-
params.stateFilePath,
|
|
2748
|
-
"--alert-log-file",
|
|
2749
|
-
params.alertLogFile,
|
|
2750
|
-
"--json"
|
|
2751
|
-
],
|
|
2752
|
-
{ encoding: "utf-8" }
|
|
2753
|
-
);
|
|
2754
|
-
if (result.status === 0) {
|
|
2755
|
-
return { ok: true };
|
|
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;
|
|
2756
3252
|
}
|
|
2757
|
-
|
|
2758
|
-
const stdout = (result.stdout ?? "").trim();
|
|
2759
|
-
const details = stderr || stdout || `exit code ${result.status ?? "unknown"}`;
|
|
2760
|
-
return { ok: false, message: details };
|
|
2761
|
-
}
|
|
2762
|
-
function stopRuntimeDaemonFromNode(params) {
|
|
2763
|
-
const cliEntrypoint = getCliEntrypoint();
|
|
2764
|
-
spawnSync(
|
|
2765
|
-
process.execPath,
|
|
2766
|
-
[
|
|
2767
|
-
cliEntrypoint,
|
|
2768
|
-
"--data-dir",
|
|
2769
|
-
params.dataDir,
|
|
2770
|
-
"--rpc-url",
|
|
2771
|
-
params.rpcUrl,
|
|
2772
|
-
"runtime",
|
|
2773
|
-
"stop",
|
|
2774
|
-
"--json"
|
|
2775
|
-
],
|
|
2776
|
-
{ encoding: "utf-8" }
|
|
2777
|
-
);
|
|
3253
|
+
return candidate;
|
|
2778
3254
|
}
|
|
2779
3255
|
|
|
3256
|
+
// src/commands/node.ts
|
|
3257
|
+
import { Command as Command8 } from "commander";
|
|
3258
|
+
|
|
2780
3259
|
// src/lib/node-start.ts
|
|
2781
3260
|
import { spawn } from "child_process";
|
|
2782
|
-
import {
|
|
2783
|
-
import { join as
|
|
3261
|
+
import { mkdirSync as mkdirSync3 } from "fs";
|
|
3262
|
+
import { join as join7 } from "path";
|
|
2784
3263
|
import {
|
|
3264
|
+
createKeyManager,
|
|
2785
3265
|
ensureFiberBinary,
|
|
2786
|
-
getDefaultBinaryPath,
|
|
2787
3266
|
ProcessManager
|
|
2788
3267
|
} from "@fiber-pay/node";
|
|
2789
3268
|
import { startRuntimeService } from "@fiber-pay/runtime";
|
|
2790
|
-
import { createKeyManager } from "@fiber-pay/sdk";
|
|
2791
3269
|
|
|
2792
3270
|
// src/lib/bootnode.ts
|
|
2793
3271
|
import { existsSync as existsSync9, readFileSync as readFileSync5 } from "fs";
|
|
@@ -2823,6 +3301,182 @@ async function autoConnectBootnodes(rpc, bootnodes) {
|
|
|
2823
3301
|
}
|
|
2824
3302
|
}
|
|
2825
3303
|
|
|
3304
|
+
// src/lib/node-migration.ts
|
|
3305
|
+
import { dirname as dirname2 } from "path";
|
|
3306
|
+
import { BinaryManager as BinaryManager2, MigrationManager } from "@fiber-pay/node";
|
|
3307
|
+
|
|
3308
|
+
// src/lib/migration-utils.ts
|
|
3309
|
+
function replaceRawMigrateHint(message) {
|
|
3310
|
+
return message.replace(
|
|
3311
|
+
/Fiber need to run some database migrations, please run `fnn-migrate[^`]*` to start migrations\.?/g,
|
|
3312
|
+
"Fiber database migration is required."
|
|
3313
|
+
);
|
|
3314
|
+
}
|
|
3315
|
+
function normalizeMigrationCheck(check) {
|
|
3316
|
+
return {
|
|
3317
|
+
...check,
|
|
3318
|
+
message: replaceRawMigrateHint(check.message)
|
|
3319
|
+
};
|
|
3320
|
+
}
|
|
3321
|
+
|
|
3322
|
+
// src/lib/node-migration.ts
|
|
3323
|
+
async function runMigrationGuard(opts) {
|
|
3324
|
+
const { dataDir, binaryPath, json } = opts;
|
|
3325
|
+
if (!MigrationManager.storeExists(dataDir)) {
|
|
3326
|
+
return { checked: false, skippedReason: "store does not exist" };
|
|
3327
|
+
}
|
|
3328
|
+
const storePath = MigrationManager.resolveStorePath(dataDir);
|
|
3329
|
+
const binaryDir = dirname2(binaryPath);
|
|
3330
|
+
const bm = new BinaryManager2(binaryDir);
|
|
3331
|
+
const migrateBinPath = bm.getMigrateBinaryPath();
|
|
3332
|
+
let migrationCheck;
|
|
3333
|
+
try {
|
|
3334
|
+
const migrationManager = new MigrationManager(migrateBinPath);
|
|
3335
|
+
migrationCheck = await migrationManager.check(storePath);
|
|
3336
|
+
} catch {
|
|
3337
|
+
return { checked: false, skippedReason: "fnn-migrate binary not available" };
|
|
3338
|
+
}
|
|
3339
|
+
if (migrationCheck.needed) {
|
|
3340
|
+
const message = migrationCheck.valid ? "Database migration required. Run `fiber-pay node upgrade --force-migrate` before starting." : replaceRawMigrateHint(migrationCheck.message);
|
|
3341
|
+
if (json) {
|
|
3342
|
+
printJsonError({
|
|
3343
|
+
code: "MIGRATION_REQUIRED",
|
|
3344
|
+
message,
|
|
3345
|
+
recoverable: true,
|
|
3346
|
+
suggestion: `Back up your store first (directory: "${storePath}"). Then run \`fiber-pay node upgrade --force-migrate\`. If migration still fails, close channels on the old fnn version, remove the store, and restart with a fresh store. If backup exists, restore it to roll back.`,
|
|
3347
|
+
details: {
|
|
3348
|
+
storePath,
|
|
3349
|
+
migrationCheck: {
|
|
3350
|
+
...migrationCheck,
|
|
3351
|
+
message
|
|
3352
|
+
}
|
|
3353
|
+
}
|
|
3354
|
+
});
|
|
3355
|
+
} else {
|
|
3356
|
+
console.error(`\u274C ${message}`);
|
|
3357
|
+
console.error(` 1) Back up store directory: ${storePath}`);
|
|
3358
|
+
console.error(" 2) Run: fiber-pay node upgrade --force-migrate");
|
|
3359
|
+
console.error(
|
|
3360
|
+
" 3) If it still fails, close channels on old fnn, remove store, then restart."
|
|
3361
|
+
);
|
|
3362
|
+
console.error(" 4) If backup exists, restore it to roll back.");
|
|
3363
|
+
}
|
|
3364
|
+
process.exit(1);
|
|
3365
|
+
}
|
|
3366
|
+
return { checked: true, migrationCheck };
|
|
3367
|
+
}
|
|
3368
|
+
|
|
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;
|
|
3375
|
+
}
|
|
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;
|
|
3383
|
+
}
|
|
3384
|
+
return port;
|
|
3385
|
+
}
|
|
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;
|
|
3391
|
+
}
|
|
3392
|
+
const pid = Number(trimmed.slice(1));
|
|
3393
|
+
if (Number.isInteger(pid) && pid > 0) {
|
|
3394
|
+
return pid;
|
|
3395
|
+
}
|
|
3396
|
+
}
|
|
3397
|
+
return void 0;
|
|
3398
|
+
}
|
|
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;
|
|
3405
|
+
}
|
|
3406
|
+
const command = (result.stdout ?? "").trim();
|
|
3407
|
+
return command.length > 0 ? command : void 0;
|
|
3408
|
+
}
|
|
3409
|
+
function findListeningProcessByPort(listen) {
|
|
3410
|
+
const port = parsePortFromListen(listen);
|
|
3411
|
+
if (!port) {
|
|
3412
|
+
return void 0;
|
|
3413
|
+
}
|
|
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
|
+
};
|
|
3428
|
+
}
|
|
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);
|
|
3478
|
+
}
|
|
3479
|
+
|
|
2826
3480
|
// src/lib/node-start.ts
|
|
2827
3481
|
async function runNodeStartCommand(config, options) {
|
|
2828
3482
|
const json = Boolean(options.json);
|
|
@@ -2932,25 +3586,41 @@ async function runNodeStartCommand(config, options) {
|
|
|
2932
3586
|
options.runtimeProxyListen ?? config.runtimeProxyListen ?? "127.0.0.1:8229"
|
|
2933
3587
|
);
|
|
2934
3588
|
const proxyListenSource = options.runtimeProxyListen ? "cli" : config.runtimeProxyListen ? "profile" : "default";
|
|
2935
|
-
const runtimeStateFilePath =
|
|
2936
|
-
const
|
|
2937
|
-
|
|
2938
|
-
|
|
2939
|
-
const
|
|
2940
|
-
|
|
2941
|
-
|
|
2942
|
-
|
|
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
|
+
}
|
|
2943
3599
|
const binaryVersion = getBinaryVersion(binaryPath);
|
|
2944
3600
|
const configFilePath = ensureNodeConfigFile(config.dataDir, config.network);
|
|
2945
3601
|
emitStage("binary_resolved", "ok", {
|
|
2946
3602
|
binaryPath,
|
|
2947
3603
|
binaryVersion,
|
|
3604
|
+
binarySource: resolvedBinary.source,
|
|
2948
3605
|
configFilePath
|
|
2949
3606
|
});
|
|
2950
3607
|
if (!json) {
|
|
2951
3608
|
console.log(`\u{1F9E9} Binary: ${binaryPath}`);
|
|
2952
3609
|
console.log(`\u{1F9E9} Version: ${binaryVersion}`);
|
|
2953
3610
|
}
|
|
3611
|
+
const guardResult = await runMigrationGuard({ dataDir: config.dataDir, binaryPath, json });
|
|
3612
|
+
if (guardResult.checked) {
|
|
3613
|
+
emitStage("migration_check", "ok", {
|
|
3614
|
+
storePath: `${config.dataDir}/store`,
|
|
3615
|
+
needed: false
|
|
3616
|
+
});
|
|
3617
|
+
} else {
|
|
3618
|
+
emitStage("migration_check", "ok", {
|
|
3619
|
+
storePath: `${config.dataDir}/store`,
|
|
3620
|
+
skipped: true,
|
|
3621
|
+
reason: guardResult.skippedReason
|
|
3622
|
+
});
|
|
3623
|
+
}
|
|
2954
3624
|
const nodeConfig = {
|
|
2955
3625
|
binaryPath,
|
|
2956
3626
|
dataDir: config.dataDir,
|
|
@@ -2992,11 +3662,11 @@ async function runNodeStartCommand(config, options) {
|
|
|
2992
3662
|
removePidFile(config.dataDir);
|
|
2993
3663
|
});
|
|
2994
3664
|
processManager.on("stdout", (text) => {
|
|
2995
|
-
|
|
3665
|
+
appendToTodayLog(config.dataDir, "fnn.stdout.log", text);
|
|
2996
3666
|
emitFnnLog("stdout", text);
|
|
2997
3667
|
});
|
|
2998
3668
|
processManager.on("stderr", (text) => {
|
|
2999
|
-
|
|
3669
|
+
appendToTodayLog(config.dataDir, "fnn.stderr.log", text);
|
|
3000
3670
|
emitFnnLog("stderr", text);
|
|
3001
3671
|
});
|
|
3002
3672
|
await processManager.start();
|
|
@@ -3041,13 +3711,38 @@ async function runNodeStartCommand(config, options) {
|
|
|
3041
3711
|
process.exit(1);
|
|
3042
3712
|
}
|
|
3043
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
|
+
}
|
|
3044
3739
|
if (runtimeDaemon) {
|
|
3045
3740
|
const daemonStart = startRuntimeDaemonFromNode({
|
|
3046
3741
|
dataDir: config.dataDir,
|
|
3047
3742
|
rpcUrl: config.rpcUrl,
|
|
3048
3743
|
proxyListen: runtimeProxyListen,
|
|
3049
3744
|
stateFilePath: runtimeStateFilePath,
|
|
3050
|
-
|
|
3745
|
+
alertLogsBaseDir: logsBaseDir
|
|
3051
3746
|
});
|
|
3052
3747
|
if (!daemonStart.ok) {
|
|
3053
3748
|
throw new Error(daemonStart.message);
|
|
@@ -3062,23 +3757,25 @@ async function runNodeStartCommand(config, options) {
|
|
|
3062
3757
|
storage: {
|
|
3063
3758
|
stateFilePath: runtimeStateFilePath
|
|
3064
3759
|
},
|
|
3065
|
-
alerts: [{ type: "stdout" }, { type: "file",
|
|
3760
|
+
alerts: [{ type: "stdout" }, { type: "daily-file", baseLogsDir: logsBaseDir }],
|
|
3066
3761
|
jobs: {
|
|
3067
3762
|
enabled: true,
|
|
3068
|
-
dbPath:
|
|
3763
|
+
dbPath: join7(config.dataDir, "runtime-jobs.db")
|
|
3069
3764
|
}
|
|
3070
3765
|
});
|
|
3071
3766
|
const runtimeStatus = runtime.service.getStatus();
|
|
3072
3767
|
writeRuntimePid(config.dataDir, process.pid);
|
|
3768
|
+
const todayLogDir = resolveLogDirForDate(config.dataDir);
|
|
3073
3769
|
writeRuntimeMeta(config.dataDir, {
|
|
3074
3770
|
pid: process.pid,
|
|
3075
3771
|
startedAt: runtimeStatus.startedAt,
|
|
3076
3772
|
fiberRpcUrl: runtimeStatus.targetUrl,
|
|
3077
3773
|
proxyListen: runtimeStatus.proxyListen,
|
|
3078
3774
|
stateFilePath: runtimeStateFilePath,
|
|
3079
|
-
alertLogFilePath:
|
|
3080
|
-
fnnStdoutLogPath,
|
|
3081
|
-
fnnStderrLogPath,
|
|
3775
|
+
alertLogFilePath: join7(todayLogDir, "runtime.alerts.jsonl"),
|
|
3776
|
+
fnnStdoutLogPath: join7(todayLogDir, "fnn.stdout.log"),
|
|
3777
|
+
fnnStderrLogPath: join7(todayLogDir, "fnn.stderr.log"),
|
|
3778
|
+
logsBaseDir,
|
|
3082
3779
|
daemon: false
|
|
3083
3780
|
});
|
|
3084
3781
|
}
|
|
@@ -3148,7 +3845,7 @@ async function runNodeStartCommand(config, options) {
|
|
|
3148
3845
|
process.exit(1);
|
|
3149
3846
|
}
|
|
3150
3847
|
emitStage("rpc_ready", "ok", { rpcUrl: config.rpcUrl });
|
|
3151
|
-
const bootnodes = nodeConfig.configFilePath ? extractBootnodeAddrs(nodeConfig.configFilePath) : extractBootnodeAddrs(
|
|
3848
|
+
const bootnodes = nodeConfig.configFilePath ? extractBootnodeAddrs(nodeConfig.configFilePath) : extractBootnodeAddrs(join7(config.dataDir, "config.yml"));
|
|
3152
3849
|
if (bootnodes.length > 0) {
|
|
3153
3850
|
await autoConnectBootnodes(rpc, bootnodes);
|
|
3154
3851
|
}
|
|
@@ -3189,9 +3886,8 @@ async function runNodeStartCommand(config, options) {
|
|
|
3189
3886
|
proxyUrl: `http://${runtimeProxyListen}`,
|
|
3190
3887
|
proxyListenSource,
|
|
3191
3888
|
logs: {
|
|
3192
|
-
|
|
3193
|
-
|
|
3194
|
-
runtimeAlerts: runtimeAlertLogPath
|
|
3889
|
+
baseDir: logsBaseDir,
|
|
3890
|
+
todayDir: resolveLogDirForDate(config.dataDir)
|
|
3195
3891
|
}
|
|
3196
3892
|
});
|
|
3197
3893
|
} else {
|
|
@@ -3201,7 +3897,7 @@ async function runNodeStartCommand(config, options) {
|
|
|
3201
3897
|
` Runtime proxy: http://${runtimeProxyListen} (browser-safe endpoint + monitoring)`
|
|
3202
3898
|
);
|
|
3203
3899
|
console.log(` Runtime mode: ${runtimeDaemon ? "daemon" : "embedded"}`);
|
|
3204
|
-
console.log(` Log files: ${
|
|
3900
|
+
console.log(` Log files: ${logsBaseDir}`);
|
|
3205
3901
|
console.log(" Press Ctrl+C to stop.");
|
|
3206
3902
|
}
|
|
3207
3903
|
let shutdownRequested = false;
|
|
@@ -3269,8 +3965,6 @@ async function runNodeStartCommand(config, options) {
|
|
|
3269
3965
|
|
|
3270
3966
|
// src/lib/node-status.ts
|
|
3271
3967
|
import { existsSync as existsSync10 } from "fs";
|
|
3272
|
-
import { join as join6 } from "path";
|
|
3273
|
-
import { getFiberBinaryInfo as getFiberBinaryInfo2 } from "@fiber-pay/node";
|
|
3274
3968
|
import {
|
|
3275
3969
|
buildMultiaddrFromNodeId,
|
|
3276
3970
|
buildMultiaddrFromRpcUrl,
|
|
@@ -3437,24 +4131,29 @@ async function runNodeStatusCommand(config, options) {
|
|
|
3437
4131
|
const json = Boolean(options.json);
|
|
3438
4132
|
const pid = readPidFile(config.dataDir);
|
|
3439
4133
|
const resolvedRpc = resolveRpcEndpoint(config);
|
|
3440
|
-
const
|
|
3441
|
-
const binaryInfo = config.binaryPath ? getCustomBinaryState(config.binaryPath) : await getFiberBinaryInfo2(join6(config.dataDir, "bin"));
|
|
4134
|
+
const { resolvedBinary, info: binaryInfo } = await getBinaryDetails(config);
|
|
3442
4135
|
const configExists = existsSync10(config.configPath);
|
|
3443
4136
|
const nodeRunning = Boolean(pid && isProcessRunning(pid));
|
|
3444
4137
|
let rpcResponsive = false;
|
|
3445
4138
|
let nodeId = null;
|
|
4139
|
+
let addresses = [];
|
|
4140
|
+
let chainHash = null;
|
|
4141
|
+
let version = null;
|
|
3446
4142
|
let peerId = null;
|
|
4143
|
+
let peersCount = 0;
|
|
3447
4144
|
let peerIdError = null;
|
|
3448
4145
|
let multiaddr = null;
|
|
3449
4146
|
let multiaddrError = null;
|
|
3450
4147
|
let multiaddrInferred = false;
|
|
3451
4148
|
let channelsTotal = 0;
|
|
3452
4149
|
let channelsReady = 0;
|
|
4150
|
+
let pendingChannelCount = 0;
|
|
3453
4151
|
let canSend = false;
|
|
3454
4152
|
let canReceive = false;
|
|
3455
4153
|
let localCkb = 0;
|
|
3456
4154
|
let remoteCkb = 0;
|
|
3457
4155
|
let fundingAddress = null;
|
|
4156
|
+
let fundingLockScript = null;
|
|
3458
4157
|
let fundingCkb = 0;
|
|
3459
4158
|
let fundingBalanceError = null;
|
|
3460
4159
|
if (nodeRunning) {
|
|
@@ -3464,6 +4163,10 @@ async function runNodeStatusCommand(config, options) {
|
|
|
3464
4163
|
const channels = await rpc.listChannels({ include_closed: false });
|
|
3465
4164
|
rpcResponsive = true;
|
|
3466
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);
|
|
3467
4170
|
try {
|
|
3468
4171
|
peerId = await nodeIdToPeerId(nodeInfo.node_id);
|
|
3469
4172
|
} catch (error) {
|
|
@@ -3490,18 +4193,17 @@ async function runNodeStatusCommand(config, options) {
|
|
|
3490
4193
|
(channel) => channel.state?.state_name === ChannelState2.ChannelReady
|
|
3491
4194
|
);
|
|
3492
4195
|
channelsReady = readyChannels.length;
|
|
4196
|
+
pendingChannelCount = Math.max(channelsTotal - channelsReady, 0);
|
|
3493
4197
|
const liquidity = summarizeChannelLiquidity(readyChannels);
|
|
3494
4198
|
canSend = liquidity.canSend;
|
|
3495
4199
|
canReceive = liquidity.canReceive;
|
|
3496
4200
|
localCkb = liquidity.localCkb;
|
|
3497
4201
|
remoteCkb = liquidity.remoteCkb;
|
|
3498
4202
|
fundingAddress = scriptToAddress(nodeInfo.default_funding_lock_script, config.network);
|
|
4203
|
+
fundingLockScript = nodeInfo.default_funding_lock_script;
|
|
3499
4204
|
if (config.ckbRpcUrl) {
|
|
3500
4205
|
try {
|
|
3501
|
-
const fundingBalance = await getLockBalanceShannons(
|
|
3502
|
-
config.ckbRpcUrl,
|
|
3503
|
-
nodeInfo.default_funding_lock_script
|
|
3504
|
-
);
|
|
4206
|
+
const fundingBalance = await getLockBalanceShannons(config.ckbRpcUrl, fundingLockScript);
|
|
3505
4207
|
fundingCkb = Number(fundingBalance) / 1e8;
|
|
3506
4208
|
} catch (error) {
|
|
3507
4209
|
fundingBalanceError = error instanceof Error ? error.message : "Failed to query CKB balance for funding address";
|
|
@@ -3532,18 +4234,24 @@ async function runNodeStatusCommand(config, options) {
|
|
|
3532
4234
|
rpcTarget: resolvedRpc.target,
|
|
3533
4235
|
resolvedRpcUrl: resolvedRpc.url,
|
|
3534
4236
|
nodeId,
|
|
4237
|
+
addresses,
|
|
4238
|
+
chainHash,
|
|
4239
|
+
version,
|
|
3535
4240
|
peerId,
|
|
4241
|
+
peersCount,
|
|
3536
4242
|
peerIdError,
|
|
3537
4243
|
multiaddr,
|
|
3538
4244
|
multiaddrError,
|
|
3539
4245
|
multiaddrInferred,
|
|
4246
|
+
fundingLockScript,
|
|
3540
4247
|
checks: {
|
|
3541
4248
|
binary: {
|
|
3542
4249
|
path: binaryInfo.path,
|
|
3543
4250
|
ready: binaryInfo.ready,
|
|
3544
4251
|
version: binaryInfo.version,
|
|
3545
|
-
source:
|
|
3546
|
-
managedPath:
|
|
4252
|
+
source: resolvedBinary.source,
|
|
4253
|
+
managedPath: resolvedBinary.managedPath,
|
|
4254
|
+
resolvedPath: resolvedBinary.binaryPath
|
|
3547
4255
|
},
|
|
3548
4256
|
config: {
|
|
3549
4257
|
path: config.configPath,
|
|
@@ -3561,6 +4269,7 @@ async function runNodeStatusCommand(config, options) {
|
|
|
3561
4269
|
channels: {
|
|
3562
4270
|
total: channelsTotal,
|
|
3563
4271
|
ready: channelsReady,
|
|
4272
|
+
pending: pendingChannelCount,
|
|
3564
4273
|
canSend,
|
|
3565
4274
|
canReceive
|
|
3566
4275
|
}
|
|
@@ -3587,6 +4296,12 @@ async function runNodeStatusCommand(config, options) {
|
|
|
3587
4296
|
console.log(`\u2705 Node is running (PID: ${output.pid})`);
|
|
3588
4297
|
if (output.rpcResponsive) {
|
|
3589
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
|
+
}
|
|
3590
4305
|
if (output.peerId) {
|
|
3591
4306
|
console.log(` Peer ID: ${String(output.peerId)}`);
|
|
3592
4307
|
} else if (output.peerIdError) {
|
|
@@ -3602,6 +4317,12 @@ async function runNodeStatusCommand(config, options) {
|
|
|
3602
4317
|
} else {
|
|
3603
4318
|
console.log(" Multiaddr: unavailable");
|
|
3604
4319
|
}
|
|
4320
|
+
if (output.addresses.length > 0) {
|
|
4321
|
+
console.log(" Addresses:");
|
|
4322
|
+
for (const address of output.addresses) {
|
|
4323
|
+
console.log(` - ${address}`);
|
|
4324
|
+
}
|
|
4325
|
+
}
|
|
3605
4326
|
} else {
|
|
3606
4327
|
console.log(" \u26A0\uFE0F RPC not responding");
|
|
3607
4328
|
}
|
|
@@ -3613,11 +4334,17 @@ async function runNodeStatusCommand(config, options) {
|
|
|
3613
4334
|
console.log("");
|
|
3614
4335
|
console.log("Diagnostics");
|
|
3615
4336
|
console.log(` Binary: ${output.checks.binary.ready ? "ready" : "missing"}`);
|
|
4337
|
+
console.log(` Binary Path: ${output.checks.binary.resolvedPath}`);
|
|
3616
4338
|
console.log(` Config: ${output.checks.config.exists ? "present" : "missing"}`);
|
|
3617
4339
|
console.log(` RPC: ${output.checks.node.rpcReachable ? "reachable" : "unreachable"}`);
|
|
3618
4340
|
console.log(
|
|
3619
|
-
` 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`
|
|
3620
4342
|
);
|
|
4343
|
+
if (output.rpcResponsive) {
|
|
4344
|
+
console.log(` Peers: ${output.peersCount}`);
|
|
4345
|
+
} else {
|
|
4346
|
+
console.log(" Peers: unavailable");
|
|
4347
|
+
}
|
|
3621
4348
|
console.log(` Can Send: ${output.checks.channels.canSend ? "yes" : "no"}`);
|
|
3622
4349
|
console.log(` Can Receive: ${output.checks.channels.canReceive ? "yes" : "no"}`);
|
|
3623
4350
|
console.log(` Recommendation:${output.recommendation}`);
|
|
@@ -3721,67 +4448,351 @@ async function runNodeReadyCommand(config, options) {
|
|
|
3721
4448
|
}
|
|
3722
4449
|
}
|
|
3723
4450
|
|
|
3724
|
-
// src/
|
|
3725
|
-
function
|
|
3726
|
-
const
|
|
3727
|
-
|
|
3728
|
-
|
|
3729
|
-
|
|
3730
|
-
|
|
3731
|
-
|
|
3732
|
-
|
|
3733
|
-
|
|
3734
|
-
|
|
3735
|
-
|
|
4451
|
+
// src/lib/node-stop.ts
|
|
4452
|
+
async function runNodeStopCommand(config, options) {
|
|
4453
|
+
const json = Boolean(options.json);
|
|
4454
|
+
const runtimeMeta = readRuntimeMeta(config.dataDir);
|
|
4455
|
+
const runtimePid = readRuntimePid(config.dataDir);
|
|
4456
|
+
if (runtimeMeta?.daemon && runtimePid && isProcessRunning(runtimePid)) {
|
|
4457
|
+
stopRuntimeDaemonFromNode({ dataDir: config.dataDir, rpcUrl: config.rpcUrl });
|
|
4458
|
+
}
|
|
4459
|
+
removeRuntimeFiles(config.dataDir);
|
|
4460
|
+
const pid = readPidFile(config.dataDir);
|
|
4461
|
+
if (!pid) {
|
|
4462
|
+
if (json) {
|
|
4463
|
+
printJsonError({
|
|
4464
|
+
code: "NODE_NOT_RUNNING",
|
|
4465
|
+
message: "No PID file found. Node may not be running.",
|
|
4466
|
+
recoverable: true,
|
|
4467
|
+
suggestion: "Run `fiber-pay node start` first if you intend to stop a node."
|
|
4468
|
+
});
|
|
4469
|
+
} else {
|
|
4470
|
+
console.log("\u274C No PID file found. Node may not be running.");
|
|
3736
4471
|
}
|
|
3737
|
-
|
|
3738
|
-
|
|
3739
|
-
|
|
3740
|
-
|
|
3741
|
-
|
|
3742
|
-
|
|
3743
|
-
|
|
3744
|
-
|
|
3745
|
-
|
|
3746
|
-
}
|
|
3747
|
-
}
|
|
3748
|
-
|
|
3749
|
-
}
|
|
3750
|
-
process.exit(1);
|
|
4472
|
+
process.exit(1);
|
|
4473
|
+
}
|
|
4474
|
+
if (!isProcessRunning(pid)) {
|
|
4475
|
+
if (json) {
|
|
4476
|
+
printJsonError({
|
|
4477
|
+
code: "NODE_NOT_RUNNING",
|
|
4478
|
+
message: `Process ${pid} is not running. Cleaning up PID file.`,
|
|
4479
|
+
recoverable: true,
|
|
4480
|
+
suggestion: "Start the node again if needed; stale PID has been cleaned.",
|
|
4481
|
+
details: { pid, stalePidFileCleaned: true }
|
|
4482
|
+
});
|
|
4483
|
+
} else {
|
|
4484
|
+
console.log(`\u274C Process ${pid} is not running. Cleaning up PID file.`);
|
|
3751
4485
|
}
|
|
3752
|
-
|
|
3753
|
-
|
|
3754
|
-
|
|
3755
|
-
|
|
3756
|
-
|
|
3757
|
-
|
|
3758
|
-
|
|
3759
|
-
|
|
3760
|
-
|
|
3761
|
-
|
|
3762
|
-
|
|
4486
|
+
removePidFile(config.dataDir);
|
|
4487
|
+
process.exit(1);
|
|
4488
|
+
}
|
|
4489
|
+
if (!json) {
|
|
4490
|
+
console.log(`\u{1F6D1} Stopping node (PID: ${pid})...`);
|
|
4491
|
+
}
|
|
4492
|
+
process.kill(pid, "SIGTERM");
|
|
4493
|
+
let attempts = 0;
|
|
4494
|
+
while (isProcessRunning(pid) && attempts < 30) {
|
|
4495
|
+
await new Promise((resolve2) => setTimeout(resolve2, 100));
|
|
4496
|
+
attempts++;
|
|
4497
|
+
}
|
|
4498
|
+
if (isProcessRunning(pid)) {
|
|
4499
|
+
process.kill(pid, "SIGKILL");
|
|
4500
|
+
}
|
|
4501
|
+
removePidFile(config.dataDir);
|
|
4502
|
+
if (json) {
|
|
4503
|
+
printJsonSuccess({ pid, stopped: true });
|
|
4504
|
+
} else {
|
|
4505
|
+
console.log("\u2705 Node stopped.");
|
|
4506
|
+
}
|
|
4507
|
+
}
|
|
4508
|
+
|
|
4509
|
+
// src/lib/node-upgrade.ts
|
|
4510
|
+
import { BinaryManager as BinaryManager3, MigrationManager as MigrationManager2 } from "@fiber-pay/node";
|
|
4511
|
+
async function runNodeUpgradeCommand(config, options) {
|
|
4512
|
+
const json = Boolean(options.json);
|
|
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);
|
|
4532
|
+
const pid = readPidFile(config.dataDir);
|
|
4533
|
+
if (pid && isProcessRunning(pid)) {
|
|
4534
|
+
const msg = "The Fiber node is currently running. Stop it before upgrading.";
|
|
4535
|
+
if (json) {
|
|
4536
|
+
printJsonError({
|
|
4537
|
+
code: "NODE_RUNNING",
|
|
4538
|
+
message: msg,
|
|
4539
|
+
recoverable: true,
|
|
4540
|
+
suggestion: "Run `fiber-pay node stop` first, then retry the upgrade."
|
|
4541
|
+
});
|
|
4542
|
+
} else {
|
|
4543
|
+
console.error(`\u274C ${msg}`);
|
|
4544
|
+
console.log(" Run: fiber-pay node stop");
|
|
4545
|
+
}
|
|
4546
|
+
process.exit(1);
|
|
4547
|
+
}
|
|
4548
|
+
let targetTag;
|
|
4549
|
+
if (options.version) {
|
|
4550
|
+
targetTag = binaryManager.normalizeTag(options.version);
|
|
4551
|
+
} else {
|
|
4552
|
+
if (!json) console.log("\u{1F50D} Resolving latest Fiber release...");
|
|
4553
|
+
targetTag = await binaryManager.getLatestTag();
|
|
4554
|
+
}
|
|
4555
|
+
if (!json) console.log(`\u{1F4E6} Target version: ${targetTag}`);
|
|
4556
|
+
const currentInfo = await binaryManager.getBinaryInfo();
|
|
4557
|
+
const targetVersion = targetTag.startsWith("v") ? targetTag.slice(1) : targetTag;
|
|
4558
|
+
const storePath = MigrationManager2.resolveStorePath(config.dataDir);
|
|
4559
|
+
const migrateBinaryPath = binaryManager.getMigrateBinaryPath();
|
|
4560
|
+
let migrationCheck = null;
|
|
4561
|
+
const storeExists = MigrationManager2.storeExists(config.dataDir);
|
|
4562
|
+
if (!json && storeExists) {
|
|
4563
|
+
console.log("\u{1F4C2} Existing store detected.");
|
|
4564
|
+
}
|
|
4565
|
+
if (currentInfo.ready && currentInfo.version === targetVersion && !options.forceMigrate) {
|
|
4566
|
+
if (storeExists) {
|
|
4567
|
+
migrationCheck = await runMigrationAndReport({
|
|
4568
|
+
migrateBinaryPath,
|
|
4569
|
+
storePath,
|
|
4570
|
+
json,
|
|
4571
|
+
checkOnly: Boolean(options.checkOnly),
|
|
4572
|
+
targetVersion,
|
|
4573
|
+
backup: options.backup !== false,
|
|
4574
|
+
forceMigrateAttempt: false
|
|
4575
|
+
});
|
|
4576
|
+
}
|
|
4577
|
+
const msg = migrationCheck ? `Already installed ${targetTag}. Store compatibility checked.` : `Already installed ${targetTag}. Use --force-migrate to run migration flow anyway.`;
|
|
4578
|
+
if (json) {
|
|
4579
|
+
printJsonSuccess({
|
|
4580
|
+
action: "none",
|
|
4581
|
+
currentVersion: currentInfo.version,
|
|
4582
|
+
targetVersion,
|
|
4583
|
+
message: msg,
|
|
4584
|
+
migration: migrationCheck
|
|
4585
|
+
});
|
|
4586
|
+
} else {
|
|
4587
|
+
console.log(`\u2705 ${msg}`);
|
|
4588
|
+
}
|
|
4589
|
+
return;
|
|
4590
|
+
}
|
|
4591
|
+
const versionMatches = currentInfo.ready && currentInfo.version === targetVersion;
|
|
4592
|
+
const shouldDownload = !versionMatches;
|
|
4593
|
+
if (!json && currentInfo.ready) {
|
|
4594
|
+
console.log(` Current version: v${currentInfo.version}`);
|
|
4595
|
+
}
|
|
4596
|
+
if (shouldDownload) {
|
|
4597
|
+
if (!json && storeExists) {
|
|
4598
|
+
console.log("\u{1F4C2} Existing store detected, will check migration after download.");
|
|
4599
|
+
}
|
|
4600
|
+
if (!json) console.log("\u2B07\uFE0F Downloading new binary...");
|
|
4601
|
+
const showProgress2 = (progress) => {
|
|
4602
|
+
if (!json) {
|
|
4603
|
+
const percent = progress.percent !== void 0 ? ` (${progress.percent}%)` : "";
|
|
4604
|
+
process.stdout.write(`\r [${progress.phase}]${percent} ${progress.message}`.padEnd(80));
|
|
4605
|
+
if (progress.phase === "installing") console.log();
|
|
3763
4606
|
}
|
|
3764
|
-
|
|
3765
|
-
|
|
4607
|
+
};
|
|
4608
|
+
await binaryManager.download({
|
|
4609
|
+
version: targetTag,
|
|
4610
|
+
force: true,
|
|
4611
|
+
onProgress: showProgress2
|
|
4612
|
+
});
|
|
4613
|
+
} else if (!json && options.forceMigrate) {
|
|
4614
|
+
console.log("\u23ED\uFE0F Skipping binary download: target version is already installed.");
|
|
4615
|
+
console.log("\u{1F501} --force-migrate enabled: attempting migration flow on existing binaries.");
|
|
4616
|
+
}
|
|
4617
|
+
if (storeExists) {
|
|
4618
|
+
migrationCheck = await runMigrationAndReport({
|
|
4619
|
+
migrateBinaryPath,
|
|
4620
|
+
storePath,
|
|
4621
|
+
json,
|
|
4622
|
+
checkOnly: Boolean(options.checkOnly),
|
|
4623
|
+
targetVersion,
|
|
4624
|
+
backup: options.backup !== false,
|
|
4625
|
+
forceMigrateAttempt: Boolean(options.forceMigrate)
|
|
4626
|
+
});
|
|
4627
|
+
}
|
|
4628
|
+
const newInfo = await binaryManager.getBinaryInfo();
|
|
4629
|
+
if (json) {
|
|
4630
|
+
printJsonSuccess({
|
|
4631
|
+
action: "upgraded",
|
|
4632
|
+
previousVersion: currentInfo.ready ? currentInfo.version : null,
|
|
4633
|
+
currentVersion: newInfo.version,
|
|
4634
|
+
binaryPath: newInfo.path,
|
|
4635
|
+
migrateBinaryPath,
|
|
4636
|
+
migration: migrationCheck
|
|
4637
|
+
});
|
|
4638
|
+
} else {
|
|
4639
|
+
console.log("\n\u2705 Upgrade complete!");
|
|
4640
|
+
console.log(` Version: v${newInfo.version}`);
|
|
4641
|
+
console.log(` Binary: ${newInfo.path}`);
|
|
4642
|
+
console.log("\n Start the node with: fiber-pay node start");
|
|
4643
|
+
}
|
|
4644
|
+
}
|
|
4645
|
+
async function runMigrationAndReport(opts) {
|
|
4646
|
+
const {
|
|
4647
|
+
migrateBinaryPath,
|
|
4648
|
+
storePath,
|
|
4649
|
+
json,
|
|
4650
|
+
checkOnly,
|
|
4651
|
+
targetVersion,
|
|
4652
|
+
backup,
|
|
4653
|
+
forceMigrateAttempt
|
|
4654
|
+
} = opts;
|
|
4655
|
+
let migrationManager;
|
|
4656
|
+
try {
|
|
4657
|
+
migrationManager = new MigrationManager2(migrateBinaryPath);
|
|
4658
|
+
} catch (err) {
|
|
4659
|
+
const msg = err instanceof Error ? err.message : "fnn-migrate binary not available";
|
|
4660
|
+
if (json) {
|
|
4661
|
+
printJsonError({
|
|
4662
|
+
code: "MIGRATION_TOOL_MISSING",
|
|
4663
|
+
message: msg,
|
|
4664
|
+
recoverable: true,
|
|
4665
|
+
suggestion: "Run `fiber-pay node upgrade` to reinstall binaries, then retry `fiber-pay node upgrade --force-migrate`."
|
|
4666
|
+
});
|
|
4667
|
+
} else {
|
|
4668
|
+
console.error(`
|
|
4669
|
+
\u26A0\uFE0F ${msg}`);
|
|
4670
|
+
console.log(
|
|
4671
|
+
" Run `fiber-pay node upgrade` to reinstall binaries, then retry `fiber-pay node upgrade --force-migrate`."
|
|
4672
|
+
);
|
|
3766
4673
|
}
|
|
3767
|
-
|
|
3768
|
-
|
|
4674
|
+
process.exit(1);
|
|
4675
|
+
}
|
|
4676
|
+
if (!json) console.log("\u{1F50D} Checking store compatibility...");
|
|
4677
|
+
let migrationCheck;
|
|
4678
|
+
try {
|
|
4679
|
+
migrationCheck = await migrationManager.check(storePath);
|
|
4680
|
+
} catch (checkErr) {
|
|
4681
|
+
const msg = checkErr instanceof Error ? checkErr.message : String(checkErr);
|
|
4682
|
+
if (json) {
|
|
4683
|
+
printJsonError({
|
|
4684
|
+
code: "MIGRATION_TOOL_MISSING",
|
|
4685
|
+
message: `Migration check failed: ${msg}`,
|
|
4686
|
+
recoverable: true,
|
|
4687
|
+
suggestion: "Run `fiber-pay node upgrade` to reinstall binaries, then retry `fiber-pay node upgrade --force-migrate`."
|
|
4688
|
+
});
|
|
4689
|
+
} else {
|
|
4690
|
+
console.error(`
|
|
4691
|
+
\u26A0\uFE0F Migration check failed: ${msg}`);
|
|
4692
|
+
console.log(
|
|
4693
|
+
" Run `fiber-pay node upgrade` to reinstall binaries, then retry `fiber-pay node upgrade --force-migrate`."
|
|
4694
|
+
);
|
|
3769
4695
|
}
|
|
3770
|
-
process.
|
|
3771
|
-
|
|
3772
|
-
|
|
3773
|
-
|
|
3774
|
-
|
|
4696
|
+
process.exit(1);
|
|
4697
|
+
}
|
|
4698
|
+
if (checkOnly) {
|
|
4699
|
+
const normalizedCheck = normalizeMigrationCheck(migrationCheck);
|
|
4700
|
+
if (json) {
|
|
4701
|
+
printJsonSuccess({
|
|
4702
|
+
action: "check-only",
|
|
4703
|
+
targetVersion,
|
|
4704
|
+
migration: normalizedCheck
|
|
4705
|
+
});
|
|
4706
|
+
} else {
|
|
4707
|
+
console.log(`
|
|
4708
|
+
\u{1F4CB} Migration status: ${normalizedCheck.message}`);
|
|
3775
4709
|
}
|
|
3776
|
-
|
|
3777
|
-
|
|
4710
|
+
process.exit(0);
|
|
4711
|
+
}
|
|
4712
|
+
if (!migrationCheck.needed) {
|
|
4713
|
+
if (!json) console.log(" Store is compatible, no migration needed.");
|
|
4714
|
+
return normalizeMigrationCheck(migrationCheck);
|
|
4715
|
+
}
|
|
4716
|
+
if (!migrationCheck.valid && !forceMigrateAttempt) {
|
|
4717
|
+
const normalizedMessage = replaceRawMigrateHint(migrationCheck.message);
|
|
4718
|
+
if (json) {
|
|
4719
|
+
printJsonError({
|
|
4720
|
+
code: "MIGRATION_INCOMPATIBLE",
|
|
4721
|
+
message: normalizedMessage,
|
|
4722
|
+
recoverable: false,
|
|
4723
|
+
suggestion: `Back up your store first (directory: "${storePath}"). Then run \`fiber-pay node upgrade --force-migrate\`. If it still fails, close all channels with the old fnn version, remove the store, and restart with a fresh store. If you attempted migration with backup enabled, you can roll back by restoring the backup directory.`,
|
|
4724
|
+
details: {
|
|
4725
|
+
storePath,
|
|
4726
|
+
migrationCheck: {
|
|
4727
|
+
...migrationCheck,
|
|
4728
|
+
message: normalizedMessage
|
|
4729
|
+
}
|
|
4730
|
+
}
|
|
4731
|
+
});
|
|
4732
|
+
} else {
|
|
4733
|
+
console.error("\n\u274C Store migration is not possible automatically.");
|
|
4734
|
+
console.log(normalizedMessage);
|
|
4735
|
+
console.log(` 1) Back up store directory: ${storePath}`);
|
|
4736
|
+
console.log(" 2) Try: fiber-pay node upgrade --force-migrate");
|
|
4737
|
+
console.log(
|
|
4738
|
+
" 3) If it still fails, close channels on old fnn, remove store, then restart."
|
|
4739
|
+
);
|
|
4740
|
+
console.log(" 4) If migration created a backup, you can roll back by restoring it.");
|
|
3778
4741
|
}
|
|
3779
|
-
|
|
4742
|
+
process.exit(1);
|
|
4743
|
+
}
|
|
4744
|
+
if (!migrationCheck.valid && !json) {
|
|
4745
|
+
console.log("\u26A0\uFE0F Store check reported incompatibility, but --force-migrate is set.");
|
|
4746
|
+
console.log(" Attempting migration anyway with backup enabled (unless --no-backup).");
|
|
4747
|
+
}
|
|
4748
|
+
if (!json) console.log("\u{1F504} Running database migration...");
|
|
4749
|
+
const result = await migrationManager.migrate({
|
|
4750
|
+
storePath,
|
|
4751
|
+
backup,
|
|
4752
|
+
force: forceMigrateAttempt
|
|
4753
|
+
});
|
|
4754
|
+
if (!result.success) {
|
|
3780
4755
|
if (json) {
|
|
3781
|
-
|
|
4756
|
+
printJsonError({
|
|
4757
|
+
code: "MIGRATION_FAILED",
|
|
4758
|
+
message: result.message,
|
|
4759
|
+
recoverable: !!result.backupPath,
|
|
4760
|
+
suggestion: result.backupPath ? `To roll back, delete the current store at "${storePath}" and restore the backup from "${result.backupPath}".` : "Re-download the previous version or start fresh.",
|
|
4761
|
+
details: { output: result.output, backupPath: result.backupPath }
|
|
4762
|
+
});
|
|
3782
4763
|
} else {
|
|
3783
|
-
console.
|
|
4764
|
+
console.error("\n\u274C Migration failed.");
|
|
4765
|
+
console.log(result.message);
|
|
4766
|
+
}
|
|
4767
|
+
process.exit(1);
|
|
4768
|
+
}
|
|
4769
|
+
if (!json) {
|
|
4770
|
+
console.log(`\u2705 ${result.message}`);
|
|
4771
|
+
if (result.backupPath) {
|
|
4772
|
+
console.log(` Backup: ${result.backupPath}`);
|
|
4773
|
+
}
|
|
4774
|
+
}
|
|
4775
|
+
try {
|
|
4776
|
+
const postCheck = await migrationManager.check(storePath);
|
|
4777
|
+
return normalizeMigrationCheck(postCheck);
|
|
4778
|
+
} catch (err) {
|
|
4779
|
+
if (!json) {
|
|
4780
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
4781
|
+
console.error("\u26A0\uFE0F Post-migration check failed; final migration status may be stale.");
|
|
4782
|
+
console.error(` ${message}`);
|
|
3784
4783
|
}
|
|
4784
|
+
return normalizeMigrationCheck(migrationCheck);
|
|
4785
|
+
}
|
|
4786
|
+
}
|
|
4787
|
+
|
|
4788
|
+
// src/commands/node.ts
|
|
4789
|
+
function createNodeCommand(config) {
|
|
4790
|
+
const node = new Command8("node").description("Node management");
|
|
4791
|
+
node.command("start").option("--daemon", "Start node in detached background mode (node + runtime)").option("--runtime-proxy-listen <host:port>", "Runtime monitor proxy listen address").option("--event-stream <format>", "Event stream format for --json mode (jsonl)", "jsonl").option("--quiet-fnn", "Do not mirror fnn stdout/stderr to console; keep file persistence").option("--json").action(async (options) => {
|
|
4792
|
+
await runNodeStartCommand(config, options);
|
|
4793
|
+
});
|
|
4794
|
+
node.command("stop").option("--json").action(async (options) => {
|
|
4795
|
+
await runNodeStopCommand(config, options);
|
|
3785
4796
|
});
|
|
3786
4797
|
node.command("status").option("--json").action(async (options) => {
|
|
3787
4798
|
await runNodeStatusCommand(config, options);
|
|
@@ -3789,34 +4800,17 @@ function createNodeCommand(config) {
|
|
|
3789
4800
|
node.command("ready").description("Agent-oriented readiness summary for automation").option("--json").action(async (options) => {
|
|
3790
4801
|
await runNodeReadyCommand(config, options);
|
|
3791
4802
|
});
|
|
3792
|
-
node.command("
|
|
3793
|
-
|
|
3794
|
-
|
|
3795
|
-
|
|
3796
|
-
|
|
3797
|
-
const output = {
|
|
3798
|
-
nodeId: nodeInfo.node_id,
|
|
3799
|
-
peerId,
|
|
3800
|
-
addresses: nodeInfo.addresses,
|
|
3801
|
-
chainHash: nodeInfo.chain_hash,
|
|
3802
|
-
fundingAddress,
|
|
3803
|
-
fundingLockScript: nodeInfo.default_funding_lock_script,
|
|
3804
|
-
version: nodeInfo.version,
|
|
3805
|
-
channelCount: parseInt(nodeInfo.channel_count, 16),
|
|
3806
|
-
pendingChannelCount: parseInt(nodeInfo.pending_channel_count, 16),
|
|
3807
|
-
peersCount: parseInt(nodeInfo.peers_count, 16)
|
|
3808
|
-
};
|
|
3809
|
-
if (options.json) {
|
|
3810
|
-
printJsonSuccess(output);
|
|
3811
|
-
} else {
|
|
3812
|
-
printNodeInfoHuman(output);
|
|
3813
|
-
}
|
|
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(
|
|
4804
|
+
"--force-migrate",
|
|
4805
|
+
"Force migration attempt even when compatibility check reports incompatible data"
|
|
4806
|
+
).option("--json").action(async (options) => {
|
|
4807
|
+
await runNodeUpgradeCommand(config, options);
|
|
3814
4808
|
});
|
|
3815
4809
|
return node;
|
|
3816
4810
|
}
|
|
3817
4811
|
|
|
3818
4812
|
// src/commands/payment.ts
|
|
3819
|
-
import { ckbToShannons as
|
|
4813
|
+
import { ckbToShannons as ckbToShannons4, shannonsToCkb as shannonsToCkb5 } from "@fiber-pay/sdk";
|
|
3820
4814
|
import { Command as Command9 } from "commander";
|
|
3821
4815
|
function createPaymentCommand(config) {
|
|
3822
4816
|
const payment = new Command9("payment").description("Payment lifecycle and status commands");
|
|
@@ -3858,9 +4852,9 @@ function createPaymentCommand(config) {
|
|
|
3858
4852
|
const paymentParams = {
|
|
3859
4853
|
invoice,
|
|
3860
4854
|
target_pubkey: recipientNodeId,
|
|
3861
|
-
amount: amountCkb ?
|
|
4855
|
+
amount: amountCkb ? ckbToShannons4(amountCkb) : void 0,
|
|
3862
4856
|
keysend: recipientNodeId ? true : void 0,
|
|
3863
|
-
max_fee_amount: maxFeeCkb ?
|
|
4857
|
+
max_fee_amount: maxFeeCkb ? ckbToShannons4(maxFeeCkb) : void 0
|
|
3864
4858
|
};
|
|
3865
4859
|
const endpoint = resolveRpcEndpoint(config);
|
|
3866
4860
|
if (endpoint.target === "runtime-proxy") {
|
|
@@ -3902,7 +4896,7 @@ function createPaymentCommand(config) {
|
|
|
3902
4896
|
const payload = {
|
|
3903
4897
|
paymentHash: result.payment_hash,
|
|
3904
4898
|
status: result.status === "Success" ? "success" : result.status === "Failed" ? "failed" : "pending",
|
|
3905
|
-
feeCkb:
|
|
4899
|
+
feeCkb: shannonsToCkb5(result.fee),
|
|
3906
4900
|
failureReason: result.failed_error
|
|
3907
4901
|
};
|
|
3908
4902
|
if (json) {
|
|
@@ -3917,6 +4911,7 @@ function createPaymentCommand(config) {
|
|
|
3917
4911
|
}
|
|
3918
4912
|
}
|
|
3919
4913
|
});
|
|
4914
|
+
registerPaymentRebalanceCommand(payment, config);
|
|
3920
4915
|
payment.command("get").argument("<paymentHash>").option("--json").action(async (paymentHash, options) => {
|
|
3921
4916
|
const rpc = await createReadyRpcClient(config);
|
|
3922
4917
|
const result = await rpc.getPayment({ payment_hash: paymentHash });
|
|
@@ -4065,7 +5060,7 @@ function createPaymentCommand(config) {
|
|
|
4065
5060
|
process.exit(1);
|
|
4066
5061
|
}
|
|
4067
5062
|
const hopsInfo = pubkeys.map((pubkey) => ({ pubkey }));
|
|
4068
|
-
const amount = options.amount ?
|
|
5063
|
+
const amount = options.amount ? ckbToShannons4(parseFloat(options.amount)) : void 0;
|
|
4069
5064
|
const result = await rpc.buildRouter({
|
|
4070
5065
|
hops_info: hopsInfo,
|
|
4071
5066
|
amount
|
|
@@ -4081,7 +5076,7 @@ function createPaymentCommand(config) {
|
|
|
4081
5076
|
console.log(
|
|
4082
5077
|
` Outpoint: ${hop.channel_outpoint.tx_hash}:${hop.channel_outpoint.index}`
|
|
4083
5078
|
);
|
|
4084
|
-
console.log(` Amount: ${
|
|
5079
|
+
console.log(` Amount: ${shannonsToCkb5(hop.amount_received)} CKB`);
|
|
4085
5080
|
console.log(` Expiry: ${hop.incoming_tlc_expiry}`);
|
|
4086
5081
|
}
|
|
4087
5082
|
}
|
|
@@ -4089,7 +5084,7 @@ function createPaymentCommand(config) {
|
|
|
4089
5084
|
payment.command("send-route").description("Send a payment using a pre-built route from `payment route`").requiredOption(
|
|
4090
5085
|
"--router <json>",
|
|
4091
5086
|
"JSON array of router hops (output of `payment route --json`)"
|
|
4092
|
-
).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) => {
|
|
4093
5088
|
const rpc = await createReadyRpcClient(config);
|
|
4094
5089
|
const json = Boolean(options.json);
|
|
4095
5090
|
let router;
|
|
@@ -4114,12 +5109,13 @@ function createPaymentCommand(config) {
|
|
|
4114
5109
|
invoice: options.invoice,
|
|
4115
5110
|
payment_hash: options.paymentHash,
|
|
4116
5111
|
keysend: options.keysend ? true : void 0,
|
|
5112
|
+
allow_self_payment: options.allowSelfPayment ? true : void 0,
|
|
4117
5113
|
dry_run: options.dryRun ? true : void 0
|
|
4118
5114
|
});
|
|
4119
5115
|
const payload = {
|
|
4120
5116
|
paymentHash: result.payment_hash,
|
|
4121
5117
|
status: result.status === "Success" ? "success" : result.status === "Failed" ? "failed" : "pending",
|
|
4122
|
-
feeCkb:
|
|
5118
|
+
feeCkb: shannonsToCkb5(result.fee),
|
|
4123
5119
|
failureReason: result.failed_error,
|
|
4124
5120
|
dryRun: Boolean(options.dryRun)
|
|
4125
5121
|
};
|
|
@@ -4143,7 +5139,7 @@ function getJobPaymentHash(job) {
|
|
|
4143
5139
|
}
|
|
4144
5140
|
function getJobFeeCkb(job) {
|
|
4145
5141
|
const result = job.result;
|
|
4146
|
-
return result?.fee ?
|
|
5142
|
+
return result?.fee ? shannonsToCkb5(result.fee) : 0;
|
|
4147
5143
|
}
|
|
4148
5144
|
function getJobFailure(job) {
|
|
4149
5145
|
const result = job.result;
|
|
@@ -4215,7 +5211,7 @@ function createPeerCommand(config) {
|
|
|
4215
5211
|
|
|
4216
5212
|
// src/commands/runtime.ts
|
|
4217
5213
|
import { spawn as spawn2 } from "child_process";
|
|
4218
|
-
import { resolve } from "path";
|
|
5214
|
+
import { join as join8, resolve } from "path";
|
|
4219
5215
|
import {
|
|
4220
5216
|
alertPriorityOrder,
|
|
4221
5217
|
formatRuntimeAlert as formatRuntimeAlert2,
|
|
@@ -4289,15 +5285,22 @@ function shouldPrintAlert(alert, filter) {
|
|
|
4289
5285
|
}
|
|
4290
5286
|
return true;
|
|
4291
5287
|
}
|
|
5288
|
+
function resolveRuntimeRecoveryListen(config) {
|
|
5289
|
+
const meta = readRuntimeMeta(config.dataDir);
|
|
5290
|
+
return meta?.proxyListen ?? config.runtimeProxyListen ?? "127.0.0.1:8229";
|
|
5291
|
+
}
|
|
4292
5292
|
function createRuntimeCommand(config) {
|
|
4293
5293
|
const runtime = new Command11("runtime").description("Polling monitor and alert runtime service");
|
|
4294
|
-
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(
|
|
4295
5295
|
"--log-min-priority <priority>",
|
|
4296
5296
|
"Minimum runtime log priority (critical|high|medium|low)"
|
|
4297
5297
|
).option("--log-type <types>", "Comma-separated runtime alert types to print").option("--json").action(async (options) => {
|
|
4298
5298
|
const asJson = Boolean(options.json);
|
|
4299
5299
|
const daemon = Boolean(options.daemon);
|
|
4300
5300
|
const isRuntimeChild = process.env.FIBER_RUNTIME_CHILD === "1";
|
|
5301
|
+
const runtimeListen = String(
|
|
5302
|
+
options.proxyListen ?? config.runtimeProxyListen ?? "127.0.0.1:8229"
|
|
5303
|
+
);
|
|
4301
5304
|
try {
|
|
4302
5305
|
const existingPid = readRuntimePid(config.dataDir);
|
|
4303
5306
|
if (existingPid && isProcessRunning(existingPid) && (!isRuntimeChild || existingPid !== process.pid)) {
|
|
@@ -4317,6 +5320,32 @@ function createRuntimeCommand(config) {
|
|
|
4317
5320
|
if (existingPid && !isProcessRunning(existingPid)) {
|
|
4318
5321
|
removeRuntimeFiles(config.dataDir);
|
|
4319
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
|
+
}
|
|
4320
5349
|
if (daemon && !isRuntimeChild) {
|
|
4321
5350
|
const childArgv = process.argv.filter((arg) => arg !== "--daemon");
|
|
4322
5351
|
const child = spawn2(process.execPath, childArgv.slice(1), {
|
|
@@ -4359,7 +5388,7 @@ function createRuntimeCommand(config) {
|
|
|
4359
5388
|
),
|
|
4360
5389
|
proxy: {
|
|
4361
5390
|
enabled: true,
|
|
4362
|
-
listen:
|
|
5391
|
+
listen: runtimeListen
|
|
4363
5392
|
},
|
|
4364
5393
|
storage: {
|
|
4365
5394
|
stateFilePath: options.stateFile ? resolve(String(options.stateFile)) : resolve(config.dataDir, "runtime-state.json"),
|
|
@@ -4370,9 +5399,21 @@ function createRuntimeCommand(config) {
|
|
|
4370
5399
|
dbPath: resolve(config.dataDir, "runtime-jobs.db")
|
|
4371
5400
|
}
|
|
4372
5401
|
};
|
|
4373
|
-
|
|
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
|
+
}
|
|
4374
5411
|
const alerts = [{ type: "stdout" }];
|
|
4375
|
-
|
|
5412
|
+
if (alertLogsBaseDir) {
|
|
5413
|
+
alerts.push({ type: "daily-file", baseLogsDir: alertLogsBaseDir });
|
|
5414
|
+
} else if (alertLogFile) {
|
|
5415
|
+
alerts.push({ type: "file", path: alertLogFile });
|
|
5416
|
+
}
|
|
4376
5417
|
if (options.webhook) {
|
|
4377
5418
|
alerts.push({ type: "webhook", url: String(options.webhook) });
|
|
4378
5419
|
}
|
|
@@ -4394,6 +5435,12 @@ function createRuntimeCommand(config) {
|
|
|
4394
5435
|
}
|
|
4395
5436
|
const runtime2 = await startRuntimeService2(runtimeConfig);
|
|
4396
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");
|
|
4397
5444
|
writeRuntimePid(config.dataDir, process.pid);
|
|
4398
5445
|
writeRuntimeMeta(config.dataDir, {
|
|
4399
5446
|
pid: process.pid,
|
|
@@ -4401,7 +5448,10 @@ function createRuntimeCommand(config) {
|
|
|
4401
5448
|
fiberRpcUrl: status.targetUrl,
|
|
4402
5449
|
proxyListen: status.proxyListen,
|
|
4403
5450
|
stateFilePath: runtimeConfig.storage?.stateFilePath,
|
|
4404
|
-
alertLogFilePath:
|
|
5451
|
+
alertLogFilePath: effectiveAlertLogPath,
|
|
5452
|
+
fnnStdoutLogPath: join8(todayLogDir, "fnn.stdout.log"),
|
|
5453
|
+
fnnStderrLogPath: join8(todayLogDir, "fnn.stderr.log"),
|
|
5454
|
+
logsBaseDir,
|
|
4405
5455
|
daemon: daemon || isRuntimeChild
|
|
4406
5456
|
});
|
|
4407
5457
|
runtime2.service.on("alert", (alert) => {
|
|
@@ -4420,14 +5470,16 @@ function createRuntimeCommand(config) {
|
|
|
4420
5470
|
fiberRpcUrl: status.targetUrl,
|
|
4421
5471
|
proxyListen: status.proxyListen,
|
|
4422
5472
|
stateFilePath: runtimeConfig.storage?.stateFilePath,
|
|
4423
|
-
alertLogFile
|
|
5473
|
+
alertLogFile: effectiveAlertLogPath,
|
|
5474
|
+
logsBaseDir
|
|
4424
5475
|
});
|
|
4425
5476
|
printJsonEvent("runtime_started", status);
|
|
4426
5477
|
} else {
|
|
4427
5478
|
console.log(`Fiber RPC: ${status.targetUrl}`);
|
|
4428
5479
|
console.log(`Proxy listen: ${status.proxyListen}`);
|
|
4429
5480
|
console.log(`State file: ${runtimeConfig.storage?.stateFilePath}`);
|
|
4430
|
-
console.log(`
|
|
5481
|
+
console.log(`Logs dir: ${logsBaseDir}`);
|
|
5482
|
+
console.log(`Alert log: ${effectiveAlertLogPath}`);
|
|
4431
5483
|
console.log("Runtime monitor is running. Press Ctrl+C to stop.");
|
|
4432
5484
|
}
|
|
4433
5485
|
const signal = await runtime2.waitForShutdownSignal();
|
|
@@ -4461,8 +5513,42 @@ function createRuntimeCommand(config) {
|
|
|
4461
5513
|
});
|
|
4462
5514
|
runtime.command("status").description("Show runtime process and health status").option("--json").action(async (options) => {
|
|
4463
5515
|
const asJson = Boolean(options.json);
|
|
4464
|
-
|
|
5516
|
+
let pid = readRuntimePid(config.dataDir);
|
|
4465
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
|
+
}
|
|
4466
5552
|
if (!pid) {
|
|
4467
5553
|
if (asJson) {
|
|
4468
5554
|
printJsonError({
|
|
@@ -4493,9 +5579,11 @@ function createRuntimeCommand(config) {
|
|
|
4493
5579
|
process.exit(1);
|
|
4494
5580
|
}
|
|
4495
5581
|
let rpcStatus;
|
|
4496
|
-
if (meta?.proxyListen) {
|
|
5582
|
+
if (meta?.proxyListen ?? recoveryListen) {
|
|
4497
5583
|
try {
|
|
4498
|
-
const response = await fetch(
|
|
5584
|
+
const response = await fetch(
|
|
5585
|
+
`http://${meta?.proxyListen ?? recoveryListen}/monitor/status`
|
|
5586
|
+
);
|
|
4499
5587
|
if (response.ok) {
|
|
4500
5588
|
rpcStatus = await response.json();
|
|
4501
5589
|
}
|
|
@@ -4523,7 +5611,41 @@ function createRuntimeCommand(config) {
|
|
|
4523
5611
|
});
|
|
4524
5612
|
runtime.command("stop").description("Stop runtime process by PID").option("--json").action(async (options) => {
|
|
4525
5613
|
const asJson = Boolean(options.json);
|
|
4526
|
-
|
|
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
|
+
}
|
|
4527
5649
|
if (!pid) {
|
|
4528
5650
|
if (asJson) {
|
|
4529
5651
|
printJsonError({
|
|
@@ -4552,14 +5674,19 @@ function createRuntimeCommand(config) {
|
|
|
4552
5674
|
}
|
|
4553
5675
|
process.exit(1);
|
|
4554
5676
|
}
|
|
4555
|
-
|
|
4556
|
-
|
|
4557
|
-
|
|
4558
|
-
|
|
4559
|
-
|
|
4560
|
-
|
|
4561
|
-
|
|
4562
|
-
|
|
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);
|
|
4563
5690
|
}
|
|
4564
5691
|
removeRuntimeFiles(config.dataDir);
|
|
4565
5692
|
if (asJson) {
|
|
@@ -4575,8 +5702,8 @@ function createRuntimeCommand(config) {
|
|
|
4575
5702
|
import { Command as Command12 } from "commander";
|
|
4576
5703
|
|
|
4577
5704
|
// src/lib/build-info.ts
|
|
4578
|
-
var CLI_VERSION = "0.1.0-rc.
|
|
4579
|
-
var CLI_COMMIT = "
|
|
5705
|
+
var CLI_VERSION = "0.1.0-rc.5";
|
|
5706
|
+
var CLI_COMMIT = "28cee07226e004d775ed747bbcbf61474f93c492";
|
|
4580
5707
|
|
|
4581
5708
|
// src/commands/version.ts
|
|
4582
5709
|
function createVersionCommand() {
|
|
@@ -4594,6 +5721,55 @@ function createVersionCommand() {
|
|
|
4594
5721
|
});
|
|
4595
5722
|
}
|
|
4596
5723
|
|
|
5724
|
+
// src/lib/argv.ts
|
|
5725
|
+
var GLOBAL_OPTIONS_WITH_VALUE = /* @__PURE__ */ new Set([
|
|
5726
|
+
"--profile",
|
|
5727
|
+
"--data-dir",
|
|
5728
|
+
"--rpc-url",
|
|
5729
|
+
"--network",
|
|
5730
|
+
"--key-password",
|
|
5731
|
+
"--binary-path"
|
|
5732
|
+
]);
|
|
5733
|
+
function isOptionToken(token) {
|
|
5734
|
+
return token.startsWith("-") && token !== "-";
|
|
5735
|
+
}
|
|
5736
|
+
function hasInlineValue(token) {
|
|
5737
|
+
return token.includes("=");
|
|
5738
|
+
}
|
|
5739
|
+
function getFirstPositional(argv) {
|
|
5740
|
+
for (let index = 2; index < argv.length; index++) {
|
|
5741
|
+
const token = argv[index];
|
|
5742
|
+
if (token === "--") {
|
|
5743
|
+
return argv[index + 1];
|
|
5744
|
+
}
|
|
5745
|
+
if (!isOptionToken(token)) {
|
|
5746
|
+
return token;
|
|
5747
|
+
}
|
|
5748
|
+
if (GLOBAL_OPTIONS_WITH_VALUE.has(token) && !hasInlineValue(token)) {
|
|
5749
|
+
const next = argv[index + 1];
|
|
5750
|
+
if (next && !isOptionToken(next)) {
|
|
5751
|
+
index++;
|
|
5752
|
+
}
|
|
5753
|
+
}
|
|
5754
|
+
}
|
|
5755
|
+
return void 0;
|
|
5756
|
+
}
|
|
5757
|
+
function hasTopLevelVersionFlag(argv) {
|
|
5758
|
+
for (let index = 2; index < argv.length; index++) {
|
|
5759
|
+
const token = argv[index];
|
|
5760
|
+
if (token === "--") {
|
|
5761
|
+
return false;
|
|
5762
|
+
}
|
|
5763
|
+
if (token === "--version" || token === "-v") {
|
|
5764
|
+
return true;
|
|
5765
|
+
}
|
|
5766
|
+
}
|
|
5767
|
+
return false;
|
|
5768
|
+
}
|
|
5769
|
+
function isTopLevelVersionRequest(argv) {
|
|
5770
|
+
return !getFirstPositional(argv) && hasTopLevelVersionFlag(argv);
|
|
5771
|
+
}
|
|
5772
|
+
|
|
4597
5773
|
// src/index.ts
|
|
4598
5774
|
function shouldOutputJson() {
|
|
4599
5775
|
return process.argv.includes("--json");
|
|
@@ -4635,6 +5811,14 @@ function applyGlobalOverrides(argv) {
|
|
|
4635
5811
|
}
|
|
4636
5812
|
break;
|
|
4637
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
|
+
}
|
|
4638
5822
|
case "--network": {
|
|
4639
5823
|
const value = getFlagValue(argv, index);
|
|
4640
5824
|
if (value) {
|
|
@@ -4674,7 +5858,7 @@ function applyGlobalOverrides(argv) {
|
|
|
4674
5858
|
}
|
|
4675
5859
|
if (!explicitDataDir && profileName) {
|
|
4676
5860
|
const homeDir = process.env.HOME ?? process.cwd();
|
|
4677
|
-
process.env.FIBER_DATA_DIR =
|
|
5861
|
+
process.env.FIBER_DATA_DIR = join9(homeDir, ".fiber-pay", "profiles", profileName);
|
|
4678
5862
|
}
|
|
4679
5863
|
}
|
|
4680
5864
|
function printFatal(error) {
|
|
@@ -4695,10 +5879,15 @@ function printFatal(error) {
|
|
|
4695
5879
|
}
|
|
4696
5880
|
}
|
|
4697
5881
|
async function main() {
|
|
4698
|
-
|
|
5882
|
+
const argv = process.argv;
|
|
5883
|
+
if (isTopLevelVersionRequest(argv)) {
|
|
5884
|
+
console.log(`${CLI_VERSION} (${CLI_COMMIT})`);
|
|
5885
|
+
return;
|
|
5886
|
+
}
|
|
5887
|
+
applyGlobalOverrides(argv);
|
|
4699
5888
|
const config = getEffectiveConfig(explicitFlags).config;
|
|
4700
5889
|
const program = new Command13();
|
|
4701
|
-
program.name("fiber-pay").description("AI Agent Payment SDK for CKB Lightning Network").
|
|
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();
|
|
4702
5891
|
program.exitOverride();
|
|
4703
5892
|
program.configureOutput({
|
|
4704
5893
|
writeOut: (str) => process.stdout.write(str),
|
|
@@ -4720,7 +5909,7 @@ async function main() {
|
|
|
4720
5909
|
program.addCommand(createConfigCommand(config));
|
|
4721
5910
|
program.addCommand(createRuntimeCommand(config));
|
|
4722
5911
|
program.addCommand(createVersionCommand());
|
|
4723
|
-
await program.parseAsync(
|
|
5912
|
+
await program.parseAsync(argv);
|
|
4724
5913
|
}
|
|
4725
5914
|
main().catch((error) => {
|
|
4726
5915
|
const commanderCode = error && typeof error === "object" && "code" in error ? String(error.code) : void 0;
|