diff --git a/Cargo.lock b/Cargo.lock index 959a144..d864e48 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -451,6 +451,20 @@ dependencies = [ "generic-array", ] +[[package]] +name = "boilerplate" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1906889b1f805a715eac02b2dea416e25c5cfa00f099530fa9d137a3cff93113" +dependencies = [ + "darling", + "mime", + "new_mime_guess", + "proc-macro2", + "quote", + "syn 2.0.58", +] + [[package]] name = "bumpalo" version = "3.16.0" @@ -569,7 +583,7 @@ dependencies = [ "anstream", "anstyle", "clap_lex", - "strsim", + "strsim 0.11.1", ] [[package]] @@ -838,6 +852,41 @@ dependencies = [ "cipher", ] +[[package]] +name = "darling" +version = "0.20.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "54e36fcd13ed84ffdfda6f5be89b31287cbb80c439841fe69e04841435464391" +dependencies = [ + "darling_core", + "darling_macro", +] + +[[package]] +name = "darling_core" +version = "0.20.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c2cf1c23a687a1feeb728783b993c4e1ad83d99f351801977dd809b48d0a70f" +dependencies = [ + "fnv", + "ident_case", + "proc-macro2", + "quote", + "strsim 0.10.0", + "syn 2.0.58", +] + +[[package]] +name = "darling_macro" +version = "0.20.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a668eda54683121533a393014d8692171709ff57a7d61f187b6e782719f8933f" +dependencies = [ + "darling_core", + "quote", + "syn 2.0.58", +] + [[package]] name = "deranged" version = "0.3.11" @@ -1250,6 +1299,12 @@ dependencies = [ "digest", ] +[[package]] +name = "html-escaper" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "459a0ca33ee92551e0a3bb1774f2d3bdd1c09fb6341845736662dd25e1fcb52a" + [[package]] name = "http" version = "0.2.12" @@ -1306,6 +1361,12 @@ dependencies = [ "pin-project-lite", ] +[[package]] +name = "http-range-header" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ce4ef31cda248bbdb6e6820603b82dfcd9e833db65a43e997a0ccec777d11fe" + [[package]] name = "httparse" version = "1.8.0" @@ -1418,6 +1479,12 @@ dependencies = [ "cc", ] +[[package]] +name = "ident_case" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" + [[package]] name = "indexmap" version = "1.9.3" @@ -1629,6 +1696,16 @@ version = "0.3.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" +[[package]] +name = "mime_guess" +version = "2.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4192263c238a5f0d0c6bfd21f336a313a4ce1c450542449ca191bb657b4642ef" +dependencies = [ + "mime", + "unicase", +] + [[package]] name = "minimal-lexical" version = "0.2.1" @@ -1673,6 +1750,7 @@ dependencies = [ [[package]] name = "neptune-core" version = "0.0.5" +source = "git+https://github.com/Neptune-Crypto/neptune-core.git?rev=9a7973259d63f148f8bf353866f260ba939d4649#9a7973259d63f148f8bf353866f260ba939d4649" dependencies = [ "aead", "aes-gcm", @@ -1730,17 +1808,30 @@ name = "neptune-explorer" version = "0.1.0" dependencies = [ "axum 0.7.5", + "boilerplate", "clap", + "html-escaper", "neptune-core", "serde", "serde_json", "tarpc", "thiserror", "tokio", + "tower-http", "tracing", "tracing-subscriber", ] +[[package]] +name = "new_mime_guess" +version = "4.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2d684d1b59e0dc07b37e2203ef576987473288f530082512aff850585c61b1f" +dependencies = [ + "mime", + "unicase", +] + [[package]] name = "nom" version = "7.1.3" @@ -2207,7 +2298,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "19de2de2a00075bf566bee3bd4db014b11587e84184d3f7a791bc17f1a8e9e48" dependencies = [ "anyhow", - "itertools 0.10.5", + "itertools 0.12.1", "proc-macro2", "quote", "syn 2.0.58", @@ -2655,6 +2746,12 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" +[[package]] +name = "strsim" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" + [[package]] name = "strsim" version = "0.11.1" @@ -2790,7 +2887,7 @@ dependencies = [ "const_format", "derive_tasm_object", "hex", - "itertools 0.10.5", + "itertools 0.12.1", "ndarray", "num", "num-traits", @@ -3048,6 +3145,31 @@ dependencies = [ "tracing", ] +[[package]] +name = "tower-http" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e9cd434a998747dd2c4276bc96ee2e0c7a2eadf3cae88e52be55a05fa9053f5" +dependencies = [ + "bitflags 2.5.0", + "bytes", + "futures-util", + "http 1.1.0", + "http-body 1.0.0", + "http-body-util", + "http-range-header", + "httpdate", + "mime", + "mime_guess", + "percent-encoding", + "pin-project-lite", + "tokio", + "tokio-util", + "tower-layer", + "tower-service", + "tracing", +] + [[package]] name = "tower-layer" version = "0.3.2" @@ -3232,6 +3354,15 @@ version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "eaea85b334db583fe3274d12b4cd1880032beab409c0d774be044d4480ab9a94" +[[package]] +name = "unicase" +version = "2.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7d2d4dafb69621809a81864c9c1b864479e1235c0dd4e199924b9742439ed89" +dependencies = [ + "version_check", +] + [[package]] name = "unicode-ident" version = "1.0.12" diff --git a/Cargo.toml b/Cargo.toml index 329c8c5..7763706 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -12,7 +12,7 @@ serde_json = "1.0.115" tokio = { version = "1.37.0", features = ["full", "tracing"] } tracing = "0.1.40" tracing-subscriber = "0.3.18" -neptune-core = {path = "../neptune-core"} +neptune-core = {git = "https://github.com/Neptune-Crypto/neptune-core.git", rev = "9a7973259d63f148f8bf353866f260ba939d4649"} tarpc = { version = "^0.34", features = [ "tokio1", "serde-transport", @@ -21,6 +21,10 @@ tarpc = { version = "^0.34", features = [ ] } clap = "4.5.4" thiserror = "1.0.58" +#boilerplate = { version = "1.0.0", features = ["axum"] } +boilerplate = { version = "1.0.0" } +html-escaper = "0.2.0" +tower-http = { version = "0.5.2", features = ["fs"] } [patch.crates-io] diff --git a/src/main.rs b/src/main.rs index de4d6f0..deb3d0f 100644 --- a/src/main.rs +++ b/src/main.rs @@ -5,10 +5,15 @@ use axum::{ routing::get, Json, Router, }; +use tower_http::{ + services::ServeFile, +}; 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; @@ -32,7 +37,7 @@ pub struct Config { port: u16, } -struct AppState { +pub struct AppState { network: Network, #[allow(dead_code)] config: Config, @@ -205,76 +210,18 @@ async fn root( State(state): State>, ) -> Html { - let network = state.network; - let genesis_block_hex = state.genesis_digest.to_hex(); + #[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 html = format!(r#" - - - Neptune Block Explorer: (network: {network}) - - - -

Neptune Block Explorer (network: {network})

- -
- Block height or digest: - - -
- - Quick Lookup: - Genesis Block | - Tip
- -

REST RPCs

- -

/block_info

- - -

/block_digest

- - -

/utxo_digest

-
-

Examples

- - /rpc/utxo_digest/2
-
- - - - "#); - - Html(html) + let root_page = RootHtmlPage(state); + Html(root_page.to_string()) } async fn block_page( @@ -285,78 +232,64 @@ async fn block_page( 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 BlockInfo {height, digest, timestamp, num_inputs, num_outputs, num_uncle_blocks, difficulty, mining_reward, fee, is_genesis, is_tip, ..} = block_info; - - let digest_hex = digest.to_hex(); - - let prev_link = match is_genesis { - true => "".to_string(), - false => format!("Previous Block", height.previous()) - }; - - let next_link = match is_tip { - true => "".to_string(), - false => format!("Next Block", height.next()) - }; - - let special_block_notice = match (is_genesis, is_tip) { - (true, false) => "

This is the Genesis Block

", - (false, true) => "

This is the Latest Block (tip)

", - _ => "", - }; - - let timestamp_display = timestamp.standard_format(); - - let html = format!( r#" - - - Neptune Block Explorer: Block Height {height} - - - -

Block height: {height}

- Digest: {digest_hex} - - {special_block_notice} - - - - - - - - - -
Created{timestamp_display}
Inputs{num_inputs}
Outputs{num_outputs}
Uncle blocks{num_uncle_blocks}
Difficulty{difficulty}
Mining Reward{mining_reward}
Fee{fee}
- -

- Home - {prev_link} - {next_link} -

- - - - "# ); - - Ok(Html(html)) + 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())) +} + #[tokio::main] async fn main() -> Result<(), RpcError> { let rpc_client = rpc_client().await; @@ -371,6 +304,7 @@ 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( @@ -379,9 +313,17 @@ async fn main() -> Result<(), RpcError> { ) .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"))) + + // add state .with_state(shared_state); println!("Running on http://localhost:3000"); diff --git a/src/web/css/styles.css b/src/web/css/styles.css new file mode 100644 index 0000000..f15e33f --- /dev/null +++ b/src/web/css/styles.css @@ -0,0 +1,42 @@ +div.box h1, +div.box h2, +div.box h3 { + margin-top: 0px; + margin-bottom: 5px; +} + +div.indent { + position: relative; + left: 20px; +} + +div.box { + margin-bottom: 10px; + border: solid 1px #ccc; + border-top-left-radius: 15px; + padding: 10px; +} + +body { + font-family: arial, helvetica; +} + +table.alt { + margin-top: 10px; + margin-bottom: 10px; + padding: 10px; + border-collapse: collapse; +} + +table.alt td, +th { + padding: 5px; +} + +table.alt tr:nth-child(odd) td { + background: #eee; +} + +table.alt tr:nth-child(even) td { + background: #fff; +} diff --git a/src/web/html/components/header.html b/src/web/html/components/header.html new file mode 100644 index 0000000..145b929 --- /dev/null +++ b/src/web/html/components/header.html @@ -0,0 +1 @@ +

{{self.site_name}} : {{self.state.network}}

\ No newline at end of file diff --git a/src/web/html/page/block_info.html b/src/web/html/page/block_info.html new file mode 100644 index 0000000..dd92c13 --- /dev/null +++ b/src/web/html/page/block_info.html @@ -0,0 +1,74 @@ + + + + Neptune Block Explorer: Block Height {{self.block_info.height}} + + + + +{{Trusted(self.header.to_string())}} + +

Block height: {{self.block_info.height}}

+ + +%% if self.block_info.is_genesis { +

This is the Genesis Block

+%% } +%% if self.block_info.is_tip { +

This is the Latest Block (tip)

+%% } + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Digest{{self.block_info.digest.to_hex()}}
Created{{self.block_info.timestamp.standard_format()}}
Inputs{{self.block_info.num_inputs}}
Outputs{{self.block_info.num_outputs}}
Uncle blocks{{self.block_info.num_uncle_blocks}}
Difficulty{{self.block_info.difficulty}}
Mining Reward{{self.block_info.mining_reward}}
Fee{{self.block_info.fee}}
+ +

+ Home + | Genesis + | Tip +%% if self.block_info.is_genesis { + | Previous Block +%% } else { + | Previous Block +%% } + +%% if self.block_info.is_tip { + | Next Block +%% } else { + | Next Block +%% } +

+ + + \ No newline at end of file diff --git a/src/web/html/page/root.html b/src/web/html/page/root.html new file mode 100644 index 0000000..7a800bd --- /dev/null +++ b/src/web/html/page/root.html @@ -0,0 +1,80 @@ + + + Neptune Block Explorer: (network: {{self.network}}) + + + +

Neptune Block Explorer (network: {{self.network}})

+ + +
+

Block Lookup

+
+Block height or digest: + + +
+ +Quick Lookup: + Genesis Block | + Tip
+
+ + +
+

Utxo Lookup

+
+ Utxo index: + + +
+
+ +

REST RPCs

+ +

/block_info

+ + +

/block_digest

+ + +

/utxo_digest

+
+

Examples

+ + /rpc/utxo_digest/2
+
+ + + diff --git a/src/web/html/page/utxo.html b/src/web/html/page/utxo.html new file mode 100644 index 0000000..5189ed8 --- /dev/null +++ b/src/web/html/page/utxo.html @@ -0,0 +1,30 @@ + + + + Neptune Block Explorer: Utxo {{self.index}} + + + + +{{Trusted(self.header.to_string())}} + +

Utxo Information

+ + + + + + + + + +
Index{{self.index}}
Digest{{self.digest.to_hex()}}
+ +

+ Home + | Genesis + | Tip +

+ + + \ No newline at end of file diff --git a/templates/web b/templates/web new file mode 120000 index 0000000..d90a754 --- /dev/null +++ b/templates/web @@ -0,0 +1 @@ +../src/web \ No newline at end of file