solana_cli_config/config.rs
1// Wallet settings that can be configured for long-term use
2use {
3 serde_derive::{Deserialize, Serialize},
4 std::{collections::HashMap, io, path::Path},
5 url::Url,
6};
7
8lazy_static! {
9 /// The default path to the CLI configuration file.
10 ///
11 /// This is a [lazy_static] of `Option<String>`, the value of which is
12 ///
13 /// > `~/.config/solana/cli/config.yml`
14 ///
15 /// It will only be `None` if it is unable to identify the user's home
16 /// directory, which should not happen under typical OS environments.
17 ///
18 /// [lazy_static]: https://docs.rs/lazy_static
19 pub static ref CONFIG_FILE: Option<String> = {
20 dirs_next::home_dir().map(|mut path| {
21 path.extend([".config", "solana", "cli", "config.yml"]);
22 path.to_str().unwrap().to_string()
23 })
24 };
25}
26
27/// The Solana CLI configuration.
28#[derive(Serialize, Deserialize, Debug, PartialEq, Eq)]
29pub struct Config {
30 /// The RPC address of a Solana validator node.
31 ///
32 /// Typical values for mainnet, devnet, and testnet are [described in the
33 /// Solana documentation][rpcdocs].
34 ///
35 /// For local testing, the typical value is `http://localhost:8899`.
36 ///
37 /// [rpcdocs]: https://solana.com/docs/core/clusters
38 pub json_rpc_url: String,
39 /// The address to connect to for receiving event notifications.
40 ///
41 /// If it is an empty string then the correct value will be derived
42 /// from `json_rpc_url`.
43 ///
44 /// The default value is the empty string.
45 pub websocket_url: String,
46 /// The default signing source, which may be a keypair file, but may also
47 /// represent several other types of signers, as described in the
48 /// documentation for `solana_clap_utils::keypair::signer_from_path`.
49 /// Because it represents sources other than a simple path, the name
50 /// `keypair_path` is misleading, and exists for backwards compatibility
51 /// reasons.
52 ///
53 /// The signing source can be loaded with either the `signer_from_path`
54 /// function, or with `solana_clap_utils::keypair::DefaultSigner`.
55 pub keypair_path: String,
56 /// A mapping from Solana addresses to human-readable names.
57 ///
58 /// By default the only value in this map is the system program.
59 #[serde(default)]
60 pub address_labels: HashMap<String, String>,
61 /// The default commitment level.
62 ///
63 /// By default the value is "confirmed", as defined by
64 /// `solana_commitment_config::CommitmentLevel::Confirmed`.
65 #[serde(default)]
66 pub commitment: String,
67}
68
69impl Default for Config {
70 fn default() -> Self {
71 let keypair_path = {
72 let mut keypair_path = dirs_next::home_dir().expect("home directory");
73 keypair_path.extend([".config", "solana", "id.json"]);
74 keypair_path.to_str().unwrap().to_string()
75 };
76 let json_rpc_url = "https://api.mainnet-beta.solana.com".to_string();
77
78 // Empty websocket_url string indicates the client should
79 // `Config::compute_websocket_url(&json_rpc_url)`
80 let websocket_url = "".to_string();
81
82 let mut address_labels = HashMap::new();
83 address_labels.insert(
84 "11111111111111111111111111111111".to_string(),
85 "System Program".to_string(),
86 );
87
88 let commitment = "confirmed".to_string();
89
90 Self {
91 json_rpc_url,
92 websocket_url,
93 keypair_path,
94 address_labels,
95 commitment,
96 }
97 }
98}
99
100impl Config {
101 /// Load a configuration from file.
102 ///
103 /// # Errors
104 ///
105 /// This function may return typical file I/O errors.
106 pub fn load(config_file: &str) -> Result<Self, io::Error> {
107 crate::load_config_file(config_file)
108 }
109
110 /// Save a configuration to file.
111 ///
112 /// If the file's directory does not exist, it will be created. If the file
113 /// already exists, it will be overwritten.
114 ///
115 /// # Errors
116 ///
117 /// This function may return typical file I/O errors.
118 pub fn save(&self, config_file: &str) -> Result<(), io::Error> {
119 crate::save_config_file(self, config_file)
120 }
121
122 /// Compute the websocket URL from the RPC URL.
123 ///
124 /// The address is created from the RPC URL by:
125 ///
126 /// - adding 1 to the port number,
127 /// - using the "wss" scheme if the RPC URL has an "https" scheme, or the
128 /// "ws" scheme if the RPC URL has an "http" scheme.
129 ///
130 /// If `json_rpc_url` cannot be parsed as a URL then this function returns
131 /// the empty string.
132 pub fn compute_websocket_url(json_rpc_url: &str) -> String {
133 let json_rpc_url: Option<Url> = json_rpc_url.parse().ok();
134 if json_rpc_url.is_none() {
135 return "".to_string();
136 }
137 let json_rpc_url = json_rpc_url.unwrap();
138 let is_secure = json_rpc_url.scheme().eq_ignore_ascii_case("https");
139 let mut ws_url = json_rpc_url.clone();
140 ws_url
141 .set_scheme(if is_secure { "wss" } else { "ws" })
142 .expect("unable to set scheme");
143 if let Some(port) = json_rpc_url.port() {
144 let port = port.checked_add(1).expect("port out of range");
145 ws_url.set_port(Some(port)).expect("unable to set port");
146 }
147 ws_url.to_string()
148 }
149
150 /// Load a map of address/name pairs from a YAML file at the given path and
151 /// insert them into the configuration.
152 pub fn import_address_labels<P>(&mut self, filename: P) -> Result<(), io::Error>
153 where
154 P: AsRef<Path>,
155 {
156 let imports: HashMap<String, String> = crate::load_config_file(filename)?;
157 for (address, label) in imports.into_iter() {
158 self.address_labels.insert(address, label);
159 }
160 Ok(())
161 }
162
163 /// Save the map of address/name pairs contained in the configuration to a
164 /// YAML file at the given path.
165 pub fn export_address_labels<P>(&self, filename: P) -> Result<(), io::Error>
166 where
167 P: AsRef<Path>,
168 {
169 crate::save_config_file(&self.address_labels, filename)
170 }
171}
172
173#[cfg(test)]
174mod test {
175 use super::*;
176
177 #[test]
178 fn compute_websocket_url() {
179 assert_eq!(
180 Config::compute_websocket_url("http://api.devnet.solana.com"),
181 "ws://api.devnet.solana.com/".to_string()
182 );
183
184 assert_eq!(
185 Config::compute_websocket_url("https://api.devnet.solana.com"),
186 "wss://api.devnet.solana.com/".to_string()
187 );
188
189 assert_eq!(
190 Config::compute_websocket_url("http://example.com:8899"),
191 "ws://example.com:8900/".to_string()
192 );
193 assert_eq!(
194 Config::compute_websocket_url("https://example.com:1234"),
195 "wss://example.com:1235/".to_string()
196 );
197
198 assert_eq!(Config::compute_websocket_url("garbage"), String::new());
199 }
200}