Skip to main content

5. Write Index Handlers

In indexer.sol, you register contract addresses and write your handler logic.

Registering Contracts

There are 2 ways to register the contract addresses that the indexer should listen to events from:

a) Register Basic Contracts (up to 30 contracts)

indexer.sol
function registerHandles() external {
graph.registerHandle(0xC15568330926E2A6f1519992b0364ca00faf6A7A);
graph.registerHandle(0xC9438f95AA8d9ee1b5edEa15c7fa4B2CAC723dcE);
}

b) Register Factory Contracts

By registering a factory contract and specifying the event and event parameter that identifies the deployed child contract, the indexing engine will automatically detect and listen to events emitted by the child contracts. This allows for efficient tracking and indexing of events from dynamically created contracts.

indexer.sol
function registerHandles() external {
graph.registerFactory(0xb4A7D971D0ADea1c73198C97d7ab3f9CE4aaFA13, GhostEventName.PairCreated, "pair");
graph.registerHandle(0xb4A7D971D0ADea1c73198C97d7ab3f9CE4aaFA13);
}

In the above example, the address shown is one of the Thruster factory addresses. We are instructing the indexing engine to keep track of the children of the factory contract and separately, the events emitted by the factory itself, because in addition to tracking swap events from pairs, which are children of the factory, we also want to track pair creations separately so that we can create and save Pair entities.

Write your Handlers

Transformations for PairCreated, Swap and Sync Events

Here's a simple example that tracks swaps and maintains the latest pair status.

indexer.sol
address constant FACTORY = 0xb4A7D971D0ADea1c73198C97d7ab3f9CE4aaFA13;

function onPairCreated(EventDetails memory details, PairCreatedEvent memory ev) external {
Pair memory pair = graph.getPair(ev.pair);
pair.token0 = ev.token0;
pair.token1 = ev.token1;
pair.createdTxHash = details.transactionHash;
pair.createdAt = details.timestamp;
graph.savePair(pair);
}

function onSwap(EventDetails memory details, SwapEvent memory ev) external {
if (details.emitter == FACTORY) {
return;
}
Swap memory swap = graph.getSwap(details.uniqueId());
swap.amount0In = ev.amount0In;
swap.amount0Out = ev.amount0Out;
swap.amount1In = ev.amount1In;
swap.amount1Out = ev.amount1Out;
swap.txHash = details.transactionHash;
swap.pairId = details.emitter;
graph.saveSwap(swap);

Pair memory pair = graph.getPair(details.emitter);
pair.lastSwapTime = details.timestamp;
pair.lastSwapTxHash = details.transactionHash;
graph.savePair(pair);
}

function onSync(EventDetails memory details, SyncEvent memory ev) external {
if (details.emitter == FACTORY) {
return;
}
Pair memory pair = graph.getPair(details.emitter);
pair.reserve0 = ev.reserve0;
pair.reserve1 = ev.reserve1;
graph.savePair(pair);
}

Explanation:

  • onPairCreated updates or creates a new Pair entity when a PairCreated event is fired.
  • onSwap creates a Swap entity based on the Swap event. It uses a unique identifier derived from the event log.
  • onSync updates the pair reserves.

graph Object

In your indexer.sol file, you can use the graph global object to fetch and save entities. After code generation, all structs defined in schema.sol will have corresponding get and save functions.

For example, if you defined Pair and Swap structs in schema.sol file, you can use graph.getPair(), graph.savePair(), graph.getSwap(), and graph.saveSwap() in the indexer.sol file.

To see all the available functions, check the gen_base.sol file after code generation

gen_base_sol

Helper Functions

Since a unique id is commonly needed when creating an entity, we provide a callable string helper function. For example, on line 2, we call uniqueId on details. For the implementation, see gen_helpers.sol.

details.uniqueId() returns a string in the form of "B<block_number>:L<log_index>" which is unique for each log emission.

If you're saving entities that have a natural unique id such as a pool address you should use that value. But for entities that do not have a natural unique id but are unique per event log you can use this helper method.

