From 22557f15d6a7b77f72d4597fc05aa06346495a33 Mon Sep 17 00:00:00 2001 From: DanConwayDev Date: Tue, 4 Nov 2025 09:31:57 +0000 Subject: docs: major cleanup and reorganization MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 --- docs/learnings/grasp-audit.md | 498 ++++++++++++++++++++++++++++++++++++ docs/learnings/nix-flakes.md | 423 +++++++++++++++++++++++++++++++ docs/learnings/nostr-sdk.md | 577 ++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 1498 insertions(+) create mode 100644 docs/learnings/grasp-audit.md create mode 100644 docs/learnings/nix-flakes.md create mode 100644 docs/learnings/nostr-sdk.md (limited to 'docs/learnings') diff --git a/docs/learnings/grasp-audit.md b/docs/learnings/grasp-audit.md new file mode 100644 index 0000000..531ebda --- /dev/null +++ b/docs/learnings/grasp-audit.md @@ -0,0 +1,498 @@ +# GRASP Audit Tool - Patterns and Learnings + +**Purpose:** Document grasp-audit architecture, patterns, and lessons learned +**Last Updated:** November 4, 2025 + +--- + +## Overview + +`grasp-audit` is a compliance testing tool for GRASP (Git Relays Authorized via Signed-Nostr Proofs) protocol implementations. It tests both Nostr relay compliance (NIP-01) and GRASP-specific functionality. + +--- + +## Architecture Decisions + +### Separate Crate Strategy + +**Decision:** Build `grasp-audit` as a separate crate from `ngit-grasp` + +**Why:** +1. **Parallel Development**: Can build tests before implementation +2. **Isolated Testing**: Tests run in isolation (CI/CD safe) +3. **Production Auditing**: Can audit live production services +4. **Reusability**: Other GRASP implementations can use it + +**Location:** `grasp-audit/` subdirectory with own `Cargo.toml` and `flake.nix` + +--- + +### Audit Event Tagging Strategy + +**Problem:** Test events pollute the relay and need cleanup without deletion events. + +**Solution:** Use special tags to mark audit events: + +```rust +// Every audit event includes these tags +[ + ["t", "grasp-audit-test-event"], // Marker + ["t", "audit-{run-id}"], // Run isolation + ["t", "audit-cleanup-after-{timestamp}"] // Cleanup time +] +``` + +**Benefits:** +- ✅ **Queryable**: Can find all audit events via tag filter +- ✅ **Isolated**: Each test run has unique run ID +- ✅ **Self-cleaning**: Cleanup timestamp indicates when to delete +- ✅ **No deletion events**: Direct database cleanup, no KIND 5 events +- ✅ **Production safe**: Won't interfere with real events + +**Reference:** See `docs/archive/2025-11-04-tag-migration.md` + +--- + +### Standard "t" Tags vs Custom Tags + +**Evolution:** +1. **Original**: Custom single-letter tags (`g`, `r`, `c`) +2. **Current**: Standard NIP-01 "t" tags with prefixed values + +**Why we changed:** +- ❌ Custom tags could conflict with other systems +- ✅ "t" tag is standard for categorization/topics +- ✅ Multiple "t" tags are expected and supported +- ✅ Self-documenting values (`audit-{run-id}` vs just `{run-id}`) +- ✅ Better namespacing with prefixes + +**Migration:** Completed November 4, 2025 + +--- + +## Code Patterns + +### Audit Configuration + +```rust +use grasp_audit::audit::AuditConfig; + +// CI mode - isolated test runs +let config = AuditConfig::ci(); +// Generates UUID run ID: "ci-{uuid}" +// Cleanup after 1 hour + +// Production mode - persistent run ID +let config = AuditConfig::production("prod-server-1"); +// Uses provided run ID +// Cleanup after 24 hours +``` + +**When to use:** +- **CI mode**: Automated testing, parallel runs, temporary +- **Production mode**: Manual audits, monitoring, persistent + +--- + +### Creating Audit Events + +```rust +use grasp_audit::audit::{AuditConfig, AuditEventBuilder}; +use nostr_sdk::prelude::*; + +let config = AuditConfig::ci(); +let keys = Keys::generate(); + +// Create audit event +let event = AuditEventBuilder::new(&config, Kind::TextNote, "test content") + .build(&keys)?; + +// Event automatically includes: +// - Audit marker tag +// - Run ID tag +// - Cleanup timestamp tag +``` + +--- + +### Querying Audit Events + +```rust +use grasp_audit::client::AuditClient; +use grasp_audit::audit::AuditConfig; + +let config = AuditConfig::ci(); +let client = AuditClient::new(config, keys); + +// Connect to relay +client.add_relay("ws://localhost:7000").await?; +client.connect().await; + +// Query audit events for this run +let events = client.query().await?; + +// Events are filtered by: +// - "grasp-audit-test-event" marker +// - Current run ID +``` + +--- + +### Test Isolation + +**Each test run is isolated by unique run ID:** + +```rust +// CI mode generates unique UUID per run +let config1 = AuditConfig::ci(); +let config2 = AuditConfig::ci(); + +// config1.run_id != config2.run_id +// Tests won't interfere with each other +``` + +**Benefits:** +- ✅ Parallel CI/CD runs don't conflict +- ✅ Can run multiple test suites simultaneously +- ✅ Easy to identify which run created which events +- ✅ Cleanup can target specific runs + +--- + +### Cleanup Strategy + +**Two-phase cleanup:** + +1. **Automatic expiry** via cleanup timestamp tag +2. **Manual cleanup** by querying and deleting + +```rust +// Events include cleanup timestamp +["t", "audit-cleanup-after-1730707200"] + +// Cleanup process: +// 1. Query events with expired cleanup timestamp +// 2. Delete from database directly (no KIND 5) +// 3. Avoid deletion event pollution +``` + +**Implementation:** To be built in relay (not in audit tool) + +--- + +## Testing Strategy + +### Test Organization + +``` +grasp-audit/src/specs/ +├── nip01_smoke.rs # NIP-01 basic functionality +├── grasp_01_relay.rs # GRASP-01 relay requirements (planned) +└── mod.rs # Test suite registry +``` + +### Unit vs Integration Tests + +**Unit Tests** (no relay required): +```rust +#[cfg(test)] +mod tests { + #[test] + fn test_audit_config() { + let config = AuditConfig::ci(); + assert!(config.run_id.starts_with("ci-")); + } +} +``` + +**Integration Tests** (relay required): +```rust +#[cfg(test)] +mod tests { + #[tokio::test] + #[ignore] // Requires relay + async fn test_smoke_tests_against_relay() { + // Test against real relay + } +} +``` + +**Running tests:** +```bash +# Unit tests (fast, no dependencies) +cargo test --lib + +# Integration tests (requires relay) +docker run --rm -p 7000:7000 scsibug/nostr-rs-relay +cargo test -- --ignored +``` + +--- + +### Test Result Reporting + +```rust +use grasp_audit::result::AuditResult; + +// Run tests +let results = vec![ + AuditResult::pass("websocket_connection", "Connected successfully"), + AuditResult::fail("invalid_event", "Expected rejection, got acceptance"), +]; + +// Report +for result in &results { + println!("{}", result); +} + +// Summary +let passed = results.iter().filter(|r| r.is_pass()).count(); +let total = results.len(); +println!("Results: {}/{} passed ({:.1}%)", + passed, total, (passed as f64 / total as f64) * 100.0); +``` + +--- + +## CLI Design + +### Command Structure + +```bash +grasp-audit audit [OPTIONS] + +Options: + --relay Relay to test (required) + --mode ci or production (default: ci) + --run-id Custom run ID (production mode only) + --spec Test spec to run (default: all) + --verbose Detailed output +``` + +### Usage Examples + +```bash +# CI mode - quick smoke test +grasp-audit audit \ + --relay ws://localhost:7000 \ + --mode ci \ + --spec nip01-smoke + +# Production mode - full compliance audit +grasp-audit audit \ + --relay wss://relay.example.com \ + --mode production \ + --run-id "audit-2025-11-04" \ + --verbose + +# Test all specs +grasp-audit audit --relay ws://localhost:7000 +``` + +--- + +## Lessons Learned + +### 1. Tag Migration is Breaking + +**Lesson:** Changing tag structure breaks event queries. + +**Impact:** Events created with old tags won't be found by new queries. + +**Mitigation:** +- ✅ Accept breaking changes in alpha stage +- ✅ Document migration clearly +- ✅ Old events auto-expire via cleanup +- ✅ No production deployments affected + +**Reference:** `docs/archive/2025-11-04-tag-migration.md` + +--- + +### 2. Test Data Lifecycle Matters + +**Lesson:** Test events accumulate and pollute relay. + +**Solution:** Built-in cleanup strategy from day one. + +**Implementation:** +- Every event has cleanup timestamp +- Relay can cleanup expired events +- No deletion event pollution (direct DB cleanup) + +--- + +### 3. Isolation Enables Parallel Testing + +**Lesson:** Unique run IDs enable parallel test execution. + +**Benefit:** CI/CD can run multiple test suites simultaneously. + +**Pattern:** +```rust +// Each CI run gets unique ID +let config = AuditConfig::ci(); +// run_id = "ci-{uuid}" + +// Tests isolated by run ID +let events = client.query().await?; +// Only returns events for this run +``` + +--- + +### 4. Standards Compliance Reduces Friction + +**Lesson:** Using standard NIP-01 "t" tags instead of custom tags. + +**Benefits:** +- ✅ No conflicts with other systems +- ✅ Standard relay filtering works +- ✅ Better interoperability +- ✅ Self-documenting + +--- + +## Future Enhancements + +### Planned Features + +- [ ] **GRASP-01 Test Suite**: Repository announcement and state event tests +- [ ] **Test Report Generation**: JSON/HTML output for CI/CD +- [ ] **Performance Benchmarks**: Measure relay performance +- [ ] **Relay Comparison**: Side-by-side compliance comparison +- [ ] **Continuous Monitoring**: Periodic production audits + +--- + +### Possible Improvements + +- [ ] **Parallel Test Execution**: Run specs in parallel +- [ ] **Retry Logic**: Handle transient failures +- [ ] **Custom Assertions**: Domain-specific test helpers +- [ ] **Event Diff Tool**: Compare expected vs actual events +- [ ] **Cleanup Automation**: Auto-cleanup after tests + +--- + +## Common Issues + +### Issue: Integration Tests Fail + +**Symptoms:** Tests timeout or fail to connect + +**Causes:** +1. No relay running +2. Wrong relay URL +3. Firewall blocking connection + +**Solution:** +```bash +# Start relay +docker run --rm -p 7000:7000 scsibug/nostr-rs-relay + +# Verify relay is running +curl http://localhost:7000 + +# Run tests +cargo test -- --ignored +``` + +--- + +### Issue: Events Not Found in Query + +**Symptoms:** Query returns empty even though events were sent + +**Causes:** +1. Wrong run ID (querying different run) +2. Connection timing (query before event propagated) +3. Tag mismatch (uppercase vs lowercase) + +**Solution:** +```rust +// Use same config for send and query +let config = AuditConfig::ci(); + +// Wait for event to propagate +tokio::time::sleep(Duration::from_millis(500)).await; + +// Verify tags match exactly +let t_tag = SingleLetterTag::lowercase(Alphabet::T); // Lowercase! +``` + +--- + +### Issue: Build Fails in CI + +**Symptoms:** `cargo build` fails with dependency errors + +**Cause:** Not in Nix dev environment + +**Solution:** +```bash +# Enter Nix environment first +cd grasp-audit +nix develop + +# Then build +cargo build +``` + +--- + +## Quick Reference + +### Configuration + +```rust +// CI mode +let config = AuditConfig::ci(); + +// Production mode +let config = AuditConfig::production("run-id"); +``` + +### Event Creation + +```rust +let event = AuditEventBuilder::new(&config, kind, content) + .build(&keys)?; +``` + +### Client Usage + +```rust +let client = AuditClient::new(config, keys); +client.add_relay("ws://localhost:7000").await?; +client.connect().await; +let events = client.query().await?; +``` + +### Running Tests + +```bash +# Unit tests +cargo test --lib + +# Integration tests +cargo test -- --ignored + +# CLI +cargo run -- audit --relay ws://localhost:7000 +``` + +--- + +## References + +- **GRASP Protocol**: https://gitworkshop.dev/danconwaydev.com/grasp +- **NIP-01**: https://github.com/nostr-protocol/nips/blob/master/01.md +- **NIP-34**: https://github.com/nostr-protocol/nips/blob/master/34.md +- **grasp-audit README**: `grasp-audit/README.md` +- **Tag Migration**: `docs/archive/2025-11-04-tag-migration.md` + +--- + +*Last updated: November 4, 2025* +*Status: Living document - update as grasp-audit evolves* diff --git a/docs/learnings/nix-flakes.md b/docs/learnings/nix-flakes.md new file mode 100644 index 0000000..6876647 --- /dev/null +++ b/docs/learnings/nix-flakes.md @@ -0,0 +1,423 @@ +# Nix Flakes - Learnings and Gotchas + +**Purpose:** Document Nix flake patterns, gotchas, and best practices learned during ngit-grasp development +**Last Updated:** November 4, 2025 + +--- + +## Critical Gotchas + +### Always Use `nix develop`, Not `nix-shell` + +**Problem:** We use `flake.nix`, not `shell.nix`. Using `nix-shell` will fail or use the wrong environment. + +```bash +# ✅ Correct - for flake.nix +cd grasp-audit +nix develop +nix develop -c cargo build + +# ❌ Wrong - for shell.nix (we don't use this) +nix-shell +nix-shell --run "cargo build" +``` + +**Why:** +- `nix-shell` looks for `shell.nix` or `default.nix` +- `nix develop` looks for `flake.nix` +- We migrated from `shell.nix` to `flake.nix` on November 4, 2025 + +**Related:** See `docs/archive/2025-11-04-flake-migration.md` + +--- + +## Flake Structure + +### Our Standard Flake Pattern + +```nix +{ + inputs = { + nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable"; + rust-overlay.url = "github:oxalica/rust-overlay"; + flake-utils.url = "github:numtide/flake-utils"; + }; + + outputs = { nixpkgs, rust-overlay, flake-utils, ... }: + flake-utils.lib.eachDefaultSystem (system: + let + overlays = [ (import rust-overlay) ]; + pkgs = import nixpkgs { inherit system overlays; }; + manifest = pkgs.lib.importTOML ./Cargo.toml; + in with pkgs; { + # Development shell + devShells.default = mkShell { + nativeBuildInputs = [ + rust-bin.stable.latest.default + pkg-config + gitlint + ]; + buildInputs = [ + openssl + ]; + shellHook = '' + echo "🦀 Development environment loaded" + export RUST_SRC_PATH=${pkgs.rustPlatform.rustLibSrc} + ''; + }; + + # Package output + packages.default = pkgs.rustPlatform.buildRustPackage { + pname = manifest.package.name; + version = manifest.package.version; + src = ./.; + cargoLock = { lockFile = ./Cargo.lock; }; + buildInputs = [ openssl ]; + nativeBuildInputs = [ pkg-config ]; + doCheck = false; # Run tests separately + }; + }); +} +``` + +### Key Components + +1. **rust-overlay**: Provides latest stable Rust toolchain +2. **flake-utils**: Cross-platform support helper +3. **manifest**: Auto-read version from Cargo.toml +4. **devShells.default**: Development environment +5. **packages.default**: Buildable package + +--- + +## Common Flake Commands + +### Essential Commands + +```bash +# Enter development shell +nix develop + +# Run command in dev shell (one-off) +nix develop -c cargo build + +# Show flake outputs +nix flake show + +# Check flake validity +nix flake check + +# Update flake inputs (like updating dependencies) +nix flake update + +# Build the package directly +nix build + +# Run without installing +nix run + +# Show flake metadata +nix flake metadata +``` + +### Debugging Commands + +```bash +# Show detailed evaluation trace +nix develop --show-trace + +# Print flake evaluation +nix eval .#devShells.x86_64-linux.default + +# Check what's in the store +nix path-info .#packages.x86_64-linux.default +``` + +--- + +## Subproject Flakes + +### grasp-audit Has Its Own Flake + +**Important:** `grasp-audit/` is a subproject with its own `flake.nix` and `Cargo.toml`. + +```bash +# ✅ Correct - enter grasp-audit environment +cd grasp-audit +nix develop +cargo build + +# ❌ Wrong - can't build from root +cd ngit-grasp +cargo build # This won't find grasp-audit dependencies +``` + +**Why:** +- Each Rust workspace needs its own Nix environment +- Dependencies are project-specific +- Flake inputs are locked per-project + +--- + +## Migration from shell.nix to flake.nix + +### What Changed + +**Before (shell.nix):** +```nix +{ pkgs ? import {} }: + +pkgs.mkShell { + buildInputs = with pkgs; [ + rustc + cargo + openssl + pkg-config + ]; +} +``` + +**After (flake.nix):** +- Locked inputs (reproducible) +- Multi-output (dev shell + package) +- Cross-platform by default +- Better tooling integration + +### Migration Steps + +1. Create `flake.nix` with standard structure +2. Run `nix flake check` to validate +3. Update all documentation: `nix-shell` → `nix develop` +4. Test that build works: `nix develop -c cargo build` +5. Remove `shell.nix` +6. Commit changes + +**Reference:** See `docs/archive/2025-11-04-flake-migration.md` + +--- + +## Benefits of Flakes + +### Reproducibility + +**Locked inputs** ensure everyone gets the same environment: + +```bash +# flake.lock contains exact commits +$ cat flake.lock +{ + "nodes": { + "nixpkgs": { + "locked": { + "lastModified": 1698611440, + "narHash": "sha256-jPjHjrerhYDy3q9+s5EAsuhyhuknNfowY6yt6pjn9pc=", + "rev": "23e89e0c8c5e2d9cf5b5e7c3e8e8e8e8e8e8e8e8" + } + } + } +} +``` + +Everyone running `nix develop` gets **exactly** this version of nixpkgs. + +### Multi-Output + +Single flake provides: +- **devShells.default**: Development environment +- **packages.default**: Buildable package +- **apps.default**: Runnable application (optional) + +### Composability + +Flakes can use other flakes as inputs: + +```nix +{ + inputs = { + grasp-audit.url = "path:./grasp-audit"; + }; +} +``` + +--- + +## Common Issues + +### Issue: "error: getting status of '/nix/store/...': No such file or directory" + +**Cause:** Flake inputs need to be updated or fetched + +**Solution:** +```bash +nix flake update +nix develop +``` + +### Issue: "error: experimental feature 'nix-command' is not enabled" + +**Cause:** Nix flakes are experimental and need to be enabled + +**Solution:** +Add to `~/.config/nix/nix.conf`: +``` +experimental-features = nix-command flakes +``` + +### Issue: Changes to flake.nix not taking effect + +**Cause:** Flake evaluation is cached + +**Solution:** +```bash +# Clear evaluation cache +nix flake update +# Or force re-evaluation +nix develop --refresh +``` + +### Issue: "error: cannot find flake 'flake:self' in the flake registries" + +**Cause:** Not in a git repository or flake.nix not committed + +**Solution:** +```bash +git add flake.nix flake.lock +git commit -m "Add flake" +``` + +**Note:** Flakes require git. Uncommitted files are ignored by default. + +--- + +## Best Practices + +### 1. Always Commit flake.lock + +```bash +git add flake.lock +git commit -m "Update flake inputs" +``` + +**Why:** Ensures reproducibility across machines and CI/CD + +### 2. Use Specific Rust Versions When Needed + +```nix +# Latest stable (default) +rust-bin.stable.latest.default + +# Specific version +rust-bin.stable."1.75.0".default + +# Nightly +rust-bin.nightly."2024-01-01".default +``` + +### 3. Include Helpful Shell Hooks + +```nix +shellHook = '' + echo "🦀 GRASP Audit development environment" + echo "" + echo "Common commands:" + echo " cargo build - Build project" + echo " cargo test - Run tests" + echo " cargo run - Run binary" + echo "" + export RUST_SRC_PATH=${pkgs.rustPlatform.rustLibSrc} +''; +``` + +### 4. Separate Build and Runtime Dependencies + +```nix +# Build-time only +nativeBuildInputs = [ + pkg-config + rustc + cargo +]; + +# Runtime needed +buildInputs = [ + openssl +]; +``` + +### 5. Disable Tests in Package Build + +```nix +packages.default = pkgs.rustPlatform.buildRustPackage { + # ... + doCheck = false; # Run tests separately with cargo test +}; +``` + +**Why:** Faster builds, tests run via `cargo test` in dev shell + +--- + +## Workflow Examples + +### Daily Development + +```bash +# Start work +cd grasp-audit +nix develop + +# Inside nix shell +cargo build +cargo test +cargo run -- --help + +# Exit shell +exit +``` + +### CI/CD + +```bash +# One-off commands (no interactive shell) +nix develop -c cargo build +nix develop -c cargo test --lib +nix develop -c cargo test -- --ignored +``` + +### Building Release + +```bash +# Build package directly +nix build + +# Result is in ./result/bin/ +./result/bin/grasp-audit --version +``` + +--- + +## References + +- **Nix Flakes Manual**: https://nixos.org/manual/nix/stable/command-ref/new-cli/nix3-flake.html +- **rust-overlay**: https://github.com/oxalica/rust-overlay +- **flake-utils**: https://github.com/numtide/flake-utils +- **Our Migration**: `docs/archive/2025-11-04-flake-migration.md` + +--- + +## Quick Reference + +| Task | Command | +|------|---------| +| Enter dev shell | `nix develop` | +| Run one command | `nix develop -c ` | +| Show outputs | `nix flake show` | +| Validate flake | `nix flake check` | +| Update inputs | `nix flake update` | +| Build package | `nix build` | +| Run package | `nix run` | + +--- + +*Last updated: November 4, 2025* +*Status: Living document - update as we learn more* 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 @@ +# 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* -- cgit v1.2.3