Skip to main content

soroban_spec_tools/
contract.rs

1use base64::{engine::general_purpose::STANDARD as base64, Engine as _};
2use std::{
3    fmt::Display,
4    io::{self, Cursor},
5};
6
7use stellar_xdr::curr::{
8    self as xdr, Limited, Limits, ReadXdr, ScEnvMetaEntry, ScEnvMetaEntryInterfaceVersion,
9    ScMetaEntry, ScMetaV0, ScSpecEntry, ScSpecFunctionV0, ScSpecUdtEnumV0, ScSpecUdtErrorEnumV0,
10    ScSpecUdtStructV0, ScSpecUdtUnionV0, StringM, WriteXdr,
11};
12
13pub struct Spec {
14    pub env_meta_base64: Option<String>,
15    pub env_meta: Vec<ScEnvMetaEntry>,
16    pub meta_base64: Option<String>,
17    pub meta: Vec<ScMetaEntry>,
18    pub spec_base64: Option<String>,
19    pub spec: Vec<ScSpecEntry>,
20}
21
22#[derive(thiserror::Error, Debug)]
23pub enum Error {
24    #[error("reading file {filepath}: {error}")]
25    CannotReadContractFile {
26        filepath: std::path::PathBuf,
27        error: io::Error,
28    },
29    #[error("cannot parse wasm file {file}: {error}")]
30    CannotParseWasm {
31        file: std::path::PathBuf,
32        error: wasmparser::BinaryReaderError,
33    },
34    #[error("xdr processing error: {0}")]
35    Xdr(#[from] xdr::Error),
36
37    #[error(transparent)]
38    Parser(#[from] wasmparser::BinaryReaderError),
39}
40
41impl Spec {
42    pub fn new(bytes: &[u8]) -> Result<Self, Error> {
43        let mut env_meta: Option<Vec<u8>> = None;
44        let mut meta: Option<Vec<u8>> = None;
45        let mut spec: Option<Vec<u8>> = None;
46        for payload in wasmparser::Parser::new(0).parse_all(bytes) {
47            let payload = payload?;
48            if let wasmparser::Payload::CustomSection(section) = payload {
49                let out = match section.name() {
50                    "contractenvmetav0" => &mut env_meta,
51                    "contractmetav0" => &mut meta,
52                    "contractspecv0" => &mut spec,
53                    _ => continue,
54                };
55
56                if let Some(existing_data) = out {
57                    let combined_data = [existing_data, section.data()].concat();
58                    *out = Some(combined_data);
59                } else {
60                    *out = Some(section.data().to_vec());
61                }
62            }
63        }
64
65        let mut env_meta_base64 = None;
66        let env_meta = if let Some(env_meta) = env_meta {
67            env_meta_base64 = Some(base64.encode(&env_meta));
68            let cursor = Cursor::new(env_meta);
69            let mut read = Limited::new(cursor, Limits::none());
70            ScEnvMetaEntry::read_xdr_iter(&mut read).collect::<Result<Vec<_>, xdr::Error>>()?
71        } else {
72            vec![]
73        };
74
75        let mut meta_base64 = None;
76        let meta = if let Some(meta) = meta {
77            meta_base64 = Some(base64.encode(&meta));
78            let cursor = Cursor::new(meta);
79            let mut depth_limit_read = Limited::new(cursor, Limits::none());
80            ScMetaEntry::read_xdr_iter(&mut depth_limit_read)
81                .collect::<Result<Vec<_>, xdr::Error>>()?
82        } else {
83            vec![]
84        };
85
86        let (spec_base64, spec) = if let Some(spec) = spec {
87            let (spec_base64, spec) = Spec::spec_to_base64(&spec)?;
88            (Some(spec_base64), spec)
89        } else {
90            (None, vec![])
91        };
92
93        Ok(Spec {
94            env_meta_base64,
95            env_meta,
96            meta_base64,
97            meta,
98            spec_base64,
99            spec,
100        })
101    }
102
103    pub fn spec_as_json_array(&self) -> Result<String, Error> {
104        let spec = self
105            .spec
106            .iter()
107            .map(|e| Ok(format!("\"{}\"", e.to_xdr_base64(Limits::none())?)))
108            .collect::<Result<Vec<_>, Error>>()?
109            .join(",\n");
110        Ok(format!("[{spec}]"))
111    }
112
113    pub fn spec_to_base64(spec: &[u8]) -> Result<(String, Vec<ScSpecEntry>), Error> {
114        let spec_base64 = base64.encode(spec);
115        let cursor = Cursor::new(spec);
116        let mut read = Limited::new(cursor, Limits::none());
117        Ok((
118            spec_base64,
119            ScSpecEntry::read_xdr_iter(&mut read).collect::<Result<Vec<_>, xdr::Error>>()?,
120        ))
121    }
122}
123
124impl Display for Spec {
125    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
126        if let Some(env_meta) = &self.env_meta_base64 {
127            writeln!(f, "Env Meta: {env_meta}")?;
128            for env_meta_entry in &self.env_meta {
129                match env_meta_entry {
130                    ScEnvMetaEntry::ScEnvMetaKindInterfaceVersion(
131                        ScEnvMetaEntryInterfaceVersion {
132                            protocol,
133                            pre_release,
134                        },
135                    ) => {
136                        writeln!(f, " • Protocol Version: {protocol}")?;
137                        if pre_release != &0 {
138                            writeln!(f, " • Pre-release Version: {pre_release})")?;
139                        }
140                    }
141                }
142            }
143            writeln!(f)?;
144        } else {
145            writeln!(f, "Env Meta: None\n")?;
146        }
147
148        if let Some(_meta) = &self.meta_base64 {
149            writeln!(f, "Contract Meta:")?;
150            for meta_entry in &self.meta {
151                match meta_entry {
152                    ScMetaEntry::ScMetaV0(ScMetaV0 { key, val }) => {
153                        writeln!(
154                            f,
155                            " • {}: {}",
156                            sanitize(&key.to_utf8_string_lossy()),
157                            sanitize(&val.to_utf8_string_lossy())
158                        )?;
159                    }
160                }
161            }
162            writeln!(f)?;
163        } else {
164            writeln!(f, "Contract Meta: None\n")?;
165        }
166
167        if let Some(_spec_base64) = &self.spec_base64 {
168            writeln!(f, "Contract Spec:")?;
169            for spec_entry in &self.spec {
170                match spec_entry {
171                    ScSpecEntry::FunctionV0(func) => write_func(f, func)?,
172                    ScSpecEntry::UdtUnionV0(udt) => write_union(f, udt)?,
173                    ScSpecEntry::UdtStructV0(udt) => write_struct(f, udt)?,
174                    ScSpecEntry::UdtEnumV0(udt) => write_enum(f, udt)?,
175                    ScSpecEntry::UdtErrorEnumV0(udt) => write_error(f, udt)?,
176                    ScSpecEntry::EventV0(_) => {}
177                }
178            }
179        } else {
180            writeln!(f, "Contract Spec: None")?;
181        }
182        Ok(())
183    }
184}
185
186fn write_func(f: &mut std::fmt::Formatter<'_>, func: &ScSpecFunctionV0) -> std::fmt::Result {
187    writeln!(
188        f,
189        " • Function: {}",
190        sanitize(&func.name.to_utf8_string_lossy())
191    )?;
192    if !func.doc.is_empty() {
193        writeln!(
194            f,
195            "     Docs: {}",
196            &indent(&sanitize(&func.doc.to_utf8_string_lossy()), 11).trim()
197        )?;
198    }
199    writeln!(
200        f,
201        "     Inputs: {}",
202        indent(&format!("{:#?}", func.inputs), 5).trim()
203    )?;
204    writeln!(
205        f,
206        "     Output: {}",
207        indent(&format!("{:#?}", func.outputs), 5).trim()
208    )?;
209    writeln!(f)?;
210    Ok(())
211}
212
213fn write_union(f: &mut std::fmt::Formatter<'_>, udt: &ScSpecUdtUnionV0) -> std::fmt::Result {
214    writeln!(f, " • Union: {}", format_name(&udt.lib, &udt.name))?;
215    if !udt.doc.is_empty() {
216        writeln!(
217            f,
218            "     Docs: {}",
219            indent(&sanitize(&udt.doc.to_utf8_string_lossy()), 10).trim()
220        )?;
221    }
222    writeln!(f, "     Cases:")?;
223    for case in udt.cases.iter() {
224        writeln!(f, "      • {}", indent(&format!("{case:#?}"), 8).trim())?;
225    }
226    writeln!(f)?;
227    Ok(())
228}
229
230fn write_struct(f: &mut std::fmt::Formatter<'_>, udt: &ScSpecUdtStructV0) -> std::fmt::Result {
231    writeln!(f, " • Struct: {}", format_name(&udt.lib, &udt.name))?;
232    if !udt.doc.is_empty() {
233        writeln!(
234            f,
235            "     Docs: {}",
236            indent(&sanitize(&udt.doc.to_utf8_string_lossy()), 10).trim()
237        )?;
238    }
239    writeln!(f, "     Fields:")?;
240    for field in udt.fields.iter() {
241        writeln!(
242            f,
243            "      • {}: {}",
244            sanitize(&field.name.to_utf8_string_lossy()),
245            indent(&format!("{:#?}", field.type_), 8).trim()
246        )?;
247        if !field.doc.is_empty() {
248            writeln!(f, "{}", indent(&format!("{:#?}", field.doc), 8))?;
249        }
250    }
251    writeln!(f)?;
252    Ok(())
253}
254
255fn write_enum(f: &mut std::fmt::Formatter<'_>, udt: &ScSpecUdtEnumV0) -> std::fmt::Result {
256    writeln!(f, " • Enum: {}", format_name(&udt.lib, &udt.name))?;
257    if !udt.doc.is_empty() {
258        writeln!(
259            f,
260            "     Docs: {}",
261            indent(&sanitize(&udt.doc.to_utf8_string_lossy()), 10).trim()
262        )?;
263    }
264    writeln!(f, "     Cases:")?;
265    for case in udt.cases.iter() {
266        writeln!(f, "      • {}", indent(&format!("{case:#?}"), 8).trim())?;
267    }
268    writeln!(f)?;
269    Ok(())
270}
271
272fn write_error(f: &mut std::fmt::Formatter<'_>, udt: &ScSpecUdtErrorEnumV0) -> std::fmt::Result {
273    writeln!(f, " • Error: {}", format_name(&udt.lib, &udt.name))?;
274    if !udt.doc.is_empty() {
275        writeln!(
276            f,
277            "     Docs: {}",
278            indent(&sanitize(&udt.doc.to_utf8_string_lossy()), 10).trim()
279        )?;
280    }
281    writeln!(f, "     Cases:")?;
282    for case in udt.cases.iter() {
283        writeln!(f, "      • {}", indent(&format!("{case:#?}"), 8).trim())?;
284    }
285    writeln!(f)?;
286    Ok(())
287}
288
289pub fn sanitize(s: &str) -> String {
290    escape_bytes::escape(s.as_bytes())
291        .into_iter()
292        .map(char::from)
293        .collect()
294}
295
296fn indent(s: &str, n: usize) -> String {
297    let pad = " ".repeat(n);
298    s.lines()
299        .map(|line| format!("{pad}{line}"))
300        .collect::<Vec<_>>()
301        .join("\n")
302}
303
304fn format_name(lib: &StringM<80>, name: &StringM<60>) -> String {
305    if lib.is_empty() {
306        sanitize(&name.to_utf8_string_lossy())
307    } else {
308        format!(
309            "{}::{}",
310            sanitize(&lib.to_utf8_string_lossy()),
311            sanitize(&name.to_utf8_string_lossy())
312        )
313    }
314}