Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ license = "MIT"
rust-version = "1.85.0"

[workspace.dependencies]
async-utility = { git = "https://github.com/yukibtc/async-utility", rev = "7d3a79983d0e848aafed9d321637af7334e01349" }
async-utility = { git = "https://github.com/yukibtc/async-utility", rev = "b323ae0c1c1424b78bf8bb8832b95e86771f1dee" }
atomic-destructor = { version = "0.3", default-features = false }
base64 = { version = "0.22", default-features = false }
bitcoin_hashes = { version = "1.1", default-features = false }
Expand Down
8 changes: 8 additions & 0 deletions nostr-sdk/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,11 @@
- Redesign `Relay::count_events` API
- Redesign `Client::try_connect_relay` API
- Return `NotificationStream<T>` from client and relay `notifications()` methods instead of a boxed stream.
- Remove `RelayStatus::Pending` and rename `RelayStatus::Terminated` to `RelayStatus::Idle` (https://github.com/nostrdevkit/nostr/pull/1491).

### Changed

- `Relay::try_connect` waits for an ongoing attempt or scheduled retry (https://github.com/nostrdevkit/nostr/pull/1491).

### Added

Expand All @@ -50,6 +55,9 @@
- Preserve the broadcast lag or closure cause in event `OK` and authentication waiters instead of returning a generic premature-exit error.
- Clean up NIP-77 request-owned subscriptions when reconciliation fails or is canceled.
- Fail NIP-77 reconciliation when a download batch is rejected or closes before its requested events arrive.
- Retain reconnect requests while a previous WebSocket closes (https://github.com/nostrdevkit/nostr/pull/1491).
- Allow banning and shutting down idle relays (https://github.com/nostrdevkit/nostr/pull/1491).
- Restore subscriptions after reconnections within the same second (https://github.com/nostrdevkit/nostr/pull/1491).

### Deprecated

Expand Down
87 changes: 87 additions & 0 deletions nostr-sdk/src/client/api/send_event.rs
Original file line number Diff line number Diff line change
Expand Up @@ -446,6 +446,8 @@ where

#[cfg(test)]
mod tests {
use std::time::Duration;

use nostr::nips::nip17::InboxRelayList;
use nostr::nips::nip65::RelayList;
use nostr::prelude::*;
Expand All @@ -454,9 +456,12 @@ mod tests {
use nostr_gossip_memory::store::NostrGossipMemory;

use super::*;
use crate::authenticator::SignerAuthenticator;
use crate::client::{GossipConfig, GossipRelayLimits};
use crate::error::ErrorKind;
use crate::local_relay::*;
use crate::relay::RelayStatus;
use crate::test_utils::{setup_client, setup_client_with_authenticator};

#[tokio::test]
async fn unverified_event_is_rejected_before_sending() {
Expand Down Expand Up @@ -779,4 +784,86 @@ mod tests {
assert_eq!(err.kind(), ErrorKind::State);
assert_eq!(err.to_string(), "gossip not configured");
}

#[tokio::test]
async fn auth_gated_clients_keep_independent_signers_across_removal_and_reconnect() {
let local = LocalRelay::builder()
.nip42(LocalRelayBuilderNip42::write())
.build();
local.run().await.unwrap();

let url = local.url().await;

let first_keys = Keys::generate();
let second_keys = Keys::generate();

let first: Client = setup_client_with_authenticator(
url.clone(),
SignerAuthenticator::new(first_keys.clone()),
)
.await;
let second: Client = setup_client_with_authenticator(
url.clone(),
SignerAuthenticator::new(second_keys.clone()),
)
.await;
let unauthenticated: Client = setup_client(url.clone()).await;

for (client, keys, content) in [
(&first, &first_keys, "first account"),
(&second, &second_keys, "second account"),
] {
let event = EventBuilder::new(Kind::TextNote, content)
.tag(Tag::protected())
.finalize(keys)
.unwrap();

let output = client
.send_event(&event)
.ok_timeout(Duration::from_secs(2))
.authentication_timeout(Duration::from_secs(2))
.await
.unwrap();

assert!(output.failed.is_empty());
assert!(output.success.get(&url).unwrap().is_ack());
}

let unauthenticated_event = EventBuilder::new(Kind::TextNote, "no signer")
.tag(Tag::protected())
.finalize(&Keys::generate())
.unwrap();
let output = unauthenticated
.send_event(&unauthenticated_event)
.ok_timeout(Duration::from_secs(2))
.await
.unwrap();

assert!(output.success.is_empty());
assert!(output.failed.contains_key(&url));

first.remove_relay(&url).force().await.unwrap();

second.disconnect().await;
second.connect().and_wait(Duration::from_secs(3)).await;

assert_eq!(
second.relay(&url).await.unwrap().unwrap().status(),
RelayStatus::Connected
);

let event = EventBuilder::new(Kind::TextNote, "second account after reconnect")
.tag(Tag::protected())
.finalize(&second_keys)
.unwrap();
let output = second
.send_event(&event)
.ok_timeout(Duration::from_secs(2))
.authentication_timeout(Duration::from_secs(2))
.await
.unwrap();

assert!(output.failed.is_empty(), "{:?}", output.failed);
assert!(output.success.get(&url).unwrap().is_ack());
}
}
36 changes: 16 additions & 20 deletions nostr-sdk/src/client/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -439,8 +439,8 @@ impl Client {
/// If the relay is already connected or currently attempting to connect,
/// this method does nothing.
///
/// If the relay is disconnected, sleeping, or otherwise inactive, a
/// background task is spawned to initiate a connection.
/// If the relay is sleeping or otherwise inactive, its persistent
/// connection task is started or woken to initiate a connection.
///
/// For further details, see the documentation of [`Relay::connect`].
///
Expand Down Expand Up @@ -468,16 +468,14 @@ impl Client {
/// Attempts to initiate a connection with a single relay.
///
/// At most **one connection per relay** is allowed at any time.
/// If the relay is already connected or currently attempting to connect,
/// this method does nothing.
/// Returns immediately if already connected; otherwise waits for the current
/// attempt or next scheduled retry, or requests a connection if inactive.
///
/// If the initial connection attempt succeeds, a background task is spawned
/// to maintain the connection and handle future reconnections.
/// If the initial attempt fails, no background task is spawned and no
/// automatic retries are scheduled.
/// A newly requested attempt that fails does not schedule automatic retries.
/// The connection task remains available until ban or shutdown.
/// Dropping the future only stops waiting for the result.
///
/// Use [`Client::connect_relay`] if you want to always spawn a background
/// connection task, regardless of whether the initial attempt succeeds.
/// Use [`Client::connect_relay`] to enable retries after an initial failure.
///
/// For further details, see the documentation of [`Relay::try_connect`].
///
Expand Down Expand Up @@ -522,8 +520,8 @@ impl Client {
/// If a relay is already connected or currently attempting to connect,
/// this method does nothing for that relay.
///
/// If a relay is disconnected, sleeping, or otherwise inactive, a
/// background task is spawned to initiate a connection.
/// If a relay is sleeping or otherwise inactive, its persistent
/// connection task is started or woken to initiate a connection.
///
/// For further details, see the documentation of [`Relay::connect`].
///
Expand Down Expand Up @@ -561,16 +559,14 @@ impl Client {
/// Attempts to initiate a connection with relays.
///
/// At most **one connection per relay** is allowed at any time.
/// If a relay is already connected or currently attempting to connect,
/// this method does nothing for that relay.
/// Returns immediately for connected relays. For other relays, waits for
/// the current attempt or next scheduled retry, or requests a connection if inactive.
///
/// If the initial connection attempt succeeds, a background task is spawned
/// to maintain the connection and handle future reconnections.
/// If the initial attempt fails, no background task is spawned and no
/// automatic retries are scheduled.
/// A newly requested attempt that fails does not schedule automatic retries.
/// Connection tasks remain available until ban or shutdown.
/// Dropping the future only stops waiting for results.
///
/// Use [`Client::connect`] if you want to always spawn a background
/// connection task, regardless of whether the initial attempt succeeds.
/// Use [`Client::connect`] to enable retries after an initial failure.
///
/// For further details, see the documentation of [`Relay::try_connect`].
///
Expand Down
1 change: 1 addition & 0 deletions nostr-sdk/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ mod future;
#[cfg(any(feature = "local-relay", test))]
pub mod local_relay;
pub mod monitor;
mod mutex;
pub mod policy;
mod pool;
pub mod prelude;
Expand Down
21 changes: 21 additions & 0 deletions nostr-sdk/src/mutex.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
pub(crate) use std::sync::MutexGuard as NonPoisoningMutexGuard;
use std::sync::{Mutex, PoisonError};

/// Adapter for `std::Mutex` that removes the poisoning aspects
/// from its API.
#[derive(Debug)]
pub(crate) struct NonPoisoningMutex<T>(Mutex<T>)
where
T: ?Sized;

impl<T> NonPoisoningMutex<T> {
#[inline]
pub(crate) fn new(value: T) -> Self {
Self(Mutex::new(value))
}

#[inline]
pub(crate) fn lock(&self) -> NonPoisoningMutexGuard<'_, T> {
self.0.lock().unwrap_or_else(PoisonError::into_inner)
}
}
2 changes: 1 addition & 1 deletion nostr-sdk/src/relay/api/count_events.rs
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,7 @@ async fn receive_count_reply(
}
_ => {}
},
RelayNotification::RelayStatus { status } if status.is_disconnected() => {
RelayNotification::RelayStatus { status } if status.is_connection_closed() => {
return Err(Error::not_connected());
}
_ => {}
Expand Down
2 changes: 1 addition & 1 deletion nostr-sdk/src/relay/api/send_event.rs
Original file line number Diff line number Diff line change
Expand Up @@ -206,7 +206,7 @@ async fn wait_for_authentication(
RelayNotification::AuthenticationFailed => {
return Err(Error::authentication_msg("authentication failed"));
}
RelayNotification::RelayStatus { status } if status.is_disconnected() => {
RelayNotification::RelayStatus { status } if status.is_connection_closed() => {
return Err(Error::not_connected());
}
_ => (),
Expand Down
2 changes: 1 addition & 1 deletion nostr-sdk/src/relay/api/sync.rs
Original file line number Diff line number Diff line change
Expand Up @@ -607,7 +607,7 @@ pub(super) async fn sync(
last_relevant_msg = Instant::now();
}
}
RelayNotification::RelayStatus { status } if status.is_disconnected() => {
RelayNotification::RelayStatus { status } if status.is_connection_closed() => {
return Err(Error::not_connected());
}
_ => (),
Expand Down
Loading
Loading