@fiber-pay/agent 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,1526 @@
1
+ // src/fiber-pay.ts
2
+ import { randomUUID } from "crypto";
3
+ import { join } from "path";
4
+ import { ensureFiberBinary, ProcessManager } from "@fiber-pay/node";
5
+ import { JobManager, SqliteJobStore } from "@fiber-pay/runtime";
6
+ import {
7
+ ChannelState,
8
+ ckbToShannons,
9
+ createKeyManager,
10
+ FiberRpcClient,
11
+ fromHex,
12
+ InvoiceVerifier,
13
+ LiquidityAnalyzer,
14
+ PaymentProofManager,
15
+ PolicyEngine,
16
+ randomBytes32,
17
+ shannonsToCkb,
18
+ toHex
19
+ } from "@fiber-pay/sdk";
20
+ var FiberPay = class {
21
+ config;
22
+ process = null;
23
+ rpc = null;
24
+ policy;
25
+ keys;
26
+ initialized = false;
27
+ invoiceVerifier = null;
28
+ paymentProofManager = null;
29
+ liquidityAnalyzer = null;
30
+ runtimeJobStore = null;
31
+ runtimeJobManager = null;
32
+ constructor(config) {
33
+ this.config = {
34
+ chain: "testnet",
35
+ autoStart: true,
36
+ rpcPort: 8227,
37
+ p2pPort: 8228,
38
+ useRuntimeJobs: true,
39
+ ...config
40
+ };
41
+ const defaultPolicy = {
42
+ name: "default",
43
+ version: "1.0.0",
44
+ enabled: true,
45
+ spending: {
46
+ maxPerTransaction: "0x2540be400",
47
+ // 100 CKB
48
+ maxPerWindow: "0x174876e800",
49
+ // 1000 CKB
50
+ windowSeconds: 3600
51
+ },
52
+ rateLimit: {
53
+ maxTransactions: 100,
54
+ windowSeconds: 3600,
55
+ cooldownSeconds: 1
56
+ },
57
+ recipients: {
58
+ allowUnknown: true
59
+ },
60
+ channels: {
61
+ allowOpen: true,
62
+ allowClose: true,
63
+ allowForceClose: false,
64
+ maxChannels: 10
65
+ },
66
+ auditLogging: true
67
+ };
68
+ this.policy = new PolicyEngine(config.policy || defaultPolicy);
69
+ this.keys = createKeyManager(config.dataDir, {
70
+ encryptionPassword: config.keyPassword,
71
+ autoGenerate: true
72
+ });
73
+ }
74
+ // ===========================================================================
75
+ // Lifecycle Methods
76
+ // ===========================================================================
77
+ /**
78
+ * Initialize the FiberPay instance
79
+ * - Downloads the binary if needed (and autoDownload is true)
80
+ * - Generates or loads keys
81
+ * - Starts the Fiber node (if autoStart is true)
82
+ * - Connects to RPC
83
+ */
84
+ async initialize(options) {
85
+ try {
86
+ let binaryPath = this.config.binaryPath;
87
+ if (!binaryPath) {
88
+ if (this.config.autoDownload !== false) {
89
+ binaryPath = await ensureFiberBinary({
90
+ installDir: `${this.config.dataDir}/bin`,
91
+ onProgress: options?.onDownloadProgress
92
+ });
93
+ } else {
94
+ return this.errorResult(
95
+ new Error("Binary path not provided and autoDownload is disabled"),
96
+ "BINARY_NOT_FOUND",
97
+ true
98
+ );
99
+ }
100
+ }
101
+ const _keyInfo = await this.keys.initialize();
102
+ this.process = new ProcessManager({
103
+ binaryPath,
104
+ dataDir: this.config.dataDir,
105
+ configFilePath: this.config.configFilePath,
106
+ chain: this.config.chain,
107
+ ckbRpcUrl: this.config.ckbRpcUrl,
108
+ keyPassword: this.config.keyPassword,
109
+ rpcListeningAddr: `127.0.0.1:${this.config.rpcPort}`,
110
+ fiberListeningAddr: `/ip4/0.0.0.0/tcp/${this.config.p2pPort}`,
111
+ bootnodeAddrs: this.config.bootnodes
112
+ });
113
+ const rpcUrl = this.config.rpcUrl || `http://127.0.0.1:${this.config.rpcPort}`;
114
+ this.rpc = new FiberRpcClient({
115
+ url: rpcUrl
116
+ });
117
+ if (this.config.autoStart) {
118
+ await this.process.start();
119
+ await this.rpc.waitForReady({ timeout: 6e4 });
120
+ }
121
+ this.invoiceVerifier = new InvoiceVerifier(this.rpc);
122
+ this.paymentProofManager = new PaymentProofManager(this.config.dataDir);
123
+ await this.paymentProofManager.load();
124
+ this.liquidityAnalyzer = new LiquidityAnalyzer(this.rpc);
125
+ if (this.config.useRuntimeJobs !== false) {
126
+ this.runtimeJobStore = new SqliteJobStore(
127
+ this.config.runtimeJobsDbPath ?? join(this.config.dataDir, "runtime-jobs.db")
128
+ );
129
+ this.runtimeJobManager = new JobManager(this.rpc, this.runtimeJobStore);
130
+ this.runtimeJobManager.start();
131
+ }
132
+ this.initialized = true;
133
+ const nodeInfo = await this.rpc.nodeInfo();
134
+ this.policy.addAuditEntry("NODE_STARTED", true, {
135
+ nodeId: nodeInfo.node_id
136
+ });
137
+ return {
138
+ success: true,
139
+ data: {
140
+ nodeId: nodeInfo.node_id
141
+ },
142
+ metadata: { timestamp: Date.now() }
143
+ };
144
+ } catch (error) {
145
+ return this.errorResult(error, "INIT_FAILED", false);
146
+ }
147
+ }
148
+ /**
149
+ * Shutdown the FiberPay instance
150
+ */
151
+ async shutdown() {
152
+ try {
153
+ if (this.paymentProofManager) {
154
+ await this.paymentProofManager.save();
155
+ }
156
+ if (this.runtimeJobManager) {
157
+ await this.runtimeJobManager.stop();
158
+ }
159
+ if (this.runtimeJobStore) {
160
+ this.runtimeJobStore.close();
161
+ }
162
+ if (this.process?.isRunning()) {
163
+ await this.process.stop();
164
+ }
165
+ this.policy.addAuditEntry("NODE_STOPPED", true, {});
166
+ this.initialized = false;
167
+ return { success: true, metadata: { timestamp: Date.now() } };
168
+ } catch (error) {
169
+ return this.errorResult(error, "SHUTDOWN_FAILED", true);
170
+ }
171
+ }
172
+ // ===========================================================================
173
+ // Payment Methods (AI Agent Friendly)
174
+ // ===========================================================================
175
+ /**
176
+ * Pay an invoice or send directly to a node
177
+ *
178
+ * @example
179
+ * // Pay invoice
180
+ * await fiber.pay({ invoice: 'fibt1...' });
181
+ *
182
+ * // Send directly (keysend)
183
+ * await fiber.pay({
184
+ * recipientNodeId: 'QmXXX...',
185
+ * amountCkb: 10,
186
+ * });
187
+ */
188
+ async pay(params) {
189
+ this.ensureInitialized();
190
+ try {
191
+ let amountHex;
192
+ let recipient;
193
+ if (params.invoice) {
194
+ const parsed = await this.getRpc().parseInvoice({ invoice: params.invoice });
195
+ amountHex = parsed.invoice.amount || "0x0";
196
+ recipient = params.invoice;
197
+ } else if (params.recipientNodeId && params.amountCkb) {
198
+ amountHex = ckbToShannons(params.amountCkb);
199
+ recipient = params.recipientNodeId;
200
+ } else {
201
+ return this.errorResult(
202
+ new Error("Either invoice or (recipientNodeId + amountCkb) required"),
203
+ "INVALID_PARAMS",
204
+ true
205
+ );
206
+ }
207
+ const policyCheck = this.policy.checkPayment({
208
+ amount: amountHex,
209
+ recipient
210
+ });
211
+ if (!policyCheck.allowed) {
212
+ this.policy.addAuditEntry("POLICY_VIOLATION", false, params, policyCheck.violations);
213
+ return {
214
+ success: false,
215
+ error: {
216
+ code: "POLICY_VIOLATION",
217
+ message: policyCheck.violations.map((v) => v.message).join("; "),
218
+ recoverable: false,
219
+ suggestion: "Reduce amount or wait for spending window to reset"
220
+ },
221
+ metadata: { timestamp: Date.now(), policyCheck }
222
+ };
223
+ }
224
+ const paymentParams = {
225
+ invoice: params.invoice,
226
+ target_pubkey: params.recipientNodeId,
227
+ amount: params.recipientNodeId ? amountHex : void 0,
228
+ keysend: params.recipientNodeId ? true : void 0,
229
+ max_fee_amount: params.maxFeeCkb ? ckbToShannons(params.maxFeeCkb) : void 0,
230
+ custom_records: params.customRecords,
231
+ max_parts: params.maxParts ? toHex(params.maxParts) : void 0
232
+ };
233
+ let result;
234
+ if (this.runtimeJobManager) {
235
+ const job = await this.runtimeJobManager.ensurePayment(
236
+ {
237
+ invoice: params.invoice,
238
+ sendPaymentParams: paymentParams
239
+ },
240
+ {
241
+ idempotencyKey: params.invoice ? `payment:invoice:${params.invoice}` : void 0
242
+ }
243
+ );
244
+ const terminal = await this.waitForRuntimeJobTerminal(job.id, 12e4);
245
+ if (terminal.type !== "payment" || terminal.state !== "succeeded" || !terminal.result) {
246
+ throw new Error(terminal.error?.message ?? "Runtime payment job failed");
247
+ }
248
+ result = {
249
+ payment_hash: terminal.result.paymentHash,
250
+ status: terminal.result.status,
251
+ fee: terminal.result.fee,
252
+ failed_error: terminal.result.failedError,
253
+ created_at: "0x0",
254
+ last_updated_at: "0x0",
255
+ custom_records: void 0,
256
+ routers: void 0
257
+ };
258
+ } else {
259
+ result = await this.getRpc().sendPayment(paymentParams);
260
+ }
261
+ if (result.status === "Success") {
262
+ this.policy.recordPayment(amountHex);
263
+ }
264
+ const paymentResult = {
265
+ paymentHash: result.payment_hash,
266
+ status: result.status === "Success" ? "success" : result.status === "Failed" ? "failed" : "pending",
267
+ amountCkb: shannonsToCkb(amountHex),
268
+ feeCkb: shannonsToCkb(result.fee),
269
+ failureReason: result.failed_error
270
+ };
271
+ if (this.paymentProofManager && result.status === "Success") {
272
+ this.paymentProofManager.recordPaymentProof(
273
+ result.payment_hash,
274
+ params.invoice || "",
275
+ {
276
+ paymentHash: result.payment_hash,
277
+ amountCkb: paymentResult.amountCkb,
278
+ description: ""
279
+ },
280
+ {
281
+ amountCkb: paymentResult.amountCkb,
282
+ feeCkb: paymentResult.feeCkb,
283
+ actualTimestamp: Date.now(),
284
+ requestTimestamp: Date.now()
285
+ },
286
+ result.status,
287
+ {
288
+ preimage: void 0
289
+ // Would need to get from RPC if available
290
+ }
291
+ );
292
+ this.paymentProofManager.save().catch(() => {
293
+ });
294
+ }
295
+ this.policy.addAuditEntry("PAYMENT_SENT", result.status === "Success", {
296
+ ...paymentResult,
297
+ recipient
298
+ });
299
+ return {
300
+ success: result.status === "Success",
301
+ data: paymentResult,
302
+ metadata: { timestamp: Date.now(), policyCheck }
303
+ };
304
+ } catch (error) {
305
+ return this.errorResult(error, "PAYMENT_FAILED", true);
306
+ }
307
+ }
308
+ /**
309
+ * Create an invoice to receive payment
310
+ *
311
+ * @example
312
+ * const invoice = await fiber.createInvoice({
313
+ * amountCkb: 10,
314
+ * description: 'For coffee',
315
+ * expiryMinutes: 60,
316
+ * });
317
+ * console.log(invoice.data?.invoice); // Share this with payer
318
+ */
319
+ async createInvoice(params) {
320
+ this.ensureInitialized();
321
+ try {
322
+ const amountHex = ckbToShannons(params.amountCkb);
323
+ const expirySeconds = (params.expiryMinutes || 60) * 60;
324
+ const preimage = randomBytes32();
325
+ const invoiceParams = {
326
+ amount: amountHex,
327
+ currency: this.config.chain === "mainnet" ? "Fibb" : "Fibt",
328
+ description: params.description,
329
+ expiry: toHex(expirySeconds),
330
+ payment_preimage: preimage
331
+ };
332
+ let invoiceAddress;
333
+ let paymentHash;
334
+ if (this.runtimeJobManager) {
335
+ const job = await this.runtimeJobManager.manageInvoice({
336
+ action: "create",
337
+ newInvoiceParams: invoiceParams,
338
+ waitForTerminal: false
339
+ });
340
+ const terminal = await this.waitForRuntimeJobTerminal(job.id, 12e4);
341
+ if (terminal.type !== "invoice" || terminal.state !== "succeeded" || !terminal.result?.invoiceAddress || !terminal.result.paymentHash) {
342
+ throw new Error(terminal.error?.message ?? "Runtime invoice job failed");
343
+ }
344
+ invoiceAddress = terminal.result.invoiceAddress;
345
+ paymentHash = terminal.result.paymentHash;
346
+ } else {
347
+ const result = await this.getRpc().newInvoice(invoiceParams);
348
+ invoiceAddress = result.invoice_address;
349
+ paymentHash = result.invoice.data.payment_hash;
350
+ }
351
+ const expiresAt = new Date(Date.now() + expirySeconds * 1e3).toISOString();
352
+ const invoiceResult = {
353
+ invoice: invoiceAddress,
354
+ paymentHash,
355
+ amountCkb: params.amountCkb,
356
+ expiresAt,
357
+ status: "open"
358
+ };
359
+ this.policy.addAuditEntry("INVOICE_CREATED", true, { ...invoiceResult });
360
+ return {
361
+ success: true,
362
+ data: invoiceResult,
363
+ metadata: { timestamp: Date.now() }
364
+ };
365
+ } catch (error) {
366
+ return this.errorResult(error, "INVOICE_FAILED", true);
367
+ }
368
+ }
369
+ /**
370
+ * Get current balance information
371
+ */
372
+ async getBalance() {
373
+ this.ensureInitialized();
374
+ try {
375
+ const channels = await this.getRpc().listChannels({});
376
+ let totalLocal = 0n;
377
+ let totalRemote = 0n;
378
+ let activeChannels = 0;
379
+ for (const channel of channels.channels) {
380
+ if (channel.state.state_name === ChannelState.ChannelReady) {
381
+ totalLocal += fromHex(channel.local_balance);
382
+ totalRemote += fromHex(channel.remote_balance);
383
+ activeChannels++;
384
+ }
385
+ }
386
+ const allowance = this.policy.getRemainingAllowance();
387
+ return {
388
+ success: true,
389
+ data: {
390
+ totalCkb: Number(totalLocal) / 1e8,
391
+ availableToSend: Number(totalLocal) / 1e8,
392
+ availableToReceive: Number(totalRemote) / 1e8,
393
+ channelCount: activeChannels,
394
+ spendingAllowance: Number(allowance.perWindow) / 1e8
395
+ },
396
+ metadata: { timestamp: Date.now() }
397
+ };
398
+ } catch (error) {
399
+ return this.errorResult(error, "BALANCE_FAILED", true);
400
+ }
401
+ }
402
+ /**
403
+ * Check payment status
404
+ */
405
+ async getPaymentStatus(paymentHash) {
406
+ this.ensureInitialized();
407
+ try {
408
+ const result = await this.getRpc().getPayment({ payment_hash: paymentHash });
409
+ return {
410
+ success: true,
411
+ data: {
412
+ paymentHash: result.payment_hash,
413
+ status: result.status === "Success" ? "success" : result.status === "Failed" ? "failed" : "pending",
414
+ amountCkb: 0,
415
+ // Amount not returned from get_payment
416
+ feeCkb: shannonsToCkb(result.fee),
417
+ failureReason: result.failed_error
418
+ },
419
+ metadata: { timestamp: Date.now() }
420
+ };
421
+ } catch (error) {
422
+ return this.errorResult(error, "STATUS_CHECK_FAILED", true);
423
+ }
424
+ }
425
+ /**
426
+ * Check invoice status
427
+ */
428
+ async getInvoiceStatus(paymentHash) {
429
+ this.ensureInitialized();
430
+ try {
431
+ const result = await this.getRpc().getInvoice({ payment_hash: paymentHash });
432
+ const amountCkb = result.invoice.amount ? shannonsToCkb(result.invoice.amount) : 0;
433
+ const expiresAt = this.getInvoiceExpiryIso(result.invoice);
434
+ return {
435
+ success: true,
436
+ data: {
437
+ invoice: result.invoice_address,
438
+ paymentHash: result.invoice.data.payment_hash,
439
+ amountCkb,
440
+ expiresAt,
441
+ status: this.mapInvoiceStatus(result.status)
442
+ },
443
+ metadata: { timestamp: Date.now() }
444
+ };
445
+ } catch (error) {
446
+ return this.errorResult(error, "INVOICE_STATUS_FAILED", true);
447
+ }
448
+ }
449
+ // ===========================================================================
450
+ // Channel Management
451
+ // ===========================================================================
452
+ /**
453
+ * List all channels
454
+ */
455
+ async listChannels() {
456
+ this.ensureInitialized();
457
+ try {
458
+ const result = await this.getRpc().listChannels({});
459
+ const channels = result.channels.map((ch) => ({
460
+ id: ch.channel_id,
461
+ peerId: ch.peer_id,
462
+ localBalanceCkb: shannonsToCkb(ch.local_balance),
463
+ remoteBalanceCkb: shannonsToCkb(ch.remote_balance),
464
+ state: ch.state.state_name,
465
+ isPublic: ch.is_public
466
+ }));
467
+ return {
468
+ success: true,
469
+ data: channels,
470
+ metadata: { timestamp: Date.now() }
471
+ };
472
+ } catch (error) {
473
+ return this.errorResult(error, "LIST_CHANNELS_FAILED", true);
474
+ }
475
+ }
476
+ /**
477
+ * Open a new channel
478
+ */
479
+ async openChannel(params) {
480
+ this.ensureInitialized();
481
+ try {
482
+ const fundingHex = ckbToShannons(params.fundingCkb);
483
+ const channels = await this.getRpc().listChannels({});
484
+ const policyCheck = this.policy.checkChannelOperation({
485
+ operation: "open",
486
+ fundingAmount: fundingHex,
487
+ currentChannelCount: channels.channels.length
488
+ });
489
+ if (!policyCheck.allowed) {
490
+ this.policy.addAuditEntry("POLICY_VIOLATION", false, params, policyCheck.violations);
491
+ return {
492
+ success: false,
493
+ error: {
494
+ code: "POLICY_VIOLATION",
495
+ message: policyCheck.violations.map((v) => v.message).join("; "),
496
+ recoverable: false
497
+ },
498
+ metadata: { timestamp: Date.now(), policyCheck }
499
+ };
500
+ }
501
+ if (params.peer.includes("/")) {
502
+ await this.getRpc().connectPeer({ address: params.peer });
503
+ const peerIdMatch = params.peer.match(/\/p2p\/([^/]+)/);
504
+ if (peerIdMatch) {
505
+ params.peer = peerIdMatch[1];
506
+ }
507
+ }
508
+ const openParams = {
509
+ peer_id: params.peer,
510
+ funding_amount: fundingHex,
511
+ public: params.isPublic ?? true
512
+ };
513
+ const idempotencyKey = params.idempotencyKey ?? `open:${openParams.peer_id}:${randomUUID()}`;
514
+ let temporaryChannelId;
515
+ if (this.runtimeJobManager) {
516
+ const job = await this.runtimeJobManager.manageChannel(
517
+ {
518
+ action: "open",
519
+ openChannelParams: openParams,
520
+ waitForReady: false,
521
+ peerId: params.peer
522
+ },
523
+ { idempotencyKey }
524
+ );
525
+ const terminal = await this.waitForRuntimeJobTerminal(job.id, 12e4);
526
+ if (terminal.type !== "channel" || terminal.state !== "succeeded" || !terminal.result?.temporaryChannelId) {
527
+ throw new Error(terminal.error?.message ?? "Runtime channel open job failed");
528
+ }
529
+ temporaryChannelId = terminal.result.temporaryChannelId;
530
+ } else {
531
+ const result = await this.getRpc().openChannel(openParams);
532
+ temporaryChannelId = result.temporary_channel_id;
533
+ }
534
+ this.policy.addAuditEntry("CHANNEL_OPENED", true, {
535
+ channelId: temporaryChannelId,
536
+ peer: params.peer,
537
+ fundingCkb: params.fundingCkb
538
+ });
539
+ return {
540
+ success: true,
541
+ data: { channelId: temporaryChannelId },
542
+ metadata: { timestamp: Date.now(), policyCheck }
543
+ };
544
+ } catch (error) {
545
+ return this.errorResult(error, "OPEN_CHANNEL_FAILED", true);
546
+ }
547
+ }
548
+ /**
549
+ * Close a channel
550
+ */
551
+ async closeChannel(params) {
552
+ this.ensureInitialized();
553
+ try {
554
+ const operation = params.force ? "force_close" : "close";
555
+ const policyCheck = this.policy.checkChannelOperation({ operation });
556
+ if (!policyCheck.allowed) {
557
+ this.policy.addAuditEntry("POLICY_VIOLATION", false, params, policyCheck.violations);
558
+ return {
559
+ success: false,
560
+ error: {
561
+ code: "POLICY_VIOLATION",
562
+ message: policyCheck.violations.map((v) => v.message).join("; "),
563
+ recoverable: false
564
+ },
565
+ metadata: { timestamp: Date.now(), policyCheck }
566
+ };
567
+ }
568
+ if (this.runtimeJobManager) {
569
+ const job = await this.runtimeJobManager.manageChannel(
570
+ {
571
+ action: "shutdown",
572
+ channelId: params.channelId,
573
+ shutdownChannelParams: {
574
+ channel_id: params.channelId,
575
+ force: params.force
576
+ },
577
+ waitForClosed: false
578
+ },
579
+ { idempotencyKey: `channel:shutdown:${params.channelId}` }
580
+ );
581
+ const terminal = await this.waitForRuntimeJobTerminal(job.id, 12e4);
582
+ if (terminal.type !== "channel" || terminal.state !== "succeeded") {
583
+ throw new Error(terminal.error?.message ?? "Runtime channel close job failed");
584
+ }
585
+ } else {
586
+ await this.getRpc().shutdownChannel({
587
+ channel_id: params.channelId,
588
+ force: params.force
589
+ });
590
+ }
591
+ this.policy.addAuditEntry("CHANNEL_CLOSED", true, params);
592
+ return {
593
+ success: true,
594
+ metadata: { timestamp: Date.now(), policyCheck }
595
+ };
596
+ } catch (error) {
597
+ return this.errorResult(error, "CLOSE_CHANNEL_FAILED", true);
598
+ }
599
+ }
600
+ // ===========================================================================
601
+ // Node Information
602
+ // ===========================================================================
603
+ /**
604
+ * Get node information
605
+ */
606
+ async getNodeInfo() {
607
+ this.ensureInitialized();
608
+ try {
609
+ const info = await this.getRpc().nodeInfo();
610
+ return {
611
+ success: true,
612
+ data: {
613
+ nodeId: info.node_id,
614
+ version: info.version,
615
+ channelCount: parseInt(info.channel_count, 16),
616
+ peersCount: parseInt(info.peers_count, 16)
617
+ },
618
+ metadata: { timestamp: Date.now() }
619
+ };
620
+ } catch (error) {
621
+ return this.errorResult(error, "NODE_INFO_FAILED", true);
622
+ }
623
+ }
624
+ // ===========================================================================
625
+ // Verification & Validation Methods
626
+ // ===========================================================================
627
+ /**
628
+ * Validate an invoice before payment
629
+ * Checks format, expiry, amount, cryptographic correctness, and peer connectivity
630
+ *
631
+ * @example
632
+ * ```typescript
633
+ * const validation = await fiber.validateInvoice('fibt1...');
634
+ * if (validation.data?.recommendation === 'reject') {
635
+ * console.log('Do not pay:', validation.data.reason);
636
+ * }
637
+ * ```
638
+ */
639
+ async validateInvoice(invoice) {
640
+ this.ensureInitialized();
641
+ try {
642
+ if (!this.invoiceVerifier) {
643
+ throw new Error("Invoice verifier not initialized");
644
+ }
645
+ const result = await this.invoiceVerifier.verifyInvoice(invoice);
646
+ this.policy.addAuditEntry("INVOICE_VALIDATED", true, {
647
+ paymentHash: result.details.paymentHash,
648
+ amountCkb: result.details.amountCkb,
649
+ valid: result.valid
650
+ });
651
+ return {
652
+ success: true,
653
+ data: result,
654
+ metadata: {
655
+ timestamp: Date.now()
656
+ }
657
+ };
658
+ } catch (error) {
659
+ return this.errorResult(error, "INVOICE_VALIDATION_FAILED", true);
660
+ }
661
+ }
662
+ /**
663
+ * Get payment proof (cryptographic evidence of payment)
664
+ * Returns stored proof if available, or creates one from RPC status
665
+ */
666
+ async getPaymentProof(paymentHash) {
667
+ this.ensureInitialized();
668
+ try {
669
+ if (!this.paymentProofManager) {
670
+ throw new Error("Payment proof manager not initialized");
671
+ }
672
+ const storedProof = this.paymentProofManager.getProof(paymentHash);
673
+ if (storedProof) {
674
+ const verification = this.paymentProofManager.verifyProof(storedProof);
675
+ await this.paymentProofManager.save();
676
+ return {
677
+ success: true,
678
+ data: {
679
+ proof: storedProof,
680
+ verified: verification.valid,
681
+ status: verification.reason
682
+ },
683
+ metadata: { timestamp: Date.now() }
684
+ };
685
+ }
686
+ const paymentStatus = await this.getRpc().getPayment({
687
+ payment_hash: paymentHash
688
+ });
689
+ return {
690
+ success: true,
691
+ data: {
692
+ proof: null,
693
+ verified: paymentStatus.status === "Success",
694
+ status: `Payment status: ${paymentStatus.status}`
695
+ },
696
+ metadata: { timestamp: Date.now() }
697
+ };
698
+ } catch (error) {
699
+ return this.errorResult(error, "PROOF_FETCH_FAILED", true);
700
+ }
701
+ }
702
+ /**
703
+ * Get payment proof summary for audit trail
704
+ */
705
+ async getPaymentProofSummary() {
706
+ this.ensureInitialized();
707
+ try {
708
+ if (!this.paymentProofManager) {
709
+ throw new Error("Payment proof manager not initialized");
710
+ }
711
+ const summary = this.paymentProofManager.getSummary();
712
+ return {
713
+ success: true,
714
+ data: summary,
715
+ metadata: { timestamp: Date.now() }
716
+ };
717
+ } catch (error) {
718
+ return this.errorResult(error, "PROOF_SUMMARY_FAILED", true);
719
+ }
720
+ }
721
+ /**
722
+ * Export payment audit report
723
+ */
724
+ async getPaymentAuditReport(options) {
725
+ this.ensureInitialized();
726
+ try {
727
+ if (!this.paymentProofManager) {
728
+ throw new Error("Payment proof manager not initialized");
729
+ }
730
+ const report = this.paymentProofManager.exportAuditReport(
731
+ options?.startTime,
732
+ options?.endTime
733
+ );
734
+ return {
735
+ success: true,
736
+ data: report,
737
+ metadata: { timestamp: Date.now() }
738
+ };
739
+ } catch (error) {
740
+ return this.errorResult(error, "AUDIT_REPORT_FAILED", true);
741
+ }
742
+ }
743
+ // ===========================================================================
744
+ // Hold Invoice & Settlement Methods
745
+ // ===========================================================================
746
+ /**
747
+ * Create a hold invoice (for escrow / conditional payments)
748
+ * The payer's funds are held until you explicitly settle with the preimage,
749
+ * or cancelled if you don't settle before expiry.
750
+ *
751
+ * @example
752
+ * ```typescript
753
+ * const invoice = await fiber.createHoldInvoice({
754
+ * amountCkb: 10,
755
+ * paymentHash: '0x...', // SHA-256 hash of your secret preimage
756
+ * description: 'Escrow for service',
757
+ * });
758
+ * // Share invoice.data.invoice with the payer
759
+ * // When conditions are met, call settleInvoice() with the preimage
760
+ * ```
761
+ */
762
+ async createHoldInvoice(params) {
763
+ this.ensureInitialized();
764
+ try {
765
+ const amountHex = ckbToShannons(params.amountCkb);
766
+ const expirySeconds = (params.expiryMinutes || 60) * 60;
767
+ const holdInvoiceParams = {
768
+ amount: amountHex,
769
+ currency: this.config.chain === "mainnet" ? "Fibb" : "Fibt",
770
+ description: params.description,
771
+ expiry: toHex(expirySeconds),
772
+ payment_hash: params.paymentHash
773
+ };
774
+ let invoiceAddress;
775
+ let paymentHash;
776
+ if (this.runtimeJobManager) {
777
+ const job = await this.runtimeJobManager.manageInvoice(
778
+ {
779
+ action: "create",
780
+ newInvoiceParams: holdInvoiceParams,
781
+ waitForTerminal: false
782
+ },
783
+ { idempotencyKey: `invoice:create:${params.paymentHash}` }
784
+ );
785
+ const terminal = await this.waitForRuntimeJobTerminal(job.id, 12e4);
786
+ if (terminal.type !== "invoice" || terminal.state !== "succeeded" || !terminal.result?.invoiceAddress || !terminal.result.paymentHash) {
787
+ throw new Error(terminal.error?.message ?? "Runtime hold invoice job failed");
788
+ }
789
+ invoiceAddress = terminal.result.invoiceAddress;
790
+ paymentHash = terminal.result.paymentHash;
791
+ } else {
792
+ const result = await this.getRpc().newInvoice(holdInvoiceParams);
793
+ invoiceAddress = result.invoice_address;
794
+ paymentHash = result.invoice.data.payment_hash;
795
+ }
796
+ const expiresAt = new Date(Date.now() + expirySeconds * 1e3).toISOString();
797
+ const invoiceResult = {
798
+ invoice: invoiceAddress,
799
+ paymentHash,
800
+ amountCkb: params.amountCkb,
801
+ expiresAt,
802
+ status: "open"
803
+ };
804
+ this.policy.addAuditEntry("HOLD_INVOICE_CREATED", true, { ...invoiceResult });
805
+ return {
806
+ success: true,
807
+ data: invoiceResult,
808
+ metadata: { timestamp: Date.now() }
809
+ };
810
+ } catch (error) {
811
+ return this.errorResult(error, "HOLD_INVOICE_FAILED", true);
812
+ }
813
+ }
814
+ /**
815
+ * Settle a hold invoice by revealing the preimage
816
+ * This releases the held funds to you. No policy check needed since
817
+ * settling receives money, it doesn't spend it.
818
+ *
819
+ * @example
820
+ * ```typescript
821
+ * await fiber.settleInvoice({
822
+ * paymentHash: '0x...',
823
+ * preimage: '0x...', // The secret preimage whose hash matches paymentHash
824
+ * });
825
+ * ```
826
+ */
827
+ async settleInvoice(params) {
828
+ this.ensureInitialized();
829
+ try {
830
+ if (this.runtimeJobManager) {
831
+ const job = await this.runtimeJobManager.manageInvoice(
832
+ {
833
+ action: "settle",
834
+ settleInvoiceParams: {
835
+ payment_hash: params.paymentHash,
836
+ payment_preimage: params.preimage
837
+ }
838
+ },
839
+ { idempotencyKey: `invoice:settle:${params.paymentHash}` }
840
+ );
841
+ const terminal = await this.waitForRuntimeJobTerminal(job.id, 12e4);
842
+ if (terminal.type !== "invoice" || terminal.state !== "succeeded") {
843
+ throw new Error(terminal.error?.message ?? "Runtime settle invoice job failed");
844
+ }
845
+ } else {
846
+ await this.getRpc().settleInvoice({
847
+ payment_hash: params.paymentHash,
848
+ payment_preimage: params.preimage
849
+ });
850
+ }
851
+ this.policy.addAuditEntry("HOLD_INVOICE_SETTLED", true, {
852
+ paymentHash: params.paymentHash
853
+ });
854
+ return {
855
+ success: true,
856
+ metadata: { timestamp: Date.now() }
857
+ };
858
+ } catch (error) {
859
+ return this.errorResult(error, "SETTLE_INVOICE_FAILED", true);
860
+ }
861
+ }
862
+ // ===========================================================================
863
+ // Waiting / Watching Methods
864
+ // ===========================================================================
865
+ /**
866
+ * Wait for a payment to complete (Success or Failed)
867
+ * Wraps the SDK-level polling helper with AgentResult return type.
868
+ */
869
+ async waitForPayment(paymentHash, options) {
870
+ this.ensureInitialized();
871
+ try {
872
+ const result = await this.getRpc().waitForPayment(paymentHash, {
873
+ timeout: options?.timeoutMs
874
+ });
875
+ return {
876
+ success: result.status === "Success",
877
+ data: {
878
+ paymentHash: result.payment_hash,
879
+ status: result.status === "Success" ? "success" : result.status === "Failed" ? "failed" : "pending",
880
+ amountCkb: 0,
881
+ // Amount not returned from get_payment
882
+ feeCkb: shannonsToCkb(result.fee),
883
+ failureReason: result.failed_error
884
+ },
885
+ metadata: { timestamp: Date.now() }
886
+ };
887
+ } catch (error) {
888
+ return this.errorResult(error, "WAIT_PAYMENT_FAILED", true);
889
+ }
890
+ }
891
+ /**
892
+ * Wait for a channel to become ready (ChannelReady state)
893
+ * Useful after opening a channel — waits for on-chain confirmation.
894
+ */
895
+ async waitForChannelReady(channelId, options) {
896
+ this.ensureInitialized();
897
+ try {
898
+ const channel = await this.getRpc().waitForChannelReady(channelId, {
899
+ timeout: options?.timeoutMs
900
+ });
901
+ return {
902
+ success: true,
903
+ data: {
904
+ id: channel.channel_id,
905
+ peerId: channel.peer_id,
906
+ localBalanceCkb: shannonsToCkb(channel.local_balance),
907
+ remoteBalanceCkb: shannonsToCkb(channel.remote_balance),
908
+ state: channel.state.state_name,
909
+ isPublic: channel.is_public
910
+ },
911
+ metadata: { timestamp: Date.now() }
912
+ };
913
+ } catch (error) {
914
+ return this.errorResult(error, "WAIT_CHANNEL_FAILED", true);
915
+ }
916
+ }
917
+ // ===========================================================================
918
+ // Liquidity & Fund Management Methods
919
+ // ===========================================================================
920
+ /**
921
+ * Analyze liquidity across all channels
922
+ * Provides detailed health metrics and recommendations
923
+ *
924
+ * @example
925
+ * ```typescript
926
+ * const analysis = await fiber.analyzeLiquidity();
927
+ * console.log(`Health score: ${analysis.data?.channels.averageHealthScore}`);
928
+ * console.log(analysis.data?.summary);
929
+ * ```
930
+ */
931
+ async analyzeLiquidity() {
932
+ this.ensureInitialized();
933
+ try {
934
+ if (!this.liquidityAnalyzer) {
935
+ throw new Error("Liquidity analyzer not initialized");
936
+ }
937
+ const report = await this.liquidityAnalyzer.analyzeLiquidity();
938
+ return {
939
+ success: true,
940
+ data: report,
941
+ metadata: { timestamp: Date.now() }
942
+ };
943
+ } catch (error) {
944
+ return this.errorResult(error, "LIQUIDITY_ANALYSIS_FAILED", true);
945
+ }
946
+ }
947
+ /**
948
+ * Check if you have enough liquidity to send a specific amount
949
+ */
950
+ async canSend(amountCkb) {
951
+ this.ensureInitialized();
952
+ try {
953
+ if (!this.liquidityAnalyzer) {
954
+ throw new Error("Liquidity analyzer not initialized");
955
+ }
956
+ const result = await this.liquidityAnalyzer.getMissingLiquidityForAmount(amountCkb);
957
+ const balance = await this.getBalance();
958
+ const availableCkb = balance.data?.availableToSend || 0;
959
+ return {
960
+ success: true,
961
+ data: {
962
+ canSend: result.canSend,
963
+ shortfallCkb: result.shortfallCkb,
964
+ availableCkb,
965
+ recommendation: result.recommendation
966
+ },
967
+ metadata: { timestamp: Date.now() }
968
+ };
969
+ } catch (error) {
970
+ return this.errorResult(error, "LIQUIDITY_CHECK_FAILED", true);
971
+ }
972
+ }
973
+ // ===========================================================================
974
+ // Policy Management
975
+ // ===========================================================================
976
+ /**
977
+ * Get remaining spending allowance
978
+ */
979
+ getSpendingAllowance() {
980
+ const allowance = this.policy.getRemainingAllowance();
981
+ return {
982
+ perTransactionCkb: Number(allowance.perTransaction) / 1e8,
983
+ perWindowCkb: Number(allowance.perWindow) / 1e8
984
+ };
985
+ }
986
+ /**
987
+ * Get audit log
988
+ */
989
+ getAuditLog(options) {
990
+ return this.policy.getAuditLog(options);
991
+ }
992
+ // ===========================================================================
993
+ // Private Helpers
994
+ // ===========================================================================
995
+ ensureInitialized() {
996
+ if (!this.initialized) {
997
+ throw new Error("FiberPay not initialized. Call initialize() first.");
998
+ }
999
+ }
1000
+ getRpc() {
1001
+ if (!this.rpc) {
1002
+ throw new Error("RPC client not initialized. Call initialize() first.");
1003
+ }
1004
+ return this.rpc;
1005
+ }
1006
+ async waitForRuntimeJobTerminal(jobId, timeoutMs) {
1007
+ const startedAt = Date.now();
1008
+ while (Date.now() - startedAt < timeoutMs) {
1009
+ const job = this.runtimeJobManager?.getJob(jobId);
1010
+ if (!job) {
1011
+ throw new Error(`Runtime job not found: ${jobId}`);
1012
+ }
1013
+ if (job.state === "succeeded" || job.state === "failed" || job.state === "cancelled") {
1014
+ return job;
1015
+ }
1016
+ await new Promise((resolve) => setTimeout(resolve, 300));
1017
+ }
1018
+ throw new Error(`Runtime job ${jobId} timed out after ${timeoutMs}ms`);
1019
+ }
1020
+ /**
1021
+ * Map RPC CkbInvoiceStatus to agent-level status string
1022
+ */
1023
+ mapInvoiceStatus(status) {
1024
+ switch (status) {
1025
+ case "Open":
1026
+ return "open";
1027
+ case "Received":
1028
+ return "accepted";
1029
+ case "Paid":
1030
+ return "settled";
1031
+ case "Cancelled":
1032
+ case "Expired":
1033
+ return "cancelled";
1034
+ default:
1035
+ return "open";
1036
+ }
1037
+ }
1038
+ getInvoiceExpiryIso(invoice) {
1039
+ try {
1040
+ const createdSeconds = fromHex(invoice.data.timestamp);
1041
+ const expiryDeltaSeconds = this.getAttributeU64(invoice.data.attrs, "ExpiryTime") ?? BigInt(60 * 60);
1042
+ return new Date(Number(createdSeconds + expiryDeltaSeconds) * 1e3).toISOString();
1043
+ } catch {
1044
+ return "";
1045
+ }
1046
+ }
1047
+ getAttributeU64(attrs, key) {
1048
+ for (const attr of attrs) {
1049
+ if (key in attr) {
1050
+ return fromHex(attr[key]);
1051
+ }
1052
+ }
1053
+ return void 0;
1054
+ }
1055
+ errorResult(error, code, recoverable) {
1056
+ const message = error instanceof Error ? error.message : String(error);
1057
+ return {
1058
+ success: false,
1059
+ error: {
1060
+ code,
1061
+ message,
1062
+ recoverable
1063
+ },
1064
+ metadata: { timestamp: Date.now() }
1065
+ };
1066
+ }
1067
+ };
1068
+ function createFiberPay(options) {
1069
+ const dataDir = options?.dataDir || `${process.env.HOME}/.fiber-pay`;
1070
+ return new FiberPay({
1071
+ binaryPath: options?.binaryPath,
1072
+ dataDir,
1073
+ configFilePath: options?.configFilePath,
1074
+ chain: options?.chain || options?.network || "testnet",
1075
+ autoDownload: options?.autoDownload ?? true,
1076
+ autoStart: options?.autoStart ?? true,
1077
+ rpcUrl: options?.rpcUrl,
1078
+ keyPassword: options?.keyPassword
1079
+ });
1080
+ }
1081
+
1082
+ // src/mcp-tools.ts
1083
+ var MCP_TOOLS = {
1084
+ fiber_pay: {
1085
+ name: "fiber_pay",
1086
+ description: `Pay an invoice or send CKB directly to a node on the Lightning Network.
1087
+
1088
+ Examples:
1089
+ - Pay an invoice: fiber_pay({ invoice: "fibt1..." })
1090
+ - Send directly: fiber_pay({ recipientNodeId: "QmXXX...", amountCkb: 10 })
1091
+
1092
+ Returns payment status and tracking hash.`,
1093
+ inputSchema: {
1094
+ type: "object",
1095
+ properties: {
1096
+ invoice: {
1097
+ type: "string",
1098
+ description: "Lightning invoice string to pay (starts with fibt or fibb)"
1099
+ },
1100
+ recipientNodeId: {
1101
+ type: "string",
1102
+ description: "Recipient node ID for direct payment (keysend)"
1103
+ },
1104
+ amountCkb: {
1105
+ type: "number",
1106
+ description: "Amount to send in CKB (required for keysend)"
1107
+ },
1108
+ maxFeeCkb: {
1109
+ type: "number",
1110
+ description: "Maximum fee willing to pay in CKB"
1111
+ }
1112
+ },
1113
+ oneOf: [{ required: ["invoice"] }, { required: ["recipientNodeId", "amountCkb"] }]
1114
+ }
1115
+ },
1116
+ fiber_create_invoice: {
1117
+ name: "fiber_create_invoice",
1118
+ description: `Create an invoice to receive payment.
1119
+
1120
+ Example: fiber_create_invoice({ amountCkb: 10, description: "For coffee" })
1121
+
1122
+ Returns invoice string to share with payer.`,
1123
+ inputSchema: {
1124
+ type: "object",
1125
+ properties: {
1126
+ amountCkb: {
1127
+ type: "number",
1128
+ description: "Amount to receive in CKB"
1129
+ },
1130
+ description: {
1131
+ type: "string",
1132
+ description: "Description for the payer"
1133
+ },
1134
+ expiryMinutes: {
1135
+ type: "number",
1136
+ description: "Invoice expiry time in minutes (default: 60)"
1137
+ }
1138
+ },
1139
+ required: ["amountCkb"]
1140
+ }
1141
+ },
1142
+ fiber_get_balance: {
1143
+ name: "fiber_get_balance",
1144
+ description: `Get current balance information including:
1145
+ - Total balance in CKB
1146
+ - Available to send
1147
+ - Available to receive
1148
+ - Number of channels
1149
+ - Remaining spending allowance
1150
+
1151
+ No parameters required.`,
1152
+ inputSchema: {
1153
+ type: "object",
1154
+ properties: {}
1155
+ }
1156
+ },
1157
+ fiber_get_payment_status: {
1158
+ name: "fiber_get_payment_status",
1159
+ description: `Check the status of a payment by its hash.
1160
+
1161
+ Example: fiber_get_payment_status({ paymentHash: "0x..." })`,
1162
+ inputSchema: {
1163
+ type: "object",
1164
+ properties: {
1165
+ paymentHash: {
1166
+ type: "string",
1167
+ description: "Payment hash to check"
1168
+ }
1169
+ },
1170
+ required: ["paymentHash"]
1171
+ }
1172
+ },
1173
+ fiber_get_invoice_status: {
1174
+ name: "fiber_get_invoice_status",
1175
+ description: `Check the status of an invoice (whether it's been paid).
1176
+
1177
+ Example: fiber_get_invoice_status({ paymentHash: "0x..." })`,
1178
+ inputSchema: {
1179
+ type: "object",
1180
+ properties: {
1181
+ paymentHash: {
1182
+ type: "string",
1183
+ description: "Payment hash of the invoice"
1184
+ }
1185
+ },
1186
+ required: ["paymentHash"]
1187
+ }
1188
+ },
1189
+ fiber_list_channels: {
1190
+ name: "fiber_list_channels",
1191
+ description: `List all payment channels with their balances and states.
1192
+
1193
+ No parameters required.`,
1194
+ inputSchema: {
1195
+ type: "object",
1196
+ properties: {}
1197
+ }
1198
+ },
1199
+ fiber_open_channel: {
1200
+ name: "fiber_open_channel",
1201
+ description: `Open a new payment channel with a peer.
1202
+
1203
+ Example: fiber_open_channel({
1204
+ peer: "/ip4/x.x.x.x/tcp/8228/p2p/QmXXX...",
1205
+ fundingCkb: 100
1206
+ })
1207
+
1208
+ Note: This requires on-chain CKB for funding.`,
1209
+ inputSchema: {
1210
+ type: "object",
1211
+ properties: {
1212
+ peer: {
1213
+ type: "string",
1214
+ description: "Peer multiaddr or node ID"
1215
+ },
1216
+ fundingCkb: {
1217
+ type: "number",
1218
+ description: "Amount of CKB to fund the channel"
1219
+ },
1220
+ isPublic: {
1221
+ type: "boolean",
1222
+ description: "Whether to make the channel public (default: true)"
1223
+ }
1224
+ },
1225
+ required: ["peer", "fundingCkb"]
1226
+ }
1227
+ },
1228
+ fiber_close_channel: {
1229
+ name: "fiber_close_channel",
1230
+ description: `Close a payment channel and settle funds on-chain.
1231
+
1232
+ Example: fiber_close_channel({ channelId: "0x..." })
1233
+
1234
+ Use force: true only if peer is unresponsive.`,
1235
+ inputSchema: {
1236
+ type: "object",
1237
+ properties: {
1238
+ channelId: {
1239
+ type: "string",
1240
+ description: "Channel ID to close"
1241
+ },
1242
+ force: {
1243
+ type: "boolean",
1244
+ description: "Force close (unilateral, use only if peer unresponsive)"
1245
+ }
1246
+ },
1247
+ required: ["channelId"]
1248
+ }
1249
+ },
1250
+ fiber_get_node_info: {
1251
+ name: "fiber_get_node_info",
1252
+ description: `Get information about this node including node ID, public key, and statistics.
1253
+
1254
+ No parameters required.`,
1255
+ inputSchema: {
1256
+ type: "object",
1257
+ properties: {}
1258
+ }
1259
+ },
1260
+ fiber_get_spending_allowance: {
1261
+ name: "fiber_get_spending_allowance",
1262
+ description: `Get remaining spending allowance based on security policy.
1263
+
1264
+ Returns:
1265
+ - Per-transaction limit in CKB
1266
+ - Remaining allowance for current time window
1267
+
1268
+ No parameters required.`,
1269
+ inputSchema: {
1270
+ type: "object",
1271
+ properties: {}
1272
+ }
1273
+ },
1274
+ fiber_download_binary: {
1275
+ name: "fiber_download_binary",
1276
+ description: `Download and install the Fiber Network Node (fnn) binary for the current platform.
1277
+
1278
+ This is required before using any Fiber payment features. The binary will be automatically
1279
+ downloaded from GitHub releases and installed to the data directory.
1280
+
1281
+ Examples:
1282
+ - Download latest: fiber_download_binary({})
1283
+ - Download specific version: fiber_download_binary({ version: "v0.4.0" })
1284
+ - Force re-download: fiber_download_binary({ force: true })
1285
+
1286
+ Returns the path to the installed binary.`,
1287
+ inputSchema: {
1288
+ type: "object",
1289
+ properties: {
1290
+ version: {
1291
+ type: "string",
1292
+ description: 'Specific version to download (e.g., "v0.4.0"). Defaults to latest.'
1293
+ },
1294
+ force: {
1295
+ type: "boolean",
1296
+ description: "Force re-download even if binary already exists"
1297
+ }
1298
+ }
1299
+ }
1300
+ },
1301
+ fiber_validate_invoice: {
1302
+ name: "fiber_validate_invoice",
1303
+ description: `Validate an invoice before payment. Checks format, cryptographic correctness, expiry,
1304
+ amount, and peer connectivity. Returns recommendation to proceed, warn, or reject.
1305
+
1306
+ Use this BEFORE paying an invoice to ensure safety.
1307
+
1308
+ Example: fiber_validate_invoice({ invoice: "fibt1..." })
1309
+
1310
+ Returns:
1311
+ - valid: boolean (overall validity)
1312
+ - details: parsed invoice details (amount, expiry, payment hash)
1313
+ - checks: individual validation results (format, expiry, amount, preimage, peer)
1314
+ - issues: list of warnings and critical issues found
1315
+ - recommendation: 'proceed' | 'warn' | 'reject'
1316
+ - reason: human-readable recommendation reason`,
1317
+ inputSchema: {
1318
+ type: "object",
1319
+ properties: {
1320
+ invoice: {
1321
+ type: "string",
1322
+ description: "Invoice string to validate (starts with fibt or fibb)"
1323
+ }
1324
+ },
1325
+ required: ["invoice"]
1326
+ }
1327
+ },
1328
+ fiber_get_payment_proof: {
1329
+ name: "fiber_get_payment_proof",
1330
+ description: `Get cryptographic proof of payment execution. Useful for audit trail and reconciliation.
1331
+
1332
+ Example: fiber_get_payment_proof({ paymentHash: "0x..." })
1333
+
1334
+ Returns stored payment proof including:
1335
+ - Invoice original
1336
+ - Preimage (if available)
1337
+ - Fee breakdown
1338
+ - Verification status
1339
+ - Proof metadata`,
1340
+ inputSchema: {
1341
+ type: "object",
1342
+ properties: {
1343
+ paymentHash: {
1344
+ type: "string",
1345
+ description: "Payment hash to retrieve proof for"
1346
+ }
1347
+ },
1348
+ required: ["paymentHash"]
1349
+ }
1350
+ },
1351
+ fiber_analyze_liquidity: {
1352
+ name: "fiber_analyze_liquidity",
1353
+ description: `Comprehensive liquidity analysis across all channels. Provides health metrics,
1354
+ identifies issues, and generates recommendations for rebalancing and funding.
1355
+
1356
+ Use this to:
1357
+ - Understand current channel health
1358
+ - Identify liquidity gaps
1359
+ - Get rebalancing recommendations
1360
+ - Estimate available runway
1361
+
1362
+ No parameters required.
1363
+
1364
+ Returns:
1365
+ - balance: total, available to send/receive
1366
+ - channels: health metrics for each channel
1367
+ - liquidity: gaps and runway estimation
1368
+ - recommendations: rebalance suggestions and funding needs
1369
+ - summary: human-readable status`,
1370
+ inputSchema: {
1371
+ type: "object",
1372
+ properties: {}
1373
+ }
1374
+ },
1375
+ fiber_can_send: {
1376
+ name: "fiber_can_send",
1377
+ description: `Check if you have enough liquidity to send a specific amount. Returns shortfall
1378
+ if insufficient and recommendations.
1379
+
1380
+ Use this BEFORE attempting a payment to verify you have enough liquidity.
1381
+
1382
+ Example: fiber_can_send({ amountCkb: 100 })
1383
+
1384
+ Returns:
1385
+ - canSend: boolean
1386
+ - shortfallCkb: missing amount (0 if can send)
1387
+ - availableCkb: current available balance
1388
+ - recommendation: what to do if insufficient`,
1389
+ inputSchema: {
1390
+ type: "object",
1391
+ properties: {
1392
+ amountCkb: {
1393
+ type: "number",
1394
+ description: "Amount in CKB to check"
1395
+ }
1396
+ },
1397
+ required: ["amountCkb"]
1398
+ }
1399
+ },
1400
+ fiber_create_hold_invoice: {
1401
+ name: "fiber_create_hold_invoice",
1402
+ description: `Create a hold invoice for escrow or conditional payments.
1403
+
1404
+ A hold invoice locks the payer's funds until you explicitly settle with the preimage,
1405
+ or the invoice expires. This enables escrow patterns without a trusted third party.
1406
+
1407
+ Example: fiber_create_hold_invoice({
1408
+ amountCkb: 10,
1409
+ paymentHash: "0x...",
1410
+ description: "Escrow for service delivery"
1411
+ })
1412
+
1413
+ Flow:
1414
+ 1. Generate a secret preimage and compute its SHA-256 hash
1415
+ 2. Create hold invoice with the hash
1416
+ 3. Share invoice with payer \u2014 their funds are held when they pay
1417
+ 4. When conditions are met, call fiber_settle_invoice with the preimage
1418
+ 5. If conditions are NOT met, let the invoice expire (funds return to payer)
1419
+
1420
+ Returns invoice string and payment hash.`,
1421
+ inputSchema: {
1422
+ type: "object",
1423
+ properties: {
1424
+ amountCkb: {
1425
+ type: "number",
1426
+ description: "Amount to receive in CKB"
1427
+ },
1428
+ paymentHash: {
1429
+ type: "string",
1430
+ description: "SHA-256 hash of your secret preimage (0x-prefixed hex)"
1431
+ },
1432
+ description: {
1433
+ type: "string",
1434
+ description: "Description for the payer"
1435
+ },
1436
+ expiryMinutes: {
1437
+ type: "number",
1438
+ description: "Invoice expiry time in minutes (default: 60)"
1439
+ }
1440
+ },
1441
+ required: ["amountCkb", "paymentHash"]
1442
+ }
1443
+ },
1444
+ fiber_settle_invoice: {
1445
+ name: "fiber_settle_invoice",
1446
+ description: `Settle a hold invoice by revealing the preimage.
1447
+
1448
+ This releases the held funds to you. Only call this after conditions are met.
1449
+ The preimage must hash (SHA-256) to the payment_hash used when creating the hold invoice.
1450
+
1451
+ Example: fiber_settle_invoice({
1452
+ paymentHash: "0x...",
1453
+ preimage: "0x..."
1454
+ })`,
1455
+ inputSchema: {
1456
+ type: "object",
1457
+ properties: {
1458
+ paymentHash: {
1459
+ type: "string",
1460
+ description: "Payment hash of the hold invoice"
1461
+ },
1462
+ preimage: {
1463
+ type: "string",
1464
+ description: "Secret preimage (0x-prefixed hex, 32 bytes)"
1465
+ }
1466
+ },
1467
+ required: ["paymentHash", "preimage"]
1468
+ }
1469
+ },
1470
+ fiber_wait_for_payment: {
1471
+ name: "fiber_wait_for_payment",
1472
+ description: `Wait for a payment to complete (reach Success or Failed status).
1473
+
1474
+ Polls the payment status until it reaches a terminal state. Useful after sending
1475
+ a payment to wait for confirmation.
1476
+
1477
+ Example: fiber_wait_for_payment({ paymentHash: "0x...", timeoutMs: 60000 })
1478
+
1479
+ Returns the final payment status.`,
1480
+ inputSchema: {
1481
+ type: "object",
1482
+ properties: {
1483
+ paymentHash: {
1484
+ type: "string",
1485
+ description: "Payment hash to wait for"
1486
+ },
1487
+ timeoutMs: {
1488
+ type: "number",
1489
+ description: "Timeout in milliseconds (default: 120000 = 2 min)"
1490
+ }
1491
+ },
1492
+ required: ["paymentHash"]
1493
+ }
1494
+ },
1495
+ fiber_wait_for_channel_ready: {
1496
+ name: "fiber_wait_for_channel_ready",
1497
+ description: `Wait for a channel to become ready after opening.
1498
+
1499
+ After opening a channel, it takes time for the funding transaction to be confirmed
1500
+ on-chain. This tool polls until the channel reaches ChannelReady state.
1501
+
1502
+ Example: fiber_wait_for_channel_ready({ channelId: "0x...", timeoutMs: 300000 })
1503
+
1504
+ Returns channel info once ready.`,
1505
+ inputSchema: {
1506
+ type: "object",
1507
+ properties: {
1508
+ channelId: {
1509
+ type: "string",
1510
+ description: "Channel ID to wait for"
1511
+ },
1512
+ timeoutMs: {
1513
+ type: "number",
1514
+ description: "Timeout in milliseconds (default: 300000 = 5 min)"
1515
+ }
1516
+ },
1517
+ required: ["channelId"]
1518
+ }
1519
+ }
1520
+ };
1521
+ export {
1522
+ FiberPay,
1523
+ MCP_TOOLS,
1524
+ createFiberPay
1525
+ };
1526
+ //# sourceMappingURL=index.js.map