@fiber-pay/sdk 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 +20 -0
- package/dist/browser.d.ts +895 -0
- package/dist/browser.js +29 -0
- package/dist/browser.js.map +1 -0
- package/dist/chunk-QQDMPGVR.js +723 -0
- package/dist/chunk-QQDMPGVR.js.map +1 -0
- package/dist/index.d.ts +492 -0
- package/dist/index.js +1336 -0
- package/dist/index.js.map +1 -0
- package/package.json +46 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,1336 @@
|
|
|
1
|
+
import {
|
|
2
|
+
ChannelState,
|
|
3
|
+
FiberRpcClient,
|
|
4
|
+
FiberRpcError,
|
|
5
|
+
buildMultiaddr,
|
|
6
|
+
buildMultiaddrFromNodeId,
|
|
7
|
+
buildMultiaddrFromRpcUrl,
|
|
8
|
+
ckbToShannons,
|
|
9
|
+
fromHex,
|
|
10
|
+
nodeIdToPeerId,
|
|
11
|
+
randomBytes32,
|
|
12
|
+
scriptToAddress,
|
|
13
|
+
shannonsToCkb,
|
|
14
|
+
toHex
|
|
15
|
+
} from "./chunk-QQDMPGVR.js";
|
|
16
|
+
|
|
17
|
+
// src/funds/liquidity-analyzer.ts
|
|
18
|
+
var LiquidityAnalyzer = class {
|
|
19
|
+
constructor(rpc) {
|
|
20
|
+
this.rpc = rpc;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Comprehensive liquidity analysis
|
|
24
|
+
*/
|
|
25
|
+
async analyzeLiquidity() {
|
|
26
|
+
const timestamp = Date.now();
|
|
27
|
+
const channels = await this.rpc.listChannels({});
|
|
28
|
+
const channelMetrics = channels.channels.map(
|
|
29
|
+
(ch) => this.analyzeChannelHealth(ch)
|
|
30
|
+
);
|
|
31
|
+
const totalCkb = channelMetrics.reduce(
|
|
32
|
+
(sum, ch) => sum + ch.localBalanceCkb + ch.remoteBalanceCkb,
|
|
33
|
+
0
|
|
34
|
+
);
|
|
35
|
+
const availableToSendCkb = channelMetrics.reduce((sum, ch) => sum + ch.availableToSendCkb, 0);
|
|
36
|
+
const availableToReceiveCkb = channelMetrics.reduce(
|
|
37
|
+
(sum, ch) => sum + ch.availableToReceiveCkb,
|
|
38
|
+
0
|
|
39
|
+
);
|
|
40
|
+
const gaps = this.identifyLiquidityGaps(channelMetrics, availableToSendCkb);
|
|
41
|
+
const rebalances = this.generateRebalanceRecommendations(channelMetrics);
|
|
42
|
+
const fundingNeeds = this.estimateFundingNeeds(channelMetrics, gaps);
|
|
43
|
+
const runway = this.estimateRunway(availableToSendCkb, channelMetrics);
|
|
44
|
+
const summary = this.generateSummary(channelMetrics, gaps, fundingNeeds, runway);
|
|
45
|
+
return {
|
|
46
|
+
timestamp,
|
|
47
|
+
balance: {
|
|
48
|
+
totalCkb,
|
|
49
|
+
availableToSendCkb,
|
|
50
|
+
availableToReceiveCkb,
|
|
51
|
+
lockedInChannelsCkb: totalCkb - availableToSendCkb - availableToReceiveCkb
|
|
52
|
+
},
|
|
53
|
+
channels: {
|
|
54
|
+
count: channels.channels.length,
|
|
55
|
+
health: channelMetrics,
|
|
56
|
+
averageHealthScore: channelMetrics.length > 0 ? channelMetrics.reduce((sum, ch) => sum + ch.healthScore, 0) / channelMetrics.length : 0,
|
|
57
|
+
balancedCount: channelMetrics.filter((ch) => ch.isBalanced).length,
|
|
58
|
+
imbalancedCount: channelMetrics.filter((ch) => !ch.isBalanced).length
|
|
59
|
+
},
|
|
60
|
+
liquidity: {
|
|
61
|
+
gaps,
|
|
62
|
+
hasCriticalGaps: gaps.some((g) => g.severity === "high"),
|
|
63
|
+
runway
|
|
64
|
+
},
|
|
65
|
+
recommendations: {
|
|
66
|
+
rebalances,
|
|
67
|
+
funding: fundingNeeds
|
|
68
|
+
},
|
|
69
|
+
summary
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Analyze individual channel health
|
|
74
|
+
*/
|
|
75
|
+
analyzeChannelHealth(channel) {
|
|
76
|
+
const localBalance = shannonsToCkb(channel.local_balance);
|
|
77
|
+
const remoteBalance = shannonsToCkb(channel.remote_balance);
|
|
78
|
+
const totalCapacity = localBalance + remoteBalance;
|
|
79
|
+
const pendingLocal = shannonsToCkb(channel.offered_tlc_balance);
|
|
80
|
+
const pendingRemote = shannonsToCkb(channel.received_tlc_balance);
|
|
81
|
+
const availableToSend = Math.max(0, localBalance - pendingLocal);
|
|
82
|
+
const availableToReceive = Math.max(0, remoteBalance - pendingRemote);
|
|
83
|
+
const utilizationPercent = totalCapacity > 0 ? (pendingLocal + pendingRemote) / totalCapacity * 100 : 0;
|
|
84
|
+
const balanceRatioPercent = totalCapacity > 0 ? localBalance / totalCapacity * 100 : 50;
|
|
85
|
+
const isBalanced = balanceRatioPercent >= 40 && balanceRatioPercent <= 60;
|
|
86
|
+
let healthScore = 100;
|
|
87
|
+
if (utilizationPercent > 80) healthScore -= 20;
|
|
88
|
+
else if (utilizationPercent > 50) healthScore -= 10;
|
|
89
|
+
const imbalance = Math.abs(50 - balanceRatioPercent);
|
|
90
|
+
healthScore -= imbalance / 50 * 20;
|
|
91
|
+
if (isBalanced && utilizationPercent < 50) healthScore += 10;
|
|
92
|
+
healthScore = Math.max(0, Math.min(100, healthScore));
|
|
93
|
+
return {
|
|
94
|
+
channelId: channel.channel_id,
|
|
95
|
+
peerId: channel.peer_id,
|
|
96
|
+
localBalanceCkb: localBalance,
|
|
97
|
+
remoteBalanceCkb: remoteBalance,
|
|
98
|
+
totalCapacityCkb: totalCapacity,
|
|
99
|
+
utilizationPercent,
|
|
100
|
+
balanceRatioPercent,
|
|
101
|
+
isBalanced,
|
|
102
|
+
pendingLocalCkb: pendingLocal,
|
|
103
|
+
pendingRemoteCkb: pendingRemote,
|
|
104
|
+
availableToSendCkb: availableToSend,
|
|
105
|
+
availableToReceiveCkb: availableToReceive,
|
|
106
|
+
healthScore,
|
|
107
|
+
state: channel.state.state_name
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* Identify liquidity shortfalls and gaps
|
|
112
|
+
*/
|
|
113
|
+
identifyLiquidityGaps(metrics, _totalSendable) {
|
|
114
|
+
const gaps = [];
|
|
115
|
+
const sendCapableCount = metrics.filter((ch) => ch.availableToSendCkb > 0).length;
|
|
116
|
+
if (sendCapableCount === 0 && metrics.length > 0) {
|
|
117
|
+
gaps.push({
|
|
118
|
+
amount: 100,
|
|
119
|
+
// Arbitrary amount - need to rebalance
|
|
120
|
+
reason: "No channels currently capable of sending. All liquidity on remote side.",
|
|
121
|
+
severity: "high",
|
|
122
|
+
affectedChannels: metrics.map((ch) => ch.channelId)
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
const allLocalHeavy = metrics.every((ch) => ch.balanceRatioPercent > 70);
|
|
126
|
+
const allRemoteHeavy = metrics.every((ch) => ch.balanceRatioPercent < 30);
|
|
127
|
+
if (allLocalHeavy) {
|
|
128
|
+
gaps.push({
|
|
129
|
+
amount: 0,
|
|
130
|
+
// Rebalance doesn't require funding
|
|
131
|
+
reason: "All channels are local-heavy. Cannot receive payments. Need inbound liquidity.",
|
|
132
|
+
severity: "high",
|
|
133
|
+
affectedChannels: metrics.map((ch) => ch.channelId)
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
if (allRemoteHeavy) {
|
|
137
|
+
gaps.push({
|
|
138
|
+
amount: 0,
|
|
139
|
+
// Need to open new channels or rebalance
|
|
140
|
+
reason: "All channels are remote-heavy. Cannot send without rebalancing or new channels.",
|
|
141
|
+
severity: "high",
|
|
142
|
+
affectedChannels: metrics.map((ch) => ch.channelId)
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
const allHighUtilization = metrics.every((ch) => ch.utilizationPercent > 70);
|
|
146
|
+
if (allHighUtilization) {
|
|
147
|
+
gaps.push({
|
|
148
|
+
amount: 0,
|
|
149
|
+
reason: "High utilization in all channels. Many pending payments. Risk of payment failures.",
|
|
150
|
+
severity: "medium",
|
|
151
|
+
affectedChannels: metrics.map((ch) => ch.channelId)
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
return gaps;
|
|
155
|
+
}
|
|
156
|
+
/**
|
|
157
|
+
* Generate rebalance recommendations between channels
|
|
158
|
+
*/
|
|
159
|
+
generateRebalanceRecommendations(metrics) {
|
|
160
|
+
const recommendations = [];
|
|
161
|
+
const localHeavy = metrics.filter((ch) => ch.balanceRatioPercent > 65);
|
|
162
|
+
const remoteHeavy = metrics.filter((ch) => ch.balanceRatioPercent < 35);
|
|
163
|
+
localHeavy.sort((a, b) => b.balanceRatioPercent - a.balanceRatioPercent);
|
|
164
|
+
remoteHeavy.sort((a, b) => a.balanceRatioPercent - b.balanceRatioPercent);
|
|
165
|
+
for (let i = 0; i < Math.min(localHeavy.length, remoteHeavy.length); i++) {
|
|
166
|
+
const source = localHeavy[i];
|
|
167
|
+
const dest = remoteHeavy[i];
|
|
168
|
+
const sourceExcess = source.localBalanceCkb - source.totalCapacityCkb * 0.5;
|
|
169
|
+
const destDeficit = dest.totalCapacityCkb * 0.5 - dest.localBalanceCkb;
|
|
170
|
+
const amountToMove = Math.min(sourceExcess, destDeficit) * 0.8;
|
|
171
|
+
if (amountToMove > 0.1) {
|
|
172
|
+
recommendations.push({
|
|
173
|
+
from: source.channelId,
|
|
174
|
+
to: dest.channelId,
|
|
175
|
+
amountCkb: amountToMove,
|
|
176
|
+
reason: `Rebalance liquidity: source is ${source.balanceRatioPercent.toFixed(0)}% local, destination is ${dest.balanceRatioPercent.toFixed(0)}% local`,
|
|
177
|
+
benefit: `Improves payment success rate by balancing channel liquidity`,
|
|
178
|
+
estimatedRoutingFeeCkb: amountToMove * 1e-3,
|
|
179
|
+
// 0.1% estimated fee
|
|
180
|
+
priority: Math.round(
|
|
181
|
+
(Math.abs(50 - source.balanceRatioPercent) + Math.abs(50 - dest.balanceRatioPercent)) / 20
|
|
182
|
+
)
|
|
183
|
+
// 1-10
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
return recommendations.sort((a, b) => b.priority - a.priority);
|
|
188
|
+
}
|
|
189
|
+
/**
|
|
190
|
+
* Estimate funding needs for future operations
|
|
191
|
+
*/
|
|
192
|
+
estimateFundingNeeds(metrics, gaps) {
|
|
193
|
+
const needs = [];
|
|
194
|
+
const criticalGaps = gaps.filter((g) => g.severity === "high");
|
|
195
|
+
if (criticalGaps.length > 0) {
|
|
196
|
+
const bestChannel = [...metrics].filter((ch) => ch.state === "CHANNEL_READY" /* ChannelReady */).sort((a, b) => {
|
|
197
|
+
const scoreA = a.healthScore + a.totalCapacityCkb / 1e3;
|
|
198
|
+
const scoreB = b.healthScore + b.totalCapacityCkb / 1e3;
|
|
199
|
+
return scoreB - scoreA;
|
|
200
|
+
})[0];
|
|
201
|
+
const fundingAmount = Math.ceil(bestChannel?.totalCapacityCkb || 100);
|
|
202
|
+
needs.push({
|
|
203
|
+
amount: fundingAmount,
|
|
204
|
+
reason: "Critical liquidity gaps detected in current channels",
|
|
205
|
+
optimalChannelPeerId: bestChannel?.peerId,
|
|
206
|
+
urgency: "high"
|
|
207
|
+
});
|
|
208
|
+
}
|
|
209
|
+
const avgCapacity = metrics.length > 0 ? metrics.reduce((sum, ch) => sum + ch.totalCapacityCkb, 0) / metrics.length : 0;
|
|
210
|
+
if (avgCapacity < 100) {
|
|
211
|
+
needs.push({
|
|
212
|
+
amount: 500,
|
|
213
|
+
// Reasonable growth target
|
|
214
|
+
reason: "Average channel capacity is small. Consider larger channels for reliability.",
|
|
215
|
+
urgency: "low"
|
|
216
|
+
});
|
|
217
|
+
}
|
|
218
|
+
return needs;
|
|
219
|
+
}
|
|
220
|
+
/**
|
|
221
|
+
* Estimate runway (days until liquidity depleted at current spending rate)
|
|
222
|
+
*/
|
|
223
|
+
estimateRunway(availableToSend, _metrics) {
|
|
224
|
+
const estimatedDailySpend = 0.1;
|
|
225
|
+
if (availableToSend > 0 && estimatedDailySpend > 0) {
|
|
226
|
+
const days = availableToSend / estimatedDailySpend;
|
|
227
|
+
return {
|
|
228
|
+
daysAtCurrentRate: Math.floor(days),
|
|
229
|
+
estimatedDailySpendCkb: estimatedDailySpend
|
|
230
|
+
};
|
|
231
|
+
}
|
|
232
|
+
return {};
|
|
233
|
+
}
|
|
234
|
+
/**
|
|
235
|
+
* Generate human-readable summary
|
|
236
|
+
*/
|
|
237
|
+
generateSummary(metrics, gaps, fundingNeeds, runway) {
|
|
238
|
+
const parts = [];
|
|
239
|
+
if (metrics.length === 0) {
|
|
240
|
+
return "No channels available. Open a channel to start making payments.";
|
|
241
|
+
}
|
|
242
|
+
parts.push(
|
|
243
|
+
`Channel Health: ${metrics.filter((ch) => ch.healthScore >= 70).length}/${metrics.length} channels healthy`
|
|
244
|
+
);
|
|
245
|
+
const avgScore = metrics.reduce((sum, ch) => sum + ch.healthScore, 0) / metrics.length;
|
|
246
|
+
if (avgScore >= 80) {
|
|
247
|
+
parts.push("Overall liquidity is good \u2713");
|
|
248
|
+
} else if (avgScore >= 60) {
|
|
249
|
+
parts.push("Overall liquidity is fair. Some rebalancing recommended.");
|
|
250
|
+
} else {
|
|
251
|
+
parts.push("Overall liquidity is poor. Rebalancing needed urgently.");
|
|
252
|
+
}
|
|
253
|
+
if (gaps.length > 0) {
|
|
254
|
+
parts.push(`${gaps.length} liquidity gap(s) detected.`);
|
|
255
|
+
}
|
|
256
|
+
if (fundingNeeds.length > 0) {
|
|
257
|
+
parts.push(
|
|
258
|
+
`Need to fund ${fundingNeeds.map((n) => n.amount).join(", ")} CKB to resolve issues.`
|
|
259
|
+
);
|
|
260
|
+
}
|
|
261
|
+
if (runway.daysAtCurrentRate) {
|
|
262
|
+
parts.push(`Estimated runway: ${runway.daysAtCurrentRate} days at current spending rate.`);
|
|
263
|
+
}
|
|
264
|
+
return parts.join(" ");
|
|
265
|
+
}
|
|
266
|
+
/**
|
|
267
|
+
* Get missing liquidity for a specific amount
|
|
268
|
+
*/
|
|
269
|
+
async getMissingLiquidityForAmount(targetCkb) {
|
|
270
|
+
const report = await this.analyzeLiquidity();
|
|
271
|
+
const shortfallCkb = Math.max(0, targetCkb - report.balance.availableToSendCkb);
|
|
272
|
+
const canSend = shortfallCkb === 0;
|
|
273
|
+
let recommendation = "";
|
|
274
|
+
if (canSend) {
|
|
275
|
+
recommendation = `You have enough liquidity to send ${targetCkb} CKB.`;
|
|
276
|
+
} else {
|
|
277
|
+
recommendation = `You need ${shortfallCkb.toFixed(4)} more CKB in send capacity. Consider rebalancing or opening larger channels.`;
|
|
278
|
+
}
|
|
279
|
+
return { canSend, shortfallCkb, recommendation };
|
|
280
|
+
}
|
|
281
|
+
};
|
|
282
|
+
|
|
283
|
+
// src/proxy/cors-proxy.ts
|
|
284
|
+
import http from "http";
|
|
285
|
+
var CorsProxy = class {
|
|
286
|
+
server = null;
|
|
287
|
+
config;
|
|
288
|
+
constructor(config) {
|
|
289
|
+
this.config = config;
|
|
290
|
+
}
|
|
291
|
+
/**
|
|
292
|
+
* Start the CORS proxy server
|
|
293
|
+
*/
|
|
294
|
+
start() {
|
|
295
|
+
return new Promise((resolve, reject) => {
|
|
296
|
+
const targetUrl = new URL(this.config.targetUrl);
|
|
297
|
+
this.server = http.createServer(async (req, res) => {
|
|
298
|
+
const origin = req.headers.origin || "*";
|
|
299
|
+
const allowedOrigin = this.getAllowedOrigin(origin);
|
|
300
|
+
res.setHeader("Access-Control-Allow-Origin", allowedOrigin);
|
|
301
|
+
res.setHeader("Access-Control-Allow-Methods", "POST, OPTIONS");
|
|
302
|
+
res.setHeader("Access-Control-Allow-Headers", "Content-Type, Authorization");
|
|
303
|
+
res.setHeader("Access-Control-Max-Age", "86400");
|
|
304
|
+
if (req.method === "OPTIONS") {
|
|
305
|
+
res.writeHead(204);
|
|
306
|
+
res.end();
|
|
307
|
+
return;
|
|
308
|
+
}
|
|
309
|
+
if (req.method !== "POST") {
|
|
310
|
+
res.writeHead(405, { "Content-Type": "application/json" });
|
|
311
|
+
res.end(JSON.stringify({ error: "Method not allowed" }));
|
|
312
|
+
return;
|
|
313
|
+
}
|
|
314
|
+
const chunks = [];
|
|
315
|
+
req.on("data", (chunk) => chunks.push(chunk));
|
|
316
|
+
req.on("end", () => {
|
|
317
|
+
const body = Buffer.concat(chunks);
|
|
318
|
+
const proxyReq = http.request(
|
|
319
|
+
{
|
|
320
|
+
hostname: targetUrl.hostname,
|
|
321
|
+
port: targetUrl.port || 80,
|
|
322
|
+
path: targetUrl.pathname,
|
|
323
|
+
method: "POST",
|
|
324
|
+
headers: {
|
|
325
|
+
"Content-Type": "application/json",
|
|
326
|
+
"Content-Length": body.length,
|
|
327
|
+
...req.headers.authorization && { Authorization: req.headers.authorization }
|
|
328
|
+
}
|
|
329
|
+
},
|
|
330
|
+
(proxyRes) => {
|
|
331
|
+
const headers = {
|
|
332
|
+
...proxyRes.headers,
|
|
333
|
+
"Access-Control-Allow-Origin": allowedOrigin
|
|
334
|
+
};
|
|
335
|
+
res.writeHead(proxyRes.statusCode || 500, headers);
|
|
336
|
+
proxyRes.pipe(res);
|
|
337
|
+
}
|
|
338
|
+
);
|
|
339
|
+
proxyReq.on("error", (err) => {
|
|
340
|
+
res.writeHead(502, { "Content-Type": "application/json" });
|
|
341
|
+
res.end(JSON.stringify({ error: `Proxy error: ${err.message}` }));
|
|
342
|
+
});
|
|
343
|
+
proxyReq.write(body);
|
|
344
|
+
proxyReq.end();
|
|
345
|
+
});
|
|
346
|
+
req.on("error", (err) => {
|
|
347
|
+
res.writeHead(400, { "Content-Type": "application/json" });
|
|
348
|
+
res.end(JSON.stringify({ error: `Request error: ${err.message}` }));
|
|
349
|
+
});
|
|
350
|
+
});
|
|
351
|
+
this.server.on("error", (err) => {
|
|
352
|
+
reject(err);
|
|
353
|
+
});
|
|
354
|
+
this.server.listen(this.config.port, () => {
|
|
355
|
+
resolve();
|
|
356
|
+
});
|
|
357
|
+
});
|
|
358
|
+
}
|
|
359
|
+
/**
|
|
360
|
+
* Stop the CORS proxy server
|
|
361
|
+
*/
|
|
362
|
+
stop() {
|
|
363
|
+
return new Promise((resolve) => {
|
|
364
|
+
if (this.server) {
|
|
365
|
+
this.server.close(() => {
|
|
366
|
+
this.server = null;
|
|
367
|
+
resolve();
|
|
368
|
+
});
|
|
369
|
+
} else {
|
|
370
|
+
resolve();
|
|
371
|
+
}
|
|
372
|
+
});
|
|
373
|
+
}
|
|
374
|
+
/**
|
|
375
|
+
* Get the allowed origin based on config
|
|
376
|
+
*/
|
|
377
|
+
getAllowedOrigin(requestOrigin) {
|
|
378
|
+
const allowed = this.config.allowedOrigins;
|
|
379
|
+
if (!allowed || allowed === "*") {
|
|
380
|
+
return "*";
|
|
381
|
+
}
|
|
382
|
+
if (Array.isArray(allowed)) {
|
|
383
|
+
return allowed.includes(requestOrigin) ? requestOrigin : allowed[0];
|
|
384
|
+
}
|
|
385
|
+
return allowed;
|
|
386
|
+
}
|
|
387
|
+
/**
|
|
388
|
+
* Get the proxy URL
|
|
389
|
+
*/
|
|
390
|
+
getUrl() {
|
|
391
|
+
return `http://127.0.0.1:${this.config.port}`;
|
|
392
|
+
}
|
|
393
|
+
};
|
|
394
|
+
|
|
395
|
+
// src/security/key-manager.ts
|
|
396
|
+
import { createDecipheriv, createHash, randomBytes, scryptSync } from "crypto";
|
|
397
|
+
import { chmodSync, existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
|
|
398
|
+
import { dirname, join } from "path";
|
|
399
|
+
var SCRYPT_N = 2 ** 14;
|
|
400
|
+
var SCRYPT_R = 8;
|
|
401
|
+
var SCRYPT_P = 1;
|
|
402
|
+
var KEY_LENGTH = 32;
|
|
403
|
+
var SALT_LENGTH = 32;
|
|
404
|
+
var IV_LENGTH = 16;
|
|
405
|
+
var AUTH_TAG_LENGTH = 16;
|
|
406
|
+
var ENCRYPTED_MAGIC = Buffer.from("FIBERENC");
|
|
407
|
+
var KeyManager = class {
|
|
408
|
+
config;
|
|
409
|
+
fiberKeyPath;
|
|
410
|
+
ckbKeyPath;
|
|
411
|
+
constructor(config) {
|
|
412
|
+
this.config = config;
|
|
413
|
+
this.fiberKeyPath = join(config.baseDir, "fiber", "sk");
|
|
414
|
+
this.ckbKeyPath = join(config.baseDir, "ckb", "key");
|
|
415
|
+
}
|
|
416
|
+
/**
|
|
417
|
+
* Initialize keys - generate if they don't exist and autoGenerate is true
|
|
418
|
+
*/
|
|
419
|
+
async initialize() {
|
|
420
|
+
const fiberExists = existsSync(this.fiberKeyPath);
|
|
421
|
+
const ckbExists = existsSync(this.ckbKeyPath);
|
|
422
|
+
if (!fiberExists || !ckbExists) {
|
|
423
|
+
if (!this.config.autoGenerate) {
|
|
424
|
+
throw new Error(
|
|
425
|
+
`Keys not found and autoGenerate is disabled. Missing: ${[!fiberExists && "fiber", !ckbExists && "ckb"].filter(Boolean).join(", ")}`
|
|
426
|
+
);
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
if (!fiberExists) {
|
|
430
|
+
await this.generateKey("fiber");
|
|
431
|
+
}
|
|
432
|
+
if (!ckbExists) {
|
|
433
|
+
await this.generateKey("ckb");
|
|
434
|
+
}
|
|
435
|
+
return {
|
|
436
|
+
fiber: await this.getKeyInfo("fiber"),
|
|
437
|
+
ckb: await this.getKeyInfo("ckb")
|
|
438
|
+
};
|
|
439
|
+
}
|
|
440
|
+
/**
|
|
441
|
+
* Generate a new key
|
|
442
|
+
*/
|
|
443
|
+
async generateKey(type) {
|
|
444
|
+
const keyPath = type === "fiber" ? this.fiberKeyPath : this.ckbKeyPath;
|
|
445
|
+
const keyDir = dirname(keyPath);
|
|
446
|
+
if (!existsSync(keyDir)) {
|
|
447
|
+
mkdirSync(keyDir, { recursive: true });
|
|
448
|
+
}
|
|
449
|
+
const privateKey = randomBytes(32);
|
|
450
|
+
let keyData;
|
|
451
|
+
if (type === "fiber") {
|
|
452
|
+
keyData = privateKey;
|
|
453
|
+
} else {
|
|
454
|
+
keyData = privateKey.toString("hex");
|
|
455
|
+
}
|
|
456
|
+
writeFileSync(keyPath, keyData);
|
|
457
|
+
chmodSync(keyPath, 384);
|
|
458
|
+
return this.getKeyInfo(type);
|
|
459
|
+
}
|
|
460
|
+
/**
|
|
461
|
+
* Get information about a key (without exposing the private key)
|
|
462
|
+
*/
|
|
463
|
+
async getKeyInfo(type) {
|
|
464
|
+
const keyPath = type === "fiber" ? this.fiberKeyPath : this.ckbKeyPath;
|
|
465
|
+
if (!existsSync(keyPath)) {
|
|
466
|
+
throw new Error(`Key not found: ${keyPath}`);
|
|
467
|
+
}
|
|
468
|
+
const keyData = readFileSync(keyPath);
|
|
469
|
+
const encrypted = this.isEncrypted(keyData);
|
|
470
|
+
const privateKey = await this.loadPrivateKey(type);
|
|
471
|
+
const publicKey = this.derivePublicKey(privateKey);
|
|
472
|
+
return {
|
|
473
|
+
publicKey,
|
|
474
|
+
encrypted,
|
|
475
|
+
path: keyPath,
|
|
476
|
+
createdAt: Date.now()
|
|
477
|
+
// TODO: get actual file creation time
|
|
478
|
+
};
|
|
479
|
+
}
|
|
480
|
+
/**
|
|
481
|
+
* Load and decrypt a private key (for internal use only)
|
|
482
|
+
* This should NEVER be exposed to the LLM context
|
|
483
|
+
*/
|
|
484
|
+
async loadPrivateKey(type) {
|
|
485
|
+
const keyPath = type === "fiber" ? this.fiberKeyPath : this.ckbKeyPath;
|
|
486
|
+
const keyData = readFileSync(keyPath);
|
|
487
|
+
if (this.isEncrypted(keyData)) {
|
|
488
|
+
if (!this.config.encryptionPassword) {
|
|
489
|
+
throw new Error("Key is encrypted but no password provided");
|
|
490
|
+
}
|
|
491
|
+
return this.decryptKey(keyData, this.config.encryptionPassword);
|
|
492
|
+
}
|
|
493
|
+
if (type === "fiber") {
|
|
494
|
+
return keyData;
|
|
495
|
+
} else {
|
|
496
|
+
const hexString = keyData.toString("utf-8").trim();
|
|
497
|
+
return Buffer.from(hexString, "hex");
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
/**
|
|
501
|
+
* Export keys for use with the Fiber node process
|
|
502
|
+
* Returns the password to use for FIBER_SECRET_KEY_PASSWORD env var
|
|
503
|
+
*/
|
|
504
|
+
getNodeKeyConfig() {
|
|
505
|
+
return {
|
|
506
|
+
password: this.config.encryptionPassword
|
|
507
|
+
};
|
|
508
|
+
}
|
|
509
|
+
/**
|
|
510
|
+
* Check if key data is encrypted
|
|
511
|
+
*/
|
|
512
|
+
isEncrypted(data) {
|
|
513
|
+
return data.length >= ENCRYPTED_MAGIC.length && data.subarray(0, ENCRYPTED_MAGIC.length).equals(ENCRYPTED_MAGIC);
|
|
514
|
+
}
|
|
515
|
+
/**
|
|
516
|
+
* Decrypt a key
|
|
517
|
+
*/
|
|
518
|
+
decryptKey(data, password) {
|
|
519
|
+
let offset = ENCRYPTED_MAGIC.length;
|
|
520
|
+
const salt = data.subarray(offset, offset + SALT_LENGTH);
|
|
521
|
+
offset += SALT_LENGTH;
|
|
522
|
+
const iv = data.subarray(offset, offset + IV_LENGTH);
|
|
523
|
+
offset += IV_LENGTH;
|
|
524
|
+
const authTag = data.subarray(offset, offset + AUTH_TAG_LENGTH);
|
|
525
|
+
offset += AUTH_TAG_LENGTH;
|
|
526
|
+
const encrypted = data.subarray(offset);
|
|
527
|
+
const derivedKey = scryptSync(password, salt, KEY_LENGTH, {
|
|
528
|
+
N: SCRYPT_N,
|
|
529
|
+
r: SCRYPT_R,
|
|
530
|
+
p: SCRYPT_P
|
|
531
|
+
});
|
|
532
|
+
const decipher = createDecipheriv("aes-256-gcm", derivedKey, iv);
|
|
533
|
+
decipher.setAuthTag(authTag);
|
|
534
|
+
return Buffer.concat([decipher.update(encrypted), decipher.final()]);
|
|
535
|
+
}
|
|
536
|
+
/**
|
|
537
|
+
* Derive public key from private key (secp256k1)
|
|
538
|
+
* This is a simplified version - in production, use a proper secp256k1 library
|
|
539
|
+
*/
|
|
540
|
+
derivePublicKey(privateKey) {
|
|
541
|
+
const hash = createHash("sha256").update(privateKey).digest();
|
|
542
|
+
return `0x${hash.toString("hex")}`;
|
|
543
|
+
}
|
|
544
|
+
};
|
|
545
|
+
function createKeyManager(baseDir, options) {
|
|
546
|
+
const password = process.env.FIBER_KEY_PASSWORD || options?.encryptionPassword;
|
|
547
|
+
return new KeyManager({
|
|
548
|
+
baseDir,
|
|
549
|
+
encryptionPassword: password,
|
|
550
|
+
autoGenerate: options?.autoGenerate ?? true
|
|
551
|
+
});
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
// src/security/policy-engine.ts
|
|
555
|
+
var PolicyEngine = class {
|
|
556
|
+
policy;
|
|
557
|
+
auditLog = [];
|
|
558
|
+
spendingState;
|
|
559
|
+
rateLimitState;
|
|
560
|
+
constructor(policy) {
|
|
561
|
+
this.policy = policy;
|
|
562
|
+
this.spendingState = {
|
|
563
|
+
...policy.spending ?? {
|
|
564
|
+
maxPerTransaction: "0x0",
|
|
565
|
+
maxPerWindow: "0x0",
|
|
566
|
+
windowSeconds: 3600
|
|
567
|
+
},
|
|
568
|
+
currentSpent: "0x0",
|
|
569
|
+
windowStart: Date.now()
|
|
570
|
+
};
|
|
571
|
+
this.rateLimitState = {
|
|
572
|
+
...policy.rateLimit ?? {
|
|
573
|
+
maxTransactions: 0,
|
|
574
|
+
windowSeconds: 3600,
|
|
575
|
+
cooldownSeconds: 0
|
|
576
|
+
},
|
|
577
|
+
currentCount: 0,
|
|
578
|
+
windowStart: Date.now(),
|
|
579
|
+
lastTransaction: 0
|
|
580
|
+
};
|
|
581
|
+
}
|
|
582
|
+
/**
|
|
583
|
+
* Check if a payment is allowed by the policy
|
|
584
|
+
*/
|
|
585
|
+
checkPayment(params) {
|
|
586
|
+
const violations = [];
|
|
587
|
+
let requiresConfirmation = false;
|
|
588
|
+
if (!this.policy.enabled) {
|
|
589
|
+
return { allowed: true, violations: [], requiresConfirmation: false };
|
|
590
|
+
}
|
|
591
|
+
const amount = fromHex(params.amount);
|
|
592
|
+
if (this.policy.spending) {
|
|
593
|
+
const maxPerTx = fromHex(this.policy.spending.maxPerTransaction);
|
|
594
|
+
if (amount > maxPerTx) {
|
|
595
|
+
violations.push({
|
|
596
|
+
type: "SPENDING_LIMIT_PER_TX",
|
|
597
|
+
message: `Amount ${amount} exceeds per-transaction limit of ${maxPerTx}`,
|
|
598
|
+
details: {
|
|
599
|
+
requested: params.amount,
|
|
600
|
+
limit: this.policy.spending.maxPerTransaction
|
|
601
|
+
}
|
|
602
|
+
});
|
|
603
|
+
}
|
|
604
|
+
this.refreshSpendingWindow();
|
|
605
|
+
const currentSpent = fromHex(this.spendingState.currentSpent);
|
|
606
|
+
const maxPerWindow = fromHex(this.policy.spending.maxPerWindow);
|
|
607
|
+
if (currentSpent + amount > maxPerWindow) {
|
|
608
|
+
const remaining = maxPerWindow - currentSpent;
|
|
609
|
+
violations.push({
|
|
610
|
+
type: "SPENDING_LIMIT_PER_WINDOW",
|
|
611
|
+
message: `Amount ${amount} would exceed window limit. Remaining: ${remaining}`,
|
|
612
|
+
details: {
|
|
613
|
+
requested: params.amount,
|
|
614
|
+
limit: this.policy.spending.maxPerWindow,
|
|
615
|
+
remaining: toHex(remaining > 0n ? remaining : 0n)
|
|
616
|
+
}
|
|
617
|
+
});
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
if (this.policy.rateLimit) {
|
|
621
|
+
this.refreshRateLimitWindow();
|
|
622
|
+
if ((this.rateLimitState.currentCount ?? 0) >= this.policy.rateLimit.maxTransactions) {
|
|
623
|
+
violations.push({
|
|
624
|
+
type: "RATE_LIMIT_EXCEEDED",
|
|
625
|
+
message: `Rate limit of ${this.policy.rateLimit.maxTransactions} transactions per ${this.policy.rateLimit.windowSeconds}s exceeded`,
|
|
626
|
+
details: {}
|
|
627
|
+
});
|
|
628
|
+
}
|
|
629
|
+
const now = Date.now();
|
|
630
|
+
const cooldownMs = this.policy.rateLimit.cooldownSeconds * 1e3;
|
|
631
|
+
const timeSinceLast = now - (this.rateLimitState.lastTransaction || 0);
|
|
632
|
+
if (timeSinceLast < cooldownMs) {
|
|
633
|
+
violations.push({
|
|
634
|
+
type: "RATE_LIMIT_COOLDOWN",
|
|
635
|
+
message: `Cooldown period not elapsed. Wait ${Math.ceil((cooldownMs - timeSinceLast) / 1e3)}s`,
|
|
636
|
+
details: {
|
|
637
|
+
cooldownRemaining: Math.ceil((cooldownMs - timeSinceLast) / 1e3)
|
|
638
|
+
}
|
|
639
|
+
});
|
|
640
|
+
}
|
|
641
|
+
}
|
|
642
|
+
if (this.policy.recipients && params.recipient) {
|
|
643
|
+
if (this.policy.recipients.blocklist?.includes(params.recipient)) {
|
|
644
|
+
violations.push({
|
|
645
|
+
type: "RECIPIENT_BLOCKED",
|
|
646
|
+
message: `Recipient ${params.recipient} is blocklisted`,
|
|
647
|
+
details: { recipient: params.recipient }
|
|
648
|
+
});
|
|
649
|
+
}
|
|
650
|
+
if (this.policy.recipients.allowlist && this.policy.recipients.allowlist.length > 0 && !this.policy.recipients.allowlist.includes(params.recipient) && !this.policy.recipients.allowUnknown) {
|
|
651
|
+
violations.push({
|
|
652
|
+
type: "RECIPIENT_NOT_ALLOWED",
|
|
653
|
+
message: `Recipient ${params.recipient} is not in allowlist`,
|
|
654
|
+
details: { recipient: params.recipient }
|
|
655
|
+
});
|
|
656
|
+
}
|
|
657
|
+
}
|
|
658
|
+
if (this.policy.confirmationThreshold) {
|
|
659
|
+
const threshold = fromHex(this.policy.confirmationThreshold);
|
|
660
|
+
if (amount > threshold) {
|
|
661
|
+
requiresConfirmation = true;
|
|
662
|
+
violations.push({
|
|
663
|
+
type: "REQUIRES_CONFIRMATION",
|
|
664
|
+
message: `Amount ${amount} exceeds confirmation threshold of ${threshold}`,
|
|
665
|
+
details: {
|
|
666
|
+
requested: params.amount,
|
|
667
|
+
limit: this.policy.confirmationThreshold
|
|
668
|
+
}
|
|
669
|
+
});
|
|
670
|
+
}
|
|
671
|
+
}
|
|
672
|
+
return {
|
|
673
|
+
allowed: violations.filter((v) => v.type !== "REQUIRES_CONFIRMATION").length === 0,
|
|
674
|
+
violations,
|
|
675
|
+
requiresConfirmation
|
|
676
|
+
};
|
|
677
|
+
}
|
|
678
|
+
/**
|
|
679
|
+
* Check if a channel operation is allowed
|
|
680
|
+
*/
|
|
681
|
+
checkChannelOperation(params) {
|
|
682
|
+
const violations = [];
|
|
683
|
+
if (!this.policy.enabled || !this.policy.channels) {
|
|
684
|
+
return { allowed: true, violations: [], requiresConfirmation: false };
|
|
685
|
+
}
|
|
686
|
+
const { channels } = this.policy;
|
|
687
|
+
if (params.operation === "open") {
|
|
688
|
+
if (!channels.allowOpen) {
|
|
689
|
+
violations.push({
|
|
690
|
+
type: "CHANNEL_OPEN_NOT_ALLOWED",
|
|
691
|
+
message: "Channel opening is not allowed by policy",
|
|
692
|
+
details: {}
|
|
693
|
+
});
|
|
694
|
+
}
|
|
695
|
+
if (params.fundingAmount && channels.maxFundingAmount) {
|
|
696
|
+
const funding = fromHex(params.fundingAmount);
|
|
697
|
+
const max = fromHex(channels.maxFundingAmount);
|
|
698
|
+
if (funding > max) {
|
|
699
|
+
violations.push({
|
|
700
|
+
type: "CHANNEL_FUNDING_EXCEEDS_MAX",
|
|
701
|
+
message: `Funding amount ${funding} exceeds maximum ${max}`,
|
|
702
|
+
details: {
|
|
703
|
+
requested: params.fundingAmount,
|
|
704
|
+
limit: channels.maxFundingAmount
|
|
705
|
+
}
|
|
706
|
+
});
|
|
707
|
+
}
|
|
708
|
+
}
|
|
709
|
+
if (params.fundingAmount && channels.minFundingAmount) {
|
|
710
|
+
const funding = fromHex(params.fundingAmount);
|
|
711
|
+
const min = fromHex(channels.minFundingAmount);
|
|
712
|
+
if (funding < min) {
|
|
713
|
+
violations.push({
|
|
714
|
+
type: "CHANNEL_FUNDING_BELOW_MIN",
|
|
715
|
+
message: `Funding amount ${funding} below minimum ${min}`,
|
|
716
|
+
details: {
|
|
717
|
+
requested: params.fundingAmount,
|
|
718
|
+
limit: channels.minFundingAmount
|
|
719
|
+
}
|
|
720
|
+
});
|
|
721
|
+
}
|
|
722
|
+
}
|
|
723
|
+
if (channels.maxChannels && params.currentChannelCount !== void 0 && params.currentChannelCount >= channels.maxChannels) {
|
|
724
|
+
violations.push({
|
|
725
|
+
type: "MAX_CHANNELS_REACHED",
|
|
726
|
+
message: `Maximum channel count of ${channels.maxChannels} reached`,
|
|
727
|
+
details: {}
|
|
728
|
+
});
|
|
729
|
+
}
|
|
730
|
+
}
|
|
731
|
+
if (params.operation === "close" && !channels.allowClose) {
|
|
732
|
+
violations.push({
|
|
733
|
+
type: "CHANNEL_CLOSE_NOT_ALLOWED",
|
|
734
|
+
message: "Channel closing is not allowed by policy",
|
|
735
|
+
details: {}
|
|
736
|
+
});
|
|
737
|
+
}
|
|
738
|
+
if (params.operation === "force_close" && !channels.allowForceClose) {
|
|
739
|
+
violations.push({
|
|
740
|
+
type: "CHANNEL_FORCE_CLOSE_NOT_ALLOWED",
|
|
741
|
+
message: "Force channel closing is not allowed by policy",
|
|
742
|
+
details: {}
|
|
743
|
+
});
|
|
744
|
+
}
|
|
745
|
+
return {
|
|
746
|
+
allowed: violations.length === 0,
|
|
747
|
+
violations,
|
|
748
|
+
requiresConfirmation: false
|
|
749
|
+
};
|
|
750
|
+
}
|
|
751
|
+
/**
|
|
752
|
+
* Record a successful payment (updates spending and rate limit state)
|
|
753
|
+
*/
|
|
754
|
+
recordPayment(amount) {
|
|
755
|
+
this.refreshSpendingWindow();
|
|
756
|
+
this.refreshRateLimitWindow();
|
|
757
|
+
const currentSpent = fromHex(this.spendingState.currentSpent);
|
|
758
|
+
const paymentAmount = fromHex(amount);
|
|
759
|
+
this.spendingState.currentSpent = toHex(currentSpent + paymentAmount);
|
|
760
|
+
this.rateLimitState.currentCount = (this.rateLimitState.currentCount ?? 0) + 1;
|
|
761
|
+
this.rateLimitState.lastTransaction = Date.now();
|
|
762
|
+
}
|
|
763
|
+
/**
|
|
764
|
+
* Add an entry to the audit log
|
|
765
|
+
*/
|
|
766
|
+
addAuditEntry(action, success, details, violations) {
|
|
767
|
+
if (!this.policy.auditLogging) return;
|
|
768
|
+
this.auditLog.push({
|
|
769
|
+
timestamp: Date.now(),
|
|
770
|
+
action,
|
|
771
|
+
success,
|
|
772
|
+
details,
|
|
773
|
+
policyViolations: violations
|
|
774
|
+
});
|
|
775
|
+
if (this.auditLog.length > 1e3) {
|
|
776
|
+
this.auditLog = this.auditLog.slice(-1e3);
|
|
777
|
+
}
|
|
778
|
+
}
|
|
779
|
+
/**
|
|
780
|
+
* Get the audit log
|
|
781
|
+
*/
|
|
782
|
+
getAuditLog(options) {
|
|
783
|
+
let log = this.auditLog;
|
|
784
|
+
const since = options?.since;
|
|
785
|
+
if (since !== void 0) {
|
|
786
|
+
log = log.filter((entry) => entry.timestamp >= since);
|
|
787
|
+
}
|
|
788
|
+
if (options?.limit) {
|
|
789
|
+
log = log.slice(-options.limit);
|
|
790
|
+
}
|
|
791
|
+
return log;
|
|
792
|
+
}
|
|
793
|
+
/**
|
|
794
|
+
* Get remaining spending allowance
|
|
795
|
+
*/
|
|
796
|
+
getRemainingAllowance() {
|
|
797
|
+
this.refreshSpendingWindow();
|
|
798
|
+
const maxPerTx = this.policy.spending ? fromHex(this.policy.spending.maxPerTransaction) : BigInt(Number.MAX_SAFE_INTEGER);
|
|
799
|
+
const maxPerWindow = this.policy.spending ? fromHex(this.policy.spending.maxPerWindow) : BigInt(Number.MAX_SAFE_INTEGER);
|
|
800
|
+
const currentSpent = fromHex(this.spendingState.currentSpent);
|
|
801
|
+
const remainingWindow = maxPerWindow - currentSpent;
|
|
802
|
+
return {
|
|
803
|
+
perTransaction: maxPerTx,
|
|
804
|
+
perWindow: remainingWindow > 0n ? remainingWindow : 0n
|
|
805
|
+
};
|
|
806
|
+
}
|
|
807
|
+
/**
|
|
808
|
+
* Update the policy
|
|
809
|
+
*/
|
|
810
|
+
updatePolicy(newPolicy) {
|
|
811
|
+
this.policy = { ...this.policy, ...newPolicy };
|
|
812
|
+
this.addAuditEntry("POLICY_UPDATED", true, { newPolicy });
|
|
813
|
+
}
|
|
814
|
+
/**
|
|
815
|
+
* Get the current policy
|
|
816
|
+
*/
|
|
817
|
+
getPolicy() {
|
|
818
|
+
return { ...this.policy };
|
|
819
|
+
}
|
|
820
|
+
// ===========================================================================
|
|
821
|
+
// Private Methods
|
|
822
|
+
// ===========================================================================
|
|
823
|
+
refreshSpendingWindow() {
|
|
824
|
+
if (!this.policy.spending) return;
|
|
825
|
+
const now = Date.now();
|
|
826
|
+
const windowMs = this.policy.spending.windowSeconds * 1e3;
|
|
827
|
+
const windowStart = this.spendingState.windowStart || now;
|
|
828
|
+
if (now - windowStart >= windowMs) {
|
|
829
|
+
this.spendingState.currentSpent = "0x0";
|
|
830
|
+
this.spendingState.windowStart = now;
|
|
831
|
+
}
|
|
832
|
+
}
|
|
833
|
+
refreshRateLimitWindow() {
|
|
834
|
+
if (!this.policy.rateLimit) return;
|
|
835
|
+
const now = Date.now();
|
|
836
|
+
const windowMs = this.policy.rateLimit.windowSeconds * 1e3;
|
|
837
|
+
const windowStart = this.rateLimitState.windowStart || now;
|
|
838
|
+
if (now - windowStart >= windowMs) {
|
|
839
|
+
this.rateLimitState.currentCount = 0;
|
|
840
|
+
this.rateLimitState.windowStart = now;
|
|
841
|
+
}
|
|
842
|
+
}
|
|
843
|
+
};
|
|
844
|
+
|
|
845
|
+
// src/verification/invoice-verifier.ts
|
|
846
|
+
var InvoiceVerifier = class {
|
|
847
|
+
constructor(rpc) {
|
|
848
|
+
this.rpc = rpc;
|
|
849
|
+
}
|
|
850
|
+
/**
|
|
851
|
+
* Fully validate an invoice before payment
|
|
852
|
+
*/
|
|
853
|
+
async verifyInvoice(invoiceString) {
|
|
854
|
+
const issues = [];
|
|
855
|
+
const checks = {
|
|
856
|
+
validFormat: false,
|
|
857
|
+
notExpired: false,
|
|
858
|
+
validAmount: false,
|
|
859
|
+
peerConnected: false
|
|
860
|
+
};
|
|
861
|
+
const formatCheck = this.validateInvoiceFormat(invoiceString);
|
|
862
|
+
checks.validFormat = formatCheck.valid;
|
|
863
|
+
if (!formatCheck.valid) {
|
|
864
|
+
issues.push({
|
|
865
|
+
type: "critical",
|
|
866
|
+
code: "INVALID_INVOICE_FORMAT",
|
|
867
|
+
message: formatCheck.error || "Invoice format is invalid"
|
|
868
|
+
});
|
|
869
|
+
}
|
|
870
|
+
let invoice = null;
|
|
871
|
+
if (checks.validFormat) {
|
|
872
|
+
try {
|
|
873
|
+
const result = await this.rpc.parseInvoice({ invoice: invoiceString });
|
|
874
|
+
invoice = result.invoice;
|
|
875
|
+
} catch (error) {
|
|
876
|
+
issues.push({
|
|
877
|
+
type: "critical",
|
|
878
|
+
code: "PARSE_INVOICE_FAILED",
|
|
879
|
+
message: `Failed to parse invoice: ${error instanceof Error ? error.message : "Unknown error"}`
|
|
880
|
+
});
|
|
881
|
+
}
|
|
882
|
+
}
|
|
883
|
+
const details = {
|
|
884
|
+
paymentHash: invoice?.data.payment_hash || "unknown",
|
|
885
|
+
amountCkb: invoice?.amount ? shannonsToCkb(invoice.amount) : 0,
|
|
886
|
+
expiresAt: this.getExpiryTimestamp(invoice),
|
|
887
|
+
description: this.getDescription(invoice),
|
|
888
|
+
isExpired: invoice ? this.isInvoiceExpired(invoice) : true
|
|
889
|
+
};
|
|
890
|
+
if (invoice) {
|
|
891
|
+
if (!details.isExpired) {
|
|
892
|
+
checks.notExpired = true;
|
|
893
|
+
} else {
|
|
894
|
+
issues.push({
|
|
895
|
+
type: "critical",
|
|
896
|
+
code: "INVOICE_EXPIRED",
|
|
897
|
+
message: `Invoice expired at ${new Date(details.expiresAt).toISOString()}`
|
|
898
|
+
});
|
|
899
|
+
}
|
|
900
|
+
}
|
|
901
|
+
if (invoice?.amount) {
|
|
902
|
+
const validAmount = this.validateAmount(invoice.amount);
|
|
903
|
+
checks.validAmount = validAmount.valid;
|
|
904
|
+
if (!validAmount.valid) {
|
|
905
|
+
issues.push({
|
|
906
|
+
type: "critical",
|
|
907
|
+
code: validAmount.code || "INVALID_AMOUNT",
|
|
908
|
+
message: validAmount.message || "Amount validation failed"
|
|
909
|
+
});
|
|
910
|
+
}
|
|
911
|
+
}
|
|
912
|
+
const payeePublicKey = this.extractNodeIdFromInvoice(invoice);
|
|
913
|
+
try {
|
|
914
|
+
const peers = await this.rpc.listPeers();
|
|
915
|
+
if (payeePublicKey) {
|
|
916
|
+
if (peers.peers && peers.peers.length > 0) {
|
|
917
|
+
checks.peerConnected = true;
|
|
918
|
+
} else {
|
|
919
|
+
issues.push({
|
|
920
|
+
type: "warning",
|
|
921
|
+
code: "NO_PEERS_CONNECTED",
|
|
922
|
+
message: `No peers connected. Cannot route payment to payee ${payeePublicKey.slice(0, 16)}...`
|
|
923
|
+
});
|
|
924
|
+
}
|
|
925
|
+
} else {
|
|
926
|
+
if (peers.peers && peers.peers.length > 0) {
|
|
927
|
+
checks.peerConnected = true;
|
|
928
|
+
} else {
|
|
929
|
+
issues.push({
|
|
930
|
+
type: "warning",
|
|
931
|
+
code: "NO_PEERS_CONNECTED",
|
|
932
|
+
message: "No peers currently connected. Payment may fail."
|
|
933
|
+
});
|
|
934
|
+
}
|
|
935
|
+
}
|
|
936
|
+
} catch {
|
|
937
|
+
issues.push({
|
|
938
|
+
type: "warning",
|
|
939
|
+
code: "PEER_CHECK_FAILED",
|
|
940
|
+
message: "Could not verify peer connectivity"
|
|
941
|
+
});
|
|
942
|
+
}
|
|
943
|
+
const criticalIssues = issues.filter((i) => i.type === "critical");
|
|
944
|
+
const valid = criticalIssues.length === 0;
|
|
945
|
+
let recommendation;
|
|
946
|
+
let reason;
|
|
947
|
+
if (!valid) {
|
|
948
|
+
recommendation = "reject";
|
|
949
|
+
reason = `Invoice has ${criticalIssues.length} critical issue(s): ${criticalIssues.map((i) => i.code).join(", ")}`;
|
|
950
|
+
} else if (issues.length > 0) {
|
|
951
|
+
recommendation = "warn";
|
|
952
|
+
reason = `Invoice is valid but has warnings: ${issues.map((i) => i.code).join(", ")}`;
|
|
953
|
+
} else {
|
|
954
|
+
recommendation = "proceed";
|
|
955
|
+
reason = "Invoice is valid and safe to pay";
|
|
956
|
+
}
|
|
957
|
+
return {
|
|
958
|
+
valid,
|
|
959
|
+
details,
|
|
960
|
+
peer: {
|
|
961
|
+
nodeId: payeePublicKey,
|
|
962
|
+
// Use the already-extracted payee public key
|
|
963
|
+
isConnected: checks.peerConnected || issues.filter((i) => i.code === "NO_PEERS_CONNECTED").length === 0,
|
|
964
|
+
trustScore: this.calculateTrustScore(checks, issues)
|
|
965
|
+
},
|
|
966
|
+
checks: {
|
|
967
|
+
...checks
|
|
968
|
+
},
|
|
969
|
+
issues,
|
|
970
|
+
recommendation,
|
|
971
|
+
reason
|
|
972
|
+
};
|
|
973
|
+
}
|
|
974
|
+
/**
|
|
975
|
+
* Quick format validation (regex-based, before RPC call)
|
|
976
|
+
*/
|
|
977
|
+
validateInvoiceFormat(invoice) {
|
|
978
|
+
const invoiceRegex = /^fib[tb]{1}[a-z0-9]{50,}$/i;
|
|
979
|
+
if (!invoiceRegex.test(invoice)) {
|
|
980
|
+
return {
|
|
981
|
+
valid: false,
|
|
982
|
+
error: "Invoice must be bech32-encoded (fibt... for testnet or fibb... for mainnet)"
|
|
983
|
+
};
|
|
984
|
+
}
|
|
985
|
+
return { valid: true };
|
|
986
|
+
}
|
|
987
|
+
/**
|
|
988
|
+
* Validate amount is positive and reasonable
|
|
989
|
+
*/
|
|
990
|
+
validateAmount(amountHex) {
|
|
991
|
+
try {
|
|
992
|
+
const amount = fromHex(amountHex);
|
|
993
|
+
if (amount <= 0n) {
|
|
994
|
+
return {
|
|
995
|
+
valid: false,
|
|
996
|
+
code: "ZERO_AMOUNT",
|
|
997
|
+
message: "Invoice amount must be greater than zero"
|
|
998
|
+
};
|
|
999
|
+
}
|
|
1000
|
+
const maxShannons = BigInt(1e6) * BigInt(1e8);
|
|
1001
|
+
if (amount > maxShannons) {
|
|
1002
|
+
const amountCkb = Number(amount) / 1e8;
|
|
1003
|
+
return {
|
|
1004
|
+
valid: false,
|
|
1005
|
+
code: "AMOUNT_TOO_LARGE",
|
|
1006
|
+
message: `Invoice amount (${amountCkb.toFixed(2)} CKB) exceeds reasonable maximum`
|
|
1007
|
+
};
|
|
1008
|
+
}
|
|
1009
|
+
return { valid: true };
|
|
1010
|
+
} catch {
|
|
1011
|
+
return {
|
|
1012
|
+
valid: false,
|
|
1013
|
+
code: "INVALID_AMOUNT_FORMAT",
|
|
1014
|
+
message: "Could not parse invoice amount"
|
|
1015
|
+
};
|
|
1016
|
+
}
|
|
1017
|
+
}
|
|
1018
|
+
/**
|
|
1019
|
+
* Check if invoice has expired
|
|
1020
|
+
*/
|
|
1021
|
+
isInvoiceExpired(invoice) {
|
|
1022
|
+
const expiryTimestamp = this.getExpiryTimestamp(invoice);
|
|
1023
|
+
return Date.now() > expiryTimestamp;
|
|
1024
|
+
}
|
|
1025
|
+
/**
|
|
1026
|
+
* Get expiry timestamp in milliseconds
|
|
1027
|
+
*/
|
|
1028
|
+
getExpiryTimestamp(invoice) {
|
|
1029
|
+
if (!invoice) return 0;
|
|
1030
|
+
try {
|
|
1031
|
+
const createdSeconds = fromHex(invoice.data.timestamp);
|
|
1032
|
+
const expiryDeltaSeconds = this.getAttributeU64(invoice.data.attrs, "ExpiryTime") ?? BigInt(60 * 60);
|
|
1033
|
+
return Number(createdSeconds + expiryDeltaSeconds) * 1e3;
|
|
1034
|
+
} catch {
|
|
1035
|
+
}
|
|
1036
|
+
return Date.now() + 60 * 60 * 1e3;
|
|
1037
|
+
}
|
|
1038
|
+
getAttributeU64(attrs, key) {
|
|
1039
|
+
for (const attr of attrs) {
|
|
1040
|
+
if (key in attr) {
|
|
1041
|
+
return fromHex(attr[key]);
|
|
1042
|
+
}
|
|
1043
|
+
}
|
|
1044
|
+
return void 0;
|
|
1045
|
+
}
|
|
1046
|
+
getDescription(invoice) {
|
|
1047
|
+
if (!invoice) return void 0;
|
|
1048
|
+
for (const attr of invoice.data.attrs) {
|
|
1049
|
+
if ("Description" in attr) {
|
|
1050
|
+
return attr.Description;
|
|
1051
|
+
}
|
|
1052
|
+
}
|
|
1053
|
+
return void 0;
|
|
1054
|
+
}
|
|
1055
|
+
/**
|
|
1056
|
+
* Try to extract payee node public key from invoice attributes
|
|
1057
|
+
* The payee public key is embedded in the invoice as a PayeePublicKey attribute
|
|
1058
|
+
*/
|
|
1059
|
+
extractNodeIdFromInvoice(invoice) {
|
|
1060
|
+
if (!invoice) {
|
|
1061
|
+
return void 0;
|
|
1062
|
+
}
|
|
1063
|
+
for (const attr of invoice.data.attrs) {
|
|
1064
|
+
if ("PayeePublicKey" in attr) {
|
|
1065
|
+
return attr.PayeePublicKey;
|
|
1066
|
+
}
|
|
1067
|
+
}
|
|
1068
|
+
return void 0;
|
|
1069
|
+
}
|
|
1070
|
+
/**
|
|
1071
|
+
* Calculate trust score (0-100) based on various factors
|
|
1072
|
+
*/
|
|
1073
|
+
calculateTrustScore(checks, issues) {
|
|
1074
|
+
let score = 100;
|
|
1075
|
+
if (!checks.validFormat) score -= 25;
|
|
1076
|
+
if (!checks.notExpired) score -= 20;
|
|
1077
|
+
if (!checks.validAmount) score -= 15;
|
|
1078
|
+
if (!checks.peerConnected) score -= 10;
|
|
1079
|
+
const warnings = issues.filter((i) => i.type === "warning").length;
|
|
1080
|
+
score -= warnings * 5;
|
|
1081
|
+
return Math.max(0, score);
|
|
1082
|
+
}
|
|
1083
|
+
};
|
|
1084
|
+
|
|
1085
|
+
// src/verification/payment-proof.ts
|
|
1086
|
+
import { createHash as createHash2 } from "crypto";
|
|
1087
|
+
import { readFile, writeFile } from "fs/promises";
|
|
1088
|
+
import { dirname as dirname2 } from "path";
|
|
1089
|
+
var PaymentProofManager = class {
|
|
1090
|
+
proofs = /* @__PURE__ */ new Map();
|
|
1091
|
+
proofFilePath;
|
|
1092
|
+
maxStoredProofs = 1e4;
|
|
1093
|
+
// Prevent unbounded growth
|
|
1094
|
+
constructor(dataDir) {
|
|
1095
|
+
this.proofFilePath = `${dataDir}/payment-proofs.json`;
|
|
1096
|
+
}
|
|
1097
|
+
/**
|
|
1098
|
+
* Load proofs from disk
|
|
1099
|
+
*/
|
|
1100
|
+
async load() {
|
|
1101
|
+
try {
|
|
1102
|
+
const data = await readFile(this.proofFilePath, "utf-8");
|
|
1103
|
+
const proofs = JSON.parse(data);
|
|
1104
|
+
this.proofs.clear();
|
|
1105
|
+
proofs.forEach((proof) => {
|
|
1106
|
+
this.proofs.set(proof.id, proof);
|
|
1107
|
+
});
|
|
1108
|
+
} catch {
|
|
1109
|
+
this.proofs.clear();
|
|
1110
|
+
}
|
|
1111
|
+
}
|
|
1112
|
+
/**
|
|
1113
|
+
* Save proofs to disk
|
|
1114
|
+
*/
|
|
1115
|
+
async save() {
|
|
1116
|
+
const proofs = Array.from(this.proofs.values());
|
|
1117
|
+
const data = JSON.stringify(proofs, null, 2);
|
|
1118
|
+
try {
|
|
1119
|
+
await writeFile(this.proofFilePath, data, "utf-8");
|
|
1120
|
+
} catch (error) {
|
|
1121
|
+
if (error instanceof Error && error.message.includes("ENOENT")) {
|
|
1122
|
+
await this.ensureDirectory();
|
|
1123
|
+
await writeFile(this.proofFilePath, data, "utf-8");
|
|
1124
|
+
} else {
|
|
1125
|
+
throw error;
|
|
1126
|
+
}
|
|
1127
|
+
}
|
|
1128
|
+
}
|
|
1129
|
+
/**
|
|
1130
|
+
* Record a payment proof after successful execution
|
|
1131
|
+
*/
|
|
1132
|
+
recordPaymentProof(paymentHash, invoice, invoiceDetails, execution, status, proof) {
|
|
1133
|
+
const now = Date.now();
|
|
1134
|
+
const fullProof = {
|
|
1135
|
+
id: paymentHash,
|
|
1136
|
+
status,
|
|
1137
|
+
invoice,
|
|
1138
|
+
invoiceDetails,
|
|
1139
|
+
execution,
|
|
1140
|
+
proof: proof || {},
|
|
1141
|
+
verified: false,
|
|
1142
|
+
metadata: {
|
|
1143
|
+
createdAt: now,
|
|
1144
|
+
updatedAt: now
|
|
1145
|
+
}
|
|
1146
|
+
};
|
|
1147
|
+
this.proofs.set(paymentHash, fullProof);
|
|
1148
|
+
if (proof?.preimage) {
|
|
1149
|
+
const isValid = this.verifyPreimageHash(proof.preimage, invoiceDetails.paymentHash);
|
|
1150
|
+
if (isValid) {
|
|
1151
|
+
fullProof.verified = true;
|
|
1152
|
+
fullProof.verifiedAt = now;
|
|
1153
|
+
fullProof.verificationMethod = "preimage_hash";
|
|
1154
|
+
}
|
|
1155
|
+
}
|
|
1156
|
+
if (this.proofs.size > this.maxStoredProofs) {
|
|
1157
|
+
this.pruneOldestProofs(this.maxStoredProofs * 0.8);
|
|
1158
|
+
}
|
|
1159
|
+
return fullProof;
|
|
1160
|
+
}
|
|
1161
|
+
/**
|
|
1162
|
+
* Get proof by payment hash
|
|
1163
|
+
*/
|
|
1164
|
+
getProof(paymentHash) {
|
|
1165
|
+
return this.proofs.get(paymentHash);
|
|
1166
|
+
}
|
|
1167
|
+
/**
|
|
1168
|
+
* Verify a proof's authenticity
|
|
1169
|
+
*/
|
|
1170
|
+
verifyProof(proof) {
|
|
1171
|
+
if (proof.verified && proof.verificationMethod === "preimage_hash") {
|
|
1172
|
+
return {
|
|
1173
|
+
valid: true,
|
|
1174
|
+
reason: "Verified via preimage hash match"
|
|
1175
|
+
};
|
|
1176
|
+
}
|
|
1177
|
+
if (proof.proof?.preimage) {
|
|
1178
|
+
const hashValid = this.verifyPreimageHash(
|
|
1179
|
+
proof.proof.preimage,
|
|
1180
|
+
proof.invoiceDetails.paymentHash
|
|
1181
|
+
);
|
|
1182
|
+
if (!hashValid) {
|
|
1183
|
+
return {
|
|
1184
|
+
valid: false,
|
|
1185
|
+
reason: "Preimage does not hash to payment hash"
|
|
1186
|
+
};
|
|
1187
|
+
}
|
|
1188
|
+
proof.verified = true;
|
|
1189
|
+
proof.verifiedAt = Date.now();
|
|
1190
|
+
proof.verificationMethod = "preimage_hash";
|
|
1191
|
+
return {
|
|
1192
|
+
valid: true,
|
|
1193
|
+
reason: "Preimage verified via hash"
|
|
1194
|
+
};
|
|
1195
|
+
}
|
|
1196
|
+
if (proof.status === "Success") {
|
|
1197
|
+
return {
|
|
1198
|
+
valid: true,
|
|
1199
|
+
reason: "Payment succeeded according to RPC (preimage not available)"
|
|
1200
|
+
};
|
|
1201
|
+
}
|
|
1202
|
+
return {
|
|
1203
|
+
valid: false,
|
|
1204
|
+
reason: "No verification method available"
|
|
1205
|
+
};
|
|
1206
|
+
}
|
|
1207
|
+
/**
|
|
1208
|
+
* Get payment timeline between two timestamps
|
|
1209
|
+
*/
|
|
1210
|
+
getPaymentChain(startTime, endTime) {
|
|
1211
|
+
return Array.from(this.proofs.values()).filter((p) => p.metadata.createdAt >= startTime && p.metadata.createdAt <= endTime).sort((a, b) => a.metadata.createdAt - b.metadata.createdAt);
|
|
1212
|
+
}
|
|
1213
|
+
/**
|
|
1214
|
+
* Get summary statistics
|
|
1215
|
+
*/
|
|
1216
|
+
getSummary() {
|
|
1217
|
+
const proofs = Array.from(this.proofs.values());
|
|
1218
|
+
let verifiedCount = 0;
|
|
1219
|
+
let pendingCount = 0;
|
|
1220
|
+
let failedCount = 0;
|
|
1221
|
+
let totalAmountCkb = 0;
|
|
1222
|
+
let totalFeesCkb = 0;
|
|
1223
|
+
let earliest;
|
|
1224
|
+
let latest;
|
|
1225
|
+
proofs.forEach((proof) => {
|
|
1226
|
+
if (proof.verified) verifiedCount++;
|
|
1227
|
+
else if (proof.status === "Inflight" || proof.status === "Created") pendingCount++;
|
|
1228
|
+
else if (proof.status === "Failed") failedCount++;
|
|
1229
|
+
totalAmountCkb += proof.execution.amountCkb;
|
|
1230
|
+
totalFeesCkb += proof.execution.feeCkb;
|
|
1231
|
+
const createdAt = proof.metadata.createdAt;
|
|
1232
|
+
if (!earliest || createdAt < earliest) earliest = createdAt;
|
|
1233
|
+
if (!latest || createdAt > latest) latest = createdAt;
|
|
1234
|
+
});
|
|
1235
|
+
return {
|
|
1236
|
+
totalProofs: proofs.length,
|
|
1237
|
+
verifiedCount,
|
|
1238
|
+
pendingCount,
|
|
1239
|
+
failedCount,
|
|
1240
|
+
totalAmountCkb,
|
|
1241
|
+
totalFeesCkb,
|
|
1242
|
+
timeRange: {
|
|
1243
|
+
earliest,
|
|
1244
|
+
latest
|
|
1245
|
+
}
|
|
1246
|
+
};
|
|
1247
|
+
}
|
|
1248
|
+
/**
|
|
1249
|
+
* Export proofs as audit report
|
|
1250
|
+
*/
|
|
1251
|
+
exportAuditReport(startTime, endTime) {
|
|
1252
|
+
const proofs = Array.from(this.proofs.values()).filter((p) => {
|
|
1253
|
+
if (!startTime && !endTime) return true;
|
|
1254
|
+
const created = p.metadata.createdAt;
|
|
1255
|
+
if (startTime && created < startTime) return false;
|
|
1256
|
+
if (endTime && created > endTime) return false;
|
|
1257
|
+
return true;
|
|
1258
|
+
}).sort((a, b) => a.metadata.createdAt - b.metadata.createdAt);
|
|
1259
|
+
const lines = [
|
|
1260
|
+
"Payment Audit Report",
|
|
1261
|
+
`Generated: ${(/* @__PURE__ */ new Date()).toISOString()}`,
|
|
1262
|
+
`Time Range: ${startTime ? new Date(startTime).toISOString() : "All"} - ${endTime ? new Date(endTime).toISOString() : "All"}`,
|
|
1263
|
+
`Total Payments: ${proofs.length}`,
|
|
1264
|
+
"",
|
|
1265
|
+
"Payment ID | Status | Amount CKB | Fee CKB | Verified | Created At",
|
|
1266
|
+
"-".repeat(80)
|
|
1267
|
+
];
|
|
1268
|
+
proofs.forEach((p) => {
|
|
1269
|
+
lines.push(
|
|
1270
|
+
`${p.id.slice(0, 16)}... | ${p.status} | ${p.execution.amountCkb.toFixed(4)} | ${p.execution.feeCkb.toFixed(8)} | ${p.verified ? "Yes" : "No"} | ${new Date(p.metadata.createdAt).toISOString()}`
|
|
1271
|
+
);
|
|
1272
|
+
});
|
|
1273
|
+
return lines.join("\n");
|
|
1274
|
+
}
|
|
1275
|
+
/**
|
|
1276
|
+
* Verify preimage hash (SHA256 of preimage = payment_hash)
|
|
1277
|
+
*/
|
|
1278
|
+
verifyPreimageHash(preimage, paymentHash) {
|
|
1279
|
+
try {
|
|
1280
|
+
const preimageHex = preimage.startsWith("0x") ? preimage.slice(2) : preimage;
|
|
1281
|
+
const paymentHashHex = paymentHash.startsWith("0x") ? paymentHash.slice(2) : paymentHash;
|
|
1282
|
+
const preimageBuffer = Buffer.from(preimageHex, "hex");
|
|
1283
|
+
const paymentHashBuffer = Buffer.from(paymentHashHex, "hex");
|
|
1284
|
+
const hash = createHash2("sha256").update(preimageBuffer).digest();
|
|
1285
|
+
return hash.equals(paymentHashBuffer);
|
|
1286
|
+
} catch {
|
|
1287
|
+
return false;
|
|
1288
|
+
}
|
|
1289
|
+
}
|
|
1290
|
+
/**
|
|
1291
|
+
* Remove oldest proofs to keep storage bounded
|
|
1292
|
+
*/
|
|
1293
|
+
pruneOldestProofs(count) {
|
|
1294
|
+
const sorted = Array.from(this.proofs.values()).sort(
|
|
1295
|
+
(a, b) => a.metadata.createdAt - b.metadata.createdAt
|
|
1296
|
+
);
|
|
1297
|
+
const toRemove = sorted.slice(0, Math.floor(sorted.length - count));
|
|
1298
|
+
toRemove.forEach((p) => {
|
|
1299
|
+
this.proofs.delete(p.id);
|
|
1300
|
+
});
|
|
1301
|
+
}
|
|
1302
|
+
/**
|
|
1303
|
+
* Ensure directory exists
|
|
1304
|
+
*/
|
|
1305
|
+
async ensureDirectory() {
|
|
1306
|
+
const dir = dirname2(this.proofFilePath);
|
|
1307
|
+
try {
|
|
1308
|
+
const fs = await import("fs");
|
|
1309
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
1310
|
+
} catch {
|
|
1311
|
+
}
|
|
1312
|
+
}
|
|
1313
|
+
};
|
|
1314
|
+
export {
|
|
1315
|
+
ChannelState,
|
|
1316
|
+
CorsProxy,
|
|
1317
|
+
FiberRpcClient,
|
|
1318
|
+
FiberRpcError,
|
|
1319
|
+
InvoiceVerifier,
|
|
1320
|
+
KeyManager,
|
|
1321
|
+
LiquidityAnalyzer,
|
|
1322
|
+
PaymentProofManager,
|
|
1323
|
+
PolicyEngine,
|
|
1324
|
+
buildMultiaddr,
|
|
1325
|
+
buildMultiaddrFromNodeId,
|
|
1326
|
+
buildMultiaddrFromRpcUrl,
|
|
1327
|
+
ckbToShannons,
|
|
1328
|
+
createKeyManager,
|
|
1329
|
+
fromHex,
|
|
1330
|
+
nodeIdToPeerId,
|
|
1331
|
+
randomBytes32,
|
|
1332
|
+
scriptToAddress,
|
|
1333
|
+
shannonsToCkb,
|
|
1334
|
+
toHex
|
|
1335
|
+
};
|
|
1336
|
+
//# sourceMappingURL=index.js.map
|