1use 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#[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
32pub 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
46pub 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#[derive(Debug, Clone, PartialEq, Eq)]
116pub enum CallMetadata {
117 Spec(SpecMethodMetadata),
118 Local(ContractMetadata),
119}
120
121#[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#[derive(Debug, Clone, PartialEq, Eq)]
132pub struct MethodMetadata {
133 pub arg_types: Vec<String>,
134 pub return_type: Option<String>,
135}
136
137#[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
147pub type TypeDefIndex<'a> = BTreeMap<ContractSpecId, &'a ContractTypeDef>;
149
150pub 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
173pub 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#[derive(Debug, Clone, PartialEq)]
194pub struct DecodedAbort {
195 pub exception: Option<String>,
197 pub value: Value,
199 pub exception_crate: Option<&'static str>,
202 pub exception_version: Option<&'static str>,
205}
206
207impl DecodedAbort {
208 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#[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
258pub 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
291pub 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
328pub 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
345pub fn format_contract_address(address: &ContractAddress) -> String {
347 thruput_json_rpc_types::ContractAddress::from(*address).to_string()
348}
349
350pub 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
364pub 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
383pub 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
400pub 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
420pub 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
439pub 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
515pub 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}