diff --git a/crates/core/src/decode/argument_decoder.rs b/crates/core/src/decode/argument_decoder.rs new file mode 100644 index 00000000..92c05aac --- /dev/null +++ b/crates/core/src/decode/argument_decoder.rs @@ -0,0 +1,307 @@ +use crate::decode::function_call_decoder::DecodedArgument; +use crate::decode::return_decoder::ReturnValueDecoder; +use crate::spec::decoder::{ContractFunction, ContractSpec}; +use serde_json::Value; +use stellar_xdr::curr::{ScSpecTypeDef, ScVal}; + +/// Decoder for contract function arguments, converting raw `ScVal` structures +/// into typed, human-readable `DecodedArgument` representations based on contract function parameter specifications. +#[derive(Debug, Clone, Default)] +pub struct ArgumentDecoder { + return_decoder: ReturnValueDecoder, +} + +impl ArgumentDecoder { + /// Creates a new `ArgumentDecoder`. + pub fn new() -> Self { + Self { + return_decoder: ReturnValueDecoder::new(), + } + } + + /// Decodes a list of raw `ScVal` arguments using a slice of parameter definitions `(name, type_def)`. + /// + /// Edge cases handled: + /// - Extra arguments beyond parameter specs are decoded dynamically with fallback names `argN`. + /// - Fewer arguments than parameter specs decode only the supplied arguments. + /// - Type conflicts between the raw `ScVal` and the `ScSpecTypeDef` fall back to dynamic decoding. + pub fn decode( + &self, + args: &[ScVal], + param_defs: &[(String, ScSpecTypeDef)], + contract_spec: Option<&ContractSpec>, + ) -> Vec { + args.iter() + .enumerate() + .map(|(i, val)| { + if let Some((param_name, type_def)) = param_defs.get(i) { + self.decode_single_argument(param_name, val, Some(type_def), contract_spec) + } else { + let fallback_name = format!("arg{i}"); + self.decode_single_argument(&fallback_name, val, None, contract_spec) + } + }) + .collect() + } + + /// Decodes a list of raw `ScVal` arguments given a target `ContractFunction` spec. + pub fn decode_function_args( + &self, + args: &[ScVal], + func: &ContractFunction, + contract_spec: Option<&ContractSpec>, + ) -> Vec { + if !func.param_defs.is_empty() { + self.decode(args, &func.param_defs, contract_spec) + } else { + args.iter() + .enumerate() + .map(|(i, val)| { + let name = func + .params + .get(i) + .map(|(pname, _)| pname.clone()) + .unwrap_or_else(|| format!("arg{i}")); + self.decode_single_argument(&name, val, None, contract_spec) + }) + .collect() + } + } + + /// Decodes raw `ScVal` arguments dynamically without parameter specifications. + pub fn decode_dynamic(&self, args: &[ScVal]) -> Vec { + args.iter() + .enumerate() + .map(|(i, val)| { + let name = format!("arg{i}"); + self.decode_single_argument(&name, val, None, None) + }) + .collect() + } + + /// Decodes a single `ScVal` argument into a `DecodedArgument`. + pub fn decode_single_argument( + &self, + name: &str, + val: &ScVal, + type_def: Option<&ScSpecTypeDef>, + contract_spec: Option<&ContractSpec>, + ) -> DecodedArgument { + let value = self.return_decoder.decode(val, type_def, contract_spec); + let formatted = Self::format_value(&value); + DecodedArgument { + name: name.to_string(), + value, + formatted, + } + } + + /// Formats a decoded JSON value into a clean human-readable string representation. + pub fn format_value(value: &Value) -> String { + match value { + Value::String(s) => s.clone(), + Value::Null => "null".to_string(), + Value::Bool(b) => b.to_string(), + Value::Number(n) => n.to_string(), + other => serde_json::to_string(other).unwrap_or_else(|_| other.to_string()), + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::spec::decoder::{ContractStructDef, ContractStructField}; + use serde_json::json; + use stellar_xdr::curr::{ + Hash, ScAddress, ScMapEntry, ScString, ScSymbol, ScVal, + }; + + fn sym(s: &str) -> ScVal { + ScVal::Symbol(ScSymbol(s.try_into().unwrap())) + } + + fn str_val(s: &str) -> ScVal { + ScVal::String(ScString(s.try_into().unwrap())) + } + + #[test] + fn test_primitive_argument_decoding() { + let decoder = ArgumentDecoder::new(); + let args = vec![ScVal::U32(42), ScVal::Bool(true), sym("admin")]; + let param_defs = vec![ + ("amount".to_string(), ScSpecTypeDef::U32), + ("active".to_string(), ScSpecTypeDef::Bool), + ("role".to_string(), ScSpecTypeDef::Symbol), + ]; + + let decoded = decoder.decode(&args, ¶m_defs, None); + assert_eq!(decoded.len(), 3); + + assert_eq!(decoded[0].name, "amount"); + assert_eq!(decoded[0].value, json!(42)); + assert_eq!(decoded[0].formatted, "42"); + + assert_eq!(decoded[1].name, "active"); + assert_eq!(decoded[1].value, json!(true)); + assert_eq!(decoded[1].formatted, "true"); + + assert_eq!(decoded[2].name, "role"); + assert_eq!(decoded[2].value, json!("admin")); + assert_eq!(decoded[2].formatted, "admin"); + } + + #[test] + fn test_argument_count_mismatch_extra_arguments() { + let decoder = ArgumentDecoder::new(); + let args = vec![ScVal::U32(100), ScVal::I32(-5), sym("extra")]; + let param_defs = vec![("count".to_string(), ScSpecTypeDef::U32)]; + + let decoded = decoder.decode(&args, ¶m_defs, None); + assert_eq!(decoded.len(), 3); + + assert_eq!(decoded[0].name, "count"); + assert_eq!(decoded[0].value, json!(100)); + + assert_eq!(decoded[1].name, "arg1"); + assert_eq!(decoded[1].value, json!(-5)); + assert_eq!(decoded[1].formatted, "-5"); + + assert_eq!(decoded[2].name, "arg2"); + assert_eq!(decoded[2].value, json!("extra")); + assert_eq!(decoded[2].formatted, "extra"); + } + + #[test] + fn test_argument_count_mismatch_fewer_arguments() { + let decoder = ArgumentDecoder::new(); + let args = vec![ScVal::U32(10)]; + let param_defs = vec![ + ("first".to_string(), ScSpecTypeDef::U32), + ("second".to_string(), ScSpecTypeDef::String), + ]; + + let decoded = decoder.decode(&args, ¶m_defs, None); + assert_eq!(decoded.len(), 1); + assert_eq!(decoded[0].name, "first"); + assert_eq!(decoded[0].value, json!(10)); + } + + #[test] + fn test_type_conflict_fallback() { + let decoder = ArgumentDecoder::new(); + // Expecting U32, but provided a Symbol + let args = vec![sym("not_a_number")]; + let param_defs = vec![("expected_num".to_string(), ScSpecTypeDef::U32)]; + + let decoded = decoder.decode(&args, ¶m_defs, None); + assert_eq!(decoded.len(), 1); + assert_eq!(decoded[0].name, "expected_num"); + assert_eq!(decoded[0].value, json!("not_a_number")); + assert_eq!(decoded[0].formatted, "not_a_number"); + } + + #[test] + fn test_udt_struct_argument_decoding() { + let decoder = ArgumentDecoder::new(); + let struct_def = ContractStructDef { + name: "Recipient".to_string(), + fields: vec![ + ContractStructField { + name: "address".to_string(), + type_name: "Address".to_string(), + doc: None, + type_def: Some(ScSpecTypeDef::Address), + }, + ContractStructField { + name: "weight".to_string(), + type_name: "U32".to_string(), + doc: None, + type_def: Some(ScSpecTypeDef::U32), + }, + ], + doc: None, + }; + + let contract_spec = ContractSpec { + errors: vec![], + functions: vec![], + structs: vec![struct_def], + name: None, + version: None, + enums: vec![], + unions: vec![], + }; + + let addr = ScAddress::Contract(Hash([1u8; 32])); + let map_val = ScVal::Map(Some( + vec![ + ScMapEntry { + key: sym("address"), + val: ScVal::Address(addr.clone()), + }, + ScMapEntry { + key: sym("weight"), + val: ScVal::U32(50), + }, + ] + .try_into() + .unwrap(), + )); + + let param_defs = vec![( + "target".to_string(), + ScSpecTypeDef::Udt(stellar_xdr::curr::ScSpecTypeUdt { + name: "Recipient".try_into().unwrap(), + }), + )]; + + let decoded = decoder.decode(&vec![map_val], ¶m_defs, Some(&contract_spec)); + assert_eq!(decoded.len(), 1); + assert_eq!(decoded[0].name, "target"); + assert_eq!( + decoded[0].value, + json!({ "address": addr.to_string(), "weight": 50 }) + ); + assert!(decoded[0].formatted.contains("weight")); + } + + #[test] + fn test_decode_function_args_convenience() { + let decoder = ArgumentDecoder::new(); + let func = ContractFunction { + name: "transfer".to_string(), + params: vec![("to".to_string(), "Address".to_string()), ("amount".to_string(), "U128".to_string())], + return_type: "Void".to_string(), + doc: None, + return_type_def: Some(ScSpecTypeDef::Void), + param_defs: vec![ + ("to".to_string(), ScSpecTypeDef::Address), + ("amount".to_string(), ScSpecTypeDef::U64), + ], + }; + + let addr = ScAddress::Contract(Hash([2u8; 32])); + let args = vec![ScVal::Address(addr.clone()), ScVal::U64(1000)]; + + let decoded = decoder.decode_function_args(&args, &func, None); + assert_eq!(decoded.len(), 2); + assert_eq!(decoded[0].name, "to"); + assert_eq!(decoded[0].value, json!(addr.to_string())); + assert_eq!(decoded[1].name, "amount"); + assert_eq!(decoded[1].value, json!(1000)); + } + + #[test] + fn test_decode_dynamic() { + let decoder = ArgumentDecoder::new(); + let args = vec![ScVal::U32(1), str_val("test")]; + let decoded = decoder.decode_dynamic(&args); + + assert_eq!(decoded.len(), 2); + assert_eq!(decoded[0].name, "arg0"); + assert_eq!(decoded[0].value, json!(1)); + assert_eq!(decoded[1].name, "arg1"); + assert_eq!(decoded[1].value, json!("test")); + } +} diff --git a/crates/core/src/decode/function_call_decoder.rs b/crates/core/src/decode/function_call_decoder.rs index 223678f4..8d956ce1 100644 --- a/crates/core/src/decode/function_call_decoder.rs +++ b/crates/core/src/decode/function_call_decoder.rs @@ -1,9 +1,13 @@ -use serde::Serialize; +use crate::decode::argument_decoder::ArgumentDecoder; +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; pub type JsonValue = Value; -#[derive(Debug, Clone, Serialize)] +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] pub struct DecodedArgument { pub name: String, @@ -12,7 +16,7 @@ pub struct DecodedArgument { pub formatted: String, } -#[derive(Debug, Clone, Serialize)] +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] pub struct DecodedFunctionCall { pub function_name: String, @@ -23,10 +27,116 @@ pub struct DecodedFunctionCall { pub formatted_return_value: Option, } -pub struct FunctionCallDecoder; +#[derive(Debug, Clone, Default)] +pub struct FunctionCallDecoder { + argument_decoder: ArgumentDecoder, + return_decoder: ReturnValueDecoder, +} impl FunctionCallDecoder { pub fn new() -> Self { - Self + Self { + argument_decoder: ArgumentDecoder::new(), + return_decoder: ReturnValueDecoder::new(), + } + } + + /// Decodes a full smart contract function call, mapping arguments and optional return value + /// using function parameter and return specs. + pub fn decode( + &self, + function_name: &str, + raw_args: &[ScVal], + func_spec: Option<&ContractFunction>, + return_val: Option<&ScVal>, + contract_spec: Option<&ContractSpec>, + ) -> DecodedFunctionCall { + let arguments = match func_spec { + Some(func) => self + .argument_decoder + .decode_function_args(raw_args, func, contract_spec), + None => self.argument_decoder.decode_dynamic(raw_args), + }; + + let (return_value, formatted_return_value) = match return_val { + Some(val) => { + let type_def = func_spec.and_then(|f| f.return_type_def.as_ref()); + let json_val = self.return_decoder.decode(val, type_def, contract_spec); + let formatted = self + .return_decoder + .decode_to_string(val, type_def, contract_spec); + (Some(json_val), Some(formatted)) + } + None => (None, None), + }; + + DecodedFunctionCall { + function_name: function_name.to_string(), + arguments, + return_value, + formatted_return_value, + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use stellar_xdr::curr::ScSpecTypeDef; + + #[test] + fn test_function_call_decoder_with_specs() { + let decoder = FunctionCallDecoder::new(); + let func = ContractFunction { + name: "add".to_string(), + params: vec![("a".to_string(), "U32".to_string()), ("b".to_string(), "U32".to_string())], + return_type: "U32".to_string(), + doc: None, + return_type_def: Some(ScSpecTypeDef::U32), + param_defs: vec![ + ("a".to_string(), ScSpecTypeDef::U32), + ("b".to_string(), ScSpecTypeDef::U32), + ], + }; + + let raw_args = vec![ScVal::U32(10), ScVal::U32(20)]; + let return_val = ScVal::U32(30); + + let decoded = decoder.decode( + "add", + &raw_args, + Some(&func), + Some(&return_val), + None, + ); + + assert_eq!(decoded.function_name, "add"); + assert_eq!(decoded.arguments.len(), 2); + assert_eq!(decoded.arguments[0].name, "a"); + assert_eq!(decoded.arguments[0].value, serde_json::json!(10)); + assert_eq!(decoded.arguments[1].name, "b"); + assert_eq!(decoded.arguments[1].value, serde_json::json!(20)); + assert_eq!(decoded.return_value, Some(serde_json::json!(30))); + assert_eq!(decoded.formatted_return_value, Some("30".to_string())); + } + + #[test] + fn test_function_call_decoder_dynamic_fallback() { + let decoder = FunctionCallDecoder::new(); + let raw_args = vec![ScVal::U32(5)]; + + let decoded = decoder.decode( + "test_func", + &raw_args, + None, + None, + None, + ); + + assert_eq!(decoded.function_name, "test_func"); + assert_eq!(decoded.arguments.len(), 1); + assert_eq!(decoded.arguments[0].name, "arg0"); + assert_eq!(decoded.arguments[0].value, serde_json::json!(5)); + assert!(decoded.return_value.is_none()); } } diff --git a/crates/core/src/decode/mod.rs b/crates/core/src/decode/mod.rs index 229f66da..7dce2d34 100644 --- a/crates/core/src/decode/mod.rs +++ b/crates/core/src/decode/mod.rs @@ -1,3 +1,4 @@ +pub mod argument_decoder; pub mod auth; pub mod auth_address_nonce; pub mod auth_signature; @@ -18,12 +19,13 @@ pub mod resource_analyzer; pub mod scval_to_json; pub mod walker; +pub use argument_decoder::ArgumentDecoder; pub use auth::{ AddressCredential, AuthChain, AuthCredential, AuthFunctionKind, AuthInvocation, AuthorizationType, }; pub use auth_address_nonce::AddressWithNonce; -pub use function_call_decoder::{DecodedFunctionCall, FunctionCallDecoder}; +pub use function_call_decoder::{DecodedArgument, DecodedFunctionCall, FunctionCallDecoder}; pub use return_decoder::ReturnValueDecoder; pub use chain_analyzer::{analyze_call_chain, CallChain, ChainAnalyzer, ChainFrame, FrameRole}; pub use resource_analyzer::{ diff --git a/crates/core/src/lib.rs b/crates/core/src/lib.rs index 3e24eec4..cb31b00a 100644 --- a/crates/core/src/lib.rs +++ b/crates/core/src/lib.rs @@ -12,9 +12,10 @@ pub mod types; pub mod xdr; pub use decode::{ - walk_diagnostic_events, AddressCredential, AddressWithNonce, AuthChain, AuthCredential, - AuthFunctionKind, AuthInvocation, DecodedFunctionCall, DiagnosticEventKind, - DiagnosticEventWalker, FunctionCallDecoder, ReturnValueDecoder, StructuredDiagnosticEvent, + walk_diagnostic_events, AddressCredential, AddressWithNonce, ArgumentDecoder, AuthChain, + AuthCredential, AuthFunctionKind, AuthInvocation, DecodedArgument, 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 4c9dd192..d4db14b9 100644 --- a/crates/core/src/spec/decoder.rs +++ b/crates/core/src/spec/decoder.rs @@ -221,51 +221,45 @@ pub fn decode_contract_spec(wasm_bytes: &[u8]) -> GratResult { 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, - }); + 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, + }); + } } - } unions.push(ContractUnionDef { name: union_name, @@ -303,13 +297,8 @@ pub fn decode_contract_spec(wasm_bytes: &[u8]) -> GratResult { doc, }); } - - structs.push(ContractStructDef { - name: struct_name, - fields, - doc, - }); - } + }, + Err(_) => break, } } @@ -494,7 +483,7 @@ fn make_struct_spec_entry( doc: &str, fields: Vec<(&str, &str, ScSpecTypeDef)>, ) -> ScSpecEntry { - use stellar_xdr::curr::{ScSpecUdtStructFieldV0, ScSpecUdtStructV0, VecM}; + use stellar_xdr::curr::{ScSpecUdtStructFieldV0, ScSpecUdtStructV0}; let struct_fields: Vec = fields .into_iter() @@ -507,6 +496,7 @@ fn make_struct_spec_entry( ScSpecEntry::UdtStructV0(ScSpecUdtStructV0 { doc: doc.try_into().unwrap(), + lib: "".try_into().unwrap(), name: name.try_into().unwrap(), fields: struct_fields.try_into().unwrap(), }) @@ -541,8 +531,6 @@ mod tests { unions: Vec::new(), name: None, version: None, - enums: Vec::new(), - unions: Vec::new(), }; assert!(resolve_error_code(&spec, 99).is_none()); assert!(resolve_error_code(&spec, 1).is_some());