soroban_cli/commands/contract/
arg_parsing.rs

1use crate::commands::contract::arg_parsing::Error::HelpMessage;
2use crate::commands::txn_result::TxnResult;
3use crate::config::{self, sc_address, UnresolvedScAddress};
4use crate::xdr::{
5    self, Hash, InvokeContractArgs, ScSpecEntry, ScSpecFunctionV0, ScSpecTypeDef, ScVal, ScVec,
6};
7use clap::error::ErrorKind::DisplayHelp;
8use clap::value_parser;
9use ed25519_dalek::SigningKey;
10use heck::ToKebabCase;
11use soroban_spec_tools::Spec;
12use std::collections::HashMap;
13use std::convert::TryInto;
14use std::env;
15use std::ffi::OsString;
16use std::fmt::Debug;
17use std::path::PathBuf;
18use stellar_xdr::curr::ContractId;
19
20#[derive(thiserror::Error, Debug)]
21pub enum Error {
22    #[error("parsing argument {arg}: {error}")]
23    CannotParseArg {
24        arg: String,
25        error: soroban_spec_tools::Error,
26    },
27    #[error("cannot print result {result:?}: {error}")]
28    CannotPrintResult {
29        result: ScVal,
30        error: soroban_spec_tools::Error,
31    },
32    #[error("function {0} was not found in the contract")]
33    FunctionNotFoundInContractSpec(String),
34    #[error("function name {0} is too long")]
35    FunctionNameTooLong(String),
36    #[error("argument count ({current}) surpasses maximum allowed count ({maximum})")]
37    MaxNumberOfArgumentsReached { current: usize, maximum: usize },
38    #[error(transparent)]
39    Xdr(#[from] xdr::Error),
40    #[error(transparent)]
41    StrVal(#[from] soroban_spec_tools::Error),
42    #[error("Missing argument {0}")]
43    MissingArgument(String),
44    #[error("")]
45    MissingFileArg(PathBuf),
46    #[error(transparent)]
47    ScAddress(#[from] sc_address::Error),
48    #[error(transparent)]
49    Config(#[from] config::Error),
50    #[error("")]
51    HelpMessage(String),
52    #[error("Unsupported ScAddress {0}")]
53    UnsupportedScAddress(String),
54}
55
56pub type HostFunctionParameters = (String, Spec, InvokeContractArgs, Vec<SigningKey>);
57
58fn running_cmd() -> String {
59    let mut args: Vec<String> = env::args().collect();
60
61    if let Some(pos) = args.iter().position(|arg| arg == "--") {
62        args.truncate(pos);
63    }
64
65    format!("{} --", args.join(" "))
66}
67
68pub fn build_host_function_parameters(
69    contract_id: &stellar_strkey::Contract,
70    slop: &[OsString],
71    spec_entries: &[ScSpecEntry],
72    config: &config::Args,
73) -> Result<HostFunctionParameters, Error> {
74    let spec = Spec(Some(spec_entries.to_vec()));
75
76    let mut cmd = clap::Command::new(running_cmd())
77        .no_binary_name(true)
78        .term_width(300)
79        .max_term_width(300);
80
81    for ScSpecFunctionV0 { name, .. } in spec.find_functions()? {
82        cmd = cmd.subcommand(build_custom_cmd(&name.to_utf8_string_lossy(), &spec)?);
83    }
84    cmd.build();
85    let long_help = cmd.render_long_help();
86
87    // try_get_matches_from returns an error if `help`, `--help` or `-h`are passed in the slop
88    // see clap documentation for more info: https://github.com/clap-rs/clap/blob/v4.1.8/src/builder/command.rs#L586
89    let maybe_matches = cmd.try_get_matches_from(slop);
90    let Some((function, matches_)) = (match maybe_matches {
91        Ok(mut matches) => &matches.remove_subcommand(),
92        Err(e) => {
93            // to not exit immediately (to be able to fetch help message in tests), check for an error
94            if e.kind() == DisplayHelp {
95                return Err(HelpMessage(e.to_string()));
96            }
97            e.exit();
98        }
99    }) else {
100        return Err(HelpMessage(format!("{long_help}")));
101    };
102
103    let func = spec.find_function(function)?;
104    // create parsed_args in same order as the inputs to func
105    let mut signers: Vec<SigningKey> = vec![];
106    let parsed_args = func
107        .inputs
108        .iter()
109        .map(|i| {
110            let name = i.name.to_utf8_string()?;
111            if let Some(mut val) = matches_.get_raw(&name) {
112                let mut s = val
113                    .next()
114                    .unwrap()
115                    .to_string_lossy()
116                    .trim_matches('"')
117                    .to_string();
118                if matches!(
119                    i.type_,
120                    ScSpecTypeDef::Address | ScSpecTypeDef::MuxedAddress
121                ) {
122                    let addr = resolve_address(&s, config)?;
123                    let signer = resolve_signer(&s, config);
124                    s = addr;
125                    if let Some(signer) = signer {
126                        signers.push(signer);
127                    }
128                }
129                spec.from_string(&s, &i.type_)
130                    .map_err(|error| Error::CannotParseArg { arg: name, error })
131            } else if matches!(i.type_, ScSpecTypeDef::Option(_)) {
132                Ok(ScVal::Void)
133            } else if let Some(arg_path) = matches_.get_one::<PathBuf>(&fmt_arg_file_name(&name)) {
134                if matches!(i.type_, ScSpecTypeDef::Bytes | ScSpecTypeDef::BytesN(_)) {
135                    Ok(ScVal::try_from(
136                        &std::fs::read(arg_path)
137                            .map_err(|_| Error::MissingFileArg(arg_path.clone()))?,
138                    )
139                    .map_err(|()| Error::CannotParseArg {
140                        arg: name.clone(),
141                        error: soroban_spec_tools::Error::Unknown,
142                    })?)
143                } else {
144                    let file_contents = std::fs::read_to_string(arg_path)
145                        .map_err(|_| Error::MissingFileArg(arg_path.clone()))?;
146                    tracing::debug!(
147                        "file {arg_path:?}, has contents:\n{file_contents}\nAnd type {:#?}\n{}",
148                        i.type_,
149                        file_contents.len()
150                    );
151                    spec.from_string(&file_contents, &i.type_)
152                        .map_err(|error| Error::CannotParseArg { arg: name, error })
153                }
154            } else {
155                Err(Error::MissingArgument(name))
156            }
157        })
158        .collect::<Result<Vec<_>, Error>>()?;
159
160    let contract_address_arg = xdr::ScAddress::Contract(ContractId(Hash(contract_id.0)));
161    let function_symbol_arg = function
162        .try_into()
163        .map_err(|()| Error::FunctionNameTooLong(function.clone()))?;
164
165    let final_args =
166        parsed_args
167            .clone()
168            .try_into()
169            .map_err(|_| Error::MaxNumberOfArgumentsReached {
170                current: parsed_args.len(),
171                maximum: ScVec::default().max_len(),
172            })?;
173
174    let invoke_args = InvokeContractArgs {
175        contract_address: contract_address_arg,
176        function_name: function_symbol_arg,
177        args: final_args,
178    };
179
180    Ok((function.clone(), spec, invoke_args, signers))
181}
182
183pub fn build_custom_cmd(name: &str, spec: &Spec) -> Result<clap::Command, Error> {
184    let func = spec
185        .find_function(name)
186        .map_err(|_| Error::FunctionNotFoundInContractSpec(name.to_string()))?;
187
188    // Parse the function arguments
189    let inputs_map = &func
190        .inputs
191        .iter()
192        .map(|i| (i.name.to_utf8_string().unwrap(), i.type_.clone()))
193        .collect::<HashMap<String, ScSpecTypeDef>>();
194    let name: &'static str = Box::leak(name.to_string().into_boxed_str());
195    let mut cmd = clap::Command::new(name)
196        .no_binary_name(true)
197        .term_width(300)
198        .max_term_width(300);
199    let kebab_name = name.to_kebab_case();
200    if kebab_name != name {
201        cmd = cmd.alias(kebab_name);
202    }
203    let doc: &'static str = Box::leak(func.doc.to_utf8_string_lossy().into_boxed_str());
204    let long_doc: &'static str = Box::leak(arg_file_help(doc).into_boxed_str());
205
206    cmd = cmd.about(Some(doc)).long_about(long_doc);
207    for (name, type_) in inputs_map {
208        let mut arg = clap::Arg::new(name);
209        let file_arg_name = fmt_arg_file_name(name);
210        let mut file_arg = clap::Arg::new(&file_arg_name);
211        arg = arg
212            .long(name)
213            .alias(name.to_kebab_case())
214            .num_args(1)
215            .value_parser(clap::builder::NonEmptyStringValueParser::new())
216            .long_help(spec.doc(name, type_)?);
217
218        file_arg = file_arg
219            .long(&file_arg_name)
220            .alias(file_arg_name.to_kebab_case())
221            .num_args(1)
222            .hide(true)
223            .value_parser(value_parser!(PathBuf))
224            .conflicts_with(name);
225
226        if let Some(value_name) = spec.arg_value_name(type_, 0) {
227            let value_name: &'static str = Box::leak(value_name.into_boxed_str());
228            arg = arg.value_name(value_name);
229        }
230
231        // Set up special-case arg rules
232        arg = match type_ {
233            ScSpecTypeDef::Bool => arg
234                .num_args(0..1)
235                .default_missing_value("true")
236                .default_value("false")
237                .num_args(0..=1),
238            ScSpecTypeDef::Option(_val) => arg.required(false),
239            ScSpecTypeDef::I256 | ScSpecTypeDef::I128 | ScSpecTypeDef::I64 | ScSpecTypeDef::I32 => {
240                arg.allow_hyphen_values(true)
241            }
242            _ => arg,
243        };
244
245        cmd = cmd.arg(arg);
246        cmd = cmd.arg(file_arg);
247    }
248    Ok(cmd)
249}
250
251fn fmt_arg_file_name(name: &str) -> String {
252    format!("{name}-file-path")
253}
254
255fn arg_file_help(docs: &str) -> String {
256    format!(
257        r"{docs}
258Usage Notes:
259Each arg has a corresponding --<arg_name>-file-path which is a path to a file containing the corresponding JSON argument.
260Note: The only types which aren't JSON are Bytes and BytesN, which are raw bytes"
261    )
262}
263
264pub fn output_to_string(
265    spec: &Spec,
266    res: &ScVal,
267    function: &str,
268) -> Result<TxnResult<String>, Error> {
269    let mut res_str = String::new();
270    if let Some(output) = spec.find_function(function)?.outputs.first() {
271        res_str = spec
272            .xdr_to_json(res, output)
273            .map_err(|e| Error::CannotPrintResult {
274                result: res.clone(),
275                error: e,
276            })?
277            .to_string();
278    }
279    Ok(TxnResult::Res(res_str))
280}
281
282fn resolve_address(addr_or_alias: &str, config: &config::Args) -> Result<String, Error> {
283    let sc_address: UnresolvedScAddress = addr_or_alias.parse().unwrap();
284    let account = match sc_address {
285        UnresolvedScAddress::Resolved(addr) => addr.to_string(),
286        addr @ UnresolvedScAddress::Alias(_) => {
287            let addr = addr.resolve(&config.locator, &config.get_network()?.network_passphrase)?;
288            match addr {
289                xdr::ScAddress::Account(account) => account.to_string(),
290                contract @ xdr::ScAddress::Contract(_) => contract.to_string(),
291                stellar_xdr::curr::ScAddress::MuxedAccount(account) => account.to_string(),
292                stellar_xdr::curr::ScAddress::ClaimableBalance(_)
293                | stellar_xdr::curr::ScAddress::LiquidityPool(_) => {
294                    return Err(Error::UnsupportedScAddress(addr.to_string()))
295                }
296            }
297        }
298    };
299    Ok(account)
300}
301
302fn resolve_signer(addr_or_alias: &str, config: &config::Args) -> Option<SigningKey> {
303    config
304        .locator
305        .read_key(addr_or_alias)
306        .ok()?
307        .private_key(None)
308        .ok()
309        .map(|pk| SigningKey::from_bytes(&pk.0))
310}