commit f7ed72face51e5be82bf2c49c61a41a95472b861
parent f77c592056b6d85e403b8232fbc3d552bce20c05
Author: triesap <tyson@radroots.org>
Date: Sat, 21 Mar 2026 22:26:51 +0000
transport: add base nip46 listener
- add encrypted kind 24133 request and response plumbing on top of the local transport bootstrap
- register inbound connect sessions and serve get_public_key, ping, and switch_relays through the rr-rs signer manager
- wire the async runtime into a live relay listener path while auto-granting switch_relays on new connections
- validate with cargo fmt, cargo check, cargo test, cargo check --locked, cargo test --locked, and cargo fmt --check
Diffstat:
6 files changed, 622 insertions(+), 7 deletions(-)
diff --git a/Cargo.lock b/Cargo.lock
@@ -917,10 +917,13 @@ dependencies = [
name = "myc"
version = "0.1.0"
dependencies = [
+ "nostr",
"radroots-identity",
"radroots-nostr",
+ "radroots-nostr-connect",
"radroots-nostr-signer",
"serde",
+ "serde_json",
"tempfile",
"thiserror 2.0.18",
"tokio",
diff --git a/Cargo.toml b/Cargo.toml
@@ -14,12 +14,15 @@ resolver = "2"
unexpected_cfgs = { level = "warn", check-cfg = ['cfg(coverage_nightly)'] }
[dependencies]
+nostr = { version = "0.44.2", features = ["nip04", "nip44"] }
radroots-identity = { path = "../lib/crates/identity" }
radroots-nostr = { path = "../lib/crates/nostr", features = ["client"] }
+radroots-nostr-connect = { path = "../lib/crates/nostr-connect" }
radroots-nostr-signer = { path = "../lib/crates/nostr-signer" }
serde = { version = "1.0", features = ["derive"] }
+serde_json = "1.0"
thiserror = "2.0"
-tokio = { version = "1.48", features = ["macros", "rt-multi-thread"] }
+tokio = { version = "1.48", features = ["macros", "rt-multi-thread", "sync"] }
toml = "0.8"
tracing = "0.1"
tracing-subscriber = { version = "0.3", features = ["env-filter", "fmt"] }
diff --git a/src/app/runtime.rs b/src/app/runtime.rs
@@ -4,7 +4,7 @@ use std::sync::Arc;
use crate::config::MycConfig;
use crate::error::MycError;
-use crate::transport::{MycNostrTransport, MycTransportSnapshot};
+use crate::transport::{MycNip46Service, MycNostrTransport, MycTransportSnapshot};
use radroots_identity::{RadrootsIdentity, RadrootsIdentityPublic};
use radroots_nostr_signer::prelude::{RadrootsNostrFileSignerStore, RadrootsNostrSignerManager};
@@ -74,29 +74,33 @@ impl MycRuntime {
}
pub fn signer_identity(&self) -> &RadrootsIdentity {
- &self.signer.signer_identity
+ self.signer.signer_identity()
}
pub fn signer_public_identity(&self) -> RadrootsIdentityPublic {
- self.signer.signer_identity.to_public()
+ self.signer.signer_public_identity()
}
pub fn user_identity(&self) -> &RadrootsIdentity {
- &self.signer.user_identity
+ self.signer.user_identity()
}
pub fn user_public_identity(&self) -> RadrootsIdentityPublic {
- self.signer.user_identity.to_public()
+ self.signer.user_public_identity()
}
pub fn signer_manager(&self) -> &RadrootsNostrSignerManager {
- &self.signer.manager
+ self.signer.signer_manager()
}
pub fn transport(&self) -> Option<&MycNostrTransport> {
self.transport.as_ref()
}
+ pub(crate) fn signer_context(&self) -> MycSignerContext {
+ self.signer.clone()
+ }
+
pub fn snapshot(&self) -> MycStartupSnapshot {
let signer_public = self.signer.signer_identity.to_public();
let user_public = self.signer.user_identity.to_public();
@@ -138,6 +142,10 @@ impl MycRuntime {
transport_connect_timeout_secs = snapshot.transport.connect_timeout_secs,
"myc runtime bootstrapped"
);
+ if let Some(transport) = self.transport.clone() {
+ let service = MycNip46Service::new(self.signer_context(), transport);
+ return service.run().await;
+ }
Ok(())
}
@@ -168,6 +176,26 @@ impl MycRuntimePaths {
}
impl MycSignerContext {
+ pub fn signer_identity(&self) -> &RadrootsIdentity {
+ &self.signer_identity
+ }
+
+ pub fn signer_public_identity(&self) -> RadrootsIdentityPublic {
+ self.signer_identity.to_public()
+ }
+
+ pub fn user_identity(&self) -> &RadrootsIdentity {
+ &self.user_identity
+ }
+
+ pub fn user_public_identity(&self) -> RadrootsIdentityPublic {
+ self.user_identity.to_public()
+ }
+
+ pub fn signer_manager(&self) -> &RadrootsNostrSignerManager {
+ &self.manager
+ }
+
fn bootstrap(paths: &MycRuntimePaths) -> Result<Self, MycError> {
let signer_identity = RadrootsIdentity::load_from_path_auto(&paths.signer_identity_path)?;
let user_identity = RadrootsIdentity::load_from_path_auto(&paths.user_identity_path)?;
diff --git a/src/error.rs b/src/error.rs
@@ -1,6 +1,8 @@
use std::path::PathBuf;
use radroots_identity::IdentityError;
+use radroots_nostr::prelude::RadrootsNostrError;
+use radroots_nostr_connect::prelude::RadrootsNostrConnectError;
use radroots_nostr_signer::prelude::RadrootsNostrSignerError;
use thiserror::Error;
@@ -37,7 +39,17 @@ pub enum MycError {
#[error(transparent)]
Identity(#[from] IdentityError),
#[error(transparent)]
+ Nostr(#[from] RadrootsNostrError),
+ #[error(transparent)]
+ NostrConnect(#[from] RadrootsNostrConnectError),
+ #[error(transparent)]
SignerState(#[from] RadrootsNostrSignerError),
+ #[error("NIP-46 decrypt failed: {0}")]
+ Nip46Decrypt(String),
+ #[error("NIP-46 encrypt failed: {0}")]
+ Nip46Encrypt(String),
+ #[error("NIP-46 listener notifications closed")]
+ Nip46ListenerClosed,
#[error(
"configured signer identity `{configured_identity_id}` at {identity_path} does not match persisted signer identity `{persisted_identity_id}` in {state_path}"
)]
diff --git a/src/transport.rs b/src/transport.rs
@@ -1,9 +1,15 @@
+pub mod nip46;
+
+use std::time::Duration;
+
use radroots_identity::RadrootsIdentity;
use radroots_nostr::prelude::{RadrootsNostrClient, RadrootsNostrRelayUrl};
use crate::config::MycTransportConfig;
use crate::error::MycError;
+pub use nip46::{MycNip46Handler, MycNip46Service};
+
#[derive(Clone)]
pub struct MycNostrTransport {
client: RadrootsNostrClient,
@@ -46,6 +52,17 @@ impl MycNostrTransport {
self.connect_timeout_secs
}
+ pub async fn connect(&self) -> Result<(), MycError> {
+ for relay in &self.relays {
+ let _ = self.client.add_relay(relay.as_str()).await?;
+ }
+ self.client.connect().await;
+ self.client
+ .wait_for_connection(Duration::from_secs(self.connect_timeout_secs))
+ .await;
+ Ok(())
+ }
+
pub fn snapshot(&self) -> MycTransportSnapshot {
MycTransportSnapshot {
enabled: true,
diff --git a/src/transport/nip46.rs b/src/transport/nip46.rs
@@ -0,0 +1,552 @@
+use nostr::nips::nip44;
+use nostr::nips::nip44::Version;
+use radroots_nostr::prelude::{
+ RadrootsNostrEvent, RadrootsNostrEventBuilder, RadrootsNostrFilter, RadrootsNostrKind,
+ RadrootsNostrPublicKey, RadrootsNostrRelayPoolNotification, RadrootsNostrRelayUrl,
+ RadrootsNostrTag, RadrootsNostrTimestamp, radroots_nostr_filter_tag, radroots_nostr_kind,
+};
+use radroots_nostr_connect::prelude::{
+ RADROOTS_NOSTR_CONNECT_RPC_KIND, RadrootsNostrConnectMethod, RadrootsNostrConnectPermission,
+ RadrootsNostrConnectPermissions, RadrootsNostrConnectRequest,
+ RadrootsNostrConnectRequestMessage, RadrootsNostrConnectResponse,
+};
+use radroots_nostr_signer::prelude::{
+ RadrootsNostrSignerConnectEvaluation, RadrootsNostrSignerConnectionRecord,
+ RadrootsNostrSignerRequestAction, RadrootsNostrSignerRequestResponseHint,
+ RadrootsNostrSignerSessionLookup,
+};
+use tokio::sync::broadcast;
+
+use crate::app::MycSignerContext;
+use crate::error::MycError;
+use crate::transport::MycNostrTransport;
+
+#[derive(Clone)]
+pub struct MycNip46Handler {
+ signer: MycSignerContext,
+ relays: Vec<RadrootsNostrRelayUrl>,
+}
+
+pub struct MycNip46Service {
+ handler: MycNip46Handler,
+ transport: MycNostrTransport,
+}
+
+impl MycNip46Handler {
+ pub fn new(signer: MycSignerContext, relays: Vec<RadrootsNostrRelayUrl>) -> Self {
+ Self { signer, relays }
+ }
+
+ pub fn filter(&self) -> Result<RadrootsNostrFilter, MycError> {
+ let filter = RadrootsNostrFilter::new()
+ .kind(RadrootsNostrKind::Custom(RADROOTS_NOSTR_CONNECT_RPC_KIND))
+ .since(RadrootsNostrTimestamp::now());
+ radroots_nostr_filter_tag(
+ filter,
+ "p",
+ vec![self.signer.signer_public_identity().public_key_hex],
+ )
+ .map_err(Into::into)
+ }
+
+ pub fn parse_request_event(
+ &self,
+ event: &RadrootsNostrEvent,
+ ) -> Result<RadrootsNostrConnectRequestMessage, MycError> {
+ let decrypted = nip44::decrypt(
+ self.signer.signer_identity().keys().secret_key(),
+ &event.pubkey,
+ &event.content,
+ )
+ .map_err(|err| MycError::Nip46Decrypt(err.to_string()))?;
+ serde_json::from_str(&decrypted)
+ .map_err(radroots_nostr_connect::prelude::RadrootsNostrConnectError::from)
+ .map_err(Into::into)
+ }
+
+ pub fn build_response_event(
+ &self,
+ client_public_key: RadrootsNostrPublicKey,
+ request_id: impl Into<String>,
+ response: RadrootsNostrConnectResponse,
+ ) -> Result<RadrootsNostrEventBuilder, MycError> {
+ let envelope = response.into_envelope(request_id.into())?;
+ let payload = serde_json::to_string(&envelope)
+ .map_err(|err| MycError::Nip46Encrypt(err.to_string()))?;
+ let ciphertext = nip44::encrypt(
+ self.signer.signer_identity().keys().secret_key(),
+ &client_public_key,
+ payload,
+ Version::V2,
+ )
+ .map_err(|err| MycError::Nip46Encrypt(err.to_string()))?;
+
+ Ok(RadrootsNostrEventBuilder::new(
+ radroots_nostr_kind(RADROOTS_NOSTR_CONNECT_RPC_KIND),
+ ciphertext,
+ )
+ .tags(vec![RadrootsNostrTag::public_key(client_public_key)]))
+ }
+
+ pub fn handle_request(
+ &self,
+ client_public_key: RadrootsNostrPublicKey,
+ request_message: RadrootsNostrConnectRequestMessage,
+ ) -> Result<RadrootsNostrConnectResponse, MycError> {
+ match request_message.request.clone() {
+ RadrootsNostrConnectRequest::Connect { secret, .. } => {
+ self.handle_connect_request(client_public_key, request_message.request, secret)
+ }
+ RadrootsNostrConnectRequest::GetPublicKey
+ | RadrootsNostrConnectRequest::Ping
+ | RadrootsNostrConnectRequest::SwitchRelays => {
+ self.handle_base_request(client_public_key, request_message)
+ }
+ _ => Ok(RadrootsNostrConnectResponse::Error {
+ result: None,
+ error: format!(
+ "method `{}` is not implemented yet",
+ request_message.request.method()
+ ),
+ }),
+ }
+ }
+
+ fn handle_connect_request(
+ &self,
+ client_public_key: RadrootsNostrPublicKey,
+ request: RadrootsNostrConnectRequest,
+ secret: Option<String>,
+ ) -> Result<RadrootsNostrConnectResponse, MycError> {
+ let evaluation = self
+ .signer
+ .signer_manager()
+ .evaluate_connect_request(client_public_key, request)?;
+
+ match evaluation {
+ RadrootsNostrSignerConnectEvaluation::ExistingConnection(_) => {
+ Ok(connect_response(secret))
+ }
+ RadrootsNostrSignerConnectEvaluation::RegistrationRequired(proposal) => {
+ let draft = proposal
+ .into_connection_draft(self.signer.user_public_identity())
+ .with_relays(self.relays.clone());
+ let connection = self.signer.signer_manager().register_connection(draft)?;
+ let granted_permissions =
+ grant_permissions_for_new_connection(connection.requested_permissions.clone());
+ let _ = self
+ .signer
+ .signer_manager()
+ .set_granted_permissions(&connection.connection_id, granted_permissions)?;
+ Ok(connect_response(secret))
+ }
+ }
+ }
+
+ fn handle_base_request(
+ &self,
+ client_public_key: RadrootsNostrPublicKey,
+ request_message: RadrootsNostrConnectRequestMessage,
+ ) -> Result<RadrootsNostrConnectResponse, MycError> {
+ let connection = match self
+ .signer
+ .signer_manager()
+ .lookup_session(&client_public_key, None)?
+ {
+ RadrootsNostrSignerSessionLookup::Connection(connection) => connection,
+ RadrootsNostrSignerSessionLookup::None => {
+ return Ok(RadrootsNostrConnectResponse::Error {
+ result: None,
+ error: "unauthorized".to_owned(),
+ });
+ }
+ RadrootsNostrSignerSessionLookup::Ambiguous(_) => {
+ return Ok(RadrootsNostrConnectResponse::Error {
+ result: None,
+ error: "ambiguous client sessions".to_owned(),
+ });
+ }
+ };
+
+ let evaluation = self
+ .signer
+ .signer_manager()
+ .evaluate_request(&connection.connection_id, request_message)?;
+
+ match evaluation.action {
+ RadrootsNostrSignerRequestAction::Denied { reason } => {
+ Ok(RadrootsNostrConnectResponse::Error {
+ result: None,
+ error: reason,
+ })
+ }
+ RadrootsNostrSignerRequestAction::Challenged { auth_challenge, .. } => Ok(
+ RadrootsNostrConnectResponse::AuthUrl(auth_challenge.auth_url),
+ ),
+ RadrootsNostrSignerRequestAction::Allowed { response_hint, .. } => {
+ response_from_hint(&evaluation.connection, response_hint)
+ }
+ }
+ }
+}
+
+impl MycNip46Service {
+ pub fn new(signer: MycSignerContext, transport: MycNostrTransport) -> Self {
+ let handler = MycNip46Handler::new(signer, transport.relays().to_vec());
+ Self { handler, transport }
+ }
+
+ pub async fn run(&self) -> Result<(), MycError> {
+ self.transport.connect().await?;
+
+ let filter = self.handler.filter()?;
+ let mut notifications = self.transport.client().notifications();
+ let subscription = self.transport.client().subscribe(filter, None).await?;
+ tracing::info!(
+ subscription_id = %subscription.val,
+ relay_count = self.transport.relays().len(),
+ "myc NIP-46 listener subscribed"
+ );
+
+ loop {
+ let notification = match notifications.recv().await {
+ Ok(notification) => notification,
+ Err(broadcast::error::RecvError::Lagged(_)) => continue,
+ Err(broadcast::error::RecvError::Closed) => {
+ return Err(MycError::Nip46ListenerClosed);
+ }
+ };
+ let RadrootsNostrRelayPoolNotification::Event { event, .. } = notification else {
+ continue;
+ };
+ let event = *event;
+ if event.kind != RadrootsNostrKind::Custom(RADROOTS_NOSTR_CONNECT_RPC_KIND) {
+ continue;
+ }
+
+ let request_message = match self.handler.parse_request_event(&event) {
+ Ok(message) => message,
+ Err(error) => {
+ tracing::warn!(error = %error, "discarding invalid NIP-46 request event");
+ continue;
+ }
+ };
+
+ let request_id = request_message.id.clone();
+ let response = match self.handler.handle_request(event.pubkey, request_message) {
+ Ok(response) => response,
+ Err(error) => {
+ tracing::warn!(error = %error, "failed to handle NIP-46 request");
+ RadrootsNostrConnectResponse::Error {
+ result: None,
+ error: error.to_string(),
+ }
+ }
+ };
+
+ let response_event =
+ self.handler
+ .build_response_event(event.pubkey, request_id, response)?;
+ if let Err(error) = self
+ .transport
+ .client()
+ .send_event_builder(response_event)
+ .await
+ {
+ tracing::warn!(error = %error, "failed to publish NIP-46 response");
+ }
+ }
+ }
+}
+
+fn connect_response(secret: Option<String>) -> RadrootsNostrConnectResponse {
+ match secret {
+ Some(secret) => RadrootsNostrConnectResponse::ConnectSecretEcho(secret),
+ None => RadrootsNostrConnectResponse::ConnectAcknowledged,
+ }
+}
+
+fn grant_permissions_for_new_connection(
+ requested_permissions: RadrootsNostrConnectPermissions,
+) -> RadrootsNostrConnectPermissions {
+ let mut granted = requested_permissions.into_vec();
+ let switch_relays =
+ RadrootsNostrConnectPermission::new(RadrootsNostrConnectMethod::SwitchRelays);
+ if !granted.contains(&switch_relays) {
+ granted.push(switch_relays);
+ }
+ granted.sort();
+ granted.dedup();
+ granted.into()
+}
+
+fn response_from_hint(
+ connection: &RadrootsNostrSignerConnectionRecord,
+ hint: RadrootsNostrSignerRequestResponseHint,
+) -> Result<RadrootsNostrConnectResponse, MycError> {
+ Ok(match hint {
+ RadrootsNostrSignerRequestResponseHint::Pong => RadrootsNostrConnectResponse::Pong,
+ RadrootsNostrSignerRequestResponseHint::UserPublicKey(public_key) => {
+ RadrootsNostrConnectResponse::UserPublicKey(public_key)
+ }
+ RadrootsNostrSignerRequestResponseHint::RelayList(relays) => {
+ if relays == connection.relays {
+ RadrootsNostrConnectResponse::RelayList(relays)
+ } else {
+ RadrootsNostrConnectResponse::RelayList(connection.relays.clone())
+ }
+ }
+ RadrootsNostrSignerRequestResponseHint::None => RadrootsNostrConnectResponse::Error {
+ result: None,
+ error: "request evaluation did not provide a response hint".to_owned(),
+ },
+ })
+}
+
+#[cfg(test)]
+mod tests {
+ use nostr::nips::nip44;
+ use nostr::nips::nip44::Version;
+ use nostr::{EventBuilder, Keys, PublicKey, SecretKey};
+ use radroots_nostr::prelude::{RadrootsNostrTag, radroots_nostr_kind};
+ use radroots_nostr_connect::prelude::{
+ RADROOTS_NOSTR_CONNECT_RPC_KIND, RadrootsNostrConnectMethod,
+ RadrootsNostrConnectPermission, RadrootsNostrConnectRequest,
+ RadrootsNostrConnectRequestMessage, RadrootsNostrConnectResponse,
+ RadrootsNostrConnectResponseEnvelope,
+ };
+
+ use crate::app::MycRuntime;
+ use crate::config::MycConfig;
+
+ use super::MycNip46Handler;
+
+ fn write_identity(path: &std::path::Path, secret_key: &str) {
+ radroots_identity::RadrootsIdentity::from_secret_key_str(secret_key)
+ .expect("identity")
+ .save_json(path)
+ .expect("save identity");
+ }
+
+ fn runtime() -> MycRuntime {
+ let temp = tempfile::tempdir().expect("tempdir");
+ let mut config = MycConfig::default();
+ config.paths.state_dir = temp.path().join("state");
+ config.paths.signer_identity_path = temp.path().join("signer.json");
+ config.paths.user_identity_path = temp.path().join("user.json");
+ config.transport.enabled = true;
+ config.transport.connect_timeout_secs = 15;
+ config.transport.relays = vec!["wss://relay.example.com".to_owned()];
+ write_identity(
+ &config.paths.signer_identity_path,
+ "1111111111111111111111111111111111111111111111111111111111111111",
+ );
+ write_identity(
+ &config.paths.user_identity_path,
+ "2222222222222222222222222222222222222222222222222222222222222222",
+ );
+ MycRuntime::bootstrap(config).expect("runtime")
+ }
+
+ fn handler(runtime: &MycRuntime) -> MycNip46Handler {
+ MycNip46Handler::new(
+ runtime.signer_context(),
+ runtime.transport().expect("transport").relays().to_vec(),
+ )
+ }
+
+ fn client_keys() -> Keys {
+ let secret =
+ SecretKey::from_hex("3333333333333333333333333333333333333333333333333333333333333333")
+ .expect("secret");
+ Keys::new(secret)
+ }
+
+ fn request_event(
+ handler: &MycNip46Handler,
+ request: RadrootsNostrConnectRequestMessage,
+ ) -> nostr::Event {
+ let client_keys = client_keys();
+ let payload = serde_json::to_string(&request).expect("serialize request");
+ let ciphertext = nip44::encrypt(
+ client_keys.secret_key(),
+ &PublicKey::parse(
+ handler
+ .signer
+ .signer_public_identity()
+ .public_key_hex
+ .as_str(),
+ )
+ .expect("signer pubkey"),
+ payload,
+ Version::V2,
+ )
+ .expect("encrypt");
+ EventBuilder::new(
+ radroots_nostr_kind(RADROOTS_NOSTR_CONNECT_RPC_KIND),
+ ciphertext,
+ )
+ .tags(vec![RadrootsNostrTag::public_key(
+ handler.signer.signer_identity().public_key(),
+ )])
+ .sign_with_keys(&client_keys)
+ .expect("sign request")
+ }
+
+ #[test]
+ fn parse_and_build_nip46_envelopes_roundtrip() {
+ let runtime = runtime();
+ let handler = handler(&runtime);
+ let request =
+ RadrootsNostrConnectRequestMessage::new("req-1", RadrootsNostrConnectRequest::Ping);
+ let event = request_event(&handler, request.clone());
+
+ let parsed = handler.parse_request_event(&event).expect("parse request");
+ assert_eq!(parsed, request);
+
+ let response_builder = handler
+ .build_response_event(event.pubkey, "req-1", RadrootsNostrConnectResponse::Pong)
+ .expect("response builder");
+ let response_event = response_builder
+ .sign_with_keys(runtime.signer_identity().keys())
+ .expect("sign response");
+ let decrypted = nip44::decrypt(
+ client_keys().secret_key(),
+ &runtime.signer_identity().public_key(),
+ &response_event.content,
+ )
+ .expect("decrypt response");
+ let envelope: RadrootsNostrConnectResponseEnvelope =
+ serde_json::from_str(&decrypted).expect("parse envelope");
+ let parsed = RadrootsNostrConnectResponse::from_envelope(
+ &RadrootsNostrConnectRequest::Ping.method(),
+ envelope,
+ )
+ .expect("parse response");
+ assert_eq!(parsed, RadrootsNostrConnectResponse::Pong);
+ }
+
+ #[test]
+ fn connect_registers_client_and_echoes_secret() {
+ let runtime = runtime();
+ let handler = handler(&runtime);
+ let response = handler
+ .handle_request(
+ client_keys().public_key(),
+ RadrootsNostrConnectRequestMessage::new(
+ "req-connect",
+ RadrootsNostrConnectRequest::Connect {
+ remote_signer_public_key: runtime.signer_identity().public_key(),
+ secret: Some("s3cr3t".to_owned()),
+ requested_permissions: Default::default(),
+ },
+ ),
+ )
+ .expect("connect response");
+
+ assert_eq!(
+ response,
+ RadrootsNostrConnectResponse::ConnectSecretEcho("s3cr3t".to_owned())
+ );
+ let connections = runtime
+ .signer_manager()
+ .list_connections()
+ .expect("connections");
+ assert_eq!(connections.len(), 1);
+ assert_eq!(
+ connections[0].user_identity.id.to_string(),
+ runtime.user_public_identity().id.to_string()
+ );
+ assert_eq!(connections[0].relays.len(), 1);
+ }
+
+ #[test]
+ fn base_methods_return_spec_results_after_connect() {
+ let runtime = runtime();
+ let handler = handler(&runtime);
+ handler
+ .handle_request(
+ client_keys().public_key(),
+ RadrootsNostrConnectRequestMessage::new(
+ "req-connect",
+ RadrootsNostrConnectRequest::Connect {
+ remote_signer_public_key: runtime.signer_identity().public_key(),
+ secret: None,
+ requested_permissions: Default::default(),
+ },
+ ),
+ )
+ .expect("connect");
+
+ let public_key = handler
+ .handle_request(
+ client_keys().public_key(),
+ RadrootsNostrConnectRequestMessage::new(
+ "req-pubkey",
+ RadrootsNostrConnectRequest::GetPublicKey,
+ ),
+ )
+ .expect("get public key");
+ assert_eq!(
+ public_key,
+ RadrootsNostrConnectResponse::UserPublicKey(runtime.user_identity().public_key())
+ );
+
+ let pong = handler
+ .handle_request(
+ client_keys().public_key(),
+ RadrootsNostrConnectRequestMessage::new(
+ "req-ping",
+ RadrootsNostrConnectRequest::Ping,
+ ),
+ )
+ .expect("ping");
+ assert_eq!(pong, RadrootsNostrConnectResponse::Pong);
+
+ let relays = handler
+ .handle_request(
+ client_keys().public_key(),
+ RadrootsNostrConnectRequestMessage::new(
+ "req-switch",
+ RadrootsNostrConnectRequest::SwitchRelays,
+ ),
+ )
+ .expect("switch relays");
+ assert_eq!(
+ relays,
+ RadrootsNostrConnectResponse::RelayList(
+ runtime.transport().expect("transport").relays().to_vec()
+ )
+ );
+ }
+
+ #[test]
+ fn new_connections_auto_grant_switch_relays() {
+ let runtime = runtime();
+ let handler = handler(&runtime);
+ handler
+ .handle_request(
+ client_keys().public_key(),
+ RadrootsNostrConnectRequestMessage::new(
+ "req-connect",
+ RadrootsNostrConnectRequest::Connect {
+ remote_signer_public_key: runtime.signer_identity().public_key(),
+ secret: None,
+ requested_permissions: Default::default(),
+ },
+ ),
+ )
+ .expect("connect");
+
+ let connection = runtime
+ .signer_manager()
+ .list_connections()
+ .expect("connections")
+ .into_iter()
+ .next()
+ .expect("connection");
+ assert!(connection.granted_permissions().as_slice().contains(
+ &RadrootsNostrConnectPermission::new(RadrootsNostrConnectMethod::SwitchRelays,)
+ ));
+ }
+}