@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/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