commit d4cf0b41c681051489c3fc3c8501ba7ecbacf690
parent 34dc13cf36e883a143adb3f2be164b68a49cc50c
Author: triesap <tyson@radroots.org>
Date: Fri, 17 Jul 2026 21:46:51 +0000
nostr: expose bounded relay subscription controls
- add relay-targeted subscribe and publish helpers
- expose explicit subscription cancellation through Radroots types
- preserve caller ownership without detached listener tasks
- qualify native behavior and WASM compilation
Diffstat:
2 files changed, 56 insertions(+), 1 deletion(-)
diff --git a/crates/nostr/Cargo.toml b/crates/nostr/Cargo.toml
@@ -34,7 +34,9 @@ nip17 = ["std", "codec", "nostr/nip44", "nostr/nip59"]
[dependencies]
radroots_event = { workspace = true, optional = true, default-features = false }
radroots_event_codec = { workspace = true, optional = true, default-features = false }
-radroots_identity = { workspace = true, optional = true, default-features = true }
+radroots_identity = { workspace = true, optional = true, default-features = false, features = [
+ "std",
+] }
nostr = { workspace = true, features = ["nip04"] }
nostr-sdk = { workspace = true, optional = true }
reqwest = { workspace = true, optional = true, default-features = false, features = [
diff --git a/crates/nostr/src/client.rs b/crates/nostr/src/client.rs
@@ -184,6 +184,22 @@ impl RadrootsNostrClient {
Ok(self.inner.subscribe(filter, opts).await?)
}
+ pub async fn subscribe_to_relays(
+ &self,
+ relays: &[RadrootsNostrRelayUrl],
+ filter: RadrootsNostrFilter,
+ opts: Option<RadrootsNostrSubscribeAutoCloseOptions>,
+ ) -> Result<RadrootsNostrOutput<RadrootsNostrSubscriptionId>, RadrootsNostrError> {
+ Ok(self
+ .inner
+ .subscribe_to(relays.iter().cloned(), filter, opts)
+ .await?)
+ }
+
+ pub async fn unsubscribe(&self, subscription_id: &RadrootsNostrSubscriptionId) {
+ self.inner.unsubscribe(subscription_id).await;
+ }
+
pub async fn send_event_builder(
&self,
event: RadrootsNostrEventBuilder,
@@ -198,6 +214,17 @@ impl RadrootsNostrClient {
Ok(self.inner.send_event(event).await?)
}
+ pub async fn send_event_to_relays(
+ &self,
+ relays: &[RadrootsNostrRelayUrl],
+ event: &RadrootsNostrEvent,
+ ) -> Result<RadrootsNostrOutput<RadrootsNostrEventId>, RadrootsNostrError> {
+ Ok(self
+ .inner
+ .send_event_to(relays.iter().cloned(), event)
+ .await?)
+ }
+
pub async fn set_metadata(
&self,
md: &RadrootsNostrMetadata,
@@ -237,6 +264,10 @@ pub async fn radroots_nostr_fetch_event_by_id(
#[cfg(test)]
mod tests {
use super::{RadrootsNostrClient, RadrootsNostrClientOptions};
+ use crate::types::{
+ RadrootsNostrEventBuilder, RadrootsNostrFilter, RadrootsNostrKeys, RadrootsNostrKind,
+ RadrootsNostrSecretKey, RadrootsNostrSubscriptionId,
+ };
#[tokio::test]
async fn signerless_client_has_no_signer() {
@@ -256,4 +287,26 @@ mod tests {
assert!(!client.has_signer().await);
}
+
+ #[tokio::test]
+ async fn targeted_operations_require_relays_and_cleanup_is_explicit() {
+ let keys = RadrootsNostrKeys::new(
+ RadrootsNostrSecretKey::from_slice(&[1_u8; 32]).expect("test secret key"),
+ );
+ let client = RadrootsNostrClient::new(keys.clone());
+ let subscription = client
+ .subscribe_to_relays(&[], RadrootsNostrFilter::new(), None)
+ .await;
+ assert!(subscription.is_err());
+
+ let event = RadrootsNostrEventBuilder::new(RadrootsNostrKind::TextNote, "test")
+ .sign_with_keys(&keys)
+ .expect("test event");
+ let published = client.send_event_to_relays(&[], &event).await;
+ assert!(published.is_err());
+
+ client
+ .unsubscribe(&RadrootsNostrSubscriptionId::new("missing"))
+ .await;
+ }
}