The core of a Sim IDX application 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.

App Structure Explained

my-idx-app/
├── sim.toml                       # App configuration
├── abis/                          # JSON ABIs for the contracts you want to index
├── apis/                          # Your TypeScript/Edge runtime APIs
└── listeners/                     #   ← a Foundry project
    ├── src/
    │   └── Main.sol               # PRIMARY editing target
    ├── test/
    │   └── Main.t.sol             # Unit tests for your listener
    └── lib/
        ├── sim-idx-sol/           # Core Sim IDX framework (DSL, context, helpers)
        └── sim-idx-generated/     # Code generated from the ABIs you add

Primary Development Folders

The abis/ and listeners/ folders are the two folders you will work with most often.

ABI Files

The abis/ folder contains JSON ABI files of smart contracts you want to index. The sample app includes abis/UniswapV3Factory.json for the Uniswap V3 Factory contract.

If you want to add an additional ABI and learn what happens when you run the command, see the sim abi add CLI command docs.

Understanding the Listener Contract

A listener is a special contract Sim IDX simulates 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 store 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

Main.sol lives in listeners/src and contains two contracts:

ContractPurpose
TriggersRegisters triggers via addTrigger. No business logic.
ListenerImplements handlers and emits events that define your database schema.

Let’s break the file 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 the enums, structs, abstract contracts and helper functions you will use while writing your indexing logic.

Generated.sol contains the Solidity code created by the sim abi add command. For the sample app this file includes the generated Solidity bindings for the Uniswap V3 Factory located at listeners/lib/sim-idx-generated/UniswapV3Factory.sol.

Triggers contract

A contract that tells Sim IDX when to run your code. Sim IDX does this through what we call a trigger. A trigger tells Sim IDX when to call one of your handlers for a function or event on an external contract.

contract Triggers is BaseTriggers {

BaseTriggers comes from Simidx.sol. It is an abstract contract that provides helper functions, most importantly addTrigger.

    function triggers() external override {

BaseTriggers requires you to implement the triggers function. Inside this function you register every trigger your application needs.

        Listener listener = new Listener();

Here you create an instance of Listener, the second contract in Main.sol. We will look at this contract in the next section.

        // Listen on Ethereum mainnet (chain 1) to Uniswap V3 Factory.createPool
        addTrigger(
            ChainIdContract(1, 0x1F98431c8aD98523631AE4a59f267346ea31F984),
            listener.triggerOnCreatePoolFunction()
        );
    }
}

addTrigger takes two arguments. The first argument identifies the on chain target by chain identifier and contract address. The second argument is the handler selector that the listener contract exposes.

Let’s take a look at the Listener contract now.

Listener contract

contract Listener is UniswapV3Factory$OnCreatePoolFunction {

Listener extends UniswapV3Factory$OnCreatePoolFunction, an abstract contract generated automatically by sim abi add and made available through Generated.sol.

After declaring the contract you define the events that you want to emit.

    event PoolCreated(
        uint64  chainId,
        address caller,
        address pool,
        address token0,
        address token1,
        uint24  fee
    );

Emitting an event defines the shape of the database that Sim IDX creates. The event name becomes the table name and each parameter becomes a column.

Next you implement the handler that the generated abstract contract expects.

    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
        );
    }
}

Because you extended UniswapV3Factory$OnCreatePoolFunction you must implement onCreatePoolFunction.

Each handler name follows the pattern of the function or event it processes. When this handler runs Sim IDX gives you three arguments: a transaction context, the decoded inputs and the decoded outputs. You can use this data to emit one or more events which Sim IDX writes to your application database.

From Events to DB

When the handler emits PoolCreated, Sim IDX creates a database view of the event’s name in snake_case. For example, PoolCreated becomes pool_created.

Here’s an example of the contents:

chainIdcallerpooltoken0token1fee
10x1f98431c8ad98523631ae4a59f267346ea31f9840xf2c1e03841e06127db207fda0c3819ed9f7889030x4a074a606ccc467c513933fa0b48cf37033cac1f0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc210000

If you add or remove parameters on the event and deploy your changes, Sim IDX automatically adjusts the table.

Capturing more event data

You can define and emit as many events as you like—each with whatever properties you care about—and you can update an event definition at any time.

In the example below we modify the Listener contract in listeners/src/Main.sol to capture one additional piece of information: the block number in which the pool was created.

Extend the event

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

Adding a parameter changes the shape of the table that Sim IDX creates for this event. blockNumber will appear as a new column after you push your changes.

Emit the new data

Listener Contract Handler Definition
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,
        block.number               // pass the new value
    );
}

