This commit is contained in:
2026-08-06 16:00:00 +07:00
parent 0c6d700395
commit 640549a2c5
16 changed files with 426 additions and 438 deletions
+41 -106
View File
@@ -1,6 +1,6 @@
use std::time::Duration;
use anyhow::{Context, Result, anyhow};
use anyhow::{Context, Result};
use nostr_gossip_memory::prelude::*;
#[cfg(not(target_arch = "wasm32"))]
use nostr_lmdb::prelude::*;
@@ -10,112 +10,47 @@ use nostr_sdk::prelude::*;
use crate::signer::UniversalSigner;
/// Owns the nostr client: relay pool, LMDB database and signer.
/// Open (or create) the LMDB database at `db_path` and build a client
/// configured for Signed, together with a fresh 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,
/// The SDK manages its own internal tokio runtime; the returned client can be
/// driven by GPUI's executors.
#[cfg(not(target_arch = "wasm32"))]
pub async fn new_backend(
db_path: impl AsRef<std::path::Path>,
) -> Result<(Client, UniversalSigner)> {
let signer = UniversalSigner::new(Keys::generate());
let database = NostrLmdb::open(db_path)
.await
.context("failed to open nostr database")?;
Ok(with_database(signer, database))
}
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<std::path::Path>) -> Result<Self> {
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<Self> {
let signer = UniversalSigner::new(Keys::generate());
Ok(Self::with_database(signer, MemoryDatabase::unbounded()))
}
fn with_database<D>(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))
.verify_subscriptions(true)
.ban_relay_on_mismatch(true)
.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(())
}
/// Add a relay used only for discovery (e.g. NIP-65 indexer relays).
/// No subscriptions or writes are routed through it.
pub async fn add_discovery_relay(&self, url: &str) -> Result<()> {
self.client
.add_relay(url)
.capabilities(RelayCapabilities::DISCOVERY)
.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<SubscriptionId> {
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<Vec<Event>> {
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<Event> {
let event = builder.finalize_async(&self.signer).await?;
let output = self.client.send_event(&event).await?;
if output.success.is_empty() && !output.failed.is_empty() {
let reasons = output
.failed
.values()
.cloned()
.collect::<Vec<String>>()
.join(", ");
return Err(anyhow!("event not accepted by any relay: {reasons}"));
}
Ok(event)
}
/// In-memory database on wasm (no LMDB available).
#[cfg(target_arch = "wasm32")]
pub fn new_backend() -> Result<(Client, UniversalSigner)> {
let signer = UniversalSigner::new(Keys::generate());
Ok(with_database(signer, MemoryDatabase::unbounded()))
}
fn with_database<D>(signer: UniversalSigner, database: D) -> (Client, UniversalSigner)
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))
.verify_subscriptions(true)
.ban_relay_on_mismatch(true)
.sleep_when_idle(SleepWhenIdle::Enabled {
timeout: Duration::from_secs(600),
})
.build();
(client, signer)
}
+1 -1
View File
@@ -2,6 +2,6 @@ mod backend;
mod signer;
mod update;
pub use backend::NostrBackend;
pub use backend::new_backend;
pub use signer::{SignedAuthUrlHandler, UniversalSigner};
pub use update::Update;
+1 -1
View File
@@ -194,7 +194,7 @@ impl AuthUrlHandler for SignedAuthUrlHandler {
auth_url: Url,
) -> Pin<Box<dyn Future<Output = Result<(), nostr_connect::error::Error>> + Send + '_>> {
Box::pin(async move {
webbrowser::open(auth_url.as_str()).unwrap();
webbrowser::open(auth_url.as_str()).map_err(nostr_connect::error::Error::other)?;
Ok(())
})
}