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
17use ink_ir::{
18    format_err_spanned,
19    utils::duplicate_config_err,
20    SignatureTopicArg,
21};
22pub use metadata::event_metadata_derive;
23
24use ink_codegen::generate_code;
25use proc_macro2::TokenStream as TokenStream2;
26use quote::{
27    quote,
28    quote_spanned,
29};
30use syn::{
31    punctuated::Punctuated,
32    spanned::Spanned,
33    Token,
34};
35
36/// Event item configurations specified by nested `ink` attributes.
37struct EventConfig {
38    /// Event is anonymous.
39    pub anonymous: bool,
40    /// Event has a specified signature topic.
41    pub signature_topic: Option<SignatureTopicArg>,
42}
43
44impl EventConfig {
45    pub fn new(anonymous: bool, signature_topic: Option<SignatureTopicArg>) -> Self {
46        EventConfig {
47            anonymous,
48            signature_topic,
49        }
50    }
51}
52
53impl TryFrom<&[syn::Meta]> for EventConfig {
54    type Error = syn::Error;
55
56    fn try_from(args: &[syn::Meta]) -> Result<Self, Self::Error> {
57        let mut anonymous: Option<&syn::Meta> = None;
58        let mut signature_topic: Option<&syn::Meta> = None;
59        for arg in args.iter() {
60            if arg.path().is_ident("anonymous") {
61                if let Some(a_meta) = anonymous {
62                    return Err(duplicate_config_err(a_meta, arg, "anonymous", "event"));
63                }
64                match arg {
65                    syn::Meta::Path(_) => anonymous = Some(arg),
66                    _ => {
67                        return Err(format_err_spanned!(
68                            arg,
69                            "`#[ink(anonymous)]` takes no arguments",
70                        ));
71                    }
72                }
73            } else if arg.path().is_ident("signature_topic") {
74                if anonymous.is_some() {
75                    return Err(format_err_spanned!(
76                        arg,
77                        "cannot specify `signature_topic` with `anonymous` in ink! event item configuration argument",
78                    ));
79                }
80
81                if let Some(lit_str) = signature_topic {
82                    return Err(duplicate_config_err(lit_str, arg, "anonymous", "event"));
83                }
84                match arg {
85                    syn::Meta::NameValue(_) => signature_topic = Some(arg),
86                    _ => {
87                        return Err(format_err_spanned!(
88                            arg,
89                            "expected a name-value pair",
90                        ));
91                    }
92                }
93            } else {
94                return Err(format_err_spanned!(
95                    arg,
96                    "encountered unknown or unsupported ink! event item configuration argument",
97                ));
98            }
99        }
100
101        let signature_topic = if let Some(meta) = signature_topic {
102            Some(parse_signature_arg(meta.clone())?)
103        } else {
104            None
105        };
106
107        Ok(EventConfig::new(anonymous.is_some(), signature_topic))
108    }
109}
110
111/// Generate code from the `#[ink::event]` attribute. This expands to the required
112/// derive macros to satisfy an event implementation.
113pub fn generate(config: TokenStream2, input: TokenStream2) -> TokenStream2 {
114    ink_ir::Event::new(config, input)
115        .map(|event| generate_code(&event))
116        .unwrap_or_else(|err| err.to_compile_error())
117}
118
119/// Derives the `ink::Event` trait for the given `struct`.
120pub fn event_derive(mut s: synstructure::Structure) -> TokenStream2 {
121    s.bind_with(|_| synstructure::BindStyle::Move)
122        .add_bounds(synstructure::AddBounds::Fields)
123        .underscore_const(true);
124    match &s.ast().data {
125        syn::Data::Struct(_) => {
126            event_derive_struct(s).unwrap_or_else(|err| err.to_compile_error())
127        }
128        _ => {
129            syn::Error::new(
130                s.ast().span(),
131                "can only derive `Event` for Rust `struct` items",
132            )
133            .to_compile_error()
134        }
135    }
136}
137
138/// `Event` derive implementation for `struct` types.
139#[allow(clippy::arithmetic_side_effects)] // todo
140fn event_derive_struct(mut s: synstructure::Structure) -> syn::Result<TokenStream2> {
141    assert_eq!(s.variants().len(), 1, "can only operate on structs");
142
143    if !s.ast().generics.params.is_empty() {
144        return Err(syn::Error::new(
145            s.ast().generics.params.span(),
146            "can only derive `Event` for structs without generics",
147        ));
148    }
149
150    let span = s.ast().span();
151    let ink_attrs = parse_arg_attrs(&s.ast().attrs)?;
152    let config = EventConfig::try_from(ink_attrs.as_slice())?;
153    let anonymous = config.anonymous;
154
155    // filter field bindings to those marked as topics
156    let mut topic_err: Option<syn::Error> = None;
157    s.variants_mut()[0].filter(|bi| {
158        match has_ink_topic_attribute(bi) {
159            Ok(has_attr) => has_attr,
160            Err(err) => {
161                match topic_err {
162                    Some(ref mut topic_err) => topic_err.combine(err),
163                    None => topic_err = Some(err),
164                }
165                false
166            }
167        }
168    });
169    if let Some(err) = topic_err {
170        return Err(err);
171    }
172
173    let variant = &s.variants()[0];
174
175    // Anonymous events require 1 fewer topics since they do not include their signature.
176    let anonymous_topics_offset = usize::from(!anonymous);
177    let len_topics = variant.bindings().len() + anonymous_topics_offset;
178
179    let remaining_topics_ty = match len_topics {
180        0 => quote_spanned!(span=> ::ink::env::event::state::NoRemainingTopics),
181        _ => {
182            quote_spanned!(span=> [::ink::env::event::state::HasRemainingTopics; #len_topics])
183        }
184    };
185
186    let event_signature_topic = if anonymous {
187        None
188    } else {
189        Some(quote_spanned!(span=>
190            .push_topic(Self::SIGNATURE_TOPIC.as_ref())
191        ))
192    };
193
194    let signature_topic = if !anonymous {
195        let event_ident = variant.ast().ident;
196        if let Some(sig_arg) = config.signature_topic {
197            let bytes = sig_arg.signature_topic();
198            quote_spanned!(span=> ::core::option::Option::Some([ #(#bytes),* ]))
199        } else {
200            let calculated_signature_topic =
201                signature_topic(variant.ast().fields, event_ident);
202            quote_spanned!(span=> ::core::option::Option::Some(#calculated_signature_topic))
203        }
204    } else {
205        quote_spanned!(span=> ::core::option::Option::None)
206    };
207
208    let topics = variant.bindings().iter().fold(quote!(), |acc, field| {
209        let field_ty = &field.ast().ty;
210        let field_span = field_ty.span();
211        quote_spanned!(field_span=>
212            #acc
213            .push_topic(::ink::as_option!(#field))
214        )
215    });
216    let pat = variant.pat();
217    let topics_builder = quote!(
218        #pat => {
219            builder
220                .build::<Self>()
221                #event_signature_topic
222                #topics
223                .finish()
224        }
225    );
226
227    Ok(s.bound_impl(quote!(::ink::env::Event), quote! {
228        type RemainingTopics = #remaining_topics_ty;
229        const SIGNATURE_TOPIC: ::core::option::Option<[::core::primitive::u8; 32]> = #signature_topic;
230
231        fn topics<E, B>(
232            &self,
233            builder: ::ink::env::event::TopicsBuilder<::ink::env::event::state::Uninit, E, B>,
234        ) -> <B as ::ink::env::event::TopicsBuilderBackend<E>>::Output
235        where
236            E: ::ink::env::Environment,
237            B: ::ink::env::event::TopicsBuilderBackend<E>,
238        {
239            match self {
240                #topics_builder
241            }
242        }
243     }))
244}
245
246/// Checks if the given field's attributes contain an `#[ink(topic)]` attribute.
247///
248/// Returns `Err` if:
249/// - the given attributes contain a `#[cfg(...)]` attribute
250/// - there are `ink` attributes other than a single `#[ink(topic)]`
251fn has_ink_topic_attribute(field: &synstructure::BindingInfo) -> syn::Result<bool> {
252    let some_cfg_attrs = field
253        .ast()
254        .attrs
255        .iter()
256        .find(|attr| attr.path().is_ident("cfg"));
257    if some_cfg_attrs.is_some() {
258        Err(syn::Error::new(
259            field.ast().span(),
260            "conditional compilation is not allowed for event fields",
261        ))
262    } else {
263        let attrs = parse_arg_attrs(&field.ast().attrs)?;
264        has_ink_attribute(&attrs, "topic")
265    }
266}
267
268/// Checks if the given attributes contain an `ink` attribute with the given path.
269fn has_ink_attribute(ink_attrs: &[syn::Meta], path: &str) -> syn::Result<bool> {
270    let mut present = false;
271    for a in ink_attrs {
272        if a.path().is_ident(path) && !present {
273            present = true;
274        } else if a.path().is_ident(path) {
275            return Err(syn::Error::new(
276                a.span(),
277                format!("Only a single `#[ink({})]` is allowed", path),
278            ));
279        } else {
280            return Err(syn::Error::new(
281                a.span(),
282                "Unknown ink! attribute at this position".to_string(),
283            ));
284        }
285    }
286    Ok(present)
287}
288
289/// Parses custom `ink` attributes with the arbitrary arguments.
290///
291/// # Errors
292/// - Attribute has no argument (i.e. `#[ink()]`)
293fn parse_arg_attrs(attrs: &[syn::Attribute]) -> syn::Result<Vec<syn::Meta>> {
294    let mut ink_attrs = Vec::new();
295    for a in attrs {
296        if !a.path().is_ident("ink") {
297            continue;
298        }
299
300        let nested = a.parse_args_with(
301            Punctuated::<syn::Meta, Token![,]>::parse_separated_nonempty,
302        )?;
303        if nested.is_empty() {
304            return Err(syn::Error::new(
305                a.span(),
306                "Expected to have an argument".to_string(),
307            ));
308        }
309        ink_attrs.extend(nested.into_iter())
310    }
311
312    Ok(ink_attrs)
313}
314
315/// Parses signature topic from the list of attributes.
316///
317/// # Errors
318/// - Name-value pair is not specified correctly.
319/// - Provided value is of wrong format.
320/// - Provided hash string is of wrong length.
321fn parse_signature_arg(meta: syn::Meta) -> syn::Result<SignatureTopicArg> {
322    if let syn::Meta::NameValue(nv) = &meta {
323        Ok(SignatureTopicArg::try_from(nv)?)
324    } else {
325        Err(syn::Error::new(
326            meta.span(),
327            "Expected to have an argument".to_string(),
328        ))
329    }
330}
331
332/// The signature topic of an event variant.
333///
334/// Calculated with `blake2b("Event(field1_type,field2_type)")`.
335fn signature_topic(fields: &syn::Fields, event_ident: &syn::Ident) -> TokenStream2 {
336    let fields = fields
337        .iter()
338        .map(|field| {
339            quote::ToTokens::to_token_stream(&field.ty)
340                .to_string()
341                .replace(' ', "")
342        })
343        .collect::<Vec<_>>()
344        .join(",");
345    let topic_str = format!("{}({fields})", event_ident);
346    quote!(::ink::blake2x256!(#topic_str))
347}