use crate::{
constant::{HERODOTUS_RS_INDEXER_STAGING_URL, HERODOTUS_RS_INDEXER_URL},
primitives::{
block::header::{
MMRDataFromNewIndexer, MMRFromNewIndexer, MMRMetaFromNewIndexer, MMRProofFromNewIndexer,
},
ChainId,
},
};
use alloy::primitives::BlockNumber;
use reqwest::Client;
use serde_json::{from_value, Value};
use std::collections::HashMap;
use thiserror::Error;
use tracing::{debug, error};
#[derive(Error, Debug)]
pub enum IndexerError {
#[error("Invalid block range")]
InvalidBlockRange,
#[error("Failed to send request")]
ReqwestError(#[from] reqwest::Error),
#[error("Failed to parse response")]
SerdeJsonError(#[from] serde_json::Error),
#[error("Validation error: {0}")]
ValidationError(String),
#[error("Failed to get headers proof: {0}")]
GetHeadersProofError(String),
}
impl ChainId {
fn get_indexer_chain_id(&self) -> &str {
match self {
ChainId::EthereumMainnet => "1",
ChainId::EthereumSepolia => "11155111",
ChainId::StarknetMainnet => "STARKNET",
ChainId::StarknetSepolia => "SN_SEPOLIA",
}
}
}
#[derive(Clone)]
pub struct Indexer {
url: String,
client: Client,
pub from_chain_id: ChainId,
pub deployed_on_chain_id: ChainId,
}
#[derive(Debug)]
pub struct IndexerHeadersProofResponse {
pub mmr_meta: MMRMetaFromNewIndexer,
pub headers: HashMap<BlockNumber, MMRProofFromNewIndexer>,
}
impl IndexerHeadersProofResponse {
pub fn new(mmr_data: MMRDataFromNewIndexer) -> Self {
let mmr_meta = mmr_data.meta;
let headers = mmr_data
.proofs
.into_iter()
.map(|block| (block.block_number, block))
.collect();
Self { mmr_meta, headers }
}
}
impl Indexer {
pub fn new(from_chain_id: ChainId, deployed_on_chain_id: ChainId) -> Self {
Self {
client: Client::new(),
from_chain_id,
deployed_on_chain_id,
url: HERODOTUS_RS_INDEXER_URL.to_string(),
}
}
pub fn staging(mut self) -> Self {
self.url = HERODOTUS_RS_INDEXER_STAGING_URL.to_string();
self
}
pub async fn get_headers_proof(
&self,
from_block: BlockNumber,
to_block: BlockNumber,
) -> Result<IndexerHeadersProofResponse, IndexerError> {
if from_block > to_block {
return Err(IndexerError::InvalidBlockRange);
}
let target_length = (to_block - from_block + 1) as usize;
let response = self
.client
.get(&self.url)
.query(&self._query(
from_block,
to_block,
self.from_chain_id.get_indexer_chain_id(),
self.deployed_on_chain_id.get_indexer_chain_id(),
))
.send()
.await
.map_err(IndexerError::ReqwestError)?;
if response.status().is_success() {
let body: Value = response.json().await.map_err(IndexerError::ReqwestError)?;
let parsed_mmr: MMRFromNewIndexer =
from_value(body).map_err(IndexerError::SerdeJsonError)?;
if parsed_mmr.data.is_empty() {
Err(IndexerError::ValidationError("No MMR found".to_string()))
} else if parsed_mmr.data.len() > 1 {
return Err(IndexerError::ValidationError(
"MMR length should be 1".to_string(),
));
} else {
if parsed_mmr.data[0].proofs.len() != target_length {
return Err(IndexerError::ValidationError(
"Indexer didn't return the correct number of headers that requested"
.to_string(),
));
}
let mmr_data = parsed_mmr.data[0].clone();
Ok(IndexerHeadersProofResponse::new(mmr_data))
}
} else {
error!(
"Failed to get headers proof from rs-indexer: {}",
response.status()
);
Err(IndexerError::GetHeadersProofError(
response.text().await.map_err(IndexerError::ReqwestError)?,
))
}
}
fn _query(
&self,
from_block: BlockNumber,
to_block: BlockNumber,
accumulates_chain_id: &str,
deployed_on_chain_id: &str,
) -> Vec<(String, String)> {
let query = vec![
(
"deployed_on_chain".to_string(),
deployed_on_chain_id.to_string(),
),
(
"accumulates_chain".to_string(),
accumulates_chain_id.to_string(),
),
("hashing_function".to_string(), "poseidon".to_string()),
("contract_type".to_string(), "AGGREGATOR".to_string()),
(
"from_block_number_inclusive".to_string(),
from_block.to_string(),
),
(
"to_block_number_inclusive".to_string(),
to_block.to_string(),
),
("is_meta_included".to_string(), "true".to_string()),
("is_whole_tree".to_string(), "true".to_string()),
("is_rlp_included".to_string(), "true".to_string()),
("is_pure_rlp".to_string(), "true".to_string()),
];
debug!("request params to indexer: {:#?}", query);
query
}
}
#[cfg(test)]
mod tests {
use super::*;
#[tokio::test]
async fn test_get_headers_proof() -> Result<(), IndexerError> {
let indexer = Indexer::new(ChainId::EthereumSepolia, ChainId::EthereumSepolia);
let response = indexer.get_headers_proof(1, 1).await?;
assert!(response.headers.len() == 1);
Ok(())
}
#[tokio::test]
async fn test_get_sn_headers_proof() -> Result<(), IndexerError> {
let indexer = Indexer::new(ChainId::StarknetSepolia, ChainId::EthereumSepolia).staging();
let response = indexer.get_headers_proof(208483, 208483).await?;
assert!(response.headers.len() == 1);
Ok(())
}
#[tokio::test]
async fn test_get_headers_proof_multiple_blocks() -> Result<(), IndexerError> {
let indexer = Indexer::new(ChainId::EthereumSepolia, ChainId::EthereumSepolia);
let response = indexer.get_headers_proof(0, 10).await?;
assert!(response.headers.len() == 11);
Ok(())
}
#[tokio::test]
async fn test_invalid_query() {
let indexer = Indexer::new(ChainId::EthereumSepolia, ChainId::EthereumSepolia);
let response = indexer.get_headers_proof(10, 1).await;
assert!(matches!(response, Err(IndexerError::InvalidBlockRange)));
}
}