checkpointq 0.1.0

Tool for establishing checkpoint quorum for finalized checkpoints across multiple checkpoint providers
Documentation
use crate::args::Network;
use crate::client::CheckpointClient;
use crate::errors::AppError;
use crate::processor::DisplayableResult;
use axum::extract::{Path, Query};
use axum::response::Response;
use axum::{extract::State, http::StatusCode, response::IntoResponse, Json, Router};
use axum_macros::debug_handler;
use serde::{Deserialize, Serialize};
use std::{net::SocketAddr, sync::Arc};
use tower_http::trace::TraceLayer;
use tracing::info;
use tracing::level_filters::LevelFilter;

use tracing_subscriber::{fmt, EnvFilter};

#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct QueryParams {
    #[serde(default)]
    pub verbose: bool,
}

#[derive(Debug, Serialize, Deserialize)]
pub struct ApiResponse {
    block_root: String,
    epoch: String,
    #[serde(skip_serializing_if = "Option::is_none")]
    #[serde(flatten)]
    payload: Option<DisplayableResult>,
}

impl IntoResponse for ApiResponse {
    fn into_response(self) -> Response {
        (StatusCode::OK, Json(self)).into_response()
    }
}

impl IntoResponse for AppError {
    fn into_response(self) -> Response {
        (StatusCode::NOT_FOUND, self.to_string()).into_response()
    }
}

#[derive(Debug)]
pub struct CheckPointMiddleware {
    checkpoint_client: CheckpointClient<reqwest::Client>,
    port: u16,
}

impl CheckPointMiddleware {
    pub fn new(checkpoint_client: CheckpointClient<reqwest::Client>, port: u16) -> Self {
        Self {
            checkpoint_client,
            port,
        }
    }

    pub async fn serve(self) {
        fmt()
            .with_env_filter(
                EnvFilter::builder()
                    .with_default_directive(LevelFilter::INFO.into())
                    .from_env_lossy(),
            )
            .init();

        info!("starting server on port {}", self.port);
        let port = self.port;
        let app = Router::new()
            .route("/:network/finalized", axum::routing::get(finalized))
            .layer(TraceLayer::new_for_http())
            .with_state(Arc::new(self));

        let addr = SocketAddr::from(([127, 0, 0, 1], port));
        axum::Server::bind(&addr)
            .serve(app.into_make_service())
            .await
            .unwrap();
    }
}

#[debug_handler]
async fn finalized(
    State(middle_ware): State<Arc<CheckPointMiddleware>>,
    Path(network): Path<Network>,
    Query(query_params): Query<QueryParams>,
) -> Result<Json<ApiResponse>, AppError> {
    let displayable_result = middle_ware
        .checkpoint_client
        .fetch_finality_checkpoints(network)
        .await?;

    let block_not_found_msg = "Finalized block root not found";
    let epoch_not_found_msg = "Epoch not found";
    let (block_root, epoch) = match displayable_result.canonical.as_ref() {
        Some(canonical) => {
            let block_root = canonical
                .keys()
                .next()
                .map(|s| s.to_string())
                .unwrap_or(block_not_found_msg.to_string());
            let epoch = canonical
                .get(&block_root)
                .and_then(|success_payloads| success_payloads.iter().next())
                .map(|success_payload| success_payload.payload.data.finalized.epoch.to_string())
                .unwrap_or(epoch_not_found_msg.to_string());
            (block_root, epoch)
        }
        None => (
            block_not_found_msg.to_string(),
            epoch_not_found_msg.to_string(),
        ),
    };

    let payload = if query_params.verbose {
        Some(displayable_result)
    } else {
        None
    };

    let api_response = ApiResponse {
        block_root,
        epoch,
        payload,
    };

    Ok(Json(api_response))
}