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;
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 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 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 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 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 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}