nostr-mcp-server 2.0.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/LICENSE +21 -0
- package/README.md +498 -0
- package/build/__tests__/basic.test.js +87 -0
- package/build/__tests__/error-handling.test.js +145 -0
- package/build/__tests__/format-conversion.test.js +137 -0
- package/build/__tests__/integration.test.js +163 -0
- package/build/__tests__/mocks.js +109 -0
- package/build/__tests__/nip19-conversion.test.js +268 -0
- package/build/__tests__/nips-search.test.js +109 -0
- package/build/__tests__/note-creation.test.js +148 -0
- package/build/__tests__/note-tools-functions.test.js +173 -0
- package/build/__tests__/note-tools-unit.test.js +97 -0
- package/build/__tests__/profile-notes-simple.test.js +78 -0
- package/build/__tests__/profile-postnote.test.js +120 -0
- package/build/__tests__/profile-tools.test.js +90 -0
- package/build/__tests__/relay-specification.test.js +136 -0
- package/build/__tests__/search-nips-simple.test.js +96 -0
- package/build/__tests__/websocket-integration.test.js +257 -0
- package/build/__tests__/zap-tools-simple.test.js +72 -0
- package/build/__tests__/zap-tools-tests.test.js +197 -0
- package/build/index.js +1285 -0
- package/build/nips/nips-tools.js +567 -0
- package/build/nips-tools.js +421 -0
- package/build/note/note-tools.js +296 -0
- package/build/note-tools.js +53 -0
- package/build/profile/profile-tools.js +260 -0
- package/build/utils/constants.js +27 -0
- package/build/utils/conversion.js +332 -0
- package/build/utils/ephemeral-relay.js +438 -0
- package/build/utils/formatting.js +34 -0
- package/build/utils/index.js +6 -0
- package/build/utils/nip19-tools.js +117 -0
- package/build/utils/pool.js +55 -0
- package/build/zap/zap-tools.js +980 -0
- package/build/zap-tools.js +989 -0
- package/package.json +59 -0
|
@@ -0,0 +1,296 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { DEFAULT_RELAYS } from "../utils/index.js";
|
|
3
|
+
import { generateKeypair, createEvent, getEventHash, signEvent, decode as nip19decode } from "snstr";
|
|
4
|
+
import { getFreshPool } from "../utils/index.js";
|
|
5
|
+
import { schnorr } from '@noble/curves/secp256k1';
|
|
6
|
+
// Schema for getProfile tool
|
|
7
|
+
export const getProfileToolConfig = {
|
|
8
|
+
pubkey: z.string().describe("Public key of the Nostr user (hex format or npub format)"),
|
|
9
|
+
relays: z.array(z.string()).optional().describe("Optional list of relays to query"),
|
|
10
|
+
};
|
|
11
|
+
// Schema for getKind1Notes tool
|
|
12
|
+
export const getKind1NotesToolConfig = {
|
|
13
|
+
pubkey: z.string().describe("Public key of the Nostr user (hex format or npub format)"),
|
|
14
|
+
limit: z.number().min(1).max(100).default(10).describe("Maximum number of notes to fetch"),
|
|
15
|
+
relays: z.array(z.string()).optional().describe("Optional list of relays to query"),
|
|
16
|
+
};
|
|
17
|
+
// Schema for getLongFormNotes tool
|
|
18
|
+
export const getLongFormNotesToolConfig = {
|
|
19
|
+
pubkey: z.string().describe("Public key of the Nostr user (hex format or npub format)"),
|
|
20
|
+
limit: z.number().min(1).max(100).default(10).describe("Maximum number of notes to fetch"),
|
|
21
|
+
relays: z.array(z.string()).optional().describe("Optional list of relays to query"),
|
|
22
|
+
};
|
|
23
|
+
// Schema for postAnonymousNote tool
|
|
24
|
+
export const postAnonymousNoteToolConfig = {
|
|
25
|
+
content: z.string().describe("Content of the note to post"),
|
|
26
|
+
relays: z.array(z.string()).optional().describe("Optional list of relays to publish to"),
|
|
27
|
+
tags: z.array(z.array(z.string())).optional().describe("Optional tags to include with the note"),
|
|
28
|
+
};
|
|
29
|
+
// Schema for createNote tool
|
|
30
|
+
export const createNoteToolConfig = {
|
|
31
|
+
privateKey: z.string().describe("Private key to sign the note with (hex format or nsec format)"),
|
|
32
|
+
content: z.string().describe("Content of the note to create"),
|
|
33
|
+
tags: z.array(z.array(z.string())).optional().describe("Optional tags to include with the note"),
|
|
34
|
+
};
|
|
35
|
+
// Schema for signNote tool
|
|
36
|
+
export const signNoteToolConfig = {
|
|
37
|
+
privateKey: z.string().describe("Private key to sign the note with (hex format or nsec format)"),
|
|
38
|
+
noteEvent: z.object({
|
|
39
|
+
kind: z.number().describe("Event kind (should be 1 for text notes)"),
|
|
40
|
+
content: z.string().describe("Content of the note"),
|
|
41
|
+
tags: z.array(z.array(z.string())).describe("Tags array"),
|
|
42
|
+
created_at: z.number().describe("Creation timestamp"),
|
|
43
|
+
pubkey: z.string().describe("Public key of the author")
|
|
44
|
+
}).describe("Unsigned note event to sign"),
|
|
45
|
+
};
|
|
46
|
+
// Schema for publishNote tool
|
|
47
|
+
export const publishNoteToolConfig = {
|
|
48
|
+
signedNote: z.object({
|
|
49
|
+
id: z.string().describe("Event ID"),
|
|
50
|
+
pubkey: z.string().describe("Public key of the author"),
|
|
51
|
+
created_at: z.number().describe("Creation timestamp"),
|
|
52
|
+
kind: z.number().describe("Event kind"),
|
|
53
|
+
tags: z.array(z.array(z.string())).describe("Tags array"),
|
|
54
|
+
content: z.string().describe("Content of the note"),
|
|
55
|
+
sig: z.string().describe("Event signature")
|
|
56
|
+
}).describe("Signed note event to publish"),
|
|
57
|
+
relays: z.array(z.string()).optional().describe("Optional list of relays to publish to"),
|
|
58
|
+
};
|
|
59
|
+
// Helper function to format profile data
|
|
60
|
+
export function formatProfile(profile) {
|
|
61
|
+
if (!profile)
|
|
62
|
+
return "No profile found";
|
|
63
|
+
let metadata = {};
|
|
64
|
+
try {
|
|
65
|
+
metadata = profile.content ? JSON.parse(profile.content) : {};
|
|
66
|
+
}
|
|
67
|
+
catch (e) {
|
|
68
|
+
console.error("Error parsing profile metadata:", e);
|
|
69
|
+
}
|
|
70
|
+
return [
|
|
71
|
+
`Name: ${metadata.name || "Unknown"}`,
|
|
72
|
+
`Display Name: ${metadata.display_name || metadata.displayName || metadata.name || "Unknown"}`,
|
|
73
|
+
`About: ${metadata.about || "No about information"}`,
|
|
74
|
+
`NIP-05: ${metadata.nip05 || "Not set"}`,
|
|
75
|
+
`Lightning Address (LUD-16): ${metadata.lud16 || "Not set"}`,
|
|
76
|
+
`LNURL (LUD-06): ${metadata.lud06 || "Not set"}`,
|
|
77
|
+
`Picture: ${metadata.picture || "No picture"}`,
|
|
78
|
+
`Website: ${metadata.website || "No website"}`,
|
|
79
|
+
`Created At: ${new Date(profile.created_at * 1000).toISOString()}`,
|
|
80
|
+
].join("\n");
|
|
81
|
+
}
|
|
82
|
+
// Helper function to format note content
|
|
83
|
+
export function formatNote(note) {
|
|
84
|
+
if (!note)
|
|
85
|
+
return "";
|
|
86
|
+
const created = new Date(note.created_at * 1000).toLocaleString();
|
|
87
|
+
return [
|
|
88
|
+
`ID: ${note.id}`,
|
|
89
|
+
`Created: ${created}`,
|
|
90
|
+
`Content: ${note.content}`,
|
|
91
|
+
`---`,
|
|
92
|
+
].join("\n");
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Post an anonymous note to the Nostr network
|
|
96
|
+
* Generates a one-time keypair and publishes the note to specified relays
|
|
97
|
+
*/
|
|
98
|
+
export async function postAnonymousNote(content, relays = DEFAULT_RELAYS, tags = []) {
|
|
99
|
+
try {
|
|
100
|
+
// console.error(`Preparing to post anonymous note to ${relays.join(", ")}`);
|
|
101
|
+
// Create a fresh pool for this request
|
|
102
|
+
const pool = getFreshPool(relays);
|
|
103
|
+
try {
|
|
104
|
+
// Generate a one-time keypair for anonymous posting
|
|
105
|
+
const keys = await generateKeypair();
|
|
106
|
+
// Create the note event template
|
|
107
|
+
const noteTemplate = createEvent({
|
|
108
|
+
kind: 1, // kind 1 is a text note
|
|
109
|
+
content,
|
|
110
|
+
tags
|
|
111
|
+
}, keys.publicKey);
|
|
112
|
+
// Get event hash and sign it
|
|
113
|
+
const eventId = await getEventHash(noteTemplate);
|
|
114
|
+
const signature = await signEvent(eventId, keys.privateKey);
|
|
115
|
+
// Create complete signed event
|
|
116
|
+
const signedNote = {
|
|
117
|
+
...noteTemplate,
|
|
118
|
+
id: eventId,
|
|
119
|
+
sig: signature
|
|
120
|
+
};
|
|
121
|
+
const publicKey = keys.publicKey;
|
|
122
|
+
// Publish to relays and wait for actual relay OK responses
|
|
123
|
+
const pubPromises = pool.publish(relays, signedNote);
|
|
124
|
+
// Wait for all publish attempts to complete or timeout
|
|
125
|
+
const results = await Promise.allSettled(pubPromises);
|
|
126
|
+
// Check if at least one relay actually accepted the event
|
|
127
|
+
// A fulfilled promise means relay responded, but we need to check if it accepted
|
|
128
|
+
const successCount = results.filter(r => r.status === 'fulfilled' && r.value.success === true).length;
|
|
129
|
+
if (successCount === 0) {
|
|
130
|
+
return {
|
|
131
|
+
success: false,
|
|
132
|
+
message: 'Failed to publish note to any relay',
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
return {
|
|
136
|
+
success: true,
|
|
137
|
+
message: `Note published to ${successCount}/${relays.length} relays`,
|
|
138
|
+
noteId: signedNote.id,
|
|
139
|
+
publicKey: publicKey,
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
catch (error) {
|
|
143
|
+
console.error("Error posting anonymous note:", error);
|
|
144
|
+
return {
|
|
145
|
+
success: false,
|
|
146
|
+
message: `Error posting anonymous note: ${error instanceof Error ? error.message : "Unknown error"}`,
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
finally {
|
|
150
|
+
// Clean up any subscriptions and close the pool
|
|
151
|
+
await pool.close();
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
catch (error) {
|
|
155
|
+
return {
|
|
156
|
+
success: false,
|
|
157
|
+
message: `Fatal error: ${error instanceof Error ? error.message : "Unknown error"}`,
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
// Helper function to convert private key to hex if nsec format
|
|
162
|
+
function normalizePrivateKey(privateKey) {
|
|
163
|
+
if (privateKey.startsWith('nsec')) {
|
|
164
|
+
const decoded = nip19decode(privateKey);
|
|
165
|
+
if (decoded.type !== 'nsec') {
|
|
166
|
+
throw new Error('Invalid nsec format');
|
|
167
|
+
}
|
|
168
|
+
return decoded.data;
|
|
169
|
+
}
|
|
170
|
+
return privateKey;
|
|
171
|
+
}
|
|
172
|
+
// Helper function to derive public key from private key
|
|
173
|
+
function getPublicKeyFromPrivate(privateKey) {
|
|
174
|
+
return Buffer.from(schnorr.getPublicKey(privateKey)).toString('hex');
|
|
175
|
+
}
|
|
176
|
+
/**
|
|
177
|
+
* Create a new kind 1 note event (unsigned)
|
|
178
|
+
*/
|
|
179
|
+
export async function createNote(privateKey, content, tags = []) {
|
|
180
|
+
try {
|
|
181
|
+
// Normalize private key
|
|
182
|
+
const normalizedPrivateKey = normalizePrivateKey(privateKey);
|
|
183
|
+
// Derive public key from private key
|
|
184
|
+
const publicKey = getPublicKeyFromPrivate(normalizedPrivateKey);
|
|
185
|
+
// Create the note event template
|
|
186
|
+
const noteTemplate = createEvent({
|
|
187
|
+
kind: 1, // kind 1 is a text note
|
|
188
|
+
content,
|
|
189
|
+
tags
|
|
190
|
+
}, publicKey);
|
|
191
|
+
return {
|
|
192
|
+
success: true,
|
|
193
|
+
message: 'Note event created successfully (unsigned)',
|
|
194
|
+
noteEvent: noteTemplate,
|
|
195
|
+
publicKey: publicKey,
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
catch (error) {
|
|
199
|
+
return {
|
|
200
|
+
success: false,
|
|
201
|
+
message: `Error creating note: ${error instanceof Error ? error.message : "Unknown error"}`,
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
/**
|
|
206
|
+
* Sign a note event
|
|
207
|
+
*/
|
|
208
|
+
export async function signNote(privateKey, noteEvent) {
|
|
209
|
+
try {
|
|
210
|
+
// Normalize private key
|
|
211
|
+
const normalizedPrivateKey = normalizePrivateKey(privateKey);
|
|
212
|
+
// Verify the public key matches the private key
|
|
213
|
+
const derivedPubkey = getPublicKeyFromPrivate(normalizedPrivateKey);
|
|
214
|
+
if (derivedPubkey !== noteEvent.pubkey) {
|
|
215
|
+
return {
|
|
216
|
+
success: false,
|
|
217
|
+
message: 'Private key does not match the public key in the note event',
|
|
218
|
+
};
|
|
219
|
+
}
|
|
220
|
+
// Get event hash and sign it
|
|
221
|
+
const eventId = await getEventHash(noteEvent);
|
|
222
|
+
const signature = await signEvent(eventId, normalizedPrivateKey);
|
|
223
|
+
// Create complete signed event
|
|
224
|
+
const signedNote = {
|
|
225
|
+
...noteEvent,
|
|
226
|
+
id: eventId,
|
|
227
|
+
sig: signature
|
|
228
|
+
};
|
|
229
|
+
return {
|
|
230
|
+
success: true,
|
|
231
|
+
message: 'Note signed successfully',
|
|
232
|
+
signedNote: signedNote,
|
|
233
|
+
};
|
|
234
|
+
}
|
|
235
|
+
catch (error) {
|
|
236
|
+
return {
|
|
237
|
+
success: false,
|
|
238
|
+
message: `Error signing note: ${error instanceof Error ? error.message : "Unknown error"}`,
|
|
239
|
+
};
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
/**
|
|
243
|
+
* Publish a signed note to relays
|
|
244
|
+
*/
|
|
245
|
+
export async function publishNote(signedNote, relays = DEFAULT_RELAYS) {
|
|
246
|
+
try {
|
|
247
|
+
// console.error(`Preparing to publish note to ${relays.join(", ")}`);
|
|
248
|
+
// If no relays specified, just return success with event validation
|
|
249
|
+
if (relays.length === 0) {
|
|
250
|
+
return {
|
|
251
|
+
success: true,
|
|
252
|
+
message: 'Note is valid and ready to publish (no relays specified)',
|
|
253
|
+
noteId: signedNote.id,
|
|
254
|
+
};
|
|
255
|
+
}
|
|
256
|
+
// Create a fresh pool for this request
|
|
257
|
+
const pool = getFreshPool(relays);
|
|
258
|
+
try {
|
|
259
|
+
// Publish to relays and wait for actual relay OK responses
|
|
260
|
+
const pubPromises = pool.publish(relays, signedNote);
|
|
261
|
+
// Wait for all publish attempts to complete or timeout
|
|
262
|
+
const results = await Promise.allSettled(pubPromises);
|
|
263
|
+
// Check if at least one relay actually accepted the event
|
|
264
|
+
// A fulfilled promise means relay responded, but we need to check if it accepted
|
|
265
|
+
const successCount = results.filter(r => r.status === 'fulfilled' && r.value.success === true).length;
|
|
266
|
+
if (successCount === 0) {
|
|
267
|
+
return {
|
|
268
|
+
success: false,
|
|
269
|
+
message: 'Failed to publish note to any relay',
|
|
270
|
+
};
|
|
271
|
+
}
|
|
272
|
+
return {
|
|
273
|
+
success: true,
|
|
274
|
+
message: `Note published to ${successCount}/${relays.length} relays`,
|
|
275
|
+
noteId: signedNote.id,
|
|
276
|
+
};
|
|
277
|
+
}
|
|
278
|
+
catch (error) {
|
|
279
|
+
console.error("Error publishing note:", error);
|
|
280
|
+
return {
|
|
281
|
+
success: false,
|
|
282
|
+
message: `Error publishing note: ${error instanceof Error ? error.message : "Unknown error"}`,
|
|
283
|
+
};
|
|
284
|
+
}
|
|
285
|
+
finally {
|
|
286
|
+
// Clean up any subscriptions and close the pool
|
|
287
|
+
await pool.close();
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
catch (error) {
|
|
291
|
+
return {
|
|
292
|
+
success: false,
|
|
293
|
+
message: `Fatal error: ${error instanceof Error ? error.message : "Unknown error"}`,
|
|
294
|
+
};
|
|
295
|
+
}
|
|
296
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
// Schema for getProfile tool
|
|
3
|
+
export const getProfileToolConfig = {
|
|
4
|
+
pubkey: z.string().describe("Public key of the Nostr user (hex format or npub format)"),
|
|
5
|
+
relays: z.array(z.string()).optional().describe("Optional list of relays to query"),
|
|
6
|
+
};
|
|
7
|
+
// Schema for getKind1Notes tool
|
|
8
|
+
export const getKind1NotesToolConfig = {
|
|
9
|
+
pubkey: z.string().describe("Public key of the Nostr user (hex format or npub format)"),
|
|
10
|
+
limit: z.number().min(1).max(100).default(10).describe("Maximum number of notes to fetch"),
|
|
11
|
+
relays: z.array(z.string()).optional().describe("Optional list of relays to query"),
|
|
12
|
+
};
|
|
13
|
+
// Schema for getLongFormNotes tool
|
|
14
|
+
export const getLongFormNotesToolConfig = {
|
|
15
|
+
pubkey: z.string().describe("Public key of the Nostr user (hex format or npub format)"),
|
|
16
|
+
limit: z.number().min(1).max(100).default(10).describe("Maximum number of notes to fetch"),
|
|
17
|
+
relays: z.array(z.string()).optional().describe("Optional list of relays to query"),
|
|
18
|
+
};
|
|
19
|
+
// Helper function to format profile data
|
|
20
|
+
export function formatProfile(profile) {
|
|
21
|
+
if (!profile)
|
|
22
|
+
return "No profile found";
|
|
23
|
+
let metadata = {};
|
|
24
|
+
try {
|
|
25
|
+
metadata = profile.content ? JSON.parse(profile.content) : {};
|
|
26
|
+
}
|
|
27
|
+
catch (e) {
|
|
28
|
+
console.error("Error parsing profile metadata:", e);
|
|
29
|
+
}
|
|
30
|
+
return [
|
|
31
|
+
`Name: ${metadata.name || "Unknown"}`,
|
|
32
|
+
`Display Name: ${metadata.display_name || metadata.displayName || metadata.name || "Unknown"}`,
|
|
33
|
+
`About: ${metadata.about || "No about information"}`,
|
|
34
|
+
`NIP-05: ${metadata.nip05 || "Not set"}`,
|
|
35
|
+
`Lightning Address (LUD-16): ${metadata.lud16 || "Not set"}`,
|
|
36
|
+
`LNURL (LUD-06): ${metadata.lud06 || "Not set"}`,
|
|
37
|
+
`Picture: ${metadata.picture || "No picture"}`,
|
|
38
|
+
`Website: ${metadata.website || "No website"}`,
|
|
39
|
+
`Created At: ${new Date(profile.created_at * 1000).toISOString()}`,
|
|
40
|
+
].join("\n");
|
|
41
|
+
}
|
|
42
|
+
// Helper function to format note content
|
|
43
|
+
export function formatNote(note) {
|
|
44
|
+
if (!note)
|
|
45
|
+
return "";
|
|
46
|
+
const created = new Date(note.created_at * 1000).toLocaleString();
|
|
47
|
+
return [
|
|
48
|
+
`ID: ${note.id}`,
|
|
49
|
+
`Created: ${created}`,
|
|
50
|
+
`Content: ${note.content}`,
|
|
51
|
+
`---`,
|
|
52
|
+
].join("\n");
|
|
53
|
+
}
|
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { DEFAULT_RELAYS } from "../utils/index.js";
|
|
3
|
+
import { generateKeypair, createEvent, getEventHash, signEvent, decode as nip19decode, encodePublicKey, encodePrivateKey } from "snstr";
|
|
4
|
+
import { getFreshPool } from "../utils/index.js";
|
|
5
|
+
import { schnorr } from '@noble/curves/secp256k1';
|
|
6
|
+
// Schema for createKeypair tool
|
|
7
|
+
export const createKeypairToolConfig = {
|
|
8
|
+
format: z.enum(["both", "hex", "npub"]).default("both").describe("Format to return keys in: hex only, npub only, or both"),
|
|
9
|
+
};
|
|
10
|
+
// Schema for createProfile tool
|
|
11
|
+
export const createProfileToolConfig = {
|
|
12
|
+
privateKey: z.string().describe("Private key to sign the profile with (hex format or nsec format)"),
|
|
13
|
+
name: z.string().optional().describe("Display name for the profile"),
|
|
14
|
+
about: z.string().optional().describe("About/bio text for the profile"),
|
|
15
|
+
picture: z.string().optional().describe("URL to profile picture"),
|
|
16
|
+
nip05: z.string().optional().describe("NIP-05 identifier (like email@domain.com)"),
|
|
17
|
+
lud16: z.string().optional().describe("Lightning address for receiving payments"),
|
|
18
|
+
lud06: z.string().optional().describe("LNURL for receiving payments"),
|
|
19
|
+
website: z.string().optional().describe("Personal website URL"),
|
|
20
|
+
relays: z.array(z.string()).optional().describe("Optional list of relays to publish to"),
|
|
21
|
+
};
|
|
22
|
+
// Schema for updateProfile tool
|
|
23
|
+
export const updateProfileToolConfig = {
|
|
24
|
+
privateKey: z.string().describe("Private key to sign the profile with (hex format or nsec format)"),
|
|
25
|
+
name: z.string().optional().describe("Display name for the profile"),
|
|
26
|
+
about: z.string().optional().describe("About/bio text for the profile"),
|
|
27
|
+
picture: z.string().optional().describe("URL to profile picture"),
|
|
28
|
+
nip05: z.string().optional().describe("NIP-05 identifier (like email@domain.com)"),
|
|
29
|
+
lud16: z.string().optional().describe("Lightning address for receiving payments"),
|
|
30
|
+
lud06: z.string().optional().describe("LNURL for receiving payments"),
|
|
31
|
+
website: z.string().optional().describe("Personal website URL"),
|
|
32
|
+
relays: z.array(z.string()).optional().describe("Optional list of relays to publish to"),
|
|
33
|
+
};
|
|
34
|
+
// Schema for postNote tool
|
|
35
|
+
export const postNoteToolConfig = {
|
|
36
|
+
privateKey: z.string().describe("Private key to sign the note with (hex format or nsec format)"),
|
|
37
|
+
content: z.string().describe("Content of the note to post"),
|
|
38
|
+
tags: z.array(z.array(z.string())).optional().describe("Optional tags to include with the note"),
|
|
39
|
+
relays: z.array(z.string()).optional().describe("Optional list of relays to publish to"),
|
|
40
|
+
};
|
|
41
|
+
// Helper function to convert private key to hex if nsec format
|
|
42
|
+
function normalizePrivateKey(privateKey) {
|
|
43
|
+
if (privateKey.startsWith('nsec')) {
|
|
44
|
+
// Validate nsec format before type assertion
|
|
45
|
+
if (!/^nsec1[0-9a-z]+$/.test(privateKey)) {
|
|
46
|
+
throw new Error('Invalid nsec format: must match pattern nsec1[0-9a-z]+');
|
|
47
|
+
}
|
|
48
|
+
const decoded = nip19decode(privateKey);
|
|
49
|
+
if (decoded.type !== 'nsec') {
|
|
50
|
+
throw new Error('Invalid nsec format');
|
|
51
|
+
}
|
|
52
|
+
return decoded.data;
|
|
53
|
+
}
|
|
54
|
+
// Validate hex format for non-nsec keys
|
|
55
|
+
if (!/^[0-9a-f]{64}$/.test(privateKey)) {
|
|
56
|
+
throw new Error('Invalid private key format: must be 64-character hex string or valid nsec format');
|
|
57
|
+
}
|
|
58
|
+
return privateKey;
|
|
59
|
+
}
|
|
60
|
+
// Helper function to derive public key from private key
|
|
61
|
+
function getPublicKeyFromPrivate(privateKey) {
|
|
62
|
+
return Buffer.from(schnorr.getPublicKey(privateKey)).toString('hex');
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Generate a new Nostr keypair
|
|
66
|
+
*/
|
|
67
|
+
export async function createKeypair(format = "both") {
|
|
68
|
+
try {
|
|
69
|
+
// Generate a new keypair
|
|
70
|
+
const keys = await generateKeypair();
|
|
71
|
+
const result = {};
|
|
72
|
+
if (format === "hex" || format === "both") {
|
|
73
|
+
result.publicKey = keys.publicKey;
|
|
74
|
+
result.privateKey = keys.privateKey;
|
|
75
|
+
}
|
|
76
|
+
if (format === "npub" || format === "both") {
|
|
77
|
+
result.npub = encodePublicKey(keys.publicKey);
|
|
78
|
+
result.nsec = encodePrivateKey(keys.privateKey);
|
|
79
|
+
}
|
|
80
|
+
return result;
|
|
81
|
+
}
|
|
82
|
+
catch (error) {
|
|
83
|
+
throw new Error(`Failed to generate keypair: ${error instanceof Error ? error.message : "Unknown error"}`);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Create a new Nostr profile (kind 0 event)
|
|
88
|
+
*/
|
|
89
|
+
export async function createProfile(privateKey, profileData, relays = DEFAULT_RELAYS) {
|
|
90
|
+
try {
|
|
91
|
+
// Normalize private key
|
|
92
|
+
const normalizedPrivateKey = normalizePrivateKey(privateKey);
|
|
93
|
+
// Derive public key from private key
|
|
94
|
+
const publicKey = getPublicKeyFromPrivate(normalizedPrivateKey);
|
|
95
|
+
// Create profile metadata object
|
|
96
|
+
const metadata = {};
|
|
97
|
+
if (profileData.name)
|
|
98
|
+
metadata.name = profileData.name;
|
|
99
|
+
if (profileData.about)
|
|
100
|
+
metadata.about = profileData.about;
|
|
101
|
+
if (profileData.picture)
|
|
102
|
+
metadata.picture = profileData.picture;
|
|
103
|
+
if (profileData.nip05)
|
|
104
|
+
metadata.nip05 = profileData.nip05;
|
|
105
|
+
if (profileData.lud16)
|
|
106
|
+
metadata.lud16 = profileData.lud16;
|
|
107
|
+
if (profileData.lud06)
|
|
108
|
+
metadata.lud06 = profileData.lud06;
|
|
109
|
+
if (profileData.website)
|
|
110
|
+
metadata.website = profileData.website;
|
|
111
|
+
// Create a fresh pool for this request
|
|
112
|
+
const pool = getFreshPool(relays);
|
|
113
|
+
try {
|
|
114
|
+
// Create the profile event template
|
|
115
|
+
const profileTemplate = createEvent({
|
|
116
|
+
kind: 0, // kind 0 is profile metadata
|
|
117
|
+
content: JSON.stringify(metadata),
|
|
118
|
+
tags: []
|
|
119
|
+
}, publicKey);
|
|
120
|
+
// Get event hash and sign it
|
|
121
|
+
const eventId = await getEventHash(profileTemplate);
|
|
122
|
+
const signature = await signEvent(eventId, normalizedPrivateKey);
|
|
123
|
+
// Create complete signed event
|
|
124
|
+
const signedProfile = {
|
|
125
|
+
...profileTemplate,
|
|
126
|
+
id: eventId,
|
|
127
|
+
sig: signature
|
|
128
|
+
};
|
|
129
|
+
// If no relays specified, just return success with event creation
|
|
130
|
+
if (relays.length === 0) {
|
|
131
|
+
return {
|
|
132
|
+
success: true,
|
|
133
|
+
message: 'Profile event created successfully (no relays specified for publishing)',
|
|
134
|
+
eventId: signedProfile.id,
|
|
135
|
+
publicKey: publicKey,
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
// Publish to relays - pool.publish returns array of promises
|
|
139
|
+
const pubPromises = pool.publish(relays, signedProfile);
|
|
140
|
+
// Wait for all publish attempts to complete or timeout
|
|
141
|
+
const results = await Promise.allSettled(pubPromises);
|
|
142
|
+
// Check if at least one relay accepted the profile
|
|
143
|
+
const successCount = results.filter(r => r.status === 'fulfilled' && r.value?.success === true).length;
|
|
144
|
+
if (successCount === 0) {
|
|
145
|
+
return {
|
|
146
|
+
success: false,
|
|
147
|
+
message: 'Failed to publish profile to any relay',
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
return {
|
|
151
|
+
success: true,
|
|
152
|
+
message: `Profile published to ${successCount}/${relays.length} relays`,
|
|
153
|
+
eventId: signedProfile.id,
|
|
154
|
+
publicKey: publicKey,
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
catch (error) {
|
|
158
|
+
console.error("Error creating profile:", error);
|
|
159
|
+
return {
|
|
160
|
+
success: false,
|
|
161
|
+
message: `Error creating profile: ${error instanceof Error ? error.message : "Unknown error"}`,
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
finally {
|
|
165
|
+
// Clean up any subscriptions and close the pool
|
|
166
|
+
await pool.close();
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
catch (error) {
|
|
170
|
+
return {
|
|
171
|
+
success: false,
|
|
172
|
+
message: `Fatal error: ${error instanceof Error ? error.message : "Unknown error"}`,
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
/**
|
|
177
|
+
* Update an existing Nostr profile (kind 0 event)
|
|
178
|
+
* This creates a new profile event that replaces the previous one
|
|
179
|
+
*/
|
|
180
|
+
export async function updateProfile(privateKey, profileData, relays = DEFAULT_RELAYS) {
|
|
181
|
+
// For kind 0 events (profiles), updating is the same as creating
|
|
182
|
+
// The newest event replaces the older one
|
|
183
|
+
return createProfile(privateKey, profileData, relays);
|
|
184
|
+
}
|
|
185
|
+
/**
|
|
186
|
+
* Post a note using an existing private key (authenticated posting)
|
|
187
|
+
* This is a convenient all-in-one function that creates, signs, and publishes a note
|
|
188
|
+
*/
|
|
189
|
+
export async function postNote(privateKey, content, tags = [], relays = DEFAULT_RELAYS) {
|
|
190
|
+
try {
|
|
191
|
+
// console.log(`Preparing to post authenticated note to ${relays.join(", ")}`);
|
|
192
|
+
// Normalize private key
|
|
193
|
+
const normalizedPrivateKey = normalizePrivateKey(privateKey);
|
|
194
|
+
// Derive public key from private key
|
|
195
|
+
const publicKey = getPublicKeyFromPrivate(normalizedPrivateKey);
|
|
196
|
+
// Create a fresh pool for this request
|
|
197
|
+
const pool = getFreshPool(relays);
|
|
198
|
+
try {
|
|
199
|
+
// Create the note event template
|
|
200
|
+
const noteTemplate = createEvent({
|
|
201
|
+
kind: 1, // kind 1 is a text note
|
|
202
|
+
content,
|
|
203
|
+
tags
|
|
204
|
+
}, publicKey);
|
|
205
|
+
// Get event hash and sign it
|
|
206
|
+
const eventId = await getEventHash(noteTemplate);
|
|
207
|
+
const signature = await signEvent(eventId, normalizedPrivateKey);
|
|
208
|
+
// Create complete signed event
|
|
209
|
+
const signedNote = {
|
|
210
|
+
...noteTemplate,
|
|
211
|
+
id: eventId,
|
|
212
|
+
sig: signature
|
|
213
|
+
};
|
|
214
|
+
// If no relays specified, just return success with event creation
|
|
215
|
+
if (relays.length === 0) {
|
|
216
|
+
return {
|
|
217
|
+
success: true,
|
|
218
|
+
message: 'Note created and signed successfully (no relays specified for publishing)',
|
|
219
|
+
noteId: signedNote.id,
|
|
220
|
+
publicKey: publicKey,
|
|
221
|
+
};
|
|
222
|
+
}
|
|
223
|
+
// Publish to relays - pool.publish returns array of promises
|
|
224
|
+
const pubPromises = pool.publish(relays, signedNote);
|
|
225
|
+
// Wait for all publish attempts to complete or timeout
|
|
226
|
+
const results = await Promise.allSettled(pubPromises);
|
|
227
|
+
// Check if at least one relay accepted the note
|
|
228
|
+
const successCount = results.filter(r => r.status === 'fulfilled' && r.value?.success === true).length;
|
|
229
|
+
if (successCount === 0) {
|
|
230
|
+
return {
|
|
231
|
+
success: false,
|
|
232
|
+
message: 'Failed to publish note to any relay',
|
|
233
|
+
};
|
|
234
|
+
}
|
|
235
|
+
return {
|
|
236
|
+
success: true,
|
|
237
|
+
message: `Note published to ${successCount}/${relays.length} relays`,
|
|
238
|
+
noteId: signedNote.id,
|
|
239
|
+
publicKey: publicKey,
|
|
240
|
+
};
|
|
241
|
+
}
|
|
242
|
+
catch (error) {
|
|
243
|
+
console.error("Error posting note:", error);
|
|
244
|
+
return {
|
|
245
|
+
success: false,
|
|
246
|
+
message: `Error posting note: ${error instanceof Error ? error.message : "Unknown error"}`,
|
|
247
|
+
};
|
|
248
|
+
}
|
|
249
|
+
finally {
|
|
250
|
+
// Clean up any subscriptions and close the pool
|
|
251
|
+
await pool.close();
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
catch (error) {
|
|
255
|
+
return {
|
|
256
|
+
success: false,
|
|
257
|
+
message: `Fatal error: ${error instanceof Error ? error.message : "Unknown error"}`,
|
|
258
|
+
};
|
|
259
|
+
}
|
|
260
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
// Set a reasonable timeout for queries
|
|
2
|
+
export const QUERY_TIMEOUT = 8000;
|
|
3
|
+
// Define default relays
|
|
4
|
+
export const DEFAULT_RELAYS = [
|
|
5
|
+
"wss://relay.damus.io",
|
|
6
|
+
"wss://relay.nostr.band",
|
|
7
|
+
"wss://relay.primal.net",
|
|
8
|
+
"wss://nos.lol",
|
|
9
|
+
"wss://purplerelay.com",
|
|
10
|
+
"wss://nostr.land"
|
|
11
|
+
];
|
|
12
|
+
// Add more popular relays that we can try if the default ones fail
|
|
13
|
+
export const FALLBACK_RELAYS = [
|
|
14
|
+
"wss://nostr.mom",
|
|
15
|
+
"wss://nostr.noones.com",
|
|
16
|
+
"wss://nostr-pub.wellorder.net",
|
|
17
|
+
"wss://nostr.bitcoiner.social",
|
|
18
|
+
"wss://at.nostrworks.com",
|
|
19
|
+
"wss://lightningrelay.com",
|
|
20
|
+
];
|
|
21
|
+
// Define event kinds
|
|
22
|
+
export const KINDS = {
|
|
23
|
+
Metadata: 0,
|
|
24
|
+
Text: 1,
|
|
25
|
+
ZapRequest: 9734,
|
|
26
|
+
ZapReceipt: 9735
|
|
27
|
+
};
|