Skip to main content

anchor_client/
cluster.rs

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                // Taken from:
32                // https://github.com/solana-labs/solana/blob/aea8f0df1610248d29d8ca3bc0d60e9fabc99e31/web3.js/src/util/url.ts
33
34                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}