use serde::{Deserialize, Serialize};
use std::io::Cursor;
use stellar_xdr::{Limited, Limits, ReadXdr, ScSpecEntry, ScSpecEntryKind, ScSpecTypeDef};
use wasmparser::Payload;
use crate::WasmError;
pub const CONTRACT_SPEC_V0: &str = "contractspecv0";
pub const CONTRACT_ENV_META_V0: &str = "contractenvmetav0";
#[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>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct TypeMember {
pub name: String,
pub doc: String,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct ContractEvent {
pub name: String,
pub doc: 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(ContractEvent {
name: e.name.to_utf8_string_lossy(),
doc: e.doc.to_utf8_string_lossy(),
}),
}
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,
})
}
fn map_type_def(t: &ScSpecTypeDef) -> ContractType {
let mut udt_name: Option<String> = None;
let (name, kind, members) = match t {
ScSpecTypeDef::Val => ("val", "primitive", vec![]),
ScSpecTypeDef::Bool => ("bool", "primitive", vec![]),
ScSpecTypeDef::Void => ("void", "primitive", vec![]),
ScSpecTypeDef::Error => ("error", "primitive", vec![]),
ScSpecTypeDef::U32 => ("u32", "primitive", vec![]),
ScSpecTypeDef::I32 => ("i32", "primitive", vec![]),
ScSpecTypeDef::U64 => ("u64", "primitive", vec![]),
ScSpecTypeDef::I64 => ("i64", "primitive", vec![]),
ScSpecTypeDef::Timepoint => ("timepoint", "primitive", vec![]),
ScSpecTypeDef::Duration => ("duration", "primitive", vec![]),
ScSpecTypeDef::U128 => ("u128", "primitive", vec![]),
ScSpecTypeDef::I128 => ("i128", "primitive", vec![]),
ScSpecTypeDef::U256 => ("u256", "primitive", vec![]),
ScSpecTypeDef::I256 => ("i256", "primitive", vec![]),
ScSpecTypeDef::Bytes => ("bytes", "primitive", vec![]),
ScSpecTypeDef::String => ("string", "primitive", vec![]),
ScSpecTypeDef::Symbol => ("symbol", "primitive", vec![]),
ScSpecTypeDef::Address => ("address", "primitive", vec![]),
ScSpecTypeDef::MuxedAddress => ("muxed_address", "primitive", vec![]),
ScSpecTypeDef::Option(_) => ("option", "compound", vec![]),
ScSpecTypeDef::Result(_) => ("result", "compound", vec![]),
ScSpecTypeDef::Vec(_) => ("vec", "compound", vec![]),
ScSpecTypeDef::Map(_) => ("map", "compound", vec![]),
ScSpecTypeDef::Tuple(_) => ("tuple", "compound", vec![]),
ScSpecTypeDef::BytesN(_) => ("bytesn", "compound", vec![]),
ScSpecTypeDef::Udt(u) => {
udt_name = Some(u.name.to_utf8_string_lossy());
("udt", "udt", vec![])
}
};
ContractType {
name: udt_name.unwrap_or_else(|| name.to_string()),
kind: kind.to_string(),
doc: String::new(),
members,
}
}
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(),
})
.collect(),
}
}
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(),
},
stellar_xdr::ScSpecUdtUnionCaseV0::TupleV0(t) => TypeMember {
name: t.name.to_utf8_string_lossy(),
doc: t.doc.to_utf8_string_lossy(),
},
})
.collect(),
}
}
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(),
})
.collect(),
}
}
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(),
})
.collect(),
}
}
#[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 {
use stellar_xdr::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: vec![].try_into().unwrap(),
data_format: stellar_xdr::ScSpecEventDataFormat::SingleValue,
})
}
pub(crate) fn udt_struct_entry(name: &str) -> ScSpecEntry {
use stellar_xdr::ScSpecUdtStructV0;
ScSpecEntry::UdtStructV0(ScSpecUdtStructV0 {
doc: "".try_into().unwrap(),
lib: "soroban_sdk".try_into().unwrap(),
name: name.try_into().unwrap(),
fields: 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");
}
}