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