Listener Patterns
After learning about listeners in the Listener Basics guide, you can use more advanced patterns to build sophisticated indexers. This page explores core Sim IDX concepts that give you more flexibility in how you trigger listeners and structure your onchain data.
We will cover advanced triggering, handling name conflicts, calling other contracts via interfaces, and solving stack too deep errors.
Trigger on an ABI
The chainAbi
helper allows you to trigger your listener on any contract that matches a specific ABI signature. This is incredibly powerful for monitoring activity across all instances of a particular standard, like ERC-721 or Uniswap V3 pools, without needing to list every contract address explicitly.
The example below shows how to trigger the onBurnEvent
handler for any contract on Ethereum that matches the UniswapV3Pool
ABI. The UniswapV3Pool$Abi()
is a helper struct that is automatically generated from that ABI file.
Trigger Globally
The chainGlobal
helper creates triggers that are not tied to any specific contract or ABI. This can be used to set up block-level handlers with onBlock
for tasks that need to run once per block, such as creating periodic data snapshots, calculating time-weighted averages, or performing end-of-block settlements.
The framework provides a built-in abstract contract, Raw$OnBlock
, for this purpose. First, add it to your listener’s inheritance list, then implement the onBlock
handler and register the trigger.
The framework also provides abstract contracts for Raw$OnCall
and Raw$OnLog
, allowing you to create global hooks for every function call or every event log on a chain.
Using Interfaces
Often, your handler is triggered by one contract, but you need to fetch additional data from another contract to enrich your event. For example, a Swap
event on a pool tells you a swap occurred, but you need to call the pool contract directly to get its current slot0
state. Solidity interfaces allow your listener to do this.
1. Define the Interface
It’s best practice to create an interfaces
directory (e.g., listeners/src/interfaces/
) and define the interface in a new .sol
file.
2. Import and Use the Interface
In your listener, import the interface. You can then cast a contract’s address to the interface type to call its functions.
Handle Name Conflicts
When working with multiple ABIs, you may encounter functions or events with the same name, which can cause compilation errors. Sim IDX provides two solutions.
1. Multiple Listeners
The recommended approach is to split your logic into separate, dedicated listener contracts for each ABI. This keeps your code clean and modular.
2. Prefixed Naming for Shared State
If you need to share state between handlers for conflicting functions within a single contract, you can configure sim.toml
to prefix the generated names.
Set codegen_naming_convention = "abi_prefix"
in your sim.toml
file.
This changes the generated function names, allowing you to implement them both in the same contract:
To learn more about the codegen_naming_convention
property and other sim.toml
configuration options, visit the App Structure page.
Emit Large Events
You may encounter a Stack too deep
compilation error if your event contains more than 16 parameters, or if your handler function declares too many local variables. This is due to a fundamental limit in the Solidity EVM.
The solution is to use a pattern called Struct Flattening. You group your event parameters into a struct
and then define your event to take this struct
as a single, unnamed parameter. Sim IDX recognizes this specific pattern and will automatically “flatten” the struct’s members into individual columns in your database. This gives you the best of both worlds: code that compiles and a clean, relational database schema.
Define a Struct
Create a struct containing all the fields you want in your database table.
Update Event Definition
Change your event to accept the struct as a single, unnamed parameter. This is the crucial step that enables struct flattening.
Populate and Emit the Struct
In your handler, create an instance of the struct, populate its fields, and emit it.
By following this pattern, you can define events with any number of parameters while keeping your code compliant with the EVM’s limitations.