use anyhow::{Result, anyhow};
use nostr_sdk::prelude::*;
use nostr_sdk::client::EventSource;
use serde::{Deserialize, Serialize};
use std::time::Duration;
use super::identity::AgentIdentity;
use super::storage::SecureStorage;
pub const DEFAULT_RELAYS: &[&str] = &[
"wss://relay.signedbyme.com", "wss://relay-sfo.signedbyme.com", "wss://relay-ams.signedbyme.com", "wss://relay-sgp.signedbyme.com", ];
pub const RELAY_URL: &str = "wss://relay.signedbyme.com";
pub const KIND_ENROLLMENT_AUTH: u16 = 28200; pub const KIND_PROOF_EVENT: u16 = 28101; pub const KIND_DELEGATION_ACK: u16 = 28102; pub const KIND_REVOCATION_ACK: u16 = 28103; pub const KIND_HUMAN_DELEGATION: u16 = 28250; pub const KIND_HUMAN_REVOCATION: u16 = 28251;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ProofEventData {
pub proof_hex: String,
pub merkle_root: String,
pub npub_x: String,
pub npub_y: String,
pub session_id: String,
pub client_id: String,
}
pub struct NostrClient {
client: Client,
keys: Keys,
agent_npub: String,
relays: Vec<String>,
}
impl NostrClient {
pub async fn new<S: SecureStorage>(identity: &AgentIdentity<S>) -> Result<Self> {
let keys = identity.get_agent_keys()?;
let agent_npub = keys.public_key().to_bech32()?;
let client = Client::new(keys.clone());
let relays: Vec<String> = DEFAULT_RELAYS.iter().map(|s| s.to_string()).collect();
let mut nostr_client = Self {
client,
keys,
agent_npub,
relays,
};
nostr_client.connect_and_auth().await?;
Ok(nostr_client)
}
async fn connect_and_auth(&mut self) -> Result<()> {
for relay_url in &self.relays {
if let Err(e) = self.client.add_relay(relay_url.as_str()).await {
eprintln!("[nostr] Warning: Failed to add relay {}: {}", relay_url, e);
}
}
let timeout = Duration::from_secs(10);
match tokio::time::timeout(timeout, self.client.connect()).await {
Ok(_) => {}
Err(_) => {
return Err(anyhow!("Connection to relays timed out after 10 seconds"));
}
}
tokio::time::sleep(Duration::from_millis(500)).await;
Ok(())
}
pub async fn add_custom_relays(&mut self, relay_urls: &[String]) -> Result<()> {
for relay_url in relay_urls {
if self.relays.contains(relay_url) {
continue;
}
self.relays.push(relay_url.clone());
if let Err(e) = self.client.add_relay(relay_url.as_str()).await {
eprintln!("[nostr] Warning: Failed to add custom relay {}: {}", relay_url, e);
}
}
self.client.connect().await;
tokio::time::sleep(Duration::from_millis(300)).await;
Ok(())
}
pub fn active_relays(&self) -> &[String] {
&self.relays
}
pub fn agent_npub(&self) -> &str {
&self.agent_npub
}
pub fn public_key(&self) -> PublicKey {
self.keys.public_key()
}
pub fn inner_client(&self) -> Client {
self.client.clone()
}
pub async fn publish_proof_event(&self, data: ProofEventData) -> Result<EventId> {
let tags = vec![
Tag::custom(TagKind::Custom("session_id".into()), vec![data.session_id.clone()]),
Tag::custom(TagKind::Custom("client_id".into()), vec![data.client_id.clone()]),
Tag::custom(TagKind::Custom("merkle_root".into()), vec![data.merkle_root.clone()]),
];
let content = serde_json::json!({
"proof": data.proof_hex,
"merkle_root": data.merkle_root,
"npub_x": data.npub_x,
"npub_y": data.npub_y,
"session_id": data.session_id,
"client_id": data.client_id,
"timestamp": current_timestamp(),
}).to_string();
let event_builder = EventBuilder::new(Kind::Custom(KIND_PROOF_EVENT), content, tags);
let output = self.client.send_event_builder(event_builder).await
.map_err(|e| anyhow!("Failed to publish proof event: {}", e))?;
Ok(output.val)
}
pub async fn publish_delegation_ack(&self, delegation_event_id: EventId) -> Result<EventId> {
let tags = vec![
Tag::event(delegation_event_id),
Tag::custom(TagKind::Custom("ack_type".into()), vec!["delegation".to_string()]),
];
let content = serde_json::json!({
"type": "delegation_ack",
"delegation_event_id": delegation_event_id.to_hex(),
"agent_npub": self.agent_npub,
"timestamp": current_timestamp(),
}).to_string();
let event_builder = EventBuilder::new(Kind::Custom(KIND_DELEGATION_ACK), content, tags);
let output = self.client.send_event_builder(event_builder).await
.map_err(|e| anyhow!("Failed to publish delegation ack: {}", e))?;
Ok(output.val)
}
pub async fn publish_revocation_ack(&self, revocation_event_id: EventId) -> Result<EventId> {
let tags = vec![
Tag::event(revocation_event_id),
Tag::custom(TagKind::Custom("ack_type".into()), vec!["revocation".to_string()]),
];
let content = serde_json::json!({
"type": "revocation_ack",
"revocation_event_id": revocation_event_id.to_hex(),
"agent_npub": self.agent_npub,
"timestamp": current_timestamp(),
}).to_string();
let event_builder = EventBuilder::new(Kind::Custom(KIND_REVOCATION_ACK), content, tags);
let output = self.client.send_event_builder(event_builder).await
.map_err(|e| anyhow!("Failed to publish revocation ack: {}", e))?;
Ok(output.val)
}
pub async fn poll_enrollment_events(&self, agent_npub: &str) -> Result<Vec<Event>> {
let filter = Filter::new()
.kind(Kind::Custom(KIND_ENROLLMENT_AUTH))
.custom_tag(SingleLetterTag::lowercase(Alphabet::P), vec![agent_npub.to_string()])
.limit(100);
let events = self.client.get_events_of(vec![filter], EventSource::relays(Some(Duration::from_secs(5)))).await
.map_err(|e| anyhow!("Failed to fetch enrollment events: {}", e))?;
Ok(events)
}
pub async fn poll_delegation_events(&self, human_npub: &str) -> Result<Vec<Event>> {
let human_pubkey = PublicKey::from_bech32(human_npub)
.or_else(|_| PublicKey::from_hex(human_npub))
.map_err(|e| anyhow!("Invalid human npub: {}", e))?;
let filter = Filter::new()
.kind(Kind::Custom(KIND_HUMAN_DELEGATION))
.author(human_pubkey)
.limit(100);
let events = self.client.get_events_of(vec![filter], EventSource::relays(Some(Duration::from_secs(5)))).await
.map_err(|e| anyhow!("Failed to fetch delegation events: {}", e))?;
Ok(events)
}
pub async fn poll_revocation_events(&self, human_npub: &str) -> Result<Vec<Event>> {
let human_pubkey = PublicKey::from_bech32(human_npub)
.or_else(|_| PublicKey::from_hex(human_npub))
.map_err(|e| anyhow!("Invalid human npub: {}", e))?;
let filter = Filter::new()
.kind(Kind::Custom(KIND_HUMAN_REVOCATION))
.author(human_pubkey)
.limit(100);
let events = self.client.get_events_of(vec![filter], EventSource::relays(Some(Duration::from_secs(5)))).await
.map_err(|e| anyhow!("Failed to fetch revocation events: {}", e))?;
Ok(events)
}
pub async fn disconnect(&self) -> Result<()> {
self.client.disconnect().await?;
Ok(())
}
}
fn current_timestamp() -> u64 {
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap_or_default()
.as_secs()
}
#[cfg(test)]
mod tests {
use super::*;
use crate::sdk::storage::EncryptedFileStorage;
use tempfile::tempdir;
#[test]
fn test_agent_keys_derivation() {
let dir = tempdir().unwrap();
let storage = EncryptedFileStorage::new(dir.path().to_path_buf()).unwrap();
let identity = AgentIdentity::new(storage);
match identity.initialize() {
Ok(state) => {
let keys = identity.get_agent_keys().unwrap();
assert_eq!(keys.public_key().to_bech32().unwrap(), state.agent_npub);
}
Err(_) => {
eprintln!("Skipping test: keyring not available");
}
}
}
#[tokio::test]
#[ignore] async fn test_nip42_connection() {
let dir = tempdir().unwrap();
let storage = EncryptedFileStorage::new(dir.path().to_path_buf()).unwrap();
let identity = AgentIdentity::new(storage);
let state = identity.initialize().expect("Failed to initialize identity");
println!("Agent npub: {}", state.agent_npub);
let client = NostrClient::new(&identity).await
.expect("Failed to create NOSTR client");
println!("Connected and authenticated to {}", RELAY_URL);
assert_eq!(client.agent_npub(), state.agent_npub);
client.disconnect().await.expect("Failed to disconnect");
println!("Disconnected successfully");
}
}