Contract Events
Events are custom data structures that are emitted by smart contracts during execution. They provide a way for smart contracts to communicate with the external world by logging information about specific occurrences in a contract.
Events play a crucial role in the integration of smart contracts in real-world applications. Take, for instance, the Non-Fungible Tokens (NFTs) minted on Starknet. An event is emitted each time a token is minted. This event is indexed and stored in some database, allowing applications to display almost instantaneously useful information to users. If the contract doesn't emit an event when minting a new token, it would be less practical, with the need of querying the state of the blockchain to get the data needed.
Defining Events
All the different events in a contract are defined under the Event
enum, which must implement the starknet::Event
trait. This trait is defined in the core library as follows:
trait Event<T> {
fn append_keys_and_data(self: T, ref keys: Array<felt252>, ref data: Array<felt252>);
fn deserialize(ref keys: Span<felt252>, ref data: Span<felt252>) -> Option<T>;
}
The #[derive(starknet::Event)]
attribute causes the compiler to generate an implementation for the above trait,
instantiated with the Event
type, which in our example is the following enum:
#[event]
#[derive(Drop, starknet::Event)]
enum Event {
StoredName: StoredName,
}
Each variant of the Event
enum has to be a struct or an enum, and each variant needs to implement the starknet::Event
trait itself. Moreover, the members of these variants must implement the Serde
trait (c.f. Appendix C: Serializing with Serde), as keys/data are added to the event using a serialization process.
The auto-implementation of the starknet::Event
trait will implement the append_keys_and_data
function for each variant of our Event
enum. The generated implementation will append a single key based on the variant name (StoredName
), and then recursively call append_keys_and_data
in the impl of the Event
trait for the variant’s type.
In our example, the Event
enum contains only one variant, which is a struct named StoredName
. We chose to name our variant with the same name as the struct name, but this is not enforced.
#[derive(Drop, starknet::Event)]
struct StoredName {
#[key]
user: ContractAddress,
name: felt252,
}
Whenever an enum that derives the starknet::Event
trait has an enum variant, this enum is nested by default. Therefore, the list of keys corresponding to the variant’s name will include the sn_keccak
hash of the variant's name itself. This can be superfluous, typically when using embedded components in contracts. Indeed, in such cases, we might want the events defined in the components to be emitted without any additional data, and it could be useful to annotate the enum variant with the #[flat]
attribute. By doing so, we allow to opt out of the nested behavior and ignore the variant name in the serialization process. On the other hand, nested events have the benefit of distinguishing between the main contract event and different components events, which might be helpful.
In our contract, we defined an event named StoredName
that emits the contract address of the caller and the name stored within the contract, where the user
field is serialized as a key and the name
field is serialized as data.
Indexing events fields allows for more efficient queries and filtering of events. To index a field as a key of an event, simply annotate it with the #[key]
attribute as demonstrated in the example for the user
key. By doing so, any indexed field will allow queries of events that contain a given value for that field with \( O(log(n)) \) time complexity, while non indexed fields require any query to iterate over all events, providing \( O(n) \) time complexity.
When emitting the event with self.emit(StoredName { user: user, name: name })
, a key corresponding to the name StoredName
, specifically sn_keccak(StoredName)
, is appended to the keys list. user
is serialized as key, thanks to the #[key]
attribute, while address is serialized as data. After everything is processed, we end up with the following keys and data: keys = [sn_keccak("StoredName"),user]
and data = [name]
.
Emitting Events
After defining events, we can emit them using self.emit
, with the following syntax:
self.emit(StoredName { user: user, name: name });
The emit
function is called on self
and takes a reference to self
, i.e., state modification capabilities are required. Therefore, it is not possible to emit events in view functions.