use anyhow::{Context, Result};
use chrono::{Duration, Local, NaiveDate};
use reqwest::blocking::Client;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::fs;
use std::path::Path;
#[derive(Debug, Deserialize)]
struct RelayResponse {
builders: Vec<RelayBuilder>,
}
#[derive(Debug, Deserialize)]
struct RelayBuilder {
info: BuilderInfo,
children: Option<Vec<ChildBuilder>>,
}
#[derive(Debug, Deserialize)]
struct BuilderInfo {
extra_data: String,
num_blocks: u64,
}
#[derive(Debug, Deserialize)]
struct ChildBuilder {
extra_data: String,
num_blocks: u64,
}
#[derive(Debug, Serialize)]
pub struct HierarchicalBuilder {
pub name: String,
pub blocks: u64,
pub children: Vec<HierarchicalChild>,
}
#[derive(Debug, Serialize)]
pub struct HierarchicalChild {
pub name: String,
pub blocks: u64,
}
pub struct StatsAggregator {
client: Client,
}
impl StatsAggregator {
pub fn new() -> Result<Self> {
let client = Client::builder().timeout(std::time::Duration::from_secs(10)).build().context("Failed to create HTTP client")?;
Ok(Self { client })
}
fn fetch_relay_data(&self, date: &str) -> Result<RelayResponse> {
let url = format!("https://www.relayscan.io/stats/day/{date}/json");
let response = self.client.get(&url).send().context(format!("Failed to fetch data for {date}"))?;
if !response.status().is_success() {
anyhow::bail!("HTTP error: {}", response.status());
}
response.json().context("Failed to parse JSON response")
}
fn aggregate_builders(&self, builders: Vec<RelayBuilder>) -> (Vec<HierarchicalBuilder>, HashMap<String, u64>) {
let mut hierarchical = Vec::new();
let mut flat_aggregated = HashMap::new();
for builder in builders {
let extra_data = builder.info.extra_data.trim().to_string();
let num_blocks = builder.info.num_blocks;
*flat_aggregated.entry(extra_data.clone()).or_insert(0) += num_blocks;
let mut builder_info = HierarchicalBuilder { name: extra_data.clone(), blocks: num_blocks, children: Vec::new() };
if let Some(children) = builder.children {
let mut child_vec = Vec::new();
for child in children {
let child_extra_data = child.extra_data.trim().to_string();
let child_blocks = child.num_blocks;
*flat_aggregated.entry(child_extra_data.clone()).or_insert(0) += child_blocks;
child_vec.push(HierarchicalChild { name: child_extra_data, blocks: child_blocks });
}
child_vec.sort_by(|a, b| b.blocks.cmp(&a.blocks));
builder_info.children = child_vec;
}
hierarchical.push(builder_info);
}
hierarchical.sort_by(|a, b| b.blocks.cmp(&a.blocks));
(hierarchical, flat_aggregated)
}
pub fn get_date_range(start: Option<&str>, end: Option<&str>, days: i64) -> Result<Vec<String>> {
let dates = if let (Some(start_str), Some(end_str)) = (start, end) {
let start_date = NaiveDate::parse_from_str(start_str, "%Y-%m-%d").context("Invalid start date format")?;
let end_date = NaiveDate::parse_from_str(end_str, "%Y-%m-%d").context("Invalid end date format")?;
let (start_date, end_date) = if start_date > end_date { (end_date, start_date) } else { (start_date, end_date) };
let mut dates = Vec::new();
let mut current = start_date;
while current <= end_date {
dates.push(current.format("%Y-%m-%d").to_string());
current = current.succ_opt().unwrap();
}
dates
} else {
let today = Local::now();
let yesterday = today - Duration::days(1);
let mut dates = Vec::new();
for i in 0..days {
let date = yesterday - Duration::days(i);
dates.push(date.format("%Y-%m-%d").to_string());
}
dates.reverse(); dates
};
Ok(dates)
}
pub fn merge_hierarchical_data(&self, all_data: Vec<Vec<HierarchicalBuilder>>) -> Vec<HierarchicalBuilder> {
let mut merged: HashMap<String, (u64, HashMap<String, u64>)> = HashMap::new();
for daily_data in all_data {
for builder in daily_data {
let entry = merged.entry(builder.name.clone()).or_insert((0, HashMap::new()));
entry.0 += builder.blocks;
for child in builder.children {
*entry.1.entry(child.name).or_insert(0) += child.blocks;
}
}
}
let mut result: Vec<HierarchicalBuilder> = merged
.into_iter()
.map(|(name, (blocks, children_map))| {
let mut children: Vec<HierarchicalChild> = children_map
.into_iter()
.map(|(child_name, child_blocks)| HierarchicalChild { name: child_name, blocks: child_blocks })
.collect();
children.sort_by(|a, b| b.blocks.cmp(&a.blocks));
HierarchicalBuilder { name, blocks, children }
})
.collect();
result.sort_by(|a, b| b.blocks.cmp(&a.blocks));
result
}
pub fn aggregate_and_save(&self, start: Option<&str>, end: Option<&str>, days: i64, output_path: &Path) -> Result<()> {
let dates = Self::get_date_range(start, end, days)?;
println!("Fetching data for dates: {} to {} ({} days)", dates.first().unwrap(), dates.last().unwrap(), dates.len());
println!("{}", "=".repeat(60));
let mut all_hierarchical_data = Vec::new();
let mut total_flat_aggregated: HashMap<String, u64> = HashMap::new();
for date_str in &dates {
println!("Fetching data for {date_str}...");
match self.fetch_relay_data(date_str) {
Ok(data) => {
let (hierarchical, flat_aggregated) = self.aggregate_builders(data.builders);
for (extra_data, num_blocks) in &flat_aggregated {
*total_flat_aggregated.entry(extra_data.clone()).or_insert(0) += num_blocks;
}
println!(" Found {} unique parent builders", hierarchical.len());
all_hierarchical_data.push(hierarchical);
}
Err(e) => {
println!(" Error: {e}");
}
}
}
if all_hierarchical_data.is_empty() {
anyhow::bail!("No data found for the specified date range");
}
let merged_hierarchical = self.merge_hierarchical_data(all_hierarchical_data);
self.print_hierarchical_results(&merged_hierarchical);
let mut sorted_flat: Vec<_> = total_flat_aggregated.into_iter().collect();
sorted_flat.sort_by(|a, b| b.1.cmp(&a.1));
let output_data: serde_json::Map<String, serde_json::Value> =
sorted_flat.into_iter().map(|(k, v)| (k, serde_json::Value::Number(v.into()))).collect();
let json = serde_json::to_string_pretty(&output_data)?;
fs::write(output_path, json).context("Failed to write output file")?;
println!("\nFlat aggregated results saved to: {}", output_path.display());
Ok(())
}
fn print_hierarchical_results(&self, hierarchical_data: &[HierarchicalBuilder]) {
println!("\n{}", "=".repeat(80));
println!("AGGREGATED BUILDER RESULTS (Hierarchical)");
println!("{}", "=".repeat(80));
println!("{:<50} {:<10} {:<10}", "Builder Name", "Blocks", "Percentage");
println!("{}", "-".repeat(80));
let total_blocks: u64 = hierarchical_data.iter().map(|b| b.blocks).sum();
for builder in hierarchical_data {
let name = if builder.name.is_empty() { "(empty)" } else { &builder.name };
let percentage = if total_blocks > 0 { (builder.blocks as f64 / total_blocks as f64) * 100.0 } else { 0.0 };
println!("{:<50} {:<10} {:<9.2}%", name, builder.blocks, percentage);
if !builder.children.is_empty() {
for (i, child) in builder.children.iter().enumerate() {
let is_last = i == builder.children.len() - 1;
let prefix = if is_last { "└── " } else { "├── " };
let child_name = if child.name.is_empty() { "(empty)" } else { &child.name };
let child_percentage = if total_blocks > 0 { (child.blocks as f64 / total_blocks as f64) * 100.0 } else { 0.0 };
let display_name = format!("{prefix}{child_name}");
println!("{:<50} {:<10} {:<9.2}%", display_name, child.blocks, child_percentage);
}
}
}
println!("{}", "-".repeat(80));
println!("{:<50} {:<10} {:<10}", "TOTAL", total_blocks, "100.00%");
}
}