indexer.sol
    function onSwap(EventDetails memory details, SwapEvent memory ev) external {
Swap memory swap = graph.getSwap(details.uniqueId());
swap.amount0In = ev.amount0In;
swap.amount0Out = ev.amount0Out;
swap.amount1In = ev.amount1In;
swap.amount1Out = ev.amount1Out;
swap.txHash = details.transactionHash;
swap.pairId = details.emitter;
graph.saveSwap(swap);
}

Event Details

Each event handler function is, in addition ot the event data, is also passed an EventDetails struct that you can optionally use in your handler logic.

struct EventDetails {
uint64 block;
address emitter;
uint32 logIndex;
bytes32 transactionHash;
uint32 txIndex;
uint32 timestamp;
}

State Calls

Sometimes, you may need to call an external contract to fetch metadata. For instance, when creating a Thruster pair, it may be useful to retrieve details such as the name, symbol, and decimals of ERC20 tokens.

Using GhostGraph, this process is straightforward:

Simply interact with it as you would when you are writing your Smart Contracts.

  1. Start by defining the interface
indexer.sol
interface IERC20 {
function name() external view returns (string memory);
function symbol() external view returns (string memory);
function decimals() external view returns (uint8);
}
  1. Then use it like how you'd call it in smart contracts
indexer.sol
function tryGetName(address token) internal view returns (string memory) {
try IERC20(token).name() returns (string memory tokenName) {
return tokenName;
} catch {
return "Unknown";
}
}

function onPairCreated(EventDetails memory details, PairCreatedEvent memory ev) external {
Pair memory pair = graph.getPair(ev.pair);
pair.token0 = ev.token0;
pair.token1 = ev.token1;
pair.token0Name = tryGetName(ev.token0);
pair.token1Name = tryGetName(ev.token1);
pair.createdTxHash = details.transactionHash;
pair.createdAt = details.timestamp;
graph.savePair(pair);
}

Full Example without state calls

indexer.sol
// SPDX-License-Identifier: MIT
pragma solidity 0.8.19;

import "./gen_schema.sol";
import "./gen_events.sol";
import "./gen_base.sol";
import "./gen_helpers.sol";

contract MyIndex is GhostGraph {
using StringHelpers for EventDetails;

function registerHandles() external {
// ex: register thruster factory contract on Blast Mainnet
// Uncomment these to register your contracts
graph.registerFactory(0xb4A7D971D0ADea1c73198C97d7ab3f9CE4aaFA13, GhostEventName.PairCreated, "pair");
graph.registerHandle(0xb4A7D971D0ADea1c73198C97d7ab3f9CE4aaFA13);
}

address constant FACTORY = 0xb4A7D971D0ADea1c73198C97d7ab3f9CE4aaFA13;

function onPairCreated(EventDetails memory details, PairCreatedEvent memory ev) external {
Pair memory pair = graph.getPair(ev.pair);
pair.token0 = ev.token0;
pair.token1 = ev.token1;
pair.createdTxHash = details.transactionHash;
pair.createdAt = details.timestamp;
graph.savePair(pair);
}

function onSwap(EventDetails memory details, SwapEvent memory ev) external {
if (details.emitter == FACTORY) {
return;
}
Swap memory swap = graph.getSwap(details.uniqueId());
swap.amount0In = ev.amount0In;
swap.amount0Out = ev.amount0Out;
swap.amount1In = ev.amount1In;
swap.amount1Out = ev.amount1Out;
swap.txHash = details.transactionHash;
swap.pairId = details.emitter;
graph.saveSwap(swap);

Pair memory pair = graph.getPair(details.emitter);
pair.lastSwapTime = details.timestamp;
pair.lastSwapTxHash = details.transactionHash;
graph.savePair(pair);
}

function onSync(EventDetails memory details, SyncEvent memory ev) external {
if (details.emitter == FACTORY) {
return;
}
Pair memory pair = graph.getPair(details.emitter);
pair.reserve0 = ev.reserve0;
pair.reserve1 = ev.reserve1;
graph.savePair(pair);
}
}