The ngit-relay migration is complete and archived operational reports and migration scripts are no longer needed. Remove the entire docs/archive directory and its documentation links, including older dangling references. Keep investigation, deployment-check, and soak notes in ignored work/ and update agent and work-directory instructions to prevent recreating the archive. Lasting product documentation belongs in the existing Diataxis directories. This changes no runtime behavior and does not rewrite published history. Validation: verified the directory is absent, checked tracked documentation and configuration for archive references, and ran git diff --check. Runtime tests are unnecessary for documentation and retired-script removal. Assisted-by: GPT-6
12 KiB
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)
[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):
let event = EventBuilder::new(kind, content, tags)
.to_event(keys)?;
After (0.43):
let event = EventBuilder::new(kind, content)
.tags(tags)
.sign_with_keys(keys)?;
Changes:
- ❌ Removed
tagsparameter 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):
let keys = Keys::generate();
let client = Client::new(&keys); // Reference
// keys still available
After (0.43):
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):
if relay.is_connected().await {
// ...
}
After (0.43):
if relay.is_connected() { // No await!
// ...
}
Why: Status check doesn't require async operation.
4. Query API Redesigned
Before (0.35):
let events = client
.get_events_of(vec![filter], EventSource::relays(Some(timeout)))
.await?;
// Returns Vec<Event>
After (0.43):
let events = client
.fetch_events(filter, timeout)
.await?;
// Returns Events (iterable collection)
// Convert to Vec if needed
let vec: Vec<Event> = events.into_iter().collect();
Changes:
- ❌ Removed
get_events_of()method - ✅ Use
fetch_events()instead - ❌ Removed
EventSourceparameter (confusing) - ✅ Direct timeout parameter
- ❌ Single filter instead of
Vec<Filter> - ✅ Returns
Eventstype instead ofVec<Event>
5. Filter Custom Tags Simplified
Before (0.35):
filter.custom_tag(tag, ["value"])
filter.custom_tag(tag, [&string_ref])
After (0.43):
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):
let event_id = client.send_event(event).await?;
After (0.43):
let output = client.send_event(&event).await?;
let event_id = *output.id();
Changes:
- Takes
&Eventinstead ofEvent(can reuse events) - Returns
SendEventOutputinstead ofEventId - Need to call
.id()to get the event ID
Common Patterns
Creating and Signing Events
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
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
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:
// 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
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)
#[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)
#[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:
# 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:
// 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:
// 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:
// 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:
// 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
// ✅ 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
// ✅ 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
// ✅ Good - reuse keys
let keys = Keys::generate();
let events: Vec<Event> = (0..100)
.map(|i| {
EventBuilder::new(Kind::TextNote, format!("Message {}", i))
.sign_with_keys(&keys)
.unwrap()
})
.collect();
// ❌ Bad - regenerate keys
let events: Vec<Event> = (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, handleSendEventOutput - Update tests
- Verify all builds pass
- Run integration tests
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