1
0
forked from GRIN/grim

Add live e2e tests for the nostr relay and NIP-05 server

tests/nostr_e2e.rs (run with --ignored):
- nip17_slatepack_roundtrip: Alice gift-wraps a slatepack payment DM with a
  subject to Bob over wss://nrelay.us-ea.st; Bob unwraps, the seal-author ==
  sender invariant holds, the slatepack and subject extract intact.
- nip05_registration_roundtrip: registers a fresh name on goblin.st with a
  real NIP-98 signature, resolves it back to the right pubkey, releases it.

Both pass against the live deployed infrastructure.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Claude
2026-06-10 01:54:37 -04:00
parent 8a6d442544
commit 87efc8bb2d
4 changed files with 208 additions and 6 deletions
+7
View File
@@ -150,3 +150,10 @@ eframe = { version = "0.33.2", default-features = false, features = ["glow", "an
[build-dependencies]
built = "0.8.0"
[dev-dependencies]
nostr-sdk = { version = "0.44", features = ["nip06", "nip44", "nip49", "nip59", "nip98"] }
tokio = { version = "1.49.0", features = ["full"] }
base64 = "0.22"
sha2 = "0.10"
hex = "0.4"
-4
View File
@@ -24,7 +24,6 @@ use egui::{Align, Layout, Margin, RichText, ScrollArea, Sense, Vec2};
use crate::gui::icons::{CLOCK, COPY, SCAN, USER_CIRCLE, WALLET};
use crate::gui::platform::PlatformCallbacks;
use crate::gui::theme::{self, fonts};
use crate::gui::views::wallets::wallet::WalletTransactionsContent;
use crate::gui::views::{Content, View};
use crate::wallet::Wallet;
use crate::wallet::types::WalletData;
@@ -46,8 +45,6 @@ pub enum Tab {
pub struct GoblinWalletView {
tab: Tab,
send: Option<SendFlow>,
/// Legacy transactions content reused for detail rendering when needed.
txs_content: WalletTransactionsContent,
}
impl Default for GoblinWalletView {
@@ -55,7 +52,6 @@ impl Default for GoblinWalletView {
Self {
tab: Tab::Home,
send: None,
txs_content: WalletTransactionsContent::new(None),
}
}
}
+2 -2
View File
@@ -30,10 +30,10 @@ pub use store::NostrStore;
mod identity;
pub use identity::{IdentitySource, NostrIdentity};
mod protocol;
pub mod protocol;
pub use protocol::*;
mod ingest;
pub mod ingest;
pub use ingest::*;
mod client;
+199
View File
@@ -0,0 +1,199 @@
// 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<String> {
let doc: serde_json::Value = serde_json::from_str(body).ok()?;
doc.get("names")?.get(name)?.as_str().map(|s| s.to_string())
}