@fiber-pay/cli 0.1.0-rc.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +21 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +4647 -0
- package/dist/cli.js.map +1 -0
- package/error-codes.json +140 -0
- package/package.json +40 -0
package/dist/cli.js
ADDED
|
@@ -0,0 +1,4647 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/index.ts
|
|
4
|
+
import { join as join7 } from "path";
|
|
5
|
+
import { Command as Command13 } from "commander";
|
|
6
|
+
|
|
7
|
+
// src/commands/binary.ts
|
|
8
|
+
import {
|
|
9
|
+
DEFAULT_FIBER_VERSION,
|
|
10
|
+
downloadFiberBinary,
|
|
11
|
+
getFiberBinaryInfo
|
|
12
|
+
} from "@fiber-pay/node";
|
|
13
|
+
import { Command } from "commander";
|
|
14
|
+
|
|
15
|
+
// src/lib/format.ts
|
|
16
|
+
import {
|
|
17
|
+
ChannelState,
|
|
18
|
+
shannonsToCkb,
|
|
19
|
+
toHex
|
|
20
|
+
} from "@fiber-pay/sdk";
|
|
21
|
+
function truncateMiddle(value, start = 10, end = 8) {
|
|
22
|
+
if (!value || value.length <= start + end + 3) {
|
|
23
|
+
return value;
|
|
24
|
+
}
|
|
25
|
+
return `${value.slice(0, start)}...${value.slice(-end)}`;
|
|
26
|
+
}
|
|
27
|
+
function parseHexTimestampMs(hexTimestamp) {
|
|
28
|
+
if (!hexTimestamp) return null;
|
|
29
|
+
try {
|
|
30
|
+
const raw = Number(BigInt(hexTimestamp));
|
|
31
|
+
if (!Number.isFinite(raw) || raw <= 0) return null;
|
|
32
|
+
return raw > 1e12 ? raw : raw * 1e3;
|
|
33
|
+
} catch {
|
|
34
|
+
return null;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
function formatAge(whenMs) {
|
|
38
|
+
if (!whenMs) return "unknown";
|
|
39
|
+
const diff = Date.now() - whenMs;
|
|
40
|
+
if (diff < 0) return "just now";
|
|
41
|
+
const seconds = Math.floor(diff / 1e3);
|
|
42
|
+
if (seconds < 60) return `${seconds}s ago`;
|
|
43
|
+
const minutes = Math.floor(seconds / 60);
|
|
44
|
+
if (minutes < 60) return `${minutes}m ago`;
|
|
45
|
+
const hours = Math.floor(minutes / 60);
|
|
46
|
+
if (hours < 24) return `${hours}h ${minutes % 60}m ago`;
|
|
47
|
+
const days = Math.floor(hours / 24);
|
|
48
|
+
return `${days}d ${hours % 24}h ago`;
|
|
49
|
+
}
|
|
50
|
+
function stateLabel(state) {
|
|
51
|
+
switch (state) {
|
|
52
|
+
case ChannelState.NegotiatingFunding:
|
|
53
|
+
return "\u{1F504} Negotiating Funding";
|
|
54
|
+
case ChannelState.CollaboratingFundingTx:
|
|
55
|
+
return "\u{1F9E9} Collaborating Funding Tx";
|
|
56
|
+
case ChannelState.SigningCommitment:
|
|
57
|
+
return "\u270D\uFE0F Signing Commitment";
|
|
58
|
+
case ChannelState.AwaitingTxSignatures:
|
|
59
|
+
return "\u23F3 Awaiting Tx Signatures";
|
|
60
|
+
case ChannelState.AwaitingChannelReady:
|
|
61
|
+
return "\u23F3 Awaiting Channel Ready";
|
|
62
|
+
case ChannelState.ChannelReady:
|
|
63
|
+
return "\u2705 Channel Ready";
|
|
64
|
+
case ChannelState.ShuttingDown:
|
|
65
|
+
return "\u{1F6D1} Shutting Down";
|
|
66
|
+
case ChannelState.Closed:
|
|
67
|
+
return "\u274C Closed";
|
|
68
|
+
default:
|
|
69
|
+
return state;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
function parseChannelState(input) {
|
|
73
|
+
if (!input) return void 0;
|
|
74
|
+
const trimmed = input.trim();
|
|
75
|
+
const legacy = trimmed.toUpperCase();
|
|
76
|
+
const normalizedInput = trimmed.replace(/[^a-zA-Z0-9]/g, "").toLowerCase();
|
|
77
|
+
const legacyMap = {
|
|
78
|
+
NEGOTIATING_FUNDING: ChannelState.NegotiatingFunding,
|
|
79
|
+
COLLABORATING_FUNDING_TX: ChannelState.CollaboratingFundingTx,
|
|
80
|
+
SIGNING_COMMITMENT: ChannelState.SigningCommitment,
|
|
81
|
+
AWAITING_TX_SIGNATURES: ChannelState.AwaitingTxSignatures,
|
|
82
|
+
AWAITING_CHANNEL_READY: ChannelState.AwaitingChannelReady,
|
|
83
|
+
CHANNEL_READY: ChannelState.ChannelReady,
|
|
84
|
+
SHUTTING_DOWN: ChannelState.ShuttingDown,
|
|
85
|
+
CLOSED: ChannelState.Closed
|
|
86
|
+
};
|
|
87
|
+
if (legacy in legacyMap) return legacyMap[legacy];
|
|
88
|
+
for (const value of Object.values(ChannelState)) {
|
|
89
|
+
const normalizedValue = value.replace(/[^a-zA-Z0-9]/g, "").toLowerCase();
|
|
90
|
+
if (normalizedValue === normalizedInput) return value;
|
|
91
|
+
}
|
|
92
|
+
return void 0;
|
|
93
|
+
}
|
|
94
|
+
function formatChannel(channel) {
|
|
95
|
+
const local = BigInt(channel.local_balance);
|
|
96
|
+
const remote = BigInt(channel.remote_balance);
|
|
97
|
+
const capacity = local + remote;
|
|
98
|
+
const localPct = capacity > 0n ? Number(local * 100n / capacity) : 0;
|
|
99
|
+
const remotePct = capacity > 0n ? 100 - localPct : 0;
|
|
100
|
+
return {
|
|
101
|
+
channelId: channel.channel_id,
|
|
102
|
+
channelIdShort: truncateMiddle(channel.channel_id, 10, 8),
|
|
103
|
+
peerId: channel.peer_id,
|
|
104
|
+
peerIdShort: truncateMiddle(channel.peer_id, 10, 8),
|
|
105
|
+
state: channel.state.state_name,
|
|
106
|
+
stateLabel: stateLabel(channel.state.state_name),
|
|
107
|
+
stateFlags: channel.state.state_flags,
|
|
108
|
+
localBalanceCkb: shannonsToCkb(channel.local_balance),
|
|
109
|
+
remoteBalanceCkb: shannonsToCkb(channel.remote_balance),
|
|
110
|
+
capacityCkb: shannonsToCkb(toHex(capacity)),
|
|
111
|
+
balanceRatio: `${localPct}/${remotePct}`,
|
|
112
|
+
pendingTlcs: channel.pending_tlcs.length,
|
|
113
|
+
enabled: channel.enabled,
|
|
114
|
+
isPublic: channel.is_public,
|
|
115
|
+
age: formatAge(parseHexTimestampMs(channel.created_at))
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
function getChannelSummary(channels) {
|
|
119
|
+
let totalLocal = 0n;
|
|
120
|
+
let totalRemote = 0n;
|
|
121
|
+
let active = 0;
|
|
122
|
+
for (const channel of channels) {
|
|
123
|
+
totalLocal += BigInt(channel.local_balance);
|
|
124
|
+
totalRemote += BigInt(channel.remote_balance || "0x0");
|
|
125
|
+
if (channel.state.state_name === ChannelState.ChannelReady) {
|
|
126
|
+
active++;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
return {
|
|
130
|
+
count: channels.length,
|
|
131
|
+
activeCount: active,
|
|
132
|
+
totalLocalCkb: shannonsToCkb(toHex(totalLocal)),
|
|
133
|
+
totalRemoteCkb: shannonsToCkb(toHex(totalRemote)),
|
|
134
|
+
totalCapacityCkb: shannonsToCkb(toHex(totalLocal + totalRemote))
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
function extractInvoiceMetadata(invoice) {
|
|
138
|
+
let description;
|
|
139
|
+
let expirySeconds;
|
|
140
|
+
for (const attr of invoice.data.attrs) {
|
|
141
|
+
if ("Description" in attr) description = attr.Description;
|
|
142
|
+
if ("ExpiryTime" in attr) {
|
|
143
|
+
try {
|
|
144
|
+
expirySeconds = Number(BigInt(attr.ExpiryTime));
|
|
145
|
+
} catch {
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
const createdMs = parseHexTimestampMs(invoice.data.timestamp);
|
|
150
|
+
const expiresAt = createdMs && expirySeconds ? new Date(createdMs + expirySeconds * 1e3).toISOString() : void 0;
|
|
151
|
+
return {
|
|
152
|
+
description,
|
|
153
|
+
expirySeconds,
|
|
154
|
+
expiresAt,
|
|
155
|
+
age: formatAge(createdMs)
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
function formatPaymentResult(payment) {
|
|
159
|
+
const createdAtMs = parseHexTimestampMs(payment.created_at);
|
|
160
|
+
const updatedAtMs = parseHexTimestampMs(payment.last_updated_at);
|
|
161
|
+
return {
|
|
162
|
+
paymentHash: payment.payment_hash,
|
|
163
|
+
status: payment.status,
|
|
164
|
+
feeCkb: shannonsToCkb(payment.fee),
|
|
165
|
+
failureReason: payment.failed_error,
|
|
166
|
+
createdAt: createdAtMs ? new Date(createdAtMs).toISOString() : payment.created_at,
|
|
167
|
+
updatedAt: updatedAtMs ? new Date(updatedAtMs).toISOString() : payment.last_updated_at,
|
|
168
|
+
routeCount: payment.routers?.length ?? 0,
|
|
169
|
+
routers: payment.routers
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
function printJson(payload) {
|
|
173
|
+
console.log(JSON.stringify(payload, null, 2));
|
|
174
|
+
}
|
|
175
|
+
function printJsonSuccess(data) {
|
|
176
|
+
printJson({ success: true, data });
|
|
177
|
+
}
|
|
178
|
+
function printJsonError(error) {
|
|
179
|
+
printJson({ success: false, error });
|
|
180
|
+
}
|
|
181
|
+
function printJsonEvent(event, data, ts = (/* @__PURE__ */ new Date()).toISOString()) {
|
|
182
|
+
process.stdout.write(`${JSON.stringify({ event, ts, data })}
|
|
183
|
+
`);
|
|
184
|
+
}
|
|
185
|
+
function printChannelDetailHuman(channel) {
|
|
186
|
+
const local = shannonsToCkb(channel.local_balance);
|
|
187
|
+
const remote = shannonsToCkb(channel.remote_balance);
|
|
188
|
+
const capacity = local + remote;
|
|
189
|
+
console.log("Channel");
|
|
190
|
+
console.log(` ID: ${channel.channel_id}`);
|
|
191
|
+
console.log(` Peer: ${channel.peer_id}`);
|
|
192
|
+
console.log(
|
|
193
|
+
` State: ${stateLabel(channel.state.state_name)} (${channel.state.state_name})`
|
|
194
|
+
);
|
|
195
|
+
console.log(` Enabled: ${channel.enabled ? "yes" : "no"}`);
|
|
196
|
+
console.log(` Public: ${channel.is_public ? "yes" : "no"}`);
|
|
197
|
+
console.log(
|
|
198
|
+
` Balance: local ${local} CKB | remote ${remote} CKB | capacity ${capacity} CKB`
|
|
199
|
+
);
|
|
200
|
+
console.log(` Pending TLCs: ${channel.pending_tlcs.length}`);
|
|
201
|
+
console.log(` Age: ${formatAge(parseHexTimestampMs(channel.created_at))}`);
|
|
202
|
+
console.log(
|
|
203
|
+
` Outpoint: ${channel.channel_outpoint ? `${channel.channel_outpoint.tx_hash}:${channel.channel_outpoint.index}` : "n/a"}`
|
|
204
|
+
);
|
|
205
|
+
console.log(` Commitment Tx: ${channel.latest_commitment_transaction_hash ?? "n/a"}`);
|
|
206
|
+
console.log(` Shutdown Tx: ${channel.shutdown_transaction_hash ?? "n/a"}`);
|
|
207
|
+
}
|
|
208
|
+
function printInvoiceDetailHuman(data) {
|
|
209
|
+
console.log("Invoice");
|
|
210
|
+
console.log(` Payment Hash: ${data.paymentHash}`);
|
|
211
|
+
console.log(` Status: ${data.status}`);
|
|
212
|
+
console.log(
|
|
213
|
+
` Amount: ${data.amountCkb ?? "n/a"} ${data.amountCkb !== void 0 ? "CKB" : ""}`.trim()
|
|
214
|
+
);
|
|
215
|
+
console.log(` Currency: ${data.currency}`);
|
|
216
|
+
console.log(` Description: ${data.description ?? "n/a"}`);
|
|
217
|
+
console.log(` Created At: ${data.createdAt}`);
|
|
218
|
+
console.log(` Expires At: ${data.expiresAt ?? "n/a"}`);
|
|
219
|
+
console.log(` Age: ${data.age}`);
|
|
220
|
+
console.log(` Invoice: ${data.invoice}`);
|
|
221
|
+
}
|
|
222
|
+
function printPaymentDetailHuman(payment) {
|
|
223
|
+
const createdAtMs = parseHexTimestampMs(payment.created_at);
|
|
224
|
+
const updatedAtMs = parseHexTimestampMs(payment.last_updated_at);
|
|
225
|
+
console.log("Payment");
|
|
226
|
+
console.log(` Hash: ${payment.payment_hash}`);
|
|
227
|
+
console.log(` Status: ${payment.status}`);
|
|
228
|
+
console.log(` Fee: ${shannonsToCkb(payment.fee)} CKB`);
|
|
229
|
+
console.log(` Failure: ${payment.failed_error ?? "n/a"}`);
|
|
230
|
+
console.log(
|
|
231
|
+
` Created At: ${createdAtMs ? new Date(createdAtMs).toISOString() : payment.created_at}`
|
|
232
|
+
);
|
|
233
|
+
console.log(
|
|
234
|
+
` Updated At: ${updatedAtMs ? new Date(updatedAtMs).toISOString() : payment.last_updated_at}`
|
|
235
|
+
);
|
|
236
|
+
const routers = payment.routers ?? [];
|
|
237
|
+
console.log(` Routes: ${routers.length}`);
|
|
238
|
+
if (routers.length > 0) {
|
|
239
|
+
for (let i = 0; i < routers.length; i++) {
|
|
240
|
+
const hops = routers[i].nodes.map((node) => truncateMiddle(node.pubkey, 8, 8)).join(" -> ");
|
|
241
|
+
console.log(` #${i + 1}: ${hops}`);
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
function printChannelListHuman(channels) {
|
|
246
|
+
if (channels.length === 0) {
|
|
247
|
+
console.log("No channels found.");
|
|
248
|
+
return;
|
|
249
|
+
}
|
|
250
|
+
const summary = getChannelSummary(channels);
|
|
251
|
+
console.log(`Channels: ${summary.count} total, ${summary.activeCount} ready`);
|
|
252
|
+
console.log(
|
|
253
|
+
`Liquidity: local ${summary.totalLocalCkb} CKB | remote ${summary.totalRemoteCkb} CKB | capacity ${summary.totalCapacityCkb} CKB`
|
|
254
|
+
);
|
|
255
|
+
console.log("");
|
|
256
|
+
console.log(
|
|
257
|
+
"ID PEER STATE LOCAL REMOTE TLC"
|
|
258
|
+
);
|
|
259
|
+
console.log(
|
|
260
|
+
"---------------------------------------------------------------------------------------------------"
|
|
261
|
+
);
|
|
262
|
+
for (const channel of channels) {
|
|
263
|
+
const id = truncateMiddle(channel.channel_id, 10, 8).padEnd(22, " ");
|
|
264
|
+
const peer = truncateMiddle(channel.peer_id, 10, 8).padEnd(22, " ");
|
|
265
|
+
const state = channel.state.state_name.padEnd(24, " ");
|
|
266
|
+
const local = `${shannonsToCkb(channel.local_balance)}`.padStart(8, " ");
|
|
267
|
+
const remote = `${shannonsToCkb(channel.remote_balance)}`.padStart(8, " ");
|
|
268
|
+
const tlcs = `${channel.pending_tlcs.length}`.padStart(4, " ");
|
|
269
|
+
console.log(`${id} ${peer} ${state} ${local} ${remote} ${tlcs}`);
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
function printPeerListHuman(peers) {
|
|
273
|
+
if (peers.length === 0) {
|
|
274
|
+
console.log("No connected peers.");
|
|
275
|
+
return;
|
|
276
|
+
}
|
|
277
|
+
console.log(`Peers: ${peers.length}`);
|
|
278
|
+
console.log("");
|
|
279
|
+
console.log("PEER ID PUBKEY ADDRESS");
|
|
280
|
+
console.log("--------------------------------------------------------------------------");
|
|
281
|
+
for (const peer of peers) {
|
|
282
|
+
const peerId = truncateMiddle(peer.peer_id, 10, 8).padEnd(22, " ");
|
|
283
|
+
const pubkey = truncateMiddle(peer.pubkey, 10, 8).padEnd(22, " ");
|
|
284
|
+
console.log(`${peerId} ${pubkey} ${peer.address}`);
|
|
285
|
+
}
|
|
286
|
+
}
|
|
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
|
+
|
|
303
|
+
// src/commands/binary.ts
|
|
304
|
+
function showProgress(progress) {
|
|
305
|
+
const percent = progress.percent !== void 0 ? ` (${progress.percent}%)` : "";
|
|
306
|
+
process.stdout.write(`\r[${progress.phase}]${percent} ${progress.message}`.padEnd(80));
|
|
307
|
+
if (progress.phase === "installing") {
|
|
308
|
+
console.log();
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
function createBinaryCommand(config) {
|
|
312
|
+
const binary = new Command("binary").description("Fiber binary management");
|
|
313
|
+
binary.command("download").option("--version <version>", "Fiber binary version", DEFAULT_FIBER_VERSION).option("--force", "Force re-download").option("--json").action(async (options) => {
|
|
314
|
+
const info = await downloadFiberBinary({
|
|
315
|
+
installDir: `${config.dataDir}/bin`,
|
|
316
|
+
version: options.version,
|
|
317
|
+
force: Boolean(options.force),
|
|
318
|
+
onProgress: options.json ? void 0 : showProgress
|
|
319
|
+
});
|
|
320
|
+
if (options.json) {
|
|
321
|
+
printJsonSuccess(info);
|
|
322
|
+
} else {
|
|
323
|
+
console.log("\n\u2705 Binary installed successfully!");
|
|
324
|
+
console.log(` Path: ${info.path}`);
|
|
325
|
+
console.log(` Version: ${info.version}`);
|
|
326
|
+
console.log(` Ready: ${info.ready ? "yes" : "no"}`);
|
|
327
|
+
}
|
|
328
|
+
});
|
|
329
|
+
binary.command("info").option("--json").action(async (options) => {
|
|
330
|
+
const info = await getFiberBinaryInfo(`${config.dataDir}/bin`);
|
|
331
|
+
if (options.json) {
|
|
332
|
+
printJsonSuccess(info);
|
|
333
|
+
} else {
|
|
334
|
+
console.log(info.ready ? "\u2705 Binary is ready" : "\u274C Binary not found or not executable");
|
|
335
|
+
console.log(` Path: ${info.path}`);
|
|
336
|
+
console.log(` Version: ${info.version}`);
|
|
337
|
+
}
|
|
338
|
+
});
|
|
339
|
+
return binary;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
// src/commands/channel.ts
|
|
343
|
+
import { randomUUID } from "crypto";
|
|
344
|
+
import { ckbToShannons } from "@fiber-pay/sdk";
|
|
345
|
+
import { Command as Command2 } from "commander";
|
|
346
|
+
|
|
347
|
+
// src/lib/async.ts
|
|
348
|
+
function sleep(ms) {
|
|
349
|
+
return new Promise((resolve2) => setTimeout(resolve2, ms));
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
// src/lib/rpc.ts
|
|
353
|
+
import { FiberRpcClient } from "@fiber-pay/sdk";
|
|
354
|
+
|
|
355
|
+
// src/lib/pid.ts
|
|
356
|
+
import { existsSync, readFileSync, unlinkSync, writeFileSync } from "fs";
|
|
357
|
+
import { join } from "path";
|
|
358
|
+
function getPidFilePath(dataDir) {
|
|
359
|
+
return join(dataDir, "fiber.pid");
|
|
360
|
+
}
|
|
361
|
+
function writePidFile(dataDir, pid) {
|
|
362
|
+
writeFileSync(getPidFilePath(dataDir), String(pid));
|
|
363
|
+
}
|
|
364
|
+
function readPidFile(dataDir) {
|
|
365
|
+
const pidPath = getPidFilePath(dataDir);
|
|
366
|
+
if (!existsSync(pidPath)) return null;
|
|
367
|
+
try {
|
|
368
|
+
return parseInt(readFileSync(pidPath, "utf-8").trim(), 10);
|
|
369
|
+
} catch {
|
|
370
|
+
return null;
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
function removePidFile(dataDir) {
|
|
374
|
+
const pidPath = getPidFilePath(dataDir);
|
|
375
|
+
if (existsSync(pidPath)) {
|
|
376
|
+
unlinkSync(pidPath);
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
function isProcessRunning(pid) {
|
|
380
|
+
try {
|
|
381
|
+
process.kill(pid, 0);
|
|
382
|
+
return true;
|
|
383
|
+
} catch {
|
|
384
|
+
return false;
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
// src/lib/runtime-meta.ts
|
|
389
|
+
import { existsSync as existsSync2, readFileSync as readFileSync2, unlinkSync as unlinkSync2, writeFileSync as writeFileSync2 } from "fs";
|
|
390
|
+
import { join as join2 } from "path";
|
|
391
|
+
function getRuntimePidFilePath(dataDir) {
|
|
392
|
+
return join2(dataDir, "runtime.pid");
|
|
393
|
+
}
|
|
394
|
+
function getRuntimeMetaFilePath(dataDir) {
|
|
395
|
+
return join2(dataDir, "runtime.meta.json");
|
|
396
|
+
}
|
|
397
|
+
function writeRuntimePid(dataDir, pid) {
|
|
398
|
+
writeFileSync2(getRuntimePidFilePath(dataDir), String(pid));
|
|
399
|
+
}
|
|
400
|
+
function readRuntimePid(dataDir) {
|
|
401
|
+
const pidPath = getRuntimePidFilePath(dataDir);
|
|
402
|
+
if (!existsSync2(pidPath)) return null;
|
|
403
|
+
try {
|
|
404
|
+
return Number.parseInt(readFileSync2(pidPath, "utf-8").trim(), 10);
|
|
405
|
+
} catch {
|
|
406
|
+
return null;
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
function writeRuntimeMeta(dataDir, meta) {
|
|
410
|
+
writeFileSync2(getRuntimeMetaFilePath(dataDir), JSON.stringify(meta, null, 2));
|
|
411
|
+
}
|
|
412
|
+
function readRuntimeMeta(dataDir) {
|
|
413
|
+
const metaPath = getRuntimeMetaFilePath(dataDir);
|
|
414
|
+
if (!existsSync2(metaPath)) return null;
|
|
415
|
+
try {
|
|
416
|
+
return JSON.parse(readFileSync2(metaPath, "utf-8"));
|
|
417
|
+
} catch {
|
|
418
|
+
return null;
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
function removeRuntimeFiles(dataDir) {
|
|
422
|
+
const pidPath = getRuntimePidFilePath(dataDir);
|
|
423
|
+
const metaPath = getRuntimeMetaFilePath(dataDir);
|
|
424
|
+
if (existsSync2(pidPath)) {
|
|
425
|
+
unlinkSync2(pidPath);
|
|
426
|
+
}
|
|
427
|
+
if (existsSync2(metaPath)) {
|
|
428
|
+
unlinkSync2(metaPath);
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
// src/lib/rpc.ts
|
|
433
|
+
function normalizeUrl(url) {
|
|
434
|
+
try {
|
|
435
|
+
const normalized = new URL(url).toString();
|
|
436
|
+
return normalized.endsWith("/") ? normalized.slice(0, -1) : normalized;
|
|
437
|
+
} catch {
|
|
438
|
+
return url.endsWith("/") ? url.slice(0, -1) : url;
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
function resolveRuntimeProxyUrl(config) {
|
|
442
|
+
const runtimeMeta = readRuntimeMeta(config.dataDir);
|
|
443
|
+
const runtimePid = readRuntimePid(config.dataDir);
|
|
444
|
+
if (!runtimeMeta || !runtimePid || !isProcessRunning(runtimePid)) {
|
|
445
|
+
return void 0;
|
|
446
|
+
}
|
|
447
|
+
if (!runtimeMeta.proxyListen || !runtimeMeta.fiberRpcUrl) {
|
|
448
|
+
return void 0;
|
|
449
|
+
}
|
|
450
|
+
if (normalizeUrl(runtimeMeta.fiberRpcUrl) !== normalizeUrl(config.rpcUrl)) {
|
|
451
|
+
return void 0;
|
|
452
|
+
}
|
|
453
|
+
if (runtimeMeta.proxyListen.startsWith("http://") || runtimeMeta.proxyListen.startsWith("https://")) {
|
|
454
|
+
return runtimeMeta.proxyListen;
|
|
455
|
+
}
|
|
456
|
+
return `http://${runtimeMeta.proxyListen}`;
|
|
457
|
+
}
|
|
458
|
+
function createRpcClient(config) {
|
|
459
|
+
const resolved = resolveRpcEndpoint(config);
|
|
460
|
+
return new FiberRpcClient({ url: resolved.url });
|
|
461
|
+
}
|
|
462
|
+
function resolveRpcEndpoint(config) {
|
|
463
|
+
const runtimeProxyUrl = resolveRuntimeProxyUrl(config);
|
|
464
|
+
if (runtimeProxyUrl) {
|
|
465
|
+
return {
|
|
466
|
+
url: runtimeProxyUrl,
|
|
467
|
+
target: "runtime-proxy"
|
|
468
|
+
};
|
|
469
|
+
}
|
|
470
|
+
return {
|
|
471
|
+
url: config.rpcUrl,
|
|
472
|
+
target: "node-rpc"
|
|
473
|
+
};
|
|
474
|
+
}
|
|
475
|
+
async function createReadyRpcClient(config, options = {}) {
|
|
476
|
+
const rpc = createRpcClient(config);
|
|
477
|
+
await rpc.waitForReady({ timeout: options.timeout ?? 3e3, interval: options.interval ?? 500 });
|
|
478
|
+
return rpc;
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
// src/lib/runtime-jobs.ts
|
|
482
|
+
async function tryCreateRuntimePaymentJob(runtimeUrl, body) {
|
|
483
|
+
try {
|
|
484
|
+
const response = await fetch(`${runtimeUrl}/jobs/payment`, {
|
|
485
|
+
method: "POST",
|
|
486
|
+
headers: { "content-type": "application/json" },
|
|
487
|
+
body: JSON.stringify(body)
|
|
488
|
+
});
|
|
489
|
+
if (!response.ok) return null;
|
|
490
|
+
return await response.json();
|
|
491
|
+
} catch {
|
|
492
|
+
return null;
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
async function tryCreateRuntimeChannelJob(runtimeUrl, body) {
|
|
496
|
+
try {
|
|
497
|
+
const response = await fetch(`${runtimeUrl}/jobs/channel`, {
|
|
498
|
+
method: "POST",
|
|
499
|
+
headers: { "content-type": "application/json" },
|
|
500
|
+
body: JSON.stringify(body)
|
|
501
|
+
});
|
|
502
|
+
if (!response.ok) return null;
|
|
503
|
+
return await response.json();
|
|
504
|
+
} catch {
|
|
505
|
+
return null;
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
async function tryCreateRuntimeInvoiceJob(runtimeUrl, body) {
|
|
509
|
+
try {
|
|
510
|
+
const response = await fetch(`${runtimeUrl}/jobs/invoice`, {
|
|
511
|
+
method: "POST",
|
|
512
|
+
headers: { "content-type": "application/json" },
|
|
513
|
+
body: JSON.stringify(body)
|
|
514
|
+
});
|
|
515
|
+
if (!response.ok) return null;
|
|
516
|
+
return await response.json();
|
|
517
|
+
} catch {
|
|
518
|
+
return null;
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
async function waitForRuntimeJobTerminal(runtimeUrl, jobId, timeoutSeconds) {
|
|
522
|
+
const startedAt = Date.now();
|
|
523
|
+
while (Date.now() - startedAt < timeoutSeconds * 1e3) {
|
|
524
|
+
const response = await fetch(`${runtimeUrl}/jobs/${jobId}`);
|
|
525
|
+
if (!response.ok) {
|
|
526
|
+
throw new Error(`Failed to fetch runtime job ${jobId}: ${response.status}`);
|
|
527
|
+
}
|
|
528
|
+
const job = await response.json();
|
|
529
|
+
if (job.state === "succeeded" || job.state === "failed" || job.state === "cancelled") {
|
|
530
|
+
return job;
|
|
531
|
+
}
|
|
532
|
+
await sleep(500);
|
|
533
|
+
}
|
|
534
|
+
throw new Error(`Timed out waiting for runtime job ${jobId}`);
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
// src/commands/channel.ts
|
|
538
|
+
function createChannelCommand(config) {
|
|
539
|
+
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("--raw").option("--json").action(async (options) => {
|
|
541
|
+
const rpc = await createReadyRpcClient(config);
|
|
542
|
+
const stateFilter = parseChannelState(options.state);
|
|
543
|
+
const response = await rpc.listChannels(
|
|
544
|
+
options.peer ? { peer_id: options.peer, include_closed: Boolean(options.includeClosed) } : { include_closed: Boolean(options.includeClosed) }
|
|
545
|
+
);
|
|
546
|
+
const channels = stateFilter ? response.channels.filter((item) => item.state.state_name === stateFilter) : response.channels;
|
|
547
|
+
if (options.raw || options.json) {
|
|
548
|
+
printJsonSuccess({ channels, count: channels.length });
|
|
549
|
+
} else {
|
|
550
|
+
printChannelListHuman(channels);
|
|
551
|
+
}
|
|
552
|
+
});
|
|
553
|
+
channel.command("get").argument("<channelId>").option("--raw").option("--json").action(async (channelId, options) => {
|
|
554
|
+
const rpc = await createReadyRpcClient(config);
|
|
555
|
+
const response = await rpc.listChannels({ include_closed: true });
|
|
556
|
+
const found = response.channels.find((item) => item.channel_id === channelId);
|
|
557
|
+
if (!found) {
|
|
558
|
+
if (options.json) {
|
|
559
|
+
printJsonError({
|
|
560
|
+
code: "CHANNEL_NOT_FOUND",
|
|
561
|
+
message: `Channel not found: ${channelId}`,
|
|
562
|
+
recoverable: true,
|
|
563
|
+
suggestion: "List channels first and retry with a valid channel id.",
|
|
564
|
+
details: { channelId }
|
|
565
|
+
});
|
|
566
|
+
} else {
|
|
567
|
+
console.error(`Error: Channel not found: ${channelId}`);
|
|
568
|
+
}
|
|
569
|
+
process.exit(1);
|
|
570
|
+
}
|
|
571
|
+
if (options.raw || options.json) {
|
|
572
|
+
printJsonSuccess(found);
|
|
573
|
+
} else {
|
|
574
|
+
printChannelDetailHuman(found);
|
|
575
|
+
}
|
|
576
|
+
});
|
|
577
|
+
channel.command("watch").option("--interval <seconds>", "Refresh interval", "5").option("--timeout <seconds>").option("--on-timeout <behavior>", "fail | success", "fail").option("--channel <channelId>").option("--peer <peerId>").option("--state <state>").option("--until <state>").option("--include-closed").option("--no-clear").option("--json").action(async (options) => {
|
|
578
|
+
const intervalSeconds = parseInt(options.interval, 10);
|
|
579
|
+
const timeoutSeconds = options.timeout ? parseInt(options.timeout, 10) : void 0;
|
|
580
|
+
const onTimeout = String(options.onTimeout ?? "fail").trim().toLowerCase();
|
|
581
|
+
const stateFilter = parseChannelState(options.state);
|
|
582
|
+
const untilState = parseChannelState(options.until);
|
|
583
|
+
const noClear = Boolean(options.noClear);
|
|
584
|
+
const json = Boolean(options.json);
|
|
585
|
+
if (!["fail", "success"].includes(onTimeout)) {
|
|
586
|
+
if (json) {
|
|
587
|
+
printJsonError({
|
|
588
|
+
code: "CHANNEL_WATCH_INPUT_INVALID",
|
|
589
|
+
message: `Invalid --on-timeout value: ${options.onTimeout}. Expected fail or success`,
|
|
590
|
+
recoverable: true,
|
|
591
|
+
suggestion: "Use `--on-timeout fail` or `--on-timeout success`.",
|
|
592
|
+
details: { provided: options.onTimeout, expected: ["fail", "success"] }
|
|
593
|
+
});
|
|
594
|
+
} else {
|
|
595
|
+
console.error(
|
|
596
|
+
`Error: Invalid --on-timeout value: ${options.onTimeout}. Expected fail or success`
|
|
597
|
+
);
|
|
598
|
+
}
|
|
599
|
+
process.exit(1);
|
|
600
|
+
}
|
|
601
|
+
const rpc = await createReadyRpcClient(config);
|
|
602
|
+
const startedAt = Date.now();
|
|
603
|
+
const previousStates = /* @__PURE__ */ new Map();
|
|
604
|
+
while (true) {
|
|
605
|
+
const response = await rpc.listChannels(
|
|
606
|
+
options.peer ? { peer_id: options.peer, include_closed: Boolean(options.includeClosed) } : { include_closed: Boolean(options.includeClosed) }
|
|
607
|
+
);
|
|
608
|
+
let channels = response.channels;
|
|
609
|
+
if (options.channel) {
|
|
610
|
+
channels = channels.filter((item) => item.channel_id === options.channel);
|
|
611
|
+
}
|
|
612
|
+
if (stateFilter) {
|
|
613
|
+
channels = channels.filter((item) => item.state.state_name === stateFilter);
|
|
614
|
+
}
|
|
615
|
+
const stateChanges = [];
|
|
616
|
+
for (const ch of channels) {
|
|
617
|
+
const prev = previousStates.get(ch.channel_id);
|
|
618
|
+
if (prev && prev !== ch.state.state_name) {
|
|
619
|
+
stateChanges.push({ channelId: ch.channel_id, from: prev, to: ch.state.state_name });
|
|
620
|
+
}
|
|
621
|
+
previousStates.set(ch.channel_id, ch.state.state_name);
|
|
622
|
+
}
|
|
623
|
+
if (json) {
|
|
624
|
+
printJsonEvent("snapshot", {
|
|
625
|
+
channels: channels.map(formatChannel),
|
|
626
|
+
summary: getChannelSummary(channels)
|
|
627
|
+
});
|
|
628
|
+
for (const change of stateChanges) {
|
|
629
|
+
printJsonEvent("state_change", {
|
|
630
|
+
channelId: change.channelId,
|
|
631
|
+
from: change.from,
|
|
632
|
+
to: change.to
|
|
633
|
+
});
|
|
634
|
+
}
|
|
635
|
+
} else {
|
|
636
|
+
if (!noClear) {
|
|
637
|
+
console.clear();
|
|
638
|
+
}
|
|
639
|
+
console.log(`\u23F1\uFE0F Channel monitor - ${(/* @__PURE__ */ new Date()).toISOString()}`);
|
|
640
|
+
console.log(
|
|
641
|
+
` Refresh: ${intervalSeconds}s${timeoutSeconds ? ` | Timeout: ${timeoutSeconds}s` : ""}${untilState ? ` | Until: ${untilState}` : ""}`
|
|
642
|
+
);
|
|
643
|
+
if (stateChanges.length > 0) {
|
|
644
|
+
console.log("\n\u{1F514} State changes:");
|
|
645
|
+
for (const change of stateChanges) {
|
|
646
|
+
console.log(` ${truncateMiddle(change.channelId)}: ${change.from} -> ${change.to}`);
|
|
647
|
+
}
|
|
648
|
+
}
|
|
649
|
+
printChannelListHuman(channels);
|
|
650
|
+
}
|
|
651
|
+
if (untilState && channels.some((item) => item.state.state_name === untilState)) {
|
|
652
|
+
if (json) {
|
|
653
|
+
printJsonEvent("terminal", { reason: "target_state_reached", untilState });
|
|
654
|
+
} else {
|
|
655
|
+
console.log(`
|
|
656
|
+
\u2705 Target state reached: ${untilState}`);
|
|
657
|
+
}
|
|
658
|
+
return;
|
|
659
|
+
}
|
|
660
|
+
if (timeoutSeconds !== void 0 && Date.now() - startedAt >= timeoutSeconds * 1e3) {
|
|
661
|
+
if (onTimeout === "success") {
|
|
662
|
+
if (json) {
|
|
663
|
+
printJsonEvent("terminal", {
|
|
664
|
+
reason: "timeout",
|
|
665
|
+
timeoutSeconds
|
|
666
|
+
});
|
|
667
|
+
} else {
|
|
668
|
+
console.log("\n\u23F0 Monitor timeout reached (treated as success).");
|
|
669
|
+
}
|
|
670
|
+
return;
|
|
671
|
+
}
|
|
672
|
+
if (json) {
|
|
673
|
+
printJsonError({
|
|
674
|
+
code: "CHANNEL_WATCH_TIMEOUT",
|
|
675
|
+
message: `Channel monitor timed out after ${timeoutSeconds}s`,
|
|
676
|
+
recoverable: true,
|
|
677
|
+
suggestion: "Increase timeout or continue monitoring with another watch run.",
|
|
678
|
+
details: { timeoutSeconds }
|
|
679
|
+
});
|
|
680
|
+
process.exit(1);
|
|
681
|
+
}
|
|
682
|
+
console.log("\n\u23F0 Monitor timeout reached.");
|
|
683
|
+
return;
|
|
684
|
+
}
|
|
685
|
+
await sleep(intervalSeconds * 1e3);
|
|
686
|
+
}
|
|
687
|
+
});
|
|
688
|
+
channel.command("open").requiredOption("--peer <peerIdOrMultiaddr>").requiredOption("--funding <ckb>").option("--private").option(
|
|
689
|
+
"--idempotency-key <key>",
|
|
690
|
+
"Reuse this key only when retrying the exact same open intent"
|
|
691
|
+
).option("--json").action(async (options) => {
|
|
692
|
+
const rpc = await createReadyRpcClient(config);
|
|
693
|
+
const json = Boolean(options.json);
|
|
694
|
+
const peerInput = options.peer;
|
|
695
|
+
const fundingCkb = parseFloat(options.funding);
|
|
696
|
+
let peerId = peerInput;
|
|
697
|
+
if (peerInput.includes("/")) {
|
|
698
|
+
await rpc.connectPeer({ address: peerInput });
|
|
699
|
+
const peerIdMatch = peerInput.match(/\/p2p\/([^/]+)/);
|
|
700
|
+
if (peerIdMatch) peerId = peerIdMatch[1];
|
|
701
|
+
}
|
|
702
|
+
const idempotencyKey = typeof options.idempotencyKey === "string" && options.idempotencyKey.trim().length > 0 ? options.idempotencyKey.trim() : `open:${peerId}:${randomUUID()}`;
|
|
703
|
+
const endpoint = resolveRpcEndpoint(config);
|
|
704
|
+
if (endpoint.target === "runtime-proxy") {
|
|
705
|
+
const created = await tryCreateRuntimeChannelJob(endpoint.url, {
|
|
706
|
+
params: {
|
|
707
|
+
action: "open",
|
|
708
|
+
peerId,
|
|
709
|
+
openChannelParams: {
|
|
710
|
+
peer_id: peerId,
|
|
711
|
+
funding_amount: ckbToShannons(fundingCkb),
|
|
712
|
+
public: !options.private
|
|
713
|
+
},
|
|
714
|
+
waitForReady: false
|
|
715
|
+
},
|
|
716
|
+
options: {
|
|
717
|
+
idempotencyKey,
|
|
718
|
+
reuseTerminal: false
|
|
719
|
+
}
|
|
720
|
+
});
|
|
721
|
+
if (created) {
|
|
722
|
+
const payload2 = {
|
|
723
|
+
jobId: created.id,
|
|
724
|
+
jobState: created.state,
|
|
725
|
+
peer: peerId,
|
|
726
|
+
fundingCkb,
|
|
727
|
+
idempotencyKey
|
|
728
|
+
};
|
|
729
|
+
if (json) {
|
|
730
|
+
printJsonSuccess(payload2);
|
|
731
|
+
} else {
|
|
732
|
+
console.log("Channel open job submitted");
|
|
733
|
+
console.log(` Job: ${payload2.jobId}`);
|
|
734
|
+
console.log(` Job State: ${payload2.jobState}`);
|
|
735
|
+
console.log(` Peer: ${payload2.peer}`);
|
|
736
|
+
console.log(` Funding: ${payload2.fundingCkb} CKB`);
|
|
737
|
+
console.log(` Idempotency Key: ${payload2.idempotencyKey}`);
|
|
738
|
+
}
|
|
739
|
+
return;
|
|
740
|
+
}
|
|
741
|
+
}
|
|
742
|
+
const result = await rpc.openChannel({
|
|
743
|
+
peer_id: peerId,
|
|
744
|
+
funding_amount: ckbToShannons(fundingCkb),
|
|
745
|
+
public: !options.private
|
|
746
|
+
});
|
|
747
|
+
const payload = { temporaryChannelId: result.temporary_channel_id, peer: peerId, fundingCkb };
|
|
748
|
+
if (json) {
|
|
749
|
+
printJsonSuccess(payload);
|
|
750
|
+
} else {
|
|
751
|
+
console.log("Channel open initiated");
|
|
752
|
+
console.log(` Temporary Channel ID: ${payload.temporaryChannelId}`);
|
|
753
|
+
console.log(` Peer: ${payload.peer}`);
|
|
754
|
+
console.log(` Funding: ${payload.fundingCkb} CKB`);
|
|
755
|
+
}
|
|
756
|
+
});
|
|
757
|
+
channel.command("accept").argument("<temporaryChannelId>").requiredOption("--funding <ckb>").option("--json").action(async (temporaryChannelId, options) => {
|
|
758
|
+
const rpc = await createReadyRpcClient(config);
|
|
759
|
+
const json = Boolean(options.json);
|
|
760
|
+
const fundingCkb = parseFloat(options.funding);
|
|
761
|
+
const endpoint = resolveRpcEndpoint(config);
|
|
762
|
+
if (endpoint.target === "runtime-proxy") {
|
|
763
|
+
const created = await tryCreateRuntimeChannelJob(endpoint.url, {
|
|
764
|
+
params: {
|
|
765
|
+
action: "accept",
|
|
766
|
+
acceptChannelParams: {
|
|
767
|
+
temporary_channel_id: temporaryChannelId,
|
|
768
|
+
funding_amount: ckbToShannons(fundingCkb)
|
|
769
|
+
}
|
|
770
|
+
},
|
|
771
|
+
options: {
|
|
772
|
+
idempotencyKey: `accept:temporary:${temporaryChannelId}`
|
|
773
|
+
}
|
|
774
|
+
});
|
|
775
|
+
if (created) {
|
|
776
|
+
const payload2 = {
|
|
777
|
+
jobId: created.id,
|
|
778
|
+
jobState: created.state,
|
|
779
|
+
temporaryChannelId,
|
|
780
|
+
fundingCkb
|
|
781
|
+
};
|
|
782
|
+
if (json) {
|
|
783
|
+
printJsonSuccess(payload2);
|
|
784
|
+
} else {
|
|
785
|
+
console.log("Channel accept job submitted");
|
|
786
|
+
console.log(` Job: ${payload2.jobId}`);
|
|
787
|
+
console.log(` Job State: ${payload2.jobState}`);
|
|
788
|
+
console.log(` Temporary Channel ID: ${payload2.temporaryChannelId}`);
|
|
789
|
+
console.log(` Funding: ${payload2.fundingCkb} CKB`);
|
|
790
|
+
}
|
|
791
|
+
return;
|
|
792
|
+
}
|
|
793
|
+
}
|
|
794
|
+
const result = await rpc.acceptChannel({
|
|
795
|
+
temporary_channel_id: temporaryChannelId,
|
|
796
|
+
funding_amount: ckbToShannons(fundingCkb)
|
|
797
|
+
});
|
|
798
|
+
const payload = { channelId: result.channel_id, temporaryChannelId, fundingCkb };
|
|
799
|
+
if (json) {
|
|
800
|
+
printJsonSuccess(payload);
|
|
801
|
+
} else {
|
|
802
|
+
console.log("Channel accepted");
|
|
803
|
+
console.log(` Channel ID: ${payload.channelId}`);
|
|
804
|
+
console.log(` Temporary Channel ID: ${payload.temporaryChannelId}`);
|
|
805
|
+
console.log(` Funding: ${payload.fundingCkb} CKB`);
|
|
806
|
+
}
|
|
807
|
+
});
|
|
808
|
+
channel.command("close").argument("<channelId>").option("--force").option("--json").action(async (channelId, options) => {
|
|
809
|
+
const rpc = await createReadyRpcClient(config);
|
|
810
|
+
const json = Boolean(options.json);
|
|
811
|
+
const endpoint = resolveRpcEndpoint(config);
|
|
812
|
+
if (endpoint.target === "runtime-proxy") {
|
|
813
|
+
const created = await tryCreateRuntimeChannelJob(endpoint.url, {
|
|
814
|
+
params: {
|
|
815
|
+
action: "shutdown",
|
|
816
|
+
channelId,
|
|
817
|
+
shutdownChannelParams: {
|
|
818
|
+
channel_id: channelId,
|
|
819
|
+
force: Boolean(options.force)
|
|
820
|
+
},
|
|
821
|
+
waitForClosed: false
|
|
822
|
+
},
|
|
823
|
+
options: {
|
|
824
|
+
idempotencyKey: `shutdown:channel:${channelId}`
|
|
825
|
+
}
|
|
826
|
+
});
|
|
827
|
+
if (created) {
|
|
828
|
+
const payload2 = {
|
|
829
|
+
jobId: created.id,
|
|
830
|
+
jobState: created.state,
|
|
831
|
+
channelId,
|
|
832
|
+
force: Boolean(options.force),
|
|
833
|
+
message: options.force ? "Channel force close job submitted" : "Channel close job submitted"
|
|
834
|
+
};
|
|
835
|
+
if (json) {
|
|
836
|
+
printJsonSuccess(payload2);
|
|
837
|
+
} else {
|
|
838
|
+
console.log(payload2.message);
|
|
839
|
+
console.log(` Job: ${payload2.jobId}`);
|
|
840
|
+
console.log(` Job State: ${payload2.jobState}`);
|
|
841
|
+
console.log(` Channel ID: ${payload2.channelId}`);
|
|
842
|
+
}
|
|
843
|
+
return;
|
|
844
|
+
}
|
|
845
|
+
}
|
|
846
|
+
await rpc.shutdownChannel({
|
|
847
|
+
channel_id: channelId,
|
|
848
|
+
force: Boolean(options.force)
|
|
849
|
+
});
|
|
850
|
+
const payload = {
|
|
851
|
+
channelId,
|
|
852
|
+
force: Boolean(options.force),
|
|
853
|
+
message: options.force ? "Channel force close initiated" : "Channel close initiated"
|
|
854
|
+
};
|
|
855
|
+
if (json) {
|
|
856
|
+
printJsonSuccess(payload);
|
|
857
|
+
} else {
|
|
858
|
+
console.log(payload.message);
|
|
859
|
+
console.log(` Channel ID: ${payload.channelId}`);
|
|
860
|
+
}
|
|
861
|
+
});
|
|
862
|
+
channel.command("abandon").argument("<channelId>").option("--json").action(async (channelId, options) => {
|
|
863
|
+
const rpc = await createReadyRpcClient(config);
|
|
864
|
+
const json = Boolean(options.json);
|
|
865
|
+
const endpoint = resolveRpcEndpoint(config);
|
|
866
|
+
if (endpoint.target === "runtime-proxy") {
|
|
867
|
+
const created = await tryCreateRuntimeChannelJob(endpoint.url, {
|
|
868
|
+
params: {
|
|
869
|
+
action: "abandon",
|
|
870
|
+
channelId,
|
|
871
|
+
abandonChannelParams: {
|
|
872
|
+
channel_id: channelId
|
|
873
|
+
}
|
|
874
|
+
},
|
|
875
|
+
options: {
|
|
876
|
+
idempotencyKey: `abandon:channel:${channelId}`
|
|
877
|
+
}
|
|
878
|
+
});
|
|
879
|
+
if (created) {
|
|
880
|
+
const payload2 = {
|
|
881
|
+
jobId: created.id,
|
|
882
|
+
jobState: created.state,
|
|
883
|
+
channelId,
|
|
884
|
+
message: "Channel abandon job submitted."
|
|
885
|
+
};
|
|
886
|
+
if (json) {
|
|
887
|
+
printJsonSuccess(payload2);
|
|
888
|
+
} else {
|
|
889
|
+
console.log(payload2.message);
|
|
890
|
+
console.log(` Job: ${payload2.jobId}`);
|
|
891
|
+
console.log(` Job State: ${payload2.jobState}`);
|
|
892
|
+
console.log(` Channel ID: ${payload2.channelId}`);
|
|
893
|
+
}
|
|
894
|
+
return;
|
|
895
|
+
}
|
|
896
|
+
}
|
|
897
|
+
await rpc.abandonChannel({ channel_id: channelId });
|
|
898
|
+
const payload = { channelId, message: "Channel abandoned." };
|
|
899
|
+
if (json) {
|
|
900
|
+
printJsonSuccess(payload);
|
|
901
|
+
} else {
|
|
902
|
+
console.log(payload.message);
|
|
903
|
+
console.log(` Channel ID: ${payload.channelId}`);
|
|
904
|
+
}
|
|
905
|
+
});
|
|
906
|
+
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
|
+
const rpc = await createReadyRpcClient(config);
|
|
908
|
+
const json = Boolean(options.json);
|
|
909
|
+
const updateParams = {
|
|
910
|
+
channel_id: channelId,
|
|
911
|
+
enabled: options.enabled !== void 0 ? options.enabled === "true" : void 0,
|
|
912
|
+
tlc_expiry_delta: options.tlcExpiryDelta,
|
|
913
|
+
tlc_minimum_value: options.tlcMinimumValue,
|
|
914
|
+
tlc_fee_proportional_millionths: options.tlcFeeProportionalMillionths
|
|
915
|
+
};
|
|
916
|
+
const endpoint = resolveRpcEndpoint(config);
|
|
917
|
+
if (endpoint.target === "runtime-proxy") {
|
|
918
|
+
const created = await tryCreateRuntimeChannelJob(endpoint.url, {
|
|
919
|
+
params: {
|
|
920
|
+
action: "update",
|
|
921
|
+
channelId,
|
|
922
|
+
updateChannelParams: updateParams
|
|
923
|
+
},
|
|
924
|
+
options: {
|
|
925
|
+
reuseTerminal: false
|
|
926
|
+
}
|
|
927
|
+
});
|
|
928
|
+
if (created) {
|
|
929
|
+
const payload2 = {
|
|
930
|
+
jobId: created.id,
|
|
931
|
+
jobState: created.state,
|
|
932
|
+
channelId,
|
|
933
|
+
message: "Channel update job submitted."
|
|
934
|
+
};
|
|
935
|
+
if (json) {
|
|
936
|
+
printJsonSuccess(payload2);
|
|
937
|
+
} else {
|
|
938
|
+
console.log(payload2.message);
|
|
939
|
+
console.log(` Job: ${payload2.jobId}`);
|
|
940
|
+
console.log(` Job State: ${payload2.jobState}`);
|
|
941
|
+
console.log(` Channel ID: ${payload2.channelId}`);
|
|
942
|
+
}
|
|
943
|
+
return;
|
|
944
|
+
}
|
|
945
|
+
}
|
|
946
|
+
await rpc.updateChannel(updateParams);
|
|
947
|
+
const payload = { channelId, message: "Channel updated." };
|
|
948
|
+
if (json) {
|
|
949
|
+
printJsonSuccess(payload);
|
|
950
|
+
} else {
|
|
951
|
+
console.log(payload.message);
|
|
952
|
+
console.log(` Channel ID: ${payload.channelId}`);
|
|
953
|
+
}
|
|
954
|
+
});
|
|
955
|
+
return channel;
|
|
956
|
+
}
|
|
957
|
+
|
|
958
|
+
// src/commands/config.ts
|
|
959
|
+
import { existsSync as existsSync4, readFileSync as readFileSync4, writeFileSync as writeFileSync4 } from "fs";
|
|
960
|
+
import { Command as Command3 } from "commander";
|
|
961
|
+
import { parseDocument, stringify as yamlStringify } from "yaml";
|
|
962
|
+
|
|
963
|
+
// src/lib/config.ts
|
|
964
|
+
import { existsSync as existsSync3, mkdirSync, readFileSync as readFileSync3, writeFileSync as writeFileSync3 } from "fs";
|
|
965
|
+
import { join as join3 } from "path";
|
|
966
|
+
|
|
967
|
+
// src/lib/config-templates.ts
|
|
968
|
+
var TESTNET_CONFIG_TEMPLATE_V061 = `# This configuration file only contains the necessary configurations for the testnet deployment.
|
|
969
|
+
# All options' descriptions can be found via \`fnn --help\` and be overridden by command line arguments or environment variables.
|
|
970
|
+
fiber:
|
|
971
|
+
listening_addr: "/ip4/127.0.0.1/tcp/8228"
|
|
972
|
+
bootnode_addrs:
|
|
973
|
+
- "/ip4/54.179.226.154/tcp/8228/p2p/Qmes1EBD4yNo9Ywkfe6eRw9tG1nVNGLDmMud1xJMsoYFKy"
|
|
974
|
+
- "/ip4/54.179.226.154/tcp/18228/p2p/QmdyQWjPtbK4NWWsvy8s69NGJaQULwgeQDT5ZpNDrTNaeV"
|
|
975
|
+
announce_listening_addr: true
|
|
976
|
+
announced_addrs:
|
|
977
|
+
# If you want to announce your fiber node public address to the network, you need to add the address here, please change the ip to your public ip accordingly.
|
|
978
|
+
# - "/ip4/YOUR-FIBER-NODE-PUBLIC-IP/tcp/8228"
|
|
979
|
+
chain: testnet
|
|
980
|
+
# lock script configurations related to fiber network
|
|
981
|
+
# https://github.com/nervosnetwork/fiber-scripts/blob/main/deployment/testnet/migrations/2025-02-28-111246.json
|
|
982
|
+
scripts:
|
|
983
|
+
- name: FundingLock
|
|
984
|
+
script:
|
|
985
|
+
code_hash: "0x6c67887fe201ee0c7853f1682c0b77c0e6214044c156c7558269390a8afa6d7c"
|
|
986
|
+
hash_type: type
|
|
987
|
+
args: "0x"
|
|
988
|
+
cell_deps:
|
|
989
|
+
- type_id:
|
|
990
|
+
code_hash: "0x00000000000000000000000000000000000000000000000000545950455f4944"
|
|
991
|
+
hash_type: type
|
|
992
|
+
args: "0x3cb7c0304fe53f75bb5727e2484d0beae4bd99d979813c6fc97c3cca569f10f6"
|
|
993
|
+
- cell_dep:
|
|
994
|
+
out_point:
|
|
995
|
+
tx_hash: "0x12c569a258dd9c5bd99f632bb8314b1263b90921ba31496467580d6b79dd14a7" # ckb_auth
|
|
996
|
+
index: 0x0
|
|
997
|
+
dep_type: code
|
|
998
|
+
- name: CommitmentLock
|
|
999
|
+
script:
|
|
1000
|
+
code_hash: "0x740dee83f87c6f309824d8fd3fbdd3c8380ee6fc9acc90b1a748438afcdf81d8"
|
|
1001
|
+
hash_type: type
|
|
1002
|
+
args: "0x"
|
|
1003
|
+
cell_deps:
|
|
1004
|
+
- type_id:
|
|
1005
|
+
code_hash: "0x00000000000000000000000000000000000000000000000000545950455f4944"
|
|
1006
|
+
hash_type: type
|
|
1007
|
+
args: "0xf7e458887495cf70dd30d1543cad47dc1dfe9d874177bf19291e4db478d5751b"
|
|
1008
|
+
- cell_dep:
|
|
1009
|
+
out_point:
|
|
1010
|
+
tx_hash: "0x12c569a258dd9c5bd99f632bb8314b1263b90921ba31496467580d6b79dd14a7" #ckb_auth
|
|
1011
|
+
index: 0x0
|
|
1012
|
+
dep_type: code
|
|
1013
|
+
|
|
1014
|
+
rpc:
|
|
1015
|
+
# By default RPC only binds to localhost, thus it only allows accessing from the same machine.
|
|
1016
|
+
# Allowing arbitrary machines to access the JSON-RPC port is dangerous and strongly discouraged.
|
|
1017
|
+
# Please strictly limit the access to only trusted machines.
|
|
1018
|
+
listening_addr: "127.0.0.1:8227"
|
|
1019
|
+
|
|
1020
|
+
ckb:
|
|
1021
|
+
rpc_url: "https://testnet.ckbapp.dev/"
|
|
1022
|
+
udt_whitelist:
|
|
1023
|
+
- name: RUSD
|
|
1024
|
+
script:
|
|
1025
|
+
code_hash: "0x1142755a044bf2ee358cba9f2da187ce928c91cd4dc8692ded0337efa677d21a"
|
|
1026
|
+
hash_type: type
|
|
1027
|
+
args: "0x878fcc6f1f08d48e87bb1c3b3d5083f23f8a39c5d5c764f253b55b998526439b"
|
|
1028
|
+
cell_deps:
|
|
1029
|
+
- type_id:
|
|
1030
|
+
code_hash: "0x00000000000000000000000000000000000000000000000000545950455f4944"
|
|
1031
|
+
hash_type: type
|
|
1032
|
+
args: "0x97d30b723c0b2c66e9cb8d4d0df4ab5d7222cbb00d4a9a2055ce2e5d7f0d8b0f"
|
|
1033
|
+
auto_accept_amount: 1000000000
|
|
1034
|
+
|
|
1035
|
+
services:
|
|
1036
|
+
- fiber
|
|
1037
|
+
- rpc
|
|
1038
|
+
- ckb
|
|
1039
|
+
`;
|
|
1040
|
+
var MAINNET_CONFIG_TEMPLATE_V061 = `# This configuration file only contains the necessary configurations for the mainnet deployment.
|
|
1041
|
+
# All options' descriptions can be found via \`fnn --help\` and be overridden by command line arguments or environment variables.
|
|
1042
|
+
fiber:
|
|
1043
|
+
listening_addr: "/ip4/0.0.0.0/tcp/8228"
|
|
1044
|
+
bootnode_addrs:
|
|
1045
|
+
- "/ip4/43.199.24.44/tcp/8228/p2p/QmZ2gCTfEF6vKsiYFF2STPeA2rRLRim9nMtzfwiE7uMQ4v"
|
|
1046
|
+
- "/ip4/54.255.71.126/tcp/8228/p2p/QmcMLnWraRyxd7PFRgvn1QeYRQS2DGsP6fPFCQjtfMs5b2"
|
|
1047
|
+
announce_listening_addr: true
|
|
1048
|
+
announced_addrs:
|
|
1049
|
+
# If you want to announce your fiber node public address to the network, you need to add the address here.
|
|
1050
|
+
# Please change the ip to your public ip accordingly, and make sure the port is open and reachable from the internet.
|
|
1051
|
+
# - "/ip4/YOUR-FIBER-NODE-PUBLIC-IP/tcp/8228"
|
|
1052
|
+
chain: mainnet
|
|
1053
|
+
# lock script configurations related to fiber network
|
|
1054
|
+
# https://github.com/nervosnetwork/fiber-scripts/blob/main/deployment/mainnet/migrations/2025-02-28-114908.json
|
|
1055
|
+
scripts:
|
|
1056
|
+
- name: FundingLock
|
|
1057
|
+
script:
|
|
1058
|
+
code_hash: "0xe45b1f8f21bff23137035a3ab751d75b36a981deec3e7820194b9c042967f4f1"
|
|
1059
|
+
hash_type: type
|
|
1060
|
+
args: "0x"
|
|
1061
|
+
cell_deps:
|
|
1062
|
+
- type_id:
|
|
1063
|
+
code_hash: "0x00000000000000000000000000000000000000000000000000545950455f4944"
|
|
1064
|
+
hash_type: type
|
|
1065
|
+
args: "0x64818d82a372312fb007c480391e1b9759d21b2c7f7959b9c177d72cdc243394"
|
|
1066
|
+
- cell_dep:
|
|
1067
|
+
out_point:
|
|
1068
|
+
tx_hash: "0x95006eee7b4c0c8ad66e0514c88ed0ae43fc8db27793427de86a348ec720b9d6" # ckb_auth
|
|
1069
|
+
index: 0x0
|
|
1070
|
+
dep_type: code
|
|
1071
|
+
- name: CommitmentLock
|
|
1072
|
+
script:
|
|
1073
|
+
code_hash: "0x2d45c4d3ed3e942f1945386ee82a5d1b7e4bb16d7fe1ab015421174ab747406c"
|
|
1074
|
+
hash_type: type
|
|
1075
|
+
args: "0x"
|
|
1076
|
+
cell_deps:
|
|
1077
|
+
- type_id:
|
|
1078
|
+
code_hash: "0x00000000000000000000000000000000000000000000000000545950455f4944"
|
|
1079
|
+
hash_type: type
|
|
1080
|
+
args: "0xdb16e6dcb17f670e5fb7c556d81e522ec5edb069ad2fa3e898e7ccea6c26a39f"
|
|
1081
|
+
- cell_dep:
|
|
1082
|
+
out_point:
|
|
1083
|
+
tx_hash: "0x95006eee7b4c0c8ad66e0514c88ed0ae43fc8db27793427de86a348ec720b9d6" #ckb_auth
|
|
1084
|
+
index: 0x0
|
|
1085
|
+
dep_type: code
|
|
1086
|
+
|
|
1087
|
+
rpc:
|
|
1088
|
+
# By default RPC only binds to localhost, thus it only allows accessing from the same machine.
|
|
1089
|
+
# Allowing arbitrary machines to access the JSON-RPC port is dangerous and strongly discouraged.
|
|
1090
|
+
# Please strictly limit the access to only trusted machines.
|
|
1091
|
+
listening_addr: "127.0.0.1:8227"
|
|
1092
|
+
|
|
1093
|
+
ckb:
|
|
1094
|
+
# Please use a trusted CKB RPC node, the node should be able to provide the correct data and should be stable.
|
|
1095
|
+
rpc_url: "http://127.0.0.1:8114/"
|
|
1096
|
+
udt_whitelist:
|
|
1097
|
+
## https://github.com/CKBFansDAO/xudtlogos/blob/f2557839ecde0409ba674516a62ae6752bc0daa9/public/tokens/token_list.json#L548
|
|
1098
|
+
- name: USDI
|
|
1099
|
+
script:
|
|
1100
|
+
code_hash: "0xbfa35a9c38a676682b65ade8f02be164d48632281477e36f8dc2f41f79e56bfc"
|
|
1101
|
+
hash_type: type
|
|
1102
|
+
args: "0xd591ebdc69626647e056e13345fd830c8b876bb06aa07ba610479eb77153ea9f"
|
|
1103
|
+
cell_deps:
|
|
1104
|
+
- type_id:
|
|
1105
|
+
code_hash: "0x00000000000000000000000000000000000000000000000000545950455f4944"
|
|
1106
|
+
hash_type: type
|
|
1107
|
+
args: "0x9105ea69838511ca609518d27855c53fed1b5ffaff4cfb334f58b40627d211c4"
|
|
1108
|
+
auto_accept_amount: 10000000
|
|
1109
|
+
|
|
1110
|
+
services:
|
|
1111
|
+
- fiber
|
|
1112
|
+
- rpc
|
|
1113
|
+
- ckb
|
|
1114
|
+
`;
|
|
1115
|
+
function getConfigTemplate(network) {
|
|
1116
|
+
return network === "mainnet" ? MAINNET_CONFIG_TEMPLATE_V061 : TESTNET_CONFIG_TEMPLATE_V061;
|
|
1117
|
+
}
|
|
1118
|
+
|
|
1119
|
+
// src/lib/config.ts
|
|
1120
|
+
var DEFAULT_DATA_DIR = `${process.env.HOME}/.fiber-pay`;
|
|
1121
|
+
var DEFAULT_RPC_URL = "http://127.0.0.1:8227";
|
|
1122
|
+
var DEFAULT_NETWORK = "testnet";
|
|
1123
|
+
function getConfigPath(dataDir) {
|
|
1124
|
+
return join3(dataDir, "config.yml");
|
|
1125
|
+
}
|
|
1126
|
+
function parseNetworkFromConfig(configContent) {
|
|
1127
|
+
const match = configContent.match(/^\s*chain:\s*(testnet|mainnet)\s*$/m);
|
|
1128
|
+
if (!match) return void 0;
|
|
1129
|
+
return match[1];
|
|
1130
|
+
}
|
|
1131
|
+
function parseRpcUrlFromConfig(configContent) {
|
|
1132
|
+
const rpcSectionMatch = configContent.match(
|
|
1133
|
+
/(^|\n)rpc:\n([\s\S]*?)(\n[a-zA-Z_]+:|\nservices:|$)/
|
|
1134
|
+
);
|
|
1135
|
+
const rpcSection = rpcSectionMatch?.[2];
|
|
1136
|
+
if (!rpcSection) return void 0;
|
|
1137
|
+
const match = rpcSection.match(/^\s*listening_addr:\s*"?([^"\n]+)"?\s*$/m);
|
|
1138
|
+
if (!match) return void 0;
|
|
1139
|
+
const listeningAddr = match[1].trim();
|
|
1140
|
+
if (!listeningAddr) return void 0;
|
|
1141
|
+
if (listeningAddr.startsWith("http://") || listeningAddr.startsWith("https://")) {
|
|
1142
|
+
return listeningAddr;
|
|
1143
|
+
}
|
|
1144
|
+
return `http://${listeningAddr}`;
|
|
1145
|
+
}
|
|
1146
|
+
function parseCkbRpcUrlFromConfig(configContent) {
|
|
1147
|
+
const ckbSectionMatch = configContent.match(
|
|
1148
|
+
/(^|\n)ckb:\n([\s\S]*?)(\n[a-zA-Z_]+:|\nservices:|$)/
|
|
1149
|
+
);
|
|
1150
|
+
const ckbSection = ckbSectionMatch?.[2];
|
|
1151
|
+
if (!ckbSection) return void 0;
|
|
1152
|
+
const match = ckbSection.match(/^\s*rpc_url:\s*"?([^"\n]+)"?\s*$/m);
|
|
1153
|
+
return match?.[1]?.trim() || void 0;
|
|
1154
|
+
}
|
|
1155
|
+
function getProfilePath(dataDir) {
|
|
1156
|
+
return join3(dataDir, "profile.json");
|
|
1157
|
+
}
|
|
1158
|
+
function loadProfileConfig(dataDir) {
|
|
1159
|
+
const profilePath = getProfilePath(dataDir);
|
|
1160
|
+
if (!existsSync3(profilePath)) return void 0;
|
|
1161
|
+
try {
|
|
1162
|
+
const raw = readFileSync3(profilePath, "utf-8");
|
|
1163
|
+
return JSON.parse(raw);
|
|
1164
|
+
} catch {
|
|
1165
|
+
return void 0;
|
|
1166
|
+
}
|
|
1167
|
+
}
|
|
1168
|
+
function saveProfileConfig(dataDir, profile) {
|
|
1169
|
+
if (!existsSync3(dataDir)) {
|
|
1170
|
+
mkdirSync(dataDir, { recursive: true });
|
|
1171
|
+
}
|
|
1172
|
+
const profilePath = getProfilePath(dataDir);
|
|
1173
|
+
writeFileSync3(profilePath, `${JSON.stringify(profile, null, 2)}
|
|
1174
|
+
`, "utf-8");
|
|
1175
|
+
}
|
|
1176
|
+
function writeNetworkConfigFile(dataDir, network, options = {}) {
|
|
1177
|
+
const configPath = getConfigPath(dataDir);
|
|
1178
|
+
const alreadyExists = existsSync3(configPath);
|
|
1179
|
+
if (alreadyExists && !options.force) {
|
|
1180
|
+
return { path: configPath, created: false, overwritten: false };
|
|
1181
|
+
}
|
|
1182
|
+
if (!existsSync3(dataDir)) {
|
|
1183
|
+
mkdirSync(dataDir, { recursive: true });
|
|
1184
|
+
}
|
|
1185
|
+
let content = getConfigTemplate(network);
|
|
1186
|
+
if (options.rpcPort !== void 0 || options.p2pPort !== void 0) {
|
|
1187
|
+
const lines = content.split("\n");
|
|
1188
|
+
let section = null;
|
|
1189
|
+
for (let i = 0; i < lines.length; i++) {
|
|
1190
|
+
const sectionMatch = lines[i].match(/^([a-zA-Z_]+):\s*$/);
|
|
1191
|
+
if (sectionMatch) {
|
|
1192
|
+
section = sectionMatch[1];
|
|
1193
|
+
continue;
|
|
1194
|
+
}
|
|
1195
|
+
if (section === "fiber" && options.p2pPort !== void 0 && /^\s*listening_addr:\s*/.test(lines[i])) {
|
|
1196
|
+
lines[i] = ` listening_addr: "/ip4/127.0.0.1/tcp/${options.p2pPort}"`;
|
|
1197
|
+
} else if (section === "rpc" && options.rpcPort !== void 0 && /^\s*listening_addr:\s*/.test(lines[i])) {
|
|
1198
|
+
lines[i] = ` listening_addr: "127.0.0.1:${options.rpcPort}"`;
|
|
1199
|
+
}
|
|
1200
|
+
}
|
|
1201
|
+
content = lines.join("\n");
|
|
1202
|
+
}
|
|
1203
|
+
writeFileSync3(configPath, content, "utf-8");
|
|
1204
|
+
return { path: configPath, created: !alreadyExists, overwritten: alreadyExists };
|
|
1205
|
+
}
|
|
1206
|
+
function ensureNodeConfigFile(dataDir, network) {
|
|
1207
|
+
const configPath = getConfigPath(dataDir);
|
|
1208
|
+
if (!existsSync3(configPath)) {
|
|
1209
|
+
writeNetworkConfigFile(dataDir, network);
|
|
1210
|
+
}
|
|
1211
|
+
return configPath;
|
|
1212
|
+
}
|
|
1213
|
+
function getEffectiveConfig(explicitFlags2) {
|
|
1214
|
+
const dataDir = process.env.FIBER_DATA_DIR || DEFAULT_DATA_DIR;
|
|
1215
|
+
const dataDirSource = explicitFlags2?.has("dataDir") ? "cli" : process.env.FIBER_DATA_DIR ? "env" : "default";
|
|
1216
|
+
const configPath = getConfigPath(dataDir);
|
|
1217
|
+
const configExists = existsSync3(configPath);
|
|
1218
|
+
const configContent = configExists ? readFileSync3(configPath, "utf-8") : void 0;
|
|
1219
|
+
const profile = loadProfileConfig(dataDir);
|
|
1220
|
+
const cliNetwork = explicitFlags2?.has("network") ? process.env.FIBER_NETWORK : void 0;
|
|
1221
|
+
const envNetwork = !explicitFlags2?.has("network") ? process.env.FIBER_NETWORK : void 0;
|
|
1222
|
+
const fileNetwork = configContent ? parseNetworkFromConfig(configContent) : void 0;
|
|
1223
|
+
const network = cliNetwork || envNetwork || fileNetwork || DEFAULT_NETWORK;
|
|
1224
|
+
const networkSource = cliNetwork ? "cli" : envNetwork ? "env" : fileNetwork ? "config" : "default";
|
|
1225
|
+
const cliRpcUrl = explicitFlags2?.has("rpcUrl") ? process.env.FIBER_RPC_URL : void 0;
|
|
1226
|
+
const envRpcUrl = !explicitFlags2?.has("rpcUrl") ? process.env.FIBER_RPC_URL : void 0;
|
|
1227
|
+
const fileRpcUrl = configContent ? parseRpcUrlFromConfig(configContent) : void 0;
|
|
1228
|
+
const rpcUrl = cliRpcUrl || envRpcUrl || fileRpcUrl || DEFAULT_RPC_URL;
|
|
1229
|
+
const rpcUrlSource = cliRpcUrl ? "cli" : envRpcUrl ? "env" : fileRpcUrl ? "config" : "default";
|
|
1230
|
+
const cliBinaryPath = explicitFlags2?.has("binaryPath") ? process.env.FIBER_BINARY_PATH : void 0;
|
|
1231
|
+
const profileBinaryPath = profile?.binaryPath;
|
|
1232
|
+
const envBinaryPath = !explicitFlags2?.has("binaryPath") ? process.env.FIBER_BINARY_PATH : void 0;
|
|
1233
|
+
const binaryPath = cliBinaryPath || profileBinaryPath || envBinaryPath || void 0;
|
|
1234
|
+
const cliKeyPassword = explicitFlags2?.has("keyPassword") ? process.env.FIBER_KEY_PASSWORD : void 0;
|
|
1235
|
+
const profileKeyPassword = profile?.keyPassword;
|
|
1236
|
+
const envKeyPassword = !explicitFlags2?.has("keyPassword") ? process.env.FIBER_KEY_PASSWORD : void 0;
|
|
1237
|
+
const keyPassword = cliKeyPassword || profileKeyPassword || envKeyPassword || void 0;
|
|
1238
|
+
const envCkbRpcUrl = process.env.FIBER_CKB_RPC_URL;
|
|
1239
|
+
const fileCkbRpcUrl = configContent ? parseCkbRpcUrlFromConfig(configContent) : void 0;
|
|
1240
|
+
const ckbRpcUrl = envCkbRpcUrl || fileCkbRpcUrl || void 0;
|
|
1241
|
+
const ckbRpcUrlSource = envCkbRpcUrl ? "env" : fileCkbRpcUrl ? "config" : "unset";
|
|
1242
|
+
const DEFAULT_RUNTIME_PROXY_LISTEN = "127.0.0.1:8229";
|
|
1243
|
+
const cliRuntimeProxyListen = explicitFlags2?.has("runtimeProxyListen") ? process.env.FIBER_RUNTIME_PROXY_LISTEN : void 0;
|
|
1244
|
+
const envRuntimeProxyListen = !explicitFlags2?.has("runtimeProxyListen") ? process.env.FIBER_RUNTIME_PROXY_LISTEN : void 0;
|
|
1245
|
+
const profileRuntimeProxyListen = profile?.runtimeProxyListen;
|
|
1246
|
+
const runtimeProxyListen = cliRuntimeProxyListen || envRuntimeProxyListen || profileRuntimeProxyListen || DEFAULT_RUNTIME_PROXY_LISTEN;
|
|
1247
|
+
const runtimeProxyListenSource = cliRuntimeProxyListen ? "cli" : envRuntimeProxyListen ? "env" : profileRuntimeProxyListen ? "profile" : "default";
|
|
1248
|
+
return {
|
|
1249
|
+
configExists,
|
|
1250
|
+
config: {
|
|
1251
|
+
binaryPath,
|
|
1252
|
+
dataDir,
|
|
1253
|
+
configPath,
|
|
1254
|
+
network,
|
|
1255
|
+
rpcUrl,
|
|
1256
|
+
keyPassword,
|
|
1257
|
+
ckbRpcUrl,
|
|
1258
|
+
runtimeProxyListen
|
|
1259
|
+
},
|
|
1260
|
+
sources: {
|
|
1261
|
+
dataDir: dataDirSource,
|
|
1262
|
+
configPath: "derived",
|
|
1263
|
+
network: networkSource,
|
|
1264
|
+
rpcUrl: rpcUrlSource,
|
|
1265
|
+
ckbRpcUrl: ckbRpcUrlSource,
|
|
1266
|
+
runtimeProxyListen: runtimeProxyListenSource
|
|
1267
|
+
}
|
|
1268
|
+
};
|
|
1269
|
+
}
|
|
1270
|
+
|
|
1271
|
+
// src/commands/config.ts
|
|
1272
|
+
function parseNetworkInput(input) {
|
|
1273
|
+
if (!input) return "testnet";
|
|
1274
|
+
if (input === "testnet" || input === "mainnet") return input;
|
|
1275
|
+
throw new Error(`Invalid network: ${input}. Expected one of: testnet, mainnet`);
|
|
1276
|
+
}
|
|
1277
|
+
function parsePortInput(input, label) {
|
|
1278
|
+
if (input === void 0) return void 0;
|
|
1279
|
+
const parsed = Number.parseInt(input, 10);
|
|
1280
|
+
if (!Number.isInteger(parsed) || parsed < 1 || parsed > 65535) {
|
|
1281
|
+
throw new Error(`Invalid ${label}: ${input}. Expected integer in range 1-65535.`);
|
|
1282
|
+
}
|
|
1283
|
+
return parsed;
|
|
1284
|
+
}
|
|
1285
|
+
function resolvePort(optionValue, envValue, label) {
|
|
1286
|
+
if (optionValue !== void 0) {
|
|
1287
|
+
return { value: parsePortInput(optionValue, label), source: "option" };
|
|
1288
|
+
}
|
|
1289
|
+
if (envValue !== void 0) {
|
|
1290
|
+
return { value: parsePortInput(envValue, label), source: "env" };
|
|
1291
|
+
}
|
|
1292
|
+
return { value: void 0, source: "unset" };
|
|
1293
|
+
}
|
|
1294
|
+
var LEGACY_PATH_ALIASES = {
|
|
1295
|
+
chain: "fiber.chain"
|
|
1296
|
+
};
|
|
1297
|
+
function resolveConfigPathAlias(path) {
|
|
1298
|
+
return LEGACY_PATH_ALIASES[path] ?? path;
|
|
1299
|
+
}
|
|
1300
|
+
function parseConfigPath(path) {
|
|
1301
|
+
const normalized = resolveConfigPathAlias(path).trim();
|
|
1302
|
+
if (!normalized) {
|
|
1303
|
+
throw new Error("Config path cannot be empty.");
|
|
1304
|
+
}
|
|
1305
|
+
const segments = [];
|
|
1306
|
+
const re = /([^.[\]]+)|\[(\d+)\]/g;
|
|
1307
|
+
for (const match of normalized.matchAll(re)) {
|
|
1308
|
+
if (match[1]) {
|
|
1309
|
+
segments.push(match[1]);
|
|
1310
|
+
} else if (match[2]) {
|
|
1311
|
+
segments.push(Number.parseInt(match[2], 10));
|
|
1312
|
+
}
|
|
1313
|
+
}
|
|
1314
|
+
if (segments.length === 0) {
|
|
1315
|
+
throw new Error(`Invalid config path: ${path}`);
|
|
1316
|
+
}
|
|
1317
|
+
return segments;
|
|
1318
|
+
}
|
|
1319
|
+
function parseTypedValue(raw, valueType) {
|
|
1320
|
+
if (valueType === "string") return raw;
|
|
1321
|
+
if (valueType === "null") return null;
|
|
1322
|
+
if (valueType === "number") {
|
|
1323
|
+
const parsed = Number(raw);
|
|
1324
|
+
if (!Number.isFinite(parsed)) {
|
|
1325
|
+
throw new Error(`Invalid number value: ${raw}`);
|
|
1326
|
+
}
|
|
1327
|
+
return parsed;
|
|
1328
|
+
}
|
|
1329
|
+
if (valueType === "boolean") {
|
|
1330
|
+
const lowered2 = raw.toLowerCase();
|
|
1331
|
+
if (lowered2 === "true") return true;
|
|
1332
|
+
if (lowered2 === "false") return false;
|
|
1333
|
+
throw new Error(`Invalid boolean value: ${raw}. Expected true or false.`);
|
|
1334
|
+
}
|
|
1335
|
+
if (valueType === "json") {
|
|
1336
|
+
try {
|
|
1337
|
+
return JSON.parse(raw);
|
|
1338
|
+
} catch {
|
|
1339
|
+
throw new Error(`Invalid JSON value: ${raw}`);
|
|
1340
|
+
}
|
|
1341
|
+
}
|
|
1342
|
+
const lowered = raw.toLowerCase();
|
|
1343
|
+
if (lowered === "true") return true;
|
|
1344
|
+
if (lowered === "false") return false;
|
|
1345
|
+
if (lowered === "null") return null;
|
|
1346
|
+
if (/^-?\d+(\.\d+)?$/.test(raw)) {
|
|
1347
|
+
return Number(raw);
|
|
1348
|
+
}
|
|
1349
|
+
if (raw.startsWith("{") && raw.endsWith("}") || raw.startsWith("[") && raw.endsWith("]")) {
|
|
1350
|
+
try {
|
|
1351
|
+
return JSON.parse(raw);
|
|
1352
|
+
} catch {
|
|
1353
|
+
return raw;
|
|
1354
|
+
}
|
|
1355
|
+
}
|
|
1356
|
+
return raw;
|
|
1357
|
+
}
|
|
1358
|
+
function ensureConfigFileOrExit(configPath, json) {
|
|
1359
|
+
if (!existsSync4(configPath)) {
|
|
1360
|
+
const msg = `Config file not found: ${configPath}. Run \`fiber-pay config init\` first.`;
|
|
1361
|
+
if (json) {
|
|
1362
|
+
printJsonError({
|
|
1363
|
+
code: "CONFIG_NOT_FOUND",
|
|
1364
|
+
message: msg,
|
|
1365
|
+
recoverable: true,
|
|
1366
|
+
suggestion: "Run `fiber-pay config init --network testnet` and retry."
|
|
1367
|
+
});
|
|
1368
|
+
} else {
|
|
1369
|
+
console.error(`Error: ${msg}`);
|
|
1370
|
+
}
|
|
1371
|
+
process.exit(1);
|
|
1372
|
+
}
|
|
1373
|
+
}
|
|
1374
|
+
function normalizeHexScalarsForMutation(content) {
|
|
1375
|
+
return content.replace(
|
|
1376
|
+
/^(\s*)(code_hash|tx_hash|args):\s*(0x[0-9a-fA-F]+)(\s*(#.*))?$/gm,
|
|
1377
|
+
(_match, indent, key, value, tail = "") => `${indent}${key}: "${value}"${tail}`
|
|
1378
|
+
);
|
|
1379
|
+
}
|
|
1380
|
+
function parseConfigDocumentForMutation(configPath) {
|
|
1381
|
+
const raw = readFileSync4(configPath, "utf-8");
|
|
1382
|
+
const normalized = normalizeHexScalarsForMutation(raw);
|
|
1383
|
+
return parseDocument(normalized, {
|
|
1384
|
+
keepSourceTokens: true
|
|
1385
|
+
});
|
|
1386
|
+
}
|
|
1387
|
+
function collectConfigPaths(value, prefix = "") {
|
|
1388
|
+
if (value === null || value === void 0) {
|
|
1389
|
+
return prefix ? [prefix] : [];
|
|
1390
|
+
}
|
|
1391
|
+
if (Array.isArray(value)) {
|
|
1392
|
+
if (value.length === 0) {
|
|
1393
|
+
return prefix ? [prefix] : [];
|
|
1394
|
+
}
|
|
1395
|
+
const result = [];
|
|
1396
|
+
for (let index = 0; index < value.length; index++) {
|
|
1397
|
+
const childPrefix = `${prefix}[${index}]`;
|
|
1398
|
+
result.push(...collectConfigPaths(value[index], childPrefix));
|
|
1399
|
+
}
|
|
1400
|
+
return result;
|
|
1401
|
+
}
|
|
1402
|
+
if (typeof value === "object") {
|
|
1403
|
+
const entries = Object.entries(value);
|
|
1404
|
+
if (entries.length === 0) {
|
|
1405
|
+
return prefix ? [prefix] : [];
|
|
1406
|
+
}
|
|
1407
|
+
const result = [];
|
|
1408
|
+
for (const [key, child] of entries) {
|
|
1409
|
+
const childPrefix = prefix ? `${prefix}.${key}` : key;
|
|
1410
|
+
result.push(...collectConfigPaths(child, childPrefix));
|
|
1411
|
+
}
|
|
1412
|
+
return result;
|
|
1413
|
+
}
|
|
1414
|
+
return prefix ? [prefix] : [];
|
|
1415
|
+
}
|
|
1416
|
+
function createConfigCommand(_config) {
|
|
1417
|
+
const config = new Command3("config").description("Single source configuration management");
|
|
1418
|
+
config.command("init").option(
|
|
1419
|
+
"--data-dir <path>",
|
|
1420
|
+
"Target data directory (overrides FIBER_DATA_DIR for this command)"
|
|
1421
|
+
).option("--network <network>", "testnet | mainnet").option("--rpc-port <port>", "Override rpc.listening_addr port in generated config").option("--p2p-port <port>", "Override fiber.listening_addr port in generated config").option("--proxy-port <port>", "Set runtime proxy port and persist in profile.json").option("--force", "Overwrite existing config file").option("--json").action(async (options) => {
|
|
1422
|
+
const effective = getEffectiveConfig();
|
|
1423
|
+
const dataDir = options.dataDir ?? effective.config.dataDir;
|
|
1424
|
+
const selectedNetwork = options.network ? parseNetworkInput(options.network) : effective.config.network;
|
|
1425
|
+
const rpcPort = resolvePort(options.rpcPort, process.env.FIBER_RPC_PORT, "rpc-port");
|
|
1426
|
+
const p2pPort = resolvePort(options.p2pPort, process.env.FIBER_P2P_PORT, "p2p-port");
|
|
1427
|
+
const result = writeNetworkConfigFile(dataDir, selectedNetwork, {
|
|
1428
|
+
force: Boolean(options.force),
|
|
1429
|
+
rpcPort: rpcPort.value,
|
|
1430
|
+
p2pPort: p2pPort.value
|
|
1431
|
+
});
|
|
1432
|
+
let proxyPort;
|
|
1433
|
+
if (options.proxyPort !== void 0) {
|
|
1434
|
+
proxyPort = parsePortInput(options.proxyPort, "proxy-port");
|
|
1435
|
+
const existing = loadProfileConfig(dataDir) ?? {};
|
|
1436
|
+
existing.runtimeProxyListen = `127.0.0.1:${proxyPort}`;
|
|
1437
|
+
saveProfileConfig(dataDir, existing);
|
|
1438
|
+
}
|
|
1439
|
+
const payload = {
|
|
1440
|
+
configPath: result.path,
|
|
1441
|
+
dataDir,
|
|
1442
|
+
network: selectedNetwork,
|
|
1443
|
+
rpcPort: rpcPort.value,
|
|
1444
|
+
rpcPortSource: rpcPort.source,
|
|
1445
|
+
p2pPort: p2pPort.value,
|
|
1446
|
+
p2pPortSource: p2pPort.source,
|
|
1447
|
+
proxyPort: proxyPort ?? null,
|
|
1448
|
+
created: result.created,
|
|
1449
|
+
overwritten: result.overwritten,
|
|
1450
|
+
skipped: !result.created && !result.overwritten
|
|
1451
|
+
};
|
|
1452
|
+
if (options.json) {
|
|
1453
|
+
printJsonSuccess(payload);
|
|
1454
|
+
} else {
|
|
1455
|
+
if (result.created) {
|
|
1456
|
+
console.log(`\u2705 Config initialized: ${result.path}`);
|
|
1457
|
+
} else if (result.overwritten) {
|
|
1458
|
+
console.log(`\u2705 Config overwritten: ${result.path}`);
|
|
1459
|
+
} else {
|
|
1460
|
+
console.log(`\u2139\uFE0F Config already exists: ${result.path}`);
|
|
1461
|
+
console.log(" Use --force to overwrite.");
|
|
1462
|
+
}
|
|
1463
|
+
if (options.dataDir !== void 0) {
|
|
1464
|
+
console.log(` Data Dir: ${dataDir} (option)`);
|
|
1465
|
+
} else {
|
|
1466
|
+
console.log(` Data Dir: ${dataDir} (${effective.sources.dataDir})`);
|
|
1467
|
+
}
|
|
1468
|
+
console.log(` Network: ${selectedNetwork}`);
|
|
1469
|
+
if (rpcPort.value !== void 0)
|
|
1470
|
+
console.log(` RPC Port: ${rpcPort.value} (${rpcPort.source})`);
|
|
1471
|
+
if (p2pPort.value !== void 0)
|
|
1472
|
+
console.log(` P2P Port: ${p2pPort.value} (${p2pPort.source})`);
|
|
1473
|
+
if (proxyPort !== void 0) console.log(` Proxy Port: ${proxyPort} (profile.json)`);
|
|
1474
|
+
}
|
|
1475
|
+
});
|
|
1476
|
+
config.command("show").option("--effective", "Debug resolved values and value source").option("--json").action(async (options) => {
|
|
1477
|
+
const effective = getEffectiveConfig();
|
|
1478
|
+
if (options.effective) {
|
|
1479
|
+
const payload = {
|
|
1480
|
+
config: effective.config,
|
|
1481
|
+
sources: effective.sources,
|
|
1482
|
+
configExists: effective.configExists
|
|
1483
|
+
};
|
|
1484
|
+
if (options.json) {
|
|
1485
|
+
printJsonSuccess(payload);
|
|
1486
|
+
} else {
|
|
1487
|
+
console.log("Effective Config");
|
|
1488
|
+
console.log(` Data Dir: ${effective.config.dataDir} (${effective.sources.dataDir})`);
|
|
1489
|
+
console.log(` Config Path: ${effective.config.configPath}`);
|
|
1490
|
+
console.log(` Network: ${effective.config.network} (${effective.sources.network})`);
|
|
1491
|
+
console.log(` RPC URL: ${effective.config.rpcUrl} (${effective.sources.rpcUrl})`);
|
|
1492
|
+
console.log(` Exists: ${effective.configExists ? "yes" : "no"}`);
|
|
1493
|
+
}
|
|
1494
|
+
return;
|
|
1495
|
+
}
|
|
1496
|
+
if (!effective.configExists) {
|
|
1497
|
+
if (options.json) {
|
|
1498
|
+
printJsonError({
|
|
1499
|
+
code: "CONFIG_NOT_FOUND",
|
|
1500
|
+
message: `Config file not found: ${effective.config.configPath}`,
|
|
1501
|
+
recoverable: true,
|
|
1502
|
+
suggestion: "Run `fiber-pay config init --network testnet` and retry.",
|
|
1503
|
+
details: { configPath: effective.config.configPath }
|
|
1504
|
+
});
|
|
1505
|
+
} else {
|
|
1506
|
+
console.error(`Error: Config file not found: ${effective.config.configPath}`);
|
|
1507
|
+
console.error("Run: fiber-pay config init --network testnet");
|
|
1508
|
+
}
|
|
1509
|
+
process.exit(1);
|
|
1510
|
+
}
|
|
1511
|
+
const content = readFileSync4(effective.config.configPath, "utf-8");
|
|
1512
|
+
const fileNetwork = parseNetworkFromConfig(content) || "unknown";
|
|
1513
|
+
if (options.json) {
|
|
1514
|
+
printJsonSuccess({
|
|
1515
|
+
path: effective.config.configPath,
|
|
1516
|
+
network: fileNetwork,
|
|
1517
|
+
content
|
|
1518
|
+
});
|
|
1519
|
+
} else {
|
|
1520
|
+
console.log(`# ${effective.config.configPath} (${fileNetwork})`);
|
|
1521
|
+
console.log(content);
|
|
1522
|
+
}
|
|
1523
|
+
});
|
|
1524
|
+
config.command("get").description("Get config value by path (e.g. fiber.chain, ckb.udt_whitelist[0].name)").argument("<path>", "Config path").option("--json").action(async (path, options) => {
|
|
1525
|
+
const effective = getEffectiveConfig();
|
|
1526
|
+
const json = Boolean(options.json);
|
|
1527
|
+
const configPath = effective.config.configPath;
|
|
1528
|
+
ensureConfigFileOrExit(configPath, json);
|
|
1529
|
+
const doc = parseConfigDocumentForMutation(configPath);
|
|
1530
|
+
const segments = parseConfigPath(path);
|
|
1531
|
+
const value = doc.getIn(segments);
|
|
1532
|
+
if (value === void 0) {
|
|
1533
|
+
const msg = `Config path not found: ${resolveConfigPathAlias(path)}`;
|
|
1534
|
+
if (json) {
|
|
1535
|
+
printJsonError({
|
|
1536
|
+
code: "CONFIG_PATH_NOT_FOUND",
|
|
1537
|
+
message: msg,
|
|
1538
|
+
recoverable: true,
|
|
1539
|
+
suggestion: "Use `fiber-pay config list` to inspect available paths."
|
|
1540
|
+
});
|
|
1541
|
+
} else {
|
|
1542
|
+
console.error(`Error: ${msg}`);
|
|
1543
|
+
}
|
|
1544
|
+
process.exit(1);
|
|
1545
|
+
}
|
|
1546
|
+
if (json) {
|
|
1547
|
+
printJsonSuccess({ path: resolveConfigPathAlias(path), value });
|
|
1548
|
+
} else if (typeof value === "object") {
|
|
1549
|
+
console.log(yamlStringify(value).trimEnd());
|
|
1550
|
+
} else {
|
|
1551
|
+
console.log(String(value));
|
|
1552
|
+
}
|
|
1553
|
+
});
|
|
1554
|
+
config.command("set").description("Set config value by path (supports nested keys and array indexes)").argument("<path>", "Config path").argument("<value>", "New value").option("--type <type>", "auto|string|number|boolean|null|json", "auto").option("--json").action(async (path, value, options) => {
|
|
1555
|
+
const effective = getEffectiveConfig();
|
|
1556
|
+
const json = Boolean(options.json);
|
|
1557
|
+
const configPath = effective.config.configPath;
|
|
1558
|
+
ensureConfigFileOrExit(configPath, json);
|
|
1559
|
+
const valueType = String(options.type ?? "auto");
|
|
1560
|
+
if (!["auto", "string", "number", "boolean", "null", "json"].includes(valueType)) {
|
|
1561
|
+
const msg = `Invalid --type: ${options.type}. Expected auto|string|number|boolean|null|json`;
|
|
1562
|
+
if (json) {
|
|
1563
|
+
printJsonError({
|
|
1564
|
+
code: "CONFIG_VALUE_TYPE_INVALID",
|
|
1565
|
+
message: msg,
|
|
1566
|
+
recoverable: true
|
|
1567
|
+
});
|
|
1568
|
+
} else {
|
|
1569
|
+
console.error(`Error: ${msg}`);
|
|
1570
|
+
}
|
|
1571
|
+
process.exit(1);
|
|
1572
|
+
}
|
|
1573
|
+
const doc = parseConfigDocumentForMutation(configPath);
|
|
1574
|
+
const resolvedPath = resolveConfigPathAlias(path);
|
|
1575
|
+
const segments = parseConfigPath(path);
|
|
1576
|
+
const parsedValue = parseTypedValue(value, valueType);
|
|
1577
|
+
doc.setIn(segments, parsedValue);
|
|
1578
|
+
writeFileSync4(configPath, doc.toString({ lineWidth: 0 }), "utf-8");
|
|
1579
|
+
if (json) {
|
|
1580
|
+
printJsonSuccess({ path: resolvedPath, value: parsedValue, configPath });
|
|
1581
|
+
} else {
|
|
1582
|
+
console.log(`\u2705 Set ${resolvedPath} = ${value} in ${configPath}`);
|
|
1583
|
+
}
|
|
1584
|
+
});
|
|
1585
|
+
config.command("unset").description("Remove config value by path").argument("<path>", "Config path").option("--json").action(async (path, options) => {
|
|
1586
|
+
const effective = getEffectiveConfig();
|
|
1587
|
+
const json = Boolean(options.json);
|
|
1588
|
+
const configPath = effective.config.configPath;
|
|
1589
|
+
ensureConfigFileOrExit(configPath, json);
|
|
1590
|
+
const doc = parseConfigDocumentForMutation(configPath);
|
|
1591
|
+
const resolvedPath = resolveConfigPathAlias(path);
|
|
1592
|
+
const segments = parseConfigPath(path);
|
|
1593
|
+
const removed = doc.deleteIn(segments);
|
|
1594
|
+
if (!removed) {
|
|
1595
|
+
const msg = `Config path not found: ${resolvedPath}`;
|
|
1596
|
+
if (json) {
|
|
1597
|
+
printJsonError({
|
|
1598
|
+
code: "CONFIG_PATH_NOT_FOUND",
|
|
1599
|
+
message: msg,
|
|
1600
|
+
recoverable: true,
|
|
1601
|
+
suggestion: "Use `fiber-pay config list` to inspect available paths."
|
|
1602
|
+
});
|
|
1603
|
+
} else {
|
|
1604
|
+
console.error(`Error: ${msg}`);
|
|
1605
|
+
}
|
|
1606
|
+
process.exit(1);
|
|
1607
|
+
}
|
|
1608
|
+
writeFileSync4(configPath, doc.toString({ lineWidth: 0 }), "utf-8");
|
|
1609
|
+
if (json) {
|
|
1610
|
+
printJsonSuccess({ path: resolvedPath, removed: true, configPath });
|
|
1611
|
+
} else {
|
|
1612
|
+
console.log(`\u2705 Removed ${resolvedPath} from ${configPath}`);
|
|
1613
|
+
}
|
|
1614
|
+
});
|
|
1615
|
+
config.command("list").description("List config key paths (optionally under a prefix)").option("--prefix <path>", "List only under this path").option("--json").action(async (options) => {
|
|
1616
|
+
const effective = getEffectiveConfig();
|
|
1617
|
+
const json = Boolean(options.json);
|
|
1618
|
+
const configPath = effective.config.configPath;
|
|
1619
|
+
ensureConfigFileOrExit(configPath, json);
|
|
1620
|
+
const doc = parseConfigDocumentForMutation(configPath);
|
|
1621
|
+
const prefix = options.prefix ? resolveConfigPathAlias(String(options.prefix)) : void 0;
|
|
1622
|
+
let rootValue = doc.toJSON();
|
|
1623
|
+
let basePrefix = "";
|
|
1624
|
+
if (prefix) {
|
|
1625
|
+
const segments = parseConfigPath(prefix);
|
|
1626
|
+
rootValue = doc.getIn(segments);
|
|
1627
|
+
if (rootValue === void 0) {
|
|
1628
|
+
const msg = `Config path not found: ${prefix}`;
|
|
1629
|
+
if (json) {
|
|
1630
|
+
printJsonError({
|
|
1631
|
+
code: "CONFIG_PATH_NOT_FOUND",
|
|
1632
|
+
message: msg,
|
|
1633
|
+
recoverable: true,
|
|
1634
|
+
suggestion: "Use `fiber-pay config list` without prefix first."
|
|
1635
|
+
});
|
|
1636
|
+
} else {
|
|
1637
|
+
console.error(`Error: ${msg}`);
|
|
1638
|
+
}
|
|
1639
|
+
process.exit(1);
|
|
1640
|
+
}
|
|
1641
|
+
basePrefix = prefix;
|
|
1642
|
+
}
|
|
1643
|
+
const paths = collectConfigPaths(rootValue, basePrefix).sort((a, b) => a.localeCompare(b));
|
|
1644
|
+
if (json) {
|
|
1645
|
+
printJsonSuccess({ prefix: prefix ?? null, paths, count: paths.length });
|
|
1646
|
+
} else {
|
|
1647
|
+
for (const path of paths) {
|
|
1648
|
+
console.log(path);
|
|
1649
|
+
}
|
|
1650
|
+
}
|
|
1651
|
+
});
|
|
1652
|
+
const profile = new Command3("profile").description(
|
|
1653
|
+
"Manage profile.json settings (CLI-only overrides)"
|
|
1654
|
+
);
|
|
1655
|
+
const PROFILE_KEYS = ["binaryPath", "keyPassword", "runtimeProxyListen"];
|
|
1656
|
+
profile.command("show").description("Show current profile.json values").option("--json").action(async (options) => {
|
|
1657
|
+
const effective = getEffectiveConfig();
|
|
1658
|
+
const profileData = loadProfileConfig(effective.config.dataDir);
|
|
1659
|
+
if (options.json) {
|
|
1660
|
+
printJsonSuccess({ dataDir: effective.config.dataDir, profile: profileData ?? {} });
|
|
1661
|
+
} else {
|
|
1662
|
+
if (!profileData || Object.keys(profileData).length === 0) {
|
|
1663
|
+
console.log("No profile settings found.");
|
|
1664
|
+
console.log(` Location: ${effective.config.dataDir}/profile.json`);
|
|
1665
|
+
return;
|
|
1666
|
+
}
|
|
1667
|
+
console.log("Profile Settings");
|
|
1668
|
+
console.log(` Location: ${effective.config.dataDir}/profile.json`);
|
|
1669
|
+
for (const key of PROFILE_KEYS) {
|
|
1670
|
+
if (profileData[key] !== void 0) {
|
|
1671
|
+
console.log(` ${key}: ${profileData[key]}`);
|
|
1672
|
+
}
|
|
1673
|
+
}
|
|
1674
|
+
}
|
|
1675
|
+
});
|
|
1676
|
+
profile.command("set").description("Set a profile key").argument("<key>", `One of: ${PROFILE_KEYS.join(", ")}`).argument("<value>").option("--json").action(async (key, value, options) => {
|
|
1677
|
+
if (!PROFILE_KEYS.includes(key)) {
|
|
1678
|
+
const msg = `Unknown profile key: ${key}. Valid keys: ${PROFILE_KEYS.join(", ")}`;
|
|
1679
|
+
if (options.json) {
|
|
1680
|
+
printJsonError({
|
|
1681
|
+
code: "PROFILE_INVALID_KEY",
|
|
1682
|
+
message: msg,
|
|
1683
|
+
recoverable: true,
|
|
1684
|
+
suggestion: `Use one of: ${PROFILE_KEYS.join(", ")}`
|
|
1685
|
+
});
|
|
1686
|
+
} else {
|
|
1687
|
+
console.error(`Error: ${msg}`);
|
|
1688
|
+
}
|
|
1689
|
+
process.exit(1);
|
|
1690
|
+
}
|
|
1691
|
+
const effective = getEffectiveConfig();
|
|
1692
|
+
const existing = loadProfileConfig(effective.config.dataDir) ?? {};
|
|
1693
|
+
existing[key] = value;
|
|
1694
|
+
saveProfileConfig(effective.config.dataDir, existing);
|
|
1695
|
+
if (options.json) {
|
|
1696
|
+
printJsonSuccess({ key, value, dataDir: effective.config.dataDir });
|
|
1697
|
+
} else {
|
|
1698
|
+
console.log(`\u2705 Profile key "${key}" set to "${value}"`);
|
|
1699
|
+
}
|
|
1700
|
+
});
|
|
1701
|
+
profile.command("unset").description("Remove a profile key").argument("<key>", `One of: ${PROFILE_KEYS.join(", ")}`).option("--json").action(async (key, options) => {
|
|
1702
|
+
if (!PROFILE_KEYS.includes(key)) {
|
|
1703
|
+
const msg = `Unknown profile key: ${key}. Valid keys: ${PROFILE_KEYS.join(", ")}`;
|
|
1704
|
+
if (options.json) {
|
|
1705
|
+
printJsonError({
|
|
1706
|
+
code: "PROFILE_INVALID_KEY",
|
|
1707
|
+
message: msg,
|
|
1708
|
+
recoverable: true,
|
|
1709
|
+
suggestion: `Use one of: ${PROFILE_KEYS.join(", ")}`
|
|
1710
|
+
});
|
|
1711
|
+
} else {
|
|
1712
|
+
console.error(`Error: ${msg}`);
|
|
1713
|
+
}
|
|
1714
|
+
process.exit(1);
|
|
1715
|
+
}
|
|
1716
|
+
const effective = getEffectiveConfig();
|
|
1717
|
+
const existing = loadProfileConfig(effective.config.dataDir) ?? {};
|
|
1718
|
+
delete existing[key];
|
|
1719
|
+
saveProfileConfig(effective.config.dataDir, existing);
|
|
1720
|
+
if (options.json) {
|
|
1721
|
+
printJsonSuccess({ key, removed: true, dataDir: effective.config.dataDir });
|
|
1722
|
+
} else {
|
|
1723
|
+
console.log(`\u2705 Profile key "${key}" removed`);
|
|
1724
|
+
}
|
|
1725
|
+
});
|
|
1726
|
+
config.addCommand(profile);
|
|
1727
|
+
return config;
|
|
1728
|
+
}
|
|
1729
|
+
|
|
1730
|
+
// src/commands/graph.ts
|
|
1731
|
+
import { shannonsToCkb as shannonsToCkb2, toHex as toHex2 } from "@fiber-pay/sdk";
|
|
1732
|
+
import { Command as Command4 } from "commander";
|
|
1733
|
+
function printGraphNodeListHuman(nodes) {
|
|
1734
|
+
if (nodes.length === 0) {
|
|
1735
|
+
console.log("No nodes found in the graph.");
|
|
1736
|
+
return;
|
|
1737
|
+
}
|
|
1738
|
+
console.log(`Graph Nodes: ${nodes.length}`);
|
|
1739
|
+
console.log("");
|
|
1740
|
+
console.log("NODE ID ALIAS VERSION MIN FUNDING AGE");
|
|
1741
|
+
console.log("---------------------------------------------------------------------------------");
|
|
1742
|
+
for (const node of nodes) {
|
|
1743
|
+
const nodeId = truncateMiddle(node.node_id, 10, 8).padEnd(22, " ");
|
|
1744
|
+
const alias = (node.node_name || "(unnamed)").slice(0, 20).padEnd(20, " ");
|
|
1745
|
+
const version = (node.version || "?").slice(0, 10).padEnd(10, " ");
|
|
1746
|
+
const minFunding = shannonsToCkb2(node.auto_accept_min_ckb_funding_amount).toString().padStart(12, " ");
|
|
1747
|
+
const age = formatAge(parseHexTimestampMs(node.timestamp));
|
|
1748
|
+
console.log(`${nodeId} ${alias} ${version} ${minFunding} ${age}`);
|
|
1749
|
+
}
|
|
1750
|
+
}
|
|
1751
|
+
function printGraphChannelListHuman(channels) {
|
|
1752
|
+
if (channels.length === 0) {
|
|
1753
|
+
console.log("No channels found in the graph.");
|
|
1754
|
+
return;
|
|
1755
|
+
}
|
|
1756
|
+
console.log(`Graph Channels: ${channels.length}`);
|
|
1757
|
+
console.log("");
|
|
1758
|
+
console.log(
|
|
1759
|
+
"OUTPOINT NODE1 NODE2 CAPACITY AGE"
|
|
1760
|
+
);
|
|
1761
|
+
console.log(
|
|
1762
|
+
"----------------------------------------------------------------------------------------------"
|
|
1763
|
+
);
|
|
1764
|
+
for (const ch of channels) {
|
|
1765
|
+
const outpoint = ch.channel_outpoint ? truncateMiddle(`${ch.channel_outpoint.tx_hash}:${ch.channel_outpoint.index}`, 10, 8) : "n/a";
|
|
1766
|
+
const n1 = truncateMiddle(ch.node1, 10, 8).padEnd(22, " ");
|
|
1767
|
+
const n2 = truncateMiddle(ch.node2, 10, 8).padEnd(22, " ");
|
|
1768
|
+
const capacity = `${shannonsToCkb2(ch.capacity)} CKB`.padStart(12, " ");
|
|
1769
|
+
const age = formatAge(parseHexTimestampMs(ch.created_timestamp));
|
|
1770
|
+
console.log(`${outpoint.padEnd(22, " ")} ${n1} ${n2} ${capacity} ${age}`);
|
|
1771
|
+
}
|
|
1772
|
+
}
|
|
1773
|
+
function createGraphCommand(config) {
|
|
1774
|
+
const graph = new Command4("graph").description("Network graph queries (nodes & channels)");
|
|
1775
|
+
graph.command("nodes").option("--limit <n>", "Maximum number of nodes to return", "50").option("--after <cursor>", "Pagination cursor from a previous query").option("--json").action(async (options) => {
|
|
1776
|
+
const rpc = await createReadyRpcClient(config);
|
|
1777
|
+
const limit = toHex2(BigInt(parseInt(options.limit, 10)));
|
|
1778
|
+
const result = await rpc.graphNodes({
|
|
1779
|
+
limit,
|
|
1780
|
+
after: options.after
|
|
1781
|
+
});
|
|
1782
|
+
if (options.json) {
|
|
1783
|
+
printJsonSuccess({ nodes: result.nodes, lastCursor: result.last_cursor });
|
|
1784
|
+
} else {
|
|
1785
|
+
printGraphNodeListHuman(result.nodes);
|
|
1786
|
+
if (result.last_cursor && result.last_cursor !== "0x0") {
|
|
1787
|
+
console.log(`
|
|
1788
|
+
Next cursor: ${result.last_cursor}`);
|
|
1789
|
+
}
|
|
1790
|
+
}
|
|
1791
|
+
});
|
|
1792
|
+
graph.command("channels").option("--limit <n>", "Maximum number of channels to return", "50").option("--after <cursor>", "Pagination cursor from a previous query").option("--json").action(async (options) => {
|
|
1793
|
+
const rpc = await createReadyRpcClient(config);
|
|
1794
|
+
const limit = toHex2(BigInt(parseInt(options.limit, 10)));
|
|
1795
|
+
const result = await rpc.graphChannels({
|
|
1796
|
+
limit,
|
|
1797
|
+
after: options.after
|
|
1798
|
+
});
|
|
1799
|
+
if (options.json) {
|
|
1800
|
+
printJsonSuccess({ channels: result.channels, lastCursor: result.last_cursor });
|
|
1801
|
+
} else {
|
|
1802
|
+
printGraphChannelListHuman(result.channels);
|
|
1803
|
+
if (result.last_cursor && result.last_cursor !== "0x0") {
|
|
1804
|
+
console.log(`
|
|
1805
|
+
Next cursor: ${result.last_cursor}`);
|
|
1806
|
+
}
|
|
1807
|
+
}
|
|
1808
|
+
});
|
|
1809
|
+
return graph;
|
|
1810
|
+
}
|
|
1811
|
+
|
|
1812
|
+
// src/commands/invoice.ts
|
|
1813
|
+
import { ckbToShannons as ckbToShannons2, randomBytes32, shannonsToCkb as shannonsToCkb3, toHex as toHex3 } from "@fiber-pay/sdk";
|
|
1814
|
+
import { Command as Command5 } from "commander";
|
|
1815
|
+
function createInvoiceCommand(config) {
|
|
1816
|
+
const invoice = new Command5("invoice").description("Invoice lifecycle and status commands");
|
|
1817
|
+
invoice.command("create").argument("[amount]").option("--amount <ckb>").option("--description <text>").option("--expiry <minutes>").option("--json").action(async (amountArg, options) => {
|
|
1818
|
+
const rpc = await createReadyRpcClient(config);
|
|
1819
|
+
const json = Boolean(options.json);
|
|
1820
|
+
const amountCkb = options.amount ? parseFloat(options.amount) : amountArg ? parseFloat(amountArg) : 0;
|
|
1821
|
+
if (!amountCkb) {
|
|
1822
|
+
if (options.json) {
|
|
1823
|
+
printJsonError({
|
|
1824
|
+
code: "INVOICE_CREATE_INPUT_INVALID",
|
|
1825
|
+
message: "Amount required. Usage: invoice create --amount <CKB>",
|
|
1826
|
+
recoverable: true,
|
|
1827
|
+
suggestion: "Provide a valid positive amount via `--amount <CKB>`."
|
|
1828
|
+
});
|
|
1829
|
+
} else {
|
|
1830
|
+
console.error("Error: Amount required. Usage: invoice create --amount <CKB>");
|
|
1831
|
+
}
|
|
1832
|
+
process.exit(1);
|
|
1833
|
+
}
|
|
1834
|
+
const expirySeconds = (options.expiry ? parseInt(options.expiry, 10) : 60) * 60;
|
|
1835
|
+
const currency = config.network === "mainnet" ? "Fibb" : "Fibt";
|
|
1836
|
+
const endpoint = resolveRpcEndpoint(config);
|
|
1837
|
+
if (endpoint.target === "runtime-proxy") {
|
|
1838
|
+
const created = await tryCreateRuntimeInvoiceJob(endpoint.url, {
|
|
1839
|
+
params: {
|
|
1840
|
+
action: "create",
|
|
1841
|
+
newInvoiceParams: {
|
|
1842
|
+
amount: ckbToShannons2(amountCkb),
|
|
1843
|
+
currency,
|
|
1844
|
+
description: options.description,
|
|
1845
|
+
expiry: toHex3(expirySeconds),
|
|
1846
|
+
payment_preimage: randomBytes32()
|
|
1847
|
+
},
|
|
1848
|
+
waitForTerminal: false
|
|
1849
|
+
}
|
|
1850
|
+
});
|
|
1851
|
+
if (created) {
|
|
1852
|
+
const job = await waitForRuntimeJobTerminal(endpoint.url, created.id, 60);
|
|
1853
|
+
if (job.state !== "succeeded") {
|
|
1854
|
+
throw new Error(job.error?.message ?? `Invoice create job ${job.state}`);
|
|
1855
|
+
}
|
|
1856
|
+
const result2 = job.result ?? {};
|
|
1857
|
+
const payload2 = {
|
|
1858
|
+
jobId: job.id,
|
|
1859
|
+
invoice: result2.invoiceAddress,
|
|
1860
|
+
paymentHash: result2.paymentHash,
|
|
1861
|
+
amountCkb,
|
|
1862
|
+
expiresAt: new Date(Date.now() + expirySeconds * 1e3).toISOString(),
|
|
1863
|
+
status: (result2.status ?? "Open").toLowerCase()
|
|
1864
|
+
};
|
|
1865
|
+
if (json) {
|
|
1866
|
+
printJsonSuccess(payload2);
|
|
1867
|
+
} else {
|
|
1868
|
+
console.log("Invoice created");
|
|
1869
|
+
console.log(` Job: ${payload2.jobId}`);
|
|
1870
|
+
console.log(` Payment Hash: ${payload2.paymentHash ?? "n/a"}`);
|
|
1871
|
+
console.log(` Amount: ${payload2.amountCkb} CKB`);
|
|
1872
|
+
console.log(` Expires At: ${payload2.expiresAt}`);
|
|
1873
|
+
console.log(` Invoice: ${payload2.invoice ?? "n/a"}`);
|
|
1874
|
+
}
|
|
1875
|
+
return;
|
|
1876
|
+
}
|
|
1877
|
+
}
|
|
1878
|
+
const result = await rpc.newInvoice({
|
|
1879
|
+
amount: ckbToShannons2(amountCkb),
|
|
1880
|
+
currency,
|
|
1881
|
+
description: options.description,
|
|
1882
|
+
expiry: toHex3(expirySeconds),
|
|
1883
|
+
payment_preimage: randomBytes32()
|
|
1884
|
+
});
|
|
1885
|
+
const payload = {
|
|
1886
|
+
invoice: result.invoice_address,
|
|
1887
|
+
paymentHash: result.invoice.data.payment_hash,
|
|
1888
|
+
amountCkb,
|
|
1889
|
+
expiresAt: new Date(Date.now() + expirySeconds * 1e3).toISOString(),
|
|
1890
|
+
status: "open"
|
|
1891
|
+
};
|
|
1892
|
+
if (json) {
|
|
1893
|
+
printJsonSuccess(payload);
|
|
1894
|
+
} else {
|
|
1895
|
+
console.log("Invoice created");
|
|
1896
|
+
console.log(` Payment Hash: ${payload.paymentHash}`);
|
|
1897
|
+
console.log(` Amount: ${payload.amountCkb} CKB`);
|
|
1898
|
+
console.log(` Expires At: ${payload.expiresAt}`);
|
|
1899
|
+
console.log(` Invoice: ${payload.invoice}`);
|
|
1900
|
+
}
|
|
1901
|
+
});
|
|
1902
|
+
invoice.command("get").argument("<paymentHash>").option("--json").action(async (paymentHash, options) => {
|
|
1903
|
+
const rpc = await createReadyRpcClient(config);
|
|
1904
|
+
const result = await rpc.getInvoice({ payment_hash: paymentHash });
|
|
1905
|
+
const metadata = extractInvoiceMetadata(result.invoice);
|
|
1906
|
+
const createdAtMs = parseHexTimestampMs(result.invoice.data.timestamp);
|
|
1907
|
+
const output = {
|
|
1908
|
+
paymentHash,
|
|
1909
|
+
status: result.status,
|
|
1910
|
+
invoice: result.invoice_address,
|
|
1911
|
+
amountCkb: result.invoice.amount ? shannonsToCkb3(result.invoice.amount) : void 0,
|
|
1912
|
+
currency: result.invoice.currency,
|
|
1913
|
+
description: metadata.description,
|
|
1914
|
+
createdAt: createdAtMs ? new Date(createdAtMs).toISOString() : result.invoice.data.timestamp,
|
|
1915
|
+
expiresAt: metadata.expiresAt,
|
|
1916
|
+
age: metadata.age
|
|
1917
|
+
};
|
|
1918
|
+
if (options.json) {
|
|
1919
|
+
printJsonSuccess(output);
|
|
1920
|
+
} else {
|
|
1921
|
+
printInvoiceDetailHuman(output);
|
|
1922
|
+
}
|
|
1923
|
+
});
|
|
1924
|
+
invoice.command("parse").argument("<invoiceString>").option("--json").action(async (invoiceString, options) => {
|
|
1925
|
+
const rpc = await createReadyRpcClient(config);
|
|
1926
|
+
const result = await rpc.parseInvoice({ invoice: invoiceString });
|
|
1927
|
+
if (options.json) {
|
|
1928
|
+
printJsonSuccess(result);
|
|
1929
|
+
} else {
|
|
1930
|
+
console.log("Invoice parsed");
|
|
1931
|
+
console.log(JSON.stringify(result, null, 2));
|
|
1932
|
+
}
|
|
1933
|
+
});
|
|
1934
|
+
invoice.command("cancel").argument("<paymentHash>").option("--json").action(async (paymentHash, options) => {
|
|
1935
|
+
const rpc = await createReadyRpcClient(config);
|
|
1936
|
+
const json = Boolean(options.json);
|
|
1937
|
+
const endpoint = resolveRpcEndpoint(config);
|
|
1938
|
+
if (endpoint.target === "runtime-proxy") {
|
|
1939
|
+
const created = await tryCreateRuntimeInvoiceJob(endpoint.url, {
|
|
1940
|
+
params: {
|
|
1941
|
+
action: "cancel",
|
|
1942
|
+
cancelInvoiceParams: { payment_hash: paymentHash }
|
|
1943
|
+
},
|
|
1944
|
+
options: {
|
|
1945
|
+
idempotencyKey: `invoice:cancel:${paymentHash}`
|
|
1946
|
+
}
|
|
1947
|
+
});
|
|
1948
|
+
if (created) {
|
|
1949
|
+
const job = await waitForRuntimeJobTerminal(endpoint.url, created.id, 60);
|
|
1950
|
+
if (job.state !== "succeeded") {
|
|
1951
|
+
throw new Error(job.error?.message ?? `Invoice cancel job ${job.state}`);
|
|
1952
|
+
}
|
|
1953
|
+
const result2 = job.result ?? {};
|
|
1954
|
+
const output2 = {
|
|
1955
|
+
jobId: job.id,
|
|
1956
|
+
paymentHash,
|
|
1957
|
+
status: result2.status ?? "Cancelled",
|
|
1958
|
+
invoice: result2.invoiceAddress
|
|
1959
|
+
};
|
|
1960
|
+
if (json) {
|
|
1961
|
+
printJsonSuccess(output2);
|
|
1962
|
+
} else {
|
|
1963
|
+
console.log("Invoice cancelled");
|
|
1964
|
+
console.log(` Job: ${output2.jobId}`);
|
|
1965
|
+
console.log(` Payment Hash: ${output2.paymentHash}`);
|
|
1966
|
+
console.log(` Status: ${output2.status}`);
|
|
1967
|
+
console.log(` Invoice: ${output2.invoice ?? "n/a"}`);
|
|
1968
|
+
}
|
|
1969
|
+
return;
|
|
1970
|
+
}
|
|
1971
|
+
}
|
|
1972
|
+
const result = await rpc.cancelInvoice({ payment_hash: paymentHash });
|
|
1973
|
+
const output = { paymentHash, status: result.status, invoice: result.invoice_address };
|
|
1974
|
+
if (json) {
|
|
1975
|
+
printJsonSuccess(output);
|
|
1976
|
+
} else {
|
|
1977
|
+
console.log("Invoice cancelled");
|
|
1978
|
+
console.log(` Payment Hash: ${output.paymentHash}`);
|
|
1979
|
+
console.log(` Status: ${output.status}`);
|
|
1980
|
+
console.log(` Invoice: ${output.invoice}`);
|
|
1981
|
+
}
|
|
1982
|
+
});
|
|
1983
|
+
invoice.command("settle").argument("<paymentHash>").requiredOption("--preimage <preimage>").option("--json").action(async (paymentHash, options) => {
|
|
1984
|
+
const rpc = await createReadyRpcClient(config);
|
|
1985
|
+
const json = Boolean(options.json);
|
|
1986
|
+
const endpoint = resolveRpcEndpoint(config);
|
|
1987
|
+
if (endpoint.target === "runtime-proxy") {
|
|
1988
|
+
const created = await tryCreateRuntimeInvoiceJob(endpoint.url, {
|
|
1989
|
+
params: {
|
|
1990
|
+
action: "settle",
|
|
1991
|
+
settleInvoiceParams: {
|
|
1992
|
+
payment_hash: paymentHash,
|
|
1993
|
+
payment_preimage: options.preimage
|
|
1994
|
+
}
|
|
1995
|
+
},
|
|
1996
|
+
options: {
|
|
1997
|
+
idempotencyKey: `invoice:settle:${paymentHash}`
|
|
1998
|
+
}
|
|
1999
|
+
});
|
|
2000
|
+
if (created) {
|
|
2001
|
+
const job = await waitForRuntimeJobTerminal(endpoint.url, created.id, 60);
|
|
2002
|
+
if (job.state !== "succeeded") {
|
|
2003
|
+
throw new Error(job.error?.message ?? `Invoice settle job ${job.state}`);
|
|
2004
|
+
}
|
|
2005
|
+
const output = { jobId: job.id, paymentHash, message: "Invoice settled." };
|
|
2006
|
+
if (json) {
|
|
2007
|
+
printJsonSuccess(output);
|
|
2008
|
+
} else {
|
|
2009
|
+
console.log(output.message);
|
|
2010
|
+
console.log(` Job: ${output.jobId}`);
|
|
2011
|
+
console.log(` Payment Hash: ${output.paymentHash}`);
|
|
2012
|
+
}
|
|
2013
|
+
return;
|
|
2014
|
+
}
|
|
2015
|
+
}
|
|
2016
|
+
await rpc.settleInvoice({
|
|
2017
|
+
payment_hash: paymentHash,
|
|
2018
|
+
payment_preimage: options.preimage
|
|
2019
|
+
});
|
|
2020
|
+
if (json) {
|
|
2021
|
+
printJsonSuccess({ paymentHash, message: "Invoice settled." });
|
|
2022
|
+
} else {
|
|
2023
|
+
console.log("Invoice settled");
|
|
2024
|
+
console.log(` Payment Hash: ${paymentHash}`);
|
|
2025
|
+
}
|
|
2026
|
+
});
|
|
2027
|
+
return invoice;
|
|
2028
|
+
}
|
|
2029
|
+
|
|
2030
|
+
// src/commands/job.ts
|
|
2031
|
+
import { existsSync as existsSync6 } from "fs";
|
|
2032
|
+
import { Command as Command6 } from "commander";
|
|
2033
|
+
|
|
2034
|
+
// src/lib/log-files.ts
|
|
2035
|
+
import { existsSync as existsSync5, readFileSync as readFileSync5 } from "fs";
|
|
2036
|
+
import { join as join4 } from "path";
|
|
2037
|
+
function resolvePersistedLogPaths(dataDir, meta) {
|
|
2038
|
+
return {
|
|
2039
|
+
runtimeAlerts: meta?.alertLogFilePath ?? join4(dataDir, "logs", "runtime.alerts.jsonl"),
|
|
2040
|
+
fnnStdout: meta?.fnnStdoutLogPath ?? join4(dataDir, "logs", "fnn.stdout.log"),
|
|
2041
|
+
fnnStderr: meta?.fnnStderrLogPath ?? join4(dataDir, "logs", "fnn.stderr.log")
|
|
2042
|
+
};
|
|
2043
|
+
}
|
|
2044
|
+
function resolvePersistedLogTargets(paths, source) {
|
|
2045
|
+
const all = [
|
|
2046
|
+
{
|
|
2047
|
+
source: "runtime",
|
|
2048
|
+
title: "runtime.alerts",
|
|
2049
|
+
path: paths.runtimeAlerts
|
|
2050
|
+
},
|
|
2051
|
+
{
|
|
2052
|
+
source: "fnn-stdout",
|
|
2053
|
+
title: "fnn.stdout",
|
|
2054
|
+
path: paths.fnnStdout
|
|
2055
|
+
},
|
|
2056
|
+
{
|
|
2057
|
+
source: "fnn-stderr",
|
|
2058
|
+
title: "fnn.stderr",
|
|
2059
|
+
path: paths.fnnStderr
|
|
2060
|
+
}
|
|
2061
|
+
];
|
|
2062
|
+
if (source === "all") {
|
|
2063
|
+
return all;
|
|
2064
|
+
}
|
|
2065
|
+
return all.filter((target) => target.source === source);
|
|
2066
|
+
}
|
|
2067
|
+
function readLastLines(filePath, maxLines) {
|
|
2068
|
+
if (!existsSync5(filePath)) {
|
|
2069
|
+
return [];
|
|
2070
|
+
}
|
|
2071
|
+
const content = readFileSync5(filePath, "utf-8");
|
|
2072
|
+
const lines = content.split(/\r?\n/).filter((line) => line.length > 0);
|
|
2073
|
+
return lines.slice(-maxLines);
|
|
2074
|
+
}
|
|
2075
|
+
|
|
2076
|
+
// src/commands/job.ts
|
|
2077
|
+
function createJobCommand(config) {
|
|
2078
|
+
const job = new Command6("job").description("Runtime job management commands");
|
|
2079
|
+
job.command("list").option("--state <state>", "Filter by job state").option("--type <type>", "Filter by job type (payment|invoice|channel)").option("--limit <n>", "Limit number of jobs").option("--offset <n>", "Offset for pagination").option("--json").action(async (options) => {
|
|
2080
|
+
const json = Boolean(options.json);
|
|
2081
|
+
const runtimeUrl = getRuntimeUrlOrExit(config, json);
|
|
2082
|
+
const query = new URLSearchParams();
|
|
2083
|
+
if (options.state) query.set("state", String(options.state));
|
|
2084
|
+
if (options.type) query.set("type", String(options.type));
|
|
2085
|
+
if (options.limit) query.set("limit", String(options.limit));
|
|
2086
|
+
if (options.offset) query.set("offset", String(options.offset));
|
|
2087
|
+
const response = await fetch(
|
|
2088
|
+
`${runtimeUrl}/jobs${query.toString() ? `?${query.toString()}` : ""}`
|
|
2089
|
+
);
|
|
2090
|
+
if (!response.ok) {
|
|
2091
|
+
return handleHttpError(response, "JOB_LIST_FAILED", json);
|
|
2092
|
+
}
|
|
2093
|
+
const payload = await response.json();
|
|
2094
|
+
if (json) {
|
|
2095
|
+
printJsonSuccess(payload);
|
|
2096
|
+
return;
|
|
2097
|
+
}
|
|
2098
|
+
if (!payload.jobs.length) {
|
|
2099
|
+
console.log("No jobs found.");
|
|
2100
|
+
return;
|
|
2101
|
+
}
|
|
2102
|
+
console.log(`Jobs (${payload.jobs.length})`);
|
|
2103
|
+
for (const item of payload.jobs) {
|
|
2104
|
+
console.log(`- ${item.id}`);
|
|
2105
|
+
console.log(` Type: ${item.type}`);
|
|
2106
|
+
console.log(` State: ${item.state}`);
|
|
2107
|
+
if (item.idempotencyKey) console.log(` Key: ${item.idempotencyKey}`);
|
|
2108
|
+
if (typeof item.retryCount === "number" && typeof item.maxRetries === "number") {
|
|
2109
|
+
console.log(` Retry: ${item.retryCount}/${item.maxRetries}`);
|
|
2110
|
+
}
|
|
2111
|
+
}
|
|
2112
|
+
});
|
|
2113
|
+
job.command("get").argument("<jobId>").option("--json").action(async (jobId, options) => {
|
|
2114
|
+
const json = Boolean(options.json);
|
|
2115
|
+
const runtimeUrl = getRuntimeUrlOrExit(config, json);
|
|
2116
|
+
const response = await fetch(`${runtimeUrl}/jobs/${jobId}`);
|
|
2117
|
+
if (!response.ok) {
|
|
2118
|
+
return handleHttpError(response, "JOB_GET_FAILED", json);
|
|
2119
|
+
}
|
|
2120
|
+
const payload = await response.json();
|
|
2121
|
+
if (json) {
|
|
2122
|
+
printJsonSuccess(payload);
|
|
2123
|
+
return;
|
|
2124
|
+
}
|
|
2125
|
+
console.log("Job");
|
|
2126
|
+
console.log(` ID: ${payload.id}`);
|
|
2127
|
+
console.log(` Type: ${payload.type}`);
|
|
2128
|
+
console.log(` State: ${payload.state}`);
|
|
2129
|
+
if (payload.idempotencyKey) console.log(` Key: ${payload.idempotencyKey}`);
|
|
2130
|
+
if (typeof payload.retryCount === "number" && typeof payload.maxRetries === "number") {
|
|
2131
|
+
console.log(` Retry: ${payload.retryCount}/${payload.maxRetries}`);
|
|
2132
|
+
}
|
|
2133
|
+
if (payload.error?.message) {
|
|
2134
|
+
console.log(` Error: ${payload.error.message}`);
|
|
2135
|
+
}
|
|
2136
|
+
if (payload.result) {
|
|
2137
|
+
console.log(" Result:");
|
|
2138
|
+
console.log(` ${JSON.stringify(payload.result)}`);
|
|
2139
|
+
}
|
|
2140
|
+
});
|
|
2141
|
+
job.command("trace").argument("<jobId>").option("--tail <n>", "Max lines to inspect per log file", "400").option("--json").action(async (jobId, options) => {
|
|
2142
|
+
const json = Boolean(options.json);
|
|
2143
|
+
const tailInput = Number.parseInt(String(options.tail ?? "400"), 10);
|
|
2144
|
+
const tail = Number.isFinite(tailInput) && tailInput > 0 ? tailInput : 400;
|
|
2145
|
+
const runtimeUrl = getRuntimeUrlOrExit(config, json);
|
|
2146
|
+
const jobResponse = await fetch(`${runtimeUrl}/jobs/${jobId}`);
|
|
2147
|
+
if (!jobResponse.ok) {
|
|
2148
|
+
return handleHttpError(jobResponse, "JOB_TRACE_GET_FAILED", json);
|
|
2149
|
+
}
|
|
2150
|
+
const eventsResponse = await fetch(`${runtimeUrl}/jobs/${jobId}/events`);
|
|
2151
|
+
if (!eventsResponse.ok) {
|
|
2152
|
+
return handleHttpError(eventsResponse, "JOB_TRACE_EVENTS_FAILED", json);
|
|
2153
|
+
}
|
|
2154
|
+
const jobRecord = await jobResponse.json();
|
|
2155
|
+
const eventsPayload = await eventsResponse.json();
|
|
2156
|
+
const tokens = collectTraceTokens(jobRecord, eventsPayload.events);
|
|
2157
|
+
const meta = readRuntimeMeta(config.dataDir);
|
|
2158
|
+
const logPaths = resolvePersistedLogPaths(config.dataDir, meta);
|
|
2159
|
+
const runtimeAlertMatches = collectRelatedLines(logPaths.runtimeAlerts, tokens, tail);
|
|
2160
|
+
const fnnStdoutMatches = collectRelatedLines(logPaths.fnnStdout, tokens, tail);
|
|
2161
|
+
const fnnStderrMatches = collectRelatedLines(logPaths.fnnStderr, tokens, tail);
|
|
2162
|
+
const result = {
|
|
2163
|
+
job: jobRecord,
|
|
2164
|
+
events: eventsPayload.events,
|
|
2165
|
+
trace: {
|
|
2166
|
+
tokens,
|
|
2167
|
+
logPaths,
|
|
2168
|
+
matches: {
|
|
2169
|
+
runtimeAlerts: runtimeAlertMatches,
|
|
2170
|
+
fnnStdout: fnnStdoutMatches,
|
|
2171
|
+
fnnStderr: fnnStderrMatches
|
|
2172
|
+
}
|
|
2173
|
+
}
|
|
2174
|
+
};
|
|
2175
|
+
if (json) {
|
|
2176
|
+
printJsonSuccess(result);
|
|
2177
|
+
return;
|
|
2178
|
+
}
|
|
2179
|
+
console.log("Job trace");
|
|
2180
|
+
console.log(` Job ID: ${jobRecord.id}`);
|
|
2181
|
+
console.log(` Type: ${jobRecord.type}`);
|
|
2182
|
+
console.log(` State: ${jobRecord.state}`);
|
|
2183
|
+
if (jobRecord.idempotencyKey) {
|
|
2184
|
+
console.log(` Idempotency: ${jobRecord.idempotencyKey}`);
|
|
2185
|
+
}
|
|
2186
|
+
if (jobRecord.error?.message) {
|
|
2187
|
+
console.log(` Error: ${jobRecord.error.message}`);
|
|
2188
|
+
}
|
|
2189
|
+
console.log(` Events: ${eventsPayload.events.length}`);
|
|
2190
|
+
if (tokens.length > 0) {
|
|
2191
|
+
console.log(" Tokens:");
|
|
2192
|
+
for (const token of tokens) {
|
|
2193
|
+
console.log(` - ${token}`);
|
|
2194
|
+
}
|
|
2195
|
+
}
|
|
2196
|
+
printTraceSection("runtime.alerts", logPaths.runtimeAlerts, runtimeAlertMatches);
|
|
2197
|
+
printTraceSection("fnn.stdout", logPaths.fnnStdout, fnnStdoutMatches);
|
|
2198
|
+
printTraceSection("fnn.stderr", logPaths.fnnStderr, fnnStderrMatches);
|
|
2199
|
+
});
|
|
2200
|
+
job.command("events").argument("<jobId>").option("--with-data", "Include event data payload in human-readable output").option("--json").action(async (jobId, options) => {
|
|
2201
|
+
const json = Boolean(options.json);
|
|
2202
|
+
const runtimeUrl = getRuntimeUrlOrExit(config, json);
|
|
2203
|
+
const response = await fetch(`${runtimeUrl}/jobs/${jobId}/events`);
|
|
2204
|
+
if (!response.ok) {
|
|
2205
|
+
return handleHttpError(response, "JOB_EVENTS_FAILED", json);
|
|
2206
|
+
}
|
|
2207
|
+
const payload = await response.json();
|
|
2208
|
+
if (json) {
|
|
2209
|
+
printJsonSuccess(payload);
|
|
2210
|
+
return;
|
|
2211
|
+
}
|
|
2212
|
+
if (!payload.events.length) {
|
|
2213
|
+
console.log("No events found for job.");
|
|
2214
|
+
return;
|
|
2215
|
+
}
|
|
2216
|
+
console.log(`Job events (${payload.events.length})`);
|
|
2217
|
+
for (const event of payload.events) {
|
|
2218
|
+
const timestamp = new Date(event.createdAt).toISOString();
|
|
2219
|
+
const transition = event.toState ? `${event.fromState ?? "(none)"} -> ${event.toState}` : event.fromState ?? "(none)";
|
|
2220
|
+
console.log(`- ${timestamp} ${event.eventType} (${transition})`);
|
|
2221
|
+
if (options.withData && event.data !== void 0) {
|
|
2222
|
+
console.log(` data: ${JSON.stringify(event.data)}`);
|
|
2223
|
+
}
|
|
2224
|
+
}
|
|
2225
|
+
});
|
|
2226
|
+
job.command("cancel").argument("<jobId>").option("--json").action(async (jobId, options) => {
|
|
2227
|
+
const json = Boolean(options.json);
|
|
2228
|
+
const runtimeUrl = getRuntimeUrlOrExit(config, json);
|
|
2229
|
+
const response = await fetch(`${runtimeUrl}/jobs/${jobId}`, { method: "DELETE" });
|
|
2230
|
+
if (!response.ok) {
|
|
2231
|
+
return handleHttpError(response, "JOB_CANCEL_FAILED", json);
|
|
2232
|
+
}
|
|
2233
|
+
const payload = { jobId, cancelled: true };
|
|
2234
|
+
if (json) {
|
|
2235
|
+
printJsonSuccess(payload);
|
|
2236
|
+
} else {
|
|
2237
|
+
console.log(`Job cancelled: ${jobId}`);
|
|
2238
|
+
}
|
|
2239
|
+
});
|
|
2240
|
+
return job;
|
|
2241
|
+
}
|
|
2242
|
+
function getRuntimeUrlOrExit(config, json) {
|
|
2243
|
+
const endpoint = resolveRpcEndpoint(config);
|
|
2244
|
+
if (endpoint.target !== "runtime-proxy") {
|
|
2245
|
+
const message = "Runtime proxy is not active for the current profile/RPC URL. Start runtime first (fiber-pay runtime start --daemon).";
|
|
2246
|
+
if (json) {
|
|
2247
|
+
printJsonError({
|
|
2248
|
+
code: "RUNTIME_PROXY_REQUIRED",
|
|
2249
|
+
message,
|
|
2250
|
+
recoverable: true,
|
|
2251
|
+
suggestion: "Start runtime and retry the job command."
|
|
2252
|
+
});
|
|
2253
|
+
} else {
|
|
2254
|
+
console.error(`Error: ${message}`);
|
|
2255
|
+
}
|
|
2256
|
+
process.exit(1);
|
|
2257
|
+
}
|
|
2258
|
+
return endpoint.url;
|
|
2259
|
+
}
|
|
2260
|
+
async function handleHttpError(response, code, json) {
|
|
2261
|
+
const body = await safeJson(response);
|
|
2262
|
+
const message = extractErrorMessage(body) ?? `HTTP ${response.status}`;
|
|
2263
|
+
if (json) {
|
|
2264
|
+
printJsonError({
|
|
2265
|
+
code,
|
|
2266
|
+
message,
|
|
2267
|
+
recoverable: response.status >= 500 || response.status === 404,
|
|
2268
|
+
suggestion: "Check runtime status and job id, then retry.",
|
|
2269
|
+
details: {
|
|
2270
|
+
status: response.status,
|
|
2271
|
+
body
|
|
2272
|
+
}
|
|
2273
|
+
});
|
|
2274
|
+
} else {
|
|
2275
|
+
console.error(`Error: ${message}`);
|
|
2276
|
+
}
|
|
2277
|
+
process.exit(1);
|
|
2278
|
+
}
|
|
2279
|
+
function extractErrorMessage(body) {
|
|
2280
|
+
if (!body || typeof body !== "object") {
|
|
2281
|
+
return void 0;
|
|
2282
|
+
}
|
|
2283
|
+
if ("error" in body) {
|
|
2284
|
+
const error = body.error;
|
|
2285
|
+
if (typeof error === "string") {
|
|
2286
|
+
return error;
|
|
2287
|
+
}
|
|
2288
|
+
if (error && typeof error === "object" && "message" in error) {
|
|
2289
|
+
const message = error.message;
|
|
2290
|
+
if (typeof message === "string") {
|
|
2291
|
+
return message;
|
|
2292
|
+
}
|
|
2293
|
+
}
|
|
2294
|
+
}
|
|
2295
|
+
if ("message" in body) {
|
|
2296
|
+
const message = body.message;
|
|
2297
|
+
if (typeof message === "string") {
|
|
2298
|
+
return message;
|
|
2299
|
+
}
|
|
2300
|
+
}
|
|
2301
|
+
return void 0;
|
|
2302
|
+
}
|
|
2303
|
+
async function safeJson(response) {
|
|
2304
|
+
try {
|
|
2305
|
+
return await response.json();
|
|
2306
|
+
} catch {
|
|
2307
|
+
return void 0;
|
|
2308
|
+
}
|
|
2309
|
+
}
|
|
2310
|
+
function collectTraceTokens(job, events) {
|
|
2311
|
+
const result = /* @__PURE__ */ new Set();
|
|
2312
|
+
addTraceToken(result, job.id);
|
|
2313
|
+
addTraceToken(result, job.idempotencyKey);
|
|
2314
|
+
collectStructuredTokens(result, job.params);
|
|
2315
|
+
collectStructuredTokens(result, job.result);
|
|
2316
|
+
collectStructuredTokens(result, job.error);
|
|
2317
|
+
for (const event of events) {
|
|
2318
|
+
addTraceToken(result, event.id);
|
|
2319
|
+
collectStructuredTokens(result, event.data);
|
|
2320
|
+
}
|
|
2321
|
+
return Array.from(result).slice(0, 20);
|
|
2322
|
+
}
|
|
2323
|
+
function addTraceToken(set, value) {
|
|
2324
|
+
if (typeof value !== "string") {
|
|
2325
|
+
return;
|
|
2326
|
+
}
|
|
2327
|
+
const normalized = value.trim();
|
|
2328
|
+
if (!normalized || normalized.length < 6) {
|
|
2329
|
+
return;
|
|
2330
|
+
}
|
|
2331
|
+
if (normalized.includes(" ")) {
|
|
2332
|
+
return;
|
|
2333
|
+
}
|
|
2334
|
+
if (normalized.length <= 128) {
|
|
2335
|
+
set.add(normalized);
|
|
2336
|
+
}
|
|
2337
|
+
}
|
|
2338
|
+
function collectStructuredTokens(set, input, depth = 0) {
|
|
2339
|
+
if (depth > 3 || input === null || input === void 0) {
|
|
2340
|
+
return;
|
|
2341
|
+
}
|
|
2342
|
+
if (typeof input === "string") {
|
|
2343
|
+
if (input.startsWith("0x") || input.includes("peer") || input.includes("channel")) {
|
|
2344
|
+
addTraceToken(set, input);
|
|
2345
|
+
}
|
|
2346
|
+
return;
|
|
2347
|
+
}
|
|
2348
|
+
if (Array.isArray(input)) {
|
|
2349
|
+
for (const item of input) {
|
|
2350
|
+
collectStructuredTokens(set, item, depth + 1);
|
|
2351
|
+
}
|
|
2352
|
+
return;
|
|
2353
|
+
}
|
|
2354
|
+
if (typeof input === "object") {
|
|
2355
|
+
for (const value of Object.values(input)) {
|
|
2356
|
+
collectStructuredTokens(set, value, depth + 1);
|
|
2357
|
+
}
|
|
2358
|
+
}
|
|
2359
|
+
}
|
|
2360
|
+
function collectRelatedLines(filePath, tokens, tail) {
|
|
2361
|
+
if (!existsSync6(filePath)) {
|
|
2362
|
+
return [];
|
|
2363
|
+
}
|
|
2364
|
+
const lines = readLastLines(filePath, tail);
|
|
2365
|
+
if (tokens.length === 0) {
|
|
2366
|
+
return lines.slice(-Math.min(30, lines.length));
|
|
2367
|
+
}
|
|
2368
|
+
const related = lines.filter((line) => tokens.some((token) => line.includes(token)));
|
|
2369
|
+
if (related.length > 0) {
|
|
2370
|
+
return related.slice(-Math.min(80, related.length));
|
|
2371
|
+
}
|
|
2372
|
+
return lines.slice(-Math.min(20, lines.length));
|
|
2373
|
+
}
|
|
2374
|
+
function printTraceSection(title, filePath, lines) {
|
|
2375
|
+
console.log(`
|
|
2376
|
+
${title}: ${filePath}`);
|
|
2377
|
+
if (!existsSync6(filePath)) {
|
|
2378
|
+
console.log(" (file not found)");
|
|
2379
|
+
return;
|
|
2380
|
+
}
|
|
2381
|
+
if (lines.length === 0) {
|
|
2382
|
+
console.log(" (no related lines)");
|
|
2383
|
+
return;
|
|
2384
|
+
}
|
|
2385
|
+
for (const line of lines.slice(-20)) {
|
|
2386
|
+
console.log(` ${line}`);
|
|
2387
|
+
}
|
|
2388
|
+
}
|
|
2389
|
+
|
|
2390
|
+
// src/commands/logs.ts
|
|
2391
|
+
import { existsSync as existsSync7 } from "fs";
|
|
2392
|
+
import { formatRuntimeAlert } from "@fiber-pay/runtime";
|
|
2393
|
+
import { Command as Command7 } from "commander";
|
|
2394
|
+
var ALLOWED_SOURCES = /* @__PURE__ */ new Set([
|
|
2395
|
+
"all",
|
|
2396
|
+
"runtime",
|
|
2397
|
+
"fnn-stdout",
|
|
2398
|
+
"fnn-stderr"
|
|
2399
|
+
]);
|
|
2400
|
+
function parseRuntimeAlertLine(line) {
|
|
2401
|
+
try {
|
|
2402
|
+
const parsed = JSON.parse(line);
|
|
2403
|
+
if (!parsed || typeof parsed !== "object") {
|
|
2404
|
+
return null;
|
|
2405
|
+
}
|
|
2406
|
+
return parsed;
|
|
2407
|
+
} catch {
|
|
2408
|
+
return null;
|
|
2409
|
+
}
|
|
2410
|
+
}
|
|
2411
|
+
function formatRuntimeAlertHuman(line) {
|
|
2412
|
+
const parsed = parseRuntimeAlertLine(line);
|
|
2413
|
+
if (!parsed) {
|
|
2414
|
+
return line;
|
|
2415
|
+
}
|
|
2416
|
+
return formatRuntimeAlert(parsed);
|
|
2417
|
+
}
|
|
2418
|
+
function coerceJsonLineForOutput(source, line) {
|
|
2419
|
+
if (source !== "runtime") {
|
|
2420
|
+
return line;
|
|
2421
|
+
}
|
|
2422
|
+
return parseRuntimeAlertLine(line) ?? line;
|
|
2423
|
+
}
|
|
2424
|
+
function createLogsCommand(config) {
|
|
2425
|
+
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) => {
|
|
2426
|
+
const json = Boolean(options.json);
|
|
2427
|
+
const follow = Boolean(options.follow);
|
|
2428
|
+
const sourceInput = String(options.source ?? "all").trim().toLowerCase();
|
|
2429
|
+
if (json && follow) {
|
|
2430
|
+
const message = "--follow is not supported with --json. Use human mode for streaming logs.";
|
|
2431
|
+
printJsonError({
|
|
2432
|
+
code: "LOG_FOLLOW_JSON_UNSUPPORTED",
|
|
2433
|
+
message,
|
|
2434
|
+
recoverable: true,
|
|
2435
|
+
suggestion: "Remove --json or remove --follow and retry."
|
|
2436
|
+
});
|
|
2437
|
+
process.exit(1);
|
|
2438
|
+
}
|
|
2439
|
+
if (!ALLOWED_SOURCES.has(sourceInput)) {
|
|
2440
|
+
const message = "Invalid --source value. Expected one of: all, runtime, fnn-stdout, fnn-stderr.";
|
|
2441
|
+
if (json) {
|
|
2442
|
+
printJsonError({
|
|
2443
|
+
code: "LOG_SOURCE_INVALID",
|
|
2444
|
+
message,
|
|
2445
|
+
recoverable: true,
|
|
2446
|
+
suggestion: "Retry with --source all|runtime|fnn-stdout|fnn-stderr.",
|
|
2447
|
+
details: { source: sourceInput }
|
|
2448
|
+
});
|
|
2449
|
+
} else {
|
|
2450
|
+
console.error(`Error: ${message}`);
|
|
2451
|
+
}
|
|
2452
|
+
process.exit(1);
|
|
2453
|
+
}
|
|
2454
|
+
const source = sourceInput;
|
|
2455
|
+
const tailInput = Number.parseInt(String(options.tail ?? "80"), 10);
|
|
2456
|
+
const tail = Number.isFinite(tailInput) && tailInput > 0 ? tailInput : 80;
|
|
2457
|
+
const intervalInput = Number.parseInt(String(options.intervalMs ?? "1000"), 10);
|
|
2458
|
+
const intervalMs = Number.isFinite(intervalInput) && intervalInput > 0 ? intervalInput : 1e3;
|
|
2459
|
+
const meta = readRuntimeMeta(config.dataDir);
|
|
2460
|
+
const paths = resolvePersistedLogPaths(config.dataDir, meta);
|
|
2461
|
+
const targets = resolvePersistedLogTargets(paths, source);
|
|
2462
|
+
if (source !== "all" && targets.length === 1 && !existsSync7(targets[0].path)) {
|
|
2463
|
+
const message = `Log file not found for source ${source}: ${targets[0].path}`;
|
|
2464
|
+
if (json) {
|
|
2465
|
+
printJsonError({
|
|
2466
|
+
code: "LOG_FILE_NOT_FOUND",
|
|
2467
|
+
message,
|
|
2468
|
+
recoverable: true,
|
|
2469
|
+
suggestion: "Start node/runtime or generate activity, then retry logs command.",
|
|
2470
|
+
details: { source, path: targets[0].path }
|
|
2471
|
+
});
|
|
2472
|
+
} else {
|
|
2473
|
+
console.error(`Error: ${message}`);
|
|
2474
|
+
}
|
|
2475
|
+
process.exit(1);
|
|
2476
|
+
}
|
|
2477
|
+
const entries = [];
|
|
2478
|
+
for (const target of targets) {
|
|
2479
|
+
const exists = existsSync7(target.path);
|
|
2480
|
+
let lines = [];
|
|
2481
|
+
if (exists) {
|
|
2482
|
+
try {
|
|
2483
|
+
lines = readLastLines(target.path, tail);
|
|
2484
|
+
} catch (error) {
|
|
2485
|
+
const message = error instanceof Error ? error.message : `Failed to read log file: ${target.path}`;
|
|
2486
|
+
if (json) {
|
|
2487
|
+
printJsonError({
|
|
2488
|
+
code: "LOG_READ_FAILED",
|
|
2489
|
+
message,
|
|
2490
|
+
recoverable: true,
|
|
2491
|
+
suggestion: "Check log file permissions and retry.",
|
|
2492
|
+
details: { source: target.source, path: target.path }
|
|
2493
|
+
});
|
|
2494
|
+
} else {
|
|
2495
|
+
console.error(`Error: ${message}`);
|
|
2496
|
+
}
|
|
2497
|
+
process.exit(1);
|
|
2498
|
+
}
|
|
2499
|
+
}
|
|
2500
|
+
entries.push({
|
|
2501
|
+
source: target.source,
|
|
2502
|
+
title: target.title,
|
|
2503
|
+
path: target.path,
|
|
2504
|
+
exists,
|
|
2505
|
+
lineCount: lines.length,
|
|
2506
|
+
lines,
|
|
2507
|
+
jsonLines: lines.map((line) => coerceJsonLineForOutput(target.source, line))
|
|
2508
|
+
});
|
|
2509
|
+
}
|
|
2510
|
+
if (json) {
|
|
2511
|
+
printJsonSuccess({
|
|
2512
|
+
source,
|
|
2513
|
+
tail,
|
|
2514
|
+
entries: entries.map((entry) => ({
|
|
2515
|
+
source: entry.source,
|
|
2516
|
+
title: entry.title,
|
|
2517
|
+
path: entry.path,
|
|
2518
|
+
exists: entry.exists,
|
|
2519
|
+
lineCount: entry.lineCount,
|
|
2520
|
+
lines: entry.jsonLines
|
|
2521
|
+
}))
|
|
2522
|
+
});
|
|
2523
|
+
return;
|
|
2524
|
+
}
|
|
2525
|
+
console.log(`Logs (source: ${source}, tail: ${tail})`);
|
|
2526
|
+
for (const entry of entries) {
|
|
2527
|
+
console.log(`
|
|
2528
|
+
${entry.title}: ${entry.path}`);
|
|
2529
|
+
if (!entry.exists) {
|
|
2530
|
+
console.log(" (file not found)");
|
|
2531
|
+
continue;
|
|
2532
|
+
}
|
|
2533
|
+
if (entry.lines.length === 0) {
|
|
2534
|
+
console.log(" (no lines)");
|
|
2535
|
+
continue;
|
|
2536
|
+
}
|
|
2537
|
+
for (const line of entry.lines) {
|
|
2538
|
+
const output = entry.source === "runtime" ? formatRuntimeAlertHuman(line) : line;
|
|
2539
|
+
console.log(` ${output}`);
|
|
2540
|
+
}
|
|
2541
|
+
}
|
|
2542
|
+
if (!follow) {
|
|
2543
|
+
return;
|
|
2544
|
+
}
|
|
2545
|
+
console.log(`
|
|
2546
|
+
Following logs (interval: ${intervalMs}ms). Press Ctrl+C to stop.`);
|
|
2547
|
+
const states = new Map(
|
|
2548
|
+
entries.map((entry) => [
|
|
2549
|
+
entry.source,
|
|
2550
|
+
{
|
|
2551
|
+
title: entry.title,
|
|
2552
|
+
path: entry.path,
|
|
2553
|
+
seenLines: entry.exists ? entry.lines.length : 0
|
|
2554
|
+
}
|
|
2555
|
+
])
|
|
2556
|
+
);
|
|
2557
|
+
await new Promise((resolve2) => {
|
|
2558
|
+
let stopped = false;
|
|
2559
|
+
const stop = () => {
|
|
2560
|
+
if (stopped) return;
|
|
2561
|
+
stopped = true;
|
|
2562
|
+
clearInterval(timer);
|
|
2563
|
+
process.off("SIGINT", stop);
|
|
2564
|
+
process.off("SIGTERM", stop);
|
|
2565
|
+
console.log("\nStopped following logs.");
|
|
2566
|
+
resolve2();
|
|
2567
|
+
};
|
|
2568
|
+
const timer = setInterval(() => {
|
|
2569
|
+
for (const target of targets) {
|
|
2570
|
+
const state = states.get(target.source);
|
|
2571
|
+
if (!state) continue;
|
|
2572
|
+
if (!existsSync7(state.path)) {
|
|
2573
|
+
continue;
|
|
2574
|
+
}
|
|
2575
|
+
const allLines = readLastLines(state.path, Number.MAX_SAFE_INTEGER);
|
|
2576
|
+
const total = allLines.length;
|
|
2577
|
+
const fromIndex = total < state.seenLines ? 0 : state.seenLines;
|
|
2578
|
+
const newLines = allLines.slice(fromIndex);
|
|
2579
|
+
if (newLines.length === 0) {
|
|
2580
|
+
continue;
|
|
2581
|
+
}
|
|
2582
|
+
for (const line of newLines) {
|
|
2583
|
+
const output = target.source === "runtime" ? formatRuntimeAlertHuman(line) : line;
|
|
2584
|
+
console.log(`[${state.title}] ${output}`);
|
|
2585
|
+
}
|
|
2586
|
+
state.seenLines = total;
|
|
2587
|
+
}
|
|
2588
|
+
}, intervalMs);
|
|
2589
|
+
process.on("SIGINT", stop);
|
|
2590
|
+
process.on("SIGTERM", stop);
|
|
2591
|
+
});
|
|
2592
|
+
});
|
|
2593
|
+
}
|
|
2594
|
+
|
|
2595
|
+
// src/commands/node.ts
|
|
2596
|
+
import { nodeIdToPeerId as nodeIdToPeerId2, scriptToAddress as scriptToAddress2 } from "@fiber-pay/sdk";
|
|
2597
|
+
import { Command as Command8 } from "commander";
|
|
2598
|
+
|
|
2599
|
+
// src/lib/node-runtime-daemon.ts
|
|
2600
|
+
import { spawnSync } from "child_process";
|
|
2601
|
+
import { existsSync as existsSync8 } from "fs";
|
|
2602
|
+
function getCustomBinaryState(binaryPath) {
|
|
2603
|
+
const exists = existsSync8(binaryPath);
|
|
2604
|
+
if (!exists) {
|
|
2605
|
+
return { path: binaryPath, ready: false, version: "unknown" };
|
|
2606
|
+
}
|
|
2607
|
+
try {
|
|
2608
|
+
const result = spawnSync(binaryPath, ["--version"], { encoding: "utf-8" });
|
|
2609
|
+
if (result.status !== 0) {
|
|
2610
|
+
return { path: binaryPath, ready: false, version: "unknown" };
|
|
2611
|
+
}
|
|
2612
|
+
const output = `${result.stdout ?? ""}${result.stderr ?? ""}`.trim();
|
|
2613
|
+
const firstLine = output.split("\n").find((line) => line.trim().length > 0) ?? "unknown";
|
|
2614
|
+
return { path: binaryPath, ready: true, version: firstLine.trim() };
|
|
2615
|
+
} catch {
|
|
2616
|
+
return { path: binaryPath, ready: false, version: "unknown" };
|
|
2617
|
+
}
|
|
2618
|
+
}
|
|
2619
|
+
function getBinaryVersion(binaryPath) {
|
|
2620
|
+
try {
|
|
2621
|
+
const result = spawnSync(binaryPath, ["--version"], { encoding: "utf-8" });
|
|
2622
|
+
if (result.status !== 0) {
|
|
2623
|
+
return "unknown";
|
|
2624
|
+
}
|
|
2625
|
+
const output = `${result.stdout ?? ""}${result.stderr ?? ""}`.trim();
|
|
2626
|
+
if (!output) {
|
|
2627
|
+
return "unknown";
|
|
2628
|
+
}
|
|
2629
|
+
const firstLine = output.split("\n").find((line) => line.trim().length > 0);
|
|
2630
|
+
return firstLine?.trim() ?? "unknown";
|
|
2631
|
+
} catch {
|
|
2632
|
+
return "unknown";
|
|
2633
|
+
}
|
|
2634
|
+
}
|
|
2635
|
+
function getCliEntrypoint() {
|
|
2636
|
+
const entrypoint = process.argv[1];
|
|
2637
|
+
if (!entrypoint) {
|
|
2638
|
+
throw new Error("Unable to resolve CLI entrypoint path");
|
|
2639
|
+
}
|
|
2640
|
+
return entrypoint;
|
|
2641
|
+
}
|
|
2642
|
+
function startRuntimeDaemonFromNode(params) {
|
|
2643
|
+
const cliEntrypoint = getCliEntrypoint();
|
|
2644
|
+
const result = spawnSync(
|
|
2645
|
+
process.execPath,
|
|
2646
|
+
[
|
|
2647
|
+
cliEntrypoint,
|
|
2648
|
+
"--data-dir",
|
|
2649
|
+
params.dataDir,
|
|
2650
|
+
"--rpc-url",
|
|
2651
|
+
params.rpcUrl,
|
|
2652
|
+
"runtime",
|
|
2653
|
+
"start",
|
|
2654
|
+
"--daemon",
|
|
2655
|
+
"--fiber-rpc-url",
|
|
2656
|
+
params.rpcUrl,
|
|
2657
|
+
"--proxy-listen",
|
|
2658
|
+
params.proxyListen,
|
|
2659
|
+
"--state-file",
|
|
2660
|
+
params.stateFilePath,
|
|
2661
|
+
"--alert-log-file",
|
|
2662
|
+
params.alertLogFile,
|
|
2663
|
+
"--json"
|
|
2664
|
+
],
|
|
2665
|
+
{ encoding: "utf-8" }
|
|
2666
|
+
);
|
|
2667
|
+
if (result.status === 0) {
|
|
2668
|
+
return { ok: true };
|
|
2669
|
+
}
|
|
2670
|
+
const stderr = (result.stderr ?? "").trim();
|
|
2671
|
+
const stdout = (result.stdout ?? "").trim();
|
|
2672
|
+
const details = stderr || stdout || `exit code ${result.status ?? "unknown"}`;
|
|
2673
|
+
return { ok: false, message: details };
|
|
2674
|
+
}
|
|
2675
|
+
function stopRuntimeDaemonFromNode(params) {
|
|
2676
|
+
const cliEntrypoint = getCliEntrypoint();
|
|
2677
|
+
spawnSync(
|
|
2678
|
+
process.execPath,
|
|
2679
|
+
[
|
|
2680
|
+
cliEntrypoint,
|
|
2681
|
+
"--data-dir",
|
|
2682
|
+
params.dataDir,
|
|
2683
|
+
"--rpc-url",
|
|
2684
|
+
params.rpcUrl,
|
|
2685
|
+
"runtime",
|
|
2686
|
+
"stop",
|
|
2687
|
+
"--json"
|
|
2688
|
+
],
|
|
2689
|
+
{ encoding: "utf-8" }
|
|
2690
|
+
);
|
|
2691
|
+
}
|
|
2692
|
+
|
|
2693
|
+
// src/lib/node-start.ts
|
|
2694
|
+
import { spawn } from "child_process";
|
|
2695
|
+
import { appendFileSync, mkdirSync as mkdirSync2 } from "fs";
|
|
2696
|
+
import { join as join5 } from "path";
|
|
2697
|
+
import {
|
|
2698
|
+
ensureFiberBinary,
|
|
2699
|
+
getDefaultBinaryPath,
|
|
2700
|
+
ProcessManager
|
|
2701
|
+
} from "@fiber-pay/node";
|
|
2702
|
+
import { startRuntimeService } from "@fiber-pay/runtime";
|
|
2703
|
+
import { createKeyManager } from "@fiber-pay/sdk";
|
|
2704
|
+
|
|
2705
|
+
// src/lib/bootnode.ts
|
|
2706
|
+
import { existsSync as existsSync9, readFileSync as readFileSync6 } from "fs";
|
|
2707
|
+
import { parse as parseYaml } from "yaml";
|
|
2708
|
+
function extractBootnodeAddrs(configFilePath) {
|
|
2709
|
+
if (!existsSync9(configFilePath)) return [];
|
|
2710
|
+
try {
|
|
2711
|
+
const content = readFileSync6(configFilePath, "utf-8");
|
|
2712
|
+
const doc = parseYaml(content);
|
|
2713
|
+
const addrs = doc?.fiber?.bootnode_addrs;
|
|
2714
|
+
if (!Array.isArray(addrs)) return [];
|
|
2715
|
+
return addrs.filter((a) => typeof a === "string" && a.startsWith("/ip"));
|
|
2716
|
+
} catch {
|
|
2717
|
+
return [];
|
|
2718
|
+
}
|
|
2719
|
+
}
|
|
2720
|
+
async function autoConnectBootnodes(rpc, bootnodes) {
|
|
2721
|
+
if (bootnodes.length === 0) return;
|
|
2722
|
+
console.log(`\u{1F517} Connecting to ${bootnodes.length} bootnode(s)...`);
|
|
2723
|
+
for (const addr of bootnodes) {
|
|
2724
|
+
const shortId = addr.match(/\/p2p\/(.+)$/)?.[1]?.slice(0, 12) || addr.slice(-12);
|
|
2725
|
+
try {
|
|
2726
|
+
await rpc.connectPeer({ address: addr });
|
|
2727
|
+
console.log(` \u2705 Connected to ${shortId}...`);
|
|
2728
|
+
} catch (err) {
|
|
2729
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
2730
|
+
if (msg.toLowerCase().includes("already")) {
|
|
2731
|
+
console.log(` \u2705 Already connected to ${shortId}...`);
|
|
2732
|
+
} else {
|
|
2733
|
+
console.error(` \u26A0\uFE0F Failed to connect to ${shortId}...: ${msg}`);
|
|
2734
|
+
}
|
|
2735
|
+
}
|
|
2736
|
+
}
|
|
2737
|
+
}
|
|
2738
|
+
|
|
2739
|
+
// src/lib/node-start.ts
|
|
2740
|
+
async function runNodeStartCommand(config, options) {
|
|
2741
|
+
const json = Boolean(options.json);
|
|
2742
|
+
const daemon = Boolean(options.daemon);
|
|
2743
|
+
const isNodeChild = process.env.FIBER_NODE_CHILD === "1";
|
|
2744
|
+
const quietFnn = Boolean(options.quietFnn);
|
|
2745
|
+
const eventStream = String(options.eventStream ?? "jsonl").toLowerCase();
|
|
2746
|
+
const emitStage = (stage, status, data) => {
|
|
2747
|
+
if (!json) return;
|
|
2748
|
+
printJsonEvent("startup_stage", { stage, status, ...data });
|
|
2749
|
+
};
|
|
2750
|
+
const emitFnnLog = (stream, text) => {
|
|
2751
|
+
if (quietFnn) {
|
|
2752
|
+
return;
|
|
2753
|
+
}
|
|
2754
|
+
if (json) {
|
|
2755
|
+
printJsonEvent("fnn_log", { stream, text });
|
|
2756
|
+
return;
|
|
2757
|
+
}
|
|
2758
|
+
const target = stream === "stderr" ? process.stderr : process.stdout;
|
|
2759
|
+
const payload = text.endsWith("\n") ? text : `${text}
|
|
2760
|
+
`;
|
|
2761
|
+
target.write(`[fnn:${stream}] ${payload}`);
|
|
2762
|
+
};
|
|
2763
|
+
if (json && eventStream !== "jsonl") {
|
|
2764
|
+
printJsonError({
|
|
2765
|
+
code: "NODE_EVENT_STREAM_INVALID",
|
|
2766
|
+
message: `Unsupported --event-stream format: ${options.eventStream}. Expected: jsonl`,
|
|
2767
|
+
recoverable: true,
|
|
2768
|
+
suggestion: "Use `--event-stream jsonl` when using --json.",
|
|
2769
|
+
details: { provided: options.eventStream, expected: ["jsonl"] }
|
|
2770
|
+
});
|
|
2771
|
+
process.exit(1);
|
|
2772
|
+
}
|
|
2773
|
+
if (daemon && !isNodeChild) {
|
|
2774
|
+
const cliEntrypoint = process.argv[1];
|
|
2775
|
+
if (!cliEntrypoint) {
|
|
2776
|
+
const message = "Unable to resolve CLI entrypoint path for --daemon mode";
|
|
2777
|
+
if (json) {
|
|
2778
|
+
printJsonError({
|
|
2779
|
+
code: "NODE_DAEMON_START_FAILED",
|
|
2780
|
+
message,
|
|
2781
|
+
recoverable: true,
|
|
2782
|
+
suggestion: "Retry without --daemon or check CLI invocation method."
|
|
2783
|
+
});
|
|
2784
|
+
} else {
|
|
2785
|
+
console.error(`\u274C ${message}`);
|
|
2786
|
+
}
|
|
2787
|
+
process.exit(1);
|
|
2788
|
+
}
|
|
2789
|
+
const childArgs = process.argv.slice(2).filter((arg) => arg !== "--daemon");
|
|
2790
|
+
const child = spawn(process.execPath, [cliEntrypoint, ...childArgs], {
|
|
2791
|
+
detached: true,
|
|
2792
|
+
stdio: "ignore",
|
|
2793
|
+
cwd: process.cwd(),
|
|
2794
|
+
env: {
|
|
2795
|
+
...process.env,
|
|
2796
|
+
FIBER_NODE_CHILD: "1",
|
|
2797
|
+
FIBER_NODE_RUNTIME_DAEMON: "1"
|
|
2798
|
+
}
|
|
2799
|
+
});
|
|
2800
|
+
child.unref();
|
|
2801
|
+
const childPid = child.pid;
|
|
2802
|
+
if (!childPid) {
|
|
2803
|
+
const message = "Failed to spawn node daemon process";
|
|
2804
|
+
if (json) {
|
|
2805
|
+
printJsonError({
|
|
2806
|
+
code: "NODE_DAEMON_START_FAILED",
|
|
2807
|
+
message,
|
|
2808
|
+
recoverable: true,
|
|
2809
|
+
suggestion: "Retry node start and inspect system process limits."
|
|
2810
|
+
});
|
|
2811
|
+
} else {
|
|
2812
|
+
console.error(`\u274C ${message}`);
|
|
2813
|
+
}
|
|
2814
|
+
process.exit(1);
|
|
2815
|
+
}
|
|
2816
|
+
if (json) {
|
|
2817
|
+
printJsonEvent("node_daemon_starting", {
|
|
2818
|
+
pid: childPid,
|
|
2819
|
+
runtimeDaemon: true
|
|
2820
|
+
});
|
|
2821
|
+
} else {
|
|
2822
|
+
console.log(`Node daemon starting (PID: ${childPid})`);
|
|
2823
|
+
console.log("Use `fiber-pay node status --json` to verify readiness.");
|
|
2824
|
+
}
|
|
2825
|
+
return;
|
|
2826
|
+
}
|
|
2827
|
+
emitStage("init", "ok", { rpcUrl: config.rpcUrl, dataDir: config.dataDir });
|
|
2828
|
+
const existingPid = readPidFile(config.dataDir);
|
|
2829
|
+
if (existingPid && isProcessRunning(existingPid)) {
|
|
2830
|
+
if (json) {
|
|
2831
|
+
printJsonError({
|
|
2832
|
+
code: "NODE_ALREADY_RUNNING",
|
|
2833
|
+
message: `Node is already running (PID: ${existingPid})`,
|
|
2834
|
+
recoverable: true,
|
|
2835
|
+
suggestion: "Skip start or run `fiber-pay node stop` before retrying.",
|
|
2836
|
+
details: { pid: existingPid }
|
|
2837
|
+
});
|
|
2838
|
+
} else {
|
|
2839
|
+
console.log(`\u274C Node is already running (PID: ${existingPid})`);
|
|
2840
|
+
}
|
|
2841
|
+
process.exit(1);
|
|
2842
|
+
}
|
|
2843
|
+
const runtimeDaemon = process.env.FIBER_NODE_RUNTIME_DAEMON === "1";
|
|
2844
|
+
const runtimeProxyListen = String(
|
|
2845
|
+
options.runtimeProxyListen ?? config.runtimeProxyListen ?? "127.0.0.1:8229"
|
|
2846
|
+
);
|
|
2847
|
+
const proxyListenSource = options.runtimeProxyListen ? "cli" : config.runtimeProxyListen ? "profile" : "default";
|
|
2848
|
+
const runtimeStateFilePath = join5(config.dataDir, "runtime-state.json");
|
|
2849
|
+
const logsDir = join5(config.dataDir, "logs");
|
|
2850
|
+
const fnnStdoutLogPath = join5(logsDir, "fnn.stdout.log");
|
|
2851
|
+
const fnnStderrLogPath = join5(logsDir, "fnn.stderr.log");
|
|
2852
|
+
const runtimeAlertLogPath = join5(logsDir, "runtime.alerts.jsonl");
|
|
2853
|
+
mkdirSync2(logsDir, { recursive: true });
|
|
2854
|
+
const binaryPath = config.binaryPath || getDefaultBinaryPath();
|
|
2855
|
+
await ensureFiberBinary();
|
|
2856
|
+
const binaryVersion = getBinaryVersion(binaryPath);
|
|
2857
|
+
const configFilePath = ensureNodeConfigFile(config.dataDir, config.network);
|
|
2858
|
+
emitStage("binary_resolved", "ok", {
|
|
2859
|
+
binaryPath,
|
|
2860
|
+
binaryVersion,
|
|
2861
|
+
configFilePath
|
|
2862
|
+
});
|
|
2863
|
+
if (!json) {
|
|
2864
|
+
console.log(`\u{1F9E9} Binary: ${binaryPath}`);
|
|
2865
|
+
console.log(`\u{1F9E9} Version: ${binaryVersion}`);
|
|
2866
|
+
}
|
|
2867
|
+
const nodeConfig = {
|
|
2868
|
+
binaryPath,
|
|
2869
|
+
dataDir: config.dataDir,
|
|
2870
|
+
configFilePath,
|
|
2871
|
+
chain: config.network,
|
|
2872
|
+
keyPassword: config.keyPassword
|
|
2873
|
+
};
|
|
2874
|
+
try {
|
|
2875
|
+
const keyManager = createKeyManager(config.dataDir, {
|
|
2876
|
+
encryptionPassword: config.keyPassword,
|
|
2877
|
+
autoGenerate: true
|
|
2878
|
+
});
|
|
2879
|
+
await keyManager.initialize();
|
|
2880
|
+
emitStage("key_initialized", "ok", {});
|
|
2881
|
+
} catch (error) {
|
|
2882
|
+
const message = `Failed to initialize node keys: ${error instanceof Error ? error.message : String(error)}`;
|
|
2883
|
+
emitStage("key_initialized", "error", { code: "NODE_KEY_INIT_FAILED", message });
|
|
2884
|
+
if (json) {
|
|
2885
|
+
printJsonError({
|
|
2886
|
+
code: "NODE_KEY_INIT_FAILED",
|
|
2887
|
+
message,
|
|
2888
|
+
recoverable: true,
|
|
2889
|
+
suggestion: "Verify key password and write permission for the data directory.",
|
|
2890
|
+
details: { dataDir: config.dataDir }
|
|
2891
|
+
});
|
|
2892
|
+
} else {
|
|
2893
|
+
console.error(`\u274C ${message}`);
|
|
2894
|
+
}
|
|
2895
|
+
process.exit(1);
|
|
2896
|
+
}
|
|
2897
|
+
const processManager = new ProcessManager(nodeConfig);
|
|
2898
|
+
let earlyStop = null;
|
|
2899
|
+
const formatStopDetails = (stop) => {
|
|
2900
|
+
if (!stop) return "";
|
|
2901
|
+
return ` (code: ${stop.code ?? "null"}, signal: ${stop.signal ?? "null"})`;
|
|
2902
|
+
};
|
|
2903
|
+
processManager.on("stopped", (code, signal) => {
|
|
2904
|
+
earlyStop = { code, signal };
|
|
2905
|
+
removePidFile(config.dataDir);
|
|
2906
|
+
});
|
|
2907
|
+
processManager.on("stdout", (text) => {
|
|
2908
|
+
appendFileSync(fnnStdoutLogPath, text, "utf-8");
|
|
2909
|
+
emitFnnLog("stdout", text);
|
|
2910
|
+
});
|
|
2911
|
+
processManager.on("stderr", (text) => {
|
|
2912
|
+
appendFileSync(fnnStderrLogPath, text, "utf-8");
|
|
2913
|
+
emitFnnLog("stderr", text);
|
|
2914
|
+
});
|
|
2915
|
+
await processManager.start();
|
|
2916
|
+
const processManagerState = processManager;
|
|
2917
|
+
const processId = processManagerState.process?.pid;
|
|
2918
|
+
if (processId !== void 0) {
|
|
2919
|
+
writePidFile(config.dataDir, processId);
|
|
2920
|
+
}
|
|
2921
|
+
emitStage("process_started", "ok", { pid: processId ?? null });
|
|
2922
|
+
let runtime;
|
|
2923
|
+
const rpc = createRpcClient(config);
|
|
2924
|
+
let rpcReady = true;
|
|
2925
|
+
try {
|
|
2926
|
+
await rpc.waitForReady({ timeout: 3e4, interval: 500 });
|
|
2927
|
+
} catch {
|
|
2928
|
+
rpcReady = false;
|
|
2929
|
+
}
|
|
2930
|
+
if (earlyStop || processManager.getState() === "stopped") {
|
|
2931
|
+
const details = formatStopDetails(earlyStop);
|
|
2932
|
+
emitStage("process_started", "error", {
|
|
2933
|
+
code: "NODE_STARTUP_EXITED",
|
|
2934
|
+
details
|
|
2935
|
+
});
|
|
2936
|
+
if (json) {
|
|
2937
|
+
printJsonError({
|
|
2938
|
+
code: "NODE_STARTUP_EXITED",
|
|
2939
|
+
message: `Fiber node exited during startup${details}`,
|
|
2940
|
+
recoverable: true,
|
|
2941
|
+
suggestion: "Inspect fnn logs and verify config ports are free before retrying.",
|
|
2942
|
+
details: earlyStop ?? void 0
|
|
2943
|
+
});
|
|
2944
|
+
} else {
|
|
2945
|
+
console.error(`\u274C Fiber node exited during startup${details}`);
|
|
2946
|
+
}
|
|
2947
|
+
removePidFile(config.dataDir);
|
|
2948
|
+
if (runtimeDaemon) {
|
|
2949
|
+
stopRuntimeDaemonFromNode({ dataDir: config.dataDir, rpcUrl: config.rpcUrl });
|
|
2950
|
+
} else if (runtime) {
|
|
2951
|
+
await runtime.stop().catch(() => void 0);
|
|
2952
|
+
}
|
|
2953
|
+
removeRuntimeFiles(config.dataDir);
|
|
2954
|
+
process.exit(1);
|
|
2955
|
+
}
|
|
2956
|
+
try {
|
|
2957
|
+
if (runtimeDaemon) {
|
|
2958
|
+
const daemonStart = startRuntimeDaemonFromNode({
|
|
2959
|
+
dataDir: config.dataDir,
|
|
2960
|
+
rpcUrl: config.rpcUrl,
|
|
2961
|
+
proxyListen: runtimeProxyListen,
|
|
2962
|
+
stateFilePath: runtimeStateFilePath,
|
|
2963
|
+
alertLogFile: runtimeAlertLogPath
|
|
2964
|
+
});
|
|
2965
|
+
if (!daemonStart.ok) {
|
|
2966
|
+
throw new Error(daemonStart.message);
|
|
2967
|
+
}
|
|
2968
|
+
} else {
|
|
2969
|
+
runtime = await startRuntimeService({
|
|
2970
|
+
fiberRpcUrl: config.rpcUrl,
|
|
2971
|
+
proxy: {
|
|
2972
|
+
enabled: true,
|
|
2973
|
+
listen: runtimeProxyListen
|
|
2974
|
+
},
|
|
2975
|
+
storage: {
|
|
2976
|
+
stateFilePath: runtimeStateFilePath
|
|
2977
|
+
},
|
|
2978
|
+
alerts: [{ type: "stdout" }, { type: "file", path: runtimeAlertLogPath }],
|
|
2979
|
+
jobs: {
|
|
2980
|
+
enabled: true,
|
|
2981
|
+
dbPath: join5(config.dataDir, "runtime-jobs.db")
|
|
2982
|
+
}
|
|
2983
|
+
});
|
|
2984
|
+
const runtimeStatus = runtime.service.getStatus();
|
|
2985
|
+
writeRuntimePid(config.dataDir, process.pid);
|
|
2986
|
+
writeRuntimeMeta(config.dataDir, {
|
|
2987
|
+
pid: process.pid,
|
|
2988
|
+
startedAt: runtimeStatus.startedAt,
|
|
2989
|
+
fiberRpcUrl: runtimeStatus.targetUrl,
|
|
2990
|
+
proxyListen: runtimeStatus.proxyListen,
|
|
2991
|
+
stateFilePath: runtimeStateFilePath,
|
|
2992
|
+
alertLogFilePath: runtimeAlertLogPath,
|
|
2993
|
+
fnnStdoutLogPath,
|
|
2994
|
+
fnnStderrLogPath,
|
|
2995
|
+
daemon: false
|
|
2996
|
+
});
|
|
2997
|
+
}
|
|
2998
|
+
emitStage("runtime_started", "ok", {
|
|
2999
|
+
proxyListen: runtimeProxyListen,
|
|
3000
|
+
daemon: runtimeDaemon
|
|
3001
|
+
});
|
|
3002
|
+
} catch (error) {
|
|
3003
|
+
const message = `Runtime failed to start: ${error instanceof Error ? error.message : String(error)}`;
|
|
3004
|
+
emitStage("runtime_started", "error", {
|
|
3005
|
+
code: "NODE_RUNTIME_START_FAILED",
|
|
3006
|
+
message,
|
|
3007
|
+
proxyListen: runtimeProxyListen
|
|
3008
|
+
});
|
|
3009
|
+
if (json) {
|
|
3010
|
+
printJsonError({
|
|
3011
|
+
code: "NODE_RUNTIME_START_FAILED",
|
|
3012
|
+
message,
|
|
3013
|
+
recoverable: true,
|
|
3014
|
+
suggestion: "Retry with a free --runtime-proxy-listen port.",
|
|
3015
|
+
details: {
|
|
3016
|
+
runtimeProxyListen
|
|
3017
|
+
}
|
|
3018
|
+
});
|
|
3019
|
+
} else {
|
|
3020
|
+
console.error(`\u274C ${message}`);
|
|
3021
|
+
}
|
|
3022
|
+
removeRuntimeFiles(config.dataDir);
|
|
3023
|
+
removePidFile(config.dataDir);
|
|
3024
|
+
await processManager.stop().catch(() => void 0);
|
|
3025
|
+
process.exit(1);
|
|
3026
|
+
}
|
|
3027
|
+
if (!rpcReady) {
|
|
3028
|
+
emitStage("rpc_ready", "error", {
|
|
3029
|
+
code: "NODE_RPC_NOT_READY",
|
|
3030
|
+
timeoutSeconds: 30
|
|
3031
|
+
});
|
|
3032
|
+
if (json) {
|
|
3033
|
+
printJsonError({
|
|
3034
|
+
code: "NODE_RPC_NOT_READY",
|
|
3035
|
+
message: "RPC did not become ready within 30s. Node startup failed.",
|
|
3036
|
+
recoverable: true,
|
|
3037
|
+
suggestion: "Retry after a delay and verify RPC port + config consistency.",
|
|
3038
|
+
details: { timeoutSeconds: 30, rpcUrl: config.rpcUrl }
|
|
3039
|
+
});
|
|
3040
|
+
} else {
|
|
3041
|
+
console.error("\u274C RPC did not become ready within 30s. Node startup failed.");
|
|
3042
|
+
}
|
|
3043
|
+
const stderrTail = processManager.getStderr(12).join("").trim();
|
|
3044
|
+
const stdoutTail = processManager.getStdout(12).join("").trim();
|
|
3045
|
+
if (!json && stderrTail.length > 0) {
|
|
3046
|
+
console.error("--- fnn stderr (tail) ---");
|
|
3047
|
+
console.error(stderrTail);
|
|
3048
|
+
}
|
|
3049
|
+
if (!json && stdoutTail.length > 0) {
|
|
3050
|
+
console.error("--- fnn stdout (tail) ---");
|
|
3051
|
+
console.error(stdoutTail);
|
|
3052
|
+
}
|
|
3053
|
+
removePidFile(config.dataDir);
|
|
3054
|
+
if (runtimeDaemon) {
|
|
3055
|
+
stopRuntimeDaemonFromNode({ dataDir: config.dataDir, rpcUrl: config.rpcUrl });
|
|
3056
|
+
} else if (runtime) {
|
|
3057
|
+
await runtime.stop().catch(() => void 0);
|
|
3058
|
+
removeRuntimeFiles(config.dataDir);
|
|
3059
|
+
}
|
|
3060
|
+
await processManager.stop().catch(() => void 0);
|
|
3061
|
+
process.exit(1);
|
|
3062
|
+
}
|
|
3063
|
+
emitStage("rpc_ready", "ok", { rpcUrl: config.rpcUrl });
|
|
3064
|
+
const bootnodes = nodeConfig.configFilePath ? extractBootnodeAddrs(nodeConfig.configFilePath) : extractBootnodeAddrs(join5(config.dataDir, "config.yml"));
|
|
3065
|
+
if (bootnodes.length > 0) {
|
|
3066
|
+
await autoConnectBootnodes(rpc, bootnodes);
|
|
3067
|
+
}
|
|
3068
|
+
emitStage("bootnodes_connected", "ok", { count: bootnodes.length });
|
|
3069
|
+
if (earlyStop || processManager.getState() === "stopped") {
|
|
3070
|
+
const details = formatStopDetails(earlyStop);
|
|
3071
|
+
emitStage("bootnodes_connected", "error", {
|
|
3072
|
+
code: "NODE_STARTUP_EXITED",
|
|
3073
|
+
details
|
|
3074
|
+
});
|
|
3075
|
+
if (json) {
|
|
3076
|
+
printJsonError({
|
|
3077
|
+
code: "NODE_STARTUP_EXITED",
|
|
3078
|
+
message: `Fiber node exited during startup${details}`,
|
|
3079
|
+
recoverable: true,
|
|
3080
|
+
suggestion: "Inspect fnn logs and verify config ports are free before retrying.",
|
|
3081
|
+
details: earlyStop ?? void 0
|
|
3082
|
+
});
|
|
3083
|
+
} else {
|
|
3084
|
+
console.error(`\u274C Fiber node exited during startup${details}`);
|
|
3085
|
+
}
|
|
3086
|
+
removePidFile(config.dataDir);
|
|
3087
|
+
process.exit(1);
|
|
3088
|
+
}
|
|
3089
|
+
if (json) {
|
|
3090
|
+
emitStage("startup_complete", "ok", {
|
|
3091
|
+
pid: processId ?? null,
|
|
3092
|
+
rpcUrl: config.rpcUrl
|
|
3093
|
+
});
|
|
3094
|
+
printJsonEvent("node_started", {
|
|
3095
|
+
rpcUrl: config.rpcUrl,
|
|
3096
|
+
binaryPath,
|
|
3097
|
+
binaryVersion,
|
|
3098
|
+
pid: processId ?? null,
|
|
3099
|
+
runtimeEnabled: true,
|
|
3100
|
+
runtimeDaemon,
|
|
3101
|
+
quietFnn,
|
|
3102
|
+
proxyUrl: `http://${runtimeProxyListen}`,
|
|
3103
|
+
proxyListenSource,
|
|
3104
|
+
logs: {
|
|
3105
|
+
fnnStdout: fnnStdoutLogPath,
|
|
3106
|
+
fnnStderr: fnnStderrLogPath,
|
|
3107
|
+
runtimeAlerts: runtimeAlertLogPath
|
|
3108
|
+
}
|
|
3109
|
+
});
|
|
3110
|
+
} else {
|
|
3111
|
+
console.log("\u2705 Fiber node started successfully!");
|
|
3112
|
+
console.log(` RPC endpoint: ${config.rpcUrl}`);
|
|
3113
|
+
console.log(
|
|
3114
|
+
` Runtime proxy: http://${runtimeProxyListen} (browser-safe endpoint + monitoring)`
|
|
3115
|
+
);
|
|
3116
|
+
console.log(` Runtime mode: ${runtimeDaemon ? "daemon" : "embedded"}`);
|
|
3117
|
+
console.log(` Log files: ${logsDir}`);
|
|
3118
|
+
console.log(" Press Ctrl+C to stop.");
|
|
3119
|
+
}
|
|
3120
|
+
let shutdownRequested = false;
|
|
3121
|
+
const shutdown = async () => {
|
|
3122
|
+
if (shutdownRequested) return;
|
|
3123
|
+
shutdownRequested = true;
|
|
3124
|
+
if (!json) {
|
|
3125
|
+
console.log("\n\u{1F6D1} Shutting down...");
|
|
3126
|
+
}
|
|
3127
|
+
if (runtimeDaemon) {
|
|
3128
|
+
stopRuntimeDaemonFromNode({ dataDir: config.dataDir, rpcUrl: config.rpcUrl });
|
|
3129
|
+
} else if (runtime) {
|
|
3130
|
+
await runtime.stop();
|
|
3131
|
+
}
|
|
3132
|
+
removeRuntimeFiles(config.dataDir);
|
|
3133
|
+
removePidFile(config.dataDir);
|
|
3134
|
+
await processManager.stop();
|
|
3135
|
+
if (json) {
|
|
3136
|
+
printJsonEvent("node_stopped", { reason: "signal" });
|
|
3137
|
+
} else {
|
|
3138
|
+
console.log("\u2705 Node stopped.");
|
|
3139
|
+
}
|
|
3140
|
+
};
|
|
3141
|
+
await new Promise((resolve2) => {
|
|
3142
|
+
const keepAlive = setInterval(() => void 0, 6e4);
|
|
3143
|
+
const cleanup = () => {
|
|
3144
|
+
clearInterval(keepAlive);
|
|
3145
|
+
process.off("SIGINT", onSigInt);
|
|
3146
|
+
process.off("SIGTERM", onSigTerm);
|
|
3147
|
+
processManager.off("stopped", onStopped);
|
|
3148
|
+
resolve2();
|
|
3149
|
+
};
|
|
3150
|
+
const onStopped = () => {
|
|
3151
|
+
cleanup();
|
|
3152
|
+
};
|
|
3153
|
+
const onSigInt = () => {
|
|
3154
|
+
shutdown().finally(cleanup);
|
|
3155
|
+
};
|
|
3156
|
+
const onSigTerm = () => {
|
|
3157
|
+
shutdown().finally(cleanup);
|
|
3158
|
+
};
|
|
3159
|
+
process.on("SIGINT", onSigInt);
|
|
3160
|
+
process.on("SIGTERM", onSigTerm);
|
|
3161
|
+
processManager.on("stopped", onStopped);
|
|
3162
|
+
});
|
|
3163
|
+
if (!shutdownRequested) {
|
|
3164
|
+
if (json) {
|
|
3165
|
+
printJsonError({
|
|
3166
|
+
code: "NODE_STOPPED_UNEXPECTEDLY",
|
|
3167
|
+
message: "Fiber node stopped unexpectedly.",
|
|
3168
|
+
recoverable: true,
|
|
3169
|
+
suggestion: "Check process logs and restart the node when configuration is healthy."
|
|
3170
|
+
});
|
|
3171
|
+
} else {
|
|
3172
|
+
console.error("\u274C Fiber node stopped unexpectedly.");
|
|
3173
|
+
}
|
|
3174
|
+
removePidFile(config.dataDir);
|
|
3175
|
+
if (runtimeDaemon) {
|
|
3176
|
+
stopRuntimeDaemonFromNode({ dataDir: config.dataDir, rpcUrl: config.rpcUrl });
|
|
3177
|
+
}
|
|
3178
|
+
removeRuntimeFiles(config.dataDir);
|
|
3179
|
+
process.exit(1);
|
|
3180
|
+
}
|
|
3181
|
+
}
|
|
3182
|
+
|
|
3183
|
+
// src/lib/node-status.ts
|
|
3184
|
+
import { existsSync as existsSync10 } from "fs";
|
|
3185
|
+
import { join as join6 } from "path";
|
|
3186
|
+
import { getFiberBinaryInfo as getFiberBinaryInfo2 } from "@fiber-pay/node";
|
|
3187
|
+
import {
|
|
3188
|
+
buildMultiaddrFromNodeId,
|
|
3189
|
+
buildMultiaddrFromRpcUrl,
|
|
3190
|
+
ChannelState as ChannelState2,
|
|
3191
|
+
nodeIdToPeerId,
|
|
3192
|
+
scriptToAddress
|
|
3193
|
+
} from "@fiber-pay/sdk";
|
|
3194
|
+
|
|
3195
|
+
// src/lib/node-recommendation.ts
|
|
3196
|
+
function summarizeChannelLiquidity(readyChannels) {
|
|
3197
|
+
const canSend = readyChannels.some((channel) => BigInt(channel.local_balance) > 0n);
|
|
3198
|
+
const canReceive = readyChannels.some((channel) => BigInt(channel.remote_balance) > 0n);
|
|
3199
|
+
let totalLocal = 0n;
|
|
3200
|
+
let totalRemote = 0n;
|
|
3201
|
+
for (const channel of readyChannels) {
|
|
3202
|
+
totalLocal += BigInt(channel.local_balance);
|
|
3203
|
+
totalRemote += BigInt(channel.remote_balance);
|
|
3204
|
+
}
|
|
3205
|
+
return {
|
|
3206
|
+
canSend,
|
|
3207
|
+
canReceive,
|
|
3208
|
+
localCkb: Number(totalLocal) / 1e8,
|
|
3209
|
+
remoteCkb: Number(totalRemote) / 1e8
|
|
3210
|
+
};
|
|
3211
|
+
}
|
|
3212
|
+
function buildStatusRecommendation(input) {
|
|
3213
|
+
const reasons = [];
|
|
3214
|
+
if (!input.binaryReady) reasons.push("Fiber binary is missing or not executable.");
|
|
3215
|
+
if (!input.configExists) reasons.push("Config file is missing.");
|
|
3216
|
+
if (!input.nodeRunning) reasons.push("Node process is not running.");
|
|
3217
|
+
if (input.nodeRunning && !input.rpcResponsive) {
|
|
3218
|
+
reasons.push("Node process is running but RPC is not reachable.");
|
|
3219
|
+
}
|
|
3220
|
+
if (input.rpcResponsive && input.channelsReady === 0) {
|
|
3221
|
+
reasons.push("No ChannelReady channel found.");
|
|
3222
|
+
}
|
|
3223
|
+
if (input.channelsReady > 0 && !input.canSend && input.canReceive) {
|
|
3224
|
+
reasons.push("Send liquidity is low on ChannelReady channels.");
|
|
3225
|
+
}
|
|
3226
|
+
if (input.channelsReady > 0 && input.canSend && !input.canReceive) {
|
|
3227
|
+
reasons.push("Receive liquidity is low on ChannelReady channels.");
|
|
3228
|
+
}
|
|
3229
|
+
if (input.channelsReady > 0 && !input.canSend && !input.canReceive) {
|
|
3230
|
+
reasons.push("ChannelReady channels exist but liquidity is zero.");
|
|
3231
|
+
}
|
|
3232
|
+
let recommendation = "READY";
|
|
3233
|
+
if (!input.binaryReady) {
|
|
3234
|
+
recommendation = "INSTALL_BINARY";
|
|
3235
|
+
} else if (!input.configExists) {
|
|
3236
|
+
recommendation = "INIT_CONFIG";
|
|
3237
|
+
} else if (!input.nodeRunning) {
|
|
3238
|
+
recommendation = "START_NODE";
|
|
3239
|
+
} else if (!input.rpcResponsive) {
|
|
3240
|
+
recommendation = "WAIT_RPC";
|
|
3241
|
+
} else if (input.channelsReady === 0) {
|
|
3242
|
+
recommendation = "OPEN_CHANNEL";
|
|
3243
|
+
} else if (!input.canSend && !input.canReceive) {
|
|
3244
|
+
recommendation = "NO_LIQUIDITY";
|
|
3245
|
+
} else if (!input.canSend && input.canReceive) {
|
|
3246
|
+
recommendation = "SEND_CAPACITY_LOW";
|
|
3247
|
+
} else if (input.canSend && !input.canReceive) {
|
|
3248
|
+
recommendation = "RECEIVE_CAPACITY_LOW";
|
|
3249
|
+
}
|
|
3250
|
+
return { recommendation, reasons };
|
|
3251
|
+
}
|
|
3252
|
+
function buildReadyRecommendation(input) {
|
|
3253
|
+
if (!input.nodeRunning) {
|
|
3254
|
+
return {
|
|
3255
|
+
recommendation: "NODE_STOPPED",
|
|
3256
|
+
reasons: ["Node process is not running."]
|
|
3257
|
+
};
|
|
3258
|
+
}
|
|
3259
|
+
if (!input.rpcReachable) {
|
|
3260
|
+
return {
|
|
3261
|
+
recommendation: "RPC_UNREACHABLE",
|
|
3262
|
+
reasons: ["Node process is running but RPC is not reachable."]
|
|
3263
|
+
};
|
|
3264
|
+
}
|
|
3265
|
+
if (input.channelsReady === 0) {
|
|
3266
|
+
return {
|
|
3267
|
+
recommendation: "NEED_CHANNEL",
|
|
3268
|
+
reasons: ["No ChannelReady channel found. Open and wait for channel readiness."]
|
|
3269
|
+
};
|
|
3270
|
+
}
|
|
3271
|
+
if (input.canSend && input.canReceive) {
|
|
3272
|
+
return {
|
|
3273
|
+
recommendation: "READY",
|
|
3274
|
+
reasons: ["Node is reachable and has send/receive liquidity."]
|
|
3275
|
+
};
|
|
3276
|
+
}
|
|
3277
|
+
if (input.canSend) {
|
|
3278
|
+
return {
|
|
3279
|
+
recommendation: "RECEIVE_CAPACITY_LOW",
|
|
3280
|
+
reasons: ["Receive liquidity is low on all ChannelReady channels."]
|
|
3281
|
+
};
|
|
3282
|
+
}
|
|
3283
|
+
if (input.canReceive) {
|
|
3284
|
+
return {
|
|
3285
|
+
recommendation: "SEND_CAPACITY_LOW",
|
|
3286
|
+
reasons: ["Send liquidity is low on all ChannelReady channels."]
|
|
3287
|
+
};
|
|
3288
|
+
}
|
|
3289
|
+
return {
|
|
3290
|
+
recommendation: "NO_LIQUIDITY",
|
|
3291
|
+
reasons: ["ChannelReady channels exist but both local/remote liquidity are zero."]
|
|
3292
|
+
};
|
|
3293
|
+
}
|
|
3294
|
+
function buildStalePidRecommendation() {
|
|
3295
|
+
return {
|
|
3296
|
+
recommendation: "NODE_STOPPED",
|
|
3297
|
+
reasons: ["Stale PID file detected and cleaned."]
|
|
3298
|
+
};
|
|
3299
|
+
}
|
|
3300
|
+
|
|
3301
|
+
// src/lib/node-rpc.ts
|
|
3302
|
+
var CELLS_PAGE_SIZE = 100;
|
|
3303
|
+
async function callJsonRpc(url, method, params) {
|
|
3304
|
+
const response = await fetch(url, {
|
|
3305
|
+
method: "POST",
|
|
3306
|
+
headers: { "content-type": "application/json" },
|
|
3307
|
+
body: JSON.stringify({ jsonrpc: "2.0", id: 1, method, params })
|
|
3308
|
+
});
|
|
3309
|
+
if (!response.ok) {
|
|
3310
|
+
throw new Error(`HTTP ${response.status} ${response.statusText}`);
|
|
3311
|
+
}
|
|
3312
|
+
const payload = await response.json();
|
|
3313
|
+
if (payload.error) {
|
|
3314
|
+
const code = payload.error.code ?? "unknown";
|
|
3315
|
+
const message = payload.error.message ?? "JSON-RPC error";
|
|
3316
|
+
throw new Error(`${message} (code: ${code})`);
|
|
3317
|
+
}
|
|
3318
|
+
if (payload.result === void 0) {
|
|
3319
|
+
throw new Error("Missing JSON-RPC result");
|
|
3320
|
+
}
|
|
3321
|
+
return payload.result;
|
|
3322
|
+
}
|
|
3323
|
+
async function getLockBalanceShannons(ckbRpcUrl, lockScript) {
|
|
3324
|
+
let cursor;
|
|
3325
|
+
let total = 0n;
|
|
3326
|
+
const limitHex = `0x${CELLS_PAGE_SIZE.toString(16)}`;
|
|
3327
|
+
for (let i = 0; i < 2e3; i++) {
|
|
3328
|
+
const params = [{ script: lockScript, script_type: "lock" }, "asc", limitHex];
|
|
3329
|
+
if (cursor) {
|
|
3330
|
+
params.push(cursor);
|
|
3331
|
+
}
|
|
3332
|
+
const page = await callJsonRpc(ckbRpcUrl, "get_cells", params);
|
|
3333
|
+
const cells = page.objects ?? [];
|
|
3334
|
+
for (const cell of cells) {
|
|
3335
|
+
if (cell.output?.capacity) {
|
|
3336
|
+
total += BigInt(cell.output.capacity);
|
|
3337
|
+
}
|
|
3338
|
+
}
|
|
3339
|
+
const nextCursor = page.last_cursor;
|
|
3340
|
+
if (!nextCursor || nextCursor === cursor || cells.length < CELLS_PAGE_SIZE) {
|
|
3341
|
+
break;
|
|
3342
|
+
}
|
|
3343
|
+
cursor = nextCursor;
|
|
3344
|
+
}
|
|
3345
|
+
return total;
|
|
3346
|
+
}
|
|
3347
|
+
|
|
3348
|
+
// src/lib/node-status.ts
|
|
3349
|
+
async function runNodeStatusCommand(config, options) {
|
|
3350
|
+
const json = Boolean(options.json);
|
|
3351
|
+
const pid = readPidFile(config.dataDir);
|
|
3352
|
+
const resolvedRpc = resolveRpcEndpoint(config);
|
|
3353
|
+
const managedBinaryPath = join6(config.dataDir, "bin", "fnn");
|
|
3354
|
+
const binaryInfo = config.binaryPath ? getCustomBinaryState(config.binaryPath) : await getFiberBinaryInfo2(join6(config.dataDir, "bin"));
|
|
3355
|
+
const configExists = existsSync10(config.configPath);
|
|
3356
|
+
const nodeRunning = Boolean(pid && isProcessRunning(pid));
|
|
3357
|
+
let rpcResponsive = false;
|
|
3358
|
+
let nodeId = null;
|
|
3359
|
+
let peerId = null;
|
|
3360
|
+
let peerIdError = null;
|
|
3361
|
+
let multiaddr = null;
|
|
3362
|
+
let multiaddrError = null;
|
|
3363
|
+
let multiaddrInferred = false;
|
|
3364
|
+
let channelsTotal = 0;
|
|
3365
|
+
let channelsReady = 0;
|
|
3366
|
+
let canSend = false;
|
|
3367
|
+
let canReceive = false;
|
|
3368
|
+
let localCkb = 0;
|
|
3369
|
+
let remoteCkb = 0;
|
|
3370
|
+
let fundingAddress = null;
|
|
3371
|
+
let fundingCkb = 0;
|
|
3372
|
+
let fundingBalanceError = null;
|
|
3373
|
+
if (nodeRunning) {
|
|
3374
|
+
try {
|
|
3375
|
+
const rpc = await createReadyRpcClient(config);
|
|
3376
|
+
const nodeInfo = await rpc.nodeInfo();
|
|
3377
|
+
const channels = await rpc.listChannels({ include_closed: false });
|
|
3378
|
+
rpcResponsive = true;
|
|
3379
|
+
nodeId = nodeInfo.node_id;
|
|
3380
|
+
try {
|
|
3381
|
+
peerId = await nodeIdToPeerId(nodeInfo.node_id);
|
|
3382
|
+
} catch (error) {
|
|
3383
|
+
peerIdError = error instanceof Error ? error.message : String(error);
|
|
3384
|
+
}
|
|
3385
|
+
const baseAddress = nodeInfo.addresses[0];
|
|
3386
|
+
if (baseAddress) {
|
|
3387
|
+
try {
|
|
3388
|
+
multiaddr = await buildMultiaddrFromNodeId(baseAddress, nodeInfo.node_id);
|
|
3389
|
+
} catch (error) {
|
|
3390
|
+
multiaddrError = error instanceof Error ? error.message : String(error);
|
|
3391
|
+
}
|
|
3392
|
+
} else if (peerId) {
|
|
3393
|
+
try {
|
|
3394
|
+
multiaddr = buildMultiaddrFromRpcUrl(config.rpcUrl, peerId);
|
|
3395
|
+
multiaddrInferred = true;
|
|
3396
|
+
} catch (error) {
|
|
3397
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
3398
|
+
multiaddrError = `no advertised addresses; infer failed: ${reason}`;
|
|
3399
|
+
}
|
|
3400
|
+
}
|
|
3401
|
+
channelsTotal = channels.channels.length;
|
|
3402
|
+
const readyChannels = channels.channels.filter(
|
|
3403
|
+
(channel) => channel.state?.state_name === ChannelState2.ChannelReady
|
|
3404
|
+
);
|
|
3405
|
+
channelsReady = readyChannels.length;
|
|
3406
|
+
const liquidity = summarizeChannelLiquidity(readyChannels);
|
|
3407
|
+
canSend = liquidity.canSend;
|
|
3408
|
+
canReceive = liquidity.canReceive;
|
|
3409
|
+
localCkb = liquidity.localCkb;
|
|
3410
|
+
remoteCkb = liquidity.remoteCkb;
|
|
3411
|
+
fundingAddress = scriptToAddress(nodeInfo.default_funding_lock_script, config.network);
|
|
3412
|
+
if (config.ckbRpcUrl) {
|
|
3413
|
+
try {
|
|
3414
|
+
const fundingBalance = await getLockBalanceShannons(
|
|
3415
|
+
config.ckbRpcUrl,
|
|
3416
|
+
nodeInfo.default_funding_lock_script
|
|
3417
|
+
);
|
|
3418
|
+
fundingCkb = Number(fundingBalance) / 1e8;
|
|
3419
|
+
} catch (error) {
|
|
3420
|
+
fundingBalanceError = error instanceof Error ? error.message : "Failed to query CKB balance for funding address";
|
|
3421
|
+
}
|
|
3422
|
+
} else {
|
|
3423
|
+
fundingBalanceError = "CKB RPC URL not configured (set ckb.rpc_url in config.yml or FIBER_CKB_RPC_URL)";
|
|
3424
|
+
}
|
|
3425
|
+
} catch {
|
|
3426
|
+
rpcResponsive = false;
|
|
3427
|
+
}
|
|
3428
|
+
} else if (pid) {
|
|
3429
|
+
removePidFile(config.dataDir);
|
|
3430
|
+
}
|
|
3431
|
+
const { recommendation, reasons } = buildStatusRecommendation({
|
|
3432
|
+
binaryReady: binaryInfo.ready,
|
|
3433
|
+
configExists,
|
|
3434
|
+
nodeRunning,
|
|
3435
|
+
rpcResponsive,
|
|
3436
|
+
channelsReady,
|
|
3437
|
+
canSend,
|
|
3438
|
+
canReceive
|
|
3439
|
+
});
|
|
3440
|
+
const output = {
|
|
3441
|
+
running: nodeRunning,
|
|
3442
|
+
pid: pid ?? null,
|
|
3443
|
+
rpcResponsive,
|
|
3444
|
+
rpcUrl: config.rpcUrl,
|
|
3445
|
+
rpcTarget: resolvedRpc.target,
|
|
3446
|
+
resolvedRpcUrl: resolvedRpc.url,
|
|
3447
|
+
nodeId,
|
|
3448
|
+
peerId,
|
|
3449
|
+
peerIdError,
|
|
3450
|
+
multiaddr,
|
|
3451
|
+
multiaddrError,
|
|
3452
|
+
multiaddrInferred,
|
|
3453
|
+
checks: {
|
|
3454
|
+
binary: {
|
|
3455
|
+
path: binaryInfo.path,
|
|
3456
|
+
ready: binaryInfo.ready,
|
|
3457
|
+
version: binaryInfo.version,
|
|
3458
|
+
source: config.binaryPath ? "env-binary-path" : "managed-binary-dir",
|
|
3459
|
+
managedPath: managedBinaryPath
|
|
3460
|
+
},
|
|
3461
|
+
config: {
|
|
3462
|
+
path: config.configPath,
|
|
3463
|
+
exists: configExists,
|
|
3464
|
+
network: config.network,
|
|
3465
|
+
rpcUrl: config.rpcUrl
|
|
3466
|
+
},
|
|
3467
|
+
node: {
|
|
3468
|
+
running: nodeRunning,
|
|
3469
|
+
pid: pid ?? null,
|
|
3470
|
+
rpcReachable: rpcResponsive,
|
|
3471
|
+
rpcTarget: resolvedRpc.target,
|
|
3472
|
+
rpcClientUrl: resolvedRpc.url
|
|
3473
|
+
},
|
|
3474
|
+
channels: {
|
|
3475
|
+
total: channelsTotal,
|
|
3476
|
+
ready: channelsReady,
|
|
3477
|
+
canSend,
|
|
3478
|
+
canReceive
|
|
3479
|
+
}
|
|
3480
|
+
},
|
|
3481
|
+
balance: {
|
|
3482
|
+
totalCkb: localCkb + fundingCkb,
|
|
3483
|
+
channelLocalCkb: localCkb,
|
|
3484
|
+
availableToSend: localCkb,
|
|
3485
|
+
availableToReceive: remoteCkb,
|
|
3486
|
+
channelCount: channelsTotal,
|
|
3487
|
+
activeChannelCount: channelsReady,
|
|
3488
|
+
fundingAddress,
|
|
3489
|
+
fundingAddressTotalCkb: fundingCkb,
|
|
3490
|
+
fundingBalanceError
|
|
3491
|
+
},
|
|
3492
|
+
recommendation,
|
|
3493
|
+
reasons
|
|
3494
|
+
};
|
|
3495
|
+
if (json) {
|
|
3496
|
+
printJsonSuccess(output);
|
|
3497
|
+
return;
|
|
3498
|
+
}
|
|
3499
|
+
if (output.running) {
|
|
3500
|
+
console.log(`\u2705 Node is running (PID: ${output.pid})`);
|
|
3501
|
+
if (output.rpcResponsive) {
|
|
3502
|
+
console.log(` Node ID: ${String(output.nodeId)}`);
|
|
3503
|
+
if (output.peerId) {
|
|
3504
|
+
console.log(` Peer ID: ${String(output.peerId)}`);
|
|
3505
|
+
} else if (output.peerIdError) {
|
|
3506
|
+
console.log(` Peer ID: unavailable (${String(output.peerIdError)})`);
|
|
3507
|
+
}
|
|
3508
|
+
console.log(` RPC: ${String(output.rpcUrl)}`);
|
|
3509
|
+
console.log(` RPC Client: ${String(output.rpcTarget)} (${String(output.resolvedRpcUrl)})`);
|
|
3510
|
+
if (output.multiaddr) {
|
|
3511
|
+
const inferredSuffix = output.multiaddrInferred ? " (inferred from RPC + peerId; no advertised addresses)" : "";
|
|
3512
|
+
console.log(` Multiaddr: ${String(output.multiaddr)}${inferredSuffix}`);
|
|
3513
|
+
} else if (output.multiaddrError) {
|
|
3514
|
+
console.log(` Multiaddr: unavailable (${String(output.multiaddrError)})`);
|
|
3515
|
+
} else {
|
|
3516
|
+
console.log(" Multiaddr: unavailable");
|
|
3517
|
+
}
|
|
3518
|
+
} else {
|
|
3519
|
+
console.log(" \u26A0\uFE0F RPC not responding");
|
|
3520
|
+
}
|
|
3521
|
+
} else if (output.pid) {
|
|
3522
|
+
console.log(`\u274C Node is not running (stale PID file: ${output.pid})`);
|
|
3523
|
+
} else {
|
|
3524
|
+
console.log("\u274C Node is not running");
|
|
3525
|
+
}
|
|
3526
|
+
console.log("");
|
|
3527
|
+
console.log("Diagnostics");
|
|
3528
|
+
console.log(` Binary: ${output.checks.binary.ready ? "ready" : "missing"}`);
|
|
3529
|
+
console.log(` Config: ${output.checks.config.exists ? "present" : "missing"}`);
|
|
3530
|
+
console.log(` RPC: ${output.checks.node.rpcReachable ? "reachable" : "unreachable"}`);
|
|
3531
|
+
console.log(
|
|
3532
|
+
` Channels: ${output.checks.channels.ready}/${output.checks.channels.total} ready/total`
|
|
3533
|
+
);
|
|
3534
|
+
console.log(` Can Send: ${output.checks.channels.canSend ? "yes" : "no"}`);
|
|
3535
|
+
console.log(` Can Receive: ${output.checks.channels.canReceive ? "yes" : "no"}`);
|
|
3536
|
+
console.log(` Recommendation:${output.recommendation}`);
|
|
3537
|
+
if (output.reasons.length > 0) {
|
|
3538
|
+
console.log(" Reasons:");
|
|
3539
|
+
for (const reason of output.reasons) {
|
|
3540
|
+
console.log(` - ${reason}`);
|
|
3541
|
+
}
|
|
3542
|
+
}
|
|
3543
|
+
console.log("");
|
|
3544
|
+
console.log("Balance");
|
|
3545
|
+
console.log(` Total CKB: ${output.balance.totalCkb.toFixed(8)}`);
|
|
3546
|
+
console.log(` Channel Local: ${output.balance.channelLocalCkb.toFixed(8)}`);
|
|
3547
|
+
console.log(` To Send: ${output.balance.availableToSend.toFixed(8)}`);
|
|
3548
|
+
console.log(` To Receive: ${output.balance.availableToReceive.toFixed(8)}`);
|
|
3549
|
+
console.log(
|
|
3550
|
+
` Channels: ${output.balance.activeChannelCount}/${output.balance.channelCount} active/total`
|
|
3551
|
+
);
|
|
3552
|
+
if (output.balance.fundingAddress) {
|
|
3553
|
+
console.log(` Funding Addr: ${output.balance.fundingAddress}`);
|
|
3554
|
+
}
|
|
3555
|
+
console.log(` Funding CKB: ${output.balance.fundingAddressTotalCkb.toFixed(8)}`);
|
|
3556
|
+
if (output.balance.fundingBalanceError) {
|
|
3557
|
+
console.log(` Funding Err: ${output.balance.fundingBalanceError}`);
|
|
3558
|
+
}
|
|
3559
|
+
}
|
|
3560
|
+
async function runNodeReadyCommand(config, options) {
|
|
3561
|
+
const json = Boolean(options.json);
|
|
3562
|
+
const pid = readPidFile(config.dataDir);
|
|
3563
|
+
const output = {
|
|
3564
|
+
nodeRunning: false,
|
|
3565
|
+
rpcReachable: false,
|
|
3566
|
+
channelsTotal: 0,
|
|
3567
|
+
channelsReady: 0,
|
|
3568
|
+
canSend: false,
|
|
3569
|
+
canReceive: false,
|
|
3570
|
+
recommendation: "NODE_STOPPED",
|
|
3571
|
+
reasons: ["Node process is not running."],
|
|
3572
|
+
pid: pid ?? null,
|
|
3573
|
+
rpcUrl: config.rpcUrl
|
|
3574
|
+
};
|
|
3575
|
+
if (pid && isProcessRunning(pid)) {
|
|
3576
|
+
output.nodeRunning = true;
|
|
3577
|
+
output.reasons = [];
|
|
3578
|
+
try {
|
|
3579
|
+
const rpc = await createReadyRpcClient(config);
|
|
3580
|
+
output.rpcReachable = true;
|
|
3581
|
+
const channels = await rpc.listChannels({ include_closed: false });
|
|
3582
|
+
output.channelsTotal = channels.channels.length;
|
|
3583
|
+
const readyChannels = channels.channels.filter(
|
|
3584
|
+
(channel) => channel.state?.state_name === ChannelState2.ChannelReady
|
|
3585
|
+
);
|
|
3586
|
+
output.channelsReady = readyChannels.length;
|
|
3587
|
+
const liquidity = summarizeChannelLiquidity(readyChannels);
|
|
3588
|
+
output.canSend = liquidity.canSend;
|
|
3589
|
+
output.canReceive = liquidity.canReceive;
|
|
3590
|
+
const readyRecommendation = buildReadyRecommendation({
|
|
3591
|
+
nodeRunning: true,
|
|
3592
|
+
rpcReachable: true,
|
|
3593
|
+
channelsReady: readyChannels.length,
|
|
3594
|
+
canSend: liquidity.canSend,
|
|
3595
|
+
canReceive: liquidity.canReceive
|
|
3596
|
+
});
|
|
3597
|
+
output.recommendation = readyRecommendation.recommendation;
|
|
3598
|
+
output.reasons = readyRecommendation.reasons;
|
|
3599
|
+
} catch {
|
|
3600
|
+
output.rpcReachable = false;
|
|
3601
|
+
const readyRecommendation = buildReadyRecommendation({
|
|
3602
|
+
nodeRunning: true,
|
|
3603
|
+
rpcReachable: false,
|
|
3604
|
+
channelsReady: 0,
|
|
3605
|
+
canSend: false,
|
|
3606
|
+
canReceive: false
|
|
3607
|
+
});
|
|
3608
|
+
output.recommendation = readyRecommendation.recommendation;
|
|
3609
|
+
output.reasons = readyRecommendation.reasons;
|
|
3610
|
+
}
|
|
3611
|
+
} else if (pid) {
|
|
3612
|
+
const staleRecommendation = buildStalePidRecommendation();
|
|
3613
|
+
output.recommendation = staleRecommendation.recommendation;
|
|
3614
|
+
output.reasons = staleRecommendation.reasons;
|
|
3615
|
+
removePidFile(config.dataDir);
|
|
3616
|
+
}
|
|
3617
|
+
if (json) {
|
|
3618
|
+
printJsonSuccess(output);
|
|
3619
|
+
} else {
|
|
3620
|
+
console.log("Node Readiness");
|
|
3621
|
+
console.log(` Node Running: ${output.nodeRunning ? "yes" : "no"}`);
|
|
3622
|
+
console.log(` RPC Reachable: ${output.rpcReachable ? "yes" : "no"}`);
|
|
3623
|
+
console.log(` Channels: ${output.channelsReady}/${output.channelsTotal} ready/total`);
|
|
3624
|
+
console.log(` Can Send: ${output.canSend ? "yes" : "no"}`);
|
|
3625
|
+
console.log(` Can Receive: ${output.canReceive ? "yes" : "no"}`);
|
|
3626
|
+
console.log(` Recommendation: ${String(output.recommendation)}`);
|
|
3627
|
+
const reasons = Array.isArray(output.reasons) ? output.reasons : [];
|
|
3628
|
+
if (reasons.length > 0) {
|
|
3629
|
+
console.log(" Reasons:");
|
|
3630
|
+
for (const reason of reasons) {
|
|
3631
|
+
console.log(` - ${String(reason)}`);
|
|
3632
|
+
}
|
|
3633
|
+
}
|
|
3634
|
+
}
|
|
3635
|
+
}
|
|
3636
|
+
|
|
3637
|
+
// src/commands/node.ts
|
|
3638
|
+
function createNodeCommand(config) {
|
|
3639
|
+
const node = new Command8("node").description("Node management");
|
|
3640
|
+
node.command("start").option("--daemon", "Start node in detached background mode (node + runtime)").option("--runtime-proxy-listen <host:port>", "Runtime monitor proxy listen address").option("--event-stream <format>", "Event stream format for --json mode (jsonl)", "jsonl").option("--quiet-fnn", "Do not mirror fnn stdout/stderr to console; keep file persistence").option("--json").action(async (options) => {
|
|
3641
|
+
await runNodeStartCommand(config, options);
|
|
3642
|
+
});
|
|
3643
|
+
node.command("stop").option("--json").action(async (options) => {
|
|
3644
|
+
const json = Boolean(options.json);
|
|
3645
|
+
const runtimeMeta = readRuntimeMeta(config.dataDir);
|
|
3646
|
+
const runtimePid = readRuntimePid(config.dataDir);
|
|
3647
|
+
if (runtimeMeta?.daemon && runtimePid && isProcessRunning(runtimePid)) {
|
|
3648
|
+
stopRuntimeDaemonFromNode({ dataDir: config.dataDir, rpcUrl: config.rpcUrl });
|
|
3649
|
+
}
|
|
3650
|
+
removeRuntimeFiles(config.dataDir);
|
|
3651
|
+
const pid = readPidFile(config.dataDir);
|
|
3652
|
+
if (!pid) {
|
|
3653
|
+
if (json) {
|
|
3654
|
+
printJsonError({
|
|
3655
|
+
code: "NODE_NOT_RUNNING",
|
|
3656
|
+
message: "No PID file found. Node may not be running.",
|
|
3657
|
+
recoverable: true,
|
|
3658
|
+
suggestion: "Run `fiber-pay node start` first if you intend to stop a node."
|
|
3659
|
+
});
|
|
3660
|
+
} else {
|
|
3661
|
+
console.log("\u274C No PID file found. Node may not be running.");
|
|
3662
|
+
}
|
|
3663
|
+
process.exit(1);
|
|
3664
|
+
}
|
|
3665
|
+
if (!isProcessRunning(pid)) {
|
|
3666
|
+
if (json) {
|
|
3667
|
+
printJsonError({
|
|
3668
|
+
code: "NODE_NOT_RUNNING",
|
|
3669
|
+
message: `Process ${pid} is not running. Cleaning up PID file.`,
|
|
3670
|
+
recoverable: true,
|
|
3671
|
+
suggestion: "Start the node again if needed; stale PID has been cleaned.",
|
|
3672
|
+
details: { pid, stalePidFileCleaned: true }
|
|
3673
|
+
});
|
|
3674
|
+
} else {
|
|
3675
|
+
console.log(`\u274C Process ${pid} is not running. Cleaning up PID file.`);
|
|
3676
|
+
}
|
|
3677
|
+
removePidFile(config.dataDir);
|
|
3678
|
+
process.exit(1);
|
|
3679
|
+
}
|
|
3680
|
+
if (!json) {
|
|
3681
|
+
console.log(`\u{1F6D1} Stopping node (PID: ${pid})...`);
|
|
3682
|
+
}
|
|
3683
|
+
process.kill(pid, "SIGTERM");
|
|
3684
|
+
let attempts = 0;
|
|
3685
|
+
while (isProcessRunning(pid) && attempts < 30) {
|
|
3686
|
+
await new Promise((resolve2) => setTimeout(resolve2, 100));
|
|
3687
|
+
attempts++;
|
|
3688
|
+
}
|
|
3689
|
+
if (isProcessRunning(pid)) {
|
|
3690
|
+
process.kill(pid, "SIGKILL");
|
|
3691
|
+
}
|
|
3692
|
+
removePidFile(config.dataDir);
|
|
3693
|
+
if (json) {
|
|
3694
|
+
printJsonSuccess({ pid, stopped: true });
|
|
3695
|
+
} else {
|
|
3696
|
+
console.log("\u2705 Node stopped.");
|
|
3697
|
+
}
|
|
3698
|
+
});
|
|
3699
|
+
node.command("status").option("--json").action(async (options) => {
|
|
3700
|
+
await runNodeStatusCommand(config, options);
|
|
3701
|
+
});
|
|
3702
|
+
node.command("ready").description("Agent-oriented readiness summary for automation").option("--json").action(async (options) => {
|
|
3703
|
+
await runNodeReadyCommand(config, options);
|
|
3704
|
+
});
|
|
3705
|
+
node.command("info").option("--json").action(async (options) => {
|
|
3706
|
+
const rpc = await createReadyRpcClient(config);
|
|
3707
|
+
const nodeInfo = await rpc.nodeInfo();
|
|
3708
|
+
const fundingAddress = scriptToAddress2(nodeInfo.default_funding_lock_script, config.network);
|
|
3709
|
+
const peerId = await nodeIdToPeerId2(nodeInfo.node_id);
|
|
3710
|
+
const output = {
|
|
3711
|
+
nodeId: nodeInfo.node_id,
|
|
3712
|
+
peerId,
|
|
3713
|
+
addresses: nodeInfo.addresses,
|
|
3714
|
+
chainHash: nodeInfo.chain_hash,
|
|
3715
|
+
fundingAddress,
|
|
3716
|
+
fundingLockScript: nodeInfo.default_funding_lock_script,
|
|
3717
|
+
version: nodeInfo.version,
|
|
3718
|
+
channelCount: parseInt(nodeInfo.channel_count, 16),
|
|
3719
|
+
pendingChannelCount: parseInt(nodeInfo.pending_channel_count, 16),
|
|
3720
|
+
peersCount: parseInt(nodeInfo.peers_count, 16)
|
|
3721
|
+
};
|
|
3722
|
+
if (options.json) {
|
|
3723
|
+
printJsonSuccess(output);
|
|
3724
|
+
} else {
|
|
3725
|
+
printNodeInfoHuman(output);
|
|
3726
|
+
}
|
|
3727
|
+
});
|
|
3728
|
+
return node;
|
|
3729
|
+
}
|
|
3730
|
+
|
|
3731
|
+
// src/commands/payment.ts
|
|
3732
|
+
import { ckbToShannons as ckbToShannons3, shannonsToCkb as shannonsToCkb4 } from "@fiber-pay/sdk";
|
|
3733
|
+
import { Command as Command9 } from "commander";
|
|
3734
|
+
function createPaymentCommand(config) {
|
|
3735
|
+
const payment = new Command9("payment").description("Payment lifecycle and status commands");
|
|
3736
|
+
payment.command("send").argument("[invoice]").option("--invoice <invoice>").option("--to <nodeId>").option("--amount <ckb>").option("--max-fee <ckb>").option("--wait", "Wait for runtime job terminal status when runtime proxy is active").option("--timeout <seconds>", "Wait timeout for --wait mode", "120").option("--json").action(async (invoiceArg, options) => {
|
|
3737
|
+
const rpc = await createReadyRpcClient(config);
|
|
3738
|
+
const json = Boolean(options.json);
|
|
3739
|
+
const invoice = options.invoice || invoiceArg;
|
|
3740
|
+
const recipientNodeId = options.to;
|
|
3741
|
+
const amountCkb = options.amount ? parseFloat(options.amount) : void 0;
|
|
3742
|
+
const maxFeeCkb = options.maxFee ? parseFloat(options.maxFee) : void 0;
|
|
3743
|
+
const shouldWait = Boolean(options.wait);
|
|
3744
|
+
const timeoutSeconds = parseInt(String(options.timeout ?? "120"), 10);
|
|
3745
|
+
if (!invoice && !recipientNodeId) {
|
|
3746
|
+
if (json) {
|
|
3747
|
+
printJsonError({
|
|
3748
|
+
code: "PAYMENT_SEND_INPUT_INVALID",
|
|
3749
|
+
message: "Either invoice or --to <nodeId> required",
|
|
3750
|
+
recoverable: true,
|
|
3751
|
+
suggestion: "Provide a valid invoice, or provide both `--to` and `--amount`."
|
|
3752
|
+
});
|
|
3753
|
+
} else {
|
|
3754
|
+
console.error("Error: Either invoice or --to <nodeId> required");
|
|
3755
|
+
}
|
|
3756
|
+
process.exit(1);
|
|
3757
|
+
}
|
|
3758
|
+
if (recipientNodeId && !amountCkb) {
|
|
3759
|
+
if (json) {
|
|
3760
|
+
printJsonError({
|
|
3761
|
+
code: "PAYMENT_SEND_INPUT_INVALID",
|
|
3762
|
+
message: "--amount required when using --to",
|
|
3763
|
+
recoverable: true,
|
|
3764
|
+
suggestion: "Add `--amount <ckb>` when using keysend mode (`--to`)."
|
|
3765
|
+
});
|
|
3766
|
+
} else {
|
|
3767
|
+
console.error("Error: --amount required when using --to");
|
|
3768
|
+
}
|
|
3769
|
+
process.exit(1);
|
|
3770
|
+
}
|
|
3771
|
+
const paymentParams = {
|
|
3772
|
+
invoice,
|
|
3773
|
+
target_pubkey: recipientNodeId,
|
|
3774
|
+
amount: amountCkb ? ckbToShannons3(amountCkb) : void 0,
|
|
3775
|
+
keysend: recipientNodeId ? true : void 0,
|
|
3776
|
+
max_fee_amount: maxFeeCkb ? ckbToShannons3(maxFeeCkb) : void 0
|
|
3777
|
+
};
|
|
3778
|
+
const endpoint = resolveRpcEndpoint(config);
|
|
3779
|
+
if (endpoint.target === "runtime-proxy") {
|
|
3780
|
+
const created = await tryCreateRuntimePaymentJob(endpoint.url, {
|
|
3781
|
+
params: {
|
|
3782
|
+
invoice,
|
|
3783
|
+
sendPaymentParams: paymentParams
|
|
3784
|
+
},
|
|
3785
|
+
options: {
|
|
3786
|
+
idempotencyKey: invoice ? `payment:invoice:${invoice}` : void 0
|
|
3787
|
+
}
|
|
3788
|
+
});
|
|
3789
|
+
if (created) {
|
|
3790
|
+
const job = shouldWait ? await waitForRuntimeJobTerminal(endpoint.url, created.id, timeoutSeconds) : created;
|
|
3791
|
+
const payload2 = {
|
|
3792
|
+
paymentHash: getJobPaymentHash(job) ?? "unknown",
|
|
3793
|
+
status: job.state === "succeeded" ? "success" : job.state === "failed" || job.state === "cancelled" ? "failed" : "pending",
|
|
3794
|
+
feeCkb: getJobFeeCkb(job),
|
|
3795
|
+
failureReason: getJobFailure(job),
|
|
3796
|
+
jobId: job.id,
|
|
3797
|
+
jobState: job.state
|
|
3798
|
+
};
|
|
3799
|
+
if (json) {
|
|
3800
|
+
printJsonSuccess(payload2);
|
|
3801
|
+
} else {
|
|
3802
|
+
console.log("Payment job submitted");
|
|
3803
|
+
console.log(` Job: ${payload2.jobId}`);
|
|
3804
|
+
console.log(` Hash: ${payload2.paymentHash}`);
|
|
3805
|
+
console.log(` Status: ${payload2.status} (${payload2.jobState})`);
|
|
3806
|
+
console.log(` Fee: ${payload2.feeCkb} CKB`);
|
|
3807
|
+
if (payload2.failureReason) {
|
|
3808
|
+
console.log(` Error: ${payload2.failureReason}`);
|
|
3809
|
+
}
|
|
3810
|
+
}
|
|
3811
|
+
return;
|
|
3812
|
+
}
|
|
3813
|
+
}
|
|
3814
|
+
const result = await rpc.sendPayment(paymentParams);
|
|
3815
|
+
const payload = {
|
|
3816
|
+
paymentHash: result.payment_hash,
|
|
3817
|
+
status: result.status === "Success" ? "success" : result.status === "Failed" ? "failed" : "pending",
|
|
3818
|
+
feeCkb: shannonsToCkb4(result.fee),
|
|
3819
|
+
failureReason: result.failed_error
|
|
3820
|
+
};
|
|
3821
|
+
if (json) {
|
|
3822
|
+
printJsonSuccess(payload);
|
|
3823
|
+
} else {
|
|
3824
|
+
console.log("Payment sent");
|
|
3825
|
+
console.log(` Hash: ${payload.paymentHash}`);
|
|
3826
|
+
console.log(` Status: ${payload.status}`);
|
|
3827
|
+
console.log(` Fee: ${payload.feeCkb} CKB`);
|
|
3828
|
+
if (payload.failureReason) {
|
|
3829
|
+
console.log(` Error: ${payload.failureReason}`);
|
|
3830
|
+
}
|
|
3831
|
+
}
|
|
3832
|
+
});
|
|
3833
|
+
payment.command("get").argument("<paymentHash>").option("--json").action(async (paymentHash, options) => {
|
|
3834
|
+
const rpc = await createReadyRpcClient(config);
|
|
3835
|
+
const result = await rpc.getPayment({ payment_hash: paymentHash });
|
|
3836
|
+
if (options.json) {
|
|
3837
|
+
printJsonSuccess(formatPaymentResult(result));
|
|
3838
|
+
} else {
|
|
3839
|
+
printPaymentDetailHuman(result);
|
|
3840
|
+
}
|
|
3841
|
+
});
|
|
3842
|
+
payment.command("watch").argument("<paymentHash>").option("--interval <seconds>", "Polling interval", "2").option("--timeout <seconds>", "Timeout", "120").option("--until <target>", "SUCCESS | FAILED | TERMINAL", "TERMINAL").option("--on-timeout <behavior>", "fail | success", "fail").option("--json").action(async (paymentHash, options) => {
|
|
3843
|
+
const json = Boolean(options.json);
|
|
3844
|
+
const intervalSeconds = parseInt(options.interval, 10);
|
|
3845
|
+
const timeoutSeconds = parseInt(options.timeout, 10);
|
|
3846
|
+
const until = String(options.until ?? "TERMINAL").trim().toUpperCase();
|
|
3847
|
+
const onTimeout = String(options.onTimeout ?? "fail").trim().toLowerCase();
|
|
3848
|
+
if (!["SUCCESS", "FAILED", "TERMINAL"].includes(until)) {
|
|
3849
|
+
if (json) {
|
|
3850
|
+
printJsonError({
|
|
3851
|
+
code: "PAYMENT_WATCH_INPUT_INVALID",
|
|
3852
|
+
message: `Invalid --until value: ${options.until}. Expected SUCCESS, FAILED, or TERMINAL`,
|
|
3853
|
+
recoverable: true,
|
|
3854
|
+
suggestion: "Use one of: SUCCESS, FAILED, TERMINAL.",
|
|
3855
|
+
details: { provided: options.until, expected: ["SUCCESS", "FAILED", "TERMINAL"] }
|
|
3856
|
+
});
|
|
3857
|
+
} else {
|
|
3858
|
+
console.error(
|
|
3859
|
+
`Error: Invalid --until value: ${options.until}. Expected SUCCESS, FAILED, or TERMINAL`
|
|
3860
|
+
);
|
|
3861
|
+
}
|
|
3862
|
+
process.exit(1);
|
|
3863
|
+
}
|
|
3864
|
+
if (!["fail", "success"].includes(onTimeout)) {
|
|
3865
|
+
if (json) {
|
|
3866
|
+
printJsonError({
|
|
3867
|
+
code: "PAYMENT_WATCH_INPUT_INVALID",
|
|
3868
|
+
message: `Invalid --on-timeout value: ${options.onTimeout}. Expected fail or success`,
|
|
3869
|
+
recoverable: true,
|
|
3870
|
+
suggestion: "Use `--on-timeout fail` or `--on-timeout success`.",
|
|
3871
|
+
details: { provided: options.onTimeout, expected: ["fail", "success"] }
|
|
3872
|
+
});
|
|
3873
|
+
} else {
|
|
3874
|
+
console.error(
|
|
3875
|
+
`Error: Invalid --on-timeout value: ${options.onTimeout}. Expected fail or success`
|
|
3876
|
+
);
|
|
3877
|
+
}
|
|
3878
|
+
process.exit(1);
|
|
3879
|
+
}
|
|
3880
|
+
const rpc = await createReadyRpcClient(config);
|
|
3881
|
+
const startedAt = Date.now();
|
|
3882
|
+
let lastStatus;
|
|
3883
|
+
while (Date.now() - startedAt < timeoutSeconds * 1e3) {
|
|
3884
|
+
const paymentResult = await rpc.getPayment({ payment_hash: paymentHash });
|
|
3885
|
+
if (paymentResult.status !== lastStatus) {
|
|
3886
|
+
if (json) {
|
|
3887
|
+
printJsonEvent("status_transition", {
|
|
3888
|
+
statusTransition: {
|
|
3889
|
+
from: lastStatus ?? null,
|
|
3890
|
+
to: paymentResult.status
|
|
3891
|
+
},
|
|
3892
|
+
payment: formatPaymentResult(paymentResult)
|
|
3893
|
+
});
|
|
3894
|
+
} else {
|
|
3895
|
+
console.log(`Status: ${lastStatus ?? "(initial)"} -> ${paymentResult.status}`);
|
|
3896
|
+
printPaymentDetailHuman(paymentResult);
|
|
3897
|
+
console.log("");
|
|
3898
|
+
}
|
|
3899
|
+
lastStatus = paymentResult.status;
|
|
3900
|
+
}
|
|
3901
|
+
const isSuccess = paymentResult.status === "Success";
|
|
3902
|
+
const isFailed = paymentResult.status === "Failed";
|
|
3903
|
+
const terminalReached = until === "TERMINAL" ? isSuccess || isFailed : until === "SUCCESS" ? isSuccess : isFailed;
|
|
3904
|
+
if (terminalReached) {
|
|
3905
|
+
if (json) {
|
|
3906
|
+
printJsonEvent("terminal", {
|
|
3907
|
+
paymentHash,
|
|
3908
|
+
terminalStatus: paymentResult.status,
|
|
3909
|
+
until
|
|
3910
|
+
});
|
|
3911
|
+
}
|
|
3912
|
+
return;
|
|
3913
|
+
}
|
|
3914
|
+
if ((isSuccess || isFailed) && until !== "TERMINAL") {
|
|
3915
|
+
if (json) {
|
|
3916
|
+
printJsonError({
|
|
3917
|
+
code: "PAYMENT_WATCH_UNEXPECTED_TERMINAL",
|
|
3918
|
+
message: `Payment reached ${paymentResult.status} before requested --until ${until}`,
|
|
3919
|
+
recoverable: true,
|
|
3920
|
+
suggestion: "Set `--until TERMINAL` or handle mismatched terminal state in caller.",
|
|
3921
|
+
details: { terminalStatus: paymentResult.status, until }
|
|
3922
|
+
});
|
|
3923
|
+
} else {
|
|
3924
|
+
console.error(
|
|
3925
|
+
`Error: Payment reached ${paymentResult.status} before requested --until ${until}`
|
|
3926
|
+
);
|
|
3927
|
+
}
|
|
3928
|
+
process.exit(1);
|
|
3929
|
+
}
|
|
3930
|
+
await sleep(intervalSeconds * 1e3);
|
|
3931
|
+
}
|
|
3932
|
+
if (onTimeout === "success") {
|
|
3933
|
+
if (json) {
|
|
3934
|
+
printJsonEvent("terminal", {
|
|
3935
|
+
paymentHash,
|
|
3936
|
+
terminalStatus: "Timeout",
|
|
3937
|
+
until,
|
|
3938
|
+
timeoutSeconds
|
|
3939
|
+
});
|
|
3940
|
+
} else {
|
|
3941
|
+
console.log(
|
|
3942
|
+
`Timeout reached (${timeoutSeconds}s) and treated as success by --on-timeout=success.`
|
|
3943
|
+
);
|
|
3944
|
+
}
|
|
3945
|
+
return;
|
|
3946
|
+
}
|
|
3947
|
+
if (json) {
|
|
3948
|
+
printJsonError({
|
|
3949
|
+
code: "PAYMENT_WATCH_TIMEOUT",
|
|
3950
|
+
message: `Payment ${paymentHash} did not reach terminal state within ${timeoutSeconds}s`,
|
|
3951
|
+
recoverable: true,
|
|
3952
|
+
suggestion: "Increase timeout, or continue polling using `payment get --json`.",
|
|
3953
|
+
details: { paymentHash, timeoutSeconds }
|
|
3954
|
+
});
|
|
3955
|
+
} else {
|
|
3956
|
+
console.error(
|
|
3957
|
+
`Error: Payment ${paymentHash} did not reach terminal state within ${timeoutSeconds}s`
|
|
3958
|
+
);
|
|
3959
|
+
}
|
|
3960
|
+
process.exit(1);
|
|
3961
|
+
});
|
|
3962
|
+
payment.command("route").description("Build a payment route through specified hops").requiredOption("--hops <pubkeys>", "Comma-separated list of node pubkeys forming the route").option("--amount <ckb>", "Amount in CKB to route").option("--json").action(async (options) => {
|
|
3963
|
+
const rpc = await createReadyRpcClient(config);
|
|
3964
|
+
const json = Boolean(options.json);
|
|
3965
|
+
const pubkeys = options.hops.split(",").map((s) => s.trim());
|
|
3966
|
+
if (pubkeys.length === 0 || pubkeys.some((pk) => !pk)) {
|
|
3967
|
+
const msg = "--hops must be a non-empty comma-separated list of pubkeys";
|
|
3968
|
+
if (json) {
|
|
3969
|
+
printJsonError({
|
|
3970
|
+
code: "PAYMENT_ROUTE_INPUT_INVALID",
|
|
3971
|
+
message: msg,
|
|
3972
|
+
recoverable: true,
|
|
3973
|
+
suggestion: "Provide pubkeys: --hops 0xabc...,0xdef..."
|
|
3974
|
+
});
|
|
3975
|
+
} else {
|
|
3976
|
+
console.error(`Error: ${msg}`);
|
|
3977
|
+
}
|
|
3978
|
+
process.exit(1);
|
|
3979
|
+
}
|
|
3980
|
+
const hopsInfo = pubkeys.map((pubkey) => ({ pubkey }));
|
|
3981
|
+
const amount = options.amount ? ckbToShannons3(parseFloat(options.amount)) : void 0;
|
|
3982
|
+
const result = await rpc.buildRouter({
|
|
3983
|
+
hops_info: hopsInfo,
|
|
3984
|
+
amount
|
|
3985
|
+
});
|
|
3986
|
+
if (json) {
|
|
3987
|
+
printJsonSuccess({ routerHops: result.router_hops });
|
|
3988
|
+
} else {
|
|
3989
|
+
console.log(`Route built: ${result.router_hops.length} hop(s)`);
|
|
3990
|
+
for (let i = 0; i < result.router_hops.length; i++) {
|
|
3991
|
+
const hop = result.router_hops[i];
|
|
3992
|
+
console.log(` #${i + 1}`);
|
|
3993
|
+
console.log(` Target: ${hop.target}`);
|
|
3994
|
+
console.log(
|
|
3995
|
+
` Outpoint: ${hop.channel_outpoint.tx_hash}:${hop.channel_outpoint.index}`
|
|
3996
|
+
);
|
|
3997
|
+
console.log(` Amount: ${shannonsToCkb4(hop.amount_received)} CKB`);
|
|
3998
|
+
console.log(` Expiry: ${hop.incoming_tlc_expiry}`);
|
|
3999
|
+
}
|
|
4000
|
+
}
|
|
4001
|
+
});
|
|
4002
|
+
payment.command("send-route").description("Send a payment using a pre-built route from `payment route`").requiredOption(
|
|
4003
|
+
"--router <json>",
|
|
4004
|
+
"JSON array of router hops (output of `payment route --json`)"
|
|
4005
|
+
).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) => {
|
|
4006
|
+
const rpc = await createReadyRpcClient(config);
|
|
4007
|
+
const json = Boolean(options.json);
|
|
4008
|
+
let router;
|
|
4009
|
+
try {
|
|
4010
|
+
router = JSON.parse(options.router);
|
|
4011
|
+
} catch {
|
|
4012
|
+
const msg = "--router must be a valid JSON array of router hops";
|
|
4013
|
+
if (json) {
|
|
4014
|
+
printJsonError({
|
|
4015
|
+
code: "PAYMENT_SEND_ROUTE_INPUT_INVALID",
|
|
4016
|
+
message: msg,
|
|
4017
|
+
recoverable: true,
|
|
4018
|
+
suggestion: "Pipe --json output of `payment route` into this flag."
|
|
4019
|
+
});
|
|
4020
|
+
} else {
|
|
4021
|
+
console.error(`Error: ${msg}`);
|
|
4022
|
+
}
|
|
4023
|
+
process.exit(1);
|
|
4024
|
+
}
|
|
4025
|
+
const result = await rpc.sendPaymentWithRouter({
|
|
4026
|
+
router,
|
|
4027
|
+
invoice: options.invoice,
|
|
4028
|
+
payment_hash: options.paymentHash,
|
|
4029
|
+
keysend: options.keysend ? true : void 0,
|
|
4030
|
+
dry_run: options.dryRun ? true : void 0
|
|
4031
|
+
});
|
|
4032
|
+
const payload = {
|
|
4033
|
+
paymentHash: result.payment_hash,
|
|
4034
|
+
status: result.status === "Success" ? "success" : result.status === "Failed" ? "failed" : "pending",
|
|
4035
|
+
feeCkb: shannonsToCkb4(result.fee),
|
|
4036
|
+
failureReason: result.failed_error,
|
|
4037
|
+
dryRun: Boolean(options.dryRun)
|
|
4038
|
+
};
|
|
4039
|
+
if (json) {
|
|
4040
|
+
printJsonSuccess(payload);
|
|
4041
|
+
} else {
|
|
4042
|
+
console.log(options.dryRun ? "Payment dry-run complete" : "Payment sent via route");
|
|
4043
|
+
console.log(` Hash: ${payload.paymentHash}`);
|
|
4044
|
+
console.log(` Status: ${payload.status}`);
|
|
4045
|
+
console.log(` Fee: ${payload.feeCkb} CKB`);
|
|
4046
|
+
if (payload.failureReason) {
|
|
4047
|
+
console.log(` Error: ${payload.failureReason}`);
|
|
4048
|
+
}
|
|
4049
|
+
}
|
|
4050
|
+
});
|
|
4051
|
+
return payment;
|
|
4052
|
+
}
|
|
4053
|
+
function getJobPaymentHash(job) {
|
|
4054
|
+
const result = job.result;
|
|
4055
|
+
return result?.paymentHash;
|
|
4056
|
+
}
|
|
4057
|
+
function getJobFeeCkb(job) {
|
|
4058
|
+
const result = job.result;
|
|
4059
|
+
return result?.fee ? shannonsToCkb4(result.fee) : 0;
|
|
4060
|
+
}
|
|
4061
|
+
function getJobFailure(job) {
|
|
4062
|
+
const result = job.result;
|
|
4063
|
+
return result?.failedError ?? job.error?.message;
|
|
4064
|
+
}
|
|
4065
|
+
|
|
4066
|
+
// src/commands/peer.ts
|
|
4067
|
+
import { Command as Command10 } from "commander";
|
|
4068
|
+
function extractPeerIdFromMultiaddr(address) {
|
|
4069
|
+
const match = address.match(/\/p2p\/([^/]+)$/);
|
|
4070
|
+
return match?.[1];
|
|
4071
|
+
}
|
|
4072
|
+
async function waitForPeerConnected(rpc, peerId, timeoutMs) {
|
|
4073
|
+
const start = Date.now();
|
|
4074
|
+
while (Date.now() - start < timeoutMs) {
|
|
4075
|
+
const peers = await rpc.listPeers();
|
|
4076
|
+
if (peers.peers.some((peer) => peer.peer_id === peerId)) {
|
|
4077
|
+
return true;
|
|
4078
|
+
}
|
|
4079
|
+
await new Promise((resolve2) => setTimeout(resolve2, 500));
|
|
4080
|
+
}
|
|
4081
|
+
return false;
|
|
4082
|
+
}
|
|
4083
|
+
function createPeerCommand(config) {
|
|
4084
|
+
const peer = new Command10("peer").description("Peer management");
|
|
4085
|
+
peer.command("list").option("--json").action(async (options) => {
|
|
4086
|
+
const rpc = await createReadyRpcClient(config);
|
|
4087
|
+
const peers = await rpc.listPeers();
|
|
4088
|
+
if (options.json) {
|
|
4089
|
+
printJsonSuccess(peers);
|
|
4090
|
+
} else {
|
|
4091
|
+
printPeerListHuman(peers.peers);
|
|
4092
|
+
}
|
|
4093
|
+
});
|
|
4094
|
+
peer.command("connect").argument("<multiaddr>").option("--timeout <sec>", "Wait timeout for peer to appear in peer list", "8").option("--json").action(async (address, options) => {
|
|
4095
|
+
const rpc = await createReadyRpcClient(config);
|
|
4096
|
+
const peerId = extractPeerIdFromMultiaddr(address);
|
|
4097
|
+
if (!peerId) {
|
|
4098
|
+
throw new Error("Invalid multiaddr: missing /p2p/<peerId> suffix");
|
|
4099
|
+
}
|
|
4100
|
+
await rpc.connectPeer({ address });
|
|
4101
|
+
const timeoutMs = Math.max(1, Number.parseInt(String(options.timeout), 10) || 8) * 1e3;
|
|
4102
|
+
const connected = await waitForPeerConnected(rpc, peerId, timeoutMs);
|
|
4103
|
+
if (!connected) {
|
|
4104
|
+
throw new Error(
|
|
4105
|
+
`connect_peer accepted but peer not found in list within ${Math.floor(timeoutMs / 1e3)}s (${peerId})`
|
|
4106
|
+
);
|
|
4107
|
+
}
|
|
4108
|
+
if (options.json) {
|
|
4109
|
+
printJsonSuccess({ address, peerId, message: "Connected" });
|
|
4110
|
+
} else {
|
|
4111
|
+
console.log("\u2705 Connected to peer");
|
|
4112
|
+
console.log(` Address: ${address}`);
|
|
4113
|
+
console.log(` Peer ID: ${peerId}`);
|
|
4114
|
+
}
|
|
4115
|
+
});
|
|
4116
|
+
peer.command("disconnect").argument("<peerId>").option("--json").action(async (peerId, options) => {
|
|
4117
|
+
const rpc = await createReadyRpcClient(config);
|
|
4118
|
+
await rpc.disconnectPeer({ peer_id: peerId });
|
|
4119
|
+
if (options.json) {
|
|
4120
|
+
printJsonSuccess({ peerId, message: "Disconnected" });
|
|
4121
|
+
} else {
|
|
4122
|
+
console.log("\u2705 Disconnected peer");
|
|
4123
|
+
console.log(` Peer ID: ${peerId}`);
|
|
4124
|
+
}
|
|
4125
|
+
});
|
|
4126
|
+
return peer;
|
|
4127
|
+
}
|
|
4128
|
+
|
|
4129
|
+
// src/commands/runtime.ts
|
|
4130
|
+
import { spawn as spawn2 } from "child_process";
|
|
4131
|
+
import { resolve } from "path";
|
|
4132
|
+
import {
|
|
4133
|
+
alertPriorityOrder,
|
|
4134
|
+
formatRuntimeAlert as formatRuntimeAlert2,
|
|
4135
|
+
isAlertPriority,
|
|
4136
|
+
isAlertType,
|
|
4137
|
+
startRuntimeService as startRuntimeService2
|
|
4138
|
+
} from "@fiber-pay/runtime";
|
|
4139
|
+
import { Command as Command11 } from "commander";
|
|
4140
|
+
|
|
4141
|
+
// src/lib/parse-options.ts
|
|
4142
|
+
function parseIntegerOption(value, name) {
|
|
4143
|
+
if (value === void 0) {
|
|
4144
|
+
return void 0;
|
|
4145
|
+
}
|
|
4146
|
+
const parsed = Number.parseInt(value, 10);
|
|
4147
|
+
if (!Number.isInteger(parsed) || parsed <= 0) {
|
|
4148
|
+
throw new Error(`Invalid ${name}: ${value}. Expected positive integer.`);
|
|
4149
|
+
}
|
|
4150
|
+
return parsed;
|
|
4151
|
+
}
|
|
4152
|
+
function parseBoolOption(value, name) {
|
|
4153
|
+
if (value === void 0) {
|
|
4154
|
+
return void 0;
|
|
4155
|
+
}
|
|
4156
|
+
const normalized = value.toLowerCase();
|
|
4157
|
+
if (normalized === "true") return true;
|
|
4158
|
+
if (normalized === "false") return false;
|
|
4159
|
+
throw new Error(`Invalid ${name}: ${value}. Expected true|false.`);
|
|
4160
|
+
}
|
|
4161
|
+
|
|
4162
|
+
// src/commands/runtime.ts
|
|
4163
|
+
function formatRuntimeAlertLine(alert) {
|
|
4164
|
+
return formatRuntimeAlert2(alert);
|
|
4165
|
+
}
|
|
4166
|
+
function parseAlertPriorityOption(value) {
|
|
4167
|
+
if (!value) {
|
|
4168
|
+
return void 0;
|
|
4169
|
+
}
|
|
4170
|
+
const normalized = value.trim().toLowerCase();
|
|
4171
|
+
if (!isAlertPriority(normalized)) {
|
|
4172
|
+
throw new Error(`Invalid log-min-priority: ${value}. Expected critical|high|medium|low.`);
|
|
4173
|
+
}
|
|
4174
|
+
return normalized;
|
|
4175
|
+
}
|
|
4176
|
+
function parseAlertTypesOption(value) {
|
|
4177
|
+
if (!value) {
|
|
4178
|
+
return void 0;
|
|
4179
|
+
}
|
|
4180
|
+
const tokens = value.split(",").map((token) => token.trim()).filter((token) => token.length > 0);
|
|
4181
|
+
if (tokens.length === 0) {
|
|
4182
|
+
return void 0;
|
|
4183
|
+
}
|
|
4184
|
+
const result = /* @__PURE__ */ new Set();
|
|
4185
|
+
for (const token of tokens) {
|
|
4186
|
+
if (!isAlertType(token)) {
|
|
4187
|
+
throw new Error(`Invalid log-type: ${token}. Use runtime alert type names, comma-separated.`);
|
|
4188
|
+
}
|
|
4189
|
+
result.add(token);
|
|
4190
|
+
}
|
|
4191
|
+
return result;
|
|
4192
|
+
}
|
|
4193
|
+
function shouldPrintAlert(alert, filter) {
|
|
4194
|
+
if (filter.minPriority) {
|
|
4195
|
+
const minimumRank = alertPriorityOrder[filter.minPriority];
|
|
4196
|
+
if (alertPriorityOrder[alert.priority] < minimumRank) {
|
|
4197
|
+
return false;
|
|
4198
|
+
}
|
|
4199
|
+
}
|
|
4200
|
+
if (filter.types && filter.types.size > 0 && !filter.types.has(alert.type)) {
|
|
4201
|
+
return false;
|
|
4202
|
+
}
|
|
4203
|
+
return true;
|
|
4204
|
+
}
|
|
4205
|
+
function createRuntimeCommand(config) {
|
|
4206
|
+
const runtime = new Command11("runtime").description("Polling monitor and alert runtime service");
|
|
4207
|
+
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(
|
|
4208
|
+
"--log-min-priority <priority>",
|
|
4209
|
+
"Minimum runtime log priority (critical|high|medium|low)"
|
|
4210
|
+
).option("--log-type <types>", "Comma-separated runtime alert types to print").option("--json").action(async (options) => {
|
|
4211
|
+
const asJson = Boolean(options.json);
|
|
4212
|
+
const daemon = Boolean(options.daemon);
|
|
4213
|
+
const isRuntimeChild = process.env.FIBER_RUNTIME_CHILD === "1";
|
|
4214
|
+
try {
|
|
4215
|
+
const existingPid = readRuntimePid(config.dataDir);
|
|
4216
|
+
if (existingPid && isProcessRunning(existingPid) && (!isRuntimeChild || existingPid !== process.pid)) {
|
|
4217
|
+
const message = `Runtime already running (PID: ${existingPid})`;
|
|
4218
|
+
if (asJson) {
|
|
4219
|
+
printJsonError({
|
|
4220
|
+
code: "RUNTIME_ALREADY_RUNNING",
|
|
4221
|
+
message,
|
|
4222
|
+
recoverable: true,
|
|
4223
|
+
suggestion: "Run `fiber-pay runtime status` or `fiber-pay runtime stop` first."
|
|
4224
|
+
});
|
|
4225
|
+
} else {
|
|
4226
|
+
console.error(`Error: ${message}`);
|
|
4227
|
+
}
|
|
4228
|
+
process.exit(1);
|
|
4229
|
+
}
|
|
4230
|
+
if (existingPid && !isProcessRunning(existingPid)) {
|
|
4231
|
+
removeRuntimeFiles(config.dataDir);
|
|
4232
|
+
}
|
|
4233
|
+
if (daemon && !isRuntimeChild) {
|
|
4234
|
+
const childArgv = process.argv.filter((arg) => arg !== "--daemon");
|
|
4235
|
+
const child = spawn2(process.execPath, childArgv.slice(1), {
|
|
4236
|
+
detached: true,
|
|
4237
|
+
stdio: "ignore",
|
|
4238
|
+
cwd: process.cwd(),
|
|
4239
|
+
env: {
|
|
4240
|
+
...process.env,
|
|
4241
|
+
FIBER_RUNTIME_CHILD: "1"
|
|
4242
|
+
}
|
|
4243
|
+
});
|
|
4244
|
+
child.unref();
|
|
4245
|
+
const childPid = child.pid;
|
|
4246
|
+
if (!childPid) {
|
|
4247
|
+
throw new Error("Failed to spawn runtime daemon process");
|
|
4248
|
+
}
|
|
4249
|
+
writeRuntimePid(config.dataDir, childPid);
|
|
4250
|
+
if (asJson) {
|
|
4251
|
+
printJsonSuccess({
|
|
4252
|
+
daemon: true,
|
|
4253
|
+
pid: childPid,
|
|
4254
|
+
message: "Runtime daemon starting"
|
|
4255
|
+
});
|
|
4256
|
+
} else {
|
|
4257
|
+
console.log(`Runtime daemon starting (PID: ${childPid})`);
|
|
4258
|
+
}
|
|
4259
|
+
return;
|
|
4260
|
+
}
|
|
4261
|
+
const runtimeConfig = {
|
|
4262
|
+
fiberRpcUrl: String(options.fiberRpcUrl ?? config.rpcUrl),
|
|
4263
|
+
channelPollIntervalMs: parseIntegerOption(options.channelPollMs, "channel-poll-ms"),
|
|
4264
|
+
invoicePollIntervalMs: parseIntegerOption(options.invoicePollMs, "invoice-poll-ms"),
|
|
4265
|
+
paymentPollIntervalMs: parseIntegerOption(options.paymentPollMs, "payment-poll-ms"),
|
|
4266
|
+
peerPollIntervalMs: parseIntegerOption(options.peerPollMs, "peer-poll-ms"),
|
|
4267
|
+
healthPollIntervalMs: parseIntegerOption(options.healthPollMs, "health-poll-ms"),
|
|
4268
|
+
includeClosedChannels: parseBoolOption(options.includeClosed, "include-closed"),
|
|
4269
|
+
completedItemTtlSeconds: parseIntegerOption(
|
|
4270
|
+
options.completedTtlSeconds,
|
|
4271
|
+
"completed-ttl-seconds"
|
|
4272
|
+
),
|
|
4273
|
+
proxy: {
|
|
4274
|
+
enabled: true,
|
|
4275
|
+
listen: String(options.proxyListen ?? config.runtimeProxyListen ?? "127.0.0.1:8229")
|
|
4276
|
+
},
|
|
4277
|
+
storage: {
|
|
4278
|
+
stateFilePath: options.stateFile ? resolve(String(options.stateFile)) : resolve(config.dataDir, "runtime-state.json"),
|
|
4279
|
+
flushIntervalMs: parseIntegerOption(options.flushMs, "flush-ms")
|
|
4280
|
+
},
|
|
4281
|
+
jobs: {
|
|
4282
|
+
enabled: true,
|
|
4283
|
+
dbPath: resolve(config.dataDir, "runtime-jobs.db")
|
|
4284
|
+
}
|
|
4285
|
+
};
|
|
4286
|
+
const alertLogFile = options.alertLogFile ? resolve(String(options.alertLogFile)) : resolve(config.dataDir, "logs", "runtime.alerts.jsonl");
|
|
4287
|
+
const alerts = [{ type: "stdout" }];
|
|
4288
|
+
alerts.push({ type: "file", path: alertLogFile });
|
|
4289
|
+
if (options.webhook) {
|
|
4290
|
+
alerts.push({ type: "webhook", url: String(options.webhook) });
|
|
4291
|
+
}
|
|
4292
|
+
if (options.websocket) {
|
|
4293
|
+
alerts.push({ type: "websocket", listen: String(options.websocket) });
|
|
4294
|
+
}
|
|
4295
|
+
runtimeConfig.alerts = alerts;
|
|
4296
|
+
const logFilter = {
|
|
4297
|
+
minPriority: parseAlertPriorityOption(options.logMinPriority),
|
|
4298
|
+
types: parseAlertTypesOption(options.logType)
|
|
4299
|
+
};
|
|
4300
|
+
if (asJson) {
|
|
4301
|
+
printJsonEvent("runtime_starting", {
|
|
4302
|
+
fiberRpcUrl: runtimeConfig.fiberRpcUrl,
|
|
4303
|
+
proxyListen: runtimeConfig.proxy?.listen
|
|
4304
|
+
});
|
|
4305
|
+
} else {
|
|
4306
|
+
console.log("Starting fiber runtime monitor...");
|
|
4307
|
+
}
|
|
4308
|
+
const runtime2 = await startRuntimeService2(runtimeConfig);
|
|
4309
|
+
const status = runtime2.service.getStatus();
|
|
4310
|
+
writeRuntimePid(config.dataDir, process.pid);
|
|
4311
|
+
writeRuntimeMeta(config.dataDir, {
|
|
4312
|
+
pid: process.pid,
|
|
4313
|
+
startedAt: status.startedAt,
|
|
4314
|
+
fiberRpcUrl: status.targetUrl,
|
|
4315
|
+
proxyListen: status.proxyListen,
|
|
4316
|
+
stateFilePath: runtimeConfig.storage?.stateFilePath,
|
|
4317
|
+
alertLogFilePath: alertLogFile,
|
|
4318
|
+
daemon: daemon || isRuntimeChild
|
|
4319
|
+
});
|
|
4320
|
+
runtime2.service.on("alert", (alert) => {
|
|
4321
|
+
if (!shouldPrintAlert(alert, logFilter)) {
|
|
4322
|
+
return;
|
|
4323
|
+
}
|
|
4324
|
+
if (asJson) {
|
|
4325
|
+
printJsonEvent("runtime_alert", alert);
|
|
4326
|
+
return;
|
|
4327
|
+
}
|
|
4328
|
+
console.log(formatRuntimeAlertLine(alert));
|
|
4329
|
+
});
|
|
4330
|
+
if (asJson) {
|
|
4331
|
+
printJsonSuccess({
|
|
4332
|
+
status: "running",
|
|
4333
|
+
fiberRpcUrl: status.targetUrl,
|
|
4334
|
+
proxyListen: status.proxyListen,
|
|
4335
|
+
stateFilePath: runtimeConfig.storage?.stateFilePath,
|
|
4336
|
+
alertLogFile
|
|
4337
|
+
});
|
|
4338
|
+
printJsonEvent("runtime_started", status);
|
|
4339
|
+
} else {
|
|
4340
|
+
console.log(`Fiber RPC: ${status.targetUrl}`);
|
|
4341
|
+
console.log(`Proxy listen: ${status.proxyListen}`);
|
|
4342
|
+
console.log(`State file: ${runtimeConfig.storage?.stateFilePath}`);
|
|
4343
|
+
console.log(`Alert log: ${alertLogFile}`);
|
|
4344
|
+
console.log("Runtime monitor is running. Press Ctrl+C to stop.");
|
|
4345
|
+
}
|
|
4346
|
+
const signal = await runtime2.waitForShutdownSignal();
|
|
4347
|
+
if (asJson) {
|
|
4348
|
+
printJsonEvent("runtime_stopping", { signal });
|
|
4349
|
+
} else {
|
|
4350
|
+
console.log(`Stopping runtime monitor on ${signal}...`);
|
|
4351
|
+
}
|
|
4352
|
+
await runtime2.stop();
|
|
4353
|
+
removeRuntimeFiles(config.dataDir);
|
|
4354
|
+
if (asJson) {
|
|
4355
|
+
printJsonEvent("runtime_stopped", { signal });
|
|
4356
|
+
} else {
|
|
4357
|
+
console.log("Runtime monitor stopped.");
|
|
4358
|
+
}
|
|
4359
|
+
} catch (error) {
|
|
4360
|
+
removeRuntimeFiles(config.dataDir);
|
|
4361
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
4362
|
+
if (asJson) {
|
|
4363
|
+
printJsonError({
|
|
4364
|
+
code: "RUNTIME_START_FAILED",
|
|
4365
|
+
message,
|
|
4366
|
+
recoverable: true,
|
|
4367
|
+
suggestion: "Check RPC URL reachability and runtime option values, then retry."
|
|
4368
|
+
});
|
|
4369
|
+
} else {
|
|
4370
|
+
console.error(`Error: ${message}`);
|
|
4371
|
+
}
|
|
4372
|
+
process.exit(1);
|
|
4373
|
+
}
|
|
4374
|
+
});
|
|
4375
|
+
runtime.command("status").description("Show runtime process and health status").option("--json").action(async (options) => {
|
|
4376
|
+
const asJson = Boolean(options.json);
|
|
4377
|
+
const pid = readRuntimePid(config.dataDir);
|
|
4378
|
+
const meta = readRuntimeMeta(config.dataDir);
|
|
4379
|
+
if (!pid) {
|
|
4380
|
+
if (asJson) {
|
|
4381
|
+
printJsonError({
|
|
4382
|
+
code: "RUNTIME_NOT_RUNNING",
|
|
4383
|
+
message: "Runtime PID file not found.",
|
|
4384
|
+
recoverable: true,
|
|
4385
|
+
suggestion: "Start runtime with `fiber-pay runtime start --daemon`."
|
|
4386
|
+
});
|
|
4387
|
+
} else {
|
|
4388
|
+
console.log("Runtime is not running.");
|
|
4389
|
+
}
|
|
4390
|
+
process.exit(1);
|
|
4391
|
+
}
|
|
4392
|
+
const running = isProcessRunning(pid);
|
|
4393
|
+
if (!running) {
|
|
4394
|
+
removeRuntimeFiles(config.dataDir);
|
|
4395
|
+
if (asJson) {
|
|
4396
|
+
printJsonError({
|
|
4397
|
+
code: "RUNTIME_NOT_RUNNING",
|
|
4398
|
+
message: `Runtime process ${pid} is not running. Stale runtime files cleaned.`,
|
|
4399
|
+
recoverable: true,
|
|
4400
|
+
suggestion: "Start runtime again with `fiber-pay runtime start`.",
|
|
4401
|
+
details: { pid, staleFilesCleaned: true }
|
|
4402
|
+
});
|
|
4403
|
+
} else {
|
|
4404
|
+
console.log(`Runtime process ${pid} is not running. Stale runtime files cleaned.`);
|
|
4405
|
+
}
|
|
4406
|
+
process.exit(1);
|
|
4407
|
+
}
|
|
4408
|
+
let rpcStatus;
|
|
4409
|
+
if (meta?.proxyListen) {
|
|
4410
|
+
try {
|
|
4411
|
+
const response = await fetch(`http://${meta.proxyListen}/monitor/status`);
|
|
4412
|
+
if (response.ok) {
|
|
4413
|
+
rpcStatus = await response.json();
|
|
4414
|
+
}
|
|
4415
|
+
} catch {
|
|
4416
|
+
rpcStatus = void 0;
|
|
4417
|
+
}
|
|
4418
|
+
}
|
|
4419
|
+
const payload = {
|
|
4420
|
+
running: true,
|
|
4421
|
+
pid,
|
|
4422
|
+
meta,
|
|
4423
|
+
proxyStatus: rpcStatus
|
|
4424
|
+
};
|
|
4425
|
+
if (asJson) {
|
|
4426
|
+
printJsonSuccess(payload);
|
|
4427
|
+
} else {
|
|
4428
|
+
console.log(`Runtime is running (PID: ${pid})`);
|
|
4429
|
+
if (meta?.fiberRpcUrl) {
|
|
4430
|
+
console.log(`Fiber RPC: ${meta.fiberRpcUrl}`);
|
|
4431
|
+
}
|
|
4432
|
+
if (meta?.proxyListen) {
|
|
4433
|
+
console.log(`Proxy listen: ${meta.proxyListen}`);
|
|
4434
|
+
}
|
|
4435
|
+
}
|
|
4436
|
+
});
|
|
4437
|
+
runtime.command("stop").description("Stop runtime process by PID").option("--json").action(async (options) => {
|
|
4438
|
+
const asJson = Boolean(options.json);
|
|
4439
|
+
const pid = readRuntimePid(config.dataDir);
|
|
4440
|
+
if (!pid) {
|
|
4441
|
+
if (asJson) {
|
|
4442
|
+
printJsonError({
|
|
4443
|
+
code: "RUNTIME_NOT_RUNNING",
|
|
4444
|
+
message: "Runtime PID file not found.",
|
|
4445
|
+
recoverable: true,
|
|
4446
|
+
suggestion: "Start runtime first with `fiber-pay runtime start --daemon`."
|
|
4447
|
+
});
|
|
4448
|
+
} else {
|
|
4449
|
+
console.log("Runtime is not running.");
|
|
4450
|
+
}
|
|
4451
|
+
process.exit(1);
|
|
4452
|
+
}
|
|
4453
|
+
if (!isProcessRunning(pid)) {
|
|
4454
|
+
removeRuntimeFiles(config.dataDir);
|
|
4455
|
+
if (asJson) {
|
|
4456
|
+
printJsonError({
|
|
4457
|
+
code: "RUNTIME_NOT_RUNNING",
|
|
4458
|
+
message: `Runtime process ${pid} is not running. Stale runtime files cleaned.`,
|
|
4459
|
+
recoverable: true,
|
|
4460
|
+
suggestion: "Start runtime again if needed.",
|
|
4461
|
+
details: { pid, staleFilesCleaned: true }
|
|
4462
|
+
});
|
|
4463
|
+
} else {
|
|
4464
|
+
console.log(`Runtime process ${pid} is not running. Stale runtime files cleaned.`);
|
|
4465
|
+
}
|
|
4466
|
+
process.exit(1);
|
|
4467
|
+
}
|
|
4468
|
+
process.kill(pid, "SIGTERM");
|
|
4469
|
+
let attempts = 0;
|
|
4470
|
+
while (isProcessRunning(pid) && attempts < 50) {
|
|
4471
|
+
await new Promise((resolve2) => setTimeout(resolve2, 100));
|
|
4472
|
+
attempts += 1;
|
|
4473
|
+
}
|
|
4474
|
+
if (isProcessRunning(pid)) {
|
|
4475
|
+
process.kill(pid, "SIGKILL");
|
|
4476
|
+
}
|
|
4477
|
+
removeRuntimeFiles(config.dataDir);
|
|
4478
|
+
if (asJson) {
|
|
4479
|
+
printJsonSuccess({ stopped: true, pid });
|
|
4480
|
+
} else {
|
|
4481
|
+
console.log(`Runtime stopped (PID: ${pid})`);
|
|
4482
|
+
}
|
|
4483
|
+
});
|
|
4484
|
+
return runtime;
|
|
4485
|
+
}
|
|
4486
|
+
|
|
4487
|
+
// src/commands/version.ts
|
|
4488
|
+
import { Command as Command12 } from "commander";
|
|
4489
|
+
|
|
4490
|
+
// src/lib/build-info.ts
|
|
4491
|
+
var CLI_VERSION = "0.1.0-rc.1";
|
|
4492
|
+
var CLI_COMMIT = "4e24396aef57755a32c20f2162b88153492d81ec";
|
|
4493
|
+
|
|
4494
|
+
// src/commands/version.ts
|
|
4495
|
+
function createVersionCommand() {
|
|
4496
|
+
return new Command12("version").description("Show CLI version and commit id").option("--json", "Output JSON").action((options) => {
|
|
4497
|
+
const payload = {
|
|
4498
|
+
version: CLI_VERSION,
|
|
4499
|
+
commit: CLI_COMMIT
|
|
4500
|
+
};
|
|
4501
|
+
if (options.json) {
|
|
4502
|
+
printJsonSuccess(payload);
|
|
4503
|
+
return;
|
|
4504
|
+
}
|
|
4505
|
+
console.log(`Version: ${payload.version}`);
|
|
4506
|
+
console.log(`Commit: ${payload.commit}`);
|
|
4507
|
+
});
|
|
4508
|
+
}
|
|
4509
|
+
|
|
4510
|
+
// src/index.ts
|
|
4511
|
+
function shouldOutputJson() {
|
|
4512
|
+
return process.argv.includes("--json");
|
|
4513
|
+
}
|
|
4514
|
+
function getFlagValue(argv, index) {
|
|
4515
|
+
const value = argv[index + 1];
|
|
4516
|
+
if (!value || value.startsWith("-")) {
|
|
4517
|
+
return void 0;
|
|
4518
|
+
}
|
|
4519
|
+
return value;
|
|
4520
|
+
}
|
|
4521
|
+
var explicitFlags = /* @__PURE__ */ new Set();
|
|
4522
|
+
function applyGlobalOverrides(argv) {
|
|
4523
|
+
let explicitDataDir = false;
|
|
4524
|
+
let profileName;
|
|
4525
|
+
explicitFlags.clear();
|
|
4526
|
+
for (let index = 0; index < argv.length; index++) {
|
|
4527
|
+
const arg = argv[index];
|
|
4528
|
+
switch (arg) {
|
|
4529
|
+
case "--profile": {
|
|
4530
|
+
const value = getFlagValue(argv, index);
|
|
4531
|
+
if (value) profileName = value;
|
|
4532
|
+
break;
|
|
4533
|
+
}
|
|
4534
|
+
case "--data-dir": {
|
|
4535
|
+
const value = getFlagValue(argv, index);
|
|
4536
|
+
if (value) {
|
|
4537
|
+
process.env.FIBER_DATA_DIR = value;
|
|
4538
|
+
explicitDataDir = true;
|
|
4539
|
+
explicitFlags.add("dataDir");
|
|
4540
|
+
}
|
|
4541
|
+
break;
|
|
4542
|
+
}
|
|
4543
|
+
case "--rpc-url": {
|
|
4544
|
+
const value = getFlagValue(argv, index);
|
|
4545
|
+
if (value) {
|
|
4546
|
+
process.env.FIBER_RPC_URL = value;
|
|
4547
|
+
explicitFlags.add("rpcUrl");
|
|
4548
|
+
}
|
|
4549
|
+
break;
|
|
4550
|
+
}
|
|
4551
|
+
case "--network": {
|
|
4552
|
+
const value = getFlagValue(argv, index);
|
|
4553
|
+
if (value) {
|
|
4554
|
+
process.env.FIBER_NETWORK = value;
|
|
4555
|
+
explicitFlags.add("network");
|
|
4556
|
+
}
|
|
4557
|
+
break;
|
|
4558
|
+
}
|
|
4559
|
+
case "--key-password": {
|
|
4560
|
+
const value = getFlagValue(argv, index);
|
|
4561
|
+
if (value) {
|
|
4562
|
+
process.env.FIBER_KEY_PASSWORD = value;
|
|
4563
|
+
explicitFlags.add("keyPassword");
|
|
4564
|
+
}
|
|
4565
|
+
break;
|
|
4566
|
+
}
|
|
4567
|
+
case "--binary-path": {
|
|
4568
|
+
const value = getFlagValue(argv, index);
|
|
4569
|
+
if (value) {
|
|
4570
|
+
process.env.FIBER_BINARY_PATH = value;
|
|
4571
|
+
explicitFlags.add("binaryPath");
|
|
4572
|
+
}
|
|
4573
|
+
break;
|
|
4574
|
+
}
|
|
4575
|
+
case "--runtime-proxy-listen":
|
|
4576
|
+
case "--proxy-listen": {
|
|
4577
|
+
const value = getFlagValue(argv, index);
|
|
4578
|
+
if (value) {
|
|
4579
|
+
process.env.FIBER_RUNTIME_PROXY_LISTEN = value;
|
|
4580
|
+
explicitFlags.add("runtimeProxyListen");
|
|
4581
|
+
}
|
|
4582
|
+
break;
|
|
4583
|
+
}
|
|
4584
|
+
default:
|
|
4585
|
+
break;
|
|
4586
|
+
}
|
|
4587
|
+
}
|
|
4588
|
+
if (!explicitDataDir && profileName) {
|
|
4589
|
+
const homeDir = process.env.HOME ?? process.cwd();
|
|
4590
|
+
process.env.FIBER_DATA_DIR = join7(homeDir, ".fiber-pay", "profiles", profileName);
|
|
4591
|
+
}
|
|
4592
|
+
}
|
|
4593
|
+
function printFatal(error) {
|
|
4594
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
4595
|
+
const commanderCode = error && typeof error === "object" && "code" in error ? String(error.code) : void 0;
|
|
4596
|
+
if (shouldOutputJson()) {
|
|
4597
|
+
printJsonError({
|
|
4598
|
+
code: commanderCode ?? "CLI_FATAL",
|
|
4599
|
+
message,
|
|
4600
|
+
recoverable: !commanderCode || commanderCode.startsWith("commander."),
|
|
4601
|
+
suggestion: commanderCode?.startsWith("commander.") ? "Run the command with --help and fix invalid arguments." : "Inspect command arguments and environment, then retry."
|
|
4602
|
+
});
|
|
4603
|
+
} else {
|
|
4604
|
+
if (commanderCode?.startsWith("commander.")) {
|
|
4605
|
+
return;
|
|
4606
|
+
}
|
|
4607
|
+
console.error("Fatal error:", message);
|
|
4608
|
+
}
|
|
4609
|
+
}
|
|
4610
|
+
async function main() {
|
|
4611
|
+
applyGlobalOverrides(process.argv);
|
|
4612
|
+
const config = getEffectiveConfig(explicitFlags).config;
|
|
4613
|
+
const program = new Command13();
|
|
4614
|
+
program.name("fiber-pay").description("AI Agent Payment SDK for CKB Lightning Network").version(`${CLI_VERSION} (${CLI_COMMIT})`, "-v, --version", "Show version and commit id").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();
|
|
4615
|
+
program.exitOverride();
|
|
4616
|
+
program.configureOutput({
|
|
4617
|
+
writeOut: (str) => process.stdout.write(str),
|
|
4618
|
+
writeErr: (str) => {
|
|
4619
|
+
if (!shouldOutputJson()) {
|
|
4620
|
+
process.stderr.write(str);
|
|
4621
|
+
}
|
|
4622
|
+
}
|
|
4623
|
+
});
|
|
4624
|
+
program.addCommand(createNodeCommand(config));
|
|
4625
|
+
program.addCommand(createChannelCommand(config));
|
|
4626
|
+
program.addCommand(createInvoiceCommand(config));
|
|
4627
|
+
program.addCommand(createPaymentCommand(config));
|
|
4628
|
+
program.addCommand(createJobCommand(config));
|
|
4629
|
+
program.addCommand(createLogsCommand(config));
|
|
4630
|
+
program.addCommand(createPeerCommand(config));
|
|
4631
|
+
program.addCommand(createGraphCommand(config));
|
|
4632
|
+
program.addCommand(createBinaryCommand(config));
|
|
4633
|
+
program.addCommand(createConfigCommand(config));
|
|
4634
|
+
program.addCommand(createRuntimeCommand(config));
|
|
4635
|
+
program.addCommand(createVersionCommand());
|
|
4636
|
+
await program.parseAsync(process.argv);
|
|
4637
|
+
}
|
|
4638
|
+
main().catch((error) => {
|
|
4639
|
+
const commanderCode = error && typeof error === "object" && "code" in error ? String(error.code) : void 0;
|
|
4640
|
+
const commanderExitCode = error && typeof error === "object" && "exitCode" in error ? Number(error.exitCode) : void 0;
|
|
4641
|
+
printFatal(error);
|
|
4642
|
+
if (commanderCode?.startsWith("commander.") && commanderExitCode === 0) {
|
|
4643
|
+
process.exit(0);
|
|
4644
|
+
}
|
|
4645
|
+
process.exit(1);
|
|
4646
|
+
});
|
|
4647
|
+
//# sourceMappingURL=cli.js.map
|