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
15use types::{generate_enum, generate_error_enum, generate_struct, generate_union};
16
17#[derive(thiserror::Error, Debug)]
18pub enum GenerateFromFileError {
19    #[error("reading file: {0}")]
20    Io(io::Error),
21    #[error("sha256 does not match, expected: {expected}")]
22    VerifySha256 { expected: String },
23    #[error("parsing contract spec: {0}")]
24    Parse(stellar_xdr::Error),
25    #[error("getting contract spec: {0}")]
26    GetSpec(FromWasmError),
27}
28
29pub fn generate_from_file(
30    file: &str,
31    verify_sha256: Option<&str>,
32) -> Result<TokenStream, GenerateFromFileError> {
33    // Read file.
34    let wasm = fs::read(file).map_err(GenerateFromFileError::Io)?;
35
36    // Generate code.
37    let code = generate_from_wasm(&wasm, file, verify_sha256)?;
38    Ok(code)
39}
40
41pub fn generate_from_wasm(
42    wasm: &[u8],
43    file: &str,
44    verify_sha256: Option<&str>,
45) -> Result<TokenStream, GenerateFromFileError> {
46    let sha256 = Sha256::digest(wasm);
47    let sha256 = format!("{:x}", sha256);
48    if let Some(verify_sha256) = verify_sha256 {
49        if verify_sha256 != sha256 {
50            return Err(GenerateFromFileError::VerifySha256 { expected: sha256 });
51        }
52    }
53
54    let spec = from_wasm(wasm).map_err(GenerateFromFileError::GetSpec)?;
55    let code = generate(&spec, file, &sha256);
56    Ok(code)
57}
58
59pub fn generate(specs: &[ScSpecEntry], file: &str, sha256: &str) -> TokenStream {
60    let generated = generate_without_file(specs);
61    quote! {
62        pub const WASM: &[u8] = soroban_sdk::contractfile!(file = #file, sha256 = #sha256);
63        #generated
64    }
65}
66
67pub fn generate_without_file(specs: &[ScSpecEntry]) -> TokenStream {
68    let mut spec_fns = Vec::new();
69    let mut spec_structs = Vec::new();
70    let mut spec_unions = Vec::new();
71    let mut spec_enums = Vec::new();
72    let mut spec_error_enums = Vec::new();
73    for s in specs {
74        match s {
75            ScSpecEntry::FunctionV0(f) => spec_fns.push(f),
76            ScSpecEntry::UdtStructV0(s) => spec_structs.push(s),
77            ScSpecEntry::UdtUnionV0(u) => spec_unions.push(u),
78            ScSpecEntry::UdtEnumV0(e) => spec_enums.push(e),
79            ScSpecEntry::UdtErrorEnumV0(e) => spec_error_enums.push(e),
80        }
81    }
82
83    let trait_name = "Contract";
84
85    let trait_ = r#trait::generate_trait(trait_name, &spec_fns);
86    let structs = spec_structs.iter().map(|s| generate_struct(s));
87    let unions = spec_unions.iter().map(|s| generate_union(s));
88    let enums = spec_enums.iter().map(|s| generate_enum(s));
89    let error_enums = spec_error_enums.iter().map(|s| generate_error_enum(s));
90
91    quote! {
92        #[soroban_sdk::contractargs(name = "Args")]
93        #[soroban_sdk::contractclient(name = "Client")]
94        #trait_
95
96        #(#structs)*
97        #(#unions)*
98        #(#enums)*
99        #(#error_enums)*
100    }
101}
102
103/// Implemented by types that can be converted into pretty formatted Strings of
104/// Rust code.
105pub trait ToFormattedString {
106    /// Converts the value to a String that is pretty formatted. If there is any
107    /// error parsing the token stream the raw String version of the code is
108    /// returned instead.
109    fn to_formatted_string(&self) -> Result<String, Error>;
110}
111
112impl ToFormattedString for TokenStream {
113    fn to_formatted_string(&self) -> Result<String, Error> {
114        let file = syn::parse2(self.clone())?;
115        Ok(prettyplease::unparse(&file))
116    }
117}
118
119#[cfg(test)]
120mod test {
121    use pretty_assertions::assert_eq;
122
123    use super::{generate, ToFormattedString};
124    use soroban_spec::read::from_wasm;
125
126    const EXAMPLE_WASM: &[u8] =
127        include_bytes!("../../target/wasm32-unknown-unknown/release/test_udt.wasm");
128
129    #[test]
130    fn example() {
131        let entries = from_wasm(EXAMPLE_WASM).unwrap();
132        let rust = generate(&entries, "<file>", "<sha256>")
133            .to_formatted_string()
134            .unwrap();
135        assert_eq!(
136            rust,
137            r#"pub const WASM: &[u8] = soroban_sdk::contractfile!(file = "<file>", sha256 = "<sha256>");
138#[soroban_sdk::contractargs(name = "Args")]
139#[soroban_sdk::contractclient(name = "Client")]
140pub trait Contract {
141    fn add(env: soroban_sdk::Env, a: UdtEnum, b: UdtEnum) -> i64;
142}
143#[soroban_sdk::contracttype(export = false)]
144#[derive(Debug, Clone, Eq, PartialEq, Ord, PartialOrd)]
145pub struct UdtTuple(pub i64, pub soroban_sdk::Vec<i64>);
146#[soroban_sdk::contracttype(export = false)]
147#[derive(Debug, Clone, Eq, PartialEq, Ord, PartialOrd)]
148pub struct UdtStruct {
149    pub a: i64,
150    pub b: i64,
151    pub c: soroban_sdk::Vec<i64>,
152}
153#[soroban_sdk::contracttype(export = false)]
154#[derive(Debug, Clone, Eq, PartialEq, Ord, PartialOrd)]
155pub enum UdtEnum {
156    UdtA,
157    UdtB(UdtStruct),
158    UdtC(UdtEnum2),
159    UdtD(UdtTuple),
160}
161#[soroban_sdk::contracttype(export = false)]
162#[derive(Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd)]
163pub enum UdtEnum2 {
164    A = 10,
165    B = 15,
166}
167"#,
168        );
169    }
170}