nostr-mcp-server 2.0.0 → 2.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +37 -33
- package/build/__tests__/basic.test.js +2 -2
- package/build/__tests__/integration.test.js +2 -1
- package/build/__tests__/mocks.js +0 -10
- package/build/__tests__/nip19-conversion.test.js +1 -0
- package/build/__tests__/note-creation.test.js +1 -0
- package/build/__tests__/note-tools-functions.test.js +10 -5
- package/build/__tests__/note-tools-unit.test.js +1 -0
- package/build/__tests__/profile-notes-simple.test.js +1 -1
- package/build/__tests__/profile-postnote.test.js +1 -0
- package/build/__tests__/profile-tools.test.js +1 -0
- package/build/__tests__/websocket-integration.test.js +2 -1
- package/build/__tests__/zap-tools-simple.test.js +1 -1
- package/build/__tests__/zap-tools-tests.test.js +1 -0
- package/build/bun.setup.js +3 -0
- package/build/index.js +4 -45
- package/build/zap/zap-tools.js +0 -1
- package/package.json +12 -15
- package/build/__tests__/error-handling.test.js +0 -145
- package/build/__tests__/format-conversion.test.js +0 -137
- package/build/__tests__/nips-search.test.js +0 -109
- package/build/__tests__/relay-specification.test.js +0 -136
- package/build/__tests__/search-nips-simple.test.js +0 -96
- package/build/nips/nips-tools.js +0 -567
- package/build/nips-tools.js +0 -421
- package/build/note-tools.js +0 -53
- package/build/zap-tools.js +0 -989
package/build/zap-tools.js
DELETED
|
@@ -1,989 +0,0 @@
|
|
|
1
|
-
import { z } from "zod";
|
|
2
|
-
import { decode } from "light-bolt11-decoder";
|
|
3
|
-
import * as nip19 from "nostr-tools/nip19";
|
|
4
|
-
import fetch from "node-fetch";
|
|
5
|
-
import { generateSecretKey, getPublicKey, finalizeEvent } from "nostr-tools/pure";
|
|
6
|
-
import { KINDS, DEFAULT_RELAYS, FALLBACK_RELAYS, QUERY_TIMEOUT, getFreshPool, npubToHex, hexToNpub } from "./utils/index.js";
|
|
7
|
-
// Simple cache implementation for zap receipts
|
|
8
|
-
export class ZapCache {
|
|
9
|
-
cache = new Map();
|
|
10
|
-
maxSize;
|
|
11
|
-
ttlMs;
|
|
12
|
-
constructor(maxSize = 1000, ttlMinutes = 10) {
|
|
13
|
-
this.maxSize = maxSize;
|
|
14
|
-
this.ttlMs = ttlMinutes * 60 * 1000;
|
|
15
|
-
}
|
|
16
|
-
add(zapReceipt, enrichedData) {
|
|
17
|
-
// Create enriched zap with processing timestamp
|
|
18
|
-
const cachedZap = {
|
|
19
|
-
...zapReceipt,
|
|
20
|
-
...enrichedData,
|
|
21
|
-
processedAt: Date.now()
|
|
22
|
-
};
|
|
23
|
-
// Add to cache
|
|
24
|
-
this.cache.set(zapReceipt.id, cachedZap);
|
|
25
|
-
// Clean cache if it exceeds max size
|
|
26
|
-
if (this.cache.size > this.maxSize) {
|
|
27
|
-
this.cleanup();
|
|
28
|
-
}
|
|
29
|
-
return cachedZap;
|
|
30
|
-
}
|
|
31
|
-
get(id) {
|
|
32
|
-
const cachedZap = this.cache.get(id);
|
|
33
|
-
// Return undefined if not found or expired
|
|
34
|
-
if (!cachedZap || Date.now() - cachedZap.processedAt > this.ttlMs) {
|
|
35
|
-
if (cachedZap) {
|
|
36
|
-
// Remove expired entry
|
|
37
|
-
this.cache.delete(id);
|
|
38
|
-
}
|
|
39
|
-
return undefined;
|
|
40
|
-
}
|
|
41
|
-
return cachedZap;
|
|
42
|
-
}
|
|
43
|
-
has(id) {
|
|
44
|
-
return this.get(id) !== undefined;
|
|
45
|
-
}
|
|
46
|
-
cleanup() {
|
|
47
|
-
const now = Date.now();
|
|
48
|
-
// Remove expired entries
|
|
49
|
-
for (const [id, zap] of this.cache.entries()) {
|
|
50
|
-
if (now - zap.processedAt > this.ttlMs) {
|
|
51
|
-
this.cache.delete(id);
|
|
52
|
-
}
|
|
53
|
-
}
|
|
54
|
-
// If still too large, remove oldest entries
|
|
55
|
-
if (this.cache.size > this.maxSize) {
|
|
56
|
-
const sortedEntries = Array.from(this.cache.entries())
|
|
57
|
-
.sort((a, b) => a[1].processedAt - b[1].processedAt);
|
|
58
|
-
const entriesToRemove = sortedEntries.slice(0, sortedEntries.length - Math.floor(this.maxSize * 0.75));
|
|
59
|
-
for (const [id] of entriesToRemove) {
|
|
60
|
-
this.cache.delete(id);
|
|
61
|
-
}
|
|
62
|
-
}
|
|
63
|
-
}
|
|
64
|
-
clear() {
|
|
65
|
-
this.cache.clear();
|
|
66
|
-
}
|
|
67
|
-
size() {
|
|
68
|
-
return this.cache.size;
|
|
69
|
-
}
|
|
70
|
-
}
|
|
71
|
-
// Create a global cache instance
|
|
72
|
-
export const zapCache = new ZapCache();
|
|
73
|
-
// Helper function to parse zap request data from description tag in zap receipt
|
|
74
|
-
export function parseZapRequestData(zapReceipt) {
|
|
75
|
-
try {
|
|
76
|
-
// Find the description tag which contains the zap request JSON
|
|
77
|
-
const descriptionTag = zapReceipt.tags.find(tag => tag[0] === "description" && tag.length > 1);
|
|
78
|
-
if (!descriptionTag || !descriptionTag[1]) {
|
|
79
|
-
return undefined;
|
|
80
|
-
}
|
|
81
|
-
// Parse the zap request JSON - this contains a serialized ZapRequest
|
|
82
|
-
const zapRequest = JSON.parse(descriptionTag[1]);
|
|
83
|
-
// Convert to the ZapRequestData format
|
|
84
|
-
const zapRequestData = {
|
|
85
|
-
pubkey: zapRequest.pubkey,
|
|
86
|
-
content: zapRequest.content,
|
|
87
|
-
created_at: zapRequest.created_at,
|
|
88
|
-
id: zapRequest.id,
|
|
89
|
-
};
|
|
90
|
-
// Extract additional data from ZapRequest tags
|
|
91
|
-
zapRequest.tags.forEach(tag => {
|
|
92
|
-
if (tag[0] === 'amount' && tag[1]) {
|
|
93
|
-
zapRequestData.amount = parseInt(tag[1], 10);
|
|
94
|
-
}
|
|
95
|
-
else if (tag[0] === 'relays' && tag.length > 1) {
|
|
96
|
-
zapRequestData.relays = tag.slice(1);
|
|
97
|
-
}
|
|
98
|
-
else if (tag[0] === 'e' && tag[1]) {
|
|
99
|
-
zapRequestData.event = tag[1];
|
|
100
|
-
}
|
|
101
|
-
else if (tag[0] === 'lnurl' && tag[1]) {
|
|
102
|
-
zapRequestData.lnurl = tag[1];
|
|
103
|
-
}
|
|
104
|
-
});
|
|
105
|
-
return zapRequestData;
|
|
106
|
-
}
|
|
107
|
-
catch (error) {
|
|
108
|
-
console.error("Error parsing zap request data:", error);
|
|
109
|
-
return undefined;
|
|
110
|
-
}
|
|
111
|
-
}
|
|
112
|
-
// Helper function to extract and decode bolt11 invoice from a zap receipt
|
|
113
|
-
export function decodeBolt11FromZap(zapReceipt) {
|
|
114
|
-
try {
|
|
115
|
-
// Find the bolt11 tag
|
|
116
|
-
const bolt11Tag = zapReceipt.tags.find(tag => tag[0] === "bolt11" && tag.length > 1);
|
|
117
|
-
if (!bolt11Tag || !bolt11Tag[1]) {
|
|
118
|
-
return undefined;
|
|
119
|
-
}
|
|
120
|
-
// Decode the bolt11 invoice
|
|
121
|
-
const decodedInvoice = decode(bolt11Tag[1]);
|
|
122
|
-
return decodedInvoice;
|
|
123
|
-
}
|
|
124
|
-
catch (error) {
|
|
125
|
-
console.error("Error decoding bolt11 invoice:", error);
|
|
126
|
-
return undefined;
|
|
127
|
-
}
|
|
128
|
-
}
|
|
129
|
-
// Extract amount in sats from decoded bolt11 invoice
|
|
130
|
-
export function getAmountFromDecodedInvoice(decodedInvoice) {
|
|
131
|
-
try {
|
|
132
|
-
if (!decodedInvoice || !decodedInvoice.sections) {
|
|
133
|
-
return undefined;
|
|
134
|
-
}
|
|
135
|
-
// Find the amount section
|
|
136
|
-
const amountSection = decodedInvoice.sections.find((section) => section.name === "amount");
|
|
137
|
-
if (!amountSection) {
|
|
138
|
-
return undefined;
|
|
139
|
-
}
|
|
140
|
-
// Convert msats to sats
|
|
141
|
-
const amountMsats = amountSection.value;
|
|
142
|
-
const amountSats = Math.floor(amountMsats / 1000);
|
|
143
|
-
return amountSats;
|
|
144
|
-
}
|
|
145
|
-
catch (error) {
|
|
146
|
-
console.error("Error extracting amount from decoded invoice:", error);
|
|
147
|
-
return undefined;
|
|
148
|
-
}
|
|
149
|
-
}
|
|
150
|
-
// Validate a zap receipt according to NIP-57 Appendix F
|
|
151
|
-
export function validateZapReceipt(zapReceipt, zapRequest) {
|
|
152
|
-
try {
|
|
153
|
-
// 1. Must be kind 9735
|
|
154
|
-
if (zapReceipt.kind !== KINDS.ZapReceipt) {
|
|
155
|
-
return { valid: false, reason: "Not a zap receipt (kind 9735)" };
|
|
156
|
-
}
|
|
157
|
-
// 2. Must have a bolt11 tag
|
|
158
|
-
const bolt11Tag = zapReceipt.tags.find(tag => tag[0] === "bolt11" && tag.length > 1);
|
|
159
|
-
if (!bolt11Tag || !bolt11Tag[1]) {
|
|
160
|
-
return { valid: false, reason: "Missing bolt11 tag" };
|
|
161
|
-
}
|
|
162
|
-
// 3. Must have a description tag with the zap request
|
|
163
|
-
const descriptionTag = zapReceipt.tags.find(tag => tag[0] === "description" && tag.length > 1);
|
|
164
|
-
if (!descriptionTag || !descriptionTag[1]) {
|
|
165
|
-
return { valid: false, reason: "Missing description tag" };
|
|
166
|
-
}
|
|
167
|
-
// 4. Parse the zap request from the description tag if not provided
|
|
168
|
-
let parsedZapRequest;
|
|
169
|
-
try {
|
|
170
|
-
parsedZapRequest = zapRequest || JSON.parse(descriptionTag[1]);
|
|
171
|
-
}
|
|
172
|
-
catch (e) {
|
|
173
|
-
return { valid: false, reason: "Invalid zap request JSON in description tag" };
|
|
174
|
-
}
|
|
175
|
-
// 5. Validate the zap request structure
|
|
176
|
-
if (parsedZapRequest.kind !== KINDS.ZapRequest) {
|
|
177
|
-
return { valid: false, reason: "Invalid zap request kind" };
|
|
178
|
-
}
|
|
179
|
-
// 6. Check that the p tag from the zap request is included in the zap receipt
|
|
180
|
-
const requestedRecipientPubkey = parsedZapRequest.tags.find(tag => tag[0] === 'p' && tag.length > 1)?.[1];
|
|
181
|
-
const receiptRecipientTag = zapReceipt.tags.find(tag => tag[0] === 'p' && tag.length > 1);
|
|
182
|
-
if (!requestedRecipientPubkey || !receiptRecipientTag || receiptRecipientTag[1] !== requestedRecipientPubkey) {
|
|
183
|
-
return { valid: false, reason: "Recipient pubkey mismatch" };
|
|
184
|
-
}
|
|
185
|
-
// 7. Check for optional e tag consistency if present in the zap request
|
|
186
|
-
const requestEventTag = parsedZapRequest.tags.find(tag => tag[0] === 'e' && tag.length > 1);
|
|
187
|
-
if (requestEventTag) {
|
|
188
|
-
const receiptEventTag = zapReceipt.tags.find(tag => tag[0] === 'e' && tag.length > 1);
|
|
189
|
-
if (!receiptEventTag || receiptEventTag[1] !== requestEventTag[1]) {
|
|
190
|
-
return { valid: false, reason: "Event ID mismatch" };
|
|
191
|
-
}
|
|
192
|
-
}
|
|
193
|
-
// 8. Check for optional amount consistency
|
|
194
|
-
const amountTag = parsedZapRequest.tags.find(tag => tag[0] === 'amount' && tag.length > 1);
|
|
195
|
-
if (amountTag) {
|
|
196
|
-
// Decode the bolt11 invoice to verify the amount
|
|
197
|
-
const decodedInvoice = decodeBolt11FromZap(zapReceipt);
|
|
198
|
-
if (decodedInvoice) {
|
|
199
|
-
const invoiceAmountMsats = decodedInvoice.sections.find((s) => s.name === "amount")?.value;
|
|
200
|
-
const requestAmountMsats = parseInt(amountTag[1], 10);
|
|
201
|
-
if (invoiceAmountMsats && Math.abs(invoiceAmountMsats - requestAmountMsats) > 10) { // Allow small rounding differences
|
|
202
|
-
return { valid: false, reason: "Amount mismatch between request and invoice" };
|
|
203
|
-
}
|
|
204
|
-
}
|
|
205
|
-
}
|
|
206
|
-
return { valid: true };
|
|
207
|
-
}
|
|
208
|
-
catch (error) {
|
|
209
|
-
return { valid: false, reason: `Validation error: ${error instanceof Error ? error.message : String(error)}` };
|
|
210
|
-
}
|
|
211
|
-
}
|
|
212
|
-
// Determine the direction of a zap relative to a pubkey
|
|
213
|
-
export function determineZapDirection(zapReceipt, pubkey) {
|
|
214
|
-
try {
|
|
215
|
-
// Check if received via lowercase 'p' tag (recipient)
|
|
216
|
-
const isReceived = zapReceipt.tags.some(tag => tag[0] === 'p' && tag[1] === pubkey);
|
|
217
|
-
// Check if sent via uppercase 'P' tag (sender, per NIP-57)
|
|
218
|
-
let isSent = zapReceipt.tags.some(tag => tag[0] === 'P' && tag[1] === pubkey);
|
|
219
|
-
if (!isSent) {
|
|
220
|
-
// Fallback: check description tag for the sender pubkey
|
|
221
|
-
const descriptionTag = zapReceipt.tags.find(tag => tag[0] === "description" && tag.length > 1);
|
|
222
|
-
if (descriptionTag && descriptionTag[1]) {
|
|
223
|
-
try {
|
|
224
|
-
const zapRequest = JSON.parse(descriptionTag[1]);
|
|
225
|
-
isSent = zapRequest && zapRequest.pubkey === pubkey;
|
|
226
|
-
}
|
|
227
|
-
catch (e) {
|
|
228
|
-
// Ignore parsing errors
|
|
229
|
-
}
|
|
230
|
-
}
|
|
231
|
-
}
|
|
232
|
-
// Determine direction
|
|
233
|
-
if (isSent && isReceived) {
|
|
234
|
-
return 'self';
|
|
235
|
-
}
|
|
236
|
-
else if (isSent) {
|
|
237
|
-
return 'sent';
|
|
238
|
-
}
|
|
239
|
-
else if (isReceived) {
|
|
240
|
-
return 'received';
|
|
241
|
-
}
|
|
242
|
-
else {
|
|
243
|
-
return 'unknown';
|
|
244
|
-
}
|
|
245
|
-
}
|
|
246
|
-
catch (error) {
|
|
247
|
-
console.error("Error determining zap direction:", error);
|
|
248
|
-
return 'unknown';
|
|
249
|
-
}
|
|
250
|
-
}
|
|
251
|
-
// Process a zap receipt into an enriched cached zap
|
|
252
|
-
export function processZapReceipt(zapReceipt, pubkey) {
|
|
253
|
-
// Check if we already have this zap in the cache
|
|
254
|
-
const existingCachedZap = zapCache.get(zapReceipt.id);
|
|
255
|
-
if (existingCachedZap) {
|
|
256
|
-
return existingCachedZap;
|
|
257
|
-
}
|
|
258
|
-
try {
|
|
259
|
-
// Determine direction relative to the specified pubkey
|
|
260
|
-
const direction = determineZapDirection(zapReceipt, pubkey);
|
|
261
|
-
// Extract target pubkey (recipient)
|
|
262
|
-
const targetPubkey = zapReceipt.tags.find(tag => tag[0] === 'p' && tag.length > 1)?.[1];
|
|
263
|
-
// Extract target event if any
|
|
264
|
-
const targetEvent = zapReceipt.tags.find(tag => tag[0] === 'e' && tag.length > 1)?.[1];
|
|
265
|
-
// Extract target coordinate if any (a tag)
|
|
266
|
-
const targetCoordinate = zapReceipt.tags.find(tag => tag[0] === 'a' && tag.length > 1)?.[1];
|
|
267
|
-
// Parse zap request to get additional data
|
|
268
|
-
const zapRequestData = parseZapRequestData(zapReceipt);
|
|
269
|
-
// Decode bolt11 invoice to get amount
|
|
270
|
-
const decodedInvoice = decodeBolt11FromZap(zapReceipt);
|
|
271
|
-
const amountSats = decodedInvoice ?
|
|
272
|
-
getAmountFromDecodedInvoice(decodedInvoice) :
|
|
273
|
-
(zapRequestData?.amount ? Math.floor(zapRequestData.amount / 1000) : undefined);
|
|
274
|
-
// Create enriched zap and add to cache
|
|
275
|
-
return zapCache.add(zapReceipt, {
|
|
276
|
-
direction,
|
|
277
|
-
amountSats,
|
|
278
|
-
targetPubkey,
|
|
279
|
-
targetEvent,
|
|
280
|
-
targetCoordinate
|
|
281
|
-
});
|
|
282
|
-
}
|
|
283
|
-
catch (error) {
|
|
284
|
-
console.error("Error processing zap receipt:", error);
|
|
285
|
-
// Still cache the basic zap with unknown direction
|
|
286
|
-
return zapCache.add(zapReceipt, { direction: 'unknown' });
|
|
287
|
-
}
|
|
288
|
-
}
|
|
289
|
-
// Helper function to format zap receipt with enhanced information
|
|
290
|
-
export function formatZapReceipt(zap, pubkeyContext) {
|
|
291
|
-
if (!zap)
|
|
292
|
-
return "";
|
|
293
|
-
try {
|
|
294
|
-
// Cast to ZapReceipt for better type safety since we know we're dealing with kind 9735
|
|
295
|
-
const zapReceipt = zap;
|
|
296
|
-
// Process the zap receipt with context if provided
|
|
297
|
-
let enrichedZap;
|
|
298
|
-
if (pubkeyContext) {
|
|
299
|
-
enrichedZap = processZapReceipt(zapReceipt, pubkeyContext);
|
|
300
|
-
}
|
|
301
|
-
else {
|
|
302
|
-
// Check if it's already in cache
|
|
303
|
-
const cachedZap = zapCache.get(zapReceipt.id);
|
|
304
|
-
if (cachedZap) {
|
|
305
|
-
enrichedZap = cachedZap;
|
|
306
|
-
}
|
|
307
|
-
else {
|
|
308
|
-
// Process without context - won't have direction information
|
|
309
|
-
enrichedZap = {
|
|
310
|
-
...zapReceipt,
|
|
311
|
-
processedAt: Date.now()
|
|
312
|
-
};
|
|
313
|
-
}
|
|
314
|
-
}
|
|
315
|
-
// Get basic zap info
|
|
316
|
-
const created = new Date(zapReceipt.created_at * 1000).toLocaleString();
|
|
317
|
-
// Get sender information from P tag or description
|
|
318
|
-
let sender = "Unknown";
|
|
319
|
-
let senderPubkey;
|
|
320
|
-
const senderPTag = zapReceipt.tags.find(tag => tag[0] === 'P' && tag.length > 1);
|
|
321
|
-
if (senderPTag && senderPTag[1]) {
|
|
322
|
-
senderPubkey = senderPTag[1];
|
|
323
|
-
const npub = hexToNpub(senderPubkey);
|
|
324
|
-
sender = npub ? `${npub.slice(0, 8)}...${npub.slice(-4)}` : `${senderPubkey.slice(0, 8)}...${senderPubkey.slice(-8)}`;
|
|
325
|
-
}
|
|
326
|
-
else {
|
|
327
|
-
// Try to get from description
|
|
328
|
-
const zapRequestData = parseZapRequestData(zapReceipt);
|
|
329
|
-
if (zapRequestData?.pubkey) {
|
|
330
|
-
senderPubkey = zapRequestData.pubkey;
|
|
331
|
-
const npub = hexToNpub(senderPubkey);
|
|
332
|
-
sender = npub ? `${npub.slice(0, 8)}...${npub.slice(-4)}` : `${senderPubkey.slice(0, 8)}...${senderPubkey.slice(-8)}`;
|
|
333
|
-
}
|
|
334
|
-
}
|
|
335
|
-
// Get recipient information
|
|
336
|
-
const recipient = zapReceipt.tags.find(tag => tag[0] === 'p' && tag.length > 1)?.[1];
|
|
337
|
-
let formattedRecipient = "Unknown";
|
|
338
|
-
if (recipient) {
|
|
339
|
-
const npub = hexToNpub(recipient);
|
|
340
|
-
formattedRecipient = npub ? `${npub.slice(0, 8)}...${npub.slice(-4)}` : `${recipient.slice(0, 8)}...${recipient.slice(-8)}`;
|
|
341
|
-
}
|
|
342
|
-
// Get amount
|
|
343
|
-
let amount = enrichedZap.amountSats !== undefined ?
|
|
344
|
-
`${enrichedZap.amountSats} sats` :
|
|
345
|
-
"Unknown";
|
|
346
|
-
// Get comment
|
|
347
|
-
let comment = "No comment";
|
|
348
|
-
const zapRequestData = parseZapRequestData(zapReceipt);
|
|
349
|
-
if (zapRequestData?.content) {
|
|
350
|
-
comment = zapRequestData.content;
|
|
351
|
-
}
|
|
352
|
-
// Check if this zap is for a specific event or coordinate
|
|
353
|
-
let zapTarget = "User";
|
|
354
|
-
let targetId = "";
|
|
355
|
-
if (enrichedZap.targetEvent) {
|
|
356
|
-
zapTarget = "Event";
|
|
357
|
-
targetId = enrichedZap.targetEvent;
|
|
358
|
-
}
|
|
359
|
-
else if (enrichedZap.targetCoordinate) {
|
|
360
|
-
zapTarget = "Replaceable Event";
|
|
361
|
-
targetId = enrichedZap.targetCoordinate;
|
|
362
|
-
}
|
|
363
|
-
// Format the output with all available information
|
|
364
|
-
const lines = [
|
|
365
|
-
`From: ${sender}`,
|
|
366
|
-
`To: ${formattedRecipient}`,
|
|
367
|
-
`Amount: ${amount}`,
|
|
368
|
-
`Created: ${created}`,
|
|
369
|
-
`Target: ${zapTarget}${targetId ? ` (${targetId.slice(0, 8)}...)` : ''}`,
|
|
370
|
-
`Comment: ${comment}`,
|
|
371
|
-
];
|
|
372
|
-
// Add payment preimage if available
|
|
373
|
-
const preimageTag = zapReceipt.tags.find(tag => tag[0] === "preimage" && tag.length > 1);
|
|
374
|
-
if (preimageTag && preimageTag[1]) {
|
|
375
|
-
lines.push(`Preimage: ${preimageTag[1].slice(0, 10)}...`);
|
|
376
|
-
}
|
|
377
|
-
// Add payment hash if available in bolt11 invoice
|
|
378
|
-
const decodedInvoice = decodeBolt11FromZap(zapReceipt);
|
|
379
|
-
if (decodedInvoice) {
|
|
380
|
-
const paymentHashSection = decodedInvoice.sections.find((section) => section.name === "payment_hash");
|
|
381
|
-
if (paymentHashSection) {
|
|
382
|
-
lines.push(`Payment Hash: ${paymentHashSection.value.slice(0, 10)}...`);
|
|
383
|
-
}
|
|
384
|
-
}
|
|
385
|
-
// Add direction information if available
|
|
386
|
-
if (enrichedZap.direction && enrichedZap.direction !== 'unknown') {
|
|
387
|
-
const directionLabels = {
|
|
388
|
-
'sent': '↑ SENT',
|
|
389
|
-
'received': '↓ RECEIVED',
|
|
390
|
-
'self': '↻ SELF ZAP'
|
|
391
|
-
};
|
|
392
|
-
lines.unshift(`[${directionLabels[enrichedZap.direction]}]`);
|
|
393
|
-
}
|
|
394
|
-
lines.push('---');
|
|
395
|
-
return lines.join("\n");
|
|
396
|
-
}
|
|
397
|
-
catch (error) {
|
|
398
|
-
console.error("Error formatting zap receipt:", error);
|
|
399
|
-
return "Error formatting zap receipt";
|
|
400
|
-
}
|
|
401
|
-
}
|
|
402
|
-
// Export the tool configurations
|
|
403
|
-
export const getReceivedZapsToolConfig = {
|
|
404
|
-
pubkey: z.string().describe("Public key of the Nostr user (hex format or npub format)"),
|
|
405
|
-
limit: z.number().min(1).max(100).default(10).describe("Maximum number of zaps to fetch"),
|
|
406
|
-
relays: z.array(z.string()).optional().describe("Optional list of relays to query"),
|
|
407
|
-
validateReceipts: z.boolean().default(true).describe("Whether to validate zap receipts according to NIP-57"),
|
|
408
|
-
debug: z.boolean().default(false).describe("Enable verbose debug logging"),
|
|
409
|
-
};
|
|
410
|
-
export const getSentZapsToolConfig = {
|
|
411
|
-
pubkey: z.string().describe("Public key of the Nostr user (hex format or npub format)"),
|
|
412
|
-
limit: z.number().min(1).max(100).default(10).describe("Maximum number of zaps to fetch"),
|
|
413
|
-
relays: z.array(z.string()).optional().describe("Optional list of relays to query"),
|
|
414
|
-
validateReceipts: z.boolean().default(true).describe("Whether to validate zap receipts according to NIP-57"),
|
|
415
|
-
debug: z.boolean().default(false).describe("Enable verbose debug logging"),
|
|
416
|
-
};
|
|
417
|
-
export const getAllZapsToolConfig = {
|
|
418
|
-
pubkey: z.string().describe("Public key of the Nostr user (hex format or npub format)"),
|
|
419
|
-
limit: z.number().min(1).max(100).default(20).describe("Maximum number of total zaps to fetch"),
|
|
420
|
-
relays: z.array(z.string()).optional().describe("Optional list of relays to query"),
|
|
421
|
-
validateReceipts: z.boolean().default(true).describe("Whether to validate zap receipts according to NIP-57"),
|
|
422
|
-
debug: z.boolean().default(false).describe("Enable verbose debug logging"),
|
|
423
|
-
};
|
|
424
|
-
// Helper function to decode a note identifier (note, nevent, naddr) to its components
|
|
425
|
-
export async function decodeEventId(id) {
|
|
426
|
-
if (!id)
|
|
427
|
-
return null;
|
|
428
|
-
try {
|
|
429
|
-
// Clean up input
|
|
430
|
-
id = id.trim();
|
|
431
|
-
// If it's already a hex event ID
|
|
432
|
-
if (/^[0-9a-fA-F]{64}$/i.test(id)) {
|
|
433
|
-
return {
|
|
434
|
-
type: 'eventId',
|
|
435
|
-
eventId: id.toLowerCase()
|
|
436
|
-
};
|
|
437
|
-
}
|
|
438
|
-
// Try to decode as a bech32 entity
|
|
439
|
-
if (id.startsWith('note1') || id.startsWith('nevent1') || id.startsWith('naddr1')) {
|
|
440
|
-
try {
|
|
441
|
-
const decoded = nip19.decode(id);
|
|
442
|
-
if (decoded.type === 'note') {
|
|
443
|
-
return {
|
|
444
|
-
type: 'note',
|
|
445
|
-
eventId: decoded.data
|
|
446
|
-
};
|
|
447
|
-
}
|
|
448
|
-
else if (decoded.type === 'nevent') {
|
|
449
|
-
const data = decoded.data;
|
|
450
|
-
return {
|
|
451
|
-
type: 'nevent',
|
|
452
|
-
eventId: data.id,
|
|
453
|
-
relays: data.relays,
|
|
454
|
-
pubkey: data.author
|
|
455
|
-
};
|
|
456
|
-
}
|
|
457
|
-
else if (decoded.type === 'naddr') {
|
|
458
|
-
const data = decoded.data;
|
|
459
|
-
return {
|
|
460
|
-
type: 'naddr',
|
|
461
|
-
pubkey: data.pubkey,
|
|
462
|
-
kind: data.kind,
|
|
463
|
-
relays: data.relays,
|
|
464
|
-
identifier: data.identifier
|
|
465
|
-
};
|
|
466
|
-
}
|
|
467
|
-
}
|
|
468
|
-
catch (decodeError) {
|
|
469
|
-
console.error("Error decoding event identifier:", decodeError);
|
|
470
|
-
return null;
|
|
471
|
-
}
|
|
472
|
-
}
|
|
473
|
-
// Not a valid event identifier format
|
|
474
|
-
return null;
|
|
475
|
-
}
|
|
476
|
-
catch (error) {
|
|
477
|
-
console.error("Error decoding event identifier:", error);
|
|
478
|
-
return null;
|
|
479
|
-
}
|
|
480
|
-
}
|
|
481
|
-
// Function to prepare an anonymous zap
|
|
482
|
-
export async function prepareAnonymousZap(target, amountSats, comment = "", relays = DEFAULT_RELAYS) {
|
|
483
|
-
try {
|
|
484
|
-
// Convert amount to millisats
|
|
485
|
-
const amountMsats = amountSats * 1000;
|
|
486
|
-
// Determine if target is a pubkey or an event
|
|
487
|
-
let hexPubkey = null;
|
|
488
|
-
let eventId = null;
|
|
489
|
-
let eventCoordinate = null;
|
|
490
|
-
// First, try to parse as a pubkey
|
|
491
|
-
hexPubkey = npubToHex(target);
|
|
492
|
-
// If not a pubkey, try to parse as an event identifier
|
|
493
|
-
if (!hexPubkey) {
|
|
494
|
-
const decodedEvent = await decodeEventId(target);
|
|
495
|
-
if (decodedEvent) {
|
|
496
|
-
if (decodedEvent.eventId) {
|
|
497
|
-
eventId = decodedEvent.eventId;
|
|
498
|
-
}
|
|
499
|
-
else if (decodedEvent.pubkey) {
|
|
500
|
-
// For naddr, we got a pubkey but no event ID
|
|
501
|
-
hexPubkey = decodedEvent.pubkey;
|
|
502
|
-
// If this is an naddr, store the information for creating an "a" tag later
|
|
503
|
-
if (decodedEvent.type === 'naddr' && decodedEvent.kind) {
|
|
504
|
-
eventCoordinate = {
|
|
505
|
-
kind: decodedEvent.kind,
|
|
506
|
-
pubkey: decodedEvent.pubkey,
|
|
507
|
-
identifier: decodedEvent.identifier || ''
|
|
508
|
-
};
|
|
509
|
-
}
|
|
510
|
-
}
|
|
511
|
-
}
|
|
512
|
-
}
|
|
513
|
-
// If we couldn't determine a valid target, return error
|
|
514
|
-
if (!hexPubkey && !eventId) {
|
|
515
|
-
return {
|
|
516
|
-
invoice: "",
|
|
517
|
-
success: false,
|
|
518
|
-
message: "Invalid target. Please provide a valid npub, hex pubkey, note ID, or event ID."
|
|
519
|
-
};
|
|
520
|
-
}
|
|
521
|
-
// Create a fresh pool for this request
|
|
522
|
-
const pool = getFreshPool();
|
|
523
|
-
try {
|
|
524
|
-
// Find the user's metadata to get their LNURL
|
|
525
|
-
let profileFilter = { kinds: [KINDS.Metadata] };
|
|
526
|
-
if (hexPubkey) {
|
|
527
|
-
profileFilter = {
|
|
528
|
-
kinds: [KINDS.Metadata],
|
|
529
|
-
authors: [hexPubkey],
|
|
530
|
-
};
|
|
531
|
-
}
|
|
532
|
-
else if (eventId) {
|
|
533
|
-
// First get the event to find the author
|
|
534
|
-
const eventFilter = { ids: [eventId] };
|
|
535
|
-
const eventPromise = pool.get(relays, eventFilter);
|
|
536
|
-
const event = await Promise.race([
|
|
537
|
-
eventPromise,
|
|
538
|
-
new Promise((_, reject) => setTimeout(() => reject(new Error("Timeout")), QUERY_TIMEOUT))
|
|
539
|
-
]);
|
|
540
|
-
if (!event) {
|
|
541
|
-
return {
|
|
542
|
-
invoice: "",
|
|
543
|
-
success: false,
|
|
544
|
-
message: `Could not find event with ID ${eventId}`
|
|
545
|
-
};
|
|
546
|
-
}
|
|
547
|
-
hexPubkey = event.pubkey;
|
|
548
|
-
profileFilter = {
|
|
549
|
-
kinds: [KINDS.Metadata],
|
|
550
|
-
authors: [hexPubkey],
|
|
551
|
-
};
|
|
552
|
-
}
|
|
553
|
-
// Collect all relays to try
|
|
554
|
-
const allRelays = [...new Set([...relays, ...DEFAULT_RELAYS, ...FALLBACK_RELAYS])];
|
|
555
|
-
// Get the user's profile
|
|
556
|
-
let profile = null;
|
|
557
|
-
for (const relaySet of [relays, DEFAULT_RELAYS, FALLBACK_RELAYS]) {
|
|
558
|
-
if (relaySet.length === 0)
|
|
559
|
-
continue;
|
|
560
|
-
try {
|
|
561
|
-
const profilePromise = pool.get(relaySet, profileFilter);
|
|
562
|
-
profile = await Promise.race([
|
|
563
|
-
profilePromise,
|
|
564
|
-
new Promise((_, reject) => setTimeout(() => reject(new Error("Timeout")), QUERY_TIMEOUT))
|
|
565
|
-
]);
|
|
566
|
-
if (profile)
|
|
567
|
-
break;
|
|
568
|
-
}
|
|
569
|
-
catch (error) {
|
|
570
|
-
// Continue to next relay set
|
|
571
|
-
}
|
|
572
|
-
}
|
|
573
|
-
if (!profile) {
|
|
574
|
-
return {
|
|
575
|
-
invoice: "",
|
|
576
|
-
success: false,
|
|
577
|
-
message: "Could not find profile for the target user. Their profile may not exist on our known relays."
|
|
578
|
-
};
|
|
579
|
-
}
|
|
580
|
-
// Parse the profile to get the lightning address or LNURL
|
|
581
|
-
let lnurl = null;
|
|
582
|
-
try {
|
|
583
|
-
const metadata = JSON.parse(profile.content);
|
|
584
|
-
// Check standard LUD-16/LUD-06 fields
|
|
585
|
-
lnurl = metadata.lud16 || metadata.lud06 || null;
|
|
586
|
-
// Check for alternate capitalizations that some clients might use
|
|
587
|
-
if (!lnurl) {
|
|
588
|
-
lnurl = metadata.LUD16 || metadata.LUD06 ||
|
|
589
|
-
metadata.Lud16 || metadata.Lud06 ||
|
|
590
|
-
metadata.lightning || metadata.LIGHTNING ||
|
|
591
|
-
metadata.lightningAddress ||
|
|
592
|
-
null;
|
|
593
|
-
}
|
|
594
|
-
if (!lnurl) {
|
|
595
|
-
// Check if there's any key that contains "lud" or "lightning"
|
|
596
|
-
const ludKey = Object.keys(metadata).find(key => key.toLowerCase().includes('lud') ||
|
|
597
|
-
key.toLowerCase().includes('lightning'));
|
|
598
|
-
if (ludKey) {
|
|
599
|
-
lnurl = metadata[ludKey];
|
|
600
|
-
}
|
|
601
|
-
}
|
|
602
|
-
if (!lnurl) {
|
|
603
|
-
return {
|
|
604
|
-
invoice: "",
|
|
605
|
-
success: false,
|
|
606
|
-
message: "Target user does not have a lightning address or LNURL configured in their profile"
|
|
607
|
-
};
|
|
608
|
-
}
|
|
609
|
-
// If it's a lightning address (contains @), convert to LNURL
|
|
610
|
-
if (lnurl.includes('@')) {
|
|
611
|
-
const [name, domain] = lnurl.split('@');
|
|
612
|
-
// Per LUD-16, properly encode username with encodeURIComponent
|
|
613
|
-
const encodedName = encodeURIComponent(name);
|
|
614
|
-
lnurl = `https://${domain}/.well-known/lnurlp/${encodedName}`;
|
|
615
|
-
}
|
|
616
|
-
else if (lnurl.toLowerCase().startsWith('lnurl')) {
|
|
617
|
-
// Decode bech32 LNURL to URL
|
|
618
|
-
try {
|
|
619
|
-
lnurl = Buffer.from(bech32ToArray(lnurl.toLowerCase().substring(5))).toString();
|
|
620
|
-
}
|
|
621
|
-
catch (e) {
|
|
622
|
-
return {
|
|
623
|
-
invoice: "",
|
|
624
|
-
success: false,
|
|
625
|
-
message: "Invalid LNURL format"
|
|
626
|
-
};
|
|
627
|
-
}
|
|
628
|
-
}
|
|
629
|
-
// Make sure it's HTTP or HTTPS if not already
|
|
630
|
-
if (!lnurl.startsWith('http://') && !lnurl.startsWith('https://')) {
|
|
631
|
-
// Default to HTTPS
|
|
632
|
-
lnurl = 'https://' + lnurl;
|
|
633
|
-
}
|
|
634
|
-
}
|
|
635
|
-
catch (error) {
|
|
636
|
-
return {
|
|
637
|
-
invoice: "",
|
|
638
|
-
success: false,
|
|
639
|
-
message: "Error parsing user profile"
|
|
640
|
-
};
|
|
641
|
-
}
|
|
642
|
-
if (!lnurl) {
|
|
643
|
-
return {
|
|
644
|
-
invoice: "",
|
|
645
|
-
success: false,
|
|
646
|
-
message: "Could not determine LNURL from user profile"
|
|
647
|
-
};
|
|
648
|
-
}
|
|
649
|
-
// Step 1: Query the LNURL to get the callback URL
|
|
650
|
-
let lnurlResponse;
|
|
651
|
-
try {
|
|
652
|
-
lnurlResponse = await fetch(lnurl, {
|
|
653
|
-
headers: {
|
|
654
|
-
'Accept': 'application/json',
|
|
655
|
-
'User-Agent': 'Nostr-MCP-Server/1.0'
|
|
656
|
-
}
|
|
657
|
-
});
|
|
658
|
-
if (!lnurlResponse.ok) {
|
|
659
|
-
let errorText = "";
|
|
660
|
-
try {
|
|
661
|
-
errorText = await lnurlResponse.text();
|
|
662
|
-
}
|
|
663
|
-
catch (e) {
|
|
664
|
-
// Ignore if we can't read the error text
|
|
665
|
-
}
|
|
666
|
-
return {
|
|
667
|
-
invoice: "",
|
|
668
|
-
success: false,
|
|
669
|
-
message: `LNURL request failed with status ${lnurlResponse.status}${errorText ? `: ${errorText}` : ""}`
|
|
670
|
-
};
|
|
671
|
-
}
|
|
672
|
-
}
|
|
673
|
-
catch (error) {
|
|
674
|
-
return {
|
|
675
|
-
invoice: "",
|
|
676
|
-
success: false,
|
|
677
|
-
message: `Error connecting to LNURL: ${error instanceof Error ? error.message : "Unknown error"}`
|
|
678
|
-
};
|
|
679
|
-
}
|
|
680
|
-
let lnurlData;
|
|
681
|
-
try {
|
|
682
|
-
const responseText = await lnurlResponse.text();
|
|
683
|
-
lnurlData = JSON.parse(responseText);
|
|
684
|
-
}
|
|
685
|
-
catch (error) {
|
|
686
|
-
return {
|
|
687
|
-
invoice: "",
|
|
688
|
-
success: false,
|
|
689
|
-
message: `Invalid JSON response from LNURL service: ${error instanceof Error ? error.message : "Unknown error"}`
|
|
690
|
-
};
|
|
691
|
-
}
|
|
692
|
-
// Extract metadata for better debugging
|
|
693
|
-
const metadataInfo = extractLnurlMetadata(lnurlData);
|
|
694
|
-
// Check if the service supports NIP-57 zaps
|
|
695
|
-
if (!lnurlData.allowsNostr) {
|
|
696
|
-
return {
|
|
697
|
-
invoice: "",
|
|
698
|
-
success: false,
|
|
699
|
-
message: "The target user's lightning service does not support Nostr zaps"
|
|
700
|
-
};
|
|
701
|
-
}
|
|
702
|
-
if (!lnurlData.nostrPubkey) {
|
|
703
|
-
return {
|
|
704
|
-
invoice: "",
|
|
705
|
-
success: false,
|
|
706
|
-
message: "The target user's lightning service does not provide a nostrPubkey for zaps"
|
|
707
|
-
};
|
|
708
|
-
}
|
|
709
|
-
// Validate the callback URL
|
|
710
|
-
if (!lnurlData.callback || !isValidUrl(lnurlData.callback)) {
|
|
711
|
-
return {
|
|
712
|
-
invoice: "",
|
|
713
|
-
success: false,
|
|
714
|
-
message: `Invalid callback URL in LNURL response: ${lnurlData.callback}`
|
|
715
|
-
};
|
|
716
|
-
}
|
|
717
|
-
// Validate amount limits
|
|
718
|
-
if (!lnurlData.minSendable || !lnurlData.maxSendable) {
|
|
719
|
-
return {
|
|
720
|
-
invoice: "",
|
|
721
|
-
success: false,
|
|
722
|
-
message: "The LNURL service did not provide valid min/max sendable amounts"
|
|
723
|
-
};
|
|
724
|
-
}
|
|
725
|
-
if (amountMsats < lnurlData.minSendable) {
|
|
726
|
-
return {
|
|
727
|
-
invoice: "",
|
|
728
|
-
success: false,
|
|
729
|
-
message: `Amount too small. Minimum is ${lnurlData.minSendable / 1000} sats (you tried to send ${amountMsats / 1000} sats)`
|
|
730
|
-
};
|
|
731
|
-
}
|
|
732
|
-
if (amountMsats > lnurlData.maxSendable) {
|
|
733
|
-
return {
|
|
734
|
-
invoice: "",
|
|
735
|
-
success: false,
|
|
736
|
-
message: `Amount too large. Maximum is ${lnurlData.maxSendable / 1000} sats (you tried to send ${amountMsats / 1000} sats)`
|
|
737
|
-
};
|
|
738
|
-
}
|
|
739
|
-
// Validate comment length if the service has a limit
|
|
740
|
-
if (lnurlData.commentAllowed && comment.length > lnurlData.commentAllowed) {
|
|
741
|
-
comment = comment.substring(0, lnurlData.commentAllowed);
|
|
742
|
-
}
|
|
743
|
-
// Step 2: Create the zap request tags
|
|
744
|
-
const zapRequestTags = [
|
|
745
|
-
["relays", ...relays.slice(0, 5)], // Include up to 5 relays
|
|
746
|
-
["amount", amountMsats.toString()],
|
|
747
|
-
["lnurl", lnurl]
|
|
748
|
-
];
|
|
749
|
-
// Add p or e tag depending on what we're zapping
|
|
750
|
-
if (hexPubkey) {
|
|
751
|
-
zapRequestTags.push(["p", hexPubkey]);
|
|
752
|
-
}
|
|
753
|
-
if (eventId) {
|
|
754
|
-
zapRequestTags.push(["e", eventId]);
|
|
755
|
-
}
|
|
756
|
-
// Add a tag for replaceable events (naddr)
|
|
757
|
-
if (eventCoordinate) {
|
|
758
|
-
const aTagValue = `${eventCoordinate.kind}:${eventCoordinate.pubkey}:${eventCoordinate.identifier}`;
|
|
759
|
-
zapRequestTags.push(["a", aTagValue]);
|
|
760
|
-
}
|
|
761
|
-
// Create a proper one-time keypair for anonymous zapping
|
|
762
|
-
const anonymousSecretKey = generateSecretKey(); // This generates a proper 32-byte private key
|
|
763
|
-
const anonymousPubkeyHex = getPublicKey(anonymousSecretKey); // This computes the corresponding public key
|
|
764
|
-
// Create the zap request event template
|
|
765
|
-
const zapRequestTemplate = {
|
|
766
|
-
kind: 9734,
|
|
767
|
-
created_at: Math.floor(Date.now() / 1000),
|
|
768
|
-
content: comment,
|
|
769
|
-
tags: zapRequestTags,
|
|
770
|
-
};
|
|
771
|
-
// Properly finalize the event (calculates ID and signs it) using nostr-tools
|
|
772
|
-
const signedZapRequest = finalizeEvent(zapRequestTemplate, anonymousSecretKey);
|
|
773
|
-
// Create different formatted versions of the zap request for compatibility
|
|
774
|
-
const completeEventParam = encodeURIComponent(JSON.stringify(signedZapRequest));
|
|
775
|
-
const basicEventParam = encodeURIComponent(JSON.stringify({
|
|
776
|
-
kind: 9734,
|
|
777
|
-
created_at: Math.floor(Date.now() / 1000),
|
|
778
|
-
content: comment,
|
|
779
|
-
tags: zapRequestTags,
|
|
780
|
-
pubkey: anonymousPubkeyHex
|
|
781
|
-
}));
|
|
782
|
-
const tagsOnlyParam = encodeURIComponent(JSON.stringify({
|
|
783
|
-
tags: zapRequestTags
|
|
784
|
-
}));
|
|
785
|
-
// Try each approach in order
|
|
786
|
-
const approaches = [
|
|
787
|
-
{ name: "Complete event with ID/sig", param: completeEventParam },
|
|
788
|
-
{ name: "Basic event without ID/sig", param: basicEventParam },
|
|
789
|
-
{ name: "Tags only", param: tagsOnlyParam },
|
|
790
|
-
// Add fallback approach without nostr parameter at all
|
|
791
|
-
{ name: "No nostr parameter", param: null }
|
|
792
|
-
];
|
|
793
|
-
// Flag to track if we've successfully processed any approach
|
|
794
|
-
let success = false;
|
|
795
|
-
let finalResult = null;
|
|
796
|
-
let lastError = "";
|
|
797
|
-
for (const approach of approaches) {
|
|
798
|
-
if (success)
|
|
799
|
-
break; // Skip if we already succeeded
|
|
800
|
-
// Create a new URL for each attempt to avoid parameter pollution
|
|
801
|
-
const currentCallbackUrl = new URL(lnurlData.callback);
|
|
802
|
-
// Add basic parameters - must include amount first per some implementations
|
|
803
|
-
currentCallbackUrl.searchParams.append("amount", amountMsats.toString());
|
|
804
|
-
// Add comment if provided and allowed
|
|
805
|
-
if (comment && (!lnurlData.commentAllowed || lnurlData.commentAllowed > 0)) {
|
|
806
|
-
currentCallbackUrl.searchParams.append("comment", comment);
|
|
807
|
-
}
|
|
808
|
-
// Add the nostr parameter for this approach (if not null)
|
|
809
|
-
if (approach.param !== null) {
|
|
810
|
-
currentCallbackUrl.searchParams.append("nostr", approach.param);
|
|
811
|
-
}
|
|
812
|
-
const callbackUrlString = currentCallbackUrl.toString();
|
|
813
|
-
try {
|
|
814
|
-
const callbackResponse = await fetch(callbackUrlString, {
|
|
815
|
-
method: 'GET', // Explicitly use GET as required by LUD-06
|
|
816
|
-
headers: {
|
|
817
|
-
'Accept': 'application/json',
|
|
818
|
-
'User-Agent': 'Nostr-MCP-Server/1.0'
|
|
819
|
-
}
|
|
820
|
-
});
|
|
821
|
-
// Attempt to read the response body regardless of status code
|
|
822
|
-
let responseText = "";
|
|
823
|
-
try {
|
|
824
|
-
responseText = await callbackResponse.text();
|
|
825
|
-
}
|
|
826
|
-
catch (e) {
|
|
827
|
-
// Ignore if we can't read the response
|
|
828
|
-
}
|
|
829
|
-
if (!callbackResponse.ok) {
|
|
830
|
-
if (responseText) {
|
|
831
|
-
lastError = `Status ${callbackResponse.status}: ${responseText}`;
|
|
832
|
-
}
|
|
833
|
-
else {
|
|
834
|
-
lastError = `Status ${callbackResponse.status}`;
|
|
835
|
-
}
|
|
836
|
-
continue; // Try the next approach
|
|
837
|
-
}
|
|
838
|
-
// Successfully got a 2xx response, now parse it
|
|
839
|
-
let invoiceData;
|
|
840
|
-
try {
|
|
841
|
-
invoiceData = JSON.parse(responseText);
|
|
842
|
-
}
|
|
843
|
-
catch (error) {
|
|
844
|
-
lastError = `Invalid JSON in response: ${responseText}`;
|
|
845
|
-
continue; // Try the next approach
|
|
846
|
-
}
|
|
847
|
-
// Check if the response has the expected structure
|
|
848
|
-
if (!invoiceData.pr) {
|
|
849
|
-
if (invoiceData.reason) {
|
|
850
|
-
lastError = invoiceData.reason;
|
|
851
|
-
// If the error message mentions the NIP-57/Nostr parameter specifically, try the next approach
|
|
852
|
-
if (lastError.toLowerCase().includes('nostr') ||
|
|
853
|
-
lastError.toLowerCase().includes('customer') ||
|
|
854
|
-
lastError.toLowerCase().includes('wallet')) {
|
|
855
|
-
continue; // Try the next approach
|
|
856
|
-
}
|
|
857
|
-
}
|
|
858
|
-
else {
|
|
859
|
-
lastError = `Missing 'pr' field in response`;
|
|
860
|
-
}
|
|
861
|
-
continue; // Try the next approach
|
|
862
|
-
}
|
|
863
|
-
// We got a valid invoice!
|
|
864
|
-
success = true;
|
|
865
|
-
finalResult = {
|
|
866
|
-
invoice: invoiceData.pr,
|
|
867
|
-
success: true,
|
|
868
|
-
message: `Successfully generated invoice using ${approach.name}`
|
|
869
|
-
};
|
|
870
|
-
break; // Exit the loop
|
|
871
|
-
}
|
|
872
|
-
catch (error) {
|
|
873
|
-
lastError = error instanceof Error ? error.message : "Unknown error";
|
|
874
|
-
// Continue to the next approach
|
|
875
|
-
}
|
|
876
|
-
}
|
|
877
|
-
// If none of our approaches worked, return an error with the last error message
|
|
878
|
-
if (!success) {
|
|
879
|
-
return {
|
|
880
|
-
invoice: "",
|
|
881
|
-
success: false,
|
|
882
|
-
message: `Failed to generate invoice: ${lastError}`
|
|
883
|
-
};
|
|
884
|
-
}
|
|
885
|
-
return finalResult;
|
|
886
|
-
}
|
|
887
|
-
catch (error) {
|
|
888
|
-
return {
|
|
889
|
-
invoice: "",
|
|
890
|
-
success: false,
|
|
891
|
-
message: `Error preparing zap: ${error instanceof Error ? error.message : "Unknown error"}`
|
|
892
|
-
};
|
|
893
|
-
}
|
|
894
|
-
finally {
|
|
895
|
-
// Clean up any subscriptions and close the pool
|
|
896
|
-
pool.close(relays);
|
|
897
|
-
}
|
|
898
|
-
}
|
|
899
|
-
catch (error) {
|
|
900
|
-
return {
|
|
901
|
-
invoice: "",
|
|
902
|
-
success: false,
|
|
903
|
-
message: `Fatal error: ${error instanceof Error ? error.message : "Unknown error"}`
|
|
904
|
-
};
|
|
905
|
-
}
|
|
906
|
-
}
|
|
907
|
-
// Helper function to decode bech32-encoded LNURL
|
|
908
|
-
function bech32ToArray(bech32Str) {
|
|
909
|
-
// Extract the 5-bit words
|
|
910
|
-
let words = [];
|
|
911
|
-
for (let i = 0; i < bech32Str.length; i++) {
|
|
912
|
-
const c = bech32Str.charAt(i);
|
|
913
|
-
const charCode = c.charCodeAt(0);
|
|
914
|
-
if (charCode < 33 || charCode > 126) {
|
|
915
|
-
throw new Error(`Invalid character: ${c}`);
|
|
916
|
-
}
|
|
917
|
-
const value = "qpzry9x8gf2tvdw0s3jn54khce6mua7l".indexOf(c.toLowerCase());
|
|
918
|
-
if (value === -1) {
|
|
919
|
-
throw new Error(`Invalid character: ${c}`);
|
|
920
|
-
}
|
|
921
|
-
words.push(value);
|
|
922
|
-
}
|
|
923
|
-
// Convert 5-bit words to 8-bit bytes
|
|
924
|
-
const result = new Uint8Array(Math.floor((words.length * 5) / 8));
|
|
925
|
-
let bitIndex = 0;
|
|
926
|
-
let byteIndex = 0;
|
|
927
|
-
for (let i = 0; i < words.length; i++) {
|
|
928
|
-
const value = words[i];
|
|
929
|
-
// Extract the bits from this word
|
|
930
|
-
for (let j = 0; j < 5; j++) {
|
|
931
|
-
const bit = (value >> (4 - j)) & 1;
|
|
932
|
-
// Set the bit in the result
|
|
933
|
-
if (bit) {
|
|
934
|
-
result[byteIndex] |= 1 << (7 - bitIndex);
|
|
935
|
-
}
|
|
936
|
-
bitIndex++;
|
|
937
|
-
if (bitIndex === 8) {
|
|
938
|
-
bitIndex = 0;
|
|
939
|
-
byteIndex++;
|
|
940
|
-
}
|
|
941
|
-
}
|
|
942
|
-
}
|
|
943
|
-
return result;
|
|
944
|
-
}
|
|
945
|
-
// Export the tool configuration for anonymous zap
|
|
946
|
-
export const sendAnonymousZapToolConfig = {
|
|
947
|
-
target: z.string().describe("Target to zap - can be a pubkey (hex or npub) or an event ID (nevent, note, naddr, or hex)"),
|
|
948
|
-
amountSats: z.number().min(1).describe("Amount to zap in satoshis"),
|
|
949
|
-
comment: z.string().default("").describe("Optional comment to include with the zap"),
|
|
950
|
-
relays: z.array(z.string()).optional().describe("Optional list of relays to query")
|
|
951
|
-
};
|
|
952
|
-
// Add a function to verify the callback URL scheme and validate the response
|
|
953
|
-
function isValidUrl(urlString) {
|
|
954
|
-
try {
|
|
955
|
-
const url = new URL(urlString);
|
|
956
|
-
return url.protocol === 'https:' || url.protocol === 'http:';
|
|
957
|
-
}
|
|
958
|
-
catch {
|
|
959
|
-
return false;
|
|
960
|
-
}
|
|
961
|
-
}
|
|
962
|
-
// Add this function to extract metadata from the LNURL response
|
|
963
|
-
function extractLnurlMetadata(lnurlData) {
|
|
964
|
-
if (!lnurlData.metadata)
|
|
965
|
-
return {};
|
|
966
|
-
try {
|
|
967
|
-
const metadata = JSON.parse(lnurlData.metadata);
|
|
968
|
-
if (!Array.isArray(metadata))
|
|
969
|
-
return {};
|
|
970
|
-
let payeeName;
|
|
971
|
-
let payeeEmail;
|
|
972
|
-
// Extract information from metadata as per LUD-06
|
|
973
|
-
for (const entry of metadata) {
|
|
974
|
-
if (Array.isArray(entry) && entry.length >= 2) {
|
|
975
|
-
if (entry[0] === "text/plain") {
|
|
976
|
-
payeeName = entry[1];
|
|
977
|
-
}
|
|
978
|
-
if (entry[0] === "text/email" || entry[0] === "text/identifier") {
|
|
979
|
-
payeeEmail = entry[1];
|
|
980
|
-
}
|
|
981
|
-
}
|
|
982
|
-
}
|
|
983
|
-
return { payeeName, payeeEmail };
|
|
984
|
-
}
|
|
985
|
-
catch (error) {
|
|
986
|
-
console.error("Error parsing LNURL metadata:", error);
|
|
987
|
-
return {};
|
|
988
|
-
}
|
|
989
|
-
}
|