Los eventos emiten datos de la ejecución del contrato hacia el recibo de la transacción (transaction receipt). El recibo contiene metadatos sobre lo que sucedió durante la ejecución, los cuales pueden ser consultados o indexados por aplicaciones externas. La sintaxis de los eventos de Cairo es más detallada que la de Solidity, pero cumple el mismo propósito.
En este artículo, entenderás cómo funcionan los eventos en Starknet.
Estructura de los Eventos en Cairo
Los eventos en Cairo deben listarse en un enum Event marcado con el atributo #[event]. A diferencia de las declaraciones de eventos individuales de Solidity, Cairo requiere que todos los eventos se organicen dentro de una estructura enum central.
Aquí hay un ejemplo que lista dos eventos, uno para el registro de usuarios y otro para el inicio de sesión de usuarios:
// Event emitted when a new user registers
#[derive(Drop, starknet::Event)]
pub struct UserRegistered {
pub user_id: u32,
pub username: ByteArray
}
// Event emitted when a user logs in
#[derive(Drop, starknet::Event)]
pub struct UserLoggedIn {
pub user_id: u32,
pub timestamp: u64
}
// Main event enum that holds all possible events this contract can emit
#[event]
#[derive(Drop, starknet::Event)]
pub enum Event {
NewUser: UserRegistered, // references UserRegistered struct
UserLogin: UserLoggedIn // references UserLoggedIn struct
}
Nota: El trait Drop permite a Cairo limpiar automáticamente de la memoria los structs y enums cuando ya no se necesitan. Verás #[derive(Drop)] en la mayoría de los structs y enums de Cairo.
La representación en Solidity de los dos eventos sería:
event NewUser(uint32 userID, string username);
event UserLogin(uint32 userID, uint64 timestamp);
En el código de Cairo anterior, definimos dos structs de eventos (UserRegistered y UserLoggedIn) que especifican la estructura de datos para cada tipo de evento. Ambos structs implementan el trait starknet::Event mediante el atributo derive.
Estos structs separados se unifican (listan) bajo un solo enum Event, donde cada variante hace referencia a su struct correspondiente. Cuando se emiten los eventos, los nombres de las variantes del enum (NewUser, UserLogin) sirven como identificadores de búsqueda de los eventos.
Aunque normalmente verás el mismo nombre para la variante del enum (como NewUser) y su struct asociado (como UserRegistered), no tienen que coincidir. Aquí se usa un nombre diferente para resaltar la distinción.
Los SDK de Starknet, como Starknet.js, pueden filtrar y consultar eventos utilizando estos identificadores. Por ejemplo, para encontrar todos los registros de usuarios, consultarías por eventos con el nombre "NewUser".
Al trabajar con eventos, a menudo querrás filtrar eventos por los datos específicos que contienen, como encontrar todos los eventos para un ID de usuario en particular o dentro de un cierto rango de valores. Aquí es donde entran en juego los parámetros indexados, al igual que en Solidity.
Eventos Indexados (Campos Clave)
Los campos de los eventos pueden ser marcados para su indexación utilizando el atributo #[key] (similar a la palabra clave indexed en Solidity). Por ejemplo, si queremos que user_id sea buscable en los registros de usuarios y timestamp sea buscable para los inicios de sesión, haríamos:
#[derive(Drop, starknet::Event)]
pub struct UserRegistered {
#[key]
pub user_id: u32, // user_id IS MARKED is AS INDEXED (searchable key)
pub username: ByteArray // username IS STORED AS EVENT DATA (not indexed)
}
#[derive(Drop, starknet::Event)]
pub struct UserLoggedIn {
pub user_id: u32, // user_id IS STORED AS EVENT DATA (not indexed)
#[key]
pub timestamp: u64 // timestamp IS MARKED AS INDEXED
}
// Main event enum that holds all possible events this contract can emit
#[event]
#[derive(Drop, starknet::Event)]
pub enum Event {
UserRegistered: UserRegistered,
UserLoggedIn: UserLoggedIn
}
Su equivalente en Solidity sería:
event UserRegistered(uint32 indexed userID, string username);
event UserLoggedIn(uint32 userID, uint64 indexed timestamp);
La ubicación de #[key] depende de qué campo específico desees hacer buscable en los logs de eventos. Un campo es un elemento de datos dentro de un struct; por ejemplo, user_id y username son campos en el struct UserRegistered.
En UserRegistered, estamos indexando user_id para filtrar por usuario, mientras que en UserLoggedIn, estamos indexando timestamp para filtrar por tiempo.
Solo deberías añadir #[key] a los campos que realmente vayas a consultar o filtrar; cada campo debe ser anotado individualmente según tus necesidades específicas de filtrado.
Los campos clave (key fields) se almacenan por separado de los campos de datos regulares en los recibos de transacción para permitir que los SDK de Starknet filtren eventos rápidamente sin procesar todos los datos del evento.
Estructura de Datos de Eventos en los Recibos de Transacción
Un recibo de transacción es un registro que contiene información detallada sobre una transacción exitosa. Incluye detalles del bloque (block_hash, block_number), el hash de la transacción, el estado de ejecución (execution_status, finality_status), el consumo de gas (execution_resources), las tarifas de transacción (actual_fee), etc., y cualquier evento que se haya emitido durante la ejecución.
Cada recibo de transacción contiene un array events con claves (keys) y datos (data) para todos los eventos emitidos donde:
data: representa un array que contiene valores de campos serializados no indexadosfrom_address: es la dirección del contrato que emitió el eventokeys: es un array que siempre contiene el hash del selector de evento enkeys[0], además de cualquier valor de campo indexado enkeys[1],keys[2], etc.
El array keys está presente en cada evento, independientemente de si usas anotaciones #[key] en tu contrato. Como mínimo, contiene el hash del selector de evento que identifica el tipo de evento (es decir, Transfer, etc.).
A continuación se muestra un recibo de transacción de ejemplo con la estructura del array events resaltada en el recuadro rosa:

