Skip to main content

soroban_spec_rust/
lib.rs

1pub mod r#trait;
2pub mod types;
3
4use std::{fs, io};
5
6use proc_macro2::TokenStream;
7use quote::quote;
8use sha2::{Digest, Sha256};
9use stellar_xdr::curr as stellar_xdr;
10use stellar_xdr::ScSpecEntry;
11use syn::Error;
12
13use soroban_spec::read::{from_wasm, FromWasmError};
14
15pub use types::GenerateOptions;
16use types::{
17    generate_enum_with_options, generate_error_enum_with_options, generate_event_with_options,
18    generate_struct_with_options, generate_union_with_options,
19};
20
21// IMPORTANT: The "docs" fields of spec entries are not output in Rust token
22// streams as rustdocs, because rustdocs can contain Rust code, and that code
23// will be executed. Generated code may be generated from untrusted Wasm
24// containing untrusted spec docs.
25
26#[derive(thiserror::Error, Debug)]
27pub enum GenerateFromFileError {
28    #[error("reading file: {0}")]
29    Io(io::Error),
30    #[error("sha256 does not match, expected: {expected}")]
31    VerifySha256 { expected: String },
32    #[error("parsing contract spec: {0}")]
33    Parse(stellar_xdr::Error),
34    #[error("getting contract spec: {0}")]
35    GetSpec(FromWasmError),
36}
37
38pub fn generate_from_file(
39    file: &str,
40    verify_sha256: Option<&str>,
41) -> Result<TokenStream, GenerateFromFileError> {
42    // Read file.
43    let wasm = fs::read(file).map_err(GenerateFromFileError::Io)?;
44
45    // Generate code.
46    let code = generate_from_wasm(&wasm, file, verify_sha256)?;
47    Ok(code)
48}
49
50pub fn generate_from_wasm(
51    wasm: &[u8],
52    file: &str,
53    verify_sha256: Option<&str>,
54) -> Result<TokenStream, GenerateFromFileError> {
55    generate_from_wasm_with_options(wasm, file, verify_sha256, &GenerateOptions::default())
56}
57
58pub fn generate_from_wasm_with_options(
59    wasm: &[u8],
60    file: &str,
61    verify_sha256: Option<&str>,
62    opts: &GenerateOptions,
63) -> Result<TokenStream, GenerateFromFileError> {
64    let sha256 = Sha256::digest(wasm);
65    let sha256 = format!("{:x}", sha256);
66    if let Some(verify_sha256) = verify_sha256 {
67        if verify_sha256 != sha256 {
68            return Err(GenerateFromFileError::VerifySha256 { expected: sha256 });
69        }
70    }
71
72    let spec = from_wasm(wasm).map_err(GenerateFromFileError::GetSpec)?;
73    let code = generate_with_options(&spec, file, &sha256, opts);
74    Ok(code)
75}
76
77pub fn generate(specs: &[ScSpecEntry], file: &str, sha256: &str) -> TokenStream {
78    generate_with_options(specs, file, sha256, &GenerateOptions::default())
79}
80
81pub fn generate_with_options(
82    specs: &[ScSpecEntry],
83    file: &str,
84    sha256: &str,
85    opts: &GenerateOptions,
86) -> TokenStream {
87    let generated = generate_without_file_with_options(specs, opts);
88    quote! {
89        pub const WASM: &[u8] = soroban_sdk::contractfile!(file = #file, sha256 = #sha256);
90        #generated
91    }
92}
93
94pub fn generate_without_file(specs: &[ScSpecEntry]) -> TokenStream {
95    generate_without_file_with_options(specs, &GenerateOptions::default())
96}
97
98pub fn generate_without_file_with_options(
99    specs: &[ScSpecEntry],
100    opts: &GenerateOptions,
101) -> TokenStream {
102    let mut spec_fns = Vec::new();
103    let mut spec_structs = Vec::new();
104    let mut spec_unions = Vec::new();
105    let mut spec_enums = Vec::new();
106    let mut spec_error_enums = Vec::new();
107    let mut spec_events = Vec::new();
108    for s in specs {
109        match s {
110            ScSpecEntry::FunctionV0(f) => spec_fns.push(f),
111            ScSpecEntry::UdtStructV0(s) => spec_structs.push(s),
112            ScSpecEntry::UdtUnionV0(u) => spec_unions.push(u),
113            ScSpecEntry::UdtEnumV0(e) => spec_enums.push(e),
114            ScSpecEntry::UdtErrorEnumV0(e) => spec_error_enums.push(e),
115            ScSpecEntry::EventV0(e) => spec_events.push(e),
116        }
117    }
118
119    let trait_name = "Contract";
120
121    let trait_ = r#trait::generate_trait(trait_name, &spec_fns);
122    let structs = spec_structs
123        .iter()
124        .map(|s| generate_struct_with_options(s, opts));
125    let unions = spec_unions
126        .iter()
127        .map(|s| generate_union_with_options(s, opts));
128    let enums = spec_enums
129        .iter()
130        .map(|s| generate_enum_with_options(s, opts));
131    let error_enums = spec_error_enums
132        .iter()
133        .map(|s| generate_error_enum_with_options(s, opts));
134    let events = spec_events
135        .iter()
136        .map(|s| generate_event_with_options(s, opts));
137
138    quote! {
139        #[soroban_sdk::contractargs(name = "Args")]
140        #[soroban_sdk::contractclient(name = "Client")]
141        #trait_
142
143        #(#structs)*
144        #(#unions)*
145        #(#enums)*
146        #(#error_enums)*
147        #(#events)*
148    }
149}
150
151/// Implemented by types that can be converted into pretty formatted Strings of
152/// Rust code.
153pub trait ToFormattedString {
154    /// Converts the value to a String that is pretty formatted. If there is any
155    /// error parsing the token stream the raw String version of the code is
156    /// returned instead.
157    fn to_formatted_string(&self) -> Result<String, Error>;
158}
159
160impl ToFormattedString for TokenStream {
161    fn to_formatted_string(&self) -> Result<String, Error> {
162        let file = syn::parse2(self.clone())?;
163        Ok(prettyplease::unparse(&file))
164    }
165}
166
167#[cfg(test)]
168mod test {
169    use pretty_assertions::assert_eq;
170
171    use super::{generate, ToFormattedString};
172    use soroban_spec::read::from_wasm;
173
174    const EXAMPLE_WASM: &[u8] = include_bytes!("../../target/wasm32v1-none/release/test_udt.wasm");
175
176    #[test]
177    fn example() {
178        let entries = from_wasm(EXAMPLE_WASM).unwrap();
179        let rust = generate(&entries, "<file>", "<sha256>")
180            .to_formatted_string()
181            .unwrap();
182        assert_eq!(
183            rust,
184            r#"pub const WASM: &[u8] = soroban_sdk::contractfile!(file = "<file>", sha256 = "<sha256>");
185#[soroban_sdk::contractargs(name = "Args")]
186#[soroban_sdk::contractclient(name = "Client")]
187pub trait Contract {
188    fn add(env: soroban_sdk::Env, a: UdtEnum, b: UdtEnum) -> i64;
189}
190#[soroban_sdk::contracttype(export = false)]
191#[derive(Debug, Clone, Eq, PartialEq, Ord, PartialOrd)]
192pub struct UdtTuple(pub i64, pub soroban_sdk::Vec<i64>);
193#[soroban_sdk::contracttype(export = false)]
194#[derive(Debug, Clone, Eq, PartialEq, Ord, PartialOrd)]
195pub struct UdtStruct {
196    pub a: i64,
197    pub b: i64,
198    pub c: soroban_sdk::Vec<i64>,
199}
200#[soroban_sdk::contracttype(export = false)]
201#[derive(Debug, Clone, Eq, PartialEq, Ord, PartialOrd)]
202pub enum UdtEnum {
203    UdtA,
204    UdtB(UdtStruct),
205    UdtC(UdtEnum2),
206    UdtD(UdtTuple),
207}
208#[soroban_sdk::contracttype(export = false)]
209#[derive(Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd)]
210pub enum UdtEnum2 {
211    A = 10,
212    B = 15,
213}
214"#,
215        );
216    }
217}