@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/README.md +145 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +1526 -0
- package/dist/index.js.map +1 -0
- package/dist/mcp-tools.d.ts +842 -0
- package/dist/mcp-tools.js +443 -0
- package/dist/mcp-tools.js.map +1 -0
- package/package.json +47 -0
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
|