use std::time::Duration; use anyhow::{Context, Result, anyhow}; use nostr_gossip_memory::prelude::*; use nostr_sdk::prelude::*; #[cfg(not(target_arch = "wasm32"))] use nostr_lmdb::prelude::*; #[cfg(target_arch = "wasm32")] use nostr_memory::prelude::*; use crate::signer::UniversalSigner; /// Owns the nostr client: relay pool, LMDB database and signer. /// /// The SDK manages its own internal tokio runtime; every method here is a /// plain async fn that can be driven by GPUI's executors. #[derive(Clone)] pub struct NostrBackend { client: Client, signer: UniversalSigner, } impl NostrBackend { /// Open (or create) the LMDB database at `db_path` and build the client. #[cfg(not(target_arch = "wasm32"))] pub async fn new(db_path: impl AsRef) -> Result { let signer = UniversalSigner::new(Keys::generate()); let database = NostrLmdb::open(db_path) .await .context("failed to open nostr database")?; Ok(Self::with_database(signer, database)) } /// In-memory database on wasm (no LMDB available). #[cfg(target_arch = "wasm32")] pub fn new() -> Result { let signer = UniversalSigner::new(Keys::generate()); Ok(Self::with_database(signer, MemoryDatabase::unbounded())) } fn with_database(signer: UniversalSigner, database: D) -> Self where D: IntoNostrDatabase, { let authenticator = SignerAuthenticator::new(signer.clone()); let client = ClientBuilder::default() .database(database) .authenticator(authenticator) .gossip(NostrGossipMemory::unbounded()) .gossip_config(GossipConfig::default().no_background_refresh()) .connect_timeout(Duration::from_secs(10)) .sleep_when_idle(SleepWhenIdle::Enabled { timeout: Duration::from_secs(600), }) .build(); Self { client, signer } } pub fn client(&self) -> Client { self.client.clone() } pub fn signer(&self) -> UniversalSigner { self.signer.clone() } pub async fn add_relay(&self, url: &str) -> Result<()> { self.client.add_relay(url).await?; Ok(()) } pub async fn connect(&self) { self.client.connect().await; } /// Start a persistent subscription. Received events are stored in the /// database automatically by the relay pool. pub async fn subscribe(&self, filter: Filter) -> Result { let output = self.client.subscribe(filter).await?; Ok(output.value) } /// Query the local database (the single source of truth for the UI). pub async fn query(&self, filter: Filter) -> Result> { let events = self.client.database().query(filter).await?; Ok(events.into_iter().collect()) } /// Sign with the current signer, broadcast, and save locally so the /// event is immediately visible to [`NostrBackend::query`]. pub async fn send(&self, builder: EventBuilder) -> Result { let event = builder.finalize_async(&self.signer).await?; let output = self.client.send_event(&event).await?; // Keep our own events in the local database; the notification pump // only fires for events received from relays. self.client.database().save_event(&event).await?; if output.success.is_empty() && !output.failed.is_empty() { let reasons = output .failed .values() .cloned() .collect::>() .join(", "); return Err(anyhow!("event not accepted by any relay: {reasons}")); } Ok(event) } }