Aquí está el código TypeScript que generó este recibo de transacción basado en el hash de transacción proporcionado utilizando el método getTransactionReceipt:
import { RpcProvider } from "starknet";
import * as dotenv from "dotenv";
dotenv.config();
async function getTxnReceipt() {
// Initialize RPC provider with Sepolia testnet endpoint
const alchemyApiKey = process.env.ALCHEMY_API_KEY;
// initialize provider for Sepolia testnet with Alchemy
const provider = new RpcProvider({
nodeUrl: `https://starknet-sepolia.g.alchemy.com/starknet/version/rpc/v0_8/${alchemyApiKey}`,
});
// Transaction hash to query (replace with actual hash)
const transactionHash =
"0x5df0e42012440f59eb9cdd7994a3001b72cebc781bd8527fb3a5343cdb9d6f7";
try {
// Fetch transaction receipt from the network
const receipt: any = await provider.getTransactionReceipt(transactionHash);
// Display formatted receipt data
console.log(JSON.stringify(receipt, null, 2));
} catch (error) {
// Handle network or transaction errors
console.error("Error getting transaction receipt:", error);
}
}
// Execute the function
getTxnReceipt();
Este ejemplo solo muestra cómo aparecen los eventos en los recibos de transacciones. Diferentes técnicas de consulta con Starknet.js se cubren en una sección posterior del artículo.
Entendiendo el Array Keys
En el recibo de transacción anterior, el evento tiene solo una clave (keys[0]) que contiene el hash del selector de evento 0x99cd8bde557814842a3121e8ddfd433a539b8c9f14bf31ebf108d12e6196e9.

Este hash del selector de evento (keys[0]) representa un evento Transfer sin parámetros indexados, con todos sus campos no indexados almacenados en el array data:

El hash del selector de evento se calcula utilizando:
const nameHash = num.toHex(hash.starknetKeccak('EventName'));
Comparación de la Estructura de Eventos de Cairo con Solidity
En Solidity, los datos de los eventos se estructuran usando topics (temas) y data (datos) como se ve en este ejemplo:

- topic0: Siempre contiene el hash de la firma del evento. En el ejemplo anterior, el evento
NewUser(uint32,string)tiene el hashkeccak256("NewUser(uint32,string)"), el cual es igual a0x37ecc4388271ab7af2220881c1f2f70fbea71e6b1635107f9daffa0fab84d5b3. - topic1: Contiene el parámetro indexado
user_id. - Campo Data: Contiene todos los parámetros no indexados (en hexadecimal).
Cairo sigue un patrón similar con el array keys. Para comparar, mira esta transacción en Starknet Sepolia que muestra un evento con múltiples claves:

