1use {
2 anyhow::{anyhow, Result},
3 serde::{Deserialize, Serialize},
4 std::str::FromStr,
5 url::Url,
6};
7
8#[derive(Serialize, Deserialize, Clone, Debug, Default, Eq, PartialEq, Ord, PartialOrd)]
9pub enum Cluster {
10 Testnet,
11 Mainnet,
12 Devnet,
13 #[default]
14 Localnet,
15 Debug,
16 Custom(String, String),
17}
18
19impl FromStr for Cluster {
20 type Err = anyhow::Error;
21 fn from_str(s: &str) -> Result<Cluster> {
22 match s.to_lowercase().as_str() {
23 "t" | "testnet" => Ok(Cluster::Testnet),
24 "m" | "mainnet" => Ok(Cluster::Mainnet),
25 "d" | "devnet" => Ok(Cluster::Devnet),
26 "l" | "localnet" => Ok(Cluster::Localnet),
27 "g" | "debug" => Ok(Cluster::Debug),
28 _ => {
29 let http_url = s;
30
31 let mut ws_url = Url::parse(http_url)?;
35 if !matches!(ws_url.scheme(), "http" | "https") {
36 return Err(anyhow::Error::msg(
37 "Cluster must be one of [localnet, testnet, mainnet, devnet] or be an \
38 http or https url\n",
39 ));
40 }
41 if let Some(port) = ws_url.port() {
42 let ws_port = port
43 .checked_add(1)
44 .ok_or_else(|| anyhow!("Unable to infer websocket port from {port}"))?;
45 ws_url
46 .set_port(Some(ws_port))
47 .map_err(|_| anyhow!("Unable to set port"))?;
48 }
49 if ws_url.scheme() == "https" {
50 ws_url
51 .set_scheme("wss")
52 .map_err(|_| anyhow!("Unable to set scheme"))?;
53 } else {
54 ws_url
55 .set_scheme("ws")
56 .map_err(|_| anyhow!("Unable to set scheme"))?;
57 }
58
59 Ok(Cluster::Custom(http_url.to_string(), ws_url.to_string()))
60 }
61 }
62 }
63}
64
65impl std::fmt::Display for Cluster {
66 fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
67 let clust_str = match self {
68 Cluster::Testnet => "testnet",
69 Cluster::Mainnet => "mainnet",
70 Cluster::Devnet => "devnet",
71 Cluster::Localnet => "localnet",
72 Cluster::Debug => "debug",
73 Cluster::Custom(url, _ws_url) => url,
74 };
75 write!(f, "{clust_str}")
76 }
77}
78
79impl Cluster {
80 pub fn url(&self) -> &str {
81 match self {
82 Cluster::Devnet => "https://api.devnet.solana.com",
83 Cluster::Testnet => "https://api.testnet.solana.com",
84 Cluster::Mainnet => "https://api.mainnet-beta.solana.com",
85 Cluster::Localnet => "http://127.0.0.1:8899",
86 Cluster::Debug => "http://34.90.18.145:8899",
87 Cluster::Custom(url, _ws_url) => url,
88 }
89 }
90 pub fn ws_url(&self) -> &str {
91 match self {
92 Cluster::Devnet => "wss://api.devnet.solana.com",
93 Cluster::Testnet => "wss://api.testnet.solana.com",
94 Cluster::Mainnet => "wss://api.mainnet-beta.solana.com",
95 Cluster::Localnet => "ws://127.0.0.1:8900",
96 Cluster::Debug => "ws://34.90.18.145:8900",
97 Cluster::Custom(_url, ws_url) => ws_url,
98 }
99 }
100}
101
102#[cfg(test)]
103mod tests {
104 use super::*;
105
106 fn test_cluster(name: &str, cluster: Cluster) {
107 assert_eq!(Cluster::from_str(name).unwrap(), cluster);
108 }
109
110 #[test]
111 fn test_cluster_parse() {
112 test_cluster("testnet", Cluster::Testnet);
113 test_cluster("mainnet", Cluster::Mainnet);
114 test_cluster("devnet", Cluster::Devnet);
115 test_cluster("localnet", Cluster::Localnet);
116 test_cluster("debug", Cluster::Debug);
117 }
118
119 #[test]
120 #[should_panic]
121 fn test_cluster_bad_parse() {
122 let bad_url = "httq://my_custom_url.test.net";
123 Cluster::from_str(bad_url).unwrap();
124 }
125
126 #[test]
127 fn test_reject_non_http_scheme_with_http_prefix() {
128 let bad_url = "httpx://my_custom_url.test.net";
129 assert!(Cluster::from_str(bad_url).is_err());
130 }
131
132 #[test]
133 fn test_http_port() {
134 let url = "http://my-url.com:7000/";
135 let cluster = Cluster::from_str(url).unwrap();
136 assert_eq!(
137 Cluster::Custom(url.to_string(), "ws://my-url.com:7001/".to_string()),
138 cluster
139 );
140 }
141
142 #[test]
143 fn test_http_no_port() {
144 let url = "http://my-url.com/";
145 let cluster = Cluster::from_str(url).unwrap();
146 assert_eq!(
147 Cluster::Custom(url.to_string(), "ws://my-url.com/".to_string()),
148 cluster
149 );
150 }
151
152 #[test]
153 fn test_https_port() {
154 let url = "https://my-url.com:7000/";
155 let cluster = Cluster::from_str(url).unwrap();
156 assert_eq!(
157 Cluster::Custom(url.to_string(), "wss://my-url.com:7001/".to_string()),
158 cluster
159 );
160 }
161
162 #[test]
163 fn test_http_max_port() {
164 let url = "http://my-url.com:65535/";
165 assert!(Cluster::from_str(url).is_err());
166 }
167
168 #[test]
169 fn test_https_no_port() {
170 let url = "https://my-url.com/";
171 let cluster = Cluster::from_str(url).unwrap();
172 assert_eq!(
173 Cluster::Custom(url.to_string(), "wss://my-url.com/".to_string()),
174 cluster
175 );
176 }
177
178 #[test]
179 fn test_upper_case() {
180 let url = "http://my-url.com/FooBar";
181 let cluster = Cluster::from_str(url).unwrap();
182 assert_eq!(
183 Cluster::Custom(url.to_string(), "ws://my-url.com/FooBar".to_string()),
184 cluster
185 );
186 }
187
188 #[test]
189 fn test_upper_case_scheme() {
190 let url = "HTTPS://my-url.com/";
191 let cluster = Cluster::from_str(url).unwrap();
192 assert_eq!(
193 Cluster::Custom(url.to_string(), "wss://my-url.com/".to_string()),
194 cluster
195 );
196 }
197}