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,136 @@
1
+ import { describe, expect, test, jest, beforeEach } from '@jest/globals';
2
+ import { NostrRelay } from '../utils/ephemeral-relay.js';
3
+ // Mock the getRelayPool function
4
+ const mockGetRelayPool = jest.fn();
5
+ // Define a mock implementation
6
+ const createMockPool = (relays) => {
7
+ return {
8
+ connect: jest.fn(),
9
+ close: jest.fn(),
10
+ relayUrls: relays,
11
+ subscribeMany: jest.fn().mockImplementation(() => {
12
+ return {
13
+ on: jest.fn(),
14
+ off: jest.fn()
15
+ };
16
+ }),
17
+ };
18
+ };
19
+ // Set up the mock implementation
20
+ mockGetRelayPool.mockImplementation((relays) => createMockPool(relays || []));
21
+ // Mock the entire module
22
+ jest.mock('../utils/pool.js', () => {
23
+ return {
24
+ getRelayPool: (relays) => mockGetRelayPool(relays)
25
+ };
26
+ });
27
+ // Create a helper to simulate fetching profile data
28
+ const fetchProfileWithRelays = (pubkey, customRelays) => {
29
+ // This simulates how the actual code would use the pool
30
+ const relayPool = mockGetRelayPool(customRelays);
31
+ // Track if we've connected to the pool
32
+ relayPool.connect();
33
+ // Simulate the subscription
34
+ const sub = relayPool.subscribeMany([{ kinds: [0], authors: [pubkey] }]);
35
+ // Return the relays that were used
36
+ return {
37
+ relaysUsed: relayPool.relayUrls,
38
+ pool: relayPool
39
+ };
40
+ };
41
+ // Define default relays for comparison
42
+ const DEFAULT_RELAYS = [
43
+ 'wss://relay.damus.io',
44
+ 'wss://relay.nostr.band',
45
+ 'wss://relay.primal.net',
46
+ 'wss://nos.lol',
47
+ 'wss://relay.current.fyi',
48
+ 'wss://nostr.bitcoiner.social'
49
+ ];
50
+ describe('Custom Relay Specification', () => {
51
+ beforeEach(() => {
52
+ // Clear all mocks before each test
53
+ jest.clearAllMocks();
54
+ // Set the default mockGetRelayPool implementation to use DEFAULT_RELAYS when no custom relays
55
+ mockGetRelayPool.mockImplementation((relays) => {
56
+ const trackedRelays = relays && relays.length > 0 ? relays : DEFAULT_RELAYS;
57
+ return createMockPool(trackedRelays);
58
+ });
59
+ });
60
+ test('uses default relays when no custom relays specified', () => {
61
+ const { relaysUsed } = fetchProfileWithRelays('test-pubkey');
62
+ // When no custom relays are specified, should use defaults
63
+ expect(relaysUsed).toEqual(DEFAULT_RELAYS);
64
+ });
65
+ test('uses custom relays when specified', () => {
66
+ const customRelays = ['wss://custom1.example.com', 'wss://custom2.example.com'];
67
+ const { relaysUsed } = fetchProfileWithRelays('test-pubkey', customRelays);
68
+ // Should use exactly the custom relays
69
+ expect(relaysUsed).toEqual(customRelays);
70
+ expect(relaysUsed).not.toEqual(DEFAULT_RELAYS);
71
+ });
72
+ test('empty custom relays array falls back to defaults', () => {
73
+ const { relaysUsed } = fetchProfileWithRelays('test-pubkey', []);
74
+ // When empty array is provided, should fall back to defaults
75
+ expect(relaysUsed).toEqual(DEFAULT_RELAYS);
76
+ });
77
+ test('maintains relay connection pool during operation', () => {
78
+ const customRelays = ['wss://custom1.example.com', 'wss://custom2.example.com'];
79
+ const { pool } = fetchProfileWithRelays('test-pubkey', customRelays);
80
+ // The connect method should be called
81
+ expect(pool.connect).toHaveBeenCalled();
82
+ // The subscribeMany method should be called with appropriate filters
83
+ expect(pool.subscribeMany).toHaveBeenCalledWith(expect.arrayContaining([
84
+ expect.objectContaining({
85
+ kinds: expect.arrayContaining([0]),
86
+ authors: expect.arrayContaining(['test-pubkey'])
87
+ })
88
+ ]));
89
+ });
90
+ test('invalid relay URLs are filtered out', () => {
91
+ // Create a special mock implementation for this test
92
+ mockGetRelayPool.mockImplementationOnce((relays) => {
93
+ // Filter invalid URLs (this simulates what the real implementation should do)
94
+ const filteredRelays = relays ? relays.filter((url) => url.startsWith('wss://') && url.includes('.')) : [];
95
+ return createMockPool(filteredRelays);
96
+ });
97
+ // Include some invalid relay URLs
98
+ const mixedRelays = [
99
+ 'wss://valid.example.com',
100
+ 'invalid-url',
101
+ 'http://not-secure.example.com', // Not WSS
102
+ 'wss://another-valid.example.com'
103
+ ];
104
+ const { relaysUsed } = fetchProfileWithRelays('test-pubkey', mixedRelays);
105
+ // Only valid WSS URLs should remain
106
+ expect(relaysUsed).toContain('wss://valid.example.com');
107
+ expect(relaysUsed).toContain('wss://another-valid.example.com');
108
+ expect(relaysUsed).not.toContain('invalid-url');
109
+ expect(relaysUsed).not.toContain('http://not-secure.example.com');
110
+ });
111
+ test('relay fallback behavior with ephemeral relay', async () => {
112
+ // Create an ephemeral relay for testing - using port 9000 for test
113
+ const relay = new NostrRelay(9000);
114
+ // Create a request that specifies two relays:
115
+ // 1. A non-existent relay that will fail
116
+ // 2. Our ephemeral relay that will work
117
+ const testRelays = [
118
+ 'wss://non-existent-relay.example.com',
119
+ relay.url
120
+ ];
121
+ // This is a more advanced test that would require actual implementation
122
+ // of relay fallback logic. We'll outline the concept here.
123
+ // In a real test with actual implementation:
124
+ // 1. Send a request to the specified relays
125
+ // 2. The first relay fails to connect
126
+ // 3. The code falls back to the second relay
127
+ // 4. We get a successful response from the second relay
128
+ // For now, we'll just verify that both relays are attempted
129
+ const { relaysUsed } = fetchProfileWithRelays('test-pubkey', testRelays);
130
+ expect(relaysUsed).toEqual(testRelays);
131
+ // In a full implementation, we would:
132
+ // 1. Set up a callback for events from the ephemeral relay
133
+ // 2. Publish a test profile to the ephemeral relay
134
+ // 3. Verify we receive it even though the first relay fails
135
+ });
136
+ });
@@ -0,0 +1,96 @@
1
+ import { searchNips } from '../nips/nips-tools.js';
2
+ // Since the NIPs tool uses a real cache, we'll test with the actual cache behavior
3
+ describe('Search NIPs Tool - Simple Tests', () => {
4
+ describe('basic search functionality', () => {
5
+ it('should search NIPs by keyword', async () => {
6
+ // This will use the real cache if available, or fetch from GitHub
7
+ const result = await searchNips('protocol', 5);
8
+ expect(Array.isArray(result)).toBe(true);
9
+ // Should find NIP-01 when searching for "protocol"
10
+ const nip01 = result.find(searchResult => searchResult.nip.number === 1);
11
+ if (nip01) {
12
+ expect(nip01.nip.title.toLowerCase()).toContain('protocol');
13
+ }
14
+ });
15
+ it('should return limited results', async () => {
16
+ const limit = 3;
17
+ const result = await searchNips('event', limit);
18
+ expect(Array.isArray(result)).toBe(true);
19
+ expect(result.length).toBeLessThanOrEqual(limit);
20
+ });
21
+ it('should return results with correct structure', async () => {
22
+ // Use a more common search term that's likely to return results
23
+ const result = await searchNips('event', 5);
24
+ expect(Array.isArray(result)).toBe(true);
25
+ // Each result should have the expected structure
26
+ if (result.length > 0) {
27
+ result.forEach(searchResult => {
28
+ expect(searchResult).toHaveProperty('nip');
29
+ expect(searchResult).toHaveProperty('relevance');
30
+ expect(searchResult).toHaveProperty('matchedTerms');
31
+ // Check the nested nip structure
32
+ expect(searchResult.nip).toHaveProperty('number');
33
+ expect(searchResult.nip).toHaveProperty('title');
34
+ expect(searchResult.nip).toHaveProperty('status');
35
+ });
36
+ }
37
+ });
38
+ it('should handle no results gracefully', async () => {
39
+ const result = await searchNips('xyz123nonexistentterm456', 10);
40
+ expect(Array.isArray(result)).toBe(true);
41
+ expect(result).toEqual([]);
42
+ });
43
+ it('should handle case-insensitive search', async () => {
44
+ const result1 = await searchNips('PROTOCOL', 5);
45
+ const result2 = await searchNips('protocol', 5);
46
+ expect(Array.isArray(result1)).toBe(true);
47
+ expect(Array.isArray(result2)).toBe(true);
48
+ // Both searches should return results
49
+ if (result1.length > 0 && result2.length > 0) {
50
+ // The results should be similar (may not be identical due to scoring)
51
+ expect(result1.length).toBeGreaterThan(0);
52
+ expect(result2.length).toBeGreaterThan(0);
53
+ }
54
+ });
55
+ });
56
+ describe('search relevance', () => {
57
+ it('should return relevant results for common terms', async () => {
58
+ // Test with terms we know exist in NIPs
59
+ const result = await searchNips('event', 5);
60
+ expect(Array.isArray(result)).toBe(true);
61
+ if (result.length > 0) {
62
+ // At least one result should have a relevance score
63
+ const hasRelevantResult = result.some(searchResult => searchResult.relevance > 0);
64
+ expect(hasRelevantResult).toBe(true);
65
+ // Results should be sorted by relevance score
66
+ expect(result[0].relevance).toBeGreaterThan(0);
67
+ }
68
+ });
69
+ it('should rank results by relevance score', async () => {
70
+ const result = await searchNips('encryption', 10);
71
+ expect(Array.isArray(result)).toBe(true);
72
+ if (result.length > 1) {
73
+ // Results should be sorted by relevance score (descending)
74
+ for (let i = 1; i < result.length; i++) {
75
+ if (result[i - 1].relevance !== undefined && result[i].relevance !== undefined) {
76
+ expect(result[i - 1].relevance).toBeGreaterThanOrEqual(result[i].relevance);
77
+ }
78
+ }
79
+ }
80
+ });
81
+ });
82
+ describe('error handling', () => {
83
+ it('should handle empty search query', async () => {
84
+ const result = await searchNips('', 10);
85
+ // Empty query should return empty array
86
+ expect(Array.isArray(result)).toBe(true);
87
+ expect(result).toEqual([]);
88
+ });
89
+ it('should handle very long search queries gracefully', async () => {
90
+ const longQuery = 'a'.repeat(1000);
91
+ const result = await searchNips(longQuery, 10);
92
+ // Should return an array (possibly empty)
93
+ expect(Array.isArray(result)).toBe(true);
94
+ });
95
+ });
96
+ });
@@ -0,0 +1,257 @@
1
+ import { NostrRelay } from '../utils/ephemeral-relay.js';
2
+ import { schnorr } from '@noble/curves/secp256k1';
3
+ import { randomBytes } from 'crypto';
4
+ import { sha256 } from '@noble/hashes/sha256';
5
+ import WebSocket from 'ws';
6
+ // Generate a keypair for testing
7
+ function generatePrivateKey() {
8
+ return Buffer.from(randomBytes(32)).toString('hex');
9
+ }
10
+ function getPublicKey(privateKey) {
11
+ return Buffer.from(schnorr.getPublicKey(privateKey)).toString('hex');
12
+ }
13
+ // Create a signed event
14
+ function createSignedEvent(privateKey, kind, content, tags = []) {
15
+ const pubkey = getPublicKey(privateKey);
16
+ const created_at = Math.floor(Date.now() / 1000);
17
+ // Create event
18
+ const event = {
19
+ pubkey,
20
+ created_at,
21
+ kind,
22
+ tags,
23
+ content,
24
+ };
25
+ // Calculate event ID
26
+ const eventData = JSON.stringify([0, event.pubkey, event.created_at, event.kind, event.tags, event.content]);
27
+ const id = Buffer.from(sha256(eventData)).toString('hex');
28
+ // Sign the event
29
+ const sig = Buffer.from(schnorr.sign(id, privateKey)).toString('hex');
30
+ return {
31
+ ...event,
32
+ id,
33
+ sig
34
+ };
35
+ }
36
+ describe('WebSocket Nostr Integration Tests', () => {
37
+ let relay;
38
+ const testPort = 9800;
39
+ let relayUrl;
40
+ let ws;
41
+ let privateKey;
42
+ let publicKey;
43
+ beforeAll(async () => {
44
+ privateKey = generatePrivateKey();
45
+ publicKey = getPublicKey(privateKey);
46
+ // Start the ephemeral relay
47
+ relay = new NostrRelay(testPort);
48
+ await relay.start();
49
+ relayUrl = `ws://localhost:${testPort}`;
50
+ });
51
+ beforeEach(async () => {
52
+ // Create a new WebSocket connection before each test
53
+ const connectPromise = new Promise((resolve, reject) => {
54
+ ws = new WebSocket(relayUrl);
55
+ ws.on('open', () => resolve());
56
+ ws.on('error', reject);
57
+ });
58
+ await connectPromise;
59
+ });
60
+ afterEach(async () => {
61
+ // Close WebSocket connection after each test
62
+ if (ws) {
63
+ if (ws.readyState === WebSocket.OPEN) {
64
+ ws.close();
65
+ // Wait for close to complete
66
+ await new Promise(resolve => {
67
+ ws.on('close', resolve);
68
+ setTimeout(resolve, 100); // Fallback timeout
69
+ });
70
+ }
71
+ ws.removeAllListeners();
72
+ }
73
+ });
74
+ afterAll(async () => {
75
+ // Ensure all WebSocket connections are closed
76
+ if (ws && ws.readyState !== WebSocket.CLOSED) {
77
+ ws.close();
78
+ await new Promise(resolve => setTimeout(resolve, 100));
79
+ }
80
+ // Shutdown relay
81
+ await relay.close();
82
+ // Give time for all async operations to complete
83
+ await new Promise(resolve => setTimeout(resolve, 200));
84
+ });
85
+ // Helper function to send a message and wait for response
86
+ const sendAndWait = (message) => {
87
+ return new Promise((resolve, reject) => {
88
+ const responses = [];
89
+ const responseHandler = (data) => {
90
+ try {
91
+ const response = JSON.parse(data.toString());
92
+ responses.push(response);
93
+ // EOSE or OK messages indicate we can resolve
94
+ if ((response[0] === 'EOSE' && response[1] === 'test-sub') ||
95
+ (response[0] === 'OK')) {
96
+ resolve(responses);
97
+ ws.off('message', responseHandler);
98
+ }
99
+ }
100
+ catch (e) {
101
+ reject(e);
102
+ }
103
+ };
104
+ ws.on('message', responseHandler);
105
+ ws.send(JSON.stringify(message));
106
+ // Add a timeout
107
+ setTimeout(() => {
108
+ resolve(responses);
109
+ ws.off('message', responseHandler);
110
+ }, 2000);
111
+ });
112
+ };
113
+ test('should connect to the relay', () => {
114
+ expect(ws.readyState).toBe(WebSocket.OPEN);
115
+ });
116
+ test('should publish an event and get OK response', async () => {
117
+ // Create a test event
118
+ const event = createSignedEvent(privateKey, 1, 'WebSocket test note');
119
+ // Send EVENT message
120
+ const responses = await sendAndWait(['EVENT', event]);
121
+ // Check for OK response
122
+ const okResponse = responses.find(resp => resp[0] === 'OK' && resp[1] === event.id);
123
+ expect(okResponse).toBeDefined();
124
+ expect(okResponse[2]).toBe(true); // Success flag
125
+ });
126
+ test('should publish an event and retrieve it with REQ', async () => {
127
+ // Create a test event with unique content
128
+ const uniqueContent = `WebSocket REQ test note ${Date.now()}`;
129
+ const event = createSignedEvent(privateKey, 1, uniqueContent);
130
+ // Send EVENT message
131
+ await sendAndWait(['EVENT', event]);
132
+ // Now send a REQ to get this event
133
+ const subId = 'test-sub';
134
+ const responses = await sendAndWait([
135
+ 'REQ',
136
+ subId,
137
+ {
138
+ kinds: [1],
139
+ authors: [publicKey],
140
+ }
141
+ ]);
142
+ // Check that we got an EVENT response with our event
143
+ const eventResponse = responses.find(resp => resp[0] === 'EVENT' &&
144
+ resp[1] === subId &&
145
+ resp[2].content === uniqueContent);
146
+ expect(eventResponse).toBeDefined();
147
+ expect(eventResponse[2].id).toBe(event.id);
148
+ // Check that we got an EOSE response
149
+ const eoseResponse = responses.find(resp => resp[0] === 'EOSE' && resp[1] === subId);
150
+ expect(eoseResponse).toBeDefined();
151
+ });
152
+ test('should handle multiple subscriptions', async () => {
153
+ // Create events of different kinds
154
+ const profileEvent = createSignedEvent(privateKey, 0, JSON.stringify({ name: 'WebSocket Test' }));
155
+ const noteEvent = createSignedEvent(privateKey, 1, 'WebSocket multi-subscription test');
156
+ // Publish both events
157
+ await sendAndWait(['EVENT', profileEvent]);
158
+ await sendAndWait(['EVENT', noteEvent]);
159
+ // Subscribe to profiles only
160
+ const profileSubId = 'profile-sub';
161
+ const profileResponses = await sendAndWait([
162
+ 'REQ',
163
+ profileSubId,
164
+ {
165
+ kinds: [0],
166
+ authors: [publicKey],
167
+ }
168
+ ]);
169
+ // Subscribe to notes only
170
+ const noteSubId = 'note-sub';
171
+ const noteResponses = await sendAndWait([
172
+ 'REQ',
173
+ noteSubId,
174
+ {
175
+ kinds: [1],
176
+ authors: [publicKey],
177
+ }
178
+ ]);
179
+ // Check profile subscription got profile event
180
+ const profileEventResponse = profileResponses.find(resp => resp[0] === 'EVENT' &&
181
+ resp[1] === profileSubId &&
182
+ resp[2].kind === 0);
183
+ expect(profileEventResponse).toBeDefined();
184
+ // Check note subscription got note event
185
+ const noteEventResponse = noteResponses.find(resp => resp[0] === 'EVENT' &&
186
+ resp[1] === noteSubId &&
187
+ resp[2].kind === 1);
188
+ expect(noteEventResponse).toBeDefined();
189
+ });
190
+ test('should support subscription closing', async () => {
191
+ // Create a test event
192
+ const event = createSignedEvent(privateKey, 1, 'Subscription close test');
193
+ // Publish the event
194
+ await sendAndWait(['EVENT', event]);
195
+ // Create a subscription
196
+ const subId = 'close-test-sub';
197
+ await sendAndWait([
198
+ 'REQ',
199
+ subId,
200
+ {
201
+ kinds: [1],
202
+ authors: [publicKey],
203
+ }
204
+ ]);
205
+ // Close the subscription
206
+ ws.send(JSON.stringify(['CLOSE', subId]));
207
+ // Create a new subscription with the same ID
208
+ // This should work if the previous subscription was properly closed
209
+ const newResponses = await sendAndWait([
210
+ 'REQ',
211
+ subId,
212
+ {
213
+ kinds: [1],
214
+ authors: [publicKey],
215
+ }
216
+ ]);
217
+ // Verify we got an EOSE for the new subscription
218
+ const eoseResponse = newResponses.find(resp => resp[0] === 'EOSE' && resp[1] === subId);
219
+ expect(eoseResponse).toBeDefined();
220
+ });
221
+ test('should reject events with invalid signatures or silently ignore them', async () => {
222
+ // Create an event with invalid signature
223
+ const invalidEvent = {
224
+ pubkey: publicKey,
225
+ created_at: Math.floor(Date.now() / 1000),
226
+ kind: 1,
227
+ tags: [],
228
+ content: 'Event with invalid signature',
229
+ id: Buffer.from(sha256(JSON.stringify([0, publicKey, Math.floor(Date.now() / 1000), 1, [], 'Event with invalid signature']))).toString('hex'),
230
+ sig: 'invalid_signature_0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000'
231
+ };
232
+ // Send EVENT message with invalid signature
233
+ const responses = await sendAndWait(['EVENT', invalidEvent]);
234
+ // Check for OK response with failure flag, or no response which means silent rejection
235
+ const okResponse = responses.find(resp => resp[0] === 'OK' &&
236
+ resp[1] === invalidEvent.id);
237
+ // If the relay responds to invalid events, it should be with failure
238
+ if (okResponse) {
239
+ expect(okResponse[2]).toBe(false); // Success flag should be false
240
+ }
241
+ // Now verify that a valid event works properly
242
+ const validEvent = createSignedEvent(privateKey, 1, 'Event with valid signature');
243
+ // Send EVENT message with valid signature
244
+ const validResponses = await sendAndWait(['EVENT', validEvent]);
245
+ // Check for OK response with success flag
246
+ const validOkResponse = validResponses.find(resp => resp[0] === 'OK' &&
247
+ resp[1] === validEvent.id);
248
+ expect(validOkResponse).toBeDefined();
249
+ expect(validOkResponse[2]).toBe(true); // Success flag should be true
250
+ // Verify the valid event made it to the relay's cache
251
+ const eventInCache = relay.cache.find(e => e.id === validEvent.id);
252
+ expect(eventInCache).toBeDefined();
253
+ // Verify the invalid event didn't make it to the relay's cache
254
+ const invalidEventInCache = relay.cache.find(e => e.id === invalidEvent.id);
255
+ expect(invalidEventInCache).toBeUndefined();
256
+ });
257
+ });
@@ -0,0 +1,72 @@
1
+ // Mock the processZapReceipt function
2
+ const processZapReceipt = (receipt, targetPubkey) => {
3
+ const targetTag = receipt.tags?.find(tag => tag[0] === 'p' && tag[1] === targetPubkey);
4
+ const direction = targetTag ? 'received' : 'sent';
5
+ const amountTag = receipt.tags?.find(tag => tag[0] === 'amount');
6
+ const amountSats = amountTag ? parseInt(amountTag[1]) / 1000 : 0; // Convert millisats to sats
7
+ return {
8
+ id: receipt.id,
9
+ direction,
10
+ amountSats,
11
+ created_at: receipt.created_at,
12
+ targetPubkey
13
+ };
14
+ };
15
+ // Simple prepareAnonymousZap function for testing
16
+ const prepareAnonymousZap = (target, amount, comment = '') => {
17
+ return Promise.resolve({
18
+ success: true,
19
+ invoice: `lnbc${amount}`,
20
+ targetData: {
21
+ type: target.startsWith('note') ? 'event' : 'profile'
22
+ },
23
+ comment
24
+ });
25
+ };
26
+ describe('Zap Tools Functions', () => {
27
+ const testPubkey = '7e7e9c42a91bfef19fa929e5fda1b72e0ebc1a4c1141673e2794234d86addf4e';
28
+ test('processZapReceipt adds targetPubkey to receipt', () => {
29
+ // Create mock zap receipt
30
+ const mockZapReceipt = {
31
+ id: 'test-zap-id',
32
+ created_at: Math.floor(Date.now() / 1000) - 3600,
33
+ tags: [
34
+ ['p', testPubkey],
35
+ ['amount', '100000'] // 100 sats in millisats
36
+ ]
37
+ };
38
+ // Process the receipt
39
+ const result = processZapReceipt(mockZapReceipt, testPubkey);
40
+ // Check the result
41
+ expect(result).toHaveProperty('targetPubkey', testPubkey);
42
+ expect(result.id).toBe(mockZapReceipt.id);
43
+ expect(result.direction).toBe('received');
44
+ expect(result.amountSats).toBe(100);
45
+ });
46
+ test('prepareAnonymousZap returns invoice for profile', async () => {
47
+ // Test with an npub target
48
+ const npubTarget = 'npub180cvv07tjdrrgpa0j7j7tmnyl2yr6yr7l8j4s3evf6u64th6gkwsyjh6w6';
49
+ const amount = 100;
50
+ const comment = 'Test zap';
51
+ // Prepare anonymous zap
52
+ const result = await prepareAnonymousZap(npubTarget, amount, comment);
53
+ // Check the result
54
+ expect(result.success).toBe(true);
55
+ expect(result.invoice).toBe(`lnbc${amount}`);
56
+ expect(result.targetData.type).toBe('profile');
57
+ expect(result.comment).toBe(comment);
58
+ });
59
+ test('prepareAnonymousZap returns invoice for event', async () => {
60
+ // Test with a note ID target
61
+ const noteTarget = 'note1abcdef';
62
+ const amount = 200;
63
+ // Prepare anonymous zap with default empty comment
64
+ const result = await prepareAnonymousZap(noteTarget, amount);
65
+ // Check the result
66
+ expect(result.success).toBe(true);
67
+ expect(result.invoice).toBe(`lnbc${amount}`);
68
+ expect(result.targetData.type).toBe('event');
69
+ expect(result.comment).toBe('');
70
+ });
71
+ });
72
+ export {};