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)
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.
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.
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
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.
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.
- Start by defining the interface
interface IERC20 {
function name() external view returns (string memory);
function symbol() external view returns (string memory);
function decimals() external view returns (uint8);
}
- Then use it like how you'd call it in smart contracts
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
// 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);
}
}