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("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.iter() {
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::curr::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(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::curr::ScAddress::MuxedAccount(account) => account.to_string(),
455 stellar_xdr::curr::ScAddress::ClaimableBalance(_)
456 | stellar_xdr::curr::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 secret = config.locator.get_secret_key(addr_or_alias).ok()?;
469 let print = Print::new(false);
470 let signer = secret.signer(config.hd_path(), print).ok()?;
471 Some(signer)
472}
473
474fn validate_json_arg(arg_name: &str, value: &str) -> Result<(), Error> {
476 if let Err(json_err) = serde_json::from_str::<serde_json::Value>(value) {
478 return Err(Error::InvalidJsonArg {
479 arg: arg_name.to_string(),
480 json_error: json_err.to_string(),
481 received_value: value.to_string(),
482 });
483 }
484 Ok(())
485}
486
487fn get_type_name(type_def: &ScSpecTypeDef) -> String {
489 match type_def {
490 ScSpecTypeDef::Val => "any value".to_string(),
491 ScSpecTypeDef::U64 => "u64 (unsigned 64-bit integer)".to_string(),
492 ScSpecTypeDef::I64 => "i64 (signed 64-bit integer)".to_string(),
493 ScSpecTypeDef::U128 => "u128 (unsigned 128-bit integer)".to_string(),
494 ScSpecTypeDef::I128 => "i128 (signed 128-bit integer)".to_string(),
495 ScSpecTypeDef::U32 => "u32 (unsigned 32-bit integer)".to_string(),
496 ScSpecTypeDef::I32 => "i32 (signed 32-bit integer)".to_string(),
497 ScSpecTypeDef::U256 => "u256 (unsigned 256-bit integer)".to_string(),
498 ScSpecTypeDef::I256 => "i256 (signed 256-bit integer)".to_string(),
499 ScSpecTypeDef::Bool => "bool (true/false)".to_string(),
500 ScSpecTypeDef::Symbol => "symbol (identifier)".to_string(),
501 ScSpecTypeDef::String => "string".to_string(),
502 ScSpecTypeDef::Bytes => "bytes (raw binary data)".to_string(),
503 ScSpecTypeDef::BytesN(n) => format!("bytes{} (exactly {} bytes)", n.n, n.n),
504 ScSpecTypeDef::Address => {
505 "address (G... for account, C... for contract, or identity name)".to_string()
506 }
507 ScSpecTypeDef::MuxedAddress => "muxed address (M... or identity name)".to_string(),
508 ScSpecTypeDef::Void => "void (no value)".to_string(),
509 ScSpecTypeDef::Error => "error".to_string(),
510 ScSpecTypeDef::Timepoint => "timepoint (timestamp)".to_string(),
511 ScSpecTypeDef::Duration => "duration (time span)".to_string(),
512 ScSpecTypeDef::Option(inner) => format!("optional {}", get_type_name(&inner.value_type)),
513 ScSpecTypeDef::Vec(inner) => format!("vector of {}", get_type_name(&inner.element_type)),
514 ScSpecTypeDef::Map(map_type) => format!(
515 "map from {} to {}",
516 get_type_name(&map_type.key_type),
517 get_type_name(&map_type.value_type)
518 ),
519 ScSpecTypeDef::Tuple(tuple_type) => {
520 let types: Vec<String> = tuple_type.value_types.iter().map(get_type_name).collect();
521 format!("tuple({})", types.join(", "))
522 }
523 ScSpecTypeDef::Result(_) => "result".to_string(),
524 ScSpecTypeDef::Udt(udt) => {
525 format!(
526 "user-defined type '{}'",
527 sanitize(&udt.name.to_utf8_string_lossy())
528 )
529 }
530 }
531}
532
533fn get_available_functions(spec: &Spec) -> String {
535 match spec.find_functions() {
536 Ok(functions) => functions
537 .map(|f| sanitize(&f.name.to_utf8_string_lossy()))
538 .collect::<Vec<_>>()
539 .join(", "),
540 Err(_) => "unknown".to_string(),
541 }
542}
543
544fn is_primitive_type(type_def: &ScSpecTypeDef) -> bool {
546 matches!(
547 type_def,
548 ScSpecTypeDef::U32
549 | ScSpecTypeDef::U64
550 | ScSpecTypeDef::U128
551 | ScSpecTypeDef::U256
552 | ScSpecTypeDef::I32
553 | ScSpecTypeDef::I64
554 | ScSpecTypeDef::I128
555 | ScSpecTypeDef::I256
556 | ScSpecTypeDef::Bool
557 | ScSpecTypeDef::Symbol
558 | ScSpecTypeDef::String
559 | ScSpecTypeDef::Bytes
560 | ScSpecTypeDef::BytesN(_)
561 | ScSpecTypeDef::Address
562 | ScSpecTypeDef::MuxedAddress
563 | ScSpecTypeDef::Timepoint
564 | ScSpecTypeDef::Duration
565 | ScSpecTypeDef::Void
566 )
567}
568
569fn get_context_suggestions(expected_type: &ScSpecTypeDef, received_value: &str) -> String {
571 match expected_type {
572 ScSpecTypeDef::U64 | ScSpecTypeDef::I64 | ScSpecTypeDef::U128 | ScSpecTypeDef::I128
573 | ScSpecTypeDef::U32 | ScSpecTypeDef::I32 | ScSpecTypeDef::U256 | ScSpecTypeDef::I256 => {
574 if received_value.starts_with('"') && received_value.ends_with('"') {
575 "For numbers, ensure no quotes around the value (e.g., use 100 instead of \"100\")".to_string()
576 } else if received_value.contains('.') {
577 "Integer types don't support decimal values - use a whole number".to_string()
578 } else {
579 "Ensure the value is a valid integer within the type's range".to_string()
580 }
581 }
582 ScSpecTypeDef::Bool => {
583 "For booleans, use 'true' or 'false' (without quotes)".to_string()
584 }
585 ScSpecTypeDef::String => {
586 if !received_value.starts_with('"') || !received_value.ends_with('"') {
587 "For strings, ensure the value is properly quoted (e.g., \"hello world\")".to_string()
588 } else {
589 "Check for proper string escaping if the string contains special characters".to_string()
590 }
591 }
592 ScSpecTypeDef::Address => {
593 "For addresses, use format: G... (account), C... (contract), or identity name (e.g., alice)".to_string()
594 }
595 ScSpecTypeDef::MuxedAddress => {
596 "For muxed addresses, use format: M... or identity name".to_string()
597 }
598 ScSpecTypeDef::Vec(_) => {
599 "For arrays, use JSON array format: [\"item1\", \"item2\"] or [{\"key\": \"value\"}]".to_string()
600 }
601 ScSpecTypeDef::Map(_) => {
602 "For maps, use JSON object format: {\"key1\": \"value1\", \"key2\": \"value2\"}".to_string()
603 }
604 ScSpecTypeDef::Option(_) => {
605 "For optional values, use null for none or the expected value type".to_string()
606 }
607 _ => {
608 "Check the contract specification for the correct argument format and type".to_string()
609 }
610 }
611}
612
613fn parse_argument_with_validation(
615 arg_name: &str,
616 value: &str,
617 expected_type: &ScSpecTypeDef,
618 spec: &Spec,
619 config: &config::Args,
620) -> Result<ScVal, Error> {
621 let expected_type_name = get_type_name(expected_type);
622
623 let is_union_udt = if let ScSpecTypeDef::Udt(udt) = expected_type {
627 spec.find(&udt.name.to_utf8_string_lossy())
628 .is_ok_and(|entry| matches!(entry, ScSpecEntry::UdtUnionV0(_)))
629 } else {
630 false
631 };
632 if !is_primitive_type(expected_type) && !is_union_udt {
633 validate_json_arg(arg_name, value)?;
634 }
635
636 let resolved = resolve_aliases(value, expected_type, spec, config)?;
639
640 spec.from_string(&resolved, expected_type)
641 .map_err(|error| Error::CannotParseArg {
642 arg: arg_name.to_string(),
643 error,
644 expected_type: expected_type_name,
645 received_value: value.to_string(),
646 suggestion: get_context_suggestions(expected_type, value),
647 })
648}
649
650fn resolve_aliases(
655 value: &str,
656 type_def: &ScSpecTypeDef,
657 spec: &Spec,
658 config: &config::Args,
659) -> Result<String, Error> {
660 let is_address = matches!(
661 type_def,
662 ScSpecTypeDef::Address | ScSpecTypeDef::MuxedAddress
663 );
664
665 let mut json = match serde_json::from_str::<serde_json::Value>(value) {
666 Ok(j) => j,
667 Err(_) if is_address => serde_json::Value::String(value.trim_matches('"').to_string()),
668 Err(_) => return Ok(value.to_string()),
669 };
670
671 let mutated = resolve_aliases_in_json(&mut json, type_def, spec, config)?;
672
673 if !mutated {
676 return Ok(value.to_string());
677 }
678
679 Ok(match (&json, is_address) {
683 (serde_json::Value::String(s), true) => s.clone(),
684 _ => json.to_string(),
685 })
686}
687
688fn resolve_aliases_in_json(
697 value: &mut serde_json::Value,
698 type_def: &ScSpecTypeDef,
699 spec: &Spec,
700 config: &config::Args,
701) -> Result<bool, Error> {
702 let mut mutated = false;
703 match type_def {
704 ScSpecTypeDef::Address | ScSpecTypeDef::MuxedAddress => {
705 if let serde_json::Value::String(s) = value {
706 let resolved = resolve_address(s, config)?;
707 if &resolved != s {
708 *s = resolved;
709 mutated = true;
710 }
711 }
712 }
713 ScSpecTypeDef::Vec(inner) => {
714 if let serde_json::Value::Array(arr) = value {
715 for item in arr.iter_mut() {
716 mutated |= resolve_aliases_in_json(item, &inner.element_type, spec, config)?;
717 }
718 }
719 }
720 ScSpecTypeDef::Tuple(tuple) => {
721 if let serde_json::Value::Array(arr) = value {
722 for (item, ty) in arr.iter_mut().zip(tuple.value_types.iter()) {
723 mutated |= resolve_aliases_in_json(item, ty, spec, config)?;
724 }
725 }
726 }
727 ScSpecTypeDef::Map(map) => {
728 if let serde_json::Value::Object(obj) = value {
729 let key_is_address = matches!(
730 map.key_type.as_ref(),
731 ScSpecTypeDef::Address | ScSpecTypeDef::MuxedAddress
732 );
733 if key_is_address {
734 let entries = std::mem::take(obj);
735 for (k, mut v) in entries {
736 mutated |= resolve_aliases_in_json(&mut v, &map.value_type, spec, config)?;
737 let resolved = resolve_address(&k, config)?;
738 if resolved != k {
739 mutated = true;
740 }
741 if obj.contains_key(&resolved) {
742 return Err(Error::DuplicateMapKey { key: resolved });
743 }
744 obj.insert(resolved, v);
745 }
746 } else {
747 for v in obj.values_mut() {
748 mutated |= resolve_aliases_in_json(v, &map.value_type, spec, config)?;
749 }
750 }
751 }
752 }
753 ScSpecTypeDef::Option(inner) if !matches!(value, serde_json::Value::Null) => {
754 mutated |= resolve_aliases_in_json(value, &inner.value_type, spec, config)?;
755 }
756 ScSpecTypeDef::Result(result) => {
757 mutated |= resolve_aliases_in_json(value, &result.ok_type, spec, config)?;
763 mutated |= resolve_aliases_in_json(value, &result.error_type, spec, config)?;
764 }
765 ScSpecTypeDef::Udt(udt) => {
766 mutated |= resolve_aliases_in_udt(value, udt, spec, config)?;
767 }
768 _ => {}
769 }
770 Ok(mutated)
771}
772
773fn resolve_aliases_in_udt(
774 value: &mut serde_json::Value,
775 udt: &stellar_xdr::curr::ScSpecTypeUdt,
776 spec: &Spec,
777 config: &config::Args,
778) -> Result<bool, Error> {
779 let mut mutated = false;
780 let name = udt.name.to_utf8_string_lossy();
781 let Ok(entry) = spec.find(&name) else {
782 return Ok(false);
783 };
784 match entry {
785 ScSpecEntry::UdtStructV0(strukt) => {
786 let is_tuple_struct = strukt
790 .fields
791 .iter()
792 .any(|f| f.name.to_utf8_string_lossy() == "0");
793 match value {
794 serde_json::Value::Array(arr) if is_tuple_struct => {
795 for (item, field) in arr.iter_mut().zip(strukt.fields.iter()) {
796 mutated |= resolve_aliases_in_json(item, &field.type_, spec, config)?;
797 }
798 }
799 serde_json::Value::Object(obj) => {
800 for field in strukt.fields.iter() {
801 let key = field.name.to_utf8_string_lossy();
802 if let Some(field_val) = obj.get_mut(key.as_str()) {
803 mutated |=
804 resolve_aliases_in_json(field_val, &field.type_, spec, config)?;
805 }
806 }
807 }
808 _ => {}
809 }
810 }
811 ScSpecEntry::UdtUnionV0(union) => {
812 mutated |= resolve_aliases_in_union(value, union, spec, config)?;
813 }
814 _ => {}
815 }
816 Ok(mutated)
817}
818
819fn resolve_aliases_in_union(
820 value: &mut serde_json::Value,
821 union: &stellar_xdr::curr::ScSpecUdtUnionV0,
822 spec: &Spec,
823 config: &config::Args,
824) -> Result<bool, Error> {
825 use stellar_xdr::curr::ScSpecUdtUnionCaseV0;
826
827 let serde_json::Value::Object(obj) = value else {
828 return Ok(false);
829 };
830 let Some((case_name, payload)) = obj.iter_mut().next() else {
831 return Ok(false);
832 };
833 let matched = union.cases.iter().find_map(|c| match c {
834 ScSpecUdtUnionCaseV0::TupleV0(t) if t.name.to_utf8_string_lossy() == *case_name => Some(t),
835 _ => None,
836 });
837 let Some(tuple) = matched else {
838 return Ok(false);
839 };
840 if tuple.type_.len() == 1 {
844 return resolve_aliases_in_json(payload, &tuple.type_[0], spec, config);
845 }
846 let mut mutated = false;
847 if let serde_json::Value::Array(arr) = payload {
848 for (item, ty) in arr.iter_mut().zip(tuple.type_.iter()) {
849 mutated |= resolve_aliases_in_json(item, ty, spec, config)?;
850 }
851 }
852 Ok(mutated)
853}
854
855#[cfg(test)]
856mod tests {
857 use super::*;
858 use stellar_xdr::curr::{ScSpecTypeBytesN, ScSpecTypeDef, ScSpecTypeOption, ScSpecTypeVec};
859
860 #[test]
861 fn test_get_type_name_primitives() {
862 assert_eq!(
863 get_type_name(&ScSpecTypeDef::U32),
864 "u32 (unsigned 32-bit integer)"
865 );
866 assert_eq!(
867 get_type_name(&ScSpecTypeDef::I64),
868 "i64 (signed 64-bit integer)"
869 );
870 assert_eq!(get_type_name(&ScSpecTypeDef::Bool), "bool (true/false)");
871 assert_eq!(get_type_name(&ScSpecTypeDef::String), "string");
872 assert_eq!(
873 get_type_name(&ScSpecTypeDef::Address),
874 "address (G... for account, C... for contract, or identity name)"
875 );
876 }
877
878 #[test]
879 fn test_get_type_name_complex() {
880 let option_type = ScSpecTypeDef::Option(Box::new(ScSpecTypeOption {
881 value_type: Box::new(ScSpecTypeDef::U32),
882 }));
883 assert_eq!(
884 get_type_name(&option_type),
885 "optional u32 (unsigned 32-bit integer)"
886 );
887
888 let vec_type = ScSpecTypeDef::Vec(Box::new(ScSpecTypeVec {
889 element_type: Box::new(ScSpecTypeDef::String),
890 }));
891 assert_eq!(get_type_name(&vec_type), "vector of string");
892 }
893
894 #[test]
895 fn test_is_primitive_type_all_primitives() {
896 assert!(is_primitive_type(&ScSpecTypeDef::U32));
897 assert!(is_primitive_type(&ScSpecTypeDef::I32));
898 assert!(is_primitive_type(&ScSpecTypeDef::U64));
899 assert!(is_primitive_type(&ScSpecTypeDef::I64));
900 assert!(is_primitive_type(&ScSpecTypeDef::U128));
901 assert!(is_primitive_type(&ScSpecTypeDef::I128));
902 assert!(is_primitive_type(&ScSpecTypeDef::U256));
903 assert!(is_primitive_type(&ScSpecTypeDef::I256));
904
905 assert!(is_primitive_type(&ScSpecTypeDef::Bool));
906 assert!(is_primitive_type(&ScSpecTypeDef::Symbol));
907 assert!(is_primitive_type(&ScSpecTypeDef::String));
908 assert!(is_primitive_type(&ScSpecTypeDef::Void));
909 assert!(is_primitive_type(&ScSpecTypeDef::Bytes));
910 assert!(is_primitive_type(&ScSpecTypeDef::BytesN(
911 ScSpecTypeBytesN { n: 32 }
912 )));
913 assert!(is_primitive_type(&ScSpecTypeDef::BytesN(
914 ScSpecTypeBytesN { n: 64 }
915 )));
916
917 assert!(is_primitive_type(&ScSpecTypeDef::Address));
918 assert!(is_primitive_type(&ScSpecTypeDef::MuxedAddress));
919 assert!(is_primitive_type(&ScSpecTypeDef::Timepoint));
920 assert!(is_primitive_type(&ScSpecTypeDef::Duration));
921
922 assert!(!is_primitive_type(&ScSpecTypeDef::Vec(Box::new(
923 ScSpecTypeVec {
924 element_type: Box::new(ScSpecTypeDef::U32),
925 }
926 ))));
927 }
928
929 #[test]
930 fn test_validate_json_arg_valid() {
931 assert!(validate_json_arg("test_arg", r#"{"key": "value"}"#).is_ok());
933 assert!(validate_json_arg("test_arg", "123").is_ok());
934 assert!(validate_json_arg("test_arg", r#""string""#).is_ok());
935 assert!(validate_json_arg("test_arg", "true").is_ok());
936 assert!(validate_json_arg("test_arg", "null").is_ok());
937 }
938
939 #[test]
940 fn test_validate_json_arg_invalid() {
941 let result = validate_json_arg("test_arg", r#"{"key": value}"#); assert!(result.is_err());
944
945 if let Err(Error::InvalidJsonArg {
946 arg,
947 json_error,
948 received_value,
949 }) = result
950 {
951 assert_eq!(arg, "test_arg");
952 assert_eq!(received_value, r#"{"key": value}"#);
953 assert!(json_error.contains("expected"));
954 } else {
955 panic!("Expected InvalidJsonArg error");
956 }
957 }
958
959 #[test]
960 fn test_validate_json_arg_malformed() {
961 let test_cases = vec![
963 r#"{"key": }"#, r#"{key: "value"}"#, r#"{"key": "value",}"#, r#"{"key" "value"}"#, ];
968
969 for case in test_cases {
970 let result = validate_json_arg("test_arg", case);
971 assert!(result.is_err(), "Expected error for case: {case}");
972 }
973 }
974
975 #[test]
976 fn test_context_aware_error_messages() {
977 use stellar_xdr::curr::ScSpecTypeDef;
978
979 let suggestion = get_context_suggestions(&ScSpecTypeDef::U64, "\"100\"");
983 assert!(suggestion.contains("no quotes around the value"));
984 assert!(suggestion.contains("use 100 instead of \"100\""));
985
986 let suggestion = get_context_suggestions(&ScSpecTypeDef::U64, "100.5");
988 assert!(suggestion.contains("don't support decimal values"));
989
990 let suggestion = get_context_suggestions(&ScSpecTypeDef::String, "hello");
992 assert!(suggestion.contains("properly quoted"));
993
994 let suggestion = get_context_suggestions(&ScSpecTypeDef::Address, "invalid_addr");
996 assert!(suggestion.contains("G... (account), C... (contract)"));
997
998 let suggestion = get_context_suggestions(&ScSpecTypeDef::Bool, "yes");
1000 assert!(suggestion.contains("'true' or 'false'"));
1001
1002 println!("=== Context-Aware Error Message Examples ===");
1003 println!("U64 with quotes: {suggestion}");
1004
1005 let decimal_suggestion = get_context_suggestions(&ScSpecTypeDef::U64, "100.5");
1006 println!("U64 with decimal: {decimal_suggestion}");
1007
1008 let string_suggestion = get_context_suggestions(&ScSpecTypeDef::String, "hello");
1009 println!("String without quotes: {string_suggestion}");
1010
1011 let address_suggestion = get_context_suggestions(&ScSpecTypeDef::Address, "invalid");
1012 println!("Invalid address: {address_suggestion}");
1013 }
1014
1015 #[test]
1016 fn test_union_udt_bare_string_accepted() {
1017 use stellar_xdr::curr::{
1018 ScSpecEntry, ScSpecTypeDef, ScSpecTypeUdt, ScSpecUdtUnionCaseV0,
1019 ScSpecUdtUnionCaseVoidV0, ScSpecUdtUnionV0, StringM,
1020 };
1021
1022 let union_name: StringM<60> = "MyEnum".try_into().unwrap();
1024 let case_name: StringM<60> = "Unit".try_into().unwrap();
1025 let spec = Spec(Some(vec![ScSpecEntry::UdtUnionV0(ScSpecUdtUnionV0 {
1026 doc: StringM::default(),
1027 lib: StringM::default(),
1028 name: union_name.clone(),
1029 cases: vec![ScSpecUdtUnionCaseV0::VoidV0(ScSpecUdtUnionCaseVoidV0 {
1030 doc: StringM::default(),
1031 name: case_name,
1032 })]
1033 .try_into()
1034 .unwrap(),
1035 })]));
1036
1037 let expected_type = ScSpecTypeDef::Udt(ScSpecTypeUdt { name: union_name });
1038 let config = crate::config::Args::default();
1039
1040 let result =
1042 parse_argument_with_validation("value", "Unit", &expected_type, &spec, &config);
1043 assert!(result.is_ok(), "bare 'Unit' should be accepted: {result:?}");
1044
1045 let result =
1047 parse_argument_with_validation("value", "\"Unit\"", &expected_type, &spec, &config);
1048 assert!(
1049 result.is_ok(),
1050 "JSON-quoted '\"Unit\"' should be accepted: {result:?}"
1051 );
1052
1053 let bare = parse_argument_with_validation("value", "Unit", &expected_type, &spec, &config)
1055 .unwrap();
1056 let quoted =
1057 parse_argument_with_validation("value", "\"Unit\"", &expected_type, &spec, &config)
1058 .unwrap();
1059 assert_eq!(
1060 bare, quoted,
1061 "bare and quoted forms should produce identical ScVal"
1062 );
1063 }
1064
1065 #[test]
1066 fn test_union_udt_tuple_variant_still_requires_json() {
1067 use stellar_xdr::curr::{
1068 ScSpecEntry, ScSpecTypeDef, ScSpecTypeUdt, ScSpecUdtUnionCaseTupleV0,
1069 ScSpecUdtUnionCaseV0, ScSpecUdtUnionCaseVoidV0, ScSpecUdtUnionV0, StringM,
1070 };
1071
1072 let union_name: StringM<60> = "MyEnum".try_into().unwrap();
1073 let spec = Spec(Some(vec![ScSpecEntry::UdtUnionV0(ScSpecUdtUnionV0 {
1074 doc: StringM::default(),
1075 lib: StringM::default(),
1076 name: union_name.clone(),
1077 cases: vec![
1078 ScSpecUdtUnionCaseV0::VoidV0(ScSpecUdtUnionCaseVoidV0 {
1079 doc: StringM::default(),
1080 name: "Unit".try_into().unwrap(),
1081 }),
1082 ScSpecUdtUnionCaseV0::TupleV0(ScSpecUdtUnionCaseTupleV0 {
1083 doc: StringM::default(),
1084 name: "WithValue".try_into().unwrap(),
1085 type_: vec![ScSpecTypeDef::U32].try_into().unwrap(),
1086 }),
1087 ]
1088 .try_into()
1089 .unwrap(),
1090 })]));
1091
1092 let expected_type = ScSpecTypeDef::Udt(ScSpecTypeUdt { name: union_name });
1093 let config = crate::config::Args::default();
1094
1095 let result = parse_argument_with_validation(
1097 "value",
1098 r#"{"WithValue":42}"#,
1099 &expected_type,
1100 &spec,
1101 &config,
1102 );
1103 assert!(
1104 result.is_ok(),
1105 "JSON object for tuple variant should be accepted: {result:?}"
1106 );
1107 }
1108
1109 #[test]
1110 fn test_error_message_format() {
1111 use stellar_xdr::curr::ScSpecTypeDef;
1112
1113 let error = Error::CannotParseArg {
1115 arg: "amount".to_string(),
1116 error: soroban_spec_tools::Error::InvalidValue(Some(ScSpecTypeDef::U64)),
1117 expected_type: "u64 (unsigned 64-bit integer)".to_string(),
1118 received_value: "\"100\"".to_string(),
1119 suggestion:
1120 "For numbers, ensure no quotes around the value (e.g., use 100 instead of \"100\")"
1121 .to_string(),
1122 };
1123
1124 let error_message = format!("{error}");
1125 println!("\n=== Complete Error Message Example ===");
1126 println!("{error_message}");
1127
1128 assert!(error_message.contains("Failed to parse argument 'amount'"));
1130 assert!(error_message.contains("Expected type u64 (unsigned 64-bit integer)"));
1131 assert!(error_message.contains("received: '\"100\"'"));
1132 assert!(error_message.contains("Suggestion: For numbers, ensure no quotes"));
1133 }
1134
1135 fn struct_spec(name: &'static str, fields: &[(&str, ScSpecTypeDef)]) -> (Spec, ScSpecTypeDef) {
1136 use stellar_xdr::curr::{
1137 ScSpecEntry, ScSpecTypeUdt, ScSpecUdtStructFieldV0, ScSpecUdtStructV0, StringM,
1138 };
1139 let struct_name: StringM<60> = name.try_into().unwrap();
1140 let fields_xdr: Vec<ScSpecUdtStructFieldV0> = fields
1141 .iter()
1142 .map(|(n, t)| ScSpecUdtStructFieldV0 {
1143 doc: StringM::default(),
1144 name: (*n).try_into().unwrap(),
1145 type_: t.clone(),
1146 })
1147 .collect();
1148 let spec = Spec(Some(vec![ScSpecEntry::UdtStructV0(ScSpecUdtStructV0 {
1149 doc: StringM::default(),
1150 lib: StringM::default(),
1151 name: struct_name.clone(),
1152 fields: fields_xdr.try_into().unwrap(),
1153 })]));
1154 let ty = ScSpecTypeDef::Udt(ScSpecTypeUdt { name: struct_name });
1155 (spec, ty)
1156 }
1157
1158 const TEST_G_ADDRESS: &str = "GD5KD2KEZJIGTC63IGW6UMUSMVUVG5IHG64HUTFWCHVZH2N2IBOQN7PS";
1160
1161 #[test]
1162 fn resolve_aliases_in_json_walks_vec_of_address() {
1163 use stellar_xdr::curr::ScSpecTypeVec;
1164
1165 let ty = ScSpecTypeDef::Vec(Box::new(ScSpecTypeVec {
1166 element_type: Box::new(ScSpecTypeDef::Address),
1167 }));
1168 let spec = Spec(Some(vec![]));
1169 let config = crate::config::Args::default();
1170
1171 let mut value = serde_json::json!([TEST_G_ADDRESS]);
1172 resolve_aliases_in_json(&mut value, &ty, &spec, &config).unwrap();
1173 assert_eq!(value, serde_json::json!([TEST_G_ADDRESS]));
1174
1175 let mut value = serde_json::json!(["definitely-not-a-known-alias"]);
1177 let err = resolve_aliases_in_json(&mut value, &ty, &spec, &config).unwrap_err();
1178 assert!(
1179 matches!(err, Error::Config(_) | Error::ScAddress(_)),
1180 "expected alias-resolution error, got {err:?}"
1181 );
1182 }
1183
1184 #[test]
1185 fn resolve_aliases_in_json_walks_tuple() {
1186 use stellar_xdr::curr::ScSpecTypeTuple;
1187
1188 let ty = ScSpecTypeDef::Tuple(Box::new(ScSpecTypeTuple {
1189 value_types: vec![ScSpecTypeDef::Address, ScSpecTypeDef::U32]
1190 .try_into()
1191 .unwrap(),
1192 }));
1193 let spec = Spec(Some(vec![]));
1194 let config = crate::config::Args::default();
1195
1196 let mut value = serde_json::json!([TEST_G_ADDRESS, 42]);
1197 resolve_aliases_in_json(&mut value, &ty, &spec, &config).unwrap();
1198 assert_eq!(value, serde_json::json!([TEST_G_ADDRESS, 42]));
1199
1200 let mut value = serde_json::json!(["bogus-alias", 42]);
1201 let err = resolve_aliases_in_json(&mut value, &ty, &spec, &config).unwrap_err();
1202 assert!(
1203 matches!(err, Error::Config(_) | Error::ScAddress(_)),
1204 "expected alias-resolution error, got {err:?}"
1205 );
1206 }
1207
1208 #[test]
1209 fn resolve_aliases_in_json_walks_struct_field() {
1210 use stellar_xdr::curr::ScSpecTypeVec;
1211
1212 let (spec, ty) = struct_spec(
1213 "Operator",
1214 &[
1215 ("count", ScSpecTypeDef::U32),
1216 (
1217 "addresses",
1218 ScSpecTypeDef::Vec(Box::new(ScSpecTypeVec {
1219 element_type: Box::new(ScSpecTypeDef::Address),
1220 })),
1221 ),
1222 ],
1223 );
1224 let config = crate::config::Args::default();
1225
1226 let mut value = serde_json::json!({"count": 1, "addresses": [TEST_G_ADDRESS]});
1227 resolve_aliases_in_json(&mut value, &ty, &spec, &config).unwrap();
1228 assert_eq!(
1229 value,
1230 serde_json::json!({"count": 1, "addresses": [TEST_G_ADDRESS]})
1231 );
1232
1233 let mut value = serde_json::json!({"count": 1, "addresses": ["bogus-alias"]});
1235 let err = resolve_aliases_in_json(&mut value, &ty, &spec, &config).unwrap_err();
1236 assert!(
1237 matches!(err, Error::Config(_) | Error::ScAddress(_)),
1238 "expected alias-resolution error, got {err:?}"
1239 );
1240 }
1241
1242 #[test]
1243 fn resolve_aliases_in_json_walks_union_tuple_variant() {
1244 use stellar_xdr::curr::{
1245 ScSpecEntry, ScSpecTypeUdt, ScSpecUdtUnionCaseTupleV0, ScSpecUdtUnionCaseV0,
1246 ScSpecUdtUnionV0, StringM,
1247 };
1248
1249 let union_name: StringM<60> = "Choice".try_into().unwrap();
1250 let spec = Spec(Some(vec![ScSpecEntry::UdtUnionV0(ScSpecUdtUnionV0 {
1251 doc: StringM::default(),
1252 lib: StringM::default(),
1253 name: union_name.clone(),
1254 cases: vec![ScSpecUdtUnionCaseV0::TupleV0(ScSpecUdtUnionCaseTupleV0 {
1255 doc: StringM::default(),
1256 name: "Pick".try_into().unwrap(),
1257 type_: vec![ScSpecTypeDef::Address, ScSpecTypeDef::U32]
1258 .try_into()
1259 .unwrap(),
1260 })]
1261 .try_into()
1262 .unwrap(),
1263 })]));
1264
1265 let ty = ScSpecTypeDef::Udt(ScSpecTypeUdt { name: union_name });
1266 let config = crate::config::Args::default();
1267
1268 let mut value = serde_json::json!({"Pick": [TEST_G_ADDRESS, 42]});
1269 resolve_aliases_in_json(&mut value, &ty, &spec, &config).unwrap();
1270 assert_eq!(value, serde_json::json!({"Pick": [TEST_G_ADDRESS, 42]}));
1271
1272 let mut value = serde_json::json!({"Pick": ["bogus-alias", 42]});
1273 let err = resolve_aliases_in_json(&mut value, &ty, &spec, &config).unwrap_err();
1274 assert!(
1275 matches!(err, Error::Config(_) | Error::ScAddress(_)),
1276 "expected alias-resolution error, got {err:?}"
1277 );
1278 }
1279
1280 #[test]
1281 fn resolve_aliases_in_json_walks_single_element_union_variant() {
1282 use stellar_xdr::curr::{
1283 ScSpecEntry, ScSpecTypeUdt, ScSpecUdtUnionCaseTupleV0, ScSpecUdtUnionCaseV0,
1284 ScSpecUdtUnionV0, StringM,
1285 };
1286
1287 let union_name: StringM<60> = "OneOf".try_into().unwrap();
1288 let spec = Spec(Some(vec![ScSpecEntry::UdtUnionV0(ScSpecUdtUnionV0 {
1289 doc: StringM::default(),
1290 lib: StringM::default(),
1291 name: union_name.clone(),
1292 cases: vec![ScSpecUdtUnionCaseV0::TupleV0(ScSpecUdtUnionCaseTupleV0 {
1293 doc: StringM::default(),
1294 name: "Only".try_into().unwrap(),
1295 type_: vec![ScSpecTypeDef::Address].try_into().unwrap(),
1296 })]
1297 .try_into()
1298 .unwrap(),
1299 })]));
1300
1301 let ty = ScSpecTypeDef::Udt(ScSpecTypeUdt { name: union_name });
1302 let config = crate::config::Args::default();
1303
1304 let mut value = serde_json::json!({"Only": TEST_G_ADDRESS});
1306 resolve_aliases_in_json(&mut value, &ty, &spec, &config).unwrap();
1307 assert_eq!(value, serde_json::json!({"Only": TEST_G_ADDRESS}));
1308
1309 let mut value = serde_json::json!({"Only": "bogus-alias"});
1310 let err = resolve_aliases_in_json(&mut value, &ty, &spec, &config).unwrap_err();
1311 assert!(
1312 matches!(err, Error::Config(_) | Error::ScAddress(_)),
1313 "expected alias-resolution error, got {err:?}"
1314 );
1315 }
1316
1317 #[test]
1318 fn resolve_aliases_in_json_walks_option_and_map() {
1319 use stellar_xdr::curr::{ScSpecTypeMap, ScSpecTypeOption};
1320
1321 let opt_ty = ScSpecTypeDef::Option(Box::new(ScSpecTypeOption {
1322 value_type: Box::new(ScSpecTypeDef::Address),
1323 }));
1324 let spec = Spec(Some(vec![]));
1325 let config = crate::config::Args::default();
1326
1327 let mut value = serde_json::Value::Null;
1328 resolve_aliases_in_json(&mut value, &opt_ty, &spec, &config).unwrap();
1329 assert_eq!(value, serde_json::Value::Null);
1330
1331 let mut value = serde_json::json!(TEST_G_ADDRESS);
1332 resolve_aliases_in_json(&mut value, &opt_ty, &spec, &config).unwrap();
1333 assert_eq!(value, serde_json::json!(TEST_G_ADDRESS));
1334
1335 let map_ty = ScSpecTypeDef::Map(Box::new(ScSpecTypeMap {
1336 key_type: Box::new(ScSpecTypeDef::Symbol),
1337 value_type: Box::new(ScSpecTypeDef::Address),
1338 }));
1339 let mut value = serde_json::json!({"owner": TEST_G_ADDRESS});
1340 resolve_aliases_in_json(&mut value, &map_ty, &spec, &config).unwrap();
1341 assert_eq!(value, serde_json::json!({"owner": TEST_G_ADDRESS}));
1342
1343 let mut value = serde_json::json!({"owner": "bogus-alias"});
1344 let err = resolve_aliases_in_json(&mut value, &map_ty, &spec, &config).unwrap_err();
1345 assert!(
1346 matches!(err, Error::Config(_) | Error::ScAddress(_)),
1347 "expected alias-resolution error, got {err:?}"
1348 );
1349 }
1350
1351 #[test]
1352 fn resolve_aliases_in_json_walks_result_inner_types() {
1353 use stellar_xdr::curr::ScSpecTypeResult;
1354
1355 let ty = ScSpecTypeDef::Result(Box::new(ScSpecTypeResult {
1356 ok_type: Box::new(ScSpecTypeDef::Address),
1357 error_type: Box::new(ScSpecTypeDef::U32),
1358 }));
1359 let spec = Spec(Some(vec![]));
1360 let config = crate::config::Args::default();
1361
1362 let mut value = serde_json::json!(TEST_G_ADDRESS);
1363 resolve_aliases_in_json(&mut value, &ty, &spec, &config).unwrap();
1364 assert_eq!(value, serde_json::json!(TEST_G_ADDRESS));
1365
1366 let mut value = serde_json::json!("bogus-alias");
1367 let err = resolve_aliases_in_json(&mut value, &ty, &spec, &config).unwrap_err();
1368 assert!(
1369 matches!(err, Error::Config(_) | Error::ScAddress(_)),
1370 "expected alias-resolution error, got {err:?}"
1371 );
1372 }
1373
1374 #[test]
1375 fn resolve_aliases_preserves_input_when_nothing_mutated() {
1376 use stellar_xdr::curr::ScSpecTypeVec;
1377
1378 let (spec, ty) = struct_spec(
1381 "Point",
1382 &[("x", ScSpecTypeDef::U32), ("y", ScSpecTypeDef::U32)],
1383 );
1384 let config = crate::config::Args::default();
1385 let pretty = r#"{ "x": 1, "y": 2 }"#;
1386 assert_eq!(
1387 resolve_aliases(pretty, &ty, &spec, &config).unwrap(),
1388 pretty
1389 );
1390
1391 let ty = ScSpecTypeDef::Vec(Box::new(ScSpecTypeVec {
1393 element_type: Box::new(ScSpecTypeDef::Address),
1394 }));
1395 let spec = Spec(Some(vec![]));
1396 let pretty = format!(r#"[ "{TEST_G_ADDRESS}" ]"#);
1397 assert_eq!(
1398 resolve_aliases(&pretty, &ty, &spec, &config).unwrap(),
1399 pretty
1400 );
1401 }
1402
1403 #[test]
1404 fn resolve_aliases_in_json_walks_map_keys() {
1405 use stellar_xdr::curr::ScSpecTypeMap;
1406
1407 let map_ty = ScSpecTypeDef::Map(Box::new(ScSpecTypeMap {
1408 key_type: Box::new(ScSpecTypeDef::Address),
1409 value_type: Box::new(ScSpecTypeDef::U32),
1410 }));
1411 let spec = Spec(Some(vec![]));
1412 let config = crate::config::Args::default();
1413
1414 let mut value = serde_json::json!({ TEST_G_ADDRESS: 1 });
1415 resolve_aliases_in_json(&mut value, &map_ty, &spec, &config).unwrap();
1416 assert_eq!(value, serde_json::json!({ TEST_G_ADDRESS: 1 }));
1417
1418 let mut value = serde_json::json!({ "bogus-alias": 1 });
1419 let err = resolve_aliases_in_json(&mut value, &map_ty, &spec, &config).unwrap_err();
1420 assert!(
1421 matches!(err, Error::Config(_) | Error::ScAddress(_)),
1422 "expected alias-resolution error, got {err:?}"
1423 );
1424 }
1425
1426 #[test]
1428 fn invoke_help_strips_control_characters() {
1429 let path = concat!(
1430 env!("CARGO_MANIFEST_DIR"),
1431 "/../crates/soroban-spec-tools/tests/fixtures/control_characters.wasm"
1432 );
1433 let bytes = std::fs::read(path).expect("fixture wasm should be readable");
1434 let spec = Spec::from_wasm(&bytes).expect("wasm should parse without error");
1435 let mut cmd = build_clap_command(&spec, true).expect("command should build without error");
1436 let help = cmd.render_long_help().to_string();
1437
1438 let bad_chars: Vec<char> = help
1439 .chars()
1440 .filter(|c| c.is_control() && *c != '\n' && *c != '\t')
1441 .collect();
1442 assert!(
1443 bad_chars.is_empty(),
1444 "invoke help contains unexpected control characters {bad_chars:?}:\n{help:?}"
1445 );
1446 }
1447}