Once you’ve saved these changes you can test your listener to check against real data and verify that everything works as expected.

Hooking into more functions and events

When you’d like to index additional calls from the same contract, Sim IDX makes it easy. Each ABI you add is converted into a Solidity file inside listeners/lib/sim-idx-generated/. That file contains every helper function, struct, and abstract contract you’ll need. All of these are exported through Generated.sol, which is imported at the top of listeners/src/Main.sol. It brings every generated hook into scope for your Listener contract.

The workflow for tapping into an extra function or event is:

  1. Discover the hook: open the generated file (e.g. UniswapV3Factory.sol) and locate the abstract contract that corresponds to the function or event you care about.
  2. Extend Listener with that abstract contract.
  3. Add a new event that matches the data you want in your database.
  4. Implement the handler to emit the event.
  5. Register the trigger inside the Triggers contract.

Below we walk through adding support for the owner() function on the Uniswap V3 Factory contract.

1. Discover the hook

In listeners/lib/sim-idx-generated/UniswapV3Factory.sol you will find:

abstract contract UniswapV3Factory$OnOwnerFunction {
    function onOwnerFunction(
        FunctionContext memory ctx,
        UniswapV3Factory$ownerFunctionOutputs memory outputs
    ) virtual external;

    function triggerOnOwnerFunction() view external returns (Trigger memory);
}

This abstract contract represents the owner() function. Implementing its handler lets your listener react whenever that function is executed. The paired triggerOnOwnerFunction() helper returns the selector you’ll reference in the Triggers contract.

2. Extend the listener

contract Listener is
    UniswapV3Factory$OnCreatePoolFunction,  // existing
    UniswapV3Factory$OnOwnerChangedEvent    // NEW
{
    // ...
}

By inheriting from the new abstract contract you promise the compiler that you’ll provide a concrete onOwnerFunction implementation, unlocking typed access to the decoded outputs.

3. Define an event

Listener Contract Event Definition
event OwnerChanged(
    uint64  chainId,
    address oldOwner,
    address newOwner,
    uint256 blockNumber
);

Events define your database schema. Here we record the queried owner plus a bit of useful context about the call.

4. Implement the handler

Listener Contract Handler Definition
function onOwnerChangedEvent(
    EventContext memory ctx, 
    UniswapV3Factory$OwnerChangedEventParams memory inputs
)
    external
    override
{
    emit OwnerChanged(
        uint64(block.chainid),
        inputs.oldOwner,
        inputs.newOwner
        block.number
    );
}

Everything you need like the execution context and the decoded outputs is provided by the generated bindings. Because the abstract contract’s method is external, remember to mark this implementation with override.

5. Register the trigger

addTrigger(
    ChainIdContract(1, 0x1F98431c8aD98523631AE4a59f267346ea31F984), 
    listener.triggerOnOwnerFunction()
);

Triggers.triggers() is your trigger registry. Adding this additional addTrigger call tells Sim IDX to invoke your new handler whenever owner() is called on the Uniswap V3 factory contract.

6. Test it out

Save your changes, then test your listener to replay historical data and confirm that OwnerChanged rows are appearing.

sim listeners evaluate \
  --chain-id 1 \
  --start-block 12369662

This replays block 12369662 on Ethereum Mainnet so you can confirm that OwnerQueried rows appear in the console.

Adding another ABI

  1. Place the ABI JSON in abis/ (e.g. abis/MyContract.json).
  2. Run:
sim abi add abis/MyContract.json

The command regenerates listeners/lib/sim-idx-generated/, re-creates Generated.sol, and autogenerates all the functions, structs, and abstract contracts you need to work with the ABI you just added.

After running the command you can open listeners/src/Main.sol and extend Listener with any of the freshly-generated *OnSomeEvent / *OnSomeFunction contracts. Browsing listeners/lib/sim-idx-generated/ is the quickest way to see every event and function that is now available to you.

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

contract Listener is MyContract$OnSomeEvent, MyContract$OnSomeFunction {
    // ...
}

Testing your listener

Because listeners/ is a Foundry project you can write standard Forge tests in listeners/test/. Running

sim test            # delegates to `forge test`

will compile the listener, execute the tests, and surface any failures in the CLI just like in any other Solidity project.

You can also run

sim listeners evaluate

to execute your listener against historical blockchain data before deploying it. See the CLI docs for more details.

Next steps

From here you can broaden the coverage of your application by adding new ABIs and triggers or start exposing the indexed data through custom APIs. Once you’re happy with your listener you can deploy the app following the deployment guide, or head over to the API development guide to build rich endpoints that power your applications.