Contract Events
Events are a way for smart contracts to inform the outside world of any changes that occur during their execution. They play a critical role in the integration of smart contracts into real-world applications.
Technically speaking, an event is a custom data structure emitted by a smart contract during its execution and stored in the corresponding transaction receipt, allowing any external tool to parse and index it (most commonly, a Starknet SDK such as Starknet.js).
Defining Events
The events of a smart contract are defined in an enum annotated with the
attribute #[event]. This enum must be named Event.
#[starknet::interface]
pub trait IEventExample<TContractState> {
fn add_book(ref self: TContractState, id: u32, title: felt252, author: felt252);
fn change_book_title(ref self: TContractState, id: u32, new_title: felt252);
fn change_book_author(ref self: TContractState, id: u32, new_author: felt252);
fn remove_book(ref self: TContractState, id: u32);
}
#[starknet::contract]
mod EventExample {
#[storage]
struct Storage {}
#[event]
#[derive(Drop, starknet::Event)]
pub enum Event {
BookAdded: BookAdded,
#[flat]
FieldUpdated: FieldUpdated,
BookRemoved: BookRemoved,
}
#[derive(Drop, starknet::Event)]
pub struct BookAdded {
pub id: u32,
pub title: felt252,
#[key]
pub author: felt252,
}
#[derive(Drop, starknet::Event)]
pub enum FieldUpdated {
Title: UpdatedTitleData,
Author: UpdatedAuthorData,
}
#[derive(Drop, starknet::Event)]
pub struct UpdatedTitleData {
#[key]
pub id: u32,
pub new_title: felt252,
}
#[derive(Drop, starknet::Event)]
pub struct UpdatedAuthorData {
#[key]
pub id: u32,
pub new_author: felt252,
}
#[derive(Drop, starknet::Event)]
pub struct BookRemoved {
pub id: u32,
}
#[abi(embed_v0)]
impl EventExampleImpl of super::IEventExample<ContractState> {
fn add_book(ref self: ContractState, id: u32, title: felt252, author: felt252) {
// ... logic to add a book in the contract storage ...
self.emit(BookAdded { id, title, author });
}
fn change_book_title(ref self: ContractState, id: u32, new_title: felt252) {
self.emit(FieldUpdated::Title(UpdatedTitleData { id, new_title }));
}
fn change_book_author(ref self: ContractState, id: u32, new_author: felt252) {
self.emit(FieldUpdated::Author(UpdatedAuthorData { id, new_author }));
}
fn remove_book(ref self: ContractState, id: u32) {
self.emit(BookRemoved { id });
}
}
}
#[cfg(test)]
mod tests;
Each variant, like BookAdded or FieldUpdated represents an event that can be
emitted by the contract. The variant data represents the data associated to an
event. It can be any struct or enum that implements the starknet::Event
trait. This can be simply achieved by adding a #[derive(starknet::Event)]
attribute on top of your type definition.
Each event data field can be annotated with the attribute #[key]. Key fields
are then stored separately than data fields to be used by external tools to
easily filter events on these keys.
Let's look at the full event definition of this example to add, update and remove books:
#[starknet::interface]
pub trait IEventExample<TContractState> {
fn add_book(ref self: TContractState, id: u32, title: felt252, author: felt252);
fn change_book_title(ref self: TContractState, id: u32, new_title: felt252);
fn change_book_author(ref self: TContractState, id: u32, new_author: felt252);
fn remove_book(ref self: TContractState, id: u32);
}
#[starknet::contract]
mod EventExample {
#[storage]
struct Storage {}
#[event]
#[derive(Drop, starknet::Event)]
pub enum Event {
BookAdded: BookAdded,
#[flat]
FieldUpdated: FieldUpdated,
BookRemoved: BookRemoved,
}
#[derive(Drop, starknet::Event)]
pub struct BookAdded {
pub id: u32,
pub title: felt252,
#[key]
pub author: felt252,
}
#[derive(Drop, starknet::Event)]
pub enum FieldUpdated {
Title: UpdatedTitleData,
Author: UpdatedAuthorData,
}
#[derive(Drop, starknet::Event)]
pub struct UpdatedTitleData {
#[key]
pub id: u32,
pub new_title: felt252,
}
#[derive(Drop, starknet::Event)]
pub struct UpdatedAuthorData {
#[key]
pub id: u32,
pub new_author: felt252,
}
#[derive(Drop, starknet::Event)]
pub struct BookRemoved {
pub id: u32,
}
#[abi(embed_v0)]
impl EventExampleImpl of super::IEventExample<ContractState> {
fn add_book(ref self: ContractState, id: u32, title: felt252, author: felt252) {
// ... logic to add a book in the contract storage ...
self.emit(BookAdded { id, title, author });
}
fn change_book_title(ref self: ContractState, id: u32, new_title: felt252) {
self.emit(FieldUpdated::Title(UpdatedTitleData { id, new_title }));
}
fn change_book_author(ref self: ContractState, id: u32, new_author: felt252) {
self.emit(FieldUpdated::Author(UpdatedAuthorData { id, new_author }));
}
fn remove_book(ref self: ContractState, id: u32) {
self.emit(BookRemoved { id });
}
}
}
#[cfg(test)]
mod tests;
In this example:
- There are 3 events:
BookAdded,FieldUpdatedandBookRemoved, BookAddedandBookRemovedevents use a simplestructto store their data while theFieldUpdatedevent uses anenumof structs,- In the
BookAddedevent, theauthorfield is a key field and will be used outside of the smart contract to filterBookAddedevents byauthor, whileidandtitleare data fields.
The variant and its associated data structure can be named differently, although it's common practice to use the same name. The variant name is used internally as the first event key to represent the name of the event and to help filter events, while the variant data name is used in the smart contract to build the event before it is emitted.
The #[flat] attribute
Sometimes you may have a complex event structure with some nested enums like the
FieldUpdated event in the previous example. In this case, you can flatten this
structure using the #[flat] attribute, which means that the inner variant name
is used as the event name instead of the variant name of the annotated enum. In
the previous example, because the FieldUpdated variant is annotated with
#[flat], when you emit a FieldUpdated::Title event, its name will be Title
instead of FieldUpdated. If you have more than 2 nested enums, you can use the
#[flat] attribute on multiple levels.
Emitting Events
Once you have defined your list of events, you want to emit them in your smart
contracts. This can be simply achieved by calling self.emit() with an event
data structure in parameter.
#[starknet::interface]
pub trait IEventExample<TContractState> {
fn add_book(ref self: TContractState, id: u32, title: felt252, author: felt252);
fn change_book_title(ref self: TContractState, id: u32, new_title: felt252);
fn change_book_author(ref self: TContractState, id: u32, new_author: felt252);
fn remove_book(ref self: TContractState, id: u32);
}
#[starknet::contract]
mod EventExample {
#[storage]
struct Storage {}
#[event]
#[derive(Drop, starknet::Event)]
pub enum Event {
BookAdded: BookAdded,
#[flat]
FieldUpdated: FieldUpdated,
BookRemoved: BookRemoved,
}
#[derive(Drop, starknet::Event)]
pub struct BookAdded {
pub id: u32,
pub title: felt252,
#[key]
pub author: felt252,
}
#[derive(Drop, starknet::Event)]
pub enum FieldUpdated {
Title: UpdatedTitleData,
Author: UpdatedAuthorData,
}
#[derive(Drop, starknet::Event)]
pub struct UpdatedTitleData {
#[key]
pub id: u32,
pub new_title: felt252,
}
#[derive(Drop, starknet::Event)]
pub struct UpdatedAuthorData {
#[key]
pub id: u32,
pub new_author: felt252,
}
#[derive(Drop, starknet::Event)]
pub struct BookRemoved {
pub id: u32,
}
#[abi(embed_v0)]
impl EventExampleImpl of super::IEventExample<ContractState> {
fn add_book(ref self: ContractState, id: u32, title: felt252, author: felt252) {
// ... logic to add a book in the contract storage ...
self.emit(BookAdded { id, title, author });
}
fn change_book_title(ref self: ContractState, id: u32, new_title: felt252) {
self.emit(FieldUpdated::Title(UpdatedTitleData { id, new_title }));
}
fn change_book_author(ref self: ContractState, id: u32, new_author: felt252) {
self.emit(FieldUpdated::Author(UpdatedAuthorData { id, new_author }));
}
fn remove_book(ref self: ContractState, id: u32) {
self.emit(BookRemoved { id });
}
}
}
#[cfg(test)]
mod tests;
To have a better understanding of what happens under the hood, let's see two examples of emitted events and how they are stored in the transaction receipt:
Example 1: Add a book
In this example, we send a transaction invoking the add_book function with
id = 42, title = 'Misery' and author = 'S. King'.
If you read the "events" section of the transaction receipt, you will get something like:
"events": [
{
"from_address": "0x27d07155a12554d4fd785d0b6d80c03e433313df03bb57939ec8fb0652dbe79",
"keys": [
"0x2d00090ebd741d3a4883f2218bd731a3aaa913083e84fcf363af3db06f235bc",
"0x532e204b696e67"
],
"data": [
"0x2a",
"0x4d6973657279"
]
}
]
In this receipt:
from_addressis the address of your smart contract,keyscontains the key fields of the emittedBookAddedevent, serialized in an array offelt252.- The first key
0x2d00090ebd741d3a4883f2218bd731a3aaa913083e84fcf363af3db06f235bcis the selector of the event name, which is the variant name in theEventenum, soselector!("BookAdded"), - The second key
0x532e204b696e67 = 'S. King'is theauthorfield of your event as it has been defined using the#[key]attribute,
- The first key
datacontains the data fields of the emittedBookAddedevent, serialized in an array offelt252. The first item0x2a = 42is theiddata field and0x4d6973657279 = 'Misery'is thetitledata field.
Example 2: Update a book author
Now we want to change the author name of the book, so we send a transaction
invoking change_book_author with id = 42 and new_author = 'Stephen
King'.
This change_book_author call emits a FieldUpdated event with the event data
FieldUpdated::Author(UpdatedAuthorData { id: 42, title: author: 'Stephen King' }).
If you read the "events" section of the transaction receipt, you will get
something like:
"events": [
{
"from_address": "0x27d07155a12554d4fd785d0b6d80c03e433313df03bb57939ec8fb0652dbe79",
"keys": [
"0x1b90a4a3fc9e1658a4afcd28ad839182217a69668000c6104560d6db882b0e1",
"0x2a"
],
"data": [
"0x5374657068656e204b696e67"
]
}
]
As the FieldUpdated variant in Event enum has been annotated with the
#[flat] attribute, this is the inner variant Author that is used as event
name, instead of FieldUpdated. So:
- the first key is
selector!("Author"), - the second key is the
idfield, annotated with#[key], - the data field is
0x5374657068656e204b696e67 = 'Stephen King'.