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.
Files changed (36) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +498 -0
  3. package/build/__tests__/basic.test.js +87 -0
  4. package/build/__tests__/error-handling.test.js +145 -0
  5. package/build/__tests__/format-conversion.test.js +137 -0
  6. package/build/__tests__/integration.test.js +163 -0
  7. package/build/__tests__/mocks.js +109 -0
  8. package/build/__tests__/nip19-conversion.test.js +268 -0
  9. package/build/__tests__/nips-search.test.js +109 -0
  10. package/build/__tests__/note-creation.test.js +148 -0
  11. package/build/__tests__/note-tools-functions.test.js +173 -0
  12. package/build/__tests__/note-tools-unit.test.js +97 -0
  13. package/build/__tests__/profile-notes-simple.test.js +78 -0
  14. package/build/__tests__/profile-postnote.test.js +120 -0
  15. package/build/__tests__/profile-tools.test.js +90 -0
  16. package/build/__tests__/relay-specification.test.js +136 -0
  17. package/build/__tests__/search-nips-simple.test.js +96 -0
  18. package/build/__tests__/websocket-integration.test.js +257 -0
  19. package/build/__tests__/zap-tools-simple.test.js +72 -0
  20. package/build/__tests__/zap-tools-tests.test.js +197 -0
  21. package/build/index.js +1285 -0
  22. package/build/nips/nips-tools.js +567 -0
  23. package/build/nips-tools.js +421 -0
  24. package/build/note/note-tools.js +296 -0
  25. package/build/note-tools.js +53 -0
  26. package/build/profile/profile-tools.js +260 -0
  27. package/build/utils/constants.js +27 -0
  28. package/build/utils/conversion.js +332 -0
  29. package/build/utils/ephemeral-relay.js +438 -0
  30. package/build/utils/formatting.js +34 -0
  31. package/build/utils/index.js +6 -0
  32. package/build/utils/nip19-tools.js +117 -0
  33. package/build/utils/pool.js +55 -0
  34. package/build/zap/zap-tools.js +980 -0
  35. package/build/zap-tools.js +989 -0
  36. 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
+ };