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::Spec;
14use std::collections::HashMap;
15use std::convert::TryInto;
16use std::env;
17use std::ffi::OsString;
18use std::fmt::Debug;
19use std::path::PathBuf;
20use stellar_xdr::curr::ContractId;
21
22#[derive(thiserror::Error, Debug)]
23pub enum Error {
24    #[error("Failed to parse argument '{arg}': {error}\n\nContext: Expected type {expected_type}, but received: '{received_value}'\n\nSuggestion: {suggestion}")]
25    CannotParseArg {
26        arg: String,
27        error: soroban_spec_tools::Error,
28        expected_type: String,
29        received_value: String,
30        suggestion: String,
31    },
32    #[error("Invalid JSON in argument '{arg}': {json_error}\n\nReceived value: '{received_value}'\n\nSuggestions:\n- Check for missing quotes around strings\n- Ensure proper JSON syntax (commas, brackets, etc.)\n- For complex objects, consider using --{arg}-file-path to load from a file")]
33    InvalidJsonArg {
34        arg: String,
35        json_error: String,
36        received_value: String,
37    },
38    #[error("Type mismatch for argument '{arg}': expected {expected_type}, but got {actual_type}\n\nReceived value: '{received_value}'\n\nSuggestions:\n- For {expected_type}, ensure the value is properly formatted\n- Check the contract specification for the correct argument type")]
39    TypeMismatch {
40        arg: String,
41        expected_type: String,
42        actual_type: String,
43        received_value: String,
44    },
45    #[error("Missing required argument '{arg}' of type {expected_type}\n\nSuggestions:\n- Add the argument: --{arg} <value>\n- Or use a file: --{arg}-file-path <path-to-json-file>\n- Check the contract specification for required arguments")]
46    MissingArgument { arg: String, expected_type: String },
47    #[error("Cannot read file {file_path:?}: {error}\n\nSuggestions:\n- Check if the file exists and is readable\n- Ensure the file path is correct\n- Verify file permissions")]
48    MissingFileArg { file_path: PathBuf, error: String },
49    #[error("cannot print result {result:?}: {error}")]
50    CannotPrintResult {
51        result: ScVal,
52        error: soroban_spec_tools::Error,
53    },
54    #[error("function '{function_name}' was not found in the contract\n\nAvailable functions: {available_functions}\n\nSuggestions:\n- Check the function name spelling\n- Use 'stellar contract invoke --help' to see available functions\n- Verify the contract ID is correct")]
55    FunctionNotFoundInContractSpec {
56        function_name: String,
57        available_functions: String,
58    },
59    #[error("function name '{function_name}' is too long (max 32 characters)\n\nReceived: {function_name} ({length} characters)")]
60    FunctionNameTooLong {
61        function_name: String,
62        length: usize,
63    },
64    #[error("argument count ({current}) surpasses maximum allowed count ({maximum})\n\nSuggestions:\n- Reduce the number of arguments\n- Consider using file-based arguments for complex data\n- Check if some arguments can be combined")]
65    MaxNumberOfArgumentsReached { current: usize, maximum: usize },
66    #[error("Unsupported address type '{address}'\n\nSupported formats:\n- Account addresses: G... (starts with G)\n- Contract addresses: C... (starts with C)\n- Muxed accounts: M... (starts with M)\n- Identity names: alice, bob, etc.\n\nReceived: '{address}'")]
67    UnsupportedScAddress { address: String },
68    #[error(transparent)]
69    Xdr(#[from] xdr::Error),
70    #[error(transparent)]
71    StrVal(#[from] soroban_spec_tools::Error),
72    #[error(transparent)]
73    ScAddress(#[from] sc_address::Error),
74    #[error(transparent)]
75    Config(#[from] config::Error),
76    #[error("")]
77    HelpMessage(String),
78    #[error(transparent)]
79    Signer(#[from] signer::Error),
80}
81
82pub type HostFunctionParameters = (String, Spec, InvokeContractArgs, Vec<Signer>);
83
84fn running_cmd() -> String {
85    let mut args: Vec<String> = env::args().collect();
86
87    if let Some(pos) = args.iter().position(|arg| arg == "--") {
88        args.truncate(pos);
89    }
90
91    format!("{} --", args.join(" "))
92}
93
94pub async fn build_host_function_parameters(
95    contract_id: &stellar_strkey::Contract,
96    slop: &[OsString],
97    spec_entries: &[ScSpecEntry],
98    config: &config::Args,
99) -> Result<HostFunctionParameters, Error> {
100    build_host_function_parameters_with_filter(contract_id, slop, spec_entries, config, true).await
101}
102
103pub async fn build_constructor_parameters(
104    contract_id: &stellar_strkey::Contract,
105    slop: &[OsString],
106    spec_entries: &[ScSpecEntry],
107    config: &config::Args,
108) -> Result<HostFunctionParameters, Error> {
109    build_host_function_parameters_with_filter(contract_id, slop, spec_entries, config, false).await
110}
111
112async fn build_host_function_parameters_with_filter(
113    contract_id: &stellar_strkey::Contract,
114    slop: &[OsString],
115    spec_entries: &[ScSpecEntry],
116    config: &config::Args,
117    filter_constructor: bool,
118) -> Result<HostFunctionParameters, Error> {
119    let spec = Spec(Some(spec_entries.to_vec()));
120    let cmd = build_clap_command(&spec, filter_constructor)?;
121    let (function, matches_) = parse_command_matches(cmd, slop)?;
122    let func = get_function_spec(&spec, &function)?;
123    let (parsed_args, signers) = parse_function_arguments(&func, &matches_, &spec, config).await?;
124    let invoke_args = build_invoke_contract_args(contract_id, &function, parsed_args)?;
125
126    Ok((function, spec, invoke_args, signers))
127}
128
129fn build_clap_command(spec: &Spec, filter_constructor: bool) -> Result<clap::Command, Error> {
130    let mut cmd = clap::Command::new(running_cmd())
131        .no_binary_name(true)
132        .term_width(300)
133        .max_term_width(300);
134
135    for ScSpecFunctionV0 { name, .. } in spec.find_functions()? {
136        let function_name = name.to_utf8_string_lossy();
137        // Filter out the constructor function from the invoke command
138        if !filter_constructor || function_name != CONSTRUCTOR_FUNCTION_NAME {
139            cmd = cmd.subcommand(build_custom_cmd(&function_name, spec)?);
140        }
141    }
142    cmd.build();
143    Ok(cmd)
144}
145
146fn parse_command_matches(
147    mut cmd: clap::Command,
148    slop: &[OsString],
149) -> Result<(String, clap::ArgMatches), Error> {
150    let long_help = cmd.render_long_help();
151    let maybe_matches = cmd.try_get_matches_from(slop);
152
153    let Some((function, matches_)) = (match maybe_matches {
154        Ok(mut matches) => matches.remove_subcommand(),
155        Err(e) => {
156            if e.kind() == DisplayHelp {
157                return Err(HelpMessage(e.to_string()));
158            }
159            e.exit();
160        }
161    }) else {
162        return Err(HelpMessage(format!("{long_help}")));
163    };
164
165    Ok((function.clone(), matches_))
166}
167
168fn get_function_spec(spec: &Spec, function: &str) -> Result<ScSpecFunctionV0, Error> {
169    spec.find_function(function)
170        .map_err(|_| Error::FunctionNotFoundInContractSpec {
171            function_name: function.to_string(),
172            available_functions: get_available_functions(spec),
173        })
174        .cloned()
175}
176
177async fn parse_function_arguments(
178    func: &ScSpecFunctionV0,
179    matches_: &clap::ArgMatches,
180    spec: &Spec,
181    config: &config::Args,
182) -> Result<(Vec<ScVal>, Vec<Signer>), Error> {
183    let mut parsed_args = Vec::with_capacity(func.inputs.len());
184    let mut signers = Vec::<Signer>::new();
185
186    for i in func.inputs.iter() {
187        parse_single_argument(i, matches_, spec, config, &mut signers, &mut parsed_args).await?;
188    }
189
190    Ok((parsed_args, signers))
191}
192
193async fn parse_single_argument(
194    input: &stellar_xdr::curr::ScSpecFunctionInputV0,
195    matches_: &clap::ArgMatches,
196    spec: &Spec,
197    config: &config::Args,
198    signers: &mut Vec<Signer>,
199    parsed_args: &mut Vec<ScVal>,
200) -> Result<(), Error> {
201    let name = input.name.to_utf8_string()?;
202    let expected_type_name = get_type_name(&input.type_); //-0--
203
204    if let Some(mut val) = matches_.get_raw(&name) {
205        let s = match val.next() {
206            Some(v) => v.to_string_lossy().to_string(),
207            None => {
208                return Err(Error::MissingArgument {
209                    arg: name.clone(),
210                    expected_type: expected_type_name,
211                });
212            }
213        };
214
215        // Handle address types with signer resolution
216        if matches!(
217            input.type_,
218            ScSpecTypeDef::Address | ScSpecTypeDef::MuxedAddress
219        ) {
220            let trimmed_s = s.trim_matches('"');
221            let addr = resolve_address(trimmed_s, config)?;
222            if let Some(signer) = resolve_signer(trimmed_s, config).await {
223                signers.push(signer);
224            }
225            parsed_args.push(parse_argument_with_validation(
226                &name,
227                &addr,
228                &input.type_,
229                spec,
230                config,
231            )?);
232            return Ok(());
233        }
234
235        parsed_args.push(parse_argument_with_validation(
236            &name,
237            &s,
238            &input.type_,
239            spec,
240            config,
241        )?);
242        Ok(())
243    } else if matches!(input.type_, ScSpecTypeDef::Option(_)) {
244        parsed_args.push(ScVal::Void);
245        Ok(())
246    } else if let Some(arg_path) = matches_.get_one::<PathBuf>(&fmt_arg_file_name(&name)) {
247        parsed_args.push(parse_file_argument(
248            &name,
249            arg_path,
250            &input.type_,
251            expected_type_name,
252            spec,
253            config,
254        )?);
255        Ok(())
256    } else {
257        Err(Error::MissingArgument {
258            arg: name,
259            expected_type: expected_type_name,
260        })
261    }
262}
263
264fn parse_file_argument(
265    name: &str,
266    arg_path: &PathBuf,
267    type_def: &ScSpecTypeDef,
268    expected_type_name: String,
269    spec: &Spec,
270    config: &config::Args,
271) -> Result<ScVal, Error> {
272    if matches!(type_def, ScSpecTypeDef::Bytes | ScSpecTypeDef::BytesN(_)) {
273        let bytes = std::fs::read(arg_path).map_err(|e| Error::MissingFileArg {
274            file_path: arg_path.clone(),
275            error: e.to_string(),
276        })?;
277        ScVal::try_from(&bytes).map_err(|()| Error::CannotParseArg {
278            arg: name.to_string(),
279            error: soroban_spec_tools::Error::Unknown,
280            expected_type: expected_type_name,
281            received_value: format!("{} bytes from file", bytes.len()),
282            suggestion: "Ensure the file contains valid binary data for the expected byte type"
283                .to_string(),
284        })
285    } else {
286        let file_contents =
287            std::fs::read_to_string(arg_path).map_err(|e| Error::MissingFileArg {
288                file_path: arg_path.clone(),
289                error: e.to_string(),
290            })?;
291        tracing::debug!(
292            "file {arg_path:?}, has contents:\n{file_contents}\nAnd type {:#?}\n{}",
293            type_def,
294            file_contents.len()
295        );
296        parse_argument_with_validation(name, &file_contents, type_def, spec, config)
297    }
298}
299
300fn build_invoke_contract_args(
301    contract_id: &stellar_strkey::Contract,
302    function: &str,
303    parsed_args: Vec<ScVal>,
304) -> Result<InvokeContractArgs, Error> {
305    let contract_address_arg = xdr::ScAddress::Contract(ContractId(Hash(contract_id.0)));
306    let function_symbol_arg = function
307        .try_into()
308        .map_err(|()| Error::FunctionNameTooLong {
309            function_name: function.to_string(),
310            length: function.len(),
311        })?;
312
313    let final_args =
314        parsed_args
315            .clone()
316            .try_into()
317            .map_err(|_| Error::MaxNumberOfArgumentsReached {
318                current: parsed_args.len(),
319                maximum: ScVec::default().max_len(),
320            })?;
321
322    Ok(InvokeContractArgs {
323        contract_address: contract_address_arg,
324        function_name: function_symbol_arg,
325        args: final_args,
326    })
327}
328
329pub fn build_custom_cmd(name: &str, spec: &Spec) -> Result<clap::Command, Error> {
330    let func = spec
331        .find_function(name)
332        .map_err(|_| Error::FunctionNotFoundInContractSpec {
333            function_name: name.to_string(),
334            available_functions: get_available_functions(spec),
335        })?;
336
337    // Parse the function arguments
338    let inputs_map = &func
339        .inputs
340        .iter()
341        .map(|i| (i.name.to_utf8_string().unwrap(), i.type_.clone()))
342        .collect::<HashMap<String, ScSpecTypeDef>>();
343    let name: &'static str = Box::leak(name.to_string().into_boxed_str());
344    let mut cmd = clap::Command::new(name)
345        .no_binary_name(true)
346        .term_width(300)
347        .max_term_width(300);
348    let kebab_name = name.to_kebab_case();
349    if kebab_name != name {
350        cmd = cmd.alias(kebab_name);
351    }
352    let doc: &'static str = Box::leak(func.doc.to_utf8_string_lossy().into_boxed_str());
353    let long_doc: &'static str = Box::leak(arg_file_help(doc).into_boxed_str());
354
355    cmd = cmd.about(Some(doc)).long_about(long_doc);
356    for (name, type_) in inputs_map {
357        let mut arg = clap::Arg::new(name);
358        let file_arg_name = fmt_arg_file_name(name);
359        let mut file_arg = clap::Arg::new(&file_arg_name);
360        arg = arg
361            .long(name)
362            .alias(name.to_kebab_case())
363            .num_args(1)
364            .value_parser(clap::builder::NonEmptyStringValueParser::new())
365            .long_help(spec.doc(name, type_)?);
366
367        file_arg = file_arg
368            .long(&file_arg_name)
369            .alias(file_arg_name.to_kebab_case())
370            .num_args(1)
371            .hide(true)
372            .value_parser(value_parser!(PathBuf))
373            .conflicts_with(name);
374
375        if let Some(value_name) = spec.arg_value_name(type_, 0) {
376            let value_name: &'static str = Box::leak(value_name.into_boxed_str());
377            arg = arg.value_name(value_name);
378        }
379
380        // Set up special-case arg rules
381        arg = match type_ {
382            ScSpecTypeDef::Bool => arg
383                .num_args(0..1)
384                .default_missing_value("true")
385                .default_value("false")
386                .num_args(0..=1),
387            ScSpecTypeDef::Option(_val) => arg.required(false),
388            ScSpecTypeDef::I256 | ScSpecTypeDef::I128 | ScSpecTypeDef::I64 | ScSpecTypeDef::I32 => {
389                arg.allow_hyphen_values(true)
390            }
391            _ => arg,
392        };
393
394        cmd = cmd.arg(arg);
395        cmd = cmd.arg(file_arg);
396    }
397    Ok(cmd)
398}
399
400fn fmt_arg_file_name(name: &str) -> String {
401    format!("{name}-file-path")
402}
403
404fn arg_file_help(docs: &str) -> String {
405    format!(
406        r"{docs}
407Usage Notes:
408Each arg has a corresponding --<arg_name>-file-path which is a path to a file containing the corresponding JSON argument.
409Note: The only types which aren't JSON are Bytes and BytesN, which are raw bytes"
410    )
411}
412
413pub fn output_to_string(
414    spec: &Spec,
415    res: &ScVal,
416    function: &str,
417) -> Result<TxnResult<String>, Error> {
418    let mut res_str = String::new();
419    if let Some(output) = spec.find_function(function)?.outputs.first() {
420        res_str = spec
421            .xdr_to_json(res, output)
422            .map_err(|e| Error::CannotPrintResult {
423                result: res.clone(),
424                error: e,
425            })?
426            .to_string();
427    }
428    Ok(TxnResult::Res(res_str))
429}
430
431fn resolve_address(addr_or_alias: &str, config: &config::Args) -> Result<String, Error> {
432    let sc_address: UnresolvedScAddress = addr_or_alias.parse().unwrap();
433    let account = match sc_address {
434        UnresolvedScAddress::Resolved(addr) => addr.to_string(),
435        addr @ UnresolvedScAddress::Alias(_) => {
436            let addr = addr.resolve(&config.locator, &config.get_network()?.network_passphrase)?;
437            match addr {
438                xdr::ScAddress::Account(account) => account.to_string(),
439                contract @ xdr::ScAddress::Contract(_) => contract.to_string(),
440                stellar_xdr::curr::ScAddress::MuxedAccount(account) => account.to_string(),
441                stellar_xdr::curr::ScAddress::ClaimableBalance(_)
442                | stellar_xdr::curr::ScAddress::LiquidityPool(_) => {
443                    return Err(Error::UnsupportedScAddress {
444                        address: addr.to_string(),
445                    })
446                }
447            }
448        }
449    };
450    Ok(account)
451}
452
453async fn resolve_signer(addr_or_alias: &str, config: &config::Args) -> Option<Signer> {
454    let secret = config.locator.get_secret_key(addr_or_alias).ok()?;
455    let print = Print::new(false);
456    let signer = secret.signer(None, print).await.ok()?;
457    Some(signer)
458}
459
460/// Validates JSON string and returns a more descriptive error if invalid
461fn validate_json_arg(arg_name: &str, value: &str) -> Result<(), Error> {
462    // Try to parse as JSON first
463    if let Err(json_err) = serde_json::from_str::<serde_json::Value>(value) {
464        return Err(Error::InvalidJsonArg {
465            arg: arg_name.to_string(),
466            json_error: json_err.to_string(),
467            received_value: value.to_string(),
468        });
469    }
470    Ok(())
471}
472
473/// Gets a human-readable type name for error messages
474fn get_type_name(type_def: &ScSpecTypeDef) -> String {
475    match type_def {
476        ScSpecTypeDef::Val => "any value".to_string(),
477        ScSpecTypeDef::U64 => "u64 (unsigned 64-bit integer)".to_string(),
478        ScSpecTypeDef::I64 => "i64 (signed 64-bit integer)".to_string(),
479        ScSpecTypeDef::U128 => "u128 (unsigned 128-bit integer)".to_string(),
480        ScSpecTypeDef::I128 => "i128 (signed 128-bit integer)".to_string(),
481        ScSpecTypeDef::U32 => "u32 (unsigned 32-bit integer)".to_string(),
482        ScSpecTypeDef::I32 => "i32 (signed 32-bit integer)".to_string(),
483        ScSpecTypeDef::U256 => "u256 (unsigned 256-bit integer)".to_string(),
484        ScSpecTypeDef::I256 => "i256 (signed 256-bit integer)".to_string(),
485        ScSpecTypeDef::Bool => "bool (true/false)".to_string(),
486        ScSpecTypeDef::Symbol => "symbol (identifier)".to_string(),
487        ScSpecTypeDef::String => "string".to_string(),
488        ScSpecTypeDef::Bytes => "bytes (raw binary data)".to_string(),
489        ScSpecTypeDef::BytesN(n) => format!("bytes{} (exactly {} bytes)", n.n, n.n),
490        ScSpecTypeDef::Address => {
491            "address (G... for account, C... for contract, or identity name)".to_string()
492        }
493        ScSpecTypeDef::MuxedAddress => "muxed address (M... or identity name)".to_string(),
494        ScSpecTypeDef::Void => "void (no value)".to_string(),
495        ScSpecTypeDef::Error => "error".to_string(),
496        ScSpecTypeDef::Timepoint => "timepoint (timestamp)".to_string(),
497        ScSpecTypeDef::Duration => "duration (time span)".to_string(),
498        ScSpecTypeDef::Option(inner) => format!("optional {}", get_type_name(&inner.value_type)),
499        ScSpecTypeDef::Vec(inner) => format!("vector of {}", get_type_name(&inner.element_type)),
500        ScSpecTypeDef::Map(map_type) => format!(
501            "map from {} to {}",
502            get_type_name(&map_type.key_type),
503            get_type_name(&map_type.value_type)
504        ),
505        ScSpecTypeDef::Tuple(tuple_type) => {
506            let types: Vec<String> = tuple_type.value_types.iter().map(get_type_name).collect();
507            format!("tuple({})", types.join(", "))
508        }
509        ScSpecTypeDef::Result(_) => "result".to_string(),
510        ScSpecTypeDef::Udt(udt) => {
511            format!("user-defined type '{}'", udt.name.to_utf8_string_lossy())
512        }
513    }
514}
515
516/// Gets available function names for error messages
517fn get_available_functions(spec: &Spec) -> String {
518    match spec.find_functions() {
519        Ok(functions) => functions
520            .map(|f| f.name.to_utf8_string_lossy())
521            .collect::<Vec<_>>()
522            .join(", "),
523        Err(_) => "unknown".to_string(),
524    }
525}
526
527/// Checks if a type is a primitive type that doesn't require JSON validation
528fn is_primitive_type(type_def: &ScSpecTypeDef) -> bool {
529    matches!(
530        type_def,
531        ScSpecTypeDef::U32
532            | ScSpecTypeDef::U64
533            | ScSpecTypeDef::U128
534            | ScSpecTypeDef::U256
535            | ScSpecTypeDef::I32
536            | ScSpecTypeDef::I64
537            | ScSpecTypeDef::I128
538            | ScSpecTypeDef::I256
539            | ScSpecTypeDef::Bool
540            | ScSpecTypeDef::Symbol
541            | ScSpecTypeDef::String
542            | ScSpecTypeDef::Bytes
543            | ScSpecTypeDef::BytesN(_)
544            | ScSpecTypeDef::Address
545            | ScSpecTypeDef::MuxedAddress
546            | ScSpecTypeDef::Timepoint
547            | ScSpecTypeDef::Duration
548            | ScSpecTypeDef::Void
549    )
550}
551
552/// Generates context-aware suggestions based on the expected type and error
553fn get_context_suggestions(expected_type: &ScSpecTypeDef, received_value: &str) -> String {
554    match expected_type {
555        ScSpecTypeDef::U64 | ScSpecTypeDef::I64 | ScSpecTypeDef::U128 | ScSpecTypeDef::I128
556        | ScSpecTypeDef::U32 | ScSpecTypeDef::I32 | ScSpecTypeDef::U256 | ScSpecTypeDef::I256 => {
557            if received_value.starts_with('"') && received_value.ends_with('"') {
558                "For numbers, ensure no quotes around the value (e.g., use 100 instead of \"100\")".to_string()
559            } else if received_value.contains('.') {
560                "Integer types don't support decimal values - use a whole number".to_string()
561            } else {
562                "Ensure the value is a valid integer within the type's range".to_string()
563            }
564        }
565        ScSpecTypeDef::Bool => {
566            "For booleans, use 'true' or 'false' (without quotes)".to_string()
567        }
568        ScSpecTypeDef::String => {
569            if !received_value.starts_with('"') || !received_value.ends_with('"') {
570                "For strings, ensure the value is properly quoted (e.g., \"hello world\")".to_string()
571            } else {
572                "Check for proper string escaping if the string contains special characters".to_string()
573            }
574        }
575        ScSpecTypeDef::Address => {
576            "For addresses, use format: G... (account), C... (contract), or identity name (e.g., alice)".to_string()
577        }
578        ScSpecTypeDef::MuxedAddress => {
579            "For muxed addresses, use format: M... or identity name".to_string()
580        }
581        ScSpecTypeDef::Vec(_) => {
582            "For arrays, use JSON array format: [\"item1\", \"item2\"] or [{\"key\": \"value\"}]".to_string()
583        }
584        ScSpecTypeDef::Map(_) => {
585            "For maps, use JSON object format: {\"key1\": \"value1\", \"key2\": \"value2\"}".to_string()
586        }
587        ScSpecTypeDef::Option(_) => {
588            "For optional values, use null for none or the expected value type".to_string()
589        }
590        _ => {
591            "Check the contract specification for the correct argument format and type".to_string()
592        }
593    }
594}
595
596/// Enhanced argument parsing with better error handling
597fn parse_argument_with_validation(
598    arg_name: &str,
599    value: &str,
600    expected_type: &ScSpecTypeDef,
601    spec: &Spec,
602    config: &config::Args,
603) -> Result<ScVal, Error> {
604    let expected_type_name = get_type_name(expected_type);
605
606    // Pre-validate JSON for non-primitive types
607    if !is_primitive_type(expected_type) {
608        validate_json_arg(arg_name, value)?;
609    }
610
611    // Handle special address types
612    if matches!(
613        expected_type,
614        ScSpecTypeDef::Address | ScSpecTypeDef::MuxedAddress
615    ) {
616        let trimmed_value = value.trim_matches('"');
617        let addr = resolve_address(trimmed_value, config)?;
618        return spec
619            .from_string(&addr, expected_type)
620            .map_err(|error| Error::CannotParseArg {
621                arg: arg_name.to_string(),
622                error,
623                expected_type: expected_type_name.clone(),
624                received_value: value.to_string(),
625                suggestion: get_context_suggestions(expected_type, value),
626            });
627    }
628
629    // Parse the argument
630    spec.from_string(value, expected_type)
631        .map_err(|error| Error::CannotParseArg {
632            arg: arg_name.to_string(),
633            error,
634            expected_type: expected_type_name,
635            received_value: value.to_string(),
636            suggestion: get_context_suggestions(expected_type, value),
637        })
638}
639
640#[cfg(test)]
641mod tests {
642    use super::*;
643    use stellar_xdr::curr::{ScSpecTypeBytesN, ScSpecTypeDef, ScSpecTypeOption, ScSpecTypeVec};
644
645    #[test]
646    fn test_get_type_name_primitives() {
647        assert_eq!(
648            get_type_name(&ScSpecTypeDef::U32),
649            "u32 (unsigned 32-bit integer)"
650        );
651        assert_eq!(
652            get_type_name(&ScSpecTypeDef::I64),
653            "i64 (signed 64-bit integer)"
654        );
655        assert_eq!(get_type_name(&ScSpecTypeDef::Bool), "bool (true/false)");
656        assert_eq!(get_type_name(&ScSpecTypeDef::String), "string");
657        assert_eq!(
658            get_type_name(&ScSpecTypeDef::Address),
659            "address (G... for account, C... for contract, or identity name)"
660        );
661    }
662
663    #[test]
664    fn test_get_type_name_complex() {
665        let option_type = ScSpecTypeDef::Option(Box::new(ScSpecTypeOption {
666            value_type: Box::new(ScSpecTypeDef::U32),
667        }));
668        assert_eq!(
669            get_type_name(&option_type),
670            "optional u32 (unsigned 32-bit integer)"
671        );
672
673        let vec_type = ScSpecTypeDef::Vec(Box::new(ScSpecTypeVec {
674            element_type: Box::new(ScSpecTypeDef::String),
675        }));
676        assert_eq!(get_type_name(&vec_type), "vector of string");
677    }
678
679    #[test]
680    fn test_is_primitive_type_all_primitives() {
681        assert!(is_primitive_type(&ScSpecTypeDef::U32));
682        assert!(is_primitive_type(&ScSpecTypeDef::I32));
683        assert!(is_primitive_type(&ScSpecTypeDef::U64));
684        assert!(is_primitive_type(&ScSpecTypeDef::I64));
685        assert!(is_primitive_type(&ScSpecTypeDef::U128));
686        assert!(is_primitive_type(&ScSpecTypeDef::I128));
687        assert!(is_primitive_type(&ScSpecTypeDef::U256));
688        assert!(is_primitive_type(&ScSpecTypeDef::I256));
689
690        assert!(is_primitive_type(&ScSpecTypeDef::Bool));
691        assert!(is_primitive_type(&ScSpecTypeDef::Symbol));
692        assert!(is_primitive_type(&ScSpecTypeDef::String));
693        assert!(is_primitive_type(&ScSpecTypeDef::Void));
694        assert!(is_primitive_type(&ScSpecTypeDef::Bytes));
695        assert!(is_primitive_type(&ScSpecTypeDef::BytesN(
696            ScSpecTypeBytesN { n: 32 }
697        )));
698        assert!(is_primitive_type(&ScSpecTypeDef::BytesN(
699            ScSpecTypeBytesN { n: 64 }
700        )));
701
702        assert!(is_primitive_type(&ScSpecTypeDef::Address));
703        assert!(is_primitive_type(&ScSpecTypeDef::MuxedAddress));
704        assert!(is_primitive_type(&ScSpecTypeDef::Timepoint));
705        assert!(is_primitive_type(&ScSpecTypeDef::Duration));
706
707        assert!(!is_primitive_type(&ScSpecTypeDef::Vec(Box::new(
708            ScSpecTypeVec {
709                element_type: Box::new(ScSpecTypeDef::U32),
710            }
711        ))));
712    }
713
714    #[test]
715    fn test_validate_json_arg_valid() {
716        // Valid JSON should not return an error
717        assert!(validate_json_arg("test_arg", r#"{"key": "value"}"#).is_ok());
718        assert!(validate_json_arg("test_arg", "123").is_ok());
719        assert!(validate_json_arg("test_arg", r#""string""#).is_ok());
720        assert!(validate_json_arg("test_arg", "true").is_ok());
721        assert!(validate_json_arg("test_arg", "null").is_ok());
722    }
723
724    #[test]
725    fn test_validate_json_arg_invalid() {
726        // Invalid JSON should return an error
727        let result = validate_json_arg("test_arg", r#"{"key": value}"#); // Missing quotes around value
728        assert!(result.is_err());
729
730        if let Err(Error::InvalidJsonArg {
731            arg,
732            json_error,
733            received_value,
734        }) = result
735        {
736            assert_eq!(arg, "test_arg");
737            assert_eq!(received_value, r#"{"key": value}"#);
738            assert!(json_error.contains("expected"));
739        } else {
740            panic!("Expected InvalidJsonArg error");
741        }
742    }
743
744    #[test]
745    fn test_validate_json_arg_malformed() {
746        // Test various malformed JSON cases
747        let test_cases = vec![
748            r#"{"key": }"#,         // Missing value
749            r#"{key: "value"}"#,    // Missing quotes around key
750            r#"{"key": "value",}"#, // Trailing comma
751            r#"{"key" "value"}"#,   // Missing colon
752        ];
753
754        for case in test_cases {
755            let result = validate_json_arg("test_arg", case);
756            assert!(result.is_err(), "Expected error for case: {case}");
757        }
758    }
759
760    #[test]
761    fn test_context_aware_error_messages() {
762        use stellar_xdr::curr::ScSpecTypeDef;
763
764        // Test context-aware suggestions for different types
765
766        // Test u64 with quoted value
767        let suggestion = get_context_suggestions(&ScSpecTypeDef::U64, "\"100\"");
768        assert!(suggestion.contains("no quotes around the value"));
769        assert!(suggestion.contains("use 100 instead of \"100\""));
770
771        // Test u64 with decimal value
772        let suggestion = get_context_suggestions(&ScSpecTypeDef::U64, "100.5");
773        assert!(suggestion.contains("don't support decimal values"));
774
775        // Test string without quotes
776        let suggestion = get_context_suggestions(&ScSpecTypeDef::String, "hello");
777        assert!(suggestion.contains("properly quoted"));
778
779        // Test address type
780        let suggestion = get_context_suggestions(&ScSpecTypeDef::Address, "invalid_addr");
781        assert!(suggestion.contains("G... (account), C... (contract)"));
782
783        // Test boolean type
784        let suggestion = get_context_suggestions(&ScSpecTypeDef::Bool, "yes");
785        assert!(suggestion.contains("'true' or 'false'"));
786
787        println!("=== Context-Aware Error Message Examples ===");
788        println!("U64 with quotes: {suggestion}");
789
790        let decimal_suggestion = get_context_suggestions(&ScSpecTypeDef::U64, "100.5");
791        println!("U64 with decimal: {decimal_suggestion}");
792
793        let string_suggestion = get_context_suggestions(&ScSpecTypeDef::String, "hello");
794        println!("String without quotes: {string_suggestion}");
795
796        let address_suggestion = get_context_suggestions(&ScSpecTypeDef::Address, "invalid");
797        println!("Invalid address: {address_suggestion}");
798    }
799
800    #[test]
801    fn test_error_message_format() {
802        use stellar_xdr::curr::ScSpecTypeDef;
803
804        // Test that our CannotParseArg error formats correctly
805        let error = Error::CannotParseArg {
806            arg: "amount".to_string(),
807            error: soroban_spec_tools::Error::InvalidValue(Some(ScSpecTypeDef::U64)),
808            expected_type: "u64 (unsigned 64-bit integer)".to_string(),
809            received_value: "\"100\"".to_string(),
810            suggestion:
811                "For numbers, ensure no quotes around the value (e.g., use 100 instead of \"100\")"
812                    .to_string(),
813        };
814
815        let error_message = format!("{error}");
816        println!("\n=== Complete Error Message Example ===");
817        println!("{error_message}");
818
819        // Verify the error message contains all expected parts
820        assert!(error_message.contains("Failed to parse argument 'amount'"));
821        assert!(error_message.contains("Expected type u64 (unsigned 64-bit integer)"));
822        assert!(error_message.contains("received: '\"100\"'"));
823        assert!(error_message.contains("Suggestion: For numbers, ensure no quotes"));
824    }
825}