feat: add boilerplate templates, utxo page

This commit is contained in:
danda 2024-05-09 08:56:59 -07:00
parent 951758902a
commit 4509b1f9a6
9 changed files with 441 additions and 136 deletions

137
Cargo.lock generated
View File

@ -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"

View File

@ -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]

View File

@ -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<Arc<AppState>>,
) -> Html<String> {
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<AppState>);
impl Deref for RootHtmlPage {
type Target = AppState;
fn deref(&self) -> &Self::Target {
&self.0
}
}
let html = format!(r#"
<html>
<head>
<title>Neptune Block Explorer: (network: {network})</title>
<style>
div.indent {{position: relative; left: 20px;}}
body {{font-family: arial, helvetica;}}
</style>
</head>
<body>
<h1>Neptune Block Explorer (network: {network})</h1>
<script>
function handle_submit(form){{
let value = form.height_or_digest.value;
var is_digest = value.length == 80;
var type = is_digest ? "digest" : "height";
var uri = form.action + "/" + type + "/" + value;
window.location.href = uri;
return false;
}}
</script>
<form action="/block" method="get" onsubmit="return handle_submit(this)">
Block height or digest:
<input type="text" size="80" name="height_or_digest"/>
<input type="submit" name="height" value="Lookup Block"/>
</form>
Quick Lookup:
<a href="/block/genesis">Genesis Block</a> |
<a href="/block/tip">Tip</a><br/>
<h2>REST RPCs</h2>
<h3>/block_info</h3>
<div class="indent">
<h4>Examples</h4>
<a href="/rpc/block_info/genesis">/rpc/block_info/genesis</a><br/>
<a href="/rpc/block_info/tip">/rpc/block_info/tip</a><br/>
<a href="/rpc/block_info/height/2">/rpc/block_info/height/2</a><br/>
<a href="/rpc/block_info/digest/{genesis_block_hex}">/rpc/block_info/digest/{genesis_block_hex}</a><br/>
<a href="/rpc/block_info/height_or_digest/1">/rpc/block_info/height_or_digest/1</a><br/>
</div>
<h3>/block_digest</h3>
<div class="indent">
<h4>Examples</h4>
<a href="/rpc/block_digest/genesis">/rpc/block_digest/genesis</a><br/>
<a href="/rpc/block_digest/tip">/rpc/block_digest/tip</a><br/>
<a href="/rpc/block_digest/height/2">/rpc/block_digest/height/2</a><br/>
<a href="/rpc/block_digest/digest/{genesis_block_hex}">/rpc/block_digest/digest/{genesis_block_hex}</a><br/>
<a href="/rpc/block_digest/height_or_digest/{genesis_block_hex}">/rpc/block_digest/height_or_digest/{genesis_block_hex}</a><br/>
</div>
<h3>/utxo_digest</h3>
<div class="indent">
<h4>Examples</h4>
<a href="/rpc/utxo_digest/2">/rpc/utxo_digest/2</a><br/>
</div>
</body>
</html>
"#);
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<AppState>,
}
#[axum::debug_handler]
async fn block_page_with_value(
Path((path_block_selector, value)): Path<(PathBlockSelector, String)>,
State(state): State<Arc<AppState>>,
) -> Result<Html<String>, 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!("<a href='/block/height/{}'>Previous Block</a>", height.previous())
};
let next_link = match is_tip {
true => "".to_string(),
false => format!("<a href='/block/height/{}'>Next Block</a>", height.next())
};
let special_block_notice = match (is_genesis, is_tip) {
(true, false) => "<p>This is the Genesis Block</p>",
(false, true) => "<p>This is the Latest Block (tip)</p>",
_ => "",
};
let timestamp_display = timestamp.standard_format();
let html = format!( r#"
<html>
<head>
<title>Neptune Block Explorer: Block Height {height}</title>
<style>
div.indent {{position: relative; left: 20px;}}
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;}}
</style>
</head>
<body>
<h1>Block height: {height}</h1>
<b>Digest:</b> {digest_hex}
{special_block_notice}
<table class="alt">
<tr><td>Created</td><td>{timestamp_display}</td></tr>
<tr><td>Inputs</td><td>{num_inputs}<br/></td></tr>
<tr><td>Outputs</td><td>{num_outputs}</td></tr>
<tr><td>Uncle blocks</td><td>{num_uncle_blocks}</td></tr>
<tr><td>Difficulty</td><td>{difficulty}</td></tr>
<tr><td>Mining Reward</td><td>{mining_reward}</td></tr>
<tr><td>Fee</td><td>{fee}</td></tr>
</table>
<p>
<a href="/">Home</a>
{prev_link}
{next_link}
</p>
</body>
</html>
"# );
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<u64>,
State(state): State<Arc<AppState>>,
) -> Result<Html<String>, 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");

42
src/web/css/styles.css Normal file
View File

@ -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;
}

View File

@ -0,0 +1 @@
<h1>{{self.site_name}} : {{self.state.network}}</h1>

View File

