Skip to main content

miden_client/rpc/
endpoint.rs

1use alloc::string::{String, ToString};
2use core::fmt;
3
4use miden_protocol::address::NetworkId;
5
6// ENDPOINT
7// ================================================================================================
8
9/// The `Endpoint` struct represents a network endpoint, consisting of a protocol, a host, and a
10/// port.
11///
12/// This struct is used to define the address of a Miden node that the client will connect to.
13#[derive(Clone, Debug, Eq, Ord, PartialEq, PartialOrd)]
14pub struct Endpoint {
15    /// The protocol used to connect to the endpoint (e.g., "http", "https").
16    protocol: String,
17    /// The hostname or IP address of the endpoint.
18    host: String,
19    /// The port number of the endpoint.
20    port: Option<u16>,
21}
22
23impl Endpoint {
24    pub(crate) const MIDEN_NODE_PORT: u16 = 57291;
25
26    /// Creates a new `Endpoint` with the specified protocol, host, and port.
27    ///
28    /// # Arguments
29    ///
30    /// * `protocol` - The protocol to use for the connection (e.g., "http", "https").
31    /// * `host` - The hostname or IP address of the endpoint.
32    /// * `port` - The port number to connect to.
33    pub const fn new(protocol: String, host: String, port: Option<u16>) -> Self {
34        Self { protocol, host, port }
35    }
36
37    /// Returns the [Endpoint] associated with the testnet network.
38    pub fn testnet() -> Self {
39        Self::new("https".into(), "rpc.testnet.miden.io".into(), None)
40    }
41
42    /// Returns the [Endpoint] associated with the devnet network.
43    pub fn devnet() -> Self {
44        Self::new("https".into(), "rpc.devnet.miden.io".into(), None)
45    }
46
47    /// Returns the [Endpoint] for a default node running in `localhost`.
48    pub fn localhost() -> Self {
49        Self::new("http".into(), "localhost".into(), Some(Self::MIDEN_NODE_PORT))
50    }
51
52    pub fn protocol(&self) -> &str {
53        &self.protocol
54    }
55
56    pub fn host(&self) -> &str {
57        &self.host
58    }
59
60    pub fn port(&self) -> Option<u16> {
61        self.port
62    }
63
64    pub fn to_network_id(&self) -> NetworkId {
65        if self == &Endpoint::testnet() {
66            NetworkId::Testnet
67        } else if self == &Endpoint::devnet() {
68            NetworkId::Devnet
69        } else if self == &Endpoint::localhost() {
70            // Network ID intended to be used when running a local instance of the node
71            NetworkId::new("mlcl").expect("mlcl should be a valid network ID")
72        } else {
73            // Default network ID for custom networks when no other match has been found
74            NetworkId::new("mcst").expect("mcst should be a valid network ID")
75        }
76    }
77}
78
79impl fmt::Display for Endpoint {
80    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
81        match self.port {
82            Some(port) => write!(f, "{}://{}:{}", self.protocol, self.host, port),
83            None => write!(f, "{}://{}", self.protocol, self.host),
84        }
85    }
86}
87
88impl Default for Endpoint {
89    fn default() -> Self {
90        Self::localhost()
91    }
92}
93
94impl TryFrom<&str> for Endpoint {
95    type Error = String;
96
97    fn try_from(endpoint: &str) -> Result<Self, Self::Error> {
98        let protocol_separator_index = endpoint.find("://");
99        let port_separator_index = endpoint.rfind(':');
100
101        // port separator index might match with the protocol separator, if so that means there was
102        // no port defined
103        let port_separator_index = if port_separator_index == protocol_separator_index {
104            None
105        } else {
106            port_separator_index
107        };
108
109        let (protocol, hostname, port) = match (protocol_separator_index, port_separator_index) {
110            (Some(protocol_idx), Some(port_idx)) => {
111                let (protocol_and_hostname, port) = endpoint.split_at(port_idx);
112                let port = port[1..]
113                    .trim_end_matches('/')
114                    .parse::<u16>()
115                    .map_err(|err| err.to_string())?;
116
117                let (protocol, hostname) = protocol_and_hostname.split_at(protocol_idx);
118                // skip the separator
119                let hostname = &hostname[3..];
120
121                (protocol, hostname, Some(port))
122            },
123            (Some(protocol_idx), None) => {
124                let (protocol, hostname) = endpoint.split_at(protocol_idx);
125                // skip the separator
126                let hostname = &hostname[3..];
127
128                (protocol, hostname, None)
129            },
130            (None, Some(port_idx)) => {
131                let (hostname, port) = endpoint.split_at(port_idx);
132                let port = port[1..]
133                    .trim_end_matches('/')
134                    .parse::<u16>()
135                    .map_err(|err| err.to_string())?;
136
137                ("https", hostname, Some(port))
138            },
139            (None, None) => ("https", endpoint, None),
140        };
141
142        let hostname = hostname.trim_end_matches('/');
143
144        if protocol.is_empty() {
145            return Err("endpoint protocol cannot be empty".to_string());
146        }
147
148        if hostname.is_empty() {
149            return Err("endpoint host cannot be empty".to_string());
150        }
151
152        Ok(Endpoint::new(protocol.to_string(), hostname.to_string(), port))
153    }
154}
155
156#[cfg(test)]
157mod test {
158    use alloc::string::ToString;
159
160    use crate::rpc::Endpoint;
161
162    #[test]
163    fn endpoint_parsing_with_hostname_only() {
164        let endpoint = Endpoint::try_from("some.test.domain").unwrap();
165        let expected_endpoint = Endpoint {
166            protocol: "https".to_string(),
167            host: "some.test.domain".to_string(),
168            port: None,
169        };
170
171        assert_eq!(endpoint, expected_endpoint);
172    }
173
174    #[test]
175    fn endpoint_parsing_with_ip() {
176        let endpoint = Endpoint::try_from("192.168.0.1").unwrap();
177        let expected_endpoint = Endpoint {
178            protocol: "https".to_string(),
179            host: "192.168.0.1".to_string(),
180            port: None,
181        };
182
183        assert_eq!(endpoint, expected_endpoint);
184    }
185
186    #[test]
187    fn endpoint_parsing_with_port() {
188        let endpoint = Endpoint::try_from("some.test.domain:8000").unwrap();
189        let expected_endpoint = Endpoint {
190            protocol: "https".to_string(),
191            host: "some.test.domain".to_string(),
192            port: Some(8000),
193        };
194
195        assert_eq!(endpoint, expected_endpoint);
196    }
197
198    #[test]
199    fn endpoint_parsing_with_ip_and_port() {
200        let endpoint = Endpoint::try_from("192.168.0.1:8000").unwrap();
201        let expected_endpoint = Endpoint {
202            protocol: "https".to_string(),
203            host: "192.168.0.1".to_string(),
204            port: Some(8000),
205        };
206
207        assert_eq!(endpoint, expected_endpoint);
208    }
209
210    #[test]
211    fn endpoint_parsing_with_protocol() {
212        let endpoint = Endpoint::try_from("hkttp://some.test.domain").unwrap();
213        let expected_endpoint = Endpoint {
214            protocol: "hkttp".to_string(),
215            host: "some.test.domain".to_string(),
216            port: None,
217        };
218
219        assert_eq!(endpoint, expected_endpoint);
220    }
221
222    #[test]
223    fn endpoint_parsing_with_protocol_and_ip() {
224        let endpoint = Endpoint::try_from("http://192.168.0.1").unwrap();
225        let expected_endpoint = Endpoint {
226            protocol: "http".to_string(),
227            host: "192.168.0.1".to_string(),
228            port: None,
229        };
230
231        assert_eq!(endpoint, expected_endpoint);
232    }
233
234    #[test]
235    fn endpoint_parsing_with_both_protocol_and_port() {
236        let endpoint = Endpoint::try_from("http://some.test.domain:8080").unwrap();
237        let expected_endpoint = Endpoint {
238            protocol: "http".to_string(),
239            host: "some.test.domain".to_string(),
240            port: Some(8080),
241        };
242
243        assert_eq!(endpoint, expected_endpoint);
244    }
245
246    #[test]
247    fn endpoint_parsing_with_ip_and_protocol_and_port() {
248        let endpoint = Endpoint::try_from("http://192.168.0.1:8080").unwrap();
249        let expected_endpoint = Endpoint {
250            protocol: "http".to_string(),
251            host: "192.168.0.1".to_string(),
252            port: Some(8080),
253        };
254
255        assert_eq!(endpoint, expected_endpoint);
256    }
257
258    #[test]
259    fn endpoint_parsing_should_fail_for_invalid_port() {
260        let endpoint = Endpoint::try_from("some.test.domain:8000/hello");
261        assert!(endpoint.is_err());
262    }
263
264    #[test]
265    fn endpoint_parsing_should_fail_for_empty_protocol() {
266        let endpoint = Endpoint::try_from("://some.test.domain:8000");
267        assert!(endpoint.is_err());
268    }
269
270    #[test]
271    fn endpoint_parsing_should_fail_for_empty_host() {
272        let endpoint = Endpoint::try_from("https://:8000");
273        assert!(endpoint.is_err());
274    }
275
276    #[test]
277    fn endpoint_parsing_with_final_forward_slash() {
278        let endpoint = Endpoint::try_from("https://some.test.domain:8000/").unwrap();
279        let expected_endpoint = Endpoint {
280            protocol: "https".to_string(),
281            host: "some.test.domain".to_string(),
282            port: Some(8000),
283        };
284
285        assert_eq!(endpoint, expected_endpoint);
286    }
287
288    #[test]
289    fn endpoint_parsing_with_protocol_and_final_forward_slash_no_port() {
290        let endpoint = Endpoint::try_from("http://some.test.domain/").unwrap();
291        let expected_endpoint = Endpoint {
292            protocol: "http".to_string(),
293            host: "some.test.domain".to_string(),
294            port: None,
295        };
296
297        assert_eq!(endpoint, expected_endpoint);
298    }
299}