mirror of
https://github.com/transatoshi-mw/grin-explorer.git
synced 2025-08-30 16:22:45 +00:00
Initial commit
This commit is contained in:
2930
Cargo.lock
generated
Normal file
2930
Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load Diff
34
Cargo.toml
Normal file
34
Cargo.toml
Normal 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
18
Explorer.toml
Normal 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
21
LICENSE
Normal 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
33
README.md
Normal 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
4
Rocket.toml
Normal file
@@ -0,0 +1,4 @@
|
||||
[default]
|
||||
address = "127.0.0.1"
|
||||
log_level = "critical"
|
||||
|
157
src/data.rs
Normal file
157
src/data.rs
Normal 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
553
src/main.rs
Normal 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
528
src/requests.rs
Normal 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
25
src/worker.rs
Normal 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
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
6314
static/scripts/bootstrap.bundle.js
vendored
Normal file
File diff suppressed because it is too large
Load Diff
8
static/scripts/darkmode.js
Normal file
8
static/scripts/darkmode.js
Normal 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");
|
||||
}
|
||||
}
|
||||
|
11
static/scripts/darkmode_toggle.js
Normal file
11
static/scripts/darkmode_toggle.js
Normal 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
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
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
12063
static/styles/bootstrap.css
vendored
Normal file
File diff suppressed because it is too large
Load Diff
BIN
static/styles/fonts/bootstrap-icons.woff
Normal file
BIN
static/styles/fonts/bootstrap-icons.woff
Normal file
Binary file not shown.
BIN
static/styles/fonts/bootstrap-icons.woff2
Normal file
BIN
static/styles/fonts/bootstrap-icons.woff2
Normal file
Binary file not shown.
266
static/styles/style.css
Normal file
266
static/styles/style.css
Normal 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
228
templates/base.html.tera
Normal 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>
|
128
templates/block_details.html.tera
Normal file
128
templates/block_details.html.tera
Normal 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 </div>
|
||||
<div class="value-text text-end">{{ block.time }}</div>
|
||||
</div>
|
||||
<br>
|
||||
<div class="d-flex justify-content-between">
|
||||
<div class="value-text">Hash </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 </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 </div>
|
||||
<div class="value-text text-end">{{ block.weight }} %</div>
|
||||
</div>
|
||||
<br>
|
||||
<div class="d-flex justify-content-between">
|
||||
<div class="value-text">Version </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] }} <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] }} <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 %}
|
||||
|
264
templates/block_list.html.tera
Normal file
264
templates/block_list.html.tera
Normal 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
23
templates/error.html.tera
Normal 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
427
templates/index.html.tera
Normal 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 </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 </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) </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) </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 </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 </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>
|
||||
</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) </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) </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 </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 </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 </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) </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 </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 </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>
|
||||
</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>
|
||||
</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) </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) </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 </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 </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 </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 </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 </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 </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 </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 </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 </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) </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) </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 </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 </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>
|
||||
</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) </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) </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 </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 </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 </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) </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 </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 </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>
|
||||
</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>
|
||||
</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) </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) </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 </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 </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 </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 </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 </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 </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 </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%}
|
||||
|
Reference in New Issue
Block a user