diff --git a/swagger.yml b/swagger.yml index f49110da6..90aa222cc 100644 --- a/swagger.yml +++ b/swagger.yml @@ -645,6 +645,40 @@ paths: name: type: string enum: [unknown, timeout, success, fail] + /api/v1/block/height/{value}: + get: + summary: Get the block by height + parameters: + - in: path + name: value + schema: + type: number + required: true + description: Block height + responses: + '200': + description: Block info + content: + application/json: + schema: + $ref: '#/components/schemas/Block' + /api/v1/block/timestamp/{value}: + get: + summary: Get the block by timestamp + parameters: + - in: path + name: value + schema: + type: number + required: true + description: Block timestamp + responses: + '200': + description: Block info + content: + application/json: + schema: + $ref: '#/components/schemas/Block' /api/v1/crawlers/timestamps: get: summary: Get timestamps of the last activity of the crawlers @@ -981,3 +1015,19 @@ components: type: string data: type: string + Block: + type: object + required: [height] + properties: + height: + type: string + hash: + type: string + appHash: + type: string + timestamp: + type: string + proposer: + type: string + epoch: + type: string diff --git a/webserver/Cargo.toml b/webserver/Cargo.toml index 39fcb69fa..5968f2f1a 100644 --- a/webserver/Cargo.toml +++ b/webserver/Cargo.toml @@ -22,6 +22,7 @@ production = [] [dependencies] axum.workspace = true +chrono.workspace = true tokio.workspace = true tower.workspace = true tower-http.workspace = true diff --git a/webserver/src/app.rs b/webserver/src/app.rs index fdbda7c5c..1dc8357ff 100644 --- a/webserver/src/app.rs +++ b/webserver/src/app.rs @@ -19,10 +19,11 @@ use tower_http::trace::TraceLayer; use crate::appstate::AppState; use crate::config::AppConfig; use crate::handler::{ - balance as balance_handlers, chain as chain_handlers, - crawler_state as crawler_state_handlers, gas as gas_handlers, - governance as gov_handlers, ibc as ibc_handler, pk as pk_handlers, - pos as pos_handlers, transaction as transaction_handlers, + balance as balance_handlers, block as block_handlers, + chain as chain_handlers, crawler_state as crawler_state_handlers, + gas as gas_handlers, governance as gov_handlers, ibc as ibc_handler, + pk as pk_handlers, pos as pos_handlers, + transaction as transaction_handlers, }; use crate::state::common::CommonState; @@ -136,6 +137,14 @@ impl ApplicationServer { ) // Server sent events endpoints .route("/chain/status", get(chain_handlers::chain_status)) + .route( + "/block/height/:value", + get(block_handlers::get_block_by_height), + ) + .route( + "/block/timestamp/:value", + get(block_handlers::get_block_by_timestamp), + ) .route( "/metrics", get(|| async move { metric_handle.render() }), diff --git a/webserver/src/error/api.rs b/webserver/src/error/api.rs index 1d0045a7c..4a826564f 100644 --- a/webserver/src/error/api.rs +++ b/webserver/src/error/api.rs @@ -2,6 +2,7 @@ use axum::response::{IntoResponse, Response}; use thiserror::Error; use super::balance::BalanceError; +use super::block::BlockError; use super::chain::ChainError; use super::crawler_state::CrawlerStateError; use super::gas::GasError; @@ -13,6 +14,8 @@ use super::transaction::TransactionError; #[derive(Error, Debug)] pub enum ApiError { + #[error(transparent)] + BlockError(#[from] BlockError), #[error(transparent)] TransactionError(#[from] TransactionError), #[error(transparent)] @@ -36,6 +39,7 @@ pub enum ApiError { impl IntoResponse for ApiError { fn into_response(self) -> Response { match self { + ApiError::BlockError(error) => error.into_response(), ApiError::TransactionError(error) => error.into_response(), ApiError::ChainError(error) => error.into_response(), ApiError::PoSError(error) => error.into_response(), diff --git a/webserver/src/error/block.rs b/webserver/src/error/block.rs new file mode 100644 index 000000000..43648a432 --- /dev/null +++ b/webserver/src/error/block.rs @@ -0,0 +1,28 @@ +use axum::http::StatusCode; +use axum::response::{IntoResponse, Response}; +use thiserror::Error; + +use crate::response::api::ApiErrorResponse; + +#[derive(Error, Debug)] +pub enum BlockError { + #[error("Block not found error at {0}: {1}")] + NotFound(String, String), + #[error("Database error: {0}")] + Database(String), + #[error("Unknown error: {0}")] + Unknown(String), +} + +impl IntoResponse for BlockError { + fn into_response(self) -> Response { + let status_code = match self { + BlockError::Unknown(_) | BlockError::Database(_) => { + StatusCode::INTERNAL_SERVER_ERROR + } + BlockError::NotFound(_, _) => StatusCode::NOT_FOUND, + }; + + ApiErrorResponse::send(status_code.as_u16(), Some(self.to_string())) + } +} diff --git a/webserver/src/error/mod.rs b/webserver/src/error/mod.rs index 67b033aa3..e41db3de2 100644 --- a/webserver/src/error/mod.rs +++ b/webserver/src/error/mod.rs @@ -1,5 +1,6 @@ pub mod api; pub mod balance; +pub mod block; pub mod chain; pub mod crawler_state; pub mod gas; diff --git a/webserver/src/handler/block.rs b/webserver/src/handler/block.rs new file mode 100644 index 000000000..ed4bb01b3 --- /dev/null +++ b/webserver/src/handler/block.rs @@ -0,0 +1,30 @@ +use axum::extract::{Path, State}; +use axum::http::HeaderMap; +use axum::Json; +use axum_macros::debug_handler; + +use crate::error::api::ApiError; +use crate::response::block::Block; +use crate::state::common::CommonState; + +#[debug_handler] +pub async fn get_block_by_height( + _headers: HeaderMap, + Path(value): Path, + State(state): State, +) -> Result, ApiError> { + let block = state.block_service.get_block_by_height(value).await?; + + Ok(Json(block)) +} + +#[debug_handler] +pub async fn get_block_by_timestamp( + _headers: HeaderMap, + Path(value): Path, + State(state): State, +) -> Result, ApiError> { + let block = state.block_service.get_block_by_timestamp(value).await?; + + Ok(Json(block)) +} diff --git a/webserver/src/handler/mod.rs b/webserver/src/handler/mod.rs index 87f96ddbc..f48188102 100644 --- a/webserver/src/handler/mod.rs +++ b/webserver/src/handler/mod.rs @@ -1,4 +1,5 @@ pub mod balance; +pub mod block; pub mod chain; pub mod crawler_state; pub mod gas; diff --git a/webserver/src/repository/block.rs b/webserver/src/repository/block.rs new file mode 100644 index 000000000..cb13f4ee1 --- /dev/null +++ b/webserver/src/repository/block.rs @@ -0,0 +1,72 @@ +use axum::async_trait; +use diesel::{ExpressionMethods, QueryDsl, RunQueryDsl, SelectableHelper}; +use orm::blocks::BlockDb; +use orm::schema::blocks; + +use crate::appstate::AppState; + +#[derive(Clone)] +pub struct BlockRepository { + pub(crate) app_state: AppState, +} + +#[async_trait] +pub trait BlockRepositoryTrait { + fn new(app_state: AppState) -> Self; + + async fn find_block_by_height( + &self, + height: i32, + ) -> Result, String>; + + async fn find_block_by_timestamp( + &self, + timestamp: i64, + ) -> Result, String>; +} + +#[async_trait] +impl BlockRepositoryTrait for BlockRepository { + fn new(app_state: AppState) -> Self { + Self { app_state } + } + + async fn find_block_by_height( + &self, + height: i32, + ) -> Result, String> { + let conn = self.app_state.get_db_connection().await; + + conn.interact(move |conn| { + blocks::table + .filter(blocks::dsl::height.eq(height)) + .select(BlockDb::as_select()) + .first(conn) + .ok() + }) + .await + .map_err(|e| e.to_string()) + } + + /// Gets the last block preceeding the given timestamp + async fn find_block_by_timestamp( + &self, + timestamp: i64, + ) -> Result, String> { + let conn = self.app_state.get_db_connection().await; + let timestamp = chrono::DateTime::from_timestamp(timestamp, 0) + .expect("Invalid timestamp") + .naive_utc(); + + conn.interact(move |conn| { + blocks::table + .filter(blocks::timestamp.le(timestamp)) + .order(blocks::timestamp.desc()) + .select(BlockDb::as_select()) + .first(conn) + .ok() + }) + .await + .map_err(|e| e.to_string()) + } +} diff --git a/webserver/src/repository/mod.rs b/webserver/src/repository/mod.rs index 0db518011..448aefb63 100644 --- a/webserver/src/repository/mod.rs +++ b/webserver/src/repository/mod.rs @@ -1,4 +1,5 @@ pub mod balance; +pub mod block; pub mod chain; pub mod gas; pub mod governance; diff --git a/webserver/src/response/block.rs b/webserver/src/response/block.rs new file mode 100644 index 000000000..3ab9b51c0 --- /dev/null +++ b/webserver/src/response/block.rs @@ -0,0 +1,28 @@ +use orm::blocks::BlockDb; +use serde::{Deserialize, Serialize}; + +#[derive(Clone, Debug, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct Block { + pub height: i32, + pub hash: Option, + pub app_hash: Option, + pub timestamp: Option, + pub proposer: Option, + pub epoch: Option, +} + +impl From for Block { + fn from(block_db: BlockDb) -> Self { + Self { + height: block_db.height, + hash: block_db.hash, + app_hash: block_db.app_hash, + timestamp: block_db + .timestamp + .map(|t| t.and_utc().timestamp().to_string()), + proposer: block_db.proposer, + epoch: block_db.epoch.map(|e| e.to_string()), + } + } +} diff --git a/webserver/src/response/mod.rs b/webserver/src/response/mod.rs index 676be021e..980eb325d 100644 --- a/webserver/src/response/mod.rs +++ b/webserver/src/response/mod.rs @@ -1,5 +1,6 @@ pub mod api; pub mod balance; +pub mod block; pub mod chain; pub mod crawler_state; pub mod gas; diff --git a/webserver/src/service/block.rs b/webserver/src/service/block.rs new file mode 100644 index 000000000..54c7d73e9 --- /dev/null +++ b/webserver/src/service/block.rs @@ -0,0 +1,52 @@ +use crate::appstate::AppState; +use crate::error::block::BlockError; +use crate::repository::block::{BlockRepository, BlockRepositoryTrait}; +use crate::response::block::Block; + +#[derive(Clone)] +pub struct BlockService { + block_repo: BlockRepository, +} + +impl BlockService { + pub fn new(app_state: AppState) -> Self { + Self { + block_repo: BlockRepository::new(app_state), + } + } + + pub async fn get_block_by_height( + &self, + height: i32, + ) -> Result { + let block = self + .block_repo + .find_block_by_height(height) + .await + .map_err(BlockError::Database)?; + let block = block.ok_or(BlockError::NotFound( + "height".to_string(), + height.to_string(), + ))?; + + Ok(Block::from(block)) + } + + pub async fn get_block_by_timestamp( + &self, + timestamp: i64, + ) -> Result { + let block = self + .block_repo + .find_block_by_timestamp(timestamp) + .await + .map_err(BlockError::Database)?; + + let block = block.ok_or(BlockError::NotFound( + "timestamp".to_string(), + timestamp.to_string(), + ))?; + + Ok(Block::from(block)) + } +} diff --git a/webserver/src/service/mod.rs b/webserver/src/service/mod.rs index a5566429a..1178a89f7 100644 --- a/webserver/src/service/mod.rs +++ b/webserver/src/service/mod.rs @@ -1,4 +1,5 @@ pub mod balance; +pub mod block; pub mod chain; pub mod crawler_state; pub mod gas; diff --git a/webserver/src/state/common.rs b/webserver/src/state/common.rs index 8c2ff5703..f4a9363bd 100644 --- a/webserver/src/state/common.rs +++ b/webserver/src/state/common.rs @@ -3,6 +3,7 @@ use namada_sdk::tendermint_rpc::HttpClient; use crate::appstate::AppState; use crate::config::AppConfig; use crate::service::balance::BalanceService; +use crate::service::block::BlockService; use crate::service::chain::ChainService; use crate::service::crawler_state::CrawlerStateService; use crate::service::gas::GasService; @@ -15,6 +16,7 @@ use crate::service::transaction::TransactionService; #[derive(Clone)] pub struct CommonState { pub pos_service: PosService, + pub block_service: BlockService, pub gov_service: GovernanceService, pub balance_service: BalanceService, pub chain_service: ChainService, @@ -30,6 +32,7 @@ pub struct CommonState { impl CommonState { pub fn new(client: HttpClient, config: AppConfig, data: AppState) -> Self { Self { + block_service: BlockService::new(data.clone()), pos_service: PosService::new(data.clone()), gov_service: GovernanceService::new(data.clone()), balance_service: BalanceService::new(data.clone()),