use serde::{Deserialize, Serialize};
use std::io::Cursor;
use stellar_xdr::{
Limited, Limits, ReadXdr, ScMetaEntry, ScSpecEntry, ScSpecEntryKind, ScSpecTypeDef,
};
use wasmparser::Payload;
use crate::{ContractMetaEntry, WasmError};
pub const CONTRACT_SPEC_V0: &str = "contractspecv0";
pub const CONTRACT_ENV_META_V0: &str = "contractenvmetav0";
pub const CONTRACT_META_V0: &str = "contractmetav0";
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct ContractSpec {
pub env_meta: Option<EnvMetaSpec>,
pub functions: Vec<ContractFunction>,
pub custom_types: Vec<ContractType>,
pub events: Vec<ContractEvent>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct EnvMetaSpec {
pub interface_version: u64,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct ContractFunction {
pub name: String,
pub doc: String,
pub parameters: Vec<ContractParameter>,
pub outputs: Vec<ContractType>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct ContractParameter {
pub name: String,
pub doc: String,
pub type_: ContractType,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct ContractType {
pub name: String,
pub kind: String,
pub doc: String,
pub members: Vec<TypeMember>,
#[serde(default)]
pub type_args: Vec<ContractType>,
#[serde(default)]
pub bytes_n: Option<u32>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct TypeMember {
pub name: String,
pub doc: String,
pub types: Vec<ContractType>,
pub value: Option<u32>,
}
pub type ContractEventParameter = EventParam;
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct ContractEvent {
pub name: String,
pub doc: String,
pub prefix_topics: Vec<String>,
pub params: Vec<EventParam>,
pub data_format: String,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct EventParam {
pub name: String,
pub doc: String,
pub type_: ContractType,
pub location: String,
}
pub fn parse_contract_spec(raw_wasm: &[u8]) -> Result<ContractSpec, WasmError> {
if raw_wasm.is_empty() {
return Err(WasmError::Empty);
}
let parser = wasmparser::Parser::new(0);
let mut functions = Vec::new();
let mut custom_types = Vec::new();
let mut events = Vec::new();
let mut env_meta: Option<EnvMetaSpec> = None;
let mut saw_spec = false;
for payload in parser.parse_all(raw_wasm) {
if let Payload::CustomSection(reader) = payload? {
match reader.name() {
CONTRACT_SPEC_V0 => {
saw_spec = true;
decode_spec_section(
reader.data(),
&mut functions,
&mut custom_types,
&mut events,
)?;
}
CONTRACT_ENV_META_V0 => {
env_meta = Some(decode_env_meta_section(reader.data())?);
}
_ => {}
}
}
}
if !saw_spec {
return Err(WasmError::NoContractSpec);
}
Ok(ContractSpec {
env_meta,
functions,
custom_types,
events,
})
}
fn decode_spec_section(
data: &[u8],
functions: &mut Vec<ContractFunction>,
custom_types: &mut Vec<ContractType>,
events: &mut Vec<ContractEvent>,
) -> Result<(), WasmError> {
let mut items = data;
while !items.is_empty() {
let mut cursor = Cursor::new(items);
let mut limited = Limited::new(&mut cursor, Limits::none());
let entry = ScSpecEntry::read_xdr(&mut limited).map_err(WasmError::SpecXdr)?;
match entry {
ScSpecEntry::FunctionV0(f) => {
functions.push(ContractFunction {
name: f.name.to_utf8_string_lossy(),
doc: f.doc.to_utf8_string_lossy(),
parameters: f
.inputs
.iter()
.map(|i| ContractParameter {
name: i.name.to_utf8_string_lossy(),
doc: i.doc.to_utf8_string_lossy(),
type_: map_type_def(&i.type_),
})
.collect(),
outputs: f.outputs.iter().map(map_type_def).collect(),
});
}
ScSpecEntry::UdtStructV0(s) => custom_types.push(map_udt_struct(s)),
ScSpecEntry::UdtUnionV0(u) => custom_types.push(map_udt_union(u)),
ScSpecEntry::UdtEnumV0(e) => custom_types.push(map_udt_enum(e)),
ScSpecEntry::UdtErrorEnumV0(e) => custom_types.push(map_udt_error_enum(e)),
ScSpecEntry::EventV0(e) => events.push(map_event(e)),
}
let consumed = cursor.position() as usize;
items = &items[consumed..];
}
Ok(())
}
fn decode_env_meta_section(data: &[u8]) -> Result<EnvMetaSpec, WasmError> {
if data.len() < 8 {
return Err(WasmError::SpecXdr(stellar_xdr::Error::Invalid));
}
let raw = [
data[0], data[1], data[2], data[3], data[4], data[5], data[6], data[7],
];
let mut cursor = Cursor::new(&raw[..]);
let mut limited = Limited::new(&mut cursor, Limits::none());
let v = stellar_xdr::Uint64::read_xdr(&mut limited).map_err(WasmError::SpecXdr)?;
Ok(EnvMetaSpec {
interface_version: v,
})
}
pub fn decode_contract_meta(data: &[u8]) -> Result<Vec<ContractMetaEntry>, WasmError> {
let mut entries = Vec::new();
let mut items = data;
while !items.is_empty() {
let mut cursor = Cursor::new(items);
let mut limited = Limited::new(&mut cursor, Limits::none());
let entry = ScMetaEntry::read_xdr(&mut limited).map_err(WasmError::MetaXdr)?;
match entry {
ScMetaEntry::ScMetaV0(v0) => entries.push(ContractMetaEntry {
key: v0.key.to_utf8_string_lossy(),
value: v0.val.to_utf8_string_lossy(),
}),
}
let consumed = cursor.position() as usize;
items = &items[consumed..];
}
Ok(entries)
}
fn map_type_def(t: &ScSpecTypeDef) -> ContractType {
fn primitive(name: &str) -> ContractType {
ContractType {
name: name.to_string(),
kind: "primitive".to_string(),
doc: String::new(),
members: vec![],
type_args: vec![],
bytes_n: None,
}
}
fn compound(name: String, type_args: Vec<ContractType>, labels: &[&str]) -> ContractType {
let members = type_args
.iter()
.zip(labels)
.map(|(ty, label)| TypeMember {
name: (*label).to_string(),
doc: String::new(),
types: vec![ty.clone()],
value: None,
})
.collect();
ContractType {
name,
kind: "compound".to_string(),
doc: String::new(),
members,
type_args,
bytes_n: None,
}
}
match t {
ScSpecTypeDef::Val => primitive("val"),
ScSpecTypeDef::Bool => primitive("bool"),
ScSpecTypeDef::Void => primitive("void"),
ScSpecTypeDef::Error => primitive("error"),
ScSpecTypeDef::U32 => primitive("u32"),
ScSpecTypeDef::I32 => primitive("i32"),
ScSpecTypeDef::U64 => primitive("u64"),
ScSpecTypeDef::I64 => primitive("i64"),
ScSpecTypeDef::Timepoint => primitive("timepoint"),
ScSpecTypeDef::Duration => primitive("duration"),
ScSpecTypeDef::U128 => primitive("u128"),
ScSpecTypeDef::I128 => primitive("i128"),
ScSpecTypeDef::U256 => primitive("u256"),
ScSpecTypeDef::I256 => primitive("i256"),
ScSpecTypeDef::Bytes => primitive("bytes"),
ScSpecTypeDef::String => primitive("string"),
ScSpecTypeDef::Symbol => primitive("symbol"),
ScSpecTypeDef::Address => primitive("address"),
ScSpecTypeDef::MuxedAddress => primitive("muxed_address"),
ScSpecTypeDef::Option(inner) => {
let args = vec![map_type_def(&inner.value_type)];
compound(format!("option<{}>", args[0].name), args, &["value_type"])
}
ScSpecTypeDef::Result(inner) => {
let args = vec![
map_type_def(&inner.ok_type),
map_type_def(&inner.error_type),
];
let name = format!("result<{}, {}>", args[0].name, args[1].name);
compound(name, args, &["ok_type", "error_type"])
}
ScSpecTypeDef::Vec(inner) => {
let args = vec![map_type_def(&inner.element_type)];
compound(format!("vec<{}>", args[0].name), args, &["element_type"])
}
ScSpecTypeDef::Map(inner) => {
let args = vec![
map_type_def(&inner.key_type),
map_type_def(&inner.value_type),
];
let name = format!("map<{}, {}>", args[0].name, args[1].name);
compound(name, args, &["key_type", "value_type"])
}
ScSpecTypeDef::Tuple(inner) => {
let args: Vec<_> = inner.value_types.iter().map(map_type_def).collect();
let name = format!(
"tuple<{}>",
args.iter()
.map(|ty| ty.name.as_str())
.collect::<Vec<_>>()
.join(", ")
);
let labels: Vec<_> = (0..args.len()).map(|i| i.to_string()).collect();
let members = args
.iter()
.zip(labels.iter())
.map(|(ty, label)| TypeMember {
name: label.clone(),
doc: String::new(),
types: vec![ty.clone()],
value: None,
})
.collect();
ContractType {
name,
kind: "compound".to_string(),
doc: String::new(),
members,
type_args: args,
bytes_n: None,
}
}
ScSpecTypeDef::BytesN(inner) => ContractType {
name: format!("bytesn<{}>", inner.n),
kind: "compound".to_string(),
doc: String::new(),
members: vec![],
type_args: vec![],
bytes_n: Some(inner.n),
},
ScSpecTypeDef::Udt(user_type) => ContractType {
name: user_type.name.to_utf8_string_lossy(),
kind: "udt".to_string(),
doc: String::new(),
members: vec![],
type_args: vec![],
bytes_n: None,
},
}
}
fn map_udt_struct(s: stellar_xdr::ScSpecUdtStructV0) -> ContractType {
ContractType {
name: s.name.to_utf8_string_lossy(),
kind: "struct".to_string(),
doc: s.doc.to_utf8_string_lossy(),
members: s
.fields
.iter()
.map(|f| TypeMember {
name: f.name.to_utf8_string_lossy(),
doc: f.doc.to_utf8_string_lossy(),
types: vec![map_type_def(&f.type_)],
value: None,
})
.collect(),
type_args: vec![],
bytes_n: None,
}
}
fn map_udt_union(u: stellar_xdr::ScSpecUdtUnionV0) -> ContractType {
ContractType {
name: u.name.to_utf8_string_lossy(),
kind: "union".to_string(),
doc: u.doc.to_utf8_string_lossy(),
members: u
.cases
.iter()
.map(|c| match c {
stellar_xdr::ScSpecUdtUnionCaseV0::VoidV0(v) => TypeMember {
name: v.name.to_utf8_string_lossy(),
doc: v.doc.to_utf8_string_lossy(),
types: vec![],
value: None,
},
stellar_xdr::ScSpecUdtUnionCaseV0::TupleV0(t) => TypeMember {
name: t.name.to_utf8_string_lossy(),
doc: t.doc.to_utf8_string_lossy(),
types: t.type_.iter().map(map_type_def).collect(),
value: None,
},
})
.collect(),
type_args: vec![],
bytes_n: None,
}
}
fn map_udt_enum(e: stellar_xdr::ScSpecUdtEnumV0) -> ContractType {
ContractType {
name: e.name.to_utf8_string_lossy(),
kind: "enum".to_string(),
doc: e.doc.to_utf8_string_lossy(),
members: e
.cases
.iter()
.map(|c| TypeMember {
name: c.name.to_utf8_string_lossy(),
doc: c.doc.to_utf8_string_lossy(),
types: vec![],
value: Some(c.value),
})
.collect(),
type_args: vec![],
bytes_n: None,
}
}
fn map_udt_error_enum(e: stellar_xdr::ScSpecUdtErrorEnumV0) -> ContractType {
ContractType {
name: e.name.to_utf8_string_lossy(),
kind: "error_enum".to_string(),
doc: e.doc.to_utf8_string_lossy(),
members: e
.cases
.iter()
.map(|c| TypeMember {
name: c.name.to_utf8_string_lossy(),
doc: c.doc.to_utf8_string_lossy(),
types: vec![],
value: Some(c.value),
})
.collect(),
type_args: vec![],
bytes_n: None,
}
}
fn map_event(e: stellar_xdr::ScSpecEventV0) -> ContractEvent {
ContractEvent {
name: e.name.to_utf8_string_lossy(),
doc: e.doc.to_utf8_string_lossy(),
prefix_topics: e
.prefix_topics
.iter()
.map(|t| t.to_utf8_string_lossy())
.collect(),
params: e
.params
.iter()
.map(|p| EventParam {
name: p.name.to_utf8_string_lossy(),
doc: p.doc.to_utf8_string_lossy(),
type_: map_type_def(&p.type_),
location: match p.location {
stellar_xdr::ScSpecEventParamLocationV0::Data => "data".to_string(),
stellar_xdr::ScSpecEventParamLocationV0::TopicList => "topic_list".to_string(),
},
})
.collect(),
data_format: match e.data_format {
stellar_xdr::ScSpecEventDataFormat::SingleValue => "single_value".to_string(),
stellar_xdr::ScSpecEventDataFormat::Vec => "vec".to_string(),
stellar_xdr::ScSpecEventDataFormat::Map => "map".to_string(),
},
}
}
#[allow(dead_code)]
fn _discriminant_name(kind: &ScSpecEntryKind) -> &'static str {
match kind {
ScSpecEntryKind::FunctionV0 => "function_v0",
ScSpecEntryKind::UdtStructV0 => "udt_struct_v0",
ScSpecEntryKind::UdtUnionV0 => "udt_union_v0",
ScSpecEntryKind::UdtEnumV0 => "udt_enum_v0",
ScSpecEntryKind::UdtErrorEnumV0 => "udt_error_enum_v0",
ScSpecEntryKind::EventV0 => "event_v0",
}
}
#[cfg(test)]
pub(crate) mod tests {
use super::*;
use stellar_xdr::WriteXdr;
const VALID_WASM: &[u8] = &[0x00, 0x61, 0x73, 0x6d, 0x01, 0x00, 0x00, 0x00];
pub(crate) fn spec_section(entries: &[ScSpecEntry]) -> Vec<u8> {
let mut section = Vec::new();
section.push(CONTRACT_SPEC_V0.len() as u8);
section.extend_from_slice(CONTRACT_SPEC_V0.as_bytes());
for e in entries {
let mut buf = Vec::new();
let mut cursor = Cursor::new(&mut buf);
let mut l = Limited::new(&mut cursor, Limits::none());
e.write_xdr(&mut l).unwrap();
section.extend_from_slice(&buf);
}
let mut result = vec![0x00, 0x61, 0x73, 0x6d, 0x01, 0x00, 0x00, 0x00];
result.push(0); let mut sz = section.len() as u32;
let mut size_bytes = Vec::new();
while sz >= 0x80 {
size_bytes.push((sz as u8 & 0x7f) | 0x80);
sz >>= 7;
}
size_bytes.push(sz as u8);
result.extend_from_slice(&size_bytes);
result.extend_from_slice(§ion);
result
}
pub(crate) fn symbol_e(s: &str) -> stellar_xdr::ScSymbol {
stellar_xdr::ScSymbol(s.to_string().try_into().unwrap())
}
pub(crate) fn func_entry(name: &str, inputs: Vec<(String, ScSpecTypeDef)>) -> ScSpecEntry {
use stellar_xdr::{ScSpecFunctionInputV0, ScSpecFunctionV0};
ScSpecEntry::FunctionV0(ScSpecFunctionV0 {
doc: "".try_into().unwrap(),
name: symbol_e(name),
inputs: inputs
.into_iter()
.map(|(n, t)| ScSpecFunctionInputV0 {
doc: "".try_into().unwrap(),
name: n.try_into().unwrap(),
type_: t,
})
.collect::<Vec<_>>()
.try_into()
.unwrap(),
outputs: vec![].try_into().unwrap(),
})
}
pub(crate) fn event_entry(name: &str) -> ScSpecEntry {
event_entry_with_params(name, vec![])
}
pub(crate) fn event_entry_with_params(
name: &str,
params: Vec<(String, ScSpecTypeDef)>,
) -> ScSpecEntry {
use stellar_xdr::{ScSpecEventParamLocationV0, ScSpecEventParamV0, ScSpecEventV0};
ScSpecEntry::EventV0(ScSpecEventV0 {
doc: "".try_into().unwrap(),
lib: "soroban_sdk".try_into().unwrap(),
name: symbol_e(name),
prefix_topics: vec![].try_into().unwrap(),
params: params
.into_iter()
.map(|(n, t)| ScSpecEventParamV0 {
doc: "".try_into().unwrap(),
name: n.try_into().unwrap(),
type_: t,
location: ScSpecEventParamLocationV0::Data,
})
.collect::<Vec<_>>()
.try_into()
.unwrap(),
data_format: stellar_xdr::ScSpecEventDataFormat::SingleValue,
})
}
pub(crate) fn udt_struct_entry(name: &str) -> ScSpecEntry {
udt_struct_entry_with_fields(name, vec![])
}
pub(crate) fn udt_struct_entry_with_fields(
name: &str,
fields: Vec<(String, ScSpecTypeDef)>,
) -> ScSpecEntry {
use stellar_xdr::{ScSpecUdtStructFieldV0, ScSpecUdtStructV0};
ScSpecEntry::UdtStructV0(ScSpecUdtStructV0 {
doc: "".try_into().unwrap(),
lib: "soroban_sdk".try_into().unwrap(),
name: name.try_into().unwrap(),
fields: fields
.into_iter()
.map(|(n, t)| ScSpecUdtStructFieldV0 {
doc: "".try_into().unwrap(),
name: n.try_into().unwrap(),
type_: t,
})
.collect::<Vec<_>>()
.try_into()
.unwrap(),
})
}
#[test]
fn empty_wasm() {
assert!(matches!(parse_contract_spec(&[]), Err(WasmError::Empty)));
}
#[test]
fn invalid_wasm() {
let res = parse_contract_spec(b"not wasm at all");
assert!(matches!(res, Err(WasmError::Parse(_))));
}
#[test]
fn no_contract_spec() {
let res = parse_contract_spec(VALID_WASM);
assert!(matches!(res, Err(WasmError::NoContractSpec)));
}
#[test]
fn valid_single_function() {
let wasm = spec_section(&[func_entry(
"greet",
vec![("name".to_string(), ScSpecTypeDef::String)],
)]);
let spec = parse_contract_spec(&wasm).unwrap();
assert_eq!(spec.functions.len(), 1);
assert_eq!(spec.functions[0].name, "greet");
assert_eq!(spec.functions[0].parameters.len(), 1);
assert_eq!(spec.functions[0].parameters[0].name, "name");
assert_eq!(spec.functions[0].parameters[0].type_.name, "string");
}
#[test]
fn multiple_functions() {
let wasm = spec_section(&[
func_entry("a", vec![]),
func_entry("b", vec![("x".to_string(), ScSpecTypeDef::U32)]),
]);
let spec = parse_contract_spec(&wasm).unwrap();
assert_eq!(spec.functions.len(), 2);
assert_eq!(spec.functions[0].name, "a");
assert_eq!(spec.functions[1].name, "b");
assert_eq!(spec.functions[1].parameters[0].type_.name, "u32");
}
#[test]
fn custom_type() {
use stellar_xdr::ScSpecUdtStructV0;
let udt = ScSpecEntry::UdtStructV0(ScSpecUdtStructV0 {
doc: "a point".try_into().unwrap(),
lib: "soroban_sdk".try_into().unwrap(),
name: "Point".try_into().unwrap(),
fields: vec![].try_into().unwrap(),
});
let wasm = spec_section(&[udt]);
let spec = parse_contract_spec(&wasm).unwrap();
assert_eq!(spec.custom_types.len(), 1);
assert_eq!(spec.custom_types[0].name, "Point");
assert_eq!(spec.custom_types[0].kind, "struct");
assert_eq!(spec.custom_types[0].doc, "a point");
}
#[test]
fn preserves_struct_union_and_event_metadata() {
use stellar_xdr::{
ScSpecEventDataFormat, ScSpecEventParamLocationV0, ScSpecEventParamV0, ScSpecEventV0,
ScSpecUdtStructFieldV0, ScSpecUdtStructV0, ScSpecUdtUnionCaseTupleV0,
ScSpecUdtUnionCaseV0, ScSpecUdtUnionV0,
};
let point = ScSpecEntry::UdtStructV0(ScSpecUdtStructV0 {
doc: "point".try_into().unwrap(),
lib: "test".try_into().unwrap(),
name: "Point".try_into().unwrap(),
fields: vec![ScSpecUdtStructFieldV0 {
doc: "x coordinate".try_into().unwrap(),
name: "x".try_into().unwrap(),
type_: ScSpecTypeDef::U64,
}]
.try_into()
.unwrap(),
});
let choice = ScSpecEntry::UdtUnionV0(ScSpecUdtUnionV0 {
doc: "choice".try_into().unwrap(),
lib: "test".try_into().unwrap(),
name: "Choice".try_into().unwrap(),
cases: vec![ScSpecUdtUnionCaseV0::TupleV0(ScSpecUdtUnionCaseTupleV0 {
doc: "some value".try_into().unwrap(),
name: "Some".try_into().unwrap(),
type_: vec![ScSpecTypeDef::String].try_into().unwrap(),
})]
.try_into()
.unwrap(),
});
let event = ScSpecEntry::EventV0(ScSpecEventV0 {
doc: "transfer event".try_into().unwrap(),
lib: "test".try_into().unwrap(),
name: symbol_e("Transfer"),
prefix_topics: vec![symbol_e("TOKEN")].try_into().unwrap(),
params: vec![ScSpecEventParamV0 {
doc: "amount".try_into().unwrap(),
name: "amount".try_into().unwrap(),
type_: ScSpecTypeDef::U128,
location: ScSpecEventParamLocationV0::Data,
}]
.try_into()
.unwrap(),
data_format: ScSpecEventDataFormat::SingleValue,
});
let spec = parse_contract_spec(&spec_section(&[point, choice, event])).unwrap();
assert_eq!(spec.custom_types[0].members[0].types[0].name, "u64");
assert_eq!(spec.custom_types[1].members[0].types[0].name, "string");
assert_eq!(spec.events[0].prefix_topics, vec!["TOKEN"]);
assert_eq!(spec.events[0].params[0].type_.name, "u128");
assert_eq!(spec.events[0].params[0].location, "data");
assert_eq!(spec.events[0].data_format, "single_value");
}
#[test]
fn preserves_compound_type_arguments_and_bytesn_length() {
use stellar_xdr::{ScSpecTypeBytesN, ScSpecTypeDef, ScSpecTypeMap, ScSpecTypeVec};
let vector = map_type_def(&ScSpecTypeDef::Vec(Box::new(ScSpecTypeVec {
element_type: Box::new(ScSpecTypeDef::U32),
})));
assert_eq!(vector.name, "vec<u32>");
assert_eq!(vector.type_args.len(), 1);
assert_eq!(vector.type_args[0].name, "u32");
let map = map_type_def(&ScSpecTypeDef::Map(Box::new(ScSpecTypeMap {
key_type: Box::new(ScSpecTypeDef::String),
value_type: Box::new(ScSpecTypeDef::Address),
})));
assert_eq!(
map.type_args
.iter()
.map(|t| t.name.as_str())
.collect::<Vec<_>>(),
["string", "address"]
);
let bytes = map_type_def(&ScSpecTypeDef::BytesN(ScSpecTypeBytesN { n: 32 }));
assert_eq!(bytes.name, "bytesn<32>");
assert_eq!(bytes.bytes_n, Some(32));
}
#[test]
fn event_retains_params() {
let wasm = spec_section(&[event_entry_with_params(
"Transfer",
vec![
("from".to_string(), ScSpecTypeDef::Address),
("amount".to_string(), ScSpecTypeDef::I128),
],
)]);
let spec = parse_contract_spec(&wasm).unwrap();
assert_eq!(spec.events.len(), 1);
let ev = &spec.events[0];
assert_eq!(ev.name, "Transfer");
assert_eq!(ev.data_format, "single_value");
assert!(ev.prefix_topics.is_empty());
assert_eq!(ev.params.len(), 2);
assert_eq!(ev.params[0].name, "from");
assert_eq!(ev.params[0].type_.name, "address");
assert_eq!(ev.params[0].location, "data");
assert_eq!(ev.params[1].name, "amount");
assert_eq!(ev.params[1].type_.name, "i128");
}
#[test]
fn udt_struct_retains_field_types() {
let wasm = spec_section(&[udt_struct_entry_with_fields(
"Point",
vec![("x".to_string(), ScSpecTypeDef::I32)],
)]);
let spec = parse_contract_spec(&wasm).unwrap();
assert_eq!(spec.custom_types.len(), 1);
assert_eq!(spec.custom_types[0].members.len(), 1);
assert_eq!(spec.custom_types[0].members[0].name, "x");
assert_eq!(spec.custom_types[0].members[0].types[0].name, "i32");
}
fn meta_entry(key: &str, val: &str) -> ScMetaEntry {
ScMetaEntry::ScMetaV0(stellar_xdr::ScMetaV0 {
key: key.to_string().try_into().unwrap(),
val: val.to_string().try_into().unwrap(),
})
}
fn encode_entries(entries: &[ScMetaEntry]) -> Vec<u8> {
let mut payload = Vec::new();
for e in entries {
let mut buf = Vec::new();
let mut cursor = Cursor::new(&mut buf);
let mut l = Limited::new(&mut cursor, Limits::none());
e.write_xdr(&mut l).unwrap();
payload.extend_from_slice(&buf);
}
payload
}
fn encode_meta_section(entries: &[ScMetaEntry]) -> Vec<u8> {
let mut section = Vec::new();
section.push(CONTRACT_META_V0.len() as u8);
section.extend_from_slice(CONTRACT_META_V0.as_bytes());
section.extend_from_slice(&encode_entries(entries));
let mut result = vec![0x00, 0x61, 0x73, 0x6d, 0x01, 0x00, 0x00, 0x00];
result.push(0); let mut sz = section.len() as u32;
let mut size_bytes = Vec::new();
while sz >= 0x80 {
size_bytes.push((sz as u8 & 0x7f) | 0x80);
sz >>= 7;
}
size_bytes.push(sz as u8);
result.extend_from_slice(&size_bytes);
result.extend_from_slice(§ion);
result
}
#[test]
fn decode_contract_meta_reads_generic_key_values_in_order() {
let payload = vec![
meta_entry("rsver", "1.97.1"),
meta_entry("totally_unknown_key", "arbitrary value"),
meta_entry("cliver", "27.1.0#deadbeef"),
];
let decoded = decode_contract_meta(&encode_entries(&payload)).unwrap();
assert_eq!(decoded.len(), 3);
assert_eq!(decoded[0].key, "rsver");
assert_eq!(decoded[0].value, "1.97.1");
assert_eq!(decoded[1].key, "totally_unknown_key");
assert_eq!(decoded[1].value, "arbitrary value");
assert_eq!(decoded[2].value, "27.1.0#deadbeef");
}
#[test]
fn decode_contract_meta_preserves_duplicate_keys() {
let payload = vec![
meta_entry("k", "first"),
meta_entry("k", "second"),
meta_entry("k", "third"),
];
let decoded = decode_contract_meta(&encode_entries(&payload)).unwrap();
let values: Vec<&str> = decoded.iter().map(|e| e.value.as_str()).collect();
assert_eq!(values, vec!["first", "second", "third"]);
}
#[test]
fn decode_contract_meta_empty_payload_is_empty_list() {
assert!(decode_contract_meta(&[]).unwrap().is_empty());
}
#[test]
fn decode_contract_meta_rejects_malformed_payload() {
assert!(matches!(
decode_contract_meta(&[0xff, 0x00, 0x00]).unwrap_err(),
WasmError::MetaXdr(_)
));
assert!(matches!(
decode_contract_meta(&[0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x40]).unwrap_err(),
WasmError::MetaXdr(_)
));
}
#[test]
fn parse_metadata_surfaces_meta_and_keeps_custom_sections_unchanged() {
let wasm = encode_meta_section(&[
meta_entry("rsver", "1.97.1"),
meta_entry("cliver", "27.1.0#8e402ea282029"),
]);
let meta = crate::parse_metadata(&wasm).unwrap();
assert_eq!(meta.custom_sections, vec![CONTRACT_META_V0.to_string()]);
assert_eq!(meta.contract_meta.len(), 2);
assert_eq!(meta.contract_meta[0].key, "rsver");
assert_eq!(meta.contract_meta[1].value, "27.1.0#8e402ea282029");
}
#[test]
fn parse_metadata_without_meta_section_is_empty() {
let meta = crate::parse_metadata(VALID_WASM).unwrap();
assert!(meta.contract_meta.is_empty());
assert!(meta.custom_sections.is_empty());
}
#[test]
fn parse_metadata_decodes_real_committed_contract_metadata() {
let bytes = std::fs::read(concat!(
env!("CARGO_MANIFEST_DIR"),
"/../sdkt-cli/tests/fixtures/us_new.wasm"
))
.expect("fixture present");
let meta = crate::parse_metadata(&bytes).unwrap();
assert!(meta.custom_sections.contains(&CONTRACT_META_V0.to_string()));
assert!(
!meta.contract_meta.is_empty(),
"real contract metadata must decode"
);
let get = |k: &str| {
meta.contract_meta
.iter()
.find(|e| e.key == k)
.map(|e| e.value.clone())
};
assert_eq!(get("rsver").as_deref(), Some("1.97.1"));
assert!(get("cliver").unwrap().starts_with("27.1.0#"));
}
}