nostr_types/nip46/
params.rs

1use crate::{Error, PublicKey, RelayUrl};
2use lazy_static::lazy_static;
3use serde::{Deserialize, Serialize};
4
5/// The connection parameters provided by an nsec bunker for a client connecting to it
6/// usually as a `bunker://` url
7#[derive(Clone, Debug, Serialize, Deserialize)]
8pub struct Nip46ConnectionParameters {
9    /// The public key of the remote signer
10    pub remote_signer_pubkey: PublicKey,
11
12    /// The relays to contact the remote signer on
13    pub relays: Vec<RelayUrl>,
14
15    /// A secret to provide in the connect request to prove this client is authorized
16    pub secret: Option<String>,
17}
18
19impl Nip46ConnectionParameters {
20    /// Parse a `bunker://` url into `Nip46ConnectionParameters`
21    #[allow(clippy::should_implement_trait)]
22    pub fn from_str(s: &str) -> Result<Nip46ConnectionParameters, Error> {
23        // "bunker://{pk}?{relay_part}&secret={secret}"
24        use regex::Regex;
25        lazy_static! {
26            static ref BUNKER_RE: Regex =
27                Regex::new(r#"^bunker://(.+)\?(.+)$"#).expect("Could not compile bunker regex");
28        }
29
30        let mut relays: Vec<RelayUrl> = Vec::new();
31        let mut secret: Option<String> = None;
32
33        let captures = match BUNKER_RE.captures(s) {
34            Some(c) => c,
35            None => return Err(Error::BadBunkerUrl),
36        };
37
38        let public_key = if let Some(pk_part) = captures.get(1) {
39            PublicKey::try_from_hex_string(pk_part.as_str(), true)?
40        } else {
41            return Err(Error::BadBunkerUrl);
42        };
43
44        if let Some(param_part) = captures.get(2) {
45            let assignments = param_part.as_str().split('&');
46            for assignment in assignments {
47                let halfs: Vec<&str> = assignment.split('=').collect();
48                if halfs.len() != 2 {
49                    return Err(Error::BadBunkerUrl);
50                }
51                let var = halfs[0];
52                let val = halfs[1];
53                match var {
54                    "relay" => relays.push(RelayUrl::try_from_str(val)?),
55                    "secret" => secret = Some(val.to_owned()),
56                    _ => continue, // ignore other terms
57                }
58            }
59        } else {
60            return Err(Error::BadBunkerUrl);
61        }
62
63        if relays.is_empty() {
64            return Err(Error::BadBunkerUrl);
65        }
66
67        Ok(Nip46ConnectionParameters {
68            remote_signer_pubkey: public_key,
69            relays,
70            secret,
71        })
72    }
73}
74
75#[cfg(test)]
76mod test {
77    use super::*;
78
79    #[test]
80    fn test_nip46_connection_params() {
81        let params = Nip46ConnectionParameters::from_str(
82            "bunker://ee11a5dff40c19a555f41fe42b48f00e618c91225622ae37b6c2bb67b76c4e49?relay=wss://chorus.mikedilger.com:444/&secret=5ijGGB0AGmgAAAAAgbaYUxymvgMQnrQh"
83        ).unwrap();
84
85        assert_eq!(params.relays.len(), 1);
86        assert_eq!(
87            params.relays[0].as_str(),
88            "wss://chorus.mikedilger.com:444/"
89        );
90        assert_eq!(
91            params.remote_signer_pubkey,
92            PublicKey::try_from_hex_string(
93                "ee11a5dff40c19a555f41fe42b48f00e618c91225622ae37b6c2bb67b76c4e49",
94                true
95            )
96            .unwrap()
97        );
98        assert_eq!(
99            params.secret,
100            Some("5ijGGB0AGmgAAAAAgbaYUxymvgMQnrQh".to_string())
101        );
102    }
103}