lib

Core libraries for Radroots
git clone https://radroots.dev/git/lib.git
Log | Files | Refs | README | LICENSE

message.rs (23415B)


      1 use crate::error::RadrootsNostrConnectError;
      2 use crate::method::RadrootsNostrConnectMethod;
      3 use crate::permission::RadrootsNostrConnectPermissions;
      4 use nostr::{Event, JsonUtil, PublicKey, RelayUrl, UnsignedEvent};
      5 use serde::{Deserialize, Deserializer, Serialize, Serializer};
      6 use serde_json::{Value, json};
      7 use std::str::FromStr;
      8 use url::Url;
      9 
     10 pub const RADROOTS_NOSTR_CONNECT_RPC_KIND: u16 = 24_133;
     11 
     12 #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
     13 pub struct RadrootsNostrConnectRemoteSessionCapability {
     14     pub user_public_key: PublicKey,
     15     pub relays: Vec<RelayUrl>,
     16     pub permissions: RadrootsNostrConnectPermissions,
     17 }
     18 
     19 #[derive(Debug, Clone, PartialEq, Eq)]
     20 pub enum RadrootsNostrConnectRequest {
     21     Connect {
     22         remote_signer_public_key: PublicKey,
     23         secret: Option<String>,
     24         requested_permissions: RadrootsNostrConnectPermissions,
     25     },
     26     GetPublicKey,
     27     GetSessionCapability,
     28     SignEvent(UnsignedEvent),
     29     Nip04Encrypt {
     30         public_key: PublicKey,
     31         plaintext: String,
     32     },
     33     Nip04Decrypt {
     34         public_key: PublicKey,
     35         ciphertext: String,
     36     },
     37     Nip44Encrypt {
     38         public_key: PublicKey,
     39         plaintext: String,
     40     },
     41     Nip44Decrypt {
     42         public_key: PublicKey,
     43         ciphertext: String,
     44     },
     45     Ping,
     46     SwitchRelays,
     47     Custom {
     48         method: RadrootsNostrConnectMethod,
     49         params: Vec<String>,
     50     },
     51 }
     52 
     53 impl RadrootsNostrConnectRequest {
     54     pub fn method(&self) -> RadrootsNostrConnectMethod {
     55         match self {
     56             Self::Connect { .. } => RadrootsNostrConnectMethod::Connect,
     57             Self::GetPublicKey => RadrootsNostrConnectMethod::GetPublicKey,
     58             Self::GetSessionCapability => RadrootsNostrConnectMethod::GetSessionCapability,
     59             Self::SignEvent(_) => RadrootsNostrConnectMethod::SignEvent,
     60             Self::Nip04Encrypt { .. } => RadrootsNostrConnectMethod::Nip04Encrypt,
     61             Self::Nip04Decrypt { .. } => RadrootsNostrConnectMethod::Nip04Decrypt,
     62             Self::Nip44Encrypt { .. } => RadrootsNostrConnectMethod::Nip44Encrypt,
     63             Self::Nip44Decrypt { .. } => RadrootsNostrConnectMethod::Nip44Decrypt,
     64             Self::Ping => RadrootsNostrConnectMethod::Ping,
     65             Self::SwitchRelays => RadrootsNostrConnectMethod::SwitchRelays,
     66             Self::Custom { method, .. } => method.clone(),
     67         }
     68     }
     69 
     70     pub fn to_params(&self) -> Vec<String> {
     71         match self {
     72             Self::Connect {
     73                 remote_signer_public_key,
     74                 secret,
     75                 requested_permissions,
     76             } => {
     77                 let mut params = vec![remote_signer_public_key.to_hex()];
     78                 let normalized_secret = secret.as_ref().filter(|value| !value.is_empty()).cloned();
     79                 if normalized_secret.is_some() || !requested_permissions.is_empty() {
     80                     params.push(normalized_secret.unwrap_or_default());
     81                 }
     82                 if !requested_permissions.is_empty() {
     83                     params.push(requested_permissions.to_string());
     84                 }
     85                 params
     86             }
     87             Self::GetPublicKey | Self::GetSessionCapability | Self::Ping | Self::SwitchRelays => {
     88                 Vec::new()
     89             }
     90             Self::SignEvent(unsigned_event) => vec![unsigned_event.as_json()],
     91             Self::Nip04Encrypt {
     92                 public_key,
     93                 plaintext,
     94             }
     95             | Self::Nip44Encrypt {
     96                 public_key,
     97                 plaintext,
     98             } => vec![public_key.to_hex(), plaintext.clone()],
     99             Self::Nip04Decrypt {
    100                 public_key,
    101                 ciphertext,
    102             }
    103             | Self::Nip44Decrypt {
    104                 public_key,
    105                 ciphertext,
    106             } => vec![public_key.to_hex(), ciphertext.clone()],
    107             Self::Custom { params, .. } => params.clone(),
    108         }
    109     }
    110 
    111     pub fn from_parts(
    112         method: RadrootsNostrConnectMethod,
    113         params: Vec<String>,
    114     ) -> Result<Self, RadrootsNostrConnectError> {
    115         match method {
    116             RadrootsNostrConnectMethod::Connect => {
    117                 if params.is_empty() || params.len() > 3 {
    118                     return Err(RadrootsNostrConnectError::InvalidParams {
    119                         method: method.to_string(),
    120                         expected: "1 to 3 params",
    121                         received: params.len(),
    122                     });
    123                 }
    124                 let remote_signer_public_key = parse_public_key(&params[0])?;
    125                 let secret = params
    126                     .get(1)
    127                     .cloned()
    128                     .and_then(|value| if value.is_empty() { None } else { Some(value) });
    129                 let requested_permissions = match params.get(2) {
    130                     Some(value) => RadrootsNostrConnectPermissions::from_str(value)?,
    131                     None => RadrootsNostrConnectPermissions::default(),
    132                 };
    133                 Ok(Self::Connect {
    134                     remote_signer_public_key,
    135                     secret,
    136                     requested_permissions,
    137                 })
    138             }
    139             RadrootsNostrConnectMethod::GetPublicKey => {
    140                 expect_param_count(&method, &params, 0)?;
    141                 Ok(Self::GetPublicKey)
    142             }
    143             RadrootsNostrConnectMethod::GetSessionCapability => {
    144                 expect_param_count(&method, &params, 0)?;
    145                 Ok(Self::GetSessionCapability)
    146             }
    147             RadrootsNostrConnectMethod::SignEvent => {
    148                 expect_param_count(&method, &params, 1)?;
    149                 let unsigned_event = serde_json::from_str(&params[0]).map_err(|error| {
    150                     RadrootsNostrConnectError::InvalidRequestPayload {
    151                         method: method.to_string(),
    152                         reason: error.to_string(),
    153                     }
    154                 })?;
    155                 Ok(Self::SignEvent(unsigned_event))
    156             }
    157             RadrootsNostrConnectMethod::Nip04Encrypt => {
    158                 expect_param_count(&method, &params, 2)?;
    159                 Ok(Self::Nip04Encrypt {
    160                     public_key: parse_public_key(&params[0])?,
    161                     plaintext: params[1].clone(),
    162                 })
    163             }
    164             RadrootsNostrConnectMethod::Nip04Decrypt => {
    165                 expect_param_count(&method, &params, 2)?;
    166                 Ok(Self::Nip04Decrypt {
    167                     public_key: parse_public_key(&params[0])?,
    168                     ciphertext: params[1].clone(),
    169                 })
    170             }
    171             RadrootsNostrConnectMethod::Nip44Encrypt => {
    172                 expect_param_count(&method, &params, 2)?;
    173                 Ok(Self::Nip44Encrypt {
    174                     public_key: parse_public_key(&params[0])?,
    175                     plaintext: params[1].clone(),
    176                 })
    177             }
    178             RadrootsNostrConnectMethod::Nip44Decrypt => {
    179                 expect_param_count(&method, &params, 2)?;
    180                 Ok(Self::Nip44Decrypt {
    181                     public_key: parse_public_key(&params[0])?,
    182                     ciphertext: params[1].clone(),
    183                 })
    184             }
    185             RadrootsNostrConnectMethod::Ping => {
    186                 expect_param_count(&method, &params, 0)?;
    187                 Ok(Self::Ping)
    188             }
    189             RadrootsNostrConnectMethod::SwitchRelays => {
    190                 expect_param_count(&method, &params, 0)?;
    191                 Ok(Self::SwitchRelays)
    192             }
    193             custom => Ok(Self::Custom {
    194                 method: custom,
    195                 params,
    196             }),
    197         }
    198     }
    199 }
    200 
    201 #[derive(Debug, Clone, PartialEq, Eq)]
    202 pub struct RadrootsNostrConnectRequestMessage {
    203     pub id: String,
    204     pub request: RadrootsNostrConnectRequest,
    205 }
    206 
    207 impl RadrootsNostrConnectRequestMessage {
    208     pub fn new(id: impl Into<String>, request: RadrootsNostrConnectRequest) -> Self {
    209         Self {
    210             id: id.into(),
    211             request,
    212         }
    213     }
    214 
    215     fn into_raw(self) -> RawRequestMessage {
    216         RawRequestMessage {
    217             id: self.id,
    218             method: self.request.method(),
    219             params: self.request.to_params(),
    220         }
    221     }
    222 
    223     fn from_raw(raw: RawRequestMessage) -> Result<Self, RadrootsNostrConnectError> {
    224         Ok(Self {
    225             id: raw.id,
    226             request: RadrootsNostrConnectRequest::from_parts(raw.method, raw.params)?,
    227         })
    228     }
    229 }
    230 
    231 impl Serialize for RadrootsNostrConnectRequestMessage {
    232     fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
    233     where
    234         S: Serializer,
    235     {
    236         self.clone().into_raw().serialize(serializer)
    237     }
    238 }
    239 
    240 impl<'de> Deserialize<'de> for RadrootsNostrConnectRequestMessage {
    241     fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
    242     where
    243         D: Deserializer<'de>,
    244     {
    245         let raw = RawRequestMessage::deserialize(deserializer)?;
    246         Self::from_raw(raw).map_err(serde::de::Error::custom)
    247     }
    248 }
    249 
    250 #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
    251 pub struct RadrootsNostrConnectResponseEnvelope {
    252     pub id: String,
    253     #[serde(default, skip_serializing_if = "Option::is_none")]
    254     pub result: Option<Value>,
    255     #[serde(default, skip_serializing_if = "Option::is_none")]
    256     pub error: Option<String>,
    257 }
    258 
    259 pub const RADROOTS_NOSTR_CONNECT_PENDING_CONNECTION_ERROR: &str = "connection is pending";
    260 
    261 #[derive(Debug, Clone, PartialEq, Eq)]
    262 pub enum RadrootsNostrConnectPendingConnectionPollOutcome {
    263     PendingApproval,
    264     Approved(PublicKey),
    265     ApprovedCapability(RadrootsNostrConnectRemoteSessionCapability),
    266     Rejected { message: String },
    267     AuthChallenge { url: String },
    268     UnexpectedResponse { response: String },
    269 }
    270 
    271 #[derive(Debug, Clone, PartialEq, Eq)]
    272 pub enum RadrootsNostrConnectResponse {
    273     ConnectAcknowledged,
    274     ConnectSecretEcho(String),
    275     PendingConnection,
    276     UserPublicKey(PublicKey),
    277     RemoteSessionCapability(RadrootsNostrConnectRemoteSessionCapability),
    278     SignedEvent(Event),
    279     Pong,
    280     Nip04Encrypt(String),
    281     Nip04Decrypt(String),
    282     Nip44Encrypt(String),
    283     Nip44Decrypt(String),
    284     RelayList(Vec<RelayUrl>),
    285     RelayListUnchanged,
    286     AuthUrl(String),
    287     Error {
    288         result: Option<Value>,
    289         error: String,
    290     },
    291     Custom {
    292         result: Option<Value>,
    293         error: Option<String>,
    294     },
    295 }
    296 
    297 impl RadrootsNostrConnectResponse {
    298     pub fn into_pending_connection_poll_outcome(
    299         self,
    300     ) -> RadrootsNostrConnectPendingConnectionPollOutcome {
    301         match self {
    302             Self::PendingConnection => {
    303                 RadrootsNostrConnectPendingConnectionPollOutcome::PendingApproval
    304             }
    305             Self::UserPublicKey(public_key) => {
    306                 RadrootsNostrConnectPendingConnectionPollOutcome::Approved(public_key)
    307             }
    308             Self::RemoteSessionCapability(capability) => {
    309                 RadrootsNostrConnectPendingConnectionPollOutcome::ApprovedCapability(capability)
    310             }
    311             Self::Error { error, .. }
    312                 if error == RADROOTS_NOSTR_CONNECT_PENDING_CONNECTION_ERROR =>
    313             {
    314                 RadrootsNostrConnectPendingConnectionPollOutcome::PendingApproval
    315             }
    316             Self::Error { error, .. } => {
    317                 RadrootsNostrConnectPendingConnectionPollOutcome::Rejected { message: error }
    318             }
    319             Self::AuthUrl(url) => {
    320                 RadrootsNostrConnectPendingConnectionPollOutcome::AuthChallenge { url }
    321             }
    322             other => RadrootsNostrConnectPendingConnectionPollOutcome::UnexpectedResponse {
    323                 response: format!("{other:?}"),
    324             },
    325         }
    326     }
    327 
    328     pub fn into_envelope(
    329         self,
    330         id: impl Into<String>,
    331     ) -> Result<RadrootsNostrConnectResponseEnvelope, RadrootsNostrConnectError> {
    332         let id = id.into();
    333         let envelope = match self {
    334             Self::ConnectAcknowledged => RadrootsNostrConnectResponseEnvelope {
    335                 id,
    336                 result: Some(Value::String("ack".to_owned())),
    337                 error: None,
    338             },
    339             Self::ConnectSecretEcho(secret) => RadrootsNostrConnectResponseEnvelope {
    340                 id,
    341                 result: Some(Value::String(secret)),
    342                 error: None,
    343             },
    344             Self::PendingConnection => RadrootsNostrConnectResponseEnvelope {
    345                 id,
    346                 result: None,
    347                 error: Some(RADROOTS_NOSTR_CONNECT_PENDING_CONNECTION_ERROR.to_owned()),
    348             },
    349             Self::UserPublicKey(public_key) => RadrootsNostrConnectResponseEnvelope {
    350                 id,
    351                 result: Some(Value::String(public_key.to_hex())),
    352                 error: None,
    353             },
    354             Self::RemoteSessionCapability(capability) => RadrootsNostrConnectResponseEnvelope {
    355                 id,
    356                 result: Some(remote_session_capability_value(capability)),
    357                 error: None,
    358             },
    359             Self::SignedEvent(event) => RadrootsNostrConnectResponseEnvelope {
    360                 id,
    361                 result: Some(Value::String(event.as_json())),
    362                 error: None,
    363             },
    364             Self::Pong => RadrootsNostrConnectResponseEnvelope {
    365                 id,
    366                 result: Some(Value::String("pong".to_owned())),
    367                 error: None,
    368             },
    369             Self::Nip04Encrypt(text)
    370             | Self::Nip04Decrypt(text)
    371             | Self::Nip44Encrypt(text)
    372             | Self::Nip44Decrypt(text) => RadrootsNostrConnectResponseEnvelope {
    373                 id,
    374                 result: Some(Value::String(text)),
    375                 error: None,
    376             },
    377             Self::RelayList(relays) => {
    378                 let relays = relays
    379                     .into_iter()
    380                     .map(|relay| relay.to_string())
    381                     .collect::<Vec<_>>();
    382                 RadrootsNostrConnectResponseEnvelope {
    383                     id,
    384                     result: Some(Value::Array(
    385                         relays.into_iter().map(Value::String).collect(),
    386                     )),
    387                     error: None,
    388                 }
    389             }
    390             Self::RelayListUnchanged => RadrootsNostrConnectResponseEnvelope {
    391                 id,
    392                 result: Some(Value::Null),
    393                 error: None,
    394             },
    395             Self::AuthUrl(url) => {
    396                 let normalized = validate_url(&url)?;
    397                 RadrootsNostrConnectResponseEnvelope {
    398                     id,
    399                     result: Some(Value::String("auth_url".to_owned())),
    400                     error: Some(normalized),
    401                 }
    402             }
    403             Self::Error { result, error } => RadrootsNostrConnectResponseEnvelope {
    404                 id,
    405                 result,
    406                 error: Some(error),
    407             },
    408             Self::Custom { result, error } => {
    409                 RadrootsNostrConnectResponseEnvelope { id, result, error }
    410             }
    411         };
    412         Ok(envelope)
    413     }
    414 
    415     pub fn from_envelope(
    416         method: &RadrootsNostrConnectMethod,
    417         envelope: RadrootsNostrConnectResponseEnvelope,
    418     ) -> Result<Self, RadrootsNostrConnectError> {
    419         if let (Some(Value::String(result)), Some(url)) = (&envelope.result, &envelope.error)
    420             && result == "auth_url"
    421         {
    422             return Ok(Self::AuthUrl(validate_url(url)?));
    423         }
    424 
    425         if let Some(error) = envelope.error {
    426             if matches!(
    427                 method,
    428                 RadrootsNostrConnectMethod::GetPublicKey
    429                     | RadrootsNostrConnectMethod::GetSessionCapability
    430             ) && envelope.result.is_none()
    431                 && error == RADROOTS_NOSTR_CONNECT_PENDING_CONNECTION_ERROR
    432             {
    433                 return Ok(Self::PendingConnection);
    434             }
    435             if let RadrootsNostrConnectMethod::Custom(_) = method {
    436                 return Ok(Self::Custom {
    437                     result: envelope.result,
    438                     error: Some(error),
    439                 });
    440             }
    441             return Ok(Self::Error {
    442                 result: envelope.result,
    443                 error,
    444             });
    445         }
    446 
    447         match method {
    448             RadrootsNostrConnectMethod::Connect => {
    449                 let result = expect_string_result(method, envelope.result)?;
    450                 if result == "ack" {
    451                     Ok(Self::ConnectAcknowledged)
    452                 } else {
    453                     Ok(Self::ConnectSecretEcho(result))
    454                 }
    455             }
    456             RadrootsNostrConnectMethod::GetPublicKey => {
    457                 let result = expect_string_result(method, envelope.result)?;
    458                 Ok(Self::UserPublicKey(parse_public_key(&result)?))
    459             }
    460             RadrootsNostrConnectMethod::GetSessionCapability => {
    461                 let capability = parse_json_string_result(method, envelope.result)?;
    462                 Ok(Self::RemoteSessionCapability(capability))
    463             }
    464             RadrootsNostrConnectMethod::SignEvent => {
    465                 let event = parse_json_string_result::<Event>(method, envelope.result)?;
    466                 Ok(Self::SignedEvent(event))
    467             }
    468             RadrootsNostrConnectMethod::Ping => {
    469                 let result = expect_string_result(method, envelope.result)?;
    470                 if result != "pong" {
    471                     return Err(RadrootsNostrConnectError::InvalidResponsePayload {
    472                         method: method.to_string(),
    473                         reason: format!("expected `pong`, got `{result}`"),
    474                     });
    475                 }
    476                 Ok(Self::Pong)
    477             }
    478             RadrootsNostrConnectMethod::Nip04Encrypt => Ok(Self::Nip04Encrypt(
    479                 expect_string_result(method, envelope.result)?,
    480             )),
    481             RadrootsNostrConnectMethod::Nip04Decrypt => Ok(Self::Nip04Decrypt(
    482                 expect_string_result(method, envelope.result)?,
    483             )),
    484             RadrootsNostrConnectMethod::Nip44Encrypt => Ok(Self::Nip44Encrypt(
    485                 expect_string_result(method, envelope.result)?,
    486             )),
    487             RadrootsNostrConnectMethod::Nip44Decrypt => Ok(Self::Nip44Decrypt(
    488                 expect_string_result(method, envelope.result)?,
    489             )),
    490             RadrootsNostrConnectMethod::SwitchRelays => {
    491                 parse_switch_relays_response(envelope.result)
    492             }
    493             RadrootsNostrConnectMethod::Custom(_) => Ok(Self::Custom {
    494                 result: envelope.result,
    495                 error: None,
    496             }),
    497         }
    498     }
    499 }
    500 
    501 fn remote_session_capability_value(
    502     capability: RadrootsNostrConnectRemoteSessionCapability,
    503 ) -> Value {
    504     json!({
    505         "user_public_key": capability.user_public_key,
    506         "relays": capability.relays,
    507         "permissions": capability.permissions,
    508     })
    509 }
    510 
    511 #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
    512 struct RawRequestMessage {
    513     id: String,
    514     method: RadrootsNostrConnectMethod,
    515     params: Vec<String>,
    516 }
    517 
    518 fn expect_param_count(
    519     method: &RadrootsNostrConnectMethod,
    520     params: &[String],
    521     expected: usize,
    522 ) -> Result<(), RadrootsNostrConnectError> {
    523     if params.len() == expected {
    524         return Ok(());
    525     }
    526 
    527     Err(RadrootsNostrConnectError::InvalidParams {
    528         method: method.to_string(),
    529         expected: if expected == 0 {
    530             "no params"
    531         } else if expected == 1 {
    532             "exactly 1 param"
    533         } else {
    534             "exactly 2 params"
    535         },
    536         received: params.len(),
    537     })
    538 }
    539 
    540 fn parse_public_key(value: &str) -> Result<PublicKey, RadrootsNostrConnectError> {
    541     PublicKey::parse(value)
    542         .or_else(|_| PublicKey::from_hex(value))
    543         .map_err(|error| RadrootsNostrConnectError::InvalidPublicKey {
    544             value: value.to_owned(),
    545             reason: error.to_string(),
    546         })
    547 }
    548 
    549 fn expect_string_result(
    550     method: &RadrootsNostrConnectMethod,
    551     result: Option<Value>,
    552 ) -> Result<String, RadrootsNostrConnectError> {
    553     match result {
    554         Some(Value::String(value)) => Ok(value),
    555         Some(other) => Err(RadrootsNostrConnectError::InvalidResponsePayload {
    556             method: method.to_string(),
    557             reason: format!("expected string result, got {other}"),
    558         }),
    559         None => Err(RadrootsNostrConnectError::MissingResult),
    560     }
    561 }
    562 
    563 fn parse_json_string_result<T>(
    564     method: &RadrootsNostrConnectMethod,
    565     result: Option<Value>,
    566 ) -> Result<T, RadrootsNostrConnectError>
    567 where
    568     T: for<'de> Deserialize<'de>,
    569 {
    570     match result {
    571         Some(Value::String(value)) => serde_json::from_str(&value).map_err(|error| {
    572             RadrootsNostrConnectError::InvalidResponsePayload {
    573                 method: method.to_string(),
    574                 reason: error.to_string(),
    575             }
    576         }),
    577         Some(other) => serde_json::from_value(other).map_err(|error| {
    578             RadrootsNostrConnectError::InvalidResponsePayload {
    579                 method: method.to_string(),
    580                 reason: error.to_string(),
    581             }
    582         }),
    583         None => Err(RadrootsNostrConnectError::MissingResult),
    584     }
    585 }
    586 
    587 fn parse_switch_relays_response(
    588     result: Option<Value>,
    589 ) -> Result<RadrootsNostrConnectResponse, RadrootsNostrConnectError> {
    590     let method = RadrootsNostrConnectMethod::SwitchRelays;
    591     match result {
    592         None | Some(Value::Null) => Ok(RadrootsNostrConnectResponse::RelayListUnchanged),
    593         Some(Value::Array(values)) => {
    594             let relays = parse_relay_values(values)?;
    595             Ok(RadrootsNostrConnectResponse::RelayList(relays))
    596         }
    597         Some(Value::String(value)) if value == "null" => {
    598             Ok(RadrootsNostrConnectResponse::RelayListUnchanged)
    599         }
    600         Some(Value::String(value)) => {
    601             let parsed = serde_json::from_str::<Value>(&value).map_err(|error| {
    602                 RadrootsNostrConnectError::InvalidResponsePayload {
    603                     method: method.to_string(),
    604                     reason: error.to_string(),
    605                 }
    606             })?;
    607             parse_switch_relays_response(Some(parsed))
    608         }
    609         Some(other) => Err(RadrootsNostrConnectError::InvalidResponsePayload {
    610             method: method.to_string(),
    611             reason: format!("expected relay list or null, got {other}"),
    612         }),
    613     }
    614 }
    615 
    616 fn parse_relay_values(values: Vec<Value>) -> Result<Vec<RelayUrl>, RadrootsNostrConnectError> {
    617     values
    618         .into_iter()
    619         .map(|value| match value {
    620             Value::String(value) => RelayUrl::parse(&value).map_err(|error| {
    621                 RadrootsNostrConnectError::InvalidRelayUrl {
    622                     value,
    623                     reason: error.to_string(),
    624                 }
    625             }),
    626             other => Err(RadrootsNostrConnectError::InvalidResponsePayload {
    627                 method: RadrootsNostrConnectMethod::SwitchRelays.to_string(),
    628                 reason: format!("expected relay string, got {other}"),
    629             }),
    630         })
    631         .collect()
    632 }
    633 
    634 fn validate_url(value: &str) -> Result<String, RadrootsNostrConnectError> {
    635     Url::parse(value)
    636         .map(|url| url.to_string())
    637         .map_err(|error| RadrootsNostrConnectError::InvalidUrl {
    638             value: value.to_owned(),
    639             reason: error.to_string(),
    640         })
    641 }