keys[0](resaltado por la flecha blanca) contiene el hash del selector de evento (Event8Indexed).- Los elementos posteriores en el array (por ejemplo,
keys[1],keys[2], …keys[10]), mostrados con la flecha verde, representan los campos indexados (#[key]) del evento. - Los parámetros no indexados se almacenan por separado en el campo
data, como se ilustra en el recuadro morado.
Basado en la imagen anterior, la sección KEYS contiene un total de diez (10) parámetros indexados. Esto resalta una ventaja clave de Cairo sobre Solidity: mientras que Solidity limita los parámetros indexados a tres (topic1-topic3), con topic0 siempre reservado para la firma del evento, Cairo permite hasta cincuenta (50) parámetros indexados. Esto elimina la necesidad de soluciones alternativas como los eventos anónimos en Solidity (que pueden tener hasta 4 parámetros indexados pero pierden la firma del evento).
Al igual que en Solidity, los campos no indexados en Cairo requieren una búsqueda manual a través del array data, mientras que los campos indexados (marcados con #[key]) se pueden filtrar eficientemente utilizando los SDK de Starknet.
Cómo Funcionan los Eventos Internamente
El sistema de eventos de Starknet en Cairo gira principalmente en torno a dos traits:
Event: maneja la serialización y deserialización de eventos.EventEmitter: proporciona la capacidad de emisión usandoself.emit(...)dentro de las funciones del contrato.
Trait Event
El trait Event proporciona métodos que serializan eventos, deserializan eventos y generan un identificador interno del tipo de evento utilizado para el filtrado e indexación de eventos. Cualquier struct o enum marcado con #[derive(starknet::Event)] obtiene automáticamente implementaciones para estos métodos clave:
| Método | Propósito |
|---|---|
append_keys_and_data |
Serializa el evento dividiendo los campos indexados (#[key]) y no indexados en arrays keys y data separados |
deserialize(ref keys, ref data) -> Option<T> |
Reconstruye el evento original a partir de los datos emitidos en el recibo de la transacción; devuelve None si es inválido |
event_type_name |
Identificador interno utilizado para calcular el selector de evento para filtrado e indexación |
Ten en cuenta que no implementas estos métodos manualmente. Se generan automáticamente cuando usas #[derive(starknet::Event)], lo que configura los eventos en el contrato con todo lo necesario para serializar y reconstruir los datos del evento.
Entendiendo la Serialización de Eventos
El trait Event maneja la serialización de eventos automáticamente, pero cuando los eventos contienen tipos de campos complejos como arrays o structs anidados, depende del trait Serde para ayuda adicional con la serialización.
El trait Serde convierte los tipos complejos de Cairo en una secuencia de valores felt252 compatibles con la Cairo VM. En Cairo, felt252 es el único tipo primitivo entendido por la Cairo VM, por lo que cualquier valor mayor a 252 bits debe descomponerse en una lista de valores felt252.
Los tipos manejados automáticamente por el trait Event son:
- Tipos simples:
u8,u16,u32,u64,u128,bool,felt252,ContractAddress ByteArray: Serializado automáticamente sin derivación manual deSerde
Como se cubrió en capítulos anteriores, ByteArray es un tipo de Cairo que representa cadenas de texto (strings). Es un struct con tres campos:
data: Array<felt252>: contiene fragmentos de 31 bytes de los datos del stringpending_word: felt252: bytes restantes después de llenar el arraydatacon fragmentos completos de 31 bytes (hasta 30 bytes)pending_word_len: u32: número de bytes enpending_word
Cairo primero empaqueta fragmentos completos de 31 bytes en el array data, y luego coloca los bytes sobrantes en pending_word. Por ejemplo, “serah” se serializa como:
data:[](array vacío - no se necesitan fragmentos de 31 bytes para un string de 5 bytes)pending_word:0x7365726168(contiene los bytes reales del string en formato hexadecimal)pending_word_len:0x5(5 bytes en total)
Dado que ByteArray se usa comúnmente y es parte de la biblioteca estándar de Cairo, el trait Event incluye soporte de serialización automática para él.
Esta serialización automática es lo que nos permite usar ByteArray en eventos sin derivar Serde manualmente. Veremos el desglose detallado de la serialización de ByteArray cuando examinemos los datos de una transacción real más adelante en el artículo.
Los tipos complejos que requieren derivación manual de Serde son:
- Structs personalizados: Estructuras definidas por el usuario como datos anidados
- Arrays:
Array<u32>,Array<ByteArray>, etc. - Enums definidos por el usuario con datos
Cuando un evento contiene campos de estos tipos complejos, esos tipos de campos deben derivar Serde para que el trait Event pueda serializarlos durante la emisión. Sin esto, el compilador no puede procesar los eventos correctamente. Un ejemplo práctico de esto se mostrará en la sección “Manejo de Tipos de Campos de Eventos Complejos” del artículo.
Trait EventEmitter
El trait EventEmitter permite que los eventos se emitan a través de:
self.emit(EventStruct { ... });
Durante la ejecución del contrato, serializa el evento usando el trait Event y almacena el resultado en el recibo de la transacción. Para eventos que contienen campos de structs personalizados, esos structs deben derivar Serde por separado para que el trait Event pueda serializarlos adecuadamente.
El siguiente diagrama visualiza el flujo de trabajo de la serialización de eventos, mostrando qué tipos de eventos se pueden serializar automáticamente y cuáles requieren soporte adicional de Serde antes de la emisión:

Con los conceptos básicos cubiertos, la siguiente sección explora cómo funcionan las estructuras de eventos cuando están anidadas o incluyen tipos complejos.
Manejo de Tipos de Campos de Eventos Complejos
Aquí está el evento UserRegistered actualizado con tipos de campos adicionales. UserMetadata es un struct personalizado que contiene datos del entorno del usuario (tipo de dispositivo e información de ubicación). Al ser un struct anidado dentro del evento UserRegistered, requiere una serialización adecuada:
#[derive(Drop, starknet::Event)]
pub struct UserRegistered {
#[key]
pub user_id: u32,
pub username: ByteArray,
pub metadata: UserMetadata,
pub tag_count: u32,
pub timestamp: u64,
}
#[derive(Drop, Serde)]
pub struct UserMetadata {
pub device_type: ByteArray,
pub ip_region: ByteArray,
}
Dado que UserMetadata es un tipo complejo, debe derivar Serde para que pueda ser serializado correctamente en valores felt252.
Esto se relaciona con nuestra nota anterior sobre las restricciones de la Cairo VM: los tipos complejos deben ser serializados correctamente para una emisión de eventos exitosa.
Sin Serde en el struct UserMetadata, el código fallará al compilar, como se muestra a continuación:

UserMetadata necesita Serde porque es un struct personalizado. El struct del evento UserRegistered solo necesita starknet::Event (el trait Event maneja los tipos básicos automáticamente pero delega en Serde para los tipos de campos complejos).
Además, ten en cuenta que los campos indexados de tipos complejos (#[key]) se almacenan como valores hasheados en el array keys del evento y no se pueden recuperar directamente desde los logs de transacciones.
Evento UserRegistered actual (enfoque recomendado):
Este diseño utiliza user_id (un tipo primitivo u32) como campo indexado, el cual permanece legible en los logs de transacciones para una consulta eficiente:
#[derive(Drop, starknet::Event)]
pub struct UserRegistered {
#[key]
pub user_id: u32, // Primitive type (stays readable)
pub username: ByteArray, // Non-indexed (in data array)
pub metadata: UserMetadata, // Non-indexed (in data array)
pub tag_count: u32, // Non-indexed (in data array)
pub timestamp: u64, // Non-indexed (in data array)
}
Recibo de transacción:
{
"keys": [
"0x...event_selector", // key[0] is always the event selector.
"0x7b" // user_id = 123 (readable as hex)
],
"data": [
"username_serialized",
"metadata_serialized",
"tag_count_serialized",
"timestamp_serialized"
]
}
Con este enfoque, puedes consultar fácilmente eventos donde user_id = 123 porque el valor 0x7b es directamente legible en el array keys (keys[1]).
Si usáramos el struct complejo UserMetadata como un campo indexado, crearía desafíos para las consultas:
#[derive(Drop, starknet::Event)]
pub struct UserRegistered {
#[key]
pub metadata: UserMetadata, // Complex struct as indexed field - BAD!
pub user_id: u32,
pub username: ByteArray,
}
Lo que verías en el recibo:
{
"keys": [
"0x...event_selector",
"0xa1b2c3d4e5f67890..." // Hashed UserMetadata - unreadable!
],
"data": ["0x7b", "username_serialized"]
}
Todo el UserMetadata se convierte en un hash ilegible en el array keys. No podemos consultar usuarios por device_type o ip_region porque estos valores están ocultos dentro del hash. Esta es la razón por la que los tipos primitivos como u32 funcionan mejor para los campos indexados cuando necesitamos filtrar eventos de manera eficiente. Por lo tanto, si planeas consultar eventos por campos indexados, es mejor usar tipos primitivos como u32, felt252 o ContractAddress.
Uso del atributo #[flat]
El atributo #[flat] altera la forma en que las variantes de eventos se nombran e identifican en los logs de transacciones. Se utiliza para aplanar enums de eventos anidados para hacer que la consulta y el filtrado de eventos específicos sean más fáciles.
Este atributo aborda estructuras enum anidadas, no tipos de campos complejos. Este es un concepto distinto de la indexación compleja que acabamos de discutir.
El atributo
#[flat]aplana la jerarquía de nombres del evento, no la estructura de datos en sí.
Cuando se usa en una variante de evento en el enum Event, cambia el cálculo del hash del selector de evento para que use el nombre de la variante interna en lugar del nombre del enum externo.
Enums Externos, Enum Interno y Variantes Internas
Para entender cómo funciona #[flat], necesitamos distinguir entre estos tres niveles de estructura enum:
// OUTER enum (the main Event enum)
pub enum Event {
UserRegistered: UserRegistered,
#[flat]
UserDataUpdated: UserDataUpdated, // <- This references the INNER enum
}
// INNER enum (nested inside the outer enum structure)
pub enum UserDataUpdated {
DeviceType: UpdatedDeviceType, // <- These are the inner variants
IpRegion: UpdatedIpRegion, // <- These are the inner variants
}
- Enum externo (Outer enum): El enum
Eventprincipal que contiene todos los eventos posibles para el contrato. - Enum interno (Inner enum): El enum
UserDataUpdatedque contiene variantes específicas (DeviceTypeeIpRegion). - Variante interna (Inner variant): Las variantes enum individuales (
DeviceTypeeIpRegion) dentro del enumUserDataUpdated, referenciando cada una a su propio struct de evento.
Ejemplo Completo
El siguiente ejemplo muestra un contrato con múltiples tipos de eventos: un evento de struct simple (UserRegistered) y un evento enum anidado (UserDataUpdated) que contiene dos variantes:
// Main event enum that holds all possible events this contract can emit
#[event]
#[derive(Drop, starknet::Event)]
pub enum Event {
UserRegistered: UserRegistered,
#[flat] // NEWLY ADDED (flattens nested event enum)
UserDataUpdated: UserDataUpdated,
}
// event for user registration
#[derive(Drop, starknet::Event)]
pub struct UserRegistered {
#[key]
pub user_id: u32, // Indexed user ID for filtering
pub username: ByteArray, // Username as event data
pub metadata: UserMetadata, // User metadata struct
}
// Nested event enum containing different types of user data updates
#[derive(Drop, starknet::Event)]
pub enum UserDataUpdated {
DeviceType: UpdatedDeviceType, // Device type change event
IpRegion: UpdatedIpRegion, // IP region change event
}
// Event for device type updates
#[derive(Drop, starknet::Event)]
pub struct UpdatedDeviceType {
#[key]
pub user_id: u32, // Indexed user ID
pub new_device_type: ByteArray, // New device type value
}
// Event for IP region updates
#[derive(Drop, starknet::Event)]
pub struct UpdatedIpRegion {
#[key]
pub user_id: u32, // Indexed user ID
pub new_ip_region: ByteArray, // New IP region value
}
// User metadata structure containing device and location info
#[derive(Drop, Serde)]
pub struct UserMetadata {
pub device_type: ByteArray, // User's device type
pub ip_region: ByteArray, // User's IP region
}
Observa cómo se aplica el atributo #[flat] a la variante UserDataUpdated (enum anidado) en el enum Event principal; esto es lo que cambia la forma en que las variantes internas (DeviceType e IpRegion) aparecen en los logs de transacciones.
Recuerda que el hash del selector de evento (almacenado en keys[0] del recibo de transacción) se calcula usando starknetKeccak("EventName").
Sin el atributo #[flat], el hash del selector de evento se deriva del nombre del enum externo: starknetKeccak("UserDataUpdated"). Esto significa que todas las variantes del enum (DeviceType e IpRegion) comparten el mismo selector de evento, por lo que no puedes consultar variantes específicas, solo puedes consultar eventos "UserDataUpdated" en general.
{
"keys": ["0x...hash_of_UserDataUpdated"], // Same selector for all variants*
"data": [...],
"from_address": "0x..."
}
Pero cuando usamos #[flat], el hash del selector de evento se calcula a partir del nombre de la variante interna: starknetKeccak("DeviceType") / starknetKeccak("IpRegion"), por lo que DeviceType e IpRegion obtienen cada uno su propio hash selector para un filtrado y consulta precisos.
// DeviceType event
{
"keys": ["0x...hash_of_DeviceType"], // Unique selector
"data": [...],
"from_address": "0x..."
}
// IpRegion event
{
"keys": ["0x...hash_of_IpRegion"], // Different unique selector
"data": [...],
"from_address": "0x..."
}
El atributo #[flat] solo afecta la nomenclatura de eventos y el cálculo del selector; la estructura de datos real, los campos y la serialización permanecen sin cambios. Esto hace que el filtrado de eventos y la inspección de logs sean mucho más fáciles al trabajar con enums de eventos anidados.
El atributo #[flat] se usa comúnmente en las bibliotecas de componentes de OpenZeppelin para asegurar que los eventos de los componentes coincidan con la estructura de eventos estándar.
Por ejemplo, al usar los componentes ERC20 y Ownable, #[flat] elimina el prefijo de ID del componente de los eventos, por lo que los eventos Transfer y Approval de ERC20, junto con el evento OwnershipTransferred de Ownable, aparecen con su propio hash selector como la primera clave (key), tal como lo harían en contratos independientes. (Los componentes se explican en detalle en el Capítulo 13 - por ahora, piensa en ellos como módulos de contrato reutilizables).
Ten en cuenta que los structs utilizados como variantes de enum en los enums de eventos deben derivar starknet::Event porque ellos mismos se convierten en tipos de eventos cuando se usan en la estructura del enum.
Pruebas de Logs de Eventos
Inicia un nuevo proyecto Scarb con scarb new testinglog y selecciona ‘Starknet Foundry (default)’ como tu ejecutor de pruebas:

Para probar los logs de eventos, considera el siguiente contrato UserManager que permite a los usuarios registrarse y rastrea cuántos usuarios se han registrado.
El contrato usa un contador para asignar identificadores (IDs) únicos a los usuarios y almacena su información en un Map. Cuando un usuario se registra, el contrato emite un evento UserRegistered que las aplicaciones externas pueden consultar. Presta atención al atributo #[key] y a cómo se almacena el struct UserMetadata.
Copia y pega el código completo en tu archivo src/lib.cairo:
// Interface defining the functions our UserManager contract will implement
#[starknet::interface]
pub trait IUserManager<TContractState> {
fn register_user(ref self: TContractState, username: ByteArray);
fn get_user_count(self: @TContractState) -> u32;
}
// Struct to store user information (derives Store to enable storage in contract)
#[derive(Drop, Serde, starknet::Store)]
pub struct UserMetadata {
pub user_id: u32,
pub username: ByteArray
}
// Event emitted when a new user registers (user_id is marked as key for indexing)
#[derive(Drop, starknet::Event)]
pub struct UserRegistered {
#[key]
pub user_id: u32,
pub username: ByteArray,
pub timestamp: u64,
}
#[starknet::contract]
pub mod UserManager {
use super::{UserRegistered, UserMetadata, IUserManager};
use starknet::{
get_block_timestamp, ContractAddress, get_caller_address,
storage::{Map, StoragePathEntry, StoragePointerReadAccess, StoragePointerWriteAccess}
};
#[storage]
struct Storage {
user_counter: u32, // Tracks total number of registered users
users: Map<ContractAddress, UserMetadata> // Maps user addresses to their metadata
}
// Main event enum that holds all possible events this contract can emit
#[event]
#[derive(Drop, starknet::Event)]
pub enum Event {
UserRegistered: UserRegistered,
}
#[abi(embed_v0)]
impl UserManagerImpl of IUserManager<ContractState> {
fn register_user(ref self: ContractState, username: ByteArray) {
// Get current user count and increment for new user ID
let current_counter = self.user_counter.read();
let user_id = current_counter + 1;
// Create user metadata with new ID and provided username
let metadata = UserMetadata {
user_id,
username: username.clone()
};
// Update counter and store user data mapped to caller's address
self.user_counter.write(user_id);
self.users.entry(get_caller_address()).write(metadata);
// Emit event with user details and current timestamp
self.emit(UserRegistered {
user_id,
username,
timestamp: get_block_timestamp(),
});
}
fn get_user_count(self: @ContractState) -> u32 {
// Return the current number of registered users
self.user_counter.read()
}
}
}
El trait IUserManager define dos funciones: register_user para el registro y get_user_count para verificar el número total de usuarios registrados.
-
El struct
UserMetadataalmacena la información del usuario (ID y username) y puede guardarse en el almacenamiento del contrato. Derivastarknet::Storeporque se almacena en el almacenamiento del contrato dentro delMap<ContractAddress, UserMetadata>.Cualquier struct personalizado que necesite ser leído o escrito en el almacenamiento del contrato debe implementar el trait
Store, el cual#[derive(starknet::Store)]genera automáticamente. -
El struct del evento
UserRegisteredregistra los detalles del registro. El campouser_idestá marcado con#[key], haciéndolo indexado para un filtrado eficiente en las consultas.
Cuando se llama a register_user, el contrato:
- Incrementa el contador de usuarios para generar un nuevo ID de usuario
- Crea y almacena los metadatos del usuario
- Emite un evento
UserRegisteredcon el ID de usuario, nombre de usuario (username) y la marca de tiempo (timestamp) del bloque actual
Navega a la carpeta de tu proyecto cd testinglog y ejecuta scarb build para compilar tu proyecto:

Hay varias maneras de probar los eventos usando Starknet Foundry. Puedes realizar pruebas para asegurar si un evento fue emitido usando el método assert_emitted, o usar assert_not_emitted para probar la ausencia de emisión de un evento. También puedes probar manualmente inspeccionando los eventos directamente.
Para las pruebas de eventos manuales, es posible que desees filtrar eventos de un contrato específico en lugar de examinar todos los eventos emitidos. El método emitted_by en la estructura Events te permite reducir los eventos a aquellos provenientes de una dirección particular.
A continuación se discutirán tanto el método assert_emitted como el método manual para probar eventos.
Prueba 1: Usando el método incorporado assert_emitted
La prueba a continuación verifica que registrar un usuario emite el evento UserRegistered correcto con los datos esperados. Navega a tests/test_contract.cairo, copia y pega esta prueba en él:
use snforge_std::{declare, ContractClassTrait, DeclareResultTrait, spy_events, EventSpyTrait, IsEmitted, Event, EventSpyAssertionsTrait};
use testinglog::{IUserManagerDispatcher, UserManager, UserRegistered, IUserManagerDispatcherTrait};
use starknet::{ContractAddress, get_block_timestamp};
fn deploy_contract(name: ByteArray) -> ContractAddress {
let contract = declare(name).unwrap().contract_class();
let (contract_address, _) = contract.deploy(@ArrayTrait::new()).unwrap();
contract_address
}
#[test]
fn test_registration_event_emission() {
// Deploy the UserManager contract
let contract_address = deploy_contract("UserManager");
// Create a dispatcher to interact with the deployed contract
let dispatcher = IUserManagerDispatcher { contract_address };
// Start spying on events before the function call
let mut spy = spy_events();
// Register a user - this should emit a UserRegistered event
dispatcher.register_user("serah");
// Verify that the expected event was emitted with correct data
spy.assert_emitted(
@array![
(
contract_address, // Event should come from our contract
UserManager::Event::UserRegistered(
UserRegistered {
user_id: 1, // First user gets ID 1
username: "serah", // Username matches what we passed
timestamp: get_block_timestamp() // Timestamp should be current block time
}
)
)
]
);
}
Imports trae las herramientas de prueba requeridas de Starknet Foundry y las definiciones de nuestro contrato.
De snforge_std, importamos declare para cargar clases de contratos, junto con traits relacionados como ContractClassTrait y DeclareResultTrait para el despliegue de contratos.
use snforge_std::{declare, ContractClassTrait, DeclareResultTrait, spy_events, EventSpyTrait,IsEmitted, Event, EventSpyAssertionsTrait};
La funcionalidad de prueba de eventos proviene de spy_events que crea nuestro espía de eventos, EventSpyTrait para las interacciones del espía, y EventSpyAssertionsTrait que añade métodos de aserción como assert_emitted. También importamos los tipos IsEmitted y Event para las operaciones de manejo de eventos.
De nuestro módulo testinglog, importamos el IUserManagerDispatcher autogenerado y su trait para llamar a las funciones del contrato, el módulo del contrato UserManager que contiene nuestras definiciones de eventos, y el struct del evento específico UserRegistered que estamos probando.
use testinglog::{IUserManagerDispatcher, UserManager, UserRegistered, IUserManagerDispatcherTrait};
Del núcleo de starknet, importamos ContractAddress para manejar direcciones de contratos y get_block_timestamp para recuperar la marca de tiempo (timestamp) actual del bloque.
use starknet::{ContractAddress, get_block_timestamp};
test_registration_event_emission() utiliza el enfoque simplificado con spy.assert_emitted()
deploy_contract("UserManager")es una función auxiliar que declara y despliega el contratoUserManager, devolviendo su direcciónIUserManagerDispatcher { contract_address }crea un despachador (dispatcher) para interactuar con el contrato desplegadospy_events()inicializa el espionaje de eventos antes de que desencadenemos la acción
Después de llamar a register_user("serah") a través del despachador, spy.assert_emitted() comprueba para verificar que el evento esperado UserRegistered fue emitido con los datos correctos (user_id: 1, username: “serah”, y el timestamp actual). La aserción verifica tanto la dirección del contrato que emitió el evento como la estructura de datos del evento.
Ejecuta scarb test y deberías ver que la prueba pasa, confirmando que nuestra prueba de eventos funciona correctamente.
Prueba 2: Usando el método manual
test_event_structure() prueba para asegurar que la función register_user trabaja correctamente y emite el evento esperado UserRegistered.
#[test]
fn test_event_structure() {
// Deploy the UserManager contract
let contract_address = deploy_contract("UserManager");
// Create a dispatcher to interact with the deployed contract
let dispatcher = IUserManagerDispatcher { contract_address };
// Start event spy to capture all emitted events
let mut spy = spy_events();
// Register a user which should emit a UserRegistered event
dispatcher.register_user("serah");
// Retrieve all captured events for analysis
let events = spy.get_events();
assert(events.events.len() == 1, 'There should be one event');
// Create the expected event structure for comparison
let expected_event = UserManager::Event::UserRegistered(
UserRegistered {
user_id: 1,
username: "serah",
timestamp: get_block_timestamp()
}
);
// Check if the expected event was actually emitted
assert!(events.is_emitted(contract_address, @expected_event));
// Create array of expected events for exact comparison
let expected_events: Array<(ContractAddress, Event)> = array![
(contract_address, expected_event.into()),
];
assert!(events.events == expected_events);
// Extract and examine the raw event data
let (from, event) = events.events.at(0);
assert(from == @contract_address, 'Emitted from wrong address');
// Verify event keys structure (event selector + indexed fields)
assert(event.keys.len() == 2, 'There should be two keys');
assert(event.keys.at(0) == @selector!("UserRegistered"), 'Wrong event name');
}
Cuando llamamos a register_user(), este recupera todos los eventos capturados usando spy.get_events() y realiza comprobaciones:
- confirmar que el evento esperado fue emitido usando
events.is_emitted(), y - examinar la estructura bruta del evento, incluyendo la dirección del contrato, el recuento de claves (las claves deben contener exactamente 2 elementos: el selector de evento + el
user_idindexado), y el selector de evento.
register_user() emite un evento UserRegistered que contiene los datos del usuario.
Este método manual permite probar propiedades específicas de eventos que las aserciones automatizadas podrían no cubrir.
Pega esta segunda prueba en el mismo archivo tests/test_contract.cairo, de modo que el archivo contenga tanto la primera como la segunda prueba. Luego procede a probar el proyecto usando scarb test.
La salida de tu terminal debería mostrar que las pruebas pasan

Visualizando Datos en Bruto de los Eventos
Para ver los datos en bruto del evento, puedes inspeccionar este contrato UserManager desplegado en Voyager. Haz clic en la pestaña “Events” para ver los logs de los eventos emitidos por la llamada a register_user, como se muestra a continuación:

Como se mencionó anteriormente, un ByteArray serializado es un struct que consiste en [data, pending_word, pending_word_len], cada uno almacenado como un felt252. Es por eso que “serah” ocupó data[0-2] como se ve en la imagen de arriba.
data(data[0]): Array vacío[0x0]porque “serah” (5 bytes) no necesita ningún fragmento de 31 bytespending_word(data[1]):0x7365726168contiene los bytes reales del stringpending_word_len(data[2]):0x5(5 bytes en total)- data[3]:
0x68c6c625indica la marca de tiempo (timestamp) en hexadecimal.
Esta vista en bruto muestra exactamente cómo Cairo serializa los datos; todo se convierte en una secuencia de valores felt252 (mostrados aquí como hexadecimales) mientras hace posible la reconstrucción de la estructura de datos original.
Las siguientes secciones muestran tres enfoques básicos para la recuperación y el procesamiento de datos de eventos en Starknet.
Consulta y Monitoreo de Eventos On-chain y Off-chain
Entender la estructura de los eventos es solo una parte. En la práctica, es posible que desees una retroalimentación inmediata de la transacción, monitoreo en tiempo real o análisis histórico. Cada uno requiere un enfoque diferente.
Parseando Logs de Eventos
Considera este ejemplo mínimo en TypeScript que ilustra cómo parsear eventos de las transacciones de contratos inteligentes de Starknet cuando necesitas retroalimentación inmediata de tus propias transacciones.
- Clona este repositorio, y navega al directorio starknet-event-parsing:
git clone https://github.com/Sayrarh/starknet-event-parsing.git
cd starknet-event-parsing
- Si no tienes yarn instalado, instálalo primero usando
npm install -g yarn - Ejecuta
yarn installpara instalar las dependencias, luego instala dotenv usandoyarn add dotenv - Crea un archivo
.enven el directorio raíz:
ACCOUNT_ADDRESS=0x...
PK=0x...
ALCHEMY_API_KEY=your_alchemy_api_key_here
- Reemplaza con tu dirección de cuenta real, clave privada y clave API de Alchemy.
- Edita el script principal (
src/event.ts) para especificar la dirección del contrato ERC-20 para el cual deseas parsear los eventosTransfer, o cualquier otra dirección de contrato (aquí se usa el token STRK en Sepolia), y también la dirección del destinatario:
const contractAddress = "0x04718f5a0fc34cc1af16a1cdee98ffb20c31f5cd61d6ab07201858f4287c938d";
const recipientAddress = "0x0207d7324a20d6A080C7EF6237D289fD57F4fb11187A64f597d4099a720FE6C5";
Asegúrate de que tu cuenta esté activa on-chain y tenga tokens STRK para las tarifas de transacción.
- Ejecuta el script usando
yarn dev. El script hará lo siguiente:- Conectarse a tu cuenta de Starknet en Sepolia
- Ejecutar una transferencia de 1 token STRK a la dirección del destinatario especificada
- Esperar la confirmación de la transacción
- Extraer y mostrar todos los eventos emitidos durante esa transacción

Esto ayuda a parsear eventos Transfer de cualquier contrato de token ERC20 en Starknet.
También puedes personalizar el script para diferentes contratos y escenarios:
await eventLogic(
"0x... your contract address",
"your_function_name",
[arg1, arg2,...]
);
Escuchando Eventos
Esto es útil cuando necesitas monitoreo en tiempo real de la actividad del contrato. Reemplaza src/event.ts con el siguiente ejemplo de código que activa un callback cada vez que un token ERC-20 emite un evento Transfer:
// Import necessary Starknet.js components for RPC interaction
import { RpcProvider} from "starknet";
import * as dotenv from "dotenv";
dotenv.config();
async function listenToTransfers() {
const alchemyApiKey = process.env.ALCHEMY_API_KEY;
// initialize provider for Sepolia testnet with Alchemy
const provider = new RpcProvider({
nodeUrl: `https://starknet-sepolia.g.alchemy.com/starknet/version/rpc/v0_8/${alchemyApiKey}`,
});
// Contract address to monitor for events
const contractAddress = "0x04718f5a0fc34cc1af16a1cdee98ffb20c31f5cd61d6ab07201858f4287c938d";
// Track the last processed block to avoid re-processing events
let lastBlock = 0;
async function checkForEvents() {
// Get the current block number from the network
const currentBlock = await provider.getBlockNumber();
// Only check for new events if there are new blocks
if (currentBlock > lastBlock) {
// Query for Transfer events between the last processed block and current block
const events = await provider.getEvents({
address: contractAddress, // Only events from our target contract
keys: [["0x99cd8bde557814842a3121e8ddfd433a539b8c9f14bf31ebf108d12e6196e9"]], // Transfer event selector (keccak hash)
from_block: { block_number: lastBlock + 1 }, // Start from next unprocessed block
to_block: { block_number: currentBlock }, // Query up to current block
chunk_size: 100 // Process events in batches of 100
});
// Process each detected Transfer event
events.events.forEach(event => {
console.log("Transfer event detected!", event);
});
// Update last processed block to current block
lastBlock = currentBlock;
}
}
// Set up polling: check for new events every 10 seconds
setInterval(checkForEvents, 10000);
// Run initial check immediately
checkForEvents();
}
// Start the event listener
listenToTransfers();
Cuando ejecutes yarn dev, verás nuevas transacciones a intervalos en la salida de tu terminal hasta que presiones Ctrl+C.

Filtrando Eventos por Rango
Cuando necesites análisis y consultas de datos históricos, puedes usar provider.getEvents() de Starknet.js para consultar eventos históricos dentro de un rango de bloques específico.
Reemplaza src/event.ts de nuevo con el siguiente ejemplo de código que busca eventos Transfer en los bloques 8000 a 9000 (1000 bloques en total) desde el contrato especificado:
import { RpcProvider } from "starknet";
import * as dotenv from "dotenv";
dotenv.config();
async function filterTransferEvents() {
const alchemyApiKey = process.env.ALCHEMY_API_KEY;
// initialize provider for Sepolia testnet with Alchemy
const provider = new RpcProvider({
nodeUrl: `https://starknet-sepolia.g.alchemy.com/starknet/version/rpc/v0_8/${alchemyApiKey}`,
});
// Target contract address to query for Transfer events
const contractAddress = "0x04718f5a0fc34cc1af16a1cdee98ffb20c31f5cd61d6ab07201858f4287c938d";
const transferSelector = "0x99cd8bde557814842a3121e8ddfd433a539b8c9f14bf31ebf108d12e6196e9";
// Query for Transfer events within a specific block range
const events = await provider.getEvents({
address: contractAddress, // Only events from our target contract
keys: [[transferSelector]], // Filter for Transfer events only
from_block: { block_number: 8000 }, // Start searching from block 8000
to_block: { block_number: 9000 }, // Search up to block 9000 (1000 block range)
chunk_size: 100 // Process events in batches of 100
});
// Display total number of Transfer events found
console.log(`Found ${events.events.length} Transfer events`);
// Process and display details for each Transfer event
events.events.forEach((event, index) => {
console.log(`\n--- Transfer Event ${index + 1} ---`);
console.log("From:", event.keys[1]);
console.log("To:", event.keys[2]);
console.log("Amount (hex):", event.data[0]);
console.log("Amount (decimal):", parseInt(event.data[0], 16));
console.log("Block:", event.block_number);
console.log("Transaction:", event.transaction_hash);
});
}
// Execute the event filtering function
filterTransferEvents();
Ejecuta yarn dev y obtendrás los eventos filtrados dentro del rango de bloques especificado. El filtrado utiliza estos parámetros:
contractAddress: El contrato específico para consultar eventostransferSelector: El hash de la firma del evento que identifica a los eventosTransferkeys: Filtra los eventos por tipo; solo se devuelven los eventosTransferfrom_block/to_block: Define el rango de bloques dentro del cual buscarchunk_size: Controla la paginación para evitar respuestas abrumadoras
Los datos del evento se decodifican luego para extraer la información; dirección del remitente (keys[1]), dirección del destinatario (keys[2]) y cantidad de la transferencia (data[0]).
¿Los nombres de las variables en los eventos son opcionales como en Solidity?
Como es de esperar, los nombres de las variables NO son opcionales en los eventos de Cairo. Mientras que Solidity permite parámetros de eventos anónimos, Cairo requiere nombres de campos explícitos en las definiciones de structs de eventos para todos los parámetros.
¿Pueden los eventos heredarse a través de contratos padre e interfaces?
Cairo no soporta la herencia de eventos. En su lugar, para reutilizar eventos a través de contratos, utilizas componentes. Los componentes definen sus propios eventos, y cuando incluyes un componente en tu contrato, haces referencia al tipo de evento del componente en el enum #[event] de tu contrato utilizando el atributo #[flat]. Esto permite que múltiples contratos emitan los mismos eventos al usar el mismo componente, sin necesidad de redefinir los eventos en cada contrato.
Para un repaso de las diferencias clave entre los eventos en Solidity y Cairo, aquí tienes una tabla que muestra una comparación clara:
Eventos: Diferencias Clave Entre Cairo y Solidity
| Aspecto | Cairo | Solidity |
|---|---|---|
| Nombres de Variables | Requeridos para todos los parámetros | Opcionales (se permiten parámetros anónimos) |
| Parámetros Indexados | Atributo #[key] (hasta 50 parámetros indexados) |
Palabra clave indexed (máximo 3, o 4 para eventos anónimos) |
| Parámetros Totales | Sin límite estricto (restricciones prácticas) | 17 argumentos en total (los arrays cuentan como 2) |
| Herencia | Sin herencia - uso de integración de componentes | Herencia completa soportada |
| Declaración de Eventos | Struct #[derive(starknet::Event)] |
event EventName(...) |
| Emisión de Eventos | self.emit(Event::EventName { ... }) |
emit EventName(...) |
| Eventos Anidados | Atributo #[flat] para aplanar |
No soportados |
Conclusión
Los eventos de Cairo requieren una estructura más explícita en comparación con Solidity y exigen definiciones de tipos y patrones de composición estrictos. En Cairo, los eventos dependen de tres traits que trabajan en conjunto:
Serdemaneja la serialización de campos complejos en valoresfelt252.Eventprepara los arrays keys y data para el recibo.EventEmitteremite el evento estructurado.
Los structs con tipos anidados o no primitivos deben derivar Serde para compilar. Los campos indexados marcados con #[key] se almacenan por separado para filtrado; usa #[key] en tipos primitivos como u32, felt252 o ContractAddress para consultas efectivas, ya que los tipos complejos se hashean y se vuelven ilegibles. El atributo #[flat] se aplica a enums de eventos anidados para aplanar la jerarquía de nombres, permitiendo selectores de eventos distintos para una mejor granularidad en las consultas.
Este artículo es parte de una serie de tutoriales sobre Programación de Cairo en Starknet