// End-to-end Nostr exchange test against the live Goblin relay. // // Proves the NIP-17 payment-message path: gift-wrap send, subscribe, unwrap, // seal-author verification, subject tag, and Goblin slatepack extraction. // Network-dependent — run explicitly: // cargo test --test nostr_e2e -- --ignored --nocapture use std::time::Duration; use grim::nostr::protocol; use nostr_sdk::prelude::*; const RELAY: &str = "wss://nrelay.us-ea.st"; /// A small but valid-looking slatepack armor block for extraction testing. const SLATEPACK: &str = "BEGINSLATEPACK. 4H1qx1wHe668tFW yC2gfL8PPd8kSgv \ pcXQhyRkHbyKHZg GN75o7uWoT3dkib R2tj1fFGN2FoRLY oeBPyKizupksgRT \ dXFdjEuMUuktR5r gCiVBSXcHSWW3KW Y56LTQ9z3QwUWmE 8sRtwR9Bn8oNN5K. \ ENDSLATEPACK."; #[tokio::test] #[ignore] async fn nip17_slatepack_roundtrip() { let alice = Keys::generate(); let bob = Keys::generate(); println!("alice: {}", alice.public_key().to_bech32().unwrap()); println!("bob: {}", bob.public_key().to_bech32().unwrap()); // Bob's client: connect, advertise DM relays, subscribe to gift wraps. let bob_client = Client::new(bob.clone()); bob_client.add_relay(RELAY).await.unwrap(); bob_client.connect().await; tokio::time::sleep(Duration::from_secs(2)).await; // Publish Bob's kind-10050 DM relay list so senders find this relay. let dm_relays = EventBuilder::new(Kind::InboxRelays, "") .tag(Tag::custom(TagKind::custom("relay"), [RELAY.to_string()])); bob_client.send_event_builder(dm_relays).await.unwrap(); let filter = Filter::new() .kind(Kind::GiftWrap) .pubkey(bob.public_key()) .since(Timestamp::now() - Duration::from_secs(3 * 86_400)); bob_client.subscribe(filter, None).await.unwrap(); // Alice's client: connect and send a NIP-17 payment DM to Bob. let alice_client = Client::new(alice.clone()); alice_client.add_relay(RELAY).await.unwrap(); alice_client.connect().await; tokio::time::sleep(Duration::from_secs(2)).await; let content = protocol::build_payment_content(SLATEPACK); let tags = protocol::build_rumor_tags(Some("lunch :)")); alice_client .send_private_msg_to([RELAY], bob.public_key(), content, tags) .await .unwrap(); println!("alice sent gift-wrapped payment DM"); // Bob waits for the gift wrap, unwraps and validates it. let mut notifications = bob_client.notifications(); let result = tokio::time::timeout(Duration::from_secs(30), async { loop { if let Ok(RelayPoolNotification::Event { event, .. }) = notifications.recv().await { if event.kind != Kind::GiftWrap { continue; } let unwrapped = match bob_client.unwrap_gift_wrap(&event).await { Ok(u) => u, Err(_) => continue, }; // Seal-author check (the NIP-17 invariant our ingest enforces). assert_eq!( unwrapped.rumor.pubkey, unwrapped.sender, "rumor author must equal seal signer" ); assert_eq!(unwrapped.sender, alice.public_key(), "sender must be Alice"); assert_eq!(unwrapped.rumor.kind, Kind::PrivateDirectMessage); return unwrapped; } } }) .await .expect("timed out waiting for gift wrap"); // The slatepack must round-trip intact, and the subject must survive. let armor = protocol::extract_slatepack(&result.rumor.content) .expect("slatepack must extract from rumor"); assert!(armor.starts_with("BEGINSLATEPACK.")); assert!(armor.ends_with("ENDSLATEPACK.")); let subject = protocol::extract_subject(&result.rumor.tags); assert_eq!(subject.as_deref(), Some("lunch :)")); println!("✓ NIP-17 slatepack roundtrip verified over {RELAY}"); bob_client.disconnect().await; alice_client.disconnect().await; } /// Register a fresh name on goblin.st with a real NIP-98 signature, then /// resolve it back — proves the live identity server end-to-end. /// Run: cargo test --test nostr_e2e nip05 -- --ignored --nocapture #[tokio::test] #[ignore] async fn nip05_registration_roundtrip() { use base64::Engine; use sha2::{Digest, Sha256}; use std::process::Command; let keys = Keys::generate(); let pubkey = keys.public_key().to_hex(); // Unique-ish name from the pubkey suffix (lowercase alnum). let name = format!("t{}", &pubkey[..8]); let server = "https://goblin.st"; let url = format!("{server}/api/v1/register"); let body = serde_json::json!({ "name": name, "pubkey": pubkey }).to_string(); // Build the NIP-98 kind-27235 auth event (same shape as the client). let payload_hash = hex::encode(Sha256::digest(body.as_bytes())); let event = EventBuilder::new(Kind::HttpAuth, "") .tag(Tag::custom(TagKind::custom("u"), [url.clone()])) .tag(Tag::custom(TagKind::custom("method"), ["POST".to_string()])) .tag(Tag::custom(TagKind::custom("payload"), [payload_hash])) .sign_with_keys(&keys) .unwrap(); let auth = format!( "Nostr {}", base64::engine::general_purpose::STANDARD.encode(event.as_json()) ); // POST the registration via curl (avoids pulling an HTTP client dep). let out = Command::new("curl") .args([ "-s", "-X", "POST", &url, "-H", &format!("Authorization: {auth}"), "-H", "Content-Type: application/json", "-d", &body, ]) .output() .expect("curl register"); let resp = String::from_utf8_lossy(&out.stdout); println!("register response: {resp}"); assert!( resp.contains("\"nip05\""), "registration should return a nip05 identifier, got: {resp}" ); assert!(resp.contains(&format!("{name}@goblin.st"))); // Resolve it back from the well-known endpoint. let wk = Command::new("curl") .args([ "-s", &format!("{server}/.well-known/nostr.json?name={name}"), ]) .output() .expect("curl well-known"); let wk_body = String::from_utf8_lossy(&wk.stdout); println!("well-known response: {wk_body}"); let resolved = protocol_parse_pubkey(&wk_body, &name); assert_eq!(resolved.as_deref(), Some(pubkey.as_str())); // Clean up: release the test name. let del_url = format!("{server}/api/v1/register/{name}"); let del_event = EventBuilder::new(Kind::HttpAuth, "") .tag(Tag::custom(TagKind::custom("u"), [del_url.clone()])) .tag(Tag::custom( TagKind::custom("method"), ["DELETE".to_string()], )) .sign_with_keys(&keys) .unwrap(); let del_auth = format!( "Nostr {}", base64::engine::general_purpose::STANDARD.encode(del_event.as_json()) ); let _ = Command::new("curl") .args([ "-s", "-X", "DELETE", &del_url, "-H", &format!("Authorization: {del_auth}"), ]) .output(); println!("✓ NIP-05 registration + resolution verified on {server}"); } /// Minimal well-known pubkey extractor for the test. fn protocol_parse_pubkey(body: &str, name: &str) -> Option { let doc: serde_json::Value = serde_json::from_str(body).ok()?; doc.get("names")?.get(name)?.as_str().map(|s| s.to_string()) }