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,145 @@
|
|
|
1
|
+
import { describe, expect, test, jest, beforeEach } from '@jest/globals';
|
|
2
|
+
// Mock the actual Nostr pool functionality
|
|
3
|
+
jest.mock('../utils/pool.js', () => {
|
|
4
|
+
return {
|
|
5
|
+
getRelayPool: jest.fn(() => ({
|
|
6
|
+
connect: jest.fn(),
|
|
7
|
+
close: jest.fn(),
|
|
8
|
+
subscribeMany: jest.fn(),
|
|
9
|
+
})),
|
|
10
|
+
};
|
|
11
|
+
});
|
|
12
|
+
// Mock setTimeout for testing timeouts
|
|
13
|
+
jest.useFakeTimers();
|
|
14
|
+
// Helper function for creating timed out promises
|
|
15
|
+
const createTimedOutPromise = () => new Promise((resolve, reject) => {
|
|
16
|
+
setTimeout(() => reject(new Error('Request timed out')), 10000);
|
|
17
|
+
});
|
|
18
|
+
// Mock getProfile that simulates various error scenarios
|
|
19
|
+
const mockGetProfile = jest.fn();
|
|
20
|
+
// Helpers for common error scenarios
|
|
21
|
+
const simulateTimeoutError = () => {
|
|
22
|
+
mockGetProfile.mockImplementationOnce(() => createTimedOutPromise());
|
|
23
|
+
};
|
|
24
|
+
const simulateNetworkError = () => {
|
|
25
|
+
mockGetProfile.mockImplementationOnce(() => Promise.reject(new Error('Failed to connect to relay')));
|
|
26
|
+
};
|
|
27
|
+
const simulateInvalidPubkey = () => {
|
|
28
|
+
mockGetProfile.mockImplementationOnce(() => Promise.reject(new Error('Invalid pubkey format')));
|
|
29
|
+
};
|
|
30
|
+
const simulateMalformedEvent = () => {
|
|
31
|
+
mockGetProfile.mockImplementationOnce(() => Promise.resolve({
|
|
32
|
+
error: 'Malformed event',
|
|
33
|
+
details: 'Event missing required signature'
|
|
34
|
+
}));
|
|
35
|
+
};
|
|
36
|
+
describe('Error Handling and Edge Cases', () => {
|
|
37
|
+
beforeEach(() => {
|
|
38
|
+
mockGetProfile.mockReset();
|
|
39
|
+
});
|
|
40
|
+
test('timeout handling', async () => {
|
|
41
|
+
simulateTimeoutError();
|
|
42
|
+
try {
|
|
43
|
+
jest.useFakeTimers();
|
|
44
|
+
const profilePromise = mockGetProfile('valid-pubkey');
|
|
45
|
+
// Fast-forward time to trigger timeout
|
|
46
|
+
jest.advanceTimersByTime(10000);
|
|
47
|
+
await profilePromise;
|
|
48
|
+
fail('Expected promise to reject with timeout error');
|
|
49
|
+
}
|
|
50
|
+
catch (error) {
|
|
51
|
+
expect(error.message).toContain('timed out');
|
|
52
|
+
}
|
|
53
|
+
});
|
|
54
|
+
test('invalid pubkey format handling', async () => {
|
|
55
|
+
simulateInvalidPubkey();
|
|
56
|
+
try {
|
|
57
|
+
await mockGetProfile('invalid-pubkey-format');
|
|
58
|
+
fail('Expected promise to reject with invalid pubkey error');
|
|
59
|
+
}
|
|
60
|
+
catch (error) {
|
|
61
|
+
expect(error.message).toContain('Invalid pubkey');
|
|
62
|
+
}
|
|
63
|
+
});
|
|
64
|
+
test('network error handling', async () => {
|
|
65
|
+
simulateNetworkError();
|
|
66
|
+
try {
|
|
67
|
+
await mockGetProfile('valid-pubkey');
|
|
68
|
+
fail('Expected promise to reject with network error');
|
|
69
|
+
}
|
|
70
|
+
catch (error) {
|
|
71
|
+
expect(error.message).toContain('Failed to connect');
|
|
72
|
+
}
|
|
73
|
+
});
|
|
74
|
+
test('malformed event handling', async () => {
|
|
75
|
+
simulateMalformedEvent();
|
|
76
|
+
const result = await mockGetProfile('valid-pubkey');
|
|
77
|
+
expect(result.error).toBeDefined();
|
|
78
|
+
expect(result.error).toContain('Malformed event');
|
|
79
|
+
});
|
|
80
|
+
test('empty pubkey handling', async () => {
|
|
81
|
+
// Empty string pubkey
|
|
82
|
+
simulateInvalidPubkey();
|
|
83
|
+
try {
|
|
84
|
+
await mockGetProfile('');
|
|
85
|
+
fail('Expected promise to reject with invalid pubkey error');
|
|
86
|
+
}
|
|
87
|
+
catch (error) {
|
|
88
|
+
expect(error.message).toContain('Invalid pubkey');
|
|
89
|
+
}
|
|
90
|
+
});
|
|
91
|
+
test('extremely long pubkey handling', async () => {
|
|
92
|
+
// Extremely long input should be rejected
|
|
93
|
+
simulateInvalidPubkey();
|
|
94
|
+
const veryLongPubkey = 'a'.repeat(1000);
|
|
95
|
+
try {
|
|
96
|
+
await mockGetProfile(veryLongPubkey);
|
|
97
|
+
fail('Expected promise to reject with invalid pubkey error');
|
|
98
|
+
}
|
|
99
|
+
catch (error) {
|
|
100
|
+
expect(error.message).toContain('Invalid pubkey');
|
|
101
|
+
}
|
|
102
|
+
});
|
|
103
|
+
test('special character handling in pubkey', async () => {
|
|
104
|
+
// Pubkey with special characters
|
|
105
|
+
simulateInvalidPubkey();
|
|
106
|
+
try {
|
|
107
|
+
await mockGetProfile('npub1<script>alert("xss")</script>');
|
|
108
|
+
fail('Expected promise to reject with invalid pubkey error');
|
|
109
|
+
}
|
|
110
|
+
catch (error) {
|
|
111
|
+
expect(error.message).toContain('Invalid pubkey');
|
|
112
|
+
}
|
|
113
|
+
});
|
|
114
|
+
test('null or undefined pubkey handling', async () => {
|
|
115
|
+
// Null pubkey
|
|
116
|
+
simulateInvalidPubkey();
|
|
117
|
+
try {
|
|
118
|
+
await mockGetProfile(null);
|
|
119
|
+
fail('Expected promise to reject with invalid pubkey error');
|
|
120
|
+
}
|
|
121
|
+
catch (error) {
|
|
122
|
+
expect(error.message).toContain('Invalid pubkey');
|
|
123
|
+
}
|
|
124
|
+
// Undefined pubkey
|
|
125
|
+
simulateInvalidPubkey();
|
|
126
|
+
try {
|
|
127
|
+
await mockGetProfile(undefined);
|
|
128
|
+
fail('Expected promise to reject with invalid pubkey error');
|
|
129
|
+
}
|
|
130
|
+
catch (error) {
|
|
131
|
+
expect(error.message).toContain('Invalid pubkey');
|
|
132
|
+
}
|
|
133
|
+
});
|
|
134
|
+
test('all relays failing scenario', async () => {
|
|
135
|
+
// Simulate all relays failing
|
|
136
|
+
simulateNetworkError();
|
|
137
|
+
try {
|
|
138
|
+
await mockGetProfile('valid-pubkey', { relays: ['wss://relay1.example.com', 'wss://relay2.example.com'] });
|
|
139
|
+
fail('Expected promise to reject when all relays fail');
|
|
140
|
+
}
|
|
141
|
+
catch (error) {
|
|
142
|
+
expect(error.message).toContain('Failed to connect');
|
|
143
|
+
}
|
|
144
|
+
});
|
|
145
|
+
});
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import { describe, expect, test } from '@jest/globals';
|
|
2
|
+
import * as nip19 from 'nostr-tools/nip19';
|
|
3
|
+
// Import local conversion utilities
|
|
4
|
+
import { npubToHex, hexToNpub } from '../utils/conversion.js';
|
|
5
|
+
describe('Nostr format conversion', () => {
|
|
6
|
+
// Known test vectors - generate these dynamically to ensure they match
|
|
7
|
+
const testHexPubkeys = [
|
|
8
|
+
'3bf0c63fcb93463407af97a5e5ee64fa883d107ef9e558472c4eb9aaaefa459d',
|
|
9
|
+
'63fe6318dc58583cfe16810f86dd09e18bfd76aabc24a0081ce2856f330504ed'
|
|
10
|
+
];
|
|
11
|
+
// Generate matching npubs using the library directly
|
|
12
|
+
const knownPairs = testHexPubkeys.map(hex => ({
|
|
13
|
+
hex,
|
|
14
|
+
npub: nip19.npubEncode(hex)
|
|
15
|
+
}));
|
|
16
|
+
// Generate test event IDs and their encoded forms
|
|
17
|
+
const testEventIds = [
|
|
18
|
+
'5c83da77af1dec6d7289834998ad7aafbd9e2191396d75ec3cc27f5a77226f36',
|
|
19
|
+
'9ae37aa68f48645127299e9595f545f942a8314455cbafd9d913ed19c7fc0462'
|
|
20
|
+
];
|
|
21
|
+
const eventIdPairs = testEventIds.map(hex => ({
|
|
22
|
+
hex,
|
|
23
|
+
note: nip19.noteEncode(hex)
|
|
24
|
+
}));
|
|
25
|
+
test('hex to npub conversion', () => {
|
|
26
|
+
knownPairs.forEach(pair => {
|
|
27
|
+
expect(hexToNpub(pair.hex)).toBe(pair.npub);
|
|
28
|
+
});
|
|
29
|
+
});
|
|
30
|
+
test('npub to hex conversion', () => {
|
|
31
|
+
knownPairs.forEach(pair => {
|
|
32
|
+
const result = npubToHex(pair.npub);
|
|
33
|
+
expect(result).toBe(pair.hex);
|
|
34
|
+
});
|
|
35
|
+
});
|
|
36
|
+
test('hex to note conversion using nostr-tools', () => {
|
|
37
|
+
eventIdPairs.forEach(pair => {
|
|
38
|
+
expect(nip19.noteEncode(pair.hex)).toBe(pair.note);
|
|
39
|
+
});
|
|
40
|
+
});
|
|
41
|
+
test('note to hex conversion using nostr-tools', () => {
|
|
42
|
+
eventIdPairs.forEach(pair => {
|
|
43
|
+
const decoded = nip19.decode(pair.note);
|
|
44
|
+
expect(decoded.type).toBe('note');
|
|
45
|
+
expect(decoded.data).toBe(pair.hex);
|
|
46
|
+
});
|
|
47
|
+
});
|
|
48
|
+
test('nevent encoding and decoding using nostr-tools', () => {
|
|
49
|
+
const eventPointer = {
|
|
50
|
+
id: '5c83da77af1dec6d7289834998ad7aafbd9e2191396d75ec3cc27f5a77226f36',
|
|
51
|
+
relays: ['wss://relay.example.com', 'wss://relay.nostr.org'],
|
|
52
|
+
author: '3bf0c63fcb93463407af97a5e5ee64fa883d107ef9e558472c4eb9aaaefa459d',
|
|
53
|
+
kind: 1
|
|
54
|
+
};
|
|
55
|
+
const nevent = nip19.neventEncode(eventPointer);
|
|
56
|
+
expect(nevent).toMatch(/^nevent1/);
|
|
57
|
+
const decoded = nip19.decode(nevent);
|
|
58
|
+
expect(decoded.type).toBe('nevent');
|
|
59
|
+
const data = decoded.data;
|
|
60
|
+
expect(data.id).toBe(eventPointer.id);
|
|
61
|
+
expect(data.author).toBe(eventPointer.author);
|
|
62
|
+
expect(data.kind).toBe(eventPointer.kind);
|
|
63
|
+
expect(data.relays).toEqual(eventPointer.relays);
|
|
64
|
+
});
|
|
65
|
+
test('naddr encoding and decoding using nostr-tools', () => {
|
|
66
|
+
const addressPointer = {
|
|
67
|
+
identifier: 'test-article',
|
|
68
|
+
pubkey: '3bf0c63fcb93463407af97a5e5ee64fa883d107ef9e558472c4eb9aaaefa459d',
|
|
69
|
+
kind: 30023,
|
|
70
|
+
relays: ['wss://relay.example.com']
|
|
71
|
+
};
|
|
72
|
+
const naddr = nip19.naddrEncode(addressPointer);
|
|
73
|
+
expect(naddr).toMatch(/^naddr1/);
|
|
74
|
+
const decoded = nip19.decode(naddr);
|
|
75
|
+
expect(decoded.type).toBe('naddr');
|
|
76
|
+
const data = decoded.data;
|
|
77
|
+
expect(data.identifier).toBe(addressPointer.identifier);
|
|
78
|
+
expect(data.pubkey).toBe(addressPointer.pubkey);
|
|
79
|
+
expect(data.kind).toBe(addressPointer.kind);
|
|
80
|
+
expect(data.relays).toEqual(addressPointer.relays);
|
|
81
|
+
});
|
|
82
|
+
test('validate Nostr identifiers using regex', () => {
|
|
83
|
+
// Valid identifiers
|
|
84
|
+
expect(/^npub1[a-z0-9]{58}$/.test(knownPairs[0].npub)).toBe(true);
|
|
85
|
+
expect(/^note1[a-z0-9]{58}$/.test(eventIdPairs[0].note)).toBe(true);
|
|
86
|
+
// Invalid identifiers with correct regex patterns
|
|
87
|
+
expect(/^npub1[a-z0-9]{58}$/.test('npub1invalid')).toBe(false);
|
|
88
|
+
expect(/^note1[a-z0-9]{58}$/.test('note1invalid')).toBe(false);
|
|
89
|
+
// Fix the regex patterns to be more specific
|
|
90
|
+
expect(/^nevent1[a-z0-9]{58,}$/.test('nevent1invalid')).toBe(false);
|
|
91
|
+
expect(/^naddr1[a-z0-9]{58,}$/.test('naddr1invalid')).toBe(false);
|
|
92
|
+
});
|
|
93
|
+
test('error handling for npub conversion', () => {
|
|
94
|
+
expect(npubToHex('npub1invalid')).toBeNull();
|
|
95
|
+
expect(hexToNpub('invalidhex')).toBeNull();
|
|
96
|
+
});
|
|
97
|
+
test('partial nevent data', () => {
|
|
98
|
+
// Test with minimal data
|
|
99
|
+
const minimalPointer = {
|
|
100
|
+
id: '5c83da77af1dec6d7289834998ad7aafbd9e2191396d75ec3cc27f5a77226f36'
|
|
101
|
+
};
|
|
102
|
+
const nevent = nip19.neventEncode(minimalPointer);
|
|
103
|
+
expect(nevent).toMatch(/^nevent1/);
|
|
104
|
+
const decoded = nip19.decode(nevent);
|
|
105
|
+
expect(decoded.type).toBe('nevent');
|
|
106
|
+
const data = decoded.data;
|
|
107
|
+
expect(data.id).toBe(minimalPointer.id);
|
|
108
|
+
expect(data.relays).toEqual([]);
|
|
109
|
+
expect(data.author).toBeUndefined();
|
|
110
|
+
expect(data.kind).toBeUndefined();
|
|
111
|
+
});
|
|
112
|
+
test('bech32 conversion limits', () => {
|
|
113
|
+
// Test with very long relay list
|
|
114
|
+
const manyRelays = Array(20).fill(0).map((_, i) => `wss://relay${i}.example.com`);
|
|
115
|
+
const eventPointer = {
|
|
116
|
+
id: '5c83da77af1dec6d7289834998ad7aafbd9e2191396d75ec3cc27f5a77226f36',
|
|
117
|
+
relays: manyRelays
|
|
118
|
+
};
|
|
119
|
+
const nevent = nip19.neventEncode(eventPointer);
|
|
120
|
+
expect(nevent).toMatch(/^nevent1/);
|
|
121
|
+
const decoded = nip19.decode(nevent);
|
|
122
|
+
expect(decoded.type).toBe('nevent');
|
|
123
|
+
const data = decoded.data;
|
|
124
|
+
expect(data.id).toBe(eventPointer.id);
|
|
125
|
+
expect(data.relays.length).toBe(manyRelays.length);
|
|
126
|
+
});
|
|
127
|
+
test('edge case: nostr URI handling', () => {
|
|
128
|
+
// Generate a fresh npub
|
|
129
|
+
const hex = '3bf0c63fcb93463407af97a5e5ee64fa883d107ef9e558472c4eb9aaaefa459d';
|
|
130
|
+
const npub = nip19.npubEncode(hex);
|
|
131
|
+
// Test with nostr: prefix
|
|
132
|
+
const nostrUriNpub = 'nostr:' + npub;
|
|
133
|
+
// When handling the nostr: prefix in our conversion functions
|
|
134
|
+
const result = npubToHex(nostrUriNpub.replace('nostr:', ''));
|
|
135
|
+
expect(result).toBe(hex);
|
|
136
|
+
});
|
|
137
|
+
});
|
|
@@ -0,0 +1,163 @@
|
|
|
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
|
+
// Generate a keypair for testing
|
|
6
|
+
function generatePrivateKey() {
|
|
7
|
+
return Buffer.from(randomBytes(32)).toString('hex');
|
|
8
|
+
}
|
|
9
|
+
function getPublicKey(privateKey) {
|
|
10
|
+
return Buffer.from(schnorr.getPublicKey(privateKey)).toString('hex');
|
|
11
|
+
}
|
|
12
|
+
// Create a signed event
|
|
13
|
+
function createSignedEvent(privateKey, kind, content, tags = []) {
|
|
14
|
+
const pubkey = getPublicKey(privateKey);
|
|
15
|
+
const created_at = Math.floor(Date.now() / 1000);
|
|
16
|
+
// Create event
|
|
17
|
+
const event = {
|
|
18
|
+
pubkey,
|
|
19
|
+
created_at,
|
|
20
|
+
kind,
|
|
21
|
+
tags,
|
|
22
|
+
content,
|
|
23
|
+
};
|
|
24
|
+
// Calculate event ID
|
|
25
|
+
const eventData = JSON.stringify([0, event.pubkey, event.created_at, event.kind, event.tags, event.content]);
|
|
26
|
+
const id = Buffer.from(sha256(eventData)).toString('hex');
|
|
27
|
+
// Sign the event
|
|
28
|
+
const sig = Buffer.from(schnorr.sign(id, privateKey)).toString('hex');
|
|
29
|
+
return {
|
|
30
|
+
...event,
|
|
31
|
+
id,
|
|
32
|
+
sig
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
describe('Nostr Integration Tests', () => {
|
|
36
|
+
let relay;
|
|
37
|
+
const testPort = 9700;
|
|
38
|
+
let privateKey;
|
|
39
|
+
let publicKey;
|
|
40
|
+
beforeAll(async () => {
|
|
41
|
+
privateKey = generatePrivateKey();
|
|
42
|
+
publicKey = getPublicKey(privateKey);
|
|
43
|
+
// Start the ephemeral relay
|
|
44
|
+
relay = new NostrRelay(testPort);
|
|
45
|
+
await relay.start();
|
|
46
|
+
});
|
|
47
|
+
afterAll(async () => {
|
|
48
|
+
// Shutdown relay
|
|
49
|
+
await relay.close();
|
|
50
|
+
});
|
|
51
|
+
test('should publish and retrieve a profile', async () => {
|
|
52
|
+
// Create a profile event (kind 0)
|
|
53
|
+
const profileContent = JSON.stringify({
|
|
54
|
+
name: 'Test User',
|
|
55
|
+
about: 'This is a test profile',
|
|
56
|
+
picture: 'https://example.com/avatar.jpg'
|
|
57
|
+
});
|
|
58
|
+
const profileEvent = createSignedEvent(privateKey, 0, profileContent);
|
|
59
|
+
// Store it in the relay
|
|
60
|
+
relay.store(profileEvent);
|
|
61
|
+
// Verify it was stored
|
|
62
|
+
expect(relay.cache.length).toBeGreaterThan(0);
|
|
63
|
+
// Find the profile in the cache
|
|
64
|
+
const retrievedProfile = relay.cache.find(event => event.kind === 0 && event.pubkey === publicKey);
|
|
65
|
+
// Verify profile data
|
|
66
|
+
expect(retrievedProfile).toBeDefined();
|
|
67
|
+
expect(retrievedProfile?.id).toBe(profileEvent.id);
|
|
68
|
+
// Parse the content
|
|
69
|
+
const parsedContent = JSON.parse(retrievedProfile?.content || '{}');
|
|
70
|
+
expect(parsedContent.name).toBe('Test User');
|
|
71
|
+
expect(parsedContent.about).toBe('This is a test profile');
|
|
72
|
+
});
|
|
73
|
+
test('should publish and retrieve a text note', async () => {
|
|
74
|
+
// Create a text note (kind 1)
|
|
75
|
+
const noteContent = 'This is a test note posted from integration tests!';
|
|
76
|
+
const noteEvent = createSignedEvent(privateKey, 1, noteContent);
|
|
77
|
+
// Store it in the relay
|
|
78
|
+
relay.store(noteEvent);
|
|
79
|
+
// Find the note in the cache
|
|
80
|
+
const retrievedNote = relay.cache.find(event => event.kind === 1 && event.pubkey === publicKey && event.content === noteContent);
|
|
81
|
+
// Verify note data
|
|
82
|
+
expect(retrievedNote).toBeDefined();
|
|
83
|
+
expect(retrievedNote?.id).toBe(noteEvent.id);
|
|
84
|
+
expect(retrievedNote?.content).toBe(noteContent);
|
|
85
|
+
});
|
|
86
|
+
test('should publish and retrieve a zap receipt', async () => {
|
|
87
|
+
// Create a mock recipient public key
|
|
88
|
+
const recipientKey = generatePrivateKey();
|
|
89
|
+
const recipientPubkey = getPublicKey(recipientKey);
|
|
90
|
+
// Create zap receipt tags
|
|
91
|
+
const zapTags = [
|
|
92
|
+
['p', recipientPubkey],
|
|
93
|
+
['amount', '100000'], // 100 sats in millisats
|
|
94
|
+
['bolt11', 'lnbc100n...'],
|
|
95
|
+
['description', ''],
|
|
96
|
+
];
|
|
97
|
+
// Create a zap receipt (kind 9735)
|
|
98
|
+
const zapEvent = createSignedEvent(privateKey, 9735, '', zapTags);
|
|
99
|
+
// Store it in the relay
|
|
100
|
+
relay.store(zapEvent);
|
|
101
|
+
// Find the zap in the cache
|
|
102
|
+
const retrievedZap = relay.cache.find(event => event.kind === 9735 && event.pubkey === publicKey);
|
|
103
|
+
// Verify zap data
|
|
104
|
+
expect(retrievedZap).toBeDefined();
|
|
105
|
+
expect(retrievedZap?.id).toBe(zapEvent.id);
|
|
106
|
+
// Verify zap tags
|
|
107
|
+
const pTag = retrievedZap?.tags.find(tag => tag[0] === 'p');
|
|
108
|
+
const amountTag = retrievedZap?.tags.find(tag => tag[0] === 'amount');
|
|
109
|
+
expect(pTag?.[1]).toBe(recipientPubkey);
|
|
110
|
+
expect(amountTag?.[1]).toBe('100000');
|
|
111
|
+
});
|
|
112
|
+
test('should filter events correctly', async () => {
|
|
113
|
+
// Create multiple events of different kinds
|
|
114
|
+
const profileEvent = createSignedEvent(privateKey, 0, JSON.stringify({ name: 'Filter Test' }));
|
|
115
|
+
const textNote1 = createSignedEvent(privateKey, 1, 'Filter test note 1');
|
|
116
|
+
const textNote2 = createSignedEvent(privateKey, 1, 'Filter test note 2');
|
|
117
|
+
const reactionEvent = createSignedEvent(privateKey, 7, '+', [['e', 'fake-event-id']]);
|
|
118
|
+
// Store all events
|
|
119
|
+
relay.store(profileEvent);
|
|
120
|
+
relay.store(textNote1);
|
|
121
|
+
relay.store(textNote2);
|
|
122
|
+
relay.store(reactionEvent);
|
|
123
|
+
// Filter for just kind 1 events
|
|
124
|
+
const textNotes = relay.cache.filter(event => event.kind === 1 && event.pubkey === publicKey);
|
|
125
|
+
// We should have at least 3 text notes (2 from this test plus 1 from earlier test)
|
|
126
|
+
expect(textNotes.length).toBeGreaterThanOrEqual(3);
|
|
127
|
+
// Filter for reaction events
|
|
128
|
+
const reactions = relay.cache.filter(event => event.kind === 7 && event.pubkey === publicKey);
|
|
129
|
+
expect(reactions.length).toBeGreaterThanOrEqual(1);
|
|
130
|
+
expect(reactions[0].content).toBe('+');
|
|
131
|
+
});
|
|
132
|
+
// The ephemeral-relay validates events during WebSocket communication,
|
|
133
|
+
// but doesn't validate during direct store() calls - this test verifies this behavior
|
|
134
|
+
test('should store events without validation when using direct store() method', () => {
|
|
135
|
+
// Create a properly signed event
|
|
136
|
+
const signedEvent = createSignedEvent(privateKey, 1, 'Verification test');
|
|
137
|
+
// Store it in the relay
|
|
138
|
+
relay.store(signedEvent);
|
|
139
|
+
// Create an event with invalid signature
|
|
140
|
+
const invalidEvent = {
|
|
141
|
+
pubkey: publicKey,
|
|
142
|
+
created_at: Math.floor(Date.now() / 1000),
|
|
143
|
+
kind: 1,
|
|
144
|
+
tags: [],
|
|
145
|
+
content: 'Invalid signature event',
|
|
146
|
+
id: 'invalid_id_that_doesnt_match_content',
|
|
147
|
+
sig: 'invalid_signature_0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000'
|
|
148
|
+
};
|
|
149
|
+
// Get the current cache size
|
|
150
|
+
const cacheSizeBefore = relay.cache.length;
|
|
151
|
+
// Store the invalid event (this should succeed since store() doesn't validate)
|
|
152
|
+
relay.store(invalidEvent);
|
|
153
|
+
// Cache size should increase since the invalid event should be added
|
|
154
|
+
const cacheSizeAfter = relay.cache.length;
|
|
155
|
+
// Verify the event was added (expected behavior for direct store calls)
|
|
156
|
+
expect(cacheSizeAfter).toBe(cacheSizeBefore + 1);
|
|
157
|
+
// Find the invalid event in the cache
|
|
158
|
+
const invalidEventInCache = relay.cache.find(event => event.id === 'invalid_id_that_doesnt_match_content');
|
|
159
|
+
expect(invalidEventInCache).toBeDefined();
|
|
160
|
+
// Note: This confirms the current behavior, but in websocket-integration.test.ts we
|
|
161
|
+
// verify that invalid events are properly rejected over WebSocket communication
|
|
162
|
+
});
|
|
163
|
+
});
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
// Mock Nostr events and utility functions for testing
|
|
2
|
+
import { jest } from '@jest/globals';
|
|
3
|
+
export const MOCK_HEX_PUBKEY = '7e7e9c42a91bfef19fa929e5fda1b72e0ebc1a4c1141673e2794234d86addf4e';
|
|
4
|
+
export const MOCK_NPUB = 'npub180cvv07tjdrrgpa0j7j7tmnyl2yr6yr7l8j4s3evf6u64th6gkwsyjh6w6';
|
|
5
|
+
export const mockProfile = {
|
|
6
|
+
id: '1234',
|
|
7
|
+
pubkey: MOCK_HEX_PUBKEY,
|
|
8
|
+
created_at: Math.floor(Date.now() / 1000) - 3600,
|
|
9
|
+
kind: 0,
|
|
10
|
+
tags: [],
|
|
11
|
+
content: JSON.stringify({
|
|
12
|
+
name: 'Test User',
|
|
13
|
+
display_name: 'Tester',
|
|
14
|
+
about: 'A test profile for unit tests',
|
|
15
|
+
picture: 'https://example.com/avatar.jpg',
|
|
16
|
+
nip05: 'test@example.com'
|
|
17
|
+
}),
|
|
18
|
+
sig: 'mock_signature'
|
|
19
|
+
};
|
|
20
|
+
export const mockNote = {
|
|
21
|
+
id: '5678',
|
|
22
|
+
pubkey: MOCK_HEX_PUBKEY,
|
|
23
|
+
created_at: Math.floor(Date.now() / 1000) - 1800,
|
|
24
|
+
kind: 1,
|
|
25
|
+
tags: [],
|
|
26
|
+
content: 'This is a test note from the test user.',
|
|
27
|
+
sig: 'mock_signature'
|
|
28
|
+
};
|
|
29
|
+
export const mockLongFormNote = {
|
|
30
|
+
id: '9012',
|
|
31
|
+
pubkey: MOCK_HEX_PUBKEY,
|
|
32
|
+
created_at: Math.floor(Date.now() / 1000) - 86400,
|
|
33
|
+
kind: 30023,
|
|
34
|
+
tags: [
|
|
35
|
+
['title', 'Test Long Form Content'],
|
|
36
|
+
['summary', 'This is a test summary of a long form article'],
|
|
37
|
+
['published_at', (Math.floor(Date.now() / 1000) - 86400).toString()],
|
|
38
|
+
['d', 'test-identifier']
|
|
39
|
+
],
|
|
40
|
+
content: 'This is a test long form content article with much more text than a normal note would have.',
|
|
41
|
+
sig: 'mock_signature'
|
|
42
|
+
};
|
|
43
|
+
export const mockZapReceipt = {
|
|
44
|
+
id: 'abcd',
|
|
45
|
+
pubkey: 'lightning_service_pubkey',
|
|
46
|
+
created_at: Math.floor(Date.now() / 1000) - 900,
|
|
47
|
+
kind: 9735,
|
|
48
|
+
tags: [
|
|
49
|
+
['p', MOCK_HEX_PUBKEY],
|
|
50
|
+
['bolt11', 'lnbc100n1...'],
|
|
51
|
+
['description', JSON.stringify({
|
|
52
|
+
content: '',
|
|
53
|
+
created_at: Math.floor(Date.now() / 1000) - 901,
|
|
54
|
+
id: 'zap_request_id',
|
|
55
|
+
kind: 9734,
|
|
56
|
+
pubkey: 'sender_pubkey',
|
|
57
|
+
tags: [
|
|
58
|
+
['amount', '10000'], // 100 sats in millisats
|
|
59
|
+
['relays', 'wss://relay.example.com'],
|
|
60
|
+
['p', MOCK_HEX_PUBKEY]
|
|
61
|
+
]
|
|
62
|
+
})]
|
|
63
|
+
],
|
|
64
|
+
content: '',
|
|
65
|
+
sig: 'mock_signature'
|
|
66
|
+
};
|
|
67
|
+
// Mock pool functions
|
|
68
|
+
export const mockPool = {
|
|
69
|
+
get: jest.fn(),
|
|
70
|
+
querySync: jest.fn(),
|
|
71
|
+
close: jest.fn()
|
|
72
|
+
};
|
|
73
|
+
// Mock for getFreshPool function
|
|
74
|
+
export const getFreshPoolMock = jest.fn().mockReturnValue(mockPool);
|
|
75
|
+
// Mock response for lightning service for anonymous zaps
|
|
76
|
+
export const mockLightningServiceResponse = {
|
|
77
|
+
callback: 'https://example.com/callback',
|
|
78
|
+
maxSendable: 100000000,
|
|
79
|
+
minSendable: 1000,
|
|
80
|
+
metadata: JSON.stringify({
|
|
81
|
+
name: 'Test User',
|
|
82
|
+
pubkey: MOCK_HEX_PUBKEY
|
|
83
|
+
}),
|
|
84
|
+
allowsNostr: true,
|
|
85
|
+
nostrPubkey: MOCK_HEX_PUBKEY
|
|
86
|
+
};
|
|
87
|
+
// Mock response for invoice generation
|
|
88
|
+
export const mockInvoiceResponse = {
|
|
89
|
+
pr: 'lnbc100n1...', // Mock lightning invoice
|
|
90
|
+
success: true,
|
|
91
|
+
verify: 'https://example.com/verify'
|
|
92
|
+
};
|
|
93
|
+
// Mock response for NIP search
|
|
94
|
+
export const mockNipSearchResults = [
|
|
95
|
+
{
|
|
96
|
+
number: 57,
|
|
97
|
+
title: 'Lightning Zaps',
|
|
98
|
+
summary: 'This NIP defines a protocol for sending zaps via the Lightning Network.',
|
|
99
|
+
relevance: 0.95,
|
|
100
|
+
content: '# NIP-57\n\n## Lightning Zaps\n\nThis is mock content for the zaps NIP.'
|
|
101
|
+
},
|
|
102
|
+
{
|
|
103
|
+
number: 1,
|
|
104
|
+
title: 'Basic protocol flow description',
|
|
105
|
+
summary: 'Basic protocol flow and interaction between clients and relays.',
|
|
106
|
+
relevance: 0.5,
|
|
107
|
+
content: '# NIP-01\n\n## Basic protocol flow description\n\nThis is mock content for the basic protocol NIP.'
|
|
108
|
+
}
|
|
109
|
+
];
|