soroban_sdk_macros/
lib.rs

1extern crate proc_macro;
2
3mod arbitrary;
4mod attribute;
5mod derive_args;
6mod derive_client;
7mod derive_contractimpl_trait_default_fns_not_overridden;
8mod derive_contractimpl_trait_macro;
9mod derive_enum;
10mod derive_enum_int;
11mod derive_error_enum_int;
12mod derive_event;
13mod derive_fn;
14mod derive_spec_fn;
15mod derive_struct;
16mod derive_struct_tuple;
17mod derive_trait;
18mod doc;
19mod map_type;
20mod path;
21mod symbol;
22mod syn_ext;
23
24use derive_args::{derive_args_impl, derive_args_type};
25use derive_client::{derive_client_impl, derive_client_type};
26use derive_contractimpl_trait_default_fns_not_overridden::derive_contractimpl_trait_default_fns_not_overridden;
27use derive_contractimpl_trait_macro::{
28    derive_contractimpl_trait_macro, generate_call_to_contractimpl_for_trait,
29};
30use derive_enum::derive_type_enum;
31use derive_enum_int::derive_type_enum_int;
32use derive_error_enum_int::derive_type_error_enum_int;
33use derive_event::derive_event;
34use derive_fn::{derive_contract_function_registration_ctor, derive_pub_fns};
35use derive_spec_fn::derive_fns_spec;
36use derive_struct::derive_type_struct;
37use derive_struct_tuple::derive_type_struct_tuple;
38use derive_trait::derive_trait;
39
40use darling::{ast::NestedMeta, FromMeta};
41use macro_string::MacroString;
42use proc_macro::TokenStream;
43use proc_macro2::{Literal, Span, TokenStream as TokenStream2};
44use quote::{format_ident, quote, ToTokens};
45use sha2::{Digest, Sha256};
46use std::{fmt::Write, fs};
47use syn::{
48    parse_macro_input, parse_str, spanned::Spanned, Data, DeriveInput, Error, Expr, Fields,
49    ItemImpl, ItemStruct, LitStr, Path, Type, Visibility,
50};
51use syn_ext::HasFnsItem;
52
53use soroban_spec_rust::{generate_from_wasm, GenerateFromFileError};
54
55use stellar_xdr::curr as stellar_xdr;
56use stellar_xdr::{Limits, ScMetaEntry, ScMetaV0, StringM, WriteXdr};
57
58pub(crate) const DEFAULT_XDR_RW_LIMITS: Limits = Limits {
59    depth: 500,
60    len: 0x1000000,
61};
62
63#[proc_macro]
64pub fn internal_symbol_short(input: TokenStream) -> TokenStream {
65    let input = parse_macro_input!(input as LitStr);
66    let crate_path: Path = syn::parse_str("crate").unwrap();
67    symbol::short(&crate_path, &input).into()
68}
69
70#[proc_macro]
71pub fn symbol_short(input: TokenStream) -> TokenStream {
72    let input = parse_macro_input!(input as LitStr);
73    let crate_path: Path = syn::parse_str("soroban_sdk").unwrap();
74    symbol::short(&crate_path, &input).into()
75}
76
77pub(crate) fn default_crate_path() -> Path {
78    parse_str("soroban_sdk").unwrap()
79}
80
81#[derive(Debug, FromMeta)]
82struct ContractSpecArgs {
83    name: Type,
84    export: Option<bool>,
85}
86
87#[proc_macro_attribute]
88pub fn contractspecfn(metadata: TokenStream, input: TokenStream) -> TokenStream {
89    let args = match NestedMeta::parse_meta_list(metadata.into()) {
90        Ok(v) => v,
91        Err(e) => {
92            return TokenStream::from(darling::Error::from(e).write_errors());
93        }
94    };
95    let args = match ContractSpecArgs::from_list(&args) {
96        Ok(v) => v,
97        Err(e) => return e.write_errors().into(),
98    };
99    let input2: TokenStream2 = input.clone().into();
100    let item = parse_macro_input!(input as HasFnsItem);
101    let methods: Vec<_> = item.fns();
102    let export = args.export.unwrap_or(true);
103
104    let derived = derive_fns_spec(&args.name, &methods, export);
105
106    match derived {
107        Ok(derived_ok) => quote! {
108            #input2
109            #derived_ok
110        }
111        .into(),
112        Err(derived_err) => quote! {
113            #input2
114            #derived_err
115        }
116        .into(),
117    }
118}
119
120#[derive(Debug, FromMeta)]
121struct ContractArgs {
122    #[darling(default = "default_crate_path")]
123    crate_path: Path,
124}
125
126#[proc_macro_attribute]
127pub fn contract(metadata: TokenStream, input: TokenStream) -> TokenStream {
128    let args = match NestedMeta::parse_meta_list(metadata.into()) {
129        Ok(v) => v,
130        Err(e) => {
131            return TokenStream::from(darling::Error::from(e).write_errors());
132        }
133    };
134    let args = match ContractArgs::from_list(&args) {
135        Ok(v) => v,
136        Err(e) => return e.write_errors().into(),
137    };
138
139    let input2: TokenStream2 = input.clone().into();
140
141    let item = parse_macro_input!(input as ItemStruct);
142
143    let ty = &item.ident;
144    let ty_str = quote!(#ty).to_string();
145
146    let client_ident = format!("{ty_str}Client");
147    let fn_set_registry_ident = format_ident!("__{}_fn_set_registry", ty_str.to_lowercase());
148    let crate_path = &args.crate_path;
149    let client = derive_client_type(&args.crate_path, &ty_str, &client_ident);
150    let args_ident = format!("{ty_str}Args");
151    let contract_args = derive_args_type(&ty_str, &args_ident);
152    let mut output = quote! {
153        #input2
154        #contract_args
155        #client
156    };
157    if cfg!(feature = "testutils") {
158        output.extend(quote! {
159            mod #fn_set_registry_ident {
160                use super::*;
161
162                extern crate std;
163                use std::sync::Mutex;
164                use std::collections::BTreeMap;
165
166                pub type F = #crate_path::testutils::ContractFunctionF;
167
168                static FUNCS: Mutex<BTreeMap<&'static str, &'static F>> = Mutex::new(BTreeMap::new());
169
170                pub fn register(name: &'static str, func: &'static F) {
171                    FUNCS.lock().unwrap().insert(name, func);
172                }
173
174                pub fn call(name: &str, env: #crate_path::Env, args: &[#crate_path::Val]) -> Option<#crate_path::Val> {
175                    let fopt: Option<&'static F> = FUNCS.lock().unwrap().get(name).map(|f| f.clone());
176                    fopt.map(|f| f(env, args))
177                }
178            }
179
180            impl #crate_path::testutils::ContractFunctionRegister for #ty {
181                fn register(name: &'static str, func: &'static #fn_set_registry_ident::F) {
182                    #fn_set_registry_ident::register(name, func);
183                }
184            }
185
186            #[doc(hidden)]
187            impl #crate_path::testutils::ContractFunctionSet for #ty {
188                fn call(&self, func: &str, env: #crate_path::Env, args: &[#crate_path::Val]) -> Option<#crate_path::Val> {
189                    #fn_set_registry_ident::call(func, env, args)
190                }
191            }
192        });
193    }
194    output.into()
195}
196
197#[derive(Debug, FromMeta)]
198struct ContractImplArgs {
199    #[darling(default = "default_crate_path")]
200    crate_path: Path,
201    #[darling(default)]
202    contracttrait: bool,
203}
204
205#[proc_macro_attribute]
206pub fn contractimpl(metadata: TokenStream, input: TokenStream) -> TokenStream {
207    let args = match NestedMeta::parse_meta_list(metadata.into()) {
208        Ok(v) => v,
209        Err(e) => {
210            return TokenStream::from(darling::Error::from(e).write_errors());
211        }
212    };
213    let args = match ContractImplArgs::from_list(&args) {
214        Ok(v) => v,
215        Err(e) => return e.write_errors().into(),
216    };
217    let crate_path = &args.crate_path;
218    let crate_path_str = quote!(#crate_path).to_string();
219
220    let imp = parse_macro_input!(input as ItemImpl);
221    let trait_ident = imp.trait_.as_ref().and_then(|x| x.1.get_ident());
222    let ty = &imp.self_ty;
223    let ty_str = quote!(#ty).to_string();
224
225    // TODO: Use imp.trait_ in generating the args ident, to create a unique
226    // args for each trait impl for a contract, to avoid conflicts.
227    let args_ident = if let Type::Path(path) = &**ty {
228        path.path
229            .segments
230            .last()
231            .map(|name| format!("{}Args", name.ident))
232    } else {
233        None
234    }
235    .unwrap_or_else(|| "Args".to_string());
236
237    // TODO: Use imp.trait_ in generating the client ident, to create a unique
238    // client for each trait impl for a contract, to avoid conflicts.
239    let client_ident = if let Type::Path(path) = &**ty {
240        path.path
241            .segments
242            .last()
243            .map(|name| format!("{}Client", name.ident))
244    } else {
245        None
246    }
247    .unwrap_or_else(|| "Client".to_string());
248
249    let pub_methods: Vec<_> = syn_ext::impl_pub_methods(&imp);
250    let pub_methods_fns: Vec<syn_ext::Fn> = pub_methods.iter().map(Into::into).collect();
251    let derived = derive_pub_fns(
252        crate_path,
253        &ty,
254        &pub_methods_fns,
255        trait_ident,
256        &client_ident,
257    );
258
259    match derived {
260        Ok(derived_ok) => {
261            let mut output = quote! {
262                #[#crate_path::contractargs(name = #args_ident, impl_only = true)]
263                #[#crate_path::contractclient(crate_path = #crate_path_str, name = #client_ident, impl_only = true)]
264                #[#crate_path::contractspecfn(name = #ty_str)]
265                #imp
266                #derived_ok
267            };
268            let contractimpl_for_trait =
269                trait_ident
270                    .filter(|_| args.contracttrait)
271                    .map(|trait_ident| {
272                        generate_call_to_contractimpl_for_trait(
273                            trait_ident.into(),
274                            ty,
275                            &pub_methods,
276                            &client_ident,
277                            &args_ident,
278                            &ty_str,
279                        )
280                    });
281            output.extend(quote! { #contractimpl_for_trait });
282            let cfs = derive_contract_function_registration_ctor(
283                crate_path,
284                ty,
285                trait_ident,
286                pub_methods.iter().map(|m| &m.sig.ident),
287            );
288            output.extend(quote! { #cfs });
289            output.into()
290        }
291        Err(derived_err) => quote! {
292            #imp
293            #derived_err
294        }
295        .into(),
296    }
297}
298
299#[proc_macro_attribute]
300pub fn contracttrait(metadata: TokenStream, input: TokenStream) -> TokenStream {
301    derive_trait(metadata.into(), input.into()).into()
302}
303
304#[proc_macro_attribute]
305pub fn contractimpl_trait_macro(metadata: TokenStream, input: TokenStream) -> TokenStream {
306    derive_contractimpl_trait_macro(metadata.into(), input.into()).into()
307}
308
309#[proc_macro]
310pub fn contractimpl_trait_default_fns_not_overridden(input: TokenStream) -> TokenStream {
311    derive_contractimpl_trait_default_fns_not_overridden(input.into()).into()
312}
313
314#[derive(Debug, FromMeta)]
315struct MetadataArgs {
316    key: String,
317    #[darling(with = darling::util::parse_expr::preserve_str_literal)]
318    val: Expr,
319}
320
321#[proc_macro]
322pub fn contractmeta(metadata: TokenStream) -> TokenStream {
323    let args = match NestedMeta::parse_meta_list(metadata.into()) {
324        Ok(v) => v,
325        Err(e) => {
326            return TokenStream::from(darling::Error::from(e).write_errors());
327        }
328    };
329    let args = match MetadataArgs::from_list(&args) {
330        Ok(v) => v,
331        Err(e) => return e.write_errors().into(),
332    };
333
334    let gen = {
335        let key: StringM = match args.key.clone().try_into() {
336            Ok(k) => k,
337            Err(e) => {
338                return Error::new(Span::call_site(), e.to_string())
339                    .into_compile_error()
340                    .into()
341            }
342        };
343
344        let val = args.val.to_token_stream().into();
345        let MacroString(val) = parse_macro_input!(val);
346        let val: StringM = match val.try_into() {
347            Ok(k) => k,
348            Err(e) => {
349                return Error::new(Span::call_site(), e.to_string())
350                    .into_compile_error()
351                    .into()
352            }
353        };
354
355        let meta_v0 = ScMetaV0 { key, val };
356        let meta_entry = ScMetaEntry::ScMetaV0(meta_v0);
357        let metadata_xdr: Vec<u8> = match meta_entry.to_xdr(DEFAULT_XDR_RW_LIMITS) {
358            Ok(v) => v,
359            Err(e) => {
360                return Error::new(Span::call_site(), e.to_string())
361                    .into_compile_error()
362                    .into()
363            }
364        };
365
366        let metadata_xdr_lit = proc_macro2::Literal::byte_string(metadata_xdr.as_slice());
367        let metadata_xdr_len = metadata_xdr.len();
368
369        let ident = format_ident!(
370            "__CONTRACT_KEY_{}",
371            args.key.as_bytes().iter().fold(String::new(), |mut s, b| {
372                let _ = write!(s, "{b:02x}");
373                s
374            })
375        );
376        quote! {
377            #[doc(hidden)]
378            #[cfg_attr(target_family = "wasm", link_section = "contractmetav0")]
379            static #ident: [u8; #metadata_xdr_len] = *#metadata_xdr_lit;
380        }
381    };
382
383    quote! {
384        #gen
385    }
386    .into()
387}
388
389#[proc_macro_attribute]
390pub fn contractevent(metadata: TokenStream, input: TokenStream) -> TokenStream {
391    derive_event(metadata.into(), input.into()).into()
392}
393
394#[derive(Debug, FromMeta)]
395struct ContractTypeArgs {
396    #[darling(default = "default_crate_path")]
397    crate_path: Path,
398    lib: Option<String>,
399    export: Option<bool>,
400}
401
402#[proc_macro_attribute]
403pub fn contracttype(metadata: TokenStream, input: TokenStream) -> TokenStream {
404    let args = match NestedMeta::parse_meta_list(metadata.into()) {
405        Ok(v) => v,
406        Err(e) => {
407            return TokenStream::from(darling::Error::from(e).write_errors());
408        }
409    };
410    let args = match ContractTypeArgs::from_list(&args) {
411        Ok(v) => v,
412        Err(e) => return e.write_errors().into(),
413    };
414    let input = parse_macro_input!(input as DeriveInput);
415    let vis = &input.vis;
416    let ident = &input.ident;
417    let attrs = &input.attrs;
418    // If the export argument has a value, do as it instructs regarding
419    // exporting. If it does not have a value, export if the type is pub.
420    let gen_spec = if let Some(export) = args.export {
421        export
422    } else {
423        matches!(input.vis, Visibility::Public(_))
424    };
425    let derived = match &input.data {
426        Data::Struct(s) => match s.fields {
427            Fields::Named(_) => {
428                derive_type_struct(&args.crate_path, vis, ident, attrs, s, gen_spec, &args.lib)
429            }
430            Fields::Unnamed(_) => derive_type_struct_tuple(
431                &args.crate_path,
432                vis,
433                ident,
434                attrs,
435                s,
436                gen_spec,
437                &args.lib,
438            ),
439            Fields::Unit => Error::new(
440                s.fields.span(),
441                "unit structs are not supported as contract types",
442            )
443            .to_compile_error(),
444        },
445        Data::Enum(e) => {
446            let count_of_variants = e.variants.len();
447            let count_of_int_variants = e
448                .variants
449                .iter()
450                .filter(|v| v.discriminant.is_some())
451                .count();
452            if count_of_int_variants == 0 {
453                derive_type_enum(&args.crate_path, vis, ident, attrs, e, gen_spec, &args.lib)
454            } else if count_of_int_variants == count_of_variants {
455                derive_type_enum_int(&args.crate_path, vis, ident, attrs, e, gen_spec, &args.lib)
456            } else {
457                Error::new(input.span(), "enums are supported as contract types only when all variants have an explicit integer literal, or when all variants are unit or single field")
458                    .to_compile_error()
459            }
460        }
461        Data::Union(u) => Error::new(
462            u.union_token.span(),
463            "unions are unsupported as contract types",
464        )
465        .to_compile_error(),
466    };
467    quote! {
468        #input
469        #derived
470    }
471    .into()
472}
473
474#[proc_macro_attribute]
475pub fn contracterror(metadata: TokenStream, input: TokenStream) -> TokenStream {
476    let args = match NestedMeta::parse_meta_list(metadata.into()) {
477        Ok(v) => v,
478        Err(e) => {
479            return TokenStream::from(darling::Error::from(e).write_errors());
480        }
481    };
482    let args = match ContractTypeArgs::from_list(&args) {
483        Ok(v) => v,
484        Err(e) => return e.write_errors().into(),
485    };
486    let input = parse_macro_input!(input as DeriveInput);
487    let ident = &input.ident;
488    let attrs = &input.attrs;
489    // If the export argument has a value, do as it instructs regarding
490    // exporting. If it does not have a value, export if the type is pub.
491    let gen_spec = if let Some(export) = args.export {
492        export
493    } else {
494        matches!(input.vis, Visibility::Public(_))
495    };
496    let derived = match &input.data {
497        Data::Enum(e) => {
498            if e.variants.iter().all(|v| v.discriminant.is_some()) {
499                derive_type_error_enum_int(&args.crate_path, ident, attrs, e, gen_spec, &args.lib)
500            } else {
501                Error::new(input.span(), "enums are supported as contract errors only when all variants have an explicit integer literal")
502                    .to_compile_error()
503            }
504        }
505        Data::Struct(s) => Error::new(
506            s.struct_token.span(),
507            "structs are unsupported as contract errors",
508        )
509        .to_compile_error(),
510        Data::Union(u) => Error::new(
511            u.union_token.span(),
512            "unions are unsupported as contract errors",
513        )
514        .to_compile_error(),
515    };
516    quote! {
517        #input
518        #derived
519    }
520    .into()
521}
522
523#[derive(Debug, FromMeta)]
524struct ContractFileArgs {
525    file: String,
526    sha256: darling::util::SpannedValue<String>,
527}
528
529#[proc_macro]
530pub fn contractfile(metadata: TokenStream) -> TokenStream {
531    let args = match NestedMeta::parse_meta_list(metadata.into()) {
532        Ok(v) => v,
533        Err(e) => {
534            return TokenStream::from(darling::Error::from(e).write_errors());
535        }
536    };
537    let args = match ContractFileArgs::from_list(&args) {
538        Ok(v) => v,
539        Err(e) => return e.write_errors().into(),
540    };
541
542    // Read WASM from file.
543    let file_abs = path::abs_from_rel_to_manifest(&args.file);
544    let wasm = match fs::read(file_abs) {
545        Ok(wasm) => wasm,
546        Err(e) => {
547            return Error::new(Span::call_site(), e.to_string())
548                .into_compile_error()
549                .into()
550        }
551    };
552
553    // Verify SHA256 hash.
554    let sha256 = Sha256::digest(&wasm);
555    let sha256 = format!("{:x}", sha256);
556    if *args.sha256 != sha256 {
557        return Error::new(
558            args.sha256.span(),
559            format!("sha256 does not match, expected: {}", sha256),
560        )
561        .into_compile_error()
562        .into();
563    }
564
565    // Render bytes.
566    let contents_lit = Literal::byte_string(&wasm);
567    quote! { #contents_lit }.into()
568}
569
570#[derive(Debug, FromMeta)]
571struct ContractArgsArgs {
572    name: String,
573    #[darling(default)]
574    impl_only: bool,
575}
576
577#[proc_macro_attribute]
578pub fn contractargs(metadata: TokenStream, input: TokenStream) -> TokenStream {
579    let args = match NestedMeta::parse_meta_list(metadata.into()) {
580        Ok(v) => v,
581        Err(e) => {
582            return TokenStream::from(darling::Error::from(e).write_errors());
583        }
584    };
585    let args = match ContractArgsArgs::from_list(&args) {
586        Ok(v) => v,
587        Err(e) => return e.write_errors().into(),
588    };
589    let input2: TokenStream2 = input.clone().into();
590    let item = parse_macro_input!(input as HasFnsItem);
591    let methods: Vec<_> = item.fns();
592    let args_type = (!args.impl_only).then(|| derive_args_type(&item.name(), &args.name));
593    let args_impl = derive_args_impl(&args.name, &methods);
594    quote! {
595        #input2
596        #args_type
597        #args_impl
598    }
599    .into()
600}
601
602#[derive(Debug, FromMeta)]
603struct ContractClientArgs {
604    #[darling(default = "default_crate_path")]
605    crate_path: Path,
606    name: String,
607    #[darling(default)]
608    impl_only: bool,
609}
610
611#[proc_macro_attribute]
612pub fn contractclient(metadata: TokenStream, input: TokenStream) -> TokenStream {
613    let args = match NestedMeta::parse_meta_list(metadata.into()) {
614        Ok(v) => v,
615        Err(e) => {
616            return TokenStream::from(darling::Error::from(e).write_errors());
617        }
618    };
619    let args = match ContractClientArgs::from_list(&args) {
620        Ok(v) => v,
621        Err(e) => return e.write_errors().into(),
622    };
623    let input2: TokenStream2 = input.clone().into();
624    let item = parse_macro_input!(input as HasFnsItem);
625    let methods: Vec<_> = item.fns();
626    let client_type =
627        (!args.impl_only).then(|| derive_client_type(&args.crate_path, &item.name(), &args.name));
628    let client_impl = derive_client_impl(&args.crate_path, &args.name, &methods);
629    quote! {
630        #input2
631        #client_type
632        #client_impl
633    }
634    .into()
635}
636
637#[derive(Debug, FromMeta)]
638struct ContractImportArgs {
639    file: String,
640    #[darling(default)]
641    sha256: darling::util::SpannedValue<Option<String>>,
642}
643#[proc_macro]
644pub fn contractimport(metadata: TokenStream) -> TokenStream {
645    let args = match NestedMeta::parse_meta_list(metadata.into()) {
646        Ok(v) => v,
647        Err(e) => {
648            return TokenStream::from(darling::Error::from(e).write_errors());
649        }
650    };
651    let args = match ContractImportArgs::from_list(&args) {
652        Ok(v) => v,
653        Err(e) => return e.write_errors().into(),
654    };
655
656    // Read WASM from file.
657    let file_abs = path::abs_from_rel_to_manifest(&args.file);
658    let wasm = match fs::read(file_abs) {
659        Ok(wasm) => wasm,
660        Err(e) => {
661            return Error::new(Span::call_site(), e.to_string())
662                .into_compile_error()
663                .into()
664        }
665    };
666
667    // Generate.
668    match generate_from_wasm(&wasm, &args.file, args.sha256.as_deref()) {
669        Ok(code) => quote! { #code },
670        Err(e @ GenerateFromFileError::VerifySha256 { .. }) => {
671            Error::new(args.sha256.span(), e.to_string()).into_compile_error()
672        }
673        Err(e) => Error::new(Span::call_site(), e.to_string()).into_compile_error(),
674    }
675    .into()
676}