mirror of
https://github.com/transatoshi-mw/grin-explorer.git
synced 2025-08-30 16:22:45 +00:00
added sqlite database, enhanced charts, bumped version
This commit is contained in:
63
Cargo.lock
generated
63
Cargo.lock
generated
@@ -17,6 +17,18 @@ version = "2.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627"
|
||||
|
||||
[[package]]
|
||||
name = "ahash"
|
||||
version = "0.8.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e89da841a80418a9b391ebaea17f5c112ffaaa96f621d2c285b5174da76b9011"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"once_cell",
|
||||
"version_check",
|
||||
"zerocopy",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "aho-corasick"
|
||||
version = "1.1.3"
|
||||
@@ -575,6 +587,18 @@ dependencies = [
|
||||
"windows-sys 0.52.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "fallible-iterator"
|
||||
version = "0.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2acce4a10f12dc2fb14a218589d4f1f62ef011b2d0cc4b3cb1bba8e94da14649"
|
||||
|
||||
[[package]]
|
||||
name = "fallible-streaming-iterator"
|
||||
version = "0.1.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7360491ce676a36bf9bb3c56c1aa791658183a54d2744120f27285738d90465a"
|
||||
|
||||
[[package]]
|
||||
name = "fastrand"
|
||||
version = "2.1.1"
|
||||
@@ -813,7 +837,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "grin-explorer"
|
||||
version = "0.1.7"
|
||||
version = "0.1.8"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"chrono",
|
||||
@@ -828,6 +852,7 @@ dependencies = [
|
||||
"reqwest",
|
||||
"rocket",
|
||||
"rocket_dyn_templates",
|
||||
"rusqlite",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"shellexpand",
|
||||
@@ -864,6 +889,18 @@ name = "hashbrown"
|
||||
version = "0.14.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1"
|
||||
dependencies = [
|
||||
"ahash",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hashlink"
|
||||
version = "0.9.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6ba4ff7128dee98c7dc9794b6a411377e1404dba1c97deb8d1a55297bd25d8af"
|
||||
dependencies = [
|
||||
"hashbrown 0.14.5",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hermit-abi"
|
||||
@@ -1158,6 +1195,16 @@ dependencies = [
|
||||
"redox_syscall",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "libsqlite3-sys"
|
||||
version = "0.30.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2e99fb7a497b1e3339bc746195567ed8d3e24945ecd636e3619d20b9de9e9149"
|
||||
dependencies = [
|
||||
"pkg-config",
|
||||
"vcpkg",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "linked-hash-map"
|
||||
version = "0.5.6"
|
||||
@@ -1936,6 +1983,20 @@ dependencies = [
|
||||
"serde_derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rusqlite"
|
||||
version = "0.32.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7753b721174eb8ff87a9a0e799e2d7bc3749323e773db92e0984debb00019d6e"
|
||||
dependencies = [
|
||||
"bitflags 2.6.0",
|
||||
"fallible-iterator",
|
||||
"fallible-streaming-iterator",
|
||||
"hashlink",
|
||||
"libsqlite3-sys",
|
||||
"smallvec",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rust-ini"
|
||||
version = "0.19.0"
|
||||
|
23
Cargo.toml
23
Cargo.toml
@@ -1,25 +1,26 @@
|
||||
[package]
|
||||
name = "grin-explorer"
|
||||
version = "0.1.7"
|
||||
version = "0.1.8"
|
||||
edition = "2021"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
anyhow = "1.0.86"
|
||||
chrono = "0.4.37"
|
||||
config = "0.14.0"
|
||||
either = "1.11.0"
|
||||
env_logger = "0.11.3"
|
||||
fs_extra = "1.3.0"
|
||||
futures = "0.3.30"
|
||||
humantime = "2.1.0"
|
||||
lazy_static = "1.4.0"
|
||||
num-format = "0.4.4"
|
||||
rocket = {version = "0.5.0", features = ["json"]}
|
||||
rusqlite = "0.32.1"
|
||||
serde = {version = "1.0.198", features = ["derive"]}
|
||||
serde_json = "1.0.111"
|
||||
num-format = "0.4.4"
|
||||
fs_extra = "1.3.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"
|
||||
anyhow = "1.0.86"
|
||||
env_logger = "0.11.3"
|
||||
tera_thousands = "0.1.0"
|
||||
|
||||
[dependencies.reqwest]
|
||||
|
@@ -27,7 +27,13 @@ public_api = "enabled"
|
||||
|
||||
# List of external node endpoints, used for peer stats.
|
||||
# Comment out if you wish, only local peer stats will be used then.
|
||||
# external_nodes = ["https://grinnode.live:3413", "https://grincoin.org"]
|
||||
# external_nodes = ["https://grinnode.live:3413", "https://grincoin.org"]
|
||||
|
||||
# Database path.
|
||||
# By default, it will be created in the current directory.
|
||||
# Comment out if you don't want to use sqlite database
|
||||
database = "database.sqlite"
|
||||
|
||||
|
||||
# Grinnode config
|
||||
# host = "grinnode.live"
|
||||
@@ -35,6 +41,7 @@ public_api = "enabled"
|
||||
# proto = "https"
|
||||
# coingecko_api = "enabled"
|
||||
# public_api = "enabled"
|
||||
# database = "database.sqlite"
|
||||
|
||||
|
||||
# Grincoin config
|
||||
@@ -42,6 +49,7 @@ public_api = "enabled"
|
||||
# proto = "https"
|
||||
# coingecko_api = "enabled"
|
||||
# public_api = "enabled"
|
||||
# database = "database.sqlite"
|
||||
|
||||
|
||||
# Testnet config
|
||||
@@ -54,4 +62,5 @@ public_api = "enabled"
|
||||
# grin_dir = "~/.grin"
|
||||
# coingecko_api = "disabled"
|
||||
# public_api = "enabled"
|
||||
# database = "database.sqlite"
|
||||
|
||||
|
@@ -5,3 +5,6 @@ address = "127.0.0.1"
|
||||
# E.g. Mainnet (8000) and Testnet (8001) instances.
|
||||
# port = 8000
|
||||
|
||||
[default.databases.sqlite_db]
|
||||
url = "database.sqlite"
|
||||
|
||||
|
@@ -191,6 +191,7 @@ pub struct ExplorerConfig {
|
||||
pub coingecko_api: String,
|
||||
pub public_api: String,
|
||||
pub external_nodes: Vec<String>,
|
||||
pub database: String,
|
||||
}
|
||||
|
||||
impl ExplorerConfig {
|
||||
@@ -208,6 +209,7 @@ impl ExplorerConfig {
|
||||
coingecko_api: String::new(),
|
||||
public_api: String::new(),
|
||||
external_nodes: Vec::new(),
|
||||
database: String::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -250,7 +252,7 @@ pub struct Statistics {
|
||||
pub txns: Vec<String>,
|
||||
pub fees: Vec<String>,
|
||||
// UTXOs
|
||||
pub utxo_count: Vec<String>,
|
||||
pub utxos: Vec<String>,
|
||||
// Kernels
|
||||
pub kernels: Vec<String>,
|
||||
}
|
||||
@@ -265,7 +267,7 @@ impl Statistics {
|
||||
hashrate: Vec::new(),
|
||||
txns: Vec::new(),
|
||||
fees: Vec::new(),
|
||||
utxo_count: Vec::new(),
|
||||
utxos: Vec::new(),
|
||||
kernels: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
40
src/database.rs
Normal file
40
src/database.rs
Normal file
@@ -0,0 +1,40 @@
|
||||
use rusqlite::{Connection, Result};
|
||||
|
||||
pub fn open_db_connection(db_name: &str) -> Result<Connection> {
|
||||
let conn = Connection::open(db_name)?;
|
||||
|
||||
Ok(conn)
|
||||
}
|
||||
|
||||
pub fn create_statistics_table(conn: &Connection) -> Result<()> {
|
||||
conn.execute(
|
||||
"CREATE TABLE IF NOT EXISTS statistics (
|
||||
id INTEGER PRIMARY KEY,
|
||||
date TEXT NOT NULL UNIQUE,
|
||||
hashrate TEXT NOT NULL,
|
||||
txns TEXT NOT NULL,
|
||||
fees TEXT NOT NULL,
|
||||
utxos TEXT NOT NULL,
|
||||
kernels TEXT NOT NULL
|
||||
)",
|
||||
(), // empty list of parameters.
|
||||
)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn read_row(conn: &Connection, row_name: &str) -> Result<Vec<String>> {
|
||||
let sql = format!("SELECT {} FROM statistics ORDER BY id", row_name);
|
||||
let mut stmt = conn.prepare(&sql)?;
|
||||
|
||||
let data_iter = stmt
|
||||
.query_map([], |row| {
|
||||
row.get(0)
|
||||
}).unwrap();
|
||||
|
||||
// Collect all the results into a vector of strings
|
||||
let data: Vec<String> = data_iter.collect::<Result<Vec<_>, _>>().unwrap();
|
||||
|
||||
Ok(data)
|
||||
}
|
||||
|
75
src/exconfig.rs
Normal file
75
src/exconfig.rs
Normal file
@@ -0,0 +1,75 @@
|
||||
use config::Config;
|
||||
use std::fs;
|
||||
use lazy_static::lazy_static;
|
||||
|
||||
use crate::data::ExplorerConfig;
|
||||
|
||||
|
||||
// Static explorer config structure
|
||||
lazy_static! {
|
||||
pub static ref CONFIG: ExplorerConfig = {
|
||||
let mut cfg = ExplorerConfig::new();
|
||||
let toml = Config::builder().add_source(config::File::with_name("Explorer")).build().unwrap();
|
||||
|
||||
// Mandatory settings
|
||||
cfg.host = toml.get_string("host").unwrap();
|
||||
cfg.proto = toml.get_string("proto").unwrap();
|
||||
cfg.coingecko_api = toml.get_string("coingecko_api").unwrap();
|
||||
cfg.public_api = toml.get_string("public_api").unwrap();
|
||||
|
||||
// Optional settings
|
||||
match toml.get_string("port") {
|
||||
Ok(v) => cfg.port = v,
|
||||
Err(_e) => {},
|
||||
}
|
||||
|
||||
match toml.get_string("user") {
|
||||
Ok(v) => cfg.user = v,
|
||||
Err(_e) => {},
|
||||
}
|
||||
|
||||
match toml.get_string("api_secret_path") {
|
||||
Ok(v) => cfg.api_secret_path = v,
|
||||
Err(_e) => {},
|
||||
}
|
||||
|
||||
match toml.get_string("foreign_api_secret_path") {
|
||||
Ok(v) => cfg.foreign_api_secret_path = v,
|
||||
Err(_e) => {},
|
||||
}
|
||||
|
||||
match toml.get_string("grin_dir") {
|
||||
Ok(v) => cfg.grin_dir = v,
|
||||
Err(_e) => {},
|
||||
}
|
||||
|
||||
match toml.get_array("external_nodes") {
|
||||
Ok(nodes) => {
|
||||
for endpoint in nodes.clone() {
|
||||
cfg.external_nodes.push(endpoint.into_string().unwrap());
|
||||
}
|
||||
},
|
||||
Err(_e) => {},
|
||||
}
|
||||
|
||||
match toml.get_string("database") {
|
||||
Ok(v) => cfg.database = v,
|
||||
Err(_e) => {},
|
||||
}
|
||||
|
||||
if cfg.api_secret_path.is_empty() == false {
|
||||
cfg.api_secret = fs::read_to_string(format!("{}", shellexpand::tilde(&cfg.api_secret_path))).unwrap();
|
||||
}
|
||||
|
||||
if cfg.foreign_api_secret_path.is_empty() == false {
|
||||
cfg.foreign_api_secret = fs::read_to_string(format!("{}", shellexpand::tilde(&cfg.foreign_api_secret_path))).unwrap();
|
||||
}
|
||||
|
||||
if cfg.grin_dir.is_empty() == false {
|
||||
cfg.grin_dir = format!("{}", shellexpand::tilde(&cfg.grin_dir));
|
||||
}
|
||||
|
||||
cfg
|
||||
};
|
||||
}
|
||||
|
150
src/main.rs
150
src/main.rs
@@ -13,9 +13,11 @@ use serde_json::Value;
|
||||
use tera_thousands::separate_with_commas;
|
||||
|
||||
use crate::data::{Block, Dashboard, Kernel, Output, Statistics, Transactions, OUTPUT_SIZE, KERNEL_SIZE};
|
||||
use crate::requests::CONFIG;
|
||||
use crate::exconfig::CONFIG;
|
||||
|
||||
mod data;
|
||||
mod database;
|
||||
mod exconfig;
|
||||
mod requests;
|
||||
mod worker;
|
||||
|
||||
@@ -219,24 +221,112 @@ pub async fn search(input: Option<&str>) -> Either<Template, Redirect> {
|
||||
fn stats(statistics: &State<Arc<Mutex<Statistics>>>) -> Template {
|
||||
let data = statistics.lock().unwrap();
|
||||
|
||||
// Get the length of our data vectors (all vectors are the same size)
|
||||
let len = data.date.len();
|
||||
|
||||
// Construct chart periods
|
||||
let mut month = 0;
|
||||
let mut six_months = 0;
|
||||
let mut year = 0;
|
||||
|
||||
// Usize type can't be negative, so check the lenght of the vector
|
||||
if len > 30 {
|
||||
month = len - 30;
|
||||
}
|
||||
if len > (30 * 6) {
|
||||
six_months = len - (30 * 6);
|
||||
}
|
||||
if len > 365 {
|
||||
year = len - 365;
|
||||
}
|
||||
|
||||
let mut m_date = data.date.clone();
|
||||
let mut m_hashrate = data.hashrate.clone();
|
||||
let mut m_txns = data.txns.clone();
|
||||
let mut m_fees = data.fees.clone();
|
||||
let mut m_utxos = data.utxos.clone();
|
||||
let mut m_kernels = data.kernels.clone();
|
||||
|
||||
// Get stats for a month period
|
||||
if month > 0 {
|
||||
m_date = data.date.get(month..).unwrap().to_vec();
|
||||
m_hashrate = data.hashrate.get(month..).unwrap().to_vec();
|
||||
m_txns = data.txns.get(month..).unwrap().to_vec();
|
||||
m_fees = data.fees.get(month..).unwrap().to_vec();
|
||||
m_utxos = data.utxos.get(month..).unwrap().to_vec();
|
||||
m_kernels = data.kernels.get(month..).unwrap().to_vec();
|
||||
}
|
||||
|
||||
let mut sm_date = data.date.clone();
|
||||
let mut sm_hashrate = data.hashrate.clone();
|
||||
let mut sm_txns = data.txns.clone();
|
||||
let mut sm_fees = data.fees.clone();
|
||||
let mut sm_utxos = data.utxos.clone();
|
||||
let mut sm_kernels = data.kernels.clone();
|
||||
|
||||
// Get stats for six months period
|
||||
if six_months > 0 {
|
||||
sm_date = data.date.get(six_months..).unwrap().to_vec();
|
||||
sm_hashrate = data.hashrate.get(six_months..).unwrap().to_vec();
|
||||
sm_txns = data.txns.get(six_months..).unwrap().to_vec();
|
||||
sm_fees = data.fees.get(six_months..).unwrap().to_vec();
|
||||
sm_utxos = data.utxos.get(six_months..).unwrap().to_vec();
|
||||
sm_kernels = data.kernels.get(six_months..).unwrap().to_vec();
|
||||
}
|
||||
|
||||
let mut y_date = data.date.clone();
|
||||
let mut y_hashrate = data.hashrate.clone();
|
||||
let mut y_txns = data.txns.clone();
|
||||
let mut y_fees = data.fees.clone();
|
||||
let mut y_utxos = data.utxos.clone();
|
||||
let mut y_kernels = data.kernels.clone();
|
||||
|
||||
// Get stats for a year period
|
||||
if year > 0 {
|
||||
y_date = data.date.get(year..).unwrap().to_vec();
|
||||
y_hashrate = data.hashrate.get(year..).unwrap().to_vec();
|
||||
y_txns = data.txns.get(year..).unwrap().to_vec();
|
||||
y_fees = data.fees.get(year..).unwrap().to_vec();
|
||||
y_utxos = data.utxos.get(year..).unwrap().to_vec();
|
||||
y_kernels = data.kernels.get(year..).unwrap().to_vec();
|
||||
}
|
||||
|
||||
Template::render("stats", context! {
|
||||
route: "stats",
|
||||
date: data.date.clone(),
|
||||
user_agent: data.user_agent.clone(),
|
||||
count: data.count.clone(),
|
||||
total: data.total,
|
||||
hashrate: data.hashrate.clone(),
|
||||
txns: data.txns.clone(),
|
||||
fees: data.fees.clone(),
|
||||
utxo_count: data.utxo_count.clone(),
|
||||
kernels: data.kernels.clone(),
|
||||
route: "stats",
|
||||
user_agent: data.user_agent.clone(),
|
||||
count: data.count.clone(),
|
||||
total: data.total,
|
||||
date: data.date.clone(),
|
||||
hashrate: data.hashrate.clone(),
|
||||
txns: data.txns.clone(),
|
||||
fees: data.fees.clone(),
|
||||
utxos: data.utxos.clone(),
|
||||
kernels: data.kernels.clone(),
|
||||
m_date,
|
||||
m_hashrate,
|
||||
m_txns,
|
||||
m_fees,
|
||||
m_utxos,
|
||||
m_kernels,
|
||||
sm_date,
|
||||
sm_hashrate,
|
||||
sm_txns,
|
||||
sm_fees,
|
||||
sm_utxos,
|
||||
sm_kernels,
|
||||
y_date,
|
||||
y_hashrate,
|
||||
y_txns,
|
||||
y_fees,
|
||||
y_utxos,
|
||||
y_kernels,
|
||||
output_size: OUTPUT_SIZE,
|
||||
kernel_size: KERNEL_SIZE,
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
// Rendering Grinflation page.
|
||||
// Rendering Emission page.
|
||||
#[get("/emission")]
|
||||
fn emission(dashboard: &State<Arc<Mutex<Dashboard>>>) -> Template {
|
||||
let data = dashboard.lock().unwrap();
|
||||
@@ -752,7 +842,36 @@ async fn main() {
|
||||
|
||||
let mut ready_data = false;
|
||||
let mut ready_stats = false;
|
||||
let mut ready_db = false;
|
||||
let mut date = "".to_string();
|
||||
|
||||
// Initializing db and table
|
||||
if CONFIG.database.is_empty() == false {
|
||||
info!("initializing db.");
|
||||
let conn = database::open_db_connection(&CONFIG.database).expect("failed to open database");
|
||||
database::create_statistics_table(&conn).expect("failed to create statistics table");
|
||||
|
||||
let mut s = stats.lock().unwrap();
|
||||
let mut d = dash.lock().unwrap();
|
||||
|
||||
// Reading the database
|
||||
s.date = database::read_row(&conn, "date").unwrap();
|
||||
s.hashrate = database::read_row(&conn, "hashrate").unwrap();
|
||||
s.txns = database::read_row(&conn, "txns").unwrap();
|
||||
s.fees = database::read_row(&conn, "fees").unwrap();
|
||||
s.utxos = database::read_row(&conn, "utxos").unwrap();
|
||||
s.kernels = database::read_row(&conn, "kernels").unwrap();
|
||||
|
||||
// Read utxos right here, because we have it in worker::stats thread launched next day only
|
||||
if s.utxos.is_empty() == false {
|
||||
d.utxo_count = s.utxos.get(s.utxos.len() - 1).unwrap().to_string();
|
||||
}
|
||||
|
||||
// Get the latest date
|
||||
if s.date.is_empty() == false {
|
||||
date = s.date.get(s.date.len() - 1).unwrap().to_string();
|
||||
}
|
||||
}
|
||||
|
||||
// Collecting main data
|
||||
tokio::spawn(async move {
|
||||
@@ -775,7 +894,7 @@ async fn main() {
|
||||
|
||||
let date_now = format!("\"{}\"", Utc::now().format("%d-%m-%Y"));
|
||||
|
||||
if date.is_empty() || date != date_now {
|
||||
if date != date_now {
|
||||
date = date_now;
|
||||
let result = worker::stats(dash_clone.clone(), txns_clone.clone(),
|
||||
stats_clone.clone()).await;
|
||||
@@ -784,6 +903,7 @@ async fn main() {
|
||||
Ok(_v) => {
|
||||
if ready_stats == false {
|
||||
ready_stats = true;
|
||||
ready_db = true;
|
||||
info!("worker::stats ready.");
|
||||
}
|
||||
},
|
||||
@@ -792,6 +912,10 @@ async fn main() {
|
||||
error!("{}", e);
|
||||
},
|
||||
}
|
||||
// Got stats from DB, indicate ready state
|
||||
} else if ready_db == false && CONFIG.database.is_empty() == false {
|
||||
info!("worker::stats ready.");
|
||||
ready_db = true;
|
||||
}
|
||||
|
||||
tokio::time::sleep(Duration::from_secs(15)).await;
|
||||
|
@@ -6,77 +6,11 @@ use fs_extra::dir::get_size;
|
||||
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::{Block, Dashboard, ExplorerConfig, Kernel, Output, Statistics, Transactions};
|
||||
use crate::data::{Block, Dashboard, Kernel, Output, Statistics, Transactions};
|
||||
use crate::data::{KERNEL_WEIGHT, INPUT_WEIGHT, OUTPUT_WEIGHT, KERNEL_SIZE, INPUT_SIZE, OUTPUT_SIZE};
|
||||
|
||||
|
||||
// Static explorer config structure
|
||||
lazy_static! {
|
||||
pub static ref CONFIG: ExplorerConfig = {
|
||||
let mut cfg = ExplorerConfig::new();
|
||||
let toml = Config::builder().add_source(config::File::with_name("Explorer")).build().unwrap();
|
||||
|
||||
// Mandatory settings
|
||||
cfg.host = toml.get_string("host").unwrap();
|
||||
cfg.proto = toml.get_string("proto").unwrap();
|
||||
cfg.coingecko_api = toml.get_string("coingecko_api").unwrap();
|
||||
cfg.public_api = toml.get_string("public_api").unwrap();
|
||||
|
||||
// Optional settings
|
||||
match toml.get_string("port") {
|
||||
Ok(v) => cfg.port = v,
|
||||
Err(_e) => {},
|
||||
}
|
||||
|
||||
match toml.get_string("user") {
|
||||
Ok(v) => cfg.user = v,
|
||||
Err(_e) => {},
|
||||
}
|
||||
|
||||
match toml.get_string("api_secret_path") {
|
||||
Ok(v) => cfg.api_secret_path = v,
|
||||
Err(_e) => {},
|
||||
}
|
||||
|
||||
match toml.get_string("foreign_api_secret_path") {
|
||||
Ok(v) => cfg.foreign_api_secret_path = v,
|
||||
Err(_e) => {},
|
||||
}
|
||||
|
||||
match toml.get_string("grin_dir") {
|
||||
Ok(v) => cfg.grin_dir = v,
|
||||
Err(_e) => {},
|
||||
}
|
||||
|
||||
match toml.get_array("external_nodes") {
|
||||
Ok(nodes) => {
|
||||
for endpoint in nodes.clone() {
|
||||
cfg.external_nodes.push(endpoint.into_string().unwrap());
|
||||
}
|
||||
},
|
||||
Err(_e) => {},
|
||||
}
|
||||
|
||||
if cfg.api_secret_path.is_empty() == false {
|
||||
cfg.api_secret = fs::read_to_string(format!("{}", shellexpand::tilde(&cfg.api_secret_path))).unwrap();
|
||||
}
|
||||
|
||||
if cfg.foreign_api_secret_path.is_empty() == false {
|
||||
cfg.foreign_api_secret = fs::read_to_string(format!("{}", shellexpand::tilde(&cfg.foreign_api_secret_path))).unwrap();
|
||||
}
|
||||
|
||||
if cfg.grin_dir.is_empty() == false {
|
||||
cfg.grin_dir = format!("{}", shellexpand::tilde(&cfg.grin_dir));
|
||||
}
|
||||
|
||||
cfg
|
||||
};
|
||||
}
|
||||
use crate::exconfig::CONFIG;
|
||||
|
||||
|
||||
// RPC requests to grin node.
|
||||
|
@@ -5,7 +5,8 @@ use crate::data::Block;
|
||||
use crate::data::Dashboard;
|
||||
use crate::data::Statistics;
|
||||
use crate::data::Transactions;
|
||||
|
||||
use crate::database;
|
||||
use crate::exconfig::CONFIG;
|
||||
use crate::requests;
|
||||
|
||||
|
||||
@@ -16,7 +17,7 @@ pub async fn data(dash: Arc<Mutex<Dashboard>>, blocks: Arc<Mutex<Vec<Block>>>,
|
||||
let _ = requests::get_mempool(dash.clone()).await?;
|
||||
let _ = requests::get_connected_peers(dash.clone(), stats.clone()).await?;
|
||||
let _ = requests::get_market(dash.clone()).await?;
|
||||
requests::get_disk_usage(dash.clone())?;
|
||||
let _ = 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?;
|
||||
@@ -26,32 +27,37 @@ pub async fn data(dash: Arc<Mutex<Dashboard>>, blocks: Arc<Mutex<Vec<Block>>>,
|
||||
|
||||
// Collecting statistics.
|
||||
pub async fn stats(dash: Arc<Mutex<Dashboard>>, txns: Arc<Mutex<Transactions>>, stats: Arc<Mutex<Statistics>>) -> Result<(), anyhow::Error> {
|
||||
|
||||
let _ = requests::get_unspent_outputs(dash.clone()).await?;
|
||||
|
||||
let mut stats = stats.lock().unwrap();
|
||||
let dash = dash.lock().unwrap();
|
||||
let txns = txns.lock().unwrap();
|
||||
|
||||
if stats.date.len() == 30 {
|
||||
stats.date.remove(0);
|
||||
stats.hashrate.remove(0);
|
||||
stats.txns.remove(0);
|
||||
stats.fees.remove(0);
|
||||
stats.utxo_count.remove(0);
|
||||
stats.kernels.remove(0);
|
||||
}
|
||||
|
||||
stats.date.push(format!("\"{}\"", Utc::now().format("%d-%m-%Y")));
|
||||
stats.hashrate.push(dash.hashrate_kgs.clone());
|
||||
stats.txns.push(txns.period_24h.clone());
|
||||
stats.fees.push(txns.fees_24h.clone());
|
||||
stats.utxo_count.push(dash.utxo_count.clone());
|
||||
stats.utxos.push(dash.utxo_count.clone());
|
||||
|
||||
let mut kernel_count = 0;
|
||||
|
||||
if dash.kernel_mmr_size.is_empty() == false {
|
||||
let kernel_count = dash.kernel_mmr_size.parse::<u64>().unwrap() / 2;
|
||||
kernel_count = dash.kernel_mmr_size.parse::<u64>().unwrap() / 2;
|
||||
stats.kernels.push(kernel_count.to_string());
|
||||
}
|
||||
|
||||
if CONFIG.database.is_empty() == false {
|
||||
// Open the database
|
||||
let conn = database::open_db_connection(&CONFIG.database).expect("failed to open database");
|
||||
|
||||
//Insert new data into the database
|
||||
conn.execute(
|
||||
"INSERT OR IGNORE INTO statistics (date, hashrate, txns, fees, utxos, kernels) VALUES (?1, ?2, ?3, ?4, ?5, ?6)",
|
||||
(&format!("\"{}\"", Utc::now().format("%d-%m-%Y")), &dash.hashrate_kgs.clone(), &txns.period_24h.clone(), &txns.fees_24h.clone(), &dash.utxo_count.clone(), &kernel_count.to_string()),
|
||||
)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
@@ -114,10 +114,9 @@ footer {
|
||||
background-color: silver !important;
|
||||
}
|
||||
|
||||
.dark-mode button:focus { outline:0; }
|
||||
|
||||
.dark-mode button:hover,
|
||||
.dark-mode button:active {
|
||||
.dark-mode button:hover,
|
||||
.dark-mode button:focus,
|
||||
.dark-mode .btn.active {
|
||||
color: silver !important;
|
||||
}
|
||||
|
||||
|
@@ -294,7 +294,7 @@
|
||||
<div class="row mb-2">
|
||||
<div class="col d-flex justify-content-center">
|
||||
<a class="text-decoration-none me-2" href="https://github.com/aglkm/grin-explorer">
|
||||
<span style="color:grey"><i class="bi bi-github me-1"></i>v.0.1.7</span>
|
||||
<span style="color:grey"><i class="bi bi-github me-1"></i>v.0.1.8</span>
|
||||
</a>
|
||||
<a class="text-decoration-none" href="/search">
|
||||
<span style="color:grey"><i class="bi bi-search me-1"></i>Search</span>
|
||||
|
@@ -19,6 +19,12 @@
|
||||
<div class="card-body" align="center">
|
||||
<div class="value-text">
|
||||
<div class="darkorange-text"><i class="bi bi-speedometer2"></i> TRANSACTIONS & FEES</div>
|
||||
<div class="btn-group" role="group" id="txnBtnGroup">
|
||||
<button type="button" class="btn btn-outline-secondary btn-sm active" onclick="txnTimePeriod(this)" value="month">1m</button>
|
||||
<button type="button" class="btn btn-outline-secondary btn-sm" onclick="txnTimePeriod(this)" value="sixmonths">6m</button>
|
||||
<button type="button" class="btn btn-outline-secondary btn-sm" onclick="txnTimePeriod(this)" value="year">1y</button>
|
||||
<button type="button" class="btn btn-outline-secondary btn-sm" onclick="txnTimePeriod(this)" value="all">All</button>
|
||||
</div>
|
||||
<div style="position: relative; height:60vh; width:90vw"><canvas id="2"></canvas></div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -28,6 +34,12 @@
|
||||
<div class="card-body" align="center">
|
||||
<div class="value-text">
|
||||
<div class="darkorange-text"><i class="bi bi-activity"></i> HASHRATE</div>
|
||||
<div class="btn-group" role="group" id="hashBtnGroup">
|
||||
<button type="button" class="btn btn-outline-secondary btn-sm active" onclick="hashTimePeriod(this)" value="month">1m</button>
|
||||
<button type="button" class="btn btn-outline-secondary btn-sm" onclick="hashTimePeriod(this)" value="sixmonths">6m</button>
|
||||
<button type="button" class="btn btn-outline-secondary btn-sm" onclick="hashTimePeriod(this)" value="year">1y</button>
|
||||
<button type="button" class="btn btn-outline-secondary btn-sm" onclick="hashTimePeriod(this)" value="all">All</button>
|
||||
</div>
|
||||
<div style="position: relative; height:60vh; width:90vw"><canvas id="3"></canvas></div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -37,6 +49,12 @@
|
||||
<div class="card-body" align="center">
|
||||
<div class="value-text">
|
||||
<div class="darkorange-text"><i class="bi bi-card-list"></i> UNSPENT OUTPUTS</div>
|
||||
<div class="btn-group" role="group" id="utxoBtnGroup">
|
||||
<button type="button" class="btn btn-outline-secondary btn-sm active" onclick="utxoTimePeriod(this)" value="month">1m</button>
|
||||
<button type="button" class="btn btn-outline-secondary btn-sm" onclick="utxoTimePeriod(this)" value="sixmonths">6m</button>
|
||||
<button type="button" class="btn btn-outline-secondary btn-sm" onclick="utxoTimePeriod(this)" value="year">1y</button>
|
||||
<button type="button" class="btn btn-outline-secondary btn-sm" onclick="utxoTimePeriod(this)" value="all">All</button>
|
||||
</div>
|
||||
<div style="position: relative; height:60vh; width:90vw"><canvas id="4"></canvas></div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -46,6 +64,12 @@
|
||||
<div class="card-body" align="center">
|
||||
<div class="value-text">
|
||||
<div class="darkorange-text"><i class="bi bi-card-list"></i> KERNELS</div>
|
||||
<div class="btn-group" role="group" id="kerBtnGroup">
|
||||
<button type="button" class="btn btn-outline-secondary btn-sm active" onclick="kerTimePeriod(this)" value="month">1m</button>
|
||||
<button type="button" class="btn btn-outline-secondary btn-sm" onclick="kerTimePeriod(this)" value="sixmonths">6m</button>
|
||||
<button type="button" class="btn btn-outline-secondary btn-sm" onclick="kerTimePeriod(this)" value="year">1y</button>
|
||||
<button type="button" class="btn btn-outline-secondary btn-sm" onclick="kerTimePeriod(this)" value="all">All</button>
|
||||
</div>
|
||||
<div style="position: relative; height:60vh; width:90vw"><canvas id="5"></canvas></div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -92,7 +116,7 @@
|
||||
plugins: {
|
||||
legend: {
|
||||
display: true
|
||||
},
|
||||
}
|
||||
},
|
||||
scales: {
|
||||
x: {
|
||||
@@ -109,24 +133,28 @@
|
||||
};
|
||||
|
||||
var data = {
|
||||
labels: {{ date }},
|
||||
labels: {{ m_date }},
|
||||
datasets: [
|
||||
{
|
||||
label: 'Transactions',
|
||||
data: {{ txns }},
|
||||
data: {{ m_txns }},
|
||||
fill: false,
|
||||
tension: 0.1
|
||||
tension: 0.1,
|
||||
radius: 0,
|
||||
hoverRadius: 4
|
||||
},
|
||||
{
|
||||
label: 'Fees',
|
||||
data: {{ fees }},
|
||||
data: {{ m_fees }},
|
||||
fill: false,
|
||||
tension: 0.1
|
||||
tension: 0.1,
|
||||
radius: 0,
|
||||
hoverRadius: 4
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
new Chart(document.getElementById("2"), {
|
||||
const txnChart = new Chart(document.getElementById("2"), {
|
||||
type: 'line',
|
||||
data: data,
|
||||
options: options
|
||||
@@ -147,7 +175,7 @@
|
||||
plugins: {
|
||||
legend: {
|
||||
display: false
|
||||
},
|
||||
}
|
||||
},
|
||||
scales: {
|
||||
x: {
|
||||
@@ -164,18 +192,20 @@
|
||||
};
|
||||
|
||||
var data = {
|
||||
labels: {{ date }},
|
||||
labels: {{ m_date }},
|
||||
datasets: [{
|
||||
label: 'Hashrate (kG/s)',
|
||||
data: {{ hashrate }},
|
||||
data: {{ m_hashrate }},
|
||||
fill: true,
|
||||
borderColor: "#b25110",
|
||||
backgroundColor: gradient_hash,
|
||||
tension: 0.1
|
||||
tension: 0.1,
|
||||
radius: 0,
|
||||
hoverRadius: 4
|
||||
}]
|
||||
};
|
||||
|
||||
new Chart(document.getElementById("3"), {
|
||||
const hashChart = new Chart(document.getElementById("3"), {
|
||||
type: 'line',
|
||||
data: data,
|
||||
options: options
|
||||
@@ -224,20 +254,22 @@
|
||||
};
|
||||
|
||||
var data = {
|
||||
labels: {{ date }},
|
||||
labels: {{ m_date }},
|
||||
datasets: [
|
||||
{
|
||||
label: 'Unspent Outputs',
|
||||
data: {{ utxo_count }},
|
||||
data: {{ m_utxos }},
|
||||
fill: true,
|
||||
borderColor: "#b25110",
|
||||
backgroundColor: gradient_utxo,
|
||||
tension: 0.1
|
||||
tension: 0.1,
|
||||
radius: 0,
|
||||
hoverRadius: 4
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
new Chart(document.getElementById("4"), {
|
||||
const utxoChart = new Chart(document.getElementById("4"), {
|
||||
type: 'line',
|
||||
data: data,
|
||||
options: options
|
||||
@@ -286,27 +318,184 @@
|
||||
};
|
||||
|
||||
var data = {
|
||||
labels: {{ date }},
|
||||
labels: {{ m_date }},
|
||||
datasets: [
|
||||
{
|
||||
label: 'Kernels',
|
||||
data: {{ kernels }},
|
||||
data: {{ m_kernels }},
|
||||
fill: true,
|
||||
borderColor: "#b25110",
|
||||
backgroundColor: gradient_utxo,
|
||||
tension: 0.1
|
||||
tension: 0.1,
|
||||
radius: 0,
|
||||
hoverRadius: 4
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
new Chart(document.getElementById("5"), {
|
||||
const kerChart = new Chart(document.getElementById("5"), {
|
||||
type: 'line',
|
||||
data: data,
|
||||
options: options
|
||||
});
|
||||
|
||||
</script>
|
||||
|
||||
// Fill Transactions&Fees chart based on period
|
||||
function txnTimePeriod(period) {
|
||||
if(period.value == 'month') {
|
||||
txnChart.data.labels = {{ m_date }};
|
||||
txnChart.data.datasets[0].data = {{ m_txns }};
|
||||
txnChart.data.datasets[1].data = {{ m_fees }};
|
||||
}
|
||||
|
||||
if(period.value == 'sixmonths') {
|
||||
txnChart.data.labels = {{ sm_date }};
|
||||
txnChart.data.datasets[0].data = {{ sm_txns }};
|
||||
txnChart.data.datasets[1].data = {{ sm_fees }};
|
||||
}
|
||||
|
||||
if(period.value == 'year') {
|
||||
txnChart.data.labels = {{ y_date }};
|
||||
txnChart.data.datasets[0].data = {{ y_txns }};
|
||||
txnChart.data.datasets[1].data = {{ y_fees }};
|
||||
}
|
||||
|
||||
if(period.value == 'all') {
|
||||
txnChart.data.labels = {{ date }};
|
||||
txnChart.data.datasets[0].data = {{ txns }};
|
||||
txnChart.data.datasets[1].data = {{ fees }};
|
||||
}
|
||||
|
||||
txnChart.update();
|
||||
}
|
||||
|
||||
// Fill Hashrate chart based on period
|
||||
function hashTimePeriod(period) {
|
||||
if(period.value == 'month') {
|
||||
hashChart.data.labels = {{ m_date }};
|
||||
hashChart.data.datasets[0].data = {{ m_hashrate }};
|
||||
}
|
||||
|
||||
if(period.value == 'sixmonths') {
|
||||
hashChart.data.labels = {{ sm_date }};
|
||||
hashChart.data.datasets[0].data = {{ sm_hashrate }};
|
||||
}
|
||||
|
||||
if(period.value == 'year') {
|
||||
hashChart.data.labels = {{ y_date }};
|
||||
hashChart.data.datasets[0].data = {{ y_hashrate }};
|
||||
}
|
||||
|
||||
if(period.value == 'all') {
|
||||
hashChart.data.labels = {{ date }};
|
||||
hashChart.data.datasets[0].data = {{ hashrate }};
|
||||
}
|
||||
|
||||
hashChart.update();
|
||||
}
|
||||
|
||||
// Fill Utxo chart based on period
|
||||
function utxoTimePeriod(period) {
|
||||
if(period.value == 'month') {
|
||||
utxoChart.data.labels = {{ m_date }};
|
||||
utxoChart.data.datasets[0].data = {{ m_utxos }};
|
||||
}
|
||||
|
||||
if(period.value == 'sixmonths') {
|
||||
utxoChart.data.labels = {{ sm_date }};
|
||||
utxoChart.data.datasets[0].data = {{ sm_utxos }};
|
||||
}
|
||||
|
||||
if(period.value == 'year') {
|
||||
utxoChart.data.labels = {{ y_date }};
|
||||
utxoChart.data.datasets[0].data = {{ y_utxos }};
|
||||
}
|
||||
|
||||
if(period.value == 'all') {
|
||||
utxoChart.data.labels = {{ date }};
|
||||
utxoChart.data.datasets[0].data = {{ utxos }};
|
||||
}
|
||||
|
||||
utxoChart.update();
|
||||
}
|
||||
|
||||
// Fill Kernels chart based on period
|
||||
function kerTimePeriod(period) {
|
||||
if(period.value == 'month') {
|
||||
kerChart.data.labels = {{ m_date }};
|
||||
kerChart.data.datasets[0].data = {{ m_kernels }};
|
||||
}
|
||||
|
||||
if(period.value == 'sixmonths') {
|
||||
kerChart.data.labels = {{ sm_date }};
|
||||
kerChart.data.datasets[0].data = {{ sm_kernels }};
|
||||
}
|
||||
|
||||
if(period.value == 'year') {
|
||||
kerChart.data.labels = {{ y_date }};
|
||||
kerChart.data.datasets[0].data = {{ y_kernels }};
|
||||
}
|
||||
|
||||
if(period.value == 'all') {
|
||||
kerChart.data.labels = {{ date }};
|
||||
kerChart.data.datasets[0].data = {{ kernels }};
|
||||
}
|
||||
|
||||
kerChart.update();
|
||||
}
|
||||
|
||||
//
|
||||
// Code to toogle .active class on period switch buttons
|
||||
//
|
||||
|
||||
// Get the container element
|
||||
var txnBtnContainer = document.getElementById("txnBtnGroup");
|
||||
var hashBtnContainer = document.getElementById("hashBtnGroup");
|
||||
var utxoBtnContainer = document.getElementById("utxoBtnGroup");
|
||||
var kerBtnContainer = document.getElementById("kerBtnGroup");
|
||||
|
||||
// Get all buttons with class="btn" inside the container
|
||||
var txnBtns = txnBtnContainer.getElementsByClassName("btn");
|
||||
var hashBtns = hashBtnContainer.getElementsByClassName("btn");
|
||||
var utxoBtns = utxoBtnContainer.getElementsByClassName("btn");
|
||||
var kerBtns = kerBtnContainer.getElementsByClassName("btn");
|
||||
|
||||
// Loop through the buttons and add the active class to the current/clicked button
|
||||
for (var i = 0; i < txnBtns.length; i++) {
|
||||
txnBtns[i].addEventListener("click", function() {
|
||||
var current = txnBtnContainer.getElementsByClassName("active");
|
||||
current[0].className = current[0].className.replace(" active", "");
|
||||
this.className += " active";
|
||||
});
|
||||
}
|
||||
|
||||
// Loop through the buttons and add the active class to the current/clicked button
|
||||
for (var i = 0; i < hashBtns.length; i++) {
|
||||
hashBtns[i].addEventListener("click", function() {
|
||||
var current = hashBtnContainer.getElementsByClassName("active");
|
||||
current[0].className = current[0].className.replace(" active", "");
|
||||
this.className += " active";
|
||||
});
|
||||
}
|
||||
|
||||
// Loop through the buttons and add the active class to the current/clicked button
|
||||
for (var i = 0; i < utxoBtns.length; i++) {
|
||||
utxoBtns[i].addEventListener("click", function() {
|
||||
var current = utxoBtnContainer.getElementsByClassName("active");
|
||||
current[0].className = current[0].className.replace(" active", "");
|
||||
this.className += " active";
|
||||
});
|
||||
}
|
||||
|
||||
// Loop through the buttons and add the active class to the current/clicked button
|
||||
for (var i = 0; i < kerBtns.length; i++) {
|
||||
kerBtns[i].addEventListener("click", function() {
|
||||
var current = kerBtnContainer.getElementsByClassName("active");
|
||||
current[0].className = current[0].className.replace(" active", "");
|
||||
this.className += " active";
|
||||
});
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
{% endblock %}
|
||||
|
||||
|
Reference in New Issue
Block a user