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 _ if s.starts_with("http") => {
29 let http_url = s;
30
31 let mut ws_url = Url::parse(http_url)?;
35 if let Some(port) = ws_url.port() {
36 ws_url
37 .set_port(Some(port + 1))
38 .map_err(|_| anyhow!("Unable to set port"))?;
39 }
40 if ws_url.scheme() == "https" {
41 ws_url
42 .set_scheme("wss")
43 .map_err(|_| anyhow!("Unable to set scheme"))?;
44 } else {
45 ws_url
46 .set_scheme("ws")
47 .map_err(|_| anyhow!("Unable to set scheme"))?;
48 }
49
50 Ok(Cluster::Custom(http_url.to_string(), ws_url.to_string()))
51 }
52 _ => Err(anyhow::Error::msg(
53 "Cluster must be one of [localnet, testnet, mainnet, devnet] or be an http or \
54 https url\n",
55 )),
56 }
57 }
58}
59
60impl std::fmt::Display for Cluster {
61 fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
62 let clust_str = match self {
63 Cluster::Testnet => "testnet",
64 Cluster::Mainnet => "mainnet",
65 Cluster::Devnet => "devnet",
66 Cluster::Localnet => "localnet",
67 Cluster::Debug => "debug",
68 Cluster::Custom(url, _ws_url) => url,
69 };
70 write!(f, "{clust_str}")
71 }
72}
73
74impl Cluster {
75 pub fn url(&self) -> &str {
76 match self {
77 Cluster::Devnet => "https://api.devnet.solana.com",
78 Cluster::Testnet => "https://api.testnet.solana.com",
79 Cluster::Mainnet => "https://api.mainnet-beta.solana.com",
80 Cluster::Localnet => "http://127.0.0.1:8899",
81 Cluster::Debug => "http://34.90.18.145:8899",
82 Cluster::Custom(url, _ws_url) => url,
83 }
84 }
85 pub fn ws_url(&self) -> &str {
86 match self {
87 Cluster::Devnet => "wss://api.devnet.solana.com",
88 Cluster::Testnet => "wss://api.testnet.solana.com",
89 Cluster::Mainnet => "wss://api.mainnet-beta.solana.com",
90 Cluster::Localnet => "ws://127.0.0.1:8900",
91 Cluster::Debug => "ws://34.90.18.145:8900",
92 Cluster::Custom(_url, ws_url) => ws_url,
93 }
94 }
95}
96
97#[cfg(test)]
98mod tests {
99 use super::*;
100
101 fn test_cluster(name: &str, cluster: Cluster) {
102 assert_eq!(Cluster::from_str(name).unwrap(), cluster);
103 }
104
105 #[test]
106 fn test_cluster_parse() {
107 test_cluster("testnet", Cluster::Testnet);
108 test_cluster("mainnet", Cluster::Mainnet);
109 test_cluster("devnet", Cluster::Devnet);
110 test_cluster("localnet", Cluster::Localnet);
111 test_cluster("debug", Cluster::Debug);
112 }
113
114 #[test]
115 #[should_panic]
116 fn test_cluster_bad_parse() {
117 let bad_url = "httq://my_custom_url.test.net";
118 Cluster::from_str(bad_url).unwrap();
119 }
120
121 #[test]
122 fn test_http_port() {
123 let url = "http://my-url.com:7000/";
124 let cluster = Cluster::from_str(url).unwrap();
125 assert_eq!(
126 Cluster::Custom(url.to_string(), "ws://my-url.com:7001/".to_string()),
127 cluster
128 );
129 }
130
131 #[test]
132 fn test_http_no_port() {
133 let url = "http://my-url.com/";
134 let cluster = Cluster::from_str(url).unwrap();
135 assert_eq!(
136 Cluster::Custom(url.to_string(), "ws://my-url.com/".to_string()),
137 cluster
138 );
139 }
140
141 #[test]
142 fn test_https_port() {
143 let url = "https://my-url.com:7000/";
144 let cluster = Cluster::from_str(url).unwrap();
145 assert_eq!(
146 Cluster::Custom(url.to_string(), "wss://my-url.com:7001/".to_string()),
147 cluster
148 );
149 }
150 #[test]
151 fn test_https_no_port() {
152 let url = "https://my-url.com/";
153 let cluster = Cluster::from_str(url).unwrap();
154 assert_eq!(
155 Cluster::Custom(url.to_string(), "wss://my-url.com/".to_string()),
156 cluster
157 );
158 }
159
160 #[test]
161 fn test_upper_case() {
162 let url = "http://my-url.com/FooBar";
163 let cluster = Cluster::from_str(url).unwrap();
164 assert_eq!(
165 Cluster::Custom(url.to_string(), "ws://my-url.com/FooBar".to_string()),
166 cluster
167 );
168 }
169}