Initial commit

This commit is contained in:
aglkm
2024-05-03 14:23:37 +03:00
commit a5ca343c52
25 changed files with 26114 additions and 0 deletions

2930
Cargo.lock generated Normal file

File diff suppressed because it is too large Load Diff

34
Cargo.toml Normal file
View File

@@ -0,0 +1,34 @@
[package]
name = "grin-explorer"
version = "0.1.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
rocket = "0.5.0"
serde = {version = "1.0.198", features = ["derive"]}
serde_json = "1.0.111"
num-format = "0.4.4"
fs_extra = "1.3.0"
colored = "2.1.0"
humantime = "2.1.0"
chrono = "0.4.37"
futures = "0.3.30"
config = "0.14.0"
lazy_static = "1.4.0"
shellexpand = "3.1.0"
either = "1.11.0"
[dependencies.rusqlite]
version = "0.30.0"
features = ["bundled"]
[dependencies.reqwest]
version = "0.11.23"
features = ["json", "blocking"]
[dependencies.rocket_dyn_templates]
version = "0.1.0"
features = ["tera"]

18
Explorer.toml Normal file
View File

@@ -0,0 +1,18 @@
# Node address.
ip = "127.0.0.1"
# Node port.
port = "3413"
# Node protocol. Either HTTP or HTTPS.
proto = "http"
# API username.
user = "grin"
# API secret path.
api_secret_path = "~/.grin/main/.api_secret"
# Foreign API secret path.
foreign_api_secret_path = "~/.grin/main/.foreign_api_secret"

21
LICENSE Normal file
View File

@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2024 aglkm
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

33
README.md Normal file
View File

@@ -0,0 +1,33 @@
# Grincoin.org (GRIN) Blockchain Explorer
Blockchain explorer for Grin cryptocurrency.
## What is Grin?
Grin is the very first, simple and fair MimbleWimble blockchain implementation.
- Scalable, privacy-preserving blockchain.
- Fair and verifiable coin distribution.
- Not controlled by any company, foundation or individual.
## Prerequisites
- Rust: https://www.rust-lang.org/tools/install.
- Grin node: https://github.com/mimblewimble/grin. You need to enable archival mode, so the explorer can see all the blocks, otherwise only the recent blocks can be explored.
## Installation
1. Clone repository: git clone https://github.com/aglkm/grin-explorer.git
2. Build explorer: cargo build --release
3. Run executable: ./target/release/grin-explorer
You will see the following output:
`[ INFO ] Starting up Explorer.`
`[ INFO ] Starting up Rocket engine.`
`🚀 Rocket has launched from http://127.0.0.1:8000`
`[ OK ] Explorer Ready.`
4. Open explorer in your browser: http://127.0.0.1:8000

4
Rocket.toml Normal file
View File

@@ -0,0 +1,4 @@
[default]
address = "127.0.0.1"
log_level = "critical"

157
src/data.rs Normal file
View File

@@ -0,0 +1,157 @@
use serde::{Serialize, Deserialize};
// Dashboard data
#[derive(Debug)]
pub struct Dashboard {
// status
pub height: String,
pub sync: String,
pub node_ver: String,
pub proto_ver: String,
// connections
pub inbound: u16,
pub outbound: u16,
//price & market
pub supply: String,
pub soft_supply: String,
pub inflation: String,
pub price_usd: String,
pub price_btc: String,
pub volume_usd: String,
pub volume_btc: String,
pub cap_usd: String,
pub cap_btc: String,
// blockchain
pub disk_usage: String,
pub age: String,
// hashrate
pub hashrate: String,
pub difficulty: String,
// mining
pub production_cost: String,
pub reward_ratio: String,
// mempool
pub txns: String,
pub stem: String,
}
impl Dashboard {
pub fn new() -> Dashboard {
Dashboard {
height: String::new(),
sync: String::new(),
node_ver: String::new(),
proto_ver: String::new(),
inbound: 0,
outbound: 0,
supply: String::new(),
soft_supply: String::new(),
inflation: String::new(),
price_usd: String::new(),
price_btc: String::new(),
volume_usd: String::new(),
volume_btc: String::new(),
cap_usd: String::new(),
cap_btc: String::new(),
disk_usage: String::new(),
age: String::new(),
hashrate: String::new(),
difficulty: String::new(),
production_cost: String::new(),
reward_ratio: String::new(),
txns: String::new(),
stem: String::new(),
}
}
}
// Block data
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Block {
pub hash: String,
pub height: String,
pub time: String,
pub version: String,
pub weight: f64,
pub fees: f64,
pub kernels: Vec<(String, String, String)>,
pub inputs: Vec<String>,
pub outputs: Vec<(String,String)>,
pub ker_len: u64,
pub in_len: u64,
pub out_len: u64,
pub raw_data: String,
}
impl Block {
pub fn new() -> Block {
Block {
hash: String::new(),
height: String::new(),
time: String::new(),
version: String::new(),
weight: 0.0,
fees: 0.0,
kernels: Vec::new(),
inputs: Vec::new(),
outputs: Vec::new(),
ker_len: 0,
in_len: 0,
out_len: 0,
raw_data: String::new(),
}
}
}
// Transactions data
#[derive(Debug)]
pub struct Transactions {
pub period_1h: String,
pub period_24h: String,
pub fees_1h: String,
pub fees_24h: String,
}
impl Transactions {
pub fn new() -> Transactions {
Transactions {
period_1h: String::new(),
period_24h: String::new(),
fees_1h: String::new(),
fees_24h: String::new(),
}
}
}
// Explorer configuration
#[derive(Debug)]
pub struct ExplorerConfig {
pub ip: String,
pub port: String,
pub proto: String,
pub user: String,
pub api_secret_path: String,
pub foreign_api_secret_path: String,
pub api_secret: String,
pub foreign_api_secret: String,
}
impl ExplorerConfig {
pub fn new() -> ExplorerConfig {
ExplorerConfig {
ip: String::new(),
port: String::new(),
proto: String::new(),
user: String::new(),
api_secret_path: String::new(),
foreign_api_secret_path: String::new(),
api_secret: String::new(),
foreign_api_secret: String::new(),
}
}
}

553
src/main.rs Normal file
View File

