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!(f, " • {key}: {val}")?;
154                    }
155                }
156            }
157            writeln!(f)?;
158        } else {
159            writeln!(f, "Contract Meta: None\n")?;
160        }
161
162        if let Some(_spec_base64) = &self.spec_base64 {
163            writeln!(f, "Contract Spec:")?;
164            for spec_entry in &self.spec {
165                match spec_entry {
166                    ScSpecEntry::FunctionV0(func) => write_func(f, func)?,
167                    ScSpecEntry::UdtUnionV0(udt) => write_union(f, udt)?,
168                    ScSpecEntry::UdtStructV0(udt) => write_struct(f, udt)?,
169                    ScSpecEntry::UdtEnumV0(udt) => write_enum(f, udt)?,
170                    ScSpecEntry::UdtErrorEnumV0(udt) => write_error(f, udt)?,
171                    ScSpecEntry::EventV0(_) => {}
172                }
173            }
174        } else {
175            writeln!(f, "Contract Spec: None")?;
176        }
177        Ok(())
178    }
179}
180
181fn write_func(f: &mut std::fmt::Formatter<'_>, func: &ScSpecFunctionV0) -> std::fmt::Result {
182    writeln!(f, " • Function: {}", func.name.to_utf8_string_lossy())?;
183    if !func.doc.is_empty() {
184        writeln!(
185            f,
186            "     Docs: {}",
187            &indent(&func.doc.to_utf8_string_lossy(), 11).trim()
188        )?;
189    }
190    writeln!(
191        f,
192        "     Inputs: {}",
193        indent(&format!("{:#?}", func.inputs), 5).trim()
194    )?;
195    writeln!(
196        f,
197        "     Output: {}",
198        indent(&format!("{:#?}", func.outputs), 5).trim()
199    )?;
200    writeln!(f)?;
201    Ok(())
202}
203
204fn write_union(f: &mut std::fmt::Formatter<'_>, udt: &ScSpecUdtUnionV0) -> std::fmt::Result {
205    writeln!(f, " • Union: {}", format_name(&udt.lib, &udt.name))?;
206    if !udt.doc.is_empty() {
207        writeln!(
208            f,
209            "     Docs: {}",
210            indent(&udt.doc.to_utf8_string_lossy(), 10).trim()
211        )?;
212    }
213    writeln!(f, "     Cases:")?;
214    for case in udt.cases.iter() {
215        writeln!(f, "      • {}", indent(&format!("{case:#?}"), 8).trim())?;
216    }
217    writeln!(f)?;
218    Ok(())
219}
220
221fn write_struct(f: &mut std::fmt::Formatter<'_>, udt: &ScSpecUdtStructV0) -> std::fmt::Result {
222    writeln!(f, " • Struct: {}", format_name(&udt.lib, &udt.name))?;
223    if !udt.doc.is_empty() {
224        writeln!(
225            f,
226            "     Docs: {}",
227            indent(&udt.doc.to_utf8_string_lossy(), 10).trim()
228        )?;
229    }
230    writeln!(f, "     Fields:")?;
231    for field in udt.fields.iter() {
232        writeln!(
233            f,
234            "      • {}: {}",
235            field.name.to_utf8_string_lossy(),
236            indent(&format!("{:#?}", field.type_), 8).trim()
237        )?;
238        if !field.doc.is_empty() {
239            writeln!(f, "{}", indent(&format!("{:#?}", field.doc), 8))?;
240        }
241    }
242    writeln!(f)?;
243    Ok(())
244}
245
246fn write_enum(f: &mut std::fmt::Formatter<'_>, udt: &ScSpecUdtEnumV0) -> std::fmt::Result {
247    writeln!(f, " • Enum: {}", format_name(&udt.lib, &udt.name))?;
248    if !udt.doc.is_empty() {
249        writeln!(
250            f,
251            "     Docs: {}",
252            indent(&udt.doc.to_utf8_string_lossy(), 10).trim()
253        )?;
254    }
255    writeln!(f, "     Cases:")?;
256    for case in udt.cases.iter() {
257        writeln!(f, "      • {}", indent(&format!("{case:#?}"), 8).trim())?;
258    }
259    writeln!(f)?;
260    Ok(())
261}
262
263fn write_error(f: &mut std::fmt::Formatter<'_>, udt: &ScSpecUdtErrorEnumV0) -> std::fmt::Result {
264    writeln!(f, " • Error: {}", format_name(&udt.lib, &udt.name))?;
265    if !udt.doc.is_empty() {
266        writeln!(
267            f,
268            "     Docs: {}",
269            indent(&udt.doc.to_utf8_string_lossy(), 10).trim()
270        )?;
271    }
272    writeln!(f, "     Cases:")?;
273    for case in udt.cases.iter() {
274        writeln!(f, "      • {}", indent(&format!("{case:#?}"), 8).trim())?;
275    }
276    writeln!(f)?;
277    Ok(())
278}
279
280fn indent(s: &str, n: usize) -> String {
281    let pad = " ".repeat(n);
282    s.lines()
283        .map(|line| format!("{pad}{line}"))
284        .collect::<Vec<_>>()
285        .join("\n")
286}
287
288fn format_name(lib: &StringM<80>, name: &StringM<60>) -> String {
289    if lib.is_empty() {
290        name.to_utf8_string_lossy()
291    } else {
292        format!(
293            "{}::{}",
294            lib.to_utf8_string_lossy(),
295            name.to_utf8_string_lossy()
296        )
297    }
298}