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::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("Duplicate map key '{key}' after alias resolution\n\nMultiple input keys resolved to the same address — likely an alias passed alongside its strkey, or two aliases pointing to the same identity.")]
69 DuplicateMapKey { key: String },
70 #[error(transparent)]
71 Xdr(#[from] xdr::Error),
72 #[error(transparent)]
73 StrVal(#[from] soroban_spec_tools::Error),
74 #[error(transparent)]
75 ScAddress(#[from] sc_address::Error),
76 #[error(transparent)]
77 Config(#[from] config::Error),
78 #[error("")]
79 HelpMessage(String),
80 #[error(transparent)]
81 Signer(#[from] signer::Error),
82}
83
84pub type HostFunctionParameters = (String, Spec, InvokeContractArgs, Vec<Signer>);
85
86fn running_cmd() -> String {
87 let mut args: Vec<String> = env::args().collect();
88
89 if let Some(pos) = args.iter().position(|arg| arg == "--") {
90 args.truncate(pos);
91 }
92
93 format!("{} --", args.join(" "))
94}
95
96pub fn build_host_function_parameters(
97 contract_id: &stellar_strkey::Contract,
98 slop: &[OsString],
99 spec_entries: &[ScSpecEntry],
100 config: &config::Args,
101) -> Result<HostFunctionParameters, Error> {
102 build_host_function_parameters_with_filter(contract_id, slop, spec_entries, config, true)
103}
104
105pub fn build_constructor_parameters(
106 contract_id: &stellar_strkey::Contract,
107 slop: &[OsString],
108 spec_entries: &[ScSpecEntry],
109 config: &config::Args,
110) -> Result<HostFunctionParameters, Error> {
111 build_host_function_parameters_with_filter(contract_id, slop, spec_entries, config, false)
112}
113
114fn build_host_function_parameters_with_filter(
115 contract_id: &stellar_strkey::Contract,
116 slop: &[OsString],
117 spec_entries: &[ScSpecEntry],
118 config: &config::Args,
119 filter_constructor: bool,
120) -> Result<HostFunctionParameters, Error> {
121 let spec = Spec(Some(spec_entries.to_vec()));
122 let cmd = build_clap_command(&spec, filter_constructor)?;
123 let (function, matches_) = parse_command_matches(cmd, slop)?;
124 let func = get_function_spec(&spec, &function)?;
125 let (parsed_args, signers) = parse_function_arguments(&func, &matches_, &spec, config)?;
126 let invoke_args = build_invoke_contract_args(contract_id, &function, parsed_args)?;
127
128 Ok((function, spec, invoke_args, signers))
129}
130
131fn build_clap_command(spec: &Spec, filter_constructor: bool) -> Result<clap::Command, Error> {
132 let mut cmd = clap::Command::new(running_cmd())
133 .no_binary_name(true)
134 .term_width(300)
135 .max_term_width(300);
136
137 for ScSpecFunctionV0 { name, .. } in spec.find_functions()? {
138 let function_name = name.to_utf8_string_lossy();
139 if !filter_constructor || function_name != CONSTRUCTOR_FUNCTION_NAME {
141 cmd = cmd.subcommand(build_custom_cmd(&function_name, spec)?);
142 }
143 }
144 cmd.build();
145 Ok(cmd)
146}
147
148fn parse_command_matches(
149 mut cmd: clap::Command,
150 slop: &[OsString],
151) -> Result<(String, clap::ArgMatches), Error> {
152 let long_help = cmd.render_long_help();
153 let maybe_matches = cmd.try_get_matches_from(slop);
154
155 let Some((function, matches_)) = (match maybe_matches {
156 Ok(mut matches) => matches.remove_subcommand(),
157 Err(e) => {
158 if e.kind() == DisplayHelp {
159 return Err(HelpMessage(e.to_string()));
160 }
161 e.exit();
162 }
163 }) else {
164 return Err(HelpMessage(format!("{long_help}")));
165 };
166
167 Ok((function.clone(), matches_))
168}
169
170fn get_function_spec(spec: &Spec, function: &str) -> Result<ScSpecFunctionV0, Error> {
171 if let Ok(f) = spec.find_function(function) {
173 return Ok(f.clone());
174 }
175 if let Ok(functions) = spec.find_functions() {
178 for f in functions {
179 if sanitize(&f.name.to_utf8_string_lossy()) == function {
180 return Ok(f.clone());
181 }
182 }
183 }
184 Err(Error::FunctionNotFoundInContractSpec {
185 function_name: function.to_string(),
186 available_functions: get_available_functions(spec),
187 })
188}
189
190fn parse_function_arguments(
191 func: &ScSpecFunctionV0,
192 matches_: &clap::ArgMatches,
193 spec: &Spec,
194 config: &config::Args,
195) -> Result<(Vec<ScVal>, Vec<Signer>), Error> {
196 let mut parsed_args = Vec::with_capacity(func.inputs.len());
197 let mut signers = Vec::<Signer>::new();
198
199 for i in &func.inputs {
200 parse_single_argument(i, matches_, spec, config, &mut signers, &mut parsed_args)?;
201 }
202
203 Ok((parsed_args, signers))
204}
205
206fn parse_single_argument(
207 input: &stellar_xdr::ScSpecFunctionInputV0,
208 matches_: &clap::ArgMatches,
209 spec: &Spec,
210 config: &config::Args,
211 signers: &mut Vec<Signer>,
212 parsed_args: &mut Vec<ScVal>,
213) -> Result<(), Error> {
214 let name = sanitize(&input.name.to_utf8_string_lossy());
215 let expected_type_name = get_type_name(&input.type_); if let Some(mut val) = matches_.get_raw(&name) {
218 let s = match val.next() {
219 Some(v) => v.to_string_lossy().to_string(),
220 None => {
221 return Err(Error::MissingArgument {
222 arg: name.clone(),
223 expected_type: expected_type_name,
224 });
225 }
226 };
227
228 if matches!(
233 input.type_,
234 ScSpecTypeDef::Address | ScSpecTypeDef::MuxedAddress
235 ) {
236 let trimmed_s = s.trim_matches('"');
237 if let Some(signer) = resolve_signer(trimmed_s, config) {
238 signers.push(signer);
239 }
240 }
241
242 parsed_args.push(parse_argument_with_validation(
243 &name,
244 &s,
245 &input.type_,
246 spec,
247 config,
248 )?);
249 Ok(())
250 } else if matches!(input.type_, ScSpecTypeDef::Option(_)) {
251 parsed_args.push(ScVal::Void);
252 Ok(())
253 } else if let Some(arg_path) = matches_.get_one::<PathBuf>(&fmt_arg_file_name(&name)) {
254 parsed_args.push(parse_file_argument(
255 &name,
256 arg_path,
257 &input.type_,
258 expected_type_name,
259 spec,
260 config,
261 )?);
262 Ok(())
263 } else {
264 Err(Error::MissingArgument {
265 arg: name,
266 expected_type: expected_type_name,
267 })
268 }
269}
270
271fn parse_file_argument(
272 name: &str,
273 arg_path: &PathBuf,
274 type_def: &ScSpecTypeDef,
275 expected_type_name: String,
276 spec: &Spec,
277 config: &config::Args,
278) -> Result<ScVal, Error> {
279 if matches!(type_def, ScSpecTypeDef::Bytes | ScSpecTypeDef::BytesN(_)) {
280 let bytes = std::fs::read(arg_path).map_err(|e| Error::MissingFileArg {
281 file_path: arg_path.clone(),
282 error: e.to_string(),
283 })?;
284 ScVal::try_from(&bytes).map_err(|()| Error::CannotParseArg {
285 arg: name.to_string(),
286 error: soroban_spec_tools::Error::Unknown,
287 expected_type: expected_type_name,
288 received_value: format!("{} bytes from file", bytes.len()),
289 suggestion: "Ensure the file contains valid binary data for the expected byte type"
290 .to_string(),
291 })
292 } else {
293 let file_contents =
294 std::fs::read_to_string(arg_path).map_err(|e| Error::MissingFileArg {
295 file_path: arg_path.clone(),
296 error: e.to_string(),
297 })?;
298 tracing::debug!(
299 "file {arg_path:?}, has contents:\n{file_contents}\nAnd type {:#?}\n{}",
300 type_def,
301 file_contents.len()
302 );
303 parse_argument_with_validation(name, &file_contents, type_def, spec, config)
304 }
305}
306
307fn build_invoke_contract_args(
308 contract_id: &stellar_strkey::Contract,
309 function: &str,
310 parsed_args: Vec<ScVal>,
311) -> Result<InvokeContractArgs, Error> {
312 let contract_address_arg = xdr::ScAddress::Contract(ContractId(Hash(contract_id.0)));
313 let function_symbol_arg = function
314 .try_into()
315 .map_err(|()| Error::FunctionNameTooLong {
316 function_name: function.to_string(),
317 length: function.len(),
318 })?;
319
320 let final_args =
321 parsed_args
322 .clone()
323 .try_into()
324 .map_err(|_| Error::MaxNumberOfArgumentsReached {
325 current: parsed_args.len(),
326 maximum: ScVec::default().max_len(),
327 })?;
328
329 Ok(InvokeContractArgs {
330 contract_address: contract_address_arg,
331 function_name: function_symbol_arg,
332 args: final_args,
333 })
334}
335
336pub fn build_custom_cmd(name: &str, spec: &Spec) -> Result<clap::Command, Error> {
337 let func = spec
338 .find_function(name)
339 .map_err(|_| Error::FunctionNotFoundInContractSpec {
340 function_name: name.to_string(),
341 available_functions: get_available_functions(spec),
342 })?;
343
344 let inputs_map = &func
346 .inputs
347 .iter()
348 .map(|i| (sanitize(&i.name.to_utf8_string_lossy()), i.type_.clone()))
349 .collect::<HashMap<String, ScSpecTypeDef>>();
350 let name: &'static str = Box::leak(sanitize(name).into_boxed_str());
351 let mut cmd = clap::Command::new(name)
352 .no_binary_name(true)
353 .term_width(300)
354 .max_term_width(300);
355 let kebab_name = name.to_kebab_case();
356 if kebab_name != name {
357 cmd = cmd.alias(kebab_name);
358 }
359 let doc: &'static str = Box::leak(sanitize(&func.doc.to_utf8_string_lossy()).into_boxed_str());
360 let long_doc: &'static str = Box::leak(arg_file_help(doc).into_boxed_str());
361
362 cmd = cmd.about(Some(doc)).long_about(long_doc);
363 for (name, type_) in inputs_map {
364 let mut arg = clap::Arg::new(name);
365 let file_arg_name = fmt_arg_file_name(name);
366 let mut file_arg = clap::Arg::new(&file_arg_name);
367 arg = arg
368 .long(name)
369 .alias(name.to_kebab_case())
370 .num_args(1)
371 .value_parser(clap::builder::NonEmptyStringValueParser::new())
372 .long_help(
373 spec.doc(name, type_)?
374 .map(|d| -> &'static str { Box::leak(sanitize(d).into_boxed_str()) }),
375 );
376
377 file_arg = file_arg
378 .long(&file_arg_name)
379 .alias(file_arg_name.to_kebab_case())
380 .num_args(1)
381 .hide(true)
382 .value_parser(value_parser!(PathBuf))
383 .conflicts_with(name);
384
385 if let Some(value_name) = spec.arg_value_name(type_, 0) {
386 let value_name: &'static str = Box::leak(sanitize(&value_name).into_boxed_str());
387 arg = arg.value_name(value_name);
388 }
389
390 arg = match type_ {
392 ScSpecTypeDef::Bool => arg
393 .num_args(0..1)
394 .default_missing_value("true")
395 .default_value("false")
396 .num_args(0..=1),
397 ScSpecTypeDef::Option(_val) => arg.required(false),
398 ScSpecTypeDef::I256 | ScSpecTypeDef::I128 | ScSpecTypeDef::I64 | ScSpecTypeDef::I32 => {
399 arg.allow_hyphen_values(true)
400 }
401 _ => arg,
402 };
403
404 cmd = cmd.arg(arg);
405 cmd = cmd.arg(file_arg);
406 }
407 Ok(cmd)
408}
409
410fn fmt_arg_file_name(name: &str) -> String {
411 format!("{name}-file-path")
412}
413
414fn arg_file_help(docs: &str) -> String {
415 format!(
416 r"{docs}
417Usage Notes:
418Each arg has a corresponding --<arg_name>-file-path which is a path to a file containing the corresponding JSON argument.
419Note: The only types which aren't JSON are Bytes and BytesN, which are raw bytes"
420 )
421}
422
423pub fn output_to_string(
424 spec: &Spec,
425 res: &ScVal,
426 function: &str,
427) -> Result<TxnResult<String>, Error> {
428 let mut res_str = String::new();
429 if let Some(output) = spec.find_function(function)?.outputs.first() {
430 res_str = spec
431 .xdr_to_json(res, output)
432 .map_err(|e| Error::CannotPrintResult {
433 result: res.clone(),
434 error: e,
435 })?
436 .to_string();
437 }
438 Ok(TxnResult::Res(res_str))
439}
440
441fn resolve_address(addr_or_alias: &str, config: &config::Args) -> Result<String, Error> {
442 let sc_address: UnresolvedScAddress = addr_or_alias.parse().unwrap();
443 let account = match sc_address {
444 UnresolvedScAddress::Resolved(addr) => addr.to_string(),
445 addr @ UnresolvedScAddress::Alias(_) => {
446 let addr = addr.resolve(
447 &config.locator,
448 &config.get_network()?.network_passphrase,
449 config.hd_path(),
450 )?;
451 match addr {
452 xdr::ScAddress::Account(account) => account.to_string(),
453 contract @ xdr::ScAddress::Contract(_) => contract.to_string(),
454 stellar_xdr::ScAddress::MuxedAccount(account) => account.to_string(),
455 stellar_xdr::ScAddress::ClaimableBalance(_)
456 | stellar_xdr::ScAddress::LiquidityPool(_) => {
457 return Err(Error::UnsupportedScAddress {
458 address: addr.to_string(),
459 })
460 }
461 }
462 }
463 };
464 Ok(account)
465}
466
467fn resolve_signer(addr_or_alias: &str, config: &config::Args) -> Option<Signer> {
468 let account: config::UnresolvedMuxedAccount = addr_or_alias.parse().ok()?;
469 let secret = account
472 .resolve_secret(&config.locator, config.hd_path())
473 .ok()?;
474 let signer = secret.signer(config.hd_path(), Print::new(false)).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 .is_ok_and(|entry| matches!(entry, ScSpecEntry::UdtUnionV0(_)))
633 } else {
634 false
635 };
636 if !is_primitive_type(expected_type) && !is_union_udt {
637 validate_json_arg(arg_name, value)?;
638 }
639
640 let resolved = resolve_aliases(value, expected_type, spec, config)?;
643
644 spec.from_string(&resolved, expected_type)
645 .map_err(|error| Error::CannotParseArg {
646 arg: arg_name.to_string(),
647 error,
648 expected_type: expected_type_name,
649 received_value: value.to_string(),
650 suggestion: get_context_suggestions(expected_type, value),
651 })
652}
653
654fn resolve_aliases(
659 value: &str,
660 type_def: &ScSpecTypeDef,
661 spec: &Spec,
662 config: &config::Args,
663) -> Result<String, Error> {
664 let is_address = matches!(
665 type_def,
666 ScSpecTypeDef::Address | ScSpecTypeDef::MuxedAddress
667 );
668
669 let mut json = match serde_json::from_str::<serde_json::Value>(value) {
670 Ok(j) => j,
671 Err(_) if is_address => serde_json::Value::String(value.trim_matches('"').to_string()),
672 Err(_) => return Ok(value.to_string()),
673 };
674
675 let mutated = resolve_aliases_in_json(&mut json, type_def, spec, config)?;
676
677 if !mutated {
680 return Ok(value.to_string());
681 }
682
683 Ok(match (&json, is_address) {
687 (serde_json::Value::String(s), true) => s.clone(),
688 _ => json.to_string(),
689 })
690}
691
692fn resolve_aliases_in_json(
701 value: &mut serde_json::Value,
702 type_def: &ScSpecTypeDef,
703 spec: &Spec,
704 config: &config::Args,
705) -> Result<bool, Error> {
706 let mut mutated = false;
707 match type_def {
708 ScSpecTypeDef::Address | ScSpecTypeDef::MuxedAddress => {
709 if let serde_json::Value::String(s) = value {
710 let resolved = resolve_address(s, config)?;
711 if &resolved != s {
712 *s = resolved;
713 mutated = true;
714 }
715 }
716 }
717 ScSpecTypeDef::Vec(inner) => {
718 if let serde_json::Value::Array(arr) = value {
719 for item in arr.iter_mut() {
720 mutated |= resolve_aliases_in_json(item, &inner.element_type, spec, config)?;
721 }
722 }
723 }
724 ScSpecTypeDef::Tuple(tuple) => {
725 if let serde_json::Value::Array(arr) = value {
726 for (item, ty) in arr.iter_mut().zip(tuple.value_types.iter()) {
727 mutated |= resolve_aliases_in_json(item, ty, spec, config)?;
728 }
729 }
730 }
731 ScSpecTypeDef::Map(map) => {
732 if let serde_json::Value::Object(obj) = value {
733 let key_is_address = matches!(
734 map.key_type.as_ref(),
735 ScSpecTypeDef::Address | ScSpecTypeDef::MuxedAddress
736 );
737 if key_is_address {
738 let entries = std::mem::take(obj);
739 for (k, mut v) in entries {
740 mutated |= resolve_aliases_in_json(&mut v, &map.value_type, spec, config)?;
741 let resolved = resolve_address(&k, config)?;
742 if resolved != k {
743 mutated = true;
744 }
745 if obj.contains_key(&resolved) {
746 return Err(Error::DuplicateMapKey { key: resolved });
747 }
748 obj.insert(resolved, v);
749 }
750 } else {
751 for v in obj.values_mut() {
752 mutated |= resolve_aliases_in_json(v, &map.value_type, spec, config)?;
753 }
754 }
755 }
756 }
757 ScSpecTypeDef::Option(inner) if !matches!(value, serde_json::Value::Null) => {
758 mutated |= resolve_aliases_in_json(value, &inner.value_type, spec, config)?;
759 }
760 ScSpecTypeDef::Result(result) => {
761 mutated |= resolve_aliases_in_json(value, &result.ok_type, spec, config)?;
767 mutated |= resolve_aliases_in_json(value, &result.error_type, spec, config)?;
768 }
769 ScSpecTypeDef::Udt(udt) => {
770 mutated |= resolve_aliases_in_udt(value, udt, spec, config)?;
771 }
772 _ => {}
773 }
774 Ok(mutated)
775}
776
777fn resolve_aliases_in_udt(
778 value: &mut serde_json::Value,
779 udt: &stellar_xdr::ScSpecTypeUdt,
780 spec: &Spec,
781 config: &config::Args,
782) -> Result<bool, Error> {
783 let mut mutated = false;
784 let name = udt.name.to_utf8_string_lossy();
785 let Ok(entry) = spec.find(&name) else {
786 return Ok(false);
787 };
788 match entry {
789 ScSpecEntry::UdtStructV0(strukt) => {
790 let is_tuple_struct = strukt
794 .fields
795 .iter()
796 .any(|f| f.name.to_utf8_string_lossy() == "0");
797 match value {
798 serde_json::Value::Array(arr) if is_tuple_struct => {
799 for (item, field) in arr.iter_mut().zip(strukt.fields.iter()) {
800 mutated |= resolve_aliases_in_json(item, &field.type_, spec, config)?;
801 }
802 }
803 serde_json::Value::Object(obj) => {
804 for field in &strukt.fields {
805 let key = field.name.to_utf8_string_lossy();
806 if let Some(field_val) = obj.get_mut(key.as_str()) {
807 mutated |=
808 resolve_aliases_in_json(field_val, &field.type_, spec, config)?;
809 }
810 }
811 }
812 _ => {}
813 }
814 }
815 ScSpecEntry::UdtUnionV0(union) => {
816 mutated |= resolve_aliases_in_union(value, union, spec, config)?;
817 }
818 _ => {}
819 }
820 Ok(mutated)
821}
822
823fn resolve_aliases_in_union(
824 value: &mut serde_json::Value,
825 union: &stellar_xdr::ScSpecUdtUnionV0,
826 spec: &Spec,
827 config: &config::Args,
828) -> Result<bool, Error> {
829 use stellar_xdr::ScSpecUdtUnionCaseV0;
830
831 let serde_json::Value::Object(obj) = value else {
832 return Ok(false);
833 };
834 let Some((case_name, payload)) = obj.iter_mut().next() else {
835 return Ok(false);
836 };
837 let matched = union.cases.iter().find_map(|c| match c {
838 ScSpecUdtUnionCaseV0::TupleV0(t) if t.name.to_utf8_string_lossy() == *case_name => Some(t),
839 _ => None,
840 });
841 let Some(tuple) = matched else {
842 return Ok(false);
843 };
844 if tuple.type_.len() == 1 {
848 return resolve_aliases_in_json(payload, &tuple.type_[0], spec, config);
849 }
850 let mut mutated = false;
851 if let serde_json::Value::Array(arr) = payload {
852 for (item, ty) in arr.iter_mut().zip(tuple.type_.iter()) {
853 mutated |= resolve_aliases_in_json(item, ty, spec, config)?;
854 }
855 }
856 Ok(mutated)
857}
858
859#[cfg(test)]
860mod tests {
861 use super::*;
862 use stellar_xdr::{ScSpecTypeBytesN, ScSpecTypeDef, ScSpecTypeOption, ScSpecTypeVec};
863
864 #[test]
865 fn test_get_type_name_primitives() {
866 assert_eq!(
867 get_type_name(&ScSpecTypeDef::U32),
868 "u32 (unsigned 32-bit integer)"
869 );
870 assert_eq!(
871 get_type_name(&ScSpecTypeDef::I64),
872 "i64 (signed 64-bit integer)"
873 );
874 assert_eq!(get_type_name(&ScSpecTypeDef::Bool), "bool (true/false)");
875 assert_eq!(get_type_name(&ScSpecTypeDef::String), "string");
876 assert_eq!(
877 get_type_name(&ScSpecTypeDef::Address),
878 "address (G... for account, C... for contract, or identity name)"
879 );
880 }
881
882 #[test]
883 fn test_get_type_name_complex() {
884 let option_type = ScSpecTypeDef::Option(Box::new(ScSpecTypeOption {
885 value_type: Box::new(ScSpecTypeDef::U32),
886 }));
887 assert_eq!(
888 get_type_name(&option_type),
889 "optional u32 (unsigned 32-bit integer)"
890 );
891
892 let vec_type = ScSpecTypeDef::Vec(Box::new(ScSpecTypeVec {
893 element_type: Box::new(ScSpecTypeDef::String),
894 }));
895 assert_eq!(get_type_name(&vec_type), "vector of string");
896 }
897
898 #[test]
899 fn test_is_primitive_type_all_primitives() {
900 assert!(is_primitive_type(&ScSpecTypeDef::U32));
901 assert!(is_primitive_type(&ScSpecTypeDef::I32));
902 assert!(is_primitive_type(&ScSpecTypeDef::U64));
903 assert!(is_primitive_type(&ScSpecTypeDef::I64));
904 assert!(is_primitive_type(&ScSpecTypeDef::U128));
905 assert!(is_primitive_type(&ScSpecTypeDef::I128));
906 assert!(is_primitive_type(&ScSpecTypeDef::U256));
907 assert!(is_primitive_type(&ScSpecTypeDef::I256));
908
909 assert!(is_primitive_type(&ScSpecTypeDef::Bool));
910 assert!(is_primitive_type(&ScSpecTypeDef::Symbol));
911 assert!(is_primitive_type(&ScSpecTypeDef::String));
912 assert!(is_primitive_type(&ScSpecTypeDef::Void));
913 assert!(is_primitive_type(&ScSpecTypeDef::Bytes));
914 assert!(is_primitive_type(&ScSpecTypeDef::BytesN(
915 ScSpecTypeBytesN { n: 32 }
916 )));
917 assert!(is_primitive_type(&ScSpecTypeDef::BytesN(
918 ScSpecTypeBytesN { n: 64 }
919 )));
920
921 assert!(is_primitive_type(&ScSpecTypeDef::Address));
922 assert!(is_primitive_type(&ScSpecTypeDef::MuxedAddress));
923 assert!(is_primitive_type(&ScSpecTypeDef::Timepoint));
924 assert!(is_primitive_type(&ScSpecTypeDef::Duration));
925
926 assert!(!is_primitive_type(&ScSpecTypeDef::Vec(Box::new(
927 ScSpecTypeVec {
928 element_type: Box::new(ScSpecTypeDef::U32),
929 }
930 ))));
931 }
932
933 #[test]
937 fn build_custom_cmd_strips_control_bytes_from_value_name() {
938 use soroban_spec_tools::test_utils::assert_no_control_chars;
939 use stellar_xdr::{
940 ScSpecEntry, ScSpecFunctionInputV0, ScSpecFunctionV0, ScSpecTypeUdt,
941 ScSpecUdtStructFieldV0, ScSpecUdtStructV0, ScSymbol,
942 };
943
944 let strukt = ScSpecEntry::UdtStructV0(ScSpecUdtStructV0 {
945 doc: "".try_into().unwrap(),
946 lib: "".try_into().unwrap(),
947 name: "S".try_into().unwrap(),
948 fields: vec![ScSpecUdtStructFieldV0 {
949 doc: "".try_into().unwrap(),
950 name: "\x1b[2Jevil".try_into().unwrap(),
951 type_: ScSpecTypeDef::U32,
952 }]
953 .try_into()
954 .unwrap(),
955 });
956 let func = ScSpecEntry::FunctionV0(ScSpecFunctionV0 {
957 doc: "".try_into().unwrap(),
958 name: ScSymbol("f".try_into().unwrap()),
959 inputs: vec![ScSpecFunctionInputV0 {
960 doc: "".try_into().unwrap(),
961 name: "s".try_into().unwrap(),
962 type_: ScSpecTypeDef::Udt(ScSpecTypeUdt {
963 name: "S".try_into().unwrap(),
964 }),
965 }]
966 .try_into()
967 .unwrap(),
968 outputs: vec![].try_into().unwrap(),
969 });
970
971 let spec = Spec(Some(vec![strukt, func]));
972
973 let cmd = build_custom_cmd("f", &spec).unwrap();
974 let arg = cmd
975 .get_arguments()
976 .find(|a| a.get_id() == "s")
977 .expect("arg `s` should be registered");
978 for value_name in arg.get_value_names().unwrap_or_default() {
979 assert_no_control_chars(value_name.as_str());
980 }
981 }
982
983 #[test]
984 fn test_validate_json_arg_valid() {
985 assert!(validate_json_arg("test_arg", r#"{"key": "value"}"#).is_ok());
987 assert!(validate_json_arg("test_arg", "123").is_ok());
988 assert!(validate_json_arg("test_arg", r#""string""#).is_ok());
989 assert!(validate_json_arg("test_arg", "true").is_ok());
990 assert!(validate_json_arg("test_arg", "null").is_ok());
991 }
992
993 #[test]
994 fn test_validate_json_arg_invalid() {
995 let result = validate_json_arg("test_arg", r#"{"key": value}"#); assert!(result.is_err());
998
999 if let Err(Error::InvalidJsonArg {
1000 arg,
1001 json_error,
1002 received_value,
1003 }) = result
1004 {
1005 assert_eq!(arg, "test_arg");
1006 assert_eq!(received_value, r#"{"key": value}"#);
1007 assert!(json_error.contains("expected"));
1008 } else {
1009 panic!("Expected InvalidJsonArg error");
1010 }
1011 }
1012
1013 #[test]
1014 fn test_validate_json_arg_malformed() {
1015 let test_cases = vec![
1017 r#"{"key": }"#, r#"{key: "value"}"#, r#"{"key": "value",}"#, r#"{"key" "value"}"#, ];
1022
1023 for case in test_cases {
1024 let result = validate_json_arg("test_arg", case);
1025 assert!(result.is_err(), "Expected error for case: {case}");
1026 }
1027 }
1028
1029 #[test]
1030 fn test_context_aware_error_messages() {
1031 use stellar_xdr::ScSpecTypeDef;
1032
1033 let suggestion = get_context_suggestions(&ScSpecTypeDef::U64, "\"100\"");
1037 assert!(suggestion.contains("no quotes around the value"));
1038 assert!(suggestion.contains("use 100 instead of \"100\""));
1039
1040 let suggestion = get_context_suggestions(&ScSpecTypeDef::U64, "100.5");
1042 assert!(suggestion.contains("don't support decimal values"));
1043
1044 let suggestion = get_context_suggestions(&ScSpecTypeDef::String, "hello");
1046 assert!(suggestion.contains("properly quoted"));
1047
1048 let suggestion = get_context_suggestions(&ScSpecTypeDef::Address, "invalid_addr");
1050 assert!(suggestion.contains("G... (account), C... (contract)"));
1051
1052 let suggestion = get_context_suggestions(&ScSpecTypeDef::Bool, "yes");
1054 assert!(suggestion.contains("'true' or 'false'"));
1055
1056 println!("=== Context-Aware Error Message Examples ===");
1057 println!("U64 with quotes: {suggestion}");
1058
1059 let decimal_suggestion = get_context_suggestions(&ScSpecTypeDef::U64, "100.5");
1060 println!("U64 with decimal: {decimal_suggestion}");
1061
1062 let string_suggestion = get_context_suggestions(&ScSpecTypeDef::String, "hello");
1063 println!("String without quotes: {string_suggestion}");
1064
1065 let address_suggestion = get_context_suggestions(&ScSpecTypeDef::Address, "invalid");
1066 println!("Invalid address: {address_suggestion}");
1067 }
1068
1069 #[test]
1070 fn test_union_udt_bare_string_accepted() {
1071 use stellar_xdr::{
1072 ScSpecEntry, ScSpecTypeDef, ScSpecTypeUdt, ScSpecUdtUnionCaseV0,
1073 ScSpecUdtUnionCaseVoidV0, ScSpecUdtUnionV0, StringM,
1074 };
1075
1076 let union_name: StringM<60> = "MyEnum".try_into().unwrap();
1078 let case_name: StringM<60> = "Unit".try_into().unwrap();
1079 let spec = Spec(Some(vec![ScSpecEntry::UdtUnionV0(ScSpecUdtUnionV0 {
1080 doc: StringM::default(),
1081 lib: StringM::default(),
1082 name: union_name.clone(),
1083 cases: vec![ScSpecUdtUnionCaseV0::VoidV0(ScSpecUdtUnionCaseVoidV0 {
1084 doc: StringM::default(),
1085 name: case_name,
1086 })]
1087 .try_into()
1088 .unwrap(),
1089 })]));
1090
1091 let expected_type = ScSpecTypeDef::Udt(ScSpecTypeUdt { name: union_name });
1092 let config = crate::config::Args::default();
1093
1094 let result =
1096 parse_argument_with_validation("value", "Unit", &expected_type, &spec, &config);
1097 assert!(result.is_ok(), "bare 'Unit' should be accepted: {result:?}");
1098
1099 let result =
1101 parse_argument_with_validation("value", "\"Unit\"", &expected_type, &spec, &config);
1102 assert!(
1103 result.is_ok(),
1104 "JSON-quoted '\"Unit\"' should be accepted: {result:?}"
1105 );
1106
1107 let bare = parse_argument_with_validation("value", "Unit", &expected_type, &spec, &config)
1109 .unwrap();
1110 let quoted =
1111 parse_argument_with_validation("value", "\"Unit\"", &expected_type, &spec, &config)
1112 .unwrap();
1113 assert_eq!(
1114 bare, quoted,
1115 "bare and quoted forms should produce identical ScVal"
1116 );
1117 }
1118
1119 #[test]
1120 fn test_union_udt_tuple_variant_still_requires_json() {
1121 use stellar_xdr::{
1122 ScSpecEntry, ScSpecTypeDef, ScSpecTypeUdt, ScSpecUdtUnionCaseTupleV0,
1123 ScSpecUdtUnionCaseV0, ScSpecUdtUnionCaseVoidV0, ScSpecUdtUnionV0, StringM,
1124 };
1125
1126 let union_name: StringM<60> = "MyEnum".try_into().unwrap();
1127 let spec = Spec(Some(vec![ScSpecEntry::UdtUnionV0(ScSpecUdtUnionV0 {
1128 doc: StringM::default(),
1129 lib: StringM::default(),
1130 name: union_name.clone(),
1131 cases: vec![
1132 ScSpecUdtUnionCaseV0::VoidV0(ScSpecUdtUnionCaseVoidV0 {
1133 doc: StringM::default(),
1134 name: "Unit".try_into().unwrap(),
1135 }),
1136 ScSpecUdtUnionCaseV0::TupleV0(ScSpecUdtUnionCaseTupleV0 {
1137 doc: StringM::default(),
1138 name: "WithValue".try_into().unwrap(),
1139 type_: vec![ScSpecTypeDef::U32].try_into().unwrap(),
1140 }),
1141 ]
1142 .try_into()
1143 .unwrap(),
1144 })]));
1145
1146 let expected_type = ScSpecTypeDef::Udt(ScSpecTypeUdt { name: union_name });
1147 let config = crate::config::Args::default();
1148
1149 let result = parse_argument_with_validation(
1151 "value",
1152 r#"{"WithValue":42}"#,
1153 &expected_type,
1154 &spec,
1155 &config,
1156 );
1157 assert!(
1158 result.is_ok(),
1159 "JSON object for tuple variant should be accepted: {result:?}"
1160 );
1161 }
1162
1163 #[test]
1164 fn test_error_message_format() {
1165 use stellar_xdr::ScSpecTypeDef;
1166
1167 let error = Error::CannotParseArg {
1169 arg: "amount".to_string(),
1170 error: soroban_spec_tools::Error::InvalidValue(Some(ScSpecTypeDef::U64)),
1171 expected_type: "u64 (unsigned 64-bit integer)".to_string(),
1172 received_value: "\"100\"".to_string(),
1173 suggestion:
1174 "For numbers, ensure no quotes around the value (e.g., use 100 instead of \"100\")"
1175 .to_string(),
1176 };
1177
1178 let error_message = format!("{error}");
1179 println!("\n=== Complete Error Message Example ===");
1180 println!("{error_message}");
1181
1182 assert!(error_message.contains("Failed to parse argument 'amount'"));
1184 assert!(error_message.contains("Expected type u64 (unsigned 64-bit integer)"));
1185 assert!(error_message.contains("received: '\"100\"'"));
1186 assert!(error_message.contains("Suggestion: For numbers, ensure no quotes"));
1187 }
1188
1189 fn struct_spec(name: &'static str, fields: &[(&str, ScSpecTypeDef)]) -> (Spec, ScSpecTypeDef) {
1190 use stellar_xdr::{
1191 ScSpecEntry, ScSpecTypeUdt, ScSpecUdtStructFieldV0, ScSpecUdtStructV0, StringM,
1192 };
1193 let struct_name: StringM<60> = name.try_into().unwrap();
1194 let fields_xdr: Vec<ScSpecUdtStructFieldV0> = fields
1195 .iter()
1196 .map(|(n, t)| ScSpecUdtStructFieldV0 {
1197 doc: StringM::default(),
1198 name: (*n).try_into().unwrap(),
1199 type_: t.clone(),
1200 })
1201 .collect();
1202 let spec = Spec(Some(vec![ScSpecEntry::UdtStructV0(ScSpecUdtStructV0 {
1203 doc: StringM::default(),
1204 lib: StringM::default(),
1205 name: struct_name.clone(),
1206 fields: fields_xdr.try_into().unwrap(),
1207 })]));
1208 let ty = ScSpecTypeDef::Udt(ScSpecTypeUdt { name: struct_name });
1209 (spec, ty)
1210 }
1211
1212 const TEST_G_ADDRESS: &str = "GD5KD2KEZJIGTC63IGW6UMUSMVUVG5IHG64HUTFWCHVZH2N2IBOQN7PS";
1214
1215 #[test]
1216 fn resolve_aliases_resolves_native_to_asset_contract_address() {
1217 let ty = ScSpecTypeDef::Address;
1218 let spec = Spec(Some(vec![]));
1219 let config = crate::config::Args::default();
1220
1221 let mut value = serde_json::json!("native");
1222 let mutated = resolve_aliases_in_json(&mut value, &ty, &spec, &config).unwrap();
1223 assert!(
1224 mutated,
1225 "native should resolve to the native asset contract"
1226 );
1227
1228 let network_passphrase = config.get_network().unwrap().network_passphrase;
1229 let expected = format!(
1230 "{}",
1231 crate::utils::contract_id_hash_from_asset(
1232 &crate::xdr::Asset::Native,
1233 &network_passphrase,
1234 )
1235 );
1236 assert_eq!(value, serde_json::Value::String(expected));
1237 }
1238
1239 #[test]
1240 fn resolve_aliases_in_json_walks_vec_of_address() {
1241 use stellar_xdr::ScSpecTypeVec;
1242
1243 let ty = ScSpecTypeDef::Vec(Box::new(ScSpecTypeVec {
1244 element_type: Box::new(ScSpecTypeDef::Address),
1245 }));
1246 let spec = Spec(Some(vec![]));
1247 let config = crate::config::Args::default();
1248
1249 let mut value = serde_json::json!([TEST_G_ADDRESS]);
1250 resolve_aliases_in_json(&mut value, &ty, &spec, &config).unwrap();
1251 assert_eq!(value, serde_json::json!([TEST_G_ADDRESS]));
1252
1253 let mut value = serde_json::json!(["definitely-not-a-known-alias"]);
1255 let err = resolve_aliases_in_json(&mut value, &ty, &spec, &config).unwrap_err();
1256 assert!(
1257 matches!(err, Error::Config(_) | Error::ScAddress(_)),
1258 "expected alias-resolution error, got {err:?}"
1259 );
1260 }
1261
1262 #[test]
1263 fn resolve_aliases_in_json_walks_tuple() {
1264 use stellar_xdr::ScSpecTypeTuple;
1265
1266 let ty = ScSpecTypeDef::Tuple(Box::new(ScSpecTypeTuple {
1267 value_types: vec![ScSpecTypeDef::Address, ScSpecTypeDef::U32]
1268 .try_into()
1269 .unwrap(),
1270 }));
1271 let spec = Spec(Some(vec![]));
1272 let config = crate::config::Args::default();
1273
1274 let mut value = serde_json::json!([TEST_G_ADDRESS, 42]);
1275 resolve_aliases_in_json(&mut value, &ty, &spec, &config).unwrap();
1276 assert_eq!(value, serde_json::json!([TEST_G_ADDRESS, 42]));
1277
1278 let mut value = serde_json::json!(["bogus-alias", 42]);
1279 let err = resolve_aliases_in_json(&mut value, &ty, &spec, &config).unwrap_err();
1280 assert!(
1281 matches!(err, Error::Config(_) | Error::ScAddress(_)),
1282 "expected alias-resolution error, got {err:?}"
1283 );
1284 }
1285
1286 #[test]
1287 fn resolve_aliases_in_json_walks_struct_field() {
1288 use stellar_xdr::ScSpecTypeVec;
1289
1290 let (spec, ty) = struct_spec(
1291 "Operator",
1292 &[
1293 ("count", ScSpecTypeDef::U32),
1294 (
1295 "addresses",
1296 ScSpecTypeDef::Vec(Box::new(ScSpecTypeVec {
1297 element_type: Box::new(ScSpecTypeDef::Address),
1298 })),
1299 ),
1300 ],
1301 );
1302 let config = crate::config::Args::default();
1303
1304 let mut value = serde_json::json!({"count": 1, "addresses": [TEST_G_ADDRESS]});
1305 resolve_aliases_in_json(&mut value, &ty, &spec, &config).unwrap();
1306 assert_eq!(
1307 value,
1308 serde_json::json!({"count": 1, "addresses": [TEST_G_ADDRESS]})
1309 );
1310
1311 let mut value = serde_json::json!({"count": 1, "addresses": ["bogus-alias"]});
1313 let err = resolve_aliases_in_json(&mut value, &ty, &spec, &config).unwrap_err();
1314 assert!(
1315 matches!(err, Error::Config(_) | Error::ScAddress(_)),
1316 "expected alias-resolution error, got {err:?}"
1317 );
1318 }
1319
1320 #[test]
1321 fn resolve_aliases_in_json_walks_union_tuple_variant() {
1322 use stellar_xdr::{
1323 ScSpecEntry, ScSpecTypeUdt, ScSpecUdtUnionCaseTupleV0, ScSpecUdtUnionCaseV0,
1324 ScSpecUdtUnionV0, StringM,
1325 };
1326
1327 let union_name: StringM<60> = "Choice".try_into().unwrap();
1328 let spec = Spec(Some(vec![ScSpecEntry::UdtUnionV0(ScSpecUdtUnionV0 {
1329 doc: StringM::default(),
1330 lib: StringM::default(),
1331 name: union_name.clone(),
1332 cases: vec![ScSpecUdtUnionCaseV0::TupleV0(ScSpecUdtUnionCaseTupleV0 {
1333 doc: StringM::default(),
1334 name: "Pick".try_into().unwrap(),
1335 type_: vec![ScSpecTypeDef::Address, ScSpecTypeDef::U32]
1336 .try_into()
1337 .unwrap(),
1338 })]
1339 .try_into()
1340 .unwrap(),
1341 })]));
1342
1343 let ty = ScSpecTypeDef::Udt(ScSpecTypeUdt { name: union_name });
1344 let config = crate::config::Args::default();
1345
1346 let mut value = serde_json::json!({"Pick": [TEST_G_ADDRESS, 42]});
1347 resolve_aliases_in_json(&mut value, &ty, &spec, &config).unwrap();
1348 assert_eq!(value, serde_json::json!({"Pick": [TEST_G_ADDRESS, 42]}));
1349
1350 let mut value = serde_json::json!({"Pick": ["bogus-alias", 42]});
1351 let err = resolve_aliases_in_json(&mut value, &ty, &spec, &config).unwrap_err();
1352 assert!(
1353 matches!(err, Error::Config(_) | Error::ScAddress(_)),
1354 "expected alias-resolution error, got {err:?}"
1355 );
1356 }
1357
1358 #[test]
1359 fn resolve_aliases_in_json_walks_single_element_union_variant() {
1360 use stellar_xdr::{
1361 ScSpecEntry, ScSpecTypeUdt, ScSpecUdtUnionCaseTupleV0, ScSpecUdtUnionCaseV0,
1362 ScSpecUdtUnionV0, StringM,
1363 };
1364
1365 let union_name: StringM<60> = "OneOf".try_into().unwrap();
1366 let spec = Spec(Some(vec![ScSpecEntry::UdtUnionV0(ScSpecUdtUnionV0 {
1367 doc: StringM::default(),
1368 lib: StringM::default(),
1369 name: union_name.clone(),
1370 cases: vec![ScSpecUdtUnionCaseV0::TupleV0(ScSpecUdtUnionCaseTupleV0 {
1371 doc: StringM::default(),
1372 name: "Only".try_into().unwrap(),
1373 type_: vec![ScSpecTypeDef::Address].try_into().unwrap(),
1374 })]
1375 .try_into()
1376 .unwrap(),
1377 })]));
1378
1379 let ty = ScSpecTypeDef::Udt(ScSpecTypeUdt { name: union_name });
1380 let config = crate::config::Args::default();
1381
1382 let mut value = serde_json::json!({"Only": TEST_G_ADDRESS});
1384 resolve_aliases_in_json(&mut value, &ty, &spec, &config).unwrap();
1385 assert_eq!(value, serde_json::json!({"Only": TEST_G_ADDRESS}));
1386
1387 let mut value = serde_json::json!({"Only": "bogus-alias"});
1388 let err = resolve_aliases_in_json(&mut value, &ty, &spec, &config).unwrap_err();
1389 assert!(
1390 matches!(err, Error::Config(_) | Error::ScAddress(_)),
1391 "expected alias-resolution error, got {err:?}"
1392 );
1393 }
1394
1395 #[test]
1396 fn resolve_aliases_in_json_walks_option_and_map() {
1397 use stellar_xdr::{ScSpecTypeMap, ScSpecTypeOption};
1398
1399 let opt_ty = ScSpecTypeDef::Option(Box::new(ScSpecTypeOption {
1400 value_type: Box::new(ScSpecTypeDef::Address),
1401 }));
1402 let spec = Spec(Some(vec![]));
1403 let config = crate::config::Args::default();
1404
1405 let mut value = serde_json::Value::Null;
1406 resolve_aliases_in_json(&mut value, &opt_ty, &spec, &config).unwrap();
1407 assert_eq!(value, serde_json::Value::Null);
1408
1409 let mut value = serde_json::json!(TEST_G_ADDRESS);
1410 resolve_aliases_in_json(&mut value, &opt_ty, &spec, &config).unwrap();
1411 assert_eq!(value, serde_json::json!(TEST_G_ADDRESS));
1412
1413 let map_ty = ScSpecTypeDef::Map(Box::new(ScSpecTypeMap {
1414 key_type: Box::new(ScSpecTypeDef::Symbol),
1415 value_type: Box::new(ScSpecTypeDef::Address),
1416 }));
1417 let mut value = serde_json::json!({"owner": TEST_G_ADDRESS});
1418 resolve_aliases_in_json(&mut value, &map_ty, &spec, &config).unwrap();
1419 assert_eq!(value, serde_json::json!({"owner": TEST_G_ADDRESS}));
1420
1421 let mut value = serde_json::json!({"owner": "bogus-alias"});
1422 let err = resolve_aliases_in_json(&mut value, &map_ty, &spec, &config).unwrap_err();
1423 assert!(
1424 matches!(err, Error::Config(_) | Error::ScAddress(_)),
1425 "expected alias-resolution error, got {err:?}"
1426 );
1427 }
1428
1429 #[test]
1430 fn resolve_aliases_in_json_walks_result_inner_types() {
1431 use stellar_xdr::ScSpecTypeResult;
1432
1433 let ty = ScSpecTypeDef::Result(Box::new(ScSpecTypeResult {
1434 ok_type: Box::new(ScSpecTypeDef::Address),
1435 error_type: Box::new(ScSpecTypeDef::U32),
1436 }));
1437 let spec = Spec(Some(vec![]));
1438 let config = crate::config::Args::default();
1439
1440 let mut value = serde_json::json!(TEST_G_ADDRESS);
1441 resolve_aliases_in_json(&mut value, &ty, &spec, &config).unwrap();
1442 assert_eq!(value, serde_json::json!(TEST_G_ADDRESS));
1443
1444 let mut value = serde_json::json!("bogus-alias");
1445 let err = resolve_aliases_in_json(&mut value, &ty, &spec, &config).unwrap_err();
1446 assert!(
1447 matches!(err, Error::Config(_) | Error::ScAddress(_)),
1448 "expected alias-resolution error, got {err:?}"
1449 );
1450 }
1451
1452 #[test]
1453 fn resolve_aliases_preserves_input_when_nothing_mutated() {
1454 use stellar_xdr::ScSpecTypeVec;
1455
1456 let (spec, ty) = struct_spec(
1459 "Point",
1460 &[("x", ScSpecTypeDef::U32), ("y", ScSpecTypeDef::U32)],
1461 );
1462 let config = crate::config::Args::default();
1463 let pretty = r#"{ "x": 1, "y": 2 }"#;
1464 assert_eq!(
1465 resolve_aliases(pretty, &ty, &spec, &config).unwrap(),
1466 pretty
1467 );
1468
1469 let ty = ScSpecTypeDef::Vec(Box::new(ScSpecTypeVec {
1471 element_type: Box::new(ScSpecTypeDef::Address),
1472 }));
1473 let spec = Spec(Some(vec![]));
1474 let pretty = format!(r#"[ "{TEST_G_ADDRESS}" ]"#);
1475 assert_eq!(
1476 resolve_aliases(&pretty, &ty, &spec, &config).unwrap(),
1477 pretty
1478 );
1479 }
1480
1481 #[test]
1482 fn resolve_aliases_in_json_walks_map_keys() {
1483 use stellar_xdr::ScSpecTypeMap;
1484
1485 let map_ty = ScSpecTypeDef::Map(Box::new(ScSpecTypeMap {
1486 key_type: Box::new(ScSpecTypeDef::Address),
1487 value_type: Box::new(ScSpecTypeDef::U32),
1488 }));
1489 let spec = Spec(Some(vec![]));
1490 let config = crate::config::Args::default();
1491
1492 let mut value = serde_json::json!({ TEST_G_ADDRESS: 1 });
1493 resolve_aliases_in_json(&mut value, &map_ty, &spec, &config).unwrap();
1494 assert_eq!(value, serde_json::json!({ TEST_G_ADDRESS: 1 }));
1495
1496 let mut value = serde_json::json!({ "bogus-alias": 1 });
1497 let err = resolve_aliases_in_json(&mut value, &map_ty, &spec, &config).unwrap_err();
1498 assert!(
1499 matches!(err, Error::Config(_) | Error::ScAddress(_)),
1500 "expected alias-resolution error, got {err:?}"
1501 );
1502 }
1503
1504 #[test]
1506 fn invoke_help_strips_control_characters() {
1507 let path = concat!(
1508 env!("CARGO_MANIFEST_DIR"),
1509 "/../crates/soroban-spec-tools/tests/fixtures/control_characters.wasm"
1510 );
1511 let bytes = std::fs::read(path).expect("fixture wasm should be readable");
1512 let spec = Spec::from_wasm(&bytes).expect("wasm should parse without error");
1513 let mut cmd = build_clap_command(&spec, true).expect("command should build without error");
1514 let help = cmd.render_long_help().to_string();
1515
1516 let bad_chars: Vec<char> = help
1517 .chars()
1518 .filter(|c| c.is_control() && *c != '\n' && *c != '\t')
1519 .collect();
1520 assert!(
1521 bad_chars.is_empty(),
1522 "invoke help contains unexpected control characters {bad_chars:?}:\n{help:?}"
1523 );
1524 }
1525}