soroban_cli/commands/contract/
arg_parsing.rs

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