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,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
|
+
});
|