Skip to main content

soroban_cli/commands/
doctor.rs

1use clap::Parser;
2use rustc_version::version;
3use semver::Version;
4use std::fmt::Debug;
5use std::process::Command;
6
7use crate::{
8    commands::global,
9    config::{
10        self, data,
11        locator::{self, KeyType},
12        network::{Network, DEFAULTS as DEFAULT_NETWORKS},
13    },
14    print::Print,
15    rpc,
16    upgrade_check::has_available_upgrade,
17};
18
19#[derive(Parser, Debug, Clone)]
20#[group(skip)]
21pub struct Cmd {
22    #[command(flatten)]
23    pub config_locator: locator::Args,
24}
25
26#[derive(thiserror::Error, Debug)]
27pub enum Error {
28    #[error(transparent)]
29    Locator(#[from] locator::Error),
30
31    #[error(transparent)]
32    Network(#[from] config::network::Error),
33
34    #[error(transparent)]
35    RpcClient(#[from] rpc::Error),
36
37    #[error(transparent)]
38    Io(#[from] std::io::Error),
39
40    #[error(transparent)]
41    Data(#[from] data::Error),
42}
43
44impl Cmd {
45    pub async fn run(&self, _global_args: &global::Args) -> Result<(), Error> {
46        let print = Print::new(false);
47
48        check_version(&print).await?;
49        check_rust_version(&print);
50        check_wasm_target(&print);
51        check_optional_features(&print);
52        show_config_path(&print, &self.config_locator)?;
53        show_data_path(&print)?;
54        show_xdr_version(&print);
55        inspect_networks(&print, &self.config_locator).await?;
56
57        Ok(())
58    }
59}
60
61fn show_config_path(print: &Print, config_locator: &locator::Args) -> Result<(), Error> {
62    let global_path = config_locator.global_config_path()?;
63
64    print.gearln(format!(
65        "Config directory: {}",
66        global_path.to_string_lossy()
67    ));
68
69    Ok(())
70}
71
72fn show_data_path(print: &Print) -> Result<(), Error> {
73    let path = data::data_local_dir()?;
74
75    print.dirln(format!("Data directory: {}", path.to_string_lossy()));
76
77    Ok(())
78}
79
80fn show_xdr_version(print: &Print) {
81    let xdr = stellar_xdr::VERSION;
82
83    print.infoln(format!("XDR version: {}", xdr.xdr_curr));
84}
85
86async fn print_network(
87    default: bool,
88    print: &Print,
89    name: &str,
90    network: &Network,
91) -> Result<(), Error> {
92    let client = network.rpc_client()?;
93    let version_info = client.get_version_info().await?;
94
95    let prefix = if default {
96        "Default network"
97    } else {
98        "Network"
99    };
100
101    print.globeln(format!("{prefix} {name:?} ({})", network.rpc_url,));
102    print.blankln(format!("protocol {}", version_info.protocol_version));
103    print.blankln(format!("rpc {}", version_info.version));
104
105    Ok(())
106}
107
108async fn inspect_networks(print: &Print, config_locator: &locator::Args) -> Result<(), Error> {
109    let saved_networks = KeyType::Network.list_paths(&config_locator.local_and_global()?)?;
110    let default_networks = DEFAULT_NETWORKS
111        .into_iter()
112        .map(|(name, network)| ((*name).to_string(), network.into()));
113
114    for (name, network) in default_networks {
115        // Skip default mainnet, because it has no default rpc url.
116        if name == "mainnet" {
117            continue;
118        }
119
120        if print_network(true, print, &name, &network).await.is_err() {
121            print.warnln(format!(
122                "Default network {name:?} ({}) is unreachable",
123                network.rpc_url
124            ));
125        }
126    }
127
128    for (name, _) in &saved_networks {
129        if let Ok(network) = config_locator.read_network(name) {
130            if print_network(false, print, name, &network).await.is_err() {
131                print.warnln(format!(
132                    "Network {name:?} ({}) is unreachable",
133                    network.rpc_url
134                ));
135            }
136        }
137    }
138
139    Ok(())
140}
141
142async fn check_version(print: &Print) -> Result<(), Error> {
143    if let Ok((upgrade_available, current_version, latest_version)) =
144        has_available_upgrade(false).await
145    {
146        if upgrade_available {
147            print.warnln(format!(
148                "A new release of Stellar CLI is available: {current_version} -> {latest_version}"
149            ));
150        } else {
151            print.checkln(format!(
152                "You are using the latest version of Stellar CLI: {current_version}"
153            ));
154        }
155    }
156
157    Ok(())
158}
159
160fn check_rust_version(print: &Print) {
161    match version() {
162        Ok(rust_version) => {
163            let v184 = Version::parse("1.84.0").unwrap();
164            let v182 = Version::parse("1.82.0").unwrap();
165
166            if rust_version >= v182 && rust_version < v184 {
167                print.errorln(format!(
168                    "Rust {rust_version} cannot be used to build contracts"
169                ));
170            } else {
171                print.infoln(format!("Rust version: {rust_version}"));
172            }
173        }
174        Err(_) => {
175            print.warnln("Could not determine Rust version".to_string());
176        }
177    }
178}
179
180fn check_wasm_target(print: &Print) {
181    let expected_target = get_expected_wasm_target();
182
183    let Ok(output) = Command::new("rustup")
184        .args(["target", "list", "--installed"])
185        .output()
186    else {
187        print.warnln("Could not retrieve Rust targets".to_string());
188        return;
189    };
190
191    if output.status.success() {
192        let targets = String::from_utf8_lossy(&output.stdout);
193
194        if targets.lines().any(|line| line.trim() == expected_target) {
195            print.checkln(format!("Rust target `{expected_target}` is installed"));
196        } else {
197            print.errorln(format!("Rust target `{expected_target}` is not installed"));
198        }
199    } else {
200        print.warnln("Could not retrieve Rust targets".to_string());
201    }
202}
203
204fn check_optional_features(print: &Print) {
205    #[cfg(feature = "additional-libs")]
206    {
207        print.checkln("Wasm optimization");
208        print.checkln("Secure store (OS keyring)");
209        print.checkln("Ledger hardware wallet");
210    }
211
212    #[cfg(not(feature = "additional-libs"))]
213    {
214        print.warnln(
215            "The following features are disabled until `--features additional-libs` is used:",
216        );
217        print.blankln("- Wasm optimization");
218        print.blankln("- Secure store (OS keyring)");
219        print.blankln("- Ledger hardware wallet");
220    }
221}
222
223fn get_expected_wasm_target() -> String {
224    let Ok(current_version) = version() else {
225        return "wasm32v1-none".into();
226    };
227
228    let v184 = Version::parse("1.84.0").unwrap();
229
230    if current_version < v184 {
231        "wasm32-unknown-unknown".into()
232    } else {
233        "wasm32v1-none".into()
234    }
235}