diff --git a/crates/core/src/decode/function_call_decoder.rs b/crates/core/src/decode/function_call_decoder.rs index afe916d4..223678f4 100644 --- a/crates/core/src/decode/function_call_decoder.rs +++ b/crates/core/src/decode/function_call_decoder.rs @@ -1,14 +1,32 @@ +use serde::Serialize; +use serde_json::Value; + +pub type JsonValue = Value; + #[derive(Debug, Clone, Serialize)] pub struct DecodedArgument { pub name: String, + pub value: JsonValue, + pub formatted: String, } #[derive(Debug, Clone, Serialize)] pub struct DecodedFunctionCall { pub function_name: String, + pub arguments: Vec, + pub return_value: Option, + pub formatted_return_value: Option, } + +pub struct FunctionCallDecoder; + +impl FunctionCallDecoder { + pub fn new() -> Self { + Self + } +} diff --git a/crates/core/src/decode/mod.rs b/crates/core/src/decode/mod.rs index 00e9b282..229f66da 100644 --- a/crates/core/src/decode/mod.rs +++ b/crates/core/src/decode/mod.rs @@ -2,7 +2,6 @@ pub mod auth; pub mod auth_address_nonce; pub mod auth_signature; pub mod chain_analyzer; -pub mod context; pub mod contract_error; pub mod contract_error_resolver; pub mod cross_contract; diff --git a/crates/core/src/decode/return_decoder.rs b/crates/core/src/decode/return_decoder.rs index bf66f5ea..7aa95802 100644 --- a/crates/core/src/decode/return_decoder.rs +++ b/crates/core/src/decode/return_decoder.rs @@ -142,7 +142,7 @@ impl ReturnValueDecoder { }, ScSpecTypeDef::Address => match val { ScVal::Address(addr) => { - json!(crate::types::address::Address::from_sc_address(addr).to_string()) + json!(addr.to_string()) } _ => Self::decode_dynamic(val), }, @@ -400,7 +400,7 @@ impl ReturnValueDecoder { 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()) + json!(addr.to_string()) } ScVal::Error(e) => json!(format!("{e:?}")), ScVal::Vec(Some(v)) => { @@ -438,7 +438,7 @@ impl ReturnValueDecoder { mod tests { use super::*; use crate::spec::decoder::{ContractStructField, ContractStructDef}; - use stellar_xdr::curr::{ScSymbol, ScVec, VecM}; + use stellar_xdr::curr::{ScString, ScSymbol}; #[test] fn test_primitive_return_decoding() { @@ -498,7 +498,7 @@ mod tests { }, stellar_xdr::curr::ScMapEntry { key: ScVal::Symbol(ScSymbol("name".try_into().unwrap())), - val: ScVal::String("Alice".try_into().unwrap()), + val: ScVal::String(ScString("Alice".try_into().unwrap())), }, ] .try_into() @@ -508,7 +508,7 @@ mod tests { let decoded = decoder.decode( &map_val, Some(&ScSpecTypeDef::Udt(stellar_xdr::curr::ScSpecTypeUdt { - name: ScSymbol("User".try_into().unwrap()), + name: "User".try_into().unwrap(), })), Some(&contract_spec), ); diff --git a/crates/core/src/spec/decoder.rs b/crates/core/src/spec/decoder.rs index e8e6d6c9..4c9dd192 100644 --- a/crates/core/src/spec/decoder.rs +++ b/crates/core/src/spec/decoder.rs @@ -221,72 +221,51 @@ pub fn decode_contract_spec(wasm_bytes: &[u8]) -> GratResult { 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), - }); - } + 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, + }); + } + } unions.push(ContractUnionDef { name: union_name, @@ -324,8 +303,13 @@ pub fn decode_contract_spec(wasm_bytes: &[u8]) -> GratResult { doc, }); } - }, - Err(_) => break, + + structs.push(ContractStructDef { + name: struct_name, + fields, + doc, + }); + } } } @@ -557,6 +541,8 @@ 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()); diff --git a/crates/core/src/spec/mod.rs b/crates/core/src/spec/mod.rs index 311306a4..ab0b521c 100644 --- a/crates/core/src/spec/mod.rs +++ b/crates/core/src/spec/mod.rs @@ -6,4 +6,6 @@ pub use decoder::{ ContractStructDef, ContractStructField, ContractUnionCase, ContractUnionDef, SpecParser, }; pub use resolver::{ContractId, ResolverStats, SCSpecResolver}; -pub use stellar_xdr::curr::ScSpecUdtStructV0; + +#[cfg(test)] +mod tests; diff --git a/crates/core/src/spec/tests/mod.rs b/crates/core/src/spec/tests/mod.rs new file mode 100644 index 00000000..172ca384 --- /dev/null +++ b/crates/core/src/spec/tests/mod.rs @@ -0,0 +1 @@ +pub mod wasm_tests; diff --git a/crates/core/src/spec/tests/wasm_tests.rs b/crates/core/src/spec/tests/wasm_tests.rs new file mode 100644 index 00000000..bd330c8c --- /dev/null +++ b/crates/core/src/spec/tests/wasm_tests.rs @@ -0,0 +1,267 @@ +use crate::spec::decoder::{decode_contract_spec, ContractStructDef}; +use stellar_xdr::curr::{ + Limits, ScSpecEntry, ScSpecTypeDef, ScSpecTypeUdt, ScSpecTypeVec, + ScSpecUdtStructFieldV0, ScSpecUdtStructV0, WriteXdr, +}; + +fn leb128_encode(mut value: u64) -> Vec { + let mut result = Vec::new(); + loop { + let mut byte = (value & 0x7f) as u8; + value >>= 7; + if value != 0 { + byte |= 0x80; + } + result.push(byte); + if value == 0 { + break; + } + } + result +} + +fn build_wasm_with_spec_entries(entries: &[ScSpecEntry]) -> Vec { + let mut xdr_data = Vec::new(); + for entry in entries { + let bytes = entry.to_xdr(Limits::none()).unwrap(); + xdr_data.extend_from_slice(&bytes); + } + + let name = "contractspecv0"; + let mut payload = Vec::new(); + payload.extend_from_slice(&leb128_encode(name.len() as u64)); + payload.extend_from_slice(name.as_bytes()); + payload.extend_from_slice(&xdr_data); + + let mut wasm = vec![0x00, 0x61, 0x73, 0x6D, 0x01, 0x00, 0x00, 0x00]; + wasm.push(0); + wasm.extend_from_slice(&leb128_encode(payload.len() as u64)); + wasm.extend_from_slice(&payload); + wasm +} + +fn inner_struct_entry() -> ScSpecEntry { + ScSpecEntry::UdtStructV0(ScSpecUdtStructV0 { + doc: "".try_into().unwrap(), + lib: "".try_into().unwrap(), + name: "Inner".try_into().unwrap(), + fields: vec![ScSpecUdtStructFieldV0 { + doc: "".try_into().unwrap(), + name: "value".try_into().unwrap(), + type_: ScSpecTypeDef::U32, + }] + .try_into() + .unwrap(), + }) +} + +fn middle_struct_entry() -> ScSpecEntry { + ScSpecEntry::UdtStructV0(ScSpecUdtStructV0 { + doc: "A struct containing Inner".try_into().unwrap(), + lib: "".try_into().unwrap(), + name: "Middle".try_into().unwrap(), + fields: vec![ + ScSpecUdtStructFieldV0 { + doc: "".try_into().unwrap(), + name: "inner".try_into().unwrap(), + type_: ScSpecTypeDef::Udt(ScSpecTypeUdt { + name: "Inner".try_into().unwrap(), + }), + }, + ScSpecUdtStructFieldV0 { + doc: "A label for the struct".try_into().unwrap(), + name: "label".try_into().unwrap(), + type_: ScSpecTypeDef::Symbol, + }, + ] + .try_into() + .unwrap(), + }) +} + +fn outer_struct_entry() -> ScSpecEntry { + ScSpecEntry::UdtStructV0(ScSpecUdtStructV0 { + doc: "Top-level struct with deep nesting".try_into().unwrap(), + lib: "".try_into().unwrap(), + name: "Outer".try_into().unwrap(), + fields: vec![ + ScSpecUdtStructFieldV0 { + doc: "".try_into().unwrap(), + name: "middle".try_into().unwrap(), + type_: ScSpecTypeDef::Udt(ScSpecTypeUdt { + name: "Middle".try_into().unwrap(), + }), + }, + ScSpecUdtStructFieldV0 { + doc: "".try_into().unwrap(), + name: "items".try_into().unwrap(), + type_: ScSpecTypeDef::Vec(Box::new(ScSpecTypeVec { + element_type: Box::new(ScSpecTypeDef::Udt(ScSpecTypeUdt { + name: "Inner".try_into().unwrap(), + })), + })), + }, + ScSpecUdtStructFieldV0 { + doc: "".try_into().unwrap(), + name: "id".try_into().unwrap(), + type_: ScSpecTypeDef::U64, + }, + ] + .try_into() + .unwrap(), + }) +} + +fn find_struct<'a>(structs: &'a [ContractStructDef], name: &str) -> Option<&'a ContractStructDef> { + structs.iter().find(|s| s.name == name) +} + +#[test] +fn test_deeply_nested_structs_extracted_correctly() { + let entries = vec![ + inner_struct_entry(), + middle_struct_entry(), + outer_struct_entry(), + ]; + let wasm = build_wasm_with_spec_entries(&entries); + let spec = decode_contract_spec(&wasm).unwrap(); + + assert_eq!(spec.structs.len(), 3, "should extract all three structs"); + + let inner = find_struct(&spec.structs, "Inner").expect("Inner struct should exist"); + assert_eq!(inner.fields.len(), 1); + assert_eq!(inner.fields[0].name, "value"); + assert_eq!(inner.fields[0].type_name, "U32"); + assert_eq!(inner.doc, None); + + let middle = find_struct(&spec.structs, "Middle").expect("Middle struct should exist"); + assert_eq!(middle.fields.len(), 2); + assert_eq!(middle.fields[0].name, "inner"); + assert_eq!(middle.fields[0].type_name, "Inner"); + assert_eq!(middle.fields[1].name, "label"); + assert_eq!(middle.fields[1].type_name, "Symbol"); + assert_eq!(middle.doc, Some("A struct containing Inner".to_string())); + + let outer = find_struct(&spec.structs, "Outer").expect("Outer struct should exist"); + assert_eq!(outer.fields.len(), 3); + assert_eq!(outer.fields[0].name, "middle"); + assert_eq!(outer.fields[0].type_name, "Middle"); + assert_eq!(outer.fields[1].name, "items"); + assert_eq!(outer.fields[1].type_name, "Vec"); + assert_eq!(outer.fields[2].name, "id"); + assert_eq!(outer.fields[2].type_name, "U64"); + assert_eq!( + outer.doc, + Some("Top-level struct with deep nesting".to_string()) + ); +} + +#[test] +fn test_nested_struct_type_defs_preserved() { + let entries = vec![inner_struct_entry(), middle_struct_entry()]; + let wasm = build_wasm_with_spec_entries(&entries); + let spec = decode_contract_spec(&wasm).unwrap(); + + let middle = find_struct(&spec.structs, "Middle").unwrap(); + + let inner_field = &middle.fields[0]; + let type_def = inner_field + .type_def + .as_ref() + .expect("field type_def should be present"); + + match type_def { + ScSpecTypeDef::Udt(udt) => { + let name: String = udt.name.to_string(); + assert_eq!(name, "Inner"); + } + other => panic!("expected Udt variant, got {other:?}"), + } + + let label_field = &middle.fields[1]; + let label_def = label_field + .type_def + .as_ref() + .expect("label type_def should be present"); + assert!(matches!(label_def, ScSpecTypeDef::Symbol)); +} + +#[test] +fn test_vec_of_nested_struct_type_def() { + let entries = vec![inner_struct_entry(), outer_struct_entry()]; + let wasm = build_wasm_with_spec_entries(&entries); + let spec = decode_contract_spec(&wasm).unwrap(); + + let outer = find_struct(&spec.structs, "Outer").unwrap(); + let items_field = &outer.fields[1]; + + assert_eq!(items_field.name, "items"); + assert_eq!(items_field.type_name, "Vec"); + + let type_def = items_field + .type_def + .as_ref() + .expect("items type_def should be present"); + + match type_def { + ScSpecTypeDef::Vec(vec) => { + match &*vec.element_type { + ScSpecTypeDef::Udt(udt) => { + let name: String = udt.name.to_string(); + assert_eq!(name, "Inner"); + } + other => panic!("expected Vec element to be Udt, got {other:?}"), + } + } + other => panic!("expected Vec variant, got {other:?}"), + } +} + +#[test] +fn test_three_deep_nesting_chain() { + let entries = vec![ + inner_struct_entry(), + middle_struct_entry(), + outer_struct_entry(), + ]; + let wasm = build_wasm_with_spec_entries(&entries); + let spec = decode_contract_spec(&wasm).unwrap(); + + let outer = find_struct(&spec.structs, "Outer").unwrap(); + + let middle_field = &outer.fields[0]; + assert_eq!(middle_field.name, "middle"); + assert_eq!(middle_field.type_name, "Middle"); + + let middle_type = middle_field + .type_def + .as_ref() + .expect("middle type_def should be present"); + match middle_type { + ScSpecTypeDef::Udt(udt) => { + let name: String = udt.name.to_string(); + assert_eq!(name, "Middle"); + } + other => panic!("expected Udt for middle field, got {other:?}"), + } + + let items_field = &outer.fields[1]; + assert_eq!(items_field.type_name, "Vec"); + + let id_field = &outer.fields[2]; + assert_eq!(id_field.type_name, "U64"); +} + +#[test] +fn test_struct_with_no_nesting() { + let entries = vec![inner_struct_entry()]; + let wasm = build_wasm_with_spec_entries(&entries); + let spec = decode_contract_spec(&wasm).unwrap(); + + assert_eq!(spec.structs.len(), 1); + let inner = &spec.structs[0]; + assert_eq!(inner.name, "Inner"); + assert_eq!(inner.fields.len(), 1); + assert_eq!(inner.fields[0].name, "value"); + assert_eq!(inner.fields[0].type_name, "U32"); +}