Initial commit

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

157
src/data.rs Normal file
View File

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

553
src/main.rs Normal file
View File

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

528
src/requests.rs Normal file
View File

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

25
src/worker.rs Normal file
View File

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