After learning about listeners in the Listener Basics guide, you can use more advanced features 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, calling other contracts via interfaces, and adding indexes to your generated database.

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.
ABI Matching is Permissive: The matching behavior is permissive - if a contract implements the functions and events in the specified ABI, it counts as a match even if it also implements other functionality. This means contracts don’t need to match the ABI exactly; they just need to include the required functions and events.
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.
Main.sol
import "./UniswapPoolListener.sol";

contract Triggers is BaseTriggers {
    function triggers() external virtual override {
        UniswapPoolListener listener = new UniswapPoolListener();
        // Trigger on any contract on Ethereum matching the UniswapV3Pool ABI
        addTrigger(chainAbi(Chains.Ethereum, UniswapV3Pool$Abi()), listener.triggerOnBurnEvent());
    }
}
UniswapPoolListener.sol
contract UniswapPoolListener is UniswapV3Pool$OnBurnEvent {
    event PoolBurn(address indexed poolAddress, address owner, int24 tickLower, int24 tickUpper, uint128 amount);

    function onBurnEvent(EventContext memory ctx, UniswapV3Pool$BurnEventParams memory inputs) external override {
        // Only emit an event if the burn amount is greater than zero
        if (inputs.amount > 0) {
            emit PoolBurn(
                ctx.txn.call.callee, // The address of the pool that emitted the event
                inputs.owner,
                inputs.tickLower,
                inputs.tickUpper,
                inputs.amount
            );
        }
    }
}

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, implement the onBlock handler and register the trigger in the Triggers contract. Next, add Raw$OnBlock to your listener’s inheritance list.
Main.sol
import "./MyBlockListener.sol";

contract Triggers is BaseTriggers {
    function triggers() external virtual override {
        MyBlockListener listener = new MyBlockListener();
        addTrigger(chainGlobal(Chains.Ethereum), listener.triggerOnBlock());
    }
}
MyBlockListener.sol
contract MyBlockListener is Raw$OnBlock {
    event BlockProcessed(uint256 blockNumber, uint256 timestamp);

    function onBlock(RawBlockContext memory /*ctx*/) external override {
        emit BlockProcessed(block.number, block.timestamp);
    }
}
The framework also provides abstract contracts for Raw$OnCall and Raw$OnLog, allowing you to create global triggers for every function call or every event log on a chain.

Use 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.
listeners/src/interfaces/IUniswapV3Pool.sol
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.13;

interface IUniswapV3Pool {
    function slot0()
        external
        view
        returns (
            uint160 sqrtPriceX96,
            int24 tick,
            uint16 observationIndex,
            uint16 observationCardinality,
            uint16 observationCardinalityNext,
            uint8 feeProtocol,
            bool unlocked
        );
    // ... other functions
}

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.
import {IUniswapV3Pool} from "./interfaces/IUniswapV3Pool.sol";

contract Listener is UniswapV3Pool$OnSwapEvent {
    // ...
    function onSwapEvent(EventContext memory ctx, ...) external override {
        // Cast the address of the contract that triggered the event
        // to the IUniswapV3Pool interface to call its functions.
        (uint160 sqrtPriceX96, , , , , , ) = IUniswapV3Pool(ctx.txn.call.callee).slot0();
    }
}
For guidance on resolving compilation issues such as name conflicts or Stack too deep errors, refer to the Listener Errors guide.

DB Indexes

Database indexes are a common way to improve database performance. Sim IDX lets you define indexes directly in the event definition of your listener contract. This gives you fine-grained control over the database performance of your app.

Learn More About Database Indexes

To learn more about how database indexes, visit the PostgreSQL documentation.

How to Add Indexes

To add a database index, use a special comment with the @custom:index annotation directly above the event definition in your Solidity listener.
/// @custom:index <index_name> <INDEX_TYPE> (<column1>, <column2>, ...);
Let’s take a closer look at what each part of this syntax means:
  • <index_name>: A unique name for your index.
  • <INDEX_TYPE>: The type of index to create (e.g., BTREE).
  • (<columns>): A comma-separated list of columns to include in the index. These names must exactly match the parameter names in your event definition, including case.
Here is an example of a multi-column index on the pool, block_number, and to_address columns.
/// @custom:index po_idx1 BTREE (pool, block_number, to_address);
event PositionOwnerChanges(
    bytes32 txn_hash,
    uint256 block_number,
    uint256 block_timestamp,
    address from_address,
    address to_address,
    uint256 token_id,
    address pool
);
You can define multiple indexes for a single event by adding multiple @custom:index lines. This is useful when your API queries the same table in different ways.
/// @custom:index lp_events_by_pool BTREE (pool, block_number);
/// @custom:index lp_events_by_owner BTREE (owner);
event LpEvents(
    bytes32 txn_hash,
    // ...
    address pool,
    address owner,
    // ...
);

Supported Index Types

Sim IDX supports several PostgreSQL index types. BTREE is the default and most common type, suitable for a wide range of queries.
TypeUse Case
BTREEThe default and most versatile index type. Good for equality and range queries on sortable data (=, >, <, BETWEEN, IN).
HASHUseful only for simple equality comparisons (=).
BRINBest for very large tables where columns have a natural correlation with their physical storage order (e.g., timestamps).
GINAn inverted index useful for composite types like array or jsonb. It can efficiently check for the presence of specific values within the composite type.
While PostgreSQL supports GIST and SP-GIST index types, they are not practically usable in Sim IDX as they require data types (like geometric types) that are not generated from Solidity events.

Learn More About Index Types

To learn more about index types and their specific use cases, visit the PostgreSQL documentation.

Validation

The sim build command automatically validates your index definitions. If it detects an error in the syntax, it will fail the build and provide a descriptive error message. For example, if you misspell a column name: Error: Cannot find column(s): 'block_numbr' in event PositionOwnerChanges