alloy_sol_macro_expander/expand/
event.rs

1//! [`ItemEvent`] expansion.
2
3use super::{anon_name, expand_event_tokenize, expand_tuple_types, ExpCtxt};
4use alloy_sol_macro_input::{mk_doc, ContainsSolAttrs};
5use ast::{EventParameter, ItemEvent, SolIdent, Spanned};
6use proc_macro2::TokenStream;
7use quote::{quote, quote_spanned};
8use syn::Result;
9
10/// Expands an [`ItemEvent`]:
11///
12/// ```ignore (pseudo-code)
13/// pub struct #name {
14///     #(pub #parameter_name: #parameter_type,)*
15/// }
16///
17/// impl SolEvent for #name {
18///     ...
19/// }
20/// ```
21pub(super) fn expand(cx: &ExpCtxt<'_>, event: &ItemEvent) -> Result<TokenStream> {
22    let params = event.params();
23
24    let (sol_attrs, mut attrs) = event.split_attrs()?;
25    cx.derives(&mut attrs, &params, true);
26    let docs = sol_attrs.docs.or(cx.attrs.docs).unwrap_or(true);
27    let abi = sol_attrs.abi.or(cx.attrs.abi).unwrap_or(false);
28
29    cx.assert_resolved(&params)?;
30    event.assert_valid()?;
31
32    let name = cx.overloaded_name(event.into());
33    let signature = cx.event_signature(event);
34    let selector = crate::utils::event_selector(&signature);
35    let anonymous = event.is_anonymous();
36
37    // prepend the first topic if not anonymous
38    let first_topic = (!anonymous).then(|| quote!(alloy_sol_types::sol_data::FixedBytes<32>));
39    let topic_list = event.indexed_params().map(|p| expand_event_topic_type(p, cx));
40    let topic_list = first_topic.into_iter().chain(topic_list);
41
42    let (data_tuple, _) = expand_tuple_types(event.non_indexed_params().map(|p| &p.ty), cx);
43
44    // skip first topic if not anonymous, which is the hash of the signature
45    let mut topic_i = !anonymous as usize;
46    let mut data_i = 0usize;
47    let new_impl = event.parameters.iter().enumerate().map(|(i, p)| {
48        let name = anon_name((i, p.name.as_ref()));
49        let param;
50        if p.is_indexed() {
51            let i = syn::Index::from(topic_i);
52            param = quote!(topics.#i);
53            topic_i += 1;
54        } else {
55            let i = syn::Index::from(data_i);
56            param = quote!(data.#i);
57            data_i += 1;
58        }
59        quote!(#name: #param)
60    });
61
62    // NOTE: We need to enumerate before filtering.
63    let topic_tuple_names = event
64        .parameters
65        .iter()
66        .enumerate()
67        .filter(|(_, p)| p.is_indexed())
68        .map(|(i, p)| (i, p.name.as_ref()))
69        .map(anon_name);
70
71    let topics_impl = if anonymous {
72        quote! {(#(self.#topic_tuple_names.clone(),)*)}
73    } else {
74        quote! {(Self::SIGNATURE_HASH.into(), #(self.#topic_tuple_names.clone(),)*)}
75    };
76
77    let encode_first_topic =
78        (!anonymous).then(|| quote!(alloy_sol_types::abi::token::WordToken(Self::SIGNATURE_HASH)));
79
80    let encode_topics_impl = event.parameters.iter().enumerate().filter(|(_, p)| p.is_indexed()).map(|(i, p)| {
81        let name = anon_name((i, p.name.as_ref()));
82        let ty = cx.expand_type(&p.ty);
83
84        if cx.indexed_as_hash(p) {
85            quote! {
86                <alloy_sol_types::sol_data::FixedBytes<32> as alloy_sol_types::EventTopic>::encode_topic(&self.#name)
87            }
88        } else {
89            quote! {
90                <#ty as alloy_sol_types::EventTopic>::encode_topic(&self.#name)
91            }
92        }
93    });
94
95    let fields = event
96        .parameters
97        .iter()
98        .enumerate()
99        .map(|(i, p)| expand_event_topic_field(i, p, p.name.as_ref(), cx));
100
101    let check_signature = (!anonymous).then(|| {
102        quote! {
103            #[inline]
104            fn check_signature(topics: &<Self::TopicList as alloy_sol_types::SolType>::RustType) -> alloy_sol_types::Result<()> {
105                if topics.0 != Self::SIGNATURE_HASH {
106                    return Err(alloy_sol_types::Error::invalid_event_signature_hash(Self::SIGNATURE, topics.0, Self::SIGNATURE_HASH));
107                }
108                Ok(())
109            }
110        }
111    });
112
113    let tokenize_body_impl = expand_event_tokenize(&event.parameters, cx);
114
115    let encode_topics_impl = encode_first_topic
116        .into_iter()
117        .chain(encode_topics_impl)
118        .enumerate()
119        .map(|(i, assign)| quote!(out[#i] = #assign;));
120
121    let doc = docs.then(|| {
122        let selector = hex::encode_prefixed(selector.array.as_slice());
123        mk_doc(format!(
124            "Event with signature `{signature}` and selector `{selector}`.\n\
125            ```solidity\n{event}\n```"
126        ))
127    });
128
129    let abi: Option<TokenStream> = abi.then(|| {
130        if_json! {
131            let event = super::to_abi::generate(event, cx);
132            quote! {
133                #[automatically_derived]
134                impl alloy_sol_types::JsonAbiExt for #name {
135                    type Abi = alloy_sol_types::private::alloy_json_abi::Event;
136
137                    #[inline]
138                    fn abi() -> Self::Abi {
139                        #event
140                    }
141                }
142            }
143        }
144    });
145
146    let alloy_sol_types = &cx.crates.sol_types;
147
148    let tokens = quote! {
149        #(#attrs)*
150        #doc
151        #[allow(non_camel_case_types, non_snake_case, clippy::pub_underscore_fields, clippy::style)]
152        #[derive(Clone)]
153        pub struct #name {
154            #( #fields, )*
155        }
156
157        #[allow(non_camel_case_types, non_snake_case, clippy::pub_underscore_fields, clippy::style)]
158        const _: () = {
159            use #alloy_sol_types as alloy_sol_types;
160
161            #[automatically_derived]
162            impl alloy_sol_types::SolEvent for #name {
163                type DataTuple<'a> = #data_tuple;
164                type DataToken<'a> = <Self::DataTuple<'a> as alloy_sol_types::SolType>::Token<'a>;
165
166                type TopicList = (#(#topic_list,)*);
167
168                const SIGNATURE: &'static str = #signature;
169                const SIGNATURE_HASH: alloy_sol_types::private::B256 =
170                    alloy_sol_types::private::B256::new(#selector);
171
172                const ANONYMOUS: bool = #anonymous;
173
174                #[allow(unused_variables)]
175                #[inline]
176                fn new(
177                    topics: <Self::TopicList as alloy_sol_types::SolType>::RustType,
178                    data: <Self::DataTuple<'_> as alloy_sol_types::SolType>::RustType,
179                ) -> Self {
180                    Self {
181                        #(#new_impl,)*
182                    }
183                }
184
185                #check_signature
186
187                #[inline]
188                fn tokenize_body(&self) -> Self::DataToken<'_> {
189                    #tokenize_body_impl
190                }
191
192                #[inline]
193                fn topics(&self) -> <Self::TopicList as alloy_sol_types::SolType>::RustType {
194                    #topics_impl
195                }
196
197                #[inline]
198                fn encode_topics_raw(
199                    &self,
200                    out: &mut [alloy_sol_types::abi::token::WordToken],
201                ) -> alloy_sol_types::Result<()> {
202                    if out.len() < <Self::TopicList as alloy_sol_types::TopicList>::COUNT {
203                        return Err(alloy_sol_types::Error::Overrun);
204                    }
205                    #(#encode_topics_impl)*
206                    Ok(())
207                }
208            }
209
210            #[automatically_derived]
211            impl alloy_sol_types::private::IntoLogData for #name {
212                fn to_log_data(&self) -> alloy_sol_types::private::LogData {
213                    From::from(self)
214                }
215
216                fn into_log_data(self) -> alloy_sol_types::private::LogData {
217                    From::from(&self)
218                }
219            }
220
221
222            #[automatically_derived]
223            impl From<&#name> for alloy_sol_types::private::LogData {
224                #[inline]
225                fn from(this: &#name) -> alloy_sol_types::private::LogData {
226                    alloy_sol_types::SolEvent::encode_log_data(this)
227                }
228            }
229
230            #abi
231        };
232    };
233    Ok(tokens)
234}
235
236fn expand_event_topic_type(param: &EventParameter, cx: &ExpCtxt<'_>) -> TokenStream {
237    let alloy_sol_types = &cx.crates.sol_types;
238    assert!(param.is_indexed());
239    if cx.indexed_as_hash(param) {
240        quote_spanned! {param.ty.span()=> #alloy_sol_types::sol_data::FixedBytes<32> }
241    } else {
242        cx.expand_type(&param.ty)
243    }
244}
245
246fn expand_event_topic_field(
247    i: usize,
248    param: &EventParameter,
249    name: Option<&SolIdent>,
250    cx: &ExpCtxt<'_>,
251) -> TokenStream {
252    let name = anon_name((i, name));
253    let ty = if cx.indexed_as_hash(param) {
254        let bytes32 = ast::Type::FixedBytes(name.span(), core::num::NonZeroU16::new(32).unwrap());
255        cx.expand_rust_type(&bytes32)
256    } else {
257        cx.expand_rust_type(&param.ty)
258    };
259    let attrs = &param.attrs;
260    quote! {
261        #(#attrs)*
262        #[allow(missing_docs)]
263        pub #name: #ty
264    }
265}