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::print::Print;
6use crate::signer::{self, Signer};
7use crate::xdr::{
8 self, Hash, InvokeContractArgs, ScSpecEntry, ScSpecFunctionV0, ScSpecTypeDef, ScVal, ScVec,
9};
10use clap::error::ErrorKind::DisplayHelp;
11use clap::value_parser;
12use heck::ToKebabCase;
13use soroban_spec_tools::{sanitize, Spec};
14use std::collections::HashMap;
15use std::convert::TryInto;
16use std::env;
17use std::ffi::OsString;
18use std::fmt::Debug;
19use std::path::PathBuf;
20use stellar_xdr::curr::ContractId;
21
22#[derive(thiserror::Error, Debug)]
23pub enum Error {
24 #[error("Failed to parse argument '{arg}': {error}\n\nContext: Expected type {expected_type}, but received: '{received_value}'\n\nSuggestion: {suggestion}")]
25 CannotParseArg {
26 arg: String,
27 error: soroban_spec_tools::Error,
28 expected_type: String,
29 received_value: String,
30 suggestion: String,
31 },
32 #[error("Invalid JSON in argument '{arg}': {json_error}\n\nReceived value: '{received_value}'\n\nSuggestions:\n- Check for missing quotes around strings\n- Ensure proper JSON syntax (commas, brackets, etc.)\n- For complex objects, consider using --{arg}-file-path to load from a file")]
33 InvalidJsonArg {
34 arg: String,
35 json_error: String,
36 received_value: String,
37 },
38 #[error("Type mismatch for argument '{arg}': expected {expected_type}, but got {actual_type}\n\nReceived value: '{received_value}'\n\nSuggestions:\n- For {expected_type}, ensure the value is properly formatted\n- Check the contract specification for the correct argument type")]
39 TypeMismatch {
40 arg: String,
41 expected_type: String,
42 actual_type: String,
43 received_value: String,
44 },
45 #[error("Missing required argument '{arg}' of type {expected_type}\n\nSuggestions:\n- Add the argument: --{arg} <value>\n- Or use a file: --{arg}-file-path <path-to-json-file>\n- Check the contract specification for required arguments")]
46 MissingArgument { arg: String, expected_type: String },
47 #[error("Cannot read file {file_path:?}: {error}\n\nSuggestions:\n- Check if the file exists and is readable\n- Ensure the file path is correct\n- Verify file permissions")]
48 MissingFileArg { file_path: PathBuf, error: String },
49 #[error("cannot print result {result:?}: {error}")]
50 CannotPrintResult {
51 result: ScVal,
52 error: soroban_spec_tools::Error,
53 },
54 #[error("function '{function_name}' was not found in the contract\n\nAvailable functions: {available_functions}\n\nSuggestions:\n- Check the function name spelling\n- Use 'stellar contract invoke --help' to see available functions\n- Verify the contract ID is correct")]
55 FunctionNotFoundInContractSpec {
56 function_name: String,
57 available_functions: String,
58 },
59 #[error("function name '{function_name}' is too long (max 32 characters)\n\nReceived: {function_name} ({length} characters)")]
60 FunctionNameTooLong {
61 function_name: String,
62 length: usize,
63 },
64 #[error("argument count ({current}) surpasses maximum allowed count ({maximum})\n\nSuggestions:\n- Reduce the number of arguments\n- Consider using file-based arguments for complex data\n- Check if some arguments can be combined")]
65 MaxNumberOfArgumentsReached { current: usize, maximum: usize },
66 #[error("Unsupported address type '{address}'\n\nSupported formats:\n- Account addresses: G... (starts with G)\n- Contract addresses: C... (starts with C)\n- Muxed accounts: M... (starts with M)\n- Identity names: alice, bob, etc.\n\nReceived: '{address}'")]
67 UnsupportedScAddress { address: String },
68 #[error(transparent)]
69 Xdr(#[from] xdr::Error),
70 #[error(transparent)]
71 StrVal(#[from] soroban_spec_tools::Error),
72 #[error(transparent)]
73 ScAddress(#[from] sc_address::Error),
74 #[error(transparent)]
75 Config(#[from] config::Error),
76 #[error("")]
77 HelpMessage(String),
78 #[error(transparent)]
79 Signer(#[from] signer::Error),
80}
81
82pub type HostFunctionParameters = (String, Spec, InvokeContractArgs, Vec<Signer>);
83
84fn running_cmd() -> String {
85 let mut args: Vec<String> = env::args().collect();
86
87 if let Some(pos) = args.iter().position(|arg| arg == "--") {
88 args.truncate(pos);
89 }
90
91 format!("{} --", args.join(" "))
92}
93
94pub async fn build_host_function_parameters(
95 contract_id: &stellar_strkey::Contract,
96 slop: &[OsString],
97 spec_entries: &[ScSpecEntry],
98 config: &config::Args,
99) -> Result<HostFunctionParameters, Error> {
100 build_host_function_parameters_with_filter(contract_id, slop, spec_entries, config, true).await
101}
102
103pub async fn build_constructor_parameters(
104 contract_id: &stellar_strkey::Contract,
105 slop: &[OsString],
106 spec_entries: &[ScSpecEntry],
107 config: &config::Args,
108) -> Result<HostFunctionParameters, Error> {
109 build_host_function_parameters_with_filter(contract_id, slop, spec_entries, config, false).await
110}
111
112async fn build_host_function_parameters_with_filter(
113 contract_id: &stellar_strkey::Contract,
114 slop: &[OsString],
115 spec_entries: &[ScSpecEntry],
116 config: &config::Args,
117 filter_constructor: bool,
118) -> Result<HostFunctionParameters, Error> {
119 let spec = Spec(Some(spec_entries.to_vec()));
120 let cmd = build_clap_command(&spec, filter_constructor)?;
121 let (function, matches_) = parse_command_matches(cmd, slop)?;
122 let func = get_function_spec(&spec, &function)?;
123 let (parsed_args, signers) = parse_function_arguments(&func, &matches_, &spec, config).await?;
124 let invoke_args = build_invoke_contract_args(contract_id, &function, parsed_args)?;
125
126 Ok((function, spec, invoke_args, signers))
127}
128
129fn build_clap_command(spec: &Spec, filter_constructor: bool) -> Result<clap::Command, Error> {
130 let mut cmd = clap::Command::new(running_cmd())
131 .no_binary_name(true)
132 .term_width(300)
133 .max_term_width(300);
134
135 for ScSpecFunctionV0 { name, .. } in spec.find_functions()? {
136 let function_name = name.to_utf8_string_lossy();
137 if !filter_constructor || function_name != CONSTRUCTOR_FUNCTION_NAME {
139 cmd = cmd.subcommand(build_custom_cmd(&function_name, spec)?);
140 }
141 }
142 cmd.build();
143 Ok(cmd)
144}
145
146fn parse_command_matches(
147 mut cmd: clap::Command,
148 slop: &[OsString],
149) -> Result<(String, clap::ArgMatches), Error> {
150 let long_help = cmd.render_long_help();
151 let maybe_matches = cmd.try_get_matches_from(slop);
152
153 let Some((function, matches_)) = (match maybe_matches {
154 Ok(mut matches) => matches.remove_subcommand(),
155 Err(e) => {
156 if e.kind() == DisplayHelp {
157 return Err(HelpMessage(e.to_string()));
158 }
159 e.exit();
160 }
161 }) else {
162 return Err(HelpMessage(format!("{long_help}")));
163 };
164
165 Ok((function.clone(), matches_))
166}
167
168fn get_function_spec(spec: &Spec, function: &str) -> Result<ScSpecFunctionV0, Error> {
169 if let Ok(f) = spec.find_function(function) {
171 return Ok(f.clone());
172 }
173 if let Ok(functions) = spec.find_functions() {
176 for f in functions {
177 if sanitize(&f.name.to_utf8_string_lossy()) == function {
178 return Ok(f.clone());
179 }
180 }
181 }
182 Err(Error::FunctionNotFoundInContractSpec {
183 function_name: function.to_string(),
184 available_functions: get_available_functions(spec),
185 })
186}
187
188async fn parse_function_arguments(
189 func: &ScSpecFunctionV0,
190 matches_: &clap::ArgMatches,
191 spec: &Spec,
192 config: &config::Args,
193) -> Result<(Vec<ScVal>, Vec<Signer>), Error> {
194 let mut parsed_args = Vec::with_capacity(func.inputs.len());
195 let mut signers = Vec::<Signer>::new();
196
197 for i in func.inputs.iter() {
198 parse_single_argument(i, matches_, spec, config, &mut signers, &mut parsed_args).await?;
199 }
200
201 Ok((parsed_args, signers))
202}
203
204async fn parse_single_argument(
205 input: &stellar_xdr::curr::ScSpecFunctionInputV0,
206 matches_: &clap::ArgMatches,
207 spec: &Spec,
208 config: &config::Args,
209 signers: &mut Vec<Signer>,
210 parsed_args: &mut Vec<ScVal>,
211) -> Result<(), Error> {
212 let name = sanitize(&input.name.to_utf8_string_lossy());
213 let expected_type_name = get_type_name(&input.type_); if let Some(mut val) = matches_.get_raw(&name) {
216 let s = match val.next() {
217 Some(v) => v.to_string_lossy().to_string(),
218 None => {
219 return Err(Error::MissingArgument {
220 arg: name.clone(),
221 expected_type: expected_type_name,
222 });
223 }
224 };
225
226 if matches!(
228 input.type_,
229 ScSpecTypeDef::Address | ScSpecTypeDef::MuxedAddress
230 ) {
231 let trimmed_s = s.trim_matches('"');
232 let addr = resolve_address(trimmed_s, config)?;
233 if let Some(signer) = resolve_signer(trimmed_s, config).await {
234 signers.push(signer);
235 }
236 parsed_args.push(parse_argument_with_validation(
237 &name,
238 &addr,
239 &input.type_,
240 spec,
241 config,
242 )?);
243 return Ok(());
244 }
245
246 parsed_args.push(parse_argument_with_validation(
247 &name,
248 &s,
249 &input.type_,
250 spec,
251 config,
252 )?);
253 Ok(())
254 } else if matches!(input.type_, ScSpecTypeDef::Option(_)) {
255 parsed_args.push(ScVal::Void);
256 Ok(())
257 } else if let Some(arg_path) = matches_.get_one::<PathBuf>(&fmt_arg_file_name(&name)) {
258 parsed_args.push(parse_file_argument(
259 &name,
260 arg_path,
261 &input.type_,
262 expected_type_name,
263 spec,
264 config,
265 )?);
266 Ok(())
267 } else {
268 Err(Error::MissingArgument {
269 arg: name,
270 expected_type: expected_type_name,
271 })
272 }
273}
274
275fn parse_file_argument(
276 name: &str,
277 arg_path: &PathBuf,
278 type_def: &ScSpecTypeDef,
279 expected_type_name: String,
280 spec: &Spec,
281 config: &config::Args,
282) -> Result<ScVal, Error> {
283 if matches!(type_def, ScSpecTypeDef::Bytes | ScSpecTypeDef::BytesN(_)) {
284 let bytes = std::fs::read(arg_path).map_err(|e| Error::MissingFileArg {
285 file_path: arg_path.clone(),
286 error: e.to_string(),
287 })?;
288 ScVal::try_from(&bytes).map_err(|()| Error::CannotParseArg {
289 arg: name.to_string(),
290 error: soroban_spec_tools::Error::Unknown,
291 expected_type: expected_type_name,
292 received_value: format!("{} bytes from file", bytes.len()),
293 suggestion: "Ensure the file contains valid binary data for the expected byte type"
294 .to_string(),
295 })
296 } else {
297 let file_contents =
298 std::fs::read_to_string(arg_path).map_err(|e| Error::MissingFileArg {
299 file_path: arg_path.clone(),
300 error: e.to_string(),
301 })?;
302 tracing::debug!(
303 "file {arg_path:?}, has contents:\n{file_contents}\nAnd type {:#?}\n{}",
304 type_def,
305 file_contents.len()
306 );
307 parse_argument_with_validation(name, &file_contents, type_def, spec, config)
308 }
309}
310
311fn build_invoke_contract_args(
312 contract_id: &stellar_strkey::Contract,
313 function: &str,
314 parsed_args: Vec<ScVal>,
315) -> Result<InvokeContractArgs, Error> {
316 let contract_address_arg = xdr::ScAddress::Contract(ContractId(Hash(contract_id.0)));
317 let function_symbol_arg = function
318 .try_into()
319 .map_err(|()| Error::FunctionNameTooLong {
320 function_name: function.to_string(),
321 length: function.len(),
322 })?;
323
324 let final_args =
325 parsed_args
326 .clone()
327 .try_into()
328 .map_err(|_| Error::MaxNumberOfArgumentsReached {
329 current: parsed_args.len(),
330 maximum: ScVec::default().max_len(),
331 })?;
332
333 Ok(InvokeContractArgs {
334 contract_address: contract_address_arg,
335 function_name: function_symbol_arg,
336 args: final_args,
337 })
338}
339
340pub fn build_custom_cmd(name: &str, spec: &Spec) -> Result<clap::Command, Error> {
341 let func = spec
342 .find_function(name)
343 .map_err(|_| Error::FunctionNotFoundInContractSpec {
344 function_name: name.to_string(),
345 available_functions: get_available_functions(spec),
346 })?;
347
348 let inputs_map = &func
350 .inputs
351 .iter()
352 .map(|i| (sanitize(&i.name.to_utf8_string_lossy()), i.type_.clone()))
353 .collect::<HashMap<String, ScSpecTypeDef>>();
354 let name: &'static str = Box::leak(sanitize(name).into_boxed_str());
355 let mut cmd = clap::Command::new(name)
356 .no_binary_name(true)
357 .term_width(300)
358 .max_term_width(300);
359 let kebab_name = name.to_kebab_case();
360 if kebab_name != name {
361 cmd = cmd.alias(kebab_name);
362 }
363 let doc: &'static str = Box::leak(sanitize(&func.doc.to_utf8_string_lossy()).into_boxed_str());
364 let long_doc: &'static str = Box::leak(arg_file_help(doc).into_boxed_str());
365
366 cmd = cmd.about(Some(doc)).long_about(long_doc);
367 for (name, type_) in inputs_map {
368 let mut arg = clap::Arg::new(name);
369 let file_arg_name = fmt_arg_file_name(name);
370 let mut file_arg = clap::Arg::new(&file_arg_name);
371 arg = arg
372 .long(name)
373 .alias(name.to_kebab_case())
374 .num_args(1)
375 .value_parser(clap::builder::NonEmptyStringValueParser::new())
376 .long_help(
377 spec.doc(name, type_)?
378 .map(|d| -> &'static str { Box::leak(sanitize(d).into_boxed_str()) }),
379 );
380
381 file_arg = file_arg
382 .long(&file_arg_name)
383 .alias(file_arg_name.to_kebab_case())
384 .num_args(1)
385 .hide(true)
386 .value_parser(value_parser!(PathBuf))
387 .conflicts_with(name);
388
389 if let Some(value_name) = spec.arg_value_name(type_, 0) {
390 let value_name: &'static str = Box::leak(value_name.into_boxed_str());
391 arg = arg.value_name(value_name);
392 }
393
394 arg = match type_ {
396 ScSpecTypeDef::Bool => arg
397 .num_args(0..1)
398 .default_missing_value("true")
399 .default_value("false")
400 .num_args(0..=1),
401 ScSpecTypeDef::Option(_val) => arg.required(false),
402 ScSpecTypeDef::I256 | ScSpecTypeDef::I128 | ScSpecTypeDef::I64 | ScSpecTypeDef::I32 => {
403 arg.allow_hyphen_values(true)
404 }
405 _ => arg,
406 };
407
408 cmd = cmd.arg(arg);
409 cmd = cmd.arg(file_arg);
410 }
411 Ok(cmd)
412}
413
414fn fmt_arg_file_name(name: &str) -> String {
415 format!("{name}-file-path")
416}
417
418fn arg_file_help(docs: &str) -> String {
419 format!(
420 r"{docs}
421Usage Notes:
422Each arg has a corresponding --<arg_name>-file-path which is a path to a file containing the corresponding JSON argument.
423Note: The only types which aren't JSON are Bytes and BytesN, which are raw bytes"
424 )
425}
426
427pub fn output_to_string(
428 spec: &Spec,
429 res: &ScVal,
430 function: &str,
431) -> Result<TxnResult<String>, Error> {
432 let mut res_str = String::new();
433 if let Some(output) = spec.find_function(function)?.outputs.first() {
434 res_str = spec
435 .xdr_to_json(res, output)
436 .map_err(|e| Error::CannotPrintResult {
437 result: res.clone(),
438 error: e,
439 })?
440 .to_string();
441 }
442 Ok(TxnResult::Res(res_str))
443}
444
445fn resolve_address(addr_or_alias: &str, config: &config::Args) -> Result<String, Error> {
446 let sc_address: UnresolvedScAddress = addr_or_alias.parse().unwrap();
447 let account = match sc_address {
448 UnresolvedScAddress::Resolved(addr) => addr.to_string(),
449 addr @ UnresolvedScAddress::Alias(_) => {
450 let addr = addr.resolve(
451 &config.locator,
452 &config.get_network()?.network_passphrase,
453 config.hd_path(),
454 )?;
455 match addr {
456 xdr::ScAddress::Account(account) => account.to_string(),
457 contract @ xdr::ScAddress::Contract(_) => contract.to_string(),
458 stellar_xdr::curr::ScAddress::MuxedAccount(account) => account.to_string(),
459 stellar_xdr::curr::ScAddress::ClaimableBalance(_)
460 | stellar_xdr::curr::ScAddress::LiquidityPool(_) => {
461 return Err(Error::UnsupportedScAddress {
462 address: addr.to_string(),
463 })
464 }
465 }
466 }
467 };
468 Ok(account)
469}
470
471async fn resolve_signer(addr_or_alias: &str, config: &config::Args) -> Option<Signer> {
472 let secret = config.locator.get_secret_key(addr_or_alias).ok()?;
473 let print = Print::new(false);
474 let signer = secret.signer(config.hd_path(), print).await.ok()?;
475 Some(signer)
476}
477
478fn validate_json_arg(arg_name: &str, value: &str) -> Result<(), Error> {
480 if let Err(json_err) = serde_json::from_str::<serde_json::Value>(value) {
482 return Err(Error::InvalidJsonArg {
483 arg: arg_name.to_string(),
484 json_error: json_err.to_string(),
485 received_value: value.to_string(),
486 });
487 }
488 Ok(())
489}
490
491fn get_type_name(type_def: &ScSpecTypeDef) -> String {
493 match type_def {
494 ScSpecTypeDef::Val => "any value".to_string(),
495 ScSpecTypeDef::U64 => "u64 (unsigned 64-bit integer)".to_string(),
496 ScSpecTypeDef::I64 => "i64 (signed 64-bit integer)".to_string(),
497 ScSpecTypeDef::U128 => "u128 (unsigned 128-bit integer)".to_string(),
498 ScSpecTypeDef::I128 => "i128 (signed 128-bit integer)".to_string(),
499 ScSpecTypeDef::U32 => "u32 (unsigned 32-bit integer)".to_string(),
500 ScSpecTypeDef::I32 => "i32 (signed 32-bit integer)".to_string(),
501 ScSpecTypeDef::U256 => "u256 (unsigned 256-bit integer)".to_string(),
502 ScSpecTypeDef::I256 => "i256 (signed 256-bit integer)".to_string(),
503 ScSpecTypeDef::Bool => "bool (true/false)".to_string(),
504 ScSpecTypeDef::Symbol => "symbol (identifier)".to_string(),
505 ScSpecTypeDef::String => "string".to_string(),
506 ScSpecTypeDef::Bytes => "bytes (raw binary data)".to_string(),
507 ScSpecTypeDef::BytesN(n) => format!("bytes{} (exactly {} bytes)", n.n, n.n),
508 ScSpecTypeDef::Address => {
509 "address (G... for account, C... for contract, or identity name)".to_string()
510 }
511 ScSpecTypeDef::MuxedAddress => "muxed address (M... or identity name)".to_string(),
512 ScSpecTypeDef::Void => "void (no value)".to_string(),
513 ScSpecTypeDef::Error => "error".to_string(),
514 ScSpecTypeDef::Timepoint => "timepoint (timestamp)".to_string(),
515 ScSpecTypeDef::Duration => "duration (time span)".to_string(),
516 ScSpecTypeDef::Option(inner) => format!("optional {}", get_type_name(&inner.value_type)),
517 ScSpecTypeDef::Vec(inner) => format!("vector of {}", get_type_name(&inner.element_type)),
518 ScSpecTypeDef::Map(map_type) => format!(
519 "map from {} to {}",
520 get_type_name(&map_type.key_type),
521 get_type_name(&map_type.value_type)
522 ),
523 ScSpecTypeDef::Tuple(tuple_type) => {
524 let types: Vec<String> = tuple_type.value_types.iter().map(get_type_name).collect();
525 format!("tuple({})", types.join(", "))
526 }
527 ScSpecTypeDef::Result(_) => "result".to_string(),
528 ScSpecTypeDef::Udt(udt) => {
529 format!(
530 "user-defined type '{}'",
531 sanitize(&udt.name.to_utf8_string_lossy())
532 )
533 }
534 }
535}
536
537fn get_available_functions(spec: &Spec) -> String {
539 match spec.find_functions() {
540 Ok(functions) => functions
541 .map(|f| sanitize(&f.name.to_utf8_string_lossy()))
542 .collect::<Vec<_>>()
543 .join(", "),
544 Err(_) => "unknown".to_string(),
545 }
546}
547
548fn is_primitive_type(type_def: &ScSpecTypeDef) -> bool {
550 matches!(
551 type_def,
552 ScSpecTypeDef::U32
553 | ScSpecTypeDef::U64
554 | ScSpecTypeDef::U128
555 | ScSpecTypeDef::U256
556 | ScSpecTypeDef::I32
557 | ScSpecTypeDef::I64
558 | ScSpecTypeDef::I128
559 | ScSpecTypeDef::I256
560 | ScSpecTypeDef::Bool
561 | ScSpecTypeDef::Symbol
562 | ScSpecTypeDef::String
563 | ScSpecTypeDef::Bytes
564 | ScSpecTypeDef::BytesN(_)
565 | ScSpecTypeDef::Address
566 | ScSpecTypeDef::MuxedAddress
567 | ScSpecTypeDef::Timepoint
568 | ScSpecTypeDef::Duration
569 | ScSpecTypeDef::Void
570 )
571}
572
573fn get_context_suggestions(expected_type: &ScSpecTypeDef, received_value: &str) -> String {
575 match expected_type {
576 ScSpecTypeDef::U64 | ScSpecTypeDef::I64 | ScSpecTypeDef::U128 | ScSpecTypeDef::I128
577 | ScSpecTypeDef::U32 | ScSpecTypeDef::I32 | ScSpecTypeDef::U256 | ScSpecTypeDef::I256 => {
578 if received_value.starts_with('"') && received_value.ends_with('"') {
579 "For numbers, ensure no quotes around the value (e.g., use 100 instead of \"100\")".to_string()
580 } else if received_value.contains('.') {
581 "Integer types don't support decimal values - use a whole number".to_string()
582 } else {
583 "Ensure the value is a valid integer within the type's range".to_string()
584 }
585 }
586 ScSpecTypeDef::Bool => {
587 "For booleans, use 'true' or 'false' (without quotes)".to_string()
588 }
589 ScSpecTypeDef::String => {
590 if !received_value.starts_with('"') || !received_value.ends_with('"') {
591 "For strings, ensure the value is properly quoted (e.g., \"hello world\")".to_string()
592 } else {
593 "Check for proper string escaping if the string contains special characters".to_string()
594 }
595 }
596 ScSpecTypeDef::Address => {
597 "For addresses, use format: G... (account), C... (contract), or identity name (e.g., alice)".to_string()
598 }
599 ScSpecTypeDef::MuxedAddress => {
600 "For muxed addresses, use format: M... or identity name".to_string()
601 }
602 ScSpecTypeDef::Vec(_) => {
603 "For arrays, use JSON array format: [\"item1\", \"item2\"] or [{\"key\": \"value\"}]".to_string()
604 }
605 ScSpecTypeDef::Map(_) => {
606 "For maps, use JSON object format: {\"key1\": \"value1\", \"key2\": \"value2\"}".to_string()
607 }
608 ScSpecTypeDef::Option(_) => {
609 "For optional values, use null for none or the expected value type".to_string()
610 }
611 _ => {
612 "Check the contract specification for the correct argument format and type".to_string()
613 }
614 }
615}
616
617fn parse_argument_with_validation(
619 arg_name: &str,
620 value: &str,
621 expected_type: &ScSpecTypeDef,
622 spec: &Spec,
623 config: &config::Args,
624) -> Result<ScVal, Error> {
625 let expected_type_name = get_type_name(expected_type);
626
627 let is_union_udt = if let ScSpecTypeDef::Udt(udt) = expected_type {
631 spec.find(&udt.name.to_utf8_string_lossy())
632 .map(|entry| matches!(entry, ScSpecEntry::UdtUnionV0(_)))
633 .unwrap_or(false)
634 } else {
635 false
636 };
637 if !is_primitive_type(expected_type) && !is_union_udt {
638 validate_json_arg(arg_name, value)?;
639 }
640
641 if matches!(
643 expected_type,
644 ScSpecTypeDef::Address | ScSpecTypeDef::MuxedAddress
645 ) {
646 let trimmed_value = value.trim_matches('"');
647 let addr = resolve_address(trimmed_value, config)?;
648 return spec
649 .from_string(&addr, expected_type)
650 .map_err(|error| Error::CannotParseArg {
651 arg: arg_name.to_string(),
652 error,
653 expected_type: expected_type_name.clone(),
654 received_value: value.to_string(),
655 suggestion: get_context_suggestions(expected_type, value),
656 });
657 }
658
659 spec.from_string(value, expected_type)
661 .map_err(|error| Error::CannotParseArg {
662 arg: arg_name.to_string(),
663 error,
664 expected_type: expected_type_name,
665 received_value: value.to_string(),
666 suggestion: get_context_suggestions(expected_type, value),
667 })
668}
669
670#[cfg(test)]
671mod tests {
672 use super::*;
673 use stellar_xdr::curr::{ScSpecTypeBytesN, ScSpecTypeDef, ScSpecTypeOption, ScSpecTypeVec};
674
675 #[test]
676 fn test_get_type_name_primitives() {
677 assert_eq!(
678 get_type_name(&ScSpecTypeDef::U32),
679 "u32 (unsigned 32-bit integer)"
680 );
681 assert_eq!(
682 get_type_name(&ScSpecTypeDef::I64),
683 "i64 (signed 64-bit integer)"
684 );
685 assert_eq!(get_type_name(&ScSpecTypeDef::Bool), "bool (true/false)");
686 assert_eq!(get_type_name(&ScSpecTypeDef::String), "string");
687 assert_eq!(
688 get_type_name(&ScSpecTypeDef::Address),
689 "address (G... for account, C... for contract, or identity name)"
690 );
691 }
692
693 #[test]
694 fn test_get_type_name_complex() {
695 let option_type = ScSpecTypeDef::Option(Box::new(ScSpecTypeOption {
696 value_type: Box::new(ScSpecTypeDef::U32),
697 }));
698 assert_eq!(
699 get_type_name(&option_type),
700 "optional u32 (unsigned 32-bit integer)"
701 );
702
703 let vec_type = ScSpecTypeDef::Vec(Box::new(ScSpecTypeVec {
704 element_type: Box::new(ScSpecTypeDef::String),
705 }));
706 assert_eq!(get_type_name(&vec_type), "vector of string");
707 }
708
709 #[test]
710 fn test_is_primitive_type_all_primitives() {
711 assert!(is_primitive_type(&ScSpecTypeDef::U32));
712 assert!(is_primitive_type(&ScSpecTypeDef::I32));
713 assert!(is_primitive_type(&ScSpecTypeDef::U64));
714 assert!(is_primitive_type(&ScSpecTypeDef::I64));
715 assert!(is_primitive_type(&ScSpecTypeDef::U128));
716 assert!(is_primitive_type(&ScSpecTypeDef::I128));
717 assert!(is_primitive_type(&ScSpecTypeDef::U256));
718 assert!(is_primitive_type(&ScSpecTypeDef::I256));
719
720 assert!(is_primitive_type(&ScSpecTypeDef::Bool));
721 assert!(is_primitive_type(&ScSpecTypeDef::Symbol));
722 assert!(is_primitive_type(&ScSpecTypeDef::String));
723 assert!(is_primitive_type(&ScSpecTypeDef::Void));
724 assert!(is_primitive_type(&ScSpecTypeDef::Bytes));
725 assert!(is_primitive_type(&ScSpecTypeDef::BytesN(
726 ScSpecTypeBytesN { n: 32 }
727 )));
728 assert!(is_primitive_type(&ScSpecTypeDef::BytesN(
729 ScSpecTypeBytesN { n: 64 }
730 )));
731
732 assert!(is_primitive_type(&ScSpecTypeDef::Address));
733 assert!(is_primitive_type(&ScSpecTypeDef::MuxedAddress));
734 assert!(is_primitive_type(&ScSpecTypeDef::Timepoint));
735 assert!(is_primitive_type(&ScSpecTypeDef::Duration));
736
737 assert!(!is_primitive_type(&ScSpecTypeDef::Vec(Box::new(
738 ScSpecTypeVec {
739 element_type: Box::new(ScSpecTypeDef::U32),
740 }
741 ))));
742 }
743
744 #[test]
745 fn test_validate_json_arg_valid() {
746 assert!(validate_json_arg("test_arg", r#"{"key": "value"}"#).is_ok());
748 assert!(validate_json_arg("test_arg", "123").is_ok());
749 assert!(validate_json_arg("test_arg", r#""string""#).is_ok());
750 assert!(validate_json_arg("test_arg", "true").is_ok());
751 assert!(validate_json_arg("test_arg", "null").is_ok());
752 }
753
754 #[test]
755 fn test_validate_json_arg_invalid() {
756 let result = validate_json_arg("test_arg", r#"{"key": value}"#); assert!(result.is_err());
759
760 if let Err(Error::InvalidJsonArg {
761 arg,
762 json_error,
763 received_value,
764 }) = result
765 {
766 assert_eq!(arg, "test_arg");
767 assert_eq!(received_value, r#"{"key": value}"#);
768 assert!(json_error.contains("expected"));
769 } else {
770 panic!("Expected InvalidJsonArg error");
771 }
772 }
773
774 #[test]
775 fn test_validate_json_arg_malformed() {
776 let test_cases = vec![
778 r#"{"key": }"#, r#"{key: "value"}"#, r#"{"key": "value",}"#, r#"{"key" "value"}"#, ];
783
784 for case in test_cases {
785 let result = validate_json_arg("test_arg", case);
786 assert!(result.is_err(), "Expected error for case: {case}");
787 }
788 }
789
790 #[test]
791 fn test_context_aware_error_messages() {
792 use stellar_xdr::curr::ScSpecTypeDef;
793
794 let suggestion = get_context_suggestions(&ScSpecTypeDef::U64, "\"100\"");
798 assert!(suggestion.contains("no quotes around the value"));
799 assert!(suggestion.contains("use 100 instead of \"100\""));
800
801 let suggestion = get_context_suggestions(&ScSpecTypeDef::U64, "100.5");
803 assert!(suggestion.contains("don't support decimal values"));
804
805 let suggestion = get_context_suggestions(&ScSpecTypeDef::String, "hello");
807 assert!(suggestion.contains("properly quoted"));
808
809 let suggestion = get_context_suggestions(&ScSpecTypeDef::Address, "invalid_addr");
811 assert!(suggestion.contains("G... (account), C... (contract)"));
812
813 let suggestion = get_context_suggestions(&ScSpecTypeDef::Bool, "yes");
815 assert!(suggestion.contains("'true' or 'false'"));
816
817 println!("=== Context-Aware Error Message Examples ===");
818 println!("U64 with quotes: {suggestion}");
819
820 let decimal_suggestion = get_context_suggestions(&ScSpecTypeDef::U64, "100.5");
821 println!("U64 with decimal: {decimal_suggestion}");
822
823 let string_suggestion = get_context_suggestions(&ScSpecTypeDef::String, "hello");
824 println!("String without quotes: {string_suggestion}");
825
826 let address_suggestion = get_context_suggestions(&ScSpecTypeDef::Address, "invalid");
827 println!("Invalid address: {address_suggestion}");
828 }
829
830 #[test]
831 fn test_union_udt_bare_string_accepted() {
832 use stellar_xdr::curr::{
833 ScSpecEntry, ScSpecTypeDef, ScSpecTypeUdt, ScSpecUdtUnionCaseV0,
834 ScSpecUdtUnionCaseVoidV0, ScSpecUdtUnionV0, StringM,
835 };
836
837 let union_name: StringM<60> = "MyEnum".try_into().unwrap();
839 let case_name: StringM<60> = "Unit".try_into().unwrap();
840 let spec = Spec(Some(vec![ScSpecEntry::UdtUnionV0(ScSpecUdtUnionV0 {
841 doc: StringM::default(),
842 lib: StringM::default(),
843 name: union_name.clone(),
844 cases: vec![ScSpecUdtUnionCaseV0::VoidV0(ScSpecUdtUnionCaseVoidV0 {
845 doc: StringM::default(),
846 name: case_name,
847 })]
848 .try_into()
849 .unwrap(),
850 })]));
851
852 let expected_type = ScSpecTypeDef::Udt(ScSpecTypeUdt { name: union_name });
853 let config = crate::config::Args::default();
854
855 let result =
857 parse_argument_with_validation("value", "Unit", &expected_type, &spec, &config);
858 assert!(result.is_ok(), "bare 'Unit' should be accepted: {result:?}");
859
860 let result =
862 parse_argument_with_validation("value", "\"Unit\"", &expected_type, &spec, &config);
863 assert!(
864 result.is_ok(),
865 "JSON-quoted '\"Unit\"' should be accepted: {result:?}"
866 );
867
868 let bare = parse_argument_with_validation("value", "Unit", &expected_type, &spec, &config)
870 .unwrap();
871 let quoted =
872 parse_argument_with_validation("value", "\"Unit\"", &expected_type, &spec, &config)
873 .unwrap();
874 assert_eq!(
875 bare, quoted,
876 "bare and quoted forms should produce identical ScVal"
877 );
878 }
879
880 #[test]
881 fn test_union_udt_tuple_variant_still_requires_json() {
882 use stellar_xdr::curr::{
883 ScSpecEntry, ScSpecTypeDef, ScSpecTypeUdt, ScSpecUdtUnionCaseTupleV0,
884 ScSpecUdtUnionCaseV0, ScSpecUdtUnionCaseVoidV0, ScSpecUdtUnionV0, StringM,
885 };
886
887 let union_name: StringM<60> = "MyEnum".try_into().unwrap();
888 let spec = Spec(Some(vec![ScSpecEntry::UdtUnionV0(ScSpecUdtUnionV0 {
889 doc: StringM::default(),
890 lib: StringM::default(),
891 name: union_name.clone(),
892 cases: vec![
893 ScSpecUdtUnionCaseV0::VoidV0(ScSpecUdtUnionCaseVoidV0 {
894 doc: StringM::default(),
895 name: "Unit".try_into().unwrap(),
896 }),
897 ScSpecUdtUnionCaseV0::TupleV0(ScSpecUdtUnionCaseTupleV0 {
898 doc: StringM::default(),
899 name: "WithValue".try_into().unwrap(),
900 type_: vec![ScSpecTypeDef::U32].try_into().unwrap(),
901 }),
902 ]
903 .try_into()
904 .unwrap(),
905 })]));
906
907 let expected_type = ScSpecTypeDef::Udt(ScSpecTypeUdt { name: union_name });
908 let config = crate::config::Args::default();
909
910 let result = parse_argument_with_validation(
912 "value",
913 r#"{"WithValue":42}"#,
914 &expected_type,
915 &spec,
916 &config,
917 );
918 assert!(
919 result.is_ok(),
920 "JSON object for tuple variant should be accepted: {result:?}"
921 );
922 }
923
924 #[test]
925 fn test_error_message_format() {
926 use stellar_xdr::curr::ScSpecTypeDef;
927
928 let error = Error::CannotParseArg {
930 arg: "amount".to_string(),
931 error: soroban_spec_tools::Error::InvalidValue(Some(ScSpecTypeDef::U64)),
932 expected_type: "u64 (unsigned 64-bit integer)".to_string(),
933 received_value: "\"100\"".to_string(),
934 suggestion:
935 "For numbers, ensure no quotes around the value (e.g., use 100 instead of \"100\")"
936 .to_string(),
937 };
938
939 let error_message = format!("{error}");
940 println!("\n=== Complete Error Message Example ===");
941 println!("{error_message}");
942
943 assert!(error_message.contains("Failed to parse argument 'amount'"));
945 assert!(error_message.contains("Expected type u64 (unsigned 64-bit integer)"));
946 assert!(error_message.contains("received: '\"100\"'"));
947 assert!(error_message.contains("Suggestion: For numbers, ensure no quotes"));
948 }
949
950 #[test]
952 fn invoke_help_strips_control_characters() {
953 let path = concat!(
954 env!("CARGO_MANIFEST_DIR"),
955 "/../crates/soroban-spec-tools/tests/fixtures/control_characters.wasm"
956 );
957 let bytes = std::fs::read(path).expect("fixture wasm should be readable");
958 let spec = Spec::from_wasm(&bytes).expect("wasm should parse without error");
959 let mut cmd = build_clap_command(&spec, true).expect("command should build without error");
960 let help = cmd.render_long_help().to_string();
961
962 let bad_chars: Vec<char> = help
963 .chars()
964 .filter(|c| c.is_control() && *c != '\n' && *c != '\t')
965 .collect();
966 assert!(
967 bad_chars.is_empty(),
968 "invoke help contains unexpected control characters {bad_chars:?}:\n{help:?}"
969 );
970 }
971}