nip05.rs (3335B)
1 use std::collections::HashMap; 2 use std::sync::mpsc::{self, Receiver, Sender}; 3 use std::time::{Duration, Instant}; 4 5 use enostr::Pubkey; 6 7 const NIP05_TTL: Duration = Duration::from_secs(8 * 3600); // 8 hours 8 9 #[derive(Debug, Clone, PartialEq)] 10 pub enum Nip05Status { 11 Pending, 12 Valid, 13 Invalid, 14 } 15 16 struct CacheEntry { 17 status: Nip05Status, 18 checked_at: Instant, 19 } 20 21 struct Completion { 22 pubkey: Pubkey, 23 status: Nip05Status, 24 } 25 26 pub struct Nip05Cache { 27 cache: HashMap<Pubkey, CacheEntry>, 28 tx: Sender<Completion>, 29 rx: Receiver<Completion>, 30 } 31 32 impl Default for Nip05Cache { 33 fn default() -> Self { 34 Self::new() 35 } 36 } 37 38 impl Nip05Cache { 39 pub fn new() -> Self { 40 let (tx, rx) = mpsc::channel(); 41 Self { 42 cache: HashMap::new(), 43 tx, 44 rx, 45 } 46 } 47 48 pub fn status(&self, pubkey: &Pubkey) -> Option<&Nip05Status> { 49 self.cache.get(pubkey).map(|entry| &entry.status) 50 } 51 52 pub fn request_validation(&mut self, pubkey: Pubkey, nip05: &str) { 53 if let Some(entry) = self.cache.get(&pubkey) { 54 if entry.checked_at.elapsed() < NIP05_TTL { 55 return; 56 } 57 } 58 59 self.cache.insert( 60 pubkey, 61 CacheEntry { 62 status: Nip05Status::Pending, 63 checked_at: Instant::now(), 64 }, 65 ); 66 67 let tx = self.tx.clone(); 68 let nip05 = nip05.to_string(); 69 70 tokio::spawn(async move { 71 let status = validate_nip05(&pubkey, &nip05).await; 72 let _ = tx.send(Completion { pubkey, status }); 73 }); 74 } 75 76 pub fn poll(&mut self) { 77 while let Ok(completion) = self.rx.try_recv() { 78 self.cache.insert( 79 completion.pubkey, 80 CacheEntry { 81 status: completion.status, 82 checked_at: Instant::now(), 83 }, 84 ); 85 } 86 } 87 } 88 89 async fn validate_nip05(pubkey: &Pubkey, nip05: &str) -> Nip05Status { 90 let Some((user, domain)) = parse_nip05(nip05) else { 91 return Nip05Status::Invalid; 92 }; 93 94 let url = format!("https://{}/.well-known/nostr.json?name={}", domain, user); 95 96 let resp = match crate::media::network::http_req(&url).await { 97 Ok(resp) => resp, 98 Err(e) => { 99 tracing::warn!("NIP-05 validation failed for {}: {}", nip05, e); 100 return Nip05Status::Invalid; 101 } 102 }; 103 104 let json: serde_json::Value = match serde_json::from_slice(&resp.bytes) { 105 Ok(v) => v, 106 Err(e) => { 107 tracing::warn!("NIP-05 JSON parse failed for {}: {}", nip05, e); 108 return Nip05Status::Invalid; 109 } 110 }; 111 112 let expected_hex = pubkey.hex(); 113 114 let valid = json 115 .get("names") 116 .and_then(|names| names.get(user)) 117 .and_then(|v| v.as_str()) 118 .map(|hex| hex.eq_ignore_ascii_case(&expected_hex)) 119 .unwrap_or(false); 120 121 if valid { 122 Nip05Status::Valid 123 } else { 124 Nip05Status::Invalid 125 } 126 } 127 128 fn parse_nip05(nip05: &str) -> Option<(&str, &str)> { 129 let at_pos = nip05.find('@')?; 130 let user = &nip05[..at_pos]; 131 let domain = &nip05[at_pos + 1..]; 132 133 if user.is_empty() || domain.is_empty() { 134 return None; 135 } 136 137 Some((user, domain)) 138 }