upleb.uk

Public git repos — served from a NIP-34 GRASP relay at git.upleb.uk

summaryrefslogtreecommitdiff
path: root/docs/learnings/nostr-sdk.md
diff options
context:
space:
mode:
authorDanConwayDev <DanConwayDev@protonmail.com>2025-11-04 09:31:57 +0000
committerDanConwayDev <DanConwayDev@protonmail.com>2025-11-04 09:31:57 +0000
commit22557f15d6a7b77f72d4597fc05aa06346495a33 (patch)
treee31e0cdecfc4cb1e28246227a7ef295b71687b09 /docs/learnings/nostr-sdk.md
parentb3031800cd95601c2d9cd2d24034364d1496b073 (diff)
docs: major cleanup and reorganization
- Archive 30 completed session documents to docs/archive/ - Extract learnings to docs/learnings/ (nix-flakes, nostr-sdk, grasp-audit) - Create CURRENT_STATUS.md as single source of truth - Create AGENTS.md with documentation guidelines - Create docs/archive/README.md for archive organization - Clean root directory: 32 files → 4 files Root directory now contains only: - README.md (project overview) - AGENTS.md (documentation guidelines) - CURRENT_STATUS.md (current state) - CLEANUP_SUMMARY.md (cleanup report) All historical documents preserved in docs/archive/ with proper dating. All reusable knowledge extracted to docs/learnings/. Benefits: - Easy to find current information - Clear document lifecycle - No more documentation sprawl - Learnings are accessible and reusable - Better onboarding for new developers/agents File counts: - Root: 4 (was 32) - Permanent docs: 7 - Learnings: 3 (new) - Archive: 32 (new) - Total: 49 well-organized docs
Diffstat (limited to 'docs/learnings/nostr-sdk.md')
-rw-r--r--docs/learnings/nostr-sdk.md577
1 files changed, 577 insertions, 0 deletions
diff --git a/docs/learnings/nostr-sdk.md b/docs/learnings/nostr-sdk.md
new file mode 100644
index 0000000..57f451a
--- /dev/null
+++ b/docs/learnings/nostr-sdk.md
@@ -0,0 +1,577 @@
1# nostr-sdk - Learnings and Patterns
2
3**Purpose:** Document nostr-sdk usage patterns, upgrade notes, and gotchas
4**Last Updated:** November 4, 2025
5
6---
7
8## Current Version
9
10**We use nostr-sdk 0.43.x (latest stable)**
11
12```toml
13[dependencies]
14nostr-sdk = "0.43"
15```
16
17**Upgraded from:** 0.35.0 on November 4, 2025
18
19---
20
21## Critical Breaking Changes (0.35 → 0.43)
22
23### 1. EventBuilder API Changed
24
25**Before (0.35):**
26```rust
27let event = EventBuilder::new(kind, content, tags)
28 .to_event(keys)?;
29```
30
31**After (0.43):**
32```rust
33let event = EventBuilder::new(kind, content)
34 .tags(tags)
35 .sign_with_keys(keys)?;
36```
37
38**Changes:**
39- ❌ Removed `tags` parameter from constructor
40- ✅ Use `.tags()` builder method instead
41- ❌ Removed `.to_event()` method
42- ✅ Use `.sign_with_keys()` instead (more descriptive)
43
44---
45
46### 2. Client Ownership of Keys
47
48**Before (0.35):**
49```rust
50let keys = Keys::generate();
51let client = Client::new(&keys); // Reference
52// keys still available
53```
54
55**After (0.43):**
56```rust
57let keys = Keys::generate();
58let client = Client::new(keys.clone()); // Ownership
59// Need to clone if we want to keep keys
60```
61
62**Why:** Allows Client to own the signer, enabling more flexible signer types.
63
64---
65
66### 3. Relay Status Check No Longer Async
67
68**Before (0.35):**
69```rust
70if relay.is_connected().await {
71 // ...
72}
73```
74
75**After (0.43):**
76```rust
77if relay.is_connected() { // No await!
78 // ...
79}
80```
81
82**Why:** Status check doesn't require async operation.
83
84---
85
86### 4. Query API Redesigned
87
88**Before (0.35):**
89```rust
90let events = client
91 .get_events_of(vec![filter], EventSource::relays(Some(timeout)))
92 .await?;
93// Returns Vec<Event>
94```
95
96**After (0.43):**
97```rust
98let events = client
99 .fetch_events(filter, timeout)
100 .await?;
101// Returns Events (iterable collection)
102
103// Convert to Vec if needed
104let vec: Vec<Event> = events.into_iter().collect();
105```
106
107**Changes:**
108- ❌ Removed `get_events_of()` method
109- ✅ Use `fetch_events()` instead
110- ❌ Removed `EventSource` parameter (confusing)
111- ✅ Direct timeout parameter
112- ❌ Single filter instead of `Vec<Filter>`
113- ✅ Returns `Events` type instead of `Vec<Event>`
114
115---
116
117### 5. Filter Custom Tags Simplified
118
119**Before (0.35):**
120```rust
121filter.custom_tag(tag, ["value"])
122filter.custom_tag(tag, [&string_ref])
123```
124
125**After (0.43):**
126```rust
127filter.custom_tag(tag, "value")
128filter.custom_tag(tag, &string_ref)
129```
130
131**Why:** Simplified API for the common case of single tag value.
132
133---
134
135### 6. Send Event Takes Reference
136
137**Before (0.35):**
138```rust
139let event_id = client.send_event(event).await?;
140```
141
142**After (0.43):**
143```rust
144let output = client.send_event(&event).await?;
145let event_id = *output.id();
146```
147
148**Changes:**
149- Takes `&Event` instead of `Event` (can reuse events)
150- Returns `SendEventOutput` instead of `EventId`
151- Need to call `.id()` to get the event ID
152
153---
154
155## Common Patterns
156
157### Creating and Signing Events
158
159```rust
160use nostr_sdk::prelude::*;
161
162// Generate keys
163let keys = Keys::generate();
164
165// Create event
166let event = EventBuilder::new(Kind::TextNote, "Hello Nostr!")
167 .tags(vec![
168 Tag::custom(TagKind::SingleLetter(SingleLetterTag::lowercase(Alphabet::T)),
169 vec!["nostr"]),
170 ])
171 .sign_with_keys(&keys)?;
172
173// Send event
174let output = client.send_event(&event).await?;
175println!("Event ID: {}", output.id());
176```
177
178---
179
180### Creating Custom Tags
181
182```rust
183use nostr_sdk::prelude::*;
184
185// Single letter tag (like "t" for topics)
186let t_tag = SingleLetterTag::lowercase(Alphabet::T);
187let tag = Tag::custom(
188 TagKind::SingleLetter(t_tag),
189 vec!["my-topic"]
190);
191
192// Custom multi-letter tag
193let tag = Tag::custom(
194 TagKind::Custom("custom-tag".to_string()),
195 vec!["value1", "value2"]
196);
197
198// Hashtag (convenience method)
199let tag = Tag::hashtag("nostr"); // Creates ["t", "nostr"]
200```
201
202---
203
204### Querying Events
205
206```rust
207use nostr_sdk::prelude::*;
208
209// Build filter
210let filter = Filter::new()
211 .kind(Kind::TextNote)
212 .custom_tag(
213 SingleLetterTag::lowercase(Alphabet::T),
214 "my-topic"
215 )
216 .since(Timestamp::now() - Duration::from_secs(3600)); // Last hour
217
218// Query events
219let timeout = Duration::from_secs(10);
220let events = client.fetch_events(filter, timeout).await?;
221
222// Process events
223for event in events.into_iter() {
224 println!("Event: {}", event.id());
225}
226```
227
228---
229
230### Multiple Filters
231
232Since `fetch_events()` takes a single filter, combine multiple queries:
233
234```rust
235// Option 1: Fetch separately and combine
236let mut all_events = Vec::new();
237for filter in filters {
238 let events = client.fetch_events(filter, timeout).await?;
239 all_events.extend(events.into_iter());
240}
241
242// Option 2: Use subscription (more efficient)
243let subscription_id = SubscriptionId::new("my-sub");
244client.subscribe(filters, None).await?;
245
246// Handle events via notification handler
247let mut notifications = client.notifications();
248while let Ok(notification) = notifications.recv().await {
249 if let RelayPoolNotification::Event { event, .. } = notification {
250 println!("Event: {}", event.id());
251 }
252}
253```
254
255---
256
257### Client Setup with Relay
258
259```rust
260use nostr_sdk::prelude::*;
261
262// Create keys
263let keys = Keys::generate();
264
265// Create client
266let client = Client::new(keys.clone());
267
268// Add relay
269client.add_relay("wss://relay.example.com").await?;
270
271// Connect
272client.connect().await;
273
274// Wait for connection
275tokio::time::sleep(Duration::from_secs(2)).await;
276
277// Check connection
278if client.relay("wss://relay.example.com")
279 .await?
280 .is_connected()
281{
282 println!("Connected!");
283}
284```
285
286---
287
288## Testing Patterns
289
290### Unit Tests (No Relay Required)
291
292```rust
293#[cfg(test)]
294mod tests {
295 use super::*;
296 use nostr_sdk::prelude::*;
297
298 #[test]
299 fn test_event_creation() {
300 let keys = Keys::generate();
301 let event = EventBuilder::new(Kind::TextNote, "test")
302 .sign_with_keys(&keys)
303 .unwrap();
304
305 assert_eq!(event.kind(), Kind::TextNote);
306 assert_eq!(event.content(), "test");
307 }
308
309 #[test]
310 fn test_tag_creation() {
311 let t_tag = SingleLetterTag::lowercase(Alphabet::T);
312 let tag = Tag::custom(
313 TagKind::SingleLetter(t_tag),
314 vec!["test-topic"]
315 );
316
317 // Verify tag structure
318 assert_eq!(tag.as_vec()[0], "t");
319 assert_eq!(tag.as_vec()[1], "test-topic");
320 }
321}
322```
323
324---
325
326### Integration Tests (Relay Required)
327
328```rust
329#[cfg(test)]
330mod tests {
331 use super::*;
332 use nostr_sdk::prelude::*;
333
334 #[tokio::test]
335 #[ignore] // Requires running relay
336 async fn test_send_and_receive() -> Result<()> {
337 // Setup
338 let keys = Keys::generate();
339 let client = Client::new(keys.clone());
340 client.add_relay("ws://localhost:7000").await?;
341 client.connect().await;
342 tokio::time::sleep(Duration::from_secs(2)).await;
343
344 // Send event
345 let event = EventBuilder::new(Kind::TextNote, "test")
346 .sign_with_keys(&keys)?;
347 let output = client.send_event(&event).await?;
348
349 // Query it back
350 let filter = Filter::new()
351 .id(*output.id());
352 let events = client.fetch_events(filter, Duration::from_secs(5)).await?;
353
354 assert_eq!(events.len(), 1);
355 Ok(())
356 }
357}
358```
359
360**Running integration tests:**
361```bash
362# Start relay first
363docker run --rm -p 7000:7000 scsibug/nostr-rs-relay
364
365# Run tests
366cargo test -- --ignored
367```
368
369---
370
371## Common Gotchas
372
373### 1. Event Validation Failures
374
375**Problem:** Events fail validation with cryptic errors
376
377**Common Causes:**
378- Invalid signature (wrong keys used)
379- Invalid event ID (content/tags changed after signing)
380- Invalid timestamp (too far in future/past)
381
382**Solution:**
383```rust
384// Always sign AFTER setting all fields
385let event = EventBuilder::new(kind, content)
386 .tags(tags) // Set tags first
387 .sign_with_keys(&keys)?; // Sign last
388
389// Don't modify event after signing!
390```
391
392---
393
394### 2. Filter Not Matching Events
395
396**Problem:** Query returns no events even though they exist
397
398**Common Causes:**
399- Tag kind mismatch (uppercase vs lowercase)
400- Wrong filter field (using `.author()` when you need `.authors()`)
401- Timeout too short
402
403**Solution:**
404```rust
405// Be explicit about tag kinds
406let t_tag = SingleLetterTag::lowercase(Alphabet::T); // Lowercase!
407
408// Use correct filter methods
409let filter = Filter::new()
410 .authors(vec![keys.public_key()]) // Note: plural
411 .kinds(vec![Kind::TextNote]); // Note: plural
412
413// Increase timeout for slow relays
414let timeout = Duration::from_secs(10);
415```
416
417---
418
419### 3. Connection Timing Issues
420
421**Problem:** Events fail to send or queries return empty
422
423**Cause:** Client not fully connected to relay
424
425**Solution:**
426```rust
427// Connect
428client.connect().await;
429
430// Wait for connection to establish
431tokio::time::sleep(Duration::from_secs(2)).await;
432
433// Verify connection
434let relay = client.relay("wss://relay.example.com").await?;
435if !relay.is_connected() {
436 return Err("Not connected".into());
437}
438
439// Now safe to send/query
440```
441
442---
443
444### 4. Clone Keys When Creating Client
445
446**Problem:** Can't use keys after creating client
447
448**Cause:** Client takes ownership in 0.43+
449
450**Solution:**
451```rust
452// Clone keys if you need them later
453let keys = Keys::generate();
454let client = Client::new(keys.clone()); // Clone!
455
456// Now can still use keys
457let pubkey = keys.public_key();
458```
459
460---
461
462## Performance Tips
463
464### 1. Reuse Clients
465
466```rust
467// ✅ Good - single client
468let client = Client::new(keys);
469client.add_relay("wss://relay1.com").await?;
470client.add_relay("wss://relay2.com").await?;
471client.connect().await;
472
473// ❌ Bad - multiple clients
474for relay in relays {
475 let client = Client::new(keys.clone()); // Wasteful!
476 client.add_relay(relay).await?;
477}
478```
479
480---
481
482### 2. Use Subscriptions for Live Updates
483
484```rust
485// ✅ Good for live updates - subscription
486let filters = vec![Filter::new().kind(Kind::TextNote)];
487client.subscribe(filters, None).await?;
488
489let mut notifications = client.notifications();
490while let Ok(notification) = notifications.recv().await {
491 // Handle events as they arrive
492}
493
494// ❌ Bad for live updates - polling
495loop {
496 let events = client.fetch_events(filter, timeout).await?;
497 tokio::time::sleep(Duration::from_secs(1)).await;
498}
499```
500
501---
502
503### 3. Batch Event Creation
504
505```rust
506// ✅ Good - reuse keys
507let keys = Keys::generate();
508let events: Vec<Event> = (0..100)
509 .map(|i| {
510 EventBuilder::new(Kind::TextNote, format!("Message {}", i))
511 .sign_with_keys(&keys)
512 .unwrap()
513 })
514 .collect();
515
516// ❌ Bad - regenerate keys
517let events: Vec<Event> = (0..100)
518 .map(|i| {
519 let keys = Keys::generate(); // Wasteful!
520 EventBuilder::new(Kind::TextNote, format!("Message {}", i))
521 .sign_with_keys(&keys)
522 .unwrap()
523 })
524 .collect();
525```
526
527---
528
529## Migration Checklist (0.35 → 0.43)
530
531When upgrading from 0.35 to 0.43:
532
533- [ ] Update `Cargo.toml`: `nostr-sdk = "0.43"`
534- [ ] Fix `EventBuilder::new()` - remove tags parameter
535- [ ] Fix `EventBuilder::to_event()` → `sign_with_keys()`
536- [ ] Fix `Client::new()` - clone keys instead of reference
537- [ ] Fix `Relay::is_connected()` - remove `.await`
538- [ ] Fix `Client::get_events_of()` → `fetch_events()`
539- [ ] Remove `EventSource::relays()` usage
540- [ ] Fix `Filter::custom_tag()` - single value instead of array
541- [ ] Fix `Client::send_event()` - pass reference, handle `SendEventOutput`
542- [ ] Update tests
543- [ ] Verify all builds pass
544- [ ] Run integration tests
545
546**Reference:** See `docs/archive/2025-11-04-nostr-sdk-upgrade.md`
547
548---
549
550## Useful Resources
551
552- **nostr-sdk docs**: https://docs.rs/nostr-sdk/0.43.0
553- **rust-nostr GitHub**: https://github.com/rust-nostr/nostr
554- **NIPs**: https://github.com/nostr-protocol/nips
555- **NIP-01 (Events)**: https://github.com/nostr-protocol/nips/blob/master/01.md
556- **NIP-34 (Git)**: https://github.com/nostr-protocol/nips/blob/master/34.md
557
558---
559
560## Quick Reference
561
562| Task | Code |
563|------|------|
564| Create event | `EventBuilder::new(kind, content).sign_with_keys(&keys)?` |
565| Add tags | `.tags(vec![tag1, tag2])` |
566| Custom tag | `Tag::custom(TagKind::SingleLetter(t), vec!["value"])` |
567| Create client | `Client::new(keys.clone())` |
568| Add relay | `client.add_relay("wss://...").await?` |
569| Connect | `client.connect().await` |
570| Send event | `client.send_event(&event).await?` |
571| Query events | `client.fetch_events(filter, timeout).await?` |
572| Subscribe | `client.subscribe(filters, None).await?` |
573
574---
575
576*Last updated: November 4, 2025*
577*Status: Living document - update as nostr-sdk evolves*