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,
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
41impl Cmd {
42    pub async fn run(&self, _global_args: &global::Args) -> Result<(), Error> {
43        let print = Print::new(false);
44
45        check_version(&print).await?;
46        check_rust_version(&print);
47        check_wasm_target(&print);
48        show_config_path(&print, &self.config_locator)?;
49        show_xdr_version(&print);
50        inspect_networks(&print, &self.config_locator).await?;
51
52        Ok(())
53    }
54}
55
56fn show_config_path(print: &Print, config_locator: &locator::Args) -> Result<(), Error> {
57    let global_path = config_locator.global_config_path()?;
58
59    print.gearln(format!(
60        "Config directory: {}",
61        global_path.to_string_lossy()
62    ));
63
64    Ok(())
65}
66
67fn show_xdr_version(print: &Print) {
68    let xdr = stellar_xdr::VERSION;
69
70    print.infoln(format!("XDR version: {}", xdr.xdr_curr));
71}
72
73async fn print_network(
74    default: bool,
75    print: &Print,
76    name: &str,
77    network: &Network,
78) -> Result<(), Error> {
79    let client = network.rpc_client()?;
80    let version_info = client.get_version_info().await?;
81
82    let prefix = if default {
83        "Default network"
84    } else {
85        "Network"
86    };
87
88    print.globeln(format!("{prefix} {name:?} ({})", network.rpc_url,));
89    print.blankln(format!(" protocol {}", version_info.protocol_version));
90    print.blankln(format!(" rpc {}", version_info.version));
91
92    Ok(())
93}
94
95async fn inspect_networks(print: &Print, config_locator: &locator::Args) -> Result<(), Error> {
96    let saved_networks = KeyType::Network.list_paths(&config_locator.local_and_global()?)?;
97    let default_networks = DEFAULT_NETWORKS
98        .into_iter()
99        .map(|(name, network)| ((*name).to_string(), network.into()));
100
101    for (name, network) in default_networks {
102        // Skip default mainnet, because it has no default rpc url.
103        if name == "mainnet" {
104            continue;
105        }
106
107        if print_network(true, print, &name, &network).await.is_err() {
108            print.warnln(format!(
109                "Default network {name:?} ({}) is unreachable",
110                network.rpc_url
111            ));
112        }
113    }
114
115    for (name, _) in &saved_networks {
116        if let Ok(network) = config_locator.read_network(name) {
117            if print_network(false, print, name, &network).await.is_err() {
118                print.warnln(format!(
119                    "Network {name:?} ({}) is unreachable",
120                    network.rpc_url
121                ));
122            }
123        }
124    }
125
126    Ok(())
127}
128
129async fn check_version(print: &Print) -> Result<(), Error> {
130    if let Ok((upgrade_available, current_version, latest_version)) =
131        has_available_upgrade(false).await
132    {
133        if upgrade_available {
134            print.warnln(format!(
135                "A new release of Stellar CLI is available: {current_version} -> {latest_version}"
136            ));
137        } else {
138            print.checkln(format!(
139                "You are using the latest version of Stellar CLI: {current_version}"
140            ));
141        }
142    }
143
144    Ok(())
145}
146
147fn check_rust_version(print: &Print) {
148    match version() {
149        Ok(rust_version) => {
150            let v184 = Version::parse("1.84.0").unwrap();
151            let v182 = Version::parse("1.82.0").unwrap();
152
153            if rust_version >= v182 && rust_version < v184 {
154                print.errorln(format!(
155                    "Rust {rust_version} cannot be used to build contracts"
156                ));
157            } else {
158                print.infoln(format!("Rust version: {rust_version}"));
159            }
160        }
161        Err(_) => {
162            print.warnln("Could not determine Rust version".to_string());
163        }
164    }
165}
166
167fn check_wasm_target(print: &Print) {
168    let expected_target = get_expected_wasm_target();
169
170    let Ok(output) = Command::new("rustup")
171        .args(["target", "list", "--installed"])
172        .output()
173    else {
174        print.warnln("Could not retrieve Rust targets".to_string());
175        return;
176    };
177
178    if output.status.success() {
179        let targets = String::from_utf8_lossy(&output.stdout);
180
181        if targets.lines().any(|line| line.trim() == expected_target) {
182            print.checkln(format!("Rust target `{expected_target}` is installed"));
183        } else {
184            print.errorln(format!("Rust target `{expected_target}` is not installed"));
185        }
186    } else {
187        print.warnln("Could not retrieve Rust targets".to_string());
188    }
189}
190
191fn get_expected_wasm_target() -> String {
192    let Ok(current_version) = version() else {
193        return "wasm32v1-none".into();
194    };
195
196    let v184 = Version::parse("1.84.0").unwrap();
197
198    if current_version < v184 {
199        "wasm32-unknown-unknown".into()
200    } else {
201        "wasm32v1-none".into()
202    }
203}