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 mainnet network.
38    pub fn mainnet() -> Self {
39        Self::new("https".into(), "rpc.mainnet.miden.io".into(), None)
40    }
41
42    /// Returns the [Endpoint] associated with the testnet network.
43    pub fn testnet() -> Self {
44        Self::new("https".into(), "rpc.testnet.miden.io".into(), None)
45    }
46
47    /// Returns the [Endpoint] associated with the devnet network.
48    pub fn devnet() -> Self {
49        Self::new("https".into(), "rpc.devnet.miden.io".into(), None)
50    }
51
52    /// Returns the [Endpoint] for a default node running in `localhost`.
53    pub fn localhost() -> Self {
54        Self::new("http".into(), "localhost".into(), Some(Self::MIDEN_NODE_PORT))
55    }
56
57    pub fn protocol(&self) -> &str {
58        &self.protocol
59    }
60
61    pub fn host(&self) -> &str {
62        &self.host
63    }
64
65    pub fn port(&self) -> Option<u16> {
66        self.port
67    }
68
69    pub fn to_network_id(&self) -> NetworkId {
70        if self == &Endpoint::mainnet() {
71            NetworkId::Mainnet
72        } else if self == &Endpoint::testnet() {
73            NetworkId::Testnet
74        } else if self == &Endpoint::devnet() {
75            NetworkId::Devnet
76        } else if self == &Endpoint::localhost() {
77            // Network ID intended to be used when running a local instance of the node
78            NetworkId::new("mlcl").expect("mlcl should be a valid network ID")
79        } else {
80            // Default network ID for custom networks when no other match has been found
81            NetworkId::new("mcst").expect("mcst should be a valid network ID")
82        }
83    }
84}
85
86impl fmt::Display for Endpoint {
87    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
88        match self.port {
89            Some(port) => write!(f, "{}://{}:{}", self.protocol, self.host, port),
90            None => write!(f, "{}://{}", self.protocol, self.host),
91        }
92    }
93}
94
95impl Default for Endpoint {
96    fn default() -> Self {
97        Self::localhost()
98    }
99}
100
101impl TryFrom<&str> for Endpoint {
102    type Error = String;
103
104    fn try_from(endpoint: &str) -> Result<Self, Self::Error> {
105        let protocol_separator_index = endpoint.find("://");
106        let port_separator_index = endpoint.rfind(':');
107
108        // port separator index might match with the protocol separator, if so that means there was
109        // no port defined
110        let port_separator_index = if port_separator_index == protocol_separator_index {
111            None
112        } else {
113            port_separator_index
114        };
115
116        let (protocol, hostname, port) = match (protocol_separator_index, port_separator_index) {
117            (Some(protocol_idx), Some(port_idx)) => {
118                let (protocol_and_hostname, port) = endpoint.split_at(port_idx);
119                let port = port[1..]
120                    .trim_end_matches('/')
121                    .parse::<u16>()
122                    .map_err(|err| err.to_string())?;
123
124                let (protocol, hostname) = protocol_and_hostname.split_at(protocol_idx);
125                // skip the separator
126                let hostname = &hostname[3..];
127
128                (protocol, hostname, Some(port))
129            },
130            (Some(protocol_idx), None) => {
131                let (protocol, hostname) = endpoint.split_at(protocol_idx);
132                // skip the separator
133                let hostname = &hostname[3..];
134
135                (protocol, hostname, None)
136            },
137            (None, Some(port_idx)) => {
138                let (hostname, port) = endpoint.split_at(port_idx);
139                let port = port[1..]
140                    .trim_end_matches('/')
141                    .parse::<u16>()
142                    .map_err(|err| err.to_string())?;
143
144                ("https", hostname, Some(port))
145            },
146            (None, None) => ("https", endpoint, None),
147        };
148
149        let hostname = hostname.trim_end_matches('/');
150
151        if protocol.is_empty() {
152            return Err("endpoint protocol cannot be empty".to_string());
153        }
154
155        if hostname.is_empty() {
156            return Err("endpoint host cannot be empty".to_string());
157        }
158
159        Ok(Endpoint::new(protocol.to_string(), hostname.to_string(), port))
160    }
161}
162
163#[cfg(test)]
164mod test {
165    use alloc::string::ToString;
166
167    use miden_protocol::address::NetworkId;
168
169    use crate::rpc::Endpoint;
170
171    #[test]
172    fn network_id_of_known_endpoints() {
173        assert_eq!(Endpoint::mainnet().to_network_id(), NetworkId::Mainnet);
174        assert_eq!(Endpoint::testnet().to_network_id(), NetworkId::Testnet);
175        assert_eq!(Endpoint::devnet().to_network_id(), NetworkId::Devnet);
176        assert_eq!(Endpoint::localhost().to_network_id().as_str(), "mlcl");
177    }
178
179    #[test]
180    fn network_id_of_parsed_mainnet_endpoint() {
181        // A configuration file stores the endpoint as a string, so the parsed form must map to the
182        // same network as the constructor.
183        let endpoint = Endpoint::try_from(Endpoint::mainnet().to_string().as_str()).unwrap();
184        assert_eq!(endpoint.to_network_id(), NetworkId::Mainnet);
185    }
186
187    #[test]
188    fn network_id_of_custom_endpoint() {
189        // An explicit port makes a known host a custom endpoint.
190        let endpoint = Endpoint::try_from("https://rpc.mainnet.miden.io:8080").unwrap();
191        assert_eq!(endpoint.to_network_id().as_str(), "mcst");
192    }
193
194    #[test]
195    fn endpoint_parsing_with_hostname_only() {
196        let endpoint = Endpoint::try_from("some.test.domain").unwrap();
197        let expected_endpoint = Endpoint {
198            protocol: "https".to_string(),
199            host: "some.test.domain".to_string(),
200            port: None,
201        };
202
203        assert_eq!(endpoint, expected_endpoint);
204    }
205
206    #[test]
207    fn endpoint_parsing_with_ip() {
208        let endpoint = Endpoint::try_from("192.168.0.1").unwrap();
209        let expected_endpoint = Endpoint {
210            protocol: "https".to_string(),
211            host: "192.168.0.1".to_string(),
212            port: None,
213        };
214
215        assert_eq!(endpoint, expected_endpoint);
216    }
217
218    #[test]
219    fn endpoint_parsing_with_port() {
220        let endpoint = Endpoint::try_from("some.test.domain:8000").unwrap();
221        let expected_endpoint = Endpoint {
222            protocol: "https".to_string(),
223            host: "some.test.domain".to_string(),
224            port: Some(8000),
225        };
226
227        assert_eq!(endpoint, expected_endpoint);
228    }
229
230    #[test]
231    fn endpoint_parsing_with_ip_and_port() {
232        let endpoint = Endpoint::try_from("192.168.0.1:8000").unwrap();
233        let expected_endpoint = Endpoint {
234            protocol: "https".to_string(),
235            host: "192.168.0.1".to_string(),
236            port: Some(8000),
237        };
238
239        assert_eq!(endpoint, expected_endpoint);
240    }
241
242    #[test]
243    fn endpoint_parsing_with_protocol() {
244        let endpoint = Endpoint::try_from("hkttp://some.test.domain").unwrap();
245        let expected_endpoint = Endpoint {
246            protocol: "hkttp".to_string(),
247            host: "some.test.domain".to_string(),
248            port: None,
249        };
250
251        assert_eq!(endpoint, expected_endpoint);
252    }
253
254    #[test]
255    fn endpoint_parsing_with_protocol_and_ip() {
256        let endpoint = Endpoint::try_from("http://192.168.0.1").unwrap();
257        let expected_endpoint = Endpoint {
258            protocol: "http".to_string(),
259            host: "192.168.0.1".to_string(),
260            port: None,
261        };
262
263        assert_eq!(endpoint, expected_endpoint);
264    }
265
266    #[test]
267    fn endpoint_parsing_with_both_protocol_and_port() {
268        let endpoint = Endpoint::try_from("http://some.test.domain:8080").unwrap();
269        let expected_endpoint = Endpoint {
270            protocol: "http".to_string(),
271            host: "some.test.domain".to_string(),
272            port: Some(8080),
273        };
274
275        assert_eq!(endpoint, expected_endpoint);
276    }
277
278    #[test]
279    fn endpoint_parsing_with_ip_and_protocol_and_port() {
280        let endpoint = Endpoint::try_from("http://192.168.0.1:8080").unwrap();
281        let expected_endpoint = Endpoint {
282            protocol: "http".to_string(),
283            host: "192.168.0.1".to_string(),
284            port: Some(8080),
285        };
286
287        assert_eq!(endpoint, expected_endpoint);
288    }
289
290    #[test]
291    fn endpoint_parsing_should_fail_for_invalid_port() {
292        let endpoint = Endpoint::try_from("some.test.domain:8000/hello");
293        assert!(endpoint.is_err());
294    }
295
296    #[test]
297    fn endpoint_parsing_should_fail_for_empty_protocol() {
298        let endpoint = Endpoint::try_from("://some.test.domain:8000");
299        assert!(endpoint.is_err());
300    }
301
302    #[test]
303    fn endpoint_parsing_should_fail_for_empty_host() {
304        let endpoint = Endpoint::try_from("https://:8000");
305        assert!(endpoint.is_err());
306    }
307
308    #[test]
309    fn endpoint_parsing_with_final_forward_slash() {
310        let endpoint = Endpoint::try_from("https://some.test.domain:8000/").unwrap();
311        let expected_endpoint = Endpoint {
312            protocol: "https".to_string(),
313            host: "some.test.domain".to_string(),
314            port: Some(8000),
315        };
316
317        assert_eq!(endpoint, expected_endpoint);
318    }
319
320    #[test]
321    fn endpoint_parsing_with_protocol_and_final_forward_slash_no_port() {
322        let endpoint = Endpoint::try_from("http://some.test.domain/").unwrap();
323        let expected_endpoint = Endpoint {
324            protocol: "http".to_string(),
325            host: "some.test.domain".to_string(),
326            port: None,
327        };
328
329        assert_eq!(endpoint, expected_endpoint);
330    }
331}