diff --git a/CHANGELOG.md b/CHANGELOG.md index 1971c2d9668..0f8261b02a8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] ### Added +- Custom signature topic in Events - #[2031](https://github.com/paritytech/ink/pull/2031) - Linter: `non_fallible_api` lint - [#2004](https://github.com/paritytech/ink/pull/2004) ### Fixed diff --git a/Cargo.lock b/Cargo.lock index c0ed203eff4..22f16fc189d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2731,6 +2731,7 @@ version = "5.0.0-rc" dependencies = [ "blake2", "either", + "impl-serde", "ink_prelude 5.0.0-rc", "itertools 0.12.0", "proc-macro2", diff --git a/README.md b/README.md index 32ce3d8e769..96354cf6410 100644 --- a/README.md +++ b/README.md @@ -230,6 +230,7 @@ In a module annotated with `#[ink::contract]` these attributes are available: | `#[ink(constructor)]` | Applicable to method. | Flags a method for the ink! storage struct as constructor making it available to the API for instantiating the contract. | | `#[ink(event)]` | On `struct` definitions. | Defines an ink! event. A contract can define multiple such ink! events. | | `#[ink(anonymous)]` | Applicable to ink! events. | Tells the ink! codegen to treat the ink! event as anonymous which omits the event signature as topic upon emitting. Very similar to anonymous events in Solidity. | +| `#[ink(signature_topic = _)]` | Applicable to ink! events. | Specifies custom signature topic of the event that allows to use manually specify shared event definition. | | `#[ink(topic)]` | Applicable on ink! event field. | Tells the ink! codegen to provide a topic hash for the given field. Every ink! event can only have a limited number of such topic fields. Similar semantics as to indexed event arguments in Solidity. | | `#[ink(payable)]` | Applicable to ink! messages. | Allows receiving value as part of the call of the ink! message. ink! constructors are implicitly payable. | | `#[ink(selector = S:u32)]` | Applicable to ink! messages and ink! constructors. | Specifies a concrete dispatch selector for the flagged entity. This allows a contract author to precisely control the selectors of their APIs making it possible to rename their API without breakage. | diff --git a/crates/engine/src/tests.rs b/crates/engine/src/tests.rs index b9359ab187a..3fc156b19d7 100644 --- a/crates/engine/src/tests.rs +++ b/crates/engine/src/tests.rs @@ -151,7 +151,7 @@ fn events() { let event = events.next().expect("event must exist"); assert_eq!(event.topics.len(), 2); assert_eq!( - event.topics.get(0).expect("first topic must exist"), + event.topics.first().expect("first topic must exist"), &topic1 ); assert_eq!( diff --git a/crates/env/src/event.rs b/crates/env/src/event.rs index 859ef7515e3..9072cad6045 100644 --- a/crates/env/src/event.rs +++ b/crates/env/src/event.rs @@ -202,9 +202,11 @@ pub trait Event: scale::Encode { /// The unique signature topic of the event. `None` for anonymous events. /// + /// It can be automatically calculated or manually specified. + /// /// Usually this is calculated using the `#[derive(ink::Event)]` derive, which by - /// default calculates this as `blake2b("Event(field1_type,field2_type)")` - const SIGNATURE_TOPIC: Option<[u8; 32]>; + /// default calculates this as `blake2b("Event(field1_type,field2_type)"` + const SIGNATURE_TOPIC: core::option::Option<[u8; 32]>; /// Guides event topic serialization using the given topics builder. fn topics( diff --git a/crates/ink/codegen/src/generator/event.rs b/crates/ink/codegen/src/generator/event.rs index 3bcbef53c6c..2289052957e 100644 --- a/crates/ink/codegen/src/generator/event.rs +++ b/crates/ink/codegen/src/generator/event.rs @@ -15,8 +15,9 @@ use crate::GenerateCode; use derive_more::From; use proc_macro2::TokenStream as TokenStream2; +use syn::spanned::Spanned; -/// Generates code for the storage item. +/// Generates code for the event item. #[derive(From, Copy, Clone)] pub struct Event<'a> { /// The storage item to generate code for. @@ -24,18 +25,26 @@ pub struct Event<'a> { } impl GenerateCode for Event<'_> { - /// Generates ink! storage item code. + /// Generates ink! event item code. fn generate_code(&self) -> TokenStream2 { let item = self.item.item(); let anonymous = self .item .anonymous() .then(|| quote::quote! { #[ink(anonymous)] }); + let signature_topic = self + .item + .signature_topic_hex() + .map(|hex_s| quote::quote! { #[ink(signature_topic = #hex_s)] }); + let cfg_attrs = self.item.get_cfg_attrs(item.span()); + quote::quote! ( + #( #cfg_attrs )* #[cfg_attr(feature = "std", derive(::ink::EventMetadata))] #[derive(::ink::Event)] #[::ink::scale_derive(Encode, Decode)] #anonymous + #signature_topic #item ) } diff --git a/crates/ink/ir/Cargo.toml b/crates/ink/ir/Cargo.toml index 74fe25698b8..43624737ae0 100644 --- a/crates/ink/ir/Cargo.toml +++ b/crates/ink/ir/Cargo.toml @@ -24,6 +24,7 @@ proc-macro2 = { workspace = true } itertools = { workspace = true } either = { workspace = true } blake2 = { workspace = true } +impl-serde = { workspace = true } ink_prelude = { workspace = true } [features] diff --git a/crates/ink/ir/src/ir/attrs.rs b/crates/ink/ir/src/ir/attrs.rs index 34d3f5feba5..b21615cb2bb 100644 --- a/crates/ink/ir/src/ir/attrs.rs +++ b/crates/ink/ir/src/ir/attrs.rs @@ -59,13 +59,13 @@ impl IsDocAttribute for syn::Attribute { fn extract_docs(&self) -> Option { if !self.is_doc_attribute() { - return None + return None; } match &self.meta { syn::Meta::NameValue(nv) => { if let syn::Expr::Lit(l) = &nv.value { if let syn::Lit::Str(s) = &l.lit { - return Some(s.value()) + return Some(s.value()); } } } @@ -172,7 +172,7 @@ impl InkAttribute { return Err(format_err!( self.span(), "unexpected first ink! attribute argument", - )) + )); } Ok(()) } @@ -200,7 +200,7 @@ impl InkAttribute { .into_combine(format_err!( seen.span(), "first equal ink! attribute argument here" - ))) + ))); } if let Some(seen) = seen2.get(&arg.kind().kind()) { return Err(format_err!( @@ -210,7 +210,7 @@ impl InkAttribute { .into_combine(format_err!( *seen, "first equal ink! attribute argument with equal kind here" - ))) + ))); } seen.insert(arg); seen2.insert(arg.kind().kind(), arg.span()); @@ -242,7 +242,7 @@ impl InkAttribute { return Err(format_err!( Span::call_site(), "encountered unexpected empty expanded ink! attribute arguments", - )) + )); } Self::ensure_no_duplicate_args(&args)?; Ok(Self { args }) @@ -268,7 +268,7 @@ impl InkAttribute { pub fn namespace(&self) -> Option { self.args().find_map(|arg| { if let ir::AttributeArg::Namespace(namespace) = arg.kind() { - return Some(namespace.clone()) + return Some(namespace.clone()); } None }) @@ -278,7 +278,17 @@ impl InkAttribute { pub fn selector(&self) -> Option { self.args().find_map(|arg| { if let ir::AttributeArg::Selector(selector) = arg.kind() { - return Some(*selector) + return Some(*selector); + } + None + }) + } + + /// Returns the signature topic of the ink! attribute if any. + pub fn signature_topic_hex(&self) -> Option { + self.args().find_map(|arg| { + if let ir::AttributeArg::SignatureTopic(hash) = arg.kind() { + return Some(hash.clone()); } None }) @@ -363,6 +373,9 @@ pub enum AttributeArgKind { /// `#[ink(selector = _)]` /// `#[ink(selector = 0xDEADBEEF)]` Selector, + /// `#[ink(signature_topic = + /// "325c98ff66bd0d9d1c10789ae1f2a17bdfb2dcf6aa3d8092669afafdef1cb72d")]` + SignatureTopicArg, /// `#[ink(function = N: u16)]` Function, /// `#[ink(namespace = "my_namespace")]` @@ -418,6 +431,9 @@ pub enum AttributeArg { /// - `#[ink(selector = _)]` Applied on ink! messages to define a fallback messages /// that is invoked if no other ink! message matches a given selector. Selector(SelectorOrWildcard), + /// `#[ink(signature_topic = + /// "325c98ff66bd0d9d1c10789ae1f2a17bdfb2dcf6aa3d8092669afafdef1cb72d")]` + SignatureTopic(String), /// `#[ink(namespace = "my_namespace")]` /// /// Applied on ink! trait implementation blocks to disambiguate other trait @@ -462,6 +478,9 @@ impl core::fmt::Display for AttributeArgKind { Self::Selector => { write!(f, "selector = S:[u8; 4] || _") } + Self::SignatureTopicArg => { + write!(f, "signature_topic = S:[u8; 32]") + } Self::Function => { write!(f, "function = N:u16)") } @@ -486,6 +505,7 @@ impl AttributeArg { Self::Constructor => AttributeArgKind::Constructor, Self::Payable => AttributeArgKind::Payable, Self::Selector(_) => AttributeArgKind::Selector, + Self::SignatureTopic(_) => AttributeArgKind::SignatureTopicArg, Self::Function(_) => AttributeArgKind::Function, Self::Namespace(_) => AttributeArgKind::Namespace, Self::Implementation => AttributeArgKind::Implementation, @@ -505,6 +525,9 @@ impl core::fmt::Display for AttributeArg { Self::Constructor => write!(f, "constructor"), Self::Payable => write!(f, "payable"), Self::Selector(selector) => core::fmt::Display::fmt(&selector, f), + Self::SignatureTopic(hash) => { + write!(f, "signature_topic = {:?}", hash) + } Self::Function(function) => { write!(f, "function = {:?}", function.into_u16()) } @@ -778,7 +801,7 @@ where { let (ink_attrs, rust_attrs) = ir::partition_attributes(attrs)?; if ink_attrs.is_empty() { - return Ok((None, rust_attrs)) + return Ok((None, rust_attrs)); } let normalized = ir::InkAttribute::from_expanded(ink_attrs).map_err(|err| { err.into_combine(format_err!(parent_span, "at this invocation",)) @@ -807,7 +830,7 @@ impl Attribute { attr.span(), "encountered duplicate ink! attribute" ) - .into_combine(format_err!(seen.span(), "first ink! attribute here"))) + .into_combine(format_err!(seen.span(), "first ink! attribute here"))); } seen.insert(attr); } @@ -820,7 +843,7 @@ impl TryFrom for Attribute { fn try_from(attr: syn::Attribute) -> Result { if attr.path().is_ident("ink") { - return >::try_from(attr).map(Into::into) + return >::try_from(attr).map(Into::into); } Ok(Attribute::Other(attr)) } @@ -837,7 +860,7 @@ impl TryFrom for InkAttribute { fn try_from(attr: syn::Attribute) -> Result { if !attr.path().is_ident("ink") { - return Err(format_err_spanned!(attr, "unexpected non-ink! attribute")) + return Err(format_err_spanned!(attr, "unexpected non-ink! attribute")); } let args: Vec<_> = attr @@ -850,7 +873,7 @@ impl TryFrom for InkAttribute { return Err(format_err_spanned!( attr, "encountered unsupported empty ink! attribute" - )) + )); } Ok(InkAttribute { args }) } @@ -898,7 +921,7 @@ impl InkAttribute { } } if let Some(err) = err { - return Err(err) + return Err(err); } Ok(()) } @@ -925,6 +948,16 @@ impl Parse for AttributeFrag { Namespace::try_from(&name_value.value) .map(AttributeArg::Namespace) } + "signature_topic" => { + if let Some(hash) = name_value.value.as_string() { + Ok(AttributeArg::SignatureTopic(hash)) + } else { + Err(format_err_spanned!( + name_value.value, + "expected String type for `S` in #[ink(signature_topic = S)]", + )) + } + } "function" => { if let Some(lit_int) = name_value.value.as_lit_int() { let id = lit_int.base10_parse::() @@ -1508,4 +1541,14 @@ mod tests { Err("encountered duplicate ink! attribute"), ) } + #[test] + fn signature_topic_works() { + let s = "11".repeat(32); + assert_attribute_try_from( + syn::parse_quote! { + #[ink(signature_topic = #s)] + }, + Ok(test::Attribute::Ink(vec![AttributeArg::SignatureTopic(s)])), + ); + } } diff --git a/crates/ink/ir/src/ir/event/config.rs b/crates/ink/ir/src/ir/event/config.rs index 45280b7b8c8..71313c36530 100644 --- a/crates/ink/ir/src/ir/event/config.rs +++ b/crates/ink/ir/src/ir/event/config.rs @@ -24,6 +24,9 @@ pub struct EventConfig { /// If set to `true`, **no** signature topic is generated or emitted for this event., /// This is the default value. anonymous: bool, + + /// Manually specified signature topic hash. + signature_topic_hex: Option, } impl TryFrom for EventConfig { @@ -31,10 +34,11 @@ impl TryFrom for EventConfig { fn try_from(args: ast::AttributeArgs) -> Result { let mut anonymous: Option = None; + let mut signature_topic: Option = None; for arg in args.into_iter() { if arg.name.is_ident("anonymous") { if let Some(lit_bool) = anonymous { - return Err(duplicate_config_err(lit_bool, arg, "anonymous", "event")) + return Err(duplicate_config_err(lit_bool, arg, "anonymous", "event")); } if let ast::MetaValue::Lit(syn::Lit::Bool(lit_bool)) = &arg.value { anonymous = Some(lit_bool.clone()) @@ -44,27 +48,56 @@ impl TryFrom for EventConfig { "expected a bool literal for `anonymous` ink! event item configuration argument", )); } + } else if arg.name.is_ident("signature_topic") { + if anonymous.is_some() { + return Err(format_err_spanned!( + arg, + "cannot specify `signature_topic` with `anonymous` in ink! event item configuration argument", + )); + } + + if let Some(lit_str) = signature_topic { + return Err(duplicate_config_err(lit_str, arg, "anonymous", "event")); + } + if let ast::MetaValue::Lit(syn::Lit::Str(lis_str)) = &arg.value { + signature_topic = Some(lis_str.clone()) + } else { + return Err(format_err_spanned!( + arg, + "expected a bool literal for `anonymous` ink! event item configuration argument", + )); + } } else { return Err(format_err_spanned!( arg, - "encountered unknown or unsupported ink! storage item configuration argument", + "encountered unknown or unsupported ink! event item configuration argument", )); } } + Ok(EventConfig::new( anonymous.map(|lit_bool| lit_bool.value).unwrap_or(false), + signature_topic.map(|lit_str| lit_str.value()), )) } } impl EventConfig { /// Construct a new [`EventConfig`]. - pub fn new(anonymous: bool) -> Self { - Self { anonymous } + pub fn new(anonymous: bool, signature_topic_hex: Option) -> Self { + Self { + anonymous, + signature_topic_hex, + } } /// Returns the anonymous configuration argument. pub fn anonymous(&self) -> bool { self.anonymous } + + /// Returns the manually specified signature topic. + pub fn signature_topic_hex(&self) -> Option<&str> { + self.signature_topic_hex.as_deref() + } } diff --git a/crates/ink/ir/src/ir/event/mod.rs b/crates/ink/ir/src/ir/event/mod.rs index 9a2e1b0b6ea..41c5d8988ad 100644 --- a/crates/ink/ir/src/ir/event/mod.rs +++ b/crates/ink/ir/src/ir/event/mod.rs @@ -13,17 +13,24 @@ // limitations under the License. mod config; +mod signature_topic; use config::EventConfig; -use proc_macro2::TokenStream as TokenStream2; +use proc_macro2::{ + Span, + TokenStream as TokenStream2, +}; use quote::ToTokens; use syn::spanned::Spanned as _; use crate::{ error::ExtError, ir, + utils::extract_cfg_attributes, }; +pub use signature_topic::SignatureTopicArg; + /// A checked ink! event with its configuration. #[derive(Debug, PartialEq, Eq)] pub struct Event { @@ -48,7 +55,7 @@ impl Event { return Err(format_err_spanned!( attr, "only one `ink::event` is allowed", - )) + )); } } @@ -75,7 +82,7 @@ impl Event { item_struct: &syn::ItemStruct, ) -> Result { if !ir::contains_ink_attributes(&item_struct.attrs) { - return Ok(false) + return Ok(false); } // At this point we know that there must be at least one ink! // attribute. This can be either the ink! storage struct, @@ -90,6 +97,20 @@ impl Event { pub fn anonymous(&self) -> bool { self.config.anonymous() } + + /// Return manually specified signature topic hash. + /// + /// # Note + /// + /// Conflicts with `anonymous` + pub fn signature_topic_hex(&self) -> Option<&str> { + self.config.signature_topic_hex() + } + + /// Returns a list of `cfg` attributes if any. + pub fn get_cfg_attrs(&self, span: Span) -> Vec { + extract_cfg_attributes(&self.item.attrs, span) + } } impl ToTokens for Event { @@ -107,21 +128,32 @@ impl TryFrom for Event { let struct_span = item_struct.span(); let (ink_attrs, other_attrs) = ir::sanitize_attributes( struct_span, - item_struct.attrs, + item_struct.attrs.clone(), &ir::AttributeArgKind::Event, |arg| { match arg.kind() { - ir::AttributeArg::Event | ir::AttributeArg::Anonymous => Ok(()), + ir::AttributeArg::Event + | ir::AttributeArg::SignatureTopic(_) + | ir::AttributeArg::Anonymous => Ok(()), _ => Err(None), } }, )?; + if ink_attrs.is_anonymous() && ink_attrs.signature_topic_hex().is_some() { + return Err(format_err_spanned!( + item_struct, + "cannot use use `anonymous` with `signature_topic`", + )); + } Ok(Self { item: syn::ItemStruct { attrs: other_attrs, ..item_struct }, - config: EventConfig::new(ink_attrs.is_anonymous()), + config: EventConfig::new( + ink_attrs.is_anonymous(), + ink_attrs.signature_topic_hex(), + ), }) } } @@ -132,8 +164,10 @@ mod tests { #[test] fn simple_try_from_works() { + let s = "11".repeat(32); let item_struct: syn::ItemStruct = syn::parse_quote! { #[ink(event)] + #[ink(signature_topic = #s)] pub struct MyEvent { #[ink(topic)] field_1: i32, @@ -179,7 +213,34 @@ mod tests { } }, "encountered duplicate ink! attribute", - ) + ); + assert_try_from_fails( + syn::parse_quote! { + #[ink(event)] + #[ink(anonymous)] + #[ink(anonymous)] + pub struct MyEvent { + #[ink(topic)] + field_1: i32, + field_2: bool, + } + }, + "encountered duplicate ink! attribute", + ); + let s = "11".repeat(32); + assert_try_from_fails( + syn::parse_quote! { + #[ink(event)] + #[ink(signature_topic = #s)] + #[ink(signature_topic = #s)] + pub struct MyEvent { + #[ink(topic)] + field_1: i32, + field_2: bool, + } + }, + "encountered duplicate ink! attribute", + ); } #[test] @@ -240,4 +301,21 @@ mod tests { } }); } + #[test] + fn signature_conflict_fails() { + let s = "11".repeat(32); + assert_try_from_fails( + syn::parse_quote! { + #[ink(event)] + #[ink(anonymous)] + #[ink(signature_topic = #s)] + pub struct MyEvent { + #[ink(topic)] + field_1: i32, + field_2: bool, + } + }, + "cannot use use `anonymous` with `signature_topic`", + ) + } } diff --git a/crates/ink/ir/src/ir/event/signature_topic.rs b/crates/ink/ir/src/ir/event/signature_topic.rs new file mode 100644 index 00000000000..4e4aa4d46f9 --- /dev/null +++ b/crates/ink/ir/src/ir/event/signature_topic.rs @@ -0,0 +1,125 @@ +// Copyright (C) Parity Technologies (UK) Ltd. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use impl_serde::serialize as serde_hex; +use syn::spanned::Spanned; + +use crate::ast; + +/// The signature topic argument of an event variant. +/// +/// Used as part of `ink::event` macro. +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct SignatureTopicArg { + topic: [u8; 32], +} + +impl SignatureTopicArg { + pub fn signature_topic(&self) -> [u8; 32] { + self.topic + } +} + +impl From<&[u8; 32]> for SignatureTopicArg { + fn from(value: &[u8; 32]) -> Self { + Self { topic: *value } + } +} + +impl TryFrom<&syn::Lit> for SignatureTopicArg { + type Error = syn::Error; + + fn try_from(lit: &syn::Lit) -> Result { + if let syn::Lit::Str(s) = lit { + let bytes: [u8; 32] = serde_hex::from_hex(&s.value()) + .map_err(|_| { + format_err_spanned!( + lit, + "`signature_topic` has invalid hex string", + ) + })? + .try_into() + .map_err(|e: Vec| { + format_err_spanned!( + lit, + "`signature_topic` is expected to be 32-byte hex string. Found {} bytes", + e.len() + ) + })?; + + Ok(Self { topic: bytes }) + } else { + Err(format_err_spanned!( + lit, + "Expected string literal argument for the `signature_topic`" + )) + } + } +} + +impl TryFrom<&ast::MetaValue> for SignatureTopicArg { + type Error = syn::Error; + + fn try_from(value: &ast::MetaValue) -> Result { + if let ast::MetaValue::Lit(lit) = value { + Self::try_from(lit) + } else { + Err(format_err_spanned!( + value, + "Expected string argument for the `signature_topic`" + )) + } + } +} + +impl TryFrom for Option { + type Error = syn::Error; + + fn try_from(args: ast::AttributeArgs) -> Result { + let mut signature_topic: Option = None; + for arg in args.into_iter() { + if arg.name.is_ident("hash") { + if signature_topic.is_some() { + return Err(format_err!( + arg.span(), + "encountered duplicate ink! event configuration argument" + )); + } + signature_topic = Some(SignatureTopicArg::try_from(&arg.value)?); + } else { + return Err(format_err_spanned!( + arg, + "encountered unknown or unsupported ink! event item configuration argument", + )); + } + } + Ok(signature_topic) + } +} + +impl TryFrom<&syn::MetaNameValue> for SignatureTopicArg { + type Error = syn::Error; + + fn try_from(nv: &syn::MetaNameValue) -> Result { + if nv.path.is_ident("signature_topic") { + if let syn::Expr::Lit(lit_expr) = &nv.value { + Self::try_from(&lit_expr.lit) + } else { + Err(format_err_spanned!(&nv.value, "Expected literal argument")) + } + } else { + Err(format_err_spanned!(nv, "Expected `signature_topic` ident")) + } + } +} diff --git a/crates/ink/ir/src/ir/mod.rs b/crates/ink/ir/src/ir/mod.rs index c7c28f681e0..14e1f9b9b29 100644 --- a/crates/ink/ir/src/ir/mod.rs +++ b/crates/ink/ir/src/ir/mod.rs @@ -70,7 +70,10 @@ pub use self::{ }, config::Config, contract::Contract, - event::Event, + event::{ + Event, + SignatureTopicArg, + }, ink_test::InkTest, item::{ InkItem, diff --git a/crates/ink/ir/src/lib.rs b/crates/ink/ir/src/lib.rs index 53ca060e85c..6aaeaff8025 100644 --- a/crates/ink/ir/src/lib.rs +++ b/crates/ink/ir/src/lib.rs @@ -77,6 +77,7 @@ pub use self::{ Receiver, Selector, SelectorMacro, + SignatureTopicArg, Storage, StorageItem, Visibility, diff --git a/crates/ink/macro/src/event/mod.rs b/crates/ink/macro/src/event/mod.rs index 06bddf8b8d2..5f438fb553c 100644 --- a/crates/ink/macro/src/event/mod.rs +++ b/crates/ink/macro/src/event/mod.rs @@ -14,6 +14,11 @@ mod metadata; +use ink_ir::{ + format_err_spanned, + utils::duplicate_config_err, + SignatureTopicArg, +}; pub use metadata::event_metadata_derive; use ink_codegen::generate_code; @@ -22,7 +27,86 @@ use quote::{ quote, quote_spanned, }; -use syn::spanned::Spanned; +use syn::{ + punctuated::Punctuated, + spanned::Spanned, + Token, +}; + +/// Event item configurations specified by nested `ink` attributes. +struct EventConfig { + /// Event is anonymous. + pub anonymous: bool, + /// Event has a specified signature topic. + pub signature_topic: Option, +} + +impl EventConfig { + pub fn new(anonymous: bool, signature_topic: Option) -> Self { + EventConfig { + anonymous, + signature_topic, + } + } +} + +impl TryFrom<&[syn::Meta]> for EventConfig { + type Error = syn::Error; + + fn try_from(args: &[syn::Meta]) -> Result { + let mut anonymous: Option<&syn::Meta> = None; + let mut signature_topic: Option<&syn::Meta> = None; + for arg in args.iter() { + if arg.path().is_ident("anonymous") { + if let Some(a_meta) = anonymous { + return Err(duplicate_config_err(a_meta, arg, "anonymous", "event")); + } + match arg { + syn::Meta::Path(_) => anonymous = Some(arg), + _ => { + return Err(format_err_spanned!( + arg, + "`#[ink(anonymous)]` takes no arguments", + )); + } + } + } else if arg.path().is_ident("signature_topic") { + if anonymous.is_some() { + return Err(format_err_spanned!( + arg, + "cannot specify `signature_topic` with `anonymous` in ink! event item configuration argument", + )); + } + + if let Some(lit_str) = signature_topic { + return Err(duplicate_config_err(lit_str, arg, "anonymous", "event")); + } + match arg { + syn::Meta::NameValue(_) => signature_topic = Some(arg), + _ => { + return Err(format_err_spanned!( + arg, + "expected a name-value pair", + )); + } + } + } else { + return Err(format_err_spanned!( + arg, + "encountered unknown or unsupported ink! event item configuration argument", + )); + } + } + + let signature_topic = if let Some(meta) = signature_topic { + Some(parse_signature_arg(meta.clone())?) + } else { + None + }; + + Ok(EventConfig::new(anonymous.is_some(), signature_topic)) + } +} /// Generate code from the `#[ink::event]` attribute. This expands to the required /// derive macros to satisfy an event implementation. @@ -59,11 +143,13 @@ fn event_derive_struct(mut s: synstructure::Structure) -> syn::Result = None; @@ -80,7 +166,7 @@ fn event_derive_struct(mut s: synstructure::Structure) -> syn::Result syn::Result ::core::option::Option::Some(#signature_topic)) - } else { - quote_spanned!(span=> ::core::option::Option::None) - }; let event_signature_topic = if anonymous { None } else { @@ -111,6 +190,20 @@ fn event_derive_struct(mut s: synstructure::Structure) -> syn::Result ::core::option::Option::Some([ #(#bytes),* ])) + } else { + let calculated_signature_topic = + signature_topic(variant.ast().fields, event_ident); + quote_spanned!(span=> ::core::option::Option::Some(#calculated_signature_topic)) + } + } else { + quote_spanned!(span=> ::core::option::Option::None) + }; + let topics = variant.bindings().iter().fold(quote!(), |acc, field| { let field_ty = &field.ast().ty; let field_span = field_ty.span(); @@ -132,7 +225,6 @@ fn event_derive_struct(mut s: synstructure::Structure) -> syn::Result = #signature_topic; fn topics( @@ -150,23 +242,6 @@ fn event_derive_struct(mut s: synstructure::Structure) -> syn::Result TokenStream2 { - let fields = fields - .iter() - .map(|field| { - quote::ToTokens::to_token_stream(&field.ty) - .to_string() - .replace(' ', "") - }) - .collect::>() - .join(","); - let topic_str = format!("{}({fields})", event_ident); - quote!(::ink::blake2x256!(#topic_str)) -} - /// Checks if the given field's attributes contain an `#[ink(topic)]` attribute. /// /// Returns `Err` if: @@ -184,46 +259,88 @@ fn has_ink_topic_attribute(field: &synstructure::BindingInfo) -> syn::Result syn::Result { + let mut present = false; + for a in ink_attrs { + if a.path().is_ident(path) && !present { + present = true; + } else if a.path().is_ident(path) { + return Err(syn::Error::new( + a.span(), + format!("Only a single `#[ink({})]` is allowed", path), + )); + } else { + return Err(syn::Error::new( + a.span(), + "Unknown ink! attribute at this position".to_string(), + )); + } + } + Ok(present) +} + +/// Parses custom `ink` attributes with the arbitrary arguments. /// /// # Errors -/// - If there are multiple `ink` attributes with the given path. -/// - If multiple arguments are given to the `ink` attribute. -/// - If any other `ink` attributes are present other than the one with the given path. -fn has_ink_attribute(attrs: &[syn::Attribute], path: &str) -> syn::Result { - let ink_attrs = attrs - .iter() - .filter_map(|attr| { - if attr.path().is_ident("ink") { - let parse_result = attr.parse_nested_meta(|meta| { - if meta.path.is_ident(path) { - if meta.input.is_empty() { - Ok(()) - } else { - Err(meta.error(format!( - "Invalid `#[ink({path})]` attribute: multiple arguments not allowed.", - ))) - } - } else { - Err(meta - .error(format!("Only `#[ink({path})]` attribute allowed."))) - } - }); - Some(parse_result.map(|_| attr)) - } else { - None - } - }) - .collect::>>()?; - if ink_attrs.len() > 1 { - return Err(syn::Error::new( - ink_attrs[1].span(), - format!("Only a single `#[ink({})]` attribute allowed.", path), +/// - Attribute has no argument (i.e. `#[ink()]`) +fn parse_arg_attrs(attrs: &[syn::Attribute]) -> syn::Result> { + let mut ink_attrs = Vec::new(); + for a in attrs { + if !a.path().is_ident("ink") { + continue; + } + + let nested = a.parse_args_with( + Punctuated::::parse_separated_nonempty, + )?; + if nested.is_empty() { + return Err(syn::Error::new( + a.span(), + "Expected to have an argument".to_string(), + )); + } + ink_attrs.extend(nested.into_iter()) + } + + Ok(ink_attrs) +} + +/// Parses signature topic from the list of attributes. +/// +/// # Errors +/// - Name-value pair is not specified correctly. +/// - Provided value is of wrong format. +/// - Provided hash string is of wrong length. +fn parse_signature_arg(meta: syn::Meta) -> syn::Result { + if let syn::Meta::NameValue(nv) = &meta { + Ok(SignatureTopicArg::try_from(nv)?) + } else { + Err(syn::Error::new( + meta.span(), + "Expected to have an argument".to_string(), )) } - Ok(!ink_attrs.is_empty()) +} + +/// The signature topic of an event variant. +/// +/// Calculated with `blake2b("Event(field1_type,field2_type)")`. +fn signature_topic(fields: &syn::Fields, event_ident: &syn::Ident) -> TokenStream2 { + let fields = fields + .iter() + .map(|field| { + quote::ToTokens::to_token_stream(&field.ty) + .to_string() + .replace(' ', "") + }) + .collect::>() + .join(","); + let topic_str = format!("{}({fields})", event_ident); + quote!(::ink::blake2x256!(#topic_str)) } diff --git a/crates/ink/macro/src/lib.rs b/crates/ink/macro/src/lib.rs index d55fbc97c84..e1738c6e837 100644 --- a/crates/ink/macro/src/lib.rs +++ b/crates/ink/macro/src/lib.rs @@ -659,6 +659,9 @@ pub fn trait_definition(attr: TokenStream, item: TokenStream) -> TokenStream { /// By default, a signature topic will be generated for the event. This allows consumers /// to filter and identify events of this type. Marking an event with `anonymous = true` /// means no signature topic will be generated or emitted. +/// Custom signature topic can be specified with `signature_topic = <32 byte hex string>`. +/// +/// `signature_topic` and `anonymous` are conflicting arguments. /// /// # Examples /// @@ -677,6 +680,15 @@ pub fn trait_definition(attr: TokenStream, item: TokenStream) -> TokenStream { /// #[ink(topic)] /// pub topic: [u8; 32], /// } +/// // Setting `signature_topic = ` specifies custom signature topic. +/// #[ink::event( +/// signature_topic = "1111111111111111111111111111111111111111111111111111111111111111" +/// )] +/// pub struct MyCustomSignatureEvent { +/// pub field: u32, +/// #[ink(topic)] +/// pub topic: [u8; 32], +/// } /// ``` #[proc_macro_attribute] pub fn event(attr: TokenStream, item: TokenStream) -> TokenStream { @@ -1332,8 +1344,7 @@ synstructure::decl_derive!( [Event, attributes(ink)] => /// Derives an implementation of the [`ink::Event`] trait for the given `struct`. /// - /// **Note** [`ink::Event`] requires a [`scale::Encode`] implementation, it is up to - /// the user to provide that: usually via the derive. + /// **Note** [`ink::Event`] requires [`scale::Encode`] implementation. /// /// Usually this is used in conjunction with the [`EventMetadata`] derive. /// @@ -1399,13 +1410,33 @@ synstructure::decl_derive!( /// } /// } /// - /// use ink::env::Event; - /// assert_ne!(::SIGNATURE_TOPIC, ::SIGNATURE_TOPIC); + /// assert_ne!(::SIGNATURE_TOPIC, ::SIGNATURE_TOPIC); /// ``` /// + /// ## Custom Signature + /// + /// Sometimes it is useful to specify the custom signature topic. + /// For example, when the event definition from the other contract is not accessible. + /// + /// The macro provides `#[ink(signature_topic = _)]` nested macro that allows to provide + /// 32 byte hex string of the custom signature topic. + /// + /// Generates custom signature topic + /// ``` + /// #[derive(ink::Event, scale::Encode)] + /// #[ink(signature_topic = "1111111111111111111111111111111111111111111111111111111111111111")] + /// pub struct MyCustomSignatureEvent { + /// pub field: u32, + /// pub topic: [u8; 32], + /// } + /// + /// assert_eq!(Some([17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17]), + /// ::SIGNATURE_TOPIC) + ///``` /// ## Anonymous Events /// /// If the event is annotated with `#[ink(anonymous)]` then no signature topic is generated. + /// `#[ink(signature_topic = _)]` should not be used. event::event_derive ); @@ -1414,8 +1445,8 @@ synstructure::decl_derive!( /// Derives the [`ink::EventMetadata`] trait for the given `struct`, which provides metadata /// about the event definition. /// - /// Requires that the `struct` also implements the [`ink::Event`] trait, so this derive is - /// usually used in combination with the [`Event`] derive. + /// Requires that the `struct` also implements the [`ink::Event`] trait, + /// so this derive is usually used in combination with the [`Event`] derive. /// /// Metadata is not embedded into the contract binary, it is generated from a separate /// compilation of the contract with the `std` feature, therefore this derive must be diff --git a/crates/ink/macro/src/tests/event.rs b/crates/ink/macro/src/tests/event.rs index 926f93ba84f..056498c2231 100644 --- a/crates/ink/macro/src/tests/event.rs +++ b/crates/ink/macro/src/tests/event.rs @@ -185,3 +185,42 @@ fn struct_with_fields_and_some_topics() { } no_build } } + +#[test] +fn custom_signature_topic() { + crate::test_derive! { + event_derive { + #[derive(scale::Encode)] + #[ink(signature_topic = "1111111111111111111111111111111111111111111111111111111111111111")] + struct UnitStruct; + } + expands to { + const _: () = { + impl ::ink::env::Event for UnitStruct { + type RemainingTopics = [::ink::env::event::state::HasRemainingTopics; 1usize]; + + const SIGNATURE_TOPIC: ::core::option::Option<[::core::primitive::u8; 32]> = + ::core::option::Option::Some( [17u8, 17u8, 17u8, 17u8, 17u8, 17u8, 17u8, 17u8, 17u8, 17u8, 17u8, 17u8, 17u8, 17u8, 17u8, 17u8, 17u8, 17u8, 17u8, 17u8, 17u8, 17u8, 17u8, 17u8, 17u8, 17u8, 17u8, 17u8, 17u8, 17u8, 17u8, 17u8] ); + + fn topics( + &self, + builder: ::ink::env::event::TopicsBuilder<::ink::env::event::state::Uninit, E, B>, + ) -> >::Output + where + E: ::ink::env::Environment, + B: ::ink::env::event::TopicsBuilderBackend, + { + match self { + UnitStruct => { + builder + .build::() + .push_topic(Self::SIGNATURE_TOPIC.as_ref()) + .finish() + } + } + } + } + }; + } no_build + } +} diff --git a/crates/ink/macro/src/tests/mod.rs b/crates/ink/macro/src/tests/mod.rs index 5d9df2e44db..1844132ad11 100644 --- a/crates/ink/macro/src/tests/mod.rs +++ b/crates/ink/macro/src/tests/mod.rs @@ -26,6 +26,7 @@ use crate::storage::{ storage_layout_derive, }; +#[cfg(test)] #[macro_export] macro_rules! test_derive { ($name:path { $($i:tt)* } expands to { $($o:tt)* }) => { diff --git a/crates/ink/tests/ui/event/fail/conficting_attributes.rs b/crates/ink/tests/ui/event/fail/conficting_attributes.rs new file mode 100644 index 00000000000..7fcd26e1603 --- /dev/null +++ b/crates/ink/tests/ui/event/fail/conficting_attributes.rs @@ -0,0 +1,8 @@ +#[ink::event] +#[ink(anonymous)] +#[ink(signature_topic = "1111111111111111111111111111111111111111111111111111111111111111")] +pub struct Event { + pub topic: [u8; 32], +} + +fn main() {} diff --git a/crates/ink/tests/ui/event/fail/conficting_attributes.stderr b/crates/ink/tests/ui/event/fail/conficting_attributes.stderr new file mode 100644 index 00000000000..bd368891655 --- /dev/null +++ b/crates/ink/tests/ui/event/fail/conficting_attributes.stderr @@ -0,0 +1,13 @@ +error: cannot specify `signature_topic` with `anonymous` in ink! event item configuration argument + --> tests/ui/event/fail/conficting_attributes.rs:3:7 + | +3 | #[ink(signature_topic = "1111111111111111111111111111111111111111111111111111111111111111")] + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +error[E0277]: the trait bound `Event: ink::ink_env::Event` is not satisfied + --> tests/ui/event/fail/conficting_attributes.rs:1:1 + | +1 | #[ink::event] + | ^^^^^^^^^^^^^ the trait `ink::ink_env::Event` is not implemented for `Event` + | + = note: this error originates in the attribute macro `ink::event` (in Nightly builds, run with -Z macro-backtrace for more info) diff --git a/crates/ink/tests/ui/event/fail/conficting_attributes_inline.rs b/crates/ink/tests/ui/event/fail/conficting_attributes_inline.rs new file mode 100644 index 00000000000..d9079a752db --- /dev/null +++ b/crates/ink/tests/ui/event/fail/conficting_attributes_inline.rs @@ -0,0 +1,7 @@ +#[ink::event] +#[ink(anonymous, signature_topic = "1111111111111111111111111111111111111111111111111111111111111111")] +pub struct Event { + pub topic: [u8; 32], +} + +fn main() {} diff --git a/crates/ink/tests/ui/event/fail/conficting_attributes_inline.stderr b/crates/ink/tests/ui/event/fail/conficting_attributes_inline.stderr new file mode 100644 index 00000000000..fb222f2865e --- /dev/null +++ b/crates/ink/tests/ui/event/fail/conficting_attributes_inline.stderr @@ -0,0 +1,13 @@ +error: cannot specify `signature_topic` with `anonymous` in ink! event item configuration argument + --> tests/ui/event/fail/conficting_attributes_inline.rs:2:18 + | +2 | #[ink(anonymous, signature_topic = "1111111111111111111111111111111111111111111111111111111111111111")] + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +error[E0277]: the trait bound `Event: ink::ink_env::Event` is not satisfied + --> tests/ui/event/fail/conficting_attributes_inline.rs:1:1 + | +1 | #[ink::event] + | ^^^^^^^^^^^^^ the trait `ink::ink_env::Event` is not implemented for `Event` + | + = note: this error originates in the attribute macro `ink::event` (in Nightly builds, run with -Z macro-backtrace for more info) diff --git a/crates/ink/tests/ui/event/fail/multiple_topic_args.stderr b/crates/ink/tests/ui/event/fail/multiple_topic_args.stderr index da54f029976..919dd2ed2bd 100644 --- a/crates/ink/tests/ui/event/fail/multiple_topic_args.stderr +++ b/crates/ink/tests/ui/event/fail/multiple_topic_args.stderr @@ -1,5 +1,5 @@ -error: Only a single `#[ink(topic)]` attribute allowed. - --> tests/ui/event/fail/multiple_topic_args.rs:4:5 +error: Only a single `#[ink(topic)]` is allowed + --> tests/ui/event/fail/multiple_topic_args.rs:4:11 | 4 | #[ink(topic)] - | ^^^^^^^^^^^^^ + | ^^^^^ diff --git a/crates/ink/tests/ui/event/fail/multiple_topic_args_inline.stderr b/crates/ink/tests/ui/event/fail/multiple_topic_args_inline.stderr index 92e8159645c..bb0e6ee4479 100644 --- a/crates/ink/tests/ui/event/fail/multiple_topic_args_inline.stderr +++ b/crates/ink/tests/ui/event/fail/multiple_topic_args_inline.stderr @@ -1,5 +1,5 @@ -error: Invalid `#[ink(topic)]` attribute: multiple arguments not allowed. - --> tests/ui/event/fail/multiple_topic_args_inline.rs:3:11 +error: Only a single `#[ink(topic)]` is allowed + --> tests/ui/event/fail/multiple_topic_args_inline.rs:3:18 | 3 | #[ink(topic, topic)] - | ^^^^^ + | ^^^^^ diff --git a/crates/ink/tests/ui/event/fail/signature_hex_short.rs b/crates/ink/tests/ui/event/fail/signature_hex_short.rs new file mode 100644 index 00000000000..e1f47889bb2 --- /dev/null +++ b/crates/ink/tests/ui/event/fail/signature_hex_short.rs @@ -0,0 +1,7 @@ +#[ink::event] +#[ink(signature_topic = "1111111111111111111111111111")] +pub struct Event { + pub topic: [u8; 32], +} + +fn main() {} diff --git a/crates/ink/tests/ui/event/fail/signature_hex_short.stderr b/crates/ink/tests/ui/event/fail/signature_hex_short.stderr new file mode 100644 index 00000000000..d5e5e7e9b81 --- /dev/null +++ b/crates/ink/tests/ui/event/fail/signature_hex_short.stderr @@ -0,0 +1,13 @@ +error: `signature_topic` is expected to be 32-byte hex string. Found 14 bytes + --> tests/ui/event/fail/signature_hex_short.rs:2:25 + | +2 | #[ink(signature_topic = "1111111111111111111111111111")] + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +error[E0277]: the trait bound `Event: ink::ink_env::Event` is not satisfied + --> tests/ui/event/fail/signature_hex_short.rs:1:1 + | +1 | #[ink::event] + | ^^^^^^^^^^^^^ the trait `ink::ink_env::Event` is not implemented for `Event` + | + = note: this error originates in the attribute macro `ink::event` (in Nightly builds, run with -Z macro-backtrace for more info) diff --git a/crates/ink/tests/ui/event/fail/unknown_attribute.rs b/crates/ink/tests/ui/event/fail/unknown_attribute.rs new file mode 100644 index 00000000000..f4be10da4cc --- /dev/null +++ b/crates/ink/tests/ui/event/fail/unknown_attribute.rs @@ -0,0 +1,8 @@ +#[ink::event] +#[ink(anonymous)] +#[ink(my_arg)] +pub struct Event { + pub topic: [u8; 32], +} + +fn main() {} diff --git a/crates/ink/tests/ui/event/fail/unknown_attribute.stderr b/crates/ink/tests/ui/event/fail/unknown_attribute.stderr new file mode 100644 index 00000000000..0cb23cee4e2 --- /dev/null +++ b/crates/ink/tests/ui/event/fail/unknown_attribute.stderr @@ -0,0 +1,13 @@ +error: encountered unknown or unsupported ink! event item configuration argument + --> tests/ui/event/fail/unknown_attribute.rs:3:7 + | +3 | #[ink(my_arg)] + | ^^^^^^ + +error[E0277]: the trait bound `Event: ink::ink_env::Event` is not satisfied + --> tests/ui/event/fail/unknown_attribute.rs:1:1 + | +1 | #[ink::event] + | ^^^^^^^^^^^^^ the trait `ink::ink_env::Event` is not implemented for `Event` + | + = note: this error originates in the attribute macro `ink::event` (in Nightly builds, run with -Z macro-backtrace for more info) diff --git a/crates/ink/tests/ui/event/pass/custom_signature_works.rs b/crates/ink/tests/ui/event/pass/custom_signature_works.rs new file mode 100644 index 00000000000..ba9275c8d1a --- /dev/null +++ b/crates/ink/tests/ui/event/pass/custom_signature_works.rs @@ -0,0 +1,9 @@ +#[ink::event] +#[ink(signature_topic = "1111111111111111111111111111111111111111111111111111111111111111")] +pub struct Event { + #[ink(topic)] + pub topic: [u8; 32], + pub field_1: u32, +} + +fn main() {} diff --git a/crates/ink/tests/ui/event/pass/inline_custom_signature_works.rs b/crates/ink/tests/ui/event/pass/inline_custom_signature_works.rs new file mode 100644 index 00000000000..d47711a6ecb --- /dev/null +++ b/crates/ink/tests/ui/event/pass/inline_custom_signature_works.rs @@ -0,0 +1,8 @@ +#[ink::event(signature_topic = "1111111111111111111111111111111111111111111111111111111111111111")] +pub struct Event { + #[ink(topic)] + pub topic: [u8; 32], + pub field_1: u32, +} + +fn main() {} diff --git a/integration-tests/events/lib.rs b/integration-tests/events/lib.rs index e1b56ae79a0..2fa51eafdcf 100644 --- a/integration-tests/events/lib.rs +++ b/integration-tests/events/lib.rs @@ -19,6 +19,14 @@ pub mod events { value: bool, } + #[ink( + event, + signature_topic = "1111111111111111111111111111111111111111111111111111111111111111" + )] + pub struct InlineCustomFlipped { + value: bool, + } + #[ink(event)] #[ink(anonymous)] pub struct InlineAnonymousEvent { @@ -49,6 +57,14 @@ pub mod events { self.env().emit_event(InlineFlipped { value: self.value }) } + /// Flips the current value of the boolean. + #[ink(message)] + pub fn flip_with_inline_custom_event(&mut self) { + self.value = !self.value; + self.env() + .emit_event(InlineCustomFlipped { value: self.value }) + } + /// Emit an event with a 32 byte topic. #[ink(message)] pub fn emit_32_byte_topic_event(&self, maybe_hash: Option<[u8; 32]>) { @@ -94,7 +110,7 @@ pub mod events { #[test] fn collects_specs_for_all_linked_and_used_events() { let event_specs = ink::metadata::collect_events(); - assert_eq!(7, event_specs.len()); + assert_eq!(8, event_specs.len()); assert!(event_specs .iter() @@ -102,6 +118,9 @@ pub mod events { assert!(event_specs .iter() .any(|evt| evt.label() == &"InlineFlipped")); + assert!(event_specs + .iter() + .any(|evt| evt.label() == &"InlineCustomFlipped")); assert!(event_specs .iter() .any(|evt| evt.label() == &"ThirtyTwoByteTopics")); @@ -179,6 +198,20 @@ pub mod events { assert_eq!(expected_topics, event.topics); } + #[ink::test] + fn custom_signature_topic() { + let mut events = Events::new(false); + events.flip_with_inline_custom_event(); + + let emitted_events = ink::env::test::recorded_events().collect::>(); + assert_eq!(1, emitted_events.len()); + + let signature_topic = + ::SIGNATURE_TOPIC; + + assert_eq!(Some([17u8; 32]), signature_topic); + } + #[ink::test] fn anonymous_events_emit_no_signature_topics() { let events = Events::new(false); @@ -195,6 +228,10 @@ pub mod events { let event = &emitted_events[1]; assert_eq!(event.topics.len(), 1); assert_eq!(event.topics[0], topic); + + let signature_topic = + ::SIGNATURE_TOPIC; + assert_eq!(None, signature_topic); } } @@ -339,5 +376,40 @@ pub mod events { Ok(()) } + + #[ink_e2e::test] + async fn emits_custom_signature_event( + mut client: Client, + ) -> E2EResult<()> { + // given + let init_value = false; + let mut constructor = EventsRef::new(init_value); + let contract = client + .instantiate("events", &ink_e2e::alice(), &mut constructor) + .submit() + .await + .expect("instantiate failed"); + let mut call = contract.call::(); + + // when + let call = call.flip_with_inline_custom_event(); + let call_res = client + .call(&ink_e2e::bob(), &call) + .submit() + .await + .expect("flip_with_inline_custom_event failed"); + + let contract_events = call_res.contract_emitted_events()?; + + // then + assert_eq!(1, contract_events.len()); + + let signature_topic = + ::SIGNATURE_TOPIC; + + assert_eq!(Some([17u8; 32]), signature_topic); + + Ok(()) + } } }