soroban_cli/commands/contract/
fetch.rs

1use std::convert::Infallible;
2
3use std::io::Write;
4use std::path::{Path, PathBuf};
5use std::str::FromStr;
6use std::{fmt::Debug, fs, io};
7
8use clap::{arg, command, Parser};
9
10use crate::{
11    commands::{global, NetworkRunnable},
12    config::{
13        self, locator,
14        network::{self, Network},
15    },
16    wasm, xdr, Pwd,
17};
18
19#[derive(Parser, Debug, Default, Clone)]
20#[allow(clippy::struct_excessive_bools)]
21#[group(skip)]
22pub struct Cmd {
23    /// Contract ID to fetch
24    #[arg(long = "id", env = "STELLAR_CONTRACT_ID")]
25    pub contract_id: Option<config::UnresolvedContract>,
26    /// Wasm to fetch
27    #[arg(long = "wasm-hash", conflicts_with = "contract_id")]
28    pub wasm_hash: Option<String>,
29    /// Where to write output otherwise stdout is used
30    #[arg(long, short = 'o')]
31    pub out_file: Option<std::path::PathBuf>,
32    #[command(flatten)]
33    pub locator: locator::Args,
34    #[command(flatten)]
35    pub network: network::Args,
36}
37
38impl FromStr for Cmd {
39    type Err = clap::error::Error;
40
41    fn from_str(s: &str) -> Result<Self, Self::Err> {
42        use clap::{CommandFactory, FromArgMatches};
43        Self::from_arg_matches_mut(&mut Self::command().get_matches_from(s.split_whitespace()))
44    }
45}
46
47impl Pwd for Cmd {
48    fn set_pwd(&mut self, pwd: &Path) {
49        self.locator.set_pwd(pwd);
50    }
51}
52
53#[derive(thiserror::Error, Debug)]
54pub enum Error {
55    #[error(transparent)]
56    Config(#[from] config::Error),
57    #[error(transparent)]
58    Locator(#[from] locator::Error),
59    #[error(transparent)]
60    Io(#[from] std::io::Error),
61    #[error("reading file {0:?}: {1}")]
62    CannotWriteContractFile(PathBuf, io::Error),
63    #[error(transparent)]
64    Network(#[from] network::Error),
65    #[error("cannot create contract directory for {0:?}")]
66    CannotCreateContractDir(PathBuf),
67    #[error(transparent)]
68    Wasm(#[from] wasm::Error),
69    #[error("wasm hash is invalid {0:?}")]
70    InvalidWasmHash(String),
71    #[error("must provide one of --wasm-hash, or --id")]
72    MissingArg,
73}
74
75impl From<Infallible> for Error {
76    fn from(_: Infallible) -> Self {
77        unreachable!()
78    }
79}
80
81impl Cmd {
82    pub async fn run(&self) -> Result<(), Error> {
83        let bytes = self.get_bytes().await?;
84        if let Some(out_file) = &self.out_file {
85            if let Some(parent) = out_file.parent() {
86                if !parent.exists() {
87                    fs::create_dir_all(parent)
88                        .map_err(|_| Error::CannotCreateContractDir(out_file.clone()))?;
89                }
90            }
91            fs::write(out_file, bytes)
92                .map_err(|io| Error::CannotWriteContractFile(out_file.clone(), io))
93        } else {
94            let stdout = std::io::stdout();
95            let mut handle = stdout.lock();
96            handle.write_all(&bytes)?;
97            handle.flush()?;
98            Ok(())
99        }
100    }
101
102    pub async fn get_bytes(&self) -> Result<Vec<u8>, Error> {
103        self.run_against_rpc_server(None, None).await
104    }
105
106    pub fn network(&self) -> Result<Network, Error> {
107        Ok(self.network.get(&self.locator)?)
108    }
109}
110
111#[async_trait::async_trait]
112impl NetworkRunnable for Cmd {
113    type Error = Error;
114    type Result = Vec<u8>;
115    async fn run_against_rpc_server(
116        &self,
117        _args: Option<&global::Args>,
118        config: Option<&config::Args>,
119    ) -> Result<Vec<u8>, Error> {
120        let network = config.map_or_else(|| self.network(), |c| Ok(c.get_network()?))?;
121        if let Some(contract_id) = &self.contract_id {
122            Ok(wasm::fetch_from_contract(
123                &contract_id.resolve_contract_id(&self.locator, &network.network_passphrase)?,
124                &network,
125            )
126            .await?)
127        } else if let Some(wasm_hash) = &self.wasm_hash {
128            let hash = hex::decode(wasm_hash)
129                .map_err(|_| Error::InvalidWasmHash(wasm_hash.clone()))?
130                .try_into()
131                .map_err(|_| Error::InvalidWasmHash(wasm_hash.clone()))?;
132            let hash = xdr::Hash(hash);
133            Ok(wasm::fetch_from_wasm_hash(hash, &network).await?)
134        } else {
135            Err(Error::MissingArg)
136        }
137    }
138}