Files
DanConwayDev e4e36862f3 docs: remove obsolete archive and migration materials
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
2026-09-24 06:52:36 +00:00

576 lines
12 KiB
Markdown

# 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<Event>
```
**After (0.43):**
```rust
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 `EventSource` parameter (confusing)
- ✅ Direct timeout parameter
- ❌ Single filter instead of `Vec<Filter>`
- ✅ Returns `Events` type instead of `Vec<Event>`
---
### 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<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, handle `SendEventOutput`
- [ ] 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*