The core of a Sim IDX app is the listener, a Solidity contract that defines what onchain data to index. By writing simple handlers for specific contract function calls or events, you instruct the Sim IDX framework on which data to capture and store in your database.

This guide covers the structure of a listener contract, how to add indexing for new functions, and how to test your logic.

Understand the Listener Contract

A listener is a special contract Sim IDX executes onchain. It has handler functions which are called when certain conditions are triggered onchain (e.g., when another contract calls a function, or a contract with a matching ABI emits an event). The Listener contract itself emits events which Sim IDX stores in your app’s database.

Mental Model

  1. A transaction is executed onchain.
  2. Sim IDX checks whether it matches any trigger you defined during deployment.
  3. When there’s a match, Sim IDX invokes the corresponding handler in your listener contract.
  4. The handler emits one or more events that capture the facts you care about.
  5. Sim IDX stores each event as a new row in the appropriate table of your app database.

File Anatomy

Before diving into listener development, make sure you understand the overall app folder structure and how the listeners/ folder fits into your Sim IDX app.

The indexing logic is primarily located in listeners/src/.

ContractPurposeLocation
TriggersRegisters all triggers via addTrigger. Contains no business logic.Must be in Main.sol.
Listener(s)One or more contracts that implement handler logic and emit events. They can have any name and be defined in any .sol file within src/.listeners/src/

Let’s break the Main.sol file from the sample app down step-by-step.

Imports

import "sim-idx-sol/Simidx.sol";
import "sim-idx-generated/Generated.sol";

These two imports pull in everything provided by the Sim IDX framework. Simidx.sol provides core helpers, while Generated.sol contains the Solidity code created from your ABIs.

Triggers Contract

This contract tells Sim IDX when to run your code using a trigger, which specifies a target contract and the handler to call.

You can also use helpers like chainAbi and chainGlobal. For other trigger types, see the Listener Patterns page.

contract Triggers is BaseTriggers {
    function triggers() external virtual override {
        Listener listener = new Listener();
        addTrigger(chainContract(Chains.Ethereum, 0x1F98431c8aD98523631AE4a59f267346ea31F984), listener.triggerOnCreatePoolFunction());
        addTrigger(chainContract(Chains.Unichain, 0x1F98400000000000000000000000000000000003), listener.triggerOnCreatePoolFunction());
        addTrigger(chainContract(Chains.Base, 0x33128a8fC17869897dcE68Ed026d694621f6FDfD), listener.triggerOnCreatePoolFunction());
    }
}
  • BaseTriggers: An abstract contract from Simidx.sol that provides the addTrigger helper.
  • triggers(): The required function where you register all your triggers.
  • chainContract(...): This helper function uses the Chains enum for readability. The sample app registers the same trigger for Ethereum, Base, and Unichain, demonstrating how to monitor a contract across multiple networks.

Listener Contract

This is where you implement your business logic. The sample app uses the name Listener.

contract Listener is UniswapV3Factory$OnCreatePoolFunction {
    event PoolCreated(uint64 chainId, address caller, address pool, address token0, address token1, uint24 fee);

    function onCreatePoolFunction(
        FunctionContext memory ctx,
        UniswapV3Factory$CreatePoolFunctionInputs memory inputs,
        UniswapV3Factory$CreatePoolFunctionOutputs memory outputs
    )
        external
        override
    {
        emit PoolCreated(uint64(block.chainid), ctx.txn.call.callee, outputs.pool, inputs.tokenA, inputs.tokenB, inputs.fee);
    }
}
  • Inheritance: The listener extends an abstract contract (UniswapV3Factory$OnCreatePoolFunction) that is automatically generated from your ABI. This provides the required handler function signature and typed structs for inputs and outputs.
  • Events: Emitting an event like PoolCreated defines the shape of your database.

While the sample app uses the generic name Listener, you can and should use more descriptive names for your contracts (e.g., UniswapV3Listener). For larger projects, you can even split logic into multiple listener contracts, each in its own .sol file within the src/ directory.

Define and Emit Events

Events are the bridge between your listener’s logic and your database. When your listener emits an event, Sim IDX creates a database record.

From Events to DB

The framework automatically maps your event to a database view. The event name is converted to snake_case to become the view name, and each event parameter becomes a column.

For example, the PoolCreated event from the sample app results in a queryable pool_created view:

chain_idcallerpooltoken0token1fee
10x1f98431c8ad98523631ae4a59f267346ea31f9840xf2c1e03841e06127db207fda0c3819ed9f7889030x4a074a606ccc467c513933fa0b48cf37033cac1f0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc210000

Extending an Event

To capture more data, you simply add parameters to your event definition and update the emit statement in your handler. Let’s modify the sample app to also record the block number.

1. Extend the Event Definition

Add the new blockNumber parameter to your PoolCreated event in Main.sol.

event PoolCreated(
    uint64   chainId,
    address  caller,
    address  pool,
    address  token0,
    address  token1,
    uint24   fee,
    uint256  blockNumber // new field
);

2. Emit the New Data

