From f6429f81d153c903607f78ae2c74dc52abfb3a5e Mon Sep 17 00:00:00 2001 From: OluRemiFour Date: Wed, 29 Jul 2026 16:23:04 +0100 Subject: [PATCH 1/3] WASM parsing with complex nested structs --- .../core/src/decode/function_call_decoder.rs | 18 ++ crates/core/src/decode/mod.rs | 1 - crates/core/src/decode/return_decoder.rs | 10 +- crates/core/src/spec/decoder.rs | 31 +- crates/core/src/spec/mod.rs | 3 + crates/core/src/spec/tests/mod.rs | 1 + crates/core/src/spec/tests/wasm_tests.rs | 267 ++++++++++++++++++ 7 files changed, 297 insertions(+), 34 deletions(-) create mode 100644 crates/core/src/spec/tests/mod.rs create mode 100644 crates/core/src/spec/tests/wasm_tests.rs 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 dff2ab17..3f68f03b 100644 --- a/crates/core/src/spec/decoder.rs +++ b/crates/core/src/spec/decoder.rs @@ -242,33 +242,7 @@ pub fn decode_contract_spec(wasm_bytes: &[u8]) -> GratResult { 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 { @@ -308,7 +282,6 @@ pub fn decode_contract_spec(wasm_bytes: &[u8]) -> GratResult { doc, }); } - _ => {} } } @@ -406,6 +379,8 @@ mod tests { structs: 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 c271b767..ab0b521c 100644 --- a/crates/core/src/spec/mod.rs +++ b/crates/core/src/spec/mod.rs @@ -6,3 +6,6 @@ pub use decoder::{ ContractStructDef, ContractStructField, ContractUnionCase, ContractUnionDef, SpecParser, }; pub use resolver::{ContractId, ResolverStats, SCSpecResolver}; + +#[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"); +} From 3b9a583a44f827accd5c4a006450c6c8fbfad816 Mon Sep 17 00:00:00 2001 From: OluRemiFour Date: Wed, 29 Jul 2026 16:25:33 +0100 Subject: [PATCH 2/3] quick fix [ci skip] From 4660d675bbfcb9c3c0c947aed3713e6bdbd7f414 Mon Sep 17 00:00:00 2001 From: OluRemiFour Date: Wed, 29 Jul 2026 16:26:02 +0100 Subject: [PATCH 3/3] quick fix [ci skip]