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