pub mod dto;
pub mod error;
pub mod handlers;
#[cfg(feature = "experimental")]
pub mod prices;
use std::{
sync::Arc,
time::{Duration, Instant, SystemTime, UNIX_EPOCH},
};
use actix_web::{web, HttpResponse, ResponseError};
pub use dto::HealthStatus;
pub use error::ApiError;
use fynd_core::{
derived::SharedDerivedDataRef, feed::market_data::SharedMarketDataRef,
worker_pool_router::WorkerPoolRouter,
};
use handlers::configure_routes;
#[cfg(feature = "experimental")]
use tycho_simulation::tycho_common::models::Address;
use tycho_simulation::tycho_common::Bytes;
use utoipa::OpenApi;
use utoipa_swagger_ui::SwaggerUi;
use crate::api::error::ErrorResponse;
#[derive(OpenApi)]
#[openapi(
paths(handlers::quote, handlers::health, handlers::info),
components(schemas(
dto::QuoteRequest,
dto::Order,
dto::OrderSide,
dto::QuoteOptions,
dto::PriceGuardConfig,
dto::Quote,
dto::OrderQuote,
dto::QuoteStatus,
dto::Route,
dto::Swap,
dto::BlockInfo,
dto::InstanceInfo,
HealthStatus,
ErrorResponse,
))
)]
pub struct ApiDoc;
#[cfg(feature = "experimental")]
#[derive(OpenApi)]
#[openapi(
paths(handlers::get_prices),
components(schemas(
prices::PricesResponse,
prices::TokenPriceEntry,
prices::SpotPriceEntry,
prices::PoolDepthEntry,
))
)]
pub struct ExperimentalApiDoc;
#[derive(Clone)]
pub(crate) struct HealthTracker {
market_data: SharedMarketDataRef,
derived_data: SharedDerivedDataRef,
gas_price_stale_threshold: Option<Duration>,
created_at: Instant,
}
impl HealthTracker {
pub(crate) fn new(
market_data: SharedMarketDataRef,
derived_data: SharedDerivedDataRef,
) -> Self {
Self {
market_data,
derived_data,
gas_price_stale_threshold: None,
created_at: Instant::now(),
}
}
pub(crate) fn with_gas_price_stale_threshold(mut self, threshold: Option<Duration>) -> Self {
self.gas_price_stale_threshold = threshold;
self
}
pub(crate) async fn age_ms(&self) -> u64 {
let data = self.market_data.read().await;
match data.last_updated() {
Some(block_info) => {
let now = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap()
.as_secs();
now.saturating_sub(block_info.timestamp())
.saturating_mul(1000)
}
None => u64::MAX, }
}
pub(crate) async fn gas_price_age_ms(&self) -> Option<u64> {
let data = self.market_data.read().await;
let gas_price = data.gas_price()?;
let now_ms = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap()
.as_millis() as u64;
let block_ms = gas_price
.block_timestamp
.saturating_mul(1000);
Some(now_ms.saturating_sub(block_ms))
}
pub(crate) async fn gas_price_stale(&self) -> bool {
let Some(threshold) = self.gas_price_stale_threshold else { return false };
match self.gas_price_age_ms().await {
Some(age_ms) => age_ms > threshold.as_millis() as u64,
None => self.created_at.elapsed() > threshold,
}
}
pub(crate) async fn derived_data_ready(&self) -> bool {
self.derived_data
.read()
.await
.derived_data_ready()
}
}
#[derive(Clone)]
pub struct AppState {
worker_router: Arc<WorkerPoolRouter>,
health_tracker: HealthTracker,
chain_id: u64,
router_address: Bytes,
permit2_address: Bytes,
#[cfg(feature = "experimental")]
pub(crate) derived_data: SharedDerivedDataRef,
#[cfg(feature = "experimental")]
pub(crate) gas_token: Address,
}
impl AppState {
pub(crate) fn new(
worker_router: WorkerPoolRouter,
health_tracker: HealthTracker,
chain_id: u64,
router_address: Bytes,
permit2_address: Bytes,
#[cfg(feature = "experimental")] derived_data: SharedDerivedDataRef,
#[cfg(feature = "experimental")] gas_token: Address,
) -> Self {
Self {
worker_router: Arc::new(worker_router),
health_tracker,
chain_id,
router_address,
permit2_address,
#[cfg(feature = "experimental")]
derived_data,
#[cfg(feature = "experimental")]
gas_token,
}
}
pub(crate) fn worker_router(&self) -> &Arc<WorkerPoolRouter> {
&self.worker_router
}
pub(crate) fn health_tracker(&self) -> &HealthTracker {
&self.health_tracker
}
pub(crate) fn chain_id(&self) -> u64 {
self.chain_id
}
pub(crate) fn router_address(&self) -> &Bytes {
&self.router_address
}
pub(crate) fn permit2_address(&self) -> &Bytes {
&self.permit2_address
}
}
pub(crate) fn configure_error_handlers(cfg: &mut web::ServiceConfig) {
cfg.app_data(web::JsonConfig::default().error_handler(|err, _req| {
let api_err = ApiError::BadRequest(format!("invalid JSON: {err}"));
actix_web::error::InternalError::from_response(err, api_err.error_response()).into()
}))
.app_data(web::QueryConfig::default().error_handler(|err, _req| {
let api_err = ApiError::BadRequest(format!("invalid query parameter: {err}"));
actix_web::error::InternalError::from_response(err, api_err.error_response()).into()
}));
}
pub(crate) fn configure_app(cfg: &mut web::ServiceConfig, state: AppState) {
#[allow(unused_mut)]
let mut openapi = ApiDoc::openapi();
#[cfg(feature = "experimental")]
{
let experimental = ExperimentalApiDoc::openapi();
openapi.merge(experimental);
}
cfg.configure(configure_error_handlers)
.app_data(web::Data::new(state))
.configure(configure_routes)
.service(SwaggerUi::new("/docs/{_:.*}").url("/api-docs/openapi.json", openapi))
.default_service(web::to(|| async {
let body = ErrorResponse::new("not found".into(), "NOT_FOUND".into());
HttpResponse::NotFound().json(body)
}));
}