From 0fad12f0ef3dc27be9cc6ae99bf14fab59e9a0e5 Mon Sep 17 00:00:00 2001 From: nazteeemba Date: Wed, 29 Jul 2026 13:49:00 +0100 Subject: [PATCH] feat(core): implement ReturnValueDecoder and FunctionCallDecoder for contract return values --- crates/core/src/decode/context.rs | 40 +- .../core/src/decode/function_call_decoder.rs | 166 ++++++ crates/core/src/decode/mod.rs | 4 + crates/core/src/decode/return_decoder.rs | 543 ++++++++++++++++++ crates/core/src/lib.rs | 4 +- crates/core/src/spec/decoder.rs | 171 +++++- crates/core/src/spec/mod.rs | 4 +- 7 files changed, 921 insertions(+), 11 deletions(-) create mode 100644 crates/core/src/decode/function_call_decoder.rs create mode 100644 crates/core/src/decode/return_decoder.rs diff --git a/crates/core/src/decode/context.rs b/crates/core/src/decode/context.rs index 27ab9f65..7ff8ff47 100644 --- a/crates/core/src/decode/context.rs +++ b/crates/core/src/decode/context.rs @@ -1,6 +1,8 @@ use crate::decode::auth::{AuthChain, AuthCredential}; use crate::decode::auth_signature::decode_auth_entry_signatures; +use crate::decode::return_decoder::ReturnValueDecoder; use crate::error::GratResult; +use crate::spec::decoder::ContractSpec; use crate::types::report::{ AuthEntryInfo, DiagnosticReport, FeeBreakdown, ResourceSummary, TransactionContext, }; @@ -8,6 +10,14 @@ use crate::xdr::codec::XdrCodec; use stellar_xdr::curr::{TransactionEnvelope, TransactionMeta, TransactionResult}; pub fn enrich_report(report: &mut DiagnosticReport, tx_data: &serde_json::Value) -> GratResult<()> { + enrich_report_with_spec(report, tx_data, None) +} + +pub fn enrich_report_with_spec( + report: &mut DiagnosticReport, + tx_data: &serde_json::Value, + contract_spec: Option<&ContractSpec>, +) -> GratResult<()> { let tx_hash = tx_data .get("hash") .and_then(|h| h.as_str()) @@ -24,7 +34,7 @@ pub fn enrich_report(report: &mut DiagnosticReport, tx_data: &serde_json::Value) ledger_sequence, function_name: extract_function_name(tx_data), arguments: extract_arguments(tx_data), - return_value: extract_return_value(tx_data), + return_value: extract_return_value(tx_data, contract_spec), fee: extract_fee_breakdown(tx_data), resources: extract_resource_summary(tx_data), }; @@ -53,11 +63,29 @@ fn extract_arguments(tx_data: &serde_json::Value) -> Vec { .unwrap_or_default() } -fn extract_return_value(tx_data: &serde_json::Value) -> Option { - tx_data - .get("returnValue") - .and_then(|r| r.as_str()) - .map(std::string::ToString::to_string) +fn extract_return_value( + tx_data: &serde_json::Value, + contract_spec: Option<&ContractSpec>, +) -> Option { + let ret_val_str = tx_data.get("returnValue").and_then(|r| r.as_str())?; + + if let Ok(sc_val) = stellar_xdr::curr::ScVal::from_xdr_base64(ret_val_str) { + let func_name = extract_function_name(tx_data); + let return_decoder = ReturnValueDecoder::new(); + + let type_def = if let (Some(cs), Some(fname)) = (contract_spec, func_name.as_deref()) { + cs.functions + .iter() + .find(|f| f.name == fname) + .and_then(|f| f.return_type_def.as_ref()) + } else { + None + }; + + Some(return_decoder.decode_to_string(&sc_val, type_def, contract_spec)) + } else { + Some(ret_val_str.to_string()) + } } fn extract_fee_breakdown(tx_data: &serde_json::Value) -> FeeBreakdown { diff --git a/crates/core/src/decode/function_call_decoder.rs b/crates/core/src/decode/function_call_decoder.rs new file mode 100644 index 00000000..12d09267 --- /dev/null +++ b/crates/core/src/decode/function_call_decoder.rs @@ -0,0 +1,166 @@ +use crate::decode::return_decoder::ReturnValueDecoder; +use crate::spec::decoder::{ContractFunction, ContractSpec}; +use serde::{Deserialize, Serialize}; +use serde_json::Value; +use stellar_xdr::curr::ScVal; + +/// A fully decoded representation of a Soroban contract function invocation. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct DecodedFunctionCall { + pub function_name: String, + + pub arguments: Vec, + + pub formatted_arguments: Vec, + + pub return_value: Option, + + pub formatted_return_value: Option, +} + +/// Decoder for contract function calls, handling argument list decoding +/// and delegating return value decoding to `ReturnValueDecoder`. +#[derive(Debug, Clone, Default)] +pub struct FunctionCallDecoder { + return_decoder: ReturnValueDecoder, +} + +impl FunctionCallDecoder { + pub fn new() -> Self { + Self { + return_decoder: ReturnValueDecoder::new(), + } + } + + /// Decodes function call arguments into a vector of typed JSON values. + pub fn decode_call_arguments( + &self, + args: &[ScVal], + func: &ContractFunction, + contract_spec: Option<&ContractSpec>, + ) -> Vec { + args.iter() + .enumerate() + .map(|(i, arg)| { + let type_def = func.param_defs.get(i).map(|(_, td)| td); + self.return_decoder.decode(arg, type_def, contract_spec) + }) + .collect() + } + + /// Decodes a function return value using the function's return specification. + pub fn decode_return_value( + &self, + return_val: &ScVal, + func: &ContractFunction, + contract_spec: Option<&ContractSpec>, + ) -> Value { + self.return_decoder + .decode_function_return(return_val, func, contract_spec) + } + + /// Decodes a full function call (name, arguments, return value) using a `ContractSpec`. + pub fn decode_function_call( + &self, + func_name: &str, + args: &[ScVal], + return_val: Option<&ScVal>, + contract_spec: &ContractSpec, + ) -> DecodedFunctionCall { + let matching_func = contract_spec.functions.iter().find(|f| f.name == func_name); + + let (arguments, formatted_arguments) = if let Some(func) = matching_func { + let decoded_args = self.decode_call_arguments(args, func, Some(contract_spec)); + let formatted: Vec = decoded_args + .iter() + .map(|v| match v { + Value::String(s) => s.clone(), + other => serde_json::to_string(other).unwrap_or_else(|_| other.to_string()), + }) + .collect(); + (decoded_args, formatted) + } else { + let decoded_args: Vec = args.iter().map(ReturnValueDecoder::decode_dynamic).collect(); + let formatted: Vec = decoded_args + .iter() + .map(|v| match v { + Value::String(s) => s.clone(), + other => serde_json::to_string(other).unwrap_or_else(|_| other.to_string()), + }) + .collect(); + (decoded_args, formatted) + }; + + let (return_value, formatted_return_value) = match return_val { + Some(rv) => { + let val = if let Some(func) = matching_func { + self.decode_return_value(rv, func, Some(contract_spec)) + } else { + ReturnValueDecoder::decode_dynamic(rv) + }; + let formatted = match &val { + Value::String(s) => s.clone(), + other => serde_json::to_string(other).unwrap_or_else(|_| other.to_string()), + }; + (Some(val), Some(formatted)) + } + None => (None, None), + }; + + DecodedFunctionCall { + function_name: func_name.to_string(), + arguments, + formatted_arguments, + return_value, + formatted_return_value, + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use stellar_xdr::curr::{ScSpecTypeDef, ScSymbol}; + + #[test] + fn test_decode_function_call() { + let decoder = FunctionCallDecoder::new(); + + let func = ContractFunction { + name: "transfer".to_string(), + params: vec![ + ("to".to_string(), "Address".to_string()), + ("amount".to_string(), "I128".to_string()), + ], + return_type: "Bool".to_string(), + doc: None, + return_type_def: Some(ScSpecTypeDef::Bool), + param_defs: vec![ + ("to".to_string(), ScSpecTypeDef::Address), + ("amount".to_string(), ScSpecTypeDef::I128), + ], + }; + + let spec = ContractSpec { + errors: vec![], + functions: vec![func], + structs: vec![], + name: None, + version: None, + enums: vec![], + unions: vec![], + }; + + let args = vec![ + ScVal::Symbol(ScSymbol("recipient".try_into().unwrap())), + ScVal::I32(500), + ]; + let return_val = ScVal::Bool(true); + + let decoded = decoder.decode_function_call("transfer", &args, Some(&return_val), &spec); + assert_eq!(decoded.function_name, "transfer"); + assert_eq!(decoded.arguments.len(), 2); + assert_eq!(decoded.return_value, Some(serde_json::json!(true))); + assert_eq!(decoded.formatted_return_value, Some("true".to_string())); + } +} diff --git a/crates/core/src/decode/mod.rs b/crates/core/src/decode/mod.rs index f34d32ad..31cd2bf3 100644 --- a/crates/core/src/decode/mod.rs +++ b/crates/core/src/decode/mod.rs @@ -6,9 +6,11 @@ pub mod contract_error; pub mod cross_contract; pub mod decode_context; pub mod diagnostic; +pub mod function_call_decoder; pub mod host_error; pub mod mappings; pub mod report; +pub mod return_decoder; pub mod walker; pub use auth::{ @@ -16,6 +18,8 @@ pub use auth::{ AuthorizationType, }; pub use auth_address_nonce::AddressWithNonce; +pub use function_call_decoder::{DecodedFunctionCall, FunctionCallDecoder}; +pub use return_decoder::ReturnValueDecoder; 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 new file mode 100644 index 00000000..bf66f5ea --- /dev/null +++ b/crates/core/src/decode/return_decoder.rs @@ -0,0 +1,543 @@ +use crate::spec::decoder::{ContractFunction, ContractSpec, ContractStructDef}; +use serde_json::{json, Value}; +use stellar_xdr::curr::{ScSpecTypeDef, ScVal}; + +/// Decoder for contract return values, converting raw `ScVal` structures +/// into typed JSON representations based on contract specifications. +#[derive(Debug, Clone, Default)] +pub struct ReturnValueDecoder; + +impl ReturnValueDecoder { + pub fn new() -> Self { + Self + } + + /// Decodes a raw return `ScVal` using the provided target function's return specification + /// and optional `ContractSpec` (for UDT struct/enum lookups). + pub fn decode( + &self, + val: &ScVal, + type_def: Option<&ScSpecTypeDef>, + contract_spec: Option<&ContractSpec>, + ) -> Value { + Self::decode_value(val, type_def, contract_spec) + } + + /// Decodes a raw return `ScVal` into a formatted String (e.g. JSON string or pretty representation). + pub fn decode_to_string( + &self, + val: &ScVal, + type_def: Option<&ScSpecTypeDef>, + contract_spec: Option<&ContractSpec>, + ) -> String { + let decoded = self.decode(val, type_def, contract_spec); + match decoded { + Value::String(s) => s, + other => serde_json::to_string(&other).unwrap_or_else(|_| other.to_string()), + } + } + + /// Decodes a function's return value directly given a target `ContractFunction`. + pub fn decode_function_return( + &self, + val: &ScVal, + func: &ContractFunction, + contract_spec: Option<&ContractSpec>, + ) -> Value { + self.decode(val, func.return_type_def.as_ref(), contract_spec) + } + + fn decode_value( + val: &ScVal, + type_def: Option<&ScSpecTypeDef>, + contract_spec: Option<&ContractSpec>, + ) -> Value { + let Some(td) = type_def else { + return Self::decode_dynamic(val); + }; + + match td { + ScSpecTypeDef::Void => Value::Null, + ScSpecTypeDef::Val => Self::decode_dynamic(val), + ScSpecTypeDef::Bool => match val { + ScVal::Bool(b) => json!(*b), + _ => Self::decode_dynamic(val), + }, + ScSpecTypeDef::U32 => match val { + ScVal::U32(u) => json!(*u), + _ => Self::decode_dynamic(val), + }, + ScSpecTypeDef::I32 => match val { + ScVal::I32(i) => json!(*i), + _ => Self::decode_dynamic(val), + }, + ScSpecTypeDef::U64 => match val { + ScVal::U64(u) => json!(*u), + _ => Self::decode_dynamic(val), + }, + ScSpecTypeDef::I64 => match val { + ScVal::I64(i) => json!(*i), + _ => Self::decode_dynamic(val), + }, + ScSpecTypeDef::Timepoint => match val { + ScVal::Timepoint(t) => json!(t.0), + ScVal::U64(u) => json!(*u), + _ => Self::decode_dynamic(val), + }, + ScSpecTypeDef::Duration => match val { + ScVal::Duration(d) => json!(d.0), + ScVal::U64(u) => json!(*u), + _ => Self::decode_dynamic(val), + }, + ScSpecTypeDef::U128 => match val { + ScVal::U128(u) => { + let num = (u128::from(u.hi) << 64) | u128::from(u.lo); + json!(num.to_string()) + } + _ => Self::decode_dynamic(val), + }, + ScSpecTypeDef::I128 => match val { + ScVal::I128(i) => { + #[allow(clippy::cast_possible_wrap)] + let lo = u128::from(i.lo) as i128; + let num = (i128::from(i.hi) << 64) | lo; + json!(num.to_string()) + } + _ => Self::decode_dynamic(val), + }, + ScSpecTypeDef::U256 => match val { + ScVal::U256(u) => { + json!(format!( + "0x{:016x}{:016x}{:016x}{:016x}", + u.hi_hi, u.hi_lo, u.lo_hi, u.lo_lo + )) + } + _ => Self::decode_dynamic(val), + }, + ScSpecTypeDef::I256 => match val { + ScVal::I256(i) => { + json!(format!( + "0x{:016x}{:016x}{:016x}{:016x}", + i.hi_hi, i.hi_lo, i.lo_hi, i.lo_lo + )) + } + _ => Self::decode_dynamic(val), + }, + ScSpecTypeDef::Bytes | ScSpecTypeDef::BytesN(_) => match val { + ScVal::Bytes(b) => { + let hex_str: String = b.iter().map(|byte| format!("{byte:02x}")).collect(); + json!(format!("0x{hex_str}")) + } + _ => Self::decode_dynamic(val), + }, + ScSpecTypeDef::String => match val { + ScVal::String(s) => json!(s.to_string()), + ScVal::Symbol(s) => json!(s.to_string()), + _ => Self::decode_dynamic(val), + }, + ScSpecTypeDef::Symbol => match val { + ScVal::Symbol(s) => json!(s.to_string()), + ScVal::String(s) => json!(s.to_string()), + _ => Self::decode_dynamic(val), + }, + ScSpecTypeDef::Address => match val { + ScVal::Address(addr) => { + json!(crate::types::address::Address::from_sc_address(addr).to_string()) + } + _ => Self::decode_dynamic(val), + }, + ScSpecTypeDef::Error => match val { + ScVal::Error(e) => json!(format!("{e:?}")), + _ => Self::decode_dynamic(val), + }, + ScSpecTypeDef::Option(opt_spec) => match val { + ScVal::Void => Value::Null, + ScVal::Vec(Some(v)) if v.is_empty() => Value::Null, + ScVal::Vec(Some(v)) if v.len() == 1 => { + Self::decode_value(&v[0], Some(&opt_spec.value_type), contract_spec) + } + other => Self::decode_value(other, Some(&opt_spec.value_type), contract_spec), + }, + ScSpecTypeDef::Result(res_spec) => match val { + ScVal::Vec(Some(v)) if v.len() == 2 => match &v[0] { + ScVal::Symbol(sym) if sym.to_string() == "Ok" => { + let ok_val = + Self::decode_value(&v[1], Some(&res_spec.ok_type), contract_spec); + json!({ "Ok": ok_val }) + } + ScVal::Symbol(sym) if sym.to_string() == "Err" => { + let err_val = + Self::decode_value(&v[1], Some(&res_spec.error_type), contract_spec); + json!({ "Err": err_val }) + } + _ => Self::decode_dynamic(val), + }, + ScVal::Error(e) => json!({ "Err": format!("{e:?}") }), + _ => Self::decode_dynamic(val), + }, + ScSpecTypeDef::Vec(vec_spec) => match val { + ScVal::Vec(Some(v)) => { + let items: Vec = v + .iter() + .map(|item| { + Self::decode_value(item, Some(&vec_spec.element_type), contract_spec) + }) + .collect(); + Value::Array(items) + } + _ => Self::decode_dynamic(val), + }, + ScSpecTypeDef::Map(map_spec) => match val { + ScVal::Map(Some(m)) => { + let mut obj = serde_json::Map::new(); + let mut can_be_obj = true; + let mut pairs = Vec::new(); + + for entry in m.iter() { + let key_val = + Self::decode_value(&entry.key, Some(&map_spec.key_type), contract_spec); + let val_val = Self::decode_value( + &entry.val, + Some(&map_spec.value_type), + contract_spec, + ); + + if let Value::String(ref k_str) = key_val { + obj.insert(k_str.clone(), val_val); + } else { + can_be_obj = false; + pairs.push(json!([key_val, val_val])); + } + } + + if can_be_obj { + Value::Object(obj) + } else { + Value::Array(pairs) + } + } + _ => Self::decode_dynamic(val), + }, + 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) + } + _ => Self::decode_dynamic(val), + }, + ScSpecTypeDef::Udt(udt_spec) => { + let udt_name = udt_spec.name.to_string(); + if let Some(cs) = contract_spec { + // 1. Check structs + if let Some(struct_def) = cs.structs.iter().find(|s| s.name == udt_name) { + return Self::decode_struct(val, struct_def, cs); + } + // 2. Check enums + if let Some(enum_def) = cs.enums.iter().find(|e| e.name == udt_name) { + return Self::decode_enum(val, enum_def); + } + // 3. Check unions + if let Some(union_def) = cs.unions.iter().find(|u| u.name == udt_name) { + return Self::decode_union(val, union_def, cs); + } + } + // Fallback to dynamic decoding if UDT definition not in spec + Self::decode_dynamic(val) + } + } + } + + fn decode_struct(val: &ScVal, struct_def: &ContractStructDef, cs: &ContractSpec) -> Value { + match val { + ScVal::Map(Some(m)) => { + let mut map_obj = serde_json::Map::new(); + for field in &struct_def.fields { + let matching_entry = m.iter().find(|entry| match &entry.key { + ScVal::Symbol(s) => s.to_string() == field.name, + ScVal::String(s) => s.to_string() == field.name, + _ => false, + }); + + let field_value = match matching_entry { + Some(entry) => Self::decode_value(&entry.val, field.type_def.as_ref(), Some(cs)), + None => Value::Null, + }; + map_obj.insert(field.name.clone(), field_value); + } + Value::Object(map_obj) + } + ScVal::Vec(Some(v)) => { + let mut map_obj = serde_json::Map::new(); + for (i, field) in struct_def.fields.iter().enumerate() { + let field_value = match v.get(i) { + Some(item) => Self::decode_value(item, field.type_def.as_ref(), Some(cs)), + None => Value::Null, + }; + map_obj.insert(field.name.clone(), field_value); + } + Value::Object(map_obj) + } + _ => Self::decode_dynamic(val), + } + } + + fn decode_enum(val: &ScVal, enum_def: &crate::spec::decoder::ContractEnumDef) -> Value { + match val { + ScVal::Symbol(sym) => json!(sym.to_string()), + ScVal::String(s) => json!(s.to_string()), + ScVal::U32(u) => { + if let Some(case) = enum_def.cases.iter().find(|c| c.value == *u) { + json!(case.name.clone()) + } else { + json!(*u) + } + } + ScVal::I32(i) if *i >= 0 => { + #[allow(clippy::cast_sign_loss)] + let u = *i as u32; + if let Some(case) = enum_def.cases.iter().find(|c| c.value == u) { + json!(case.name.clone()) + } else { + json!(*i) + } + } + _ => Self::decode_dynamic(val), + } + } + + fn decode_union(val: &ScVal, union_def: &crate::spec::decoder::ContractUnionDef, cs: &ContractSpec) -> Value { + match val { + ScVal::Symbol(sym) => json!(sym.to_string()), + ScVal::Vec(Some(v)) if !v.is_empty() => { + let variant_name = match &v[0] { + ScVal::Symbol(s) => s.to_string(), + ScVal::String(s) => s.to_string(), + _ => return Self::decode_dynamic(val), + }; + + let matching_case = union_def.cases.iter().find(|c| c.name == variant_name); + match matching_case { + Some(case) => { + if let Some(ref type_defs) = case.value_types { + let payload: Vec = v[1..] + .iter() + .enumerate() + .map(|(i, item)| { + Self::decode_value(item, type_defs.get(i), Some(cs)) + }) + .collect(); + if payload.len() == 1 { + json!({ variant_name: payload[0].clone() }) + } else { + json!({ variant_name: payload }) + } + } else if let Some(ref fields) = case.fields { + let mut map_obj = serde_json::Map::new(); + let items = &v[1..]; + for (i, field) in fields.iter().enumerate() { + let field_val = match items.get(i) { + Some(item) => Self::decode_value(item, field.type_def.as_ref(), Some(cs)), + None => Value::Null, + }; + map_obj.insert(field.name.clone(), field_val); + } + json!({ variant_name: Value::Object(map_obj) }) + } else { + json!(variant_name) + } + } + None => json!({ variant_name: Self::decode_dynamic(&v[1]) }), + } + } + _ => Self::decode_dynamic(val), + } + } + + /// Dynamic fallback to decode any `ScVal` into structured JSON without type specifications. + pub fn decode_dynamic(val: &ScVal) -> Value { + match val { + ScVal::Void => Value::Null, + ScVal::Bool(b) => json!(*b), + ScVal::U32(u) => json!(*u), + ScVal::I32(i) => json!(*i), + ScVal::U64(u) => json!(*u), + ScVal::I64(i) => json!(*i), + ScVal::Timepoint(t) => json!(t.0), + ScVal::Duration(d) => json!(d.0), + ScVal::U128(u) => { + let num = (u128::from(u.hi) << 64) | u128::from(u.lo); + json!(num.to_string()) + } + ScVal::I128(i) => { + #[allow(clippy::cast_possible_wrap)] + let lo = u128::from(i.lo) as i128; + let num = (i128::from(i.hi) << 64) | lo; + json!(num.to_string()) + } + ScVal::U256(u) => { + json!(format!( + "0x{:016x}{:016x}{:016x}{:016x}", + u.hi_hi, u.hi_lo, u.lo_hi, u.lo_lo + )) + } + ScVal::I256(i) => { + json!(format!( + "0x{:016x}{:016x}{:016x}{:016x}", + i.hi_hi, i.hi_lo, i.lo_hi, i.lo_lo + )) + } + ScVal::Bytes(b) => { + let hex_str: String = b.iter().map(|byte| format!("{byte:02x}")).collect(); + json!(format!("0x{hex_str}")) + } + ScVal::String(s) => json!(s.to_string()), + ScVal::Symbol(s) => json!(s.to_string()), + ScVal::Address(addr) => { + json!(crate::types::address::Address::from_sc_address(addr).to_string()) + } + ScVal::Error(e) => json!(format!("{e:?}")), + ScVal::Vec(Some(v)) => { + let items: Vec = v.iter().map(Self::decode_dynamic).collect(); + Value::Array(items) + } + ScVal::Map(Some(m)) => { + let mut obj = serde_json::Map::new(); + let mut all_string_keys = true; + let mut pairs = Vec::new(); + + for entry in m.iter() { + let k = Self::decode_dynamic(&entry.key); + let v = Self::decode_dynamic(&entry.val); + if let Value::String(ref k_str) = k { + obj.insert(k_str.clone(), v); + } else { + all_string_keys = false; + pairs.push(json!([k, v])); + } + } + + if all_string_keys { + Value::Object(obj) + } else { + Value::Array(pairs) + } + } + _ => json!(format!("{val:?}")), + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::spec::decoder::{ContractStructField, ContractStructDef}; + use stellar_xdr::curr::{ScSymbol, ScVec, VecM}; + + #[test] + fn test_primitive_return_decoding() { + let decoder = ReturnValueDecoder::new(); + assert_eq!( + decoder.decode(&ScVal::U32(42), Some(&ScSpecTypeDef::U32), None), + json!(42) + ); + assert_eq!( + decoder.decode(&ScVal::Bool(true), Some(&ScSpecTypeDef::Bool), None), + json!(true) + ); + assert_eq!( + decoder.decode(&ScVal::Void, Some(&ScSpecTypeDef::Void), None), + Value::Null + ); + } + + #[test] + fn test_struct_return_decoding_map_encoded() { + let decoder = ReturnValueDecoder::new(); + + let struct_def = ContractStructDef { + name: "User".to_string(), + fields: vec![ + ContractStructField { + name: "id".to_string(), + type_name: "U64".to_string(), + doc: None, + type_def: Some(ScSpecTypeDef::U64), + }, + ContractStructField { + name: "name".to_string(), + type_name: "String".to_string(), + doc: None, + type_def: Some(ScSpecTypeDef::String), + }, + ], + doc: None, + }; + + let contract_spec = ContractSpec { + errors: vec![], + functions: vec![], + structs: vec![struct_def], + name: None, + version: None, + enums: vec![], + unions: vec![], + }; + + let map_val = ScVal::Map(Some( + vec![ + stellar_xdr::curr::ScMapEntry { + key: ScVal::Symbol(ScSymbol("id".try_into().unwrap())), + val: ScVal::U64(100), + }, + stellar_xdr::curr::ScMapEntry { + key: ScVal::Symbol(ScSymbol("name".try_into().unwrap())), + val: ScVal::String("Alice".try_into().unwrap()), + }, + ] + .try_into() + .unwrap(), + )); + + let decoded = decoder.decode( + &map_val, + Some(&ScSpecTypeDef::Udt(stellar_xdr::curr::ScSpecTypeUdt { + name: ScSymbol("User".try_into().unwrap()), + })), + Some(&contract_spec), + ); + + assert_eq!(decoded, json!({ "id": 100, "name": "Alice" })); + } + + #[test] + fn test_result_return_decoding() { + let decoder = ReturnValueDecoder::new(); + + let ok_val = ScVal::Vec(Some( + vec![ScVal::Symbol(ScSymbol("Ok".try_into().unwrap())), ScVal::U32(200)] + .try_into() + .unwrap(), + )); + + let res_spec = ScSpecTypeDef::Result(Box::new(stellar_xdr::curr::ScSpecTypeResult { + ok_type: Box::new(ScSpecTypeDef::U32), + error_type: Box::new(ScSpecTypeDef::U32), + })); + + let decoded = decoder.decode(&ok_val, Some(&res_spec), None); + assert_eq!(decoded, json!({ "Ok": 200 })); + } + + #[test] + fn test_dynamic_fallback() { + let val = ScVal::Symbol(ScSymbol("hello".try_into().unwrap())); + assert_eq!(ReturnValueDecoder::decode_dynamic(&val), json!("hello")); + } +} diff --git a/crates/core/src/lib.rs b/crates/core/src/lib.rs index fc097961..3e24eec4 100644 --- a/crates/core/src/lib.rs +++ b/crates/core/src/lib.rs @@ -13,8 +13,8 @@ pub mod xdr; pub use decode::{ walk_diagnostic_events, AddressCredential, AddressWithNonce, AuthChain, AuthCredential, - AuthFunctionKind, AuthInvocation, DiagnosticEventKind, DiagnosticEventWalker, - StructuredDiagnosticEvent, + AuthFunctionKind, AuthInvocation, DecodedFunctionCall, DiagnosticEventKind, + DiagnosticEventWalker, FunctionCallDecoder, ReturnValueDecoder, StructuredDiagnosticEvent, }; pub use error::{GratError, GratResult}; pub use network::config::Network; diff --git a/crates/core/src/spec/decoder.rs b/crates/core/src/spec/decoder.rs index 88744830..dff2ab17 100644 --- a/crates/core/src/spec/decoder.rs +++ b/crates/core/src/spec/decoder.rs @@ -20,6 +20,12 @@ pub struct ContractFunction { pub return_type: String, pub doc: Option, + + #[serde(skip_serializing_if = "Option::is_none", default)] + pub return_type_def: Option, + + #[serde(skip_serializing_if = "Vec::is_empty", default)] + pub param_defs: Vec<(String, ScSpecTypeDef)>, } #[derive(Debug, Clone, Serialize, Deserialize)] @@ -29,6 +35,9 @@ pub struct ContractStructField { pub type_name: String, pub doc: Option, + + #[serde(skip_serializing_if = "Option::is_none", default)] + pub type_def: Option, } #[derive(Debug, Clone, Serialize, Deserialize)] @@ -40,6 +49,46 @@ pub struct ContractStructDef { pub doc: Option, } +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ContractEnumCase { + pub name: String, + + pub value: u32, + + pub doc: Option, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ContractEnumDef { + pub name: String, + + pub cases: Vec, + + pub doc: Option, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ContractUnionCase { + pub name: String, + + pub doc: Option, + + #[serde(skip_serializing_if = "Option::is_none", default)] + pub value_types: Option>, + + #[serde(skip_serializing_if = "Option::is_none", default)] + pub fields: Option>, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ContractUnionDef { + pub name: String, + + pub cases: Vec, + + pub doc: Option, +} + #[derive(Debug, Clone, Serialize, Deserialize)] pub struct ContractSpec { pub errors: Vec, @@ -51,6 +100,12 @@ pub struct ContractSpec { pub name: Option, pub version: Option, + + #[serde(skip_serializing_if = "Vec::is_empty", default)] + pub enums: Vec, + + #[serde(skip_serializing_if = "Vec::is_empty", default)] + pub unions: Vec, } pub fn decode_contract_spec(wasm_bytes: &[u8]) -> GratResult { @@ -59,6 +114,8 @@ pub fn decode_contract_spec(wasm_bytes: &[u8]) -> GratResult { let mut errors = Vec::new(); let mut functions = Vec::new(); let mut structs = Vec::new(); + let mut enums = Vec::new(); + let mut unions = Vec::new(); let name = None; let version = None; @@ -75,12 +132,20 @@ pub fn decode_contract_spec(wasm_bytes: &[u8]) -> GratResult { }; let mut params = Vec::new(); + let mut param_defs = Vec::new(); for input in func.inputs.iter() { let param_name = input.name.to_string(); let param_type = format_type_def(&input.type_); - params.push((param_name, param_type)); + params.push((param_name.clone(), param_type)); + param_defs.push((param_name, input.type_.clone())); } + let return_type_def = if func.outputs.is_empty() { + Some(ScSpecTypeDef::Void) + } else { + Some(func.outputs[0].clone()) + }; + let return_type = if func.outputs.is_empty() { "Void".to_string() } else { @@ -92,6 +157,8 @@ pub fn decode_contract_spec(wasm_bytes: &[u8]) -> GratResult { params, return_type, doc, + return_type_def, + param_defs, }); } ScSpecEntry::UdtErrorEnumV0(err_enum) => { @@ -111,6 +178,105 @@ pub fn decode_contract_spec(wasm_bytes: &[u8]) -> GratResult { }); } } + ScSpecEntry::UdtEnumV0(enum_spec) => { + let enum_name = enum_spec.name.to_string(); + let doc = if enum_spec.doc.is_empty() { + None + } else { + Some(enum_spec.doc.to_string()) + }; + let mut cases = Vec::new(); + for case in enum_spec.cases.iter() { + let case_doc = if case.doc.is_empty() { + None + } else { + Some(case.doc.to_string()) + }; + cases.push(ContractEnumCase { + name: case.name.to_string(), + value: case.value, + doc: case_doc, + }); + } + enums.push(ContractEnumDef { + name: enum_name, + cases, + doc, + }); + } + ScSpecEntry::UdtUnionV0(union_spec) => { + let union_name = union_spec.name.to_string(); + let doc = if union_spec.doc.is_empty() { + None + } else { + Some(union_spec.doc.to_string()) + }; + let mut cases = Vec::new(); + for case in union_spec.cases.iter() { + match case { + stellar_xdr::curr::ScSpecUdtUnionCaseV0::VoidV0(c) => { + let case_doc = if c.doc.is_empty() { + None + } else { + Some(c.doc.to_string()) + }; + cases.push(ContractUnionCase { + name: c.name.to_string(), + doc: case_doc, + value_types: None, + fields: None, + }); + } + stellar_xdr::curr::ScSpecUdtUnionCaseV0::TupleV0(c) => { + let case_doc = if c.doc.is_empty() { + None + } else { + Some(c.doc.to_string()) + }; + let value_types: Vec = + c.type_.iter().cloned().collect(); + cases.push(ContractUnionCase { + name: c.name.to_string(), + doc: case_doc, + value_types: Some(value_types), + fields: None, + }); + } + stellar_xdr::curr::ScSpecUdtUnionCaseV0::StructV0(c) => { + let case_doc = if c.doc.is_empty() { + None + } else { + Some(c.doc.to_string()) + }; + let mut fields = Vec::new(); + for field in c.fields.iter() { + let field_doc = if field.doc.is_empty() { + None + } else { + Some(field.doc.to_string()) + }; + fields.push(ContractStructField { + name: field.name.to_string(), + type_name: format_type_def(&field.type_), + doc: field_doc, + type_def: Some(field.type_.clone()), + }); + } + cases.push(ContractUnionCase { + name: c.name.to_string(), + doc: case_doc, + value_types: None, + fields: Some(fields), + }); + } + } + } + unions.push(ContractUnionDef { + name: union_name, + cases, + doc, + }); + } ScSpecEntry::UdtStructV0(struct_spec) => { let struct_name = struct_spec.name.to_string(); let doc = if struct_spec.doc.is_empty() { @@ -132,6 +298,7 @@ pub fn decode_contract_spec(wasm_bytes: &[u8]) -> GratResult { name: field_name, type_name: field_type, doc: field_doc, + type_def: Some(field.type_.clone()), }); } @@ -151,6 +318,8 @@ pub fn decode_contract_spec(wasm_bytes: &[u8]) -> GratResult { structs, name, version, + enums, + unions, }) } diff --git a/crates/core/src/spec/mod.rs b/crates/core/src/spec/mod.rs index fdc6c1ab..c271b767 100644 --- a/crates/core/src/spec/mod.rs +++ b/crates/core/src/spec/mod.rs @@ -2,7 +2,7 @@ pub mod decoder; pub mod resolver; pub use decoder::{ - ContractErrorEntry, ContractFunction, ContractSpec, ContractStructDef, ContractStructField, - SpecParser, + ContractEnumCase, ContractEnumDef, ContractErrorEntry, ContractFunction, ContractSpec, + ContractStructDef, ContractStructField, ContractUnionCase, ContractUnionDef, SpecParser, }; pub use resolver::{ContractId, ResolverStats, SCSpecResolver};