diff --git a/src/html/page/block.rs b/src/html/page/block.rs index 1cf23b8..cfce7d0 100644 --- a/src/html/page/block.rs +++ b/src/html/page/block.rs @@ -1,7 +1,7 @@ use crate::html::component::header::HeaderHtml; use crate::html::page::not_found::not_found_html_response; use crate::model::app_state::AppState; -use crate::model::path_block_selector::PathBlockSelector; +use crate::model::block_selector_extended::BlockSelectorExtended; use axum::extract::rejection::PathRejection; use axum::extract::Path; use axum::extract::State; @@ -13,20 +13,9 @@ use neptune_core::models::blockchain::block::block_info::BlockInfo; use std::sync::Arc; use tarpc::context; -pub async fn block_page( - user_input_maybe: Result, PathRejection>, - state: State>, -) -> Result, Response> { - let Path(path_block_selector) = user_input_maybe - .map_err(|e| not_found_html_response(state.clone(), Some(e.to_string())))?; - - let value_path: Path<(PathBlockSelector, String)> = Path((path_block_selector, "".to_string())); - block_page_with_value(Ok(value_path), state).await -} - #[axum::debug_handler] -pub async fn block_page_with_value( - user_input_maybe: Result, PathRejection>, +pub async fn block_page( + user_input_maybe: Result, PathRejection>, State(state): State>, ) -> Result, Response> { #[derive(boilerplate::Boilerplate)] @@ -36,21 +25,17 @@ pub async fn block_page_with_value( block_info: BlockInfo, } - let Path((path_block_selector, value)) = user_input_maybe + let Path(block_selector) = user_input_maybe .map_err(|e| not_found_html_response(State(state.clone()), Some(e.to_string())))?; let header = HeaderHtml { state: state.clone(), }; - let block_selector = path_block_selector - .as_block_selector(&value) - .map_err(|e| not_found_html_response(State(state.clone()), Some(e.to_string())))?; - let block_info = match state .clone() .rpc_client - .block_info(context::current(), block_selector) + .block_info(context::current(), block_selector.into()) .await .map_err(|e| not_found_html_response(State(state.clone()), Some(e.to_string())))? { diff --git a/src/main.rs b/src/main.rs index da3bde3..6e49700 100644 --- a/src/main.rs +++ b/src/main.rs @@ -4,7 +4,6 @@ use clap::Parser; use neptune_core::models::blockchain::block::block_selector::BlockSelector; use neptune_core::rpc_server::RPCClient; use neptune_explorer::html::page::block::block_page; -use neptune_explorer::html::page::block::block_page_with_value; use neptune_explorer::html::page::not_found::not_found_html_fallback; use neptune_explorer::html::page::redirect_qs_to_path::redirect_query_string_to_path; use neptune_explorer::html::page::root::root; @@ -12,9 +11,7 @@ use neptune_explorer::html::page::utxo::utxo_page; use neptune_explorer::model::app_state::AppState; use neptune_explorer::model::config::Config; use neptune_explorer::rpc::block_digest::block_digest; -use neptune_explorer::rpc::block_digest::block_digest_with_value; use neptune_explorer::rpc::block_info::block_info; -use neptune_explorer::rpc::block_info::block_info_with_value; use neptune_explorer::rpc::utxo_digest::utxo_digest; use std::net::Ipv4Addr; use std::net::SocketAddr; @@ -43,21 +40,12 @@ async fn main() -> Result<(), RpcError> { let app = Router::new() // -- RPC calls -- - .route("/rpc/block_info/:selector", get(block_info)) - .route( - "/rpc/block_info/:selector/:value", - get(block_info_with_value), - ) - .route( - "/rpc/block_digest/:selector/:value", - get(block_digest_with_value), - ) - .route("/rpc/block_digest/:selector", get(block_digest)) + .route("/rpc/block_info/*selector", get(block_info)) + .route("/rpc/block_digest/*selector", get(block_digest)) .route("/rpc/utxo_digest/:index", get(utxo_digest)) // -- Dynamic HTML pages -- .route("/", get(root)) - .route("/block/:selector", get(block_page)) - .route("/block/:selector/:value", get(block_page_with_value)) + .route("/block/*selector", get(block_page)) .route("/utxo/:value", get(utxo_page)) // -- Rewrite query-strings to path -- .route("/rqs", get(redirect_query_string_to_path)) diff --git a/src/model/block_selector_extended.rs b/src/model/block_selector_extended.rs new file mode 100644 index 0000000..4f79dfe --- /dev/null +++ b/src/model/block_selector_extended.rs @@ -0,0 +1,71 @@ +use super::height_or_digest::HeightOrDigest; +use neptune_core::models::blockchain::block::block_selector::BlockSelector; +use neptune_core::models::blockchain::block::block_selector::BlockSelectorParseError; +use serde::de::Error; +use serde::Deserialize; +use serde::Deserializer; +use std::str::FromStr; + +/// newtype for `BlockSelector` that provides ability to parse `height_or_digest/value`. +/// +/// This is useful for HTML form(s) that allow user to enter either height or +/// digest into the same text input field. +/// +/// In particular it is necessary to support javascript-free website with such +/// an html form. +#[derive(Debug, Clone)] +pub struct BlockSelectorExtended(BlockSelector); + +impl std::fmt::Display for BlockSelectorExtended { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.0) + } +} + +impl FromStr for BlockSelectorExtended { + type Err = BlockSelectorParseError; + + // note: this parses BlockSelector, plus height_or_digest/ + fn from_str(s: &str) -> Result { + match BlockSelector::from_str(s) { + Ok(bs) => Ok(Self::from(bs)), + Err(e) => { + let parts: Vec<_> = s.split('/').collect(); + if parts.len() == 2 && parts[0] == "height_or_digest" { + Ok(Self::from(HeightOrDigest::from_str(parts[1])?)) + } else { + Err(e) + } + } + } + } +} + +// note: axum uses serde Deserialize for Path elements. +impl<'de> Deserialize<'de> for BlockSelectorExtended { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + let s = String::deserialize(deserializer)?; + Self::from_str(&s).map_err(D::Error::custom) + } +} + +impl From for BlockSelectorExtended { + fn from(hd: HeightOrDigest) -> Self { + Self(hd.into()) + } +} + +impl From for BlockSelectorExtended { + fn from(v: BlockSelector) -> Self { + Self(v) + } +} + +impl From for BlockSelector { + fn from(v: BlockSelectorExtended) -> Self { + v.0 + } +} diff --git a/src/model/height_or_digest.rs b/src/model/height_or_digest.rs new file mode 100644 index 0000000..92bc9d5 --- /dev/null +++ b/src/model/height_or_digest.rs @@ -0,0 +1,47 @@ +use neptune_core::models::blockchain::block::block_height::BlockHeight; +use neptune_core::models::blockchain::block::block_selector::BlockSelector; +use neptune_core::models::blockchain::block::block_selector::BlockSelectorParseError; +use neptune_core::prelude::tasm_lib::Digest; +use serde::{Deserialize, Serialize}; +use std::str::FromStr; + +/// represents either a block-height or a block digest +#[derive(Debug, Clone, Serialize, Deserialize)] +pub enum HeightOrDigest { + /// Identifies block by Digest (hash) + Digest(Digest), + /// Identifies block by Height (count from genesis) + Height(BlockHeight), +} + +impl std::fmt::Display for HeightOrDigest { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::Digest(d) => write!(f, "{}", d), + Self::Height(h) => write!(f, "{}", h), + } + } +} + +impl FromStr for HeightOrDigest { + type Err = BlockSelectorParseError; + + // note: this parses the output of impl Display for HeightOrDigest + // note: this is used by clap parser in neptune-cli for block-info command + // and probably future commands as well. + fn from_str(s: &str) -> Result { + Ok(match s.parse::() { + Ok(h) => Self::Height(h.into()), + Err(_) => Self::Digest(Digest::try_from_hex(s)?), + }) + } +} + +impl From for BlockSelector { + fn from(hd: HeightOrDigest) -> Self { + match hd { + HeightOrDigest::Height(h) => Self::Height(h), + HeightOrDigest::Digest(d) => Self::Digest(d), + } + } +} diff --git a/src/model/mod.rs b/src/model/mod.rs index ab761ae..a3c8ed6 100644 --- a/src/model/mod.rs +++ b/src/model/mod.rs @@ -1,3 +1,4 @@ pub mod app_state; +pub mod block_selector_extended; pub mod config; -pub mod path_block_selector; +pub mod height_or_digest; diff --git a/src/model/path_block_selector.rs b/src/model/path_block_selector.rs deleted file mode 100644 index 12159ba..0000000 --- a/src/model/path_block_selector.rs +++ /dev/null @@ -1,73 +0,0 @@ -use axum::http::StatusCode; -use axum::response::IntoResponse; -use axum::response::Response; -use neptune_core::models::blockchain::block::block_height::BlockHeight; -use neptune_core::models::blockchain::block::block_selector::BlockSelector; -use neptune_core::prelude::tasm_lib::Digest; -use neptune_core::prelude::twenty_first::error::TryFromHexDigestError; -use serde::{Deserialize, Serialize}; -use std::str::FromStr; - -#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)] -pub enum PathBlockSelector { - #[serde(rename = "genesis")] - Genesis, - #[serde(rename = "tip")] - Tip, - #[serde(rename = "digest")] - Digest, - #[serde(rename = "height")] - Height, - #[serde(rename = "height_or_digest")] - HeightOrDigest, -} - -#[derive(thiserror::Error, Debug)] -pub enum PathBlockSelectorError { - #[error("Genesis does not accept an argument")] - GenesisNoArg, - - #[error("Tip does not accept an argument")] - TipNoArg, - - #[error("Digest could not be parsed")] - DigestNotParsed(#[from] TryFromHexDigestError), - - #[error("Height could not be parsed")] - HeightNotParsed(#[from] std::num::ParseIntError), -} -impl PathBlockSelectorError { - fn as_response_tuple(&self) -> (StatusCode, String) { - (StatusCode::NOT_FOUND, self.to_string()) - } -} -impl IntoResponse for PathBlockSelectorError { - fn into_response(self) -> Response { - self.as_response_tuple().into_response() - } -} -impl From for Response { - fn from(e: PathBlockSelectorError) -> Response { - e.as_response_tuple().into_response() - } -} -impl PathBlockSelector { - pub fn as_block_selector(&self, value: &str) -> Result { - match self { - PathBlockSelector::Genesis if !value.is_empty() => { - Err(PathBlockSelectorError::GenesisNoArg) - } - PathBlockSelector::Genesis => Ok(BlockSelector::Genesis), - PathBlockSelector::Tip if !value.is_empty() => Err(PathBlockSelectorError::TipNoArg), - PathBlockSelector::Tip => Ok(BlockSelector::Tip), - PathBlockSelector::Digest => Ok(BlockSelector::Digest(Digest::try_from_hex(value)?)), - PathBlockSelector::Height => Ok(BlockSelector::Height(BlockHeight::from( - u64::from_str(value)?, - ))), - PathBlockSelector::HeightOrDigest => Ok(match u64::from_str(value) { - Ok(height) => BlockSelector::Height(BlockHeight::from(height)), - Err(_) => BlockSelector::Digest(Digest::try_from_hex(value)?), - }), - } - } -} diff --git a/src/rpc/block_digest.rs b/src/rpc/block_digest.rs index a9b6c13..07f8bad 100644 --- a/src/rpc/block_digest.rs +++ b/src/rpc/block_digest.rs @@ -1,3 +1,7 @@ +use crate::http_util::not_found_err; +use crate::http_util::rpc_err; +use crate::model::app_state::AppState; +use crate::model::block_selector_extended::BlockSelectorExtended; use axum::extract::Path; use axum::extract::State; use axum::response::IntoResponse; @@ -6,37 +10,14 @@ use neptune_core::prelude::twenty_first::math::digest::Digest; use std::sync::Arc; use tarpc::context; -use crate::{ - http_util::{not_found_err, rpc_err}, - model::{app_state::AppState, path_block_selector::PathBlockSelector}, -}; - #[axum::debug_handler] pub async fn block_digest( - Path(path_block_selector): Path, + Path(selector): Path, State(state): State>, ) -> Result, impl IntoResponse> { - block_digest_with_value_worker(state, path_block_selector, "").await -} - -#[axum::debug_handler] -pub async fn block_digest_with_value( - Path((path_block_selector, value)): Path<(PathBlockSelector, String)>, - State(state): State>, -) -> Result, impl IntoResponse> { - block_digest_with_value_worker(state, path_block_selector, &value).await -} - -async fn block_digest_with_value_worker( - state: Arc, - path_block_selector: PathBlockSelector, - value: &str, -) -> Result, impl IntoResponse> { - let block_selector = path_block_selector.as_block_selector(value)?; - match state .rpc_client - .block_digest(context::current(), block_selector) + .block_digest(context::current(), selector.into()) .await .map_err(rpc_err)? { diff --git a/src/rpc/block_info.rs b/src/rpc/block_info.rs index d8cdbc5..02dd181 100644 --- a/src/rpc/block_info.rs +++ b/src/rpc/block_info.rs @@ -1,3 +1,7 @@ +use crate::http_util::not_found_err; +use crate::http_util::rpc_err; +use crate::model::app_state::AppState; +use crate::model::block_selector_extended::BlockSelectorExtended; use axum::extract::Path; use axum::extract::State; use axum::response::Json; @@ -6,43 +10,17 @@ use neptune_core::models::blockchain::block::block_info::BlockInfo; use std::sync::Arc; use tarpc::context; -use crate::{ - http_util::{not_found_err, rpc_err}, - model::{app_state::AppState, path_block_selector::PathBlockSelector}, -}; - #[axum::debug_handler] pub async fn block_info( - Path(path_block_selector): Path, + Path(selector): Path, State(state): State>, ) -> Result, Response> { - let block_info = block_info_with_value_worker(state, path_block_selector, "").await?; - Ok(Json(block_info)) -} - -#[axum::debug_handler] -pub async fn block_info_with_value( - Path((path_block_selector, value)): Path<(PathBlockSelector, String)>, - State(state): State>, -) -> Result, Response> { - let block_info = block_info_with_value_worker(state, path_block_selector, &value).await?; - Ok(Json(block_info)) -} - -pub(crate) async fn block_info_with_value_worker( - state: Arc, - path_block_selector: PathBlockSelector, - value: &str, -) -> Result { - let block_selector = path_block_selector.as_block_selector(value)?; - - match state + let block_info = state .rpc_client - .block_info(context::current(), block_selector) + .block_info(context::current(), selector.into()) .await .map_err(rpc_err)? - { - Some(info) => Ok(info), - None => Err(not_found_err()), - } + .ok_or_else(not_found_err)?; + + Ok(Json(block_info)) }