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::{container::shared::Engine, 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        check_container_engine(&print);
54        show_config_path(&print, &self.config_locator)?;
55        show_data_path(&print)?;
56        show_xdr_version(&print);
57        inspect_networks(&print, &self.config_locator).await?;
58
59        Ok(())
60    }
61}
62
63fn show_config_path(print: &Print, config_locator: &locator::Args) -> Result<(), Error> {
64    let global_path = config_locator.global_config_path()?;
65
66    print.gearln(format!(
67        "Config directory: {}",
68        global_path.to_string_lossy()
69    ));
70
71    Ok(())
72}
73
74fn show_data_path(print: &Print) -> Result<(), Error> {
75    let path = data::data_local_dir()?;
76
77    print.dirln(format!("Data directory: {}", path.to_string_lossy()));
78
79    Ok(())
80}
81
82fn show_xdr_version(print: &Print) {
83    let xdr = stellar_xdr::VERSION;
84
85    print.infoln(format!("XDR version: {}", xdr.xdr));
86}
87
88async fn print_network(
89    default: bool,
90    print: &Print,
91    name: &str,
92    network: &Network,
93) -> Result<(), Error> {
94    let client = network.rpc_client()?;
95    let version_info = client.get_version_info().await?;
96
97    let prefix = if default {
98        "Default network"
99    } else {
100        "Network"
101    };
102
103    print.globeln(format!(
104        "{prefix} {name:?} ({})",
105        redact_url(&network.rpc_url)
106    ));
107    print.blankln(format!("protocol {}", version_info.protocol_version));
108    print.blankln(format!("rpc {}", version_info.version));
109
110    Ok(())
111}
112
113async fn inspect_networks(print: &Print, config_locator: &locator::Args) -> Result<(), Error> {
114    let saved_networks = KeyType::Network.list_paths(&config_locator.local_and_global()?)?;
115    let default_networks = DEFAULT_NETWORKS
116        .into_iter()
117        .map(|(name, network)| ((*name).to_string(), network.into()));
118
119    for (name, network) in default_networks {
120        // Skip default mainnet, because it has no default rpc url.
121        if name == "mainnet" {
122            continue;
123        }
124
125        if print_network(true, print, &name, &network).await.is_err() {
126            print.warnln(format!(
127                "Default network {name:?} ({}) is unreachable",
128                redact_url(&network.rpc_url)
129            ));
130        }
131    }
132
133    for (name, _) in &saved_networks {
134        if let Ok(network) = config_locator.read_network(name) {
135            if print_network(false, print, name, &network).await.is_err() {
136                print.warnln(format!(
137                    "Network {name:?} ({}) is unreachable",
138                    redact_url(&network.rpc_url)
139                ));
140            }
141        }
142    }
143
144    Ok(())
145}
146
147async fn check_version(print: &Print) -> Result<(), Error> {
148    if let Ok((upgrade_available, current_version, latest_version)) =
149        has_available_upgrade(false).await
150    {
151        if upgrade_available {
152            print.warnln(format!(
153                "A new release of Stellar CLI is available: {current_version} -> {latest_version}"
154            ));
155        } else {
156            print.checkln(format!(
157                "You are using the latest version of Stellar CLI: {current_version}"
158            ));
159        }
160    }
161
162    Ok(())
163}
164
165fn check_rust_version(print: &Print) {
166    match version() {
167        Ok(rust_version) => {
168            let v184 = Version::parse("1.84.0").unwrap();
169            let v182 = Version::parse("1.82.0").unwrap();
170
171            if rust_version >= v182 && rust_version < v184 {
172                print.errorln(format!(
173                    "Rust {rust_version} cannot be used to build contracts"
174                ));
175            } else {
176                print.infoln(format!("Rust version: {rust_version}"));
177            }
178        }
179        Err(_) => {
180            print.warnln("Could not determine Rust version".to_string());
181        }
182    }
183}
184
185fn check_wasm_target(print: &Print) {
186    let expected_target = get_expected_wasm_target();
187
188    let Ok(output) = Command::new("rustup")
189        .args(["target", "list", "--installed"])
190        .output()
191    else {
192        print.warnln("Could not retrieve Rust targets".to_string());
193        return;
194    };
195
196    if output.status.success() {
197        let targets = String::from_utf8_lossy(&output.stdout);
198
199        if targets.lines().any(|line| line.trim() == expected_target) {
200            print.checkln(format!("Rust target `{expected_target}` is installed"));
201        } else {
202            print.errorln(format!("Rust target `{expected_target}` is not installed"));
203        }
204    } else {
205        print.warnln("Could not retrieve Rust targets".to_string());
206    }
207}
208
209fn check_container_engine(print: &Print) {
210    // `resolved_default` silently falls back to docker on a bad
211    // `STELLAR_CONTAINER_ENGINE`, so probe the raw value first to warn about a
212    // typo instead of reporting a phantom docker as available.
213    if !Engine::is_valid_engine() {
214        let value = std::env::var("STELLAR_CONTAINER_ENGINE").unwrap_or_default();
215        print.warnln(format!(
216            "Unknown container engine `{value}`; expected one of: {}",
217            Engine::supported_engines()
218        ));
219        return;
220    }
221
222    let engine = Engine::resolved_default();
223
224    // `output()` succeeds as long as the binary spawned, regardless of exit
225    // status, so it tells us whether the engine is installed on `PATH`.
226    if Command::new(engine.program())
227        .arg("--version")
228        .output()
229        .is_ok()
230    {
231        print.checkln(format!("Container engine `{engine}` is available"));
232    } else {
233        print.warnln(format!("Container engine `{engine}` is not installed"));
234    }
235}
236
237fn check_optional_features(print: &Print) {
238    #[cfg(feature = "additional-libs")]
239    {
240        print.checkln("Wasm optimization");
241        print.checkln("Secure store (OS keyring)");
242        print.checkln("Ledger hardware wallet");
243    }
244
245    #[cfg(not(feature = "additional-libs"))]
246    {
247        print.warnln(
248            "The following features are disabled until `--features additional-libs` is used:",
249        );
250        print.blankln("- Wasm optimization");
251        print.blankln("- Secure store (OS keyring)");
252        print.blankln("- Ledger hardware wallet");
253    }
254}
255
256fn get_expected_wasm_target() -> String {
257    let Ok(current_version) = version() else {
258        return "wasm32v1-none".into();
259    };
260
261    let v184 = Version::parse("1.84.0").unwrap();
262
263    if current_version < v184 {
264        "wasm32-unknown-unknown".into()
265    } else {
266        "wasm32v1-none".into()
267    }
268}