Skip to main content

alloy_sol_macro_expander/expand/
event.rs

1//! [`ItemEvent`] expansion.
2
3use super::{ExpCtxt, anon_name, expand_event_tokenize, expand_tuple_types};
4use alloy_sol_macro_input::{ContainsSolAttrs, mk_doc};
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(
114        &event.parameters,
115        cx,
116        event.parameters.len(),
117        super::FieldKind::Original,
118    );
119
120    let encode_topics_impl = encode_first_topic
121        .into_iter()
122        .chain(encode_topics_impl)
123        .enumerate()
124        .map(|(i, assign)| quote!(out[#i] = #assign;));
125
126    let doc = docs.then(|| {
127        let selector = hex::encode_prefixed(selector.array.as_slice());
128        mk_doc(format!(
129            "Event with signature `{signature}` and selector `{selector}`.\n\
130            ```solidity\n{event}\n```"
131        ))
132    });
133
134    let abi: Option<TokenStream> = abi.then(|| {
135        if_json! {
136            let event = super::to_abi::generate(event, cx);
137            quote! {
138                #[automatically_derived]
139                impl alloy_sol_types::JsonAbiExt for #name {
140                    type Abi = alloy_sol_types::private::alloy_json_abi::Event;
141
142                    fn abi() -> Self::Abi {
143                        #event
144                    }
145                }
146            }
147        }
148    });
149
150    let alloy_sol_types = &cx.crates.sol_types;
151
152    let event_struct = if event.parameters.is_empty() {
153        quote! {
154            pub struct #name;
155        }
156    } else {
157        quote! {
158            pub struct #name {
159                #(#fields,)*
160            }
161        }
162    };
163
164    let tokens = quote! {
165        #(#attrs)*
166        #doc
167        #[allow(non_camel_case_types, non_snake_case, clippy::pub_underscore_fields, clippy::style)]
168        #[derive(Clone)]
169        #event_struct
170
171        #[allow(non_camel_case_types, non_snake_case, clippy::pub_underscore_fields, clippy::style)]
172        const _: () = {
173            use #alloy_sol_types as alloy_sol_types;
174
175            #[automatically_derived]
176            impl alloy_sol_types::SolEvent for #name {
177                type DataTuple<'a> = #data_tuple;
178                type DataToken<'a> = <Self::DataTuple<'a> as alloy_sol_types::SolType>::Token<'a>;
179
180                type TopicList = (#(#topic_list,)*);
181
182                const SIGNATURE: &'static str = #signature;
183                const SIGNATURE_HASH: alloy_sol_types::private::B256 =
184                    alloy_sol_types::private::B256::new(#selector);
185
186                const ANONYMOUS: bool = #anonymous;
187
188                #[allow(unused_variables)]
189                #[inline]
190                fn new(
191                    topics: <Self::TopicList as alloy_sol_types::SolType>::RustType,
192                    data: <Self::DataTuple<'_> as alloy_sol_types::SolType>::RustType,
193                ) -> Self {
194                    Self {
195                        #(#new_impl,)*
196                    }
197                }
198
199                #check_signature
200
201                #[inline]
202                fn tokenize_body(&self) -> Self::DataToken<'_> {
203                    #tokenize_body_impl
204                }
205
206                #[inline]
207                fn topics(&self) -> <Self::TopicList as alloy_sol_types::SolType>::RustType {
208                    #topics_impl
209                }
210
211                #[inline]
212                fn encode_topics_raw(
213                    &self,
214                    out: &mut [alloy_sol_types::abi::token::WordToken],
215                ) -> alloy_sol_types::Result<()> {
216                    if out.len() < <Self::TopicList as alloy_sol_types::TopicList>::COUNT {
217                        return Err(alloy_sol_types::Error::Overrun);
218                    }
219                    #(#encode_topics_impl)*
220                    Ok(())
221                }
222            }
223
224            #[automatically_derived]
225            impl alloy_sol_types::private::IntoLogData for #name {
226                fn to_log_data(&self) -> alloy_sol_types::private::LogData {
227                    From::from(self)
228                }
229
230                fn into_log_data(self) -> alloy_sol_types::private::LogData {
231                    From::from(&self)
232                }
233            }
234
235
236            #[automatically_derived]
237            impl From<&#name> for alloy_sol_types::private::LogData {
238                #[inline]
239                fn from(this: &#name) -> alloy_sol_types::private::LogData {
240                    alloy_sol_types::SolEvent::encode_log_data(this)
241                }
242            }
243
244            #abi
245        };
246    };
247    Ok(tokens)
248}
249
250fn expand_event_topic_type(param: &EventParameter, cx: &ExpCtxt<'_>) -> TokenStream {
251    let alloy_sol_types = &cx.crates.sol_types;
252    assert!(param.is_indexed());
253    if cx.indexed_as_hash(param) {
254        quote_spanned! {param.ty.span()=> #alloy_sol_types::sol_data::FixedBytes<32> }
255    } else {
256        cx.expand_type(&param.ty)
257    }
258}
259
260fn expand_event_topic_field(
261    i: usize,
262    param: &EventParameter,
263    name: Option<&SolIdent>,
264    cx: &ExpCtxt<'_>,
265) -> TokenStream {
266    let name = anon_name((i, name));
267    let ty = cx.expand_event_param_type(param);
268    let attrs = &param.attrs;
269    quote! {
270        #(#attrs)*
271        #[allow(missing_docs)]
272        pub #name: #ty
273    }
274}