# nostr-sdk - Learnings and Patterns **Purpose:** Document nostr-sdk usage patterns, upgrade notes, and gotchas **Last Updated:** November 4, 2025 --- ## Current Version **We use nostr-sdk 0.43.x (latest stable)** ```toml [dependencies] nostr-sdk = "0.43" ``` **Upgraded from:** 0.35.0 on November 4, 2025 --- ## Critical Breaking Changes (0.35 → 0.43) ### 1. EventBuilder API Changed **Before (0.35):** ```rust let event = EventBuilder::new(kind, content, tags) .to_event(keys)?; ``` **After (0.43):** ```rust let event = EventBuilder::new(kind, content) .tags(tags) .sign_with_keys(keys)?; ``` **Changes:** - ❌ Removed `tags` parameter from constructor - ✅ Use `.tags()` builder method instead - ❌ Removed `.to_event()` method - ✅ Use `.sign_with_keys()` instead (more descriptive) --- ### 2. Client Ownership of Keys **Before (0.35):** ```rust let keys = Keys::generate(); let client = Client::new(&keys); // Reference // keys still available ``` **After (0.43):** ```rust let keys = Keys::generate(); let client = Client::new(keys.clone()); // Ownership // Need to clone if we want to keep keys ``` **Why:** Allows Client to own the signer, enabling more flexible signer types. --- ### 3. Relay Status Check No Longer Async **Before (0.35):** ```rust if relay.is_connected().await { // ... } ``` **After (0.43):** ```rust if relay.is_connected() { // No await! // ... } ``` **Why:** Status check doesn't require async operation. --- ### 4. Query API Redesigned **Before (0.35):** ```rust let events = client .get_events_of(vec![filter], EventSource::relays(Some(timeout))) .await?; // Returns Vec ``` **After (0.43):** ```rust let events = client .fetch_events(filter, timeout) .await?; // Returns Events (iterable collection) // Convert to Vec if needed let vec: Vec = events.into_iter().collect(); ``` **Changes:** - ❌ Removed `get_events_of()` method - ✅ Use `fetch_events()` instead - ❌ Removed `EventSource` parameter (confusing) - ✅ Direct timeout parameter - ❌ Single filter instead of `Vec` - ✅ Returns `Events` type instead of `Vec` --- ### 5. Filter Custom Tags Simplified **Before (0.35):** ```rust filter.custom_tag(tag, ["value"]) filter.custom_tag(tag, [&string_ref]) ``` **After (0.43):** ```rust filter.custom_tag(tag, "value") filter.custom_tag(tag, &string_ref) ``` **Why:** Simplified API for the common case of single tag value. --- ### 6. Send Event Takes Reference **Before (0.35):** ```rust let event_id = client.send_event(event).await?; ``` **After (0.43):** ```rust let output = client.send_event(&event).await?; let event_id = *output.id(); ``` **Changes:** - Takes `&Event` instead of `Event` (can reuse events) - Returns `SendEventOutput` instead of `EventId` - Need to call `.id()` to get the event ID --- ## Common Patterns ### Creating and Signing Events ```rust use nostr_sdk::prelude::*; // Generate keys let keys = Keys::generate(); // Create event let event = EventBuilder::new(Kind::TextNote, "Hello Nostr!") .tags(vec![ Tag::custom(TagKind::SingleLetter(SingleLetterTag::lowercase(Alphabet::T)), vec!["nostr"]), ]) .sign_with_keys(&keys)?; // Send event let output = client.send_event(&event).await?; println!("Event ID: {}", output.id()); ``` --- ### Creating Custom Tags ```rust use nostr_sdk::prelude::*; // Single letter tag (like "t" for topics) let t_tag = SingleLetterTag::lowercase(Alphabet::T); let tag = Tag::custom( TagKind::SingleLetter(t_tag), vec!["my-topic"] ); // Custom multi-letter tag let tag = Tag::custom( TagKind::Custom("custom-tag".to_string()), vec!["value1", "value2"] ); // Hashtag (convenience method) let tag = Tag::hashtag("nostr"); // Creates ["t", "nostr"] ``` --- ### Querying Events ```rust use nostr_sdk::prelude::*; // Build filter let filter = Filter::new() .kind(Kind::TextNote) .custom_tag( SingleLetterTag::lowercase(Alphabet::T), "my-topic" ) .since(Timestamp::now() - Duration::from_secs(3600)); // Last hour // Query events let timeout = Duration::from_secs(10); let events = client.fetch_events(filter, timeout).await?; // Process events for event in events.into_iter() { println!("Event: {}", event.id()); } ``` --- ### Multiple Filters Since `fetch_events()` takes a single filter, combine multiple queries: ```rust // Option 1: Fetch separately and combine let mut all_events = Vec::new(); for filter in filters { let events = client.fetch_events(filter, timeout).await?; all_events.extend(events.into_iter()); } // Option 2: Use subscription (more efficient) let subscription_id = SubscriptionId::new("my-sub"); client.subscribe(filters, None).await?; // Handle events via notification handler let mut notifications = client.notifications(); while let Ok(notification) = notifications.recv().await { if let RelayPoolNotification::Event { event, .. } = notification { println!("Event: {}", event.id()); } } ``` --- ### Client Setup with Relay ```rust use nostr_sdk::prelude::*; // Create keys let keys = Keys::generate(); // Create client let client = Client::new(keys.clone()); // Add relay client.add_relay("wss://relay.example.com").await?; // Connect client.connect().await; // Wait for connection tokio::time::sleep(Duration::from_secs(2)).await; // Check connection if client.relay("wss://relay.example.com") .await? .is_connected() { println!("Connected!"); } ``` --- ## Testing Patterns ### Unit Tests (No Relay Required) ```rust #[cfg(test)] mod tests { use super::*; use nostr_sdk::prelude::*; #[test] fn test_event_creation() { let keys = Keys::generate(); let event = EventBuilder::new(Kind::TextNote, "test") .sign_with_keys(&keys) .unwrap(); assert_eq!(event.kind(), Kind::TextNote); assert_eq!(event.content(), "test"); } #[test] fn test_tag_creation() { let t_tag = SingleLetterTag::lowercase(Alphabet::T); let tag = Tag::custom( TagKind::SingleLetter(t_tag), vec!["test-topic"] ); // Verify tag structure assert_eq!(tag.as_vec()[0], "t"); assert_eq!(tag.as_vec()[1], "test-topic"); } } ``` --- ### Integration Tests (Relay Required) ```rust #[cfg(test)] mod tests { use super::*; use nostr_sdk::prelude::*; #[tokio::test] #[ignore] // Requires running relay async fn test_send_and_receive() -> Result<()> { // Setup let keys = Keys::generate(); let client = Client::new(keys.clone()); client.add_relay("ws://localhost:7000").await?; client.connect().await; tokio::time::sleep(Duration::from_secs(2)).await; // Send event let event = EventBuilder::new(Kind::TextNote, "test") .sign_with_keys(&keys)?; let output = client.send_event(&event).await?; // Query it back let filter = Filter::new() .id(*output.id()); let events = client.fetch_events(filter, Duration::from_secs(5)).await?; assert_eq!(events.len(), 1); Ok(()) } } ``` **Running integration tests:** ```bash # Start relay first docker run --rm -p 7000:7000 scsibug/nostr-rs-relay # Run tests cargo test -- --ignored ``` --- ## Common Gotchas ### 1. Event Validation Failures **Problem:** Events fail validation with cryptic errors **Common Causes:** - Invalid signature (wrong keys used) - Invalid event ID (content/tags changed after signing) - Invalid timestamp (too far in future/past) **Solution:** ```rust // Always sign AFTER setting all fields let event = EventBuilder::new(kind, content) .tags(tags) // Set tags first .sign_with_keys(&keys)?; // Sign last // Don't modify event after signing! ``` --- ### 2. Filter Not Matching Events **Problem:** Query returns no events even though they exist **Common Causes:** - Tag kind mismatch (uppercase vs lowercase) - Wrong filter field (using `.author()` when you need `.authors()`) - Timeout too short **Solution:** ```rust // Be explicit about tag kinds let t_tag = SingleLetterTag::lowercase(Alphabet::T); // Lowercase! // Use correct filter methods let filter = Filter::new() .authors(vec![keys.public_key()]) // Note: plural .kinds(vec![Kind::TextNote]); // Note: plural // Increase timeout for slow relays let timeout = Duration::from_secs(10); ``` --- ### 3. Connection Timing Issues **Problem:** Events fail to send or queries return empty **Cause:** Client not fully connected to relay **Solution:** ```rust // Connect client.connect().await; // Wait for connection to establish tokio::time::sleep(Duration::from_secs(2)).await; // Verify connection let relay = client.relay("wss://relay.example.com").await?; if !relay.is_connected() { return Err("Not connected".into()); } // Now safe to send/query ``` --- ### 4. Clone Keys When Creating Client **Problem:** Can't use keys after creating client **Cause:** Client takes ownership in 0.43+ **Solution:** ```rust // Clone keys if you need them later let keys = Keys::generate(); let client = Client::new(keys.clone()); // Clone! // Now can still use keys let pubkey = keys.public_key(); ``` --- ## Performance Tips ### 1. Reuse Clients ```rust // ✅ Good - single client let client = Client::new(keys); client.add_relay("wss://relay1.com").await?; client.add_relay("wss://relay2.com").await?; client.connect().await; // ❌ Bad - multiple clients for relay in relays { let client = Client::new(keys.clone()); // Wasteful! client.add_relay(relay).await?; } ``` --- ### 2. Use Subscriptions for Live Updates ```rust // ✅ Good for live updates - subscription let filters = vec![Filter::new().kind(Kind::TextNote)]; client.subscribe(filters, None).await?; let mut notifications = client.notifications(); while let Ok(notification) = notifications.recv().await { // Handle events as they arrive } // ❌ Bad for live updates - polling loop { let events = client.fetch_events(filter, timeout).await?; tokio::time::sleep(Duration::from_secs(1)).await; } ``` --- ### 3. Batch Event Creation ```rust // ✅ Good - reuse keys let keys = Keys::generate(); let events: Vec = (0..100) .map(|i| { EventBuilder::new(Kind::TextNote, format!("Message {}", i)) .sign_with_keys(&keys) .unwrap() }) .collect(); // ❌ Bad - regenerate keys let events: Vec = (0..100) .map(|i| { let keys = Keys::generate(); // Wasteful! EventBuilder::new(Kind::TextNote, format!("Message {}", i)) .sign_with_keys(&keys) .unwrap() }) .collect(); ``` --- ## Migration Checklist (0.35 → 0.43) When upgrading from 0.35 to 0.43: - [ ] Update `Cargo.toml`: `nostr-sdk = "0.43"` - [ ] Fix `EventBuilder::new()` - remove tags parameter - [ ] Fix `EventBuilder::to_event()` → `sign_with_keys()` - [ ] Fix `Client::new()` - clone keys instead of reference - [ ] Fix `Relay::is_connected()` - remove `.await` - [ ] Fix `Client::get_events_of()` → `fetch_events()` - [ ] Remove `EventSource::relays()` usage - [ ] Fix `Filter::custom_tag()` - single value instead of array - [ ] Fix `Client::send_event()` - pass reference, handle `SendEventOutput` - [ ] Update tests - [ ] Verify all builds pass - [ ] Run integration tests **Reference:** See `docs/archive/2025-11-04-nostr-sdk-upgrade.md` --- ## Useful Resources - **nostr-sdk docs**: https://docs.rs/nostr-sdk/0.43.0 - **rust-nostr GitHub**: https://github.com/rust-nostr/nostr - **NIPs**: https://github.com/nostr-protocol/nips - **NIP-01 (Events)**: https://github.com/nostr-protocol/nips/blob/master/01.md - **NIP-34 (Git)**: https://github.com/nostr-protocol/nips/blob/master/34.md --- ## Quick Reference | Task | Code | |------|------| | Create event | `EventBuilder::new(kind, content).sign_with_keys(&keys)?` | | Add tags | `.tags(vec![tag1, tag2])` | | Custom tag | `Tag::custom(TagKind::SingleLetter(t), vec!["value"])` | | Create client | `Client::new(keys.clone())` | | Add relay | `client.add_relay("wss://...").await?` | | Connect | `client.connect().await` | | Send event | `client.send_event(&event).await?` | | Query events | `client.fetch_events(filter, timeout).await?` | | Subscribe | `client.subscribe(filters, None).await?` | --- *Last updated: November 4, 2025* *Status: Living document - update as nostr-sdk evolves*