Pass the new value when you emit the event in your onCreatePoolFunction handler.

function onCreatePoolFunction(...) external override {
    emit PoolCreated(
        uint64(block.chainid),
        ctx.txn.call.callee,
        outputs.pool,
        inputs.tokenA,
        inputs.tokenB,
        inputs.fee,
        block.number // pass the new value
    );
}

After deploying these changes, your pool_created table will automatically include the new block_number column.

Trigger Onchain Activity

Sim IDX can trigger on contract events as well as function calls, both before and after they execute. This allows you to capture a wide range of onchain activity.

To add a new trigger to your listener, you’ll follow a simple, five-step process:

  1. Discover the Trigger: Find the abstract contract for your target function or event in the generated files.
  2. Extend the Listener: Add the abstract contract to your listener’s inheritance list.
  3. Define a New Event: Create a Solidity event to define your database schema.
  4. Implement the Handler: Write the function required by the abstract contract to process the data and emit your event.
  5. Register the Trigger: Call addTrigger in your Triggers contract to activate the trigger.

Let’s walk through an example of adding a new event trigger to the sample app’s Listener contract. We will extend the Listener to also index the OwnerChanged event from the Uniswap V3 Factory.

1. Discover the Trigger

Look inside listeners/lib/sim-idx-generated/UniswapV3Factory.sol. You will find an abstract contract for the OwnerChanged event.

abstract contract UniswapV3Factory$OnOwnerChangedEvent {
    function onOwnerChangedEvent(EventContext memory ctx, UniswapV3Factory$OwnerChangedEventParams memory inputs) virtual external;
    function triggerOnOwnerChangedEvent() view external returns (Trigger memory);
}

2. Extend the Listener

Add UniswapV3Factory$OnOwnerChangedEvent to the inheritance list of the Listener contract in Main.sol.

contract Listener is
    UniswapV3Factory$OnCreatePoolFunction, // existing
    UniswapV3Factory$OnOwnerChangedEvent   // new
{
    // ... existing events and handlers
}

3. Define a New Event

Inside the Listener contract, add a new event to define the schema for the owner_changed table.

event OwnerChanged(
    uint64  chainId,
    address oldOwner,
    address newOwner
);

4. Implement the Handler

Implement the onOwnerChangedEvent function required by the abstract contract, also inside Listener.

function onOwnerChangedEvent(
    EventContext memory /*ctx*/,
    UniswapV3Factory$OwnerChangedEventParams memory inputs
) external override {
    emit OwnerChanged(
        uint64(block.chainid),
        inputs.oldOwner,
        inputs.newOwner
    );
}

5. Register the Trigger

Finally, add a new trigger for this handler in your Triggers contract.

// In Triggers.triggers()
addTrigger(
    chainContract(Chains.Ethereum, 0x1F98431c8aD98523631AE4a59f267346ea31F984),
    listener.triggerOnOwnerChangedEvent() // new trigger
);

Function Triggers

The framework supports both post-execution and pre-execution function triggers.

Post-Execution: This is what the sample app uses with onCreatePoolFunction. The handler is called after the contract’s function completes, so it has access to both inputs and outputs.

Pre-Execution: To react to a function before it executes, you use the corresponding Pre- abstract contract (e.g., preCreatePoolFunction). The handler receives a PreFunctionContext and only has access to the function’s inputs, as outputs have not yet been generated.

contract Listener is UniswapV3Factory$PreCreatePoolFunction {
    // Fires *before* createPool executes
    event PoolWillBeCreated(
        uint64  chainId,
        address token0,
        address token1,
        uint24  fee
    );

    function preCreatePoolFunction(
        PreFunctionContext memory /*ctx*/,
        UniswapV3Factory$CreatePoolFunctionInputs memory inputs
    )
        external
        override
    {
        emit PoolWillBeCreated(
            uint64(block.chainid),
            inputs.tokenA,
            inputs.tokenB,
            inputs.fee
        );
    }
}

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

        address factory = 0x1F98431c8aD98523631AE4a59f267346ea31F984; // Uniswap V3 Factory (Ethereum)

        addTrigger(chainContract(Chains.Ethereum, factory), listener.triggerPreCreatePoolFunction());
    }
}

Test Your Listener

Sim IDX gives you two ways to make sure your listener behaves as expected while you build.

Unit Tests with Foundry

The listeners folder is a Foundry project. sim test is a thin wrapper around forge test. It will compile your contracts, execute all Forge unit tests inside listeners/test/, and surface any failures.

Historical Replay

Use sim listeners evaluate to see how your listener reacts to real onchain data before pushing your updates. This command compiles your listener and executes the transactions in any block range you specify.

sim listeners evaluate \
  --chain-id 1 \
  --start-block 12369662 \
  --end-block 12369670 \
  --listeners=Listener

The --listeners=Listener flag specifies which listener contract to evaluate. You can update this to match your specific listener contract name. Visit the sim listeners evaluate documentation to learn more about the available flags.

Next Steps

You’ve now seen how to create triggers, emit events, and validate your listener. Here are a few great ways to level-up your Sim IDX app.