@fibscope/agent 0.1.0
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/.env.example +7 -0
- package/PUBLISH.md +27 -0
- package/README.md +123 -0
- package/index.mjs +3244 -0
- package/lib/bridge/recommendation.mjs +311 -0
- package/lib/fnn-discovery.mjs +293 -0
- package/lib/observe/collector.mjs +1039 -0
- package/lib/pulse/engine.mjs +21 -0
- package/lib/pulse/index.mjs +6 -0
- package/lib/pulse/intelligence.mjs +154 -0
- package/lib/pulse/providers.mjs +321 -0
- package/lib/pulse/server.mjs +54 -0
- package/lib/pulse/snapshot.mjs +155 -0
- package/lib/route/route.mjs +1053 -0
- package/package.json +29 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { analyzeFiberState, calculatePaymentReadiness } from "./intelligence.mjs";
|
|
2
|
+
import { collectFiberSnapshot } from "./snapshot.mjs";
|
|
3
|
+
|
|
4
|
+
export * from "./snapshot.mjs";
|
|
5
|
+
export * from "./intelligence.mjs";
|
|
6
|
+
|
|
7
|
+
export async function collectFiberStateReport(providerOrOptions, maybeOptions = {}) {
|
|
8
|
+
const provider = isProvider(providerOrOptions) ? providerOrOptions : undefined;
|
|
9
|
+
const options = isProvider(providerOrOptions) ? maybeOptions : (providerOrOptions ?? maybeOptions);
|
|
10
|
+
const snapshot = await collectFiberSnapshot(provider ?? options, provider ? options : undefined);
|
|
11
|
+
return analyzeFiberState(snapshot, options);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export async function getPaymentReadiness(amount, providerOrOptions, maybeOptions = {}) {
|
|
15
|
+
const report = await collectFiberStateReport(providerOrOptions, maybeOptions);
|
|
16
|
+
return calculatePaymentReadiness(amount, report);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function isProvider(value) {
|
|
20
|
+
return Boolean(value && typeof value === "object" && "getNodeInfo" in value && "getPeers" in value && "getChannels" in value);
|
|
21
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export { collectFiberStateReport, getPaymentReadiness } from "./engine.mjs";
|
|
2
|
+
export { collectChannels, collectFiberSnapshot, collectNode, collectPayments, collectPeers, buildProviderCandidates, offlineSnapshot, selectActiveFiberProvider } from "./snapshot.mjs";
|
|
3
|
+
export { FiberReadinessError, analyzeFiberState, calculateChannelHealth, calculateHealth, calculateLiquidity, calculateNodeHealth, calculatePaymentReadiness, calculatePeerHealth, generateDiagnostics } from "./intelligence.mjs";
|
|
4
|
+
export { FiberCliProvider, FiberProviderError, FiberRpcProvider, normalizeChannels, normalizeNodeInfo, normalizePayments, normalizePeers, } from "./providers.mjs";
|
|
5
|
+
export { createFiberApiServer, startFiberApiServer, } from "./server.mjs";
|
|
6
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
export class FiberReadinessError extends Error {
|
|
2
|
+
constructor(message, code, details) {
|
|
3
|
+
super(message);
|
|
4
|
+
this.code = code;
|
|
5
|
+
this.details = details;
|
|
6
|
+
this.name = "FiberReadinessError";
|
|
7
|
+
}
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function analyzeFiberState(snapshot, options = {}) {
|
|
11
|
+
const liquidity = calculateLiquidity(snapshot.channels);
|
|
12
|
+
const report = {
|
|
13
|
+
timestamp: snapshot.timestamp,
|
|
14
|
+
provider: snapshot.provider,
|
|
15
|
+
node: snapshot.node,
|
|
16
|
+
peers: snapshot.peers,
|
|
17
|
+
channels: snapshot.channels,
|
|
18
|
+
payments: snapshot.payments ?? [],
|
|
19
|
+
liquidity,
|
|
20
|
+
health: calculateHealth(snapshot, liquidity),
|
|
21
|
+
diagnostics: [],
|
|
22
|
+
};
|
|
23
|
+
if (options.readinessAmount !== undefined) {
|
|
24
|
+
report.paymentReadiness = calculatePaymentReadiness(options.readinessAmount, report);
|
|
25
|
+
}
|
|
26
|
+
report.diagnostics = generateDiagnostics(report, {
|
|
27
|
+
...options,
|
|
28
|
+
collectionErrors: snapshot.collectionErrors ?? [],
|
|
29
|
+
});
|
|
30
|
+
report.health = calculateHealth(snapshot, liquidity, report.diagnostics);
|
|
31
|
+
return report;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function calculateLiquidity(channels) {
|
|
35
|
+
const open = channels.filter((channel) => channel.status === "open");
|
|
36
|
+
return {
|
|
37
|
+
totalOutbound: open.reduce((sum, channel) => sum + channel.localBalance, 0n),
|
|
38
|
+
totalInbound: open.reduce((sum, channel) => sum + channel.remoteBalance, 0n),
|
|
39
|
+
channelCount: open.length,
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function calculatePaymentReadiness(amount, reportOrLiquidity) {
|
|
44
|
+
const requested = parseAmount(amount);
|
|
45
|
+
const liquidity = "liquidity" in reportOrLiquidity ? reportOrLiquidity.liquidity : reportOrLiquidity;
|
|
46
|
+
const diagnostics = "diagnostics" in reportOrLiquidity ? reportOrLiquidity.diagnostics : [];
|
|
47
|
+
const critical = diagnostics.find((item) => item.severity === "critical" && ["NODE_OFFLINE", "NO_PEERS", "NO_CHANNELS"].includes(item.code));
|
|
48
|
+
if (critical) {
|
|
49
|
+
return { canSend: false, maxSendable: liquidity.totalOutbound, reason: critical.code };
|
|
50
|
+
}
|
|
51
|
+
if (requested > liquidity.totalOutbound) {
|
|
52
|
+
return { canSend: false, maxSendable: liquidity.totalOutbound, reason: "INSUFFICIENT_OUTBOUND_LIQUIDITY" };
|
|
53
|
+
}
|
|
54
|
+
return { canSend: true, maxSendable: liquidity.totalOutbound };
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function calculateHealth(snapshot, liquidity = calculateLiquidity(snapshot.channels ?? []), diagnostics = []) {
|
|
58
|
+
const node = calculateNodeHealth(snapshot.node);
|
|
59
|
+
const peers = calculatePeerHealth(snapshot.peers ?? []);
|
|
60
|
+
const channels = calculateChannelHealth(snapshot.channels ?? [], liquidity);
|
|
61
|
+
const diagnosticPenalty = diagnostics.some((item) => item.severity === "critical") ? 30 : diagnostics.some((item) => item.severity === "warning") ? 10 : 0;
|
|
62
|
+
const score = clamp(Math.round((node.score + peers.score + channels.score) / 3) - diagnosticPenalty);
|
|
63
|
+
return {
|
|
64
|
+
overall: healthScore(score),
|
|
65
|
+
node,
|
|
66
|
+
peers,
|
|
67
|
+
channels,
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export function calculateNodeHealth(node = {}) {
|
|
72
|
+
return healthScore(node.online ? 100 : 0);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export function calculatePeerHealth(peers = []) {
|
|
76
|
+
if (peers.length === 0) {
|
|
77
|
+
return { ...healthScore(0), totalPeers: 0, connectedPeers: 0, offlinePeers: 0 };
|
|
78
|
+
}
|
|
79
|
+
const connectedPeers = peers.filter((peer) => peer.connected).length;
|
|
80
|
+
const score = Math.round((connectedPeers / peers.length) * 100);
|
|
81
|
+
return {
|
|
82
|
+
...healthScore(score),
|
|
83
|
+
totalPeers: peers.length,
|
|
84
|
+
connectedPeers,
|
|
85
|
+
offlinePeers: peers.length - connectedPeers,
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export function calculateChannelHealth(channels = [], liquidity = calculateLiquidity(channels)) {
|
|
90
|
+
if (channels.length === 0) {
|
|
91
|
+
return { ...healthScore(0), totalChannels: 0, openChannels: 0 };
|
|
92
|
+
}
|
|
93
|
+
const openChannels = channels.filter((channel) => channel.status === "open").length;
|
|
94
|
+
const openScore = Math.round((openChannels / channels.length) * 100);
|
|
95
|
+
const liquidityScore = liquidity.totalOutbound > 0n ? 100 : 30;
|
|
96
|
+
return {
|
|
97
|
+
...healthScore(Math.round((openScore + liquidityScore) / 2)),
|
|
98
|
+
totalChannels: channels.length,
|
|
99
|
+
openChannels,
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export function generateDiagnostics(report, options = {}) {
|
|
104
|
+
const diagnostics = [];
|
|
105
|
+
if (!report.node.online) {
|
|
106
|
+
diagnostics.push(diagnostic("NODE_OFFLINE", "critical", "FNN is not reachable", "The agent could not confirm that the local Fiber node is running or reachable through the configured RPC provider.", "Start FNN and confirm the configured Fiber RPC URL is reachable."));
|
|
107
|
+
}
|
|
108
|
+
if (report.peers.length === 0) {
|
|
109
|
+
diagnostics.push(diagnostic("NO_PEERS", "critical", "No Fiber peers", "The node has no known peers.", "Connect to a Fiber peer."));
|
|
110
|
+
} else if (report.peers.every((peer) => !peer.connected)) {
|
|
111
|
+
diagnostics.push(diagnostic("NO_CONNECTED_PEERS", "warning", "No connected Fiber peers", "Peers exist, but none are currently connected.", "Check peer addresses and network reachability."));
|
|
112
|
+
}
|
|
113
|
+
if (report.channels.length === 0) {
|
|
114
|
+
diagnostics.push(diagnostic("NO_CHANNELS", "critical", "No Fiber channels", "No channels were discovered.", "Open a channel."));
|
|
115
|
+
}
|
|
116
|
+
const threshold = options.lowOutboundThreshold === undefined ? 1n : parseAmount(options.lowOutboundThreshold);
|
|
117
|
+
if (report.channels.length > 0 && report.liquidity.totalOutbound <= threshold) {
|
|
118
|
+
diagnostics.push(diagnostic("LOW_OUTBOUND_LIQUIDITY", "warning", "Low outbound liquidity", "The node has little or no outbound liquidity available for sending payments.", "Add liquidity or rebalance."));
|
|
119
|
+
}
|
|
120
|
+
for (const failure of options.collectionErrors ?? []) {
|
|
121
|
+
diagnostics.push(diagnostic("PROVIDER_UNAVAILABLE", "warning", "Provider collection issue", `${failure.provider ?? "provider"}: ${failure.message}`, "Start FNN, expose Fiber RPC, or install/configure fnn-cli."));
|
|
122
|
+
}
|
|
123
|
+
if (diagnostics.length === 0) {
|
|
124
|
+
diagnostics.push(diagnostic("FIBER_READY", "info", "Fiber node appears payment-ready", "Peers, channels, and outbound liquidity are available."));
|
|
125
|
+
}
|
|
126
|
+
return diagnostics;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function healthScore(score) {
|
|
130
|
+
const normalized = clamp(score);
|
|
131
|
+
return {
|
|
132
|
+
score: normalized,
|
|
133
|
+
status: normalized >= 80 ? "healthy" : normalized >= 50 ? "warning" : "critical",
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function clamp(value) {
|
|
138
|
+
return Math.max(0, Math.min(100, Number.isFinite(value) ? value : 0));
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function diagnostic(code, severity, title, description, recommendation) {
|
|
142
|
+
return { code, severity, title, description, recommendation };
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function parseAmount(value) {
|
|
146
|
+
if (typeof value === "bigint") return value;
|
|
147
|
+
if (typeof value === "number") {
|
|
148
|
+
if (!Number.isFinite(value) || value < 0) throw new FiberReadinessError("Amount must be a non-negative finite number.", "INVALID_AMOUNT");
|
|
149
|
+
return BigInt(Math.trunc(value));
|
|
150
|
+
}
|
|
151
|
+
if (/^0x[0-9a-fA-F]+$/.test(value)) return BigInt(value);
|
|
152
|
+
if (/^[0-9]+$/.test(value)) return BigInt(value);
|
|
153
|
+
throw new FiberReadinessError("Amount must be a decimal or 0x-prefixed integer.", "INVALID_AMOUNT", { amount: value });
|
|
154
|
+
}
|
|
@@ -0,0 +1,321 @@
|
|
|
1
|
+
import { execFile } from "node:child_process";
|
|
2
|
+
import { promisify } from "node:util";
|
|
3
|
+
const execFileAsync = promisify(execFile);
|
|
4
|
+
export class FiberProviderError extends Error {
|
|
5
|
+
code;
|
|
6
|
+
details;
|
|
7
|
+
constructor(message, code, details) {
|
|
8
|
+
super(message);
|
|
9
|
+
this.code = code;
|
|
10
|
+
this.details = details;
|
|
11
|
+
this.name = "FiberProviderError";
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
export class FiberCliProvider {
|
|
15
|
+
options;
|
|
16
|
+
kind = "cli";
|
|
17
|
+
label;
|
|
18
|
+
bin;
|
|
19
|
+
baseArgs;
|
|
20
|
+
constructor(options = {}) {
|
|
21
|
+
this.options = options;
|
|
22
|
+
this.bin = options.bin ?? "fnn-cli";
|
|
23
|
+
this.baseArgs = options.baseArgs ?? [];
|
|
24
|
+
this.label = `cli:${this.bin}`;
|
|
25
|
+
}
|
|
26
|
+
async getNodeInfo() {
|
|
27
|
+
return normalizeNodeInfo(await this.run(["info"]));
|
|
28
|
+
}
|
|
29
|
+
async getPeers() {
|
|
30
|
+
return normalizePeers(await this.run(["peer", "list_peers"]));
|
|
31
|
+
}
|
|
32
|
+
async getChannels() {
|
|
33
|
+
return normalizeChannels(await this.run(["channel", "list_channels"]));
|
|
34
|
+
}
|
|
35
|
+
async getPayments() {
|
|
36
|
+
return normalizePayments(await this.run(["payment", "list_payments"]));
|
|
37
|
+
}
|
|
38
|
+
async run(args) {
|
|
39
|
+
try {
|
|
40
|
+
const result = await execFileAsync(this.bin, [...this.baseArgs, ...args], {
|
|
41
|
+
cwd: this.options.cwd,
|
|
42
|
+
env: this.options.env ? { ...process.env, ...this.options.env } : process.env,
|
|
43
|
+
timeout: this.options.timeoutMs ?? 10_000,
|
|
44
|
+
maxBuffer: this.options.maxBuffer ?? 1024 * 1024,
|
|
45
|
+
});
|
|
46
|
+
return parseCliOutput(result.stdout);
|
|
47
|
+
}
|
|
48
|
+
catch (error) {
|
|
49
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
50
|
+
throw new FiberProviderError(`Fiber CLI command failed: ${message}`, "FIBER_CLI_FAILED", { args });
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
export class FiberRpcProvider {
|
|
55
|
+
options;
|
|
56
|
+
kind = "rpc";
|
|
57
|
+
label;
|
|
58
|
+
methods;
|
|
59
|
+
constructor(options) {
|
|
60
|
+
this.options = options;
|
|
61
|
+
this.label = `rpc:${options.rpcUrl}`;
|
|
62
|
+
this.methods = {
|
|
63
|
+
info: options.methods?.info ?? "node_info",
|
|
64
|
+
listPeers: options.methods?.listPeers ?? "list_peers",
|
|
65
|
+
listChannels: options.methods?.listChannels ?? "list_channels",
|
|
66
|
+
listPayments: options.methods?.listPayments ?? "list_payments",
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
async getNodeInfo() {
|
|
70
|
+
return normalizeNodeInfo(await this.call(this.methods.info));
|
|
71
|
+
}
|
|
72
|
+
async getPeers() {
|
|
73
|
+
return normalizePeers(await this.call(this.methods.listPeers));
|
|
74
|
+
}
|
|
75
|
+
async getChannels() {
|
|
76
|
+
return normalizeChannels(await this.call(this.methods.listChannels, [{}]));
|
|
77
|
+
}
|
|
78
|
+
async getPayments() {
|
|
79
|
+
return normalizePayments(await this.call(this.methods.listPayments, [{}]));
|
|
80
|
+
}
|
|
81
|
+
async call(method, params = []) {
|
|
82
|
+
const controller = new AbortController();
|
|
83
|
+
const timeout = setTimeout(() => controller.abort(), this.options.timeoutMs ?? 10_000);
|
|
84
|
+
try {
|
|
85
|
+
const response = await fetch(this.options.rpcUrl, {
|
|
86
|
+
method: "POST",
|
|
87
|
+
headers: {
|
|
88
|
+
"content-type": "application/json",
|
|
89
|
+
...(this.options.headers ?? {}),
|
|
90
|
+
},
|
|
91
|
+
body: JSON.stringify({ id: 1, jsonrpc: "2.0", method, params }),
|
|
92
|
+
signal: controller.signal,
|
|
93
|
+
});
|
|
94
|
+
if (!response.ok) {
|
|
95
|
+
throw new FiberProviderError(`Fiber RPC HTTP ${response.status}`, "FIBER_RPC_HTTP_ERROR", { method, status: response.status });
|
|
96
|
+
}
|
|
97
|
+
const payload = await response.json();
|
|
98
|
+
if (payload.error) {
|
|
99
|
+
throw new FiberProviderError(payload.error.message ?? "Fiber RPC error", "FIBER_RPC_ERROR", { method, error: payload.error });
|
|
100
|
+
}
|
|
101
|
+
return payload.result;
|
|
102
|
+
}
|
|
103
|
+
finally {
|
|
104
|
+
clearTimeout(timeout);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
export function normalizeNodeInfo(raw) {
|
|
109
|
+
const value = unwrap(raw);
|
|
110
|
+
const now = Date.now();
|
|
111
|
+
return {
|
|
112
|
+
nodeId: stringFrom(value, ["nodeId", "node_id", "peerId", "peer_id", "id", "pubkey", "public_key"]) ?? "",
|
|
113
|
+
version: stringFrom(value, ["version", "commit", "buildVersion", "build_version"]) ?? "",
|
|
114
|
+
addresses: stringsFrom(value, ["addresses", "announcedAddrs", "announced_addrs", "listenAddresses", "listen_addresses"]),
|
|
115
|
+
online: true,
|
|
116
|
+
peerCount: numberFrom(value, ["peerCount", "peer_count", "peers_count", "num_peers"]) ?? 0,
|
|
117
|
+
channelCount: numberFrom(value, ["channelCount", "channel_count", "channels_count", "num_channels"]) ?? 0,
|
|
118
|
+
lastUpdated: now,
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
export function normalizePeers(raw) {
|
|
122
|
+
const now = Date.now();
|
|
123
|
+
return extractList(raw, ["peers", "peer_states", "items", "list"]).map((peer) => {
|
|
124
|
+
const status = valueFrom(peer, ["status", "state", "connection_state"]);
|
|
125
|
+
const connected = booleanFrom(peer, ["connected", "is_connected", "online"]) ?? (status === undefined ? true : statusConnected(status));
|
|
126
|
+
return {
|
|
127
|
+
peerId: stringFrom(peer, ["peerId", "peer_id", "nodeId", "node_id", "id", "pubkey", "public_key"]) ?? "",
|
|
128
|
+
connected,
|
|
129
|
+
address: stringFrom(peer, ["address", "addr", "multiaddr", "multi_addr"]) ?? firstString(valueFrom(peer, ["addresses"])),
|
|
130
|
+
lastSeen: timeFrom(peer, ["lastSeen", "last_seen", "updatedAt", "updated_at"]) ?? (connected ? now : undefined),
|
|
131
|
+
};
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
export function normalizeChannels(raw) {
|
|
135
|
+
return extractList(raw, ["channels", "channel_states", "items", "list"]).map((channel) => {
|
|
136
|
+
const localBalance = amountLike(valueFrom(channel, ["localBalance", "local_balance", "outboundLiquidity", "outbound_liquidity"])) ?? 0n;
|
|
137
|
+
const remoteBalance = amountLike(valueFrom(channel, ["remoteBalance", "remote_balance", "inboundLiquidity", "inbound_liquidity"])) ?? 0n;
|
|
138
|
+
const offeredTlcBalance = amountLike(valueFrom(channel, ["offeredTlcBalance", "offered_tlc_balance"])) ?? 0n;
|
|
139
|
+
const receivedTlcBalance = amountLike(valueFrom(channel, ["receivedTlcBalance", "received_tlc_balance"])) ?? 0n;
|
|
140
|
+
return {
|
|
141
|
+
channelId: stringFrom(channel, ["channelId", "channel_id", "id", "outpoint", "funding_outpoint"]) ?? "",
|
|
142
|
+
peerId: stringFrom(channel, ["peerId", "peer_id", "pubkey", "counterparty", "counterparty_node_id"]) ?? "",
|
|
143
|
+
status: normalizeChannelStatus(channelStatusValue(channel)),
|
|
144
|
+
capacity: amountLike(valueFrom(channel, ["capacity", "totalCapacity", "total_capacity"])) ?? (localBalance + remoteBalance + offeredTlcBalance + receivedTlcBalance),
|
|
145
|
+
localBalance,
|
|
146
|
+
remoteBalance,
|
|
147
|
+
assetType: assetTypeFrom(channel),
|
|
148
|
+
};
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
export function normalizePayments(raw) {
|
|
152
|
+
return extractList(raw, ["payments", "payment_states", "items", "list"]).map((payment) => ({
|
|
153
|
+
paymentId: stringFrom(payment, ["paymentId", "payment_id", "id", "hash"]) ?? "unknown",
|
|
154
|
+
status: stringFrom(payment, ["status", "state"]) ?? "unknown",
|
|
155
|
+
amount: amountLike(valueFrom(payment, ["amount", "value"])) ?? 0n,
|
|
156
|
+
assetType: stringFrom(payment, ["assetType", "asset_type", "asset"]) ?? "CKB",
|
|
157
|
+
direction: stringFrom(payment, ["direction"]),
|
|
158
|
+
route: valueFrom(payment, ["route", "hops"]) ?? [],
|
|
159
|
+
fee: amountLike(valueFrom(payment, ["fee", "routingFee", "routing_fee", "totalFee", "total_fee"])),
|
|
160
|
+
latencyMs: numberFrom(payment, ["latencyMs", "latency_ms", "durationMs", "duration_ms"]),
|
|
161
|
+
error: valueFrom(payment, ["error", "failure_reason"]),
|
|
162
|
+
timestamp: timeFrom(payment, ["timestamp", "created_at", "updated_at"]) ?? Date.now(),
|
|
163
|
+
}));
|
|
164
|
+
}
|
|
165
|
+
function parseCliOutput(stdout) {
|
|
166
|
+
const text = stdout.trim();
|
|
167
|
+
if (!text)
|
|
168
|
+
return {};
|
|
169
|
+
const whole = tryJson(text);
|
|
170
|
+
if (whole.ok)
|
|
171
|
+
return whole.value;
|
|
172
|
+
const lines = text.split(/\r?\n/).map((line) => line.trim()).filter(Boolean);
|
|
173
|
+
const jsonLines = lines.map((line) => tryJson(line));
|
|
174
|
+
if (jsonLines.length > 0 && jsonLines.every((line) => line.ok))
|
|
175
|
+
return jsonLines.map((line) => line.value);
|
|
176
|
+
const object = {};
|
|
177
|
+
for (const line of lines) {
|
|
178
|
+
const match = line.match(/^([^:=]+)\s*[:=]\s*(.+)$/);
|
|
179
|
+
if (match)
|
|
180
|
+
object[toCamel(match[1].trim())] = parseScalar(match[2].trim());
|
|
181
|
+
}
|
|
182
|
+
return Object.keys(object).length > 0 ? object : { text };
|
|
183
|
+
}
|
|
184
|
+
function unwrap(raw) {
|
|
185
|
+
if (raw && typeof raw === "object" && "result" in raw)
|
|
186
|
+
return unwrap(raw.result);
|
|
187
|
+
return raw && typeof raw === "object" ? raw : {};
|
|
188
|
+
}
|
|
189
|
+
function extractList(raw, keys) {
|
|
190
|
+
const value = raw && typeof raw === "object" && "result" in raw ? raw.result : raw;
|
|
191
|
+
if (Array.isArray(value))
|
|
192
|
+
return value.filter((item) => Boolean(item && typeof item === "object"));
|
|
193
|
+
if (!value || typeof value !== "object")
|
|
194
|
+
return [];
|
|
195
|
+
for (const key of keys) {
|
|
196
|
+
const item = value[key];
|
|
197
|
+
if (Array.isArray(item))
|
|
198
|
+
return item.filter((entry) => Boolean(entry && typeof entry === "object"));
|
|
199
|
+
}
|
|
200
|
+
return [];
|
|
201
|
+
}
|
|
202
|
+
function valueFrom(object, keys) {
|
|
203
|
+
if (!object || typeof object !== "object")
|
|
204
|
+
return undefined;
|
|
205
|
+
for (const key of keys) {
|
|
206
|
+
const value = object[key];
|
|
207
|
+
if (value !== undefined && value !== null && value !== "")
|
|
208
|
+
return value;
|
|
209
|
+
}
|
|
210
|
+
return undefined;
|
|
211
|
+
}
|
|
212
|
+
function stringFrom(object, keys) {
|
|
213
|
+
const value = valueFrom(object, keys);
|
|
214
|
+
return value === undefined ? undefined : String(value);
|
|
215
|
+
}
|
|
216
|
+
function numberFrom(object, keys) {
|
|
217
|
+
const value = valueFrom(object, keys);
|
|
218
|
+
if (value === undefined)
|
|
219
|
+
return undefined;
|
|
220
|
+
const number = Number(value);
|
|
221
|
+
return Number.isFinite(number) ? number : undefined;
|
|
222
|
+
}
|
|
223
|
+
function booleanFrom(object, keys) {
|
|
224
|
+
const value = valueFrom(object, keys);
|
|
225
|
+
return typeof value === "boolean" ? value : undefined;
|
|
226
|
+
}
|
|
227
|
+
function amountLike(value) {
|
|
228
|
+
if (value === undefined || value === null || value === "")
|
|
229
|
+
return undefined;
|
|
230
|
+
if (typeof value === "bigint")
|
|
231
|
+
return value;
|
|
232
|
+
if (typeof value === "number" && Number.isFinite(value))
|
|
233
|
+
return BigInt(Math.trunc(value));
|
|
234
|
+
if (typeof value === "string" && /^0x[0-9a-fA-F]+$/.test(value))
|
|
235
|
+
return BigInt(value);
|
|
236
|
+
if (typeof value === "string" && /^[0-9]+$/.test(value))
|
|
237
|
+
return BigInt(value);
|
|
238
|
+
return undefined;
|
|
239
|
+
}
|
|
240
|
+
function timeFrom(object, keys) {
|
|
241
|
+
const value = stringFrom(object, keys);
|
|
242
|
+
if (!value)
|
|
243
|
+
return undefined;
|
|
244
|
+
if (/^\d+$/.test(value)) {
|
|
245
|
+
const number = Number(value);
|
|
246
|
+
return number < 10_000_000_000 ? number * 1000 : number;
|
|
247
|
+
}
|
|
248
|
+
const parsed = Date.parse(value);
|
|
249
|
+
return Number.isNaN(parsed) ? undefined : parsed;
|
|
250
|
+
}
|
|
251
|
+
function normalizeChannelStatus(value) {
|
|
252
|
+
const status = String(value ?? "").toLowerCase();
|
|
253
|
+
const compact = status.replace(/[^a-z0-9]/g, "");
|
|
254
|
+
if (["open", "active", "ready", "normal", "usable", "channelready", "opened"].includes(compact))
|
|
255
|
+
return "open";
|
|
256
|
+
if (["pending", "pendingopen", "opening", "funding", "waitingforfunding", "negotiating", "collaboratingfundingtx", "signing", "awaitingtxsignatures", "awaitingchannelready"].includes(compact))
|
|
257
|
+
return "pending";
|
|
258
|
+
if (["closing", "shuttingdown", "shutdown", "pendingclose"].includes(compact))
|
|
259
|
+
return "closing";
|
|
260
|
+
return "closed";
|
|
261
|
+
}
|
|
262
|
+
function channelStatusValue(channel) {
|
|
263
|
+
const direct = valueFrom(channel, ["status", "state_name", "channel_state"]);
|
|
264
|
+
if (direct !== undefined)
|
|
265
|
+
return direct;
|
|
266
|
+
const state = valueFrom(channel, ["state"]);
|
|
267
|
+
const nested = valueFrom(state, ["state_name", "status", "name"]);
|
|
268
|
+
if (nested !== undefined)
|
|
269
|
+
return nested;
|
|
270
|
+
if (state && typeof state === "object") {
|
|
271
|
+
const [variant] = Object.keys(state).filter((key) => key !== "flags");
|
|
272
|
+
return variant;
|
|
273
|
+
}
|
|
274
|
+
return state;
|
|
275
|
+
}
|
|
276
|
+
function assetTypeFrom(channel) {
|
|
277
|
+
const explicit = stringFrom(channel, ["assetType", "asset_type", "asset", "currency"]);
|
|
278
|
+
if (explicit)
|
|
279
|
+
return explicit;
|
|
280
|
+
const udt = valueFrom(channel, ["funding_udt_type_script", "udt_type_script"]);
|
|
281
|
+
if (!udt)
|
|
282
|
+
return "CKB";
|
|
283
|
+
if (typeof udt === "object") {
|
|
284
|
+
const codeHash = stringFrom(udt, ["code_hash", "codeHash"]);
|
|
285
|
+
return codeHash ? `UDT:${codeHash}` : "UDT";
|
|
286
|
+
}
|
|
287
|
+
return String(udt);
|
|
288
|
+
}
|
|
289
|
+
function statusConnected(value) {
|
|
290
|
+
return ["connected", "online", "active", "established", "ready"].includes(String(value ?? "").toLowerCase());
|
|
291
|
+
}
|
|
292
|
+
function firstString(value) {
|
|
293
|
+
return Array.isArray(value) ? value.find((item) => typeof item === "string") : undefined;
|
|
294
|
+
}
|
|
295
|
+
function stringsFrom(object, keys) {
|
|
296
|
+
const value = valueFrom(object, keys);
|
|
297
|
+
if (Array.isArray(value))
|
|
298
|
+
return value.filter((item) => typeof item === "string" && item.trim()).map((item) => item.trim());
|
|
299
|
+
if (typeof value === "string" && value.trim())
|
|
300
|
+
return [value.trim()];
|
|
301
|
+
return [];
|
|
302
|
+
}
|
|
303
|
+
function tryJson(text) {
|
|
304
|
+
try {
|
|
305
|
+
return { ok: true, value: JSON.parse(text) };
|
|
306
|
+
}
|
|
307
|
+
catch {
|
|
308
|
+
return { ok: false };
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
function parseScalar(value) {
|
|
312
|
+
if (/^(true|false)$/i.test(value))
|
|
313
|
+
return value.toLowerCase() === "true";
|
|
314
|
+
if (/^-?\d+$/.test(value))
|
|
315
|
+
return Number(value);
|
|
316
|
+
return value;
|
|
317
|
+
}
|
|
318
|
+
function toCamel(value) {
|
|
319
|
+
return value.replace(/[-_\s]+(.)?/g, (_, char) => char ? char.toUpperCase() : "");
|
|
320
|
+
}
|
|
321
|
+
//# sourceMappingURL=providers.js.map
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { createServer } from "node:http";
|
|
2
|
+
import { calculatePaymentReadiness, collectFiberStateReport } from "./engine.mjs";
|
|
3
|
+
export function createFiberApiServer(options = {}) {
|
|
4
|
+
return createServer(async (request, response) => {
|
|
5
|
+
try {
|
|
6
|
+
await handleRequest(request, response, options);
|
|
7
|
+
}
|
|
8
|
+
catch (error) {
|
|
9
|
+
json(response, 500, {
|
|
10
|
+
error: error instanceof Error ? error.message : String(error),
|
|
11
|
+
});
|
|
12
|
+
}
|
|
13
|
+
});
|
|
14
|
+
}
|
|
15
|
+
export async function startFiberApiServer(options = {}) {
|
|
16
|
+
const server = createFiberApiServer(options);
|
|
17
|
+
const host = options.host ?? "127.0.0.1";
|
|
18
|
+
const port = options.port ?? 8787;
|
|
19
|
+
await new Promise((resolve, reject) => {
|
|
20
|
+
server.once("error", reject);
|
|
21
|
+
server.listen(port, host, () => {
|
|
22
|
+
server.off("error", reject);
|
|
23
|
+
resolve();
|
|
24
|
+
});
|
|
25
|
+
});
|
|
26
|
+
return server;
|
|
27
|
+
}
|
|
28
|
+
async function handleRequest(request, response, options) {
|
|
29
|
+
const url = new URL(request.url ?? "/", `http://${request.headers.host ?? "127.0.0.1"}`);
|
|
30
|
+
if (request.method !== "GET") {
|
|
31
|
+
json(response, 405, { error: "Only GET is supported." });
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
if (url.pathname === "/state") {
|
|
35
|
+
json(response, 200, await collectFiberStateReport(options));
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
if (url.pathname === "/readiness") {
|
|
39
|
+
const amount = url.searchParams.get("amount");
|
|
40
|
+
if (!amount) {
|
|
41
|
+
json(response, 400, { error: "Missing amount query parameter." });
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
const report = await collectFiberStateReport(options);
|
|
45
|
+
json(response, 200, calculatePaymentReadiness(amount, report));
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
json(response, 404, { error: "Not found.", endpoints: ["/state", "/readiness?amount=100"] });
|
|
49
|
+
}
|
|
50
|
+
function json(response, status, value) {
|
|
51
|
+
response.writeHead(status, { "content-type": "application/json; charset=utf-8" });
|
|
52
|
+
response.end(JSON.stringify(value, (_, nested) => typeof nested === "bigint" ? nested.toString() : nested, 2));
|
|
53
|
+
}
|
|
54
|
+
//# sourceMappingURL=server.js.map
|