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