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(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]
934 fn test_validate_json_arg_valid() {
935 assert!(validate_json_arg("test_arg", r#"{"key": "value"}"#).is_ok());
937 assert!(validate_json_arg("test_arg", "123").is_ok());
938 assert!(validate_json_arg("test_arg", r#""string""#).is_ok());
939 assert!(validate_json_arg("test_arg", "true").is_ok());
940 assert!(validate_json_arg("test_arg", "null").is_ok());
941 }
942
943 #[test]
944 fn test_validate_json_arg_invalid() {
945 let result = validate_json_arg("test_arg", r#"{"key": value}"#); assert!(result.is_err());
948
949 if let Err(Error::InvalidJsonArg {
950 arg,
951 json_error,
952 received_value,
953 }) = result
954 {
955 assert_eq!(arg, "test_arg");
956 assert_eq!(received_value, r#"{"key": value}"#);
957 assert!(json_error.contains("expected"));
958 } else {
959 panic!("Expected InvalidJsonArg error");
960 }
961 }
962
963 #[test]
964 fn test_validate_json_arg_malformed() {
965 let test_cases = vec![
967 r#"{"key": }"#, r#"{key: "value"}"#, r#"{"key": "value",}"#, r#"{"key" "value"}"#, ];
972
973 for case in test_cases {
974 let result = validate_json_arg("test_arg", case);
975 assert!(result.is_err(), "Expected error for case: {case}");
976 }
977 }
978
979 #[test]
980 fn test_context_aware_error_messages() {
981 use stellar_xdr::ScSpecTypeDef;
982
983 let suggestion = get_context_suggestions(&ScSpecTypeDef::U64, "\"100\"");
987 assert!(suggestion.contains("no quotes around the value"));
988 assert!(suggestion.contains("use 100 instead of \"100\""));
989
990 let suggestion = get_context_suggestions(&ScSpecTypeDef::U64, "100.5");
992 assert!(suggestion.contains("don't support decimal values"));
993
994 let suggestion = get_context_suggestions(&ScSpecTypeDef::String, "hello");
996 assert!(suggestion.contains("properly quoted"));
997
998 let suggestion = get_context_suggestions(&ScSpecTypeDef::Address, "invalid_addr");
1000 assert!(suggestion.contains("G... (account), C... (contract)"));
1001
1002 let suggestion = get_context_suggestions(&ScSpecTypeDef::Bool, "yes");
1004 assert!(suggestion.contains("'true' or 'false'"));
1005
1006 println!("=== Context-Aware Error Message Examples ===");
1007 println!("U64 with quotes: {suggestion}");
1008
1009 let decimal_suggestion = get_context_suggestions(&ScSpecTypeDef::U64, "100.5");
1010 println!("U64 with decimal: {decimal_suggestion}");
1011
1012 let string_suggestion = get_context_suggestions(&ScSpecTypeDef::String, "hello");
1013 println!("String without quotes: {string_suggestion}");
1014
1015 let address_suggestion = get_context_suggestions(&ScSpecTypeDef::Address, "invalid");
1016 println!("Invalid address: {address_suggestion}");
1017 }
1018
1019 #[test]
1020 fn test_union_udt_bare_string_accepted() {
1021 use stellar_xdr::{
1022 ScSpecEntry, ScSpecTypeDef, ScSpecTypeUdt, ScSpecUdtUnionCaseV0,
1023 ScSpecUdtUnionCaseVoidV0, ScSpecUdtUnionV0, StringM,
1024 };
1025
1026 let union_name: StringM<60> = "MyEnum".try_into().unwrap();
1028 let case_name: StringM<60> = "Unit".try_into().unwrap();
1029 let spec = Spec(Some(vec![ScSpecEntry::UdtUnionV0(ScSpecUdtUnionV0 {
1030 doc: StringM::default(),
1031 lib: StringM::default(),
1032 name: union_name.clone(),
1033 cases: vec![ScSpecUdtUnionCaseV0::VoidV0(ScSpecUdtUnionCaseVoidV0 {
1034 doc: StringM::default(),
1035 name: case_name,
1036 })]
1037 .try_into()
1038 .unwrap(),
1039 })]));
1040
1041 let expected_type = ScSpecTypeDef::Udt(ScSpecTypeUdt { name: union_name });
1042 let config = crate::config::Args::default();
1043
1044 let result =
1046 parse_argument_with_validation("value", "Unit", &expected_type, &spec, &config);
1047 assert!(result.is_ok(), "bare 'Unit' should be accepted: {result:?}");
1048
1049 let result =
1051 parse_argument_with_validation("value", "\"Unit\"", &expected_type, &spec, &config);
1052 assert!(
1053 result.is_ok(),
1054 "JSON-quoted '\"Unit\"' should be accepted: {result:?}"
1055 );
1056
1057 let bare = parse_argument_with_validation("value", "Unit", &expected_type, &spec, &config)
1059 .unwrap();
1060 let quoted =
1061 parse_argument_with_validation("value", "\"Unit\"", &expected_type, &spec, &config)
1062 .unwrap();
1063 assert_eq!(
1064 bare, quoted,
1065 "bare and quoted forms should produce identical ScVal"
1066 );
1067 }
1068
1069 #[test]
1070 fn test_union_udt_tuple_variant_still_requires_json() {
1071 use stellar_xdr::{
1072 ScSpecEntry, ScSpecTypeDef, ScSpecTypeUdt, ScSpecUdtUnionCaseTupleV0,
1073 ScSpecUdtUnionCaseV0, ScSpecUdtUnionCaseVoidV0, ScSpecUdtUnionV0, StringM,
1074 };
1075
1076 let union_name: StringM<60> = "MyEnum".try_into().unwrap();
1077 let spec = Spec(Some(vec![ScSpecEntry::UdtUnionV0(ScSpecUdtUnionV0 {
1078 doc: StringM::default(),
1079 lib: StringM::default(),
1080 name: union_name.clone(),
1081 cases: vec![
1082 ScSpecUdtUnionCaseV0::VoidV0(ScSpecUdtUnionCaseVoidV0 {
1083 doc: StringM::default(),
1084 name: "Unit".try_into().unwrap(),
1085 }),
1086 ScSpecUdtUnionCaseV0::TupleV0(ScSpecUdtUnionCaseTupleV0 {
1087 doc: StringM::default(),
1088 name: "WithValue".try_into().unwrap(),
1089 type_: vec![ScSpecTypeDef::U32].try_into().unwrap(),
1090 }),
1091 ]
1092 .try_into()
1093 .unwrap(),
1094 })]));
1095
1096 let expected_type = ScSpecTypeDef::Udt(ScSpecTypeUdt { name: union_name });
1097 let config = crate::config::Args::default();
1098
1099 let result = parse_argument_with_validation(
1101 "value",
1102 r#"{"WithValue":42}"#,
1103 &expected_type,
1104 &spec,
1105 &config,
1106 );
1107 assert!(
1108 result.is_ok(),
1109 "JSON object for tuple variant should be accepted: {result:?}"
1110 );
1111 }
1112
1113 #[test]
1114 fn test_error_message_format() {
1115 use stellar_xdr::ScSpecTypeDef;
1116
1117 let error = Error::CannotParseArg {
1119 arg: "amount".to_string(),
1120 error: soroban_spec_tools::Error::InvalidValue(Some(ScSpecTypeDef::U64)),
1121 expected_type: "u64 (unsigned 64-bit integer)".to_string(),
1122 received_value: "\"100\"".to_string(),
1123 suggestion:
1124 "For numbers, ensure no quotes around the value (e.g., use 100 instead of \"100\")"
1125 .to_string(),
1126 };
1127
1128 let error_message = format!("{error}");
1129 println!("\n=== Complete Error Message Example ===");
1130 println!("{error_message}");
1131
1132 assert!(error_message.contains("Failed to parse argument 'amount'"));
1134 assert!(error_message.contains("Expected type u64 (unsigned 64-bit integer)"));
1135 assert!(error_message.contains("received: '\"100\"'"));
1136 assert!(error_message.contains("Suggestion: For numbers, ensure no quotes"));
1137 }
1138
1139 fn struct_spec(name: &'static str, fields: &[(&str, ScSpecTypeDef)]) -> (Spec, ScSpecTypeDef) {
1140 use stellar_xdr::{
1141 ScSpecEntry, ScSpecTypeUdt, ScSpecUdtStructFieldV0, ScSpecUdtStructV0, StringM,
1142 };
1143 let struct_name: StringM<60> = name.try_into().unwrap();
1144 let fields_xdr: Vec<ScSpecUdtStructFieldV0> = fields
1145 .iter()
1146 .map(|(n, t)| ScSpecUdtStructFieldV0 {
1147 doc: StringM::default(),
1148 name: (*n).try_into().unwrap(),
1149 type_: t.clone(),
1150 })
1151 .collect();
1152 let spec = Spec(Some(vec![ScSpecEntry::UdtStructV0(ScSpecUdtStructV0 {
1153 doc: StringM::default(),
1154 lib: StringM::default(),
1155 name: struct_name.clone(),
1156 fields: fields_xdr.try_into().unwrap(),
1157 })]));
1158 let ty = ScSpecTypeDef::Udt(ScSpecTypeUdt { name: struct_name });
1159 (spec, ty)
1160 }
1161
1162 const TEST_G_ADDRESS: &str = "GD5KD2KEZJIGTC63IGW6UMUSMVUVG5IHG64HUTFWCHVZH2N2IBOQN7PS";
1164
1165 #[test]
1166 fn resolve_aliases_resolves_native_to_asset_contract_address() {
1167 let ty = ScSpecTypeDef::Address;
1168 let spec = Spec(Some(vec![]));
1169 let config = crate::config::Args::default();
1170
1171 let mut value = serde_json::json!("native");
1172 let mutated = resolve_aliases_in_json(&mut value, &ty, &spec, &config).unwrap();
1173 assert!(
1174 mutated,
1175 "native should resolve to the native asset contract"
1176 );
1177
1178 let network_passphrase = config.get_network().unwrap().network_passphrase;
1179 let expected = format!(
1180 "{}",
1181 crate::utils::contract_id_hash_from_asset(
1182 &crate::xdr::Asset::Native,
1183 &network_passphrase,
1184 )
1185 );
1186 assert_eq!(value, serde_json::Value::String(expected));
1187 }
1188
1189 #[test]
1190 fn resolve_aliases_in_json_walks_vec_of_address() {
1191 use stellar_xdr::ScSpecTypeVec;
1192
1193 let ty = ScSpecTypeDef::Vec(Box::new(ScSpecTypeVec {
1194 element_type: Box::new(ScSpecTypeDef::Address),
1195 }));
1196 let spec = Spec(Some(vec![]));
1197 let config = crate::config::Args::default();
1198
1199 let mut value = serde_json::json!([TEST_G_ADDRESS]);
1200 resolve_aliases_in_json(&mut value, &ty, &spec, &config).unwrap();
1201 assert_eq!(value, serde_json::json!([TEST_G_ADDRESS]));
1202
1203 let mut value = serde_json::json!(["definitely-not-a-known-alias"]);
1205 let err = resolve_aliases_in_json(&mut value, &ty, &spec, &config).unwrap_err();
1206 assert!(
1207 matches!(err, Error::Config(_) | Error::ScAddress(_)),
1208 "expected alias-resolution error, got {err:?}"
1209 );
1210 }
1211
1212 #[test]
1213 fn resolve_aliases_in_json_walks_tuple() {
1214 use stellar_xdr::ScSpecTypeTuple;
1215
1216 let ty = ScSpecTypeDef::Tuple(Box::new(ScSpecTypeTuple {
1217 value_types: vec![ScSpecTypeDef::Address, ScSpecTypeDef::U32]
1218 .try_into()
1219 .unwrap(),
1220 }));
1221 let spec = Spec(Some(vec![]));
1222 let config = crate::config::Args::default();
1223
1224 let mut value = serde_json::json!([TEST_G_ADDRESS, 42]);
1225 resolve_aliases_in_json(&mut value, &ty, &spec, &config).unwrap();
1226 assert_eq!(value, serde_json::json!([TEST_G_ADDRESS, 42]));
1227
1228 let mut value = serde_json::json!(["bogus-alias", 42]);
1229 let err = resolve_aliases_in_json(&mut value, &ty, &spec, &config).unwrap_err();
1230 assert!(
1231 matches!(err, Error::Config(_) | Error::ScAddress(_)),
1232 "expected alias-resolution error, got {err:?}"
1233 );
1234 }
1235
1236 #[test]
1237 fn resolve_aliases_in_json_walks_struct_field() {
1238 use stellar_xdr::ScSpecTypeVec;
1239
1240 let (spec, ty) = struct_spec(
1241 "Operator",
1242 &[
1243 ("count", ScSpecTypeDef::U32),
1244 (
1245 "addresses",
1246 ScSpecTypeDef::Vec(Box::new(ScSpecTypeVec {
1247 element_type: Box::new(ScSpecTypeDef::Address),
1248 })),
1249 ),
1250 ],
1251 );
1252 let config = crate::config::Args::default();
1253
1254 let mut value = serde_json::json!({"count": 1, "addresses": [TEST_G_ADDRESS]});
1255 resolve_aliases_in_json(&mut value, &ty, &spec, &config).unwrap();
1256 assert_eq!(
1257 value,
1258 serde_json::json!({"count": 1, "addresses": [TEST_G_ADDRESS]})
1259 );
1260
1261 let mut value = serde_json::json!({"count": 1, "addresses": ["bogus-alias"]});
1263 let err = resolve_aliases_in_json(&mut value, &ty, &spec, &config).unwrap_err();
1264 assert!(
1265 matches!(err, Error::Config(_) | Error::ScAddress(_)),
1266 "expected alias-resolution error, got {err:?}"
1267 );
1268 }
1269
1270 #[test]
1271 fn resolve_aliases_in_json_walks_union_tuple_variant() {
1272 use stellar_xdr::{
1273 ScSpecEntry, ScSpecTypeUdt, ScSpecUdtUnionCaseTupleV0, ScSpecUdtUnionCaseV0,
1274 ScSpecUdtUnionV0, StringM,
1275 };
1276
1277 let union_name: StringM<60> = "Choice".try_into().unwrap();
1278 let spec = Spec(Some(vec![ScSpecEntry::UdtUnionV0(ScSpecUdtUnionV0 {
1279 doc: StringM::default(),
1280 lib: StringM::default(),
1281 name: union_name.clone(),
1282 cases: vec![ScSpecUdtUnionCaseV0::TupleV0(ScSpecUdtUnionCaseTupleV0 {
1283 doc: StringM::default(),
1284 name: "Pick".try_into().unwrap(),
1285 type_: vec![ScSpecTypeDef::Address, ScSpecTypeDef::U32]
1286 .try_into()
1287 .unwrap(),
1288 })]
1289 .try_into()
1290 .unwrap(),
1291 })]));
1292
1293 let ty = ScSpecTypeDef::Udt(ScSpecTypeUdt { name: union_name });
1294 let config = crate::config::Args::default();
1295
1296 let mut value = serde_json::json!({"Pick": [TEST_G_ADDRESS, 42]});
1297 resolve_aliases_in_json(&mut value, &ty, &spec, &config).unwrap();
1298 assert_eq!(value, serde_json::json!({"Pick": [TEST_G_ADDRESS, 42]}));
1299
1300 let mut value = serde_json::json!({"Pick": ["bogus-alias", 42]});
1301 let err = resolve_aliases_in_json(&mut value, &ty, &spec, &config).unwrap_err();
1302 assert!(
1303 matches!(err, Error::Config(_) | Error::ScAddress(_)),
1304 "expected alias-resolution error, got {err:?}"
1305 );
1306 }
1307
1308 #[test]
1309 fn resolve_aliases_in_json_walks_single_element_union_variant() {
1310 use stellar_xdr::{
1311 ScSpecEntry, ScSpecTypeUdt, ScSpecUdtUnionCaseTupleV0, ScSpecUdtUnionCaseV0,
1312 ScSpecUdtUnionV0, StringM,
1313 };
1314
1315 let union_name: StringM<60> = "OneOf".try_into().unwrap();
1316 let spec = Spec(Some(vec![ScSpecEntry::UdtUnionV0(ScSpecUdtUnionV0 {
1317 doc: StringM::default(),
1318 lib: StringM::default(),
1319 name: union_name.clone(),
1320 cases: vec![ScSpecUdtUnionCaseV0::TupleV0(ScSpecUdtUnionCaseTupleV0 {
1321 doc: StringM::default(),
1322 name: "Only".try_into().unwrap(),
1323 type_: vec![ScSpecTypeDef::Address].try_into().unwrap(),
1324 })]
1325 .try_into()
1326 .unwrap(),
1327 })]));
1328
1329 let ty = ScSpecTypeDef::Udt(ScSpecTypeUdt { name: union_name });
1330 let config = crate::config::Args::default();
1331
1332 let mut value = serde_json::json!({"Only": TEST_G_ADDRESS});
1334 resolve_aliases_in_json(&mut value, &ty, &spec, &config).unwrap();
1335 assert_eq!(value, serde_json::json!({"Only": TEST_G_ADDRESS}));
1336
1337 let mut value = serde_json::json!({"Only": "bogus-alias"});
1338 let err = resolve_aliases_in_json(&mut value, &ty, &spec, &config).unwrap_err();
1339 assert!(
1340 matches!(err, Error::Config(_) | Error::ScAddress(_)),
1341 "expected alias-resolution error, got {err:?}"
1342 );
1343 }
1344
1345 #[test]
1346 fn resolve_aliases_in_json_walks_option_and_map() {
1347 use stellar_xdr::{ScSpecTypeMap, ScSpecTypeOption};
1348
1349 let opt_ty = ScSpecTypeDef::Option(Box::new(ScSpecTypeOption {
1350 value_type: Box::new(ScSpecTypeDef::Address),
1351 }));
1352 let spec = Spec(Some(vec![]));
1353 let config = crate::config::Args::default();
1354
1355 let mut value = serde_json::Value::Null;
1356 resolve_aliases_in_json(&mut value, &opt_ty, &spec, &config).unwrap();
1357 assert_eq!(value, serde_json::Value::Null);
1358
1359 let mut value = serde_json::json!(TEST_G_ADDRESS);
1360 resolve_aliases_in_json(&mut value, &opt_ty, &spec, &config).unwrap();
1361 assert_eq!(value, serde_json::json!(TEST_G_ADDRESS));
1362
1363 let map_ty = ScSpecTypeDef::Map(Box::new(ScSpecTypeMap {
1364 key_type: Box::new(ScSpecTypeDef::Symbol),
1365 value_type: Box::new(ScSpecTypeDef::Address),
1366 }));
1367 let mut value = serde_json::json!({"owner": TEST_G_ADDRESS});
1368 resolve_aliases_in_json(&mut value, &map_ty, &spec, &config).unwrap();
1369 assert_eq!(value, serde_json::json!({"owner": TEST_G_ADDRESS}));
1370
1371 let mut value = serde_json::json!({"owner": "bogus-alias"});
1372 let err = resolve_aliases_in_json(&mut value, &map_ty, &spec, &config).unwrap_err();
1373 assert!(
1374 matches!(err, Error::Config(_) | Error::ScAddress(_)),
1375 "expected alias-resolution error, got {err:?}"
1376 );
1377 }
1378
1379 #[test]
1380 fn resolve_aliases_in_json_walks_result_inner_types() {
1381 use stellar_xdr::ScSpecTypeResult;
1382
1383 let ty = ScSpecTypeDef::Result(Box::new(ScSpecTypeResult {
1384 ok_type: Box::new(ScSpecTypeDef::Address),
1385 error_type: Box::new(ScSpecTypeDef::U32),
1386 }));
1387 let spec = Spec(Some(vec![]));
1388 let config = crate::config::Args::default();
1389
1390 let mut value = serde_json::json!(TEST_G_ADDRESS);
1391 resolve_aliases_in_json(&mut value, &ty, &spec, &config).unwrap();
1392 assert_eq!(value, serde_json::json!(TEST_G_ADDRESS));
1393
1394 let mut value = serde_json::json!("bogus-alias");
1395 let err = resolve_aliases_in_json(&mut value, &ty, &spec, &config).unwrap_err();
1396 assert!(
1397 matches!(err, Error::Config(_) | Error::ScAddress(_)),
1398 "expected alias-resolution error, got {err:?}"
1399 );
1400 }
1401
1402 #[test]
1403 fn resolve_aliases_preserves_input_when_nothing_mutated() {
1404 use stellar_xdr::ScSpecTypeVec;
1405
1406 let (spec, ty) = struct_spec(
1409 "Point",
1410 &[("x", ScSpecTypeDef::U32), ("y", ScSpecTypeDef::U32)],
1411 );
1412 let config = crate::config::Args::default();
1413 let pretty = r#"{ "x": 1, "y": 2 }"#;
1414 assert_eq!(
1415 resolve_aliases(pretty, &ty, &spec, &config).unwrap(),
1416 pretty
1417 );
1418
1419 let ty = ScSpecTypeDef::Vec(Box::new(ScSpecTypeVec {
1421 element_type: Box::new(ScSpecTypeDef::Address),
1422 }));
1423 let spec = Spec(Some(vec![]));
1424 let pretty = format!(r#"[ "{TEST_G_ADDRESS}" ]"#);
1425 assert_eq!(
1426 resolve_aliases(&pretty, &ty, &spec, &config).unwrap(),
1427 pretty
1428 );
1429 }
1430
1431 #[test]
1432 fn resolve_aliases_in_json_walks_map_keys() {
1433 use stellar_xdr::ScSpecTypeMap;
1434
1435 let map_ty = ScSpecTypeDef::Map(Box::new(ScSpecTypeMap {
1436 key_type: Box::new(ScSpecTypeDef::Address),
1437 value_type: Box::new(ScSpecTypeDef::U32),
1438 }));
1439 let spec = Spec(Some(vec![]));
1440 let config = crate::config::Args::default();
1441
1442 let mut value = serde_json::json!({ TEST_G_ADDRESS: 1 });
1443 resolve_aliases_in_json(&mut value, &map_ty, &spec, &config).unwrap();
1444 assert_eq!(value, serde_json::json!({ TEST_G_ADDRESS: 1 }));
1445
1446 let mut value = serde_json::json!({ "bogus-alias": 1 });
1447 let err = resolve_aliases_in_json(&mut value, &map_ty, &spec, &config).unwrap_err();
1448 assert!(
1449 matches!(err, Error::Config(_) | Error::ScAddress(_)),
1450 "expected alias-resolution error, got {err:?}"
1451 );
1452 }
1453
1454 #[test]
1456 fn invoke_help_strips_control_characters() {
1457 let path = concat!(
1458 env!("CARGO_MANIFEST_DIR"),
1459 "/../crates/soroban-spec-tools/tests/fixtures/control_characters.wasm"
1460 );
1461 let bytes = std::fs::read(path).expect("fixture wasm should be readable");
1462 let spec = Spec::from_wasm(&bytes).expect("wasm should parse without error");
1463 let mut cmd = build_clap_command(&spec, true).expect("command should build without error");
1464 let help = cmd.render_long_help().to_string();
1465
1466 let bad_chars: Vec<char> = help
1467 .chars()
1468 .filter(|c| c.is_control() && *c != '\n' && *c != '\t')
1469 .collect();
1470 assert!(
1471 bad_chars.is_empty(),
1472 "invoke help contains unexpected control characters {bad_chars:?}:\n{help:?}"
1473 );
1474 }
1475}