1use serde::{Deserialize, Serialize};
11use std::io::Cursor;
12use stellar_xdr::{
13 Limited, Limits, ReadXdr, ScMetaEntry, ScSpecEntry, ScSpecEntryKind, ScSpecTypeDef,
14};
15use wasmparser::Payload;
16
17use crate::{ContractMetaEntry, WasmError};
18
19pub const CONTRACT_SPEC_V0: &str = "contractspecv0";
21pub const CONTRACT_ENV_META_V0: &str = "contractenvmetav0";
23pub const CONTRACT_META_V0: &str = "contractmetav0";
25
26#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
28pub struct ContractSpec {
29 pub env_meta: Option<EnvMetaSpec>,
32 pub functions: Vec<ContractFunction>,
34 pub custom_types: Vec<ContractType>,
36 pub events: Vec<ContractEvent>,
38}
39
40#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
42pub struct EnvMetaSpec {
43 pub interface_version: u64,
45}
46
47#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
49pub struct ContractFunction {
50 pub name: String,
52 pub doc: String,
54 pub parameters: Vec<ContractParameter>,
56 pub outputs: Vec<ContractType>,
58}
59
60#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
62pub struct ContractParameter {
63 pub name: String,
65 pub doc: String,
67 pub type_: ContractType,
69}
70
71#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
73pub struct ContractType {
74 pub name: String,
76 pub kind: String,
78 pub doc: String,
80 pub members: Vec<TypeMember>,
82 #[serde(default)]
85 pub type_args: Vec<ContractType>,
86 #[serde(default)]
88 pub bytes_n: Option<u32>,
89}
90
91#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
93pub struct TypeMember {
94 pub name: String,
96 pub doc: String,
98 pub types: Vec<ContractType>,
103 pub value: Option<u32>,
107}
108
109pub type ContractEventParameter = EventParam;
111
112#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
118pub struct ContractEvent {
119 pub name: String,
121 pub doc: String,
123 pub prefix_topics: Vec<String>,
125 pub params: Vec<EventParam>,
127 pub data_format: String,
129}
130
131#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
133pub struct EventParam {
134 pub name: String,
136 pub doc: String,
138 pub type_: ContractType,
140 pub location: String,
142}
143
144pub fn parse_contract_spec(raw_wasm: &[u8]) -> Result<ContractSpec, WasmError> {
157 if raw_wasm.is_empty() {
158 return Err(WasmError::Empty);
159 }
160
161 let parser = wasmparser::Parser::new(0);
162 let mut functions = Vec::new();
163 let mut custom_types = Vec::new();
164 let mut events = Vec::new();
165 let mut env_meta: Option<EnvMetaSpec> = None;
166 let mut saw_spec = false;
167
168 for payload in parser.parse_all(raw_wasm) {
169 if let Payload::CustomSection(reader) = payload? {
170 match reader.name() {
171 CONTRACT_SPEC_V0 => {
172 saw_spec = true;
173 decode_spec_section(
174 reader.data(),
175 &mut functions,
176 &mut custom_types,
177 &mut events,
178 )?;
179 }
180 CONTRACT_ENV_META_V0 => {
181 env_meta = Some(decode_env_meta_section(reader.data())?);
182 }
183 _ => {}
184 }
185 }
186 }
187
188 if !saw_spec {
189 return Err(WasmError::NoContractSpec);
190 }
191
192 Ok(ContractSpec {
193 env_meta,
194 functions,
195 custom_types,
196 events,
197 })
198}
199
200fn decode_spec_section(
202 data: &[u8],
203 functions: &mut Vec<ContractFunction>,
204 custom_types: &mut Vec<ContractType>,
205 events: &mut Vec<ContractEvent>,
206) -> Result<(), WasmError> {
207 let mut items = data;
208 while !items.is_empty() {
212 let mut cursor = Cursor::new(items);
213 let mut limited = Limited::new(&mut cursor, Limits::none());
214 let entry = ScSpecEntry::read_xdr(&mut limited).map_err(WasmError::SpecXdr)?;
215
216 match entry {
217 ScSpecEntry::FunctionV0(f) => {
218 functions.push(ContractFunction {
219 name: f.name.to_utf8_string_lossy(),
220 doc: f.doc.to_utf8_string_lossy(),
221 parameters: f
222 .inputs
223 .iter()
224 .map(|i| ContractParameter {
225 name: i.name.to_utf8_string_lossy(),
226 doc: i.doc.to_utf8_string_lossy(),
227 type_: map_type_def(&i.type_),
228 })
229 .collect(),
230 outputs: f.outputs.iter().map(map_type_def).collect(),
231 });
232 }
233 ScSpecEntry::UdtStructV0(s) => custom_types.push(map_udt_struct(s)),
234 ScSpecEntry::UdtUnionV0(u) => custom_types.push(map_udt_union(u)),
235 ScSpecEntry::UdtEnumV0(e) => custom_types.push(map_udt_enum(e)),
236 ScSpecEntry::UdtErrorEnumV0(e) => custom_types.push(map_udt_error_enum(e)),
237 ScSpecEntry::EventV0(e) => events.push(map_event(e)),
238 }
239
240 let consumed = cursor.position() as usize;
241 items = &items[consumed..];
242 }
243 Ok(())
244}
245
246fn decode_env_meta_section(data: &[u8]) -> Result<EnvMetaSpec, WasmError> {
248 if data.len() < 8 {
249 return Err(WasmError::SpecXdr(stellar_xdr::Error::Invalid));
250 }
251 let raw = [
252 data[0], data[1], data[2], data[3], data[4], data[5], data[6], data[7],
253 ];
254 let mut cursor = Cursor::new(&raw[..]);
256 let mut limited = Limited::new(&mut cursor, Limits::none());
257 let v = stellar_xdr::Uint64::read_xdr(&mut limited).map_err(WasmError::SpecXdr)?;
258 Ok(EnvMetaSpec {
259 interface_version: v,
260 })
261}
262
263pub fn decode_contract_meta(data: &[u8]) -> Result<Vec<ContractMetaEntry>, WasmError> {
275 let mut entries = Vec::new();
276 let mut items = data;
277 while !items.is_empty() {
278 let mut cursor = Cursor::new(items);
279 let mut limited = Limited::new(&mut cursor, Limits::none());
280 let entry = ScMetaEntry::read_xdr(&mut limited).map_err(WasmError::MetaXdr)?;
281
282 match entry {
283 ScMetaEntry::ScMetaV0(v0) => entries.push(ContractMetaEntry {
284 key: v0.key.to_utf8_string_lossy(),
285 value: v0.val.to_utf8_string_lossy(),
286 }),
287 }
288
289 let consumed = cursor.position() as usize;
290 items = &items[consumed..];
291 }
292 Ok(entries)
293}
294
295fn map_type_def(t: &ScSpecTypeDef) -> ContractType {
296 fn primitive(name: &str) -> ContractType {
297 ContractType {
298 name: name.to_string(),
299 kind: "primitive".to_string(),
300 doc: String::new(),
301 members: vec![],
302 type_args: vec![],
303 bytes_n: None,
304 }
305 }
306
307 fn compound(name: String, type_args: Vec<ContractType>, labels: &[&str]) -> ContractType {
308 let members = type_args
309 .iter()
310 .zip(labels)
311 .map(|(ty, label)| TypeMember {
312 name: (*label).to_string(),
313 doc: String::new(),
314 types: vec![ty.clone()],
315 value: None,
316 })
317 .collect();
318 ContractType {
319 name,
320 kind: "compound".to_string(),
321 doc: String::new(),
322 members,
323 type_args,
324 bytes_n: None,
325 }
326 }
327
328 match t {
329 ScSpecTypeDef::Val => primitive("val"),
330 ScSpecTypeDef::Bool => primitive("bool"),
331 ScSpecTypeDef::Void => primitive("void"),
332 ScSpecTypeDef::Error => primitive("error"),
333 ScSpecTypeDef::U32 => primitive("u32"),
334 ScSpecTypeDef::I32 => primitive("i32"),
335 ScSpecTypeDef::U64 => primitive("u64"),
336 ScSpecTypeDef::I64 => primitive("i64"),
337 ScSpecTypeDef::Timepoint => primitive("timepoint"),
338 ScSpecTypeDef::Duration => primitive("duration"),
339 ScSpecTypeDef::U128 => primitive("u128"),
340 ScSpecTypeDef::I128 => primitive("i128"),
341 ScSpecTypeDef::U256 => primitive("u256"),
342 ScSpecTypeDef::I256 => primitive("i256"),
343 ScSpecTypeDef::Bytes => primitive("bytes"),
344 ScSpecTypeDef::String => primitive("string"),
345 ScSpecTypeDef::Symbol => primitive("symbol"),
346 ScSpecTypeDef::Address => primitive("address"),
347 ScSpecTypeDef::MuxedAddress => primitive("muxed_address"),
348 ScSpecTypeDef::Option(inner) => {
349 let args = vec![map_type_def(&inner.value_type)];
350 compound(format!("option<{}>", args[0].name), args, &["value_type"])
351 }
352 ScSpecTypeDef::Result(inner) => {
353 let args = vec![
354 map_type_def(&inner.ok_type),
355 map_type_def(&inner.error_type),
356 ];
357 let name = format!("result<{}, {}>", args[0].name, args[1].name);
358 compound(name, args, &["ok_type", "error_type"])
359 }
360 ScSpecTypeDef::Vec(inner) => {
361 let args = vec![map_type_def(&inner.element_type)];
362 compound(format!("vec<{}>", args[0].name), args, &["element_type"])
363 }
364 ScSpecTypeDef::Map(inner) => {
365 let args = vec![
366 map_type_def(&inner.key_type),
367 map_type_def(&inner.value_type),
368 ];
369 let name = format!("map<{}, {}>", args[0].name, args[1].name);
370 compound(name, args, &["key_type", "value_type"])
371 }
372 ScSpecTypeDef::Tuple(inner) => {
373 let args: Vec<_> = inner.value_types.iter().map(map_type_def).collect();
374 let name = format!(
375 "tuple<{}>",
376 args.iter()
377 .map(|ty| ty.name.as_str())
378 .collect::<Vec<_>>()
379 .join(", ")
380 );
381 let labels: Vec<_> = (0..args.len()).map(|i| i.to_string()).collect();
382 let members = args
383 .iter()
384 .zip(labels.iter())
385 .map(|(ty, label)| TypeMember {
386 name: label.clone(),
387 doc: String::new(),
388 types: vec![ty.clone()],
389 value: None,
390 })
391 .collect();
392 ContractType {
393 name,
394 kind: "compound".to_string(),
395 doc: String::new(),
396 members,
397 type_args: args,
398 bytes_n: None,
399 }
400 }
401 ScSpecTypeDef::BytesN(inner) => ContractType {
402 name: format!("bytesn<{}>", inner.n),
403 kind: "compound".to_string(),
404 doc: String::new(),
405 members: vec![],
406 type_args: vec![],
407 bytes_n: Some(inner.n),
408 },
409 ScSpecTypeDef::Udt(user_type) => ContractType {
410 name: user_type.name.to_utf8_string_lossy(),
411 kind: "udt".to_string(),
412 doc: String::new(),
413 members: vec![],
414 type_args: vec![],
415 bytes_n: None,
416 },
417 }
418}
419
420fn map_udt_struct(s: stellar_xdr::ScSpecUdtStructV0) -> ContractType {
421 ContractType {
422 name: s.name.to_utf8_string_lossy(),
423 kind: "struct".to_string(),
424 doc: s.doc.to_utf8_string_lossy(),
425 members: s
426 .fields
427 .iter()
428 .map(|f| TypeMember {
429 name: f.name.to_utf8_string_lossy(),
430 doc: f.doc.to_utf8_string_lossy(),
431 types: vec![map_type_def(&f.type_)],
432 value: None,
433 })
434 .collect(),
435 type_args: vec![],
436 bytes_n: None,
437 }
438}
439
440fn map_udt_union(u: stellar_xdr::ScSpecUdtUnionV0) -> ContractType {
441 ContractType {
442 name: u.name.to_utf8_string_lossy(),
443 kind: "union".to_string(),
444 doc: u.doc.to_utf8_string_lossy(),
445 members: u
446 .cases
447 .iter()
448 .map(|c| match c {
449 stellar_xdr::ScSpecUdtUnionCaseV0::VoidV0(v) => TypeMember {
450 name: v.name.to_utf8_string_lossy(),
451 doc: v.doc.to_utf8_string_lossy(),
452 types: vec![],
453 value: None,
454 },
455 stellar_xdr::ScSpecUdtUnionCaseV0::TupleV0(t) => TypeMember {
456 name: t.name.to_utf8_string_lossy(),
457 doc: t.doc.to_utf8_string_lossy(),
458 types: t.type_.iter().map(map_type_def).collect(),
459 value: None,
460 },
461 })
462 .collect(),
463 type_args: vec![],
464 bytes_n: None,
465 }
466}
467
468fn map_udt_enum(e: stellar_xdr::ScSpecUdtEnumV0) -> ContractType {
469 ContractType {
470 name: e.name.to_utf8_string_lossy(),
471 kind: "enum".to_string(),
472 doc: e.doc.to_utf8_string_lossy(),
473 members: e
474 .cases
475 .iter()
476 .map(|c| TypeMember {
477 name: c.name.to_utf8_string_lossy(),
478 doc: c.doc.to_utf8_string_lossy(),
479 types: vec![],
480 value: Some(c.value),
481 })
482 .collect(),
483 type_args: vec![],
484 bytes_n: None,
485 }
486}
487
488fn map_udt_error_enum(e: stellar_xdr::ScSpecUdtErrorEnumV0) -> ContractType {
489 ContractType {
490 name: e.name.to_utf8_string_lossy(),
491 kind: "error_enum".to_string(),
492 doc: e.doc.to_utf8_string_lossy(),
493 members: e
494 .cases
495 .iter()
496 .map(|c| TypeMember {
497 name: c.name.to_utf8_string_lossy(),
498 doc: c.doc.to_utf8_string_lossy(),
499 types: vec![],
500 value: Some(c.value),
501 })
502 .collect(),
503 type_args: vec![],
504 bytes_n: None,
505 }
506}
507
508fn map_event(e: stellar_xdr::ScSpecEventV0) -> ContractEvent {
511 ContractEvent {
512 name: e.name.to_utf8_string_lossy(),
513 doc: e.doc.to_utf8_string_lossy(),
514 prefix_topics: e
515 .prefix_topics
516 .iter()
517 .map(|t| t.to_utf8_string_lossy())
518 .collect(),
519 params: e
520 .params
521 .iter()
522 .map(|p| EventParam {
523 name: p.name.to_utf8_string_lossy(),
524 doc: p.doc.to_utf8_string_lossy(),
525 type_: map_type_def(&p.type_),
526 location: match p.location {
527 stellar_xdr::ScSpecEventParamLocationV0::Data => "data".to_string(),
528 stellar_xdr::ScSpecEventParamLocationV0::TopicList => "topic_list".to_string(),
529 },
530 })
531 .collect(),
532 data_format: match e.data_format {
533 stellar_xdr::ScSpecEventDataFormat::SingleValue => "single_value".to_string(),
534 stellar_xdr::ScSpecEventDataFormat::Vec => "vec".to_string(),
535 stellar_xdr::ScSpecEventDataFormat::Map => "map".to_string(),
536 },
537 }
538}
539
540#[allow(dead_code)]
543fn _discriminant_name(kind: &ScSpecEntryKind) -> &'static str {
544 match kind {
545 ScSpecEntryKind::FunctionV0 => "function_v0",
546 ScSpecEntryKind::UdtStructV0 => "udt_struct_v0",
547 ScSpecEntryKind::UdtUnionV0 => "udt_union_v0",
548 ScSpecEntryKind::UdtEnumV0 => "udt_enum_v0",
549 ScSpecEntryKind::UdtErrorEnumV0 => "udt_error_enum_v0",
550 ScSpecEntryKind::EventV0 => "event_v0",
551 }
552}
553
554#[cfg(test)]
555pub(crate) mod tests {
556 use super::*;
557 use stellar_xdr::WriteXdr;
558
559 const VALID_WASM: &[u8] = &[0x00, 0x61, 0x73, 0x6d, 0x01, 0x00, 0x00, 0x00];
561
562 pub(crate) fn spec_section(entries: &[ScSpecEntry]) -> Vec<u8> {
564 let mut section = Vec::new();
566 section.push(CONTRACT_SPEC_V0.len() as u8);
567 section.extend_from_slice(CONTRACT_SPEC_V0.as_bytes());
568 for e in entries {
569 let mut buf = Vec::new();
570 let mut cursor = Cursor::new(&mut buf);
571 let mut l = Limited::new(&mut cursor, Limits::none());
572 e.write_xdr(&mut l).unwrap();
573 section.extend_from_slice(&buf);
574 }
575 let mut result = vec![0x00, 0x61, 0x73, 0x6d, 0x01, 0x00, 0x00, 0x00];
577 result.push(0); let mut sz = section.len() as u32;
579 let mut size_bytes = Vec::new();
580 while sz >= 0x80 {
581 size_bytes.push((sz as u8 & 0x7f) | 0x80);
582 sz >>= 7;
583 }
584 size_bytes.push(sz as u8);
585 result.extend_from_slice(&size_bytes);
586 result.extend_from_slice(§ion);
587 result
588 }
589
590 pub(crate) fn symbol_e(s: &str) -> stellar_xdr::ScSymbol {
591 stellar_xdr::ScSymbol(s.to_string().try_into().unwrap())
592 }
593
594 pub(crate) fn func_entry(name: &str, inputs: Vec<(String, ScSpecTypeDef)>) -> ScSpecEntry {
595 use stellar_xdr::{ScSpecFunctionInputV0, ScSpecFunctionV0};
596 ScSpecEntry::FunctionV0(ScSpecFunctionV0 {
597 doc: "".try_into().unwrap(),
598 name: symbol_e(name),
599 inputs: inputs
600 .into_iter()
601 .map(|(n, t)| ScSpecFunctionInputV0 {
602 doc: "".try_into().unwrap(),
603 name: n.try_into().unwrap(),
604 type_: t,
605 })
606 .collect::<Vec<_>>()
607 .try_into()
608 .unwrap(),
609 outputs: vec![].try_into().unwrap(),
610 })
611 }
612
613 pub(crate) fn event_entry(name: &str) -> ScSpecEntry {
614 event_entry_with_params(name, vec![])
615 }
616
617 pub(crate) fn event_entry_with_params(
618 name: &str,
619 params: Vec<(String, ScSpecTypeDef)>,
620 ) -> ScSpecEntry {
621 use stellar_xdr::{ScSpecEventParamLocationV0, ScSpecEventParamV0, ScSpecEventV0};
622 ScSpecEntry::EventV0(ScSpecEventV0 {
623 doc: "".try_into().unwrap(),
624 lib: "soroban_sdk".try_into().unwrap(),
625 name: symbol_e(name),
626 prefix_topics: vec![].try_into().unwrap(),
627 params: params
628 .into_iter()
629 .map(|(n, t)| ScSpecEventParamV0 {
630 doc: "".try_into().unwrap(),
631 name: n.try_into().unwrap(),
632 type_: t,
633 location: ScSpecEventParamLocationV0::Data,
634 })
635 .collect::<Vec<_>>()
636 .try_into()
637 .unwrap(),
638 data_format: stellar_xdr::ScSpecEventDataFormat::SingleValue,
639 })
640 }
641
642 pub(crate) fn udt_struct_entry(name: &str) -> ScSpecEntry {
643 udt_struct_entry_with_fields(name, vec![])
644 }
645
646 pub(crate) fn udt_struct_entry_with_fields(
647 name: &str,
648 fields: Vec<(String, ScSpecTypeDef)>,
649 ) -> ScSpecEntry {
650 use stellar_xdr::{ScSpecUdtStructFieldV0, ScSpecUdtStructV0};
651 ScSpecEntry::UdtStructV0(ScSpecUdtStructV0 {
652 doc: "".try_into().unwrap(),
653 lib: "soroban_sdk".try_into().unwrap(),
654 name: name.try_into().unwrap(),
655 fields: fields
656 .into_iter()
657 .map(|(n, t)| ScSpecUdtStructFieldV0 {
658 doc: "".try_into().unwrap(),
659 name: n.try_into().unwrap(),
660 type_: t,
661 })
662 .collect::<Vec<_>>()
663 .try_into()
664 .unwrap(),
665 })
666 }
667
668 #[test]
669 fn empty_wasm() {
670 assert!(matches!(parse_contract_spec(&[]), Err(WasmError::Empty)));
671 }
672
673 #[test]
674 fn invalid_wasm() {
675 let res = parse_contract_spec(b"not wasm at all");
676 assert!(matches!(res, Err(WasmError::Parse(_))));
677 }
678
679 #[test]
680 fn no_contract_spec() {
681 let res = parse_contract_spec(VALID_WASM);
683 assert!(matches!(res, Err(WasmError::NoContractSpec)));
684 }
685
686 #[test]
687 fn valid_single_function() {
688 let wasm = spec_section(&[func_entry(
689 "greet",
690 vec![("name".to_string(), ScSpecTypeDef::String)],
691 )]);
692 let spec = parse_contract_spec(&wasm).unwrap();
693 assert_eq!(spec.functions.len(), 1);
694 assert_eq!(spec.functions[0].name, "greet");
695 assert_eq!(spec.functions[0].parameters.len(), 1);
696 assert_eq!(spec.functions[0].parameters[0].name, "name");
697 assert_eq!(spec.functions[0].parameters[0].type_.name, "string");
698 }
699
700 #[test]
701 fn multiple_functions() {
702 let wasm = spec_section(&[
703 func_entry("a", vec![]),
704 func_entry("b", vec![("x".to_string(), ScSpecTypeDef::U32)]),
705 ]);
706 let spec = parse_contract_spec(&wasm).unwrap();
707 assert_eq!(spec.functions.len(), 2);
708 assert_eq!(spec.functions[0].name, "a");
709 assert_eq!(spec.functions[1].name, "b");
710 assert_eq!(spec.functions[1].parameters[0].type_.name, "u32");
711 }
712
713 #[test]
714 fn custom_type() {
715 use stellar_xdr::ScSpecUdtStructV0;
716 let udt = ScSpecEntry::UdtStructV0(ScSpecUdtStructV0 {
717 doc: "a point".try_into().unwrap(),
718 lib: "soroban_sdk".try_into().unwrap(),
719 name: "Point".try_into().unwrap(),
720 fields: vec![].try_into().unwrap(),
721 });
722 let wasm = spec_section(&[udt]);
723 let spec = parse_contract_spec(&wasm).unwrap();
724 assert_eq!(spec.custom_types.len(), 1);
725 assert_eq!(spec.custom_types[0].name, "Point");
726 assert_eq!(spec.custom_types[0].kind, "struct");
727 assert_eq!(spec.custom_types[0].doc, "a point");
728 }
729
730 #[test]
731 fn preserves_struct_union_and_event_metadata() {
732 use stellar_xdr::{
733 ScSpecEventDataFormat, ScSpecEventParamLocationV0, ScSpecEventParamV0, ScSpecEventV0,
734 ScSpecUdtStructFieldV0, ScSpecUdtStructV0, ScSpecUdtUnionCaseTupleV0,
735 ScSpecUdtUnionCaseV0, ScSpecUdtUnionV0,
736 };
737
738 let point = ScSpecEntry::UdtStructV0(ScSpecUdtStructV0 {
739 doc: "point".try_into().unwrap(),
740 lib: "test".try_into().unwrap(),
741 name: "Point".try_into().unwrap(),
742 fields: vec![ScSpecUdtStructFieldV0 {
743 doc: "x coordinate".try_into().unwrap(),
744 name: "x".try_into().unwrap(),
745 type_: ScSpecTypeDef::U64,
746 }]
747 .try_into()
748 .unwrap(),
749 });
750 let choice = ScSpecEntry::UdtUnionV0(ScSpecUdtUnionV0 {
751 doc: "choice".try_into().unwrap(),
752 lib: "test".try_into().unwrap(),
753 name: "Choice".try_into().unwrap(),
754 cases: vec![ScSpecUdtUnionCaseV0::TupleV0(ScSpecUdtUnionCaseTupleV0 {
755 doc: "some value".try_into().unwrap(),
756 name: "Some".try_into().unwrap(),
757 type_: vec![ScSpecTypeDef::String].try_into().unwrap(),
758 })]
759 .try_into()
760 .unwrap(),
761 });
762 let event = ScSpecEntry::EventV0(ScSpecEventV0 {
763 doc: "transfer event".try_into().unwrap(),
764 lib: "test".try_into().unwrap(),
765 name: symbol_e("Transfer"),
766 prefix_topics: vec![symbol_e("TOKEN")].try_into().unwrap(),
767 params: vec![ScSpecEventParamV0 {
768 doc: "amount".try_into().unwrap(),
769 name: "amount".try_into().unwrap(),
770 type_: ScSpecTypeDef::U128,
771 location: ScSpecEventParamLocationV0::Data,
772 }]
773 .try_into()
774 .unwrap(),
775 data_format: ScSpecEventDataFormat::SingleValue,
776 });
777
778 let spec = parse_contract_spec(&spec_section(&[point, choice, event])).unwrap();
779 assert_eq!(spec.custom_types[0].members[0].types[0].name, "u64");
780 assert_eq!(spec.custom_types[1].members[0].types[0].name, "string");
781 assert_eq!(spec.events[0].prefix_topics, vec!["TOKEN"]);
782 assert_eq!(spec.events[0].params[0].type_.name, "u128");
783 assert_eq!(spec.events[0].params[0].location, "data");
784 assert_eq!(spec.events[0].data_format, "single_value");
785 }
786
787 #[test]
788 fn preserves_compound_type_arguments_and_bytesn_length() {
789 use stellar_xdr::{ScSpecTypeBytesN, ScSpecTypeDef, ScSpecTypeMap, ScSpecTypeVec};
790
791 let vector = map_type_def(&ScSpecTypeDef::Vec(Box::new(ScSpecTypeVec {
792 element_type: Box::new(ScSpecTypeDef::U32),
793 })));
794 assert_eq!(vector.name, "vec<u32>");
795 assert_eq!(vector.type_args.len(), 1);
796 assert_eq!(vector.type_args[0].name, "u32");
797
798 let map = map_type_def(&ScSpecTypeDef::Map(Box::new(ScSpecTypeMap {
799 key_type: Box::new(ScSpecTypeDef::String),
800 value_type: Box::new(ScSpecTypeDef::Address),
801 })));
802 assert_eq!(
803 map.type_args
804 .iter()
805 .map(|t| t.name.as_str())
806 .collect::<Vec<_>>(),
807 ["string", "address"]
808 );
809
810 let bytes = map_type_def(&ScSpecTypeDef::BytesN(ScSpecTypeBytesN { n: 32 }));
811 assert_eq!(bytes.name, "bytesn<32>");
812 assert_eq!(bytes.bytes_n, Some(32));
813 }
814
815 #[test]
816 fn event_retains_params() {
817 let wasm = spec_section(&[event_entry_with_params(
819 "Transfer",
820 vec![
821 ("from".to_string(), ScSpecTypeDef::Address),
822 ("amount".to_string(), ScSpecTypeDef::I128),
823 ],
824 )]);
825 let spec = parse_contract_spec(&wasm).unwrap();
826 assert_eq!(spec.events.len(), 1);
827 let ev = &spec.events[0];
828 assert_eq!(ev.name, "Transfer");
829 assert_eq!(ev.data_format, "single_value");
830 assert!(ev.prefix_topics.is_empty());
831 assert_eq!(ev.params.len(), 2);
832 assert_eq!(ev.params[0].name, "from");
833 assert_eq!(ev.params[0].type_.name, "address");
834 assert_eq!(ev.params[0].location, "data");
835 assert_eq!(ev.params[1].name, "amount");
836 assert_eq!(ev.params[1].type_.name, "i128");
837 }
838
839 #[test]
840 fn udt_struct_retains_field_types() {
841 let wasm = spec_section(&[udt_struct_entry_with_fields(
842 "Point",
843 vec![("x".to_string(), ScSpecTypeDef::I32)],
844 )]);
845 let spec = parse_contract_spec(&wasm).unwrap();
846 assert_eq!(spec.custom_types.len(), 1);
847 assert_eq!(spec.custom_types[0].members.len(), 1);
848 assert_eq!(spec.custom_types[0].members[0].name, "x");
849 assert_eq!(spec.custom_types[0].members[0].types[0].name, "i32");
850 }
851
852 fn meta_entry(key: &str, val: &str) -> ScMetaEntry {
859 ScMetaEntry::ScMetaV0(stellar_xdr::ScMetaV0 {
860 key: key.to_string().try_into().unwrap(),
861 val: val.to_string().try_into().unwrap(),
862 })
863 }
864
865 fn encode_entries(entries: &[ScMetaEntry]) -> Vec<u8> {
867 let mut payload = Vec::new();
868 for e in entries {
869 let mut buf = Vec::new();
870 let mut cursor = Cursor::new(&mut buf);
871 let mut l = Limited::new(&mut cursor, Limits::none());
872 e.write_xdr(&mut l).unwrap();
873 payload.extend_from_slice(&buf);
874 }
875 payload
876 }
877
878 fn encode_meta_section(entries: &[ScMetaEntry]) -> Vec<u8> {
880 let mut section = Vec::new();
881 section.push(CONTRACT_META_V0.len() as u8);
882 section.extend_from_slice(CONTRACT_META_V0.as_bytes());
883 section.extend_from_slice(&encode_entries(entries));
884 let mut result = vec![0x00, 0x61, 0x73, 0x6d, 0x01, 0x00, 0x00, 0x00];
885 result.push(0); let mut sz = section.len() as u32;
887 let mut size_bytes = Vec::new();
888 while sz >= 0x80 {
889 size_bytes.push((sz as u8 & 0x7f) | 0x80);
890 sz >>= 7;
891 }
892 size_bytes.push(sz as u8);
893 result.extend_from_slice(&size_bytes);
894 result.extend_from_slice(§ion);
895 result
896 }
897
898 #[test]
899 fn decode_contract_meta_reads_generic_key_values_in_order() {
900 let payload = vec![
901 meta_entry("rsver", "1.97.1"),
902 meta_entry("totally_unknown_key", "arbitrary value"),
903 meta_entry("cliver", "27.1.0#deadbeef"),
904 ];
905 let decoded = decode_contract_meta(&encode_entries(&payload)).unwrap();
906 assert_eq!(decoded.len(), 3);
907 assert_eq!(decoded[0].key, "rsver");
908 assert_eq!(decoded[0].value, "1.97.1");
909 assert_eq!(decoded[1].key, "totally_unknown_key");
910 assert_eq!(decoded[1].value, "arbitrary value");
911 assert_eq!(decoded[2].value, "27.1.0#deadbeef");
912 }
913
914 #[test]
915 fn decode_contract_meta_preserves_duplicate_keys() {
916 let payload = vec![
917 meta_entry("k", "first"),
918 meta_entry("k", "second"),
919 meta_entry("k", "third"),
920 ];
921 let decoded = decode_contract_meta(&encode_entries(&payload)).unwrap();
922 let values: Vec<&str> = decoded.iter().map(|e| e.value.as_str()).collect();
923 assert_eq!(values, vec!["first", "second", "third"]);
924 }
925
926 #[test]
927 fn decode_contract_meta_empty_payload_is_empty_list() {
928 assert!(decode_contract_meta(&[]).unwrap().is_empty());
929 }
930
931 #[test]
932 fn decode_contract_meta_rejects_malformed_payload() {
933 assert!(matches!(
935 decode_contract_meta(&[0xff, 0x00, 0x00]).unwrap_err(),
936 WasmError::MetaXdr(_)
937 ));
938 assert!(matches!(
940 decode_contract_meta(&[0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x40]).unwrap_err(),
941 WasmError::MetaXdr(_)
942 ));
943 }
944
945 #[test]
946 fn parse_metadata_surfaces_meta_and_keeps_custom_sections_unchanged() {
947 let wasm = encode_meta_section(&[
948 meta_entry("rsver", "1.97.1"),
949 meta_entry("cliver", "27.1.0#8e402ea282029"),
950 ]);
951 let meta = crate::parse_metadata(&wasm).unwrap();
952 assert_eq!(meta.custom_sections, vec![CONTRACT_META_V0.to_string()]);
954 assert_eq!(meta.contract_meta.len(), 2);
955 assert_eq!(meta.contract_meta[0].key, "rsver");
956 assert_eq!(meta.contract_meta[1].value, "27.1.0#8e402ea282029");
957 }
958
959 #[test]
960 fn parse_metadata_without_meta_section_is_empty() {
961 let meta = crate::parse_metadata(VALID_WASM).unwrap();
962 assert!(meta.contract_meta.is_empty());
963 assert!(meta.custom_sections.is_empty());
964 }
965
966 #[test]
967 fn parse_metadata_decodes_real_committed_contract_metadata() {
968 let bytes = std::fs::read(concat!(
971 env!("CARGO_MANIFEST_DIR"),
972 "/../sdkt-cli/tests/fixtures/us_new.wasm"
973 ))
974 .expect("fixture present");
975 let meta = crate::parse_metadata(&bytes).unwrap();
976 assert!(meta.custom_sections.contains(&CONTRACT_META_V0.to_string()));
977 assert!(
978 !meta.contract_meta.is_empty(),
979 "real contract metadata must decode"
980 );
981 let get = |k: &str| {
982 meta.contract_meta
983 .iter()
984 .find(|e| e.key == k)
985 .map(|e| e.value.clone())
986 };
987 assert_eq!(get("rsver").as_deref(), Some("1.97.1"));
988 assert!(get("cliver").unwrap().starts_with("27.1.0#"));
989 }
990}