Events
Onchain storage keeps the current state of the application: objects, their fields, and their owners. What it does not keep is the history of actions that led to this state. A marketplace module stores listed items, but once an item is sold and the object changes hands, there is no onchain trace of the purchase - the price paid, the time of the sale, or the parties involved. Applications, however, often need exactly that: an activity feed, a trading history, or analytics.
Events are the mechanism for this. An event is a piece of data attached to the result of a successful transaction and stored offchain. Emitting an event does not modify any objects and costs no storage fees; instead, events are indexed by full nodes, and offchain services can query or subscribe to them. Events are the main way for a Move program to communicate with the outside world.
Definition
Events are emitted with the emit function defined in the sui::event module of the Sui Framework:
module sui::event;
/// Emit a custom Move event, sending the data offchain.
///
/// Used for creating custom indexes and tracking onchain
/// activity in a way that suits a specific application the most.
///
/// The type `T` is the main way to index the event, and can contain
/// phantom parameters, eg `emit(MyEvent<phantom T>)`.
public native fun emit<T: copy + drop>(event: T);
An event can be any custom type with the copy and drop abilities. Additionally, the Sui Verifier requires the type to be internal to the module that emits it: it is impossible to emit a type defined in another module, and, even though they satisfy the copy + drop requirement, primitive types cannot be emitted either. This rule makes the event type an unforgeable label - an ItemPurchased event can only ever originate from the module that declares it.
Emitting Events
To emit an event, define a struct for it and pass an instance of the struct to event::emit. The event data is passed by value and sent offchain as part of the transaction result:
module book::events;
use sui::coin::Coin;
use sui::sui::SUI;
use sui::event;
/// The item that can be purchased.
public struct Item has key { id: UID }
/// Event emitted when an item is purchased. Contains the ID of the item and
/// the price for which it was purchased.
public struct ItemPurchased has copy, drop {
item: ID,
price: u64
}
/// A marketplace function which performs the purchase of an item.
public fun purchase(seller: address, coin: Coin<SUI>, ctx: &mut TxContext): Item {
let item = Item { id: object::new(ctx) };
// Create an instance of `ItemPurchased` and pass it to `event::emit`.
event::emit(ItemPurchased {
item: object::id(&item),
price: coin.value()
});
// Send the payment to the seller, return the item to the caller.
transfer::public_transfer(coin, seller);
item
}
The type of the event serves as the primary filter for offchain queries - services subscribe to ItemPurchased events by naming the type. This suggests a simple design principle: emit a dedicated type per action, and name it after the action that happened, in past tense - ItemPurchased, AuctionStarted, ConfigUpdated. Inside the event, include the values an indexer would need to make sense of the action without fetching anything else: the IDs of the objects involved, amounts, and the relevant addresses.
Note that events are attached to a successful transaction: if the transaction aborts after the emit call, no events are recorded.
Event Structure
Events become part of the transaction effects, and the system attaches metadata to each of them:
- the sender - the address that signed the transaction;
- the transaction digest - linking the event to the transaction that emitted it;
- the timestamp - the time of the checkpoint that finalized the transaction, shared by all events of that transaction;
- the type signature of the event, including the package and module that emitted it.
Because the sender and the transaction digest are always present in the metadata, there is no need to duplicate them in the event fields. A sender: address field in an event struct is redundant, unless the "logical" sender differs from the transaction signer (for example, in a sponsored transaction executed on behalf of a user).
It is important to understand that events are a one-way channel. Emitted events are not stored onchain and cannot be read back by Move code - not in the same transaction, and not in any later one. If a value needs to be accessed by the program, it belongs in an object; if it needs to be seen by the outside world, it belongs in an event.
Testing Events
Because events are the interface between the application and its offchain services, it is important to test that the right events are emitted with the right values. The sui::event module provides two test-only functions for this: num_events, returning the number of events emitted so far in the test, and events_by_type<T>, returning a vector of all emitted events of type T.
#[test]
fun test_emit_item_purchased() {
let ctx = &mut tx_context::dummy();
let item = Item { id: object::new(ctx) };
let item_id = object::id(&item);
event::emit(ItemPurchased { item: item_id, price: 100 });
// Total number of events emitted in this test so far.
assert_eq!(event::num_events(), 1);
// Read back all `ItemPurchased` events and check their contents.
let purchases = event::events_by_type<ItemPurchased>();
assert_eq!(purchases.length(), 1);
assert_eq!(purchases[0].item, item_id);
assert_eq!(purchases[0].price, 100);
std::unit_test::destroy(item);
}
Since event structs are internal to the module, tests placed in the same module (or in a test module of the same package with appropriate accessors) can inspect their fields directly.
Summary
- Events attach application-defined data to the transaction result; they are indexed offchain and are the main way to notify the outside world about onchain activity.
- Any custom type with copy and drop can be an event, but it must be internal to the emitting module - this makes the event type an unforgeable label.
- Event metadata already contains the sender, the transaction digest, and a timestamp; event fields should carry action-specific data, such as object IDs and amounts.
- Events cannot be read back by Move code - they are a one-way channel.
- Use num_events and events_by_type<T> to test emitted events.
Further Reading
- sui::event module documentation.
- Using Events in the Sui Documentation - querying and subscribing to events offchain.