@@ -0,0 +1,553 @@
#[macro_use] extern crate rocket;
use rocket_dyn_templates::Template;
use rocket_dyn_templates::context;
use rocket::fs::FileServer;
use rocket::State;
use std::sync::{Arc, Mutex};
use std::time::Duration;
use colored::Colorize;
use rocket::tokio;
use rocket::response::Redirect;
use either::Either;
mod worker;
mod requests;
mod data;
use crate::data::Dashboard;
use crate::data::Block;
use crate::data::Transactions;
// Rendering main (Dashboard) page.
#[get("/")]
fn index(dashboard: &State<Arc<Mutex<Dashboard>>>) -> Template {
let data = dashboard.lock().unwrap();
Template::render("index", context! {
route: "index",
node_ver: &data.node_ver,
proto_ver: &data.proto_ver,
})
}
// Rendering block list (Blocks) page.
#[get("/block_list")]
fn block_list() -> Template {
Template::render("block_list", context! {
route: "block_list",
})
}
// Rendering block list starting with a specified height.
// [<--] and [-->] buttons at the bottom of the block list (Blocks) page.
#[get("/block_list/<input_height>")]
async fn block_list_by_height(input_height: &str) -> Template {
let mut blocks = Vec::<Block>::new();
// Store current latest height
let mut height = 0;
let _ = requests::get_block_list_by_height(&input_height, &mut blocks, &mut height).await;
// Check if user's input doesn't overflow current height
if blocks.is_empty() == false && blocks[0].height.is_empty() == false {
let index = blocks[0].height.parse::<u64>().unwrap();
if index >= height {
Template::render("block_list", context! {
route: "block_list",
})
} else {
Template::render("block_list", context! {
route: "block_list_by_height",
index,
blocks,
height,
})
}
} else {
Template::render("block_list", context! {
route: "block_list",
})
}
}
// Rendering page for a specified block (by height).
#[get("/block/<height>")]
async fn block_details_by_height(height: &str) -> Template {
let mut block = Block::new();
if height.is_empty() == false && height.chars().all(char::is_numeric) == true {
let _ = requests::get_block_data(&height, &mut block).await;
if block.height.is_empty() == false {
return Template::render("block_details", context! {
route: "block_details",
block,
});
}
}
Template::render("error", context! {
route: "error",
})
}
// Rendering page for a specified block (by hash).
#[get("/hash/<hash>")]
async fn block_header_by_hash(hash: &str) -> Either<Template, Redirect> {
let mut height = String::new();
let _ = requests::get_block_header(&hash, &mut height).await;
if hash.is_empty() == false {
if height.is_empty() == false {
return Either::Right(Redirect::to(uri!(block_details_by_height(height.as_str()))));
}
}
return Either::Left(Template::render("error", context! {
route: "error",
}))
}
// Rendering page for a specified kernel.
#[get("/kernel/<kernel>")]
async fn kernel(kernel: &str) -> Either<Template, Redirect> {
let mut height = String::new();
let _ = requests::get_kernel(&kernel, &mut height).await;
if kernel.is_empty() == false {
if height.is_empty() == false {
return Either::Right(Redirect::to(uri!(block_details_by_height(height.as_str()))));
}
}
return Either::Left(Template::render("error", context! {
route: "error",
}))
}
// Handling search request.
#[post("/search", data="<input>")]
fn search(input: &str) -> Either<Template, Redirect> {
// Trim 'search=' from the request data
let input = &input[7..].to_lowercase();
//Check if input is valid
if input.is_empty() == false {
if input.chars().all(|x| (x >= 'a' && x <= 'f') || (x >= '0' && x <= '9')) == true {
// Block number
if input.chars().all(char::is_numeric) == true {
return Either::Right(Redirect::to(uri!(block_details_by_height(input))));
// Block hash
} else if input.len() == 64 {
return Either::Right(Redirect::to(uri!(block_header_by_hash(input))));
// Kernel hash
} else if input.len() == 66 {
return Either::Right(Redirect::to(uri!(kernel(input))));
}
}
}
Either::Left(Template::render("error", context! {
route: "error",
}))
}
// Start of HTMX routes.
#[get("/rpc/peers/inbound")]
fn peers_inbound(dashboard: &State<Arc<Mutex<Dashboard>>>) -> String {
let data = dashboard.lock().unwrap();
data.inbound.to_string()
}
#[get("/rpc/peers/outbound")]
fn peers_outbound(dashboard: &State<Arc<Mutex<Dashboard>>>) -> String {
let data = dashboard.lock().unwrap();
data.outbound.to_string()
}
#[get("/rpc/sync/status")]
fn sync_status(dashboard: &State<Arc<Mutex<Dashboard>>>) -> String {
let data = dashboard.lock().unwrap();
if data.sync == "no_sync" {
"Synced".to_string()
} else {
format!("Syncing ({})
<div class='spinner-grow spinner-grow-sm' role='status'>
<span class='visually-hidden'>Syncing...</span></div>", data.sync)
}
}
#[get("/rpc/market/supply")]
fn market_supply(dashboard: &State<Arc<Mutex<Dashboard>>>) -> String {
let data = dashboard.lock().unwrap();
format!("{}", data.supply)
}
#[get("/rpc/market/soft_supply")]
fn soft_supply(dashboard: &State<Arc<Mutex<Dashboard>>>) -> String {
let data = dashboard.lock().unwrap();
if data.supply.is_empty() == false {
return format!("{} % ({}M/3150M)", data.soft_supply, &data.supply[..3]);
}
"3150M".to_string()
}
#[get("/rpc/inflation/rate")]
fn inflation_rate(dashboard: &State<Arc<Mutex<Dashboard>>>) -> String {
let data = dashboard.lock().unwrap();
format!("{} %", data.inflation)
}
#[get("/rpc/market/volume_usd")]
fn volume_usd(dashboard: &State<Arc<Mutex<Dashboard>>>) -> String {
let data = dashboard.lock().unwrap();
format!("$ {}", data.volume_usd)
}
#[get("/rpc/market/volume_btc")]
fn volume_btc(dashboard: &State<Arc<Mutex<Dashboard>>>) -> String {
let data = dashboard.lock().unwrap();
format!("{}", data.volume_btc)
}
#[get("/rpc/price/usd")]
fn price_usd(dashboard: &State<Arc<Mutex<Dashboard>>>) -> String {
let data = dashboard.lock().unwrap();
format!("$ {}", data.price_usd)
}
#[get("/rpc/price/btc")]
fn price_btc(dashboard: &State<Arc<Mutex<Dashboard>>>) -> String {
let data = dashboard.lock().unwrap();
let trim: &[_] = &['0', '.'];
format!("{} sats", data.price_btc.trim_start_matches(trim))
}
#[get("/rpc/market/cap_usd")]
fn mcap_usd(dashboard: &State<Arc<Mutex<Dashboard>>>) -> String {
let data = dashboard.lock().unwrap();
format!("$ {}", data.cap_usd)
}
#[get("/rpc/market/cap_btc")]
fn mcap_btc(dashboard: &State<Arc<Mutex<Dashboard>>>) -> String {
let data = dashboard.lock().unwrap();
format!("{}", data.cap_btc)
}
#[get("/rpc/block/latest")]
fn latest_height(dashboard: &State<Arc<Mutex<Dashboard>>>) -> String {
let data = dashboard.lock().unwrap();
data.height.clone()
}
#[get("/rpc/block/time_since_last")]
fn last_block_age(blocks: &State<Arc<Mutex<Vec<Block>>>>) -> String {
let data = blocks.lock().unwrap();
if data.is_empty() == false {
return data[0].time.clone();
}
"".to_string()
}
#[get("/rpc/disk/usage")]
fn disk_usage(dashboard: &State<Arc<Mutex<Dashboard>>>) -> String {
let data = dashboard.lock().unwrap();
format!("{} GB", data.disk_usage)
}
#[get("/rpc/network/hashrate")]
fn network_hashrate(dashboard: &State<Arc<Mutex<Dashboard>>>) -> String {
let data = dashboard.lock().unwrap();
format!("{} KG/s", data.hashrate)
}
#[get("/rpc/mining/production_cost")]
fn production_cost(dashboard: &State<Arc<Mutex<Dashboard>>>) -> String {
let data = dashboard.lock().unwrap();
format!("$ {}", data.production_cost)
}
#[get("/rpc/mining/reward_ratio")]
fn reward_ratio(dashboard: &State<Arc<Mutex<Dashboard>>>) -> String {
let data = dashboard.lock().unwrap();
if data.reward_ratio.is_empty() == false {
let ratio = data.reward_ratio.parse::<f64>().unwrap();
if ratio <= 1.0 {
return format!("x{} <i class='bi bi-hand-thumbs-down'></i>", data.reward_ratio);
} else if ratio < 2.0 {
return format!("x{} <i class='bi bi-hand-thumbs-up'></i>", data.reward_ratio);
} else if ratio < 3.0 {
return format!("x{} <i class='bi bi-emoji-sunglasses'></i>", data.reward_ratio);
} else if ratio >= 3.0 {
return format!("x{} <i class='bi bi-rocket-takeoff'></i>", data.reward_ratio);
}
}
data.reward_ratio.clone()
}
#[get("/rpc/network/difficulty")]
fn network_difficulty(dashboard: &State<Arc<Mutex<Dashboard>>>) -> String {
let data = dashboard.lock().unwrap();
data.difficulty.to_string()
}
#[get("/rpc/mempool/txns")]
fn mempool_txns(dashboard: &State<Arc<Mutex<Dashboard>>>) -> String {
let data = dashboard.lock().unwrap();
data.txns.to_string()
}
#[get("/rpc/mempool/stem")]
fn mempool_stem(dashboard: &State<Arc<Mutex<Dashboard>>>) -> String {
let data = dashboard.lock().unwrap();
data.stem.to_string()
}
#[get("/rpc/txns/count_1h")]
fn txns_count_1h(transactions: &State<Arc<Mutex<Transactions>>>) -> String {
let data = transactions.lock().unwrap();
format!("{}, ツ {}", data.period_1h, data.fees_1h)
}
#[get("/rpc/txns/count_24h")]
fn txns_count_24h(transactions: &State<Arc<Mutex<Transactions>>>) -> String {
let data = transactions.lock().unwrap();
format!("{}, ツ {}", data.period_24h, data.fees_24h)
}
#[get("/rpc/block/link?<count>")]
fn block_link(count: usize, blocks: &State<Arc<Mutex<Vec<Block>>>>) -> String {
let data = blocks.lock().unwrap();
if data.is_empty() == false {
return format!("<a href=/block/{} class='text-decoration-none'>{}</a>",
data[count].height, data[count].height);
}
"".to_string()
}
#[get("/rpc/block/link_color?<count>")]
fn block_link_color(count: usize, blocks: &State<Arc<Mutex<Vec<Block>>>>) -> String {
let data = blocks.lock().unwrap();
if data.is_empty() == false {
return format!("<a href=/block/{} class='text-decoration-none darkorange-text'>{}</a>",
data[count].height, data[count].height);
}
"".to_string()
}
#[get("/rpc/block/time?<count>")]
fn block_time(count: usize, blocks: &State<Arc<Mutex<Vec<Block>>>>) -> String {
let data = blocks.lock().unwrap();
if data.is_empty() == false {
return data[count].time.clone();
}
"".to_string()
}
#[get("/rpc/block/kernels?<count>")]
fn block_txns(count: usize, blocks: &State<Arc<Mutex<Vec<Block>>>>) -> String {
let data = blocks.lock().unwrap();
if data.is_empty() == false {
return data[count].ker_len.to_string();
}
"".to_string()
}
#[get("/rpc/block/inputs?<count>")]
fn block_inputs(count: usize, blocks: &State<Arc<Mutex<Vec<Block>>>>) -> String {
let data = blocks.lock().unwrap();
if data.is_empty() == false {
return data[count].in_len.to_string();
}
"".to_string()
}
#[get("/rpc/block/outputs?<count>")]
fn block_outputs(count: usize, blocks: &State<Arc<Mutex<Vec<Block>>>>) -> String {
let data = blocks.lock().unwrap();
if data.is_empty() == false {
return data[count].out_len.to_string();
}
"".to_string()
}
#[get("/rpc/block/fees?<count>")]
fn block_fees(count: usize, blocks: &State<Arc<Mutex<Vec<Block>>>>) -> String {
let data = blocks.lock().unwrap();
if data.is_empty() == false {
return format!("{}", data[count].fees / 1000000000.0);
}
"".to_string()
}
#[get("/rpc/block/weight?<count>")]
fn block_weight(count: usize, blocks: &State<Arc<Mutex<Vec<Block>>>>) -> String {
let data = blocks.lock().unwrap();
if data.is_empty() == false {
return format!("{} %", data[count].weight);
}
"".to_string()
}
#[get("/rpc/block_list/index")]
fn block_list_index(dashboard: &State<Arc<Mutex<Dashboard>>>) -> String {
let data = dashboard.lock().unwrap();
if data.height.is_empty() == false {
return format!("<a class='text-decoration-none' href='/block_list/{}'>
<div class='col-sm'><h2><i class='bi bi-arrow-right-square'></i></h2></div>
</a>", data.height.parse::<u64>().unwrap() - 10);
}
"".to_string()
}
// End of HTMX backends.
// Main
#[rocket::main]
async fn main() {
println!("{} Starting up Explorer.", "[ INFO ]".cyan());
let dash = Arc::new(Mutex::new(Dashboard::new()));
let dash_clone = dash.clone();
let blocks = Arc::new(Mutex::new(Vec::<Block>::new()));
let blocks_clone = blocks.clone();
let txns = Arc::new(Mutex::new(Transactions::new()));
let txns_clone = txns.clone();
let mut ready = false;
// Starting the Worker
tokio::spawn(async move {
loop {
let result = worker::run(dash_clone.clone(), blocks_clone.clone(),
txns_clone.clone()).await;
match result {
Ok(_v) => {
if ready == false {
ready = true;
println!("{} Explorer Ready.", "[ OK ]".green());
}
},
Err(e) => {
ready = false;
println!("{} {}.", "[ ERROR ]".red(), e);
},
}
tokio::time::sleep(Duration::from_secs(15)).await;
}
});
println!("{} Starting up Rocket engine.", "[ INFO ]".cyan());
// Starting Rocket engine.
let _ = rocket::build()
.manage(dash)
.manage(blocks)
.manage(txns)
.mount("/", routes![index, peers_inbound, peers_outbound, sync_status, market_supply,
inflation_rate, volume_usd, volume_btc, price_usd, price_btc,
mcap_usd, mcap_btc,latest_height, disk_usage, network_hashrate,
network_difficulty, mempool_txns, mempool_stem, txns_count_1h,
txns_count_24h, block_list, block_link, block_link_color,
block_time, block_txns, block_inputs, block_outputs, block_fees,
block_weight, block_details_by_height, block_header_by_hash,
soft_supply, production_cost, reward_ratio, last_block_age,
block_list_by_height, block_list_index, search, kernel])
.mount("/static", FileServer::from("static"))
.attach(Template::fairing())
.launch()
.await;
}

528
src/requests.rs Normal file
View File

@@ -0,0 +1,528 @@
use reqwest::Error;
use serde_json::Value;
use std::sync::{Arc, Mutex};
use num_format::{Locale, ToFormattedString};
use fs_extra::dir::get_size;
use colored::Colorize;
use humantime::format_duration;
use std::time::Duration;
use chrono::{Utc, DateTime};
use config::Config;
use std::collections::HashMap;
use std::fs;
use lazy_static::lazy_static;
use crate::data::Dashboard;
use crate::data::Block;
use crate::data::Transactions;
use crate::data::ExplorerConfig;
// Static explorer config structure
lazy_static! {
static ref CONFIG: ExplorerConfig = {
let mut cfg = ExplorerConfig::new();
let settings = Config::builder().add_source(config::File::with_name("Explorer"))
.build().unwrap();
let settings: HashMap<String, String> = settings.try_deserialize().unwrap();
for (name, value) in settings {
match name.as_str() {
"ip" => cfg.ip = value,
"port" => cfg.port = value,
"proto" => cfg.proto = value,
"user" => cfg.user = value,
"api_secret_path" => cfg.api_secret_path = value,
"foreign_api_secret_path" => cfg.foreign_api_secret_path = value,
_ => println!("{} Unknown config setting '{}'.", "[ ERROR ]".red(), name),
}
}
cfg.api_secret = fs::read_to_string(format!("{}",
shellexpand::tilde(&cfg.api_secret_path))).unwrap();
cfg.foreign_api_secret = fs::read_to_string(format!("{}",
shellexpand::tilde(&cfg.foreign_api_secret_path))).unwrap();
cfg
};
}
// RPC requests to grin node.
async fn call(method: &str, params: &str, rpc_type: &str) -> Result<Value, Error> {
let rpc_url;
let secret;
if rpc_type == "owner" {
rpc_url = format!("{}://{}:{}/v2/owner", CONFIG.proto, CONFIG.ip, CONFIG.port);
secret = CONFIG.api_secret.clone();
}
else {
rpc_url = format!("{}://{}:{}/v2/foreign", CONFIG.proto, CONFIG.ip, CONFIG.port);
secret = CONFIG.foreign_api_secret.clone();
}
let client = reqwest::Client::new();
let result = client.post(rpc_url)
.body(format!("{{\"method\": \"{}\", \"params\": {}, \"id\":1}}", method, params))
.basic_auth(CONFIG.user.clone(), Some(secret))
.header("content-type", "plain/text")
.send()
.await?;
let val: Value = serde_json::from_str(&result.text().await.unwrap()).unwrap();
Ok(val)
}
// Collecting: height, sync, node_ver, proto_ver.
pub async fn get_status(dashboard: Arc<Mutex<Dashboard>>) -> Result<(), Error> {
let resp = call("get_status", "[]", "owner").await?;
let mut data = dashboard.lock().unwrap();
if resp != Value::Null {
data.height = resp["result"]["Ok"]["tip"]["height"].to_string();
data.sync = resp["result"]["Ok"]["sync_status"].as_str().unwrap().to_string();
data.node_ver = resp["result"]["Ok"]["user_agent"].as_str().unwrap().to_string();
data.proto_ver = resp["result"]["Ok"]["protocol_version"].to_string();
}
Ok(())
}
// Collecting: txns, stem.
pub async fn get_mempool(dashboard: Arc<Mutex<Dashboard>>) -> Result<(), Error> {
let resp1 = call("get_pool_size", "[]", "foreign").await?;
let resp2 = call("get_stempool_size", "[]", "foreign").await?;
let mut data = dashboard.lock().unwrap();
if resp1 != Value::Null && resp1 != Value::Null {
data.txns = resp1["result"]["Ok"].to_string();
data.stem = resp2["result"]["Ok"].to_string();
}
Ok(())
}
// Collecting: inbound, outbound.
pub async fn get_connected_peers(dashboard: Arc<Mutex<Dashboard>>) -> Result<(), Error> {
let resp = call("get_connected_peers", "[]", "owner").await?;
let mut data = dashboard.lock().unwrap();
if resp != Value::Null {
let mut inbound = 0;
let mut outbound = 0;
for peer in resp["result"]["Ok"].as_array().unwrap() {
if peer["direction"] == "Inbound" {
inbound += 1;
}
if peer["direction"] == "Outbound" {
outbound += 1;
}
}
data.inbound = inbound;
data.outbound = outbound;
}
Ok(())
}
// Collecting: supply, inflation, price_usd, price_btc, volume_usd, volume_btc, cap_usd, cap_btc.
pub async fn get_market(dashboard: Arc<Mutex<Dashboard>>) -> Result<(), Error> {
let client = reqwest::Client::new();
let result = client.get("https://api.coingecko.com/api/v3/simple/price?ids=grin&vs_currencies=usd%2Cbtc&include_24hr_vol=true")
.send()
.await?;
let val: Value = serde_json::from_str(&result.text().await.unwrap()).unwrap();
let mut data = dashboard.lock().unwrap();
if data.height.is_empty() == false {
// Calculating coin supply
// Adding +1 as block index starts with 0
let supply = (data.height.parse::<u64>().unwrap() + 1) * 60;
// 31536000 seconds in a year
let inflation = (31536000.0 / (supply as f64)) * 100.0;
data.inflation = format!("{:.2}", inflation);
data.supply = supply.to_formatted_string(&Locale::en);
// https://john-tromp.medium.com/a-case-for-using-soft-total-supply-1169a188d153
data.soft_supply = format!("{:.2}",
supply.to_string().parse::<f64>().unwrap() / 3150000000.0 * 100.0);
// Check if CoingGecko API returned error
if let Some(status) = val.get("status") {
println!("{} {}.", "[ WARNING ]".yellow(),
status["error_message"].as_str().unwrap().to_string());
} else {
data.price_usd = format!("{:.3}", val["grin"]["usd"].to_string().parse::<f64>().unwrap());
data.price_btc = format!("{:.8}", val["grin"]["btc"].to_string().parse::<f64>().unwrap());
data.volume_usd = (val["grin"]["usd_24h_vol"].to_string().parse::<f64>().unwrap() as u64)
.to_formatted_string(&Locale::en);
data.volume_btc = format!("{:.2}", val["grin"]["btc_24h_vol"].to_string().parse::<f64>()
.unwrap());
data.cap_usd = (((supply as f64) * data.price_usd.parse::<f64>().unwrap()) as u64)
.to_formatted_string(&Locale::en);
data.cap_btc = (((supply as f64) * data.price_btc.parse::<f64>().unwrap()) as u64)
.to_formatted_string(&Locale::en);
}
}
Ok(())
}
// Collecting: disk_usage.
pub fn get_disk_usage(dashboard: Arc<Mutex<Dashboard>>) -> Result<(), Error> {
let mut data = dashboard.lock().unwrap();
let grin_dir = format!("{}/.grin", std::env::var("HOME").unwrap());
data.disk_usage = format!("{:.2}", (get_size(grin_dir).unwrap() as f64) / 1000.0 / 1000.0 / 1000.0);
Ok(())
}
// Collecting: hashrate, difficulty, production cost.
pub async fn get_mining_stats(dashboard: Arc<Mutex<Dashboard>>) -> Result<(), Error> {
let difficulty_window = 60;
let height = get_current_height(dashboard.clone());
if height.is_empty() == false {
let params1 = &format!("[{}, null, null]", height)[..];
let params2 = &format!("[{}, null, null]", height.parse::<u64>().unwrap()
- difficulty_window)[..];
let resp1 = call("get_block", params1, "foreign").await?;
let resp2 = call("get_block", params2, "foreign").await?;
let mut data = dashboard.lock().unwrap();
if resp1 != Value::Null && resp2 != Value::Null {
// Calculate network difficulty
let net_diff = (resp1["result"]["Ok"]["header"]["total_difficulty"]
.to_string().parse::<u64>().unwrap()
- resp2["result"]["Ok"]["header"]["total_difficulty"]
.to_string().parse::<u64>().unwrap()) /
difficulty_window;
// https://forum.grin.mw/t/on-dual-pow-graph-rates-gps-and-difficulty/2144/52
// https://forum.grin.mw/t/difference-c31-and-c32-c33/7018/7
let hashrate = (net_diff as f64) * 42.0 / 60.0 / 16384.0;
data.hashrate = format!("{:.2}", hashrate / 1000.0);
data.difficulty = net_diff.to_string();
// Calculating G1-mini production per hour
let coins_per_hour = 1.2 / hashrate * 60.0 * 60.0;
// Calculating production cost of 1 grin
// Assuming $0,07 per kW/h
data.production_cost = format!("{:.3}", 120.0 / 1000.0 * 0.07 * (1.0 / coins_per_hour));
data.reward_ratio = format!("{:.2}", data.price_usd.parse::<f64>().unwrap()
/ data.production_cost.parse::<f64>().unwrap());
}
}
Ok(())
}
// Collecting block data for recent blocks (block_list page).
pub async fn get_block_list_data(height: &String, block: &mut Block)
-> Result<(), Error> {
// Max block weight is 40000
// One unit of weight is 32 bytes
let kernel_weight = 3.0;
let input_weight = 1.0;
let output_weight = 21.0;
if height.is_empty() == false {
let params = &format!("[{}, null, null]", height)[..];
let resp = call("get_block", params, "foreign").await.unwrap();
if resp["result"]["Ok"].is_null() == false {
block.height = resp["result"]["Ok"]["header"]["height"].to_string();
let dt: DateTime<Utc> = resp["result"]["Ok"]["header"]["timestamp"]
.as_str().unwrap().to_string().parse().unwrap();
// Utc --> human time
let duration = Duration::new((Utc::now().timestamp() - dt.timestamp()) as u64, 0);
if duration.as_secs() > 2592000 {
let string = format_duration(duration).to_string();
let (a, _b) = string.split_once(" ").unwrap();
block.time = format!("{} ago", a);
} else {
block.time = format_duration(duration).to_string();
}
for kernel in resp["result"]["Ok"]["kernels"].as_array().unwrap() {
let fee = kernel["fee"].to_string().parse::<f64>().unwrap();
block.fees += fee;
block.weight += kernel_weight;
block.ker_len = block.ker_len + 1;
}
for _input in resp["result"]["Ok"]["inputs"].as_array().unwrap() {
block.weight += input_weight;
block.in_len = block.in_len + 1;
}
for _output in resp["result"]["Ok"]["outputs"].as_array().unwrap() {
block.weight += output_weight;
block.out_len = block.out_len + 1;
}
} else {
return Ok(());
}
}
block.weight = format!("{:.2}", block.weight / 40000.0 * 100.0).parse::<f64>().unwrap();
Ok(())
}
// Collecting block data.
pub async fn get_block_data(height: &str, block: &mut Block)
-> Result<(), Error> {
// Max block weight is 40000
// One unit of weight is 32 bytes
let kernel_weight = 3.0;
let input_weight = 1.0;
let output_weight = 21.0;
if height.is_empty() == false {
let params = &format!("[{}, null, null]", height)[..];
let resp = call("get_block", params, "foreign").await?;
if resp["result"]["Ok"].is_null() == false {
block.hash = resp["result"]["Ok"]["header"]["hash"].as_str().unwrap().to_string();
block.height = resp["result"]["Ok"]["header"]["height"].to_string();
let dt: DateTime<Utc> = resp["result"]["Ok"]["header"]["timestamp"]
.as_str().unwrap().to_string().parse().unwrap();
block.time = dt.to_string();
block.version = resp["result"]["Ok"]["header"]["version"].to_string();
for kernel in resp["result"]["Ok"]["kernels"].as_array().unwrap() {
let fee = kernel["fee"].to_string().parse::<f64>().unwrap();
block.kernels.push((kernel["excess"].as_str().unwrap().to_string(),
kernel["features"].as_str().unwrap().to_string(),
(fee / 1000000000.0).to_string()));
block.fees += fee;
block.weight += kernel_weight;
}
for input in resp["result"]["Ok"]["inputs"].as_array().unwrap() {
block.inputs.push(input.as_str().unwrap().to_string());
block.weight += input_weight;
}
for output in resp["result"]["Ok"]["outputs"].as_array().unwrap() {
block.outputs.push((output["commit"].as_str().unwrap().to_string(),
output["output_type"].as_str().unwrap().to_string()));
block.weight += output_weight;
}
block.weight = format!("{:.2}", block.weight / 40000.0 * 100.0).parse::<f64>().unwrap();
block.ker_len = block.kernels.iter().count() as u64;
block.in_len = block.inputs.iter().count() as u64;
block.out_len = block.outputs.iter().count() as u64;
block.raw_data = serde_json::to_string_pretty(&resp).unwrap();
}
}
Ok(())
}
// Get block height by hash.
pub async fn get_block_header(hash: &str, height: &mut String)
-> Result<(), Error> {
let params = &format!("[null, \"{}\", null]", hash)[..];
let resp = call("get_header", params, "foreign").await.unwrap();
if resp["result"]["Ok"].is_null() == false {
*height = resp["result"]["Ok"]["height"].to_string();
}
Ok(())
}
// Get kernel.
pub async fn get_kernel(kernel: &str, height: &mut String)
-> Result<(), Error> {
let params = &format!("[\"{}\", null, null]", kernel)[..];
let resp = call("get_kernel", params, "foreign").await.unwrap();
if resp["result"]["Ok"].is_null() == false {
*height = resp["result"]["Ok"]["height"].to_string();
}
Ok(())
}
// Collecting block kernels for transactions stats.
pub async fn get_block_kernels(height: &String, blocks: &mut Vec<Block>)
-> Result<(), Error> {
if height.is_empty() == false {
let params = &format!("[{}, {}, 1440, false]", height.parse::<u64>().unwrap() - 1440,
height)[..];
let resp = call("get_blocks", params, "foreign").await.unwrap();
for resp_block in resp["result"]["Ok"]["blocks"].as_array().unwrap() {
for kernel in resp_block["kernels"].as_array().unwrap() {
let mut block = Block::new();
block.kernels.push((kernel["excess"].to_string(),
kernel["features"].as_str().unwrap().to_string(),
kernel["fee"].to_string()));
blocks.push(block);
}
}
}
Ok(())
}
// Collecting: period_1h, period_24h, fees_1h, fees_24h.
pub async fn get_txn_stats(dashboard: Arc<Mutex<Dashboard>>,
transactions: Arc<Mutex<Transactions>>) -> Result<(), Error> {
let mut blocks = Vec::<Block>::new();
let height = get_current_height(dashboard.clone());
if height.is_empty() == false {
// Collecting kernels for 1440 blocks
let _ = get_block_kernels(&height, &mut blocks).await;
if blocks.is_empty() == false {
let mut ker_count_1h = 0;
let mut ker_count_24h = 0;
let mut fees_1h = 0.0;
let mut fees_24h = 0.0;
let mut index = 0;
for block in blocks {
if index < 60 {
for kernel in block.kernels.clone() {
if kernel.1 != "Coinbase" {
ker_count_1h = ker_count_1h + 1;
fees_1h = fees_1h + kernel.2.parse::<f64>().unwrap();
}
}
}
for kernel in block.kernels {
if kernel.1 != "Coinbase" {
ker_count_24h = ker_count_24h + 1;
fees_24h = fees_24h + kernel.2.to_string().parse::<f64>().unwrap();
}
}
index = index + 1;
}
let mut txns = transactions.lock().unwrap();
txns.period_1h = ker_count_1h.to_string();
txns.period_24h = ker_count_24h.to_string();
txns.fees_1h = format!("{:.2}", fees_1h / 1000000000.0);
txns.fees_24h = format!("{:.2}", fees_24h / 1000000000.0);
}
}
Ok(())
}
// Return current block height
pub fn get_current_height(dashboard: Arc<Mutex<Dashboard>>) -> String {
let data = dashboard.lock().unwrap();
data.height.clone()
}
// Collecting recent blocks data.
pub async fn get_recent_blocks(dashboard: Arc<Mutex<Dashboard>>,
blocks: Arc<Mutex<Vec<Block>>>) -> Result<(), Error> {
let mut i = 0;
let height_str = get_current_height(dashboard.clone());
if height_str.is_empty() == false {
let height = height_str.parse::<u64>().unwrap();
let mut blocks_vec = Vec::<Block>::new();
while i < 10 {
let mut block = Block::new();
let height_index = height - i;
let _ = get_block_list_data(&height_index.to_string(), &mut block).await;
blocks_vec.push(block);
i = i + 1;
}
let mut blcks = blocks.lock().unwrap();
blcks.clear();
*blcks = blocks_vec;
}
Ok(())
}
// Collecting a specified list of blocks.
pub async fn get_block_list_by_height(height: &str, blocks: &mut Vec<Block>,
latest_height: &mut u64) -> Result<(), Error> {
let mut i = 0;
let height = height.to_string();
let resp = call("get_status", "[]", "owner").await.unwrap();
if resp != Value::Null {
*latest_height = resp["result"]["Ok"]["tip"]["height"].to_string().parse::<u64>().unwrap();
if height.is_empty() == false && height.chars().all(char::is_numeric) == true {
let mut height = height.parse::<u64>().unwrap();
if height < 10 {
height = 9;
}
while i < 10 {
let mut block = Block::new();
let _ = get_block_list_data(&(height - i).to_string(), &mut block).await;
blocks.push(block);
i = i + 1;
}
}
}
Ok(())
}

25
src/worker.rs Normal file
View File

@@ -0,0 +1,25 @@
use std::sync::{Arc, Mutex};
use reqwest::Error;
use crate::data::Dashboard;
use crate::data::Block;
use crate::data::Transactions;
use crate::requests;
// Tokio Runtime Worker.
// Collecting all the data.
pub async fn run(dash: Arc<Mutex<Dashboard>>, blocks: Arc<Mutex<Vec<Block>>>,
txns: Arc<Mutex<Transactions>>) -> Result<(), Error> {
let _ = requests::get_status(dash.clone()).await?;
let _ = requests::get_mempool(dash.clone()).await?;
let _ = requests::get_connected_peers(dash.clone()).await?;
let _ = requests::get_market(dash.clone()).await?;
requests::get_disk_usage(dash.clone())?;
let _ = requests::get_mining_stats(dash.clone()).await?;
let _ = requests::get_recent_blocks(dash.clone(), blocks.clone()).await?;
let _ = requests::get_txn_stats(dash.clone(), txns.clone()).await?;
Ok(())
}

BIN
static/icon/grin-logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 37 KiB

6314
static/scripts/bootstrap.bundle.js vendored Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,8 @@
function checkDarkMode() {
var mode = localStorage.getItem('mode');
if(mode === 'disabled') {
document.body.classList.remove("dark-mode");
document.body.classList.add("bg-light");
}
}

View File

@@ -0,0 +1,11 @@
function darkModeFunc() {
document.body.classList.toggle("dark-mode");
document.body.classList.toggle("bg-light");
var mode = localStorage.getItem('mode');
if(mode === 'disabled')
localStorage.setItem('mode', 'enabled');
else
localStorage.setItem('mode', 'disabled');
}

1
static/scripts/htmx.min.js vendored Normal file

File diff suppressed because one or more lines are too long

2078
static/styles/bootstrap-icons.css vendored Normal file

File diff suppressed because it is too large Load Diff

12063
static/styles/bootstrap.css vendored Normal file

File diff suppressed because it is too large Load Diff

Binary file not shown.

Binary file not shown.

266
static/styles/style.css Normal file
View File

@@ -0,0 +1,266 @@
.darkgray-text {
color: #636c72;
}
.darkorange-text {
color: #FF7518 !important;
}
.grey_background {
background-color: red !important;
}
a, a:link, a:visited {
color: black;
}
a:hover, a:active {
color: silver;
}
.disabled {
pointer-events: none;
color: #636c72 !important;
}
.btn-search {
border-color: silver !important;
}
button {
color: black !important;
background-color: white !important;
border-color: white !important;
border: 0;
}
button:hover,
button:active {
color: darkgrey !important;
}
h4, h5, h6, code {
color: black;
}
div.card {
color: black;
border-color: #f8f9fa;
}
.value-text {
color: black;
}
div.rp,
span.rp {
color: red;
}
div.bluechblk,
span.bluechblk {
color: blue;
}
div.toast-header,
div.toast-body {
background-color: #f2a900;
color: #14151a;
border-color: #14151a;
}
.nav-item,
.nav-link {
color: #71797E !important;
border-style: none;
}
footer {
background-color: white;
}
.dark-mode .navbar-brand {
color: silver !important;
}
.dark-mode .darkorange-text {
color: #f2a900 !important;
}
.dark-mode .darkgray-text {
color: #636c72;
}
.dark-mode .navbar-toggler {
background-color: #f2a900 !important;
}
.dark-mode input,
.dark-mode input:focus {
color: silver !important;
border-color: #1f2029 !important;
background-color: #1f2029 !important;
}
.dark-mode input::placeholder {
color: grey !important;
}
.dark-mode a {
border-color: #14151a !important;
background-color: #14151a !important;
}
.dark-mode button {
color: grey !important;
background-color: #14151a !important;
border-color: #14151a !important;
border: 0;
}
.dark-mode .btn-search {
background-color: #1f2029 !important;
border-color: #1f2029 !important;
}
.dark-mode .btn-close {
background-color: silver !important;
}
.dark-mode button:focus { outline:0; }
.dark-mode button:hover,
.dark-mode button:active {
color: silver !important;
}
.dark-mode .value-text {
color: silver !important;
}
.dark-mode span .badge {
badge-color: red;
}
.dark-mode {
background-color: #1f2029 !important;
color: silver !important;
}
.dark-mode div.card,
.dark-mode a.card {
background-color: #14151a;
color: silver;
border-color: #1f2029;
}
.dark-mode div.toast-header,
.dark-mode div.toast-body {
background-color: black;
color: silver;
border-color: #14151a;
}
.dark-mode div.bluechblk,
.dark-mode span.bluechblk {
color: royalblue;
}
.dark-mode div.rp,
.dark-mode span.rp {
color: lightcoral;
}
.dark-mode .bg-style {
background-color: #14151a !important;
}
.dark-mode a,
.dark-mode a:link,
.dark-mode a:visited,
.dark-mode a:active {
color: silver;
}
.dark-mode a:hover {
color: white;
}
.dark-mode .nav-tabs {
border-color: #14151a;
}
.dark-mode .nav-tabs
.dark-mode .nav-item {
border-color: #14151a;
}
.dark-mode .nav-tabs,
.dark-mode .nav-item,
.dark-mode .nav-link {
color: #636c72 !important;
border-style: none;
}
.dark-mode .nav-link.active {
background-color: #14151a;
color: #636c72 !important;
border-style: none;
}
.dark-mode .modal-header {
background-color: #14151a;
border-style: none;
}
.dark-mode .modal-body {
background-color: #1f2029;
border-style: none;
}
.dark-mode .modal-content {
border-style: none;
}
.dark-mode .pagination .page-item .page-link {
background-color: #14151a;
border-color: silver;
}
.dark-mode .navbar-nav > li > .dropdown-menu,
.dark-mode .navbar-nav > li > .dropdown-menu a,
.dark-mode .navbar-nav > li > .dropdown-menu a:link,
.dark-mode .navbar-nav > li > .dropdown-menu a:hover {
color: silver;
background-color: #1f2029;
}
.dark-mode .dropdown-menu {
background-color: #1f2029;
}
.dark-mode .dropdown-menu .dropdown-item {
color: silver;
background-color: #343a40;
}
.dark-mode .close {
color: silver;
}
.dark-mode hr {
border-color: #1f2029;
}
.dark-mode h4,
.dark-mode h5,
.dark-mode h6,
.dark-mode p,
.dark-mode code,
.dark-mode pre {
color: silver;
}
.dark-mode footer {
background-color: #14151a;
}

228
templates/base.html.tera Normal file
View File

@@ -0,0 +1,228 @@
<!doctype html>
<html>
<head>
<title>Grin Blockchain Explorer</title>
<meta property="og:site_name" content="Grincoin.org (GRIN) Blockchain Explorer" />
<meta property="og:url" content="https://grincoin.org" />
<meta property="og:type" content="website" />
<meta property="og:title" content="Grin Blockchain Explorer" />
<meta property="og:description" content="Grincoin.org website allows you to explore Grin blockchain." />
<meta name="description" content="Grincoin.org website allows you to explore Grin blockchain." />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link rel="stylesheet" type= "text/css" href="/static/styles/style.css" />
<link rel= "stylesheet" type= "text/css" href= "/static/styles/bootstrap.css" />
<link rel= "stylesheet" type= "text/css" href= "/static/styles/bootstrap-icons.css" />
<link rel="shortcut icon" type="image/png" href="/static/icon/grin-logo.png" />
<script src="/static/scripts/darkmode.js"></script>
<script src="/static/scripts/darkmode_toggle.js"></script>
<script src="/static/scripts/bootstrap.bundle.js"></script>
<script src="/static/scripts/htmx.min.js"></script>
</head>
<body class="d-flex flex-column min-vh-100 dark-mode">
<nav class="navbar bg-style navbar-expand-lg shadow-sm py-0">
<div class="container-fluid">
<code><a class="navbar-brand fs-2" href="/"><svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" viewBox="0 0 191 191" height="35" width="35" style="enable-background:new 0 0 191 191;" xml:space="preserve"><style type="text/css">.st0{fill:#f2a900;}</style><circle cx="95.5" cy="95.5" r="95.5"/><circle class="st0" cx="95.3" cy="95.5" r="85.3"/><path d="M135.7,63.5c-2-4.6-3.9-13.6-9.1-15.6c-6.7-2.6-10.9,14.3-11.9,18.6h-1c-1.7-7.3-4-17.7-12-20c-3.7,15.6,3.9,32.5,11,46
c7.6-3.6,11.3-15,12-23h1l8,24c6.4-1.8,8.7-8.2,11-14c5-12.4,9.2-27.7,6-41C141.1,41.2,138.2,55,135.7,63.5 M39.7,95.5
c8.6-4.1,12.7-17.1,14-26h1c1,4.3,3.7,15.4,9.1,16.1c6.8,0.9,10.4-14.3,10.9-19.1h1c2.2,7.5,4.5,17.3,12,21
c2.8-11.7-0.5-24.1-4.8-35c-1.1-2.8-3.3-10.4-7.2-10.4c-6.3,0-9.9,16.7-11,21.4h-1l-8-24C41.6,46.2,34,82,39.7,95.5 M24.7,107.5
c10.4,45.6,58.6,71.4,102,52.1c15.8-7,28.3-19.5,35.5-35.1c1.9-4,5.5-11.4,3.4-16.7c-2.1-5.3-22.8,3.4-27.9,5.6
c-0.7,0.3-1.2,1-1.4,1.8c-0.3,2.2,1.2,4.3,3.4,4.6c0.1,0,0.2,0,0.3,0l8.7,0.7c-15.7,30.4-58.1,43.8-88,19.5
C54,134.6,48,128.2,44,120.5c-1.9-3.7-3.7-8.4-7-11C33.8,107.1,29,106.3,24.7,107.5L24.7,107.5z"/></svg></a></code>
<button class="navbar-toggler my-3" type="button" data-bs-toggle="collapse" data-bs-target="#navbarCollapse" aria-controls="navbarCollapse" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarCollapse">
<ul class="navbar-nav me-auto">
{% if route == "index" %}
<li class="nav-item">
<a class="nav-link" href="/"><div class="darkorange-text"><i class="bi bi-speedometer"></i> Dashboard</div></a>
</li>
<li class="nav-item">
<a class="nav-link" href="/block_list"><i class="bi bi-box"></i> Blocks</a>
</li>
{% elif route == "block_list" or route == "block_list_by_height" %}
<li class="nav-item">
<a class="nav-link" href="/"><i class="bi bi-speedometer"></i> Dashboard</a>
</li>
<li class="nav-item">
<a class="nav-link" href="/block_list"><div class="darkorange-text"><i class="bi bi-box"></i> Blocks</div></a>
</li>
{% else %}
<li class="nav-item">
<a class="nav-link" href="/"><i class="bi bi-speedometer"></i> Dashboard</a>
</li>
<li class="nav-item">
<a class="nav-link" href="/block_list"><i class="bi bi-box"></i> Blocks</a>
</li>
{% endif %}
</ul>
<div class="row">
<div class="col-12">
<form class="input-group my-3" role="search" action="/search" method="POST" autocomplete="off">
<input class="form-control ms-0 me-2" type="search" placeholder="Explore Grin Network" aria-label="Search" name="search" required>
<button class="btn btn-outline-secondary btn-search" type="submit">
<i class="bi bi-search"></i>
</button>
</form>
</div>
</div>
</div>
</div>
</nav>
<div class="container-fluid">
<br>
{% block content %}{% endblock content %}
</div>
<footer class="shadow">
<code>
<br>
<div class="container-fluid">
<div class="card-group">
<div class="card border-0">
<div class="card-body" align="left">
<div class="darkorange-text">Community</div>
<br>
<div class="d-flex justify-content-start">
<div class="value-text">
<a class="text-decoration-none" href="https://grin.mw/">Grin.MW</a>
</div>
</div>
<br>
<div class="d-flex justify-content-start">
<div class="value-text">
<a class="text-decoration-none" href="https://forum.grin.mw/">Forum</a>
</div>
</div>
<br>
<div class="d-flex justify-content-start">
<div class="value-text">
<a class="text-decoration-none" href="https://www.grincc.mw/">Council</a>
</div>
</div>
<br>
<div class="d-flex justify-content-start">
<div class="value-text">
<a class="text-decoration-none" href="https://keybase.io/team/grincoin">Keybase</a>
</div>
</div>
<br>
<div class="d-flex justify-content-start">
<div class="me-3">
<a class="text-decoration-none" href="https://t.me/grinprivacy">
<h2><i class="bi bi-telegram"></i></h2>
</a>
</div>
<div class="me-3">
<a class="text-decoration-none" href="https://discord.gg/5p4vCQY9km">
<h2><i class="bi bi-discord"></i></h2>
</a>
</div>
<div class="me-3">
<a class="text-decoration-none" href="https://www.reddit.com/r/grincoin/">
<h2><i class="bi bi-reddit"></i></h2>
</a>
</div>
</div>
</div>
</div>
<div class="card border-0">
<div class="card-body" align="left">
<div class="darkorange-text">Resources</div>
<br>
<div class="d-flex justify-content-start">
<div class="value-text">
<a class="text-decoration-none" href="https://docs.grin.mw/about-grin/story/">
About Grin
</a>
</div>
</div>
<br>
<div class="d-flex justify-content-start">
<div class="value-text">
<a class="text-decoration-none" href="https://phyro.github.io/what-is-grin">
What Is Grin
</a>
</div>
</div>
<br>
<div class="d-flex justify-content-start">
<div class="value-text">
<a class="text-decoration-none" href="https://phyro.github.io/grinvestigation/why_grin.html">Why Grin</a>
</div>
</div>
<br>
<div class="d-flex justify-content-start">
<div class="value-text">
<a class="text-decoration-none" href="https://grinnode.live/">Grinnode</a>
</div>
</div>
<br>
<div class="d-flex justify-content-start">
<div class="value-text">
<a class="text-decoration-none" href="https://grinflation.com/">Grinflation</a>
</div>
</div>
</div>
</div>
<div class="card border-0">
<div class="card-body" align="left">
<div class="darkorange-text">Build</div>
<br>
<div class="d-flex justify-content-start">
<div class="value-text">
<a class="text-decoration-none" href="https://github.com/mimblewimble/grin">
Github
</a>
</div>
</div>
<br>
<div class="d-flex justify-content-start">
<div class="value-text">
<a class="text-decoration-none" href="https://docs.grin.mw/">Documentation</a>
</div>
</div>
<br>
<div class="d-flex justify-content-start">
<div class="value-text">
<a class="text-decoration-none" href="https://github.com/mimblewimble/docs/wiki/A-Brief-History-of-MimbleWimble-White-Paper">Whitepaper</a>
</div>
</div>
</div>
</div>
</div>
<br>
<div class="row">
<div class="col d-flex justify-content-center">
<span class="custom-control custom-switch custom-control-inline ms-0">
<button class="btn-sm btn-outline-secondary" onclick="darkModeFunc()">
<h3><i class="bi bi-brightness-low"></i></h3>
</button>
</span>
</div>
</div>
<div class="row">
<div class="col d-flex justify-content-center" style="color:grey">
Powered by CoinGecko
</div>
</div>
</div>
</div>
<br>
</code>
</footer>
<script>
checkDarkMode()
</script>
</body>
</html>

View File

@@ -0,0 +1,128 @@
{% extends "base" %}
{% block content %}
<code>
<div class="card">
<div class="card-body" align="left">
<div class="darkorange-text"><i class="bi bi-box"></i> BLOCK {{ block.height }}</div>
<br>
<div class="d-flex justify-content-between">
<div class="value-text">Time&nbsp;</div>
<div class="value-text text-end">{{ block.time }}</div>
</div>
<br>
<div class="d-flex justify-content-between">
<div class="value-text">Hash&nbsp;</div>
<div class="value-text text-break text-end">{{ block.hash }}</div>
</div>
<br>
<div class="d-flex justify-content-between">
<div class="value-text">Fees&nbsp;</div>
<div class="value-text text-end">ツ {{ block.fees / 1000000000.0 }}</div>
</div>
<br>
<div class="d-flex justify-content-between">
<div class="value-text">Weight&nbsp;</div>
<div class="value-text text-end">{{ block.weight }} %</div>
</div>
<br>
<div class="d-flex justify-content-between">
<div class="value-text">Version&nbsp;</div>
<div class="value-text text-end">{{ block.version }}</div>
</div>
</div>
</div>
<br>
<div class="card">
<div class="card-body mb-2" align="left">
<div class="darkorange-text"><i class="bi bi-receipt"></i> TRANSACTIONS</div>
<br>
<div class="value-text" align="center">Kernels ({{ block.ker_len }})</div>
{% for i in range(end=block.ker_len) %}
<br>
<div class="row">
<!-- kernels[i][0] - Hash
kernels[i][1] - Type
kernels[i][2] - Fee -->
{% if block.kernels[i][1] == "Coinbase" %}
<div class="col-sm value-text" align="left">
<span class="badge bg-dark-subtle text-black px-2 py-1">ツ {{ block.kernels[i][2] }}</span> <span class="badge bg-info text-black px-2 py-1">{{ block.kernels[i][1] }}&nbsp;<i class="bi bi-hammer"></i></span>
</div>
{% else %}
<div class="col-sm value-text" align="left">
<span class="badge bg-dark-subtle text-black px-2 py-1">ツ {{ block.kernels[i][2] }}</span> <span class="badge bg-dark-subtle text-black px-2 py-1">{{ block.kernels[i][1] }}</span>
</div>
{% endif %}
<div class="col-sm value-text">{{ block.kernels[i][0] }}</div>
<div class="col-sm value-text"></div>
</div>
{% endfor %}
</div>
<div class="card-body mb-2" align="center">
<div class="d-flex flex-row justify-content-center">
<i class="bi bi-asterisk me-4"></i>
<i class="bi bi-asterisk me-4"></i>
<i class="bi bi-asterisk"></i>
</div>
</div>
<div class="card-body mb-1" align="center">
<div class="value-text">Inputs ({{ block.in_len }})</div>
{% for i in range(end=block.in_len) %}
<br>
<div class="row">
<div class="col-sm"></div>
<div class="col-sm" align="left">{{ block.inputs[i] }}</div>
<div class="col-sm"></div>
</div>
{% endfor %}
</div>
<div class="card-body mb-1" align="center">
<h2><i class="bi bi-arrow-down-circle"></i></h2>
</div>
<div class="card-body" align="center">
<div class="value-text">Outputs ({{ block.out_len }})</div>
{% for i in range(end=block.out_len) %}
<br>
<div class="row">
<!-- outputs[i][0] - Hash
outputs[i][1] - Type -->
{% if block.outputs[i][1] == "Coinbase" %}
<div class="col-sm value-text" align="left">
<span class="badge bg-info text-black px-2 py-1">{{ block.outputs[i][1] }}&nbsp;<i class="bi bi-hammer"></i></span>
</div>
{% else %}
<div class="col-sm value-text" align="left">
<span class="badge bg-dark-subtle text-black px-2 py-1">{{ block.outputs[i][1] }}</span>
</div>
{% endif %}
<div class="col-sm value-text" align="left">{{ block.outputs[i][0] }}</div>
<div class="col-sm value-text"></div>
</div>
{% endfor %}
</div>
</div>
<br>
<div class="card">
<div class="card-body" align="left">
<div class="darkorange-text"><i class="bi bi-layout-text-sidebar-reverse"></i> RAW DATA</div>
<br>
<div class="value-text">{{ block.raw_data }}</div>
</div>
</div>
<br>
</code>
{% endblock %}

View File

@@ -0,0 +1,264 @@
{% extends "base" %}
{% block content %}
<code>
<div class="d-none d-md-block mb-4"> <!-- Show on >= md screens -->
<div class="card-group">
<div class="card rounded-0">
<div class="card-body">
<div class="darkorange-text">
HEIGHT
</div>
</div>
</div>
<div class="card rounded-0">
<div class="card-body">
<div class="darkorange-text">
AGE
</div>
</div>
</div>
<div class="card rounded-0">
<div class="card-body">
<div class="darkorange-text">
KERNELS
</div>
</div>
</div>
<div class="card rounded-0">
<div class="card-body">
<div class="darkorange-text">
INPUTS
</div>
</div>
</div>
<div class="card rounded-0">
<div class="card-body">
<div class="darkorange-text">
OUTPUTS
</div>
</div>
</div>
<div class="card rounded-0">
<div class="card-body">
<div class="darkorange-text">
FEES
</div>
</div>
</div>
<div class="card rounded-0">
<div class="card-body">
<div class="darkorange-text">
WEIGHT
</div>
</div>
</div>
</div>
{% for i in range(end=10) %}
<div class="card-group rounded-0">
<div class="card rounded-0 mt-1">
<div class="card-body">
{% if route == "block_list_by_height" %}
<div class="value-text">
<a class="text-decoration-none" href="/block/{{ blocks[i].height }}">
{{ blocks[i].height }}
</a>
</div>
{% else %}
<div class="value-text" hx-get="/rpc/block/link" hx-vals='{"count": "{{ i }}"}' hx-trigger="load, every 10s"></div>
{% endif %}
</div>
</div>
<div class="card rounded-0 mt-1">
<div class="card-body">
{% if route == "block_list_by_height" %}
<div class="value-text">{{ blocks[i].time }}</div>
{% else %}
<div class="value-text" hx-get="/rpc/block/time" hx-vals='{"count": "{{ i }}"}' hx-trigger="load, every 10s"></div>
{% endif %}
</div>
</div>
<div class="card rounded-0 mt-1">
<div class="card-body">
{% if route == "block_list_by_height" %}
<div class="value-text">{{ blocks[i].ker_len }}</div>
{% else %}
<div class="value-text" hx-get="/rpc/block/kernels" hx-vals='{"count": "{{ i }}"}' hx-trigger="load, every 10s"></div>
{% endif %}
</div>
</div>
<div class="card rounded-0 mt-1">
<div class="card-body">
{% if route == "block_list_by_height" %}
<div class="value-text">{{ blocks[i].in_len }}</div>
{% else %}
<div class="value-text" hx-get="/rpc/block/inputs" hx-vals='{"count": "{{ i }}"}' hx-trigger="load, every 10s"></div>
{% endif %}
</div>
</div>
<div class="card rounded-0 mt-1">
<div class="card-body">
{% if route == "block_list_by_height" %}
<div class="value-text">{{ blocks[i].out_len }}</div>
{% else %}
<div class="value-text" hx-get="/rpc/block/outputs" hx-vals='{"count": "{{ i }}"}' hx-trigger="load, every 10s"></div>
{% endif %}
</div>
</div>
<div class="card rounded-0 mt-1">
<div class="card-body">
{% if route == "block_list_by_height" %}
<div class="value-text">ツ {{ blocks[i].fees / 1000000000.0 }}</div>
{% else %}
<div class="value-text" hx-get="/rpc/block/fees" hx-vals='{"count": "{{ i }}"}' hx-trigger="load, every 10s"></div>
{% endif %}
</div>
</div>
<div class="card rounded-0 mt-1">
<div class="card-body">
{% if route == "block_list_by_height" %}
<div class="value-text">{{ blocks[i].weight }} %</div>
{% else %}
<div class="value-text" hx-get="/rpc/block/weight" hx-vals='{"count": "{{ i }}"}' hx-trigger="load, every 10s"></div>
{% endif %}
</div>
</div>
</div>
{% endfor %}
</div>
<div class="d-md-none mb-4"> <!-- Show on < md screens-->
{% for i in range(end=10) %}
<div class="card mb-3">
<div class="card-body" align="left">
<div class="d-flex justify-content-between">
<div class="value-text">Block</div>
{% if route == "block_list_by_height" %}
<div class="value-text">
<a class="text-decoration-none darkorange-text" href="/block/{{ blocks[i].height }}">
{{ blocks[i].height }}
</a>
</div>
{% else %}
<div class="value-text text-end" hx-get="/rpc/block/link_color" hx-vals='{"count": "{{ i }}"}' hx-trigger="load, every 10s"></div>
{% endif %}
</div>
<br>
<div class="d-flex justify-content-between">
<div class="value-text">Age</div>
{% if route == "block_list_by_height" %}
<div class="value-text">{{ blocks[i].time }}</div>
{% else %}
<div class="value-text text-end" hx-get="/rpc/block/time" hx-vals='{"count": "{{ i }}"}' hx-trigger="load, every 10s"></div>
{% endif %}
</div>
<br>
<div class="d-flex justify-content-between">
<div class="value-text">Kernels</div>
{% if route == "block_list_by_height" %}
<div class="value-text">{{ blocks[i].ker_len }}</div>
{% else %}
<div class="value-text text-end" hx-get="/rpc/block/kernels" hx-vals='{"count": "{{ i }}"}' hx-trigger="load, every 10s"></div>
{% endif %}
</div>
<br>
<div class="d-flex justify-content-between">
<div class="value-text">Inputs</div>
{% if route == "block_list_by_height" %}
<div class="value-text">{{ blocks[i].in_len }}</div>
{% else %}
<div class="value-text text-end" hx-get="/rpc/block/inputs" hx-vals='{"count": "{{ i }}"}' hx-trigger="load, every 10s"></div>
{% endif %}
</div>
<br>
<div class="d-flex justify-content-between">
<div class="value-text">Outputs</div>
{% if route == "block_list_by_height" %}
<div class="value-text">{{ blocks[i].out_len }}</div>
{% else %}
<div class="value-text text-end" hx-get="/rpc/block/outputs" hx-vals='{"count": "{{ i }}"}' hx-trigger="load, every 10s"></div>
{% endif %}
</div>
<br>
<div class="d-flex justify-content-between">
<div class="value-text">Fees</div>
{% if route == "block_list_by_height" %}
<div class="value-text">ツ {{ blocks[i].fees / 1000000000.0 }}</div>
{% else %}
<div class="value-text text-end" hx-get="/rpc/block/fees" hx-vals='{"count": "{{ i }}"}' hx-trigger="load, every 10s"></div>
{% endif %}
</div>
<br>
<div class="d-flex justify-content-between">
<div class="value-text">Weight</div>
{% if route == "block_list_by_height" %}
<div class="value-text">{{ blocks[i].weight }} %</div>
{% else %}
<div class="value-text text-end" hx-get="/rpc/block/weight" hx-vals='{"count": "{{ i }}"}' hx-trigger="load, every 10s"></div>
{% endif %}
</div>
</div>
</div>
{% endfor %}
</div>
{% if route == "block_list_by_height" %}
<div class="d-flex justify-content-center sticky-bottom mb-3">
<div class="me-5">
{% if height >= (index + 10) %}
<a class="text-decoration-none" href="/block_list/{{ index + 10 }}">
{% elif height >= (index) and height < (index + 10) %}
<a class="text-decoration-none" href="/block_list/{{ height }}">
{% endif %}
<h2><i class="bi bi-arrow-left-square"></i></h2>
</a>
</div>
<div class="me-5">
<a class="text-decoration-none" href="/block_list">
<h2><i class="bi bi-house"></i></h2>
</a>
</div>
<div>
{% if index >= 20 %}
<a class="text-decoration-none" href="/block_list/{{ index - 10 }}">
{% elif index >= 10 and index < 20 %}
<a class="text-decoration-none" href="/block_list/{{ 9 }}">
{% else %}
<a class="text-decoration-none disabled" href="">
{% endif %}
<h2><i class="bi bi-arrow-right-square"></i></h2>
</a>
</div>
</div>
{% else %}
<div class="d-flex justify-content-center sticky-bottom mb-3">
<div class="me-5">
<a class="text-decoration-none disabled" href="">
<h2><i class="bi bi-arrow-left-square"></i></h2>
</a>
</div>
<div class="me-5">
<a class="text-decoration-none" href="/block_list">
<h2><i class="bi bi-house"></i></h2>
</a>
</div>
<div>
<div hx-get="/rpc/block_list/index" hx-trigger="load, every 10s"></div>
</div>
</div>
{% endif %}
</code>
{% endblock %}

23
templates/error.html.tera Normal file
View File

@@ -0,0 +1,23 @@
{% extends "base" %}
{% block content %}
<code>
<div class="card">
<div class="card-body">
<h4>No results found.</h4><br>
Explorer supports requests by block number, block hash or kernel hash.<br><br>
Examples:<br>
Block number - <a class="text-decoration-none" href="/block/2765726">2765726</a><br>
Block hash - <a class="text-decoration-none" href="/hash/0000fc4d93e5717579b955ab840165d96603f009804a228be22da76f6f906a3c">0000fc4d93e5717579b955ab840165d96603f009804a228be22da76f6f906a3c</a><br>
Kernel hash - <a class="text-decoration-none" href="/kernel/084caeb931b7e8cb73d6419ea74ea157a3cef19f6e9307108a8a808df58437a4ef">084caeb931b7e8cb73d6419ea74ea157a3cef19f6e9307108a8a808df58437a4ef</a>
</div>
</div>
</code>
<br>
{% endblock %}

427
templates/index.html.tera Normal file
View File

@@ -0,0 +1,427 @@
{% extends "base" %}
{% block content %}
<code>
<div class="d-none d-md-block"> <!-- Show on >= md screens -->
<div class="card-group mb-2">
<div class="card me-2">
<div class="card-body" align="left">
<div class="darkorange-text"><i class="bi bi-cash-coin"></i> PRICE</div>
<br>
<div class="d-flex justify-content-between">
<div class="value-text">GRIN_BTC&nbsp;</div><div class="value-text text-end" hx-get="/rpc/price/btc" hx-trigger="load, every 10s"></div>
</div>
<br>
<div class="d-flex justify-content-between">
<div class="value-text">GRIN_USD&nbsp;</div><div class="value-text text-end" hx-get="/rpc/price/usd" hx-trigger="load, every 10s"></div>
</div>
<br>
<div class="d-flex justify-content-between">
<div class="value-text">24H Volume (USD)&nbsp;</div><div class="value-text text-end" hx-get="/rpc/market/volume_usd" hx-trigger="load, every 10s"></div>
</div>
<br>
<div class="d-flex justify-content-between">
<div class="value-text">24H Volume (BTC)&nbsp;</div><div class="value-text text-end" hx-get="/rpc/market/volume_btc" hx-trigger="load, every 10s"></div>
</div>
</div>
</div>
<div class="card">
<div class="card-body" align="left">
<div class="darkorange-text"><i class="bi bi-bank"></i> MARKET</div>
<br>
<div class="d-flex justify-content-between">
<div class="value-text">Yearly Inflation Rate&nbsp;</div><div class="value-text text-end" hx-get="/rpc/inflation/rate" hx-trigger="load, every 10s"> %</div>
</div>
<br>
<div class="d-flex justify-content-between">
<div class="value-text">Coin Supply&nbsp;</div><div class="value-text text-end" hx-get="/rpc/market/supply" hx-trigger="load, every 10s"></div>
</div>
<br>
<div class="d-flex justify-content-between">
<div class="value-text">Soft Total Supply
<!-- Button trigger soft supply explanation modal -->
<button class="btn-sm shadow-none" data-bs-toggle="modal" data-bs-target="#soft_sup">
<i class="bi bi-question-circle"></i>
</button>
&nbsp;</div><div class="value-text text-end" hx-get="/rpc/market/soft_supply" hx-trigger="load, every 10s"></div>
</div>
<br>
<div class="d-flex justify-content-between">
<div class="value-text">Market Cap (USD)&nbsp;</div><div class="value-text text-end" hx-get="/rpc/market/cap_usd" hx-trigger="load, every 10s"></div>
</div>
<br>
<div class="d-flex justify-content-between">
<div class="value-text">Market Cap (BTC)&nbsp;</div><div class="value-text text-end" hx-get="/rpc/market/cap_btc" hx-trigger="load, every 10s"></div>
</div>
</div>
</div>
</div>
<div class="card-group mb-2">
<div class="card me-2">
<div class="card-body" align="left">
<div class="d-flex justify-content-between">
<div class="darkorange-text"><i class="bi bi-grid"></i> BLOCKCHAIN</div>
</div>
<br>
<div class="d-flex justify-content-between">
<div class="value-text">Size&nbsp;</div><div class="value-text text-end" hx-get="/rpc/disk/usage" hx-trigger="load, every 10s"></div>
</div>
<br>
<div class="d-flex justify-content-between">
<div class="value-text">Block Height&nbsp;</div><div class="value-text text-end" hx-get="/rpc/block/latest" hx-trigger="load, every 10s"></div>
</div>
<br>
<div class="d-flex justify-content-between">
<div class="value-text">Time Since Last Block&nbsp;</div><div class="value-text text-end" hx-get="/rpc/block/time_since_last" hx-trigger="load, every 10s"></div>
</div>
</div>
</div>
<div class="card me-2">
<div class="card-body" align="left">
<div class="darkorange-text"><i class="bi bi-hammer"></i> MINING</div>
<br>
<div class="d-flex justify-content-between">
<div class="value-text">Hashrate (1440 Blocks)&nbsp;</div><div class="value-text text-end" hx-get="/rpc/network/hashrate" hx-trigger="load, every 10s"> KG/s</div>
</div>
<br>
<div class="d-flex justify-content-between">
<div class="value-text">Difficulty&nbsp;</div><div class="value-text text-end" hx-get="/rpc/network/difficulty" hx-trigger="load, every 10s"></div>
</div>
<br>
<div class="d-flex justify-content-between">
<div class="value-text">Block Reward&nbsp;</div><div class="value-text text-end">ツ 60</div>
</div>
<br>
<div class="d-flex justify-content-between">
<div class="value-text">Estimated Mining Cost
<!-- Button trigger mining cost explanation modal -->
<button type="button" class="btn-sm" data-bs-toggle="modal" data-bs-target="#mining_cost">
<i class="bi bi-question-circle"></i>
</button>
&nbsp;</div><div class="value-text text-end" hx-get="/rpc/mining/production_cost" hx-trigger="load, every 10s"></div>
</div>
<br>
<div class="d-flex justify-content-between">
<div class="value-text">Reward/Cost Ratio
<!-- Button trigger ratio explanation modal -->
<button type="button" class="btn-sm" data-bs-toggle="modal" data-bs-target="#ratio">
<i class="bi bi-question-circle"></i>
</button>
&nbsp;</div><div class="value-text text-end" hx-get="/rpc/mining/reward_ratio" hx-trigger="load, every 10s"></div>
</div>
</div>
</div>
<div class="card">
<div class="card-body" align="left">
<div class="darkorange-text"><i class="bi bi-speedometer2"></i> TRANSACTIONS & FEES</div>
<br>
<div class="d-flex justify-content-between">
<div class="value-text">1H Period (60 Blocks)&nbsp;</div><div class="value-text text-end" hx-get="/rpc/txns/count_1h" hx-trigger="load, every 10s"></div>
</div>
<br>
<div class="d-flex justify-content-between">
<div class="value-text">24H Period (1440 Blocks)&nbsp;</div><div class="value-text text-end" hx-get="/rpc/txns/count_24h" hx-trigger="load, every 10s"></div>
</div>
</div>
</div>
</div>
<div class="card-group mb-4">
<div class="card me-2">
<div class="card-body" align="left">
<div class="darkorange-text"><i class="bi bi-receipt"></i> MEMPOOL</div>
<br>
<div class="d-flex justify-content-between">
<div class="value-text">Transactions&nbsp;</div><div class="value-text text-end" hx-get="/rpc/mempool/txns" hx-trigger="load, every 10s"></div>
</div>
<br>
<div class="d-flex justify-content-between">
<div class="value-text">Stem&nbsp;</div><div class="value-text text-end" hx-get="/rpc/mempool/stem" hx-trigger="load, every 10s"></div>
</div>
</div>
</div>
<div class="card me-2">
<div class="card-body" align="left">
<div class="darkorange-text"><i class="bi bi-diagram-3"></i> CONNECTIONS</div>
<br>
<div class="d-flex justify-content-between">
<div class="value-text">Inbound&nbsp;</div><div class="value-text text-end" hx-get="/rpc/peers/inbound" hx-trigger="load, every 10s"></div>
</div>
<br>
<div class="d-flex justify-content-between">
<div class="value-text">Outbound&nbsp;</div><div class="value-text text-end" hx-get="/rpc/peers/outbound" hx-trigger="load, every 10s"></div>
</div>
</div>
</div>
<div class="card">
<div class="card-body" align="left">
<div class="darkorange-text"><i class="bi bi-pc-display-horizontal"></i> NODE</div>
<br>
<div class="d-flex justify-content-between">
<div class="value-text">Version&nbsp;</div><div class="value-text text-end">{{ node_ver }}</div>
</div>
<br>
<div class="d-flex justify-content-between">
<div class="value-text me-1">Protocol&nbsp;</div><div class="value-text text-end">{{ proto_ver }}</div>
</div>
<br>
<div class="d-flex justify-content-between">
<div class="value-text me-1">Sync Status&nbsp;</div><div class="value-text text-end" hx-get="/rpc/sync/status" hx-trigger="load, every 10s"></div>
</div>
</div>
</div>
</div>
</div>
<div class="d-md-none"> <!-- Show on < md screens-->
<div class="card mb-3">
<div class="card-body" align="left">
<div class="darkorange-text"><i class="bi bi-cash-coin"></i> PRICE</div>
<br>
<div class="d-flex justify-content-between">
<div class="value-text">GRIN_BTC&nbsp;</div><div class="value-text text-end" hx-get="/rpc/price/btc" hx-trigger="load, every 10s"></div>
</div>
<br>
<div class="d-flex justify-content-between">
<div class="value-text">GRIN_USD&nbsp;</div><div class="value-text text-end" hx-get="/rpc/price/usd" hx-trigger="load, every 10s"></div>
</div>
<br>
<div class="d-flex justify-content-between">
<div class="value-text">24H Volume (USD)&nbsp;</div><div class="value-text text-end" hx-get="/rpc/market/volume_usd" hx-trigger="load, every 10s"></div>
</div>
<br>
<div class="d-flex justify-content-between">
<div class="value-text">24H Volume (BTC)&nbsp;</div><div class="value-text text-end" hx-get="/rpc/market/volume_btc" hx-trigger="load, every 10s"></div>
</div>
</div>
</div>
<div class="card mb-3">
<div class="card-body" align="left">
<div class="darkorange-text"><i class="bi bi-bank"></i> MARKET</div>
<br>
<div class="d-flex justify-content-between">
<div class="value-text">Yearly Inflation Rate&nbsp;</div><div class="value-text text-end" hx-get="/rpc/inflation/rate" hx-trigger="load, every 10s"> %</div>
</div>
<br>
<div class="d-flex justify-content-between">
<div class="value-text">Coin Supply&nbsp;</div><div class="value-text text-end" hx-get="/rpc/market/supply" hx-trigger="load, every 10s"></div>
</div>
<br>
<div class="d-flex justify-content-between">
<div class="value-text">Soft Total Supply
<!-- Button trigger soft supply explanation modal -->
<button type="button" class="btn-sm" data-bs-toggle="modal" data-bs-target="#soft_sup">
<i class="bi bi-question-circle"></i>
</button>
&nbsp;</div><div class="value-text text-end" hx-get="/rpc/market/soft_supply" hx-trigger="load, every 10s"></div>
</div>
<br>
<div class="d-flex justify-content-between">
<div class="value-text">Market Cap (USD)&nbsp;</div><div class="value-text text-end" hx-get="/rpc/market/cap_usd" hx-trigger="load, every 10s"></div>
</div>
<br>
<div class="d-flex justify-content-between">
<div class="value-text">Market Cap (BTC)&nbsp;</div><div class="value-text text-end" hx-get="/rpc/market/cap_btc" hx-trigger="load, every 10s"></div>
</div>
</div>
</div>
<div class="card mb-3">
<div class="card-body" align="left">
<div class="d-flex justify-content-between">
<div class="darkorange-text"><i class="bi bi-grid"></i> BLOCKCHAIN</div>
</div>
<br>
<div class="d-flex justify-content-between">
<div class="value-text">Size&nbsp;</div><div class="value-text text-end" hx-get="/rpc/disk/usage" hx-trigger="load, every 10s"></div>
</div>
<br>
<div class="d-flex justify-content-between">
<div class="value-text">Block Height&nbsp;</div><div class="value-text text-end" hx-get="/rpc/block/latest" hx-trigger="load, every 10s"></div>
</div>
<br>
<div class="d-flex justify-content-between">
<div class="value-text">Time Since Last Block&nbsp;</div><div class="value-text text-end" hx-get="/rpc/block/time_since_last" hx-trigger="load, every 10s"></div>
</div>
</div>
</div>
<div class="card mb-3">
<div class="card-body" align="left">
<div class="darkorange-text"><i class="bi bi-hammer"></i> MINING</div>
<br>
<div class="d-flex justify-content-between">
<div class="value-text">Hashrate (1440 Blocks)&nbsp;</div><div class="value-text text-end" hx-get="/rpc/network/hashrate" hx-trigger="load, every 10s"> KG/s</div>
</div>
<br>
<div class="d-flex justify-content-between">
<div class="value-text">Difficulty&nbsp;</div><div class="value-text text-end" hx-get="/rpc/network/difficulty" hx-trigger="load, every 10s"></div>
</div>
<br>
<div class="d-flex justify-content-between">
<div class="value-text">Block Reward&nbsp;</div><div class="value-text text-end">ツ 60</div>
</div>
<br>
<div class="d-flex justify-content-between">
<div class="value-text">Estimated Mining Cost
<!-- Button trigger mining cost explanation modal -->
<button type="button" class="btn-sm" data-bs-toggle="modal" data-bs-target="#mining_cost">
<i class="bi bi-question-circle"></i>
</button>
&nbsp;</div><div class="value-text text-end" hx-get="/rpc/mining/production_cost" hx-trigger="load, every 10s"></div>
</div>
<br>
<div class="d-flex justify-content-between">
<div class="value-text">Reward/Cost Ratio
<!-- Button trigger ratio explanation modal -->
<button type="button" class="btn-sm" data-bs-toggle="modal" data-bs-target="#ratio">
<i class="bi bi-question-circle"></i>
</button>
&nbsp;</div><div class="value-text text-end" hx-get="/rpc/mining/reward_ratio" hx-trigger="load, every 10s"></div>
</div>
</div>
</div>
<div class="card mb-3">
<div class="card-body" align="left">
<div class="darkorange-text"><i class="bi bi-speedometer2"></i> TRANSACTIONS & FEES</div>
<br>
<div class="d-flex justify-content-between">
<div class="value-text">1H Period (60 Blocks)&nbsp;</div><div class="value-text text-end" hx-get="/rpc/txns/count_1h" hx-trigger="load, every 10s"></div>
</div>
<br>
<div class="d-flex justify-content-between">
<div class="value-text">24H Period (1440 Blocks)&nbsp;</div><div class="value-text text-end" hx-get="/rpc/txns/count_24h" hx-trigger="load, every 10s"></div>
</div>
</div>
</div>
<div class="card mb-3">
<div class="card-body" align="left">
<div class="darkorange-text"><i class="bi bi-receipt"></i> MEMPOOL</div>
<br>
<div class="d-flex justify-content-between">
<div class="value-text">Transactions&nbsp;</div><div class="value-text text-end" hx-get="/rpc/mempool/txns" hx-trigger="load, every 10s"></div>
</div>
<br>
<div class="d-flex justify-content-between">
<div class="value-text">Stem&nbsp;</div><div class="value-text text-end" hx-get="/rpc/mempool/stem" hx-trigger="load, every 10s"></div>
</div>
</div>
</div>
<div class="card mb-3">
<div class="card-body" align="left">
<div class="darkorange-text"><i class="bi bi-diagram-3"></i> CONNECTIONS</div>
<br>
<div class="d-flex justify-content-between">
<div class="value-text">Inbound&nbsp;</div><div class="value-text text-end" hx-get="/rpc/peers/inbound" hx-trigger="load, every 10s"></div>
</div>
<br>
<div class="d-flex justify-content-between">
<div class="value-text">Outbound&nbsp;</div><div class="value-text text-end" hx-get="/rpc/peers/outbound" hx-trigger="load, every 10s"></div>
</div>
</div>
</div>
<div class="card mb-4">
<div class="card-body" align="left">
<div class="darkorange-text"><i class="bi bi-pc-display-horizontal"></i> NODE</div>
<br>
<div class="d-flex justify-content-between">
<div class="value-text">Version&nbsp;</div><div class="value-text text-end">{{ node_ver }}</div>
</div>
<br>
<div class="d-flex justify-content-between">
<div class="value-text me-1">Protocol&nbsp;</div><div class="value-text text-end">{{ proto_ver }}</div>
</div>
<br>
<div class="d-flex justify-content-between">
<div class="value-text me-1">Sync Status&nbsp;</div><div class="value-text text-end" hx-get="/rpc/sync/status" hx-trigger="load, every 10s"></div>
</div>
</div>
</div>
</div>
<!-- Modals. Explanations of several dashboard stats. -->
<div class="card border-0">
<div class="modal fade" id="soft_sup" tabindex="-1" aria-labelledby="soft_sup_label" aria-hidden="true">
<div class="modal-dialog modal-dialog-centered">
<div class="modal-content">
<div class="modal-header">
<h1 class="modal-title fs-5" id="soft_sup_label">Soft Total Supply</h1>
<div data-bs-theme="light">
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
</div>
<div class="modal-body">
Percentage of issued coins from the soft total supply (3150M) when inflation will reach <1%.
<br>
<br>
<a class="text-decoration-none" href="https://john-tromp.medium.com/a-case-for-using-soft-total-supply-1169a188d153">https://john-tromp.medium.com/a-case-for-using-soft-total-supply-1169a188d153</a>
</div>
</div>
</div>
</div>
<div class="modal fade" id="mining_cost" tabindex="-1" aria-labelledby="mining_cost_label" aria-hidden="true">
<div class="modal-dialog modal-dialog-centered">
<div class="modal-content">
<div class="modal-header">
<h1 class="modal-title fs-5" id="mining_cost_label">Estimated Mining Cost</h1>
<div data-bs-theme="light">
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
</div>
<div class="modal-body">
Mining cost to produce 1 grin coin.<br>
Assuming that:<br>
Miner is G1-mini ASIC.<br>
Electricity cost is $0.07 per kW/h.<br>
<br>
<a class="text-decoration-none" href="https://ipollo.com/products/ipollo-g1-mini">https://ipollo.com/products/ipollo-g1-mini</a>
</div>
</div>
</div>
</div>
<div class="modal fade" id="ratio" tabindex="-1" aria-labelledby="ratio_label" aria-hidden="true">
<div class="modal-dialog modal-dialog-centered">
<div class="modal-content">
<div class="modal-header">
<h1 class="modal-title fs-5" id="ratio_label">Reward/Cost Ratio</h1>
<div data-bs-theme="light">
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
</div>
<div class="modal-body">
Shows the result of the following formula:<br>
Price of 1 Grin (USD) / Mining Cost of 1 Grin (USD).<br>
<br>
<i class="bi bi-hand-thumbs-down"></i> - <= 1<br>
<i class="bi bi-hand-thumbs-up"></i> - from 1 to 2<br>
<i class="bi bi-emoji-sunglasses"></i> - from 2 to 3<br>
<i class='bi bi-rocket-takeoff'></i> - >= 3
</div>
</div>
</div>
</div>
</div>
</code>
{% endblock content%}