From 9bbeffd759635c9268d677576cc9ae68de07d546 Mon Sep 17 00:00:00 2001 From: Andrew Jones Date: Mon, 24 Jul 2023 15:12:26 +0100 Subject: [PATCH] Events `2.0` (#1827) * WIP derive Event * Move tests to top level * Add failing Event derive test * Test compiles, now fails * Passing test, now impl * Passing test with no fields * Add events integration-tests example * Expose Event derive macro at the top level ::ink::Event * Remove println! * Remove ContractEventBase * WIP rewiring event codegen for use with derive. * Direct impl of emit_event * Use bound_impl * WIP impl derive EventMetadata * WIP add event metadata derive * Add some metadata derive todos * WIP Collect metadata from linked events * Refactor event metadata collection * Add metadata test * Check for external and Inline events * Add todo for inline events * WIP adding compile time topics check to emit_event. * fmt * Refactor EventRespectsTopicsLimit to use const assertion * Fmt * Remove unused code from legacy events generation * test and generate SIGNATURE topic * use built in ink macro for generating signature topic * Remove all warnings * Implement anonymous event * Remove PrefixedValue * Implement field topics * Use TOPICS_LEN for topics remaining * WIP don't push Option::None topics * Fix only publishing if not `Option::None` * Fix only publishing if not `Option::None` * Only publish value of `Some` topic, not none. * Fix event derive codegen tests * Add test for None topic * Rename Topics trait to Event * Fmt * Remove println * Remove EmitEvent imports * WIP adding E2E test for topics * Clippy * Unnused imports * E2E test contract events emitted * WIP e2e testing topics * Add e2e test which checks topics * Add signature topic to event spec metadata * Use Result in EventMetadata derive * Tests for generating events metadata * Remove max topics len compile time check * Add failing test for topics len validation at metadata generation time * Check for max topics limit breach in metadata generation * Add extra event with no signature topic * Remove checks for generics and pub visibility * Fix anonymous attr * Move topics attr validation to derive macro * Fmt and remove remaining MAX_TOPICS check * Remove event spec field type name * Clippy * WIP add `#[ink::event]` which expands to derives * Check for signature topics collisions when building metadata * Commentl * Remove EventRespectsTopicLimits * Remove cfg attr test * Remove unused topics attr test * Fmt * Fix trait erc20 tests * Fix erc20 tests and fmt * Fix duplicate inline attrs. Remove offchain duplicate topics check. * Fmt * Clippy * clippy * events clippy * Push 0 topic if `None` * Expose signature_topic * Change SignatureTopic to expose as_bytes * Add test for None topic value * Fix erc20 topic tests * Add module_path * Fix max topics in tests * Fix event docs test * Clippy * Use path dependency for ink_env * Remove ink_env dependency * RUSTFLAGS for test to fix linking issue * RUSTFLAGS for test to fix linking issue * Fix custom environment, remove extra topic * Use ink::event syntax * Add ui test for cfg attributes * Add success ui tests for ink::event * UI test for `enum` should fail * Refactor topic attribute fn to accept BindingInfo * Fmt * Remove commented out code * Event docs * Add event field docs * Fix event param spec doc tests * Fix metadata codegen tests * Implement docs for `Event` derive macro * Spellcheck * Check for multiple ink attributes * Spellcheck * Ink attribute validation * Remove Topic * Oops * Oops again * Test for events in different crates being used in the metadata. * Test for unused event not included in metadata * Add test for inline event definition metadata. * Add test for emitting inline event * Fmt * Remove todo, duplicate attributes are checked in the derive impls * Add docs * Fix examples-test * Add docs for `#[ink::event]` * Add docs for `EventMetadata` trait * Update Event trait comments * Docs * SIGNATURE_TOPIC docs * Fix docs * fmt * Try setting lto = "thin" for metadata crate to fix met * Add lto to test profile of unit test * Add test to check disallow generics for Event derive * Fmt * Change to `#[ink::event(anonymous = true)]` syntax * Fix test * Annotate derived event struct with anon * UI test * Document limitation of signature topic derive * Fix doctest * Fix up errors after merge * Usage of the crate includes defined there events (#1842) * Usage of the crate includes defined there events * Inclusion of the event from `event_def_unused` is expected * Update anonymous syntax * Anonymous comment * Move haashing macro inline signature topic gen fn * Fix signature_topic return type --------- Co-authored-by: Green Baneling --- .gitlab-ci.yml | 8 + crates/e2e/src/client.rs | 87 ++- crates/e2e/src/events.rs | 102 ++++ crates/e2e/src/lib.rs | 1 + crates/env/src/api.rs | 8 +- crates/env/src/backend.rs | 6 +- crates/env/src/engine/off_chain/impls.rs | 16 +- crates/env/src/engine/off_chain/tests.rs | 2 +- crates/env/src/engine/on_chain/impls.rs | 12 +- crates/env/src/{topics.rs => event.rs} | 58 +- crates/env/src/lib.rs | 6 +- crates/ink/codegen/src/generator/contract.rs | 7 +- crates/ink/codegen/src/generator/event.rs | 41 ++ crates/ink/codegen/src/generator/events.rs | 301 ---------- .../ink/codegen/src/generator/item_impls.rs | 6 - crates/ink/codegen/src/generator/metadata.rs | 50 +- crates/ink/codegen/src/generator/mod.rs | 4 +- crates/ink/codegen/src/generator/storage.rs | 7 - crates/ink/codegen/src/lib.rs | 4 + crates/ink/ir/src/ir/attrs.rs | 12 - crates/ink/ir/src/ir/event/config.rs | 70 +++ crates/ink/ir/src/ir/event/mod.rs | 243 ++++++++ crates/ink/ir/src/ir/item/event.rs | 517 ------------------ crates/ink/ir/src/ir/item/mod.rs | 6 +- crates/ink/ir/src/ir/mod.rs | 3 +- crates/ink/macro/src/event/metadata.rs | 106 ++++ crates/ink/macro/src/event/mod.rs | 229 ++++++++ crates/ink/macro/src/lib.rs | 157 ++++++ crates/ink/macro/src/storage/mod.rs | 3 - crates/ink/macro/src/tests/event.rs | 187 +++++++ crates/ink/macro/src/tests/event_metadata.rs | 150 +++++ .../ink/macro/src/{storage => }/tests/mod.rs | 10 +- .../macro/src/{storage => }/tests/storable.rs | 0 .../src/{storage => }/tests/storable_hint.rs | 0 .../src/{storage => }/tests/storage_key.rs | 0 .../src/{storage => }/tests/storage_layout.rs | 0 crates/ink/src/codegen/event/emit.rs | 26 - crates/ink/src/codegen/event/mod.rs | 26 - crates/ink/src/codegen/event/topics.rs | 88 --- crates/ink/src/codegen/mod.rs | 8 - crates/ink/src/env_access.rs | 8 + crates/ink/src/lib.rs | 7 + crates/ink/src/option_info.rs | 77 +++ crates/ink/src/reflect/event.rs | 52 -- crates/ink/src/reflect/mod.rs | 2 - crates/ink/tests/compile_tests.rs | 3 + crates/ink/tests/events_metadata.rs | 107 ++++ .../fail/event-too-many-topics-anonymous.rs | 60 -- .../event-too-many-topics-anonymous.stderr | 21 - .../ui/contract/fail/event-too-many-topics.rs | 56 -- .../fail/event-too-many-topics.stderr | 21 - .../tests/ui/event/fail/cfg_attr_on_field.rs | 7 + .../ui/event/fail/cfg_attr_on_field.stderr | 5 + .../tests/ui/event/fail/cfg_attr_on_topic.rs | 8 + .../ui/event/fail/cfg_attr_on_topic.stderr | 5 + crates/ink/tests/ui/event/fail/event_enum.rs | 10 + .../ink/tests/ui/event/fail/event_enum.stderr | 17 + crates/ink/tests/ui/event/fail/generics.rs | 7 + .../ink/tests/ui/event/fail/generics.stderr | 5 + .../ui/event/fail/multiple_topic_args.rs | 8 + .../ui/event/fail/multiple_topic_args.stderr | 5 + .../event/fail/multiple_topic_args_inline.rs | 7 + .../fail/multiple_topic_args_inline.stderr | 5 + .../ui/event/pass/anonymous_flag_works.rs | 8 + .../ui/event/pass/event_attribute_works.rs | 8 + crates/ink/tests/unique_topics.rs | 101 ---- crates/metadata/Cargo.toml | 1 + crates/metadata/src/lib.rs | 28 + crates/metadata/src/specs.rs | 159 +++++- crates/metadata/src/tests.rs | 181 +++++- integration-tests/custom-environment/lib.rs | 15 +- integration-tests/erc20/lib.rs | 87 +-- integration-tests/events/.gitignore | 9 + integration-tests/events/Cargo.toml | 39 ++ .../events/event-def-unused/Cargo.toml | 17 + .../events/event-def-unused/src/lib.rs | 15 + integration-tests/events/event-def/Cargo.toml | 17 + integration-tests/events/event-def/src/lib.rs | 14 + .../events/event-def2/Cargo.toml | 17 + .../events/event-def2/src/lib.rs | 9 + integration-tests/events/lib.rs | 330 +++++++++++ integration-tests/trait-erc20/lib.rs | 74 +-- 82 files changed, 2574 insertions(+), 1625 deletions(-) create mode 100644 crates/e2e/src/events.rs rename crates/env/src/{topics.rs => event.rs} (81%) create mode 100644 crates/ink/codegen/src/generator/event.rs delete mode 100644 crates/ink/codegen/src/generator/events.rs create mode 100644 crates/ink/ir/src/ir/event/config.rs create mode 100644 crates/ink/ir/src/ir/event/mod.rs delete mode 100644 crates/ink/ir/src/ir/item/event.rs create mode 100644 crates/ink/macro/src/event/metadata.rs create mode 100644 crates/ink/macro/src/event/mod.rs create mode 100644 crates/ink/macro/src/tests/event.rs create mode 100644 crates/ink/macro/src/tests/event_metadata.rs rename crates/ink/macro/src/{storage => }/tests/mod.rs (91%) rename crates/ink/macro/src/{storage => }/tests/storable.rs (100%) rename crates/ink/macro/src/{storage => }/tests/storable_hint.rs (100%) rename crates/ink/macro/src/{storage => }/tests/storage_key.rs (100%) rename crates/ink/macro/src/{storage => }/tests/storage_layout.rs (100%) delete mode 100644 crates/ink/src/codegen/event/emit.rs delete mode 100644 crates/ink/src/codegen/event/mod.rs delete mode 100644 crates/ink/src/codegen/event/topics.rs create mode 100644 crates/ink/src/option_info.rs delete mode 100644 crates/ink/src/reflect/event.rs create mode 100644 crates/ink/tests/events_metadata.rs delete mode 100644 crates/ink/tests/ui/contract/fail/event-too-many-topics-anonymous.rs delete mode 100644 crates/ink/tests/ui/contract/fail/event-too-many-topics-anonymous.stderr delete mode 100644 crates/ink/tests/ui/contract/fail/event-too-many-topics.rs delete mode 100644 crates/ink/tests/ui/contract/fail/event-too-many-topics.stderr create mode 100644 crates/ink/tests/ui/event/fail/cfg_attr_on_field.rs create mode 100644 crates/ink/tests/ui/event/fail/cfg_attr_on_field.stderr create mode 100644 crates/ink/tests/ui/event/fail/cfg_attr_on_topic.rs create mode 100644 crates/ink/tests/ui/event/fail/cfg_attr_on_topic.stderr create mode 100644 crates/ink/tests/ui/event/fail/event_enum.rs create mode 100644 crates/ink/tests/ui/event/fail/event_enum.stderr create mode 100644 crates/ink/tests/ui/event/fail/generics.rs create mode 100644 crates/ink/tests/ui/event/fail/generics.stderr create mode 100644 crates/ink/tests/ui/event/fail/multiple_topic_args.rs create mode 100644 crates/ink/tests/ui/event/fail/multiple_topic_args.stderr create mode 100644 crates/ink/tests/ui/event/fail/multiple_topic_args_inline.rs create mode 100644 crates/ink/tests/ui/event/fail/multiple_topic_args_inline.stderr create mode 100644 crates/ink/tests/ui/event/pass/anonymous_flag_works.rs create mode 100644 crates/ink/tests/ui/event/pass/event_attribute_works.rs delete mode 100644 crates/ink/tests/unique_topics.rs create mode 100644 integration-tests/events/.gitignore create mode 100644 integration-tests/events/Cargo.toml create mode 100644 integration-tests/events/event-def-unused/Cargo.toml create mode 100644 integration-tests/events/event-def-unused/src/lib.rs create mode 100644 integration-tests/events/event-def/Cargo.toml create mode 100644 integration-tests/events/event-def/src/lib.rs create mode 100644 integration-tests/events/event-def2/Cargo.toml create mode 100644 integration-tests/events/event-def2/src/lib.rs create mode 100644 integration-tests/events/lib.rs diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 7167c80dd2b..985fcf1f35b 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -270,6 +270,8 @@ test: - job: check-std artifacts: false variables: + # Fix for linking of `linkme` for `cargo test`: https://github.com/dtolnay/linkme/issues/49 + RUSTFLAGS: "-Clink-arg=-z -Clink-arg=nostart-stop-gc" # Since we run the tests with `--all-features` this implies the feature # `ink-fuzz-tests` as well -- i.e. the fuzz tests are run. # There's no way to disable a single feature while enabling all features @@ -370,6 +372,9 @@ examples-test: needs: - job: clippy-std artifacts: false + variables: + # Fix linking of `linkme`: https://github.com/dtolnay/linkme/issues/49 + RUSTFLAGS: "-Clink-arg=-z -Clink-arg=nostart-stop-gc" script: - for example in integration-tests/*/; do if [ "$example" = "integration-tests/lang-err-integration-tests/" ]; then continue; fi; @@ -399,6 +404,9 @@ examples-contract-build: stage: examples <<: *docker-env <<: *test-refs + variables: + # Fix for linking of `linkme` for `cargo contract build`: https://github.com/dtolnay/linkme/issues/49 + RUSTFLAGS: "-Clink-arg=-z -Clink-arg=nostart-stop-gc" script: - rustup component add rust-src --toolchain stable - cargo contract -V diff --git a/crates/e2e/src/client.rs b/crates/e2e/src/client.rs index 8c221c7f43d..7d4c56f3af9 100644 --- a/crates/e2e/src/client.rs +++ b/crates/e2e/src/client.rs @@ -17,6 +17,11 @@ use super::{ constructor_exec_input, CreateBuilderPartial, }, + events::{ + CodeStoredEvent, + ContractInstantiatedEvent, + EventWithTopics, + }, log_error, log_info, sr25519, @@ -49,18 +54,15 @@ use std::{ path::PathBuf, }; +use crate::events; use subxt::{ blocks::ExtrinsicEvents, config::ExtrinsicParams, events::EventDetails, - ext::{ - scale_decode, - scale_encode, - scale_value::{ - Composite, - Value, - ValueDef, - }, + ext::scale_value::{ + Composite, + Value, + ValueDef, }, tx::PairSigner, }; @@ -80,54 +82,6 @@ pub type CallBuilderFinal = ink_env::call::CallBuilder< Set>, >; -/// A contract was successfully instantiated. -#[derive( - Debug, - scale::Decode, - scale::Encode, - scale_decode::DecodeAsType, - scale_encode::EncodeAsType, -)] -#[decode_as_type(trait_bounds = "", crate_path = "subxt::ext::scale_decode")] -#[encode_as_type(crate_path = "subxt::ext::scale_encode")] -struct ContractInstantiatedEvent { - /// Account id of the deployer. - pub deployer: E::AccountId, - /// Account id where the contract was instantiated to. - pub contract: E::AccountId, -} - -impl subxt::events::StaticEvent for ContractInstantiatedEvent -where - E: Environment, -{ - const PALLET: &'static str = "Contracts"; - const EVENT: &'static str = "Instantiated"; -} - -/// Code with the specified hash has been stored. -#[derive( - Debug, - scale::Decode, - scale::Encode, - scale_decode::DecodeAsType, - scale_encode::EncodeAsType, -)] -#[decode_as_type(trait_bounds = "", crate_path = "subxt::ext::scale_decode")] -#[encode_as_type(crate_path = "subxt::ext::scale_encode")] -struct CodeStoredEvent { - /// Hash under which the contract code was stored. - pub code_hash: E::Hash, -} - -impl subxt::events::StaticEvent for CodeStoredEvent -where - E: Environment, -{ - const PALLET: &'static str = "Contracts"; - const EVENT: &'static str = "CodeStored"; -} - /// The `Client` takes care of communicating with the node. /// /// This node's RPC interface will be used for instantiating the contract @@ -726,4 +680,25 @@ impl CallResult> { event.pallet_name() == pallet_name && event.variant_name() == variant_name }) } + + /// Returns all the `ContractEmitted` events emitted by the contract. + pub fn contract_emitted_events( + &self, + ) -> Result>>, subxt::Error> + where + C::Hash: Into, + { + let mut events_with_topics = Vec::new(); + for event in self.events.iter() { + let event = event?; + if let Some(decoded_event) = event.as_event::>()? { + let event_with_topics = EventWithTopics { + event: decoded_event, + topics: event.topics().iter().cloned().map(Into::into).collect(), + }; + events_with_topics.push(event_with_topics); + } + } + Ok(events_with_topics) + } } diff --git a/crates/e2e/src/events.rs b/crates/e2e/src/events.rs new file mode 100644 index 00000000000..0bf7ff54c75 --- /dev/null +++ b/crates/e2e/src/events.rs @@ -0,0 +1,102 @@ +// Copyright 2018-2022 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 ink_env::Environment; +#[cfg(feature = "std")] +use std::fmt::Debug; + +use subxt::{ + events::StaticEvent, + ext::{ + scale_decode, + scale_encode, + }, +}; + +/// A contract was successfully instantiated. +#[derive( + Debug, + scale::Decode, + scale::Encode, + scale_decode::DecodeAsType, + scale_encode::EncodeAsType, +)] +#[decode_as_type(trait_bounds = "", crate_path = "subxt::ext::scale_decode")] +#[encode_as_type(crate_path = "subxt::ext::scale_encode")] +pub struct ContractInstantiatedEvent { + /// Account id of the deployer. + pub deployer: E::AccountId, + /// Account id where the contract was instantiated to. + pub contract: E::AccountId, +} + +impl StaticEvent for ContractInstantiatedEvent +where + E: Environment, +{ + const PALLET: &'static str = "Contracts"; + const EVENT: &'static str = "Instantiated"; +} + +/// Code with the specified hash has been stored. +#[derive( + Debug, + scale::Decode, + scale::Encode, + scale_decode::DecodeAsType, + scale_encode::EncodeAsType, +)] +#[decode_as_type(trait_bounds = "", crate_path = "subxt::ext::scale_decode")] +#[encode_as_type(crate_path = "subxt::ext::scale_encode")] +pub struct CodeStoredEvent { + /// Hash under which the contract code was stored. + pub code_hash: E::Hash, +} + +impl StaticEvent for CodeStoredEvent +where + E: Environment, +{ + const PALLET: &'static str = "Contracts"; + const EVENT: &'static str = "CodeStored"; +} + +#[derive( + scale::Decode, + scale::Encode, + scale_decode::DecodeAsType, + scale_encode::EncodeAsType, + Debug, +)] +#[decode_as_type(trait_bounds = "", crate_path = "subxt::ext::scale_decode")] +#[encode_as_type(crate_path = "subxt::ext::scale_encode")] +/// A custom event emitted by the contract. +pub struct ContractEmitted { + pub contract: E::AccountId, + pub data: Vec, +} + +impl StaticEvent for ContractEmitted +where + E: Environment, +{ + const PALLET: &'static str = "Contracts"; + const EVENT: &'static str = "ContractEmitted"; +} + +/// A decoded event with its associated topics. +pub struct EventWithTopics { + pub topics: Vec, + pub event: T, +} diff --git a/crates/e2e/src/lib.rs b/crates/e2e/src/lib.rs index 795c3411cc4..8da67d3724e 100644 --- a/crates/e2e/src/lib.rs +++ b/crates/e2e/src/lib.rs @@ -24,6 +24,7 @@ mod client; mod contract_results; mod default_accounts; mod error; +pub mod events; mod node_proc; mod xts; diff --git a/crates/env/src/api.rs b/crates/env/src/api.rs index e0c59586aa6..7fc6e0ad757 100644 --- a/crates/env/src/api.rs +++ b/crates/env/src/api.rs @@ -32,11 +32,11 @@ use crate::{ EnvInstance, OnInstance, }, + event::Event, hash::{ CryptoHash, HashOutput, }, - topics::Topics, types::Gas, Environment, Result, @@ -175,13 +175,13 @@ where } /// Emits an event with the given event data. -pub fn emit_event(event: Event) +pub fn emit_event(event: Evt) where E: Environment, - Event: Topics + scale::Encode, + Evt: Event, { ::on_instance(|instance| { - TypedEnvBackend::emit_event::(instance, event) + TypedEnvBackend::emit_event::(instance, event) }) } diff --git a/crates/env/src/backend.rs b/crates/env/src/backend.rs index c7ff7a0b6e5..b9988a52b82 100644 --- a/crates/env/src/backend.rs +++ b/crates/env/src/backend.rs @@ -21,11 +21,11 @@ use crate::{ DelegateCall, FromAccountId, }, + event::Event, hash::{ CryptoHash, HashOutput, }, - topics::Topics, Environment, Result, }; @@ -405,10 +405,10 @@ pub trait TypedEnvBackend: EnvBackend { /// # Note /// /// For more details visit: [`emit_event`][`crate::emit_event`] - fn emit_event(&mut self, event: Event) + fn emit_event(&mut self, event: Evt) where E: Environment, - Event: Topics + scale::Encode; + Evt: Event; /// Invokes a contract message and returns its result. /// diff --git a/crates/env/src/engine/off_chain/impls.rs b/crates/env/src/engine/off_chain/impls.rs index 1970b28041c..1cb8444c838 100644 --- a/crates/env/src/engine/off_chain/impls.rs +++ b/crates/env/src/engine/off_chain/impls.rs @@ -22,6 +22,10 @@ use crate::{ DelegateCall, FromAccountId, }, + event::{ + Event, + TopicsBuilderBackend, + }, hash::{ Blake2x128, Blake2x256, @@ -30,10 +34,6 @@ use crate::{ Keccak256, Sha2x256, }, - topics::{ - Topics, - TopicsBuilderBackend, - }, Clear, EnvBackend, Environment, @@ -150,10 +150,6 @@ where } let off_hash = result.as_ref(); let off_hash = off_hash.to_vec(); - debug_assert!( - !self.topics.contains(&off_hash), - "duplicate topic hash discovered!" - ); self.topics.push(off_hash); } @@ -422,10 +418,10 @@ impl TypedEnvBackend for EnvInstance { }) } - fn emit_event(&mut self, event: Event) + fn emit_event(&mut self, event: Evt) where E: Environment, - Event: Topics + scale::Encode, + Evt: Event, { let builder = TopicsBuilder::default(); let enc_topics = event.topics::(builder.into()); diff --git a/crates/env/src/engine/off_chain/tests.rs b/crates/env/src/engine/off_chain/tests.rs index f94b6001f8c..170dab02013 100644 --- a/crates/env/src/engine/off_chain/tests.rs +++ b/crates/env/src/engine/off_chain/tests.rs @@ -14,7 +14,7 @@ use crate::{ engine::off_chain::impls::TopicsBuilder, - topics::TopicsBuilderBackend, + event::TopicsBuilderBackend, Result, }; diff --git a/crates/env/src/engine/on_chain/impls.rs b/crates/env/src/engine/on_chain/impls.rs index b21ccea386e..78cd0320cf2 100644 --- a/crates/env/src/engine/on_chain/impls.rs +++ b/crates/env/src/engine/on_chain/impls.rs @@ -27,6 +27,10 @@ use crate::{ DelegateCall, FromAccountId, }, + event::{ + Event, + TopicsBuilderBackend, + }, hash::{ Blake2x128, Blake2x256, @@ -35,10 +39,6 @@ use crate::{ Keccak256, Sha2x256, }, - topics::{ - Topics, - TopicsBuilderBackend, - }, Clear, EnvBackend, Environment, @@ -392,10 +392,10 @@ impl TypedEnvBackend for EnvInstance { self.get_property_little_endian::(ext::minimum_balance) } - fn emit_event(&mut self, event: Event) + fn emit_event(&mut self, event: Evt) where E: Environment, - Event: Topics + scale::Encode, + Evt: Event, { let (mut scope, enc_topics) = event.topics::(TopicsBuilder::from(self.scoped_buffer()).into()); diff --git a/crates/env/src/topics.rs b/crates/env/src/event.rs similarity index 81% rename from crates/env/src/topics.rs rename to crates/env/src/event.rs index a55eac11b65..cda111d739a 100644 --- a/crates/env/src/topics.rs +++ b/crates/env/src/event.rs @@ -82,11 +82,11 @@ where /// to serialize. /// /// The number of expected topics is given implicitly by the `E` type parameter. - pub fn build( + pub fn build( mut self, - ) -> TopicsBuilder<::RemainingTopics, E, B> { + ) -> TopicsBuilder<::RemainingTopics, E, B> { self.backend - .expect(<::RemainingTopics as EventTopicsAmount>::AMOUNT); + .expect(<::RemainingTopics as EventTopicsAmount>::AMOUNT); TopicsBuilder { backend: self.backend, state: Default::default(), @@ -106,12 +106,17 @@ where /// than before the call. pub fn push_topic( mut self, - value: &T, + value: Option<&T>, ) -> TopicsBuilder<::Next, E, B> where T: scale::Encode, { - self.backend.push_topic(value); + // Only publish the topic if it is not an `Option::None`. + if let Some(topic) = value { + self.backend.push_topic::(topic); + } else { + self.backend.push_topic::(&0u8); + } TopicsBuilder { backend: self.backend, state: Default::default(), @@ -189,12 +194,18 @@ impl EventTopicsAmount for state::NoRemainingTopics { /// Implemented by event types to guide the event topic serialization using the topics /// builder. /// -/// Normally this trait should be implemented automatically via the ink! codegen. -pub trait Topics { +/// Normally this trait should be implemented automatically via `#[derive(ink::Event)`. +pub trait Event: scale::Encode { /// Type state indicating how many event topics are to be expected by the topics /// builder. type RemainingTopics: EventTopicsAmount; + /// The unique signature topic of the event. `None` for anonymous events. + /// + /// 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]>; + /// Guides event topic serialization using the given topics builder. fn topics( &self, @@ -204,36 +215,3 @@ pub trait Topics { E: Environment, B: TopicsBuilderBackend; } - -/// For each topic a hash is generated. This hash must be unique -/// for a field and its value. The `prefix` is concatenated -/// with the `value`. This result is then hashed. -/// The `prefix` is typically set to the path a field has in -/// an event struct plus the identifier of the event struct. -/// -/// For example, in the case of our ERC-20 example contract the -/// prefix `Erc20::Transfer::from` is concatenated with the -/// field value of `from` and then hashed. -/// In this example `Erc20` would be the contract identified, -/// `Transfer` the event identifier, and `from` the field identifier. -#[doc(hidden)] -pub struct PrefixedValue<'a, 'b, T> { - pub prefix: &'a [u8], - pub value: &'b T, -} - -impl scale::Encode for PrefixedValue<'_, '_, X> -where - X: scale::Encode, -{ - #[inline] - fn size_hint(&self) -> usize { - self.prefix.size_hint() + self.value.size_hint() - } - - #[inline] - fn encode_to(&self, dest: &mut T) { - self.prefix.encode_to(dest); - self.value.encode_to(dest); - } -} diff --git a/crates/env/src/lib.rs b/crates/env/src/lib.rs index 80bccd066cb..46bf445f06d 100644 --- a/crates/env/src/lib.rs +++ b/crates/env/src/lib.rs @@ -85,9 +85,9 @@ pub mod chain_extension; mod contract; mod engine; mod error; -pub mod hash; #[doc(hidden)] -pub mod topics; +pub mod event; +pub mod hash; mod types; #[cfg(test)] @@ -115,7 +115,7 @@ pub use self::{ Error, Result, }, - topics::Topics, + event::Event, types::{ AccountIdGuard, DefaultEnvironment, diff --git a/crates/ink/codegen/src/generator/contract.rs b/crates/ink/codegen/src/generator/contract.rs index 1662b826428..cb3ca9727c3 100644 --- a/crates/ink/codegen/src/generator/contract.rs +++ b/crates/ink/codegen/src/generator/contract.rs @@ -38,7 +38,10 @@ impl GenerateCode for Contract<'_> { let vis = module.vis(); let env = self.generate_code_using::(); let storage = self.generate_code_using::(); - let events = self.generate_code_using::(); + let events = module.events().map(move |event| { + let event_generator = generator::Event::from(event); + event_generator.generate_code() + }); let dispatch2 = self.generate_code_using::(); let item_impls = self.generate_code_using::(); let metadata = self.generate_code_using::(); @@ -55,7 +58,7 @@ impl GenerateCode for Contract<'_> { #vis mod #ident { #env #storage - #events + #( #events )* #dispatch2 #item_impls #contract_reference diff --git a/crates/ink/codegen/src/generator/event.rs b/crates/ink/codegen/src/generator/event.rs new file mode 100644 index 00000000000..7cf0e9e20ab --- /dev/null +++ b/crates/ink/codegen/src/generator/event.rs @@ -0,0 +1,41 @@ +// Copyright 2018-2022 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 crate::GenerateCode; +use derive_more::From; +use proc_macro2::TokenStream as TokenStream2; + +/// Generates code for the storage item. +#[derive(From, Copy, Clone)] +pub struct Event<'a> { + /// The storage item to generate code for. + item: &'a ir::Event, +} + +impl GenerateCode for Event<'_> { + /// Generates ink! storage item code. + fn generate_code(&self) -> TokenStream2 { + let item = self.item.item(); + let anonymous = self + .item + .anonymous() + .then(|| quote::quote! { #[ink(anonymous)] }); + quote::quote! ( + #[cfg_attr(feature = "std", derive(::ink::EventMetadata))] + #[derive(::ink::Event, ::scale::Encode, ::scale::Decode)] + #anonymous + #item + ) + } +} diff --git a/crates/ink/codegen/src/generator/events.rs b/crates/ink/codegen/src/generator/events.rs deleted file mode 100644 index 48f16b14ebe..00000000000 --- a/crates/ink/codegen/src/generator/events.rs +++ /dev/null @@ -1,301 +0,0 @@ -// Copyright 2018-2022 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 crate::GenerateCode; -use derive_more::From; -use proc_macro2::{ - Span, - TokenStream as TokenStream2, -}; -use quote::{ - quote, - quote_spanned, -}; -use syn::spanned::Spanned as _; - -/// Generates code for the ink! event structs of the contract. -#[derive(From)] -pub struct Events<'a> { - contract: &'a ir::Contract, -} -impl_as_ref_for_generator!(Events); - -impl GenerateCode for Events<'_> { - fn generate_code(&self) -> TokenStream2 { - if self.contract.module().events().next().is_none() { - // Generate no code in case there are no event definitions. - return TokenStream2::new() - } - let emit_event_trait_impl = self.generate_emit_event_trait_impl(); - let event_base = self.generate_event_base(); - let topic_guards = self.generate_topic_guards(); - let topics_impls = self.generate_topics_impls(); - let event_structs = self.generate_event_structs(); - quote! { - #emit_event_trait_impl - #event_base - #( #topic_guards )* - #( #event_structs )* - #( #topics_impls )* - } - } -} - -impl<'a> Events<'a> { - /// Used to allow emitting user defined events directly instead of converting - /// them first into the automatically generated base trait of the contract. - fn generate_emit_event_trait_impl(&self) -> TokenStream2 { - let storage_ident = &self.contract.module().storage().ident(); - quote! { - const _: () = { - impl<'a> ::ink::codegen::EmitEvent<#storage_ident> for ::ink::EnvAccess<'a, Environment> { - fn emit_event(self, event: E) - where - E: Into<<#storage_ident as ::ink::reflect::ContractEventBase>::Type>, - { - ::ink::env::emit_event::< - Environment, - <#storage_ident as ::ink::reflect::ContractEventBase>::Type - >(event.into()); - } - } - }; - } - } - - /// Generates the base event enum that comprises all user defined events. - /// All emitted events are converted into a variant of this enum before being - /// serialized and emitted to apply their unique event discriminant (ID). - /// - /// # Developer Note - /// - /// The `__ink_dylint_EventBase` config attribute is used here to convey the - /// information that the generated enum is an ink! event to `dylint`. - fn generate_event_base(&self) -> TokenStream2 { - let storage_ident = &self.contract.module().storage().ident(); - let event_idents = self - .contract - .module() - .events() - .map(|event| event.ident()) - .collect::>(); - let event_idents_cfgs = self - .contract - .module() - .events() - .map(|event| event.get_cfg_attrs(event.span())) - .collect::>(); - let base_event_ident = - proc_macro2::Ident::new("__ink_EventBase", Span::call_site()); - quote! { - #[allow(non_camel_case_types)] - #[derive(::scale::Encode, ::scale::Decode)] - #[cfg(not(feature = "__ink_dylint_EventBase"))] - pub enum #base_event_ident { - #( - #( #event_idents_cfgs )* - #event_idents(#event_idents), - )* - } - - const _: () = { - impl ::ink::reflect::ContractEventBase for #storage_ident { - type Type = #base_event_ident; - } - }; - - #( - #( #event_idents_cfgs )* - const _: () = { - impl From<#event_idents> for #base_event_ident { - fn from(event: #event_idents) -> Self { - Self::#event_idents(event) - } - } - }; - )* - - const _: () = { - pub enum __ink_UndefinedAmountOfTopics {} - impl ::ink::env::topics::EventTopicsAmount for __ink_UndefinedAmountOfTopics { - const AMOUNT: usize = 0; - } - - impl ::ink::env::Topics for #base_event_ident { - type RemainingTopics = __ink_UndefinedAmountOfTopics; - - fn topics( - &self, - builder: ::ink::env::topics::TopicsBuilder<::ink::env::topics::state::Uninit, E, B>, - ) -> >::Output - where - E: ::ink::env::Environment, - B: ::ink::env::topics::TopicsBuilderBackend, - { - match self { - #( - #( #event_idents_cfgs )* - Self::#event_idents(event) => { - <#event_idents as ::ink::env::Topics>::topics::(event, builder) - } - )*, - _ => panic!("Event does not exist!") - } - } - } - }; - } - } - - /// Generate checks to guard against too many topics in event definitions. - fn generate_topics_guard(&self, event: &ir::Event) -> TokenStream2 { - let span = event.span(); - let storage_ident = self.contract.module().storage().ident(); - let event_ident = event.ident(); - let cfg_attrs = event.get_cfg_attrs(span); - let len_topics = event.fields().filter(|event| event.is_topic).count(); - let max_len_topics = quote_spanned!(span=> - <<#storage_ident as ::ink::env::ContractEnv>::Env - as ::ink::env::Environment>::MAX_EVENT_TOPICS - ); - quote_spanned!(span=> - #( #cfg_attrs )* - impl ::ink::codegen::EventLenTopics for #event_ident { - type LenTopics = ::ink::codegen::EventTopics<#len_topics>; - } - - #( #cfg_attrs )* - const _: () = ::ink::codegen::utils::consume_type::< - ::ink::codegen::EventRespectsTopicLimit< - #event_ident, - { #max_len_topics }, - > - >(); - ) - } - - /// Generates the guard code that protects against having too many topics defined on - /// an ink! event. - fn generate_topic_guards(&'a self) -> impl Iterator + 'a { - self.contract.module().events().map(move |event| { - let span = event.span(); - let topics_guard = self.generate_topics_guard(event); - quote_spanned!(span => - #topics_guard - ) - }) - } - - /// Generates the `Topics` trait implementations for the user defined events. - fn generate_topics_impls(&'a self) -> impl Iterator + 'a { - let contract_ident = self.contract.module().storage().ident(); - self.contract.module().events().map(move |event| { - let span = event.span(); - let event_ident = event.ident(); - let event_signature = syn::LitByteStr::new( - format!("{contract_ident}::{event_ident}" - ).as_bytes(), span); - let len_event_signature = event_signature.value().len(); - let len_topics = event.fields().filter(|field| field.is_topic).count(); - let topic_impls = event - .fields() - .enumerate() - .filter(|(_, field)| field.is_topic) - .map(|(n, topic_field)| { - let span = topic_field.span(); - let field_ident = topic_field - .ident() - .map(quote::ToTokens::into_token_stream) - .unwrap_or_else(|| quote_spanned!(span => #n)); - let field_type = topic_field.ty(); - let signature = syn::LitByteStr::new( - format!("{contract_ident}::{event_ident}::{field_ident}" - ).as_bytes(), span); - quote_spanned!(span => - .push_topic::<::ink::env::topics::PrefixedValue<#field_type>>( - &::ink::env::topics::PrefixedValue { value: &self.#field_ident, prefix: #signature } - ) - ) - }); - // Only include topic for event signature in case of non-anonymous event. - let event_signature_topic = match event.anonymous { - true => None, - false => Some(quote_spanned!(span=> - .push_topic::<::ink::env::topics::PrefixedValue<[u8; #len_event_signature]>>( - &::ink::env::topics::PrefixedValue { value: #event_signature, prefix: b"" } - ) - )) - }; - // Anonymous events require 1 fewer topics since they do not include their signature. - let anonymous_topics_offset = usize::from(!event.anonymous); - let remaining_topics_ty = match len_topics + anonymous_topics_offset { - 0 => quote_spanned!(span=> ::ink::env::topics::state::NoRemainingTopics), - n => quote_spanned!(span=> [::ink::env::topics::state::HasRemainingTopics; #n]), - }; - let cfg_attrs = event.get_cfg_attrs(span); - quote_spanned!(span => - #( #cfg_attrs )* - const _: () = { - impl ::ink::env::Topics for #event_ident { - type RemainingTopics = #remaining_topics_ty; - - fn topics( - &self, - builder: ::ink::env::topics::TopicsBuilder<::ink::env::topics::state::Uninit, E, B>, - ) -> >::Output - where - E: ::ink::env::Environment, - B: ::ink::env::topics::TopicsBuilderBackend, - { - builder - .build::() - #event_signature_topic - #( - #topic_impls - )* - .finish() - } - } - }; - ) - }) - } - - /// Generates all the user defined event struct definitions. - fn generate_event_structs(&'a self) -> impl Iterator + 'a { - self.contract.module().events().map(move |event| { - let span = event.span(); - let ident = event.ident(); - let attrs = event.attrs(); - let fields = event.fields().map(|event_field| { - let span = event_field.span(); - let attrs = event_field.attrs(); - let vis = event_field.vis(); - let ident = event_field.ident(); - let ty = event_field.ty(); - quote_spanned!(span=> - #( #attrs )* - #vis #ident : #ty - ) - }); - quote_spanned!(span => - #( #attrs )* - #[derive(scale::Encode, scale::Decode)] - pub struct #ident { - #( #fields ),* - } - ) - }) - } -} diff --git a/crates/ink/codegen/src/generator/item_impls.rs b/crates/ink/codegen/src/generator/item_impls.rs index ca472091c88..ab4f2395032 100644 --- a/crates/ink/codegen/src/generator/item_impls.rs +++ b/crates/ink/codegen/src/generator/item_impls.rs @@ -46,16 +46,10 @@ impl GenerateCode for ItemImpls<'_> { .map(|item_impl| self.generate_item_impl(item_impl)); let inout_guards = self.generate_input_output_guards(); let trait_message_property_guards = self.generate_trait_message_property_guards(); - let use_emit_event = - self.contract.module().events().next().is_some().then(|| { - // Required to make `self.env().emit_event(...)` syntax available. - quote! { use ::ink::codegen::EmitEvent as _; } - }); quote! { const _: () = { // Required to make `self.env()` and `Self::env()` syntax available. use ::ink::codegen::{Env as _, StaticEnv as _}; - #use_emit_event #( #item_impls )* #inout_guards diff --git a/crates/ink/codegen/src/generator/metadata.rs b/crates/ink/codegen/src/generator/metadata.rs index 260867533a7..83f1f26c471 100644 --- a/crates/ink/codegen/src/generator/metadata.rs +++ b/crates/ink/codegen/src/generator/metadata.rs @@ -88,7 +88,6 @@ impl Metadata<'_> { fn generate_contract(&self) -> TokenStream2 { let constructors = self.generate_constructors(); let messages = self.generate_messages(); - let events = self.generate_events(); let docs = self .contract .module() @@ -108,9 +107,7 @@ impl Metadata<'_> { .messages([ #( #messages ),* ]) - .events([ - #( #events ),* - ]) + .collect_events() .docs([ #( #docs ),* ]) @@ -374,51 +371,6 @@ impl Metadata<'_> { ) } - /// Generates ink! metadata for all user provided ink! event definitions. - fn generate_events(&self) -> impl Iterator + '_ { - self.contract.module().events().map(|event| { - let span = event.span(); - let ident = event.ident(); - let docs = event.attrs().iter().filter_map(|attr| attr.extract_docs()); - let args = Self::generate_event_args(event); - let cfg_attrs = event.get_cfg_attrs(span); - quote_spanned!(span => - #( #cfg_attrs )* - ::ink::metadata::EventSpec::new(::core::stringify!(#ident)) - .args([ - #( #args ),* - ]) - .docs([ - #( #docs ),* - ]) - .done() - ) - }) - } - - /// Generate ink! metadata for a single argument of an ink! event definition. - fn generate_event_args(event: &ir::Event) -> impl Iterator + '_ { - event.fields().map(|event_field| { - let span = event_field.span(); - let ident = event_field.ident(); - let is_topic = event_field.is_topic; - let docs = event_field - .attrs() - .into_iter() - .filter_map(|attr| attr.extract_docs()); - let ty = Self::generate_type_spec(event_field.ty()); - quote_spanned!(span => - ::ink::metadata::EventParamSpec::new(::core::stringify!(#ident)) - .of_type(#ty) - .indexed(#is_topic) - .docs([ - #( #docs ),* - ]) - .done() - ) - }) - } - fn generate_environment(&self) -> TokenStream2 { let span = self.contract.module().span(); diff --git a/crates/ink/codegen/src/generator/mod.rs b/crates/ink/codegen/src/generator/mod.rs index 960a1b58dd7..453dbc6fbb7 100644 --- a/crates/ink/codegen/src/generator/mod.rs +++ b/crates/ink/codegen/src/generator/mod.rs @@ -33,7 +33,7 @@ mod chain_extension; mod contract; mod dispatch; mod env; -mod events; +mod event; mod ink_test; mod item_impls; mod metadata; @@ -58,7 +58,7 @@ pub use self::{ contract::Contract, dispatch::Dispatch, env::Env, - events::Events, + event::Event, ink_test::InkTest, item_impls::ItemImpls, metadata::Metadata, diff --git a/crates/ink/codegen/src/generator/storage.rs b/crates/ink/codegen/src/generator/storage.rs index a69c05648ab..cdc47776815 100644 --- a/crates/ink/codegen/src/generator/storage.rs +++ b/crates/ink/codegen/src/generator/storage.rs @@ -33,12 +33,6 @@ impl GenerateCode for Storage<'_> { let storage_span = self.contract.module().storage().span(); let access_env_impls = self.generate_access_env_trait_impls(); let storage_struct = self.generate_storage_struct(); - let use_emit_event = - self.contract.module().events().next().is_some().then(|| { - // Required to allow for `self.env().emit_event(...)` in messages and - // constructors. - quote! { use ::ink::codegen::EmitEvent as _; } - }); quote_spanned!(storage_span => #storage_struct #access_env_impls @@ -50,7 +44,6 @@ impl GenerateCode for Storage<'_> { Env as _, StaticEnv as _, }; - #use_emit_event }; ) } diff --git a/crates/ink/codegen/src/lib.rs b/crates/ink/codegen/src/lib.rs index b5d9631c077..78f020ff4c5 100644 --- a/crates/ink/codegen/src/lib.rs +++ b/crates/ink/codegen/src/lib.rs @@ -58,6 +58,10 @@ impl<'a> CodeGenerator for &'a ir::Contract { type Generator = generator::Contract<'a>; } +impl<'a> CodeGenerator for &'a ir::Event { + type Generator = generator::Event<'a>; +} + impl<'a> CodeGenerator for &'a ir::StorageItem { type Generator = generator::StorageItem<'a>; } diff --git a/crates/ink/ir/src/ir/attrs.rs b/crates/ink/ir/src/ir/attrs.rs index 0ae6a7f11ce..77d1966c6b0 100644 --- a/crates/ink/ir/src/ir/attrs.rs +++ b/crates/ink/ir/src/ir/attrs.rs @@ -350,8 +350,6 @@ pub enum AttributeArgKind { Event, /// `#[ink(anonymous)]` Anonymous, - /// `#[ink(topic)]` - Topic, /// `#[ink(message)]` Message, /// `#[ink(constructor)]` @@ -393,10 +391,6 @@ pub enum AttributeArg { /// to reduce event emitting overhead. This is especially useful for user /// defined events. Anonymous, - /// `#[ink(topic)]` - /// - /// Applied on fields of ink! event types to indicate that they are topics. - Topic, /// `#[ink(message)]` /// /// Applied on `&self` or `&mut self` methods to flag them for being an ink! @@ -460,7 +454,6 @@ impl core::fmt::Display for AttributeArgKind { Self::Storage => write!(f, "storage"), Self::Event => write!(f, "event"), Self::Anonymous => write!(f, "anonymous"), - Self::Topic => write!(f, "topic"), Self::Message => write!(f, "message"), Self::Constructor => write!(f, "constructor"), Self::Payable => write!(f, "payable"), @@ -487,7 +480,6 @@ impl AttributeArg { Self::Storage => AttributeArgKind::Storage, Self::Event => AttributeArgKind::Event, Self::Anonymous => AttributeArgKind::Anonymous, - Self::Topic => AttributeArgKind::Topic, Self::Message => AttributeArgKind::Message, Self::Constructor => AttributeArgKind::Constructor, Self::Payable => AttributeArgKind::Payable, @@ -507,7 +499,6 @@ impl core::fmt::Display for AttributeArg { Self::Storage => write!(f, "storage"), Self::Event => write!(f, "event"), Self::Anonymous => write!(f, "anonymous"), - Self::Topic => write!(f, "topic"), Self::Message => write!(f, "message"), Self::Constructor => write!(f, "constructor"), Self::Payable => write!(f, "payable"), @@ -980,7 +971,6 @@ impl Parse for AttributeFrag { "constructor" => Ok(AttributeArg::Constructor), "event" => Ok(AttributeArg::Event), "anonymous" => Ok(AttributeArg::Anonymous), - "topic" => Ok(AttributeArg::Topic), "payable" => Ok(AttributeArg::Payable), "default" => Ok(AttributeArg::Default), "impl" => Ok(AttributeArg::Implementation), @@ -1420,7 +1410,6 @@ mod tests { message, constructor, event, - topic, payable, impl, )] @@ -1430,7 +1419,6 @@ mod tests { AttributeArg::Message, AttributeArg::Constructor, AttributeArg::Event, - AttributeArg::Topic, AttributeArg::Payable, AttributeArg::Implementation, ])), diff --git a/crates/ink/ir/src/ir/event/config.rs b/crates/ink/ir/src/ir/event/config.rs new file mode 100644 index 00000000000..181910c1fe1 --- /dev/null +++ b/crates/ink/ir/src/ir/event/config.rs @@ -0,0 +1,70 @@ +// Copyright 2018-2022 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 crate::{ + ast, + utils::duplicate_config_err, +}; + +/// The configuration arguments to the `#[ink::event(..)]` attribute macro. +#[derive(Debug, PartialEq, Eq)] +pub struct EventConfig { + /// If set to `false`, a signature topic is generated and emitted for this event. + /// If set to `true`, **no** signature topic is generated or emitted for this event., + /// This is the default value. + anonymous: bool, +} + +impl TryFrom for EventConfig { + type Error = syn::Error; + + fn try_from(args: ast::AttributeArgs) -> Result { + let mut anonymous: 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")) + } + if let ast::MetaValue::Lit(syn::Lit::Bool(lit_bool)) = &arg.value { + anonymous = Some(lit_bool.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", + )); + } + } + Ok(EventConfig::new( + anonymous.map(|lit_bool| lit_bool.value).unwrap_or(false), + )) + } +} + +impl EventConfig { + /// Construct a new [`EventConfig`]. + pub fn new(anonymous: bool) -> Self { + Self { anonymous } + } + + /// Returns the anonymous configuration argument. + pub fn anonymous(&self) -> bool { + self.anonymous + } +} diff --git a/crates/ink/ir/src/ir/event/mod.rs b/crates/ink/ir/src/ir/event/mod.rs new file mode 100644 index 00000000000..a559971b8e8 --- /dev/null +++ b/crates/ink/ir/src/ir/event/mod.rs @@ -0,0 +1,243 @@ +// Copyright 2018-2022 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. + +mod config; + +use config::EventConfig; +use proc_macro2::TokenStream as TokenStream2; +use quote::ToTokens; +use syn::spanned::Spanned as _; + +use crate::{ + error::ExtError, + ir, +}; + +/// A checked ink! event with its configuration. +#[derive(Debug, PartialEq, Eq)] +pub struct Event { + item: syn::ItemStruct, + config: EventConfig, +} + +impl Event { + /// Returns `Ok` if the input matches all requirements for an ink! event. + pub fn new(config: TokenStream2, item: TokenStream2) -> Result { + let item = syn::parse2::(item.clone()).map_err(|err| { + err.into_combine(format_err_spanned!( + item, + "event definition must be a `struct`", + )) + })?; + let parsed_config = syn::parse2::(config)?; + let config = EventConfig::try_from(parsed_config)?; + + for attr in &item.attrs { + if attr.path().to_token_stream().to_string().contains("event") { + return Err(format_err_spanned!( + attr, + "only one `ink::event` is allowed", + )) + } + } + + Ok(Self { item, config }) + } + + /// Returns the event definition . + pub fn item(&self) -> &syn::ItemStruct { + &self.item + } + + /// Returns `true` if the first ink! annotation on the given struct is + /// `#[ink(event)]`. + /// + /// # Errors + /// + /// If the first found ink! attribute is malformed. + /// + /// # Note + /// + /// This is used for legacy "inline" event definitions, i.e. event definitions that + /// are defined within a module annotated with `#[ink::contract]`. + pub(super) fn is_ink_event( + item_struct: &syn::ItemStruct, + ) -> Result { + if !ir::contains_ink_attributes(&item_struct.attrs) { + 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, + // an ink! event or an invalid ink! attribute. + let attr = ir::first_ink_attribute(&item_struct.attrs)? + .expect("missing expected ink! attribute for struct"); + Ok(matches!(attr.first().kind(), ir::AttributeArg::Event)) + } + + /// Returns if the event is marked as anonymous, if true then no signature topic is + /// generated or emitted. + pub fn anonymous(&self) -> bool { + self.config.anonymous() + } +} + +impl ToTokens for Event { + /// We mainly implement this trait for this ink! type to have a derived + /// [`Spanned`](`syn::spanned::Spanned`) implementation for it. + fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) { + self.item.to_tokens(tokens) + } +} + +impl TryFrom for Event { + type Error = syn::Error; + + fn try_from(item_struct: syn::ItemStruct) -> Result { + let struct_span = item_struct.span(); + let (ink_attrs, other_attrs) = ir::sanitize_attributes( + struct_span, + item_struct.attrs, + &ir::AttributeArgKind::Event, + |arg| { + match arg.kind() { + ir::AttributeArg::Event | ir::AttributeArg::Anonymous => Ok(()), + _ => Err(None), + } + }, + )?; + Ok(Self { + item: syn::ItemStruct { + attrs: other_attrs, + ..item_struct + }, + config: EventConfig::new(ink_attrs.is_anonymous()), + }) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn simple_try_from_works() { + let item_struct: syn::ItemStruct = syn::parse_quote! { + #[ink(event)] + pub struct MyEvent { + #[ink(topic)] + field_1: i32, + field_2: bool, + } + }; + assert!(Event::try_from(item_struct).is_ok()); + } + + fn assert_try_from_fails(item_struct: syn::ItemStruct, expected: &str) { + assert_eq!( + Event::try_from(item_struct).map_err(|err| err.to_string()), + Err(expected.to_string()) + ) + } + + #[test] + fn conflicting_struct_attributes_fails() { + assert_try_from_fails( + syn::parse_quote! { + #[ink(event)] + #[ink(storage)] + pub struct MyEvent { + #[ink(topic)] + field_1: i32, + field_2: bool, + } + }, + "encountered conflicting ink! attribute argument", + ) + } + + #[test] + fn duplicate_struct_attributes_fails() { + assert_try_from_fails( + syn::parse_quote! { + #[ink(event)] + #[ink(event)] + pub struct MyEvent { + #[ink(topic)] + field_1: i32, + field_2: bool, + } + }, + "encountered duplicate ink! attribute", + ) + } + + #[test] + fn wrong_first_struct_attribute_fails() { + assert_try_from_fails( + syn::parse_quote! { + #[ink(storage)] + #[ink(event)] + pub struct MyEvent { + #[ink(topic)] + field_1: i32, + field_2: bool, + } + }, + "unexpected first ink! attribute argument", + ) + } + + #[test] + fn missing_event_attribute_fails() { + assert_try_from_fails( + syn::parse_quote! { + pub struct MyEvent { + #[ink(topic)] + field_1: i32, + field_2: bool, + } + }, + "encountered unexpected empty expanded ink! attribute arguments", + ) + } + + #[test] + fn anonymous_event_works() { + fn assert_anonymous_event(event: syn::ItemStruct) { + match Event::try_from(event) { + Ok(event) => { + assert!(event.anonymous()); + } + Err(_) => panic!("encountered unexpected invalid anonymous event"), + } + } + assert_anonymous_event(syn::parse_quote! { + #[ink(event)] + #[ink(anonymous)] + pub struct MyEvent { + #[ink(topic)] + field_1: i32, + field_2: bool, + } + }); + assert_anonymous_event(syn::parse_quote! { + #[ink(event, anonymous)] + pub struct MyEvent { + #[ink(topic)] + field_1: i32, + field_2: bool, + } + }); + } +} diff --git a/crates/ink/ir/src/ir/item/event.rs b/crates/ink/ir/src/ir/item/event.rs deleted file mode 100644 index 476c6d40b70..00000000000 --- a/crates/ink/ir/src/ir/item/event.rs +++ /dev/null @@ -1,517 +0,0 @@ -// Copyright 2018-2022 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 crate::{ - error::ExtError as _, - ir::{ - self, - utils, - utils::extract_cfg_attributes, - CFG_IDENT, - }, -}; -use proc_macro2::{ - Ident, - Span, - TokenStream, -}; -use syn::spanned::Spanned as _; - -/// An ink! event struct definition. -/// -/// # Example -/// -/// ``` -/// # let event = >::try_from(syn::parse_quote! { -/// #[ink(event)] -/// pub struct Transaction { -/// #[ink(topic)] -/// from: AccountId, -/// #[ink(topic)] -/// to: AccountId, -/// value: Balance, -/// } -/// # }).unwrap(); -/// ``` -#[derive(Debug, PartialEq, Eq)] -pub struct Event { - item: syn::ItemStruct, - pub anonymous: bool, -} - -impl quote::ToTokens for Event { - /// We mainly implement this trait for this ink! type to have a derived - /// [`Spanned`](`syn::spanned::Spanned`) implementation for it. - fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) { - self.item.to_tokens(tokens) - } -} - -impl Event { - /// Returns `true` if the first ink! annotation on the given struct is - /// `#[ink(event)]`. - /// - /// # Errors - /// - /// If the first found ink! attribute is malformed. - pub(super) fn is_ink_event( - item_struct: &syn::ItemStruct, - ) -> Result { - if !ir::contains_ink_attributes(&item_struct.attrs) { - 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, - // an ink! event or an invalid ink! attribute. - let attr = ir::first_ink_attribute(&item_struct.attrs)? - .expect("missing expected ink! attribute for struct"); - Ok(matches!(attr.first().kind(), ir::AttributeArg::Event)) - } -} - -impl TryFrom for Event { - type Error = syn::Error; - - fn try_from(item_struct: syn::ItemStruct) -> Result { - let struct_span = item_struct.span(); - let (ink_attrs, other_attrs) = ir::sanitize_attributes( - struct_span, - item_struct.attrs, - &ir::AttributeArgKind::Event, - |arg| { - match arg.kind() { - ir::AttributeArg::Event | ir::AttributeArg::Anonymous => Ok(()), - _ => Err(None), - } - }, - )?; - if !item_struct.generics.params.is_empty() { - return Err(format_err_spanned!( - item_struct.generics.params, - "generic ink! event structs are not supported", - )) - } - utils::ensure_pub_visibility("event structs", struct_span, &item_struct.vis)?; - 'repeat: for field in item_struct.fields.iter() { - let field_span = field.span(); - let some_cfg_attrs = field - .attrs - .iter() - .find(|attr| attr.path().is_ident(CFG_IDENT)); - if some_cfg_attrs.is_some() { - return Err(format_err!( - field_span, - "conditional compilation is not allowed for event field" - )) - } - let (ink_attrs, _) = ir::partition_attributes(field.attrs.clone())?; - if ink_attrs.is_empty() { - continue 'repeat - } - let normalized = - ir::InkAttribute::from_expanded(ink_attrs).map_err(|err| { - err.into_combine(format_err!(field_span, "at this invocation",)) - })?; - if !matches!(normalized.first().kind(), ir::AttributeArg::Topic) { - return Err(format_err!( - field_span, - "first optional ink! attribute of an event field must be #[ink(topic)]", - )); - } - for arg in normalized.args() { - if !matches!(arg.kind(), ir::AttributeArg::Topic) { - return Err(format_err!( - arg.span(), - "encountered conflicting ink! attribute for event field", - )) - } - } - } - Ok(Self { - item: syn::ItemStruct { - attrs: other_attrs, - ..item_struct - }, - anonymous: ink_attrs.is_anonymous(), - }) - } -} - -impl Event { - /// Returns the identifier of the event struct. - pub fn ident(&self) -> &Ident { - &self.item.ident - } - - /// Returns an iterator yielding all the `#[ink(topic)]` annotated fields - /// of the event struct. - pub fn fields(&self) -> EventFieldsIter { - EventFieldsIter::new(self) - } - - /// Returns all non-ink! attributes. - pub fn attrs(&self) -> &[syn::Attribute] { - &self.item.attrs - } - - /// Returns a list of `cfg` attributes if any. - pub fn get_cfg_attrs(&self, span: Span) -> Vec { - extract_cfg_attributes(self.attrs(), span) - } -} - -/// An event field with a flag indicating if this field is an event topic. -#[derive(Debug, Copy, Clone, PartialEq, Eq)] -pub struct EventField<'a> { - /// The associated `field` is an event topic if this is `true`. - pub is_topic: bool, - /// The event field. - field: &'a syn::Field, -} - -impl<'a> EventField<'a> { - /// Returns the span of the event field. - pub fn span(self) -> Span { - self.field.span() - } - - /// Returns all non-ink! attributes of the event field. - pub fn attrs(self) -> Vec { - let (_, non_ink_attrs) = ir::partition_attributes(self.field.attrs.clone()) - .unwrap_or_else(|err| { - panic!("encountered invalid event field attributes: {err}") - }); - non_ink_attrs - } - - /// Returns the visibility of the event field. - pub fn vis(self) -> &'a syn::Visibility { - &self.field.vis - } - - /// Returns the identifier of the event field if any. - pub fn ident(self) -> Option<&'a Ident> { - self.field.ident.as_ref() - } - - /// Returns the type of the event field. - pub fn ty(self) -> &'a syn::Type { - &self.field.ty - } -} - -/// Iterator yielding all `#[ink(topic)]` annotated fields of an event struct. -pub struct EventFieldsIter<'a> { - iter: syn::punctuated::Iter<'a, syn::Field>, -} - -impl<'a> EventFieldsIter<'a> { - /// Creates a new topics fields iterator for the given ink! event struct. - fn new(event: &'a Event) -> Self { - Self { - iter: event.item.fields.iter(), - } - } -} - -impl<'a> Iterator for EventFieldsIter<'a> { - type Item = EventField<'a>; - - fn next(&mut self) -> Option { - match self.iter.next() { - None => None, - Some(field) => { - let is_topic = ir::first_ink_attribute(&field.attrs) - .unwrap_or_default() - .map(|attr| matches!(attr.first().kind(), ir::AttributeArg::Topic)) - .unwrap_or_default(); - Some(EventField { is_topic, field }) - } - } - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn simple_try_from_works() { - let item_struct: syn::ItemStruct = syn::parse_quote! { - #[ink(event)] - pub struct MyEvent { - #[ink(topic)] - field_1: i32, - field_2: bool, - } - }; - assert!(Event::try_from(item_struct).is_ok()); - } - - fn assert_try_from_fails(item_struct: syn::ItemStruct, expected: &str) { - assert_eq!( - Event::try_from(item_struct).map_err(|err| err.to_string()), - Err(expected.to_string()) - ) - } - - #[test] - fn conflicting_struct_attributes_fails() { - assert_try_from_fails( - syn::parse_quote! { - #[ink(event)] - #[ink(storage)] - pub struct MyEvent { - #[ink(topic)] - field_1: i32, - field_2: bool, - } - }, - "encountered conflicting ink! attribute argument", - ) - } - - #[test] - fn duplicate_struct_attributes_fails() { - assert_try_from_fails( - syn::parse_quote! { - #[ink(event)] - #[ink(event)] - pub struct MyEvent { - #[ink(topic)] - field_1: i32, - field_2: bool, - } - }, - "encountered duplicate ink! attribute", - ) - } - - #[test] - fn wrong_first_struct_attribute_fails() { - assert_try_from_fails( - syn::parse_quote! { - #[ink(storage)] - #[ink(event)] - pub struct MyEvent { - #[ink(topic)] - field_1: i32, - field_2: bool, - } - }, - "unexpected first ink! attribute argument", - ) - } - - #[test] - fn missing_storage_attribute_fails() { - assert_try_from_fails( - syn::parse_quote! { - pub struct MyEvent { - #[ink(topic)] - field_1: i32, - field_2: bool, - } - }, - "encountered unexpected empty expanded ink! attribute arguments", - ) - } - - #[test] - fn generic_event_fails() { - assert_try_from_fails( - syn::parse_quote! { - #[ink(event)] - pub struct GenericEvent { - #[ink(topic)] - field_1: T, - field_2: bool, - } - }, - "generic ink! event structs are not supported", - ) - } - - #[test] - fn non_pub_event_struct() { - assert_try_from_fails( - syn::parse_quote! { - #[ink(event)] - struct PrivateEvent { - #[ink(topic)] - field_1: i32, - field_2: bool, - } - }, - "non `pub` ink! event structs are not supported", - ) - } - - #[test] - fn duplicate_field_attributes_fails() { - assert_try_from_fails( - syn::parse_quote! { - #[ink(event)] - pub struct MyEvent { - #[ink(topic)] - #[ink(topic)] - field_1: i32, - field_2: bool, - } - }, - "encountered duplicate ink! attribute", - ) - } - - #[test] - fn invalid_field_attributes_fails() { - assert_try_from_fails( - syn::parse_quote! { - #[ink(event)] - pub struct MyEvent { - #[ink(message)] - field_1: i32, - field_2: bool, - } - }, - "first optional ink! attribute of an event field must be #[ink(topic)]", - ) - } - - #[test] - fn conflicting_field_attributes_fails() { - assert_try_from_fails( - syn::parse_quote! { - #[ink(event)] - pub struct MyEvent { - #[ink(topic)] - #[ink(payable)] - field_1: i32, - field_2: bool, - } - }, - "encountered conflicting ink! attribute for event field", - ) - } - - #[test] - fn cfg_marked_field_attribute_fails() { - assert_try_from_fails( - syn::parse_quote! { - #[ink(event)] - pub struct MyEvent { - #[ink(topic)] - field_1: i32, - #[cfg(unix)] - field_2: bool, - } - }, - "conditional compilation is not allowed for event field", - ) - } - - /// Used for the event fields iterator unit test because `syn::Field` does - /// not provide a `syn::parse::Parse` implementation. - #[derive(Debug, PartialEq, Eq)] - struct NamedField(syn::Field); - - impl syn::parse::Parse for NamedField { - fn parse(input: syn::parse::ParseStream) -> syn::Result { - Ok(Self(syn::Field::parse_named(input)?)) - } - } - - impl NamedField { - /// Returns the identifier of the named field. - pub fn ident(&self) -> &Ident { - self.0.ident.as_ref().unwrap() - } - - /// Returns the type of the named field. - pub fn ty(&self) -> &syn::Type { - &self.0.ty - } - } - - #[test] - fn event_fields_iter_works() { - let expected_fields: Vec<(bool, NamedField)> = vec![ - ( - true, - syn::parse_quote! { - field_1: i32 - }, - ), - ( - false, - syn::parse_quote! { - field_2: u64 - }, - ), - ( - true, - syn::parse_quote! { - field_3: [u8; 32] - }, - ), - ]; - let input = >::try_from(syn::parse_quote! { - #[ink(event)] - pub struct MyEvent { - #[ink(topic)] - field_1: i32, - field_2: u64, - #[ink(topic)] - field_3: [u8; 32], - } - }) - .unwrap(); - let mut fields_iter = input.fields(); - for (is_topic, expected_field) in expected_fields { - let field = fields_iter.next().unwrap(); - assert_eq!(field.is_topic, is_topic); - assert_eq!(field.ident(), Some(expected_field.ident())); - assert_eq!(field.ty(), expected_field.ty()); - } - } - - #[test] - fn anonymous_event_works() { - fn assert_anonymous_event(event: syn::ItemStruct) { - match Event::try_from(event) { - Ok(event) => { - assert!(event.anonymous); - } - Err(_) => panic!("encountered unexpected invalid anonymous event"), - } - } - assert_anonymous_event(syn::parse_quote! { - #[ink(event)] - #[ink(anonymous)] - pub struct MyEvent { - #[ink(topic)] - field_1: i32, - field_2: bool, - } - }); - assert_anonymous_event(syn::parse_quote! { - #[ink(event, anonymous)] - pub struct MyEvent { - #[ink(topic)] - field_1: i32, - field_2: bool, - } - }); - } -} diff --git a/crates/ink/ir/src/ir/item/mod.rs b/crates/ink/ir/src/ir/item/mod.rs index 5bb3d6b83ed..766e5baecd5 100644 --- a/crates/ink/ir/src/ir/item/mod.rs +++ b/crates/ink/ir/src/ir/item/mod.rs @@ -12,16 +12,12 @@ // See the License for the specific language governing permissions and // limitations under the License. -mod event; mod storage; #[cfg(test)] mod tests; -pub use self::{ - event::Event, - storage::Storage, -}; +pub use self::storage::Storage; use crate::{ error::ExtError as _, diff --git a/crates/ink/ir/src/ir/mod.rs b/crates/ink/ir/src/ir/mod.rs index a2e8ea09498..8d47fd535f1 100644 --- a/crates/ink/ir/src/ir/mod.rs +++ b/crates/ink/ir/src/ir/mod.rs @@ -19,6 +19,7 @@ mod blake2; mod chain_extension; mod config; mod contract; +mod event; mod idents_lint; mod ink_test; mod item; @@ -69,9 +70,9 @@ pub use self::{ }, config::Config, contract::Contract, + event::Event, ink_test::InkTest, item::{ - Event, InkItem, Item, Storage, diff --git a/crates/ink/macro/src/event/metadata.rs b/crates/ink/macro/src/event/metadata.rs new file mode 100644 index 00000000000..c65061c719c --- /dev/null +++ b/crates/ink/macro/src/event/metadata.rs @@ -0,0 +1,106 @@ +// Copyright 2018-2022 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 ink_ir::IsDocAttribute; +use proc_macro2::TokenStream as TokenStream2; +use quote::quote_spanned; +use syn::spanned::Spanned; + +/// Derives the `ink::Event` trait for the given `struct`. +pub fn event_metadata_derive(mut s: synstructure::Structure) -> TokenStream2 { + s.bind_with(|_| synstructure::BindStyle::Move) + .add_bounds(synstructure::AddBounds::Fields) + .underscore_const(true); + match &s.ast().data { + syn::Data::Struct(_) => { + event_metadata_derive_struct(s).unwrap_or_else(|err| err.to_compile_error()) + } + _ => { + syn::Error::new( + s.ast().span(), + "can only derive `EventMetadata` for Rust `struct` items", + ) + .to_compile_error() + } + } +} + +/// `Event` derive implementation for `struct` types. +fn event_metadata_derive_struct(s: synstructure::Structure) -> syn::Result { + assert_eq!(s.variants().len(), 1, "can only operate on structs"); + let span = s.ast().span(); + + let variant = &s.variants()[0]; + let ident = variant.ast().ident; + + let docs = variant + .ast() + .attrs + .iter() + .filter_map(|attr| attr.extract_docs()); + + let args = variant.bindings().iter().map( |field| { + let field_ty = &field.ast().ty; + let field_span = field_ty.span(); + if let Some(field_name) = field.ast().ident.as_ref() { + let indexed = super::has_ink_topic_attribute(field)?; + let docs = field + .ast() + .attrs + .iter() + .filter_map(|attr| attr.extract_docs()); + Ok(quote_spanned!(field_span => + ::ink::metadata::EventParamSpec::new(::core::stringify!(#field_name)) + .of_type(::ink::metadata::TypeSpec::of_type::<#field_ty>()) + .indexed(#indexed) + .docs([ #( #docs ),* ]) + .done() + )) + } else { + Err(syn::Error::new( + field_span, + "can only derive `EventMetadata` for Rust `struct` items with named fields", + )) + } + }).collect::>>()?; + + Ok(s.bound_impl( + quote_spanned!(span=> ::ink::metadata::EventMetadata), + quote_spanned!(span=> + const MODULE_PATH: &'static str = ::core::module_path!(); + + fn event_spec() -> ::ink::metadata::EventSpec { + // register this event metadata function in the distributed slice for combining all + // events referenced in the contract binary. + #[::ink::metadata::linkme::distributed_slice(::ink::metadata::EVENTS)] + #[linkme(crate = ::ink::metadata::linkme)] + static EVENT_METADATA: fn() -> ::ink::metadata::EventSpec = + <#ident as ::ink::metadata::EventMetadata>::event_spec; + + ::ink::metadata::EventSpec::new(::core::stringify!(#ident)) + .module_path(::core::module_path!()) + .signature_topic( + ::SIGNATURE_TOPIC + ) + .args([ + #( #args ),* + ]) + .docs([ + #( #docs ),* + ]) + .done() + } + ), + )) +} diff --git a/crates/ink/macro/src/event/mod.rs b/crates/ink/macro/src/event/mod.rs new file mode 100644 index 00000000000..b3582124409 --- /dev/null +++ b/crates/ink/macro/src/event/mod.rs @@ -0,0 +1,229 @@ +// Copyright 2018-2022 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. + +mod metadata; + +pub use metadata::event_metadata_derive; + +use ink_codegen::generate_code; +use proc_macro2::TokenStream as TokenStream2; +use quote::{ + quote, + quote_spanned, +}; +use syn::spanned::Spanned; + +/// Generate code from the `#[ink::event]` attribute. This expands to the required +/// derive macros to satisfy an event implementation. +pub fn generate(config: TokenStream2, input: TokenStream2) -> TokenStream2 { + ink_ir::Event::new(config, input) + .map(|event| generate_code(&event)) + .unwrap_or_else(|err| err.to_compile_error()) +} + +/// Derives the `ink::Event` trait for the given `struct`. +pub fn event_derive(mut s: synstructure::Structure) -> TokenStream2 { + s.bind_with(|_| synstructure::BindStyle::Move) + .add_bounds(synstructure::AddBounds::Fields) + .underscore_const(true); + match &s.ast().data { + syn::Data::Struct(_) => { + event_derive_struct(s).unwrap_or_else(|err| err.to_compile_error()) + } + _ => { + syn::Error::new( + s.ast().span(), + "can only derive `Event` for Rust `struct` items", + ) + .to_compile_error() + } + } +} + +/// `Event` derive implementation for `struct` types. +fn event_derive_struct(mut s: synstructure::Structure) -> syn::Result { + assert_eq!(s.variants().len(), 1, "can only operate on structs"); + + if !s.ast().generics.params.is_empty() { + return Err(syn::Error::new( + s.ast().generics.params.span(), + "can only derive `Event` for structs without generics", + )) + } + + let span = s.ast().span(); + let anonymous = has_ink_attribute(&s.ast().attrs, "anonymous")?; + + // filter field bindings to those marked as topics + let mut topic_err: Option = None; + s.variants_mut()[0].filter(|bi| { + match has_ink_topic_attribute(bi) { + Ok(has_attr) => has_attr, + Err(err) => { + match topic_err { + Some(ref mut topic_err) => topic_err.combine(err), + None => topic_err = Some(err), + } + false + } + } + }); + if let Some(err) = topic_err { + return Err(err) + } + + let variant = &s.variants()[0]; + + // Anonymous events require 1 fewer topics since they do not include their signature. + let anonymous_topics_offset = usize::from(!anonymous); + let len_topics = variant.bindings().len() + anonymous_topics_offset; + + let remaining_topics_ty = match len_topics { + 0 => quote_spanned!(span=> ::ink::env::event::state::NoRemainingTopics), + _ => { + quote_spanned!(span=> [::ink::env::event::state::HasRemainingTopics; #len_topics]) + } + }; + + let event_ident = variant.ast().ident; + let signature_topic = if !anonymous { + let signature_topic = signature_topic(variant.ast().fields, event_ident); + quote_spanned!(span=> ::core::option::Option::Some(#signature_topic)) + } else { + quote_spanned!(span=> ::core::option::Option::None) + }; + let event_signature_topic = if anonymous { + None + } else { + Some(quote_spanned!(span=> + .push_topic(Self::SIGNATURE_TOPIC.as_ref()) + )) + }; + + let topics = variant.bindings().iter().fold(quote!(), |acc, field| { + let field_ty = &field.ast().ty; + let field_span = field_ty.span(); + quote_spanned!(field_span=> + #acc + .push_topic(::ink::as_option!(#field)) + ) + }); + let pat = variant.pat(); + let topics_builder = quote!( + #pat => { + builder + .build::() + #event_signature_topic + #topics + .finish() + } + ); + + Ok(s.bound_impl(quote!(::ink::env::Event), quote! { + type RemainingTopics = #remaining_topics_ty; + + const SIGNATURE_TOPIC: ::core::option::Option<[::core::primitive::u8; 32]> = #signature_topic; + + 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 { + #topics_builder + } + } + })) +} + +/// 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)) +} + +/// Checks if the given field's attributes contain an `#[ink(topic)]` attribute. +/// +/// Returns `Err` if: +/// - the given attributes contain a `#[cfg(...)]` attribute +/// - there are `ink` attributes other than a single `#[ink(topic)]` +fn has_ink_topic_attribute(field: &synstructure::BindingInfo) -> syn::Result { + let some_cfg_attrs = field + .ast() + .attrs + .iter() + .find(|attr| attr.path().is_ident("cfg")); + if some_cfg_attrs.is_some() { + Err(syn::Error::new( + field.ast().span(), + "conditional compilation is not allowed for event fields", + )) + } else { + has_ink_attribute(&field.ast().attrs, "topic") + } +} + +/// Checks if the given attributes contain an `ink` attribute with the given path. +/// +/// # 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), + )) + } + Ok(!ink_attrs.is_empty()) +} diff --git a/crates/ink/macro/src/lib.rs b/crates/ink/macro/src/lib.rs index 4cd03837d65..f0945a18767 100644 --- a/crates/ink/macro/src/lib.rs +++ b/crates/ink/macro/src/lib.rs @@ -22,12 +22,16 @@ extern crate proc_macro; mod blake2b; mod chain_extension; mod contract; +mod event; mod ink_test; mod selector; mod storage; mod storage_item; mod trait_def; +#[cfg(test)] +mod tests; + use proc_macro::TokenStream; /// Computes and expands into the BLAKE2b 256-bit hash of the string input. @@ -646,6 +650,36 @@ pub fn trait_definition(attr: TokenStream, item: TokenStream) -> TokenStream { trait_def::analyze(attr.into(), item.into()).into() } +/// Implements the necessary traits for a `struct` to be emitted as an event from a +/// contract. +/// +/// 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. +/// +/// # Examples +/// +/// ``` +/// #[ink::event] +/// pub struct MyEvent { +/// pub field: u32, +/// #[ink(topic)] +/// pub topic: [u8; 32], +/// } +/// +/// // Setting `anonymous = true` means no signature topic will be emitted for the event. +/// #[ink::event(anonymous = true)] +/// pub struct MyAnonEvent { +/// pub field: u32, +/// #[ink(topic)] +/// pub topic: [u8; 32], +/// } +/// ``` +#[proc_macro_attribute] +pub fn event(attr: TokenStream, item: TokenStream) -> TokenStream { + event::generate(attr.into(), item.into()).into() +} + /// Prepares the type to be fully compatible and usable with the storage. /// It implements all necessary traits and calculates the storage key for types. /// `Packed` types don't have a storage key, but non-packed types (like `Mapping`, `Lazy` @@ -1279,6 +1313,129 @@ pub fn chain_extension(attr: TokenStream, item: TokenStream) -> TokenStream { chain_extension::generate(attr.into(), item.into()).into() } +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. + /// + /// Usually this is used in conjunction with the [`EventMetadata`] derive. + /// + /// For convenience there is the [`event`] attribute macro that will expand to all the necessary + /// derives for an event implementation, including this one. + /// + /// # Example + /// + /// ``` + /// use ink::{ + /// Event, + /// env::DefaultEnvironment, + /// }; + /// use scale::Encode; + /// + /// #[derive(Event, Encode)] + /// struct MyEvent { + /// a: u32, + /// #[ink(topic)] + /// b: [u8; 32], + /// } + /// + /// #[derive(Event, Encode)] + /// #[ink(anonymous)] // anonymous events do not have a signature topic + /// struct MyAnonEvent { + /// a: u32, + /// #[ink(topic)] + /// b: [u8; 32], + /// } + /// + /// ink_env::emit_event::(MyEvent { a: 42, b: [0x42; 32] }); + /// ink_env::emit_event::(MyAnonEvent { a: 42, b: [0x42; 32] }); + /// ``` + /// + /// # The Signature Topic + /// + /// By default, the [`ink::Event::SIGNATURE_TOPIC`] is calculated as follows: + /// + /// `blake2b("EventStructName(field1_type_name,field2_type_name)")` + /// + /// The hashing of the topic is done at codegen time in the derive macro, and as such only has + /// access to the **names** of the field types as they appear in the code. As such, if the + /// name of a field of a struct changes, the signature topic will change too, even if the + /// concrete type itself has not changed. This can happen with type aliases, generics, or a + /// change in the use of a `path::to::Type` qualification. + /// + /// Practically this means that two otherwise identical event definitions will have different + /// signature topics if the name of a field type differs. For example, the following two events + /// will have different signature topics: + /// + /// ``` + /// #[derive(ink::Event, scale::Encode)] + /// pub struct MyEvent { + /// a: u32, + /// } + /// + /// mod other_event { + /// type MyU32 = u32; + /// + /// #[derive(ink::Event, scale::Encode)] + /// pub struct MyEvent { + /// a: MyU32, + /// } + /// } + /// + /// use ink::env::Event; + /// assert_ne!(::SIGNATURE_TOPIC, ::SIGNATURE_TOPIC); + /// ``` + /// + /// ## Anonymous Events + /// + /// If the event is annotated with `#[ink(anonymous)]` then no signature topic is generated. + event::event_derive +); + +synstructure::decl_derive!( + [EventMetadata] => + /// 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. + /// + /// 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 + /// conditionally compiled e.g. `#[cfg_attr(feature = "std", derive(::ink::EventMetadata))]` + /// (see example below). + /// + /// For convenience there is the [`event`] attribute macro that will expand to all the necessary + /// derives for an event implementation, including this one. + /// + /// # Example + /// + /// ``` + /// use ink::{ + /// Event, + /// env::DefaultEnvironment, + /// }; + /// use scale::Encode; + /// + /// #[cfg_attr(feature = "std", derive(::ink::EventMetadata))] + /// #[derive(Event, Encode)] + /// struct MyEvent { + /// a: u32, + /// #[ink(topic)] + /// b: [u8; 32], + /// } + /// + /// assert_eq!(::event_spec().args().len(), 2); + /// ``` + /// + /// The generated code will also register this implementation with the global static distributed + /// slice [`ink::metadata::EVENTS`], in order that the metadata of all events used in a contract + /// can be collected. + event::event_metadata_derive +); + synstructure::decl_derive!( [Storable] => /// Derives `ink::storage`'s `Storable` trait for the given `struct`, `enum` or `union`. diff --git a/crates/ink/macro/src/storage/mod.rs b/crates/ink/macro/src/storage/mod.rs index c46e882640a..99cbbf66e23 100644 --- a/crates/ink/macro/src/storage/mod.rs +++ b/crates/ink/macro/src/storage/mod.rs @@ -28,6 +28,3 @@ pub use self::{ storage_key::storage_key_derive, storage_layout::storage_layout_derive, }; - -#[cfg(test)] -mod tests; diff --git a/crates/ink/macro/src/tests/event.rs b/crates/ink/macro/src/tests/event.rs new file mode 100644 index 00000000000..924564fc4cb --- /dev/null +++ b/crates/ink/macro/src/tests/event.rs @@ -0,0 +1,187 @@ +// Copyright 2018-2022 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. + +// These tests are partly testing if code is expanded correctly. +// Hence the syntax contains a number of verbose statements which +// are not properly cleaned up. +#![allow(clippy::absurd_extreme_comparisons)] +#![allow(clippy::identity_op)] +#![allow(clippy::eq_op)] +#![allow(clippy::match_single_binding)] + +use crate::event::event_derive; + +#[test] +fn unit_struct_works() { + crate::test_derive! { + event_derive { + #[derive(scale::Encode)] + 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( ::ink::blake2x256!("UnitStruct()") ); + + 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() + } + } + } + } + }; + } + } +} + +#[test] +fn unit_struct_anonymous_has_no_topics() { + crate::test_derive! { + event_derive { + #[derive(scale::Encode)] + #[ink(anonymous)] + struct UnitStruct; + } + expands to { + const _: () = { + impl ::ink::env::Event for UnitStruct { + type RemainingTopics = ::ink::env::event::state::NoRemainingTopics; + + const SIGNATURE_TOPIC: ::core::option::Option<[::core::primitive::u8; 32]> = + ::core::option::Option::None; + + 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::() + .finish() + } + } + } + } + }; + } no_build + } +} + +#[test] +fn struct_with_fields_no_topics() { + crate::test_derive! { + event_derive { + #[derive(scale::Encode)] + struct Event { + field_1: u32, + field_2: u64, + field_3: u128, + } + } + expands to { + const _: () = { + impl ::ink::env::Event for Event { + type RemainingTopics = [::ink::env::event::state::HasRemainingTopics; 1usize]; + + const SIGNATURE_TOPIC: ::core::option::Option<[::core::primitive::u8; 32]> = + ::core::option::Option::Some( ::ink::blake2x256!("Event(u32,u64,u128)") ); + + 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 { + Event { .. } => { + builder + .build::() + .push_topic(Self::SIGNATURE_TOPIC.as_ref()) + .finish() + } + } + } + } + }; + } + } +} + +#[test] +fn struct_with_fields_and_some_topics() { + crate::test_derive! { + event_derive { + #[derive(scale::Encode)] + struct Event { + field_1: u32, + #[ink(topic)] + field_2: u64, + #[ink(topic)] + field_3: u128, + } + } + expands to { + const _: () = { + impl ::ink::env::Event for Event { + type RemainingTopics = [::ink::env::event::state::HasRemainingTopics; 3usize]; + + const SIGNATURE_TOPIC: ::core::option::Option<[::core::primitive::u8; 32]> = + ::core::option::Option::Some( ::ink::blake2x256!("Event(u32,u64,u128)") ); + + 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 { + Event { field_2 : __binding_1 , field_3 : __binding_2 , .. } => { + builder + .build::() + .push_topic(Self::SIGNATURE_TOPIC.as_ref()) + .push_topic(::ink::as_option!(__binding_1)) + .push_topic(::ink::as_option!(__binding_2)) + .finish() + } + } + } + } + }; + } no_build + } +} diff --git a/crates/ink/macro/src/tests/event_metadata.rs b/crates/ink/macro/src/tests/event_metadata.rs new file mode 100644 index 00000000000..ff919b55508 --- /dev/null +++ b/crates/ink/macro/src/tests/event_metadata.rs @@ -0,0 +1,150 @@ +// Copyright 2018-2022 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 crate::event::event_metadata_derive; + +#[test] +fn unit_struct_works() { + crate::test_derive! { + event_metadata_derive { + #[derive(ink::Event, scale::Encode)] + struct UnitStruct; + } + expands to { + const _: () = { + impl ::ink::metadata::EventMetadata for UnitStruct { + const MODULE_PATH: &'static str = ::core::module_path!(); + + fn event_spec() -> ::ink::metadata::EventSpec { + #[::ink::metadata::linkme::distributed_slice(::ink::metadata::EVENTS)] + #[linkme(crate = ::ink::metadata::linkme)] + static EVENT_METADATA: fn() -> ::ink::metadata::EventSpec = + ::event_spec; + + ::ink::metadata::EventSpec::new(::core::stringify!(UnitStruct)) + .module_path(::core::module_path!()) + .signature_topic(::SIGNATURE_TOPIC) + .args([]) + .docs([]) + .done() + } + } + }; + } + } +} + +#[test] +fn struct_with_fields_no_topics() { + crate::test_derive! { + event_metadata_derive { + #[derive(ink::Event, scale::Encode)] + struct Event { + field_1: u32, + field_2: u64, + field_3: u128, + } + } + expands to { + const _: () = { + impl ::ink::metadata::EventMetadata for Event { + const MODULE_PATH: &'static str = ::core::module_path!(); + + fn event_spec() -> ::ink::metadata::EventSpec { + #[::ink::metadata::linkme::distributed_slice(::ink::metadata::EVENTS)] + #[linkme(crate = ::ink::metadata::linkme)] + static EVENT_METADATA: fn() -> ::ink::metadata::EventSpec = + ::event_spec; + + ::ink::metadata::EventSpec::new(::core::stringify!(Event)) + .module_path(::core::module_path!()) + .signature_topic(::SIGNATURE_TOPIC) + .args([ + ::ink::metadata::EventParamSpec::new(::core::stringify!(field_1)) + .of_type(::ink::metadata::TypeSpec::of_type::()) + .indexed(false) + .docs([]) + .done(), + ::ink::metadata::EventParamSpec::new(::core::stringify!(field_2)) + .of_type(::ink::metadata::TypeSpec::of_type::()) + .indexed(false) + .docs([]) + .done(), + ::ink::metadata::EventParamSpec::new(::core::stringify!(field_3)) + .of_type(::ink::metadata::TypeSpec::of_type::()) + .indexed(false) + .docs([]) + .done() + ]) + .docs([]) + .done() + } + } + }; + } + } +} + +#[test] +fn struct_with_fields_and_some_topics() { + crate::test_derive! { + event_metadata_derive { + #[derive(ink::Event, scale::Encode)] + struct Event { + field_1: u32, + #[ink(topic)] + field_2: u64, + #[ink(topic)] + field_3: u128, + } + } + expands to { + const _: () = { + impl ::ink::metadata::EventMetadata for Event { + const MODULE_PATH: &'static str = ::core::module_path!(); + + fn event_spec() -> ::ink::metadata::EventSpec { + #[::ink::metadata::linkme::distributed_slice(::ink::metadata::EVENTS)] + #[linkme(crate = ::ink::metadata::linkme)] + static EVENT_METADATA: fn() -> ::ink::metadata::EventSpec = + ::event_spec; + + ::ink::metadata::EventSpec::new(::core::stringify!(Event)) + .module_path(::core::module_path!()) + .signature_topic(::SIGNATURE_TOPIC) + .args([ + ::ink::metadata::EventParamSpec::new(::core::stringify!(field_1)) + .of_type(::ink::metadata::TypeSpec::of_type::()) + .indexed(false) + .docs([]) + .done(), + ::ink::metadata::EventParamSpec::new(::core::stringify!(field_2)) + .of_type(::ink::metadata::TypeSpec::of_type::()) + .indexed(true) + .docs([]) + .done(), + ::ink::metadata::EventParamSpec::new(::core::stringify!(field_3)) + .of_type(::ink::metadata::TypeSpec::of_type::()) + .indexed(true) + .docs([]) + .done() + ]) + .docs([]) + .done() + } + } + }; + } + } +} diff --git a/crates/ink/macro/src/storage/tests/mod.rs b/crates/ink/macro/src/tests/mod.rs similarity index 91% rename from crates/ink/macro/src/storage/tests/mod.rs rename to crates/ink/macro/src/tests/mod.rs index 8a0bbfbe70f..aeac3974775 100644 --- a/crates/ink/macro/src/storage/tests/mod.rs +++ b/crates/ink/macro/src/tests/mod.rs @@ -12,16 +12,18 @@ // See the License for the specific language governing permissions and // limitations under the License. +mod event; +mod event_metadata; mod storable; mod storable_hint; mod storage_key; mod storage_layout; use crate::storage::{ - storable::storable_derive, - storable_hint::storable_hint_derive, - storage_key::storage_key_derive, - storage_layout::storage_layout_derive, + storable_derive, + storable_hint_derive, + storage_key_derive, + storage_layout_derive, }; #[macro_export] diff --git a/crates/ink/macro/src/storage/tests/storable.rs b/crates/ink/macro/src/tests/storable.rs similarity index 100% rename from crates/ink/macro/src/storage/tests/storable.rs rename to crates/ink/macro/src/tests/storable.rs diff --git a/crates/ink/macro/src/storage/tests/storable_hint.rs b/crates/ink/macro/src/tests/storable_hint.rs similarity index 100% rename from crates/ink/macro/src/storage/tests/storable_hint.rs rename to crates/ink/macro/src/tests/storable_hint.rs diff --git a/crates/ink/macro/src/storage/tests/storage_key.rs b/crates/ink/macro/src/tests/storage_key.rs similarity index 100% rename from crates/ink/macro/src/storage/tests/storage_key.rs rename to crates/ink/macro/src/tests/storage_key.rs diff --git a/crates/ink/macro/src/storage/tests/storage_layout.rs b/crates/ink/macro/src/tests/storage_layout.rs similarity index 100% rename from crates/ink/macro/src/storage/tests/storage_layout.rs rename to crates/ink/macro/src/tests/storage_layout.rs diff --git a/crates/ink/src/codegen/event/emit.rs b/crates/ink/src/codegen/event/emit.rs deleted file mode 100644 index 702bfd687f5..00000000000 --- a/crates/ink/src/codegen/event/emit.rs +++ /dev/null @@ -1,26 +0,0 @@ -// Copyright 2018-2022 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 crate::reflect::ContractEventBase; - -/// Allows for `self.env().emit_event(...)` syntax in ink! implementation blocks. -pub trait EmitEvent -where - C: ContractEventBase, -{ - /// Emits an event that can be trivially converted into the base event. - fn emit_event(self, event: E) - where - E: Into<::Type>; -} diff --git a/crates/ink/src/codegen/event/mod.rs b/crates/ink/src/codegen/event/mod.rs deleted file mode 100644 index 73002bdc4da..00000000000 --- a/crates/ink/src/codegen/event/mod.rs +++ /dev/null @@ -1,26 +0,0 @@ -// Copyright 2018-2022 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. - -mod emit; -mod topics; - -pub use self::{ - emit::EmitEvent, - topics::{ - EventLenTopics, - EventRespectsTopicLimit, - EventTopics, - RespectTopicLimit, - }, -}; diff --git a/crates/ink/src/codegen/event/topics.rs b/crates/ink/src/codegen/event/topics.rs deleted file mode 100644 index b360c8a75de..00000000000 --- a/crates/ink/src/codegen/event/topics.rs +++ /dev/null @@ -1,88 +0,0 @@ -// Copyright 2018-2022 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 core::marker::PhantomData; - -/// Guards that an ink! event definitions respects the topic limit. -/// -/// # Usage -/// -/// ``` -/// // #[ink(event)] -/// pub struct ExampleEvent {} -/// -/// /// The amount of the topics of the example event struct. -/// const LEN_TOPICS: usize = 3; -/// -/// /// The limit for the amount of topics per ink! event definition. -/// const TOPICS_LIMIT: usize = 4; -/// -/// impl ::ink::codegen::EventLenTopics for ExampleEvent { -/// type LenTopics = ::ink::codegen::EventTopics; -/// } -/// -/// // The below code only compiles successfully if the example ink! event -/// // definitions respects the topic limitation: it must have an amount of -/// // topics less than or equal to the topic limit. -/// const _: () = ::ink::codegen::utils::consume_type::< -/// ::ink::codegen::EventRespectsTopicLimit, -/// >(); -/// ``` -pub struct EventRespectsTopicLimit -where - Event: EventLenTopics, - ::LenTopics: RespectTopicLimit, -{ - marker: PhantomData Event>, -} - -/// Guards that an amount of event topics respects the event topic limit. -/// -/// # Note -/// -/// Implemented by `EventTopics` if M is less or equal to N. -/// Automatically implemented for up to 12 event topics. -pub trait RespectTopicLimit {} - -/// Represents an the amount of topics for an ink! event definition. -pub struct EventTopics; - -macro_rules! impl_is_smaller_or_equals { - ( $first:literal $( , $rest:literal )* $(,)? ) => { - impl RespectTopicLimit<$first> for EventTopics<$first> {} - $( - impl RespectTopicLimit<$rest> for EventTopics<$first> {} - )* - - impl_is_smaller_or_equals! { $( $rest ),* } - }; - ( ) => {}; -} -impl_is_smaller_or_equals! { - 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12 -} - -/// Stores the number of event topics of the ink! event definition. -pub trait EventLenTopics { - /// Type denoting the number of event topics. - /// - /// # Note - /// - /// We use an associated type instead of an associated constant here - /// because Rust does not yet allow for generics in constant parameter - /// position which would be required in the `EventRespectsTopicLimit` - /// trait definition. - /// As soon as this is possible in Rust we might change this to a constant. - type LenTopics; -} diff --git a/crates/ink/src/codegen/mod.rs b/crates/ink/src/codegen/mod.rs index d53fc3517b6..d7ebf7e84e8 100644 --- a/crates/ink/src/codegen/mod.rs +++ b/crates/ink/src/codegen/mod.rs @@ -16,7 +16,6 @@ mod dispatch; mod env; -mod event; mod implies_return; mod trait_def; pub mod utils; @@ -32,13 +31,6 @@ pub use self::{ Env, StaticEnv, }, - event::{ - EmitEvent, - EventLenTopics, - EventRespectsTopicLimit, - EventTopics, - RespectTopicLimit, - }, implies_return::ImpliesReturn, trait_def::{ TraitCallBuilder, diff --git a/crates/ink/src/env_access.rs b/crates/ink/src/env_access.rs index eb8c1418f1c..95de5eaae8f 100644 --- a/crates/ink/src/env_access.rs +++ b/crates/ink/src/env_access.rs @@ -413,6 +413,14 @@ where ink_env::minimum_balance::() } + /// Emits an event. + pub fn emit_event(self, event: Evt) + where + Evt: ink_env::Event, + { + ink_env::emit_event::(event) + } + /// Instantiates another contract. /// /// # Example diff --git a/crates/ink/src/lib.rs b/crates/ink/src/lib.rs index 700be363516..69ba6a4977a 100644 --- a/crates/ink/src/lib.rs +++ b/crates/ink/src/lib.rs @@ -18,6 +18,10 @@ )] #![cfg_attr(not(feature = "std"), no_std)] +#[macro_use] +#[doc(hidden)] +pub mod option_info; + #[macro_use] #[doc(hidden)] pub mod result_info; @@ -70,11 +74,14 @@ pub use ink_macro::{ blake2x256, chain_extension, contract, + event, selector_bytes, selector_id, storage_item, test, trait_definition, + Event, + EventMetadata, }; pub use ink_primitives::{ ConstructorResult, diff --git a/crates/ink/src/option_info.rs b/crates/ink/src/option_info.rs new file mode 100644 index 00000000000..361dc8b6127 --- /dev/null +++ b/crates/ink/src/option_info.rs @@ -0,0 +1,77 @@ +// Copyright 2018-2022 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. + +pub struct AsOption<'lt, T>(pub &'lt T); + +impl AsOption<'_, ::core::option::Option> { + #[inline] + // We need to allow for dead code at this point because + // the Rust compiler thinks this function is unused even + // though it acts as the specialized case for detection. + #[allow(dead_code)] + pub fn value(&self) -> Option<&T> { + self.0.as_ref() + } +} + +impl<'lt, T> AsOption<'lt, &'lt ::core::option::Option> { + #[inline] + pub fn value(&self) -> Option<&T> { + self.0.as_ref() + } +} + +pub trait AsOptionFallback { + fn value(&self) -> Option<&T>; +} +impl AsOptionFallback for AsOption<'_, T> { + #[inline] + fn value(&self) -> Option<&T> { + Some(self.0) + } +} + +/// Evaluates to `None` if the given expression is a `Option::None`. +/// +/// # Note +/// +/// This given expression is not required to be of type `Option`. +#[macro_export] +#[doc(hidden)] +macro_rules! as_option { + ( $e:expr $(,)? ) => {{ + #[allow(unused_imports)] + use $crate::option_info::AsOptionFallback as _; + $crate::option_info::AsOption(&$e).value() + }}; +} + +#[cfg(test)] +mod tests { + #[test] + fn as_option_works() { + assert_eq!(Some(&true), as_option!(true)); + assert_eq!(Some(&42), as_option!(42)); + assert_eq!(Some(&"Hello, World!"), as_option!("Hello, World!")); + + assert_eq!(Some(&()), as_option!(Some(()))); + assert_eq!(Some(&5), as_option!(Some(5))); + assert_eq!(Some(&true), as_option!(Some(true))); + assert_eq!(Some(&true), as_option!(&Some(true))); + + assert_eq!(None, as_option!(Option::::None)); + assert_eq!(None, as_option!(Option::::None)); + assert_eq!(None, as_option!(&Option::::None)); + } +} diff --git a/crates/ink/src/reflect/event.rs b/crates/ink/src/reflect/event.rs deleted file mode 100644 index 69181659e3c..00000000000 --- a/crates/ink/src/reflect/event.rs +++ /dev/null @@ -1,52 +0,0 @@ -// Copyright 2018-2022 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. - -/// Defines a base event type for the contract. -/// -/// This is usually the event enum that comprises all defined event types. -/// -/// # Usage -/// -/// ``` -/// #[ink::contract] -/// pub mod contract { -/// #[ink(storage)] -/// pub struct Contract {} -/// -/// #[ink(event)] -/// pub struct Event1 {} -/// -/// #[ink(event)] -/// pub struct Event2 {} -/// -/// impl Contract { -/// #[ink(constructor)] -/// pub fn constructor() -> Self { -/// Self {} -/// } -/// -/// #[ink(message)] -/// pub fn message(&self) {} -/// } -/// } -/// -/// use contract::Contract; -/// # use ink::reflect::ContractEventBase; -/// -/// type BaseEvent = ::Type; -/// ``` -pub trait ContractEventBase { - /// The generated base event enum. - type Type; -} diff --git a/crates/ink/src/reflect/mod.rs b/crates/ink/src/reflect/mod.rs index 349ca327e16..4df2d8e1c5c 100644 --- a/crates/ink/src/reflect/mod.rs +++ b/crates/ink/src/reflect/mod.rs @@ -25,7 +25,6 @@ mod contract; mod dispatch; -mod event; mod trait_def; pub use self::{ @@ -41,7 +40,6 @@ pub use self::{ DispatchableMessageInfo, ExecuteDispatchable, }, - event::ContractEventBase, trait_def::{ TraitDefinitionRegistry, TraitInfo, diff --git a/crates/ink/tests/compile_tests.rs b/crates/ink/tests/compile_tests.rs index 78635475b0f..c38ed7414b6 100644 --- a/crates/ink/tests/compile_tests.rs +++ b/crates/ink/tests/compile_tests.rs @@ -28,6 +28,9 @@ fn ui_tests() { t.pass("tests/ui/contract/pass/*.rs"); t.compile_fail("tests/ui/contract/fail/*.rs"); + t.pass("tests/ui/event/pass/*.rs"); + t.compile_fail("tests/ui/event/fail/*.rs"); + t.pass("tests/ui/storage_item/pass/*.rs"); t.compile_fail("tests/ui/storage_item/fail/*.rs"); diff --git a/crates/ink/tests/events_metadata.rs b/crates/ink/tests/events_metadata.rs new file mode 100644 index 00000000000..77a682a9a37 --- /dev/null +++ b/crates/ink/tests/events_metadata.rs @@ -0,0 +1,107 @@ +// Copyright 2018-2022 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. + +#![cfg_attr(not(feature = "std"), no_std)] + +#[ink::event] +/// EventExternal docs +pub struct EventExternal { + f1: bool, + /// f2 docs + #[ink(topic)] + f2: u32, +} + +#[ink::contract] +mod contract { + #[ink(storage)] + pub struct Contract {} + + #[ink(event)] + /// EventInline docs + pub struct EventInline { + #[ink(topic)] + f3: bool, + /// f4 docs + f4: u32, + } + + impl Contract { + #[ink(constructor)] + pub fn new(_x: u8) -> Self { + Self {} + } + } + + impl Contract { + #[ink(message)] + pub fn get_value(&self) -> u32 { + 42 + } + } +} + +#[cfg(test)] +mod tests { + fn generate_metadata() -> ink_metadata::InkProject { + extern "Rust" { + fn __ink_generate_metadata() -> ink_metadata::InkProject; + } + + unsafe { __ink_generate_metadata() } + } + + #[test] + fn collects_all_events() { + let metadata = generate_metadata(); + + assert_eq!(metadata.spec().events().len(), 2); + + let event_external = metadata + .spec() + .events() + .iter() + .find(|e| e.label() == "EventExternal") + .expect("EventExternal should be present"); + + assert_eq!(event_external.docs(), &["EventExternal docs"]); + assert_eq!(event_external.args().len(), 2); + + let arg_f2 = event_external + .args() + .iter() + .find(|a| a.label() == "f2") + .expect("f2 should be present"); + assert_eq!(arg_f2.docs(), &["f2 docs"]); + assert!(arg_f2.indexed()); + + let event_inline = metadata + .spec() + .events() + .iter() + .find(|e| e.label() == "EventInline") + .expect("EventInline should be present"); + + assert_eq!(event_inline.docs(), &["EventInline docs"]); + assert_eq!(event_inline.args().len(), 2); + + let arg_f4 = event_inline + .args() + .iter() + .find(|a| a.label() == "f4") + .expect("f4 should be present"); + assert_eq!(arg_f4.docs(), &["f4 docs"]); + assert!(!arg_f4.indexed()); + } +} diff --git a/crates/ink/tests/ui/contract/fail/event-too-many-topics-anonymous.rs b/crates/ink/tests/ui/contract/fail/event-too-many-topics-anonymous.rs deleted file mode 100644 index 440ba81c419..00000000000 --- a/crates/ink/tests/ui/contract/fail/event-too-many-topics-anonymous.rs +++ /dev/null @@ -1,60 +0,0 @@ -use ink_env::{ - DefaultEnvironment, - Environment, -}; - -pub struct EnvironmentMoreTopics; - -impl ink_env::Environment for EnvironmentMoreTopics { - const MAX_EVENT_TOPICS: usize = 2; - - type AccountId = ::AccountId; - type Balance = ::Balance; - type Hash = ::Hash; - type Timestamp = ::Timestamp; - type BlockNumber = ::BlockNumber; - type ChainExtension = (); -} - -#[ink::contract(env = super::EnvironmentMoreTopics)] -mod contract { - #[ink(storage)] - pub struct Contract {} - - #[ink(event, anonymous)] - pub struct Event { - #[ink(topic)] - arg_1: i8, - #[ink(topic)] - arg_2: i16, - #[ink(topic)] - arg_3: i32, - #[ink(topic)] - arg_4: i32, - } - - impl Contract { - #[ink(constructor)] - pub fn constructor() -> Self { - Self::env().emit_event(Event { - arg_1: 1, - arg_2: 2, - arg_3: 3, - arg_4: 4, - }); - Self {} - } - - #[ink(message)] - pub fn message(&self) { - self.env().emit_event(Event { - arg_1: 1, - arg_2: 2, - arg_3: 3, - arg_4: 4, - }); - } - } -} - -fn main() {} diff --git a/crates/ink/tests/ui/contract/fail/event-too-many-topics-anonymous.stderr b/crates/ink/tests/ui/contract/fail/event-too-many-topics-anonymous.stderr deleted file mode 100644 index b3781159671..00000000000 --- a/crates/ink/tests/ui/contract/fail/event-too-many-topics-anonymous.stderr +++ /dev/null @@ -1,21 +0,0 @@ -error[E0277]: the trait bound `EventTopics<4>: RespectTopicLimit<2>` is not satisfied - --> tests/ui/contract/fail/event-too-many-topics-anonymous.rs:25:5 - | -25 | pub struct Event { - | ^^^ the trait `RespectTopicLimit<2>` is not implemented for `EventTopics<4>` - | - = help: the following other types implement trait `RespectTopicLimit`: - as RespectTopicLimit<10>> - as RespectTopicLimit<11>> - as RespectTopicLimit<12>> - as RespectTopicLimit<4>> - as RespectTopicLimit<5>> - as RespectTopicLimit<6>> - as RespectTopicLimit<7>> - as RespectTopicLimit<8>> - as RespectTopicLimit<9>> -note: required by a bound in `EventRespectsTopicLimit` - --> src/codegen/event/topics.rs - | - | ::LenTopics: RespectTopicLimit, - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ required by this bound in `EventRespectsTopicLimit` diff --git a/crates/ink/tests/ui/contract/fail/event-too-many-topics.rs b/crates/ink/tests/ui/contract/fail/event-too-many-topics.rs deleted file mode 100644 index 4b7be584017..00000000000 --- a/crates/ink/tests/ui/contract/fail/event-too-many-topics.rs +++ /dev/null @@ -1,56 +0,0 @@ -use ink_env::{ - DefaultEnvironment, - Environment, -}; - -pub struct EnvironmentMoreTopics; - -impl ink_env::Environment for EnvironmentMoreTopics { - const MAX_EVENT_TOPICS: usize = 2; - - type AccountId = ::AccountId; - type Balance = ::Balance; - type Hash = ::Hash; - type Timestamp = ::Timestamp; - type BlockNumber = ::BlockNumber; - type ChainExtension = (); -} - -#[ink::contract(env = super::EnvironmentMoreTopics)] -mod contract { - #[ink(storage)] - pub struct Contract {} - - #[ink(event)] - pub struct Event { - #[ink(topic)] - arg_1: i8, - #[ink(topic)] - arg_2: i16, - #[ink(topic)] - arg_3: i32, - } - - impl Contract { - #[ink(constructor)] - pub fn constructor() -> Self { - Self::env().emit_event(Event { - arg_1: 1, - arg_2: 2, - arg_3: 3, - }); - Self {} - } - - #[ink(message)] - pub fn message(&self) { - self.env().emit_event(Event { - arg_1: 1, - arg_2: 2, - arg_3: 3, - }); - } - } -} - -fn main() {} diff --git a/crates/ink/tests/ui/contract/fail/event-too-many-topics.stderr b/crates/ink/tests/ui/contract/fail/event-too-many-topics.stderr deleted file mode 100644 index e542e880c0c..00000000000 --- a/crates/ink/tests/ui/contract/fail/event-too-many-topics.stderr +++ /dev/null @@ -1,21 +0,0 @@ -error[E0277]: the trait bound `EventTopics<3>: RespectTopicLimit<2>` is not satisfied - --> tests/ui/contract/fail/event-too-many-topics.rs:25:5 - | -25 | pub struct Event { - | ^^^ the trait `RespectTopicLimit<2>` is not implemented for `EventTopics<3>` - | - = help: the following other types implement trait `RespectTopicLimit`: - as RespectTopicLimit<10>> - as RespectTopicLimit<11>> - as RespectTopicLimit<12>> - as RespectTopicLimit<3>> - as RespectTopicLimit<4>> - as RespectTopicLimit<5>> - as RespectTopicLimit<6>> - as RespectTopicLimit<7>> - and $N others -note: required by a bound in `EventRespectsTopicLimit` - --> src/codegen/event/topics.rs - | - | ::LenTopics: RespectTopicLimit, - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ required by this bound in `EventRespectsTopicLimit` diff --git a/crates/ink/tests/ui/event/fail/cfg_attr_on_field.rs b/crates/ink/tests/ui/event/fail/cfg_attr_on_field.rs new file mode 100644 index 00000000000..615e0ad3745 --- /dev/null +++ b/crates/ink/tests/ui/event/fail/cfg_attr_on_field.rs @@ -0,0 +1,7 @@ +#[ink::event] +pub struct Event { + #[cfg(feature = "std")] + pub a: [u8; 32], +} + +fn main() {} \ No newline at end of file diff --git a/crates/ink/tests/ui/event/fail/cfg_attr_on_field.stderr b/crates/ink/tests/ui/event/fail/cfg_attr_on_field.stderr new file mode 100644 index 00000000000..e3b4bdb2f4a --- /dev/null +++ b/crates/ink/tests/ui/event/fail/cfg_attr_on_field.stderr @@ -0,0 +1,5 @@ +error: conditional compilation is not allowed for event fields + --> tests/ui/event/fail/cfg_attr_on_field.rs:3:5 + | +3 | #[cfg(feature = "std")] + | ^ diff --git a/crates/ink/tests/ui/event/fail/cfg_attr_on_topic.rs b/crates/ink/tests/ui/event/fail/cfg_attr_on_topic.rs new file mode 100644 index 00000000000..4528625c549 --- /dev/null +++ b/crates/ink/tests/ui/event/fail/cfg_attr_on_topic.rs @@ -0,0 +1,8 @@ +#[ink::event] +pub struct Event { + #[cfg(feature = "std")] + #[ink(topic)] + pub topic: [u8; 32], +} + +fn main() {} \ No newline at end of file diff --git a/crates/ink/tests/ui/event/fail/cfg_attr_on_topic.stderr b/crates/ink/tests/ui/event/fail/cfg_attr_on_topic.stderr new file mode 100644 index 00000000000..57fdb45b1f3 --- /dev/null +++ b/crates/ink/tests/ui/event/fail/cfg_attr_on_topic.stderr @@ -0,0 +1,5 @@ +error: conditional compilation is not allowed for event fields + --> tests/ui/event/fail/cfg_attr_on_topic.rs:3:5 + | +3 | #[cfg(feature = "std")] + | ^ diff --git a/crates/ink/tests/ui/event/fail/event_enum.rs b/crates/ink/tests/ui/event/fail/event_enum.rs new file mode 100644 index 00000000000..f068725beb5 --- /dev/null +++ b/crates/ink/tests/ui/event/fail/event_enum.rs @@ -0,0 +1,10 @@ +#[ink::event] +pub enum Event { + Variant1 { + field_1: i8, + #[ink(topic)] + field_2: i16, + } +} + +fn main() {} \ No newline at end of file diff --git a/crates/ink/tests/ui/event/fail/event_enum.stderr b/crates/ink/tests/ui/event/fail/event_enum.stderr new file mode 100644 index 00000000000..db73d82b462 --- /dev/null +++ b/crates/ink/tests/ui/event/fail/event_enum.stderr @@ -0,0 +1,17 @@ +error: expected `struct` + --> tests/ui/event/fail/event_enum.rs:2:5 + | +2 | pub enum Event { + | ^^^^ + +error: event definition must be a `struct` + --> tests/ui/event/fail/event_enum.rs:2:1 + | +2 | / pub enum Event { +3 | | Variant1 { +4 | | field_1: i8, +5 | | #[ink(topic)] +6 | | field_2: i16, +7 | | } +8 | | } + | |_^ diff --git a/crates/ink/tests/ui/event/fail/generics.rs b/crates/ink/tests/ui/event/fail/generics.rs new file mode 100644 index 00000000000..72487d3eeba --- /dev/null +++ b/crates/ink/tests/ui/event/fail/generics.rs @@ -0,0 +1,7 @@ +#[derive(ink::Event, scale::Encode)] +pub struct Event { + #[ink(topic)] + pub topic: T, +} + +fn main() {} \ No newline at end of file diff --git a/crates/ink/tests/ui/event/fail/generics.stderr b/crates/ink/tests/ui/event/fail/generics.stderr new file mode 100644 index 00000000000..0b790c8cf15 --- /dev/null +++ b/crates/ink/tests/ui/event/fail/generics.stderr @@ -0,0 +1,5 @@ +error: can only derive `Event` for structs without generics + --> tests/ui/event/fail/generics.rs:2:18 + | +2 | pub struct Event { + | ^ diff --git a/crates/ink/tests/ui/event/fail/multiple_topic_args.rs b/crates/ink/tests/ui/event/fail/multiple_topic_args.rs new file mode 100644 index 00000000000..fa05b660253 --- /dev/null +++ b/crates/ink/tests/ui/event/fail/multiple_topic_args.rs @@ -0,0 +1,8 @@ +#[ink::event] +pub struct Event { + #[ink(topic)] + #[ink(topic)] + pub topic: [u8; 32], +} + +fn main() {} \ No newline at end of file diff --git a/crates/ink/tests/ui/event/fail/multiple_topic_args.stderr b/crates/ink/tests/ui/event/fail/multiple_topic_args.stderr new file mode 100644 index 00000000000..cbe9999135d --- /dev/null +++ b/crates/ink/tests/ui/event/fail/multiple_topic_args.stderr @@ -0,0 +1,5 @@ +error: Only a single `#[ink(topic)]` attribute allowed. + --> tests/ui/event/fail/multiple_topic_args.rs:4:5 + | +4 | #[ink(topic)] + | ^ diff --git a/crates/ink/tests/ui/event/fail/multiple_topic_args_inline.rs b/crates/ink/tests/ui/event/fail/multiple_topic_args_inline.rs new file mode 100644 index 00000000000..e5974cd6ab1 --- /dev/null +++ b/crates/ink/tests/ui/event/fail/multiple_topic_args_inline.rs @@ -0,0 +1,7 @@ +#[ink::event] +pub struct Event { + #[ink(topic, topic)] + pub topic: [u8; 32], +} + +fn main() {} \ No newline at end of file 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 new file mode 100644 index 00000000000..92e8159645c --- /dev/null +++ b/crates/ink/tests/ui/event/fail/multiple_topic_args_inline.stderr @@ -0,0 +1,5 @@ +error: Invalid `#[ink(topic)]` attribute: multiple arguments not allowed. + --> tests/ui/event/fail/multiple_topic_args_inline.rs:3:11 + | +3 | #[ink(topic, topic)] + | ^^^^^ diff --git a/crates/ink/tests/ui/event/pass/anonymous_flag_works.rs b/crates/ink/tests/ui/event/pass/anonymous_flag_works.rs new file mode 100644 index 00000000000..34a6e705d2e --- /dev/null +++ b/crates/ink/tests/ui/event/pass/anonymous_flag_works.rs @@ -0,0 +1,8 @@ +#[ink::event(anonymous = true)] +pub struct Event { + #[ink(topic)] + pub topic: [u8; 32], + pub field_1: u32, +} + +fn main() {} \ No newline at end of file diff --git a/crates/ink/tests/ui/event/pass/event_attribute_works.rs b/crates/ink/tests/ui/event/pass/event_attribute_works.rs new file mode 100644 index 00000000000..6e22adbb2ce --- /dev/null +++ b/crates/ink/tests/ui/event/pass/event_attribute_works.rs @@ -0,0 +1,8 @@ +#[ink::event] +pub struct Event { + #[ink(topic)] + pub topic: [u8; 32], + pub field_1: u32, +} + +fn main() {} \ No newline at end of file diff --git a/crates/ink/tests/unique_topics.rs b/crates/ink/tests/unique_topics.rs deleted file mode 100644 index 8234d8470fa..00000000000 --- a/crates/ink/tests/unique_topics.rs +++ /dev/null @@ -1,101 +0,0 @@ -// Copyright 2018-2022 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. - -#![cfg_attr(not(feature = "std"), no_std)] - -#[ink::contract] -mod my_contract { - #[ink(storage)] - pub struct MyContract {} - - /// Exemplary event - #[ink(event)] - pub struct MyEvent { - #[ink(topic)] - v0: Option, - #[ink(topic)] - v1: Balance, - #[ink(topic)] - v2: bool, - #[ink(topic)] - v3: bool, - } - - impl MyContract { - /// Creates a new `MyContract` instance. - #[ink(constructor)] - pub fn new() -> Self { - MyContract {} - } - - /// Emits a `MyEvent`. - #[ink(message)] - pub fn emit_my_event(&self) { - self.env().emit_event(MyEvent { - v0: None, - v1: 0, - v2: false, - v3: false, - }); - } - } - - impl Default for MyContract { - fn default() -> Self { - Self::new() - } - } - - #[cfg(test)] - mod tests { - use super::*; - use ink_env::test::EmittedEvent; - - #[ink::test] - fn event_must_have_unique_topics() { - // given - let my_contract = MyContract::new(); - - // when - MyContract::emit_my_event(&my_contract); - - // then - // all topics must be unique - let emitted_events = - ink_env::test::recorded_events().collect::>(); - let mut encoded_topics: std::vec::Vec<&[u8]> = emitted_events[0] - .topics - .iter() - .map(|topic| topic.as_slice()) - .collect(); - assert!(!has_duplicates(&mut encoded_topics)); - } - - /// Finds duplicates in a given vector. - /// - /// This function has complexity of `O(n * log n)` and no additional memory - /// is required, although the order of items is not preserved. - fn has_duplicates>(items: &mut [T]) -> bool { - // Sort the vector - items.sort_by(|a, b| Ord::cmp(a.as_ref(), b.as_ref())); - // And then find any two consecutive equal elements. - items.windows(2).any(|w| { - match w { - [ref a, ref b] => a == b, - _ => false, - } - }) - } - } -} diff --git a/crates/metadata/Cargo.toml b/crates/metadata/Cargo.toml index 333f03ece3d..83a4a60cf31 100644 --- a/crates/metadata/Cargo.toml +++ b/crates/metadata/Cargo.toml @@ -21,6 +21,7 @@ ink_primitives = { version = "=4.2.0", path = "../primitives/", default-features serde = { version = "1.0", default-features = false, features = ["derive", "alloc"] } impl-serde = "0.4.0" derive_more = { version = "0.99", default-features = false, features = ["from"] } +linkme = "0.3.9" scale-info = { version = "2.6", default-features = false, features = ["derive", "serde", "decode", "schema"] } schemars = "0.8" diff --git a/crates/metadata/src/lib.rs b/crates/metadata/src/lib.rs index d37fa7536c4..0f044eab8d6 100644 --- a/crates/metadata/src/lib.rs +++ b/crates/metadata/src/lib.rs @@ -20,6 +20,7 @@ #[cfg(not(feature = "std"))] extern crate alloc; +extern crate core; #[cfg(test)] mod tests; @@ -53,7 +54,10 @@ pub use self::specs::{ use impl_serde::serialize as serde_hex; +#[doc(hidden)] +pub use linkme; pub use scale_info::TypeInfo; + #[cfg(feature = "derive")] use scale_info::{ form::PortableForm, @@ -154,3 +158,27 @@ impl InkProject { &self.spec } } + +/// Any event which derives `#[derive(ink::EventMetadata)]` and is used in the contract +/// binary will have its implementation added to this distributed slice at linking time. +#[linkme::distributed_slice] +pub static EVENTS: [fn() -> EventSpec] = [..]; + +/// Collect the [`EventSpec`] metadata of all event definitions linked and used in the +/// binary. +pub fn collect_events() -> Vec { + EVENTS.iter().map(|event| event()).collect() +} + +/// Provides metadata about an ink! event. +/// +/// Implementations must be registered into the [`static@EVENTS`] distributed slice, in +/// order to be included in the contract metadata. This is done automatically by the +/// `#[derive(ink::EventMetadata)]` +pub trait EventMetadata { + /// The full path to the event type, usually provided by [`module_path`]. + const MODULE_PATH: &'static str; + + /// Returns the metadata of the event. + fn event_spec() -> EventSpec; +} diff --git a/crates/metadata/src/specs.rs b/crates/metadata/src/specs.rs index 1b113331b05..84b4bafe917 100644 --- a/crates/metadata/src/specs.rs +++ b/crates/metadata/src/specs.rs @@ -16,15 +16,24 @@ use crate::{ serde_hex, - utils::trim_extra_whitespace, + utils::{ + deserialize_from_byte_str, + serialize_as_byte_str, + trim_extra_whitespace, + }, }; #[cfg(not(feature = "std"))] use alloc::{ + collections::BTreeMap, format, + string::String, vec, vec::Vec, }; -use core::marker::PhantomData; +use core::{ + fmt::Display, + marker::PhantomData, +}; use scale_info::{ form::{ Form, @@ -42,6 +51,8 @@ use serde::{ Deserialize, Serialize, }; +#[cfg(feature = "std")] +use std::collections::BTreeMap; /// Describes a contract. #[derive(Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] @@ -240,9 +251,17 @@ where } } +impl ContractSpecBuilder { + /// Collect metadata for all events linked into the contract. + pub fn collect_events(self) -> Self { + self.events(crate::collect_events()) + } +} + impl ContractSpecBuilder where F: Form, + F::String: Display, TypeSpec: Default, { /// Finalizes construction of the contract specification. @@ -263,6 +282,60 @@ where self.spec.messages.iter().filter(|m| m.default).count() < 2, "only one default message is allowed" ); + + let max_topics = self.spec.environment.max_event_topics; + let events_exceeding_max_topics_limit = self + .spec + .events + .iter() + .filter_map(|e| { + let signature_topic = if e.signature_topic.is_some() { 1 } else { 0 }; + let topics_count = + signature_topic + e.args.iter().filter(|a| a.indexed).count(); + if topics_count > max_topics { + Some(format!( + "`{}::{}` ({} topics)", + e.module_path, e.label, topics_count + )) + } else { + None + } + }) + .collect::>(); + assert!( + events_exceeding_max_topics_limit.is_empty(), + "maximum of {max_topics} event topics exceeded: {}", + events_exceeding_max_topics_limit.join(", ") + ); + + let mut signature_topics: BTreeMap, Vec> = BTreeMap::new(); + for e in self.spec.events.iter() { + if let Some(signature_topic) = &e.signature_topic { + signature_topics + .entry(signature_topic.bytes.clone()) + .or_default() + .push(format!("`{}::{}`", e.module_path, e.label)); + } + } + let signature_topic_collisions = signature_topics + .iter() + .filter_map(|(_, topics)| { + if topics.len() > 1 { + Some(format!( + "event signature topic collision: {}", + topics.join(", ") + )) + } else { + None + } + }) + .collect::>(); + assert!( + signature_topic_collisions.is_empty(), + "{}", + signature_topic_collisions.join("\n") + ); + self.spec } } @@ -846,12 +919,45 @@ impl IntoPortable for MessageSpec { pub struct EventSpec { /// The label of the event. label: F::String, + /// The module path to the event type definition. + module_path: F::String, + /// The signature topic of the event. `None` if the event is anonymous. + signature_topic: Option, /// The event arguments. args: Vec>, /// The event documentation. docs: Vec, } +/// The value of the signature topic for a non anonymous event. +#[derive(Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] +#[serde(transparent)] +pub struct SignatureTopic { + #[serde( + serialize_with = "serialize_as_byte_str", + deserialize_with = "deserialize_from_byte_str" + )] + bytes: Vec, +} + +impl From for SignatureTopic +where + T: AsRef<[u8]>, +{ + fn from(bytes: T) -> Self { + SignatureTopic { + bytes: bytes.as_ref().to_vec(), + } + } +} + +impl SignatureTopic { + /// Returns the bytes of the signature topic. + pub fn as_bytes(&self) -> &[u8] { + &self.bytes + } +} + /// An event specification builder. #[must_use] pub struct EventSpecBuilder @@ -865,6 +971,16 @@ impl EventSpecBuilder where F: Form, { + /// Sets the module path to the event type definition. + pub fn module_path<'a>(self, path: &'a str) -> Self + where + F::String: From<&'a str>, + { + let mut this = self; + this.spec.module_path = path.into(); + this + } + /// Sets the input arguments of the event specification. pub fn args(self, args: A) -> Self where @@ -876,6 +992,17 @@ where this } + /// Sets the signature topic of the event specification. + pub fn signature_topic(self, topic: Option) -> Self + where + T: AsRef<[u8]>, + { + let mut this = self; + debug_assert!(this.spec.signature_topic.is_none()); + this.spec.signature_topic = topic.as_ref().map(SignatureTopic::from); + this + } + /// Sets the input arguments of the event specification. pub fn docs<'a, D>(self, docs: D) -> Self where @@ -903,6 +1030,8 @@ impl IntoPortable for EventSpec { fn into_portable(self, registry: &mut Registry) -> Self::Output { EventSpec { label: self.label.to_string(), + module_path: self.module_path.to_string(), + signature_topic: self.signature_topic, args: self .args .into_iter() @@ -916,12 +1045,15 @@ impl IntoPortable for EventSpec { impl EventSpec where F: Form, + F::String: Default, { /// Creates a new event specification builder. pub fn new(label: ::String) -> EventSpecBuilder { EventSpecBuilder { spec: Self { label, + module_path: Default::default(), + signature_topic: None, args: Vec::new(), docs: Vec::new(), }, @@ -943,6 +1075,11 @@ where &self.args } + /// The signature topic of the event. `None` if the event is anonymous. + pub fn signature_topic(&self) -> Option<&SignatureTopic> { + self.signature_topic.as_ref() + } + /// The event documentation. pub fn docs(&self) -> &[F::String] { &self.docs @@ -1154,7 +1291,7 @@ where pub struct EventParamSpec { /// The label of the parameter. label: F::String, - /// If the event parameter is indexed. + /// If the event parameter is indexed as a topic. indexed: bool, /// The type of the parameter. #[serde(rename = "type")] @@ -1186,7 +1323,7 @@ where EventParamSpecBuilder { spec: Self { label, - // By default event parameters are not indexed. + // By default event parameters are not indexed as topics. indexed: false, // We initialize every parameter type as `()`. ty: Default::default(), @@ -1200,7 +1337,7 @@ where &self.label } - /// Returns true if the event parameter is indexed. + /// Returns true if the event parameter is indexed as a topic. pub fn indexed(&self) -> bool { self.indexed } @@ -1237,7 +1374,7 @@ where this } - /// If the event parameter is indexed. + /// If the event parameter is indexed as a topic. pub fn indexed(self, is_indexed: bool) -> Self { let mut this = self; this.spec.indexed = is_indexed; @@ -1245,14 +1382,18 @@ where } /// Sets the documentation of the event parameter. - pub fn docs(self, docs: D) -> Self + pub fn docs<'a, D>(self, docs: D) -> Self where - D: IntoIterator::String>, + D: IntoIterator, + F::String: From<&'a str>, { debug_assert!(self.spec.docs.is_empty()); Self { spec: EventParamSpec { - docs: docs.into_iter().collect::>(), + docs: docs + .into_iter() + .map(|s| trim_extra_whitespace(s).into()) + .collect::>(), ..self.spec }, } diff --git a/crates/metadata/src/tests.rs b/crates/metadata/src/tests.rs index 9beee133c0b..2866c94fba2 100644 --- a/crates/metadata/src/tests.rs +++ b/crates/metadata/src/tests.rs @@ -151,6 +151,163 @@ fn spec_contract_only_one_default_constructor_allowed() { .done(); } +#[test] +#[should_panic( + expected = "maximum of 2 event topics exceeded: `path::to::Event` (3 topics), `path::to::Event2` (3 topics)" +)] +fn spec_contract_event_definition_exceeds_environment_topics_limit() { + const MAX_EVENT_TOPICS: usize = 2; + + ContractSpec::new() + .constructors(vec![ConstructorSpec::from_label("new") + .selector([94u8, 189u8, 136u8, 214u8]) + .payable(true) + .args(vec![MessageParamSpec::new("init_value") + .of_type(TypeSpec::with_name_segs::( + vec!["i32"].into_iter().map(AsRef::as_ref), + )) + .done()]) + .returns(ReturnTypeSpec::new(None)) + .docs(Vec::new()) + .default(true) + .done()]) + .messages(vec![MessageSpec::from_label("inc") + .selector([231u8, 208u8, 89u8, 15u8]) + .mutates(true) + .payable(true) + .args(vec![MessageParamSpec::new("by") + .of_type(TypeSpec::with_name_segs::( + vec!["i32"].into_iter().map(AsRef::as_ref), + )) + .done()]) + .returns(ReturnTypeSpec::new(None)) + .default(true) + .done()]) + .events(vec![ + EventSpec::new("Event") + .module_path("path::to") + // has a signature topic which counts towards the limit + .signature_topic(Some([0u8; 32])) + .args([ + EventParamSpec::new("field1_no_topic") + .of_type(TypeSpec::of_type::()) + .indexed(false) + .done(), + EventParamSpec::new("field2_topic") + .of_type(TypeSpec::of_type::()) + .indexed(true) + .done(), + EventParamSpec::new("field3_topic") + .of_type(TypeSpec::of_type::()) + .indexed(true) + .done(), + ]) + .done(), + EventSpec::new("Event2") + .module_path("path::to") + // is an anonymous event with no signature topic + .signature_topic::<[u8; 32]>(None) + .args([ + EventParamSpec::new("field1_topic") + .of_type(TypeSpec::of_type::()) + .indexed(true) + .done(), + EventParamSpec::new("field2_topic") + .of_type(TypeSpec::of_type::()) + .indexed(true) + .done(), + EventParamSpec::new("field3_topic") + .of_type(TypeSpec::of_type::()) + .indexed(true) + .done(), + ]) + .done(), + ]) + .lang_error(TypeSpec::with_name_segs::( + ::core::iter::Iterator::map( + ::core::iter::IntoIterator::into_iter(["ink", "LangError"]), + ::core::convert::AsRef::as_ref, + ), + )) + .environment( + EnvironmentSpec::new() + .account_id(TypeSpec::of_type::()) + .balance(TypeSpec::of_type::()) + .hash(TypeSpec::of_type::()) + .timestamp(TypeSpec::of_type::()) + .block_number(TypeSpec::of_type::()) + .chain_extension(TypeSpec::of_type::<()>()) + .max_event_topics(MAX_EVENT_TOPICS) + .done(), + ) + .done(); +} + +#[test] +#[should_panic( + expected = "event signature topic collision: `path::to::Event`, `path::to::Event2`" +)] +fn spec_contract_event_definition_signature_topic_collision() { + const SIGNATURE_TOPIC: Option<[u8; 32]> = Some([42u8; 32]); + + ContractSpec::new() + .constructors(vec![ConstructorSpec::from_label("new") + .selector([94u8, 189u8, 136u8, 214u8]) + .payable(true) + .args(vec![MessageParamSpec::new("init_value") + .of_type(TypeSpec::with_name_segs::( + vec!["i32"].into_iter().map(AsRef::as_ref), + )) + .done()]) + .returns(ReturnTypeSpec::new(None)) + .docs(Vec::new()) + .default(true) + .done()]) + .messages(vec![MessageSpec::from_label("inc") + .selector([231u8, 208u8, 89u8, 15u8]) + .mutates(true) + .payable(true) + .args(vec![MessageParamSpec::new("by") + .of_type(TypeSpec::with_name_segs::( + vec!["i32"].into_iter().map(AsRef::as_ref), + )) + .done()]) + .returns(ReturnTypeSpec::new(None)) + .default(true) + .done()]) + .events(vec![ + EventSpec::new("Event") + .module_path("path::to") + // has a signature topic which counts towards the limit + .signature_topic(SIGNATURE_TOPIC) + .args([]) + .done(), + EventSpec::new("Event2") + .module_path("path::to") + .signature_topic::<[u8; 32]>(SIGNATURE_TOPIC) + .args([]) + .done(), + ]) + .lang_error(TypeSpec::with_name_segs::( + ::core::iter::Iterator::map( + ::core::iter::IntoIterator::into_iter(["ink", "LangError"]), + ::core::convert::AsRef::as_ref, + ), + )) + .environment( + EnvironmentSpec::new() + .account_id(TypeSpec::of_type::()) + .balance(TypeSpec::of_type::()) + .hash(TypeSpec::of_type::()) + .timestamp(TypeSpec::of_type::()) + .block_number(TypeSpec::of_type::()) + .chain_extension(TypeSpec::of_type::<()>()) + .max_event_topics(2) + .done(), + ) + .done(); +} + #[test] fn spec_contract_json() { #[cfg_attr(feature = "std", derive(scale_info::TypeInfo))] @@ -508,9 +665,11 @@ fn should_trim_whitespaces_in_events_docs() { let args = [EventParamSpec::new("something".into()) .of_type(spec) .indexed(true) - .docs(vec!["test".to_string()]) + .docs(["test"]) .done()]; let es = EventSpec::new("foobar".into()) + .module_path("foo") + .signature_topic(Some([0u8; 32])) .args(args) .docs([" FooBarEvent "]) .done(); @@ -520,6 +679,8 @@ fn should_trim_whitespaces_in_events_docs() { // when let expected_event_spec = serde_json::json!( { + "module_path": "foo", + "signature_topic": "0x0000000000000000000000000000000000000000000000000000000000000000", "args": [ { "docs": ["test"], @@ -543,6 +704,19 @@ fn should_trim_whitespaces_in_events_docs() { assert_eq!(event_spec_name, expected_event_spec); } +/// Create a default environment spec with the `max_event_topics` set to `4`. +fn environment_spec() -> EnvironmentSpec { + EnvironmentSpec::new() + .account_id(Default::default()) + .balance(Default::default()) + .hash(Default::default()) + .timestamp(Default::default()) + .block_number(Default::default()) + .chain_extension(Default::default()) + .max_event_topics(4) + .done() +} + /// Helper for creating a constructor spec at runtime fn runtime_constructor_spec() -> ConstructorSpec { let path: Path = Path::from_segments_unchecked(["FooType".to_string()]); @@ -588,6 +762,8 @@ fn runtime_event_spec() -> EventSpec { .docs(vec![]) .done()]; EventSpec::new("foobar".into()) + .signature_topic(Some([42u8; 32])) + .module_path("foo") .args(args) .docs(["foobar event"]) .done() @@ -597,6 +773,7 @@ fn runtime_event_spec() -> EventSpec { #[test] fn construct_runtime_contract_spec() { let spec = ContractSpec::new() + .environment(environment_spec()) .constructors([runtime_constructor_spec()]) .messages([runtime_message_spec()]) .events([runtime_event_spec()]) @@ -667,6 +844,8 @@ fn construct_runtime_contract_spec() { let expected_event_spec = serde_json::json!( { "label": "foobar", + "module_path": "foo", + "signature_topic": "0x2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a", "args": [ { "label": "something", diff --git a/integration-tests/custom-environment/lib.rs b/integration-tests/custom-environment/lib.rs index 971485a993d..e14b9f6424e 100644 --- a/integration-tests/custom-environment/lib.rs +++ b/integration-tests/custom-environment/lib.rs @@ -46,8 +46,6 @@ mod runtime_call { third_topic: Balance, #[ink(topic)] fourth_topic: Balance, - #[ink(topic)] - fifth_topic: Balance, } impl Topics { @@ -67,8 +65,6 @@ mod runtime_call { mod tests { use super::*; - type Event = ::Type; - #[ink::test] fn emits_event_with_many_topics() { let mut contract = Topics::new(); @@ -77,14 +73,11 @@ mod runtime_call { let emitted_events = ink::env::test::recorded_events().collect::>(); assert_eq!(emitted_events.len(), 1); - let emitted_event = - ::decode(&mut &emitted_events[0].data[..]) - .expect("encountered invalid contract event data buffer"); + let emitted_event = ::decode( + &mut &emitted_events[0].data[..], + ); - assert!(matches!( - emitted_event, - Event::EventWithTopics(EventWithTopics { .. }) - )); + assert!(emitted_event.is_ok()); } } diff --git a/integration-tests/erc20/lib.rs b/integration-tests/erc20/lib.rs index f00c2eb25e7..668ba3859fb 100644 --- a/integration-tests/erc20/lib.rs +++ b/integration-tests/erc20/lib.rs @@ -223,41 +223,35 @@ mod erc20 { Hash, }; - type Event = ::Type; - fn assert_transfer_event( event: &ink::env::test::EmittedEvent, expected_from: Option, expected_to: Option, expected_value: Balance, ) { - let decoded_event = ::decode(&mut &event.data[..]) + let decoded_event = ::decode(&mut &event.data[..]) .expect("encountered invalid contract event data buffer"); - if let Event::Transfer(Transfer { from, to, value }) = decoded_event { - assert_eq!(from, expected_from, "encountered invalid Transfer.from"); - assert_eq!(to, expected_to, "encountered invalid Transfer.to"); - assert_eq!(value, expected_value, "encountered invalid Trasfer.value"); + let Transfer { from, to, value } = decoded_event; + assert_eq!(from, expected_from, "encountered invalid Transfer.from"); + assert_eq!(to, expected_to, "encountered invalid Transfer.to"); + assert_eq!(value, expected_value, "encountered invalid Trasfer.value"); + + let mut expected_topics = Vec::new(); + expected_topics.push( + ink::blake2x256!("Transfer(Option,Option,Balance)") + .into(), + ); + if let Some(from) = expected_from { + expected_topics.push(encoded_into_hash(from)); } else { - panic!("encountered unexpected event kind: expected a Transfer event") + expected_topics.push(Hash::CLEAR_HASH); } - let expected_topics = vec![ - encoded_into_hash(&PrefixedValue { - value: b"Erc20::Transfer", - prefix: b"", - }), - encoded_into_hash(&PrefixedValue { - prefix: b"Erc20::Transfer::from", - value: &expected_from, - }), - encoded_into_hash(&PrefixedValue { - prefix: b"Erc20::Transfer::to", - value: &expected_to, - }), - encoded_into_hash(&PrefixedValue { - prefix: b"Erc20::Transfer::value", - value: &expected_value, - }), - ]; + if let Some(to) = expected_to { + expected_topics.push(encoded_into_hash(to)); + } else { + expected_topics.push(Hash::CLEAR_HASH); + } + expected_topics.push(encoded_into_hash(value)); let topics = event.topics.clone(); for (n, (actual_topic, expected_topic)) in @@ -482,29 +476,7 @@ mod erc20 { ) } - /// For calculating the event topic hash. - struct PrefixedValue<'a, 'b, T> { - pub prefix: &'a [u8], - pub value: &'b T, - } - - impl scale::Encode for PrefixedValue<'_, '_, X> - where - X: scale::Encode, - { - #[inline] - fn size_hint(&self) -> usize { - self.prefix.size_hint() + self.value.size_hint() - } - - #[inline] - fn encode_to(&self, dest: &mut T) { - self.prefix.encode_to(dest); - self.value.encode_to(dest); - } - } - - fn encoded_into_hash(entity: &T) -> Hash + fn encoded_into_hash(entity: T) -> Hash where T: scale::Encode, { @@ -558,7 +530,7 @@ mod erc20 { let bob_account = ink_e2e::account_id(ink_e2e::AccountKeyring::Bob); let transfer_to_bob = 500_000_000u128; - let transfer = call.transfer(bob_account.clone(), transfer_to_bob); + let transfer = call.transfer(bob_account, transfer_to_bob); let _transfer_res = client .call(&ink_e2e::alice(), &transfer, 0, None) .await @@ -598,8 +570,7 @@ mod erc20 { let amount = 500_000_000u128; // tx - let transfer_from = - call.transfer_from(bob_account.clone(), charlie_account.clone(), amount); + let transfer_from = call.transfer_from(bob_account, charlie_account, amount); let transfer_from_result = client .call(&ink_e2e::charlie(), &transfer_from, 0, None) .await; @@ -611,18 +582,15 @@ mod erc20 { // Bob approves Charlie to transfer up to amount on his behalf let approved_value = 1_000u128; - let approve_call = call.approve(charlie_account.clone(), approved_value); + let approve_call = call.approve(charlie_account, approved_value); client .call(&ink_e2e::bob(), &approve_call, 0, None) .await .expect("approve failed"); // `transfer_from` the approved amount - let transfer_from = call.transfer_from( - bob_account.clone(), - charlie_account.clone(), - approved_value, - ); + let transfer_from = + call.transfer_from(bob_account, charlie_account, approved_value); let transfer_from_result = client .call(&ink_e2e::charlie(), &transfer_from, 0, None) .await; @@ -637,8 +605,7 @@ mod erc20 { .await; // `transfer_from` again, this time exceeding the approved amount - let transfer_from = - call.transfer_from(bob_account.clone(), charlie_account.clone(), 1); + let transfer_from = call.transfer_from(bob_account, charlie_account, 1); let transfer_from_result = client .call(&ink_e2e::charlie(), &transfer_from, 0, None) .await; diff --git a/integration-tests/events/.gitignore b/integration-tests/events/.gitignore new file mode 100644 index 00000000000..bf910de10af --- /dev/null +++ b/integration-tests/events/.gitignore @@ -0,0 +1,9 @@ +# Ignore build artifacts from the local tests sub-crate. +/target/ + +# Ignore backup files creates by cargo fmt. +**/*.rs.bk + +# Remove Cargo.lock when creating an executable, leave it for libraries +# More information here http://doc.crates.io/guide.html#cargotoml-vs-cargolock +Cargo.lock \ No newline at end of file diff --git a/integration-tests/events/Cargo.toml b/integration-tests/events/Cargo.toml new file mode 100644 index 00000000000..cc45c92161b --- /dev/null +++ b/integration-tests/events/Cargo.toml @@ -0,0 +1,39 @@ +[package] +name = "events" +version = "4.2.0" +authors = ["Parity Technologies "] +edition = "2021" +publish = false + +[dependencies] +ink = { path = "../../crates/ink", default-features = false } +scale = { package = "parity-scale-codec", version = "3", default-features = false, features = ["derive"] } +scale-info = { version = "2.5", default-features = false, features = ["derive"], optional = true } + +event-def = { path = "event-def", default-features = false } +event-def2 = { path = "event-def2", default-features = false } +event-def-unused = { path = "event-def-unused", default-features = false } + +[dev-dependencies] +ink_e2e = { path = "../../crates/e2e" } + +[lib] +path = "lib.rs" + +[features] +default = ["std"] +std = [ + "ink/std", + "scale/std", + "scale-info/std", + "event-def/std", + "event-def2/std", + "event-def-unused/std", +] +ink-as-dependency = [] +e2e-tests = [] + +[profile.test] +# Need this for linkme crate to work for the event metadata unit test. +# See https://github.com/dtolnay/linkme/issues/61#issuecomment-1503653702 +lto = "thin" diff --git a/integration-tests/events/event-def-unused/Cargo.toml b/integration-tests/events/event-def-unused/Cargo.toml new file mode 100644 index 00000000000..eb344647fbf --- /dev/null +++ b/integration-tests/events/event-def-unused/Cargo.toml @@ -0,0 +1,17 @@ +[package] +name = "event-def-unused" +version = "0.1.0" +edition = "2021" + +[dependencies] +ink = { path = "../../../crates/ink", default-features = false } +scale = { package = "parity-scale-codec", version = "3", default-features = false, features = ["derive"] } +scale-info = { version = "2.5", default-features = false, features = ["derive"], optional = true } + +[features] +default = ["std"] +std = [ + "ink/std", + "scale/std", + "scale-info/std", +] diff --git a/integration-tests/events/event-def-unused/src/lib.rs b/integration-tests/events/event-def-unused/src/lib.rs new file mode 100644 index 00000000000..cbf2d719fd8 --- /dev/null +++ b/integration-tests/events/event-def-unused/src/lib.rs @@ -0,0 +1,15 @@ +#![cfg_attr(not(feature = "std"), no_std, no_main)] + +#[ink::trait_definition] +pub trait FlipperTrait { + #[ink(message)] + fn flip(&mut self); +} + +#[ink::event] +pub struct EventDefUnused { + #[ink(topic)] + pub hash: [u8; 32], + #[ink(topic)] + pub maybe_hash: Option<[u8; 32]>, +} diff --git a/integration-tests/events/event-def/Cargo.toml b/integration-tests/events/event-def/Cargo.toml new file mode 100644 index 00000000000..b5ed9cd33cb --- /dev/null +++ b/integration-tests/events/event-def/Cargo.toml @@ -0,0 +1,17 @@ +[package] +name = "event-def" +version = "0.1.0" +edition = "2021" + +[dependencies] +ink = { path = "../../../crates/ink", default-features = false } +scale = { package = "parity-scale-codec", version = "3", default-features = false, features = ["derive"] } +scale-info = { version = "2.5", default-features = false, features = ["derive"], optional = true } + +[features] +default = ["std"] +std = [ + "ink/std", + "scale/std", + "scale-info/std", +] diff --git a/integration-tests/events/event-def/src/lib.rs b/integration-tests/events/event-def/src/lib.rs new file mode 100644 index 00000000000..4f9e8ffac10 --- /dev/null +++ b/integration-tests/events/event-def/src/lib.rs @@ -0,0 +1,14 @@ +#![cfg_attr(not(feature = "std"), no_std, no_main)] + +#[ink::event] +pub struct ForeignFlipped { + pub value: bool, +} + +#[ink::event] +pub struct ThirtyTwoByteTopics { + #[ink(topic)] + pub hash: [u8; 32], + #[ink(topic)] + pub maybe_hash: Option<[u8; 32]>, +} diff --git a/integration-tests/events/event-def2/Cargo.toml b/integration-tests/events/event-def2/Cargo.toml new file mode 100644 index 00000000000..beda2da2e5c --- /dev/null +++ b/integration-tests/events/event-def2/Cargo.toml @@ -0,0 +1,17 @@ +[package] +name = "event-def2" +version = "0.1.0" +edition = "2021" + +[dependencies] +ink = { path = "../../../crates/ink", default-features = false } +scale = { package = "parity-scale-codec", version = "3", default-features = false, features = ["derive"] } +scale-info = { version = "2.5", default-features = false, features = ["derive"], optional = true } + +[features] +default = ["std"] +std = [ + "ink/std", + "scale/std", + "scale-info/std", +] diff --git a/integration-tests/events/event-def2/src/lib.rs b/integration-tests/events/event-def2/src/lib.rs new file mode 100644 index 00000000000..f27050f895d --- /dev/null +++ b/integration-tests/events/event-def2/src/lib.rs @@ -0,0 +1,9 @@ +#![cfg_attr(not(feature = "std"), no_std, no_main)] + +#[ink::event] +pub struct EventDefAnotherCrate { + #[ink(topic)] + pub hash: [u8; 32], + #[ink(topic)] + pub maybe_hash: Option<[u8; 32]>, +} diff --git a/integration-tests/events/lib.rs b/integration-tests/events/lib.rs new file mode 100644 index 00000000000..aa88c7af2f8 --- /dev/null +++ b/integration-tests/events/lib.rs @@ -0,0 +1,330 @@ +#![cfg_attr(not(feature = "std"), no_std, no_main)] + +#[ink::event(anonymous = true)] +pub struct AnonymousEvent { + #[ink(topic)] + pub topic: [u8; 32], + pub field_1: u32, +} + +#[ink::contract] +pub mod events { + #[ink(storage)] + pub struct Events { + value: bool, + } + + #[ink(event)] + pub struct InlineFlipped { + value: bool, + } + + #[ink(event)] + #[ink(anonymous)] + pub struct InlineAnonymousEvent { + #[ink(topic)] + pub topic: [u8; 32], + pub field_1: u32, + } + + impl Events { + /// Creates a new events smart contract initialized with the given value. + #[ink(constructor)] + pub fn new(init_value: bool) -> Self { + Self { value: init_value } + } + + /// Flips the current value of the boolean. + #[ink(message)] + pub fn flip_with_foreign_event(&mut self) { + self.value = !self.value; + self.env() + .emit_event(event_def::ForeignFlipped { value: self.value }) + } + + /// Flips the current value of the boolean. + #[ink(message)] + pub fn flip_with_inline_event(&mut self) { + self.value = !self.value; + self.env().emit_event(InlineFlipped { 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]>) { + self.env().emit_event(event_def::ThirtyTwoByteTopics { + hash: [0x42; 32], + maybe_hash, + }) + } + + /// Emit an event from a different crate. + #[ink(message)] + pub fn emit_event_from_a_different_crate(&self, maybe_hash: Option<[u8; 32]>) { + self.env().emit_event(event_def2::EventDefAnotherCrate { + hash: [0x42; 32], + maybe_hash, + }) + } + + /// Emit a inline and standalone anonymous events + #[ink(message)] + pub fn emit_anonymous_events(&self, topic: [u8; 32]) { + self.env() + .emit_event(InlineAnonymousEvent { topic, field_1: 42 }); + self.env() + .emit_event(super::AnonymousEvent { topic, field_1: 42 }); + } + } + + /// Implementing the trait from the `event_def_unused` crate includes all defined + /// events there. + impl event_def_unused::FlipperTrait for Events { + #[ink(message)] + fn flip(&mut self) { + self.value = !self.value; + } + } + + #[cfg(test)] + mod tests { + use super::*; + use scale::Decode as _; + + #[test] + fn collects_specs_for_all_linked_and_used_events() { + let event_specs = ink::metadata::collect_events(); + assert_eq!(7, event_specs.len()); + + assert!(event_specs + .iter() + .any(|evt| evt.label() == &"ForeignFlipped")); + assert!(event_specs + .iter() + .any(|evt| evt.label() == &"InlineFlipped")); + assert!(event_specs + .iter() + .any(|evt| evt.label() == &"ThirtyTwoByteTopics")); + assert!(event_specs + .iter() + .any(|evt| evt.label() == &"EventDefAnotherCrate")); + assert!(event_specs + .iter() + .any(|evt| evt.label() == &"AnonymousEvent")); + assert!(event_specs + .iter() + .any(|evt| evt.label() == &"InlineAnonymousEvent")); + + // The event is not used in the code by being included in the metadata + // because we implement trait form `event_def_unused` crate. + assert!(event_specs + .iter() + .any(|evt| evt.label() == &"EventDefUnused")); + } + + #[ink::test] + fn it_works() { + let mut events = Events::new(false); + events.flip_with_foreign_event(); + + let emitted_events = ink::env::test::recorded_events().collect::>(); + assert_eq!(1, emitted_events.len()); + let event = &emitted_events[0]; + + let decoded_event = ::decode(&mut &event.data[..]) + .expect("encountered invalid contract event data buffer"); + assert!(decoded_event.value); + } + + #[ink::test] + fn option_topic_some_has_topic() { + let events = Events::new(false); + events.emit_32_byte_topic_event(Some([0xAA; 32])); + + let emitted_events = ink::env::test::recorded_events().collect::>(); + assert_eq!(1, emitted_events.len()); + let event = &emitted_events[0]; + + assert_eq!(event.topics.len(), 3); + let signature_topic = + ::SIGNATURE_TOPIC + .map(|topic| topic.to_vec()); + assert_eq!(Some(&event.topics[0]), signature_topic.as_ref()); + assert_eq!(event.topics[1], [0x42; 32]); + assert_eq!( + event.topics[2], [0xAA; 32], + "option topic should be published" + ); + } + + #[ink::test] + fn option_topic_none_encoded_as_0() { + let events = Events::new(false); + events.emit_32_byte_topic_event(None); + + let emitted_events = ink::env::test::recorded_events().collect::>(); + assert_eq!(1, emitted_events.len()); + let event = &emitted_events[0]; + + let signature_topic = + ::SIGNATURE_TOPIC + .map(|topic| topic.to_vec()) + .unwrap(); + + let expected_topics = vec![ + signature_topic, + [0x42; 32].to_vec(), + [0x00; 32].to_vec(), // None is encoded as 0x00 + ]; + assert_eq!(expected_topics, event.topics); + } + + #[ink::test] + fn anonymous_events_emit_no_signature_topics() { + let events = Events::new(false); + let topic = [0x42; 32]; + events.emit_anonymous_events(topic); + + let emitted_events = ink::env::test::recorded_events().collect::>(); + assert_eq!(2, emitted_events.len()); + + let event = &emitted_events[0]; + assert_eq!(event.topics.len(), 1); + assert_eq!(event.topics[0], topic); + + let event = &emitted_events[1]; + assert_eq!(event.topics.len(), 1); + assert_eq!(event.topics[0], topic); + } + } + + #[cfg(all(test, feature = "e2e-tests"))] + mod e2e_tests { + use super::*; + use ink_e2e::H256; + + type E2EResult = std::result::Result>; + + #[ink_e2e::test] + async fn emits_foreign_event(mut client: ink_e2e::Client) -> E2EResult<()> { + // given + let init_value = false; + let constructor = EventsRef::new(init_value); + let contract = client + .instantiate("events", &ink_e2e::alice(), constructor, 0, None) + .await + .expect("instantiate failed"); + let mut call = contract.call::(); + + // when + let flip = call.flip_with_foreign_event(); + let flip_res = client + .call(&ink_e2e::bob(), &flip, 0, None) + .await + .expect("flip failed"); + + let contract_events = flip_res.contract_emitted_events()?; + + // then + assert_eq!(1, contract_events.len()); + let contract_event = &contract_events[0]; + let flipped: event_def::ForeignFlipped = + scale::Decode::decode(&mut &contract_event.event.data[..]) + .expect("encountered invalid contract event data buffer"); + assert_eq!(!init_value, flipped.value); + + let signature_topic = + ::SIGNATURE_TOPIC + .map(H256::from) + .unwrap(); + + let expected_topics = vec![signature_topic]; + assert_eq!(expected_topics, contract_event.topics); + + Ok(()) + } + + #[ink_e2e::test] + async fn emits_inline_event(mut client: ink_e2e::Client) -> E2EResult<()> { + // given + let init_value = false; + let constructor = EventsRef::new(init_value); + let contract = client + .instantiate("events", &ink_e2e::alice(), constructor, 0, None) + .await + .expect("instantiate failed"); + let mut call = contract.call::(); + + // when + let flip = call.flip_with_inline_event(); + let flip_res = client + .call(&ink_e2e::bob(), &flip, 0, None) + .await + .expect("flip failed"); + + let contract_events = flip_res.contract_emitted_events()?; + + // then + assert_eq!(1, contract_events.len()); + let contract_event = &contract_events[0]; + let flipped: InlineFlipped = + scale::Decode::decode(&mut &contract_event.event.data[..]) + .expect("encountered invalid contract event data buffer"); + assert_eq!(!init_value, flipped.value); + + let signature_topic = ::SIGNATURE_TOPIC + .map(H256::from) + .unwrap(); + + let expected_topics = vec![signature_topic]; + assert_eq!(expected_topics, contract_event.topics); + + Ok(()) + } + + #[ink_e2e::test] + async fn emits_event_with_option_topic_none( + mut client: ink_e2e::Client, + ) -> E2EResult<()> { + // given + let init_value = false; + let constructor = EventsRef::new(init_value); + let contract = client + .instantiate("events", &ink_e2e::alice(), constructor, 0, None) + .await + .expect("instantiate failed"); + let call = contract.call::(); + + // when + let call = call.emit_32_byte_topic_event(None); + let call_res = client + .call(&ink_e2e::bob(), &call, 0, None) + .await + .expect("emit_32_byte_topic_event failed"); + + let contract_events = call_res.contract_emitted_events()?; + + // then + assert_eq!(1, contract_events.len()); + let contract_event = &contract_events[0]; + let event: event_def::ThirtyTwoByteTopics = + scale::Decode::decode(&mut &contract_event.event.data[..]) + .expect("encountered invalid contract event data buffer"); + assert!(event.maybe_hash.is_none()); + + let signature_topic = + ::SIGNATURE_TOPIC + .map(H256::from) + .unwrap(); + + let expected_topics = vec![ + signature_topic, + [0x42; 32].into(), + [0x00; 32].into(), // None is encoded as 0x00 + ]; + assert_eq!(expected_topics, contract_event.topics); + + Ok(()) + } + } +} diff --git a/integration-tests/trait-erc20/lib.rs b/integration-tests/trait-erc20/lib.rs index 30f5517b725..4fb73f9ea07 100644 --- a/integration-tests/trait-erc20/lib.rs +++ b/integration-tests/trait-erc20/lib.rs @@ -269,25 +269,20 @@ mod erc20 { primitives::Clear, }; - type Event = ::Type; - fn assert_transfer_event( event: &ink::env::test::EmittedEvent, expected_from: Option, expected_to: Option, expected_value: Balance, ) { - let decoded_event = ::decode(&mut &event.data[..]) + let decoded_event = ::decode(&mut &event.data[..]) .expect("encountered invalid contract event data buffer"); - if let Event::Transfer(Transfer { from, to, value }) = decoded_event { - assert_eq!(from, expected_from, "encountered invalid Transfer.from"); - assert_eq!(to, expected_to, "encountered invalid Transfer.to"); - assert_eq!(value, expected_value, "encountered invalid Trasfer.value"); - } else { - panic!("encountered unexpected event kind: expected a Transfer event") - } + let Transfer { from, to, value } = decoded_event; + assert_eq!(from, expected_from, "encountered invalid Transfer.from"); + assert_eq!(to, expected_to, "encountered invalid Transfer.to"); + assert_eq!(value, expected_value, "encountered invalid Trasfer.value"); - fn encoded_into_hash(entity: &T) -> Hash + fn encoded_into_hash(entity: T) -> Hash where T: scale::Encode, { @@ -307,24 +302,23 @@ mod erc20 { result } - let expected_topics = [ - encoded_into_hash(&PrefixedValue { - prefix: b"", - value: b"Erc20::Transfer", - }), - encoded_into_hash(&PrefixedValue { - prefix: b"Erc20::Transfer::from", - value: &expected_from, - }), - encoded_into_hash(&PrefixedValue { - prefix: b"Erc20::Transfer::to", - value: &expected_to, - }), - encoded_into_hash(&PrefixedValue { - prefix: b"Erc20::Transfer::value", - value: &expected_value, - }), - ]; + let mut expected_topics = Vec::new(); + expected_topics.push( + ink::blake2x256!("Transfer(Option,Option,Balance)") + .into(), + ); + if let Some(from) = expected_from { + expected_topics.push(encoded_into_hash(from)); + } else { + expected_topics.push(Hash::CLEAR_HASH); + } + if let Some(to) = expected_to { + expected_topics.push(encoded_into_hash(to)); + } else { + expected_topics.push(Hash::CLEAR_HASH); + } + expected_topics.push(encoded_into_hash(value)); + for (n, (actual_topic, expected_topic)) in event.topics.iter().zip(expected_topics).enumerate() { @@ -546,27 +540,5 @@ mod erc20 { fn set_caller(sender: AccountId) { ink::env::test::set_caller::(sender); } - - /// For calculating the event topic hash. - struct PrefixedValue<'a, 'b, T> { - pub prefix: &'a [u8], - pub value: &'b T, - } - - impl scale::Encode for PrefixedValue<'_, '_, X> - where - X: scale::Encode, - { - #[inline] - fn size_hint(&self) -> usize { - self.prefix.size_hint() + self.value.size_hint() - } - - #[inline] - fn encode_to(&self, dest: &mut T) { - self.prefix.encode_to(dest); - self.value.encode_to(dest); - } - } } }