From de726a4b2d83dcac5bf67f7c45dd55928d6c08cb Mon Sep 17 00:00:00 2001 From: Harsh Date: Fri, 22 May 2026 02:39:07 +0530 Subject: [PATCH] feat: server identity parsing, relay list fetching, and fetch_events (CEP-17) --- src/relay/mock.rs | 27 +- src/relay/mod.rs | 32 +- src/transport/client/mod.rs | 55 ++- src/transport/client/server_identity.rs | 102 +++++ .../client/server_relay_discovery.rs | 369 ++++++++++++++++++ src/transport/server/announcement_manager.rs | 12 +- tests/transport_integration.rs | 4 +- 7 files changed, 553 insertions(+), 48 deletions(-) create mode 100644 src/transport/client/server_identity.rs create mode 100644 src/transport/client/server_relay_discovery.rs diff --git a/src/relay/mock.rs b/src/relay/mock.rs index cf934f6..301d639 100644 --- a/src/relay/mock.rs +++ b/src/relay/mock.rs @@ -10,6 +10,7 @@ use std::collections::HashMap; use std::sync::Arc; +use std::time::Duration; use async_trait::async_trait; use tokio::sync::Mutex; @@ -127,6 +128,13 @@ impl MockRelayPool { pub async fn stored_events(&self) -> Vec { self.inner.lock().await.events.clone() } + + /// Inject an externally-built event into the store without broadcasting. + /// + /// Useful for seeding kind 10002 relay-list events for `fetch_events()` tests. + pub async fn inject_event(&self, event: Event) { + self.inner.lock().await.events.push(event); + } } impl Default for MockRelayPool { @@ -235,19 +243,20 @@ impl RelayPoolTrait for MockRelayPool { self.publish(builder).await } - /// Return stored events matching the filter. - async fn fetch_events( - &self, - filter: Filter, - _timeout: std::time::Duration, - ) -> Result> { + /// Return stored events that match the given filters' kind and author constraints. + async fn fetch_events(&self, filters: Vec, _timeout: Duration) -> Result> { let inner = self.inner.lock().await; - Ok(inner + let matched: Vec = inner .events .iter() - .filter(|e| filter.match_event(e, MatchEventOptions::default())) + .filter(|e| { + filters + .iter() + .any(|f| f.match_event(e, MatchEventOptions::default())) + }) .cloned() - .collect()) + .collect(); + Ok(matched) } } diff --git a/src/relay/mod.rs b/src/relay/mod.rs index 0af8388..7db9da5 100644 --- a/src/relay/mod.rs +++ b/src/relay/mod.rs @@ -37,8 +37,8 @@ pub trait RelayPoolTrait: Send + Sync { async fn subscribe(&self, filters: Vec) -> Result<()>; /// Sign and publish an event to specific relay URLs. async fn publish_to(&self, urls: &[String], builder: EventBuilder) -> Result; - /// Fetch events matching a filter from connected relays. - async fn fetch_events(&self, filter: Filter, timeout: Duration) -> Result>; + /// Fetch events matching filters from connected relays. + async fn fetch_events(&self, filters: Vec, timeout: Duration) -> Result>; } /// Relay pool wrapper for managing Nostr relay connections. @@ -151,14 +151,22 @@ impl RelayPool { Ok(output.val) } - /// Fetch events matching a filter from connected relays. - pub async fn fetch_events(&self, filter: Filter, timeout: Duration) -> Result> { - let events = self - .client - .fetch_events(filter, timeout) - .await - .map_err(|e| Error::Transport(e.to_string()))?; - Ok(events.into_iter().collect()) + /// Fetch events matching filters from connected relays. + pub async fn fetch_events( + &self, + filters: Vec, + timeout: Duration, + ) -> Result> { + let mut all_events = Vec::new(); + for filter in filters { + let events = self + .client + .fetch_events(filter, timeout) + .await + .map_err(|e| Error::Transport(e.to_string()))?; + all_events.extend(events); + } + Ok(all_events) } } @@ -207,7 +215,7 @@ impl RelayPoolTrait for RelayPool { RelayPool::publish_to(self, urls, builder).await } - async fn fetch_events(&self, filter: Filter, timeout: Duration) -> Result> { - RelayPool::fetch_events(self, filter, timeout).await + async fn fetch_events(&self, filters: Vec, timeout: Duration) -> Result> { + RelayPool::fetch_events(self, filters, timeout).await } } diff --git a/src/transport/client/mod.rs b/src/transport/client/mod.rs index 424cea6..044bc14 100644 --- a/src/transport/client/mod.rs +++ b/src/transport/client/mod.rs @@ -4,6 +4,8 @@ //! kind 25910 events, correlates responses via `e` tag. pub mod correlation_store; +pub mod server_identity; +pub mod server_relay_discovery; pub use correlation_store::ClientCorrelationStore; @@ -17,7 +19,7 @@ use nostr_sdk::prelude::*; use tokio_util::sync::CancellationToken; use crate::core::constants::*; -use crate::core::error::{Error, Result}; +use crate::core::error::Result; use crate::core::serializers; use crate::core::types::*; use crate::core::validation; @@ -34,7 +36,10 @@ const LOG_TARGET: &str = "contextvm_sdk::transport::client"; pub struct NostrClientTransportConfig { /// Relay URLs to connect to. pub relay_urls: Vec, - /// The server's public key (hex). + /// The server's public key (hex, npub, or nprofile). + /// + /// When an nprofile is provided, embedded relay hints are extracted and used + /// during CEP-17 relay resolution. pub server_pubkey: String, /// Encryption mode. pub encryption_mode: EncryptionMode, @@ -64,7 +69,7 @@ impl Default for NostrClientTransportConfig { } impl NostrClientTransportConfig { - /// Set the server's public key (hex). + /// Set the server's public key (hex, npub, or nprofile). pub fn with_server_pubkey(mut self, pubkey: impl Into) -> Self { self.server_pubkey = pubkey.into(); self @@ -101,6 +106,9 @@ pub struct NostrClientTransport { base: BaseTransport, config: NostrClientTransportConfig, server_pubkey: PublicKey, + /// Populated from nprofile relay hints; used by relay resolution in `start()` (CEP-17). + #[allow(dead_code)] + hinted_relay_urls: Vec, /// Pending request event IDs awaiting responses. pending_requests: ClientCorrelationStore, /// CEP-35: one-shot flag for client discovery tag emission. @@ -130,15 +138,16 @@ impl NostrClientTransport { where T: IntoNostrSigner, { - let server_pubkey = PublicKey::from_hex(&config.server_pubkey).map_err(|error| { - tracing::error!( - target: LOG_TARGET, - error = %error, - server_pubkey = %config.server_pubkey, - "Invalid server pubkey" - ); - Error::Other(format!("Invalid server pubkey: {error}")) - })?; + let (server_pubkey, hinted_relay_urls) = + server_identity::parse_server_identity(&config.server_pubkey).map_err(|error| { + tracing::error!( + target: LOG_TARGET, + error = %error, + server_pubkey = %config.server_pubkey, + "Invalid server pubkey" + ); + error + })?; let relay_pool: Arc = Arc::new(RelayPool::new(signer).await.map_err(|error| { @@ -169,6 +178,7 @@ impl NostrClientTransport { }, config, server_pubkey, + hinted_relay_urls, pending_requests: ClientCorrelationStore::new(), has_sent_discovery_tags: AtomicBool::new(false), discovered_server_capabilities: Arc::new(Mutex::new(PeerCapabilities::default())), @@ -187,15 +197,16 @@ impl NostrClientTransport { config: NostrClientTransportConfig, relay_pool: Arc, ) -> Result { - let server_pubkey = PublicKey::from_hex(&config.server_pubkey).map_err(|error| { - tracing::error!( - target: LOG_TARGET, - error = %error, - server_pubkey = %config.server_pubkey, - "Invalid server pubkey" - ); - Error::Other(format!("Invalid server pubkey: {error}")) - })?; + let (server_pubkey, hinted_relay_urls) = + server_identity::parse_server_identity(&config.server_pubkey).map_err(|error| { + tracing::error!( + target: LOG_TARGET, + error = %error, + server_pubkey = %config.server_pubkey, + "Invalid server pubkey" + ); + error + })?; let (tx, rx) = tokio::sync::mpsc::unbounded_channel(); let seen_gift_wrap_ids = Arc::new(Mutex::new(LruCache::new( @@ -217,6 +228,7 @@ impl NostrClientTransport { }, config, server_pubkey, + hinted_relay_urls, pending_requests: ClientCorrelationStore::new(), has_sent_discovery_tags: AtomicBool::new(false), discovered_server_capabilities: Arc::new(Mutex::new(PeerCapabilities::default())), @@ -1092,6 +1104,7 @@ mod tests { ..Default::default() }, server_pubkey: keys.public_key(), + hinted_relay_urls: vec![], pending_requests: ClientCorrelationStore::new(), has_sent_discovery_tags: AtomicBool::new(false), discovered_server_capabilities: Arc::new(Mutex::new(PeerCapabilities::default())), diff --git a/src/transport/client/server_identity.rs b/src/transport/client/server_identity.rs new file mode 100644 index 0000000..21e46b0 --- /dev/null +++ b/src/transport/client/server_identity.rs @@ -0,0 +1,102 @@ +//! Server identity parsing for CEP-17 client-side relay discovery. +//! +//! Accepts hex pubkeys, npub (NIP-19), or nprofile (NIP-19) strings and +//! extracts the server's public key and any embedded relay hints. +//! Mirrors the TS SDK's `parseServerIdentity()`. + +use nostr_sdk::prelude::*; + +use crate::core::error::{Error, Result}; + +/// Parse a server identity string into a public key and optional relay hints. +/// +/// Supported formats: +/// - **Hex**: 64-character hex-encoded public key +/// - **npub**: NIP-19 bech32-encoded public key +/// - **nprofile**: NIP-19 bech32-encoded profile (pubkey + relay hints) +/// +/// Returns `(PublicKey, Vec)` where the second element contains relay +/// hint URLs extracted from an nprofile, or an empty vec for hex/npub. +pub fn parse_server_identity(input: &str) -> Result<(PublicKey, Vec)> { + // Try hex first + if let Ok(pk) = PublicKey::from_hex(input) { + return Ok((pk, vec![])); + } + + // Try bech32 (npub / nprofile) + match Nip19::from_bech32(input) { + Ok(Nip19::Pubkey(pk)) => Ok((pk, vec![])), + Ok(Nip19::Profile(profile)) => { + let relays: Vec = profile.relays.into_iter().map(|r| r.to_string()).collect(); + Ok((profile.public_key, relays)) + } + _ => Err(Error::Other(format!( + "Invalid serverPubkey format: {input}. Expected hex pubkey, npub, or nprofile." + ))), + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn hex_pubkey_roundtrip() { + let keys = Keys::generate(); + let hex = keys.public_key().to_hex(); + let (pk, hints) = parse_server_identity(&hex).unwrap(); + assert_eq!(pk, keys.public_key()); + assert!(hints.is_empty()); + } + + #[test] + fn npub_roundtrip() { + let keys = Keys::generate(); + let npub = keys.public_key().to_bech32().unwrap(); + let (pk, hints) = parse_server_identity(&npub).unwrap(); + assert_eq!(pk, keys.public_key()); + assert!(hints.is_empty()); + } + + #[test] + fn nprofile_with_relays() { + let keys = Keys::generate(); + let relay1 = RelayUrl::parse("wss://relay1.example.com").unwrap(); + let relay2 = RelayUrl::parse("wss://relay2.example.com").unwrap(); + let profile = Nip19Profile::new(keys.public_key(), vec![relay1.clone(), relay2.clone()]); + let nprofile = profile.to_bech32().unwrap(); + + let (pk, hints) = parse_server_identity(&nprofile).unwrap(); + assert_eq!(pk, keys.public_key()); + assert_eq!(hints.len(), 2); + assert!(hints.contains(&relay1.to_string())); + assert!(hints.contains(&relay2.to_string())); + } + + #[test] + fn nprofile_without_relays() { + let keys = Keys::generate(); + let profile = Nip19Profile::new(keys.public_key(), Vec::::new()); + let nprofile = profile.to_bech32().unwrap(); + + let (pk, hints) = parse_server_identity(&nprofile).unwrap(); + assert_eq!(pk, keys.public_key()); + assert!(hints.is_empty()); + } + + #[test] + fn invalid_string_returns_error() { + let result = parse_server_identity("not-a-valid-key"); + assert!(result.is_err()); + let err = result.unwrap_err().to_string(); + assert!(err.contains("Invalid serverPubkey format")); + assert!(err.contains("Expected hex pubkey, npub, or nprofile")); + } + + #[test] + fn invalid_npub_checksum_returns_error() { + // Valid npub prefix but corrupted data + let result = parse_server_identity("npub1invalidchecksum"); + assert!(result.is_err()); + } +} diff --git a/src/transport/client/server_relay_discovery.rs b/src/transport/client/server_relay_discovery.rs new file mode 100644 index 0000000..42812c0 --- /dev/null +++ b/src/transport/client/server_relay_discovery.rs @@ -0,0 +1,369 @@ +//! CEP-17 server relay list fetching and operational relay selection. +//! +//! Fetches kind 10002 relay-list metadata events from discovery relays and +//! selects operational relay URLs based on marker precedence (unmarked > read+write). +//! Mirrors the TS SDK's `fetchServerRelayList()` and `selectOperationalRelayUrls()`. + +use std::collections::HashSet; +use std::sync::Arc; +use std::time::Duration; + +use nostr_sdk::prelude::*; + +use crate::core::constants::{tags, RELAY_LIST_METADATA_KIND}; +use crate::core::error::Result; +use crate::relay::{RelayPool, RelayPoolTrait}; + +/// A single entry from a kind 10002 relay list event. +/// +/// Mirrors the TS SDK `RelayListEntry` interface. +#[derive(Debug, Clone, PartialEq)] +pub struct RelayListEntry { + /// The relay URL. + pub url: String, + /// Optional marker: `"read"`, `"write"`, or `None` (unmarked). + pub marker: Option, +} + +/// Select operational relay URLs from relay list entries using marker precedence. +/// +/// 1. If any entries are unmarked (no marker), return those URLs (deduplicated). +/// 2. Otherwise, return the union of `read` + `write` entries (deduplicated). +/// 3. Empty strings are filtered out in all cases. +/// +/// Mirrors the TS SDK `selectOperationalRelayUrls()`. +pub fn select_operational_relay_urls(entries: &[RelayListEntry]) -> Vec { + // Collect unmarked entries + let unmarked: Vec<&str> = entries + .iter() + .filter(|e| e.marker.is_none() && !e.url.is_empty()) + .map(|e| e.url.as_str()) + .collect(); + + if !unmarked.is_empty() { + return dedup(unmarked); + } + + // Fall back to read + write union + let read_write: Vec<&str> = entries + .iter() + .filter(|e| { + !e.url.is_empty() && matches!(e.marker.as_deref(), Some("read") | Some("write")) + }) + .map(|e| e.url.as_str()) + .collect(); + + dedup(read_write) +} + +/// Deduplicate URLs preserving first-seen order. +fn dedup(urls: Vec<&str>) -> Vec { + let mut seen = HashSet::new(); + urls.into_iter() + .filter(|u| seen.insert(*u)) + .map(|u| u.to_string()) + .collect() +} + +/// Fetch the server's kind 10002 relay list from discovery relays. +/// +/// Creates a temporary relay pool, connects to `relay_urls`, fetches kind 10002 +/// events for `server_pubkey`, extracts relay entries from the latest event, +/// and disconnects. Mirrors the TS SDK `fetchServerRelayList()`. +pub async fn fetch_server_relay_list( + server_pubkey: &PublicKey, + relay_urls: &[String], + signer: Arc, + timeout: Duration, +) -> Result> { + let pool = RelayPool::new(signer).await?; + pool.connect(relay_urls).await?; + let result = fetch_relay_list_from_pool(server_pubkey, &pool, timeout).await; + let _ = pool.disconnect().await; + result +} + +/// Core relay-list fetch+parse logic operating on an existing pool. +/// +/// Separated from [`fetch_server_relay_list`] so tests can inject events via +/// `MockRelayPool` without needing network access. +pub(crate) async fn fetch_relay_list_from_pool( + server_pubkey: &PublicKey, + relay_pool: &dyn RelayPoolTrait, + timeout: Duration, +) -> Result> { + let filter = Filter::new() + .kind(Kind::Custom(RELAY_LIST_METADATA_KIND)) + .author(*server_pubkey) + .limit(1); + + let mut events = relay_pool.fetch_events(vec![filter], timeout).await?; + + if events.is_empty() { + return Ok(vec![]); + } + + // Sort by created_at descending, take the latest + events.sort_by_key(|e| std::cmp::Reverse(e.created_at)); + let latest = &events[0]; + + // Extract relay entries from "r" tags. + // NOTE: Empty URLs are filtered here (diverges from TS SDK which keeps them); + // malformed empty-URL tags don't occur per NIP-65. + let entries: Vec = latest + .tags + .iter() + .filter_map(|tag| { + let parts = tag.clone().to_vec(); + if parts.first().map(|s| s.as_str()) != Some(tags::RELAY) { + return None; + } + let url = parts.get(1)?.clone(); + if url.is_empty() { + return None; + } + // NOTE: An empty-string marker becomes Some(""), not None. The TS SDK + // treats "" as unmarked (JS falsy). In practice NIP-65 tags never + // carry an empty marker, so this divergence is benign. + let marker = parts.get(2).cloned(); + Some(RelayListEntry { url, marker }) + }) + .collect(); + + Ok(entries) +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::relay::MockRelayPool; + + // ── select_operational_relay_urls tests ─────────────────────── + + #[test] + fn select_all_unmarked_returns_all_urls() { + let entries = vec![ + RelayListEntry { + url: "wss://relay1.example.com".to_string(), + marker: None, + }, + RelayListEntry { + url: "wss://relay2.example.com".to_string(), + marker: None, + }, + ]; + let result = select_operational_relay_urls(&entries); + assert_eq!(result.len(), 2); + assert!(result.contains(&"wss://relay1.example.com".to_string())); + assert!(result.contains(&"wss://relay2.example.com".to_string())); + } + + #[test] + fn select_mixed_markers_prefers_unmarked() { + let entries = vec![ + RelayListEntry { + url: "wss://unmarked.example.com".to_string(), + marker: None, + }, + RelayListEntry { + url: "wss://read.example.com".to_string(), + marker: Some("read".to_string()), + }, + RelayListEntry { + url: "wss://write.example.com".to_string(), + marker: Some("write".to_string()), + }, + ]; + let result = select_operational_relay_urls(&entries); + assert_eq!(result, vec!["wss://unmarked.example.com"]); + } + + #[test] + fn select_only_read_write_returns_union() { + let entries = vec![ + RelayListEntry { + url: "wss://read.example.com".to_string(), + marker: Some("read".to_string()), + }, + RelayListEntry { + url: "wss://write.example.com".to_string(), + marker: Some("write".to_string()), + }, + ]; + let result = select_operational_relay_urls(&entries); + assert_eq!(result.len(), 2); + assert!(result.contains(&"wss://read.example.com".to_string())); + assert!(result.contains(&"wss://write.example.com".to_string())); + } + + #[test] + fn select_empty_input_returns_empty() { + let result = select_operational_relay_urls(&[]); + assert!(result.is_empty()); + } + + #[test] + fn select_deduplicates_urls() { + let entries = vec![ + RelayListEntry { + url: "wss://relay.example.com".to_string(), + marker: None, + }, + RelayListEntry { + url: "wss://relay.example.com".to_string(), + marker: None, + }, + ]; + let result = select_operational_relay_urls(&entries); + assert_eq!(result, vec!["wss://relay.example.com"]); + } + + #[test] + fn select_filters_empty_strings() { + let entries = vec![ + RelayListEntry { + url: String::new(), + marker: None, + }, + RelayListEntry { + url: "wss://relay.example.com".to_string(), + marker: None, + }, + ]; + let result = select_operational_relay_urls(&entries); + assert_eq!(result, vec!["wss://relay.example.com"]); + } + + // ── fetch_server_relay_list tests ──────────────────────────── + + fn build_relay_list_event(keys: &Keys, tags: Vec, created_at: u64) -> Event { + let builder = EventBuilder::new(Kind::Custom(RELAY_LIST_METADATA_KIND), "") + .tags(tags) + .custom_created_at(Timestamp::from(created_at)); + builder.sign_with_keys(keys).unwrap() + } + + #[tokio::test] + async fn fetch_returns_parsed_entries_from_injected_event() { + let pool = MockRelayPool::new(); + let server_keys = Keys::generate(); + + let tags = vec![ + Tag::custom( + TagKind::Custom(tags::RELAY.into()), + vec!["wss://relay1.example.com"], + ), + Tag::custom( + TagKind::Custom(tags::RELAY.into()), + vec!["wss://relay2.example.com"], + ), + ]; + let event = build_relay_list_event(&server_keys, tags, 1000); + pool.inject_event(event).await; + + let entries = + fetch_relay_list_from_pool(&server_keys.public_key(), &pool, Duration::from_secs(5)) + .await + .unwrap(); + + assert_eq!(entries.len(), 2); + assert_eq!(entries[0].url, "wss://relay1.example.com"); + assert!(entries[0].marker.is_none()); + assert_eq!(entries[1].url, "wss://relay2.example.com"); + assert!(entries[1].marker.is_none()); + } + + #[tokio::test] + async fn fetch_no_events_returns_empty() { + let pool = MockRelayPool::new(); + let server_keys = Keys::generate(); + + let entries = + fetch_relay_list_from_pool(&server_keys.public_key(), &pool, Duration::from_secs(5)) + .await + .unwrap(); + + assert!(entries.is_empty()); + } + + #[tokio::test] + async fn fetch_multiple_events_returns_latest() { + let pool = MockRelayPool::new(); + let server_keys = Keys::generate(); + + // Older event + let old_tags = vec![Tag::custom( + TagKind::Custom(tags::RELAY.into()), + vec!["wss://old.example.com"], + )]; + let old_event = build_relay_list_event(&server_keys, old_tags, 1000); + pool.inject_event(old_event).await; + + // Newer event + let new_tags = vec![Tag::custom( + TagKind::Custom(tags::RELAY.into()), + vec!["wss://new.example.com"], + )]; + let new_event = build_relay_list_event(&server_keys, new_tags, 2000); + pool.inject_event(new_event).await; + + let entries = + fetch_relay_list_from_pool(&server_keys.public_key(), &pool, Duration::from_secs(5)) + .await + .unwrap(); + + assert_eq!(entries.len(), 1); + assert_eq!(entries[0].url, "wss://new.example.com"); + } + + #[tokio::test] + async fn fetch_extracts_marker_from_third_tag_element() { + let pool = MockRelayPool::new(); + let server_keys = Keys::generate(); + + let tags = vec![ + Tag::custom( + TagKind::Custom(tags::RELAY.into()), + vec!["wss://read.example.com", "read"], + ), + Tag::custom( + TagKind::Custom(tags::RELAY.into()), + vec!["wss://write.example.com", "write"], + ), + Tag::custom( + TagKind::Custom(tags::RELAY.into()), + vec!["wss://both.example.com"], + ), + ]; + let event = build_relay_list_event(&server_keys, tags, 1000); + pool.inject_event(event).await; + + let entries = + fetch_relay_list_from_pool(&server_keys.public_key(), &pool, Duration::from_secs(5)) + .await + .unwrap(); + + assert_eq!(entries.len(), 3); + assert_eq!( + entries[0], + RelayListEntry { + url: "wss://read.example.com".to_string(), + marker: Some("read".to_string()), + } + ); + assert_eq!( + entries[1], + RelayListEntry { + url: "wss://write.example.com".to_string(), + marker: Some("write".to_string()), + } + ); + assert_eq!( + entries[2], + RelayListEntry { + url: "wss://both.example.com".to_string(), + marker: None, + } + ); + } +} diff --git a/src/transport/server/announcement_manager.rs b/src/transport/server/announcement_manager.rs index cd8d755..40f075a 100644 --- a/src/transport/server/announcement_manager.rs +++ b/src/transport/server/announcement_manager.rs @@ -365,7 +365,7 @@ impl AnnouncementManager { let filter = Filter::new().kind(Kind::Custom(kind)).author(pubkey); let events = self .relay_pool - .fetch_events(filter, Duration::from_secs(10)) + .fetch_events(vec![filter], Duration::from_secs(10)) .await?; if events.is_empty() { continue; @@ -1699,7 +1699,7 @@ mod tests { ] { let filter = Filter::new().kind(Kind::Custom(kind)).author(pubkey); let events = pool - .fetch_events(filter, Duration::from_secs(1)) + .fetch_events(vec![filter], Duration::from_secs(1)) .await .unwrap(); assert_eq!(events.len(), 1, "kind {kind} should have exactly 1 event"); @@ -1803,8 +1803,12 @@ mod tests { } self.inner.publish_to(urls, builder).await } - async fn fetch_events(&self, filter: Filter, timeout: Duration) -> Result> { - self.inner.fetch_events(filter, timeout).await + async fn fetch_events( + &self, + filters: Vec, + timeout: Duration, + ) -> Result> { + self.inner.fetch_events(filters, timeout).await } } diff --git a/tests/transport_integration.rs b/tests/transport_integration.rs index 45df053..bb9309a 100644 --- a/tests/transport_integration.rs +++ b/tests/transport_integration.rs @@ -131,10 +131,10 @@ impl RelayPoolTrait for TestRelayPool { async fn fetch_events( &self, - filter: Filter, + filters: Vec, timeout: Duration, ) -> contextvm_sdk::Result> { - self.inner.fetch_events(filter, timeout).await + self.inner.fetch_events(filters, timeout).await } }