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