diff --git a/Cargo.lock b/Cargo.lock index d864e48..875ac32 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1812,6 +1812,7 @@ dependencies = [ "clap", "html-escaper", "neptune-core", + "readonly", "serde", "serde_json", "tarpc", @@ -2443,6 +2444,17 @@ dependencies = [ "crossbeam-utils", ] +[[package]] +name = "readonly" +version = "0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a25d631e41bfb5fdcde1d4e2215f62f7f0afa3ff11e26563765bd6ea1d229aeb" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.58", +] + [[package]] name = "redox_syscall" version = "0.4.1" diff --git a/Cargo.toml b/Cargo.toml index 7763706..48e9fc8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -25,6 +25,7 @@ thiserror = "1.0.58" boilerplate = { version = "1.0.0" } html-escaper = "0.2.0" tower-http = { version = "0.5.2", features = ["fs"] } +readonly = "0.2.12" [patch.crates-io] diff --git a/src/html/component/header.rs b/src/html/component/header.rs new file mode 100644 index 0000000..2e07400 --- /dev/null +++ b/src/html/component/header.rs @@ -0,0 +1,10 @@ +use crate::model::app_state::AppState; +use html_escaper::Escape; +use std::sync::Arc; + +#[derive(boilerplate::Boilerplate)] +#[boilerplate(filename = "web/html/components/header.html")] +pub struct HeaderHtml { + pub site_name: String, + pub state: Arc, +} diff --git a/src/html/component/mod.rs b/src/html/component/mod.rs new file mode 100644 index 0000000..f505d68 --- /dev/null +++ b/src/html/component/mod.rs @@ -0,0 +1 @@ +pub mod header; diff --git a/src/html/mod.rs b/src/html/mod.rs new file mode 100644 index 0000000..2895b1d --- /dev/null +++ b/src/html/mod.rs @@ -0,0 +1,2 @@ +pub mod component; +pub mod page; diff --git a/src/html/page/block.rs b/src/html/page/block.rs new file mode 100644 index 0000000..fefe41a --- /dev/null +++ b/src/html/page/block.rs @@ -0,0 +1,42 @@ +use crate::html::component::header::HeaderHtml; +use crate::model::app_state::AppState; +use crate::model::path_block_selector::PathBlockSelector; +use crate::rpc::block_info::block_info_with_value_worker; +use axum::extract::Path; +use axum::extract::State; +use axum::response::Html; +use axum::response::Response; +use html_escaper::Escape; +use html_escaper::Trusted; +use neptune_core::rpc_server::BlockInfo; +use std::sync::Arc; + +pub async fn block_page( + Path(path_block_selector): Path, + state: State>, +) -> Result, Response> { + let value_path: Path<(PathBlockSelector, String)> = Path((path_block_selector, "".to_string())); + block_page_with_value(value_path, state).await +} + +#[axum::debug_handler] +pub async fn block_page_with_value( + Path((path_block_selector, value)): Path<(PathBlockSelector, String)>, + State(state): State>, +) -> Result, Response> { + #[derive(boilerplate::Boilerplate)] + #[boilerplate(filename = "web/html/page/block_info.html")] + pub struct BlockInfoHtmlPage { + header: HeaderHtml, + block_info: BlockInfo, + } + + let header = HeaderHtml { + site_name: "Neptune Explorer".to_string(), + state: state.clone(), + }; + + let block_info = block_info_with_value_worker(state, path_block_selector, &value).await?; + let block_info_page = BlockInfoHtmlPage { header, block_info }; + Ok(Html(block_info_page.to_string())) +} diff --git a/src/html/page/mod.rs b/src/html/page/mod.rs new file mode 100644 index 0000000..a705c02 --- /dev/null +++ b/src/html/page/mod.rs @@ -0,0 +1,3 @@ +pub mod block; +pub mod root; +pub mod utxo; diff --git a/src/html/page/root.rs b/src/html/page/root.rs new file mode 100644 index 0000000..7516c98 --- /dev/null +++ b/src/html/page/root.rs @@ -0,0 +1,22 @@ +use crate::model::app_state::AppState; +use axum::extract::State; +use axum::response::Html; +use html_escaper::Escape; +use std::ops::Deref; +use std::sync::Arc; + +#[axum::debug_handler] +pub async fn root(State(state): State>) -> Html { + #[derive(boilerplate::Boilerplate)] + #[boilerplate(filename = "web/html/page/root.html")] + pub struct RootHtmlPage(Arc); + impl Deref for RootHtmlPage { + type Target = AppState; + fn deref(&self) -> &Self::Target { + &self.0 + } + } + + let root_page = RootHtmlPage(state); + Html(root_page.to_string()) +} diff --git a/src/html/page/utxo.rs b/src/html/page/utxo.rs new file mode 100644 index 0000000..7a25926 --- /dev/null +++ b/src/html/page/utxo.rs @@ -0,0 +1,49 @@ +use crate::html::component::header::HeaderHtml; +use crate::http_util::not_found_err; +use crate::http_util::rpc_err; +use crate::model::app_state::AppState; +use axum::extract::Path; +use axum::extract::State; +use axum::response::Html; +use axum::response::Response; +use html_escaper::Escape; +use html_escaper::Trusted; +use neptune_core::prelude::tasm_lib::Digest; +use std::sync::Arc; +use tarpc::context; + +#[axum::debug_handler] +pub async fn utxo_page( + Path(index): Path, + State(state): State>, +) -> Result, Response> { + #[derive(boilerplate::Boilerplate)] + #[boilerplate(filename = "web/html/page/utxo.html")] + pub struct UtxoHtmlPage { + header: HeaderHtml, + index: u64, + digest: Digest, + } + + let digest = match state + .rpc_client + .utxo_digest(context::current(), index) + .await + .map_err(rpc_err)? + { + Some(digest) => digest, + None => return Err(not_found_err()), + }; + + let header = HeaderHtml { + site_name: "Neptune Explorer".to_string(), + state: state.clone(), + }; + + let utxo_page = UtxoHtmlPage { + index, + header, + digest, + }; + Ok(Html(utxo_page.to_string())) +} diff --git a/src/http_util.rs b/src/http_util.rs new file mode 100644 index 0000000..975ba3e --- /dev/null +++ b/src/http_util.rs @@ -0,0 +1,15 @@ +use axum::http::StatusCode; +use axum::response::IntoResponse; +use axum::response::Response; +use tarpc::client::RpcError; + +// note: http StatusCodes are defined at: +// https://docs.rs/http/1.1.0/http/status/struct.StatusCode.html + +pub fn not_found_err() -> Response { + (StatusCode::NOT_FOUND, "Not Found".to_string()).into_response() +} + +pub fn rpc_err(e: RpcError) -> Response { + (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()).into_response() +} diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..55b45af --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,4 @@ +pub mod html; +pub mod http_util; +pub mod model; +pub mod rpc; diff --git a/src/main.rs b/src/main.rs index deb3d0f..2703fcd 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,328 +1,70 @@ -use axum::{ - extract::{Path, State}, - http::StatusCode, - response::{Html, IntoResponse, Response}, - routing::get, - Json, Router, -}; -use tower_http::{ - services::ServeFile, -}; +use axum::routing::get; +use axum::routing::Router; use clap::Parser; -use serde::{Deserialize, Serialize}; -use std::str::FromStr; -use std::ops::Deref; -use thiserror::Error; -use html_escaper::{Escape, Trusted}; - -use neptune_core::config_models::network::Network; -use neptune_core::models::blockchain::block::block_height::BlockHeight; -use neptune_core::rpc_server::{BlockInfo, BlockSelector, RPCClient}; -use neptune_core::prelude::twenty_first::error::TryFromHexDigestError; -use neptune_core::prelude::twenty_first::math::digest::Digest; -use std::net::{Ipv4Addr, SocketAddr}; +use neptune_core::rpc_server::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::root::root; +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; use std::sync::Arc; +use tarpc::client; use tarpc::client::RpcError; +use tarpc::context; use tarpc::tokio_serde::formats::Json as RpcJson; -use tarpc::{client, context}; - -// note: http StatusCodes are defined at: -// https://docs.rs/http/1.1.0/http/status/struct.StatusCode.html - -#[derive(Debug, clap::Parser, Clone)] -#[clap(name = "neptune-explorer", about = "Neptune Block Explorer")] -pub struct Config { - /// Sets the server address to connect to. - #[clap(long, default_value = "9799", value_name = "PORT")] - port: u16, -} - -pub struct AppState { - network: Network, - #[allow(dead_code)] - config: Config, - rpc_client: RPCClient, - genesis_digest: Digest, -} - -#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)] -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(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() - } -} - -fn not_found_err() -> Response { - (StatusCode::NOT_FOUND, "Not Found".to_string()).into_response() -} -fn rpc_err(e: RpcError) -> Response { - (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()).into_response() -} - -impl PathBlockSelector { - 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)?), - }) - } - } - } -} - -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) - .await - .map_err(rpc_err)? - { - Some(digest) => Ok(Json(digest)), - None => Err(not_found_err()), - } -} - -#[axum::debug_handler] -async fn block_digest( - Path(path_block_selector): Path, - State(state): State>, -) -> Result, impl IntoResponse> { - block_digest_with_value_worker(state, path_block_selector, "").await -} - -#[axum::debug_handler] -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_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 - .rpc_client - .block_info(context::current(), block_selector) - .await - .map_err(rpc_err)? - { - Some(info) => Ok(info), - None => Err(not_found_err()), - } -} - -#[axum::debug_handler] -async fn block_info( - Path(path_block_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] -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)) -} - -#[axum::debug_handler] -async fn utxo_digest( - Path(index): Path, - State(state): State>, -) -> Result, impl IntoResponse> { - match state - .rpc_client - .utxo_digest(context::current(), index) - .await - .map_err(rpc_err)? - { - Some(digest) => Ok(Json(digest)), - None => Err(not_found_err()), - } -} - -#[axum::debug_handler] -async fn root( - State(state): State>, -) -> Html { - - #[derive(boilerplate::Boilerplate)] - #[boilerplate(filename = "web/html/page/root.html")] - pub struct RootHtmlPage(Arc); - impl Deref for RootHtmlPage { - type Target = AppState; - fn deref(&self) -> &Self::Target { - &self.0 - } - } - - let root_page = RootHtmlPage(state); - Html(root_page.to_string()) -} - -async fn block_page( - Path(path_block_selector): Path, - state: State>, -) -> Result, Response> { - let value_path: Path<(PathBlockSelector, String)> = Path((path_block_selector, "".to_string())); - block_page_with_value(value_path, state).await -} - -#[derive(boilerplate::Boilerplate)] -#[boilerplate(filename = "web/html/components/header.html")] -pub struct HeaderHtml{ - site_name: String, - state: Arc, -} - -#[axum::debug_handler] -async fn block_page_with_value( - Path((path_block_selector, value)): Path<(PathBlockSelector, String)>, - State(state): State>, -) -> Result, Response> { - - #[derive(boilerplate::Boilerplate)] - #[boilerplate(filename = "web/html/page/block_info.html")] - pub struct BlockInfoHtmlPage{ - header: HeaderHtml, - block_info: BlockInfo - } - - let header = HeaderHtml{site_name: "Neptune Explorer".to_string(), state: state.clone()}; - - let block_info = block_info_with_value_worker(state, path_block_selector, &value).await?; - let block_info_page = BlockInfoHtmlPage{header, block_info}; - Ok(Html(block_info_page.to_string())) -} - - -#[axum::debug_handler] -async fn utxo_page( - Path(index): Path, - State(state): State>, -) -> Result, Response> { - - #[derive(boilerplate::Boilerplate)] - #[boilerplate(filename = "web/html/page/utxo.html")] - pub struct UtxoHtmlPage{ - header: HeaderHtml, - index: u64, - digest: Digest, - } - - let digest = match state - .rpc_client - .utxo_digest(context::current(), index) - .await - .map_err(rpc_err)? - { - Some(digest) => digest, - None => return Err(not_found_err()), - }; - - let header = HeaderHtml{site_name: "Neptune Explorer".to_string(), state: state.clone()}; - - let utxo_page = UtxoHtmlPage{index, header, digest}; - Ok(Html(utxo_page.to_string())) -} +use tower_http::services::ServeFile; #[tokio::main] async fn main() -> Result<(), RpcError> { let rpc_client = rpc_client().await; let network = rpc_client.network(context::current()).await?; - let genesis_digest = rpc_client.block_digest(context::current(), BlockSelector::Genesis).await?.expect("Genesis block should be found"); + let genesis_digest = rpc_client + .block_digest(context::current(), BlockSelector::Genesis) + .await? + .expect("Genesis block should be found"); - let shared_state = Arc::new(AppState { - rpc_client, - config: Config::parse(), + let shared_state = Arc::new(AppState::from(( network, + Config::parse(), + rpc_client, genesis_digest, - }); + ))); 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_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/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("/utxo/:value", get(utxo_page)) - // -- Static files -- - .route_service("/css/styles.css", ServeFile::new(concat!(env!("CARGO_MANIFEST_DIR"), "/src/web/css/styles.css"))) - + .route_service( + "/css/styles.css", + ServeFile::new(concat!( + env!("CARGO_MANIFEST_DIR"), + "/src/web/css/styles.css" + )), + ) // add state .with_state(shared_state); diff --git a/src/model/app_state.rs b/src/model/app_state.rs new file mode 100644 index 0000000..1c7b379 --- /dev/null +++ b/src/model/app_state.rs @@ -0,0 +1,25 @@ +use crate::model::config::Config; +use neptune_core::config_models::network::Network; +use neptune_core::prelude::twenty_first::math::digest::Digest; +use neptune_core::rpc_server::RPCClient; + +#[readonly::make] +pub struct AppState { + pub network: Network, + pub config: Config, + pub rpc_client: RPCClient, + pub genesis_digest: Digest, +} + +impl From<(Network, Config, RPCClient, Digest)> for AppState { + fn from( + (network, config, rpc_client, genesis_digest): (Network, Config, RPCClient, Digest), + ) -> Self { + Self { + network, + config, + rpc_client, + genesis_digest, + } + } +} diff --git a/src/model/config.rs b/src/model/config.rs new file mode 100644 index 0000000..e7cf162 --- /dev/null +++ b/src/model/config.rs @@ -0,0 +1,8 @@ +#[readonly::make] +#[derive(Debug, clap::Parser, Clone)] +#[clap(name = "neptune-explorer", about = "Neptune Block Explorer")] +pub struct Config { + /// Sets the server address to connect to. + #[clap(long, default_value = "9799", value_name = "PORT")] + pub port: u16, +} diff --git a/src/model/mod.rs b/src/model/mod.rs new file mode 100644 index 0000000..ab761ae --- /dev/null +++ b/src/model/mod.rs @@ -0,0 +1,3 @@ +pub mod app_state; +pub mod config; +pub mod path_block_selector; diff --git a/src/model/path_block_selector.rs b/src/model/path_block_selector.rs new file mode 100644 index 0000000..dfe9a1f --- /dev/null +++ b/src/model/path_block_selector.rs @@ -0,0 +1,73 @@ +use axum::http::StatusCode; +use axum::response::IntoResponse; +use axum::response::Response; +use neptune_core::models::blockchain::block::block_height::BlockHeight; +use neptune_core::prelude::tasm_lib::Digest; +use neptune_core::prelude::twenty_first::error::TryFromHexDigestError; +use neptune_core::rpc_server::BlockSelector; +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 new file mode 100644 index 0000000..a9b6c13 --- /dev/null +++ b/src/rpc/block_digest.rs @@ -0,0 +1,46 @@ +use axum::extract::Path; +use axum::extract::State; +use axum::response::IntoResponse; +use axum::response::Json; +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, + 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) + .await + .map_err(rpc_err)? + { + Some(digest) => Ok(Json(digest)), + None => Err(not_found_err()), + } +} diff --git a/src/rpc/block_info.rs b/src/rpc/block_info.rs new file mode 100644 index 0000000..cfe892b --- /dev/null +++ b/src/rpc/block_info.rs @@ -0,0 +1,48 @@ +use axum::extract::Path; +use axum::extract::State; +use axum::response::Json; +use axum::response::Response; +use neptune_core::rpc_server::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, + 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 + .rpc_client + .block_info(context::current(), block_selector) + .await + .map_err(rpc_err)? + { + Some(info) => Ok(info), + None => Err(not_found_err()), + } +} diff --git a/src/rpc/mod.rs b/src/rpc/mod.rs new file mode 100644 index 0000000..2d1a655 --- /dev/null +++ b/src/rpc/mod.rs @@ -0,0 +1,3 @@ +pub mod block_digest; +pub mod block_info; +pub mod utxo_digest; diff --git a/src/rpc/utxo_digest.rs b/src/rpc/utxo_digest.rs new file mode 100644 index 0000000..ffe664c --- /dev/null +++ b/src/rpc/utxo_digest.rs @@ -0,0 +1,28 @@ +use axum::extract::Path; +use axum::extract::State; +use axum::response::IntoResponse; +use axum::response::Json; +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, +}; + +#[axum::debug_handler] +pub async fn utxo_digest( + Path(index): Path, + State(state): State>, +) -> Result, impl IntoResponse> { + match state + .rpc_client + .utxo_digest(context::current(), index) + .await + .map_err(rpc_err)? + { + Some(digest) => Ok(Json(digest)), + None => Err(not_found_err()), + } +} diff --git a/src/web/html/page/root.html b/src/web/html/page/root.html index 7a800bd..848bbcc 100644 --- a/src/web/html/page/root.html +++ b/src/web/html/page/root.html @@ -47,6 +47,8 @@ Quick Lookup:

REST RPCs

+ + + + +

/utxo_digest

@@ -76,5 +84,7 @@ Quick Lookup: /rpc/utxo_digest/2
+
+