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,268 @@
1
+ import { convertNip19, analyzeNip19 } from '../utils/nip19-tools.js';
2
+ import { generateKeypair, encodePublicKey, encodePrivateKey, encodeNoteId, encodeProfile, encodeEvent, encodeAddress } from 'snstr';
3
+ describe('NIP-19 Conversion Tools', () => {
4
+ let testKeys;
5
+ let testNpub;
6
+ let testNsec;
7
+ beforeAll(async () => {
8
+ // Generate test keypair
9
+ testKeys = await generateKeypair();
10
+ testNpub = encodePublicKey(testKeys.publicKey);
11
+ testNsec = encodePrivateKey(testKeys.privateKey);
12
+ });
13
+ describe('convertNip19', () => {
14
+ describe('hex to other formats', () => {
15
+ it('should convert hex pubkey to npub', async () => {
16
+ const result = await convertNip19(testKeys.publicKey, 'npub');
17
+ expect(result.success).toBe(true);
18
+ expect(result.result).toBe(testNpub);
19
+ expect(result.originalType).toBe('hex');
20
+ expect(result.message).toContain('Successfully converted');
21
+ });
22
+ it('should convert hex to hex (no-op)', async () => {
23
+ const result = await convertNip19(testKeys.publicKey, 'hex');
24
+ expect(result.success).toBe(true);
25
+ expect(result.result).toBe(testKeys.publicKey);
26
+ expect(result.originalType).toBe('hex');
27
+ });
28
+ it('should convert hex event ID to note', async () => {
29
+ const eventId = '1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef';
30
+ const result = await convertNip19(eventId, 'note');
31
+ expect(result.success).toBe(true);
32
+ expect(result.result).toMatch(/^note1/);
33
+ expect(result.originalType).toBe('hex');
34
+ });
35
+ it('should convert hex to nprofile with relays', async () => {
36
+ const relays = ['wss://relay.damus.io', 'wss://nos.lol'];
37
+ const result = await convertNip19(testKeys.publicKey, 'nprofile', relays);
38
+ expect(result.success).toBe(true);
39
+ expect(result.result).toMatch(/^nprofile1/);
40
+ expect(result.originalType).toBe('hex');
41
+ });
42
+ it('should convert hex to nevent with metadata', async () => {
43
+ const eventId = '1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef';
44
+ const relays = ['wss://relay.damus.io'];
45
+ const result = await convertNip19(eventId, 'nevent', relays, testKeys.publicKey, 1);
46
+ expect(result.success).toBe(true);
47
+ expect(result.result).toMatch(/^nevent1/);
48
+ expect(result.originalType).toBe('hex');
49
+ });
50
+ it('should convert hex to naddr with required fields', async () => {
51
+ const relays = ['wss://relay.damus.io'];
52
+ const result = await convertNip19(testKeys.publicKey, 'naddr', relays, undefined, 30023, 'test-identifier');
53
+ expect(result.success).toBe(true);
54
+ expect(result.result).toMatch(/^naddr1/);
55
+ expect(result.originalType).toBe('hex');
56
+ });
57
+ });
58
+ describe('npub conversions', () => {
59
+ it('should convert npub to hex', async () => {
60
+ const result = await convertNip19(testNpub, 'hex');
61
+ expect(result.success).toBe(true);
62
+ expect(result.result).toBe(testKeys.publicKey);
63
+ expect(result.originalType).toBe('npub');
64
+ });
65
+ it('should convert npub to nprofile', async () => {
66
+ const relays = ['wss://relay.primal.net'];
67
+ const result = await convertNip19(testNpub, 'nprofile', relays);
68
+ expect(result.success).toBe(true);
69
+ expect(result.result).toMatch(/^nprofile1/);
70
+ expect(result.originalType).toBe('npub');
71
+ });
72
+ });
73
+ describe('nsec conversions', () => {
74
+ it('should convert nsec to hex', async () => {
75
+ const result = await convertNip19(testNsec, 'hex');
76
+ expect(result.success).toBe(true);
77
+ expect(result.result).toBe(testKeys.privateKey);
78
+ expect(result.originalType).toBe('nsec');
79
+ });
80
+ it('should handle nsec to npub conversion', async () => {
81
+ const result = await convertNip19(testNsec, 'npub');
82
+ // The implementation might derive the public key from the private key
83
+ // or it might fail - let's check what actually happens
84
+ if (result.success) {
85
+ // If it succeeds, it should return the corresponding npub
86
+ expect(result.result).toMatch(/^npub1/);
87
+ expect(result.originalType).toBe('nsec');
88
+ }
89
+ else {
90
+ // If it fails, check the error message
91
+ expect(result.message).toBeDefined();
92
+ }
93
+ });
94
+ });
95
+ describe('complex entity conversions', () => {
96
+ it('should convert nprofile to npub', async () => {
97
+ const nprofile = encodeProfile({
98
+ pubkey: testKeys.publicKey,
99
+ relays: ['wss://relay.damus.io']
100
+ });
101
+ const result = await convertNip19(nprofile, 'npub');
102
+ expect(result.success).toBe(true);
103
+ expect(result.result).toBe(testNpub);
104
+ expect(result.originalType).toBe('nprofile');
105
+ });
106
+ it('should convert nevent to note', async () => {
107
+ const eventId = '1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef';
108
+ const nevent = encodeEvent({
109
+ id: eventId,
110
+ relays: ['wss://relay.damus.io'],
111
+ author: testKeys.publicKey,
112
+ kind: 1
113
+ });
114
+ const result = await convertNip19(nevent, 'note');
115
+ expect(result.success).toBe(true);
116
+ expect(result.result).toBe(encodeNoteId(eventId));
117
+ expect(result.originalType).toBe('nevent');
118
+ });
119
+ it('should extract author from nevent to npub', async () => {
120
+ const eventId = '1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef';
121
+ const nevent = encodeEvent({
122
+ id: eventId,
123
+ relays: ['wss://relay.damus.io'],
124
+ author: testKeys.publicKey,
125
+ kind: 1
126
+ });
127
+ const result = await convertNip19(nevent, 'npub');
128
+ expect(result.success).toBe(true);
129
+ expect(result.result).toBe(testNpub);
130
+ expect(result.originalType).toBe('nevent');
131
+ });
132
+ it('should convert naddr to hex (pubkey)', async () => {
133
+ const naddr = encodeAddress({
134
+ identifier: 'test-article',
135
+ pubkey: testKeys.publicKey,
136
+ kind: 30023,
137
+ relays: ['wss://relay.damus.io']
138
+ });
139
+ const result = await convertNip19(naddr, 'hex');
140
+ expect(result.success).toBe(true);
141
+ expect(result.result).toBe(testKeys.publicKey);
142
+ expect(result.originalType).toBe('naddr');
143
+ });
144
+ });
145
+ describe('error handling', () => {
146
+ it('should fail with invalid input', async () => {
147
+ const result = await convertNip19('invalid_input', 'npub');
148
+ expect(result.success).toBe(false);
149
+ expect(result.message).toContain('not a valid NIP-19 entity');
150
+ });
151
+ it('should handle converting note to npub', async () => {
152
+ const note = encodeNoteId('1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef');
153
+ const result = await convertNip19(note, 'npub');
154
+ // Note entities don't contain pubkey information
155
+ if (!result.success) {
156
+ expect(result.message).toBeDefined();
157
+ }
158
+ });
159
+ it('should fail naddr conversion without required fields', async () => {
160
+ const result = await convertNip19(testKeys.publicKey, 'naddr');
161
+ expect(result.success).toBe(false);
162
+ expect(result.message).toContain('requires identifier and kind');
163
+ });
164
+ it('should filter out invalid relay URLs', async () => {
165
+ const invalidRelays = [
166
+ 'wss://valid.relay.com',
167
+ 'https://invalid.relay.com', // Wrong protocol
168
+ 'wss://user:pass@relay.com', // Has credentials
169
+ 'invalid-url' // Not a URL
170
+ ];
171
+ const result = await convertNip19(testKeys.publicKey, 'nprofile', invalidRelays);
172
+ expect(result.success).toBe(true);
173
+ expect(result.result).toMatch(/^nprofile1/);
174
+ // Should only include the valid relay
175
+ expect(result.data).toBeDefined();
176
+ });
177
+ });
178
+ });
179
+ describe('analyzeNip19', () => {
180
+ it('should analyze hex string', async () => {
181
+ const result = await analyzeNip19(testKeys.publicKey);
182
+ expect(result.success).toBe(true);
183
+ expect(result.type).toBe('hex');
184
+ expect(result.data).toBe(testKeys.publicKey);
185
+ expect(result.message).toContain('Valid 64-character hex string');
186
+ });
187
+ it('should analyze npub', async () => {
188
+ const result = await analyzeNip19(testNpub);
189
+ expect(result.success).toBe(true);
190
+ expect(result.type).toBe('npub');
191
+ expect(result.data).toBe(testKeys.publicKey);
192
+ expect(result.message).toContain('Valid npub entity');
193
+ });
194
+ it('should analyze nsec', async () => {
195
+ const result = await analyzeNip19(testNsec);
196
+ expect(result.success).toBe(true);
197
+ expect(result.type).toBe('nsec');
198
+ expect(result.data).toBe(testKeys.privateKey);
199
+ expect(result.message).toContain('Valid nsec entity');
200
+ });
201
+ it('should analyze note', async () => {
202
+ const eventId = '1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef';
203
+ const note = encodeNoteId(eventId);
204
+ const result = await analyzeNip19(note);
205
+ expect(result.success).toBe(true);
206
+ expect(result.type).toBe('note');
207
+ expect(result.data).toBe(eventId);
208
+ expect(result.message).toContain('Valid note entity');
209
+ });
210
+ it('should analyze nprofile with relays', async () => {
211
+ const nprofile = encodeProfile({
212
+ pubkey: testKeys.publicKey,
213
+ relays: ['wss://valid.relay.com']
214
+ });
215
+ const result = await analyzeNip19(nprofile);
216
+ expect(result.success).toBe(true);
217
+ expect(result.type).toBe('nprofile');
218
+ expect(result.data.pubkey).toBe(testKeys.publicKey);
219
+ expect(result.data.relays).toEqual(['wss://valid.relay.com']);
220
+ expect(result.message).toContain('Valid nprofile entity');
221
+ });
222
+ it('should analyze nevent', async () => {
223
+ const eventId = '1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef';
224
+ const nevent = encodeEvent({
225
+ id: eventId,
226
+ relays: ['wss://relay.damus.io'],
227
+ author: testKeys.publicKey,
228
+ kind: 1
229
+ });
230
+ const result = await analyzeNip19(nevent);
231
+ expect(result.success).toBe(true);
232
+ expect(result.type).toBe('nevent');
233
+ expect(result.data.id).toBe(eventId);
234
+ expect(result.data.author).toBe(testKeys.publicKey);
235
+ if (result.data.kind)
236
+ expect(result.data.kind).toBe(1);
237
+ expect(result.data.relays).toEqual(['wss://relay.damus.io']);
238
+ expect(result.message).toContain('Valid nevent entity');
239
+ });
240
+ it('should analyze naddr', async () => {
241
+ const naddr = encodeAddress({
242
+ identifier: 'test-article',
243
+ pubkey: testKeys.publicKey,
244
+ kind: 30023,
245
+ relays: ['wss://relay.damus.io']
246
+ });
247
+ const result = await analyzeNip19(naddr);
248
+ expect(result.success).toBe(true);
249
+ expect(result.type).toBe('naddr');
250
+ expect(result.data.identifier).toBe('test-article');
251
+ expect(result.data.pubkey).toBe(testKeys.publicKey);
252
+ expect(result.data.kind).toBe(30023);
253
+ expect(result.data.relays).toEqual(['wss://relay.damus.io']);
254
+ expect(result.message).toContain('Valid naddr entity');
255
+ });
256
+ it('should fail with invalid input', async () => {
257
+ const result = await analyzeNip19('not_a_valid_nip19_or_hex');
258
+ expect(result.success).toBe(false);
259
+ expect(result.message).toBeDefined();
260
+ expect(result.message.toLowerCase()).toContain('unknown prefix');
261
+ });
262
+ it('should fail with invalid hex (wrong length)', async () => {
263
+ const result = await analyzeNip19('abcdef123456'); // Too short
264
+ expect(result.success).toBe(false);
265
+ expect(result.message).toBeDefined();
266
+ });
267
+ });
268
+ });
@@ -0,0 +1,109 @@
1
+ import { describe, expect, test, jest } from '@jest/globals';
2
+ // Mock NIP data for testing
3
+ const mockNips = [
4
+ {
5
+ id: '01',
6
+ title: 'Basic Protocol Flow Description',
7
+ content: 'This NIP defines the basic protocol flow between clients and relays...',
8
+ keywords: ['protocol', 'relay', 'client', 'basic']
9
+ },
10
+ {
11
+ id: '19',
12
+ title: 'bech32-encoded entities',
13
+ content: 'This NIP defines how to encode and reference different Nostr entities...',
14
+ keywords: ['encoding', 'bech32', 'npub', 'note', 'nevent', 'naddr', 'identifier']
15
+ },
16
+ {
17
+ id: '57',
18
+ title: 'Lightning Zaps',
19
+ content: 'This NIP defines how users can send zaps to each other using lightning...',
20
+ keywords: ['lightning', 'zap', 'payment', 'tip', 'lnurl']
21
+ }
22
+ ];
23
+ // Mock data for testing
24
+ const mockSearchResults = (query, limit = 10, includeContent = false) => {
25
+ // Simple relevance scoring algorithm for testing
26
+ const results = mockNips.map(nip => {
27
+ const titleMatches = (nip.title.toLowerCase().includes(query.toLowerCase()) ? 2 : 0);
28
+ const keywordMatches = nip.keywords.filter(kw => kw.includes(query.toLowerCase())).length;
29
+ const contentMatches = (nip.content.toLowerCase().includes(query.toLowerCase()) ? 1 : 0);
30
+ const relevance = titleMatches + keywordMatches + contentMatches;
31
+ return {
32
+ id: nip.id,
33
+ title: nip.title,
34
+ relevance: relevance,
35
+ content: includeContent ? nip.content : undefined
36
+ };
37
+ })
38
+ .filter(result => result.relevance > 0)
39
+ .sort((a, b) => b.relevance - a.relevance)
40
+ .slice(0, limit);
41
+ return results;
42
+ };
43
+ // Mock search function
44
+ const searchNips = jest.fn((query, options = {}) => {
45
+ const { limit = 10, includeContent = false } = options;
46
+ return Promise.resolve(mockSearchResults(query, limit, includeContent));
47
+ });
48
+ describe('NIP Search Functionality', () => {
49
+ beforeEach(() => {
50
+ searchNips.mockClear();
51
+ });
52
+ test('basic search returns relevant results', async () => {
53
+ const results = await searchNips('lightning');
54
+ expect(results.length).toBeGreaterThan(0);
55
+ expect(results[0].id).toBe('57'); // NIP-57 is most relevant for 'lightning'
56
+ expect(results[0].title).toBe('Lightning Zaps');
57
+ expect(results[0].relevance).toBeGreaterThan(0);
58
+ expect(results[0].content).toBeUndefined(); // Content shouldn't be included by default
59
+ });
60
+ test('search with includeContent option', async () => {
61
+ const results = await searchNips('protocol', { includeContent: true });
62
+ expect(results.length).toBeGreaterThan(0);
63
+ expect(results[0].id).toBe('01'); // NIP-01 is most relevant for 'protocol'
64
+ expect(results[0].content).toBeDefined();
65
+ expect(results[0].content).toContain('protocol flow');
66
+ });
67
+ test('search respects limit parameter', async () => {
68
+ // Create a search term that matches multiple NIPs
69
+ const results = await searchNips('n', { limit: 2 });
70
+ // Should return only two results even though more match
71
+ expect(results.length).toBe(2);
72
+ });
73
+ test('search with no matches returns empty array', async () => {
74
+ const results = await searchNips('nonexistentterm');
75
+ expect(results).toEqual([]);
76
+ });
77
+ test('search relevance sorting', async () => {
78
+ // 'encoding' appears in both NIP-19 title/keywords and NIP-57 content, but NIP-19 should rank higher
79
+ const results = await searchNips('encoding');
80
+ expect(results.length).toBeGreaterThan(0);
81
+ expect(results[0].id).toBe('19');
82
+ });
83
+ test('search is case insensitive', async () => {
84
+ const lowerResults = await searchNips('lightning');
85
+ const upperResults = await searchNips('LIGHTNING');
86
+ const mixedResults = await searchNips('LiGhTnInG');
87
+ expect(lowerResults.length).toBeGreaterThan(0);
88
+ expect(upperResults.length).toBeGreaterThan(0);
89
+ expect(mixedResults.length).toBeGreaterThan(0);
90
+ // All should find the same results
91
+ expect(lowerResults[0].id).toBe(upperResults[0].id);
92
+ expect(lowerResults[0].id).toBe(mixedResults[0].id);
93
+ });
94
+ test('search works with partial word matches', async () => {
95
+ const results = await searchNips('light');
96
+ expect(results.length).toBeGreaterThan(0);
97
+ expect(results[0].id).toBe('57'); // Should find 'lightning' in NIP-57
98
+ });
99
+ test('very large limit is handled gracefully', async () => {
100
+ // Even with a large limit, should only return matches
101
+ const results = await searchNips('protocol', { limit: 1000 });
102
+ // Should not break or return more than the available matches
103
+ expect(results.length).toBeLessThanOrEqual(mockNips.length);
104
+ });
105
+ test('zero limit returns empty array', async () => {
106
+ const results = await searchNips('protocol', { limit: 0 });
107
+ expect(results).toEqual([]);
108
+ });
109
+ });
@@ -0,0 +1,148 @@
1
+ import { createNote, signNote, publishNote } from '../note/note-tools.js';
2
+ import { createKeypair } from '../profile/profile-tools.js';
3
+ describe('Note Creation Tools', () => {
4
+ describe('createNote', () => {
5
+ it('should create a valid unsigned note event', async () => {
6
+ const { privateKey } = await createKeypair('hex');
7
+ const result = await createNote(privateKey, 'Hello Nostr world!', [['t', 'test']]);
8
+ expect(result.success).toBe(true);
9
+ expect(result.message).toContain('created successfully');
10
+ expect(result.noteEvent).toBeDefined();
11
+ expect(result.publicKey).toBeDefined();
12
+ // Check the note event structure
13
+ const note = result.noteEvent;
14
+ expect(note.kind).toBe(1);
15
+ expect(note.content).toBe('Hello Nostr world!');
16
+ expect(note.tags).toEqual([['t', 'test']]);
17
+ expect(note.pubkey).toBe(result.publicKey);
18
+ expect(note.created_at).toBeDefined();
19
+ expect(typeof note.created_at).toBe('number');
20
+ // Should not have id or sig (unsigned)
21
+ expect(note.id).toBeUndefined();
22
+ expect(note.sig).toBeUndefined();
23
+ });
24
+ it('should create a note with no tags', async () => {
25
+ const { privateKey } = await createKeypair('hex');
26
+ const result = await createNote(privateKey, 'Simple note without tags');
27
+ expect(result.success).toBe(true);
28
+ expect(result.noteEvent.tags).toEqual([]);
29
+ });
30
+ it('should handle nsec format private keys', async () => {
31
+ const { nsec } = await createKeypair('npub');
32
+ const result = await createNote(nsec, 'Note with nsec key');
33
+ expect(result.success).toBe(true);
34
+ expect(result.noteEvent).toBeDefined();
35
+ expect(result.publicKey).toBeDefined();
36
+ });
37
+ it('should fail with invalid private key', async () => {
38
+ const result = await createNote('invalid_private_key', 'This should fail');
39
+ expect(result.success).toBe(false);
40
+ expect(result.message).toContain('Error creating note');
41
+ });
42
+ });
43
+ describe('signNote', () => {
44
+ it('should sign a note event correctly', async () => {
45
+ const { privateKey } = await createKeypair('hex');
46
+ // First create a note
47
+ const createResult = await createNote(privateKey, 'Note to be signed', [['hashtag', 'test']]);
48
+ expect(createResult.success).toBe(true);
49
+ // Then sign it
50
+ const signResult = await signNote(privateKey, createResult.noteEvent);
51
+ expect(signResult.success).toBe(true);
52
+ expect(signResult.message).toContain('signed successfully');
53
+ expect(signResult.signedNote).toBeDefined();
54
+ // Check the signed note structure
55
+ const signedNote = signResult.signedNote;
56
+ expect(signedNote.id).toBeDefined();
57
+ expect(signedNote.sig).toBeDefined();
58
+ expect(signedNote.kind).toBe(1);
59
+ expect(signedNote.content).toBe('Note to be signed');
60
+ expect(signedNote.tags).toEqual([['hashtag', 'test']]);
61
+ expect(signedNote.pubkey).toBe(createResult.publicKey);
62
+ // Verify the signature is valid (basic check)
63
+ expect(typeof signedNote.id).toBe('string');
64
+ expect(signedNote.id).toMatch(/^[0-9a-f]{64}$/);
65
+ expect(typeof signedNote.sig).toBe('string');
66
+ expect(signedNote.sig).toMatch(/^[0-9a-f]{128}$/);
67
+ });
68
+ it('should fail when private key does not match note pubkey', async () => {
69
+ const { privateKey: privateKey1 } = await createKeypair('hex');
70
+ const { privateKey: privateKey2 } = await createKeypair('hex');
71
+ // Create note with first key
72
+ const createResult = await createNote(privateKey1, 'Note created with key 1');
73
+ expect(createResult.success).toBe(true);
74
+ // Try to sign with second key
75
+ const signResult = await signNote(privateKey2, createResult.noteEvent);
76
+ expect(signResult.success).toBe(false);
77
+ expect(signResult.message).toContain('does not match');
78
+ });
79
+ it('should handle nsec format private keys', async () => {
80
+ const { nsec, npub } = await createKeypair('npub');
81
+ // Create note using nsec
82
+ const createResult = await createNote(nsec, 'Note with nsec');
83
+ expect(createResult.success).toBe(true);
84
+ // Sign note using nsec
85
+ const signResult = await signNote(nsec, createResult.noteEvent);
86
+ expect(signResult.success).toBe(true);
87
+ expect(signResult.signedNote).toBeDefined();
88
+ });
89
+ });
90
+ describe('publishNote', () => {
91
+ it('should handle publishing with no relays', async () => {
92
+ const { privateKey } = await createKeypair('hex');
93
+ // Create and sign a note
94
+ const createResult = await createNote(privateKey, 'Test note');
95
+ const signResult = await signNote(privateKey, createResult.noteEvent);
96
+ expect(createResult.success).toBe(true);
97
+ expect(signResult.success).toBe(true);
98
+ // Publish with no relays
99
+ const publishResult = await publishNote(signResult.signedNote, []);
100
+ expect(publishResult.success).toBe(true);
101
+ expect(publishResult.message).toContain('no relays specified');
102
+ expect(publishResult.noteId).toBe(signResult.signedNote.id);
103
+ });
104
+ it('should validate note structure', async () => {
105
+ const { privateKey } = await createKeypair('hex');
106
+ // Create and sign a note
107
+ const createResult = await createNote(privateKey, 'Valid note');
108
+ const signResult = await signNote(privateKey, createResult.noteEvent);
109
+ expect(createResult.success).toBe(true);
110
+ expect(signResult.success).toBe(true);
111
+ // The note should have all required fields
112
+ const note = signResult.signedNote;
113
+ expect(note.id).toBeDefined();
114
+ expect(note.pubkey).toBeDefined();
115
+ expect(note.created_at).toBeDefined();
116
+ expect(note.kind).toBe(1);
117
+ expect(note.tags).toBeDefined();
118
+ expect(note.content).toBe('Valid note');
119
+ expect(note.sig).toBeDefined();
120
+ });
121
+ });
122
+ describe('Full workflow', () => {
123
+ it('should complete a full note creation, signing, and publishing workflow', async () => {
124
+ const { privateKey } = await createKeypair('hex');
125
+ // Step 1: Create note
126
+ const createResult = await createNote(privateKey, 'Complete workflow test', [['t', 'workflow'], ['client', 'test']]);
127
+ expect(createResult.success).toBe(true);
128
+ expect(createResult.noteEvent.kind).toBe(1);
129
+ expect(createResult.noteEvent.content).toBe('Complete workflow test');
130
+ // Step 2: Sign note
131
+ const signResult = await signNote(privateKey, createResult.noteEvent);
132
+ expect(signResult.success).toBe(true);
133
+ expect(signResult.signedNote.id).toBeDefined();
134
+ expect(signResult.signedNote.sig).toBeDefined();
135
+ // Step 3: Publish note (no relays for testing)
136
+ const publishResult = await publishNote(signResult.signedNote, []);
137
+ expect(publishResult.success).toBe(true);
138
+ expect(publishResult.noteId).toBe(signResult.signedNote.id);
139
+ // Verify the complete signed event
140
+ const finalNote = signResult.signedNote;
141
+ expect(finalNote.id).toMatch(/^[0-9a-f]{64}$/);
142
+ expect(finalNote.sig).toMatch(/^[0-9a-f]{128}$/);
143
+ expect(finalNote.pubkey).toBe(createResult.publicKey);
144
+ expect(finalNote.content).toBe('Complete workflow test');
145
+ expect(finalNote.tags).toEqual([['t', 'workflow'], ['client', 'test']]);
146
+ });
147
+ });
148
+ });