@ -0,0 +1,74 @@
<html>
<head>
<title>Neptune Block Explorer: Block Height {{self.block_info.height}}</title>
<link rel="stylesheet" type="text/css" href="/css/styles.css" media="screen" />
</head>
<body>
{{Trusted(self.header.to_string())}}
<h2>Block height: {{self.block_info.height}}</h2>
<!-- special_block_notice -->
%% if self.block_info.is_genesis {
<p>This is the Genesis Block</p>
%% }
%% if self.block_info.is_tip {
<p>This is the Latest Block (tip)</p>
%% }
<table class="alt">
<tr>
<td>Digest</td>
<td>{{self.block_info.digest.to_hex()}}</td>
</tr>
<tr>
<td>Created</td>
<td>{{self.block_info.timestamp.standard_format()}}</td>
</tr>
<tr>
<td>Inputs</td>
<td>{{self.block_info.num_inputs}}<br /></td>
</tr>
<tr>
<td>Outputs</td>
<td>{{self.block_info.num_outputs}}</td>
</tr>
<tr>
<td>Uncle blocks</td>
<td>{{self.block_info.num_uncle_blocks}}</td>
</tr>
<tr>
<td>Difficulty</td>
<td>{{self.block_info.difficulty}}</td>
</tr>
<tr>
<td>Mining Reward</td>
<td>{{self.block_info.mining_reward}}</td>
</tr>
<tr>
<td>Fee</td>
<td>{{self.block_info.fee}}</td>
</tr>
</table>
<p>
<a href="/">Home</a>
| <a href='/block/genesis'>Genesis</a>
| <a href='/block/tip'>Tip</a>
%% if self.block_info.is_genesis {
| Previous Block
%% } else {
| <a href='/block/height/{{self.block_info.height.previous()}}'>Previous Block</a>
%% }
%% if self.block_info.is_tip {
| Next Block
%% } else {
| <a href='/block/height/{{self.block_info.height.next()}}'>Next Block</a>
%% }
</p>
</body>
</html>

View File

@ -0,0 +1,80 @@
<html>
<head>
<title>Neptune Block Explorer: (network: {{self.network}})</title>
<link rel="stylesheet" type="text/css" href="/css/styles.css" media="screen" />
</head>
<body>
<h1>Neptune Block Explorer (network: {{self.network}})</h1>
<script>
function handle_submit(form){
let value = form.height_or_digest.value;
var is_digest = value.length == 80;
var type = is_digest ? "digest" : "height";
var uri = form.action + "/" + type + "/" + value;
window.location.href = uri;
return false;
}
function handle_utxo_submit(form) {
let value = form.utxo.value;
var uri = form.action + "/" + value;
window.location.href = uri;
return false;
}
</script>
<div class="box">
<h3>Block Lookup</h3>
<form action="/block" method="get" onsubmit="return handle_submit(this)">
Block height or digest:
<input type="text" size="80" name="height_or_digest"/>
<input type="submit" name="height" value="Lookup Block"/>
</form>
Quick Lookup:
<a href="/block/genesis">Genesis Block</a> |
<a href="/block/tip">Tip</a><br/>
</div>
<div class="box">
<h3>Utxo Lookup</h3>
<form action="/utxo" method="get" onsubmit="return handle_utxo_submit(this)">
Utxo index:
<input type="text" size="10" name="utxo" />
<input type="submit" name="height" value="Lookup Utxo" />
</form>
</div>
<h2>REST RPCs</h2>
<h3>/block_info</h3>
<div class="indent">
<h4>Examples</h4>
<a href="/rpc/block_info/genesis">/rpc/block_info/genesis</a><br/>
<a href="/rpc/block_info/tip">/rpc/block_info/tip</a><br/>
<a href="/rpc/block_info/height/2">/rpc/block_info/height/2</a><br/>
<a href="/rpc/block_info/digest/{{self.genesis_digest.to_hex()}}">/rpc/block_info/digest/{{self.genesis_digest.to_hex()}}</a><br/>
<a href="/rpc/block_info/height_or_digest/1">/rpc/block_info/height_or_digest/1</a><br/>
</div>
<h3>/block_digest</h3>
<div class="indent">
<h4>Examples</h4>
<a href="/rpc/block_digest/genesis">/rpc/block_digest/genesis</a><br/>
<a href="/rpc/block_digest/tip">/rpc/block_digest/tip</a><br/>
<a href="/rpc/block_digest/height/2">/rpc/block_digest/height/2</a><br/>
<a href="/rpc/block_digest/digest/{genesis_block_hex}">/rpc/block_digest/digest/{genesis_block_hex}</a><br/>
<a href="/rpc/block_digest/height_or_digest/{genesis_block_hex}">/rpc/block_digest/height_or_digest/{genesis_block_hex}</a><br/>
</div>
<h3>/utxo_digest</h3>
<div class="indent">
<h4>Examples</h4>
<a href="/rpc/utxo_digest/2">/rpc/utxo_digest/2</a><br/>
</div>
</body>
</html>

View File

@ -0,0 +1,30 @@
<html>
<head>
<title>Neptune Block Explorer: Utxo {{self.index}}</title>
<link rel="stylesheet" type="text/css" href="/css/styles.css" media="screen" />
</head>
<body>
{{Trusted(self.header.to_string())}}
<h3>Utxo Information</h3>
<table class="alt">
<tr>
<td>Index</td>
<td>{{self.index}}</td>
</tr>
<tr>
<td>Digest</td>
<td>{{self.digest.to_hex()}}</td>
</tr>
</table>
<p>
<a href="/">Home</a>
| <a href='/block/genesis'>Genesis</a>
| <a href='/block/tip'>Tip</a>
</p>
</body>
</html>

1
templates/web Symbolic link
View File

@ -0,0 +1 @@
../src/web