Skip to main content

soroban_spec_rust/
lib.rs

1mod syn_ext;
2pub mod r#trait;
3pub mod types;
4
5use std::borrow::Cow;
6use std::{fs, io};
7
8use proc_macro2::TokenStream;
9use quote::quote;
10use sha2::{Digest, Sha256};
11use stellar_xdr::{ScSpecEntry, ScSpecTypeDef, ScSpecTypeUdt, ScSpecUdtUnionCaseV0};
12use syn::Error;
13
14use soroban_spec::read::{from_wasm, FromWasmError};
15
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};
20pub use types::{GenerateError, GenerateOptions};
21
22// IMPORTANT: The "docs" fields of spec entries are not output in Rust token
23// streams as rustdocs, because rustdocs can contain Rust code, and that code
24// will be executed. Generated code may be generated from untrusted Wasm
25// containing untrusted spec docs.
26
27#[derive(thiserror::Error, Debug)]
28pub enum GenerateFromFileError {
29    #[error("reading file: {0}")]
30    Io(io::Error),
31    #[error("sha256 does not match, expected: {expected}")]
32    VerifySha256 { expected: String },
33    #[error("parsing contract spec: {0}")]
34    Parse(stellar_xdr::Error),
35    #[error("getting contract spec: {0}")]
36    GetSpec(FromWasmError),
37    #[error("generating code: {0}")]
38    Generate(GenerateError),
39}
40
41pub fn generate_from_file(
42    file: &str,
43    verify_sha256: Option<&str>,
44) -> Result<TokenStream, GenerateFromFileError> {
45    // Read file.
46    let wasm = fs::read(file).map_err(GenerateFromFileError::Io)?;
47
48    // Generate code.
49    let code = generate_from_wasm(&wasm, file, verify_sha256)?;
50    Ok(code)
51}
52
53pub fn generate_from_wasm(
54    wasm: &[u8],
55    file: &str,
56    verify_sha256: Option<&str>,
57) -> Result<TokenStream, GenerateFromFileError> {
58    generate_from_wasm_with_options(wasm, file, verify_sha256, &GenerateOptions::default())
59}
60
61pub fn generate_from_wasm_with_options(
62    wasm: &[u8],
63    file: &str,
64    verify_sha256: Option<&str>,
65    opts: &GenerateOptions,
66) -> Result<TokenStream, GenerateFromFileError> {
67    let sha256 = Sha256::digest(wasm);
68    let sha256 = format!("{:x}", sha256);
69    if let Some(verify_sha256) = verify_sha256 {
70        if verify_sha256 != sha256 {
71            return Err(GenerateFromFileError::VerifySha256 { expected: sha256 });
72        }
73    }
74
75    let spec = from_wasm(wasm).map_err(GenerateFromFileError::GetSpec)?;
76    let code = generate_with_options(&spec, file, &sha256, opts)
77        .map_err(GenerateFromFileError::Generate)?;
78    Ok(code)
79}
80
81pub fn generate(
82    specs: &[ScSpecEntry],
83    file: &str,
84    sha256: &str,
85) -> Result<TokenStream, GenerateError> {
86    generate_with_options(specs, file, sha256, &GenerateOptions::default())
87}
88
89pub fn generate_with_options(
90    specs: &[ScSpecEntry],
91    file: &str,
92    sha256: &str,
93    opts: &GenerateOptions,
94) -> Result<TokenStream, GenerateError> {
95    let generated = generate_without_file_with_options(specs, opts)?;
96    Ok(quote! {
97        pub const WASM: &[u8] = soroban_sdk::contractfile!(file = #file, sha256 = #sha256);
98        #generated
99    })
100}
101
102pub fn generate_without_file(specs: &[ScSpecEntry]) -> Result<TokenStream, GenerateError> {
103    generate_without_file_with_options(specs, &GenerateOptions::default())
104}
105
106pub fn generate_without_file_with_options(
107    specs: &[ScSpecEntry],
108    opts: &GenerateOptions,
109) -> Result<TokenStream, GenerateError> {
110    let specs = apply_error_udt_override(specs);
111    let specs: &[ScSpecEntry] = &specs;
112
113    let mut spec_fns = Vec::new();
114    let mut spec_structs = Vec::new();
115    let mut spec_unions = Vec::new();
116    let mut spec_enums = Vec::new();
117    let mut spec_error_enums = Vec::new();
118    let mut spec_events = Vec::new();
119    for s in specs {
120        match s {
121            ScSpecEntry::FunctionV0(f) => spec_fns.push(f),
122            ScSpecEntry::UdtStructV0(s) => spec_structs.push(s),
123            ScSpecEntry::UdtUnionV0(u) => spec_unions.push(u),
124            ScSpecEntry::UdtEnumV0(e) => spec_enums.push(e),
125            ScSpecEntry::UdtErrorEnumV0(e) => spec_error_enums.push(e),
126            ScSpecEntry::EventV0(e) => spec_events.push(e),
127        }
128    }
129
130    let trait_name = "Contract";
131
132    let trait_ = r#trait::generate_trait(trait_name, &spec_fns)?;
133    let structs = spec_structs
134        .iter()
135        .map(|s| generate_struct_with_options(s, opts))
136        .collect::<Result<Vec<_>, _>>()?;
137    let unions = spec_unions
138        .iter()
139        .map(|s| generate_union_with_options(s, opts))
140        .collect::<Result<Vec<_>, _>>()?;
141    let enums = spec_enums
142        .iter()
143        .map(|s| generate_enum_with_options(s, opts))
144        .collect::<Result<Vec<_>, _>>()?;
145    let error_enums = spec_error_enums
146        .iter()
147        .map(|s| generate_error_enum_with_options(s, opts))
148        .collect::<Result<Vec<_>, _>>()?;
149    let events = spec_events
150        .iter()
151        .map(|s| generate_event_with_options(s, opts))
152        .collect::<Result<Vec<_>, _>>()?;
153
154    Ok(quote! {
155        #[soroban_sdk::contractargs(name = "Args")]
156        #[soroban_sdk::contractclient(name = "Client")]
157        #trait_
158
159        #(#structs)*
160        #(#unions)*
161        #(#enums)*
162        #(#error_enums)*
163        #(#events)*
164    })
165}
166
167/// The `#[contractimpl]` macro emits any type named `Error` in a contract's
168/// function signatures as the built-in `ScSpecTypeDef::Error` in the spec,
169/// regardless of whether the contract defined its own error enum named `Error`
170/// or used `soroban_sdk::Error` directly. To let clients of contracts that
171/// define their own `Error` enum see the user-defined type instead of
172/// `soroban_sdk::Error`, this pass rewrites every `ScSpecTypeDef::Error`
173/// reference in the spec to `Udt { name: "Error" }` whenever the spec also
174/// contains a `UdtErrorEnumV0` named `Error`.
175///
176/// This keeps the on-the-wire spec format unchanged (so already-deployed
177/// contracts benefit without redeployment) and shifts the resolution to the
178/// client generator.
179///
180/// Returns a borrowed slice when no rewrite is needed, otherwise a
181/// freshly-owned `Vec` with the rewrite applied.
182fn apply_error_udt_override(specs: &[ScSpecEntry]) -> Cow<'_, [ScSpecEntry]> {
183    let has_error_udt = specs.iter().any(|e| {
184        matches!(
185            e,
186            ScSpecEntry::UdtErrorEnumV0(err) if err.name.to_utf8_string_lossy() == "Error"
187        )
188    });
189    if has_error_udt {
190        let mut v = specs.to_vec();
191        rewrite_error_to_udt(&mut v);
192        Cow::Owned(v)
193    } else {
194        Cow::Borrowed(specs)
195    }
196}
197
198/// Rewrites every `ScSpecTypeDef::Error` reference in the given entries to
199/// `ScSpecTypeDef::Udt { name: "Error" }`. Called only when the spec contains
200/// a user-defined error enum named `Error`, so the UDT reference resolves to
201/// that enum during code generation.
202fn rewrite_error_to_udt(entries: &mut [ScSpecEntry]) {
203    fn rewrite_ty(t: &mut ScSpecTypeDef) {
204        match t {
205            ScSpecTypeDef::Error => {
206                *t = ScSpecTypeDef::Udt(ScSpecTypeUdt {
207                    name: "Error".try_into().unwrap(),
208                });
209            }
210            ScSpecTypeDef::Option(o) => rewrite_ty(&mut o.value_type),
211            ScSpecTypeDef::Result(r) => {
212                rewrite_ty(&mut r.ok_type);
213                rewrite_ty(&mut r.error_type);
214            }
215            ScSpecTypeDef::Vec(v) => rewrite_ty(&mut v.element_type),
216            ScSpecTypeDef::Map(m) => {
217                rewrite_ty(&mut m.key_type);
218                rewrite_ty(&mut m.value_type);
219            }
220            ScSpecTypeDef::Tuple(tu) => {
221                for vt in tu.value_types.iter_mut() {
222                    rewrite_ty(vt);
223                }
224            }
225            _ => {}
226        }
227    }
228    for entry in entries.iter_mut() {
229        match entry {
230            ScSpecEntry::FunctionV0(f) => {
231                for input in f.inputs.iter_mut() {
232                    rewrite_ty(&mut input.type_);
233                }
234                for output in f.outputs.iter_mut() {
235                    rewrite_ty(output);
236                }
237            }
238            ScSpecEntry::UdtStructV0(s) => {
239                for field in s.fields.iter_mut() {
240                    rewrite_ty(&mut field.type_);
241                }
242            }
243            ScSpecEntry::UdtUnionV0(u) => {
244                for case in u.cases.iter_mut() {
245                    if let ScSpecUdtUnionCaseV0::TupleV0(t) = case {
246                        for ty in t.type_.iter_mut() {
247                            rewrite_ty(ty);
248                        }
249                    }
250                }
251            }
252            ScSpecEntry::UdtEnumV0(_) | ScSpecEntry::UdtErrorEnumV0(_) => {}
253            ScSpecEntry::EventV0(e) => {
254                for p in e.params.iter_mut() {
255                    rewrite_ty(&mut p.type_);
256                }
257            }
258        }
259    }
260}
261
262/// Implemented by types that can be converted into pretty formatted Strings of
263/// Rust code.
264pub trait ToFormattedString {
265    /// Converts the value to a String that is pretty formatted. If there is any
266    /// error parsing the token stream the raw String version of the code is
267    /// returned instead.
268    fn to_formatted_string(&self) -> Result<String, Error>;
269}
270
271impl ToFormattedString for TokenStream {
272    fn to_formatted_string(&self) -> Result<String, Error> {
273        let file = syn::parse2(self.clone())?;
274        Ok(prettyplease::unparse(&file))
275    }
276}
277
278#[cfg(test)]
279mod test {
280    use pretty_assertions::assert_eq;
281
282    use super::{generate, ToFormattedString};
283    use soroban_spec::read::from_wasm;
284
285    const EXAMPLE_WASM: &[u8] = include_bytes!("../../target/wasm32v1-none/release/test_udt.wasm");
286
287    #[test]
288    fn example() {
289        let entries = from_wasm(EXAMPLE_WASM).unwrap();
290        let rust = generate(&entries, "<file>", "<sha256>")
291            .unwrap()
292            .to_formatted_string()
293            .unwrap();
294        assert_eq!(
295            rust,
296            r#"pub const WASM: &[u8] = soroban_sdk::contractfile!(file = "<file>", sha256 = "<sha256>");
297#[soroban_sdk::contractargs(name = "Args")]
298#[soroban_sdk::contractclient(name = "Client")]
299pub trait Contract {
300    fn add(env: soroban_sdk::Env, a: UdtEnum, b: UdtEnum) -> i64;
301    fn recursive(env: soroban_sdk::Env, a: UdtRecursive) -> Option<UdtRecursive>;
302    fn recursive_enum(
303        env: soroban_sdk::Env,
304        a: RecursiveEnum,
305        key: u32,
306    ) -> Result<Option<RecursiveEnum>, soroban_sdk::Error>;
307}
308#[soroban_sdk::contracttype(export = false)]
309#[derive(Debug, Clone, Eq, PartialEq, Ord, PartialOrd)]
310pub struct UdtTuple(pub i64, pub soroban_sdk::Vec<i64>);
311#[soroban_sdk::contracttype(export = false)]
312#[derive(Debug, Clone, Eq, PartialEq, Ord, PartialOrd)]
313pub struct UdtStruct {
314    pub a: i64,
315    pub b: i64,
316    pub c: soroban_sdk::Vec<i64>,
317}
318#[soroban_sdk::contracttype(export = false)]
319#[derive(Debug, Clone, Eq, PartialEq, Ord, PartialOrd)]
320pub struct UdtRecursive {
321    pub a: soroban_sdk::Symbol,
322    pub b: soroban_sdk::Vec<UdtRecursive>,
323}
324#[soroban_sdk::contracttype(export = false)]
325#[derive(Debug, Clone, Eq, PartialEq, Ord, PartialOrd)]
326pub struct RecursiveToEnum {
327    pub a: soroban_sdk::Symbol,
328    pub b: soroban_sdk::Map<u32, RecursiveEnum>,
329}
330#[soroban_sdk::contracttype(export = false)]
331#[derive(Debug, Clone, Eq, PartialEq, Ord, PartialOrd)]
332pub struct ContractContext {
333    pub args: soroban_sdk::Vec<soroban_sdk::Val>,
334    pub contract: soroban_sdk::Address,
335    pub fn_name: soroban_sdk::Symbol,
336}
337#[soroban_sdk::contracttype(export = false)]
338#[derive(Debug, Clone, Eq, PartialEq, Ord, PartialOrd)]
339pub struct SubContractInvocation {
340    pub context: ContractContext,
341    pub sub_invocations: soroban_sdk::Vec<InvokerContractAuthEntry>,
342}
343#[soroban_sdk::contracttype(export = false)]
344#[derive(Debug, Clone, Eq, PartialEq, Ord, PartialOrd)]
345pub struct CreateContractHostFnContext {
346    pub executable: ContractExecutable,
347    pub salt: soroban_sdk::BytesN<32>,
348}
349#[soroban_sdk::contracttype(export = false)]
350#[derive(Debug, Clone, Eq, PartialEq, Ord, PartialOrd)]
351pub struct CreateContractWithConstructorHostFnContext {
352    pub constructor_args: soroban_sdk::Vec<soroban_sdk::Val>,
353    pub executable: ContractExecutable,
354    pub salt: soroban_sdk::BytesN<32>,
355}
356#[soroban_sdk::contracttype(export = false)]
357#[derive(Debug, Clone, Eq, PartialEq, Ord, PartialOrd)]
358pub enum UdtEnum {
359    UdtA,
360    UdtB(UdtStruct),
361    UdtC(UdtEnum2),
362    UdtD(UdtTuple),
363}
364#[soroban_sdk::contracttype(export = false)]
365#[derive(Debug, Clone, Eq, PartialEq, Ord, PartialOrd)]
366pub enum RecursiveEnum {
367    NotRecursive,
368    Recursive(RecursiveToEnum),
369}
370#[soroban_sdk::contracttype(export = false)]
371#[derive(Debug, Clone, Eq, PartialEq, Ord, PartialOrd)]
372pub enum Context {
373    Contract(ContractContext),
374    CreateContractHostFn(CreateContractHostFnContext),
375    CreateContractWithCtorHostFn(CreateContractWithConstructorHostFnContext),
376}
377#[soroban_sdk::contracttype(export = false)]
378#[derive(Debug, Clone, Eq, PartialEq, Ord, PartialOrd)]
379pub enum ContractExecutable {
380    Wasm(soroban_sdk::BytesN<32>),
381}
382#[soroban_sdk::contracttype(export = false)]
383#[derive(Debug, Clone, Eq, PartialEq, Ord, PartialOrd)]
384pub enum InvokerContractAuthEntry {
385    Contract(SubContractInvocation),
386    CreateContractHostFn(CreateContractHostFnContext),
387    CreateContractWithCtorHostFn(CreateContractWithConstructorHostFnContext),
388}
389#[soroban_sdk::contracttype(export = false)]
390#[derive(Debug, Clone, Eq, PartialEq, Ord, PartialOrd)]
391pub enum Executable {
392    Wasm(soroban_sdk::BytesN<32>),
393    StellarAsset,
394    Account,
395}
396#[soroban_sdk::contracttype(export = false)]
397#[derive(Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd)]
398pub enum UdtEnum2 {
399    A = 10,
400    B = 15,
401}
402"#,
403        );
404    }
405
406    const ADD_U64_WASM: &[u8] =
407        include_bytes!("../../target/wasm32v1-none/release/test_add_u64.wasm");
408
409    /// Test that Result types with user-defined error types are generated correctly.
410    /// This specifically tests that:
411    /// - An error enum named `Error` generates `Result<u64, Error>` (not `Result<u64, soroban_sdk::Error>`)
412    /// - An error enum named `MyError` generates `Result<u64, MyError>`
413    #[test]
414    fn test_add_u64_result_types() {
415        let entries = from_wasm(ADD_U64_WASM).unwrap();
416        let rust = generate(&entries, "<file>", "<sha256>")
417            .unwrap()
418            .to_formatted_string()
419            .unwrap();
420        assert_eq!(
421            rust,
422            r#"pub const WASM: &[u8] = soroban_sdk::contractfile!(file = "<file>", sha256 = "<sha256>");
423#[soroban_sdk::contractargs(name = "Args")]
424#[soroban_sdk::contractclient(name = "Client")]
425pub trait Contract {
426    fn add(env: soroban_sdk::Env, a: u64, b: u64) -> u64;
427    fn safe_add(env: soroban_sdk::Env, a: u64, b: u64) -> Result<u64, Error>;
428    fn safe_add_two(env: soroban_sdk::Env, a: u64, b: u64) -> Result<u64, MyError>;
429}
430#[soroban_sdk::contracttype(export = false)]
431#[derive(Debug, Clone, Eq, PartialEq, Ord, PartialOrd)]
432pub struct ContractContext {
433    pub args: soroban_sdk::Vec<soroban_sdk::Val>,
434    pub contract: soroban_sdk::Address,
435    pub fn_name: soroban_sdk::Symbol,
436}
437#[soroban_sdk::contracttype(export = false)]
438#[derive(Debug, Clone, Eq, PartialEq, Ord, PartialOrd)]
439pub struct SubContractInvocation {
440    pub context: ContractContext,
441    pub sub_invocations: soroban_sdk::Vec<InvokerContractAuthEntry>,
442}
443#[soroban_sdk::contracttype(export = false)]
444#[derive(Debug, Clone, Eq, PartialEq, Ord, PartialOrd)]
445pub struct CreateContractHostFnContext {
446    pub executable: ContractExecutable,
447    pub salt: soroban_sdk::BytesN<32>,
448}
449#[soroban_sdk::contracttype(export = false)]
450#[derive(Debug, Clone, Eq, PartialEq, Ord, PartialOrd)]
451pub struct CreateContractWithConstructorHostFnContext {
452    pub constructor_args: soroban_sdk::Vec<soroban_sdk::Val>,
453    pub executable: ContractExecutable,
454    pub salt: soroban_sdk::BytesN<32>,
455}
456#[soroban_sdk::contracttype(export = false)]
457#[derive(Debug, Clone, Eq, PartialEq, Ord, PartialOrd)]
458pub enum Context {
459    Contract(ContractContext),
460    CreateContractHostFn(CreateContractHostFnContext),
461    CreateContractWithCtorHostFn(CreateContractWithConstructorHostFnContext),
462}
463#[soroban_sdk::contracttype(export = false)]
464#[derive(Debug, Clone, Eq, PartialEq, Ord, PartialOrd)]
465pub enum ContractExecutable {
466    Wasm(soroban_sdk::BytesN<32>),
467}
468#[soroban_sdk::contracttype(export = false)]
469#[derive(Debug, Clone, Eq, PartialEq, Ord, PartialOrd)]
470pub enum InvokerContractAuthEntry {
471    Contract(SubContractInvocation),
472    CreateContractHostFn(CreateContractHostFnContext),
473    CreateContractWithCtorHostFn(CreateContractWithConstructorHostFnContext),
474}
475#[soroban_sdk::contracttype(export = false)]
476#[derive(Debug, Clone, Eq, PartialEq, Ord, PartialOrd)]
477pub enum Executable {
478    Wasm(soroban_sdk::BytesN<32>),
479    StellarAsset,
480    Account,
481}
482#[soroban_sdk::contracterror(export = false)]
483#[derive(Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd)]
484pub enum Error {
485    Overflow = 1,
486}
487#[soroban_sdk::contracterror(export = false)]
488#[derive(Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd)]
489pub enum MyError {
490    Overflow = 1,
491}
492"#,
493        );
494    }
495
496    /// Test that shows the raw spec entries from the wasm.
497    /// Verifies that the on-the-wire spec format is unchanged: a contract
498    /// error enum named `Error` is still emitted as the built-in
499    /// `ScSpecTypeDef::Error` in function signatures (the user-defined-vs-SDK
500    /// disambiguation happens at client generation time, not here). A
501    /// differently-named error enum (`MyError`) is emitted as a UDT reference.
502    #[test]
503    fn test_add_u64_spec_entries() {
504        use super::ScSpecEntry;
505        use stellar_xdr::ScSpecTypeDef;
506
507        let entries = from_wasm(ADD_U64_WASM).unwrap();
508
509        // Find the safe_add function spec
510        let safe_add_fn = entries
511            .iter()
512            .find_map(|e| match e {
513                ScSpecEntry::FunctionV0(f) if f.name.to_utf8_string().unwrap() == "safe_add" => {
514                    Some(f)
515                }
516                _ => None,
517            })
518            .expect("safe_add function not found");
519
520        let output = safe_add_fn.outputs.to_option().expect("should have output");
521        let ScSpecTypeDef::Result(r) = output else {
522            panic!("output should be a Result type");
523        };
524        assert!(
525            matches!(r.ok_type.as_ref(), ScSpecTypeDef::U64),
526            "ok_type should be U64"
527        );
528        assert!(
529            matches!(r.error_type.as_ref(), ScSpecTypeDef::Error),
530            "error_type should be the built-in Error in the wasm spec, got {:?}",
531            r.error_type
532        );
533
534        // Find the safe_add_two function spec
535        let safe_add_two_fn = entries
536            .iter()
537            .find_map(|e| match e {
538                ScSpecEntry::FunctionV0(f)
539                    if f.name.to_utf8_string().unwrap() == "safe_add_two" =>
540                {
541                    Some(f)
542                }
543                _ => None,
544            })
545            .expect("safe_add_two function not found");
546
547        let output = safe_add_two_fn
548            .outputs
549            .to_option()
550            .expect("should have output");
551        let ScSpecTypeDef::Result(r) = output else {
552            panic!("output should be a Result type");
553        };
554        assert!(
555            matches!(r.ok_type.as_ref(), ScSpecTypeDef::U64),
556            "ok_type should be U64"
557        );
558        let ScSpecTypeDef::Udt(u) = r.error_type.as_ref() else {
559            panic!(
560                "error_type should be a UDT for MyError, got {:?}",
561                r.error_type
562            );
563        };
564        assert_eq!(
565            u.name.to_utf8_string().unwrap(),
566            "MyError",
567            "error_type should be MyError UDT"
568        );
569    }
570
571    /// When the spec references `ScSpecTypeDef::Error` and contains no error
572    /// enum named `Error`, the generator must leave it as `soroban_sdk::Error`.
573    /// This covers contracts that use `soroban_sdk::Error` directly as their
574    /// Result error type, including every contract compiled before the
575    /// error-enum override was introduced.
576    #[test]
577    fn test_missing_error_udt_falls_back_to_sdk_error() {
578        use super::ScSpecEntry;
579        use stellar_xdr::{ScSpecFunctionV0, ScSpecTypeDef, ScSpecTypeResult};
580
581        let func = ScSpecFunctionV0 {
582            doc: "".try_into().unwrap(),
583            name: "safe_add".try_into().unwrap(),
584            inputs: [].try_into().unwrap(),
585            outputs: [ScSpecTypeDef::Result(Box::new(ScSpecTypeResult {
586                ok_type: Box::new(ScSpecTypeDef::U64),
587                error_type: Box::new(ScSpecTypeDef::Error),
588            }))]
589            .try_into()
590            .unwrap(),
591        };
592        let entries = [ScSpecEntry::FunctionV0(func)];
593        let rust = generate(&entries, "<file>", "<sha256>")
594            .unwrap()
595            .to_formatted_string()
596            .unwrap();
597        assert_eq!(
598            rust,
599            r#"pub const WASM: &[u8] = soroban_sdk::contractfile!(file = "<file>", sha256 = "<sha256>");
600#[soroban_sdk::contractargs(name = "Args")]
601#[soroban_sdk::contractclient(name = "Client")]
602pub trait Contract {
603    fn safe_add(env: soroban_sdk::Env) -> Result<u64, soroban_sdk::Error>;
604}
605"#,
606        );
607    }
608
609    /// When the spec contains a user-defined `Error` error enum, every
610    /// `ScSpecTypeDef::Error` reference in the spec must be rewritten to
611    /// reference that UDT instead of `soroban_sdk::Error`.
612    #[test]
613    fn test_error_udt_overrides_sdk_error() {
614        use super::ScSpecEntry;
615        use stellar_xdr::{
616            ScSpecFunctionV0, ScSpecTypeDef, ScSpecTypeResult, ScSpecUdtErrorEnumCaseV0,
617            ScSpecUdtErrorEnumV0,
618        };
619
620        let func = ScSpecFunctionV0 {
621            doc: "".try_into().unwrap(),
622            name: "safe_add".try_into().unwrap(),
623            inputs: [].try_into().unwrap(),
624            outputs: [ScSpecTypeDef::Result(Box::new(ScSpecTypeResult {
625                ok_type: Box::new(ScSpecTypeDef::U64),
626                error_type: Box::new(ScSpecTypeDef::Error),
627            }))]
628            .try_into()
629            .unwrap(),
630        };
631        let error_enum = ScSpecUdtErrorEnumV0 {
632            doc: "".try_into().unwrap(),
633            lib: "".try_into().unwrap(),
634            name: "Error".try_into().unwrap(),
635            cases: [ScSpecUdtErrorEnumCaseV0 {
636                doc: "".try_into().unwrap(),
637                name: "Overflow".try_into().unwrap(),
638                value: 1,
639            }]
640            .try_into()
641            .unwrap(),
642        };
643        let entries = [
644            ScSpecEntry::FunctionV0(func),
645            ScSpecEntry::UdtErrorEnumV0(error_enum),
646        ];
647        let rust = generate(&entries, "<file>", "<sha256>")
648            .unwrap()
649            .to_formatted_string()
650            .unwrap();
651        assert_eq!(
652            rust,
653            r#"pub const WASM: &[u8] = soroban_sdk::contractfile!(file = "<file>", sha256 = "<sha256>");
654#[soroban_sdk::contractargs(name = "Args")]
655#[soroban_sdk::contractclient(name = "Client")]
656pub trait Contract {
657    fn safe_add(env: soroban_sdk::Env) -> Result<u64, Error>;
658}
659#[soroban_sdk::contracterror(export = false)]
660#[derive(Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd)]
661pub enum Error {
662    Overflow = 1,
663}
664"#,
665        );
666    }
667
668    /// When the `Error` override applies, nested `ScSpecTypeDef::Error`
669    /// references must be rewritten too.
670    #[test]
671    fn test_error_udt_override_rewrites_nested_vec() {
672        use super::ScSpecEntry;
673        use stellar_xdr::{
674            ScSpecFunctionV0, ScSpecTypeDef, ScSpecTypeVec, ScSpecUdtErrorEnumCaseV0,
675            ScSpecUdtErrorEnumV0,
676        };
677
678        let func = ScSpecFunctionV0 {
679            doc: "".try_into().unwrap(),
680            name: "errors".try_into().unwrap(),
681            inputs: [].try_into().unwrap(),
682            outputs: [ScSpecTypeDef::Vec(Box::new(ScSpecTypeVec {
683                element_type: Box::new(ScSpecTypeDef::Error),
684            }))]
685            .try_into()
686            .unwrap(),
687        };
688        let error_enum = ScSpecUdtErrorEnumV0 {
689            doc: "".try_into().unwrap(),
690            lib: "".try_into().unwrap(),
691            name: "Error".try_into().unwrap(),
692            cases: [ScSpecUdtErrorEnumCaseV0 {
693                doc: "".try_into().unwrap(),
694                name: "Overflow".try_into().unwrap(),
695                value: 1,
696            }]
697            .try_into()
698            .unwrap(),
699        };
700        let entries = [
701            ScSpecEntry::FunctionV0(func),
702            ScSpecEntry::UdtErrorEnumV0(error_enum),
703        ];
704        let rust = generate(&entries, "<file>", "<sha256>")
705            .unwrap()
706            .to_formatted_string()
707            .unwrap();
708        assert_eq!(
709            rust,
710            r#"pub const WASM: &[u8] = soroban_sdk::contractfile!(file = "<file>", sha256 = "<sha256>");
711#[soroban_sdk::contractargs(name = "Args")]
712#[soroban_sdk::contractclient(name = "Client")]
713pub trait Contract {
714    fn errors(env: soroban_sdk::Env) -> soroban_sdk::Vec<Error>;
715}
716#[soroban_sdk::contracterror(export = false)]
717#[derive(Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd)]
718pub enum Error {
719    Overflow = 1,
720}
721"#,
722        );
723    }
724}