soroban_cli/commands/contract/
arg_parsing.rs1use 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 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 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 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!(i.type_, ScSpecTypeDef::Address) {
119 let addr = resolve_address(&s, config)?;
120 let signer = resolve_signer(&s, config);
121 s = addr;
122 if let Some(signer) = signer {
123 signers.push(signer);
124 }
125 }
126 spec.from_string(&s, &i.type_)
127 .map_err(|error| Error::CannotParseArg { arg: name, error })
128 } else if matches!(i.type_, ScSpecTypeDef::Option(_)) {
129 Ok(ScVal::Void)
130 } else if let Some(arg_path) = matches_.get_one::<PathBuf>(&fmt_arg_file_name(&name)) {
131 if matches!(i.type_, ScSpecTypeDef::Bytes | ScSpecTypeDef::BytesN(_)) {
132 Ok(ScVal::try_from(
133 &std::fs::read(arg_path)
134 .map_err(|_| Error::MissingFileArg(arg_path.clone()))?,
135 )
136 .map_err(|()| Error::CannotParseArg {
137 arg: name.clone(),
138 error: soroban_spec_tools::Error::Unknown,
139 })?)
140 } else {
141 let file_contents = std::fs::read_to_string(arg_path)
142 .map_err(|_| Error::MissingFileArg(arg_path.clone()))?;
143 tracing::debug!(
144 "file {arg_path:?}, has contents:\n{file_contents}\nAnd type {:#?}\n{}",
145 i.type_,
146 file_contents.len()
147 );
148 spec.from_string(&file_contents, &i.type_)
149 .map_err(|error| Error::CannotParseArg { arg: name, error })
150 }
151 } else {
152 Err(Error::MissingArgument(name))
153 }
154 })
155 .collect::<Result<Vec<_>, Error>>()?;
156
157 let contract_address_arg = xdr::ScAddress::Contract(ContractId(Hash(contract_id.0)));
158 let function_symbol_arg = function
159 .try_into()
160 .map_err(|()| Error::FunctionNameTooLong(function.clone()))?;
161
162 let final_args =
163 parsed_args
164 .clone()
165 .try_into()
166 .map_err(|_| Error::MaxNumberOfArgumentsReached {
167 current: parsed_args.len(),
168 maximum: ScVec::default().max_len(),
169 })?;
170
171 let invoke_args = InvokeContractArgs {
172 contract_address: contract_address_arg,
173 function_name: function_symbol_arg,
174 args: final_args,
175 };
176
177 Ok((function.clone(), spec, invoke_args, signers))
178}
179
180pub fn build_custom_cmd(name: &str, spec: &Spec) -> Result<clap::Command, Error> {
181 let func = spec
182 .find_function(name)
183 .map_err(|_| Error::FunctionNotFoundInContractSpec(name.to_string()))?;
184
185 let inputs_map = &func
187 .inputs
188 .iter()
189 .map(|i| (i.name.to_utf8_string().unwrap(), i.type_.clone()))
190 .collect::<HashMap<String, ScSpecTypeDef>>();
191 let name: &'static str = Box::leak(name.to_string().into_boxed_str());
192 let mut cmd = clap::Command::new(name)
193 .no_binary_name(true)
194 .term_width(300)
195 .max_term_width(300);
196 let kebab_name = name.to_kebab_case();
197 if kebab_name != name {
198 cmd = cmd.alias(kebab_name);
199 }
200 let doc: &'static str = Box::leak(func.doc.to_utf8_string_lossy().into_boxed_str());
201 let long_doc: &'static str = Box::leak(arg_file_help(doc).into_boxed_str());
202
203 cmd = cmd.about(Some(doc)).long_about(long_doc);
204 for (name, type_) in inputs_map {
205 let mut arg = clap::Arg::new(name);
206 let file_arg_name = fmt_arg_file_name(name);
207 let mut file_arg = clap::Arg::new(&file_arg_name);
208 arg = arg
209 .long(name)
210 .alias(name.to_kebab_case())
211 .num_args(1)
212 .value_parser(clap::builder::NonEmptyStringValueParser::new())
213 .long_help(spec.doc(name, type_)?);
214
215 file_arg = file_arg
216 .long(&file_arg_name)
217 .alias(file_arg_name.to_kebab_case())
218 .num_args(1)
219 .hide(true)
220 .value_parser(value_parser!(PathBuf))
221 .conflicts_with(name);
222
223 if let Some(value_name) = spec.arg_value_name(type_, 0) {
224 let value_name: &'static str = Box::leak(value_name.into_boxed_str());
225 arg = arg.value_name(value_name);
226 }
227
228 arg = match type_ {
230 ScSpecTypeDef::Bool => arg
231 .num_args(0..1)
232 .default_missing_value("true")
233 .default_value("false")
234 .num_args(0..=1),
235 ScSpecTypeDef::Option(_val) => arg.required(false),
236 ScSpecTypeDef::I256 | ScSpecTypeDef::I128 | ScSpecTypeDef::I64 | ScSpecTypeDef::I32 => {
237 arg.allow_hyphen_values(true)
238 }
239 _ => arg,
240 };
241
242 cmd = cmd.arg(arg);
243 cmd = cmd.arg(file_arg);
244 }
245 Ok(cmd)
246}
247
248fn fmt_arg_file_name(name: &str) -> String {
249 format!("{name}-file-path")
250}
251
252fn arg_file_help(docs: &str) -> String {
253 format!(
254 r"{docs}
255Usage Notes:
256Each arg has a corresponding --<arg_name>-file-path which is a path to a file containing the corresponding JSON argument.
257Note: The only types which aren't JSON are Bytes and BytesN, which are raw bytes"
258 )
259}
260
261pub fn output_to_string(
262 spec: &Spec,
263 res: &ScVal,
264 function: &str,
265) -> Result<TxnResult<String>, Error> {
266 let mut res_str = String::new();
267 if let Some(output) = spec.find_function(function)?.outputs.first() {
268 res_str = spec
269 .xdr_to_json(res, output)
270 .map_err(|e| Error::CannotPrintResult {
271 result: res.clone(),
272 error: e,
273 })?
274 .to_string();
275 }
276 Ok(TxnResult::Res(res_str))
277}
278
279fn resolve_address(addr_or_alias: &str, config: &config::Args) -> Result<String, Error> {
280 let sc_address: UnresolvedScAddress = addr_or_alias.parse().unwrap();
281 let account = match sc_address {
282 UnresolvedScAddress::Resolved(addr) => addr.to_string(),
283 addr @ UnresolvedScAddress::Alias(_) => {
284 let addr = addr.resolve(&config.locator, &config.get_network()?.network_passphrase)?;
285 match addr {
286 xdr::ScAddress::Account(account) => account.to_string(),
287 contract @ xdr::ScAddress::Contract(_) => contract.to_string(),
288 stellar_xdr::curr::ScAddress::MuxedAccount(account) => account.to_string(),
289 stellar_xdr::curr::ScAddress::ClaimableBalance(_)
290 | stellar_xdr::curr::ScAddress::LiquidityPool(_) => {
291 return Err(Error::UnsupportedScAddress(addr.to_string()))
292 }
293 }
294 }
295 };
296 Ok(account)
297}
298
299fn resolve_signer(addr_or_alias: &str, config: &config::Args) -> Option<SigningKey> {
300 config
301 .locator
302 .read_key(addr_or_alias)
303 .ok()?
304 .private_key(None)
305 .ok()
306 .map(|pk| SigningKey::from_bytes(&pk.0))
307}