Skip to main content

thruput_client/
codec.rs

1//! Call argument and return-value codecs for contract specs and local source metadata.
2
3use std::collections::BTreeMap;
4use std::path::PathBuf;
5use thruput_json_rpc_types::hex::{FixedHexError, decode_fixed_hex, prefixed_hex};
6
7use serde_json::Value;
8use thruput_contract_spec::{
9    ContractArrayType, ContractEnumDef, ContractExceptionType, ContractField, ContractSpecId,
10    ContractSpecMethod, ContractSpecString, ContractStructDef, ContractType, ContractTypeDef,
11    ContractUnionCase, ContractUnionCaseType, ContractUnionDef, ContractVectorType,
12};
13use thruput_xdr::contract::ContractEntrypointID;
14use thruput_xdr::types::{ContractAddress, Ed25519PublicKey, SHA256Hash};
15use xdrs::{Reader as _, Writer as _};
16
17/// Contract metadata, argument encoding, or value decoding failed.
18#[derive(Debug, thiserror::Error)]
19pub enum Error {
20    #[error("contract spec finalization failed: {0}")]
21    SpecFailed(String),
22    #[error("invalid {argument}: {message}")]
23    InvalidArgument { argument: String, message: String },
24    #[error("{path}: {message}")]
25    CallEncode { path: String, message: String },
26    #[error("{path}: {message}")]
27    CallDecode { path: String, message: String },
28    #[error("{0}")]
29    Other(String),
30}
31
32/// Derive the canonical method selector from interface identity and method name.
33pub fn method_entrypoint_id(
34    interface_name: &str,
35    version: &str,
36    method: &str,
37) -> ContractEntrypointID {
38    let sig = thruput_contract_util::NormalizedMethodSignature::new(method);
39    let id = thruput_contract_util::contract_entrypoint_id(Some(interface_name), version, &sig);
40    let bytes: [u8; 8] = id.as_ref()[..8]
41        .try_into()
42        .expect("bug: entrypoint ID is always 8 bytes");
43    ContractEntrypointID::from(bytes)
44}
45
46/// Resolve a method from a supplied spec, rejecting ambiguous interface matches.
47/// Returns `None` when the method is absent; does not read files or discover projects.
48pub fn resolve_spec_method(
49    spec: &thruput_contract_spec::ContractSpec,
50    method: &str,
51) -> Result<Option<SpecMethodMetadata>, Error> {
52    let mut interface_matches = Vec::new();
53
54    for interface in spec.interfaces.iter() {
55        let matched_methods = interface
56            .methods
57            .iter()
58            .filter_map(|candidate| match spec_string_text(&candidate.name) {
59                Ok(name) if name == method => Some(Ok(candidate.clone())),
60                Ok(_) => None,
61                Err(err) => Some(Err(err)),
62            })
63            .collect::<Result<Vec<_>, Error>>()?;
64        if matched_methods.is_empty() {
65            continue;
66        }
67
68        interface_matches.push((
69            (
70                spec_string_text(&interface.name)?.to_owned(),
71                spec_string_text(&interface.vers)?.to_owned(),
72            ),
73            interface.throws.iter().cloned().collect::<Vec<_>>(),
74            matched_methods,
75        ));
76    }
77
78    if interface_matches.is_empty() {
79        return Ok(None);
80    }
81    interface_matches.sort_by(|(left, _, _), (right, _, _)| left.cmp(right));
82    if interface_matches.len() > 1 {
83        let descriptions = interface_matches
84            .iter()
85            .map(|(candidate, _, _)| format!("{} v{}", candidate.0, candidate.1))
86            .collect::<Vec<_>>()
87            .join(", ");
88        return Err(Error::Other(format!(
89            "method `{method}` matches multiple interfaces in the contract spec: \
90             {descriptions}"
91        )));
92    }
93
94    let (matched, throws, mut methods) = interface_matches
95        .into_iter()
96        .next()
97        .expect("bug: interface match count should be non-zero");
98    if methods.len() > 1 {
99        return Err(Error::SpecFailed(format!(
100            "contract spec defines `{}` v{}::{method} multiple times",
101            matched.0, matched.1
102        )));
103    }
104
105    Ok(Some(SpecMethodMetadata {
106        interface_name: matched.0,
107        version: matched.1,
108        method: methods.remove(0),
109        type_defs: spec.type_defs.iter().cloned().collect(),
110        throws,
111    }))
112}
113
114/// Type metadata used to encode arguments and decode return values.
115#[derive(Debug, Clone, PartialEq, Eq)]
116pub enum CallMetadata {
117    Spec(SpecMethodMetadata),
118    Local(ContractMetadata),
119}
120
121/// Locally parsed method metadata recovered from project source files.
122#[derive(Debug, Clone, PartialEq, Eq)]
123pub struct ContractMetadata {
124    pub interface_name: String,
125    pub version: String,
126    pub source: PathBuf,
127    pub method: MethodMetadata,
128}
129
130/// Local argument and return type names parsed from a Rust method signature.
131#[derive(Debug, Clone, PartialEq, Eq)]
132pub struct MethodMetadata {
133    pub arg_types: Vec<String>,
134    pub return_type: Option<String>,
135}
136
137/// Contract spec metadata for one resolved contract method.
138#[derive(Debug, Clone, PartialEq, Eq)]
139pub struct SpecMethodMetadata {
140    pub interface_name: String,
141    pub version: String,
142    pub method: ContractSpecMethod,
143    pub type_defs: Vec<ContractTypeDef>,
144    pub throws: Vec<ContractExceptionType>,
145}
146
147/// Named contract type definitions keyed by contract type name.
148pub type TypeDefIndex<'a> = BTreeMap<ContractSpecId, &'a ContractTypeDef>;
149
150/// Encode argument strings into contract calldata using the resolved metadata.
151pub fn encode_args(args: &[String], metadata: Option<&CallMetadata>) -> Result<Vec<u8>, Error> {
152    if let Some(metadata) = metadata {
153        return match metadata {
154            CallMetadata::Spec(metadata) => encode_spec_typed_args(args, metadata),
155            CallMetadata::Local(metadata) => encode_local_typed_args(args, &metadata.method),
156        };
157    }
158    if args.is_empty() {
159        return Ok(vec![]);
160    }
161    if args.len() == 1 {
162        let stripped = args[0].strip_prefix("0x").unwrap_or(&args[0]);
163        return hex::decode(stripped)
164            .map_err(|e| encode_value_error("calldata", format!("invalid calldata hex: {e}")));
165    }
166    Err(encode_value_error(
167        "arguments",
168        "multiple call arguments require contract metadata or a single hex-encoded calldata argument"
169            .to_owned(),
170    ))
171}
172
173/// Decode contract return bytes into JSON using the resolved metadata when available.
174pub fn decode_return(bytes: &[u8], metadata: Option<&CallMetadata>) -> Option<Value> {
175    match metadata? {
176        CallMetadata::Spec(metadata) => {
177            if matches!(metadata.method.return_type, ContractType::Void) {
178                return None;
179            }
180            let type_defs = build_type_def_index(&metadata.type_defs).ok()?;
181            decode_spec_value(&metadata.method.return_type, bytes, &type_defs).ok()
182        }
183        CallMetadata::Local(metadata) => {
184            let ty = metadata.method.return_type.as_deref()?;
185            decode_single_value(ty, bytes)
186        }
187    }
188}
189
190/// A contract abort payload decoded through exception metadata.
191///
192/// Frontends choose which fields to include in their output.
193#[derive(Debug, Clone, PartialEq)]
194pub struct DecodedAbort {
195    /// Exception type name, when the thrown type is a named spec type.
196    pub exception: Option<String>,
197    /// Decoded exception payload.
198    pub value: Value,
199    /// Crate that defines the exception, when it decoded through the
200    /// describe registry rather than the method's declared spec exceptions.
201    pub exception_crate: Option<&'static str>,
202    /// Major version of the defining crate baked into the abort selector,
203    /// when known.
204    pub exception_version: Option<&'static str>,
205}
206
207impl DecodedAbort {
208    /// Exception name annotated with its defining crate and major version,
209    /// e.g. `LibraryExn (thruput-sdk, v1)`; the bare name when the origin is unknown.
210    pub fn exception_label(&self) -> Option<String> {
211        let name = self.exception.as_deref()?;
212        match (self.exception_crate, self.exception_version) {
213            (Some(crate_name), Some(version)) => Some(format!("{name} ({crate_name}, v{version})")),
214            _ => Some(name.to_owned()),
215        }
216    }
217}
218
219pub fn is_erc20_too_large(decoded_abort: Option<&Result<DecodedAbort, AbortDecodeError>>) -> bool {
220    matches!(
221        decoded_abort,
222        Some(Ok(DecodedAbort {
223            exception: Some(exception),
224            value: Value::String(value),
225            exception_crate: Some("thruput-erc20-if"),
226            ..
227        })) if exception == "ERC20Error" && value == "TooLarge"
228    )
229}
230
231/// Why an abort buffer could not be decoded into an exception value.
232///
233/// The cases mirror the SDK's generated client error enum: `Unknown` and
234/// `BadDecode` retain the parsed selector and payload (the abort prefix was
235/// readable), while `UnparseableAbort` means even the selector could not be
236/// read and only the raw buffer is meaningful.
237#[derive(Debug, Clone, PartialEq, thiserror::Error)]
238pub enum AbortDecodeError {
239    #[error("failed to read the exception selector: {detail}")]
240    UnparseableAbort { detail: String },
241    #[error(
242        "abort selector 0x{selector:016x} does not match a declared exception or a known \
243         standard exception; payload: 0x{}", hex::encode(.payload)
244    )]
245    Unknown { selector: u64, payload: Vec<u8> },
246    #[error(
247        "abort payload for {exception} (selector 0x{selector:016x}) failed to decode: {detail}; \
248         payload: 0x{}", hex::encode(.payload)
249    )]
250    BadDecode {
251        selector: u64,
252        exception: String,
253        payload: Vec<u8>,
254        detail: String,
255    },
256}
257
258/// Decode an abort buffer (`[selector][payload]`) into the exception it
259/// carries.
260///
261/// The selector is matched against the resolved method's declared spec
262/// exceptions. Frontends may resolve an unknown selector against their own
263/// standard exception registry and call [`decode_abort_payload`].
264/// An unknown selector or a payload that does not decode cleanly against
265/// the matched type is an error carrying the parsed selector and payload;
266/// callers report it alongside the raw hex.
267pub fn decode_abort(
268    bytes: &[u8],
269    metadata: Option<&CallMetadata>,
270) -> Result<DecodedAbort, AbortDecodeError> {
271    let mut reader = xdrs::cxs::buf_reader::BufReader::new(bytes);
272    let selector: u64 = read_xdr_value(&mut reader, "abort selector").map_err(|e| {
273        AbortDecodeError::UnparseableAbort {
274            detail: e.to_string(),
275        }
276    })?;
277    let payload = &bytes[bytes.len() - reader.remaining()..];
278
279    if let Some(CallMetadata::Spec(spec)) = metadata
280        && let Some(exception) = spec.throws.iter().find(|e| e.selector == selector)
281    {
282        return decode_abort_payload(&exception.ty, &spec.type_defs, selector, payload);
283    }
284
285    Err(AbortDecodeError::Unknown {
286        selector,
287        payload: payload.to_vec(),
288    })
289}
290
291/// Decode a payload against caller-supplied exception and type definitions.
292pub fn decode_abort_payload(
293    ty: &ContractType,
294    type_defs: &[ContractTypeDef],
295    selector: u64,
296    payload: &[u8],
297) -> Result<DecodedAbort, AbortDecodeError> {
298    let exception = match ty {
299        ContractType::Named { named } => spec_string_text(&named.declared).ok().map(str::to_owned),
300        _ => None,
301    };
302    let bad_decode = |detail: String| AbortDecodeError::BadDecode {
303        selector,
304        exception: exception
305            .clone()
306            .unwrap_or_else(|| "the declared exception".to_owned()),
307        payload: payload.to_vec(),
308        detail,
309    };
310    let index = build_type_def_index(type_defs).map_err(|e| bad_decode(e.to_string()))?;
311    let mut reader = xdrs::cxs::buf_reader::BufReader::new(payload);
312    let value = decode_spec_value_from_reader(ty, &mut reader, &index, "abort value")
313        .map_err(|e| bad_decode(e.to_string()))?;
314    if reader.remaining() != 0 {
315        return Err(bad_decode(format!(
316            "{} unread byte(s) remain after decoding",
317            reader.remaining()
318        )));
319    }
320    Ok(DecodedAbort {
321        exception,
322        value,
323        exception_crate: None,
324        exception_version: None,
325    })
326}
327
328/// Parse a Z `StrKey` or base58 contract address, including in typed calldata.
329pub fn parse_contract_address(value: &str) -> Result<ContractAddress, Error> {
330    thruput_signing::strkey::parse_contract_address(value).map_err(|error| Error::InvalidArgument {
331        argument: "contract address".into(),
332        message: error.to_string(),
333    })
334}
335
336fn parse_public_key(value: &str) -> Result<Ed25519PublicKey, Error> {
337    thruput_signing::strkey::parse_public_key(value)
338        .map(Ed25519PublicKey::from)
339        .map_err(|error| Error::InvalidArgument {
340            argument: "ed25519 public key".into(),
341            message: error.to_string(),
342        })
343}
344
345/// Format a contract address as unprefixed, case-sensitive base58.
346pub fn format_contract_address(address: &ContractAddress) -> String {
347    thruput_json_rpc_types::ContractAddress::from(*address).to_string()
348}
349
350/// Render a spec id for a diagnostic, validating every segment.
351///
352/// Only for messages. Lookups compare whole [`ContractSpecId`]s, which is
353/// what keeps two same-named types in different crates or modules apart;
354/// this rendering joins the segments and so cannot be taken apart again.
355/// The rendering is `ContractSpecId`'s own `Display`, which the spec tool uses
356/// too, so the CLI and the spec tool name the same type the same way.
357pub fn spec_id_text(id: &ContractSpecId) -> Result<String, Error> {
358    for segment in [&id.crate_name, &id.major, &id.module_path, &id.declared] {
359        spec_string_text(segment)?;
360    }
361    Ok(id.to_string())
362}
363
364/// Decode a spec string as UTF-8 text and validate it as a safe identifier.
365///
366/// Spec strings originate from Rust type and method names. A string
367/// containing quotes, backslashes, or whitespace signals a malformed
368/// spec and is rejected rather than propagated into generated output.
369pub fn spec_string_text(value: &ContractSpecString) -> Result<&str, Error> {
370    let s = std::str::from_utf8(value.as_ref())
371        .map_err(|e| Error::SpecFailed(format!("contract spec contains invalid UTF-8: {e}")))?;
372    if let Some(bad) = s
373        .chars()
374        .find(|&c| c == '"' || c == '\\' || c.is_whitespace())
375    {
376        return Err(Error::SpecFailed(format!(
377            "contract spec name `{s}` contains invalid character `{bad}`"
378        )));
379    }
380    Ok(s)
381}
382
383/// Build an index of named embedded type definitions.
384pub fn build_type_def_index(type_defs: &[ContractTypeDef]) -> Result<TypeDefIndex<'_>, Error> {
385    let mut index = BTreeMap::new();
386
387    for type_def in type_defs {
388        let id = spec_type_def_id(type_def)?;
389        if index.insert(id.clone(), type_def).is_some() {
390            return Err(Error::SpecFailed(format!(
391                "contract spec defines `{}` multiple times",
392                spec_id_text(id)?
393            )));
394        }
395    }
396
397    Ok(index)
398}
399
400/// Decode one top-level contract spec value from a byte slice.
401pub fn decode_spec_value(
402    ty: &ContractType,
403    bytes: &[u8],
404    type_defs: &TypeDefIndex<'_>,
405) -> Result<Value, Error> {
406    let mut reader = xdrs::cxs::buf_reader::BufReader::new(bytes);
407    let value = decode_spec_value_from_reader(ty, &mut reader, type_defs, "return value")?;
408    if reader.remaining() != 0 {
409        return Err(decode_value_error(
410            "return value",
411            format!(
412                "{} unread byte(s) remain after decoding",
413                reader.remaining()
414            ),
415        ));
416    }
417    Ok(value)
418}
419
420/// Decode one contract spec value from the current XDR reader position.
421pub fn decode_spec_value_from_reader(
422    ty: &ContractType,
423    reader: &mut xdrs::cxs::buf_reader::BufReader<'_>,
424    type_defs: &TypeDefIndex<'_>,
425    path: &str,
426) -> Result<Value, Error> {
427    decode_spec_value_from_reader_with_policy(
428        ty,
429        reader,
430        type_defs,
431        path,
432        &mut DecodePolicy {
433            strict_enums: false,
434            remaining_values: None,
435        },
436    )
437}
438
439/// Decode one event value while rejecting enum discriminants absent from the spec.
440pub fn decode_event_spec_value_from_reader(
441    ty: &ContractType,
442    reader: &mut xdrs::cxs::buf_reader::BufReader<'_>,
443    type_defs: &TypeDefIndex<'_>,
444    path: &str,
445    remaining_values: &mut usize,
446) -> Result<Value, Error> {
447    decode_spec_value_from_reader_with_policy(
448        ty,
449        reader,
450        type_defs,
451        path,
452        &mut DecodePolicy {
453            strict_enums: true,
454            remaining_values: Some(remaining_values),
455        },
456    )
457}
458
459struct DecodePolicy<'a> {
460    strict_enums: bool,
461    remaining_values: Option<&'a mut usize>,
462}
463
464fn decode_spec_value_from_reader_with_policy(
465    ty: &ContractType,
466    reader: &mut xdrs::cxs::buf_reader::BufReader<'_>,
467    type_defs: &TypeDefIndex<'_>,
468    path: &str,
469    policy: &mut DecodePolicy<'_>,
470) -> Result<Value, Error> {
471    if let ContractType::Option { option_inner } = ty {
472        return decode_option_value_from_reader(
473            option_inner.as_ref(),
474            reader,
475            type_defs,
476            path,
477            policy,
478        );
479    }
480    charge_decoded_values(policy, path, 1)?;
481    if let Some(decoded) = decode_spec_scalar_value_from_reader(ty, reader, path) {
482        return decoded;
483    }
484
485    match ty {
486        ContractType::Vector { vector } => {
487            decode_vector_value_from_reader(vector, reader, type_defs, path, policy)
488        }
489        ContractType::Tuple { tuple } => {
490            ensure_decoded_values(policy, path, tuple.len())?;
491            let mut values = Vec::with_capacity(tuple.len());
492            for (index, item) in tuple.iter().enumerate() {
493                values.push(decode_spec_value_from_reader_with_policy(
494                    item,
495                    reader,
496                    type_defs,
497                    &index_path(path, index),
498                    policy,
499                )?);
500            }
501            Ok(Value::Array(values))
502        }
503        ContractType::Named { named } => {
504            decode_named_value_from_reader(named, reader, type_defs, path, policy)
505        }
506        ContractType::Array { array } => {
507            decode_array_value_from_reader(array, reader, type_defs, path, policy)
508        }
509        _ => Err(Error::SpecFailed(
510            "contract spec contains an unsupported contract type".into(),
511        )),
512    }
513}
514
515/// Normalize supported Rust syntax from locally parsed type names.
516pub fn normalize_type_name(ty: &str) -> String {
517    let mut ty = ty.trim();
518
519    while let Some(stripped) = ty.strip_prefix('&') {
520        ty = stripped.trim_start();
521        if let Some(lifetime_rest) = ty.strip_prefix('\'') {
522            ty = lifetime_rest
523                .find(char::is_whitespace)
524                .map_or(lifetime_rest, |idx| &lifetime_rest[idx..])
525                .trim_start();
526        }
527        if let Some(stripped) = ty.strip_prefix("mut ") {
528            ty = stripped.trim_start();
529        }
530    }
531
532    match ty {
533        "thruput_sdk::U256" | "sdk::U256" | "groundhog_sdk::U256" => "U256".into(),
534        "thruput_sdk::I256" | "sdk::I256" | "groundhog_sdk::I256" => "I256".into(),
535        _ => ty.to_owned(),
536    }
537}
538
539fn encode_spec_typed_args(
540    args: &[String],
541    metadata: &SpecMethodMetadata,
542) -> Result<Vec<u8>, Error> {
543    if args.len() != metadata.method.args.len() {
544        return Err(encode_value_error(
545            "arguments",
546            format!(
547                "method expects {} argument(s) but {} were provided",
548                metadata.method.args.len(),
549                args.len()
550            ),
551        ));
552    }
553    if metadata.method.args.is_empty() {
554        return Ok(vec![]);
555    }
556
557    let type_defs = build_type_def_index(&metadata.type_defs)?;
558    let mut encoded = Vec::new();
559
560    for (index, (arg, field)) in args.iter().zip(metadata.method.args.iter()).enumerate() {
561        let field_name = spec_string_text(&field.name)?;
562        let path = format!("argument {} (`{field_name}`)", index + 1);
563        encoded.extend(encode_cli_spec_value(&field.ty, arg, &type_defs, &path)?);
564    }
565
566    Ok(encoded)
567}
568
569fn encode_local_typed_args(args: &[String], method: &MethodMetadata) -> Result<Vec<u8>, Error> {
570    if args.len() != method.arg_types.len() {
571        return Err(encode_value_error(
572            "arguments",
573            format!(
574                "method expects {} argument(s) but {} were provided",
575                method.arg_types.len(),
576                args.len()
577            ),
578        ));
579    }
580    if method.arg_types.is_empty() {
581        return Ok(vec![]);
582    }
583
584    let mut encoded = Vec::new();
585    for (index, (arg, ty)) in args.iter().zip(&method.arg_types).enumerate() {
586        let path = format!("argument {}", index + 1);
587        encoded.extend(encode_single_value(ty, arg, &path)?);
588    }
589    Ok(encoded)
590}
591
592fn spec_type_def_id(type_def: &ContractTypeDef) -> Result<&ContractSpecId, Error> {
593    match type_def {
594        ContractTypeDef::TypeDefStruct { struct_def } => Ok(&struct_def.name),
595        ContractTypeDef::TypeDefEnum { enum_def } => Ok(&enum_def.name),
596        ContractTypeDef::TypeDefUnion { union_def } => Ok(&union_def.name),
597        _ => Err(Error::SpecFailed(
598            "contract spec contains an unsupported type definition kind".into(),
599        )),
600    }
601}
602
603fn resolve_named_type_def<'a>(
604    named: &ContractSpecId,
605    type_defs: &'a TypeDefIndex<'a>,
606) -> Result<&'a ContractTypeDef, Error> {
607    match type_defs.get(named).copied() {
608        Some(type_def) => Ok(type_def),
609        None => Err(Error::SpecFailed(format!(
610            "contract spec is missing `{}`",
611            spec_id_text(named)?
612        ))),
613    }
614}
615
616fn encode_cli_spec_value(
617    ty: &ContractType,
618    argument: &str,
619    type_defs: &TypeDefIndex<'_>,
620    path: &str,
621) -> Result<Vec<u8>, Error> {
622    match ty {
623        ContractType::Void => Err(encode_value_error(
624            path,
625            "void is not a valid contract argument type",
626        )),
627        ContractType::Option { option_inner } => {
628            let mut encoded = encode_xdr_value(argument != "null")
629                .map_err(|e| encode_value_error(path, e.to_string()))?;
630            if argument != "null" {
631                encoded.extend(encode_cli_spec_value(
632                    option_inner.as_ref(),
633                    argument,
634                    type_defs,
635                    path,
636                )?);
637            }
638            Ok(encoded)
639        }
640        ContractType::Vector { vector } if vector_contains_u8(&vector.element) => {
641            let bytes = parse_bytes_argument(argument, path)?;
642            if bytes.len() > vector.max_len as usize {
643                return Err(encode_value_error(
644                    path,
645                    format!(
646                        "byte vector exceeds max length {} (got {})",
647                        vector.max_len,
648                        bytes.len()
649                    ),
650                ));
651            }
652            encode_variable_bytes(bytes, path)
653        }
654        ContractType::Array { array } if vector_contains_u8(&array.element) => {
655            let bytes = parse_bytes_argument(argument, path)?;
656            if bytes.len() != array.len as usize {
657                return Err(encode_value_error(
658                    path,
659                    format!(
660                        "byte array expects length {} but got {}",
661                        array.len,
662                        bytes.len()
663                    ),
664                ));
665            }
666            encode_fixed_bytes(&bytes, path)
667        }
668        ContractType::Tuple { .. } | ContractType::Vector { .. } | ContractType::Array { .. } => {
669            let json = parse_json_argument(argument, path)?;
670            encode_json_spec_value(ty, &json, type_defs, path)
671        }
672        ContractType::Named { named } => match resolve_named_type_def(named, type_defs)? {
673            ContractTypeDef::TypeDefStruct { .. } => {
674                let json = parse_json_argument(argument, path)?;
675                encode_json_spec_value(ty, &json, type_defs, path)
676            }
677            ContractTypeDef::TypeDefEnum { enum_def } => {
678                encode_enum_cli_value(enum_def, argument, path)
679            }
680            ContractTypeDef::TypeDefUnion { union_def } => {
681                encode_union_cli_value(union_def, argument, type_defs, path)
682            }
683            _ => Err(Error::SpecFailed(
684                "contract spec contains an unsupported type definition kind".into(),
685            )),
686        },
687        _ => encode_scalar_cli_value(ty, argument, path),
688    }
689}
690
691fn encode_json_spec_value(
692    ty: &ContractType,
693    value: &Value,
694    type_defs: &TypeDefIndex<'_>,
695    path: &str,
696) -> Result<Vec<u8>, Error> {
697    if let Some(encoded) = encode_json_scalar_value(ty, value, path) {
698        return encoded;
699    }
700
701    match ty {
702        ContractType::Option { option_inner } => {
703            encode_option_json_value(option_inner.as_ref(), value, type_defs, path)
704        }
705        ContractType::Vector { vector } => encode_vector_json_value(vector, value, type_defs, path),
706        ContractType::Tuple { tuple } => encode_tuple_json_items(tuple, value, type_defs, path),
707        ContractType::Named { named } => encode_named_json_value(named, value, type_defs, path),
708        ContractType::Array { array } => encode_array_json_value(array, value, type_defs, path),
709        _ => Err(Error::SpecFailed(
710            "contract spec contains an unsupported contract type".into(),
711        )),
712    }
713}
714
715fn encode_json_scalar_value(
716    ty: &ContractType,
717    value: &Value,
718    path: &str,
719) -> Option<Result<Vec<u8>, Error>> {
720    match ty {
721        ContractType::Void => Some(match value {
722            Value::Null => Ok(vec![]),
723            _ => Err(encode_value_error(path, "expected null for a void value")),
724        }),
725        ContractType::Bool => Some(parse_json_bool(value, path).and_then(|value| {
726            encode_xdr_value(value).map_err(|e| encode_value_error(path, e.to_string()))
727        })),
728        ContractType::U8 => Some(
729            parse_json_numeric::<u8>(value, "u8", path).and_then(|value| {
730                encode_xdr_value(u32::from(value))
731                    .map_err(|e| encode_value_error(path, e.to_string()))
732            }),
733        ),
734        ContractType::U32 => Some(parse_json_numeric::<u32>(value, "u32", path).and_then(
735            |value| encode_xdr_value(value).map_err(|e| encode_value_error(path, e.to_string())),
736        )),
737        ContractType::I32 => Some(parse_json_numeric::<i32>(value, "i32", path).and_then(
738            |value| encode_xdr_value(value).map_err(|e| encode_value_error(path, e.to_string())),
739        )),
740        ContractType::U64 => Some(parse_json_numeric::<u64>(value, "u64", path).and_then(
741            |value| encode_xdr_value(value).map_err(|e| encode_value_error(path, e.to_string())),
742        )),
743        ContractType::I64 => Some(parse_json_numeric::<i64>(value, "i64", path).and_then(
744            |value| encode_xdr_value(value).map_err(|e| encode_value_error(path, e.to_string())),
745        )),
746        ContractType::U128 => Some(parse_json_numeric::<u128>(value, "u128", path).and_then(
747            |value| encode_xdr_value(value).map_err(|e| encode_value_error(path, e.to_string())),
748        )),
749        ContractType::I128 => Some(parse_json_numeric::<i128>(value, "i128", path).and_then(
750            |value| encode_xdr_value(value).map_err(|e| encode_value_error(path, e.to_string())),
751        )),
752        ContractType::U256 => Some(
753            parse_json_numeric::<thruput_u256::U256>(value, "U256", path).and_then(|value| {
754                encode_xdr_value(value).map_err(|e| encode_value_error(path, e.to_string()))
755            }),
756        ),
757        ContractType::I256 => Some(
758            parse_json_numeric::<thruput_u256::I256>(value, "I256", path).and_then(|value| {
759                encode_xdr_value(value).map_err(|e| encode_value_error(path, e.to_string()))
760            }),
761        ),
762        ContractType::String => Some(parse_json_string(value, path).and_then(|value| {
763            encode_xdr_value(value.to_owned()).map_err(|e| encode_value_error(path, e.to_string()))
764        })),
765        ContractType::Bytes => Some(
766            parse_bytes_json_value(value, path)
767                .and_then(|bytes| encode_variable_bytes(bytes, path)),
768        ),
769        ContractType::Address => Some(parse_json_string(value, path).and_then(|value| {
770            let address = parse_contract_address(value)?;
771            encode_xdr_value(address).map_err(|e| encode_value_error(path, e.to_string()))
772        })),
773        _ => None,
774    }
775}
776
777fn encode_option_json_value(
778    inner: &ContractType,
779    value: &Value,
780    type_defs: &TypeDefIndex<'_>,
781    path: &str,
782) -> Result<Vec<u8>, Error> {
783    let mut encoded =
784        encode_xdr_value(!value.is_null()).map_err(|e| encode_value_error(path, e.to_string()))?;
785    if !value.is_null() {
786        encoded.extend(encode_json_spec_value(inner, value, type_defs, path)?);
787    }
788    Ok(encoded)
789}
790
791fn encode_vector_json_value(
792    vector: &ContractVectorType,
793    value: &Value,
794    type_defs: &TypeDefIndex<'_>,
795    path: &str,
796) -> Result<Vec<u8>, Error> {
797    let inner = vector.element.as_ref();
798    if vector_contains_u8(inner) {
799        let bytes = parse_bytes_json_value(value, path)?;
800        if bytes.len() > vector.max_len as usize {
801            return Err(encode_value_error(
802                path,
803                format!(
804                    "byte vector exceeds max length {} (got {})",
805                    vector.max_len,
806                    bytes.len()
807                ),
808            ));
809        }
810        return encode_variable_bytes(bytes, path);
811    }
812
813    let items = parse_json_array(value, path)?;
814    if items.len() > vector.max_len as usize {
815        return Err(encode_value_error(
816            path,
817            format!(
818                "vector exceeds max length {} (got {})",
819                vector.max_len,
820                items.len()
821            ),
822        ));
823    }
824
825    let mut encoded =
826        encode_xdr_value(u32::try_from(items.len()).map_err(|_| {
827            encode_value_error(path, "vector length exceeds the supported u32 range")
828        })?)
829        .map_err(|e| encode_value_error(path, e.to_string()))?;
830    for (index, item) in items.iter().enumerate() {
831        encoded.extend(encode_json_spec_value(
832            inner,
833            item,
834            type_defs,
835            &index_path(path, index),
836        )?);
837    }
838    Ok(encoded)
839}
840
841fn encode_named_json_value(
842    named: &ContractSpecId,
843    value: &Value,
844    type_defs: &TypeDefIndex<'_>,
845    path: &str,
846) -> Result<Vec<u8>, Error> {
847    match resolve_named_type_def(named, type_defs)? {
848        ContractTypeDef::TypeDefStruct { struct_def } => {
849            encode_struct_json_value(struct_def, value, type_defs, path)
850        }
851        ContractTypeDef::TypeDefEnum { enum_def } => encode_enum_json_value(enum_def, value, path),
852        ContractTypeDef::TypeDefUnion { union_def } => {
853            encode_union_json_value(union_def, value, type_defs, path)
854        }
855        _ => Err(Error::SpecFailed(
856            "contract spec contains an unsupported type definition kind".into(),
857        )),
858    }
859}
860
861fn encode_array_json_value(
862    array: &ContractArrayType,
863    value: &Value,
864    type_defs: &TypeDefIndex<'_>,
865    path: &str,
866) -> Result<Vec<u8>, Error> {
867    let inner = array.element.as_ref();
868    if vector_contains_u8(inner) {
869        let bytes = parse_bytes_json_value(value, path)?;
870        if bytes.len() != array.len as usize {
871            return Err(encode_value_error(
872                path,
873                format!(
874                    "byte array expects length {} but got {}",
875                    array.len,
876                    bytes.len()
877                ),
878            ));
879        }
880        return encode_fixed_bytes(&bytes, path);
881    }
882
883    let items = parse_json_array(value, path)?;
884    if items.len() != array.len as usize {
885        return Err(encode_value_error(
886            path,
887            format!("array expects length {} but got {}", array.len, items.len()),
888        ));
889    }
890
891    let mut encoded = Vec::new();
892    for (index, item) in items.iter().enumerate() {
893        encoded.extend(encode_json_spec_value(
894            inner,
895            item,
896            type_defs,
897            &index_path(path, index),
898        )?);
899    }
900    Ok(encoded)
901}
902
903fn encode_struct_json_value(
904    struct_def: &ContractStructDef,
905    value: &Value,
906    type_defs: &TypeDefIndex<'_>,
907    path: &str,
908) -> Result<Vec<u8>, Error> {
909    encode_field_list(struct_def.fields.iter(), value, type_defs, path)
910}
911
912fn encode_field_list<'a>(
913    fields: impl ExactSizeIterator<Item = &'a ContractField>,
914    value: &Value,
915    type_defs: &TypeDefIndex<'_>,
916    path: &str,
917) -> Result<Vec<u8>, Error> {
918    let fields = fields.collect::<Vec<_>>();
919    match value {
920        Value::Array(values) => {
921            if values.len() != fields.len() {
922                return Err(encode_value_error(
923                    path,
924                    format!(
925                        "expected {} field value(s) but got {}",
926                        fields.len(),
927                        values.len()
928                    ),
929                ));
930            }
931
932            let mut encoded = Vec::new();
933            for (index, (field, value)) in fields.iter().zip(values).enumerate() {
934                encoded.extend(encode_json_spec_value(
935                    &field.ty,
936                    value,
937                    type_defs,
938                    &index_path(path, index),
939                )?);
940            }
941            Ok(encoded)
942        }
943        Value::Object(object) => {
944            let expected_names = fields
945                .iter()
946                .map(|field| spec_string_text(&field.name))
947                .collect::<Result<Vec<_>, _>>()?;
948            for key in object.keys() {
949                if !expected_names.iter().any(|expected| expected == key) {
950                    return Err(encode_value_error(
951                        path,
952                        format!("unexpected field `{key}`"),
953                    ));
954                }
955            }
956
957            let mut encoded = Vec::new();
958            for field in fields {
959                let field_name = spec_string_text(&field.name)?;
960                let field_value = object.get(field_name).ok_or_else(|| {
961                    encode_value_error(path, format!("missing field `{field_name}`"))
962                })?;
963                encoded.extend(encode_json_spec_value(
964                    &field.ty,
965                    field_value,
966                    type_defs,
967                    &field_path(path, field_name),
968                )?);
969            }
970            Ok(encoded)
971        }
972        _ => Err(encode_value_error(path, "expected a JSON object or array")),
973    }
974}
975
976fn encode_tuple_json_items(
977    tuple: &[ContractType],
978    value: &Value,
979    type_defs: &TypeDefIndex<'_>,
980    path: &str,
981) -> Result<Vec<u8>, Error> {
982    let items = parse_json_array(value, path)?;
983    if items.len() != tuple.len() {
984        return Err(encode_value_error(
985            path,
986            format!(
987                "expected {} tuple item(s) but got {}",
988                tuple.len(),
989                items.len()
990            ),
991        ));
992    }
993
994    let mut encoded = Vec::new();
995    for (index, (ty, item)) in tuple.iter().zip(items).enumerate() {
996        encoded.extend(encode_json_spec_value(
997            ty,
998            item,
999            type_defs,
1000            &index_path(path, index),
1001        )?);
1002    }
1003    Ok(encoded)
1004}
1005
1006fn encode_enum_cli_value(
1007    enum_def: &ContractEnumDef,
1008    argument: &str,
1009    path: &str,
1010) -> Result<Vec<u8>, Error> {
1011    if let Ok(json) = serde_json::from_str::<Value>(argument)
1012        && matches!(json, Value::String(_) | Value::Number(_))
1013    {
1014        return encode_enum_json_value(enum_def, &json, path);
1015    }
1016    encode_enum_json_value(enum_def, &Value::String(argument.to_owned()), path)
1017}
1018
1019fn encode_enum_json_value(
1020    enum_def: &ContractEnumDef,
1021    value: &Value,
1022    path: &str,
1023) -> Result<Vec<u8>, Error> {
1024    let discriminant = match value {
1025        Value::String(name) => {
1026            if let Some(discriminant) = enum_def.variants.iter().find_map(|variant| {
1027                let variant_name = spec_string_text(&variant.name).ok()?;
1028                (variant_name == name).then_some(variant.value)
1029            }) {
1030                discriminant
1031            } else {
1032                name.parse::<i32>().map_err(|_| {
1033                    encode_value_error(path, format!("unknown enum variant `{name}`"))
1034                })?
1035            }
1036        }
1037        Value::Number(number) => number
1038            .as_i64()
1039            .and_then(|value| i32::try_from(value).ok())
1040            .ok_or_else(|| encode_value_error(path, "enum discriminants must fit in i32"))?,
1041        _ => {
1042            return Err(encode_value_error(
1043                path,
1044                "expected an enum variant name or discriminant",
1045            ));
1046        }
1047    };
1048
1049    encode_xdr_value(discriminant).map_err(|e| encode_value_error(path, e.to_string()))
1050}
1051
1052fn encode_union_cli_value(
1053    union_def: &ContractUnionDef,
1054    argument: &str,
1055    type_defs: &TypeDefIndex<'_>,
1056    path: &str,
1057) -> Result<Vec<u8>, Error> {
1058    if let Ok(json) = serde_json::from_str::<Value>(argument) {
1059        return encode_union_json_value(union_def, &json, type_defs, path);
1060    }
1061
1062    let case = resolve_union_case(union_def, &Value::String(argument.to_owned()), path)?;
1063    encode_union_case(union_def, case, None, type_defs, path)
1064}
1065
1066fn encode_union_json_value(
1067    union_def: &ContractUnionDef,
1068    value: &Value,
1069    type_defs: &TypeDefIndex<'_>,
1070    path: &str,
1071) -> Result<Vec<u8>, Error> {
1072    match value {
1073        Value::Object(object) => {
1074            let case_value = object
1075                .get("case")
1076                .or_else(|| object.get("discriminant"))
1077                .ok_or_else(|| {
1078                    encode_value_error(
1079                        path,
1080                        "union values must include a `case` (or `discriminant`) field",
1081                    )
1082                })?;
1083            let case = resolve_union_case(union_def, case_value, &field_path(path, "case"))?;
1084            encode_union_case(union_def, case, object.get("value"), type_defs, path)
1085        }
1086        Value::String(_) | Value::Number(_) => {
1087            let case = resolve_union_case(union_def, value, path)?;
1088            encode_union_case(union_def, case, None, type_defs, path)
1089        }
1090        _ => Err(encode_value_error(
1091            path,
1092            "expected a union case name/discriminant or an object with `case` and optional `value`",
1093        )),
1094    }
1095}
1096
1097fn resolve_union_case<'a>(
1098    union_def: &'a ContractUnionDef,
1099    value: &Value,
1100    path: &str,
1101) -> Result<&'a ContractUnionCase, Error> {
1102    match value {
1103        Value::String(name) => {
1104            if let Some(case) = union_def
1105                .cases
1106                .iter()
1107                .find(|case| spec_string_text(&case.name).ok() == Some(name.as_str()))
1108            {
1109                Ok(case)
1110            } else if let Ok(discriminant) = name.parse::<i32>() {
1111                union_def
1112                    .cases
1113                    .iter()
1114                    .find(|case| case.discriminant == discriminant)
1115                    .ok_or_else(|| {
1116                        encode_value_error(
1117                            path,
1118                            format!("unknown union discriminant `{discriminant}`"),
1119                        )
1120                    })
1121            } else {
1122                Err(encode_value_error(
1123                    path,
1124                    format!("unknown union case `{name}`"),
1125                ))
1126            }
1127        }
1128        Value::Number(number) => {
1129            let discriminant = number
1130                .as_i64()
1131                .and_then(|value| i32::try_from(value).ok())
1132                .ok_or_else(|| encode_value_error(path, "union discriminants must fit in i32"))?;
1133            union_def
1134                .cases
1135                .iter()
1136                .find(|case| case.discriminant == discriminant)
1137                .ok_or_else(|| {
1138                    encode_value_error(path, format!("unknown union discriminant `{discriminant}`"))
1139                })
1140        }
1141        _ => Err(encode_value_error(
1142            path,
1143            "expected a union case name or discriminant",
1144        )),
1145    }
1146}
1147
1148fn encode_union_case(
1149    union_def: &ContractUnionDef,
1150    case: &ContractUnionCase,
1151    payload: Option<&Value>,
1152    type_defs: &TypeDefIndex<'_>,
1153    path: &str,
1154) -> Result<Vec<u8>, Error> {
1155    let mut encoded =
1156        encode_xdr_value(case.discriminant).map_err(|e| encode_value_error(path, e.to_string()))?;
1157    match &case.case_type {
1158        ContractUnionCaseType::UnionCaseVoid => {
1159            if let Some(value) = payload
1160                && !value.is_null()
1161            {
1162                return Err(encode_value_error(
1163                    path,
1164                    format!(
1165                        "union case `{}` does not take a payload",
1166                        spec_string_text(&case.name)?
1167                    ),
1168                ));
1169            }
1170        }
1171        ContractUnionCaseType::UnionCaseTuple { tuple } => {
1172            let case_name = spec_string_text(&case.name)?;
1173            let payload = payload.ok_or_else(|| {
1174                encode_value_error(
1175                    path,
1176                    format!("union case `{case_name}` requires a `value` payload"),
1177                )
1178            })?;
1179            encoded.extend(encode_tuple_json_items(
1180                tuple,
1181                payload,
1182                type_defs,
1183                &field_path(path, "value"),
1184            )?);
1185        }
1186        ContractUnionCaseType::UnionCaseStruct { struct_fields } => {
1187            let case_name = spec_string_text(&case.name)?;
1188            let payload = payload.ok_or_else(|| {
1189                encode_value_error(
1190                    path,
1191                    format!("union case `{case_name}` requires a `value` payload"),
1192                )
1193            })?;
1194            encoded.extend(encode_field_list(
1195                struct_fields.iter(),
1196                payload,
1197                type_defs,
1198                &field_path(path, "value"),
1199            )?);
1200        }
1201        _ => {
1202            return Err(Error::SpecFailed(format!(
1203                "contract spec contains an unsupported union case in `{}`",
1204                spec_id_text(&union_def.name)?
1205            )));
1206        }
1207    }
1208
1209    Ok(encoded)
1210}
1211
1212fn encode_single_value(ty: &str, value: &str, path: &str) -> Result<Vec<u8>, Error> {
1213    let ty = normalize_type_name(ty);
1214    match ty.as_str() {
1215        "u16" => encode_local_xdr_value::<u16>(parse_numeric(value, &ty)?, path),
1216        "u32" => encode_local_xdr_value::<u32>(parse_numeric(value, &ty)?, path),
1217        "u64" => encode_local_xdr_value::<u64>(parse_numeric(value, &ty)?, path),
1218        "u128" => encode_local_xdr_value::<u128>(parse_numeric(value, &ty)?, path),
1219        "U256" => encode_local_xdr_value::<thruput_u256::U256>(parse_numeric(value, &ty)?, path),
1220        "i16" => encode_local_xdr_value::<i16>(parse_numeric(value, &ty)?, path),
1221        "i32" => encode_local_xdr_value::<i32>(parse_numeric(value, &ty)?, path),
1222        "i64" => encode_local_xdr_value::<i64>(parse_numeric(value, &ty)?, path),
1223        "i128" => encode_local_xdr_value::<i128>(parse_numeric(value, &ty)?, path),
1224        "I256" => encode_local_xdr_value::<thruput_u256::I256>(parse_numeric(value, &ty)?, path),
1225        "bool" => {
1226            let parsed = value
1227                .parse::<bool>()
1228                .map_err(|e| encode_value_error(path, format!("invalid bool `{value}`: {e}")))?;
1229            encode_local_xdr_value(parsed, path)
1230        }
1231        "String" | "str" => encode_local_xdr_value(value.to_string(), path),
1232        "ContractAddress" => encode_local_xdr_value(parse_contract_address(value)?, path),
1233        "Ed25519PublicKey" => encode_local_xdr_value(parse_public_key(value)?, path),
1234        "SHA256Hash" => encode_local_xdr_value(
1235            SHA256Hash::from(thruput_encoding::decode_fixed(value).map_err(|error| {
1236                Error::InvalidArgument {
1237                    argument: "sha256 hash".into(),
1238                    message: error.to_string(),
1239                }
1240            })?),
1241            path,
1242        ),
1243        "ContractEntrypointID" => encode_local_xdr_value(
1244            ContractEntrypointID::from(parse_fixed_hex::<8>(value, "contract entrypoint")?),
1245            path,
1246        ),
1247        _ => Err(encode_value_error(
1248            path,
1249            format!(
1250                "typed call encoding for `{ty}` is not implemented yet; pass an explicit \
1251                 0x-prefixed entrypoint ID and raw calldata instead"
1252            ),
1253        )),
1254    }
1255}
1256
1257fn parse_numeric<T>(value: &str, ty: &str) -> Result<T, Error>
1258where
1259    T: std::str::FromStr,
1260    T::Err: std::fmt::Display,
1261{
1262    value.parse::<T>().map_err(|e| Error::InvalidArgument {
1263        argument: format!("{ty} argument"),
1264        message: format!("`{value}` is not a valid {ty}: {e}"),
1265    })
1266}
1267
1268fn encode_xdr_value<T>(value: T) -> Result<Vec<u8>, Error>
1269where
1270    T: xdrs::WriteXDR,
1271{
1272    xdrs::cxs::encode(&value).map_err(|e| Error::Other(format!("failed to encode calldata: {e}")))
1273}
1274
1275fn encode_local_xdr_value<T>(value: T, path: &str) -> Result<Vec<u8>, Error>
1276where
1277    T: xdrs::WriteXDR,
1278{
1279    encode_xdr_value(value).map_err(|e| encode_value_error(path, e.to_string()))
1280}
1281
1282fn encode_variable_bytes(bytes: Vec<u8>, path: &str) -> Result<Vec<u8>, Error> {
1283    encode_xdr_value(bytes).map_err(|e| encode_value_error(path, e.to_string()))
1284}
1285
1286fn encode_fixed_bytes(bytes: &[u8], path: &str) -> Result<Vec<u8>, Error> {
1287    let mut writer = xdrs::cxs::buf_writer::BufWriter::with_capacity(bytes.len());
1288    writer
1289        .write_opaque_fixed(bytes)
1290        .map_err(|e| encode_value_error(path, format!("failed to encode fixed bytes: {e}")))?;
1291    Ok(writer.into())
1292}
1293
1294fn parse_json_argument(argument: &str, path: &str) -> Result<Value, Error> {
1295    serde_json::from_str(argument)
1296        .map_err(|e| encode_value_error(path, format!("must be valid JSON: {e}")))
1297}
1298
1299fn parse_json_array<'a>(value: &'a Value, path: &str) -> Result<&'a Vec<Value>, Error> {
1300    value
1301        .as_array()
1302        .ok_or_else(|| encode_value_error(path, "expected a JSON array"))
1303}
1304
1305fn parse_json_string<'a>(value: &'a Value, path: &str) -> Result<&'a str, Error> {
1306    value
1307        .as_str()
1308        .ok_or_else(|| encode_value_error(path, "expected a JSON string"))
1309}
1310
1311fn parse_json_numeric<T>(value: &Value, ty: &str, path: &str) -> Result<T, Error>
1312where
1313    T: std::str::FromStr,
1314    T::Err: std::fmt::Display,
1315{
1316    if matches!(ty, "u64" | "i64" | "u128" | "i128" | "U256" | "I256") {
1317        let text = parse_json_string(value, path)?;
1318        let digits = text.strip_prefix('-').unwrap_or(text);
1319        if text == "-0"
1320            || digits.is_empty()
1321            || (digits.len() > 1 && digits.starts_with('0'))
1322            || !digits.bytes().all(|byte| byte.is_ascii_digit())
1323        {
1324            return Err(encode_value_error(
1325                path,
1326                format!("expected a canonical decimal {ty} string"),
1327            ));
1328        }
1329        return parse_numeric(text, ty);
1330    }
1331    match value {
1332        Value::Number(number) => parse_numeric(&number.to_string(), ty),
1333        Value::String(string) => parse_numeric(string, ty),
1334        _ => Err(encode_value_error(path, format!("expected a {ty}"))),
1335    }
1336}
1337
1338fn parse_json_bool(value: &Value, path: &str) -> Result<bool, Error> {
1339    match value {
1340        Value::Bool(value) => Ok(*value),
1341        Value::String(value) => value
1342            .parse::<bool>()
1343            .map_err(|e| encode_value_error(path, format!("invalid bool `{value}`: {e}"))),
1344        _ => Err(encode_value_error(path, "expected a bool")),
1345    }
1346}
1347
1348fn encode_scalar_cli_value(ty: &ContractType, value: &str, path: &str) -> Result<Vec<u8>, Error> {
1349    match ty {
1350        ContractType::Bool => encode_xdr_value(
1351            value
1352                .parse::<bool>()
1353                .map_err(|e| encode_value_error(path, format!("invalid bool `{value}`: {e}")))?,
1354        )
1355        .map_err(|e| encode_value_error(path, e.to_string())),
1356        ContractType::U8 => encode_xdr_value(u32::from(parse_numeric::<u8>(value, "u8")?))
1357            .map_err(|e| encode_value_error(path, e.to_string())),
1358        ContractType::U32 => encode_xdr_value(parse_numeric::<u32>(value, "u32")?)
1359            .map_err(|e| encode_value_error(path, e.to_string())),
1360        ContractType::I32 => encode_xdr_value(parse_numeric::<i32>(value, "i32")?)
1361            .map_err(|e| encode_value_error(path, e.to_string())),
1362        ContractType::U64 => encode_xdr_value(parse_numeric::<u64>(value, "u64")?)
1363            .map_err(|e| encode_value_error(path, e.to_string())),
1364        ContractType::I64 => encode_xdr_value(parse_numeric::<i64>(value, "i64")?)
1365            .map_err(|e| encode_value_error(path, e.to_string())),
1366        ContractType::U128 => encode_xdr_value(parse_numeric::<u128>(value, "u128")?)
1367            .map_err(|e| encode_value_error(path, e.to_string())),
1368        ContractType::I128 => encode_xdr_value(parse_numeric::<i128>(value, "i128")?)
1369            .map_err(|e| encode_value_error(path, e.to_string())),
1370        ContractType::U256 => encode_xdr_value(parse_numeric::<thruput_u256::U256>(value, "U256")?)
1371            .map_err(|e| encode_value_error(path, e.to_string())),
1372        ContractType::I256 => encode_xdr_value(parse_numeric::<thruput_u256::I256>(value, "I256")?)
1373            .map_err(|e| encode_value_error(path, e.to_string())),
1374        ContractType::String => {
1375            encode_xdr_value(value.to_owned()).map_err(|e| encode_value_error(path, e.to_string()))
1376        }
1377        ContractType::Bytes => encode_variable_bytes(parse_bytes_argument(value, path)?, path),
1378        ContractType::Address => encode_xdr_value(parse_contract_address(value)?)
1379            .map_err(|e| encode_value_error(path, e.to_string())),
1380        _ => Err(encode_value_error(
1381            path,
1382            "this contract argument type requires structured JSON input",
1383        )),
1384    }
1385}
1386
1387fn parse_bytes_argument(argument: &str, path: &str) -> Result<Vec<u8>, Error> {
1388    if let Ok(json) = serde_json::from_str::<Value>(argument)
1389        && matches!(json, Value::String(_) | Value::Array(_))
1390    {
1391        return parse_bytes_json_value(&json, path);
1392    }
1393    parse_byte_string(argument, path)
1394}
1395
1396fn parse_bytes_json_value(value: &Value, path: &str) -> Result<Vec<u8>, Error> {
1397    match value {
1398        Value::String(value) => parse_byte_string(value, path),
1399        Value::Array(values) => values
1400            .iter()
1401            .enumerate()
1402            .map(|(index, value)| parse_json_numeric::<u8>(value, "u8", &index_path(path, index)))
1403            .collect(),
1404        _ => Err(encode_value_error(
1405            path,
1406            "expected a hex string, UTF-8 string, or JSON array of byte values",
1407        )),
1408    }
1409}
1410
1411fn parse_byte_string(value: &str, path: &str) -> Result<Vec<u8>, Error> {
1412    if let Some(hex) = value
1413        .strip_prefix("0x")
1414        .or_else(|| value.strip_prefix("0X"))
1415    {
1416        return hex::decode(hex)
1417            .map_err(|e| encode_value_error(path, format!("invalid byte hex `{value}`: {e}")));
1418    }
1419    Ok(value.as_bytes().to_vec())
1420}
1421
1422fn vector_contains_u8(element: &ContractType) -> bool {
1423    matches!(element, ContractType::U8)
1424}
1425
1426fn encode_value_error(path: &str, message: impl Into<String>) -> Error {
1427    Error::CallEncode {
1428        path: path.to_owned(),
1429        message: message.into(),
1430    }
1431}
1432
1433fn decode_value_error(path: &str, message: impl Into<String>) -> Error {
1434    Error::CallDecode {
1435        path: path.to_owned(),
1436        message: message.into(),
1437    }
1438}
1439
1440fn ensure_decoded_values(
1441    policy: &mut DecodePolicy<'_>,
1442    path: &str,
1443    required: usize,
1444) -> Result<(), Error> {
1445    if policy
1446        .remaining_values
1447        .as_deref()
1448        .is_some_and(|remaining| required > *remaining)
1449    {
1450        return Err(decode_value_error(
1451            path,
1452            "decoded value exceeds the per-event value limit",
1453        ));
1454    }
1455    Ok(())
1456}
1457
1458fn charge_decoded_values(
1459    policy: &mut DecodePolicy<'_>,
1460    path: &str,
1461    count: usize,
1462) -> Result<(), Error> {
1463    ensure_decoded_values(policy, path, count)?;
1464    if let Some(remaining) = policy.remaining_values.as_deref_mut() {
1465        *remaining -= count;
1466    }
1467    Ok(())
1468}
1469
1470fn field_path(path: &str, field: &str) -> String {
1471    format!("{path}.{field}")
1472}
1473
1474fn index_path(path: &str, index: usize) -> String {
1475    format!("{path}[{index}]")
1476}
1477
1478fn decode_spec_scalar_value_from_reader(
1479    ty: &ContractType,
1480    reader: &mut xdrs::cxs::buf_reader::BufReader<'_>,
1481    path: &str,
1482) -> Option<Result<Value, Error>> {
1483    match ty {
1484        ContractType::Void => Some(Ok(Value::Null)),
1485        ContractType::Bool => Some(read_xdr_value::<bool>(reader, path).map(Value::from)),
1486        ContractType::U8 => Some(read_xdr_value::<u32>(reader, path).and_then(|value| {
1487            let value: u8 = value
1488                .try_into()
1489                .map_err(|_| decode_value_error(path, "decoded u8 value is out of range"))?;
1490            Ok(Value::from(value))
1491        })),
1492        ContractType::U32 => Some(read_xdr_value::<u32>(reader, path).map(Value::from)),
1493        ContractType::I32 => Some(read_xdr_value::<i32>(reader, path).map(Value::from)),
1494        ContractType::U64 => {
1495            Some(read_xdr_value::<u64>(reader, path).map(|value| Value::from(value.to_string())))
1496        }
1497        ContractType::I64 => {
1498            Some(read_xdr_value::<i64>(reader, path).map(|value| Value::from(value.to_string())))
1499        }
1500        ContractType::U128 => {
1501            Some(read_xdr_value::<u128>(reader, path).map(|value| Value::from(value.to_string())))
1502        }
1503        ContractType::I128 => {
1504            Some(read_xdr_value::<i128>(reader, path).map(|value| Value::from(value.to_string())))
1505        }
1506        ContractType::U256 => Some(
1507            read_xdr_value::<thruput_u256::U256>(reader, path)
1508                .map(|value| Value::from(value.to_string())),
1509        ),
1510        ContractType::I256 => Some(
1511            read_xdr_value::<thruput_u256::I256>(reader, path)
1512                .map(|value| Value::from(value.to_string())),
1513        ),
1514        ContractType::String => Some(read_xdr_value::<String>(reader, path).map(Value::from)),
1515        ContractType::Bytes => Some(
1516            reader
1517                .read_opaque_variable_owned(u32::MAX)
1518                .map(hex_json_value)
1519                .map_err(|e| decode_value_error(path, format!("failed to decode bytes: {e}"))),
1520        ),
1521        ContractType::Address => {
1522            Some(read_xdr_value::<ContractAddress>(reader, path).map(base58_json_value))
1523        }
1524        _ => None,
1525    }
1526}
1527
1528fn decode_option_value_from_reader(
1529    inner: &ContractType,
1530    reader: &mut xdrs::cxs::buf_reader::BufReader<'_>,
1531    type_defs: &TypeDefIndex<'_>,
1532    path: &str,
1533    policy: &mut DecodePolicy<'_>,
1534) -> Result<Value, Error> {
1535    if read_xdr_value::<bool>(reader, path)? {
1536        decode_spec_value_from_reader_with_policy(inner, reader, type_defs, path, policy)
1537    } else {
1538        charge_decoded_values(policy, path, 1)?;
1539        Ok(Value::Null)
1540    }
1541}
1542
1543fn decode_vector_value_from_reader(
1544    vector: &ContractVectorType,
1545    reader: &mut xdrs::cxs::buf_reader::BufReader<'_>,
1546    type_defs: &TypeDefIndex<'_>,
1547    path: &str,
1548    policy: &mut DecodePolicy<'_>,
1549) -> Result<Value, Error> {
1550    let inner = vector.element.as_ref();
1551    if vector_contains_u8(inner) {
1552        return reader
1553            .read_opaque_variable_owned(vector.max_len)
1554            .map(hex_json_value)
1555            .map_err(|e| decode_value_error(path, format!("failed to decode byte vector: {e}")));
1556    }
1557
1558    let len = read_xdr_value::<u32>(reader, path)? as usize;
1559    if len > vector.max_len as usize {
1560        return Err(decode_value_error(
1561            path,
1562            format!("decoded vector length {len} exceeds max {}", vector.max_len),
1563        ));
1564    }
1565    ensure_decoded_values(policy, path, len)?;
1566    let mut values = Vec::with_capacity(len);
1567    for index in 0..len {
1568        values.push(decode_spec_value_from_reader_with_policy(
1569            inner,
1570            reader,
1571            type_defs,
1572            &index_path(path, index),
1573            policy,
1574        )?);
1575    }
1576    Ok(Value::Array(values))
1577}
1578
1579fn decode_named_value_from_reader(
1580    named: &ContractSpecId,
1581    reader: &mut xdrs::cxs::buf_reader::BufReader<'_>,
1582    type_defs: &TypeDefIndex<'_>,
1583    path: &str,
1584    policy: &mut DecodePolicy<'_>,
1585) -> Result<Value, Error> {
1586    match resolve_named_type_def(named, type_defs)? {
1587        ContractTypeDef::TypeDefStruct { struct_def } => {
1588            decode_struct_value(struct_def, reader, type_defs, path, policy)
1589        }
1590        ContractTypeDef::TypeDefEnum { enum_def } => {
1591            decode_enum_value(enum_def, reader, path, policy.strict_enums)
1592        }
1593        ContractTypeDef::TypeDefUnion { union_def } => {
1594            decode_union_value(union_def, reader, type_defs, path, policy)
1595        }
1596        _ => Err(Error::SpecFailed(
1597            "contract spec contains an unsupported type definition kind".into(),
1598        )),
1599    }
1600}
1601
1602fn decode_array_value_from_reader(
1603    array: &ContractArrayType,
1604    reader: &mut xdrs::cxs::buf_reader::BufReader<'_>,
1605    type_defs: &TypeDefIndex<'_>,
1606    path: &str,
1607    policy: &mut DecodePolicy<'_>,
1608) -> Result<Value, Error> {
1609    let inner = array.element.as_ref();
1610    if vector_contains_u8(inner) {
1611        return reader
1612            .read_opaque_fixed_owned(array.len)
1613            .map(hex_json_value)
1614            .map_err(|e| decode_value_error(path, format!("failed to decode byte array: {e}")));
1615    }
1616
1617    ensure_decoded_values(policy, path, array.len as usize)?;
1618    let mut values = Vec::with_capacity(array.len as usize);
1619    for index in 0..array.len as usize {
1620        values.push(decode_spec_value_from_reader_with_policy(
1621            inner,
1622            reader,
1623            type_defs,
1624            &index_path(path, index),
1625            policy,
1626        )?);
1627    }
1628    Ok(Value::Array(values))
1629}
1630
1631fn decode_struct_value(
1632    struct_def: &ContractStructDef,
1633    reader: &mut xdrs::cxs::buf_reader::BufReader<'_>,
1634    type_defs: &TypeDefIndex<'_>,
1635    path: &str,
1636    policy: &mut DecodePolicy<'_>,
1637) -> Result<Value, Error> {
1638    decode_field_values(struct_def.fields.iter(), reader, type_defs, path, policy)
1639}
1640
1641fn decode_field_values<'a>(
1642    fields: impl ExactSizeIterator<Item = &'a ContractField>,
1643    reader: &mut xdrs::cxs::buf_reader::BufReader<'_>,
1644    type_defs: &TypeDefIndex<'_>,
1645    path: &str,
1646    policy: &mut DecodePolicy<'_>,
1647) -> Result<Value, Error> {
1648    let fields = fields.collect::<Vec<_>>();
1649    ensure_decoded_values(policy, path, fields.len())?;
1650    if fields_decode_to_object(&fields)? {
1651        let mut object = serde_json::Map::with_capacity(fields.len());
1652        for field in fields {
1653            let field_name = spec_string_text(&field.name)?;
1654            let value = decode_spec_value_from_reader_with_policy(
1655                &field.ty,
1656                reader,
1657                type_defs,
1658                &field_path(path, field_name),
1659                policy,
1660            )?;
1661            object.insert(field_name.to_owned(), value);
1662        }
1663        Ok(Value::Object(object))
1664    } else {
1665        let mut values = Vec::with_capacity(fields.len());
1666        for (index, field) in fields.iter().enumerate() {
1667            values.push(decode_spec_value_from_reader_with_policy(
1668                &field.ty,
1669                reader,
1670                type_defs,
1671                &index_path(path, index),
1672                policy,
1673            )?);
1674        }
1675        Ok(Value::Array(values))
1676    }
1677}
1678
1679fn fields_decode_to_object(fields: &[&ContractField]) -> Result<bool, Error> {
1680    for field in fields {
1681        let name = spec_string_text(&field.name)?;
1682        if name.is_empty() || name.parse::<usize>().is_ok() {
1683            return Ok(false);
1684        }
1685    }
1686    Ok(true)
1687}
1688
1689fn decode_enum_value(
1690    enum_def: &ContractEnumDef,
1691    reader: &mut xdrs::cxs::buf_reader::BufReader<'_>,
1692    path: &str,
1693    strict: bool,
1694) -> Result<Value, Error> {
1695    let discriminant = read_xdr_value::<i32>(reader, path)?;
1696    for variant in enum_def.variants.iter() {
1697        if variant.value == discriminant {
1698            return Ok(Value::from(spec_string_text(&variant.name)?.to_owned()));
1699        }
1700    }
1701    if strict {
1702        Err(decode_value_error(
1703            path,
1704            format!("unknown enum discriminant `{discriminant}`"),
1705        ))
1706    } else {
1707        Ok(Value::from(discriminant))
1708    }
1709}
1710
1711fn decode_union_value(
1712    union_def: &ContractUnionDef,
1713    reader: &mut xdrs::cxs::buf_reader::BufReader<'_>,
1714    type_defs: &TypeDefIndex<'_>,
1715    path: &str,
1716    policy: &mut DecodePolicy<'_>,
1717) -> Result<Value, Error> {
1718    let discriminant = read_xdr_value::<i32>(reader, path)?;
1719    let case = union_def
1720        .cases
1721        .iter()
1722        .find(|case| case.discriminant == discriminant)
1723        .ok_or_else(|| {
1724            decode_value_error(path, format!("unknown union discriminant `{discriminant}`"))
1725        })?;
1726
1727    let mut object = serde_json::Map::new();
1728    object.insert(
1729        "case".into(),
1730        Value::from(spec_string_text(&case.name)?.to_owned()),
1731    );
1732    charge_decoded_values(policy, path, 1)?;
1733
1734    match &case.case_type {
1735        ContractUnionCaseType::UnionCaseVoid => {}
1736        ContractUnionCaseType::UnionCaseTuple { tuple } => {
1737            object.insert(
1738                "value".into(),
1739                decode_spec_value_from_reader_with_policy(
1740                    &ContractType::Tuple {
1741                        tuple: tuple.clone(),
1742                    },
1743                    reader,
1744                    type_defs,
1745                    &field_path(path, "value"),
1746                    policy,
1747                )?,
1748            );
1749        }
1750        ContractUnionCaseType::UnionCaseStruct { struct_fields } => {
1751            charge_decoded_values(policy, &field_path(path, "value"), 1)?;
1752            object.insert(
1753                "value".into(),
1754                decode_field_values(
1755                    struct_fields.iter(),
1756                    reader,
1757                    type_defs,
1758                    &field_path(path, "value"),
1759                    policy,
1760                )?,
1761            );
1762        }
1763        _ => {
1764            return Err(Error::SpecFailed(format!(
1765                "contract spec contains an unsupported union in `{}`",
1766                spec_id_text(&union_def.name)?
1767            )));
1768        }
1769    }
1770
1771    Ok(Value::Object(object))
1772}
1773
1774fn decode_single_value(ty: &str, bytes: &[u8]) -> Option<Value> {
1775    let ty = normalize_type_name(ty);
1776    match ty.as_str() {
1777        "u16" => decode_xdr_value::<u16>(bytes).map(Value::from),
1778        "u32" => decode_xdr_value::<u32>(bytes).map(Value::from),
1779        "u64" => decode_xdr_value::<u64>(bytes).map(|value| Value::from(value.to_string())),
1780        "u128" => decode_xdr_value::<u128>(bytes).map(|value| Value::from(value.to_string())),
1781        "U256" => decode_xdr_value::<thruput_u256::U256>(bytes)
1782            .map(|value| Value::from(value.to_string())),
1783        "i16" => decode_xdr_value::<i16>(bytes).map(Value::from),
1784        "i32" => decode_xdr_value::<i32>(bytes).map(Value::from),
1785        "i64" => decode_xdr_value::<i64>(bytes).map(|value| Value::from(value.to_string())),
1786        "i128" => decode_xdr_value::<i128>(bytes).map(|value| Value::from(value.to_string())),
1787        "I256" => decode_xdr_value::<thruput_u256::I256>(bytes)
1788            .map(|value| Value::from(value.to_string())),
1789        "bool" => decode_xdr_value::<bool>(bytes).map(Value::from),
1790        "String" | "str" => decode_xdr_value::<String>(bytes).map(Value::from),
1791        "ContractAddress" => decode_xdr_value::<ContractAddress>(bytes).map(base58_json_value),
1792        "Ed25519PublicKey" => decode_xdr_value::<Ed25519PublicKey>(bytes).map(base58_json_value),
1793        "SHA256Hash" => decode_xdr_value::<SHA256Hash>(bytes).map(base58_json_value),
1794        "ContractEntrypointID" => {
1795            decode_xdr_value::<ContractEntrypointID>(bytes).map(hex_json_value)
1796        }
1797        _ => None,
1798    }
1799}
1800
1801fn decode_xdr_value<T>(bytes: &[u8]) -> Option<T>
1802where
1803    T: xdrs::ReadXDROwned,
1804{
1805    xdrs::cxs::decode(bytes).ok()
1806}
1807
1808fn read_xdr_value<T>(
1809    reader: &mut xdrs::cxs::buf_reader::BufReader<'_>,
1810    path: &str,
1811) -> Result<T, Error>
1812where
1813    T: xdrs::ReadXDROwned,
1814{
1815    xdrs::read_xdr(reader)
1816        .map_err(|e| decode_value_error(path, format!("failed to decode value: {e}")))
1817}
1818
1819fn parse_fixed_hex<const N: usize>(value: &str, label: &str) -> Result<[u8; N], Error> {
1820    decode_fixed_hex(value).map_err(|error| Error::InvalidArgument {
1821        argument: label.to_string(),
1822        message: match error {
1823            FixedHexError::InvalidHex(error) => format!("`{value}` is not valid hex: {error}"),
1824            FixedHexError::InvalidLength { len, .. } => format!("must be {N} bytes, got {len}"),
1825        },
1826    })
1827}
1828
1829fn base58_json_value<T: AsRef<[u8]>>(value: T) -> Value {
1830    Value::from(thruput_encoding::encode(value.as_ref()))
1831}
1832
1833fn hex_json_value<T>(value: T) -> Value
1834where
1835    T: AsRef<[u8]>,
1836{
1837    Value::from(prefixed_hex(value))
1838}