soroban_sdk_macros/
lib.rs

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