Skip to main content

soroban_cli/commands/contract/
arg_parsing.rs

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        // Filter out the constructor function from the invoke command
140        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    // Exact match (normal path).
172    if let Ok(f) = spec.find_function(function) {
173        return Ok(f.clone());
174    }
175    // Fallback: match against sanitized names for functions whose names contain
176    // control characters (clap registers the sanitized form as the command name).
177    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_); //-0--
216
217    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        // Collect a signer up front for top-level address args, so the
229        // alias-named identity can also sign the transaction. Alias-to-strkey
230        // resolution itself happens inside parse_argument_with_validation,
231        // which uniformly handles top-level and nested Address positions.
232        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    // Parse the function arguments
345    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        // Set up special-case arg rules
391        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    // A raw public key is matched to an identity at `--hd-path`, so the signer
470    // is built at that same path; an alias or secret honors it the same way.
471    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
478/// Validates JSON string and returns a more descriptive error if invalid
479fn validate_json_arg(arg_name: &str, value: &str) -> Result<(), Error> {
480    // Try to parse as JSON first
481    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
491/// Gets a human-readable type name for error messages
492fn 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
537/// Gets available function names for error messages
538fn 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
548/// Checks if a type is a primitive type that doesn't require JSON validation
549fn 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
573/// Generates context-aware suggestions based on the expected type and error
574fn 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
617/// Enhanced argument parsing with better error handling
618fn 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    // Pre-validate JSON for non-primitive types, but skip for union (enum) UDTs since
628    // both bare strings (e.g. `Unit`) and JSON strings (e.g. `"Unit"`) are valid for
629    // unit variants — from_string in soroban-spec-tools handles both forms correctly.
630    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    // Walk the input through resolve_aliases_in_json so identity aliases are
641    // resolved at every Address/MuxedAddress position, top-level or nested.
642    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
654/// Returns the input with identity aliases resolved to strkeys at every
655/// `Address`/`MuxedAddress` position the spec describes. Inputs that aren't
656/// JSON (e.g. a bare top-level alias `alice`) are wrapped as a JSON string
657/// for `Address`-typed args so the walker can still resolve them.
658fn 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    // Nothing was rewritten — return the original input verbatim so we don't
678    // disturb whitespace, key ordering, or number formatting just to reparse it.
679    if !mutated {
680        return Ok(value.to_string());
681    }
682
683    // For top-level Address inputs, hand back the bare strkey rather than a
684    // JSON-quoted form — `Spec::from_string` accepts both, but the bare form
685    // matches what the original Address path produced.
686    Ok(match (&json, is_address) {
687        (serde_json::Value::String(s), true) => s.clone(),
688        _ => json.to_string(),
689    })
690}
691
692/// Walks a JSON value alongside the contract spec type tree, rewriting any
693/// string at an `Address`/`MuxedAddress` position into the resolved address
694/// via the locator. Strings that are already a valid account, contract, or
695/// muxed strkey pass through unchanged.
696///
697/// This makes identity aliases work inside nested arguments (struct fields,
698/// vec/map/tuple elements, option values, union tuple-variant payloads), not
699/// just at the top level. Returns whether any string was actually rewritten.
700fn 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            // Result is rarely used as an input type. The walker descends into
762            // both branches; the inner `match value` no-ops when the JSON
763            // shape doesn't fit the branch's type. Resolution is idempotent
764            // (a strkey re-resolves to itself), so descending twice is safe
765            // when both branches happen to share a shape.
766            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            // Soroban's contract macros emit numeric field names ("0", "1", …)
791            // for tuple structs and identifier names for regular structs, so a
792            // field literally named "0" reliably distinguishes the two.
793            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    // Single-element tuple variants take a bare payload — `{"Variant": value}` —
845    // matching the form `soroban_spec_tools` accepts. Variants with two or more
846    // elements take an array payload — `{"Variant": [a, b, ...]}`.
847    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        // Valid JSON should not return an error
936        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        // Invalid JSON should return an error
946        let result = validate_json_arg("test_arg", r#"{"key": value}"#); // Missing quotes around value
947        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        // Test various malformed JSON cases
966        let test_cases = vec![
967            r#"{"key": }"#,         // Missing value
968            r#"{key: "value"}"#,    // Missing quotes around key
969            r#"{"key": "value",}"#, // Trailing comma
970            r#"{"key" "value"}"#,   // Missing colon
971        ];
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        // Test context-aware suggestions for different types
984
985        // Test u64 with quoted value
986        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        // Test u64 with decimal value
991        let suggestion = get_context_suggestions(&ScSpecTypeDef::U64, "100.5");
992        assert!(suggestion.contains("don't support decimal values"));
993
994        // Test string without quotes
995        let suggestion = get_context_suggestions(&ScSpecTypeDef::String, "hello");
996        assert!(suggestion.contains("properly quoted"));
997
998        // Test address type
999        let suggestion = get_context_suggestions(&ScSpecTypeDef::Address, "invalid_addr");
1000        assert!(suggestion.contains("G... (account), C... (contract)"));
1001
1002        // Test boolean type
1003        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        // Build a minimal Spec with a union type: enum MyEnum { Unit }
1027        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        // Bare string (no JSON quoting) should be accepted
1045        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        // JSON-quoted string should also be accepted
1050        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        // Both forms should produce the same ScVal
1058        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        // Tuple variant with a value must still use JSON object syntax
1100        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        // Test that our CannotParseArg error formats correctly
1118        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        // Verify the error message contains all expected parts
1133        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    // A real account strkey that should pass through resolve_address unchanged.
1163    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        // An unknown alias-shaped string at a nested Address position must surface as an error.
1204        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        // Walker must reach the Address inside Vec inside the struct field.
1262        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        // Bare payload form: {"Only": addr} — not {"Only": [addr]}.
1333        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        // Type with no Address positions: input is returned verbatim,
1407        // including whitespace that compact JSON re-serialization would drop.
1408        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        // Type with Address positions but no aliases: also returned verbatim.
1420        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    /// Mirrors `stellar contract invoke`: Spec::from_wasm -> build_clap_command -> render_long_help.
1455    #[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}