@fiber-pay/cli 0.1.0-rc.4 → 0.1.0-rc.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.js +1341 -276
- package/dist/cli.js.map +1 -1
- package/error-codes.json +10 -0
- package/package.json +4 -4
package/dist/cli.js
CHANGED
|
@@ -1,29 +1,229 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// src/index.ts
|
|
4
|
-
import { join as
|
|
5
|
-
import { Command as
|
|
4
|
+
import { join as join9 } from "path";
|
|
5
|
+
import { Command as Command14 } 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,
|
|
18
169
|
shannonsToCkb,
|
|
19
170
|
toHex
|
|
20
171
|
} from "@fiber-pay/sdk";
|
|
172
|
+
var SHANNONS_PER_CKB = 100000000n;
|
|
21
173
|
function truncateMiddle(value, start = 10, end = 8) {
|
|
22
174
|
if (!value || value.length <= start + end + 3) {
|
|
23
175
|
return value;
|
|
24
176
|
}
|
|
25
177
|
return `${value.slice(0, start)}...${value.slice(-end)}`;
|
|
26
178
|
}
|
|
179
|
+
function sanitizeForTerminal(value) {
|
|
180
|
+
const input = String(value ?? "");
|
|
181
|
+
let output = "";
|
|
182
|
+
for (let i = 0; i < input.length; i++) {
|
|
183
|
+
const code = input.charCodeAt(i);
|
|
184
|
+
if (code === 27) {
|
|
185
|
+
i++;
|
|
186
|
+
if (i >= input.length) break;
|
|
187
|
+
if (input.charCodeAt(i) === 91) {
|
|
188
|
+
i++;
|
|
189
|
+
while (i < input.length) {
|
|
190
|
+
const csiCode = input.charCodeAt(i);
|
|
191
|
+
if (csiCode >= 64 && csiCode <= 126) {
|
|
192
|
+
break;
|
|
193
|
+
}
|
|
194
|
+
i++;
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
continue;
|
|
198
|
+
}
|
|
199
|
+
if (code === 9 || code === 10 || code === 13) {
|
|
200
|
+
output += " ";
|
|
201
|
+
continue;
|
|
202
|
+
}
|
|
203
|
+
if (code >= 0 && code <= 8 || code >= 11 && code <= 31) {
|
|
204
|
+
continue;
|
|
205
|
+
}
|
|
206
|
+
if (code >= 127 && code <= 159) {
|
|
207
|
+
continue;
|
|
208
|
+
}
|
|
209
|
+
output += input[i];
|
|
210
|
+
}
|
|
211
|
+
return output;
|
|
212
|
+
}
|
|
213
|
+
function formatShannonsAsCkb(shannons, fractionDigits = 8) {
|
|
214
|
+
const value = typeof shannons === "bigint" ? shannons : BigInt(shannons);
|
|
215
|
+
const sign = value < 0n ? "-" : "";
|
|
216
|
+
const abs = value < 0n ? -value : value;
|
|
217
|
+
const safeDigits = Math.max(0, Math.min(8, Math.trunc(fractionDigits)));
|
|
218
|
+
const multiplier = 10n ** BigInt(safeDigits);
|
|
219
|
+
const scaled = (abs * multiplier + SHANNONS_PER_CKB / 2n) / SHANNONS_PER_CKB;
|
|
220
|
+
const whole = scaled / multiplier;
|
|
221
|
+
if (safeDigits === 0) {
|
|
222
|
+
return `${sign}${whole}`;
|
|
223
|
+
}
|
|
224
|
+
const fraction = (scaled % multiplier).toString().padStart(safeDigits, "0");
|
|
225
|
+
return `${sign}${whole}.${fraction}`;
|
|
226
|
+
}
|
|
27
227
|
function parseHexTimestampMs(hexTimestamp) {
|
|
28
228
|
if (!hexTimestamp) return null;
|
|
29
229
|
try {
|
|
@@ -284,21 +484,6 @@ function printPeerListHuman(peers) {
|
|
|
284
484
|
console.log(`${peerId} ${pubkey} ${peer.address}`);
|
|
285
485
|
}
|
|
286
486
|
}
|
|
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
487
|
|
|
303
488
|
// src/commands/binary.ts
|
|
304
489
|
function showProgress(progress) {
|
|
@@ -311,14 +496,36 @@ function showProgress(progress) {
|
|
|
311
496
|
function createBinaryCommand(config) {
|
|
312
497
|
const binary = new Command("binary").description("Fiber binary management");
|
|
313
498
|
binary.command("download").option("--version <version>", "Fiber binary version", DEFAULT_FIBER_VERSION).option("--force", "Force re-download").option("--json").action(async (options) => {
|
|
499
|
+
const resolvedBinary = resolveBinaryPath(config);
|
|
500
|
+
let installDir;
|
|
501
|
+
try {
|
|
502
|
+
installDir = getBinaryManagerInstallDirOrThrow(resolvedBinary);
|
|
503
|
+
} catch (error) {
|
|
504
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
505
|
+
if (options.json) {
|
|
506
|
+
printJsonError({
|
|
507
|
+
code: "BINARY_PATH_INCOMPATIBLE",
|
|
508
|
+
message,
|
|
509
|
+
recoverable: true,
|
|
510
|
+
suggestion: "Use `fiber-pay config profile unset binaryPath` or set binaryPath to a standard fnn filename in the target directory."
|
|
511
|
+
});
|
|
512
|
+
} else {
|
|
513
|
+
console.error(`\u274C ${message}`);
|
|
514
|
+
}
|
|
515
|
+
process.exit(1);
|
|
516
|
+
}
|
|
314
517
|
const info = await downloadFiberBinary({
|
|
315
|
-
installDir
|
|
518
|
+
installDir,
|
|
316
519
|
version: options.version,
|
|
317
520
|
force: Boolean(options.force),
|
|
318
521
|
onProgress: options.json ? void 0 : showProgress
|
|
319
522
|
});
|
|
320
523
|
if (options.json) {
|
|
321
|
-
printJsonSuccess(
|
|
524
|
+
printJsonSuccess({
|
|
525
|
+
...info,
|
|
526
|
+
source: resolvedBinary.source,
|
|
527
|
+
resolvedPath: resolvedBinary.binaryPath
|
|
528
|
+
});
|
|
322
529
|
} else {
|
|
323
530
|
console.log("\n\u2705 Binary installed successfully!");
|
|
324
531
|
console.log(` Path: ${info.path}`);
|
|
@@ -327,9 +534,13 @@ function createBinaryCommand(config) {
|
|
|
327
534
|
}
|
|
328
535
|
});
|
|
329
536
|
binary.command("info").option("--json").action(async (options) => {
|
|
330
|
-
const info = await
|
|
537
|
+
const { resolvedBinary, info } = await getBinaryDetails(config);
|
|
331
538
|
if (options.json) {
|
|
332
|
-
printJsonSuccess(
|
|
539
|
+
printJsonSuccess({
|
|
540
|
+
...info,
|
|
541
|
+
source: resolvedBinary.source,
|
|
542
|
+
resolvedPath: resolvedBinary.binaryPath
|
|
543
|
+
});
|
|
333
544
|
} else {
|
|
334
545
|
console.log(info.ready ? "\u2705 Binary is ready" : "\u274C Binary not found or not executable");
|
|
335
546
|
console.log(` Path: ${info.path}`);
|
|
@@ -341,7 +552,7 @@ function createBinaryCommand(config) {
|
|
|
341
552
|
|
|
342
553
|
// src/commands/channel.ts
|
|
343
554
|
import { randomUUID } from "crypto";
|
|
344
|
-
import { ckbToShannons } from "@fiber-pay/sdk";
|
|
555
|
+
import { ckbToShannons as ckbToShannons2 } from "@fiber-pay/sdk";
|
|
345
556
|
import { Command as Command2 } from "commander";
|
|
346
557
|
|
|
347
558
|
// src/lib/async.ts
|
|
@@ -353,17 +564,17 @@ function sleep(ms) {
|
|
|
353
564
|
import { FiberRpcClient } from "@fiber-pay/sdk";
|
|
354
565
|
|
|
355
566
|
// src/lib/pid.ts
|
|
356
|
-
import { existsSync, readFileSync, unlinkSync, writeFileSync } from "fs";
|
|
357
|
-
import { join } from "path";
|
|
567
|
+
import { existsSync as existsSync2, readFileSync, unlinkSync, writeFileSync } from "fs";
|
|
568
|
+
import { join as join2 } from "path";
|
|
358
569
|
function getPidFilePath(dataDir) {
|
|
359
|
-
return
|
|
570
|
+
return join2(dataDir, "fiber.pid");
|
|
360
571
|
}
|
|
361
572
|
function writePidFile(dataDir, pid) {
|
|
362
573
|
writeFileSync(getPidFilePath(dataDir), String(pid));
|
|
363
574
|
}
|
|
364
575
|
function readPidFile(dataDir) {
|
|
365
576
|
const pidPath = getPidFilePath(dataDir);
|
|
366
|
-
if (!
|
|
577
|
+
if (!existsSync2(pidPath)) return null;
|
|
367
578
|
try {
|
|
368
579
|
return parseInt(readFileSync(pidPath, "utf-8").trim(), 10);
|
|
369
580
|
} catch {
|
|
@@ -372,7 +583,7 @@ function readPidFile(dataDir) {
|
|
|
372
583
|
}
|
|
373
584
|
function removePidFile(dataDir) {
|
|
374
585
|
const pidPath = getPidFilePath(dataDir);
|
|
375
|
-
if (
|
|
586
|
+
if (existsSync2(pidPath)) {
|
|
376
587
|
unlinkSync(pidPath);
|
|
377
588
|
}
|
|
378
589
|
}
|
|
@@ -386,20 +597,20 @@ function isProcessRunning(pid) {
|
|
|
386
597
|
}
|
|
387
598
|
|
|
388
599
|
// src/lib/runtime-meta.ts
|
|
389
|
-
import { existsSync as
|
|
390
|
-
import { join as
|
|
600
|
+
import { existsSync as existsSync3, readFileSync as readFileSync2, unlinkSync as unlinkSync2, writeFileSync as writeFileSync2 } from "fs";
|
|
601
|
+
import { join as join3 } from "path";
|
|
391
602
|
function getRuntimePidFilePath(dataDir) {
|
|
392
|
-
return
|
|
603
|
+
return join3(dataDir, "runtime.pid");
|
|
393
604
|
}
|
|
394
605
|
function getRuntimeMetaFilePath(dataDir) {
|
|
395
|
-
return
|
|
606
|
+
return join3(dataDir, "runtime.meta.json");
|
|
396
607
|
}
|
|
397
608
|
function writeRuntimePid(dataDir, pid) {
|
|
398
609
|
writeFileSync2(getRuntimePidFilePath(dataDir), String(pid));
|
|
399
610
|
}
|
|
400
611
|
function readRuntimePid(dataDir) {
|
|
401
612
|
const pidPath = getRuntimePidFilePath(dataDir);
|
|
402
|
-
if (!
|
|
613
|
+
if (!existsSync3(pidPath)) return null;
|
|
403
614
|
try {
|
|
404
615
|
return Number.parseInt(readFileSync2(pidPath, "utf-8").trim(), 10);
|
|
405
616
|
} catch {
|
|
@@ -411,7 +622,7 @@ function writeRuntimeMeta(dataDir, meta) {
|
|
|
411
622
|
}
|
|
412
623
|
function readRuntimeMeta(dataDir) {
|
|
413
624
|
const metaPath = getRuntimeMetaFilePath(dataDir);
|
|
414
|
-
if (!
|
|
625
|
+
if (!existsSync3(metaPath)) return null;
|
|
415
626
|
try {
|
|
416
627
|
return JSON.parse(readFileSync2(metaPath, "utf-8"));
|
|
417
628
|
} catch {
|
|
@@ -421,10 +632,10 @@ function readRuntimeMeta(dataDir) {
|
|
|
421
632
|
function removeRuntimeFiles(dataDir) {
|
|
422
633
|
const pidPath = getRuntimePidFilePath(dataDir);
|
|
423
634
|
const metaPath = getRuntimeMetaFilePath(dataDir);
|
|
424
|
-
if (
|
|
635
|
+
if (existsSync3(pidPath)) {
|
|
425
636
|
unlinkSync2(pidPath);
|
|
426
637
|
}
|
|
427
|
-
if (
|
|
638
|
+
if (existsSync3(metaPath)) {
|
|
428
639
|
unlinkSync2(metaPath);
|
|
429
640
|
}
|
|
430
641
|
}
|
|
@@ -457,7 +668,10 @@ function resolveRuntimeProxyUrl(config) {
|
|
|
457
668
|
}
|
|
458
669
|
function createRpcClient(config) {
|
|
459
670
|
const resolved = resolveRpcEndpoint(config);
|
|
460
|
-
return new FiberRpcClient({
|
|
671
|
+
return new FiberRpcClient({
|
|
672
|
+
url: resolved.url,
|
|
673
|
+
biscuitToken: config.rpcBiscuitToken
|
|
674
|
+
});
|
|
461
675
|
}
|
|
462
676
|
function resolveRpcEndpoint(config) {
|
|
463
677
|
const runtimeProxyUrl = resolveRuntimeProxyUrl(config);
|
|
@@ -534,23 +748,252 @@ async function waitForRuntimeJobTerminal(runtimeUrl, jobId, timeoutSeconds) {
|
|
|
534
748
|
throw new Error(`Timed out waiting for runtime job ${jobId}`);
|
|
535
749
|
}
|
|
536
750
|
|
|
751
|
+
// src/commands/rebalance.ts
|
|
752
|
+
import { ckbToShannons, shannonsToCkb as shannonsToCkb2 } from "@fiber-pay/sdk";
|
|
753
|
+
async function executeRebalance(config, params) {
|
|
754
|
+
const rpc = await createReadyRpcClient(config);
|
|
755
|
+
const amountCkb = parseFloat(params.amountInput);
|
|
756
|
+
const maxFeeCkb = params.maxFeeInput !== void 0 ? parseFloat(params.maxFeeInput) : void 0;
|
|
757
|
+
const manualHops = params.hops ?? [];
|
|
758
|
+
if (!Number.isFinite(amountCkb) || amountCkb <= 0) {
|
|
759
|
+
const message = "Invalid --amount value. Expected a positive CKB amount.";
|
|
760
|
+
if (params.json) {
|
|
761
|
+
printJsonError({
|
|
762
|
+
code: params.errorCode,
|
|
763
|
+
message,
|
|
764
|
+
recoverable: true,
|
|
765
|
+
suggestion: "Provide a positive number, e.g. `--amount 10`.",
|
|
766
|
+
details: { amount: params.amountInput }
|
|
767
|
+
});
|
|
768
|
+
} else {
|
|
769
|
+
console.error(`Error: ${message}`);
|
|
770
|
+
}
|
|
771
|
+
process.exit(1);
|
|
772
|
+
}
|
|
773
|
+
if (maxFeeCkb !== void 0 && (!Number.isFinite(maxFeeCkb) || maxFeeCkb < 0 || manualHops.length > 0)) {
|
|
774
|
+
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.";
|
|
775
|
+
if (params.json) {
|
|
776
|
+
printJsonError({
|
|
777
|
+
code: params.errorCode,
|
|
778
|
+
message,
|
|
779
|
+
recoverable: true,
|
|
780
|
+
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`.",
|
|
781
|
+
details: { maxFee: params.maxFeeInput, hasManualHops: manualHops.length > 0 }
|
|
782
|
+
});
|
|
783
|
+
} else {
|
|
784
|
+
console.error(`Error: ${message}`);
|
|
785
|
+
}
|
|
786
|
+
process.exit(1);
|
|
787
|
+
}
|
|
788
|
+
const selfPubkey = (await rpc.nodeInfo()).node_id;
|
|
789
|
+
const amount = ckbToShannons(amountCkb);
|
|
790
|
+
const isManual = manualHops.length > 0;
|
|
791
|
+
let routeHopCount;
|
|
792
|
+
const result = isManual ? await (async () => {
|
|
793
|
+
const hopsInfo = [
|
|
794
|
+
...manualHops.map((pubkey) => ({ pubkey })),
|
|
795
|
+
...manualHops[manualHops.length - 1] === selfPubkey ? [] : [{ pubkey: selfPubkey }]
|
|
796
|
+
];
|
|
797
|
+
const route = await rpc.buildRouter({
|
|
798
|
+
amount,
|
|
799
|
+
hops_info: hopsInfo
|
|
800
|
+
});
|
|
801
|
+
routeHopCount = route.router_hops.length;
|
|
802
|
+
return rpc.sendPaymentWithRouter({
|
|
803
|
+
router: route.router_hops,
|
|
804
|
+
keysend: true,
|
|
805
|
+
allow_self_payment: true,
|
|
806
|
+
dry_run: params.dryRun ? true : void 0
|
|
807
|
+
});
|
|
808
|
+
})() : await rpc.sendPayment({
|
|
809
|
+
target_pubkey: selfPubkey,
|
|
810
|
+
amount,
|
|
811
|
+
keysend: true,
|
|
812
|
+
allow_self_payment: true,
|
|
813
|
+
max_fee_amount: maxFeeCkb !== void 0 ? ckbToShannons(maxFeeCkb) : void 0,
|
|
814
|
+
dry_run: params.dryRun ? true : void 0
|
|
815
|
+
});
|
|
816
|
+
const payload = {
|
|
817
|
+
mode: isManual ? "manual" : "auto",
|
|
818
|
+
selfPubkey,
|
|
819
|
+
amountCkb,
|
|
820
|
+
maxFeeCkb: isManual ? void 0 : maxFeeCkb,
|
|
821
|
+
routeHopCount,
|
|
822
|
+
paymentHash: result.payment_hash,
|
|
823
|
+
status: result.status === "Success" ? "success" : result.status === "Failed" ? "failed" : "pending",
|
|
824
|
+
feeCkb: shannonsToCkb2(result.fee),
|
|
825
|
+
failureReason: result.failed_error,
|
|
826
|
+
dryRun: params.dryRun
|
|
827
|
+
};
|
|
828
|
+
if (params.json) {
|
|
829
|
+
printJsonSuccess(payload);
|
|
830
|
+
} else {
|
|
831
|
+
console.log(
|
|
832
|
+
payload.dryRun ? `Rebalance dry-run complete (${payload.mode} route)` : `Rebalance sent (${payload.mode} route)`
|
|
833
|
+
);
|
|
834
|
+
console.log(` Self: ${payload.selfPubkey}`);
|
|
835
|
+
console.log(` Amount: ${payload.amountCkb} CKB`);
|
|
836
|
+
if (payload.mode === "manual" && payload.routeHopCount !== void 0) {
|
|
837
|
+
console.log(` Hops: ${payload.routeHopCount}`);
|
|
838
|
+
}
|
|
839
|
+
console.log(` Hash: ${payload.paymentHash}`);
|
|
840
|
+
console.log(` Status: ${payload.status}`);
|
|
841
|
+
console.log(` Fee: ${payload.feeCkb} CKB`);
|
|
842
|
+
if (payload.mode === "auto" && payload.maxFeeCkb !== void 0) {
|
|
843
|
+
console.log(` MaxFee: ${payload.maxFeeCkb} CKB`);
|
|
844
|
+
}
|
|
845
|
+
if (payload.failureReason) {
|
|
846
|
+
console.log(` Error: ${payload.failureReason}`);
|
|
847
|
+
}
|
|
848
|
+
}
|
|
849
|
+
}
|
|
850
|
+
function registerPaymentRebalanceCommand(parent, config) {
|
|
851
|
+
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(
|
|
852
|
+
"--hops <pubkeys>",
|
|
853
|
+
"Comma-separated peer pubkeys for manual route mode (self pubkey appended automatically)"
|
|
854
|
+
).option("--dry-run", "Simulate route/payment and return estimated result").option("--json").action(async (options) => {
|
|
855
|
+
const hasHopsOption = typeof options.hops === "string";
|
|
856
|
+
const manualHops = hasHopsOption ? options.hops.split(",").map((item) => item.trim()).filter(Boolean) : [];
|
|
857
|
+
if (hasHopsOption && manualHops.length === 0) {
|
|
858
|
+
const message = "Invalid --hops value. Expected a non-empty comma-separated list of pubkeys.";
|
|
859
|
+
if (options.json) {
|
|
860
|
+
printJsonError({
|
|
861
|
+
code: "PAYMENT_REBALANCE_INPUT_INVALID",
|
|
862
|
+
message,
|
|
863
|
+
recoverable: true,
|
|
864
|
+
suggestion: "Provide pubkeys like `--hops 0xabc...,0xdef...`.",
|
|
865
|
+
details: { hops: options.hops }
|
|
866
|
+
});
|
|
867
|
+
} else {
|
|
868
|
+
console.error(`Error: ${message}`);
|
|
869
|
+
}
|
|
870
|
+
process.exit(1);
|
|
871
|
+
}
|
|
872
|
+
await executeRebalance(config, {
|
|
873
|
+
amountInput: options.amount,
|
|
874
|
+
maxFeeInput: options.maxFee,
|
|
875
|
+
hops: manualHops,
|
|
876
|
+
dryRun: Boolean(options.dryRun),
|
|
877
|
+
json: Boolean(options.json),
|
|
878
|
+
errorCode: "PAYMENT_REBALANCE_INPUT_INVALID"
|
|
879
|
+
});
|
|
880
|
+
});
|
|
881
|
+
}
|
|
882
|
+
function registerChannelRebalanceCommand(parent, config) {
|
|
883
|
+
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) => {
|
|
884
|
+
const json = Boolean(options.json);
|
|
885
|
+
const fromChannelId = options.fromChannel;
|
|
886
|
+
const toChannelId = options.toChannel;
|
|
887
|
+
if (fromChannelId && !toChannelId || !fromChannelId && toChannelId) {
|
|
888
|
+
const message = "Both --from-channel and --to-channel must be provided together for guided channel rebalance.";
|
|
889
|
+
if (json) {
|
|
890
|
+
printJsonError({
|
|
891
|
+
code: "CHANNEL_REBALANCE_INPUT_INVALID",
|
|
892
|
+
message,
|
|
893
|
+
recoverable: true,
|
|
894
|
+
suggestion: "Provide both channel ids, or provide neither to run auto mode.",
|
|
895
|
+
details: { fromChannel: fromChannelId, toChannel: toChannelId }
|
|
896
|
+
});
|
|
897
|
+
} else {
|
|
898
|
+
console.error(`Error: ${message}`);
|
|
899
|
+
}
|
|
900
|
+
process.exit(1);
|
|
901
|
+
}
|
|
902
|
+
let guidedHops;
|
|
903
|
+
if (fromChannelId && toChannelId) {
|
|
904
|
+
const rpc = await createReadyRpcClient(config);
|
|
905
|
+
const channels = (await rpc.listChannels({ include_closed: true })).channels;
|
|
906
|
+
const fromChannel = channels.find((item) => item.channel_id === fromChannelId);
|
|
907
|
+
const toChannel = channels.find((item) => item.channel_id === toChannelId);
|
|
908
|
+
if (!fromChannel || !toChannel) {
|
|
909
|
+
const message = "Invalid channel selection: source/target channel id not found.";
|
|
910
|
+
if (json) {
|
|
911
|
+
printJsonError({
|
|
912
|
+
code: "CHANNEL_REBALANCE_INPUT_INVALID",
|
|
913
|
+
message,
|
|
914
|
+
recoverable: true,
|
|
915
|
+
suggestion: "Run `channel list --json` and retry with valid channel ids.",
|
|
916
|
+
details: { fromChannel: fromChannelId, toChannel: toChannelId }
|
|
917
|
+
});
|
|
918
|
+
} else {
|
|
919
|
+
console.error(`Error: ${message}`);
|
|
920
|
+
}
|
|
921
|
+
process.exit(1);
|
|
922
|
+
}
|
|
923
|
+
if (fromChannel.peer_id === toChannel.peer_id) {
|
|
924
|
+
const message = "Source and target channels point to the same peer; choose two different channel peers.";
|
|
925
|
+
if (json) {
|
|
926
|
+
printJsonError({
|
|
927
|
+
code: "CHANNEL_REBALANCE_INPUT_INVALID",
|
|
928
|
+
message,
|
|
929
|
+
recoverable: true,
|
|
930
|
+
suggestion: "Select channels with different peer ids for guided rebalance.",
|
|
931
|
+
details: {
|
|
932
|
+
fromChannel: fromChannelId,
|
|
933
|
+
toChannel: toChannelId,
|
|
934
|
+
peerId: fromChannel.peer_id
|
|
935
|
+
}
|
|
936
|
+
});
|
|
937
|
+
} else {
|
|
938
|
+
console.error(`Error: ${message}`);
|
|
939
|
+
}
|
|
940
|
+
process.exit(1);
|
|
941
|
+
}
|
|
942
|
+
const peers = (await rpc.listPeers()).peers;
|
|
943
|
+
const pubkeyByPeerId = new Map(peers.map((peer) => [peer.peer_id, peer.pubkey]));
|
|
944
|
+
const fromPubkey = pubkeyByPeerId.get(fromChannel.peer_id);
|
|
945
|
+
const toPubkey = pubkeyByPeerId.get(toChannel.peer_id);
|
|
946
|
+
if (!fromPubkey || !toPubkey) {
|
|
947
|
+
const message = "Unable to resolve selected channel peer_id to pubkey for guided rebalance route.";
|
|
948
|
+
if (json) {
|
|
949
|
+
printJsonError({
|
|
950
|
+
code: "CHANNEL_REBALANCE_INPUT_INVALID",
|
|
951
|
+
message,
|
|
952
|
+
recoverable: true,
|
|
953
|
+
suggestion: "Ensure both peers are connected (`peer list --json`) and retry guided mode, or use `payment rebalance --hops`.",
|
|
954
|
+
details: {
|
|
955
|
+
fromChannel: fromChannelId,
|
|
956
|
+
toChannel: toChannelId,
|
|
957
|
+
fromPeerId: fromChannel.peer_id,
|
|
958
|
+
toPeerId: toChannel.peer_id,
|
|
959
|
+
resolvedPeers: peers.length
|
|
960
|
+
}
|
|
961
|
+
});
|
|
962
|
+
} else {
|
|
963
|
+
console.error(`Error: ${message}`);
|
|
964
|
+
}
|
|
965
|
+
process.exit(1);
|
|
966
|
+
}
|
|
967
|
+
guidedHops = [fromPubkey, toPubkey];
|
|
968
|
+
}
|
|
969
|
+
await executeRebalance(config, {
|
|
970
|
+
amountInput: options.amount,
|
|
971
|
+
maxFeeInput: options.maxFee,
|
|
972
|
+
hops: guidedHops,
|
|
973
|
+
dryRun: Boolean(options.dryRun),
|
|
974
|
+
json,
|
|
975
|
+
errorCode: "CHANNEL_REBALANCE_INPUT_INVALID"
|
|
976
|
+
});
|
|
977
|
+
});
|
|
978
|
+
}
|
|
979
|
+
|
|
537
980
|
// src/commands/channel.ts
|
|
538
981
|
function createChannelCommand(config) {
|
|
539
982
|
const channel = new Command2("channel").description("Channel lifecycle and status commands");
|
|
540
|
-
channel.command("list").option("--state <state>").option("--peer <peerId>").option("--include-closed").option("--
|
|
983
|
+
channel.command("list").option("--state <state>").option("--peer <peerId>").option("--include-closed").option("--json").action(async (options) => {
|
|
541
984
|
const rpc = await createReadyRpcClient(config);
|
|
542
985
|
const stateFilter = parseChannelState(options.state);
|
|
543
986
|
const response = await rpc.listChannels(
|
|
544
987
|
options.peer ? { peer_id: options.peer, include_closed: Boolean(options.includeClosed) } : { include_closed: Boolean(options.includeClosed) }
|
|
545
988
|
);
|
|
546
989
|
const channels = stateFilter ? response.channels.filter((item) => item.state.state_name === stateFilter) : response.channels;
|
|
547
|
-
if (options.
|
|
990
|
+
if (options.json) {
|
|
548
991
|
printJsonSuccess({ channels, count: channels.length });
|
|
549
992
|
} else {
|
|
550
993
|
printChannelListHuman(channels);
|
|
551
994
|
}
|
|
552
995
|
});
|
|
553
|
-
channel.command("get").argument("<channelId>").option("--
|
|
996
|
+
channel.command("get").argument("<channelId>").option("--json").action(async (channelId, options) => {
|
|
554
997
|
const rpc = await createReadyRpcClient(config);
|
|
555
998
|
const response = await rpc.listChannels({ include_closed: true });
|
|
556
999
|
const found = response.channels.find((item) => item.channel_id === channelId);
|
|
@@ -568,7 +1011,7 @@ function createChannelCommand(config) {
|
|
|
568
1011
|
}
|
|
569
1012
|
process.exit(1);
|
|
570
1013
|
}
|
|
571
|
-
if (options.
|
|
1014
|
+
if (options.json) {
|
|
572
1015
|
printJsonSuccess(found);
|
|
573
1016
|
} else {
|
|
574
1017
|
printChannelDetailHuman(found);
|
|
@@ -708,7 +1151,7 @@ function createChannelCommand(config) {
|
|
|
708
1151
|
peerId,
|
|
709
1152
|
openChannelParams: {
|
|
710
1153
|
peer_id: peerId,
|
|
711
|
-
funding_amount:
|
|
1154
|
+
funding_amount: ckbToShannons2(fundingCkb),
|
|
712
1155
|
public: !options.private
|
|
713
1156
|
},
|
|
714
1157
|
waitForReady: false
|
|
@@ -741,7 +1184,7 @@ function createChannelCommand(config) {
|
|
|
741
1184
|
}
|
|
742
1185
|
const result = await rpc.openChannel({
|
|
743
1186
|
peer_id: peerId,
|
|
744
|
-
funding_amount:
|
|
1187
|
+
funding_amount: ckbToShannons2(fundingCkb),
|
|
745
1188
|
public: !options.private
|
|
746
1189
|
});
|
|
747
1190
|
const payload = { temporaryChannelId: result.temporary_channel_id, peer: peerId, fundingCkb };
|
|
@@ -765,7 +1208,7 @@ function createChannelCommand(config) {
|
|
|
765
1208
|
action: "accept",
|
|
766
1209
|
acceptChannelParams: {
|
|
767
1210
|
temporary_channel_id: temporaryChannelId,
|
|
768
|
-
funding_amount:
|
|
1211
|
+
funding_amount: ckbToShannons2(fundingCkb)
|
|
769
1212
|
}
|
|
770
1213
|
},
|
|
771
1214
|
options: {
|
|
@@ -793,7 +1236,7 @@ function createChannelCommand(config) {
|
|
|
793
1236
|
}
|
|
794
1237
|
const result = await rpc.acceptChannel({
|
|
795
1238
|
temporary_channel_id: temporaryChannelId,
|
|
796
|
-
funding_amount:
|
|
1239
|
+
funding_amount: ckbToShannons2(fundingCkb)
|
|
797
1240
|
});
|
|
798
1241
|
const payload = { channelId: result.channel_id, temporaryChannelId, fundingCkb };
|
|
799
1242
|
if (json) {
|
|
@@ -818,7 +1261,7 @@ function createChannelCommand(config) {
|
|
|
818
1261
|
channel_id: channelId,
|
|
819
1262
|
force: Boolean(options.force)
|
|
820
1263
|
},
|
|
821
|
-
waitForClosed:
|
|
1264
|
+
waitForClosed: Boolean(options.force)
|
|
822
1265
|
},
|
|
823
1266
|
options: {
|
|
824
1267
|
idempotencyKey: `shutdown:channel:${channelId}`
|
|
@@ -903,6 +1346,7 @@ function createChannelCommand(config) {
|
|
|
903
1346
|
console.log(` Channel ID: ${payload.channelId}`);
|
|
904
1347
|
}
|
|
905
1348
|
});
|
|
1349
|
+
registerChannelRebalanceCommand(channel, config);
|
|
906
1350
|
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
1351
|
const rpc = await createReadyRpcClient(config);
|
|
908
1352
|
const json = Boolean(options.json);
|
|
@@ -956,13 +1400,13 @@ function createChannelCommand(config) {
|
|
|
956
1400
|
}
|
|
957
1401
|
|
|
958
1402
|
// src/commands/config.ts
|
|
959
|
-
import { existsSync as
|
|
1403
|
+
import { existsSync as existsSync5, readFileSync as readFileSync4, writeFileSync as writeFileSync4 } from "fs";
|
|
960
1404
|
import { Command as Command3 } from "commander";
|
|
961
1405
|
import { parseDocument, stringify as yamlStringify } from "yaml";
|
|
962
1406
|
|
|
963
1407
|
// src/lib/config.ts
|
|
964
|
-
import { existsSync as
|
|
965
|
-
import { join as
|
|
1408
|
+
import { existsSync as existsSync4, mkdirSync, readFileSync as readFileSync3, writeFileSync as writeFileSync3 } from "fs";
|
|
1409
|
+
import { join as join4 } from "path";
|
|
966
1410
|
|
|
967
1411
|
// src/lib/config-templates.ts
|
|
968
1412
|
var TESTNET_CONFIG_TEMPLATE_V071 = `# This configuration file only contains the necessary configurations for the testnet deployment.
|
|
@@ -1121,7 +1565,7 @@ var DEFAULT_DATA_DIR = `${process.env.HOME}/.fiber-pay`;
|
|
|
1121
1565
|
var DEFAULT_RPC_URL = "http://127.0.0.1:8227";
|
|
1122
1566
|
var DEFAULT_NETWORK = "testnet";
|
|
1123
1567
|
function getConfigPath(dataDir) {
|
|
1124
|
-
return
|
|
1568
|
+
return join4(dataDir, "config.yml");
|
|
1125
1569
|
}
|
|
1126
1570
|
function parseNetworkFromConfig(configContent) {
|
|
1127
1571
|
const match = configContent.match(/^\s*chain:\s*(testnet|mainnet)\s*$/m);
|
|
@@ -1153,11 +1597,11 @@ function parseCkbRpcUrlFromConfig(configContent) {
|
|
|
1153
1597
|
return match?.[1]?.trim() || void 0;
|
|
1154
1598
|
}
|
|
1155
1599
|
function getProfilePath(dataDir) {
|
|
1156
|
-
return
|
|
1600
|
+
return join4(dataDir, "profile.json");
|
|
1157
1601
|
}
|
|
1158
1602
|
function loadProfileConfig(dataDir) {
|
|
1159
1603
|
const profilePath = getProfilePath(dataDir);
|
|
1160
|
-
if (!
|
|
1604
|
+
if (!existsSync4(profilePath)) return void 0;
|
|
1161
1605
|
try {
|
|
1162
1606
|
const raw = readFileSync3(profilePath, "utf-8");
|
|
1163
1607
|
return JSON.parse(raw);
|
|
@@ -1166,7 +1610,7 @@ function loadProfileConfig(dataDir) {
|
|
|
1166
1610
|
}
|
|
1167
1611
|
}
|
|
1168
1612
|
function saveProfileConfig(dataDir, profile) {
|
|
1169
|
-
if (!
|
|
1613
|
+
if (!existsSync4(dataDir)) {
|
|
1170
1614
|
mkdirSync(dataDir, { recursive: true });
|
|
1171
1615
|
}
|
|
1172
1616
|
const profilePath = getProfilePath(dataDir);
|
|
@@ -1175,11 +1619,11 @@ function saveProfileConfig(dataDir, profile) {
|
|
|
1175
1619
|
}
|
|
1176
1620
|
function writeNetworkConfigFile(dataDir, network, options = {}) {
|
|
1177
1621
|
const configPath = getConfigPath(dataDir);
|
|
1178
|
-
const alreadyExists =
|
|
1622
|
+
const alreadyExists = existsSync4(configPath);
|
|
1179
1623
|
if (alreadyExists && !options.force) {
|
|
1180
1624
|
return { path: configPath, created: false, overwritten: false };
|
|
1181
1625
|
}
|
|
1182
|
-
if (!
|
|
1626
|
+
if (!existsSync4(dataDir)) {
|
|
1183
1627
|
mkdirSync(dataDir, { recursive: true });
|
|
1184
1628
|
}
|
|
1185
1629
|
let content = getConfigTemplate(network);
|
|
@@ -1205,7 +1649,7 @@ function writeNetworkConfigFile(dataDir, network, options = {}) {
|
|
|
1205
1649
|
}
|
|
1206
1650
|
function ensureNodeConfigFile(dataDir, network) {
|
|
1207
1651
|
const configPath = getConfigPath(dataDir);
|
|
1208
|
-
if (!
|
|
1652
|
+
if (!existsSync4(configPath)) {
|
|
1209
1653
|
writeNetworkConfigFile(dataDir, network);
|
|
1210
1654
|
}
|
|
1211
1655
|
return configPath;
|
|
@@ -1214,7 +1658,7 @@ function getEffectiveConfig(explicitFlags2) {
|
|
|
1214
1658
|
const dataDir = process.env.FIBER_DATA_DIR || DEFAULT_DATA_DIR;
|
|
1215
1659
|
const dataDirSource = explicitFlags2?.has("dataDir") ? "cli" : process.env.FIBER_DATA_DIR ? "env" : "default";
|
|
1216
1660
|
const configPath = getConfigPath(dataDir);
|
|
1217
|
-
const configExists =
|
|
1661
|
+
const configExists = existsSync4(configPath);
|
|
1218
1662
|
const configContent = configExists ? readFileSync3(configPath, "utf-8") : void 0;
|
|
1219
1663
|
const profile = loadProfileConfig(dataDir);
|
|
1220
1664
|
const cliNetwork = explicitFlags2?.has("network") ? process.env.FIBER_NETWORK : void 0;
|
|
@@ -1227,6 +1671,10 @@ function getEffectiveConfig(explicitFlags2) {
|
|
|
1227
1671
|
const fileRpcUrl = configContent ? parseRpcUrlFromConfig(configContent) : void 0;
|
|
1228
1672
|
const rpcUrl = cliRpcUrl || envRpcUrl || fileRpcUrl || DEFAULT_RPC_URL;
|
|
1229
1673
|
const rpcUrlSource = cliRpcUrl ? "cli" : envRpcUrl ? "env" : fileRpcUrl ? "config" : "default";
|
|
1674
|
+
const cliRpcBiscuitToken = explicitFlags2?.has("rpcBiscuitToken") ? process.env.FIBER_RPC_BISCUIT_TOKEN : void 0;
|
|
1675
|
+
const envRpcBiscuitToken = !explicitFlags2?.has("rpcBiscuitToken") ? process.env.FIBER_RPC_BISCUIT_TOKEN : void 0;
|
|
1676
|
+
const rpcBiscuitToken = cliRpcBiscuitToken || envRpcBiscuitToken || void 0;
|
|
1677
|
+
const rpcBiscuitTokenSource = cliRpcBiscuitToken ? "cli" : envRpcBiscuitToken ? "env" : "unset";
|
|
1230
1678
|
const cliBinaryPath = explicitFlags2?.has("binaryPath") ? process.env.FIBER_BINARY_PATH : void 0;
|
|
1231
1679
|
const profileBinaryPath = profile?.binaryPath;
|
|
1232
1680
|
const envBinaryPath = !explicitFlags2?.has("binaryPath") ? process.env.FIBER_BINARY_PATH : void 0;
|
|
@@ -1253,6 +1701,7 @@ function getEffectiveConfig(explicitFlags2) {
|
|
|
1253
1701
|
configPath,
|
|
1254
1702
|
network,
|
|
1255
1703
|
rpcUrl,
|
|
1704
|
+
rpcBiscuitToken,
|
|
1256
1705
|
keyPassword,
|
|
1257
1706
|
ckbRpcUrl,
|
|
1258
1707
|
runtimeProxyListen
|
|
@@ -1262,6 +1711,7 @@ function getEffectiveConfig(explicitFlags2) {
|
|
|
1262
1711
|
configPath: "derived",
|
|
1263
1712
|
network: networkSource,
|
|
1264
1713
|
rpcUrl: rpcUrlSource,
|
|
1714
|
+
rpcBiscuitToken: rpcBiscuitTokenSource,
|
|
1265
1715
|
ckbRpcUrl: ckbRpcUrlSource,
|
|
1266
1716
|
runtimeProxyListen: runtimeProxyListenSource
|
|
1267
1717
|
}
|
|
@@ -1356,7 +1806,7 @@ function parseTypedValue(raw, valueType) {
|
|
|
1356
1806
|
return raw;
|
|
1357
1807
|
}
|
|
1358
1808
|
function ensureConfigFileOrExit(configPath, json) {
|
|
1359
|
-
if (!
|
|
1809
|
+
if (!existsSync5(configPath)) {
|
|
1360
1810
|
const msg = `Config file not found: ${configPath}. Run \`fiber-pay config init\` first.`;
|
|
1361
1811
|
if (json) {
|
|
1362
1812
|
printJsonError({
|
|
@@ -1728,7 +2178,7 @@ function createConfigCommand(_config) {
|
|
|
1728
2178
|
}
|
|
1729
2179
|
|
|
1730
2180
|
// src/commands/graph.ts
|
|
1731
|
-
import { shannonsToCkb as
|
|
2181
|
+
import { shannonsToCkb as shannonsToCkb3, toHex as toHex2 } from "@fiber-pay/sdk";
|
|
1732
2182
|
import { Command as Command4 } from "commander";
|
|
1733
2183
|
function printGraphNodeListHuman(nodes) {
|
|
1734
2184
|
if (nodes.length === 0) {
|
|
@@ -1743,7 +2193,7 @@ function printGraphNodeListHuman(nodes) {
|
|
|
1743
2193
|
const nodeId = truncateMiddle(node.node_id, 10, 8).padEnd(22, " ");
|
|
1744
2194
|
const alias = (node.node_name || "(unnamed)").slice(0, 20).padEnd(20, " ");
|
|
1745
2195
|
const version = (node.version || "?").slice(0, 10).padEnd(10, " ");
|
|
1746
|
-
const minFunding =
|
|
2196
|
+
const minFunding = shannonsToCkb3(node.auto_accept_min_ckb_funding_amount).toString().padStart(12, " ");
|
|
1747
2197
|
const age = formatAge(parseHexTimestampMs(node.timestamp));
|
|
1748
2198
|
console.log(`${nodeId} ${alias} ${version} ${minFunding} ${age}`);
|
|
1749
2199
|
}
|
|
@@ -1765,7 +2215,7 @@ function printGraphChannelListHuman(channels) {
|
|
|
1765
2215
|
const outpoint = ch.channel_outpoint ? truncateMiddle(`${ch.channel_outpoint.tx_hash}:${ch.channel_outpoint.index}`, 10, 8) : "n/a";
|
|
1766
2216
|
const n1 = truncateMiddle(ch.node1, 10, 8).padEnd(22, " ");
|
|
1767
2217
|
const n2 = truncateMiddle(ch.node2, 10, 8).padEnd(22, " ");
|
|
1768
|
-
const capacity = `${
|
|
2218
|
+
const capacity = `${shannonsToCkb3(ch.capacity)} CKB`.padStart(12, " ");
|
|
1769
2219
|
const age = formatAge(parseHexTimestampMs(ch.created_timestamp));
|
|
1770
2220
|
console.log(`${outpoint.padEnd(22, " ")} ${n1} ${n2} ${capacity} ${age}`);
|
|
1771
2221
|
}
|
|
@@ -1810,7 +2260,7 @@ Next cursor: ${result.last_cursor}`);
|
|
|
1810
2260
|
}
|
|
1811
2261
|
|
|
1812
2262
|
// src/commands/invoice.ts
|
|
1813
|
-
import { ckbToShannons as
|
|
2263
|
+
import { ckbToShannons as ckbToShannons3, randomBytes32, shannonsToCkb as shannonsToCkb4, toHex as toHex3 } from "@fiber-pay/sdk";
|
|
1814
2264
|
import { Command as Command5 } from "commander";
|
|
1815
2265
|
function createInvoiceCommand(config) {
|
|
1816
2266
|
const invoice = new Command5("invoice").description("Invoice lifecycle and status commands");
|
|
@@ -1839,7 +2289,7 @@ function createInvoiceCommand(config) {
|
|
|
1839
2289
|
params: {
|
|
1840
2290
|
action: "create",
|
|
1841
2291
|
newInvoiceParams: {
|
|
1842
|
-
amount:
|
|
2292
|
+
amount: ckbToShannons3(amountCkb),
|
|
1843
2293
|
currency,
|
|
1844
2294
|
description: options.description,
|
|
1845
2295
|
expiry: toHex3(expirySeconds),
|
|
@@ -1876,7 +2326,7 @@ function createInvoiceCommand(config) {
|
|
|
1876
2326
|
}
|
|
1877
2327
|
}
|
|
1878
2328
|
const result = await rpc.newInvoice({
|
|
1879
|
-
amount:
|
|
2329
|
+
amount: ckbToShannons3(amountCkb),
|
|
1880
2330
|
currency,
|
|
1881
2331
|
description: options.description,
|
|
1882
2332
|
expiry: toHex3(expirySeconds),
|
|
@@ -1908,7 +2358,7 @@ function createInvoiceCommand(config) {
|
|
|
1908
2358
|
paymentHash,
|
|
1909
2359
|
status: result.status,
|
|
1910
2360
|
invoice: result.invoice_address,
|
|
1911
|
-
amountCkb: result.invoice.amount ?
|
|
2361
|
+
amountCkb: result.invoice.amount ? shannonsToCkb4(result.invoice.amount) : void 0,
|
|
1912
2362
|
currency: result.invoice.currency,
|
|
1913
2363
|
description: metadata.description,
|
|
1914
2364
|
createdAt: createdAtMs ? new Date(createdAtMs).toISOString() : result.invoice.data.timestamp,
|
|
@@ -2028,19 +2478,104 @@ function createInvoiceCommand(config) {
|
|
|
2028
2478
|
}
|
|
2029
2479
|
|
|
2030
2480
|
// src/commands/job.ts
|
|
2031
|
-
import { existsSync as
|
|
2481
|
+
import { existsSync as existsSync7 } from "fs";
|
|
2032
2482
|
import { Command as Command6 } from "commander";
|
|
2033
2483
|
|
|
2034
2484
|
// src/lib/log-files.ts
|
|
2035
|
-
import {
|
|
2036
|
-
|
|
2037
|
-
|
|
2485
|
+
import {
|
|
2486
|
+
appendFileSync,
|
|
2487
|
+
closeSync,
|
|
2488
|
+
createReadStream,
|
|
2489
|
+
existsSync as existsSync6,
|
|
2490
|
+
mkdirSync as mkdirSync2,
|
|
2491
|
+
openSync,
|
|
2492
|
+
readdirSync,
|
|
2493
|
+
readSync,
|
|
2494
|
+
statSync
|
|
2495
|
+
} from "fs";
|
|
2496
|
+
import { join as join5 } from "path";
|
|
2497
|
+
var DATE_DIR_PATTERN = /^\d{4}-\d{2}-\d{2}$/;
|
|
2498
|
+
function todayDateString() {
|
|
2499
|
+
const now = /* @__PURE__ */ new Date();
|
|
2500
|
+
const y = now.getUTCFullYear();
|
|
2501
|
+
const m = String(now.getUTCMonth() + 1).padStart(2, "0");
|
|
2502
|
+
const d = String(now.getUTCDate()).padStart(2, "0");
|
|
2503
|
+
return `${y}-${m}-${d}`;
|
|
2504
|
+
}
|
|
2505
|
+
function validateLogDate(date) {
|
|
2506
|
+
const value = date.trim();
|
|
2507
|
+
if (!DATE_DIR_PATTERN.test(value)) {
|
|
2508
|
+
throw new Error(`Invalid date '${value}'. Expected format YYYY-MM-DD.`);
|
|
2509
|
+
}
|
|
2510
|
+
if (value.includes("/") || value.includes("\\") || value.includes("..")) {
|
|
2511
|
+
throw new Error(`Invalid date '${value}'. Path separators or '..' are not allowed.`);
|
|
2512
|
+
}
|
|
2513
|
+
return value;
|
|
2514
|
+
}
|
|
2515
|
+
function resolveLogDirForDate(dataDir, date) {
|
|
2516
|
+
return resolveLogDirForDateWithOptions(dataDir, date, {});
|
|
2517
|
+
}
|
|
2518
|
+
function resolveLogDirForDateWithOptions(dataDir, date, options) {
|
|
2519
|
+
const dateStr = date ?? todayDateString();
|
|
2520
|
+
const logsBaseDir = options.logsBaseDir ?? join5(dataDir, "logs");
|
|
2521
|
+
if (date !== void 0) {
|
|
2522
|
+
validateLogDate(dateStr);
|
|
2523
|
+
}
|
|
2524
|
+
const dir = join5(logsBaseDir, dateStr);
|
|
2525
|
+
const ensureExists = options.ensureExists ?? true;
|
|
2526
|
+
if (ensureExists) {
|
|
2527
|
+
mkdirSync2(dir, { recursive: true });
|
|
2528
|
+
}
|
|
2529
|
+
return dir;
|
|
2530
|
+
}
|
|
2531
|
+
function resolvePersistedLogPaths(dataDir, meta, date) {
|
|
2532
|
+
const logsBaseDir = meta?.logsBaseDir ?? join5(dataDir, "logs");
|
|
2533
|
+
if (date) {
|
|
2534
|
+
const dir2 = resolveLogDirForDateWithOptions(dataDir, date, {
|
|
2535
|
+
logsBaseDir,
|
|
2536
|
+
ensureExists: false
|
|
2537
|
+
});
|
|
2538
|
+
return {
|
|
2539
|
+
runtimeAlerts: join5(dir2, "runtime.alerts.jsonl"),
|
|
2540
|
+
fnnStdout: join5(dir2, "fnn.stdout.log"),
|
|
2541
|
+
fnnStderr: join5(dir2, "fnn.stderr.log")
|
|
2542
|
+
};
|
|
2543
|
+
}
|
|
2544
|
+
if (meta?.alertLogFilePath || meta?.fnnStdoutLogPath || meta?.fnnStderrLogPath) {
|
|
2545
|
+
const defaultDir = resolveLogDirForDateWithOptions(dataDir, void 0, {
|
|
2546
|
+
logsBaseDir,
|
|
2547
|
+
ensureExists: false
|
|
2548
|
+
});
|
|
2549
|
+
return {
|
|
2550
|
+
runtimeAlerts: meta.alertLogFilePath ?? join5(defaultDir, "runtime.alerts.jsonl"),
|
|
2551
|
+
fnnStdout: meta.fnnStdoutLogPath ?? join5(defaultDir, "fnn.stdout.log"),
|
|
2552
|
+
fnnStderr: meta.fnnStderrLogPath ?? join5(defaultDir, "fnn.stderr.log")
|
|
2553
|
+
};
|
|
2554
|
+
}
|
|
2555
|
+
const dir = resolveLogDirForDateWithOptions(dataDir, void 0, {
|
|
2556
|
+
logsBaseDir,
|
|
2557
|
+
ensureExists: false
|
|
2558
|
+
});
|
|
2038
2559
|
return {
|
|
2039
|
-
runtimeAlerts:
|
|
2040
|
-
fnnStdout:
|
|
2041
|
-
fnnStderr:
|
|
2560
|
+
runtimeAlerts: join5(dir, "runtime.alerts.jsonl"),
|
|
2561
|
+
fnnStdout: join5(dir, "fnn.stdout.log"),
|
|
2562
|
+
fnnStderr: join5(dir, "fnn.stderr.log")
|
|
2042
2563
|
};
|
|
2043
2564
|
}
|
|
2565
|
+
function listLogDates(dataDir, logsBaseDir) {
|
|
2566
|
+
const logsDir = logsBaseDir ?? join5(dataDir, "logs");
|
|
2567
|
+
if (!existsSync6(logsDir)) {
|
|
2568
|
+
return [];
|
|
2569
|
+
}
|
|
2570
|
+
const entries = readdirSync(logsDir, { withFileTypes: true });
|
|
2571
|
+
const dates = entries.filter((entry) => entry.isDirectory() && DATE_DIR_PATTERN.test(entry.name)).map((entry) => entry.name);
|
|
2572
|
+
dates.sort((a, b) => a > b ? -1 : a < b ? 1 : 0);
|
|
2573
|
+
return dates;
|
|
2574
|
+
}
|
|
2575
|
+
function appendToTodayLog(dataDir, filename, text) {
|
|
2576
|
+
const dir = resolveLogDirForDate(dataDir);
|
|
2577
|
+
appendFileSync(join5(dir, filename), text, "utf-8");
|
|
2578
|
+
}
|
|
2044
2579
|
function resolvePersistedLogTargets(paths, source) {
|
|
2045
2580
|
const all = [
|
|
2046
2581
|
{
|
|
@@ -2065,7 +2600,7 @@ function resolvePersistedLogTargets(paths, source) {
|
|
|
2065
2600
|
return all.filter((target) => target.source === source);
|
|
2066
2601
|
}
|
|
2067
2602
|
function readLastLines(filePath, maxLines) {
|
|
2068
|
-
if (!
|
|
2603
|
+
if (!existsSync6(filePath)) {
|
|
2069
2604
|
return [];
|
|
2070
2605
|
}
|
|
2071
2606
|
if (!Number.isFinite(maxLines) || maxLines <= 0) {
|
|
@@ -2109,7 +2644,7 @@ function readLastLines(filePath, maxLines) {
|
|
|
2109
2644
|
}
|
|
2110
2645
|
}
|
|
2111
2646
|
async function readAppendedLines(filePath, offset, remainder = "") {
|
|
2112
|
-
if (!
|
|
2647
|
+
if (!existsSync6(filePath)) {
|
|
2113
2648
|
return { lines: [], nextOffset: 0, remainder: "" };
|
|
2114
2649
|
}
|
|
2115
2650
|
const size = statSync(filePath).size;
|
|
@@ -2209,10 +2744,11 @@ function createJobCommand(config) {
|
|
|
2209
2744
|
console.log(` ${JSON.stringify(payload.result)}`);
|
|
2210
2745
|
}
|
|
2211
2746
|
});
|
|
2212
|
-
job.command("trace").argument("<jobId>").option("--tail <n>", "Max lines to inspect per log file", "400").option("--json").action(async (jobId, options) => {
|
|
2747
|
+
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
2748
|
const json = Boolean(options.json);
|
|
2214
2749
|
const tailInput = Number.parseInt(String(options.tail ?? "400"), 10);
|
|
2215
2750
|
const tail = Number.isFinite(tailInput) && tailInput > 0 ? tailInput : 400;
|
|
2751
|
+
const date = options.date ? String(options.date).trim() : void 0;
|
|
2216
2752
|
const runtimeUrl = getRuntimeUrlOrExit(config, json);
|
|
2217
2753
|
const jobResponse = await fetch(`${runtimeUrl}/jobs/${jobId}`);
|
|
2218
2754
|
if (!jobResponse.ok) {
|
|
@@ -2226,7 +2762,24 @@ function createJobCommand(config) {
|
|
|
2226
2762
|
const eventsPayload = await eventsResponse.json();
|
|
2227
2763
|
const tokens = collectTraceTokens(jobRecord, eventsPayload.events);
|
|
2228
2764
|
const meta = readRuntimeMeta(config.dataDir);
|
|
2229
|
-
|
|
2765
|
+
let logPaths;
|
|
2766
|
+
try {
|
|
2767
|
+
logPaths = resolvePersistedLogPaths(config.dataDir, meta, date);
|
|
2768
|
+
} catch (error) {
|
|
2769
|
+
const message = error instanceof Error ? error.message : "Invalid --date value.";
|
|
2770
|
+
if (json) {
|
|
2771
|
+
printJsonError({
|
|
2772
|
+
code: "JOB_TRACE_DATE_INVALID",
|
|
2773
|
+
message,
|
|
2774
|
+
recoverable: true,
|
|
2775
|
+
suggestion: "Retry with --date in YYYY-MM-DD format.",
|
|
2776
|
+
details: { date }
|
|
2777
|
+
});
|
|
2778
|
+
} else {
|
|
2779
|
+
console.error(`Error: ${message}`);
|
|
2780
|
+
}
|
|
2781
|
+
process.exit(1);
|
|
2782
|
+
}
|
|
2230
2783
|
const runtimeAlertMatches = collectRelatedLines(logPaths.runtimeAlerts, tokens, tail);
|
|
2231
2784
|
const fnnStdoutMatches = collectRelatedLines(logPaths.fnnStdout, tokens, tail);
|
|
2232
2785
|
const fnnStderrMatches = collectRelatedLines(logPaths.fnnStderr, tokens, tail);
|
|
@@ -2429,7 +2982,7 @@ function collectStructuredTokens(set, input, depth = 0) {
|
|
|
2429
2982
|
}
|
|
2430
2983
|
}
|
|
2431
2984
|
function collectRelatedLines(filePath, tokens, tail) {
|
|
2432
|
-
if (!
|
|
2985
|
+
if (!existsSync7(filePath)) {
|
|
2433
2986
|
return [];
|
|
2434
2987
|
}
|
|
2435
2988
|
const lines = readLastLines(filePath, tail);
|
|
@@ -2445,7 +2998,7 @@ function collectRelatedLines(filePath, tokens, tail) {
|
|
|
2445
2998
|
function printTraceSection(title, filePath, lines) {
|
|
2446
2999
|
console.log(`
|
|
2447
3000
|
${title}: ${filePath}`);
|
|
2448
|
-
if (!
|
|
3001
|
+
if (!existsSync7(filePath)) {
|
|
2449
3002
|
console.log(" (file not found)");
|
|
2450
3003
|
return;
|
|
2451
3004
|
}
|
|
@@ -2459,7 +3012,8 @@ ${title}: ${filePath}`);
|
|
|
2459
3012
|
}
|
|
2460
3013
|
|
|
2461
3014
|
// src/commands/logs.ts
|
|
2462
|
-
import { existsSync as
|
|
3015
|
+
import { existsSync as existsSync8, statSync as statSync2 } from "fs";
|
|
3016
|
+
import { join as join6 } from "path";
|
|
2463
3017
|
import { formatRuntimeAlert } from "@fiber-pay/runtime";
|
|
2464
3018
|
import { Command as Command7 } from "commander";
|
|
2465
3019
|
var ALLOWED_SOURCES = /* @__PURE__ */ new Set([
|
|
@@ -2468,6 +3022,7 @@ var ALLOWED_SOURCES = /* @__PURE__ */ new Set([
|
|
|
2468
3022
|
"fnn-stdout",
|
|
2469
3023
|
"fnn-stderr"
|
|
2470
3024
|
]);
|
|
3025
|
+
var DATE_DIR_PATTERN2 = /^\d{4}-\d{2}-\d{2}$/;
|
|
2471
3026
|
function parseRuntimeAlertLine(line) {
|
|
2472
3027
|
try {
|
|
2473
3028
|
const parsed = JSON.parse(line);
|
|
@@ -2493,9 +3048,45 @@ function coerceJsonLineForOutput(source, line) {
|
|
|
2493
3048
|
return parseRuntimeAlertLine(line) ?? line;
|
|
2494
3049
|
}
|
|
2495
3050
|
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) => {
|
|
3051
|
+
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
3052
|
const json = Boolean(options.json);
|
|
2498
3053
|
const follow = Boolean(options.follow);
|
|
3054
|
+
const listDates = Boolean(options.listDates);
|
|
3055
|
+
const date = options.date ? String(options.date).trim() : void 0;
|
|
3056
|
+
const meta = readRuntimeMeta(config.dataDir);
|
|
3057
|
+
if (follow && date) {
|
|
3058
|
+
const message = "--follow cannot be used with --date. --follow only streams today's logs.";
|
|
3059
|
+
if (json) {
|
|
3060
|
+
printJsonError({
|
|
3061
|
+
code: "LOG_FOLLOW_DATE_UNSUPPORTED",
|
|
3062
|
+
message,
|
|
3063
|
+
recoverable: true,
|
|
3064
|
+
suggestion: "Remove --date or remove --follow and retry."
|
|
3065
|
+
});
|
|
3066
|
+
} else {
|
|
3067
|
+
console.error(`Error: ${message}`);
|
|
3068
|
+
}
|
|
3069
|
+
process.exit(1);
|
|
3070
|
+
}
|
|
3071
|
+
if (listDates) {
|
|
3072
|
+
const logsDir = meta?.logsBaseDir ?? join6(config.dataDir, "logs");
|
|
3073
|
+
const dates = listLogDates(config.dataDir, logsDir);
|
|
3074
|
+
if (json) {
|
|
3075
|
+
printJsonSuccess({ dates, logsDir });
|
|
3076
|
+
} else {
|
|
3077
|
+
if (dates.length === 0) {
|
|
3078
|
+
console.log("No log dates found.");
|
|
3079
|
+
} else {
|
|
3080
|
+
console.log(`Log dates (${dates.length}):`);
|
|
3081
|
+
for (const date2 of dates) {
|
|
3082
|
+
console.log(` ${date2}`);
|
|
3083
|
+
}
|
|
3084
|
+
console.log(`
|
|
3085
|
+
Logs directory: ${logsDir}`);
|
|
3086
|
+
}
|
|
3087
|
+
}
|
|
3088
|
+
return;
|
|
3089
|
+
}
|
|
2499
3090
|
const sourceInput = String(options.source ?? "all").trim().toLowerCase();
|
|
2500
3091
|
if (json && follow) {
|
|
2501
3092
|
const message = "--follow is not supported with --json. Use human mode for streaming logs.";
|
|
@@ -2527,18 +3118,36 @@ function createLogsCommand(config) {
|
|
|
2527
3118
|
const tail = Number.isFinite(tailInput) && tailInput > 0 ? tailInput : 80;
|
|
2528
3119
|
const intervalInput = Number.parseInt(String(options.intervalMs ?? "1000"), 10);
|
|
2529
3120
|
const intervalMs = Number.isFinite(intervalInput) && intervalInput > 0 ? intervalInput : 1e3;
|
|
2530
|
-
|
|
2531
|
-
|
|
3121
|
+
let paths;
|
|
3122
|
+
try {
|
|
3123
|
+
paths = resolvePersistedLogPaths(config.dataDir, meta, date);
|
|
3124
|
+
} catch (error) {
|
|
3125
|
+
const message = error instanceof Error ? error.message : "Invalid --date value.";
|
|
3126
|
+
if (json) {
|
|
3127
|
+
printJsonError({
|
|
3128
|
+
code: "LOG_DATE_INVALID",
|
|
3129
|
+
message,
|
|
3130
|
+
recoverable: true,
|
|
3131
|
+
suggestion: "Retry with --date in YYYY-MM-DD format.",
|
|
3132
|
+
details: { date }
|
|
3133
|
+
});
|
|
3134
|
+
} else {
|
|
3135
|
+
console.error(`Error: ${message}`);
|
|
3136
|
+
}
|
|
3137
|
+
process.exit(1);
|
|
3138
|
+
}
|
|
2532
3139
|
const targets = resolvePersistedLogTargets(paths, source);
|
|
2533
|
-
|
|
2534
|
-
|
|
3140
|
+
const displayDate = date ?? inferDateFromPaths(paths);
|
|
3141
|
+
if (source !== "all" && targets.length === 1 && !existsSync8(targets[0].path)) {
|
|
3142
|
+
const dateLabel = displayDate ? ` on ${displayDate}` : "";
|
|
3143
|
+
const message = `Log file not found for source ${source}${dateLabel}: ${targets[0].path}`;
|
|
2535
3144
|
if (json) {
|
|
2536
3145
|
printJsonError({
|
|
2537
3146
|
code: "LOG_FILE_NOT_FOUND",
|
|
2538
3147
|
message,
|
|
2539
3148
|
recoverable: true,
|
|
2540
|
-
suggestion: "Start node/runtime or generate activity, then retry logs command.",
|
|
2541
|
-
details: { source, path: targets[0].path }
|
|
3149
|
+
suggestion: "Start node/runtime or generate activity, then retry logs command. Use --list-dates to see available dates.",
|
|
3150
|
+
details: { source, date: displayDate ?? null, path: targets[0].path }
|
|
2542
3151
|
});
|
|
2543
3152
|
} else {
|
|
2544
3153
|
console.error(`Error: ${message}`);
|
|
@@ -2547,7 +3156,7 @@ function createLogsCommand(config) {
|
|
|
2547
3156
|
}
|
|
2548
3157
|
const entries = [];
|
|
2549
3158
|
for (const target of targets) {
|
|
2550
|
-
const exists =
|
|
3159
|
+
const exists = existsSync8(target.path);
|
|
2551
3160
|
let lines = [];
|
|
2552
3161
|
if (exists) {
|
|
2553
3162
|
try {
|
|
@@ -2582,6 +3191,7 @@ function createLogsCommand(config) {
|
|
|
2582
3191
|
printJsonSuccess({
|
|
2583
3192
|
source,
|
|
2584
3193
|
tail,
|
|
3194
|
+
date: displayDate ?? null,
|
|
2585
3195
|
entries: entries.map((entry) => ({
|
|
2586
3196
|
source: entry.source,
|
|
2587
3197
|
title: entry.title,
|
|
@@ -2593,7 +3203,8 @@ function createLogsCommand(config) {
|
|
|
2593
3203
|
});
|
|
2594
3204
|
return;
|
|
2595
3205
|
}
|
|
2596
|
-
|
|
3206
|
+
const headerDate = displayDate ? `, date: ${displayDate}` : "";
|
|
3207
|
+
console.log(`Logs (source: ${source}${headerDate}, tail: ${tail})`);
|
|
2597
3208
|
for (const entry of entries) {
|
|
2598
3209
|
console.log(`
|
|
2599
3210
|
${entry.title}: ${entry.path}`);
|
|
@@ -2647,7 +3258,7 @@ Following logs (interval: ${intervalMs}ms). Press Ctrl+C to stop.`);
|
|
|
2647
3258
|
for (const target of targets) {
|
|
2648
3259
|
const state = states.get(target.source);
|
|
2649
3260
|
if (!state) continue;
|
|
2650
|
-
if (!
|
|
3261
|
+
if (!existsSync8(state.path)) {
|
|
2651
3262
|
state.offset = 0;
|
|
2652
3263
|
state.remainder = "";
|
|
2653
3264
|
continue;
|
|
@@ -2678,28 +3289,199 @@ Following logs (interval: ${intervalMs}ms). Press Ctrl+C to stop.`);
|
|
|
2678
3289
|
});
|
|
2679
3290
|
});
|
|
2680
3291
|
}
|
|
3292
|
+
function inferDateFromPaths(paths) {
|
|
3293
|
+
const candidate = paths.runtimeAlerts.split("/").at(-2);
|
|
3294
|
+
if (!candidate || !DATE_DIR_PATTERN2.test(candidate)) {
|
|
3295
|
+
return void 0;
|
|
3296
|
+
}
|
|
3297
|
+
const stdoutDate = paths.fnnStdout.split("/").at(-2);
|
|
3298
|
+
const stderrDate = paths.fnnStderr.split("/").at(-2);
|
|
3299
|
+
if (stdoutDate !== candidate || stderrDate !== candidate) {
|
|
3300
|
+
return void 0;
|
|
3301
|
+
}
|
|
3302
|
+
return candidate;
|
|
3303
|
+
}
|
|
2681
3304
|
|
|
2682
3305
|
// src/commands/node.ts
|
|
2683
|
-
import { nodeIdToPeerId as nodeIdToPeerId2, scriptToAddress as scriptToAddress2 } from "@fiber-pay/sdk";
|
|
2684
3306
|
import { Command as Command8 } from "commander";
|
|
2685
3307
|
|
|
3308
|
+
// src/lib/node-info.ts
|
|
3309
|
+
async function runNodeInfoCommand(config, options) {
|
|
3310
|
+
const rpc = await createReadyRpcClient(config);
|
|
3311
|
+
const nodeInfo = await rpc.nodeInfo();
|
|
3312
|
+
if (options.json) {
|
|
3313
|
+
printJsonSuccess(nodeInfo);
|
|
3314
|
+
return;
|
|
3315
|
+
}
|
|
3316
|
+
console.log("\u2705 Node info retrieved");
|
|
3317
|
+
console.log(` Version: ${nodeInfo.version}`);
|
|
3318
|
+
console.log(` Commit: ${nodeInfo.commit_hash}`);
|
|
3319
|
+
console.log(` Node ID: ${nodeInfo.node_id}`);
|
|
3320
|
+
if (nodeInfo.features.length > 0) {
|
|
3321
|
+
console.log(" Features:");
|
|
3322
|
+
for (const feature of nodeInfo.features) {
|
|
3323
|
+
console.log(` - ${sanitizeForTerminal(feature)}`);
|
|
3324
|
+
}
|
|
3325
|
+
}
|
|
3326
|
+
console.log(` Name: ${sanitizeForTerminal(nodeInfo.node_name ?? "-")}`);
|
|
3327
|
+
if (nodeInfo.addresses.length > 0) {
|
|
3328
|
+
console.log(" Addresses:");
|
|
3329
|
+
for (const address of nodeInfo.addresses) {
|
|
3330
|
+
console.log(` - ${sanitizeForTerminal(address)}`);
|
|
3331
|
+
}
|
|
3332
|
+
}
|
|
3333
|
+
console.log(` Chain Hash: ${nodeInfo.chain_hash}`);
|
|
3334
|
+
console.log(` Channels: ${BigInt(nodeInfo.channel_count)}`);
|
|
3335
|
+
console.log(` Pending Channels: ${BigInt(nodeInfo.pending_channel_count)}`);
|
|
3336
|
+
console.log(` Peers: ${BigInt(nodeInfo.peers_count)}`);
|
|
3337
|
+
if (nodeInfo.udt_cfg_infos.length > 0) {
|
|
3338
|
+
console.log(" UDT Configs:");
|
|
3339
|
+
for (const udt of nodeInfo.udt_cfg_infos) {
|
|
3340
|
+
console.log(` - Name: ${sanitizeForTerminal(udt.name)}`);
|
|
3341
|
+
console.log(` Script: ${JSON.stringify(udt.script, null, 6)}`);
|
|
3342
|
+
if (udt.auto_accept_amount) {
|
|
3343
|
+
console.log(` Auto Accept Amount: ${BigInt(udt.auto_accept_amount)}`);
|
|
3344
|
+
}
|
|
3345
|
+
if (udt.cell_deps.length > 0) {
|
|
3346
|
+
console.log(" Cell Deps:");
|
|
3347
|
+
for (const dep of udt.cell_deps) {
|
|
3348
|
+
console.log(
|
|
3349
|
+
` - Cell Dep: ${dep.cell_dep ? JSON.stringify(dep.cell_dep) : "null"}`
|
|
3350
|
+
);
|
|
3351
|
+
console.log(` Type ID: ${dep.type_id ? JSON.stringify(dep.type_id) : "null"}`);
|
|
3352
|
+
}
|
|
3353
|
+
}
|
|
3354
|
+
}
|
|
3355
|
+
}
|
|
3356
|
+
}
|
|
3357
|
+
|
|
3358
|
+
// src/lib/node-network.ts
|
|
3359
|
+
import { shannonsToCkb as shannonsToCkb5, toHex as toHex4 } from "@fiber-pay/sdk";
|
|
3360
|
+
async function runNodeNetworkCommand(config, options) {
|
|
3361
|
+
const rpc = await createReadyRpcClient(config);
|
|
3362
|
+
const [nodeInfo, localPeers, localChannels, graphNodes, graphChannels] = await Promise.all([
|
|
3363
|
+
rpc.nodeInfo(),
|
|
3364
|
+
rpc.listPeers(),
|
|
3365
|
+
rpc.listChannels({ include_closed: false }),
|
|
3366
|
+
rpc.graphNodes({}),
|
|
3367
|
+
rpc.graphChannels({})
|
|
3368
|
+
]);
|
|
3369
|
+
const graphNodesMap = /* @__PURE__ */ new Map();
|
|
3370
|
+
for (const node of graphNodes.nodes) {
|
|
3371
|
+
graphNodesMap.set(node.node_id, node);
|
|
3372
|
+
}
|
|
3373
|
+
const peerIdToNodeIdMap = /* @__PURE__ */ new Map();
|
|
3374
|
+
for (const peer of localPeers.peers) {
|
|
3375
|
+
peerIdToNodeIdMap.set(peer.peer_id, peer.pubkey);
|
|
3376
|
+
}
|
|
3377
|
+
const graphChannelsMap = /* @__PURE__ */ new Map();
|
|
3378
|
+
for (const channel of graphChannels.channels) {
|
|
3379
|
+
if (channel.channel_outpoint) {
|
|
3380
|
+
const outpointKey = `${channel.channel_outpoint.tx_hash}:${channel.channel_outpoint.index}`;
|
|
3381
|
+
graphChannelsMap.set(outpointKey, channel);
|
|
3382
|
+
}
|
|
3383
|
+
}
|
|
3384
|
+
const enrichedPeers = localPeers.peers.map((peer) => ({
|
|
3385
|
+
...peer,
|
|
3386
|
+
nodeInfo: graphNodesMap.get(peer.pubkey)
|
|
3387
|
+
}));
|
|
3388
|
+
const enrichedChannels = localChannels.channels.map((channel) => {
|
|
3389
|
+
const nodeId = peerIdToNodeIdMap.get(channel.peer_id) || channel.peer_id;
|
|
3390
|
+
const peerNodeInfo = graphNodesMap.get(nodeId);
|
|
3391
|
+
let graphChannelInfo;
|
|
3392
|
+
if (channel.channel_outpoint) {
|
|
3393
|
+
const outpointKey = `${channel.channel_outpoint.tx_hash}:${channel.channel_outpoint.index}`;
|
|
3394
|
+
graphChannelInfo = graphChannelsMap.get(outpointKey);
|
|
3395
|
+
}
|
|
3396
|
+
return {
|
|
3397
|
+
...channel,
|
|
3398
|
+
peerNodeInfo,
|
|
3399
|
+
graphChannelInfo
|
|
3400
|
+
};
|
|
3401
|
+
});
|
|
3402
|
+
const activeChannels = enrichedChannels.filter((ch) => ch.state?.state_name === "CHANNEL_READY");
|
|
3403
|
+
const totalChannelCapacityShannons = activeChannels.reduce((sum, ch) => {
|
|
3404
|
+
const capacity = ch.graphChannelInfo?.capacity ? ch.graphChannelInfo.capacity : toHex4(BigInt(ch.local_balance) + BigInt(ch.remote_balance));
|
|
3405
|
+
return sum + BigInt(capacity);
|
|
3406
|
+
}, 0n);
|
|
3407
|
+
const totalChannelCapacity = formatShannonsAsCkb(totalChannelCapacityShannons, 1);
|
|
3408
|
+
const networkData = {
|
|
3409
|
+
localNodeId: nodeInfo.node_id,
|
|
3410
|
+
peers: enrichedPeers,
|
|
3411
|
+
channels: enrichedChannels,
|
|
3412
|
+
graphNodes: graphNodes.nodes,
|
|
3413
|
+
graphChannels: graphChannels.channels,
|
|
3414
|
+
summary: {
|
|
3415
|
+
connectedPeers: enrichedPeers.length,
|
|
3416
|
+
activeChannels: activeChannels.length,
|
|
3417
|
+
totalChannelCapacity
|
|
3418
|
+
}
|
|
3419
|
+
};
|
|
3420
|
+
if (options.json) {
|
|
3421
|
+
printJsonSuccess(networkData);
|
|
3422
|
+
return;
|
|
3423
|
+
}
|
|
3424
|
+
printNodeNetworkHuman(networkData);
|
|
3425
|
+
}
|
|
3426
|
+
function printNodeNetworkHuman(data) {
|
|
3427
|
+
console.log("Node Network Overview");
|
|
3428
|
+
console.log("=====================");
|
|
3429
|
+
console.log("");
|
|
3430
|
+
console.log(`Connected Peers: ${data.summary.connectedPeers}`);
|
|
3431
|
+
console.log(`Active Channels: ${data.summary.activeChannels}`);
|
|
3432
|
+
console.log(`Total Channel Capacity: ${data.summary.totalChannelCapacity} CKB`);
|
|
3433
|
+
console.log("");
|
|
3434
|
+
if (data.peers.length > 0) {
|
|
3435
|
+
console.log("Peers:");
|
|
3436
|
+
console.log(" PEER_ID ALIAS ADDRESS VERSION");
|
|
3437
|
+
console.log(
|
|
3438
|
+
" --------------------------------------------------------------------------------"
|
|
3439
|
+
);
|
|
3440
|
+
for (const peer of data.peers) {
|
|
3441
|
+
const peerId = truncateMiddle(peer.peer_id, 10, 8).padEnd(22, " ");
|
|
3442
|
+
const alias = sanitizeForTerminal(peer.nodeInfo?.node_name || "(unnamed)").slice(0, 20).padEnd(20, " ");
|
|
3443
|
+
const address = truncateMiddle(sanitizeForTerminal(peer.address), 15, 8).padEnd(25, " ");
|
|
3444
|
+
const version = sanitizeForTerminal(peer.nodeInfo?.version || "?").slice(0, 8).padEnd(8, " ");
|
|
3445
|
+
console.log(` ${peerId} ${alias} ${address} ${version}`);
|
|
3446
|
+
}
|
|
3447
|
+
console.log("");
|
|
3448
|
+
}
|
|
3449
|
+
if (data.channels.length > 0) {
|
|
3450
|
+
console.log("Channels:");
|
|
3451
|
+
console.log(
|
|
3452
|
+
" CHANNEL_ID PEER_ALIAS STATE LOCAL_BAL REMOTE_BAL CAPACITY"
|
|
3453
|
+
);
|
|
3454
|
+
console.log(
|
|
3455
|
+
" -----------------------------------------------------------------------------------------------"
|
|
3456
|
+
);
|
|
3457
|
+
for (const channel of data.channels) {
|
|
3458
|
+
const channelId = truncateMiddle(channel.channel_id, 10, 8).padEnd(22, " ");
|
|
3459
|
+
const peerAlias = sanitizeForTerminal(channel.peerNodeInfo?.node_name || "(unnamed)").slice(0, 18).padEnd(18, " ");
|
|
3460
|
+
const state = (channel.state?.state_name || "UNKNOWN").slice(0, 13).padEnd(13, " ");
|
|
3461
|
+
const localBal = shannonsToCkb5(channel.local_balance).toFixed(1).padStart(11, " ");
|
|
3462
|
+
const remoteBal = shannonsToCkb5(channel.remote_balance).toFixed(1).padStart(11, " ");
|
|
3463
|
+
const capacity = channel.graphChannelInfo?.capacity ? shannonsToCkb5(channel.graphChannelInfo.capacity).toFixed(1).padStart(8, " ") : shannonsToCkb5(toHex4(BigInt(channel.local_balance) + BigInt(channel.remote_balance))).toFixed(1).padStart(8, " ");
|
|
3464
|
+
console.log(` ${channelId} ${peerAlias} ${state} ${localBal} ${remoteBal} ${capacity}`);
|
|
3465
|
+
}
|
|
3466
|
+
}
|
|
3467
|
+
}
|
|
3468
|
+
|
|
2686
3469
|
// src/lib/node-start.ts
|
|
2687
3470
|
import { spawn } from "child_process";
|
|
2688
|
-
import {
|
|
2689
|
-
import { join as
|
|
3471
|
+
import { mkdirSync as mkdirSync3 } from "fs";
|
|
3472
|
+
import { join as join7 } from "path";
|
|
2690
3473
|
import {
|
|
2691
3474
|
createKeyManager,
|
|
2692
3475
|
ensureFiberBinary,
|
|
2693
|
-
getDefaultBinaryPath,
|
|
2694
3476
|
ProcessManager
|
|
2695
3477
|
} from "@fiber-pay/node";
|
|
2696
3478
|
import { startRuntimeService } from "@fiber-pay/runtime";
|
|
2697
3479
|
|
|
2698
3480
|
// src/lib/bootnode.ts
|
|
2699
|
-
import { existsSync as
|
|
3481
|
+
import { existsSync as existsSync9, readFileSync as readFileSync5 } from "fs";
|
|
2700
3482
|
import { parse as parseYaml } from "yaml";
|
|
2701
3483
|
function extractBootnodeAddrs(configFilePath) {
|
|
2702
|
-
if (!
|
|
3484
|
+
if (!existsSync9(configFilePath)) return [];
|
|
2703
3485
|
try {
|
|
2704
3486
|
const content = readFileSync5(configFilePath, "utf-8");
|
|
2705
3487
|
const doc = parseYaml(content);
|
|
@@ -2730,8 +3512,8 @@ async function autoConnectBootnodes(rpc, bootnodes) {
|
|
|
2730
3512
|
}
|
|
2731
3513
|
|
|
2732
3514
|
// src/lib/node-migration.ts
|
|
2733
|
-
import { dirname } from "path";
|
|
2734
|
-
import { BinaryManager, MigrationManager } from "@fiber-pay/node";
|
|
3515
|
+
import { dirname as dirname2 } from "path";
|
|
3516
|
+
import { BinaryManager as BinaryManager2, MigrationManager } from "@fiber-pay/node";
|
|
2735
3517
|
|
|
2736
3518
|
// src/lib/migration-utils.ts
|
|
2737
3519
|
function replaceRawMigrateHint(message) {
|
|
@@ -2754,8 +3536,8 @@ async function runMigrationGuard(opts) {
|
|
|
2754
3536
|
return { checked: false, skippedReason: "store does not exist" };
|
|
2755
3537
|
}
|
|
2756
3538
|
const storePath = MigrationManager.resolveStorePath(dataDir);
|
|
2757
|
-
const binaryDir =
|
|
2758
|
-
const bm = new
|
|
3539
|
+
const binaryDir = dirname2(binaryPath);
|
|
3540
|
+
const bm = new BinaryManager2(binaryDir);
|
|
2759
3541
|
const migrateBinPath = bm.getMigrateBinaryPath();
|
|
2760
3542
|
let migrationCheck;
|
|
2761
3543
|
try {
|
|
@@ -2794,98 +3576,115 @@ async function runMigrationGuard(opts) {
|
|
|
2794
3576
|
return { checked: true, migrationCheck };
|
|
2795
3577
|
}
|
|
2796
3578
|
|
|
2797
|
-
// src/lib/
|
|
2798
|
-
import { spawnSync } from "child_process";
|
|
2799
|
-
|
|
2800
|
-
|
|
2801
|
-
|
|
2802
|
-
|
|
2803
|
-
return { path: binaryPath, ready: false, version: "unknown" };
|
|
3579
|
+
// src/lib/runtime-port.ts
|
|
3580
|
+
import { spawnSync as spawnSync2 } from "child_process";
|
|
3581
|
+
function parsePortFromListen(listen) {
|
|
3582
|
+
const value = listen.trim();
|
|
3583
|
+
if (!value) {
|
|
3584
|
+
return void 0;
|
|
2804
3585
|
}
|
|
2805
|
-
|
|
2806
|
-
|
|
2807
|
-
|
|
2808
|
-
return { path: binaryPath, ready: false, version: "unknown" };
|
|
2809
|
-
}
|
|
2810
|
-
const output = `${result.stdout ?? ""}${result.stderr ?? ""}`.trim();
|
|
2811
|
-
const firstLine = output.split("\n").find((line) => line.trim().length > 0) ?? "unknown";
|
|
2812
|
-
return { path: binaryPath, ready: true, version: firstLine.trim() };
|
|
2813
|
-
} catch {
|
|
2814
|
-
return { path: binaryPath, ready: false, version: "unknown" };
|
|
3586
|
+
const lastColon = value.lastIndexOf(":");
|
|
3587
|
+
if (lastColon < 0 || lastColon === value.length - 1) {
|
|
3588
|
+
return void 0;
|
|
2815
3589
|
}
|
|
3590
|
+
const port = Number(value.slice(lastColon + 1));
|
|
3591
|
+
if (!Number.isInteger(port) || port < 1 || port > 65535) {
|
|
3592
|
+
return void 0;
|
|
3593
|
+
}
|
|
3594
|
+
return port;
|
|
2816
3595
|
}
|
|
2817
|
-
function
|
|
2818
|
-
|
|
2819
|
-
const
|
|
2820
|
-
if (
|
|
2821
|
-
|
|
3596
|
+
function extractFirstPidFromLsofOutput(output) {
|
|
3597
|
+
for (const line of output.split("\n")) {
|
|
3598
|
+
const trimmed = line.trim();
|
|
3599
|
+
if (!trimmed.startsWith("p") || trimmed.length < 2) {
|
|
3600
|
+
continue;
|
|
2822
3601
|
}
|
|
2823
|
-
const
|
|
2824
|
-
if (
|
|
2825
|
-
return
|
|
3602
|
+
const pid = Number(trimmed.slice(1));
|
|
3603
|
+
if (Number.isInteger(pid) && pid > 0) {
|
|
3604
|
+
return pid;
|
|
2826
3605
|
}
|
|
2827
|
-
const firstLine = output.split("\n").find((line) => line.trim().length > 0);
|
|
2828
|
-
return firstLine?.trim() ?? "unknown";
|
|
2829
|
-
} catch {
|
|
2830
|
-
return "unknown";
|
|
2831
3606
|
}
|
|
3607
|
+
return void 0;
|
|
2832
3608
|
}
|
|
2833
|
-
function
|
|
2834
|
-
const
|
|
2835
|
-
|
|
2836
|
-
|
|
3609
|
+
function readProcessCommand(pid) {
|
|
3610
|
+
const result = spawnSync2("ps", ["-p", String(pid), "-o", "command="], {
|
|
3611
|
+
encoding: "utf-8"
|
|
3612
|
+
});
|
|
3613
|
+
if (result.error || result.status !== 0) {
|
|
3614
|
+
return void 0;
|
|
2837
3615
|
}
|
|
2838
|
-
|
|
3616
|
+
const command = (result.stdout ?? "").trim();
|
|
3617
|
+
return command.length > 0 ? command : void 0;
|
|
2839
3618
|
}
|
|
2840
|
-
function
|
|
2841
|
-
const
|
|
2842
|
-
|
|
2843
|
-
|
|
2844
|
-
[
|
|
2845
|
-
cliEntrypoint,
|
|
2846
|
-
"--data-dir",
|
|
2847
|
-
params.dataDir,
|
|
2848
|
-
"--rpc-url",
|
|
2849
|
-
params.rpcUrl,
|
|
2850
|
-
"runtime",
|
|
2851
|
-
"start",
|
|
2852
|
-
"--daemon",
|
|
2853
|
-
"--fiber-rpc-url",
|
|
2854
|
-
params.rpcUrl,
|
|
2855
|
-
"--proxy-listen",
|
|
2856
|
-
params.proxyListen,
|
|
2857
|
-
"--state-file",
|
|
2858
|
-
params.stateFilePath,
|
|
2859
|
-
"--alert-log-file",
|
|
2860
|
-
params.alertLogFile,
|
|
2861
|
-
"--json"
|
|
2862
|
-
],
|
|
2863
|
-
{ encoding: "utf-8" }
|
|
2864
|
-
);
|
|
2865
|
-
if (result.status === 0) {
|
|
2866
|
-
return { ok: true };
|
|
3619
|
+
function findListeningProcessByPort(listen) {
|
|
3620
|
+
const port = parsePortFromListen(listen);
|
|
3621
|
+
if (!port) {
|
|
3622
|
+
return void 0;
|
|
2867
3623
|
}
|
|
2868
|
-
const
|
|
2869
|
-
|
|
2870
|
-
|
|
2871
|
-
|
|
3624
|
+
const result = spawnSync2("lsof", ["-nP", `-iTCP:${port}`, "-sTCP:LISTEN", "-Fp"], {
|
|
3625
|
+
encoding: "utf-8"
|
|
3626
|
+
});
|
|
3627
|
+
if (result.error || result.status !== 0) {
|
|
3628
|
+
return void 0;
|
|
3629
|
+
}
|
|
3630
|
+
const pid = extractFirstPidFromLsofOutput(result.stdout ?? "");
|
|
3631
|
+
if (!pid) {
|
|
3632
|
+
return void 0;
|
|
3633
|
+
}
|
|
3634
|
+
return {
|
|
3635
|
+
pid,
|
|
3636
|
+
command: readProcessCommand(pid)
|
|
3637
|
+
};
|
|
2872
3638
|
}
|
|
2873
|
-
function
|
|
2874
|
-
|
|
2875
|
-
|
|
2876
|
-
|
|
2877
|
-
|
|
2878
|
-
|
|
2879
|
-
|
|
2880
|
-
|
|
2881
|
-
|
|
2882
|
-
|
|
2883
|
-
|
|
2884
|
-
|
|
2885
|
-
|
|
2886
|
-
|
|
2887
|
-
|
|
2888
|
-
|
|
3639
|
+
function isFiberRuntimeCommand(command) {
|
|
3640
|
+
if (!command) {
|
|
3641
|
+
return false;
|
|
3642
|
+
}
|
|
3643
|
+
const normalized = command.toLowerCase();
|
|
3644
|
+
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");
|
|
3645
|
+
if (!hasFiberIdentifier) {
|
|
3646
|
+
return false;
|
|
3647
|
+
}
|
|
3648
|
+
return normalized.includes("runtime") && normalized.includes("start");
|
|
3649
|
+
}
|
|
3650
|
+
async function terminateProcess(pid, timeoutMs = 5e3) {
|
|
3651
|
+
if (!isProcessRunning(pid)) {
|
|
3652
|
+
return true;
|
|
3653
|
+
}
|
|
3654
|
+
try {
|
|
3655
|
+
process.kill(pid, "SIGTERM");
|
|
3656
|
+
} catch (error) {
|
|
3657
|
+
if (error.code === "ESRCH") {
|
|
3658
|
+
return true;
|
|
3659
|
+
}
|
|
3660
|
+
return false;
|
|
3661
|
+
}
|
|
3662
|
+
const deadline = Date.now() + timeoutMs;
|
|
3663
|
+
while (Date.now() < deadline) {
|
|
3664
|
+
if (!isProcessRunning(pid)) {
|
|
3665
|
+
return true;
|
|
3666
|
+
}
|
|
3667
|
+
await new Promise((resolve2) => setTimeout(resolve2, 100));
|
|
3668
|
+
}
|
|
3669
|
+
if (!isProcessRunning(pid)) {
|
|
3670
|
+
return true;
|
|
3671
|
+
}
|
|
3672
|
+
try {
|
|
3673
|
+
process.kill(pid, "SIGKILL");
|
|
3674
|
+
} catch (error) {
|
|
3675
|
+
if (error.code === "ESRCH") {
|
|
3676
|
+
return true;
|
|
3677
|
+
}
|
|
3678
|
+
return false;
|
|
3679
|
+
}
|
|
3680
|
+
const killDeadline = Date.now() + 1e3;
|
|
3681
|
+
while (Date.now() < killDeadline) {
|
|
3682
|
+
if (!isProcessRunning(pid)) {
|
|
3683
|
+
return true;
|
|
3684
|
+
}
|
|
3685
|
+
await new Promise((resolve2) => setTimeout(resolve2, 50));
|
|
3686
|
+
}
|
|
3687
|
+
return !isProcessRunning(pid);
|
|
2889
3688
|
}
|
|
2890
3689
|
|
|
2891
3690
|
// src/lib/node-start.ts
|
|
@@ -2997,19 +3796,22 @@ async function runNodeStartCommand(config, options) {
|
|
|
2997
3796
|
options.runtimeProxyListen ?? config.runtimeProxyListen ?? "127.0.0.1:8229"
|
|
2998
3797
|
);
|
|
2999
3798
|
const proxyListenSource = options.runtimeProxyListen ? "cli" : config.runtimeProxyListen ? "profile" : "default";
|
|
3000
|
-
const runtimeStateFilePath =
|
|
3001
|
-
const
|
|
3002
|
-
|
|
3003
|
-
|
|
3004
|
-
const
|
|
3005
|
-
|
|
3006
|
-
|
|
3007
|
-
|
|
3799
|
+
const runtimeStateFilePath = join7(config.dataDir, "runtime-state.json");
|
|
3800
|
+
const logsBaseDir = join7(config.dataDir, "logs");
|
|
3801
|
+
mkdirSync3(logsBaseDir, { recursive: true });
|
|
3802
|
+
resolveLogDirForDate(config.dataDir);
|
|
3803
|
+
const resolvedBinary = resolveBinaryPath(config);
|
|
3804
|
+
const binaryPath = resolvedBinary.binaryPath;
|
|
3805
|
+
if (resolvedBinary.source === "profile-managed") {
|
|
3806
|
+
const installDir = getBinaryManagerInstallDirOrThrow(resolvedBinary);
|
|
3807
|
+
await ensureFiberBinary({ installDir });
|
|
3808
|
+
}
|
|
3008
3809
|
const binaryVersion = getBinaryVersion(binaryPath);
|
|
3009
3810
|
const configFilePath = ensureNodeConfigFile(config.dataDir, config.network);
|
|
3010
3811
|
emitStage("binary_resolved", "ok", {
|
|
3011
3812
|
binaryPath,
|
|
3012
3813
|
binaryVersion,
|
|
3814
|
+
binarySource: resolvedBinary.source,
|
|
3013
3815
|
configFilePath
|
|
3014
3816
|
});
|
|
3015
3817
|
if (!json) {
|
|
@@ -3070,11 +3872,11 @@ async function runNodeStartCommand(config, options) {
|
|
|
3070
3872
|
removePidFile(config.dataDir);
|
|
3071
3873
|
});
|
|
3072
3874
|
processManager.on("stdout", (text) => {
|
|
3073
|
-
|
|
3875
|
+
appendToTodayLog(config.dataDir, "fnn.stdout.log", text);
|
|
3074
3876
|
emitFnnLog("stdout", text);
|
|
3075
3877
|
});
|
|
3076
3878
|
processManager.on("stderr", (text) => {
|
|
3077
|
-
|
|
3879
|
+
appendToTodayLog(config.dataDir, "fnn.stderr.log", text);
|
|
3078
3880
|
emitFnnLog("stderr", text);
|
|
3079
3881
|
});
|
|
3080
3882
|
await processManager.start();
|
|
@@ -3119,13 +3921,38 @@ async function runNodeStartCommand(config, options) {
|
|
|
3119
3921
|
process.exit(1);
|
|
3120
3922
|
}
|
|
3121
3923
|
try {
|
|
3924
|
+
const runtimePortProcess = findListeningProcessByPort(runtimeProxyListen);
|
|
3925
|
+
if (runtimePortProcess) {
|
|
3926
|
+
if (isFiberRuntimeCommand(runtimePortProcess.command)) {
|
|
3927
|
+
const terminated = await terminateProcess(runtimePortProcess.pid);
|
|
3928
|
+
if (!terminated) {
|
|
3929
|
+
throw new Error(
|
|
3930
|
+
`Runtime proxy ${runtimeProxyListen} is occupied by stale fiber-pay runtime PID ${runtimePortProcess.pid}, but termination failed`
|
|
3931
|
+
);
|
|
3932
|
+
}
|
|
3933
|
+
removeRuntimeFiles(config.dataDir);
|
|
3934
|
+
emitStage("runtime_preflight", "ok", {
|
|
3935
|
+
proxyListen: runtimeProxyListen,
|
|
3936
|
+
cleanedStaleProcessPid: runtimePortProcess.pid
|
|
3937
|
+
});
|
|
3938
|
+
} else if (runtimePortProcess.command) {
|
|
3939
|
+
const details = runtimePortProcess.command ? `PID ${runtimePortProcess.pid} (${runtimePortProcess.command})` : `PID ${runtimePortProcess.pid}`;
|
|
3940
|
+
throw new Error(
|
|
3941
|
+
`Runtime proxy ${runtimeProxyListen} is already in use by non-fiber-pay process: ${details}`
|
|
3942
|
+
);
|
|
3943
|
+
} else {
|
|
3944
|
+
throw new Error(
|
|
3945
|
+
`Runtime proxy ${runtimeProxyListen} is already in use by process PID ${runtimePortProcess.pid}. Unable to determine command owner; inspect this PID manually before retrying.`
|
|
3946
|
+
);
|
|
3947
|
+
}
|
|
3948
|
+
}
|
|
3122
3949
|
if (runtimeDaemon) {
|
|
3123
3950
|
const daemonStart = startRuntimeDaemonFromNode({
|
|
3124
3951
|
dataDir: config.dataDir,
|
|
3125
3952
|
rpcUrl: config.rpcUrl,
|
|
3126
3953
|
proxyListen: runtimeProxyListen,
|
|
3127
3954
|
stateFilePath: runtimeStateFilePath,
|
|
3128
|
-
|
|
3955
|
+
alertLogsBaseDir: logsBaseDir
|
|
3129
3956
|
});
|
|
3130
3957
|
if (!daemonStart.ok) {
|
|
3131
3958
|
throw new Error(daemonStart.message);
|
|
@@ -3140,23 +3967,25 @@ async function runNodeStartCommand(config, options) {
|
|
|
3140
3967
|
storage: {
|
|
3141
3968
|
stateFilePath: runtimeStateFilePath
|
|
3142
3969
|
},
|
|
3143
|
-
alerts: [{ type: "stdout" }, { type: "file",
|
|
3970
|
+
alerts: [{ type: "stdout" }, { type: "daily-file", baseLogsDir: logsBaseDir }],
|
|
3144
3971
|
jobs: {
|
|
3145
3972
|
enabled: true,
|
|
3146
|
-
dbPath:
|
|
3973
|
+
dbPath: join7(config.dataDir, "runtime-jobs.db")
|
|
3147
3974
|
}
|
|
3148
3975
|
});
|
|
3149
3976
|
const runtimeStatus = runtime.service.getStatus();
|
|
3150
3977
|
writeRuntimePid(config.dataDir, process.pid);
|
|
3978
|
+
const todayLogDir = resolveLogDirForDate(config.dataDir);
|
|
3151
3979
|
writeRuntimeMeta(config.dataDir, {
|
|
3152
3980
|
pid: process.pid,
|
|
3153
3981
|
startedAt: runtimeStatus.startedAt,
|
|
3154
3982
|
fiberRpcUrl: runtimeStatus.targetUrl,
|
|
3155
3983
|
proxyListen: runtimeStatus.proxyListen,
|
|
3156
3984
|
stateFilePath: runtimeStateFilePath,
|
|
3157
|
-
alertLogFilePath:
|
|
3158
|
-
fnnStdoutLogPath,
|
|
3159
|
-
fnnStderrLogPath,
|
|
3985
|
+
alertLogFilePath: join7(todayLogDir, "runtime.alerts.jsonl"),
|
|
3986
|
+
fnnStdoutLogPath: join7(todayLogDir, "fnn.stdout.log"),
|
|
3987
|
+
fnnStderrLogPath: join7(todayLogDir, "fnn.stderr.log"),
|
|
3988
|
+
logsBaseDir,
|
|
3160
3989
|
daemon: false
|
|
3161
3990
|
});
|
|
3162
3991
|
}
|
|
@@ -3226,7 +4055,7 @@ async function runNodeStartCommand(config, options) {
|
|
|
3226
4055
|
process.exit(1);
|
|
3227
4056
|
}
|
|
3228
4057
|
emitStage("rpc_ready", "ok", { rpcUrl: config.rpcUrl });
|
|
3229
|
-
const bootnodes = nodeConfig.configFilePath ? extractBootnodeAddrs(nodeConfig.configFilePath) : extractBootnodeAddrs(
|
|
4058
|
+
const bootnodes = nodeConfig.configFilePath ? extractBootnodeAddrs(nodeConfig.configFilePath) : extractBootnodeAddrs(join7(config.dataDir, "config.yml"));
|
|
3230
4059
|
if (bootnodes.length > 0) {
|
|
3231
4060
|
await autoConnectBootnodes(rpc, bootnodes);
|
|
3232
4061
|
}
|
|
@@ -3267,9 +4096,8 @@ async function runNodeStartCommand(config, options) {
|
|
|
3267
4096
|
proxyUrl: `http://${runtimeProxyListen}`,
|
|
3268
4097
|
proxyListenSource,
|
|
3269
4098
|
logs: {
|
|
3270
|
-
|
|
3271
|
-
|
|
3272
|
-
runtimeAlerts: runtimeAlertLogPath
|
|
4099
|
+
baseDir: logsBaseDir,
|
|
4100
|
+
todayDir: resolveLogDirForDate(config.dataDir)
|
|
3273
4101
|
}
|
|
3274
4102
|
});
|
|
3275
4103
|
} else {
|
|
@@ -3279,7 +4107,7 @@ async function runNodeStartCommand(config, options) {
|
|
|
3279
4107
|
` Runtime proxy: http://${runtimeProxyListen} (browser-safe endpoint + monitoring)`
|
|
3280
4108
|
);
|
|
3281
4109
|
console.log(` Runtime mode: ${runtimeDaemon ? "daemon" : "embedded"}`);
|
|
3282
|
-
console.log(` Log files: ${
|
|
4110
|
+
console.log(` Log files: ${logsBaseDir}`);
|
|
3283
4111
|
console.log(" Press Ctrl+C to stop.");
|
|
3284
4112
|
}
|
|
3285
4113
|
let shutdownRequested = false;
|
|
@@ -3347,8 +4175,6 @@ async function runNodeStartCommand(config, options) {
|
|
|
3347
4175
|
|
|
3348
4176
|
// src/lib/node-status.ts
|
|
3349
4177
|
import { existsSync as existsSync10 } from "fs";
|
|
3350
|
-
import { join as join6 } from "path";
|
|
3351
|
-
import { getFiberBinaryInfo as getFiberBinaryInfo2 } from "@fiber-pay/node";
|
|
3352
4178
|
import {
|
|
3353
4179
|
buildMultiaddrFromNodeId,
|
|
3354
4180
|
buildMultiaddrFromRpcUrl,
|
|
@@ -3515,24 +4341,30 @@ async function runNodeStatusCommand(config, options) {
|
|
|
3515
4341
|
const json = Boolean(options.json);
|
|
3516
4342
|
const pid = readPidFile(config.dataDir);
|
|
3517
4343
|
const resolvedRpc = resolveRpcEndpoint(config);
|
|
3518
|
-
const
|
|
3519
|
-
const binaryInfo = config.binaryPath ? getCustomBinaryState(config.binaryPath) : await getFiberBinaryInfo2(join6(config.dataDir, "bin"));
|
|
4344
|
+
const { resolvedBinary, info: binaryInfo } = await getBinaryDetails(config);
|
|
3520
4345
|
const configExists = existsSync10(config.configPath);
|
|
3521
4346
|
const nodeRunning = Boolean(pid && isProcessRunning(pid));
|
|
3522
4347
|
let rpcResponsive = false;
|
|
3523
4348
|
let nodeId = null;
|
|
4349
|
+
let nodeName = null;
|
|
4350
|
+
let addresses = [];
|
|
4351
|
+
let chainHash = null;
|
|
4352
|
+
let version = null;
|
|
3524
4353
|
let peerId = null;
|
|
4354
|
+
let peersCount = 0;
|
|
3525
4355
|
let peerIdError = null;
|
|
3526
4356
|
let multiaddr = null;
|
|
3527
4357
|
let multiaddrError = null;
|
|
3528
4358
|
let multiaddrInferred = false;
|
|
3529
4359
|
let channelsTotal = 0;
|
|
3530
4360
|
let channelsReady = 0;
|
|
4361
|
+
let pendingChannelCount = 0;
|
|
3531
4362
|
let canSend = false;
|
|
3532
4363
|
let canReceive = false;
|
|
3533
4364
|
let localCkb = 0;
|
|
3534
4365
|
let remoteCkb = 0;
|
|
3535
4366
|
let fundingAddress = null;
|
|
4367
|
+
let fundingLockScript = null;
|
|
3536
4368
|
let fundingCkb = 0;
|
|
3537
4369
|
let fundingBalanceError = null;
|
|
3538
4370
|
if (nodeRunning) {
|
|
@@ -3542,6 +4374,11 @@ async function runNodeStatusCommand(config, options) {
|
|
|
3542
4374
|
const channels = await rpc.listChannels({ include_closed: false });
|
|
3543
4375
|
rpcResponsive = true;
|
|
3544
4376
|
nodeId = nodeInfo.node_id;
|
|
4377
|
+
nodeName = nodeInfo.node_name;
|
|
4378
|
+
addresses = nodeInfo.addresses;
|
|
4379
|
+
chainHash = nodeInfo.chain_hash;
|
|
4380
|
+
version = nodeInfo.version;
|
|
4381
|
+
peersCount = parseInt(nodeInfo.peers_count, 16);
|
|
3545
4382
|
try {
|
|
3546
4383
|
peerId = await nodeIdToPeerId(nodeInfo.node_id);
|
|
3547
4384
|
} catch (error) {
|
|
@@ -3568,18 +4405,17 @@ async function runNodeStatusCommand(config, options) {
|
|
|
3568
4405
|
(channel) => channel.state?.state_name === ChannelState2.ChannelReady
|
|
3569
4406
|
);
|
|
3570
4407
|
channelsReady = readyChannels.length;
|
|
4408
|
+
pendingChannelCount = Math.max(channelsTotal - channelsReady, 0);
|
|
3571
4409
|
const liquidity = summarizeChannelLiquidity(readyChannels);
|
|
3572
4410
|
canSend = liquidity.canSend;
|
|
3573
4411
|
canReceive = liquidity.canReceive;
|
|
3574
4412
|
localCkb = liquidity.localCkb;
|
|
3575
4413
|
remoteCkb = liquidity.remoteCkb;
|
|
3576
4414
|
fundingAddress = scriptToAddress(nodeInfo.default_funding_lock_script, config.network);
|
|
4415
|
+
fundingLockScript = nodeInfo.default_funding_lock_script;
|
|
3577
4416
|
if (config.ckbRpcUrl) {
|
|
3578
4417
|
try {
|
|
3579
|
-
const fundingBalance = await getLockBalanceShannons(
|
|
3580
|
-
config.ckbRpcUrl,
|
|
3581
|
-
nodeInfo.default_funding_lock_script
|
|
3582
|
-
);
|
|
4418
|
+
const fundingBalance = await getLockBalanceShannons(config.ckbRpcUrl, fundingLockScript);
|
|
3583
4419
|
fundingCkb = Number(fundingBalance) / 1e8;
|
|
3584
4420
|
} catch (error) {
|
|
3585
4421
|
fundingBalanceError = error instanceof Error ? error.message : "Failed to query CKB balance for funding address";
|
|
@@ -3610,18 +4446,25 @@ async function runNodeStatusCommand(config, options) {
|
|
|
3610
4446
|
rpcTarget: resolvedRpc.target,
|
|
3611
4447
|
resolvedRpcUrl: resolvedRpc.url,
|
|
3612
4448
|
nodeId,
|
|
4449
|
+
nodeName,
|
|
4450
|
+
addresses,
|
|
4451
|
+
chainHash,
|
|
4452
|
+
version,
|
|
3613
4453
|
peerId,
|
|
4454
|
+
peersCount,
|
|
3614
4455
|
peerIdError,
|
|
3615
4456
|
multiaddr,
|
|
3616
4457
|
multiaddrError,
|
|
3617
4458
|
multiaddrInferred,
|
|
4459
|
+
fundingLockScript,
|
|
3618
4460
|
checks: {
|
|
3619
4461
|
binary: {
|
|
3620
4462
|
path: binaryInfo.path,
|
|
3621
4463
|
ready: binaryInfo.ready,
|
|
3622
4464
|
version: binaryInfo.version,
|
|
3623
|
-
source:
|
|
3624
|
-
managedPath:
|
|
4465
|
+
source: resolvedBinary.source,
|
|
4466
|
+
managedPath: resolvedBinary.managedPath,
|
|
4467
|
+
resolvedPath: resolvedBinary.binaryPath
|
|
3625
4468
|
},
|
|
3626
4469
|
config: {
|
|
3627
4470
|
path: config.configPath,
|
|
@@ -3639,6 +4482,7 @@ async function runNodeStatusCommand(config, options) {
|
|
|
3639
4482
|
channels: {
|
|
3640
4483
|
total: channelsTotal,
|
|
3641
4484
|
ready: channelsReady,
|
|
4485
|
+
pending: pendingChannelCount,
|
|
3642
4486
|
canSend,
|
|
3643
4487
|
canReceive
|
|
3644
4488
|
}
|
|
@@ -3665,6 +4509,15 @@ async function runNodeStatusCommand(config, options) {
|
|
|
3665
4509
|
console.log(`\u2705 Node is running (PID: ${output.pid})`);
|
|
3666
4510
|
if (output.rpcResponsive) {
|
|
3667
4511
|
console.log(` Node ID: ${String(output.nodeId)}`);
|
|
4512
|
+
if (output.nodeName) {
|
|
4513
|
+
console.log(` Name: ${sanitizeForTerminal(output.nodeName)}`);
|
|
4514
|
+
}
|
|
4515
|
+
if (output.version) {
|
|
4516
|
+
console.log(` Version: ${sanitizeForTerminal(output.version)}`);
|
|
4517
|
+
}
|
|
4518
|
+
if (output.chainHash) {
|
|
4519
|
+
console.log(` Chain Hash: ${String(output.chainHash)}`);
|
|
4520
|
+
}
|
|
3668
4521
|
if (output.peerId) {
|
|
3669
4522
|
console.log(` Peer ID: ${String(output.peerId)}`);
|
|
3670
4523
|
} else if (output.peerIdError) {
|
|
@@ -3680,6 +4533,12 @@ async function runNodeStatusCommand(config, options) {
|
|
|
3680
4533
|
} else {
|
|
3681
4534
|
console.log(" Multiaddr: unavailable");
|
|
3682
4535
|
}
|
|
4536
|
+
if (output.addresses.length > 0) {
|
|
4537
|
+
console.log(" Addresses:");
|
|
4538
|
+
for (const address of output.addresses) {
|
|
4539
|
+
console.log(` - ${sanitizeForTerminal(address)}`);
|
|
4540
|
+
}
|
|
4541
|
+
}
|
|
3683
4542
|
} else {
|
|
3684
4543
|
console.log(" \u26A0\uFE0F RPC not responding");
|
|
3685
4544
|
}
|
|
@@ -3691,11 +4550,17 @@ async function runNodeStatusCommand(config, options) {
|
|
|
3691
4550
|
console.log("");
|
|
3692
4551
|
console.log("Diagnostics");
|
|
3693
4552
|
console.log(` Binary: ${output.checks.binary.ready ? "ready" : "missing"}`);
|
|
4553
|
+
console.log(` Binary Path: ${output.checks.binary.resolvedPath}`);
|
|
3694
4554
|
console.log(` Config: ${output.checks.config.exists ? "present" : "missing"}`);
|
|
3695
4555
|
console.log(` RPC: ${output.checks.node.rpcReachable ? "reachable" : "unreachable"}`);
|
|
3696
4556
|
console.log(
|
|
3697
|
-
` Channels: ${output.checks.channels.ready}/${output.checks.channels.total} ready/total`
|
|
4557
|
+
` Channels: ${output.checks.channels.ready}/${output.checks.channels.pending}/${output.checks.channels.total} ready/pending/total`
|
|
3698
4558
|
);
|
|
4559
|
+
if (output.rpcResponsive) {
|
|
4560
|
+
console.log(` Peers: ${output.peersCount}`);
|
|
4561
|
+
} else {
|
|
4562
|
+
console.log(" Peers: unavailable");
|
|
4563
|
+
}
|
|
3699
4564
|
console.log(` Can Send: ${output.checks.channels.canSend ? "yes" : "no"}`);
|
|
3700
4565
|
console.log(` Can Receive: ${output.checks.channels.canReceive ? "yes" : "no"}`);
|
|
3701
4566
|
console.log(` Recommendation:${output.recommendation}`);
|
|
@@ -3858,11 +4723,28 @@ async function runNodeStopCommand(config, options) {
|
|
|
3858
4723
|
}
|
|
3859
4724
|
|
|
3860
4725
|
// src/lib/node-upgrade.ts
|
|
3861
|
-
import { BinaryManager as
|
|
4726
|
+
import { BinaryManager as BinaryManager3, MigrationManager as MigrationManager2 } from "@fiber-pay/node";
|
|
3862
4727
|
async function runNodeUpgradeCommand(config, options) {
|
|
3863
4728
|
const json = Boolean(options.json);
|
|
3864
|
-
const
|
|
3865
|
-
|
|
4729
|
+
const resolvedBinary = resolveBinaryPath(config);
|
|
4730
|
+
let installDir;
|
|
4731
|
+
try {
|
|
4732
|
+
installDir = getBinaryManagerInstallDirOrThrow(resolvedBinary);
|
|
4733
|
+
} catch (error) {
|
|
4734
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
4735
|
+
if (json) {
|
|
4736
|
+
printJsonError({
|
|
4737
|
+
code: "BINARY_PATH_INCOMPATIBLE",
|
|
4738
|
+
message,
|
|
4739
|
+
recoverable: true,
|
|
4740
|
+
suggestion: "Use `fiber-pay config profile unset binaryPath` or set binaryPath to a standard fnn filename in the target directory."
|
|
4741
|
+
});
|
|
4742
|
+
} else {
|
|
4743
|
+
console.error(`\u274C ${message}`);
|
|
4744
|
+
}
|
|
4745
|
+
process.exit(1);
|
|
4746
|
+
}
|
|
4747
|
+
const binaryManager = new BinaryManager3(installDir);
|
|
3866
4748
|
const pid = readPidFile(config.dataDir);
|
|
3867
4749
|
if (pid && isProcessRunning(pid)) {
|
|
3868
4750
|
const msg = "The Fiber node is currently running. Stop it before upgrading.";
|
|
@@ -4131,32 +5013,15 @@ function createNodeCommand(config) {
|
|
|
4131
5013
|
node.command("status").option("--json").action(async (options) => {
|
|
4132
5014
|
await runNodeStatusCommand(config, options);
|
|
4133
5015
|
});
|
|
5016
|
+
node.command("network").description("Display comprehensive network topology and connections").option("--json").action(async (options) => {
|
|
5017
|
+
await runNodeNetworkCommand(config, options);
|
|
5018
|
+
});
|
|
5019
|
+
node.command("info").description("Display information about the running node").option("--json").action(async (options) => {
|
|
5020
|
+
await runNodeInfoCommand(config, options);
|
|
5021
|
+
});
|
|
4134
5022
|
node.command("ready").description("Agent-oriented readiness summary for automation").option("--json").action(async (options) => {
|
|
4135
5023
|
await runNodeReadyCommand(config, options);
|
|
4136
5024
|
});
|
|
4137
|
-
node.command("info").option("--json").action(async (options) => {
|
|
4138
|
-
const rpc = await createReadyRpcClient(config);
|
|
4139
|
-
const nodeInfo = await rpc.nodeInfo();
|
|
4140
|
-
const fundingAddress = scriptToAddress2(nodeInfo.default_funding_lock_script, config.network);
|
|
4141
|
-
const peerId = await nodeIdToPeerId2(nodeInfo.node_id);
|
|
4142
|
-
const output = {
|
|
4143
|
-
nodeId: nodeInfo.node_id,
|
|
4144
|
-
peerId,
|
|
4145
|
-
addresses: nodeInfo.addresses,
|
|
4146
|
-
chainHash: nodeInfo.chain_hash,
|
|
4147
|
-
fundingAddress,
|
|
4148
|
-
fundingLockScript: nodeInfo.default_funding_lock_script,
|
|
4149
|
-
version: nodeInfo.version,
|
|
4150
|
-
channelCount: parseInt(nodeInfo.channel_count, 16),
|
|
4151
|
-
pendingChannelCount: parseInt(nodeInfo.pending_channel_count, 16),
|
|
4152
|
-
peersCount: parseInt(nodeInfo.peers_count, 16)
|
|
4153
|
-
};
|
|
4154
|
-
if (options.json) {
|
|
4155
|
-
printJsonSuccess(output);
|
|
4156
|
-
} else {
|
|
4157
|
-
printNodeInfoHuman(output);
|
|
4158
|
-
}
|
|
4159
|
-
});
|
|
4160
5025
|
node.command("upgrade").description("Upgrade the Fiber node binary and migrate the database if needed").option("--version <version>", "Target Fiber version (default: latest)").option("--no-backup", "Skip creating a store backup before migration").option("--check-only", "Only check if migration is needed, do not migrate").option(
|
|
4161
5026
|
"--force-migrate",
|
|
4162
5027
|
"Force migration attempt even when compatibility check reports incompatible data"
|
|
@@ -4167,7 +5032,7 @@ function createNodeCommand(config) {
|
|
|
4167
5032
|
}
|
|
4168
5033
|
|
|
4169
5034
|
// src/commands/payment.ts
|
|
4170
|
-
import { ckbToShannons as
|
|
5035
|
+
import { ckbToShannons as ckbToShannons4, shannonsToCkb as shannonsToCkb6 } from "@fiber-pay/sdk";
|
|
4171
5036
|
import { Command as Command9 } from "commander";
|
|
4172
5037
|
function createPaymentCommand(config) {
|
|
4173
5038
|
const payment = new Command9("payment").description("Payment lifecycle and status commands");
|
|
@@ -4209,9 +5074,9 @@ function createPaymentCommand(config) {
|
|
|
4209
5074
|
const paymentParams = {
|
|
4210
5075
|
invoice,
|
|
4211
5076
|
target_pubkey: recipientNodeId,
|
|
4212
|
-
amount: amountCkb ?
|
|
5077
|
+
amount: amountCkb ? ckbToShannons4(amountCkb) : void 0,
|
|
4213
5078
|
keysend: recipientNodeId ? true : void 0,
|
|
4214
|
-
max_fee_amount: maxFeeCkb ?
|
|
5079
|
+
max_fee_amount: maxFeeCkb ? ckbToShannons4(maxFeeCkb) : void 0
|
|
4215
5080
|
};
|
|
4216
5081
|
const endpoint = resolveRpcEndpoint(config);
|
|
4217
5082
|
if (endpoint.target === "runtime-proxy") {
|
|
@@ -4253,7 +5118,7 @@ function createPaymentCommand(config) {
|
|
|
4253
5118
|
const payload = {
|
|
4254
5119
|
paymentHash: result.payment_hash,
|
|
4255
5120
|
status: result.status === "Success" ? "success" : result.status === "Failed" ? "failed" : "pending",
|
|
4256
|
-
feeCkb:
|
|
5121
|
+
feeCkb: shannonsToCkb6(result.fee),
|
|
4257
5122
|
failureReason: result.failed_error
|
|
4258
5123
|
};
|
|
4259
5124
|
if (json) {
|
|
@@ -4268,6 +5133,7 @@ function createPaymentCommand(config) {
|
|
|
4268
5133
|
}
|
|
4269
5134
|
}
|
|
4270
5135
|
});
|
|
5136
|
+
registerPaymentRebalanceCommand(payment, config);
|
|
4271
5137
|
payment.command("get").argument("<paymentHash>").option("--json").action(async (paymentHash, options) => {
|
|
4272
5138
|
const rpc = await createReadyRpcClient(config);
|
|
4273
5139
|
const result = await rpc.getPayment({ payment_hash: paymentHash });
|
|
@@ -4416,7 +5282,7 @@ function createPaymentCommand(config) {
|
|
|
4416
5282
|
process.exit(1);
|
|
4417
5283
|
}
|
|
4418
5284
|
const hopsInfo = pubkeys.map((pubkey) => ({ pubkey }));
|
|
4419
|
-
const amount = options.amount ?
|
|
5285
|
+
const amount = options.amount ? ckbToShannons4(parseFloat(options.amount)) : void 0;
|
|
4420
5286
|
const result = await rpc.buildRouter({
|
|
4421
5287
|
hops_info: hopsInfo,
|
|
4422
5288
|
amount
|
|
@@ -4432,7 +5298,7 @@ function createPaymentCommand(config) {
|
|
|
4432
5298
|
console.log(
|
|
4433
5299
|
` Outpoint: ${hop.channel_outpoint.tx_hash}:${hop.channel_outpoint.index}`
|
|
4434
5300
|
);
|
|
4435
|
-
console.log(` Amount: ${
|
|
5301
|
+
console.log(` Amount: ${shannonsToCkb6(hop.amount_received)} CKB`);
|
|
4436
5302
|
console.log(` Expiry: ${hop.incoming_tlc_expiry}`);
|
|
4437
5303
|
}
|
|
4438
5304
|
}
|
|
@@ -4440,7 +5306,7 @@ function createPaymentCommand(config) {
|
|
|
4440
5306
|
payment.command("send-route").description("Send a payment using a pre-built route from `payment route`").requiredOption(
|
|
4441
5307
|
"--router <json>",
|
|
4442
5308
|
"JSON array of router hops (output of `payment route --json`)"
|
|
4443
|
-
).option("--invoice <invoice>", "Invoice to pay").option("--payment-hash <hash>", "Payment hash (for keysend)").option("--keysend", "Keysend mode").option("--dry-run", "Simulate\u2014do not actually send").option("--json").action(async (options) => {
|
|
5309
|
+
).option("--invoice <invoice>", "Invoice to pay").option("--payment-hash <hash>", "Payment hash (for keysend)").option("--keysend", "Keysend mode").option("--allow-self-payment", "Allow self-payment for circular route rebalancing").option("--dry-run", "Simulate\u2014do not actually send").option("--json").action(async (options) => {
|
|
4444
5310
|
const rpc = await createReadyRpcClient(config);
|
|
4445
5311
|
const json = Boolean(options.json);
|
|
4446
5312
|
let router;
|
|
@@ -4465,12 +5331,13 @@ function createPaymentCommand(config) {
|
|
|
4465
5331
|
invoice: options.invoice,
|
|
4466
5332
|
payment_hash: options.paymentHash,
|
|
4467
5333
|
keysend: options.keysend ? true : void 0,
|
|
5334
|
+
allow_self_payment: options.allowSelfPayment ? true : void 0,
|
|
4468
5335
|
dry_run: options.dryRun ? true : void 0
|
|
4469
5336
|
});
|
|
4470
5337
|
const payload = {
|
|
4471
5338
|
paymentHash: result.payment_hash,
|
|
4472
5339
|
status: result.status === "Success" ? "success" : result.status === "Failed" ? "failed" : "pending",
|
|
4473
|
-
feeCkb:
|
|
5340
|
+
feeCkb: shannonsToCkb6(result.fee),
|
|
4474
5341
|
failureReason: result.failed_error,
|
|
4475
5342
|
dryRun: Boolean(options.dryRun)
|
|
4476
5343
|
};
|
|
@@ -4494,7 +5361,7 @@ function getJobPaymentHash(job) {
|
|
|
4494
5361
|
}
|
|
4495
5362
|
function getJobFeeCkb(job) {
|
|
4496
5363
|
const result = job.result;
|
|
4497
|
-
return result?.fee ?
|
|
5364
|
+
return result?.fee ? shannonsToCkb6(result.fee) : 0;
|
|
4498
5365
|
}
|
|
4499
5366
|
function getJobFailure(job) {
|
|
4500
5367
|
const result = job.result;
|
|
@@ -4566,7 +5433,7 @@ function createPeerCommand(config) {
|
|
|
4566
5433
|
|
|
4567
5434
|
// src/commands/runtime.ts
|
|
4568
5435
|
import { spawn as spawn2 } from "child_process";
|
|
4569
|
-
import { resolve } from "path";
|
|
5436
|
+
import { join as join8, resolve } from "path";
|
|
4570
5437
|
import {
|
|
4571
5438
|
alertPriorityOrder,
|
|
4572
5439
|
formatRuntimeAlert as formatRuntimeAlert2,
|
|
@@ -4640,15 +5507,22 @@ function shouldPrintAlert(alert, filter) {
|
|
|
4640
5507
|
}
|
|
4641
5508
|
return true;
|
|
4642
5509
|
}
|
|
5510
|
+
function resolveRuntimeRecoveryListen(config) {
|
|
5511
|
+
const meta = readRuntimeMeta(config.dataDir);
|
|
5512
|
+
return meta?.proxyListen ?? config.runtimeProxyListen ?? "127.0.0.1:8229";
|
|
5513
|
+
}
|
|
4643
5514
|
function createRuntimeCommand(config) {
|
|
4644
5515
|
const runtime = new Command11("runtime").description("Polling monitor and alert runtime service");
|
|
4645
|
-
runtime.command("start").description("Start runtime monitor service in foreground").option("--daemon", "Start runtime monitor in detached background mode").option("--fiber-rpc-url <url>", "Target fiber rpc URL (defaults to --rpc-url/global config)").option("--proxy-listen <host:port>", "Monitor proxy listen address").option("--channel-poll-ms <ms>", "Channel polling interval in milliseconds").option("--invoice-poll-ms <ms>", "Invoice polling interval in milliseconds").option("--payment-poll-ms <ms>", "Payment polling interval in milliseconds").option("--peer-poll-ms <ms>", "Peer polling interval in milliseconds").option("--health-poll-ms <ms>", "RPC health polling interval in milliseconds").option("--include-closed <bool>", "Monitor closed channels (true|false)").option("--completed-ttl-seconds <seconds>", "TTL for completed invoices/payments in tracker").option("--state-file <path>", "State file path for snapshots and history").option("--alert-log-file <path>", "Path to runtime alert JSONL log file").option("--flush-ms <ms>", "State flush interval in milliseconds").option("--webhook <url>", "Webhook URL to receive alert POST payloads").option("--websocket <host:port>", "WebSocket alert broadcast listen address").option(
|
|
5516
|
+
runtime.command("start").description("Start runtime monitor service in foreground").option("--daemon", "Start runtime monitor in detached background mode").option("--fiber-rpc-url <url>", "Target fiber rpc URL (defaults to --rpc-url/global config)").option("--proxy-listen <host:port>", "Monitor proxy listen address").option("--channel-poll-ms <ms>", "Channel polling interval in milliseconds").option("--invoice-poll-ms <ms>", "Invoice polling interval in milliseconds").option("--payment-poll-ms <ms>", "Payment polling interval in milliseconds").option("--peer-poll-ms <ms>", "Peer polling interval in milliseconds").option("--health-poll-ms <ms>", "RPC health polling interval in milliseconds").option("--include-closed <bool>", "Monitor closed channels (true|false)").option("--completed-ttl-seconds <seconds>", "TTL for completed invoices/payments in tracker").option("--state-file <path>", "State file path for snapshots and history").option("--alert-log-file <path>", "Path to runtime alert JSONL log file (legacy static path)").option("--alert-logs-base-dir <dir>", "Base logs directory for daily-rotated alert files").option("--flush-ms <ms>", "State flush interval in milliseconds").option("--webhook <url>", "Webhook URL to receive alert POST payloads").option("--websocket <host:port>", "WebSocket alert broadcast listen address").option(
|
|
4646
5517
|
"--log-min-priority <priority>",
|
|
4647
5518
|
"Minimum runtime log priority (critical|high|medium|low)"
|
|
4648
5519
|
).option("--log-type <types>", "Comma-separated runtime alert types to print").option("--json").action(async (options) => {
|
|
4649
5520
|
const asJson = Boolean(options.json);
|
|
4650
5521
|
const daemon = Boolean(options.daemon);
|
|
4651
5522
|
const isRuntimeChild = process.env.FIBER_RUNTIME_CHILD === "1";
|
|
5523
|
+
const runtimeListen = String(
|
|
5524
|
+
options.proxyListen ?? config.runtimeProxyListen ?? "127.0.0.1:8229"
|
|
5525
|
+
);
|
|
4652
5526
|
try {
|
|
4653
5527
|
const existingPid = readRuntimePid(config.dataDir);
|
|
4654
5528
|
if (existingPid && isProcessRunning(existingPid) && (!isRuntimeChild || existingPid !== process.pid)) {
|
|
@@ -4668,6 +5542,32 @@ function createRuntimeCommand(config) {
|
|
|
4668
5542
|
if (existingPid && !isProcessRunning(existingPid)) {
|
|
4669
5543
|
removeRuntimeFiles(config.dataDir);
|
|
4670
5544
|
}
|
|
5545
|
+
const discovered = findListeningProcessByPort(runtimeListen);
|
|
5546
|
+
if (discovered && (!existingPid || discovered.pid !== existingPid)) {
|
|
5547
|
+
if (isFiberRuntimeCommand(discovered.command)) {
|
|
5548
|
+
const terminated = await terminateProcess(discovered.pid);
|
|
5549
|
+
if (!terminated) {
|
|
5550
|
+
throw new Error(
|
|
5551
|
+
`Runtime port ${runtimeListen} is occupied by stale fiber-pay runtime PID ${discovered.pid} but termination failed.`
|
|
5552
|
+
);
|
|
5553
|
+
}
|
|
5554
|
+
removeRuntimeFiles(config.dataDir);
|
|
5555
|
+
if (!asJson) {
|
|
5556
|
+
console.log(
|
|
5557
|
+
`Recovered stale runtime process on ${runtimeListen} (PID: ${discovered.pid}).`
|
|
5558
|
+
);
|
|
5559
|
+
}
|
|
5560
|
+
} else if (discovered.command) {
|
|
5561
|
+
const details = discovered.command ? `PID ${discovered.pid} (${discovered.command})` : `PID ${discovered.pid}`;
|
|
5562
|
+
throw new Error(
|
|
5563
|
+
`Runtime proxy listen ${runtimeListen} is already in use by non-fiber-pay process: ${details}`
|
|
5564
|
+
);
|
|
5565
|
+
} else {
|
|
5566
|
+
throw new Error(
|
|
5567
|
+
`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.`
|
|
5568
|
+
);
|
|
5569
|
+
}
|
|
5570
|
+
}
|
|
4671
5571
|
if (daemon && !isRuntimeChild) {
|
|
4672
5572
|
const childArgv = process.argv.filter((arg) => arg !== "--daemon");
|
|
4673
5573
|
const child = spawn2(process.execPath, childArgv.slice(1), {
|
|
@@ -4710,7 +5610,7 @@ function createRuntimeCommand(config) {
|
|
|
4710
5610
|
),
|
|
4711
5611
|
proxy: {
|
|
4712
5612
|
enabled: true,
|
|
4713
|
-
listen:
|
|
5613
|
+
listen: runtimeListen
|
|
4714
5614
|
},
|
|
4715
5615
|
storage: {
|
|
4716
5616
|
stateFilePath: options.stateFile ? resolve(String(options.stateFile)) : resolve(config.dataDir, "runtime-state.json"),
|
|
@@ -4721,9 +5621,21 @@ function createRuntimeCommand(config) {
|
|
|
4721
5621
|
dbPath: resolve(config.dataDir, "runtime-jobs.db")
|
|
4722
5622
|
}
|
|
4723
5623
|
};
|
|
4724
|
-
|
|
5624
|
+
let alertLogsBaseDir;
|
|
5625
|
+
let alertLogFile;
|
|
5626
|
+
if (options.alertLogsBaseDir) {
|
|
5627
|
+
alertLogsBaseDir = resolve(String(options.alertLogsBaseDir));
|
|
5628
|
+
} else if (options.alertLogFile) {
|
|
5629
|
+
alertLogFile = resolve(String(options.alertLogFile));
|
|
5630
|
+
} else {
|
|
5631
|
+
alertLogsBaseDir = resolve(config.dataDir, "logs");
|
|
5632
|
+
}
|
|
4725
5633
|
const alerts = [{ type: "stdout" }];
|
|
4726
|
-
|
|
5634
|
+
if (alertLogsBaseDir) {
|
|
5635
|
+
alerts.push({ type: "daily-file", baseLogsDir: alertLogsBaseDir });
|
|
5636
|
+
} else if (alertLogFile) {
|
|
5637
|
+
alerts.push({ type: "file", path: alertLogFile });
|
|
5638
|
+
}
|
|
4727
5639
|
if (options.webhook) {
|
|
4728
5640
|
alerts.push({ type: "webhook", url: String(options.webhook) });
|
|
4729
5641
|
}
|
|
@@ -4745,6 +5657,12 @@ function createRuntimeCommand(config) {
|
|
|
4745
5657
|
}
|
|
4746
5658
|
const runtime2 = await startRuntimeService2(runtimeConfig);
|
|
4747
5659
|
const status = runtime2.service.getStatus();
|
|
5660
|
+
const logsBaseDir = alertLogsBaseDir ?? resolve(config.dataDir, "logs");
|
|
5661
|
+
const todayLogDir = resolveLogDirForDateWithOptions(config.dataDir, void 0, {
|
|
5662
|
+
logsBaseDir,
|
|
5663
|
+
ensureExists: false
|
|
5664
|
+
});
|
|
5665
|
+
const effectiveAlertLogPath = alertLogsBaseDir ? join8(todayLogDir, "runtime.alerts.jsonl") : alertLogFile ?? join8(todayLogDir, "runtime.alerts.jsonl");
|
|
4748
5666
|
writeRuntimePid(config.dataDir, process.pid);
|
|
4749
5667
|
writeRuntimeMeta(config.dataDir, {
|
|
4750
5668
|
pid: process.pid,
|
|
@@ -4752,7 +5670,10 @@ function createRuntimeCommand(config) {
|
|
|
4752
5670
|
fiberRpcUrl: status.targetUrl,
|
|
4753
5671
|
proxyListen: status.proxyListen,
|
|
4754
5672
|
stateFilePath: runtimeConfig.storage?.stateFilePath,
|
|
4755
|
-
alertLogFilePath:
|
|
5673
|
+
alertLogFilePath: effectiveAlertLogPath,
|
|
5674
|
+
fnnStdoutLogPath: join8(todayLogDir, "fnn.stdout.log"),
|
|
5675
|
+
fnnStderrLogPath: join8(todayLogDir, "fnn.stderr.log"),
|
|
5676
|
+
logsBaseDir,
|
|
4756
5677
|
daemon: daemon || isRuntimeChild
|
|
4757
5678
|
});
|
|
4758
5679
|
runtime2.service.on("alert", (alert) => {
|
|
@@ -4771,14 +5692,16 @@ function createRuntimeCommand(config) {
|
|
|
4771
5692
|
fiberRpcUrl: status.targetUrl,
|
|
4772
5693
|
proxyListen: status.proxyListen,
|
|
4773
5694
|
stateFilePath: runtimeConfig.storage?.stateFilePath,
|
|
4774
|
-
alertLogFile
|
|
5695
|
+
alertLogFile: effectiveAlertLogPath,
|
|
5696
|
+
logsBaseDir
|
|
4775
5697
|
});
|
|
4776
5698
|
printJsonEvent("runtime_started", status);
|
|
4777
5699
|
} else {
|
|
4778
5700
|
console.log(`Fiber RPC: ${status.targetUrl}`);
|
|
4779
5701
|
console.log(`Proxy listen: ${status.proxyListen}`);
|
|
4780
5702
|
console.log(`State file: ${runtimeConfig.storage?.stateFilePath}`);
|
|
4781
|
-
console.log(`
|
|
5703
|
+
console.log(`Logs dir: ${logsBaseDir}`);
|
|
5704
|
+
console.log(`Alert log: ${effectiveAlertLogPath}`);
|
|
4782
5705
|
console.log("Runtime monitor is running. Press Ctrl+C to stop.");
|
|
4783
5706
|
}
|
|
4784
5707
|
const signal = await runtime2.waitForShutdownSignal();
|
|
@@ -4812,8 +5735,42 @@ function createRuntimeCommand(config) {
|
|
|
4812
5735
|
});
|
|
4813
5736
|
runtime.command("status").description("Show runtime process and health status").option("--json").action(async (options) => {
|
|
4814
5737
|
const asJson = Boolean(options.json);
|
|
4815
|
-
|
|
5738
|
+
let pid = readRuntimePid(config.dataDir);
|
|
4816
5739
|
const meta = readRuntimeMeta(config.dataDir);
|
|
5740
|
+
const recoveryListen = resolveRuntimeRecoveryListen(config);
|
|
5741
|
+
if (!pid) {
|
|
5742
|
+
const fallback = findListeningProcessByPort(recoveryListen);
|
|
5743
|
+
if (fallback && isFiberRuntimeCommand(fallback.command)) {
|
|
5744
|
+
pid = fallback.pid;
|
|
5745
|
+
writeRuntimePid(config.dataDir, pid);
|
|
5746
|
+
} else if (fallback?.command) {
|
|
5747
|
+
const details = fallback.command ? `PID ${fallback.pid} (${fallback.command})` : `PID ${fallback.pid}`;
|
|
5748
|
+
if (asJson) {
|
|
5749
|
+
printJsonError({
|
|
5750
|
+
code: "RUNTIME_PORT_IN_USE",
|
|
5751
|
+
message: `Runtime proxy port is in use by non-fiber-pay process: ${details}`,
|
|
5752
|
+
recoverable: true,
|
|
5753
|
+
suggestion: "Stop that process or use a different --proxy-listen port."
|
|
5754
|
+
});
|
|
5755
|
+
} else {
|
|
5756
|
+
console.log(`Runtime proxy port is in use by non-fiber-pay process: ${details}`);
|
|
5757
|
+
}
|
|
5758
|
+
process.exit(1);
|
|
5759
|
+
} else if (fallback) {
|
|
5760
|
+
const message = `Runtime proxy port is in use by process PID ${fallback.pid}. The owning command could not be determined; inspect this PID manually.`;
|
|
5761
|
+
if (asJson) {
|
|
5762
|
+
printJsonError({
|
|
5763
|
+
code: "RUNTIME_PORT_IN_USE",
|
|
5764
|
+
message,
|
|
5765
|
+
recoverable: true,
|
|
5766
|
+
suggestion: "Inspect the PID owner manually or use a different --proxy-listen port."
|
|
5767
|
+
});
|
|
5768
|
+
} else {
|
|
5769
|
+
console.log(message);
|
|
5770
|
+
}
|
|
5771
|
+
process.exit(1);
|
|
5772
|
+
}
|
|
5773
|
+
}
|
|
4817
5774
|
if (!pid) {
|
|
4818
5775
|
if (asJson) {
|
|
4819
5776
|
printJsonError({
|
|
@@ -4844,9 +5801,11 @@ function createRuntimeCommand(config) {
|
|
|
4844
5801
|
process.exit(1);
|
|
4845
5802
|
}
|
|
4846
5803
|
let rpcStatus;
|
|
4847
|
-
if (meta?.proxyListen) {
|
|
5804
|
+
if (meta?.proxyListen ?? recoveryListen) {
|
|
4848
5805
|
try {
|
|
4849
|
-
const response = await fetch(
|
|
5806
|
+
const response = await fetch(
|
|
5807
|
+
`http://${meta?.proxyListen ?? recoveryListen}/monitor/status`
|
|
5808
|
+
);
|
|
4850
5809
|
if (response.ok) {
|
|
4851
5810
|
rpcStatus = await response.json();
|
|
4852
5811
|
}
|
|
@@ -4874,7 +5833,41 @@ function createRuntimeCommand(config) {
|
|
|
4874
5833
|
});
|
|
4875
5834
|
runtime.command("stop").description("Stop runtime process by PID").option("--json").action(async (options) => {
|
|
4876
5835
|
const asJson = Boolean(options.json);
|
|
4877
|
-
|
|
5836
|
+
let pid = readRuntimePid(config.dataDir);
|
|
5837
|
+
const recoveryListen = resolveRuntimeRecoveryListen(config);
|
|
5838
|
+
if (!pid) {
|
|
5839
|
+
const fallback = findListeningProcessByPort(recoveryListen);
|
|
5840
|
+
if (fallback && isFiberRuntimeCommand(fallback.command)) {
|
|
5841
|
+
pid = fallback.pid;
|
|
5842
|
+
writeRuntimePid(config.dataDir, pid);
|
|
5843
|
+
} else if (fallback?.command) {
|
|
5844
|
+
const details = fallback.command ? `PID ${fallback.pid} (${fallback.command})` : `PID ${fallback.pid}`;
|
|
5845
|
+
if (asJson) {
|
|
5846
|
+
printJsonError({
|
|
5847
|
+
code: "RUNTIME_PORT_IN_USE",
|
|
5848
|
+
message: `Runtime proxy port is in use by non-fiber-pay process: ${details}`,
|
|
5849
|
+
recoverable: true,
|
|
5850
|
+
suggestion: "Stop that process manually; it is not managed by fiber-pay runtime PID files."
|
|
5851
|
+
});
|
|
5852
|
+
} else {
|
|
5853
|
+
console.log(`Runtime proxy port is in use by non-fiber-pay process: ${details}`);
|
|
5854
|
+
}
|
|
5855
|
+
process.exit(1);
|
|
5856
|
+
} else if (fallback) {
|
|
5857
|
+
const message = `Runtime proxy port is in use by process PID ${fallback.pid}. The owning command could not be determined; inspect this PID manually.`;
|
|
5858
|
+
if (asJson) {
|
|
5859
|
+
printJsonError({
|
|
5860
|
+
code: "RUNTIME_PORT_IN_USE",
|
|
5861
|
+
message,
|
|
5862
|
+
recoverable: true,
|
|
5863
|
+
suggestion: "Inspect the PID owner manually; it may not be managed by fiber-pay runtime PID files."
|
|
5864
|
+
});
|
|
5865
|
+
} else {
|
|
5866
|
+
console.log(message);
|
|
5867
|
+
}
|
|
5868
|
+
process.exit(1);
|
|
5869
|
+
}
|
|
5870
|
+
}
|
|
4878
5871
|
if (!pid) {
|
|
4879
5872
|
if (asJson) {
|
|
4880
5873
|
printJsonError({
|
|
@@ -4903,14 +5896,19 @@ function createRuntimeCommand(config) {
|
|
|
4903
5896
|
}
|
|
4904
5897
|
process.exit(1);
|
|
4905
5898
|
}
|
|
4906
|
-
|
|
4907
|
-
|
|
4908
|
-
|
|
4909
|
-
|
|
4910
|
-
|
|
4911
|
-
|
|
4912
|
-
|
|
4913
|
-
|
|
5899
|
+
const terminated = await terminateProcess(pid);
|
|
5900
|
+
if (!terminated) {
|
|
5901
|
+
if (asJson) {
|
|
5902
|
+
printJsonError({
|
|
5903
|
+
code: "RUNTIME_STOP_FAILED",
|
|
5904
|
+
message: `Failed to terminate runtime process ${pid}.`,
|
|
5905
|
+
recoverable: true,
|
|
5906
|
+
suggestion: `Try stopping PID ${pid} manually and rerun runtime stop.`
|
|
5907
|
+
});
|
|
5908
|
+
} else {
|
|
5909
|
+
console.log(`Failed to terminate runtime process ${pid}.`);
|
|
5910
|
+
}
|
|
5911
|
+
process.exit(1);
|
|
4914
5912
|
}
|
|
4915
5913
|
removeRuntimeFiles(config.dataDir);
|
|
4916
5914
|
if (asJson) {
|
|
@@ -4926,8 +5924,8 @@ function createRuntimeCommand(config) {
|
|
|
4926
5924
|
import { Command as Command12 } from "commander";
|
|
4927
5925
|
|
|
4928
5926
|
// src/lib/build-info.ts
|
|
4929
|
-
var CLI_VERSION = "0.1.0-rc.
|
|
4930
|
-
var CLI_COMMIT = "
|
|
5927
|
+
var CLI_VERSION = "0.1.0-rc.6";
|
|
5928
|
+
var CLI_COMMIT = "632dc5658ea5122cea5af371decc1b758b004461";
|
|
4931
5929
|
|
|
4932
5930
|
// src/commands/version.ts
|
|
4933
5931
|
function createVersionCommand() {
|
|
@@ -4945,6 +5943,64 @@ function createVersionCommand() {
|
|
|
4945
5943
|
});
|
|
4946
5944
|
}
|
|
4947
5945
|
|
|
5946
|
+
// src/commands/wallet.ts
|
|
5947
|
+
import { Command as Command13 } from "commander";
|
|
5948
|
+
|
|
5949
|
+
// src/lib/wallet-address.ts
|
|
5950
|
+
import { scriptToAddress as scriptToAddress2 } from "@fiber-pay/sdk";
|
|
5951
|
+
async function runWalletAddressCommand(config, options) {
|
|
5952
|
+
const rpc = await createReadyRpcClient(config);
|
|
5953
|
+
const nodeInfo = await rpc.nodeInfo();
|
|
5954
|
+
const address = scriptToAddress2(
|
|
5955
|
+
nodeInfo.default_funding_lock_script,
|
|
5956
|
+
config.network === "mainnet" ? "mainnet" : "testnet"
|
|
5957
|
+
);
|
|
5958
|
+
if (options.json) {
|
|
5959
|
+
printJsonSuccess({ address });
|
|
5960
|
+
return;
|
|
5961
|
+
}
|
|
5962
|
+
console.log("\u2705 Funding address retrieved");
|
|
5963
|
+
console.log(` Address: ${address}`);
|
|
5964
|
+
}
|
|
5965
|
+
|
|
5966
|
+
// src/lib/wallet-balance.ts
|
|
5967
|
+
async function runWalletBalanceCommand(config, options) {
|
|
5968
|
+
if (!config.ckbRpcUrl) {
|
|
5969
|
+
throw new Error(
|
|
5970
|
+
"CKB RPC URL is not configured. Set FIBER_CKB_RPC_URL or add ckb.rpc_url to config.yml."
|
|
5971
|
+
);
|
|
5972
|
+
}
|
|
5973
|
+
const rpc = await createReadyRpcClient(config);
|
|
5974
|
+
const nodeInfo = await rpc.nodeInfo();
|
|
5975
|
+
const balanceShannons = await getLockBalanceShannons(
|
|
5976
|
+
config.ckbRpcUrl,
|
|
5977
|
+
nodeInfo.default_funding_lock_script
|
|
5978
|
+
);
|
|
5979
|
+
const balanceCkb = formatShannonsAsCkb(balanceShannons, 8);
|
|
5980
|
+
if (options.json) {
|
|
5981
|
+
printJsonSuccess({
|
|
5982
|
+
balance_ckb: balanceCkb,
|
|
5983
|
+
balance_shannons: balanceShannons.toString()
|
|
5984
|
+
});
|
|
5985
|
+
return;
|
|
5986
|
+
}
|
|
5987
|
+
console.log("\u2705 CKB balance retrieved");
|
|
5988
|
+
console.log(` Balance: ${balanceCkb} CKB`);
|
|
5989
|
+
console.log(` Balance (shannons): ${balanceShannons.toString()}`);
|
|
5990
|
+
}
|
|
5991
|
+
|
|
5992
|
+
// src/commands/wallet.ts
|
|
5993
|
+
function createWalletCommand(config) {
|
|
5994
|
+
const wallet = new Command13("wallet").description("Wallet management");
|
|
5995
|
+
wallet.command("address").description("Display the funding address").option("--json").action(async (options) => {
|
|
5996
|
+
await runWalletAddressCommand(config, options);
|
|
5997
|
+
});
|
|
5998
|
+
wallet.command("balance").description("Display the CKB balance").option("--json").action(async (options) => {
|
|
5999
|
+
await runWalletBalanceCommand(config, options);
|
|
6000
|
+
});
|
|
6001
|
+
return wallet;
|
|
6002
|
+
}
|
|
6003
|
+
|
|
4948
6004
|
// src/lib/argv.ts
|
|
4949
6005
|
var GLOBAL_OPTIONS_WITH_VALUE = /* @__PURE__ */ new Set([
|
|
4950
6006
|
"--profile",
|
|
@@ -5035,6 +6091,14 @@ function applyGlobalOverrides(argv) {
|
|
|
5035
6091
|
}
|
|
5036
6092
|
break;
|
|
5037
6093
|
}
|
|
6094
|
+
case "--rpc-biscuit-token": {
|
|
6095
|
+
const value = getFlagValue(argv, index);
|
|
6096
|
+
if (value) {
|
|
6097
|
+
process.env.FIBER_RPC_BISCUIT_TOKEN = value;
|
|
6098
|
+
explicitFlags.add("rpcBiscuitToken");
|
|
6099
|
+
}
|
|
6100
|
+
break;
|
|
6101
|
+
}
|
|
5038
6102
|
case "--network": {
|
|
5039
6103
|
const value = getFlagValue(argv, index);
|
|
5040
6104
|
if (value) {
|
|
@@ -5074,7 +6138,7 @@ function applyGlobalOverrides(argv) {
|
|
|
5074
6138
|
}
|
|
5075
6139
|
if (!explicitDataDir && profileName) {
|
|
5076
6140
|
const homeDir = process.env.HOME ?? process.cwd();
|
|
5077
|
-
process.env.FIBER_DATA_DIR =
|
|
6141
|
+
process.env.FIBER_DATA_DIR = join9(homeDir, ".fiber-pay", "profiles", profileName);
|
|
5078
6142
|
}
|
|
5079
6143
|
}
|
|
5080
6144
|
function printFatal(error) {
|
|
@@ -5102,8 +6166,8 @@ async function main() {
|
|
|
5102
6166
|
}
|
|
5103
6167
|
applyGlobalOverrides(argv);
|
|
5104
6168
|
const config = getEffectiveConfig(explicitFlags).config;
|
|
5105
|
-
const program = new
|
|
5106
|
-
program.name("fiber-pay").description("AI Agent Payment SDK for CKB Lightning Network").option("--profile <name>", "Use profile at ~/.fiber-pay/profiles/<name>").option("--data-dir <path>", "Override data directory for all commands").option("--rpc-url <url>", "Override RPC URL for all commands").option("--network <network>", "Override network for all commands (testnet|mainnet)").option("--key-password <password>", "Override key password for all commands").option("--binary-path <path>", "Override fiber binary path for all commands").showHelpAfterError().showSuggestionAfterError();
|
|
6169
|
+
const program = new Command14();
|
|
6170
|
+
program.name("fiber-pay").description("AI Agent Payment SDK for CKB Lightning Network").option("--profile <name>", "Use profile at ~/.fiber-pay/profiles/<name>").option("--data-dir <path>", "Override data directory for all commands").option("--rpc-url <url>", "Override RPC URL for all commands").option("--rpc-biscuit-token <token>", "Set RPC Authorization Bearer token for all commands").option("--network <network>", "Override network for all commands (testnet|mainnet)").option("--key-password <password>", "Override key password for all commands").option("--binary-path <path>", "Override fiber binary path for all commands").showHelpAfterError().showSuggestionAfterError();
|
|
5107
6171
|
program.exitOverride();
|
|
5108
6172
|
program.configureOutput({
|
|
5109
6173
|
writeOut: (str) => process.stdout.write(str),
|
|
@@ -5125,6 +6189,7 @@ async function main() {
|
|
|
5125
6189
|
program.addCommand(createConfigCommand(config));
|
|
5126
6190
|
program.addCommand(createRuntimeCommand(config));
|
|
5127
6191
|
program.addCommand(createVersionCommand());
|
|
6192
|
+
program.addCommand(createWalletCommand(config));
|
|
5128
6193
|
await program.parseAsync(argv);
|
|
5129
6194
|
}
|
|
5130
6195
|
main().catch((error) => {
|