ink_macro/event/
mod.rs

1// Copyright (C) Use Ink (UK) Ltd.
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15mod metadata;
16
17pub use metadata::event_metadata_derive;
18
19use ink_codegen::generate_code;
20use ink_ir::EventConfig;
21use ink_primitives::abi::Abi;
22use proc_macro2::TokenStream as TokenStream2;
23use quote::{
24    quote,
25    quote_spanned,
26};
27use syn::{
28    Token,
29    punctuated::Punctuated,
30    spanned::Spanned,
31};
32
33/// Generate code from the `#[ink::event]` attribute. This expands to the required
34/// derive macros to satisfy an event implementation.
35pub fn generate(config: TokenStream2, input: TokenStream2) -> TokenStream2 {
36    ink_ir::Event::new(config, input)
37        .map(|event| generate_code(&event))
38        .unwrap_or_else(|err| err.to_compile_error())
39}
40
41/// Derives the `ink::Event` trait for the given `struct`.
42pub fn event_derive(mut s: synstructure::Structure) -> TokenStream2 {
43    s.bind_with(|_| synstructure::BindStyle::Move)
44        .add_bounds(synstructure::AddBounds::Fields)
45        .underscore_const(true);
46    match &s.ast().data {
47        syn::Data::Struct(_) => {
48            event_derive_struct(s).unwrap_or_else(|err| err.to_compile_error())
49        }
50        _ => {
51            syn::Error::new(
52                s.ast().span(),
53                "can only derive `Event` for Rust `struct` items",
54            )
55            .to_compile_error()
56        }
57    }
58}
59
60/// `Event` derive implementation for `struct` types.
61#[allow(clippy::arithmetic_side_effects)] // todo
62fn event_derive_struct(s: synstructure::Structure) -> syn::Result<TokenStream2> {
63    assert_eq!(s.variants().len(), 1, "can only operate on structs");
64
65    if !s.ast().generics.params.is_empty() {
66        return Err(syn::Error::new(
67            s.ast().generics.params.span(),
68            "can only derive `Event` for structs without generics",
69        ));
70    }
71
72    let span = s.ast().span();
73    let config = EventConfig::try_from(s.ast().attrs.as_slice())?;
74    let anonymous = config.anonymous();
75    let variant = &s.variants()[0];
76
77    // Partition field bindings between topic and data fields.
78    let mut topic_fields = Vec::new();
79    let mut data_fields = Vec::new();
80    let mut topic_err: Option<syn::Error> = None;
81    for field in variant.bindings() {
82        match has_ink_topic_attribute(field) {
83            Ok(is_topic) => {
84                if is_topic {
85                    topic_fields.push(field);
86                } else {
87                    data_fields.push(field);
88                }
89            }
90            Err(err) => {
91                match topic_err {
92                    Some(ref mut topic_err) => topic_err.combine(err),
93                    None => topic_err = Some(err),
94                }
95            }
96        }
97    }
98    if let Some(err) = topic_err {
99        return Err(err);
100    }
101
102    // Anonymous events require 1 fewer topics since they do not include their signature.
103    let anonymous_topics_offset = usize::from(!anonymous);
104    let len_topics = topic_fields.len() + anonymous_topics_offset;
105
106    // Enforces `pallet-revive` and Solidity ABI topic limits.
107    // Ref: <https://github.com/paritytech/polkadot-sdk/blob/7ede4fd048f8a99e62ef31050aa2e167e99d54b9/substrate/frame/revive/src/limits.rs#L46-L49>
108    // Ref: <https://docs.soliditylang.org/en/latest/abi-spec.html#events>
109    if len_topics > 4 {
110        return Err(syn::Error::new(
111            span,
112            format!(
113                "Events{} can only have up to {} fields annotated with an \
114                `#[ink(topic)]` attribute",
115                if anonymous {
116                    " with an `anonymous` attribute argument"
117                } else {
118                    ""
119                },
120                if anonymous { 4 } else { 3 }
121            ),
122        ));
123    }
124
125    let remaining_topics_ty = match len_topics {
126        0 => quote_spanned!(span=> ::ink::env::event::state::NoRemainingTopics),
127        _ => {
128            quote_spanned!(span=> [::ink::env::event::state::HasRemainingTopics; #len_topics])
129        }
130    };
131
132    Ok(generate_abi_impls!(@type |abi| {
133        let abi_ty = match abi {
134            Abi::Ink => quote!(::ink::abi::Ink),
135            Abi::Sol => quote!(::ink::abi::Sol),
136        };
137
138        let event_signature_topic = if anonymous {
139            None
140        } else {
141            let value = match abi {
142                Abi::Ink => quote!(<Self as ::ink::env::Event<::ink::abi::Ink>>::SIGNATURE_TOPIC.as_ref()),
143                Abi::Sol => {
144                    quote! {
145                        ::ink::sol::FixedBytes::from_ref(
146                            <Self as ::ink::env::Event<::ink::abi::Sol>>::SIGNATURE_TOPIC
147                                .as_ref()
148                                .expect("Expected a signature topic")
149                        )
150                    }
151                }
152            };
153            Some(quote_spanned!(span=>
154                .push_topic(#value)
155            ))
156        };
157
158        let signature_topic = if !anonymous {
159            let event_name = config
160                .name()
161                .map(ToString::to_string)
162                .unwrap_or_else(|| variant.ast().ident.to_string());
163            match abi {
164                Abi::Ink => {
165                    if let Some(sig_arg) = config.signature_topic() {
166                        let bytes = sig_arg.to_bytes();
167                        quote_spanned!(span=> ::core::option::Option::Some([ #(#bytes),* ]))
168                    } else {
169                        let calculated_signature_topic =
170                            signature_topic(variant.ast().fields, event_name);
171                        quote_spanned!(span=> ::core::option::Option::Some(#calculated_signature_topic))
172                    }
173                }
174                Abi::Sol => {
175                    let calculated_signature_topic =
176                        signature_topic_sol(variant.ast().fields, event_name);
177                    quote_spanned!(span=> ::core::option::Option::Some(#calculated_signature_topic))
178                }
179            }
180        } else {
181            quote_spanned!(span=> ::core::option::Option::None)
182        };
183
184        let topics = topic_fields.iter().fold(quote!(), |acc, field| {
185            let field_ty = &field.ast().ty;
186            let field_span = field_ty.span();
187            let value = match abi {
188                Abi::Ink => quote!(::ink::as_option!(#field)),
189                Abi::Sol => quote!(#field),
190            };
191            quote_spanned!(field_span=>
192                #acc
193                .push_topic(#value)
194            )
195        });
196        let pat = variant.pat();
197        let topics_builder = quote!(
198            #pat => {
199                builder
200                    .build::<Self>()
201                    #event_signature_topic
202                    #topics
203                    .finish()
204            }
205        );
206
207        let encode_data = match abi {
208            Abi::Ink => quote! {
209                ::ink::abi::AbiEncodeWith::<::ink::abi::Ink>::encode_with(self)
210            },
211            Abi::Sol => {
212                // For Solidity ABI encoding, only un-indexed fields are encoded as data.
213                let data_field_tys = data_fields.iter().map(|field| {
214                    let ty = &field.ast().ty;
215                    quote!( &#ty )
216                });
217                let data_field_values = data_fields.iter().map(|field| {
218                    &field.binding
219                });
220                quote! {
221                    match self {
222                        #pat => {
223                            ::ink::sol::encode_sequence::<( #( #data_field_tys, )* )>(
224                                &( #( #data_field_values, )* ),
225                            )
226                        }
227                    }
228                }
229            },
230        };
231
232        s.bound_impl(quote!(::ink::env::Event<#abi_ty>), quote! {
233            type RemainingTopics = #remaining_topics_ty;
234            const SIGNATURE_TOPIC: ::core::option::Option<[::core::primitive::u8; 32]> = #signature_topic;
235
236            fn topics<B>(
237                &self,
238                builder: ::ink::env::event::TopicsBuilder<::ink::env::event::state::Uninit, B, #abi_ty>,
239            ) -> <B as ::ink::env::event::TopicsBuilderBackend<#abi_ty>>::Output
240            where
241                B: ::ink::env::event::TopicsBuilderBackend<#abi_ty>,
242            {
243                match self {
244                    #topics_builder
245                }
246            }
247
248            fn encode_data(&self) -> ::ink::prelude::vec::Vec<::core::primitive::u8> {
249                #encode_data
250            }
251        })
252    }))
253}
254
255/// Checks if the given field's attributes contain an `#[ink(topic)]` attribute.
256///
257/// Returns `Err` if:
258/// - the given attributes contain a `#[cfg(...)]` attribute
259/// - there are `ink` attributes other than a single `#[ink(topic)]`
260fn has_ink_topic_attribute(field: &synstructure::BindingInfo) -> syn::Result<bool> {
261    let some_cfg_attrs = field
262        .ast()
263        .attrs
264        .iter()
265        .find(|attr| attr.path().is_ident("cfg"));
266    if some_cfg_attrs.is_some() {
267        Err(syn::Error::new(
268            field.ast().span(),
269            "conditional compilation is not allowed for event fields",
270        ))
271    } else {
272        let attrs = parse_arg_attrs(&field.ast().attrs)?;
273        has_ink_attribute(&attrs, "topic")
274    }
275}
276
277/// Checks if the given attributes contain an `ink` attribute with the given path.
278fn has_ink_attribute(ink_attrs: &[syn::Meta], path: &str) -> syn::Result<bool> {
279    let mut present = false;
280    for a in ink_attrs {
281        if a.path().is_ident(path) && !present {
282            present = true;
283        } else if a.path().is_ident(path) {
284            return Err(syn::Error::new(
285                a.span(),
286                format!("Only a single `#[ink({path})]` is allowed"),
287            ));
288        } else {
289            return Err(syn::Error::new(
290                a.span(),
291                "Unknown ink! attribute at this position",
292            ));
293        }
294    }
295    Ok(present)
296}
297
298/// Parses custom `ink` attributes with the arbitrary arguments.
299///
300/// # Errors
301/// - Attribute has no argument (i.e. `#[ink()]`)
302fn parse_arg_attrs(attrs: &[syn::Attribute]) -> syn::Result<Vec<syn::Meta>> {
303    let mut ink_attrs = Vec::new();
304    for a in attrs {
305        if !a.path().is_ident("ink") {
306            continue;
307        }
308
309        let nested = a.parse_args_with(
310            Punctuated::<syn::Meta, Token![,]>::parse_separated_nonempty,
311        )?;
312        if nested.is_empty() {
313            return Err(syn::Error::new(
314                a.span(),
315                "Expected to have an argument".to_string(),
316            ));
317        }
318        ink_attrs.extend(nested.into_iter())
319    }
320
321    Ok(ink_attrs)
322}
323
324/// The signature topic of an event variant.
325///
326/// Calculated with `blake2b("Event(field1_type,field2_type)")`.
327fn signature_topic(fields: &syn::Fields, event_name: String) -> TokenStream2 {
328    let fields = fields
329        .iter()
330        .map(|field| {
331            quote::ToTokens::to_token_stream(&field.ty)
332                .to_string()
333                .replace(' ', "")
334        })
335        .collect::<Vec<_>>()
336        .join(",");
337    let topic_str = format!("{event_name}({fields})");
338    quote!(::ink::blake2x256!(#topic_str))
339}
340
341/// The Solidity ABI signature topic of an event.
342///
343/// (i.e. the Keccak-256 hash of the Solidity ABI event signature).
344fn signature_topic_sol(fields: &syn::Fields, event_name: String) -> TokenStream2 {
345    let param_tys = fields.iter().map(|field| {
346        let ty = &field.ty;
347        quote! {
348            <#ty as ::ink::SolEncode>::SOL_NAME
349        }
350    });
351    let sig_arg_fmt_params = (0..fields.len())
352        .map(|_| "{}")
353        .collect::<Vec<_>>()
354        .join(",");
355    let sig_fmt_str = format!("{{}}({sig_arg_fmt_params})");
356    let sig_str = quote! {
357        ::ink::codegen::utils::const_format!(#sig_fmt_str, #event_name #(,#param_tys)*)
358    };
359    quote!(::ink::keccak_256!(#sig_str))
360}