Events
A contract may emit events throughout its execution. Each event contains the following fields:
- from_address: address of the contract emitting the events
- keys: a list of field elements
- data: a list of field elements
The keys can be used for indexing the events, while the data may contain any information that we wish to log.
The events are recorded in the blocks of the blockchain.
Example of Cairo code for an event:
#[derive(Drop, starknet::Event)]
struct EventPanic {
#[key]
errorType: u8,
errorDescription: felt252,
}
Here we have an event called EventPanic
, with an u8 stored in keys, and a felt252 (text) in data.
Why events ?​
Events are a useful tool for logging and notifying external entities about specific occurrences within a contract, with a timestamp (the block number). They emit data that can be accessed by everybody.
Some cases:
- When a specific value is reached in a contract, an event can be created to store the fact that this value has been reached at a specific block number.
- When the L1 network has triggered the execution of a L2 contract, you can store in the event some results and when it occurs.
An event can be useful also when you invoke a contract. When you invoke a Cairo function (meaning to write in the network), the API does not authorize any response (only call functions can provide an answer). To generate an event in the code is a way to provide a response (for example for the creation of an account, an event is generated to return the account address).
With the Transaction hash​
If you use Starknet.js to invoke a Cairo function that will trigger a new event, you will receive the transaction hash as a response. Preserve it so it can be used to easily retrieve the event data.
Example of invocation:
const transactionHash = myContract.invoke('emitEventPanic', [8, 'Mega Panic.']);
Then get the transaction receipt:
const txReceipt = await provider.waitForTransaction(transactionHash);
Raw response​
You can recover all the events related to this transaction hash:
if (txReceipt.isSuccess()) {
const listEvents = txReceipt.events;
}
The result is an array of events (here only one event):
[
{
from_address: '0x47cb13bf174043adde61f7bea49ab2d9ebc575b0431f85bcbfa113a6f93fc4',
keys: ['0x3ba972537cb2f8e811809bba7623a2119f4f1133ac9e955a53d5a605af72bf2', '0x8'],
data: ['0x4d6567612050616e69632e'],
},
];
The first parameter in the keys
array is a hash of the name of the event, calculated this way:
const nameHash = num.toHex(hash.starknetKeccak('EventPanic'));
In some cases (when an event is coded in a Cairo component, without the #[flat]
flag), this hash is handled in several numbers.
The second parameter is the errorType
variable content (stored in keys array because of the #[key]
flag in the Cairo code).
The data
array contains the errorDescription
variable content ('0x4d6567612050616e69632e'
corresponds to the encoded value of "Mega Panic.")
You can decode it with:
const ErrorMessage = shortString.decodeShortString('0x4d6567612050616e69632e');
Parsed response​
Once you have the transaction receipt, you can parse the events to have something easier to process.
We will perform parsing this way:
const events = myTestContract.parseEvents(txReceipt);
The result is an array of parsed events (here only one event):
events = [
{
EventPanic: { errorType: 8n, errorDescription: 93566154138418073030976302n },
},
];
Easier to read and process, isn't it?
Without transaction hash​
If you don't have the transaction Hash of the contract execution that created the event, it will be necessary to search inside the blocks of the Starknet blockchain.
In this example, if you want to read the events recorded in the last 10 blocks, you need to use a method available from an RPC node. The class RpcProvider
is available for this case:
import { RpcProvider } from 'starknet';
const provider = new RpcProvider({ nodeUrl: `${myNodeUrl}` });
const lastBlock = await provider.getBlock('latest');
const keyFilter = [[num.toHex(hash.starknetKeccak('EventPanic')), '0x8']];
const eventsList = await provider.getEvents({
address: myContractAddress,
from_block: { block_number: lastBlock.block_number - 9 },
to_block: { block_number: lastBlock.block_number },
keys: keyFilter,
chunk_size: 10,
});
address, from_block, to_block, keys
are all optional parameters.
If you don't want to filter by key, you can either remove the keys
parameter, or affect it this way: [[]]
.
An event can be nested in a Cairo component (See the Cairo code of the contract to verify). In this case, the array of keys will start with additional hashes, and you will have to adapt your code in consequence ; in this example, we have to skip one hash :
const keyFilter = [[], [num.toHex(hash.starknetKeccak('EventPanic'))]];
Here we have only one event. You can easily read this event:
const event = eventsList.events[0];
console.log('data length =', event.data.length, 'key length =', event.keys.length, ':');
console.log('\nkeys =', event.keys, 'data =', event.data);
To limit the workload of the node, the parameter chunk_size
defines a size of chunk to read. If the request needs an additional chunk, the response includes a key continuation_token
containing a string to use in the next request.
Hereunder a code to read all the chunks of a request:
const keyFilter = [num.toHex(hash.starknetKeccak('EventPanic')), '0x8'];
let block = await provider.getBlock('latest');
console.log('bloc #', block.block_number);
let continuationToken: string | undefined = '0';
let chunkNum: number = 1;
while (continuationToken) {
const eventsRes = await providerRPC.getEvents({
from_block: {
block_number: block.block_number - 30,
},
to_block: {
block_number: block.block_number,
},
address: myContractAddress,
keys: [keyFilter],
chunk_size: 5,
continuation_token: continuationToken === '0' ? undefined : continuationToken,
});
const nbEvents = eventsRes.events.length;
continuationToken = eventsRes.continuation_token;
console.log('chunk nb =', chunkNum, '.', nbEvents, 'events recovered.');
console.log('continuation_token =', continuationToken);
for (let i = 0; i < nbEvents; i++) {
const event = eventsRes.events[i];
console.log(
'event #',
i,
'data length =',
event.data.length,
'key length =',
event.keys.length,
':'
);
console.log('\nkeys =', event.keys, 'data =', event.data);
}
chunkNum++;
}
If you want to parse an array of events of the same contract (abi of the contract available) :
const abiEvents = events.getAbiEvents(abi);
const abiStructs = CallData.getAbiStruct(abi);
const abiEnums = CallData.getAbiEnum(abi);
const parsed = events.parseEvents(eventsRes.events, abiEvents, abiStructs, abiEnums);
console.log('parsed events=', parsed);