l1x_sdk_macros/
lib.rs

1use proc_macro::TokenStream;
2use proc_macro2::Span;
3use proc_macro2::TokenStream as TokenStream2;
4use quote::quote;
5use syn::ItemImpl;
6use syn::Signature;
7use syn::Visibility;
8
9fn input_struct_deser(sig: &Signature) -> TokenStream2 {
10    let mut fields = TokenStream2::new();
11    for arg in &sig.inputs {
12        match arg {
13            syn::FnArg::Receiver(_) => todo!(),
14            syn::FnArg::Typed(typed) => {
15                let ident = &typed.pat;
16                let ty = &typed.ty;
17                fields.extend(quote! {
18                    #ident: #ty,
19                });
20            }
21        }
22    }
23    quote! {
24        #[derive(serde::Deserialize)]
25        struct Input {
26            #fields
27        }
28    }
29}
30
31/// Walks over public methods and generates wrappers for each method it finds.
32///
33/// The generated wrapper reads method arguments [`l1x_sdk::input`], deserializes them, and calls the original method.
34/// When the original method returns, the wrapper serializes the returned value and writes the serialized value with `l1x_sdk::output`
35///
36/// # Example
37/// ```
38/// use l1x_sdk_macros::contract;
39///
40/// struct Contract {};
41///
42/// #[contract]
43/// impl Contract {
44///     pub fn say(msg: String) {
45///         // say "hello"
46///     }
47/// }
48/// ```
49#[proc_macro_attribute]
50pub fn contract(_attr: TokenStream, item: TokenStream) -> TokenStream {
51    if let Ok(input) = syn::parse::<ItemImpl>(item) {
52        let struct_type = &input.self_ty;
53        let mut generated_code = TokenStream2::new();
54        for item in &input.items {
55            match item {
56                syn::ImplItem::Method(method) => {
57                    if !matches!(method.vis, Visibility::Public(_)) {
58                        continue;
59                    }
60                    let ident = &method.sig.ident;
61                    let arg_struct = input_struct_deser(&method.sig);
62                    let mut arg_list = TokenStream2::new();
63                    for arg in &method.sig.inputs {
64                        match arg {
65                            syn::FnArg::Receiver(_) => todo!(),
66                            syn::FnArg::Typed(typed) => {
67                                let ident = &typed.pat;
68                                arg_list.extend(quote! {
69                                    #ident,
70                                });
71                            }
72                        }
73                    }
74                    let ouput_serialization = match method.sig.output {
75                        syn::ReturnType::Default => quote! {},
76                        syn::ReturnType::Type(_, _) => quote! {
77                            let result = serde_json::to_vec(&result).expect("Failed to serialize the return value using JSON.");
78                            l1x_sdk::output(&result);
79                        },
80                    };
81                    generated_code.extend(quote! {
82                        #[cfg(target_arch = "wasm32")]
83                        #[no_mangle]
84                        pub extern "C" fn #ident() {
85                            let REENTRANCY_GUARD_KEY: &[u8] = b"__REENTRANCY_GUARD__";
86                            let REENTRANCY_GUARD: &[u8] = b"";
87                            l1x_sdk::setup_panic_hook();
88                            let write_perm = l1x_sdk::storage_write_perm();
89                            if write_perm {
90                                if l1x_sdk::storage_write(&REENTRANCY_GUARD_KEY, REENTRANCY_GUARD) {
91                                    panic!("Found a cross-contract call loop");
92                                }
93                            } else {
94                                if l1x_sdk::storage_read(&REENTRANCY_GUARD_KEY).is_some() {
95                                    panic!("Found a cross-contract call loop");
96                                }
97                            }
98                            #arg_struct
99                            let Input {
100                                #arg_list
101                            } = serde_json::from_slice(
102                                &l1x_sdk::input().expect("Expected input since method has arguments.")
103                            ).expect("Failed to deserialize input from JSON.");
104                            let result = #struct_type::#ident(#arg_list);
105                            #ouput_serialization
106                            if write_perm {
107                                l1x_sdk::storage_remove(&REENTRANCY_GUARD_KEY);
108                            }
109                        }
110                    })
111                }
112                _ => {
113                    return TokenStream::from(
114                        syn::Error::new(
115                            Span::call_site(),
116                            "#[contract] only supports methods for now.",
117                        )
118                        .to_compile_error(),
119                    )
120                }
121            }
122        }
123
124        TokenStream::from(quote! {
125            #input
126            #generated_code
127        })
128    } else {
129        TokenStream::from(
130            syn::Error::new(
131                Span::call_site(),
132                "#[contract] can only be used on impl sections.",
133            )
134            .to_compile_error(),
135        )
136    }
137}