From 64a8eba3b77b7b489cc282d81346e4353768e622 Mon Sep 17 00:00:00 2001 From: Jo-anny Date: Tue, 29 Sep 2026 18:18:25 +0100 Subject: [PATCH] feat(core): implement tuple decoder --- crates/core/src/decode/mod.rs | 2 + crates/core/src/decode/return_decoder.rs | 22 +- crates/core/src/decode/tuple_decoder.rs | 251 +++++++++++++++++++++++ 3 files changed, 265 insertions(+), 10 deletions(-) create mode 100644 crates/core/src/decode/tuple_decoder.rs diff --git a/crates/core/src/decode/mod.rs b/crates/core/src/decode/mod.rs index 9fddd5d7..2e86c8ec 100644 --- a/crates/core/src/decode/mod.rs +++ b/crates/core/src/decode/mod.rs @@ -21,6 +21,7 @@ pub mod report; pub mod resource_analyzer; pub mod return_decoder; pub mod scval_to_json; +pub mod tuple_decoder; pub mod walker; pub use argument_decoder::ArgumentDecoder; @@ -39,6 +40,7 @@ pub use resource_analyzer::{ }; pub use return_decoder::ReturnValueDecoder; pub use scval_to_json::scval_to_json; +pub use tuple_decoder::{TupleDecodeError, TupleDecoder}; pub use walker::{ walk_diagnostic_events, DiagnosticEventKind, DiagnosticEventWalker, StructuredDiagnosticEvent, }; diff --git a/crates/core/src/decode/return_decoder.rs b/crates/core/src/decode/return_decoder.rs index f74892db..5caca28d 100644 --- a/crates/core/src/decode/return_decoder.rs +++ b/crates/core/src/decode/return_decoder.rs @@ -1,3 +1,4 @@ +use crate::decode::tuple_decoder::TupleDecoder; use crate::spec::decoder::{ContractFunction, ContractSpec, ContractStructDef}; use serde_json::{json, Value}; use stellar_xdr::curr::{ScSpecTypeDef, ScVal}; @@ -224,17 +225,18 @@ impl ReturnValueDecoder { }, ScSpecTypeDef::Tuple(tuple_spec) => match val { ScVal::Vec(Some(v)) => { - let items: Vec = v - .iter() - .enumerate() - .map(|(i, item)| { - let elem_td = tuple_spec.value_types.get(i); - Self::decode_value(item, elem_td, contract_spec) - }) - .collect(); - Value::Array(items) + match TupleDecoder.decode_vec( + v, + &tuple_spec.value_types, + |item, type_def| { + Self::decode_value(item, Some(type_def), contract_spec) + }, + ) { + Ok(items) => Value::Array(items), + Err(error) => json!({ "error": error.to_string() }), + } } - _ => Self::decode_dynamic(val), + _ => json!({ "error": "expected an SCVec for tuple decoding" }), }, ScSpecTypeDef::Udt(udt_spec) => { let udt_name = udt_spec.name.to_string(); diff --git a/crates/core/src/decode/tuple_decoder.rs b/crates/core/src/decode/tuple_decoder.rs new file mode 100644 index 00000000..34b48ef8 --- /dev/null +++ b/crates/core/src/decode/tuple_decoder.rs @@ -0,0 +1,251 @@ +use serde_json::Value; +use std::fmt; +use stellar_xdr::curr::{ScSpecTypeDef, ScVal, ScVec}; + +/// Errors raised when an `SCVec` does not match a tuple signature. +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum TupleDecodeError { + ExpectedVec, + LengthMismatch { expected: usize, actual: usize }, + TypeMismatch { + index: usize, + expected: String, + actual: String, + }, +} + +impl fmt::Display for TupleDecodeError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::ExpectedVec => write!(f, "expected an SCVec for tuple decoding"), + Self::LengthMismatch { expected, actual } => write!( + f, + "tuple length mismatch: expected {expected} elements, got {actual}" + ), + Self::TypeMismatch { + index, + expected, + actual, + } => write!( + f, + "tuple element {index} type mismatch: expected {expected}, got {actual}" + ), + } + } +} + +impl std::error::Error for TupleDecodeError {} + +/// Decodes tuple values in signature order, rejecting vectors with the wrong length. +#[derive(Debug, Clone, Copy, Default)] +pub struct TupleDecoder; + +impl TupleDecoder { + /// Decodes each vector element with its corresponding tuple type definition. + pub fn decode( + &self, + value: &ScVal, + signature: &[ScSpecTypeDef], + decode_element: F, + ) -> Result, TupleDecodeError> + where + F: FnMut(&ScVal, &ScSpecTypeDef) -> Value, + { + let ScVal::Vec(Some(values)) = value else { + return Err(TupleDecodeError::ExpectedVec); + }; + + self.decode_vec(values, signature, decode_element) + } + + /// Decodes an already-extracted vector after validating its exact length. + pub fn decode_vec( + &self, + values: &ScVec, + signature: &[ScSpecTypeDef], + mut decode_element: F, + ) -> Result, TupleDecodeError> + where + F: FnMut(&ScVal, &ScSpecTypeDef) -> Value, + { + if values.len() != signature.len() { + return Err(TupleDecodeError::LengthMismatch { + expected: signature.len(), + actual: values.len(), + }); + } + + let mut decoded = Vec::with_capacity(values.len()); + for (index, (element, type_def)) in values.iter().zip(signature).enumerate() { + if !matches_type(element, type_def) { + return Err(TupleDecodeError::TypeMismatch { + index, + expected: format!("{type_def:?}"), + actual: scval_type_name(element).to_string(), + }); + } + decoded.push(decode_element(element, type_def)); + } + + Ok(decoded) + } +} + +fn matches_type(value: &ScVal, type_def: &ScSpecTypeDef) -> bool { + match type_def { + ScSpecTypeDef::Void => matches!(value, ScVal::Void), + ScSpecTypeDef::Val => true, + ScSpecTypeDef::Bool => matches!(value, ScVal::Bool(_)), + ScSpecTypeDef::U32 => matches!(value, ScVal::U32(_)), + ScSpecTypeDef::I32 => matches!(value, ScVal::I32(_)), + ScSpecTypeDef::U64 => matches!(value, ScVal::U64(_)), + ScSpecTypeDef::I64 => matches!(value, ScVal::I64(_)), + ScSpecTypeDef::Timepoint => matches!(value, ScVal::Timepoint(_) | ScVal::U64(_)), + ScSpecTypeDef::Duration => matches!(value, ScVal::Duration(_) | ScVal::U64(_)), + ScSpecTypeDef::U128 => matches!(value, ScVal::U128(_)), + ScSpecTypeDef::I128 => matches!(value, ScVal::I128(_)), + ScSpecTypeDef::U256 => matches!(value, ScVal::U256(_)), + ScSpecTypeDef::I256 => matches!(value, ScVal::I256(_)), + ScSpecTypeDef::Bytes => matches!(value, ScVal::Bytes(_)), + ScSpecTypeDef::BytesN(spec) => { + matches!(value, ScVal::Bytes(bytes) if bytes.len() == spec.n as usize) + } + ScSpecTypeDef::String => matches!(value, ScVal::String(_) | ScVal::Symbol(_)), + ScSpecTypeDef::Symbol => matches!(value, ScVal::Symbol(_) | ScVal::String(_)), + ScSpecTypeDef::Address => matches!(value, ScVal::Address(_)), + ScSpecTypeDef::Error => matches!(value, ScVal::Error(_)), + ScSpecTypeDef::Option(spec) => match value { + ScVal::Void => true, + ScVal::Vec(Some(values)) if values.is_empty() => true, + ScVal::Vec(Some(values)) if values.len() == 1 => { + matches_type(&values[0], &spec.value_type) + } + _ => matches_type(value, &spec.value_type), + }, + ScSpecTypeDef::Result(spec) => match value { + ScVal::Error(_) => true, + ScVal::Vec(Some(values)) if values.len() == 2 => match &values[0] { + ScVal::Symbol(symbol) if symbol.to_string() == "Ok" => { + matches_type(&values[1], &spec.ok_type) + } + ScVal::Symbol(symbol) if symbol.to_string() == "Err" => { + matches_type(&values[1], &spec.error_type) + } + _ => false, + }, + _ => false, + }, + ScSpecTypeDef::Vec(spec) => match value { + ScVal::Vec(Some(values)) => values + .iter() + .all(|element| matches_type(element, &spec.element_type)), + _ => false, + }, + ScSpecTypeDef::Map(spec) => match value { + ScVal::Map(Some(entries)) => entries.iter().all(|entry| { + matches_type(&entry.key, &spec.key_type) + && matches_type(&entry.val, &spec.value_type) + }), + _ => false, + }, + ScSpecTypeDef::Tuple(spec) => match value { + ScVal::Vec(Some(values)) if values.len() == spec.value_types.len() => values + .iter() + .zip(&spec.value_types) + .all(|(element, element_type)| matches_type(element, element_type)), + _ => false, + }, + // UDT encodings depend on their definitions in ContractSpec, which the element + // decoder resolves after this generic structural validation. + ScSpecTypeDef::Udt(_) => true, + } +} + +fn scval_type_name(value: &ScVal) -> &'static str { + match value { + ScVal::Bool(_) => "Bool", + ScVal::Void => "Void", + ScVal::U32(_) => "U32", + ScVal::I32(_) => "I32", + ScVal::U64(_) => "U64", + ScVal::I64(_) => "I64", + ScVal::Timepoint(_) => "Timepoint", + ScVal::Duration(_) => "Duration", + ScVal::U128(_) => "U128", + ScVal::I128(_) => "I128", + ScVal::U256(_) => "U256", + ScVal::I256(_) => "I256", + ScVal::Bytes(_) => "Bytes", + ScVal::String(_) => "String", + ScVal::Symbol(_) => "Symbol", + ScVal::Address(_) => "Address", + ScVal::Error(_) => "Error", + ScVal::Vec(_) => "Vec", + ScVal::Map(_) => "Map", + _ => "other", + } +} + +#[cfg(test)] +mod tests { + use super::*; + use serde_json::json; + + fn scvec(values: Vec) -> ScVal { + ScVal::Vec(Some(values.try_into().unwrap())) + } + + #[test] + fn decodes_elements_using_their_sequential_signature_types() { + let decoder = TupleDecoder; + let value = scvec(vec![ScVal::U32(7), ScVal::Bool(true)]); + let signature = [ScSpecTypeDef::U32, ScSpecTypeDef::Bool]; + + let decoded = decoder + .decode(&value, &signature, |element, type_def| { + json!({ "type": format!("{type_def:?}"), "value": format!("{element:?}") }) + }) + .unwrap(); + + assert_eq!(decoded[0]["type"], "U32"); + assert_eq!(decoded[1]["type"], "Bool"); + } + + #[test] + fn rejects_vectors_with_a_different_length() { + let decoder = TupleDecoder; + let value = scvec(vec![ScVal::U32(7)]); + let signature = [ScSpecTypeDef::U32, ScSpecTypeDef::Bool]; + + assert_eq!( + decoder.decode(&value, &signature, |_, _| Value::Null), + Err(TupleDecodeError::LengthMismatch { + expected: 2, + actual: 1, + }) + ); + } + + #[test] + fn rejects_elements_that_do_not_match_their_signature_type() { + let value = scvec(vec![ScVal::Bool(true)]); + let signature = [ScSpecTypeDef::U32]; + + assert_eq!( + TupleDecoder.decode(&value, &signature, |_, _| Value::Null), + Err(TupleDecodeError::TypeMismatch { + index: 0, + expected: "U32".to_string(), + actual: "Bool".to_string(), + }) + ); + } + + #[test] + fn rejects_non_vector_values() { + assert_eq!( + TupleDecoder.decode(&ScVal::U32(7), &[], |_, _| Value::Null), + Err(TupleDecodeError::ExpectedVec) + ); + } +} \ No newline at end of file