GhostCrab is a rust library that allows you to index Ethereum smart contracts. It provides a simple and easy-to-use API for building performant and scalable indexers.
To get started with GhostCrab, you need to install the Rust toolchain and the ghost-crab
crate. You can find the installation instructions in the Rust documentation.
Once you have installed the Rust toolchain, you can add the ghost-crab
crate to your project's Cargo.toml
file:
[dependencies]
ghost-crab = "0.2.0"
To use GhostCrab, you need to create a new instance of the Indexer
struct. This struct provides methods for loading event handlers and block handlers, as well as starting the indexer.
use ghost_crab::prelude::*;
use handlers::etherfi;
use handlers::stader;
#[tokio::main]
async fn main() {
let mut indexer = ghost_crab::Indexer::new().unwrap();
indexer
.load_event_handler(etherfi::EtherFiTVLUpdated::new())
.await
.unwrap();
indexer
.load_block_handler(stader::StaderBlockHandler::new())
.await
.unwrap();
indexer.start().await.unwrap();
}
Event handlers are used to process events emitted by smart contracts. They are defined as closures that implement the Handler
trait. The Handler
trait provides methods for accessing the event data, the contract address, and other useful information.
Here's an example of an event handler that processes TVLUpdated
events emitted by the EtherFi Oracle contract:
use alloy::eips::BlockNumberOrTag;
use ghost_crab::prelude::*;
#[event_handler(EtherFi.TVLUpdated)]
async fn EtherFiTVLUpdated(ctx: Context) {
let block_number = ctx.log.block_number.unwrap() as i64;
let current_tvl = event._currentTvl.to_string();
let log_index = ctx.log.log_index.unwrap() as i64;
let block = ctx.block().await.unwrap().unwrap();
let block_timestamp = block.header.timestamp as i64;
// Save the data to your database
}
In the current version, we do not offer any kind of abstractions for the DB interactions, so you will have to use an external library like sqlx
to interact with the DB, and save the data in your desired format.
In the above example, EtherFi
is defined in the configuration as follows:
{
"database": "$DATABASE_URL",
"dataSources": {
"EtherFi": {
"startBlock": 105927637,
"address": "0x6329004E903B7F420245E7aF3f355186f2432466",
"abi": "abis/etherfi/TVLOracle.json",
"network": "optimism"
}
},
"networks": {
"optimism": {
"rpcUrl": "$OPT_RPC_URL",
"requestsPerSecond": 30
}
}
}
Block handlers are used to process blocks. They are defined as closures that implement the BlockHandler
trait. The BlockHandler
trait provides methods for accessing the block data and other useful information.
Here's an example of a block handler that processes blocks:
use alloy::{eips::BlockId, sol};
use ghost_crab::prelude::*;
sol!(
#[sol(rpc)]
StaderStakePoolsManager,
"abis/stader/StaderStakePoolsManager.json"
);
const STADER: Address = address!("cf5EA1b38380f6aF39068375516Daf40Ed70D299");
#[block_handler(Stader)]
async fn StaderBlockHandler(ctx: BlockContext) {
let stader_staking_manager = StaderStakePoolsManager::new(STADER, &ctx.provider);
let total_assets = stader_staking_manager
.totalAssets()
.block(BlockId::from(ctx.block_number))
.call()
.await
.unwrap();
let db = db::get().await;
let eth = total_assets._0.to_string();
let block_number = ctx.block_number as i64;
let block = ctx.block().await.unwrap().unwrap();
let block_timestamp = block.header.timestamp as i64;
// Save the data to your database
}
In the above example, Stader
is defined in the configuration as follows:
{
"database": "$DATABASE_URL",
"blockHandlers": {
"Stader": {
"startBlock": 17416153,
"network": "ethereum",
"step": 720
}
},
"networks": {
"ethereum": {
"rpcUrl": "$ETH_RPC_URL",
"requestsPerSecond": 30
}
}
}
Templates are ideal to dynamically trigger new indexing processes. They are defined as closures that implement the Handler
trait. The Handler
trait provides methods for accessing the event data, the contract address, and other useful information.
Here's an example on how to use templates:
use alloy::eips::BlockNumberOrTag;
use ghost_crab::prelude::*;
#[template(ETHVault.Deposited)]
async fn ETHVaultDeposited(ctx: Context) {
// Handler Logic
}
#[event_handler(VaultsRegistry.VaultAdded)]
async fn VaultsRegistry(ctx: Context) {
ctx.templates
.start(Template {
address: vault.clone(),
start_block: ctx.log.block_number.unwrap(),
handler: ETHVaultDeposited::new(),
})
.await;
}
In the above example, we are tracking a VaultsRegistry
contract which emits a VaultAdded
event every time a new vault is added. Under the hood, GhostCrab is using the TemplateManager
to start a new indexing process for the ETHVault
contract on the specified address, and start block.
In this particular case, there is no way we could have known the address of the ETHVault
contract before the VaultAdded
event was emitted, so this is when templates come handy to dynamically start the indexing processes for new contracts.
GhostCrab uses a configuration file to specify the data sources, templates, and block handlers. Here's an example of a configuration file:
{
"dataSources": {
"MyDataSourceName": {
"abi": "my_contract_abi.json",
"address": "0x1234567890123456789012345678901234567890",
"start_block": 1000000,
"network": "mainnet"
}
},
"templates": {
"MySecondDataSourceName": {
"abi": "my_second_contract_abi.json",
"network": "mainnet"
}
},
"networks": {
"mainnet": {
"rpcUrl": "$MAINNET_RPC_URL",
"requestsPerSecond": 30
}
},
"blockHandlers": {
"MyThirdDataSourceName": {
"start_block": 1000000,
"network": "mainnet"
}
}
}
In summary:
- If you want to use an environment variable, you can use the
$ENV_VAR
syntax within the configuration file. - If you want to create an event handler, you need to define a data source. This data source will be loaded by the proc macro
event_handler
. - If you want to create a template, you need to define a template. This template will be loaded by the proc macro
template
. - If you want to create a block handler, you need to define a block handler. This block handler will be loaded by the procedural macro
block_handler
.
If you want to see some examples of how to use GhostCrab, you can check out our indexers repo, where we maintain a collection of smart contracts indexers for our staking analytics dashboard.