1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
// Wallet settings that can be configured for long-term use
use {
    serde_derive::{Deserialize, Serialize},
    std::{collections::HashMap, io, path::Path},
    url::Url,
};

lazy_static! {
    /// The default path to the CLI configuration file.
    ///
    /// This is a [lazy_static] of `Option<String>`, the value of which is
    ///
    /// > `~/.config/solana/cli/config.yml`
    ///
    /// It will only be `None` if it is unable to identify the user's home
    /// directory, which should not happen under typical OS environments.
    ///
    /// [lazy_static]: https://docs.rs/lazy_static
    pub static ref CONFIG_FILE: Option<String> = {
        dirs_next::home_dir().map(|mut path| {
            path.extend([".config", "solana", "cli", "config.yml"]);
            path.to_str().unwrap().to_string()
        })
    };
}

/// The Safecoin CLI configuration.
#[derive(Serialize, Deserialize, Debug, PartialEq, Eq)]
pub struct Config {
    /// The RPC address of a Safecoin validator node.
    ///
    /// Typical values for mainnet, devnet, and testnet are [described in the
    /// Safecoin documentation][rpcdocs].
    ///
    /// For local testing, the typical value is `http://localhost:8899`.
    ///
    /// [rpcdocs]: https://docs.solana.com/cluster/rpc-endpoints
    pub json_rpc_url: String,
    /// The address to connect to for receiving event notifications.
    ///
    /// If it is an empty string then the correct value will be derived
    /// from `json_rpc_url`.
    ///
    /// The default value is the empty string.
    pub websocket_url: String,
    /// The default signing source, which may be a keypair file, but may also
    /// represent several other types of signers, as described in the
    /// documentation for `safecoin_clap_utils::keypair::signer_from_path`.
    /// Because it represents sources other than a simple path, the name
    /// `keypair_path` is misleading, and exists for backwards compatibility
    /// reasons.
    ///
    /// The signing source can be loaded with either the `signer_from_path`
    /// function, or with `safecoin_clap_utils::keypair::DefaultSigner`.
    pub keypair_path: String,
    /// A mapping from Safecoin addresses to human-readable names.
    ///
    /// By default the only value in this map is the system program.
    #[serde(default)]
    pub address_labels: HashMap<String, String>,
    /// The default commitment level.
    ///
    /// By default the value is "confirmed", as defined by
    /// `solana_sdk::commitment_config::CommitmentLevel::Confirmed`.
    #[serde(default)]
    pub commitment: String,
}

impl Default for Config {
    fn default() -> Self {
        let keypair_path = {
            let mut keypair_path = dirs_next::home_dir().expect("home directory");
            keypair_path.extend([".config", "solana", "id.json"]);
            keypair_path.to_str().unwrap().to_string()
        };
        let json_rpc_url = "https://api.mainnet-beta.safecoin.org".to_string();

        // Empty websocket_url string indicates the client should
        // `Config::compute_websocket_url(&json_rpc_url)`
        let websocket_url = "".to_string();

        let mut address_labels = HashMap::new();
        address_labels.insert(
            "11111111111111111111111111111111".to_string(),
            "System Program".to_string(),
        );

        let commitment = "confirmed".to_string();

        Self {
            json_rpc_url,
            websocket_url,
            keypair_path,
            address_labels,
            commitment,
        }
    }
}

impl Config {
    /// Load a configuration from file.
    ///
    /// # Errors
    ///
    /// This function may return typical file I/O errors.
    pub fn load(config_file: &str) -> Result<Self, io::Error> {
        crate::load_config_file(config_file)
    }

    /// Save a configuration to file.
    ///
    /// If the file's directory does not exist, it will be created. If the file
    /// already exists, it will be overwritten.
    ///
    /// # Errors
    ///
    /// This function may return typical file I/O errors.
    pub fn save(&self, config_file: &str) -> Result<(), io::Error> {
        crate::save_config_file(self, config_file)
    }

    /// Compute the websocket URL from the RPC URL.
    ///
    /// The address is created from the RPC URL by:
    ///
    /// - adding 1 to the port number,
    /// - using the "wss" scheme if the RPC URL has an "https" scheme, or the
    ///   "ws" scheme if the RPC URL has an "http" scheme.
    ///
    /// If `json_rpc_url` cannot be parsed as a URL then this function returns
    /// the empty string.
    pub fn compute_websocket_url(json_rpc_url: &str) -> String {
        let json_rpc_url: Option<Url> = json_rpc_url.parse().ok();
        if json_rpc_url.is_none() {
            return "".to_string();
        }
        let json_rpc_url = json_rpc_url.unwrap();
        let is_secure = json_rpc_url.scheme().to_ascii_lowercase() == "https";
        let mut ws_url = json_rpc_url.clone();
        ws_url
            .set_scheme(if is_secure { "wss" } else { "ws" })
            .expect("unable to set scheme");
        if let Some(port) = json_rpc_url.port() {
            let port = port.checked_add(1).expect("port out of range");
            ws_url.set_port(Some(port)).expect("unable to set port");
        }
        ws_url.to_string()
    }

    /// Load a map of address/name pairs from a YAML file at the given path and
    /// insert them into the configuration.
    pub fn import_address_labels<P>(&mut self, filename: P) -> Result<(), io::Error>
    where
        P: AsRef<Path>,
    {
        let imports: HashMap<String, String> = crate::load_config_file(filename)?;
        for (address, label) in imports.into_iter() {
            self.address_labels.insert(address, label);
        }
        Ok(())
    }

    /// Save the map of address/name pairs contained in the configuration to a
    /// YAML file at the given path.
    pub fn export_address_labels<P>(&self, filename: P) -> Result<(), io::Error>
    where
        P: AsRef<Path>,
    {
        crate::save_config_file(&self.address_labels, filename)
    }
}

#[cfg(test)]
mod test {
    use super::*;

    #[test]
    fn compute_websocket_url() {
        assert_eq!(
            Config::compute_websocket_url("http://api.devnet.safecoin.org"),
            "ws://api.devnet.safecoin.org/".to_string()
        );

        assert_eq!(
            Config::compute_websocket_url("https://api.devnet.safecoin.org"),
            "wss://api.devnet.safecoin.org/".to_string()
        );

        assert_eq!(
            Config::compute_websocket_url("http://example.com:8899"),
            "ws://example.com:8900/".to_string()
        );
        assert_eq!(
            Config::compute_websocket_url("https://example.com:1234"),
            "wss://example.com:1235/".to_string()
        );

        assert_eq!(Config::compute_websocket_url("garbage"), String::new());
    }
}