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.

Register Multiple Triggers

addTriggers (plural) lets you register several handler functions for the same contract, ABI, or global target in one call. It accepts an array of trigger functions.
addTriggers lives in the same Triggers contract as addTrigger. It is purely a convenience helper and behaviour is identical. If you are new to triggers, start with the Listener Basics guide where addTrigger is introduced.
listeners/src/Main.sol
import "./MyPoolListener.sol";

contract Triggers is BaseTriggers {
    function triggers() external override {
        MyPoolListener listener = new MyPoolListener();

        // Collect every handler we care about for this pool
        Trigger[] memory poolTriggers = [
            listener.UniswapV3Pool$triggerOnSwapEvent(),
            listener.UniswapV3Pool$triggerOnMintEvent(),
            listener.UniswapV3Pool$triggerOnBurnEvent()
        ];

        // Register all three triggers for the same contract in one call
        addTriggers(
            chainContract(Chains.Ethereum, 0x1F98431c8aD98523631AE4a59f267346ea31F984),
            poolTriggers
        );
    }
}
Use addTriggers when your listener exposes multiple handlers that share a target.

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 on the event definition of your listener contract, giving you fine-grained control of your database’s performance.

Learn More About PostgreSQL Database Indexes

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

Index Definition Syntax

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>, ...);
The syntax components:
  • <index_name>: A unique name for your index.
  • <INDEX_TYPE>: The type of index to create (e.g., BTREE, HASH).
  • (<columns>): A comma-separated list of one or more columns to include in the index. These names must exactly match the field names in your event or struct definition, including case.

Multiple Indexes Per Event

You can define multiple indexes for a single event by adding multiple @custom:index lines. This is useful when you want to query the same table in different ways.
struct LiquidityEventData {
    bytes32 txnHash;
    uint64 blockNumber;
    uint64 blockTimestamp;
    address pool;
    address owner;
    int24 tickLower;
    int24 tickUpper;
    uint128 liquidity;
    uint256 amount0;
    uint256 amount1;
}

/// @custom:index lp_events_by_pool BTREE (pool, blockNumber);
/// @custom:index lp_events_by_owner BRIN (owner, blockTimestamp);
/// @custom:index lp_events_by_tick_range HASH (pool, tickLower, tickUpper);
event LiquidityEvent(LiquidityEventData);
When events use unnamed structs to solve Solidity’s stack errors, Sim IDX automatically flattens the struct fields into individual database columns. Your index annotations reference these struct field names.

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.

Learn More About PostgreSQL Index Types

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

Syntax 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

Block Ranges

You can specify custom block ranges for your triggers to target specific blocks or time periods. This is particularly useful for historical data analysis, testing specific time periods, or limiting triggers to certain blockchain events. Chain helper functions support .withStartBlock(), .withEndBlock(), and .withBlockRange() methods:
// Listen to events starting from a specific block onwards
addTrigger(
    chainContract(Chains.Ethereum.withStartBlock(10000000), 0x1F98431c8aD98523631AE4a59f267346ea31F984), 
    listener.triggerOnPoolCreatedEvent()
);

// Listen to events within a specific block range (inclusive)
addTrigger(
    chainContract(Chains.Base.withStartBlock(5000000).withEndBlock(5000100), 0x1F98431c8aD98523631AE4a59f267346ea31F984), 
    listener.triggerOnPoolCreatedEvent()
);

// Block range support works with ABI triggers as well
addTrigger(
    chainAbi(Chains.Ethereum.withStartBlock(18000000), UniswapV3Pool$Abi()), 
    listener.triggerOnBurnEvent()
);

// And with global triggers
addTrigger(
    chainGlobal(Chains.Ethereum.withBlockRange(19000000, 19001000)), 
    listener.triggerOnBlock()
);

Arbitrum One Pre-Nitro Limitation

Sim IDX does not support blocks on Arbitrum One created before the Nitro upgrade. Any triggers configured with a start block earlier than the Nitro boundary (block 22,207,818, which occurred on 2022-08-31) will not be able to process pre-Nitro blocks.
When setting a block range for Arbitrum One, ensure your start block is 22207818 or greater:
// Correct: Start indexing after the Nitro upgrade
addTrigger(
    chainContract(Chains.Arbitrum.withStartBlock(22207818), 0x...), 
    listener.triggerOnPoolCreatedEvent()
);