nostr-mcp-server 2.0.0 → 3.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/README.md +101 -35
- package/build/__tests__/basic.test.js +2 -2
- package/build/__tests__/dm-tools.test.js +192 -0
- package/build/__tests__/event-tools.test.js +64 -0
- package/build/__tests__/integration.test.js +4 -3
- package/build/__tests__/mocks.js +0 -10
- package/build/__tests__/nip19-conversion.test.js +1 -0
- package/build/__tests__/nip42-auth.test.js +53 -0
- package/build/__tests__/note-creation.test.js +1 -0
- package/build/__tests__/note-tools-functions.test.js +10 -5
- package/build/__tests__/note-tools-unit.test.js +1 -0
- package/build/__tests__/profile-notes-simple.test.js +1 -1
- package/build/__tests__/profile-postnote.test.js +1 -0
- package/build/__tests__/profile-tools.test.js +1 -0
- package/build/__tests__/relay-tools.test.js +73 -0
- package/build/__tests__/social-tools.test.js +75 -0
- package/build/__tests__/websocket-integration.test.js +5 -4
- package/build/__tests__/zap-tools-simple.test.js +1 -1
- package/build/__tests__/zap-tools-tests.test.js +1 -0
- package/build/bun.setup.js +3 -0
- package/build/dm/dm-tools.js +300 -0
- package/build/event/event-tools.js +482 -0
- package/build/index.js +247 -54
- package/build/note/note-tools.js +4 -0
- package/build/relay/relay-tools.js +164 -0
- package/build/social/social-tools.js +297 -0
- package/build/utils/constants.js +21 -1
- package/build/utils/conversion.js +10 -5
- package/build/utils/ephemeral-relay.js +100 -16
- package/build/utils/formatting.js +40 -0
- package/build/utils/index.js +1 -0
- package/build/utils/keys.js +26 -0
- package/build/zap/zap-tools.js +6 -1
- package/package.json +13 -16
package/README.md
CHANGED
|
@@ -1,12 +1,15 @@
|
|
|
1
1
|
# Nostr MCP Server
|
|
2
2
|
|
|
3
|
+
[](https://www.npmjs.com/package/nostr-mcp-server)
|
|
4
|
+
[](https://opensource.org/licenses/MIT)
|
|
5
|
+
|
|
3
6
|
A Model Context Protocol (MCP) server that provides Nostr capabilities to LLMs like Claude.
|
|
4
7
|
|
|
5
8
|
https://github.com/user-attachments/assets/1d2d47d0-c61b-44e2-85be-5985d2a81c64
|
|
6
9
|
|
|
7
10
|
## Features
|
|
8
11
|
|
|
9
|
-
This server implements
|
|
12
|
+
This server implements 40 tools for interacting with the Nostr network:
|
|
10
13
|
|
|
11
14
|
### Reading & Querying Tools
|
|
12
15
|
1. `getProfile`: Fetches a user's profile information by public key
|
|
@@ -15,26 +18,54 @@ This server implements 18 tools for interacting with the Nostr network:
|
|
|
15
18
|
4. `getReceivedZaps`: Fetches zaps received by a user, including detailed payment information
|
|
16
19
|
5. `getSentZaps`: Fetches zaps sent by a user, including detailed payment information
|
|
17
20
|
6. `getAllZaps`: Fetches both sent and received zaps for a user, clearly labeled with direction and totals
|
|
18
|
-
7. `
|
|
21
|
+
7. `queryEvents`: Generic event query tool supporting kinds/authors/ids/tags and timestamps
|
|
22
|
+
8. `getContactList`: Fetches a user's contact list (kind 3) and followed pubkeys
|
|
23
|
+
9. `getFollowing`: Alias of `getContactList`
|
|
24
|
+
10. `getRelayList`: Fetches a user's relay list metadata (NIP-65 kind 10002)
|
|
19
25
|
|
|
20
26
|
### Identity & Profile Management Tools
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
### Note Creation & Publishing Tools
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
27
|
+
11. `createKeypair`: Generate new Nostr keypairs in hex and/or npub/nsec format
|
|
28
|
+
12. `createProfile`: Create a new Nostr profile (kind 0 event) with metadata
|
|
29
|
+
13. `updateProfile`: Update an existing Nostr profile with new metadata
|
|
30
|
+
|
|
31
|
+
### Note Creation & Publishing Tools
|
|
32
|
+
14. `createNote`: Create unsigned kind 1 note events with specified content and tags
|
|
33
|
+
15. `signNote`: Sign note events with a private key, generating cryptographically valid signatures
|
|
34
|
+
16. `publishNote`: Publish signed notes to specified Nostr relays
|
|
35
|
+
17. `postNote`: All-in-one authenticated note posting using an existing private key (nsec/hex)
|
|
36
|
+
|
|
37
|
+
### Generic Event Tools
|
|
38
|
+
18. `createNostrEvent`: Create unsigned Nostr events of any kind (provides low-level building block for NIPs beyond notes/profiles)
|
|
39
|
+
19. `signNostrEvent`: Sign any unsigned Nostr event with a private key
|
|
40
|
+
20. `publishNostrEvent`: Publish any signed Nostr event to relays
|
|
41
|
+
|
|
42
|
+
### Social Tools
|
|
43
|
+
21. `setRelayList`: Publish your relay list metadata (NIP-65 kind 10002)
|
|
44
|
+
22. `follow`: Follow a pubkey by updating your contact list (kind 3)
|
|
45
|
+
23. `unfollow`: Unfollow a pubkey by updating your contact list (kind 3)
|
|
46
|
+
24. `reactToEvent`: React to an event (kind 7)
|
|
47
|
+
25. `repostEvent`: Repost an event (kind 6)
|
|
48
|
+
26. `deleteEvent`: Delete one or more events (kind 5 deletion request)
|
|
49
|
+
27. `replyToEvent`: Reply to an event with correct NIP-10 thread tags (kind 1)
|
|
50
|
+
|
|
51
|
+
### Messaging Tools
|
|
52
|
+
28. `encryptNip04`: Encrypt plaintext using NIP-04 (AES-CBC) for direct messages
|
|
53
|
+
29. `decryptNip04`: Decrypt ciphertext using NIP-04 (AES-CBC) for direct messages
|
|
54
|
+
30. `sendDmNip04`: Send a NIP-04 encrypted DM (kind 4)
|
|
55
|
+
31. `getDmConversationNip04`: Fetch and optionally decrypt a NIP-04 DM conversation (kind 4)
|
|
56
|
+
32. `encryptNip44`: Encrypt plaintext using NIP-44 (ChaCha20 + HMAC)
|
|
57
|
+
33. `decryptNip44`: Decrypt ciphertext using NIP-44 (ChaCha20 + HMAC)
|
|
58
|
+
34. `sendDmNip44`: Send a NIP-44 encrypted DM using NIP-17 gift wrap (kind 1059)
|
|
59
|
+
35. `decryptDmNip44`: Decrypt a NIP-17 gift wrapped DM (kind 1059)
|
|
60
|
+
36. `getDmInboxNip44`: Fetch and decrypt your NIP-44 DM inbox (NIP-17 gift wraps, kind 1059)
|
|
30
61
|
|
|
31
62
|
### Anonymous Tools
|
|
32
|
-
|
|
33
|
-
|
|
63
|
+
37. `sendAnonymousZap`: Prepare an anonymous zap to a profile or event, generating a lightning invoice for payment
|
|
64
|
+
38. `postAnonymousNote`: Post an anonymous note using a randomly generated one-time keypair
|
|
34
65
|
|
|
35
66
|
### NIP-19 Entity Tools
|
|
36
|
-
|
|
37
|
-
|
|
67
|
+
39. `convertNip19`: Convert between different NIP-19 entity formats (hex, npub, nsec, note, nprofile, nevent, naddr)
|
|
68
|
+
40. `analyzeNip19`: Analyze and decode any NIP-19 entity to understand its type and contents
|
|
38
69
|
|
|
39
70
|
All tools fully support both hex public keys and npub format, with user-friendly display of Nostr identifiers.
|
|
40
71
|
|
|
@@ -46,7 +77,21 @@ All tools fully support both hex public keys and npub format, with user-friendly
|
|
|
46
77
|
npm install -g nostr-mcp-server
|
|
47
78
|
```
|
|
48
79
|
|
|
49
|
-
### Option 2: Install from source
|
|
80
|
+
### Option 2: Install from source (using Bun - Recommended)
|
|
81
|
+
|
|
82
|
+
```bash
|
|
83
|
+
# Clone the repository
|
|
84
|
+
git clone https://github.com/austinkelsay/nostr-mcp-server.git
|
|
85
|
+
cd nostr-mcp-server
|
|
86
|
+
|
|
87
|
+
# Install dependencies
|
|
88
|
+
bun install
|
|
89
|
+
|
|
90
|
+
# Build the project
|
|
91
|
+
bun run build
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
### Option 3: Install from source (using npm)
|
|
50
95
|
|
|
51
96
|
```bash
|
|
52
97
|
# Clone the repository
|
|
@@ -214,9 +259,6 @@ Once configured, you can ask Claude to use the Nostr tools by making requests li
|
|
|
214
259
|
- "How many zaps has npub1qny3tkh0acurzla8x3zy4nhrjz5zd8ne6dvrjehx9n9hr3lnj08qwuzwc8 received?"
|
|
215
260
|
- "Show me the zaps sent by npub1qny3tkh0acurzla8x3zy4nhrjz5zd8ne6dvrjehx9n9hr3lnj08qwuzwc8"
|
|
216
261
|
- "Show me all zaps (both sent and received) for npub1qny3tkh0acurzla8x3zy4nhrjz5zd8ne6dvrjehx9n9hr3lnj08qwuzwc8"
|
|
217
|
-
- "Search for NIPs about zaps"
|
|
218
|
-
- "What NIPs are related to long-form content?"
|
|
219
|
-
- "Show me NIP-23 with full content"
|
|
220
262
|
|
|
221
263
|
### Identity & Profile Management
|
|
222
264
|
- "Generate a new Nostr keypair for me"
|
|
@@ -230,6 +272,24 @@ Once configured, you can ask Claude to use the Nostr tools by making requests li
|
|
|
230
272
|
- "Publish this signed note to wss://relay.damus.io and wss://nos.lol"
|
|
231
273
|
- "Post a note saying 'GM Nostr! ☀️' using my private key nsec1xyz..."
|
|
232
274
|
|
|
275
|
+
### Generic Event Operations
|
|
276
|
+
- "Query kind 7 reaction events from this pubkey from the last 24 hours"
|
|
277
|
+
- "Create a kind 7 reaction event to this note ID with content '+'"
|
|
278
|
+
- "Sign this generic Nostr event with my private key and publish it"
|
|
279
|
+
|
|
280
|
+
### Social & Relay Management
|
|
281
|
+
- "Follow this pubkey using my private key nsec1xyz..."
|
|
282
|
+
- "Unfollow this pubkey using my private key"
|
|
283
|
+
- "Reply to this event ID with 'Great point' using my private key"
|
|
284
|
+
- "Set my relay list to wss://relay.damus.io and wss://nos.lol"
|
|
285
|
+
- "Fetch the relay list for this npub"
|
|
286
|
+
|
|
287
|
+
### Direct Messaging (NIP-04 / NIP-44)
|
|
288
|
+
- "Encrypt 'hey' with NIP-04 for this recipient pubkey"
|
|
289
|
+
- "Send a NIP-04 DM saying 'hello' to this pubkey using my private key"
|
|
290
|
+
- "Send a NIP-44 DM saying 'check this out' to this pubkey"
|
|
291
|
+
- "Fetch and decrypt my NIP-44 inbox using my private key"
|
|
292
|
+
|
|
233
293
|
### Anonymous Operations
|
|
234
294
|
- "Send an anonymous zap of 100 sats to npub1qny3tkh0acurzla8x3zy4nhrjz5zd8ne6dvrjehx9n9hr3lnj08qwuzwc8"
|
|
235
295
|
- "Send 1000 sats to note1abcdef... with a comment saying 'Great post!'"
|
|
@@ -349,22 +409,17 @@ For zap queries, you can enable extra validation and debugging:
|
|
|
349
409
|
|
|
350
410
|
- "Show me all zaps for npub1qny3tkh0acurzla8x3zy4nhrjz5zd8ne6dvrjehx9n9hr3lnj08qwuzwc8 with validation and debug enabled"
|
|
351
411
|
|
|
352
|
-
For NIP searches, you can control the number of results and include full content:
|
|
353
|
-
|
|
354
|
-
- "Search for NIPs about zaps with full content"
|
|
355
|
-
- "Show me the top 5 NIPs about relays"
|
|
356
|
-
- "What NIPs are related to encryption? Show me 15 results"
|
|
357
|
-
|
|
358
412
|
## Limitations
|
|
359
413
|
|
|
360
414
|
- The server has a default 8-second timeout for queries to prevent hanging
|
|
361
|
-
-
|
|
415
|
+
- When a tool expects a public key, both hex and npub formats are supported
|
|
362
416
|
- Only a subset of relays is used by default
|
|
363
417
|
|
|
364
418
|
## Implementation Details
|
|
365
419
|
|
|
366
420
|
- Built with **[snstr](https://github.com/austinkelsay/snstr)** - a lightweight, modern TypeScript library for Nostr protocol implementation
|
|
367
421
|
- Native support for npub format using NIP-19 encoding/decoding
|
|
422
|
+
- Relay authentication support for NIP-42 where relays require AUTH events
|
|
368
423
|
- NIP-57 compliant zap receipt detection with direction-awareness (sent/received/self)
|
|
369
424
|
- Advanced bolt11 invoice parsing with payment amount extraction
|
|
370
425
|
- Smart caching system for improved performance with large volumes of zaps
|
|
@@ -417,49 +472,57 @@ To modify or extend this server:
|
|
|
417
472
|
- `index.ts`: Main server and tool registration
|
|
418
473
|
- `profile/profile-tools.ts`: Identity management, keypair generation, profile creation ([Documentation](./profile/README.md))
|
|
419
474
|
- `note/note-tools.ts`: Note creation, signing, publishing, and reading functionality ([Documentation](./note/README.md))
|
|
475
|
+
- `event/event-tools.ts`: Generic event querying/creation/signing/publishing
|
|
476
|
+
- `social/social-tools.ts`: Follow/unfollow/reaction/repost/delete/reply flows
|
|
477
|
+
- `relay/relay-tools.ts`: Relay list (NIP-65) read/write and NIP-42 auth support
|
|
478
|
+
- `dm/dm-tools.ts`: NIP-04 and NIP-44/NIP-17 direct message tooling
|
|
420
479
|
- `zap/zap-tools.ts`: Zap-related functionality ([Documentation](./zap/README.md))
|
|
421
|
-
- `nips/nips-tools.ts`: Functions for searching NIPs ([Documentation](./nips/README.md))
|
|
422
480
|
- `utils/`: Shared utility functions
|
|
423
481
|
- `constants.ts`: Global constants and relay configurations
|
|
424
482
|
- `conversion.ts`: NIP-19 entity conversion utilities (hex/npub/nprofile/nevent/naddr)
|
|
425
483
|
- `formatting.ts`: Output formatting helpers
|
|
484
|
+
- `keys.ts`: Shared private key normalization and auth key helpers
|
|
426
485
|
- `nip19-tools.ts`: NIP-19 entity conversion and analysis tools
|
|
427
486
|
- `pool.ts`: Nostr connection pool management
|
|
428
487
|
- `ephemeral-relay.ts`: In-memory Nostr relay for testing
|
|
429
488
|
|
|
430
|
-
2. Run `npm run build` to compile
|
|
489
|
+
2. Run `bun run build` (or `npm run build`) to compile.
|
|
431
490
|
|
|
432
491
|
3. Restart Claude for Desktop or Cursor to pick up your changes
|
|
433
492
|
|
|
434
493
|
## Testing
|
|
435
494
|
|
|
436
|
-
We've implemented a comprehensive test suite using
|
|
495
|
+
We've implemented a comprehensive test suite using Bun's native test runner to test both basic functionality and integration with the Nostr protocol:
|
|
437
496
|
|
|
438
497
|
```bash
|
|
439
|
-
# Run all tests
|
|
440
|
-
|
|
498
|
+
# Run all tests (using Bun - Recommended)
|
|
499
|
+
bun test
|
|
441
500
|
|
|
442
501
|
# Run a specific test file
|
|
443
|
-
|
|
502
|
+
bun test __tests__/basic.test.ts
|
|
444
503
|
|
|
445
504
|
# Run integration tests
|
|
446
|
-
|
|
505
|
+
bun test __tests__/integration.test.ts
|
|
447
506
|
```
|
|
448
507
|
|
|
449
508
|
The test suite includes:
|
|
450
509
|
|
|
451
510
|
### Unit Tests
|
|
452
511
|
- `basic.test.ts` - Tests simple profile formatting and zap receipt processing
|
|
512
|
+
- `dm-tools.test.ts` - Tests NIP-04/NIP-44 direct message encryption/decryption and message workflows
|
|
513
|
+
- `event-tools.test.ts` - Tests generic event query/create/sign/publish helpers
|
|
453
514
|
- `profile-notes-simple.test.ts` - Tests profile and note data structures
|
|
454
515
|
- `profile-tools.test.ts` - Tests keypair generation, profile creation, and identity management
|
|
455
516
|
- `note-creation.test.ts` - Tests note creation, signing, and publishing workflows
|
|
456
517
|
- `note-tools-functions.test.ts` - Tests note formatting, creation, signing, and publishing functions
|
|
457
518
|
- `note-tools-unit.test.ts` - Unit tests for note formatting functions
|
|
458
519
|
- `profile-postnote.test.ts` - Tests authenticated note posting with existing private keys
|
|
520
|
+
- `relay-tools.test.ts` - Tests relay list tooling (NIP-65) and authenticated relay interactions
|
|
521
|
+
- `social-tools.test.ts` - Tests follow/unfollow, reactions, reposts, deletes, and replies
|
|
459
522
|
- `zap-tools-simple.test.ts` - Tests zap processing and anonymous zap preparation
|
|
460
523
|
- `zap-tools-tests.test.ts` - Tests zap validation, parsing, and direction determination
|
|
461
|
-
- `search-nips-simple.test.ts` - Tests NIPs search functionality with relevance scoring
|
|
462
524
|
- `nip19-conversion.test.ts` - Tests NIP-19 entity conversion and analysis (28 test cases)
|
|
525
|
+
- `nip42-auth.test.ts` - Tests NIP-42 relay authentication flow and key handling
|
|
463
526
|
|
|
464
527
|
### Integration Tests
|
|
465
528
|
- `integration.test.ts` - Tests interaction with an ephemeral Nostr relay including:
|
|
@@ -491,8 +554,11 @@ The codebase is organized into modules:
|
|
|
491
554
|
- Specialized functionality in dedicated directories:
|
|
492
555
|
- [`profile/`](./profile/README.md): Identity management, keypair generation, and profile creation
|
|
493
556
|
- [`note/`](./note/README.md): Note creation, signing, publishing, and reading functionality
|
|
557
|
+
- `event/`: Generic event query/create/sign/publish functionality
|
|
558
|
+
- `social/`: Social interactions (follow/unfollow/reactions/reposts/deletes/replies)
|
|
559
|
+
- `relay/`: Relay list tooling and relay auth support
|
|
560
|
+
- `dm/`: NIP-04 and NIP-44/NIP-17 direct messaging tools
|
|
494
561
|
- [`zap/`](./zap/README.md): Zap handling and anonymous zapping
|
|
495
|
-
- [`nips/`](./nips/README.md): NIPs search and caching functionality
|
|
496
562
|
- Common utilities in the `utils/` directory
|
|
497
563
|
|
|
498
564
|
This modular structure makes the codebase more maintainable, reduces duplication, and enables easier feature extensions. For detailed information about each module's features and implementation, see their respective documentation.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { describe, test, expect, mock } from 'bun:test';
|
|
2
2
|
// Mock the formatProfile function
|
|
3
|
-
const mockFormatProfile =
|
|
3
|
+
const mockFormatProfile = mock((profile) => {
|
|
4
4
|
const content = typeof profile.content === 'string'
|
|
5
5
|
? JSON.parse(profile.content)
|
|
6
6
|
: profile.content;
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
import { describe, test, expect, beforeAll, afterAll } from "bun:test";
|
|
2
|
+
import { schnorr } from "@noble/curves/secp256k1";
|
|
3
|
+
import { NostrRelay } from "../utils/ephemeral-relay.js";
|
|
4
|
+
import { QUERY_TIMEOUT } from "../utils/constants.js";
|
|
5
|
+
import { encryptNip04, decryptNip04, sendDmNip04, getDmConversationNip04, encryptNip44, decryptNip44, sendDmNip44, getDmInboxNip44, } from "../dm/dm-tools.js";
|
|
6
|
+
describe("dm-tools", () => {
|
|
7
|
+
let relay;
|
|
8
|
+
let relayUrl;
|
|
9
|
+
let authRelay;
|
|
10
|
+
let authRelayUrl;
|
|
11
|
+
// Fixed test keys (32 bytes hex). Not used outside this test context.
|
|
12
|
+
const alicePriv = "0000000000000000000000000000000000000000000000000000000000000001";
|
|
13
|
+
const bobPriv = "0000000000000000000000000000000000000000000000000000000000000002";
|
|
14
|
+
const alicePub = Buffer.from(schnorr.getPublicKey(alicePriv)).toString("hex");
|
|
15
|
+
const bobPub = Buffer.from(schnorr.getPublicKey(bobPriv)).toString("hex");
|
|
16
|
+
beforeAll(async () => {
|
|
17
|
+
relay = new NostrRelay(0);
|
|
18
|
+
await relay.start();
|
|
19
|
+
relayUrl = relay.url;
|
|
20
|
+
authRelay = new NostrRelay(0, undefined, true);
|
|
21
|
+
await authRelay.start();
|
|
22
|
+
authRelayUrl = authRelay.url;
|
|
23
|
+
});
|
|
24
|
+
afterAll(async () => {
|
|
25
|
+
await relay.close();
|
|
26
|
+
await authRelay.close();
|
|
27
|
+
});
|
|
28
|
+
test("NIP-04 encrypt/decrypt roundtrip", async () => {
|
|
29
|
+
const msg = `hello-${Date.now()}`;
|
|
30
|
+
const enc = await encryptNip04({ privateKey: alicePriv, recipientPubkey: bobPub, plaintext: msg });
|
|
31
|
+
expect(enc.success).toBe(true);
|
|
32
|
+
expect(enc.ciphertext).toBeTruthy();
|
|
33
|
+
const dec = await decryptNip04({ privateKey: bobPriv, senderPubkey: alicePub, ciphertext: enc.ciphertext });
|
|
34
|
+
expect(dec.success).toBe(true);
|
|
35
|
+
expect(dec.plaintext).toBe(msg);
|
|
36
|
+
});
|
|
37
|
+
test("NIP-44 encrypt/decrypt roundtrip", async () => {
|
|
38
|
+
const msg = `hello44-${Date.now()}`;
|
|
39
|
+
const enc = await encryptNip44({ privateKey: alicePriv, recipientPubkey: bobPub, plaintext: msg });
|
|
40
|
+
expect(enc.success).toBe(true);
|
|
41
|
+
expect(enc.ciphertext).toBeTruthy();
|
|
42
|
+
const dec = await decryptNip44({ privateKey: bobPriv, senderPubkey: alicePub, ciphertext: enc.ciphertext });
|
|
43
|
+
expect(dec.success).toBe(true);
|
|
44
|
+
expect(dec.plaintext).toBe(msg);
|
|
45
|
+
});
|
|
46
|
+
test("sendDmNip04 + getDmConversationNip04 decrypts both directions", async () => {
|
|
47
|
+
const a1 = await sendDmNip04({
|
|
48
|
+
privateKey: alicePriv,
|
|
49
|
+
recipientPubkey: bobPub,
|
|
50
|
+
content: "a->b",
|
|
51
|
+
relays: [relayUrl],
|
|
52
|
+
});
|
|
53
|
+
expect(a1.success).toBe(true);
|
|
54
|
+
const b1 = await sendDmNip04({
|
|
55
|
+
privateKey: bobPriv,
|
|
56
|
+
recipientPubkey: alicePub,
|
|
57
|
+
content: "b->a",
|
|
58
|
+
relays: [relayUrl],
|
|
59
|
+
});
|
|
60
|
+
expect(b1.success).toBe(true);
|
|
61
|
+
// Poll until both are queryable (relay OK may arrive before store).
|
|
62
|
+
const deadline = Date.now() + QUERY_TIMEOUT;
|
|
63
|
+
let convo = null;
|
|
64
|
+
while (Date.now() < deadline) {
|
|
65
|
+
convo = await getDmConversationNip04({
|
|
66
|
+
privateKey: alicePriv,
|
|
67
|
+
peerPubkey: bobPub,
|
|
68
|
+
relays: [relayUrl],
|
|
69
|
+
limit: 10,
|
|
70
|
+
decrypt: true,
|
|
71
|
+
});
|
|
72
|
+
if (convo.success && (convo.messages ?? []).length >= 2)
|
|
73
|
+
break;
|
|
74
|
+
await new Promise((r) => setTimeout(r, 25));
|
|
75
|
+
}
|
|
76
|
+
expect(convo?.success).toBe(true);
|
|
77
|
+
const contents = (convo.messages ?? []).map((m) => m.content);
|
|
78
|
+
expect(contents).toContain("a->b");
|
|
79
|
+
expect(contents).toContain("b->a");
|
|
80
|
+
});
|
|
81
|
+
test("sendDmNip44 + getDmInboxNip44 decrypts gift wrapped message", async () => {
|
|
82
|
+
const msg = `gift-${Date.now()}`;
|
|
83
|
+
const sent = await sendDmNip44({ privateKey: alicePriv, recipientPubkey: bobPub, content: msg, relays: [relayUrl] });
|
|
84
|
+
expect(sent.success).toBe(true);
|
|
85
|
+
const deadline = Date.now() + QUERY_TIMEOUT;
|
|
86
|
+
let inbox = null;
|
|
87
|
+
while (Date.now() < deadline) {
|
|
88
|
+
inbox = await getDmInboxNip44({ privateKey: bobPriv, relays: [relayUrl], limit: 25 });
|
|
89
|
+
if (inbox.success && (inbox.messages ?? []).some((m) => m.content === msg))
|
|
90
|
+
break;
|
|
91
|
+
await new Promise((r) => setTimeout(r, 25));
|
|
92
|
+
}
|
|
93
|
+
expect(inbox?.success).toBe(true);
|
|
94
|
+
const found = (inbox.messages ?? []).find((m) => m.content === msg);
|
|
95
|
+
expect(found).toBeTruthy();
|
|
96
|
+
expect(found.from).toBe(alicePub);
|
|
97
|
+
});
|
|
98
|
+
test("DM tools pass authPrivateKey through for NIP-42 relays", async () => {
|
|
99
|
+
const nip04Msg = `auth-nip04-${Date.now()}`;
|
|
100
|
+
const noAuth04 = await sendDmNip04({
|
|
101
|
+
privateKey: alicePriv,
|
|
102
|
+
recipientPubkey: bobPub,
|
|
103
|
+
content: nip04Msg,
|
|
104
|
+
relays: [authRelayUrl],
|
|
105
|
+
});
|
|
106
|
+
expect(noAuth04.success).toBe(false);
|
|
107
|
+
const withAuth04 = await sendDmNip04({
|
|
108
|
+
privateKey: alicePriv,
|
|
109
|
+
recipientPubkey: bobPub,
|
|
110
|
+
content: nip04Msg,
|
|
111
|
+
relays: [authRelayUrl],
|
|
112
|
+
authPrivateKey: alicePriv,
|
|
113
|
+
});
|
|
114
|
+
expect(withAuth04.success).toBe(true);
|
|
115
|
+
const noAuth04Query = await getDmConversationNip04({
|
|
116
|
+
privateKey: bobPriv,
|
|
117
|
+
peerPubkey: alicePub,
|
|
118
|
+
relays: [authRelayUrl],
|
|
119
|
+
limit: 10,
|
|
120
|
+
});
|
|
121
|
+
expect(noAuth04Query.success).toBe(false);
|
|
122
|
+
const deadline04 = Date.now() + QUERY_TIMEOUT;
|
|
123
|
+
let convo04 = null;
|
|
124
|
+
while (Date.now() < deadline04) {
|
|
125
|
+
convo04 = await getDmConversationNip04({
|
|
126
|
+
privateKey: bobPriv,
|
|
127
|
+
peerPubkey: alicePub,
|
|
128
|
+
relays: [authRelayUrl],
|
|
129
|
+
authPrivateKey: bobPriv,
|
|
130
|
+
limit: 10,
|
|
131
|
+
decrypt: true,
|
|
132
|
+
});
|
|
133
|
+
if (convo04.success && (convo04.messages ?? []).some((m) => m.content === nip04Msg))
|
|
134
|
+
break;
|
|
135
|
+
await new Promise((r) => setTimeout(r, 25));
|
|
136
|
+
}
|
|
137
|
+
expect(convo04?.success).toBe(true);
|
|
138
|
+
expect((convo04.messages ?? []).some((m) => m.content === nip04Msg)).toBe(true);
|
|
139
|
+
const nip44Msg = `auth-nip44-${Date.now()}`;
|
|
140
|
+
const noAuth44 = await sendDmNip44({
|
|
141
|
+
privateKey: alicePriv,
|
|
142
|
+
recipientPubkey: bobPub,
|
|
143
|
+
content: nip44Msg,
|
|
144
|
+
relays: [authRelayUrl],
|
|
145
|
+
});
|
|
146
|
+
expect(noAuth44.success).toBe(false);
|
|
147
|
+
const withAuth44 = await sendDmNip44({
|
|
148
|
+
privateKey: alicePriv,
|
|
149
|
+
recipientPubkey: bobPub,
|
|
150
|
+
content: nip44Msg,
|
|
151
|
+
relays: [authRelayUrl],
|
|
152
|
+
authPrivateKey: alicePriv,
|
|
153
|
+
});
|
|
154
|
+
expect(withAuth44.success).toBe(true);
|
|
155
|
+
const noAuth44Query = await getDmInboxNip44({
|
|
156
|
+
privateKey: bobPriv,
|
|
157
|
+
relays: [authRelayUrl],
|
|
158
|
+
limit: 25,
|
|
159
|
+
});
|
|
160
|
+
expect(noAuth44Query.success).toBe(false);
|
|
161
|
+
const deadline44 = Date.now() + QUERY_TIMEOUT;
|
|
162
|
+
let inbox44 = null;
|
|
163
|
+
while (Date.now() < deadline44) {
|
|
164
|
+
inbox44 = await getDmInboxNip44({
|
|
165
|
+
privateKey: bobPriv,
|
|
166
|
+
relays: [authRelayUrl],
|
|
167
|
+
authPrivateKey: bobPriv,
|
|
168
|
+
limit: 25,
|
|
169
|
+
});
|
|
170
|
+
if (inbox44.success && (inbox44.messages ?? []).some((m) => m.content === nip44Msg))
|
|
171
|
+
break;
|
|
172
|
+
await new Promise((r) => setTimeout(r, 25));
|
|
173
|
+
}
|
|
174
|
+
expect(inbox44?.success).toBe(true);
|
|
175
|
+
expect((inbox44.messages ?? []).some((m) => m.content === nip44Msg)).toBe(true);
|
|
176
|
+
});
|
|
177
|
+
test("DM conversation/inbox return structured error for invalid private keys", async () => {
|
|
178
|
+
const convo = await getDmConversationNip04({
|
|
179
|
+
privateKey: "bad-key",
|
|
180
|
+
peerPubkey: bobPub,
|
|
181
|
+
relays: [relayUrl],
|
|
182
|
+
});
|
|
183
|
+
expect(convo.success).toBe(false);
|
|
184
|
+
expect(convo.message).toBe("Invalid private key format.");
|
|
185
|
+
const inbox = await getDmInboxNip44({
|
|
186
|
+
privateKey: "bad-key",
|
|
187
|
+
relays: [relayUrl],
|
|
188
|
+
});
|
|
189
|
+
expect(inbox.success).toBe(false);
|
|
190
|
+
expect(inbox.message).toBe("Invalid private key format.");
|
|
191
|
+
});
|
|
192
|
+
});
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { describe, test, expect, beforeAll, afterAll } from "bun:test";
|
|
2
|
+
import { NostrRelay } from "../utils/ephemeral-relay.js";
|
|
3
|
+
import { DEFAULT_RELAYS, KINDS, QUERY_TIMEOUT } from "../utils/constants.js";
|
|
4
|
+
import { createNostrEvent, signNostrEvent, publishNostrEvent, queryEvents, } from "../event/event-tools.js";
|
|
5
|
+
describe("event-tools", () => {
|
|
6
|
+
let relay;
|
|
7
|
+
let relayUrl;
|
|
8
|
+
beforeAll(async () => {
|
|
9
|
+
relay = new NostrRelay(0);
|
|
10
|
+
await relay.start();
|
|
11
|
+
relayUrl = relay.url;
|
|
12
|
+
});
|
|
13
|
+
afterAll(async () => {
|
|
14
|
+
await relay.close();
|
|
15
|
+
});
|
|
16
|
+
test("create/sign/publish/query a generic kind 7 reaction event", async () => {
|
|
17
|
+
// Fixed test key (32 bytes hex). Not used outside this test context.
|
|
18
|
+
const privateKey = "0000000000000000000000000000000000000000000000000000000000000001";
|
|
19
|
+
const created = await createNostrEvent({
|
|
20
|
+
kind: KINDS.REACTION,
|
|
21
|
+
content: "+",
|
|
22
|
+
tags: [["e", "deadbeef".repeat(8)]],
|
|
23
|
+
privateKey,
|
|
24
|
+
});
|
|
25
|
+
expect(created.success).toBe(true);
|
|
26
|
+
expect(created.event?.kind).toBe(KINDS.REACTION);
|
|
27
|
+
expect(created.event?.content).toBe("+");
|
|
28
|
+
const signed = await signNostrEvent({
|
|
29
|
+
privateKey,
|
|
30
|
+
event: created.event,
|
|
31
|
+
});
|
|
32
|
+
expect(signed.success).toBe(true);
|
|
33
|
+
expect(signed.signedEvent?.id).toBeTruthy();
|
|
34
|
+
expect(signed.signedEvent?.sig).toBeTruthy();
|
|
35
|
+
// Prefer the ephemeral relay for test determinism; fall back to defaults if it's unavailable.
|
|
36
|
+
const relays = relayUrl ? [relayUrl] : DEFAULT_RELAYS;
|
|
37
|
+
const published = await publishNostrEvent({
|
|
38
|
+
signedEvent: signed.signedEvent,
|
|
39
|
+
relays,
|
|
40
|
+
});
|
|
41
|
+
if (!published.success) {
|
|
42
|
+
throw new Error(published.message);
|
|
43
|
+
}
|
|
44
|
+
expect(published.success).toBe(true);
|
|
45
|
+
// Poll queryEvents until it shows up (relay sends OK before storing).
|
|
46
|
+
const deadline = Date.now() + QUERY_TIMEOUT;
|
|
47
|
+
let last = null;
|
|
48
|
+
while (Date.now() < deadline) {
|
|
49
|
+
last = await queryEvents({
|
|
50
|
+
relays,
|
|
51
|
+
kinds: [KINDS.REACTION],
|
|
52
|
+
authors: [signed.signedEvent.pubkey],
|
|
53
|
+
limit: 25,
|
|
54
|
+
});
|
|
55
|
+
if (last.success && (last.events ?? []).some((e) => e.id === signed.signedEvent.id))
|
|
56
|
+
break;
|
|
57
|
+
await new Promise((r) => setTimeout(r, 25));
|
|
58
|
+
}
|
|
59
|
+
if (!last?.success) {
|
|
60
|
+
throw new Error(last?.message || "queryEvents failed");
|
|
61
|
+
}
|
|
62
|
+
expect((last.events ?? []).some((e) => e.id === signed.signedEvent.id)).toBe(true);
|
|
63
|
+
});
|
|
64
|
+
});
|
|
@@ -1,6 +1,7 @@
|
|
|
1
|
+
import { describe, test, expect, beforeAll, afterAll } from 'bun:test';
|
|
1
2
|
import { NostrRelay } from '../utils/ephemeral-relay.js';
|
|
2
3
|
import { schnorr } from '@noble/curves/secp256k1';
|
|
3
|
-
import { randomBytes } from 'crypto';
|
|
4
|
+
import { randomBytes } from 'node:crypto';
|
|
4
5
|
import { sha256 } from '@noble/hashes/sha256';
|
|
5
6
|
// Generate a keypair for testing
|
|
6
7
|
function generatePrivateKey() {
|
|
@@ -34,14 +35,14 @@ function createSignedEvent(privateKey, kind, content, tags = []) {
|
|
|
34
35
|
}
|
|
35
36
|
describe('Nostr Integration Tests', () => {
|
|
36
37
|
let relay;
|
|
37
|
-
const testPort = 9700;
|
|
38
38
|
let privateKey;
|
|
39
39
|
let publicKey;
|
|
40
40
|
beforeAll(async () => {
|
|
41
41
|
privateKey = generatePrivateKey();
|
|
42
42
|
publicKey = getPublicKey(privateKey);
|
|
43
43
|
// Start the ephemeral relay
|
|
44
|
-
|
|
44
|
+
// Use port 0 to avoid CI/local port conflicts.
|
|
45
|
+
relay = new NostrRelay(0);
|
|
45
46
|
await relay.start();
|
|
46
47
|
});
|
|
47
48
|
afterAll(async () => {
|
package/build/__tests__/mocks.js
CHANGED
|
@@ -1,5 +1,3 @@
|
|
|
1
|
-
// Mock Nostr events and utility functions for testing
|
|
2
|
-
import { jest } from '@jest/globals';
|
|
3
1
|
export const MOCK_HEX_PUBKEY = '7e7e9c42a91bfef19fa929e5fda1b72e0ebc1a4c1141673e2794234d86addf4e';
|
|
4
2
|
export const MOCK_NPUB = 'npub180cvv07tjdrrgpa0j7j7tmnyl2yr6yr7l8j4s3evf6u64th6gkwsyjh6w6';
|
|
5
3
|
export const mockProfile = {
|
|
@@ -64,14 +62,6 @@ export const mockZapReceipt = {
|
|
|
64
62
|
content: '',
|
|
65
63
|
sig: 'mock_signature'
|
|
66
64
|
};
|
|
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
65
|
// Mock response for lightning service for anonymous zaps
|
|
76
66
|
export const mockLightningServiceResponse = {
|
|
77
67
|
callback: 'https://example.com/callback',
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { describe, it, expect, beforeAll } from 'bun:test';
|
|
1
2
|
import { convertNip19, analyzeNip19 } from '../utils/nip19-tools.js';
|
|
2
3
|
import { generateKeypair, encodePublicKey, encodePrivateKey, encodeNoteId, encodeProfile, encodeEvent, encodeAddress } from 'snstr';
|
|
3
4
|
describe('NIP-19 Conversion Tools', () => {
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { describe, test, expect, beforeAll, afterAll } from "bun:test";
|
|
2
|
+
import { NostrRelay } from "../utils/ephemeral-relay.js";
|
|
3
|
+
import { createNostrEvent, signNostrEvent, publishNostrEvent, queryEvents } from "../event/event-tools.js";
|
|
4
|
+
import { KINDS } from "../utils/constants.js";
|
|
5
|
+
describe("NIP-42 AUTH", () => {
|
|
6
|
+
let relay;
|
|
7
|
+
let relayUrl;
|
|
8
|
+
// Fixed test key (32 bytes hex). Not used outside this test context.
|
|
9
|
+
const privateKey = "0000000000000000000000000000000000000000000000000000000000000001";
|
|
10
|
+
beforeAll(async () => {
|
|
11
|
+
relay = new NostrRelay(0, undefined, true);
|
|
12
|
+
await relay.start();
|
|
13
|
+
relayUrl = relay.url;
|
|
14
|
+
});
|
|
15
|
+
afterAll(async () => {
|
|
16
|
+
await relay.close();
|
|
17
|
+
});
|
|
18
|
+
test("publish/query fail without authPrivateKey and succeed with it", async () => {
|
|
19
|
+
const created = await createNostrEvent({
|
|
20
|
+
kind: KINDS.TEXT,
|
|
21
|
+
content: `auth-${Date.now()}`,
|
|
22
|
+
tags: [],
|
|
23
|
+
privateKey,
|
|
24
|
+
});
|
|
25
|
+
expect(created.success).toBe(true);
|
|
26
|
+
const signed = await signNostrEvent({ privateKey, event: created.event });
|
|
27
|
+
expect(signed.success).toBe(true);
|
|
28
|
+
const noAuthPub = await publishNostrEvent({ signedEvent: signed.signedEvent, relays: [relayUrl] });
|
|
29
|
+
expect(noAuthPub.success).toBe(false);
|
|
30
|
+
const withAuthPub = await publishNostrEvent({
|
|
31
|
+
signedEvent: signed.signedEvent,
|
|
32
|
+
relays: [relayUrl],
|
|
33
|
+
authPrivateKey: privateKey,
|
|
34
|
+
});
|
|
35
|
+
expect(withAuthPub.success).toBe(true);
|
|
36
|
+
const noAuthQuery = await queryEvents({
|
|
37
|
+
relays: [relayUrl],
|
|
38
|
+
kinds: [KINDS.TEXT],
|
|
39
|
+
authors: [signed.signedEvent.pubkey],
|
|
40
|
+
limit: 10,
|
|
41
|
+
});
|
|
42
|
+
expect(noAuthQuery.success).toBe(false);
|
|
43
|
+
const withAuthQuery = await queryEvents({
|
|
44
|
+
relays: [relayUrl],
|
|
45
|
+
authPrivateKey: privateKey,
|
|
46
|
+
kinds: [KINDS.TEXT],
|
|
47
|
+
authors: [signed.signedEvent.pubkey],
|
|
48
|
+
limit: 10,
|
|
49
|
+
});
|
|
50
|
+
expect(withAuthQuery.success).toBe(true);
|
|
51
|
+
expect((withAuthQuery.events ?? []).some((e) => e.id === signed.signedEvent.id)).toBe(true);
|
|
52
|
+
});
|
|
53
|
+
});
|
|
@@ -1,16 +1,16 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { mock, describe, it, expect, beforeAll, beforeEach } from 'bun:test';
|
|
2
2
|
import { generateKeypair } from 'snstr';
|
|
3
3
|
// Mock the pool to prevent real WebSocket connections
|
|
4
4
|
const mockPool = {
|
|
5
|
-
close:
|
|
6
|
-
publish:
|
|
5
|
+
close: mock(() => { }),
|
|
6
|
+
publish: mock(() => [
|
|
7
7
|
Promise.resolve({ success: true }),
|
|
8
8
|
Promise.resolve({ success: true })
|
|
9
9
|
])
|
|
10
10
|
};
|
|
11
11
|
// Mock the pool module directly
|
|
12
|
-
|
|
13
|
-
getFreshPool:
|
|
12
|
+
mock.module('../utils/pool.js', () => ({
|
|
13
|
+
getFreshPool: mock(() => mockPool)
|
|
14
14
|
}));
|
|
15
15
|
// Now import the functions that use the mocked module
|
|
16
16
|
import { formatProfile, formatNote, createNote, signNote, publishNote } from '../note/note-tools.js';
|
|
@@ -19,6 +19,11 @@ describe('Note Tools Functions', () => {
|
|
|
19
19
|
beforeAll(async () => {
|
|
20
20
|
testKeys = await generateKeypair();
|
|
21
21
|
});
|
|
22
|
+
beforeEach(() => {
|
|
23
|
+
// Reset mock state between tests
|
|
24
|
+
mockPool.close.mockClear();
|
|
25
|
+
mockPool.publish.mockClear();
|
|
26
|
+
});
|
|
22
27
|
describe('formatProfile', () => {
|
|
23
28
|
it('should format a complete profile', () => {
|
|
24
29
|
const profileEvent = {
|