mirror of
https://github.com/transatoshi-mw/grin-explorer.git
synced 2025-10-21 13:33:41 +00:00
9
Cargo.lock
generated
9
Cargo.lock
generated
@@ -41,6 +41,12 @@ dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "anyhow"
|
||||
version = "1.0.86"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b3d1d046238990b9cf5bcde22a3fb3584ee5cf65fb2765f454ed428c7a0063da"
|
||||
|
||||
[[package]]
|
||||
name = "arrayvec"
|
||||
version = "0.7.4"
|
||||
@@ -730,8 +736,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "grin-explorer"
|
||||
version = "0.1.2"
|
||||
version = "0.1.3"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"chrono",
|
||||
"colored",
|
||||
"config",
|
||||
|
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "grin-explorer"
|
||||
version = "0.1.2"
|
||||
version = "0.1.3"
|
||||
edition = "2021"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
@@ -19,6 +19,7 @@ config = "0.14.0"
|
||||
lazy_static = "1.4.0"
|
||||
shellexpand = "3.1.0"
|
||||
either = "1.11.0"
|
||||
anyhow = "1.0.86"
|
||||
|
||||
[dependencies.reqwest]
|
||||
version = "0.11.23"
|
||||
|
@@ -16,6 +16,20 @@ api_secret_path = "~/.grin/main/.api_secret"
|
||||
# Foreign API secret path.
|
||||
foreign_api_secret_path = "~/.grin/main/.foreign_api_secret"
|
||||
|
||||
# Path to Grin directory
|
||||
# Path to Grin directory.
|
||||
grin_dir = "~/.grin"
|
||||
|
||||
# CoinGecko API on/off switch.
|
||||
coingecko_api = "on"
|
||||
|
||||
|
||||
# Testnet config
|
||||
# ip = "127.0.0.1"
|
||||
# port = "13413"
|
||||
# proto = "http"
|
||||
# user = "grin"
|
||||
# api_secret_path = "~/.grin/test/.api_secret"
|
||||
# foreign_api_secret_path = "~/.grin/test/.foreign_api_secret"
|
||||
# grin_dir = "~/.grin"
|
||||
# coingecko_api = "off"
|
||||
|
||||
|
@@ -2,3 +2,7 @@
|
||||
address = "127.0.0.1"
|
||||
log_level = "critical"
|
||||
|
||||
# Uncomment and change default port number (8000) if another instance of the explorer is needed to run.
|
||||
# E.g. Mainnet (8000) and Testnet (8001) instances.
|
||||
# port = 8000
|
||||
|
||||
|
28
src/data.rs
28
src/data.rs
@@ -35,6 +35,8 @@ pub struct Dashboard {
|
||||
// mempool
|
||||
pub txns: String,
|
||||
pub stem: String,
|
||||
// coingecko api
|
||||
pub cg_api: String,
|
||||
}
|
||||
|
||||
impl Dashboard {
|
||||
@@ -64,6 +66,7 @@ impl Dashboard {
|
||||
breakeven_cost: String::new(),
|
||||
txns: String::new(),
|
||||
stem: String::new(),
|
||||
cg_api: String::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -108,6 +111,29 @@ impl Block {
|
||||
}
|
||||
|
||||
|
||||
// Kernel data
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct Kernel {
|
||||
pub height: String,
|
||||
pub excess: String,
|
||||
pub ker_type: String,
|
||||
pub fee: String,
|
||||
pub raw_data: String,
|
||||
}
|
||||
|
||||
impl Kernel {
|
||||
pub fn new() -> Kernel {
|
||||
Kernel {
|
||||
height: String::new(),
|
||||
excess: String::new(),
|
||||
ker_type: String::new(),
|
||||
fee: String::new(),
|
||||
raw_data: String::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Transactions data
|
||||
#[derive(Debug)]
|
||||
pub struct Transactions {
|
||||
@@ -141,6 +167,7 @@ pub struct ExplorerConfig {
|
||||
pub grin_dir: String,
|
||||
pub api_secret: String,
|
||||
pub foreign_api_secret: String,
|
||||
pub coingecko_api: String,
|
||||
}
|
||||
|
||||
impl ExplorerConfig {
|
||||
@@ -155,6 +182,7 @@ impl ExplorerConfig {
|
||||
grin_dir: String::new(),
|
||||
api_secret: String::new(),
|
||||
foreign_api_secret: String::new(),
|
||||
coingecko_api: String::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
116
src/main.rs
116
src/main.rs
@@ -9,6 +9,7 @@ use colored::Colorize;
|
||||
use rocket::tokio;
|
||||
use rocket::response::Redirect;
|
||||
use either::Either;
|
||||
use serde_json::Value;
|
||||
|
||||
mod worker;
|
||||
mod requests;
|
||||
@@ -17,6 +18,7 @@ mod data;
|
||||
use crate::data::Dashboard;
|
||||
use crate::data::Block;
|
||||
use crate::data::Transactions;
|
||||
use crate::data::Kernel;
|
||||
|
||||
|
||||
// Rendering main (Dashboard) page.
|
||||
@@ -25,9 +27,10 @@ fn index(dashboard: &State<Arc<Mutex<Dashboard>>>) -> Template {
|
||||
let data = dashboard.lock().unwrap();
|
||||
|
||||
Template::render("index", context! {
|
||||
route: "index",
|
||||
node_ver: &data.node_ver,
|
||||
route: "index",
|
||||
node_ver: &data.node_ver,
|
||||
proto_ver: &data.proto_ver,
|
||||
cg_api: &data.cg_api,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -117,32 +120,34 @@ async fn block_header_by_hash(hash: &str) -> Either<Template, Redirect> {
|
||||
|
||||
|
||||
// Rendering page for a specified kernel.
|
||||
#[get("/kernel/<kernel>")]
|
||||
async fn kernel(kernel: &str) -> Either<Template, Redirect> {
|
||||
let mut height = String::new();
|
||||
#[get("/kernel/<excess>")]
|
||||
async fn kernel(excess: &str) -> Template {
|
||||
let mut kernel = Kernel::new();
|
||||
|
||||
let _ = requests::get_kernel(&kernel, &mut height).await;
|
||||
let _ = requests::get_kernel(&excess, &mut kernel).await;
|
||||
|
||||
if kernel.is_empty() == false {
|
||||
if height.is_empty() == false {
|
||||
return Either::Right(Redirect::to(uri!(block_details_by_height(height.as_str()))));
|
||||
}
|
||||
if kernel.height.is_empty() == false {
|
||||
return Template::render("kernel", context! {
|
||||
route: "kernel",
|
||||
kernel,
|
||||
})
|
||||
}
|
||||
|
||||
return Either::Left(Template::render("error", context! {
|
||||
return 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 {
|
||||
//Check input length
|
||||
if input.len() > "search=".len() {
|
||||
// Trim 'search=' from the request data
|
||||
let input = &input[7..].to_lowercase();
|
||||
|
||||
// Check for valid chars
|
||||
if input.chars().all(|x| (x >= 'a' && x <= 'f') || (x >= '0' && x <= '9')) == true {
|
||||
|
||||
// Block number
|
||||
@@ -153,7 +158,7 @@ fn search(input: &str) -> Either<Template, Redirect> {
|
||||
} else if input.len() == 64 {
|
||||
return Either::Right(Redirect::to(uri!(block_header_by_hash(input))));
|
||||
|
||||
// Kernel hash
|
||||
// Kernel
|
||||
} else if input.len() == 66 {
|
||||
return Either::Right(Redirect::to(uri!(kernel(input))));
|
||||
}
|
||||
@@ -166,6 +171,66 @@ fn search(input: &str) -> Either<Template, Redirect> {
|
||||
}
|
||||
|
||||
|
||||
// Owner API.
|
||||
#[post("/v2/owner", data="<data>")]
|
||||
async fn api_owner(data: &str) -> String {
|
||||
let result = serde_json::from_str(data);
|
||||
|
||||
let v: Value = match result {
|
||||
Ok(value) => value,
|
||||
Err(_err) => return "{\"error\":\"bad syntax\"}".to_string(),
|
||||
};
|
||||
|
||||
let method = match v["method"].as_str() {
|
||||
Some(value) => value,
|
||||
_ => return "{\"error\":\"bad syntax\"}".to_string(),
|
||||
};
|
||||
|
||||
// Whitelisted methods: get_connected_peer, get_peers, get_status.
|
||||
if method == "get_connected_peers" || method == "get_peers" || method == "get_status" {
|
||||
let resp = requests::call(method, v["params"].to_string().as_str(), v["id"].to_string().as_str(), "owner").await;
|
||||
|
||||
let result = match resp {
|
||||
Ok(value) => value,
|
||||
Err(_err) => return "{\"error\":\"rpc call failed\"}".to_string(),
|
||||
};
|
||||
|
||||
return result.to_string();
|
||||
}
|
||||
|
||||
"{\"error\":\"not allowed\"}".to_string()
|
||||
}
|
||||
|
||||
|
||||
// Foreign API.
|
||||
// All methods are whitelisted.
|
||||
#[post("/v2/foreign", data="<data>")]
|
||||
async fn api_foreign(data: &str) -> String {
|
||||
let result = serde_json::from_str(data);
|
||||
|
||||
let v: Value = match result {
|
||||
Ok(value) => value,
|
||||
Err(_err) => return "{\"error\":\"bad syntax\"}".to_string(),
|
||||
};
|
||||
|
||||
let method = match v["method"].as_str() {
|
||||
Some(value) => value,
|
||||
_ => return "{\"error\":\"bad syntax\"}".to_string(),
|
||||
};
|
||||
|
||||
println!("{}", method);
|
||||
println!("{}", data);
|
||||
let resp = requests::call(method, v["params"].to_string().as_str(), v["id"].to_string().as_str(), "foreign").await;
|
||||
|
||||
let result = match resp {
|
||||
Ok(value) => value,
|
||||
Err(_err) => return "{\"error\":\"rpc call failed\"}".to_string(),
|
||||
};
|
||||
|
||||
result.to_string()
|
||||
}
|
||||
|
||||
|
||||
// Start of HTMX routes.
|
||||
#[get("/rpc/peers/inbound")]
|
||||
fn peers_inbound(dashboard: &State<Arc<Mutex<Dashboard>>>) -> String {
|
||||
@@ -210,7 +275,13 @@ 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]);
|
||||
// 9 digits plus 2 commas, e.g. 168,038,400
|
||||
if data.supply.len() == 11 {
|
||||
return format!("{} % ({}M/3150M)", data.soft_supply, &data.supply[..3]);
|
||||
// 10 digits plus 2 commas
|
||||
} else if data.supply.len() == 12 {
|
||||
return format!("{} % ({}M/3150M)", data.soft_supply, &data.supply[..4]);
|
||||
}
|
||||
}
|
||||
|
||||
"3150M".to_string()
|
||||
@@ -306,7 +377,7 @@ fn disk_usage(dashboard: &State<Arc<Mutex<Dashboard>>>) -> String {
|
||||
fn network_hashrate(dashboard: &State<Arc<Mutex<Dashboard>>>) -> String {
|
||||
let data = dashboard.lock().unwrap();
|
||||
|
||||
format!("{} KG/s", data.hashrate)
|
||||
data.hashrate.clone()
|
||||
}
|
||||
|
||||
|
||||
@@ -490,7 +561,7 @@ fn block_weight(count: usize, blocks: &State<Arc<Mutex<Vec<Block>>>>) -> String
|
||||
fn block_list_index(dashboard: &State<Arc<Mutex<Dashboard>>>) -> String {
|
||||
let data = dashboard.lock().unwrap();
|
||||
|
||||
if data.height.is_empty() == false {
|
||||
if data.height.is_empty() == false && data.height.parse::<u64>().unwrap() > 10 {
|
||||
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);
|
||||
@@ -552,7 +623,8 @@ async fn main() {
|
||||
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, breakeven_cost,
|
||||
last_block_age, block_list_by_height, block_list_index, search, kernel])
|
||||
last_block_age, block_list_by_height, block_list_index, search, kernel,
|
||||
api_owner, api_foreign])
|
||||
.mount("/static", FileServer::from("static"))
|
||||
.attach(Template::fairing())
|
||||
.launch()
|
||||
|
180
src/requests.rs
180
src/requests.rs
@@ -16,6 +16,7 @@ use crate::data::Dashboard;
|
||||
use crate::data::Block;
|
||||
use crate::data::Transactions;
|
||||
use crate::data::ExplorerConfig;
|
||||
use crate::Kernel;
|
||||
|
||||
|
||||
// Static explorer config structure
|
||||
@@ -36,6 +37,7 @@ lazy_static! {
|
||||
"api_secret_path" => cfg.api_secret_path = value,
|
||||
"foreign_api_secret_path" => cfg.foreign_api_secret_path = value,
|
||||
"grin_dir" => cfg.grin_dir = value,
|
||||
"coingecko_api" => cfg.coingecko_api = value,
|
||||
_ => println!("{} Unknown config setting '{}'.", "[ ERROR ]".red(), name),
|
||||
}
|
||||
}
|
||||
@@ -52,7 +54,7 @@ lazy_static! {
|
||||
|
||||
|
||||
// RPC requests to grin node.
|
||||
async fn call(method: &str, params: &str, rpc_type: &str) -> Result<Value, Error> {
|
||||
pub async fn call(method: &str, params: &str, id: &str, rpc_type: &str) -> Result<Value, anyhow::Error> {
|
||||
let rpc_url;
|
||||
let secret;
|
||||
|
||||
@@ -67,21 +69,21 @@ async fn call(method: &str, params: &str, rpc_type: &str) -> Result<Value, Error
|
||||
|
||||
let client = reqwest::Client::new();
|
||||
let result = client.post(rpc_url)
|
||||
.body(format!("{{\"method\": \"{}\", \"params\": {}, \"id\":1}}", method, params))
|
||||
.body(format!("{{\"method\": \"{}\", \"params\": {}, \"id\": {}}}", method, params, id))
|
||||
.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();
|
||||
let val: Value = serde_json::from_str(&result.text().await.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?;
|
||||
pub async fn get_status(dashboard: Arc<Mutex<Dashboard>>) -> Result<(), anyhow::Error> {
|
||||
let resp = call("get_status", "[]", "1", "owner").await?;
|
||||
|
||||
let mut data = dashboard.lock().unwrap();
|
||||
|
||||
@@ -92,14 +94,17 @@ pub async fn get_status(dashboard: Arc<Mutex<Dashboard>>) -> Result<(), Error> {
|
||||
data.proto_ver = resp["result"]["Ok"]["protocol_version"].to_string();
|
||||
}
|
||||
|
||||
// Also set cg_api value
|
||||
data.cg_api = CONFIG.coingecko_api.clone();
|
||||
|
||||
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?;
|
||||
pub async fn get_mempool(dashboard: Arc<Mutex<Dashboard>>) -> Result<(), anyhow::Error> {
|
||||
let resp1 = call("get_pool_size", "[]", "1", "foreign").await?;
|
||||
let resp2 = call("get_stempool_size", "[]", "1", "foreign").await?;
|
||||
|
||||
let mut data = dashboard.lock().unwrap();
|
||||
|
||||
@@ -113,8 +118,9 @@ pub async fn get_mempool(dashboard: Arc<Mutex<Dashboard>>) -> Result<(), Error>
|
||||
|
||||
|
||||
// Collecting: inbound, outbound.
|
||||
pub async fn get_connected_peers(dashboard: Arc<Mutex<Dashboard>>) -> Result<(), Error> {
|
||||
let resp = call("get_connected_peers", "[]", "owner").await?;
|
||||
pub async fn get_connected_peers(dashboard: Arc<Mutex<Dashboard>>)
|
||||
-> Result<(), anyhow::Error> {
|
||||
let resp = call("get_connected_peers", "[]", "1", "owner").await?;
|
||||
|
||||
let mut data = dashboard.lock().unwrap();
|
||||
|
||||
@@ -140,15 +146,18 @@ pub async fn get_connected_peers(dashboard: Arc<Mutex<Dashboard>>) -> Result<(),
|
||||
|
||||
// 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 client;
|
||||
let result;
|
||||
let mut val = Value::Null;
|
||||
|
||||
if CONFIG.coingecko_api == "on" {
|
||||
client = reqwest::Client::new();
|
||||
result = client.get("https://api.coingecko.com/api/v3/simple/price?ids=grin&vs_currencies=usd%2Cbtc&include_24hr_vol=true").send().await?;
|
||||
val = serde_json::from_str(&result.text().await.unwrap()).unwrap();
|
||||
}
|
||||
|
||||
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
|
||||
@@ -157,28 +166,32 @@ pub async fn get_market(dashboard: Arc<Mutex<Dashboard>>) -> Result<(), Error> {
|
||||
// 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);
|
||||
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);
|
||||
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);
|
||||
if CONFIG.coingecko_api == "on" && val != Value::Null {
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -189,8 +202,13 @@ pub async fn get_market(dashboard: Arc<Mutex<Dashboard>>) -> Result<(), Error> {
|
||||
// Collecting: disk_usage.
|
||||
pub fn get_disk_usage(dashboard: Arc<Mutex<Dashboard>>) -> Result<(), Error> {
|
||||
let mut data = dashboard.lock().unwrap();
|
||||
let chain_data;
|
||||
|
||||
let chain_data = format!("{}/main/chain_data", CONFIG.grin_dir);
|
||||
if CONFIG.coingecko_api == "on" {
|
||||
chain_data = format!("{}/main/chain_data", CONFIG.grin_dir);
|
||||
} else {
|
||||
chain_data = format!("{}/test/chain_data", CONFIG.grin_dir);
|
||||
}
|
||||
|
||||
data.disk_usage = format!("{:.2}", (get_size(chain_data).unwrap() as f64)
|
||||
/ 1000.0 / 1000.0 / 1000.0);
|
||||
@@ -200,16 +218,16 @@ pub fn get_disk_usage(dashboard: Arc<Mutex<Dashboard>>) -> Result<(), Error> {
|
||||
|
||||
|
||||
// Collecting: hashrate, difficulty, production cost, breakeven cost.
|
||||
pub async fn get_mining_stats(dashboard: Arc<Mutex<Dashboard>>) -> Result<(), Error> {
|
||||
pub async fn get_mining_stats(dashboard: Arc<Mutex<Dashboard>>) -> Result<(), anyhow::Error> {
|
||||
let difficulty_window = 1440;
|
||||
let height = get_current_height(dashboard.clone());
|
||||
|
||||
if height.is_empty() == false {
|
||||
if height.is_empty() == false && height.parse::<u64>().unwrap() > 1440 {
|
||||
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 resp1 = call("get_block", params1, "1", "foreign").await?;
|
||||
let resp2 = call("get_block", params2, "1", "foreign").await?;
|
||||
|
||||
let mut data = dashboard.lock().unwrap();
|
||||
|
||||
@@ -223,21 +241,31 @@ pub async fn get_mining_stats(dashboard: Arc<Mutex<Dashboard>>) -> Result<(), Er
|
||||
|
||||
// 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);
|
||||
let hashrate = (net_diff as f64) * 42.0 / 60.0 / 16384.0;
|
||||
|
||||
// KG/s
|
||||
if hashrate > 1000.0 {
|
||||
data.hashrate = format!("{:.2} KG/s", hashrate / 1000.0);
|
||||
// G/s
|
||||
} else {
|
||||
data.hashrate = format!("{:.2} G/s", hashrate);
|
||||
}
|
||||
|
||||
data.difficulty = net_diff.to_string();
|
||||
|
||||
// Calculating G1-mini production per hour
|
||||
let coins_per_hour = 1.2 / hashrate * 60.0 * 60.0;
|
||||
if CONFIG.coingecko_api == "on" {
|
||||
// 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));
|
||||
// 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());
|
||||
data.breakeven_cost = format!("{:.2}", data.price_usd.parse::<f64>().unwrap()
|
||||
/ (120.0 / 1000.0 * (1.0 / coins_per_hour)));
|
||||
data.reward_ratio = format!("{:.2}", data.price_usd.parse::<f64>().unwrap()
|
||||
/ data.production_cost.parse::<f64>().unwrap());
|
||||
data.breakeven_cost = format!("{:.2}", data.price_usd.parse::<f64>().unwrap()
|
||||
/ (120.0 / 1000.0 * (1.0 / coins_per_hour)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -247,7 +275,7 @@ pub async fn get_mining_stats(dashboard: Arc<Mutex<Dashboard>>) -> Result<(), Er
|
||||
|
||||
// Collecting block data for recent blocks (block_list page).
|
||||
pub async fn get_block_list_data(height: &String, block: &mut Block)
|
||||
-> Result<(), Error> {
|
||||
-> Result<(), anyhow::Error> {
|
||||
// Max block weight is 40000
|
||||
// One unit of weight is 32 bytes
|
||||
let kernel_weight = 3.0;
|
||||
@@ -255,11 +283,11 @@ pub async fn get_block_list_data(height: &String, block: &mut Block)
|
||||
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();
|
||||
let params = &format!("[{}, null, null]", height)[..];
|
||||
let resp = call("get_block", params, "1", "foreign").await?;
|
||||
|
||||
if resp["result"]["Ok"].is_null() == false {
|
||||
block.height = resp["result"]["Ok"]["header"]["height"].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();
|
||||
@@ -306,7 +334,7 @@ pub async fn get_block_list_data(height: &String, block: &mut Block)
|
||||
|
||||
// Collecting block data.
|
||||
pub async fn get_block_data(height: &str, block: &mut Block)
|
||||
-> Result<(), Error> {
|
||||
-> Result<(), anyhow::Error> {
|
||||
// Max block weight is 40000
|
||||
// One unit of weight is 32 bytes
|
||||
let kernel_weight = 3.0;
|
||||
@@ -316,7 +344,7 @@ pub async fn get_block_data(height: &str, block: &mut Block)
|
||||
if height.is_empty() == false {
|
||||
let params = &format!("[{}, null, null]", height)[..];
|
||||
|
||||
let resp = call("get_block", params, "foreign").await?;
|
||||
let resp = call("get_block", params, "1", "foreign").await?;
|
||||
|
||||
if resp["result"]["Ok"].is_null() == false {
|
||||
block.hash = resp["result"]["Ok"]["header"]["hash"].as_str().unwrap().to_string();
|
||||
@@ -362,10 +390,10 @@ pub async fn get_block_data(height: &str, block: &mut Block)
|
||||
|
||||
// Get block height by hash.
|
||||
pub async fn get_block_header(hash: &str, height: &mut String)
|
||||
-> Result<(), Error> {
|
||||
-> Result<(), anyhow::Error> {
|
||||
let params = &format!("[null, \"{}\", null]", hash)[..];
|
||||
|
||||
let resp = call("get_header", params, "foreign").await.unwrap();
|
||||
let resp = call("get_header", params, "1", "foreign").await?;
|
||||
|
||||
if resp["result"]["Ok"].is_null() == false {
|
||||
*height = resp["result"]["Ok"]["height"].to_string();
|
||||
@@ -376,14 +404,26 @@ pub async fn get_block_header(hash: &str, height: &mut String)
|
||||
|
||||
|
||||
// Get kernel.
|
||||
pub async fn get_kernel(kernel: &str, height: &mut String)
|
||||
-> Result<(), Error> {
|
||||
let params = &format!("[\"{}\", null, null]", kernel)[..];
|
||||
pub async fn get_kernel(excess: &str, kernel: &mut Kernel)
|
||||
-> Result<(), anyhow::Error> {
|
||||
let params = &format!("[\"{}\", null, null]", excess)[..];
|
||||
|
||||
let resp = call("get_kernel", params, "foreign").await.unwrap();
|
||||
let resp = call("get_kernel", params, "1", "foreign").await?;
|
||||
|
||||
if resp["result"]["Ok"].is_null() == false {
|
||||
*height = resp["result"]["Ok"]["height"].to_string();
|
||||
kernel.height = resp["result"]["Ok"]["height"].to_string();
|
||||
kernel.excess = resp["result"]["Ok"]["tx_kernel"]["excess"].as_str().unwrap().to_string();
|
||||
if resp["result"]["Ok"]["tx_kernel"]["features"]["Plain"].is_null() == false {
|
||||
kernel.ker_type = "Plain".to_string();
|
||||
kernel.fee = format!("ツ {}",
|
||||
resp["result"]["Ok"]["tx_kernel"]["features"]["Plain"]["fee"]
|
||||
.to_string().parse::<f64>().unwrap() / 1000000000.0);
|
||||
} else {
|
||||
kernel.ker_type = resp["result"]["Ok"]["tx_kernel"]["features"].as_str().unwrap().to_string();
|
||||
kernel.fee = "ツ 0".to_string();
|
||||
}
|
||||
|
||||
kernel.raw_data = serde_json::to_string_pretty(&resp).unwrap()
|
||||
}
|
||||
|
||||
Ok(())
|
||||
@@ -392,11 +432,11 @@ pub async fn get_kernel(kernel: &str, height: &mut String)
|
||||
|
||||
// Collecting block kernels for transactions stats.
|
||||
pub async fn get_block_kernels(height: &String, blocks: &mut Vec<Block>)
|
||||
-> Result<(), Error> {
|
||||
-> Result<(), anyhow::Error> {
|
||||
if height.is_empty() == false {
|
||||
let params = &format!("[{}, {}, 720, false]", height.parse::<u64>().unwrap() - 720,
|
||||
height)[..];
|
||||
let resp = call("get_blocks", params, "foreign").await.unwrap();
|
||||
let resp = call("get_blocks", params, "1", "foreign").await?;
|
||||
|
||||
for resp_block in resp["result"]["Ok"]["blocks"].as_array().unwrap() {
|
||||
let mut block = Block::new();
|
||||
@@ -421,7 +461,7 @@ pub async fn get_txn_stats(dashboard: Arc<Mutex<Dashboard>>,
|
||||
let mut blocks = Vec::<Block>::new();
|
||||
let height = get_current_height(dashboard.clone());
|
||||
|
||||
if height.is_empty() == false {
|
||||
if height.is_empty() == false && height.parse::<u64>().unwrap() > 1440 {
|
||||
// get_blocks grin rpc has limit of maximum of 1000 blocks request
|
||||
// https://github.com/mimblewimble/grin/blob/master/api/src/handlers/blocks_api.rs#L27
|
||||
// So, collecting kernels 2 times by 720 blocks to get a day of blocks
|
||||
@@ -484,7 +524,7 @@ pub async fn get_recent_blocks(dashboard: Arc<Mutex<Dashboard>>,
|
||||
let mut i = 0;
|
||||
let height_str = get_current_height(dashboard.clone());
|
||||
|
||||
if height_str.is_empty() == false {
|
||||
if height_str.is_empty() == false && height_str.parse::<u64>().unwrap() > 0 {
|
||||
let height = height_str.parse::<u64>().unwrap();
|
||||
let mut blocks_vec = Vec::<Block>::new();
|
||||
|
||||
@@ -510,11 +550,11 @@ pub async fn get_recent_blocks(dashboard: Arc<Mutex<Dashboard>>,
|
||||
|
||||
// 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> {
|
||||
latest_height: &mut u64) -> Result<(), anyhow::Error> {
|
||||
let mut i = 0;
|
||||
let height = height.to_string();
|
||||
|
||||
let resp = call("get_status", "[]", "owner").await.unwrap();
|
||||
let resp = call("get_status", "[]", "1", "owner").await?;
|
||||
|
||||
if resp != Value::Null {
|
||||
*latest_height = resp["result"]["Ok"]["tip"]["height"].to_string().parse::<u64>().unwrap();
|
||||
|
@@ -1,5 +1,4 @@
|
||||
use std::sync::{Arc, Mutex};
|
||||
use reqwest::Error;
|
||||
|
||||
use crate::data::Dashboard;
|
||||
use crate::data::Block;
|
||||
@@ -10,7 +9,7 @@ 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> {
|
||||
txns: Arc<Mutex<Transactions>>) -> Result<(), anyhow::Error> {
|
||||
let _ = requests::get_status(dash.clone()).await?;
|
||||
let _ = requests::get_mempool(dash.clone()).await?;
|
||||
let _ = requests::get_connected_peers(dash.clone()).await?;
|
||||
|
@@ -1,5 +1,5 @@
|
||||
<!doctype html>
|
||||
<html>
|
||||
<html lang="en" class="h-100">
|
||||
<head>
|
||||
<title>Grin Blockchain Explorer</title>
|
||||
<meta property="og:site_name" content="Grincoin.org (GRIN) Blockchain Explorer" />
|
||||
@@ -78,7 +78,7 @@
|
||||
{% block content %}{% endblock content %}
|
||||
</div>
|
||||
|
||||
<footer class="shadow">
|
||||
<footer class="shadow mt-auto">
|
||||
<code>
|
||||
<br>
|
||||
<div class="container-fluid">
|
||||
@@ -212,7 +212,7 @@
|
||||
<div class="row mb-2">
|
||||
<div class="col d-flex justify-content-center">
|
||||
<a class="text-decoration-none" href="https://github.com/aglkm/grin-explorer">
|
||||
<span style="color:grey">v.0.1.2 <i class="bi bi-github"></i></span>
|
||||
<span style="color:grey">v.0.1.3 <i class="bi bi-github"></i></span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
@@ -7,11 +7,7 @@
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<h4>No results found.</h4><br>
|
||||
Explorer supports requests by block number, block hash or kernel.<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 - <a class="text-decoration-none" href="/kernel/084caeb931b7e8cb73d6419ea74ea157a3cef19f6e9307108a8a808df58437a4ef">084caeb931b7e8cb73d6419ea74ea157a3cef19f6e9307108a8a808df58437a4ef</a>
|
||||
Explorer supports requests by block number, block hash or kernel.<br>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
@@ -4,6 +4,11 @@
|
||||
|
||||
<code>
|
||||
|
||||
{# We have different UI to display if CoinGecko API is disabled by user #}
|
||||
|
||||
{% if cg_api == "on" %}
|
||||
{# CoinGecko API is enabled #}
|
||||
|
||||
<div class="d-none d-md-block"> <!-- Show on >= md screens -->
|
||||
<div class="card-group mb-2">
|
||||
<div class="card me-2">
|
||||
@@ -456,6 +461,349 @@
|
||||
|
||||
</div>
|
||||
|
||||
{% else %}
|
||||
{# CoinGecko API is disabled #}
|
||||
|
||||
<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-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>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<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 </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>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<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-grid"></i> BLOCKCHAIN</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">
|
||||
<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 </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 </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-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>
|
||||
</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 </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>
|
||||
</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 </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 </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 class="modal fade" id="breakeven" tabindex="-1" aria-labelledby="breakeven_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="breakeven_label">Breakeven Electricity 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">
|
||||
Electricity threshold cost below which mining is profitable.<br>
|
||||
Assuming G1-mini ASIC as a miner device.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
{% endif %}
|
||||
|
||||
</code>
|
||||
|
||||
{% endblock content%}
|
||||
|
52
templates/kernel.html.tera
Normal file
52
templates/kernel.html.tera
Normal file
@@ -0,0 +1,52 @@
|
||||
{% extends "base" %}
|
||||
|
||||
{% block content %}
|
||||
|
||||
<code>
|
||||
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<div class="darkorange-text"><i class="bi bi-card-text"></i> KERNEL</div>
|
||||
<br>
|
||||
<div class="d-flex justify-content-between">
|
||||
<div class="value-text">Excess </div>
|
||||
<div class="value-text text-break text-end">{{ kernel.excess }}</div>
|
||||
</div>
|
||||
<br>
|
||||
<div class="d-flex justify-content-between">
|
||||
<div class="value-text">Block Height </div>
|
||||
<div class="value-text text-end">
|
||||
<a class="text-decoration-none" href="/block/{{ kernel.height }}">
|
||||
{{ kernel.height }} <i class="bi bi-box-arrow-up-right"></i>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<br>
|
||||
<div class="d-flex justify-content-between">
|
||||
<div class="value-text">Type </div>
|
||||
<div class="value-text text-end">{{ kernel.ker_type }}</div>
|
||||
</div>
|
||||
<br>
|
||||
<div class="d-flex justify-content-between">
|
||||
<div class="value-text">Fee </div>
|
||||
<div class="value-text text-end">{{ kernel.fee }}</div>
|
||||
</div>
|
||||
</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">{{ kernel.raw_data }}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<br>
|
||||
|
||||
</code>
|
||||
|
||||
{% endblock %}
|
||||
|
Reference in New Issue
Block a user