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::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 spec.find_function(function)
170 .map_err(|_| Error::FunctionNotFoundInContractSpec {
171 function_name: function.to_string(),
172 available_functions: get_available_functions(spec),
173 })
174 .cloned()
175}
176
177async fn parse_function_arguments(
178 func: &ScSpecFunctionV0,
179 matches_: &clap::ArgMatches,
180 spec: &Spec,
181 config: &config::Args,
182) -> Result<(Vec<ScVal>, Vec<Signer>), Error> {
183 let mut parsed_args = Vec::with_capacity(func.inputs.len());
184 let mut signers = Vec::<Signer>::new();
185
186 for i in func.inputs.iter() {
187 parse_single_argument(i, matches_, spec, config, &mut signers, &mut parsed_args).await?;
188 }
189
190 Ok((parsed_args, signers))
191}
192
193async fn parse_single_argument(
194 input: &stellar_xdr::curr::ScSpecFunctionInputV0,
195 matches_: &clap::ArgMatches,
196 spec: &Spec,
197 config: &config::Args,
198 signers: &mut Vec<Signer>,
199 parsed_args: &mut Vec<ScVal>,
200) -> Result<(), Error> {
201 let name = input.name.to_utf8_string()?;
202 let expected_type_name = get_type_name(&input.type_); if let Some(mut val) = matches_.get_raw(&name) {
205 let s = match val.next() {
206 Some(v) => v.to_string_lossy().to_string(),
207 None => {
208 return Err(Error::MissingArgument {
209 arg: name.clone(),
210 expected_type: expected_type_name,
211 });
212 }
213 };
214
215 if matches!(
217 input.type_,
218 ScSpecTypeDef::Address | ScSpecTypeDef::MuxedAddress
219 ) {
220 let trimmed_s = s.trim_matches('"');
221 let addr = resolve_address(trimmed_s, config)?;
222 if let Some(signer) = resolve_signer(trimmed_s, config).await {
223 signers.push(signer);
224 }
225 parsed_args.push(parse_argument_with_validation(
226 &name,
227 &addr,
228 &input.type_,
229 spec,
230 config,
231 )?);
232 return Ok(());
233 }
234
235 parsed_args.push(parse_argument_with_validation(
236 &name,
237 &s,
238 &input.type_,
239 spec,
240 config,
241 )?);
242 Ok(())
243 } else if matches!(input.type_, ScSpecTypeDef::Option(_)) {
244 parsed_args.push(ScVal::Void);
245 Ok(())
246 } else if let Some(arg_path) = matches_.get_one::<PathBuf>(&fmt_arg_file_name(&name)) {
247 parsed_args.push(parse_file_argument(
248 &name,
249 arg_path,
250 &input.type_,
251 expected_type_name,
252 spec,
253 config,
254 )?);
255 Ok(())
256 } else {
257 Err(Error::MissingArgument {
258 arg: name,
259 expected_type: expected_type_name,
260 })
261 }
262}
263
264fn parse_file_argument(
265 name: &str,
266 arg_path: &PathBuf,
267 type_def: &ScSpecTypeDef,
268 expected_type_name: String,
269 spec: &Spec,
270 config: &config::Args,
271) -> Result<ScVal, Error> {
272 if matches!(type_def, ScSpecTypeDef::Bytes | ScSpecTypeDef::BytesN(_)) {
273 let bytes = std::fs::read(arg_path).map_err(|e| Error::MissingFileArg {
274 file_path: arg_path.clone(),
275 error: e.to_string(),
276 })?;
277 ScVal::try_from(&bytes).map_err(|()| Error::CannotParseArg {
278 arg: name.to_string(),
279 error: soroban_spec_tools::Error::Unknown,
280 expected_type: expected_type_name,
281 received_value: format!("{} bytes from file", bytes.len()),
282 suggestion: "Ensure the file contains valid binary data for the expected byte type"
283 .to_string(),
284 })
285 } else {
286 let file_contents =
287 std::fs::read_to_string(arg_path).map_err(|e| Error::MissingFileArg {
288 file_path: arg_path.clone(),
289 error: e.to_string(),
290 })?;
291 tracing::debug!(
292 "file {arg_path:?}, has contents:\n{file_contents}\nAnd type {:#?}\n{}",
293 type_def,
294 file_contents.len()
295 );
296 parse_argument_with_validation(name, &file_contents, type_def, spec, config)
297 }
298}
299
300fn build_invoke_contract_args(
301 contract_id: &stellar_strkey::Contract,
302 function: &str,
303 parsed_args: Vec<ScVal>,
304) -> Result<InvokeContractArgs, Error> {
305 let contract_address_arg = xdr::ScAddress::Contract(ContractId(Hash(contract_id.0)));
306 let function_symbol_arg = function
307 .try_into()
308 .map_err(|()| Error::FunctionNameTooLong {
309 function_name: function.to_string(),
310 length: function.len(),
311 })?;
312
313 let final_args =
314 parsed_args
315 .clone()
316 .try_into()
317 .map_err(|_| Error::MaxNumberOfArgumentsReached {
318 current: parsed_args.len(),
319 maximum: ScVec::default().max_len(),
320 })?;
321
322 Ok(InvokeContractArgs {
323 contract_address: contract_address_arg,
324 function_name: function_symbol_arg,
325 args: final_args,
326 })
327}
328
329pub fn build_custom_cmd(name: &str, spec: &Spec) -> Result<clap::Command, Error> {
330 let func = spec
331 .find_function(name)
332 .map_err(|_| Error::FunctionNotFoundInContractSpec {
333 function_name: name.to_string(),
334 available_functions: get_available_functions(spec),
335 })?;
336
337 let inputs_map = &func
339 .inputs
340 .iter()
341 .map(|i| (i.name.to_utf8_string().unwrap(), i.type_.clone()))
342 .collect::<HashMap<String, ScSpecTypeDef>>();
343 let name: &'static str = Box::leak(name.to_string().into_boxed_str());
344 let mut cmd = clap::Command::new(name)
345 .no_binary_name(true)
346 .term_width(300)
347 .max_term_width(300);
348 let kebab_name = name.to_kebab_case();
349 if kebab_name != name {
350 cmd = cmd.alias(kebab_name);
351 }
352 let doc: &'static str = Box::leak(func.doc.to_utf8_string_lossy().into_boxed_str());
353 let long_doc: &'static str = Box::leak(arg_file_help(doc).into_boxed_str());
354
355 cmd = cmd.about(Some(doc)).long_about(long_doc);
356 for (name, type_) in inputs_map {
357 let mut arg = clap::Arg::new(name);
358 let file_arg_name = fmt_arg_file_name(name);
359 let mut file_arg = clap::Arg::new(&file_arg_name);
360 arg = arg
361 .long(name)
362 .alias(name.to_kebab_case())
363 .num_args(1)
364 .value_parser(clap::builder::NonEmptyStringValueParser::new())
365 .long_help(spec.doc(name, type_)?);
366
367 file_arg = file_arg
368 .long(&file_arg_name)
369 .alias(file_arg_name.to_kebab_case())
370 .num_args(1)
371 .hide(true)
372 .value_parser(value_parser!(PathBuf))
373 .conflicts_with(name);
374
375 if let Some(value_name) = spec.arg_value_name(type_, 0) {
376 let value_name: &'static str = Box::leak(value_name.into_boxed_str());
377 arg = arg.value_name(value_name);
378 }
379
380 arg = match type_ {
382 ScSpecTypeDef::Bool => arg
383 .num_args(0..1)
384 .default_missing_value("true")
385 .default_value("false")
386 .num_args(0..=1),
387 ScSpecTypeDef::Option(_val) => arg.required(false),
388 ScSpecTypeDef::I256 | ScSpecTypeDef::I128 | ScSpecTypeDef::I64 | ScSpecTypeDef::I32 => {
389 arg.allow_hyphen_values(true)
390 }
391 _ => arg,
392 };
393
394 cmd = cmd.arg(arg);
395 cmd = cmd.arg(file_arg);
396 }
397 Ok(cmd)
398}
399
400fn fmt_arg_file_name(name: &str) -> String {
401 format!("{name}-file-path")
402}
403
404fn arg_file_help(docs: &str) -> String {
405 format!(
406 r"{docs}
407Usage Notes:
408Each arg has a corresponding --<arg_name>-file-path which is a path to a file containing the corresponding JSON argument.
409Note: The only types which aren't JSON are Bytes and BytesN, which are raw bytes"
410 )
411}
412
413pub fn output_to_string(
414 spec: &Spec,
415 res: &ScVal,
416 function: &str,
417) -> Result<TxnResult<String>, Error> {
418 let mut res_str = String::new();
419 if let Some(output) = spec.find_function(function)?.outputs.first() {
420 res_str = spec
421 .xdr_to_json(res, output)
422 .map_err(|e| Error::CannotPrintResult {
423 result: res.clone(),
424 error: e,
425 })?
426 .to_string();
427 }
428 Ok(TxnResult::Res(res_str))
429}
430
431fn resolve_address(addr_or_alias: &str, config: &config::Args) -> Result<String, Error> {
432 let sc_address: UnresolvedScAddress = addr_or_alias.parse().unwrap();
433 let account = match sc_address {
434 UnresolvedScAddress::Resolved(addr) => addr.to_string(),
435 addr @ UnresolvedScAddress::Alias(_) => {
436 let addr = addr.resolve(&config.locator, &config.get_network()?.network_passphrase)?;
437 match addr {
438 xdr::ScAddress::Account(account) => account.to_string(),
439 contract @ xdr::ScAddress::Contract(_) => contract.to_string(),
440 stellar_xdr::curr::ScAddress::MuxedAccount(account) => account.to_string(),
441 stellar_xdr::curr::ScAddress::ClaimableBalance(_)
442 | stellar_xdr::curr::ScAddress::LiquidityPool(_) => {
443 return Err(Error::UnsupportedScAddress {
444 address: addr.to_string(),
445 })
446 }
447 }
448 }
449 };
450 Ok(account)
451}
452
453async fn resolve_signer(addr_or_alias: &str, config: &config::Args) -> Option<Signer> {
454 let secret = config.locator.get_secret_key(addr_or_alias).ok()?;
455 let print = Print::new(false);
456 let signer = secret.signer(None, print).await.ok()?;
457 Some(signer)
458}
459
460fn validate_json_arg(arg_name: &str, value: &str) -> Result<(), Error> {
462 if let Err(json_err) = serde_json::from_str::<serde_json::Value>(value) {
464 return Err(Error::InvalidJsonArg {
465 arg: arg_name.to_string(),
466 json_error: json_err.to_string(),
467 received_value: value.to_string(),
468 });
469 }
470 Ok(())
471}
472
473fn get_type_name(type_def: &ScSpecTypeDef) -> String {
475 match type_def {
476 ScSpecTypeDef::Val => "any value".to_string(),
477 ScSpecTypeDef::U64 => "u64 (unsigned 64-bit integer)".to_string(),
478 ScSpecTypeDef::I64 => "i64 (signed 64-bit integer)".to_string(),
479 ScSpecTypeDef::U128 => "u128 (unsigned 128-bit integer)".to_string(),
480 ScSpecTypeDef::I128 => "i128 (signed 128-bit integer)".to_string(),
481 ScSpecTypeDef::U32 => "u32 (unsigned 32-bit integer)".to_string(),
482 ScSpecTypeDef::I32 => "i32 (signed 32-bit integer)".to_string(),
483 ScSpecTypeDef::U256 => "u256 (unsigned 256-bit integer)".to_string(),
484 ScSpecTypeDef::I256 => "i256 (signed 256-bit integer)".to_string(),
485 ScSpecTypeDef::Bool => "bool (true/false)".to_string(),
486 ScSpecTypeDef::Symbol => "symbol (identifier)".to_string(),
487 ScSpecTypeDef::String => "string".to_string(),
488 ScSpecTypeDef::Bytes => "bytes (raw binary data)".to_string(),
489 ScSpecTypeDef::BytesN(n) => format!("bytes{} (exactly {} bytes)", n.n, n.n),
490 ScSpecTypeDef::Address => {
491 "address (G... for account, C... for contract, or identity name)".to_string()
492 }
493 ScSpecTypeDef::MuxedAddress => "muxed address (M... or identity name)".to_string(),
494 ScSpecTypeDef::Void => "void (no value)".to_string(),
495 ScSpecTypeDef::Error => "error".to_string(),
496 ScSpecTypeDef::Timepoint => "timepoint (timestamp)".to_string(),
497 ScSpecTypeDef::Duration => "duration (time span)".to_string(),
498 ScSpecTypeDef::Option(inner) => format!("optional {}", get_type_name(&inner.value_type)),
499 ScSpecTypeDef::Vec(inner) => format!("vector of {}", get_type_name(&inner.element_type)),
500 ScSpecTypeDef::Map(map_type) => format!(
501 "map from {} to {}",
502 get_type_name(&map_type.key_type),
503 get_type_name(&map_type.value_type)
504 ),
505 ScSpecTypeDef::Tuple(tuple_type) => {
506 let types: Vec<String> = tuple_type.value_types.iter().map(get_type_name).collect();
507 format!("tuple({})", types.join(", "))
508 }
509 ScSpecTypeDef::Result(_) => "result".to_string(),
510 ScSpecTypeDef::Udt(udt) => {
511 format!("user-defined type '{}'", udt.name.to_utf8_string_lossy())
512 }
513 }
514}
515
516fn get_available_functions(spec: &Spec) -> String {
518 match spec.find_functions() {
519 Ok(functions) => functions
520 .map(|f| f.name.to_utf8_string_lossy())
521 .collect::<Vec<_>>()
522 .join(", "),
523 Err(_) => "unknown".to_string(),
524 }
525}
526
527fn is_primitive_type(type_def: &ScSpecTypeDef) -> bool {
529 matches!(
530 type_def,
531 ScSpecTypeDef::U32
532 | ScSpecTypeDef::U64
533 | ScSpecTypeDef::U128
534 | ScSpecTypeDef::U256
535 | ScSpecTypeDef::I32
536 | ScSpecTypeDef::I64
537 | ScSpecTypeDef::I128
538 | ScSpecTypeDef::I256
539 | ScSpecTypeDef::Bool
540 | ScSpecTypeDef::Symbol
541 | ScSpecTypeDef::String
542 | ScSpecTypeDef::Bytes
543 | ScSpecTypeDef::BytesN(_)
544 | ScSpecTypeDef::Address
545 | ScSpecTypeDef::MuxedAddress
546 | ScSpecTypeDef::Timepoint
547 | ScSpecTypeDef::Duration
548 | ScSpecTypeDef::Void
549 )
550}
551
552fn get_context_suggestions(expected_type: &ScSpecTypeDef, received_value: &str) -> String {
554 match expected_type {
555 ScSpecTypeDef::U64 | ScSpecTypeDef::I64 | ScSpecTypeDef::U128 | ScSpecTypeDef::I128
556 | ScSpecTypeDef::U32 | ScSpecTypeDef::I32 | ScSpecTypeDef::U256 | ScSpecTypeDef::I256 => {
557 if received_value.starts_with('"') && received_value.ends_with('"') {
558 "For numbers, ensure no quotes around the value (e.g., use 100 instead of \"100\")".to_string()
559 } else if received_value.contains('.') {
560 "Integer types don't support decimal values - use a whole number".to_string()
561 } else {
562 "Ensure the value is a valid integer within the type's range".to_string()
563 }
564 }
565 ScSpecTypeDef::Bool => {
566 "For booleans, use 'true' or 'false' (without quotes)".to_string()
567 }
568 ScSpecTypeDef::String => {
569 if !received_value.starts_with('"') || !received_value.ends_with('"') {
570 "For strings, ensure the value is properly quoted (e.g., \"hello world\")".to_string()
571 } else {
572 "Check for proper string escaping if the string contains special characters".to_string()
573 }
574 }
575 ScSpecTypeDef::Address => {
576 "For addresses, use format: G... (account), C... (contract), or identity name (e.g., alice)".to_string()
577 }
578 ScSpecTypeDef::MuxedAddress => {
579 "For muxed addresses, use format: M... or identity name".to_string()
580 }
581 ScSpecTypeDef::Vec(_) => {
582 "For arrays, use JSON array format: [\"item1\", \"item2\"] or [{\"key\": \"value\"}]".to_string()
583 }
584 ScSpecTypeDef::Map(_) => {
585 "For maps, use JSON object format: {\"key1\": \"value1\", \"key2\": \"value2\"}".to_string()
586 }
587 ScSpecTypeDef::Option(_) => {
588 "For optional values, use null for none or the expected value type".to_string()
589 }
590 _ => {
591 "Check the contract specification for the correct argument format and type".to_string()
592 }
593 }
594}
595
596fn parse_argument_with_validation(
598 arg_name: &str,
599 value: &str,
600 expected_type: &ScSpecTypeDef,
601 spec: &Spec,
602 config: &config::Args,
603) -> Result<ScVal, Error> {
604 let expected_type_name = get_type_name(expected_type);
605
606 if !is_primitive_type(expected_type) {
608 validate_json_arg(arg_name, value)?;
609 }
610
611 if matches!(
613 expected_type,
614 ScSpecTypeDef::Address | ScSpecTypeDef::MuxedAddress
615 ) {
616 let trimmed_value = value.trim_matches('"');
617 let addr = resolve_address(trimmed_value, config)?;
618 return spec
619 .from_string(&addr, expected_type)
620 .map_err(|error| Error::CannotParseArg {
621 arg: arg_name.to_string(),
622 error,
623 expected_type: expected_type_name.clone(),
624 received_value: value.to_string(),
625 suggestion: get_context_suggestions(expected_type, value),
626 });
627 }
628
629 spec.from_string(value, expected_type)
631 .map_err(|error| Error::CannotParseArg {
632 arg: arg_name.to_string(),
633 error,
634 expected_type: expected_type_name,
635 received_value: value.to_string(),
636 suggestion: get_context_suggestions(expected_type, value),
637 })
638}
639
640#[cfg(test)]
641mod tests {
642 use super::*;
643 use stellar_xdr::curr::{ScSpecTypeBytesN, ScSpecTypeDef, ScSpecTypeOption, ScSpecTypeVec};
644
645 #[test]
646 fn test_get_type_name_primitives() {
647 assert_eq!(
648 get_type_name(&ScSpecTypeDef::U32),
649 "u32 (unsigned 32-bit integer)"
650 );
651 assert_eq!(
652 get_type_name(&ScSpecTypeDef::I64),
653 "i64 (signed 64-bit integer)"
654 );
655 assert_eq!(get_type_name(&ScSpecTypeDef::Bool), "bool (true/false)");
656 assert_eq!(get_type_name(&ScSpecTypeDef::String), "string");
657 assert_eq!(
658 get_type_name(&ScSpecTypeDef::Address),
659 "address (G... for account, C... for contract, or identity name)"
660 );
661 }
662
663 #[test]
664 fn test_get_type_name_complex() {
665 let option_type = ScSpecTypeDef::Option(Box::new(ScSpecTypeOption {
666 value_type: Box::new(ScSpecTypeDef::U32),
667 }));
668 assert_eq!(
669 get_type_name(&option_type),
670 "optional u32 (unsigned 32-bit integer)"
671 );
672
673 let vec_type = ScSpecTypeDef::Vec(Box::new(ScSpecTypeVec {
674 element_type: Box::new(ScSpecTypeDef::String),
675 }));
676 assert_eq!(get_type_name(&vec_type), "vector of string");
677 }
678
679 #[test]
680 fn test_is_primitive_type_all_primitives() {
681 assert!(is_primitive_type(&ScSpecTypeDef::U32));
682 assert!(is_primitive_type(&ScSpecTypeDef::I32));
683 assert!(is_primitive_type(&ScSpecTypeDef::U64));
684 assert!(is_primitive_type(&ScSpecTypeDef::I64));
685 assert!(is_primitive_type(&ScSpecTypeDef::U128));
686 assert!(is_primitive_type(&ScSpecTypeDef::I128));
687 assert!(is_primitive_type(&ScSpecTypeDef::U256));
688 assert!(is_primitive_type(&ScSpecTypeDef::I256));
689
690 assert!(is_primitive_type(&ScSpecTypeDef::Bool));
691 assert!(is_primitive_type(&ScSpecTypeDef::Symbol));
692 assert!(is_primitive_type(&ScSpecTypeDef::String));
693 assert!(is_primitive_type(&ScSpecTypeDef::Void));
694 assert!(is_primitive_type(&ScSpecTypeDef::Bytes));
695 assert!(is_primitive_type(&ScSpecTypeDef::BytesN(
696 ScSpecTypeBytesN { n: 32 }
697 )));
698 assert!(is_primitive_type(&ScSpecTypeDef::BytesN(
699 ScSpecTypeBytesN { n: 64 }
700 )));
701
702 assert!(is_primitive_type(&ScSpecTypeDef::Address));
703 assert!(is_primitive_type(&ScSpecTypeDef::MuxedAddress));
704 assert!(is_primitive_type(&ScSpecTypeDef::Timepoint));
705 assert!(is_primitive_type(&ScSpecTypeDef::Duration));
706
707 assert!(!is_primitive_type(&ScSpecTypeDef::Vec(Box::new(
708 ScSpecTypeVec {
709 element_type: Box::new(ScSpecTypeDef::U32),
710 }
711 ))));
712 }
713
714 #[test]
715 fn test_validate_json_arg_valid() {
716 assert!(validate_json_arg("test_arg", r#"{"key": "value"}"#).is_ok());
718 assert!(validate_json_arg("test_arg", "123").is_ok());
719 assert!(validate_json_arg("test_arg", r#""string""#).is_ok());
720 assert!(validate_json_arg("test_arg", "true").is_ok());
721 assert!(validate_json_arg("test_arg", "null").is_ok());
722 }
723
724 #[test]
725 fn test_validate_json_arg_invalid() {
726 let result = validate_json_arg("test_arg", r#"{"key": value}"#); assert!(result.is_err());
729
730 if let Err(Error::InvalidJsonArg {
731 arg,
732 json_error,
733 received_value,
734 }) = result
735 {
736 assert_eq!(arg, "test_arg");
737 assert_eq!(received_value, r#"{"key": value}"#);
738 assert!(json_error.contains("expected"));
739 } else {
740 panic!("Expected InvalidJsonArg error");
741 }
742 }
743
744 #[test]
745 fn test_validate_json_arg_malformed() {
746 let test_cases = vec![
748 r#"{"key": }"#, r#"{key: "value"}"#, r#"{"key": "value",}"#, r#"{"key" "value"}"#, ];
753
754 for case in test_cases {
755 let result = validate_json_arg("test_arg", case);
756 assert!(result.is_err(), "Expected error for case: {case}");
757 }
758 }
759
760 #[test]
761 fn test_context_aware_error_messages() {
762 use stellar_xdr::curr::ScSpecTypeDef;
763
764 let suggestion = get_context_suggestions(&ScSpecTypeDef::U64, "\"100\"");
768 assert!(suggestion.contains("no quotes around the value"));
769 assert!(suggestion.contains("use 100 instead of \"100\""));
770
771 let suggestion = get_context_suggestions(&ScSpecTypeDef::U64, "100.5");
773 assert!(suggestion.contains("don't support decimal values"));
774
775 let suggestion = get_context_suggestions(&ScSpecTypeDef::String, "hello");
777 assert!(suggestion.contains("properly quoted"));
778
779 let suggestion = get_context_suggestions(&ScSpecTypeDef::Address, "invalid_addr");
781 assert!(suggestion.contains("G... (account), C... (contract)"));
782
783 let suggestion = get_context_suggestions(&ScSpecTypeDef::Bool, "yes");
785 assert!(suggestion.contains("'true' or 'false'"));
786
787 println!("=== Context-Aware Error Message Examples ===");
788 println!("U64 with quotes: {suggestion}");
789
790 let decimal_suggestion = get_context_suggestions(&ScSpecTypeDef::U64, "100.5");
791 println!("U64 with decimal: {decimal_suggestion}");
792
793 let string_suggestion = get_context_suggestions(&ScSpecTypeDef::String, "hello");
794 println!("String without quotes: {string_suggestion}");
795
796 let address_suggestion = get_context_suggestions(&ScSpecTypeDef::Address, "invalid");
797 println!("Invalid address: {address_suggestion}");
798 }
799
800 #[test]
801 fn test_error_message_format() {
802 use stellar_xdr::curr::ScSpecTypeDef;
803
804 let error = Error::CannotParseArg {
806 arg: "amount".to_string(),
807 error: soroban_spec_tools::Error::InvalidValue(Some(ScSpecTypeDef::U64)),
808 expected_type: "u64 (unsigned 64-bit integer)".to_string(),
809 received_value: "\"100\"".to_string(),
810 suggestion:
811 "For numbers, ensure no quotes around the value (e.g., use 100 instead of \"100\")"
812 .to_string(),
813 };
814
815 let error_message = format!("{error}");
816 println!("\n=== Complete Error Message Example ===");
817 println!("{error_message}");
818
819 assert!(error_message.contains("Failed to parse argument 'amount'"));
821 assert!(error_message.contains("Expected type u64 (unsigned 64-bit integer)"));
822 assert!(error_message.contains("received: '\"100\"'"));
823 assert!(error_message.contains("Suggestion: For numbers, ensure no quotes"));
824 }
825}