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        Ok(Endpoint::new(protocol.to_string(), hostname.to_string(), port))
143    }
144}
145
146#[cfg(test)]
147mod test {
148    use alloc::string::ToString;
149
150    use crate::rpc::Endpoint;
151
152    #[test]
153    fn endpoint_parsing_with_hostname_only() {
154        let endpoint = Endpoint::try_from("some.test.domain").unwrap();
155        let expected_endpoint = Endpoint {
156            protocol: "https".to_string(),
157            host: "some.test.domain".to_string(),
158            port: None,
159        };
160
161        assert_eq!(endpoint, expected_endpoint);
162    }
163
164    #[test]
165    fn endpoint_parsing_with_ip() {
166        let endpoint = Endpoint::try_from("192.168.0.1").unwrap();
167        let expected_endpoint = Endpoint {
168            protocol: "https".to_string(),
169            host: "192.168.0.1".to_string(),
170            port: None,
171        };
172
173        assert_eq!(endpoint, expected_endpoint);
174    }
175
176    #[test]
177    fn endpoint_parsing_with_port() {
178        let endpoint = Endpoint::try_from("some.test.domain:8000").unwrap();
179        let expected_endpoint = Endpoint {
180            protocol: "https".to_string(),
181            host: "some.test.domain".to_string(),
182            port: Some(8000),
183        };
184
185        assert_eq!(endpoint, expected_endpoint);
186    }
187
188    #[test]
189    fn endpoint_parsing_with_ip_and_port() {
190        let endpoint = Endpoint::try_from("192.168.0.1:8000").unwrap();
191        let expected_endpoint = Endpoint {
192            protocol: "https".to_string(),
193            host: "192.168.0.1".to_string(),
194            port: Some(8000),
195        };
196
197        assert_eq!(endpoint, expected_endpoint);
198    }
199
200    #[test]
201    fn endpoint_parsing_with_protocol() {
202        let endpoint = Endpoint::try_from("hkttp://some.test.domain").unwrap();
203        let expected_endpoint = Endpoint {
204            protocol: "hkttp".to_string(),
205            host: "some.test.domain".to_string(),
206            port: None,
207        };
208
209        assert_eq!(endpoint, expected_endpoint);
210    }
211
212    #[test]
213    fn endpoint_parsing_with_protocol_and_ip() {
214        let endpoint = Endpoint::try_from("http://192.168.0.1").unwrap();
215        let expected_endpoint = Endpoint {
216            protocol: "http".to_string(),
217            host: "192.168.0.1".to_string(),
218            port: None,
219        };
220
221        assert_eq!(endpoint, expected_endpoint);
222    }
223
224    #[test]
225    fn endpoint_parsing_with_both_protocol_and_port() {
226        let endpoint = Endpoint::try_from("http://some.test.domain:8080").unwrap();
227        let expected_endpoint = Endpoint {
228            protocol: "http".to_string(),
229            host: "some.test.domain".to_string(),
230            port: Some(8080),
231        };
232
233        assert_eq!(endpoint, expected_endpoint);
234    }
235
236    #[test]
237    fn endpoint_parsing_with_ip_and_protocol_and_port() {
238        let endpoint = Endpoint::try_from("http://192.168.0.1:8080").unwrap();
239        let expected_endpoint = Endpoint {
240            protocol: "http".to_string(),
241            host: "192.168.0.1".to_string(),
242            port: Some(8080),
243        };
244
245        assert_eq!(endpoint, expected_endpoint);
246    }
247
248    #[test]
249    fn endpoint_parsing_should_fail_for_invalid_port() {
250        let endpoint = Endpoint::try_from("some.test.domain:8000/hello");
251        assert!(endpoint.is_err());
252    }
253
254    #[test]
255    fn endpoint_parsing_with_final_forward_slash() {
256        let endpoint = Endpoint::try_from("https://some.test.domain:8000/").unwrap();
257        let expected_endpoint = Endpoint {
258            protocol: "https".to_string(),
259            host: "some.test.domain".to_string(),
260            port: Some(8000),
261        };
262
263        assert_eq!(endpoint, expected_endpoint);
264    }
265}