soroban_cli/commands/contract/info/
shared.rs

1use std::path::PathBuf;
2
3use clap::arg;
4
5use crate::{
6    commands::contract::info::shared::Error::InvalidWasmHash,
7    config::{
8        self, locator,
9        network::{self, Network},
10    },
11    print::Print,
12    utils::rpc::get_remote_wasm_from_hash,
13    wasm::{self, Error::ContractIsStellarAsset},
14    xdr,
15};
16
17#[derive(Debug, clap::Args, Clone, Default)]
18#[command(group(
19    clap::ArgGroup::new("Source")
20    .required(true)
21    .args(& ["wasm", "wasm_hash", "contract_id"]),
22))]
23#[group(skip)]
24pub struct Args {
25    /// Wasm file path on local filesystem. Provide this OR `--wasm-hash` OR `--contract-id`.
26    #[arg(
27        long,
28        group = "Source",
29        conflicts_with = "contract_id",
30        conflicts_with = "wasm_hash"
31    )]
32    pub wasm: Option<PathBuf>,
33    /// Hash of Wasm blob on a network. Provide this OR `--wasm` OR `--contract-id`.
34    #[arg(
35        long = "wasm-hash",
36        group = "Source",
37        conflicts_with = "contract_id",
38        conflicts_with = "wasm"
39    )]
40    pub wasm_hash: Option<String>,
41    /// Contract ID/alias on a network. Provide this OR `--wasm-hash` OR `--wasm`.
42    #[arg(
43        long,
44        env = "STELLAR_CONTRACT_ID",
45        group = "Source",
46        visible_alias = "id",
47        conflicts_with = "wasm",
48        conflicts_with = "wasm_hash"
49    )]
50    pub contract_id: Option<config::UnresolvedContract>,
51    #[command(flatten)]
52    pub network: network::Args,
53    #[command(flatten)]
54    pub locator: locator::Args,
55}
56
57#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq, clap::ValueEnum, Default)]
58pub enum MetasInfoOutput {
59    /// Text output of the meta info entry
60    #[default]
61    Text,
62    /// XDR output of the info entry
63    XdrBase64,
64    /// JSON output of the info entry (one line, not formatted)
65    Json,
66    /// Formatted (multiline) JSON output of the info entry
67    JsonFormatted,
68}
69
70#[derive(thiserror::Error, Debug)]
71pub enum Error {
72    #[error(transparent)]
73    Network(#[from] network::Error),
74    #[error(transparent)]
75    Wasm(#[from] wasm::Error),
76    #[error("provided wasm hash is invalid {0:?}")]
77    InvalidWasmHash(String),
78    #[error("must provide one of --wasm, --wasm-hash, or --contract-id")]
79    MissingArg,
80    #[error(transparent)]
81    Rpc(#[from] soroban_rpc::Error),
82    #[error(transparent)]
83    Locator(#[from] locator::Error),
84}
85
86pub struct Fetched {
87    pub contract: Contract,
88    pub source: Source,
89}
90
91pub enum Contract {
92    Wasm { wasm_bytes: Vec<u8> },
93    StellarAssetContract,
94}
95
96pub enum Source {
97    File {
98        path: PathBuf,
99    },
100    Wasm {
101        hash: String,
102        network: Network,
103    },
104    Contract {
105        resolved_address: String,
106        network: Network,
107    },
108}
109
110impl Source {
111    pub fn network(&self) -> Option<&Network> {
112        match self {
113            Source::File { .. } => None,
114            Source::Wasm { ref network, .. } | Source::Contract { ref network, .. } => {
115                Some(network)
116            }
117        }
118    }
119}
120
121pub async fn fetch(args: &Args, print: &Print) -> Result<Fetched, Error> {
122    // Check if a local WASM file path is provided
123    if let Some(path) = &args.wasm {
124        // Read the WASM file and return its contents
125        print.infoln("Loading contract spec from file...");
126        let wasm_bytes = wasm::Args { wasm: path.clone() }.read()?;
127        return Ok(Fetched {
128            contract: Contract::Wasm { wasm_bytes },
129            source: Source::File { path: path.clone() },
130        });
131    }
132
133    // If no local wasm, then check for wasm_hash and fetch from the network
134    let network = &args.network.get(&args.locator)?;
135    print.infoln(format!("Network: {}", network.network_passphrase));
136
137    if let Some(wasm_hash) = &args.wasm_hash {
138        let hash = hex::decode(wasm_hash)
139            .map_err(|_| InvalidWasmHash(wasm_hash.clone()))?
140            .try_into()
141            .map_err(|_| InvalidWasmHash(wasm_hash.clone()))?;
142
143        let hash = xdr::Hash(hash);
144
145        let client = network.rpc_client()?;
146
147        client
148            .verify_network_passphrase(Some(&network.network_passphrase))
149            .await?;
150
151        print.globeln(format!(
152            "Downloading contract spec for wasm hash: {wasm_hash}"
153        ));
154        let wasm_bytes = get_remote_wasm_from_hash(&client, &hash).await?;
155        Ok(Fetched {
156            contract: Contract::Wasm { wasm_bytes },
157            source: Source::Wasm {
158                hash: wasm_hash.clone(),
159                network: network.clone(),
160            },
161        })
162    } else if let Some(contract_id) = &args.contract_id {
163        let contract_id =
164            contract_id.resolve_contract_id(&args.locator, &network.network_passphrase)?;
165        let derived_address = xdr::ScAddress::Contract(xdr::Hash(contract_id.0)).to_string();
166        print.globeln(format!("Downloading contract spec: {derived_address}"));
167        let res = wasm::fetch_from_contract(&contract_id, network).await;
168        if let Some(ContractIsStellarAsset) = res.as_ref().err() {
169            return Ok(Fetched {
170                contract: Contract::StellarAssetContract,
171                source: Source::Contract {
172                    resolved_address: derived_address,
173                    network: network.clone(),
174                },
175            });
176        }
177        Ok(Fetched {
178            contract: Contract::Wasm { wasm_bytes: res? },
179            source: Source::Contract {
180                resolved_address: derived_address,
181                network: network.clone(),
182            },
183        })
184    } else {
185        return Err(Error::MissingArg);
186    }
187}