Event Handlers
Event handlers are your main tool. Event handlers will get triggered when the indexer encounters the specified in the manifest file (link to where we talk about specifying events) events. They are by far the fastest way to collect the needed data.
From within the handler you will have access to data such as, contract address, transaction hash, block data, log topics, log data, receipts, etc.
For example, lets assume we are listening to the ERC721 Transfer event.
This is our handler definition in the subgraph.yaml file
...
eventHandlers:
- event: Transfer(indexed address,indexed address,indexed uint256)
handler: handleTransfer
...
This is how you can access different information within the mappings
...
export function handleTransfer(event: Transfer): void {
// Contract address
event.address;
// Event params
event.params.from;
event.params.to;
event.params.tokenId;
// Block data
event.block.number;
event.block.hash;
// Transaction data
event.transaction.hash;
event.transaction.index;
event.transaction.from;
event.transaction.to;
}
...
Note: Sometimes events include indexed params. Indexed params are used for filtering events, and each event can have up to 3 indexed params (except anonymous events, which can have 4), which become event topics and are excluded from the event data
. Event topic length is 32 bytes, so values that are less than 32 bytes will get padded with zeros. In the case with indexed
parameters that have complex or dynamic types, such as String, Array, Tuple or Struct, those parameters will be keccak256
hashed to a hash with length of 32 bytes. In this case event.params
will return the keccak256
hash, which cannot be decoded back to the original value and you’ll have to get that value using other means. Keep this in mind when writing your own contracts and events, and choose your indexed parameters wisely.
Starting from specVersion
0.0.5
and apiVersion
0.0.7
, event handlers can have access to the receipt for the transaction which emitted them, this allows you to have access additional transaction information or even other events emitted in the same transaction. You’ll first need to enable the receipts in the subgraph manifest by adding receipt: true
to your handler declaration. It’s an optional key and defaults to false
.
eventHandlers:
- event: MyEvent(address, uint256)
handler: handleMyEvent
receipt: true
Then in your handler you can access the receipt via the Event receipt
field.
export function handleMyEvent(event: MyEvent) {
...
let receipt = event.receipt;
if (receipt) {
// do something with the receipt
}
}
If the receipt
key is omitted or set to false in the manifest, null
value will be returned instead.
Note: Because the receipt
filed type is TransactionReceipt | null
we always need to perform a nullability check, otherwise the compiler will throw an error.
All available fields can be found here.
Example for using receipts:
You may notice that in our eventHandler
definition, the keyword indexed
is present in the event signature [The code has been generated by the graph-cli] As you may know every Event has up to 4 topics. The first topic or topic0
is the keccak256
hash of the event signature, but the indexed
keyword is ignored when the signature is hashed. The rest of the topics are populated with the parameters marked as indexed
. For example the ERC20
and ERC721
Transfer
events have the same topic0
, both have the following signature Transfer(address,address,uint256)
The difference between the two is that ERC20 has 2 indexed
events and ERC721 has three. So for example if you want to track any Transfer
event and want to know if it’s an ERC20 or ERC721, you can do the following:
-
Activate receipts by adding
receipt: true
in the handler definition, like so:eventHandlers: - event: Transfer(indexed address,indexed address,indexed uint256) handler: handleTransfer receipt: true
-
In the handler do the following
...
export function handleTransfer(event: Transfer): void {
let topic0 = Bytes::fromHexString("0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef")
...
let receipt = event.receipt;
if (receipt) {
let logs = receipt.logs;
for (let i = 0, k = logs.length; i < k; ++i) {
if (logs[i].topics[0] = topic0) {
if (logs[i].topics.length == 3) {
// Is ERC20
...
} else if(logs[i].topics.length == 4) {
// Is ERC721
...
}
}
}
}
...
Anonymous events
Anonymous events are declared using the anonymous
keyword. Those events don’t have a selector and topic0 is not the keccak256
hash of the event signature. For that reason anonymous events can have up to 4 indexed parameters, contrary to the non-anonymous, which can have 3. To track anonymous events you will need to provide the topic0
, which for anonymous events will be the the first indexed param.
eventHandlers:
- event: LogNote(bytes4,address,bytes32,bytes32,uint256,bytes)
topic0: '0x644843f351d3fba4abcd60109eaff9f54bac8fb8ccf0bab941009c21df21cf31'
handler: handleGive
More info on Solidity events can be found here
Call Handlers
Sometimes it is not possible to collect all needed information with event handlers, because either the contract does not emit any, in order to optimise cost, or not all information the subgraph developer wants to index is emitted via events. A subgraph can subscribe to calls made to the defined datasource addresses.
One important note about call handlers is that they depend on the Parity
trace
API, which most newer EVM compatible chains do not support. Be sure to check if the EVM chain you want to index supports it.
Full information on Call Handler can be found in TheGraph docs
Block Handlers
Along with event and call handlers, subgraph can subscribe to new blocks. This means that when a new block is appended to the chain, the subgraph will run a function. But running a function on every block is expensive and time consuming, which will make your subgraph sync times very, very slow. Yet, sometimes, we need to do something every n
blocks or on specific occasion. TheGraph provides several filters
, that can be helpful in some use cases. It is really discouraged to use Block Handlers without filters
.
We won’t go in details about Block Handlers here, you can check TheGraph docs to get basic understanding, and we will focus mostly on couple of use cases, where block handlers can be very helpful.
Call
filter:
blockHandlers:
- handler: handleBlockWithCallToContract
filter:
kind: call
This block handler will trigger on blocks that contain calls to the dataSource contract address. This filter can be useful for contracts that are rarely called, but we need to do something when that happens. Otherwise it will behave just like a regular block handler and it will make your subgraph slow to sync.
Note: Like the Call Handlers, call filters rely on the Parity trace
API. Be sure to check if your network supports it, before using the call
filter.
Polling
filter
A block handler using the polling
filter will trigger every n
blocks.
blockHandlers:
- handler: handleBlock
filter:
kind: polling
every: 10
This type of block handlers are useful in several caaes, for example:
- To do some calculations every
n
blocks - Follow the change of price of a token
- Check accumulated rewards
Once
filter
A block handler using the once
filter (also called initialization handler
) will trigger only once when the first block is indexed. Keep in mind that this is still a block handler, and as we mentioned in the manifest section, block handlers are triggered last, after the event and call handlers. To get around that, set the startBlock
of the dataSource one block before the contract deployment block.
This type of block handlers are could be used to:
- Setup initial entities
- Grab on-chain data before indexing, such as configurations or list of allowed addresses
- Dynamically initialise dataSources from templates for specific addresses
For more info Initialization and Polling handlers, you can watch this video by Kent from GraphBuildersDAO