From 8d2743831a53cc78a6d9b7bcfe94658b326268fd Mon Sep 17 00:00:00 2001 From: aglkm <39521015+aglkm@users.noreply.github.com> Date: Wed, 18 Mar 2026 20:52:41 +0300 Subject: [PATCH] added network page --- Cargo.lock | 20 +++- Cargo.toml | 3 +- src/data.rs | 68 ++++++++++- src/exconfig.rs | 15 ++- src/main.rs | 44 ++++++-- src/requests.rs | 136 ++++++++++++++++++++-- src/worker.rs | 10 +- templates/base.html.tera | 31 ++++++ templates/network.html.tera | 217 ++++++++++++++++++++++++++++++++++++ 9 files changed, 514 insertions(+), 30 deletions(-) create mode 100644 templates/network.html.tera diff --git a/Cargo.lock b/Cargo.lock index dfb2299..6158b80 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -386,6 +386,17 @@ version = "0.8.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" +[[package]] +name = "country-emoji" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88f41dcf7008e5669247a47e0e704390241718e347fecfd2e6865ede17a3e798" +dependencies = [ + "once_cell", + "regex", + "unidecode", +] + [[package]] name = "cpufeatures" version = "0.2.14" @@ -837,11 +848,12 @@ dependencies = [ [[package]] name = "grin-explorer" -version = "0.1.8" +version = "0.1.9" dependencies = [ "anyhow", "chrono", "config", + "country-emoji", "either", "env_logger", "fs_extra", @@ -2719,6 +2731,12 @@ version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "229730647fbc343e3a80e463c1db7f78f3855d3f3739bee0dda773c9a037c90a" +[[package]] +name = "unidecode" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "402bb19d8e03f1d1a7450e2bd613980869438e0666331be3e073089124aa1adc" + [[package]] name = "url" version = "2.5.2" diff --git a/Cargo.toml b/Cargo.toml index a6e8c20..e19ac08 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "grin-explorer" -version = "0.1.8" +version = "0.1.9" edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html @@ -9,6 +9,7 @@ edition = "2021" anyhow = "1.0.86" chrono = "0.4.37" config = "0.14.0" +country-emoji = "0.3.3" either = "1.11.0" env_logger = "0.11.3" fs_extra = "1.3.0" diff --git a/src/data.rs b/src/data.rs index dc6dcc0..fdad46a 100644 --- a/src/data.rs +++ b/src/data.rs @@ -192,7 +192,8 @@ pub struct ExplorerConfig { pub foreign_api_secret: String, pub coingecko_api: String, pub public_api: String, - pub external_nodes: Vec, + pub stats_source: Vec, + pub public_nodes: Vec, pub database: String, } @@ -210,7 +211,8 @@ impl ExplorerConfig { foreign_api_secret: String::new(), coingecko_api: String::new(), public_api: String::new(), - external_nodes: Vec::new(), + stats_source: Vec::new(), + public_nodes: Vec::new(), database: String::new(), } } @@ -275,3 +277,65 @@ impl Statistics { } } +// Public node data +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct PublicNode { + pub name: String, + pub version: String, + pub height: String, + pub hash: String, +} + +impl PublicNode { + pub fn new() -> PublicNode { + PublicNode { + name: String::new(), + version: String::new(), + height: String::new(), + hash: String::new(), + } + } +} + + +// Connected node data +#[derive(PartialEq, Eq, Debug, Clone, Serialize, Deserialize)] +pub struct ConnectedNode { + pub address: String, + pub user_agent: String, + pub location: String, + pub flag: String, + pub isp: String, +} + +impl ConnectedNode { + pub fn new() -> ConnectedNode { + ConnectedNode { + address: String::new(), + user_agent: String::new(), + location: String::new(), + flag: String::new(), + isp: String::new(), + } + } +} + + +// Network data +#[derive(Debug, Serialize)] +pub struct NetStats { + pub pub_nodes: Vec, + pub reach_nodes: Vec, + pub conn_nodes: Vec, +} + +impl NetStats { + pub fn new() -> NetStats { + NetStats { + pub_nodes: Vec::new(), + reach_nodes: Vec::new(), + conn_nodes: Vec::new(), + } + } +} + diff --git a/src/exconfig.rs b/src/exconfig.rs index e736c79..7b20d76 100644 --- a/src/exconfig.rs +++ b/src/exconfig.rs @@ -43,15 +43,24 @@ lazy_static! { Err(_e) => {}, } - match toml.get_array("external_nodes") { + match toml.get_array("stats_source") { Ok(nodes) => { for endpoint in nodes.clone() { - cfg.external_nodes.push(endpoint.into_string().unwrap()); + cfg.stats_source.push(endpoint.into_string().unwrap()); } }, Err(_e) => {}, } - + + match toml.get_array("public_nodes") { + Ok(nodes) => { + for endpoint in nodes.clone() { + cfg.public_nodes.push(endpoint.into_string().unwrap()); + } + }, + Err(_e) => {}, + } + match toml.get_string("database") { Ok(v) => cfg.database = v, Err(_e) => {}, diff --git a/src/main.rs b/src/main.rs index 1e806b2..9b9680b 100644 --- a/src/main.rs +++ b/src/main.rs @@ -12,7 +12,7 @@ use std::time::Duration; 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::data::{Block, Dashboard, Kernel, NetStats, Output, Statistics, Transactions, OUTPUT_SIZE, KERNEL_SIZE}; use crate::exconfig::CONFIG; mod data; @@ -387,6 +387,7 @@ fn donate() -> Template { }) } + // Rendering API Overview page. #[get("/api_overview")] fn api_overview() -> Template { @@ -398,6 +399,21 @@ fn api_overview() -> Template { } +// Rendering Network page. +#[get("/network")] +fn network(netstats: &State>>) -> Template { + let data = netstats.lock().unwrap(); + + Template::render("network", context! { + route: "network", + pub_nodes: &data.pub_nodes, + reach_nodes: &data.reach_nodes, + reach_len: &data.reach_nodes.len(), + cg_api: CONFIG.coingecko_api.clone(), + }) +} + + // Owner API. // Whitelisted methods: get_connected_peers, get_peers, get_status. #[post("/v2/owner", data="")] @@ -879,14 +895,16 @@ async fn main() { info!("starting up."); - let dash = Arc::new(Mutex::new(Dashboard::new())); - let dash_clone = dash.clone(); - let blocks = Arc::new(Mutex::new(Vec::::new())); - let blocks_clone = blocks.clone(); - let txns = Arc::new(Mutex::new(Transactions::new())); - let txns_clone = txns.clone(); - let stats = Arc::new(Mutex::new(Statistics::new())); - let stats_clone = stats.clone(); + let dash = Arc::new(Mutex::new(Dashboard::new())); + let dash_clone = dash.clone(); + let blocks = Arc::new(Mutex::new(Vec::::new())); + let blocks_clone = blocks.clone(); + let txns = Arc::new(Mutex::new(Transactions::new())); + let txns_clone = txns.clone(); + let stats = Arc::new(Mutex::new(Statistics::new())); + let stats_clone = stats.clone(); + let netstats = Arc::new(Mutex::new(NetStats::new())); + let netstats_clone = netstats.clone(); let mut ready_data = false; let mut ready_stats = false; @@ -925,7 +943,8 @@ async fn main() { tokio::spawn(async move { loop { let result = worker::data(dash_clone.clone(), blocks_clone.clone(), - txns_clone.clone(), stats_clone.clone()).await; + txns_clone.clone(), stats_clone.clone(), + netstats_clone.clone()).await; match result { Ok(_v) => { @@ -945,7 +964,7 @@ async fn main() { if date != date_now { date = date_now; let result = worker::stats(dash_clone.clone(), txns_clone.clone(), - stats_clone.clone()).await; + stats_clone.clone(), netstats_clone.clone()).await; match result { Ok(_v) => { @@ -976,6 +995,7 @@ async fn main() { .manage(blocks) .manage(txns) .manage(stats) + .manage(netstats) .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, @@ -986,7 +1006,7 @@ async fn main() { soft_supply, production_cost, reward_ratio, breakeven_cost, last_block_age, block_list_by_height, block_list_index, search, kernel, output, api_owner, api_foreign, stats, unspent_outputs, kernels, - emission, api_overview, donate, supply_raw]) + emission, api_overview, donate, supply_raw, network]) .mount("/static", FileServer::from("static")) .attach(Template::custom(|engines| {engines.tera.register_filter("separate_with_commas", separate_with_commas)})) .launch() diff --git a/src/requests.rs b/src/requests.rs index f9de793..9cb4b91 100644 --- a/src/requests.rs +++ b/src/requests.rs @@ -1,15 +1,17 @@ use chrono::{Utc, DateTime}; +use country_emoji::code_to_flag; use fs_extra::dir::get_size; use humantime::format_duration; use num_format::{Locale, ToFormattedString}; use reqwest::Error; use serde_json::Value; +use std::net::{TcpStream, SocketAddr}; use std::sync::{Arc, Mutex}; use std::sync::atomic::{AtomicU32, Ordering}; use std::time::Duration; use std::collections::HashMap; -use crate::data::{Block, Dashboard, Kernel, Output, Statistics, Transactions}; +use crate::data::{Block, ConnectedNode, Dashboard, Kernel, NetStats, Output, PublicNode, Statistics, Transactions}; use crate::data::{KERNEL_WEIGHT, INPUT_WEIGHT, OUTPUT_WEIGHT, KERNEL_SIZE, INPUT_SIZE, OUTPUT_SIZE}; use crate::exconfig::CONFIG; @@ -128,16 +130,20 @@ pub async fn get_mempool(dashboard: Arc>) -> Result<(), anyhow: // Collecting: inbound, outbound, user_agent. -pub async fn get_connected_peers(dashboard: Arc>, statistics: Arc>) -> Result<(), anyhow::Error> { - let mut peers = HashMap::new(); - let mut addrs = Vec::new(); - let mut inbound = 0; - let mut outbound = 0; +pub async fn get_connected_peers(dashboard: Arc>, statistics: Arc>, + netstats: Arc>) -> Result<(), anyhow::Error> { + let mut peers = HashMap::new(); + let mut addrs = Vec::new(); + let mut connected_nodes = Vec::::new(); + let mut inbound = 0; + let mut outbound = 0; let resp = call("get_connected_peers", "[]", "1", "owner").await?; if resp != Value::Null { + let mut node = ConnectedNode::new(); + // Collecting peers from local node for peer in resp["result"]["Ok"].as_array().unwrap() { if peer["direction"] == "Inbound" { inbound += 1; @@ -149,22 +155,29 @@ pub async fn get_connected_peers(dashboard: Arc>, statistics: A if !addrs.contains(&peer["addr"].to_string()) { *peers.entry(peer["user_agent"].to_string()).or_insert(0) += 1; addrs.push(peer["addr"].to_string()); + node.address = peer["addr"].as_str().unwrap().to_string(); + node.user_agent = peer["user_agent"].as_str().unwrap().to_string(); + connected_nodes.push(node.clone()); } } } - // Collecting peers stats from external endpoints - for endpoint in CONFIG.external_nodes.clone() { + // Collecting peers from external endpoints + for endpoint in CONFIG.stats_source.clone() { match call_external("get_connected_peers", "[]", "1", "owner", endpoint).await { Ok(resp) => { if resp != Value::Null { + let mut node = ConnectedNode::new(); if resp["result"]["Ok"].is_null() == false { for peer in resp["result"]["Ok"].as_array().unwrap() { // Collecting user_agent nodes stats if !addrs.contains(&peer["addr"].to_string()) { *peers.entry(peer["user_agent"].to_string()).or_insert(0) += 1; addrs.push(peer["addr"].to_string()); + node.address = peer["addr"].as_str().unwrap().to_string(); + node.user_agent = peer["user_agent"].as_str().unwrap().to_string(); + connected_nodes.push(node.clone()); } } } @@ -194,6 +207,10 @@ pub async fn get_connected_peers(dashboard: Arc>, statistics: A dash.inbound = inbound; dash.outbound = outbound; + let mut nstats = netstats.lock().unwrap(); + + nstats.conn_nodes = connected_nodes.clone(); + Ok(()) } @@ -780,3 +797,106 @@ pub async fn get_unspent_outputs(dashboard: Arc>) -> Result<(), Ok(()) } +// Get public nodes data +pub async fn get_pubnodes_stats(netstats: Arc>) -> Result<(), anyhow::Error> { + let mut nodes = Vec::::new(); + + for endpoint in CONFIG.public_nodes.clone() { + let mut node = PublicNode::new(); + + node.name = endpoint + .strip_prefix("https://") + .or_else(|| endpoint.strip_prefix("http://")) + .unwrap_or(&endpoint) + .split('/') + .next() + .unwrap_or("") + .to_string(); + + match call_external("get_version", "[]", "1", "foreign", endpoint.clone()).await { + Ok(resp) => { + if resp != Value::Null { + node.version = resp["result"]["Ok"]["node_version"].as_str().unwrap().to_string(); + } + }, + Err(e) => { + warn!("{}", e); + continue; + }, + } + + match call_external("get_tip", "[]", "1", "foreign", endpoint).await { + Ok(resp) => { + if resp != Value::Null { + node.height = resp["result"]["Ok"]["height"].to_string(); + node.hash = resp["result"]["Ok"]["last_block_pushed"].as_str().unwrap().to_string(); + } + }, + Err(e) => { + warn!("{}", e); + continue; + }, + } + nodes.push(node); + } + + let mut network = netstats.lock().unwrap(); + + network.pub_nodes = nodes.clone(); + + Ok(()) +} + + +pub async fn get_reachable_nodes(netstats: Arc>) -> Result<(), anyhow::Error> { + let conn_nodes = get_conn_nodes(netstats.clone()); + let mut reach_nodes = Vec::::new(); + + for mut node in conn_nodes.clone() { + let socket_addr: SocketAddr = match node.address.parse() { + Ok(addr) => addr, + Err(_) => continue, + }; + + // Attempt to connect with a timeout + match TcpStream::connect_timeout(&socket_addr, Duration::from_millis(3000)) { + Ok(_) => { + let client = reqwest::Client::new(); + if let Some((ip, _port)) = node.address.split_once(':') { + //let url = format!("https://api.country.is/{}", ip); + let url = format!("http://ip-api.com/json/{}", ip); + + let resp: Value = client.get(&url).send().await?.json().await?; + if resp != Value::Null && resp["status"] == "success" { + if let Some(code) = resp["countryCode"].as_str() { + node.location = resp["country"].as_str().unwrap().to_string(); + node.isp = resp["isp"].as_str().unwrap().to_string(); + node.flag = code_to_flag(code).unwrap().to_string(); + } + } + } + + if !reach_nodes.contains(&node) { + reach_nodes.push(node.clone()); + } + }, + Err(_) => { + reach_nodes.retain(|value| value.address != node.address); + }, + } + } + + let mut nstats = netstats.lock().unwrap(); + + nstats.reach_nodes = reach_nodes.clone(); + + Ok(()) +} + + +pub fn get_conn_nodes(netstats: Arc>) -> Vec { + let nstats = netstats.lock().unwrap(); + + nstats.conn_nodes.clone() +} + diff --git a/src/worker.rs b/src/worker.rs index 88692d3..e139742 100644 --- a/src/worker.rs +++ b/src/worker.rs @@ -3,6 +3,7 @@ use std::sync::{Arc, Mutex}; use crate::data::Block; use crate::data::Dashboard; +use crate::data::NetStats; use crate::data::Statistics; use crate::data::Transactions; use crate::database; @@ -12,23 +13,26 @@ use crate::requests; // Collecting main data. pub async fn data(dash: Arc>, blocks: Arc>>, - txns: Arc>, stats: Arc>) -> Result<(), anyhow::Error> { + txns: Arc>, stats: Arc>, + netstats: Arc>) -> Result<(), anyhow::Error> { let _ = requests::get_status(dash.clone()).await?; let _ = requests::get_mempool(dash.clone()).await?; - let _ = requests::get_connected_peers(dash.clone(), stats.clone()).await?; + let _ = requests::get_connected_peers(dash.clone(), stats.clone(), netstats.clone()).await?; let _ = requests::get_market(dash.clone()).await?; 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?; + let _ = requests::get_pubnodes_stats(netstats.clone()).await?; Ok(()) } // Collecting statistics. -pub async fn stats(dash: Arc>, txns: Arc>, stats: Arc>) -> Result<(), anyhow::Error> { +pub async fn stats(dash: Arc>, txns: Arc>, stats: Arc>, netstats: Arc>) -> Result<(), anyhow::Error> { let _ = requests::get_unspent_outputs(dash.clone()).await?; + let _ = requests::get_reachable_nodes(netstats.clone()).await?; let mut stats = stats.lock().unwrap(); let dash = dash.lock().unwrap(); diff --git a/templates/base.html.tera b/templates/base.html.tera index 11ba851..44b0bfd 100644 --- a/templates/base.html.tera +++ b/templates/base.html.tera @@ -50,6 +50,9 @@ + {% elif route == "block_list" or route == "block_list_by_height" %} + {% elif route == "stats" %} + {% elif route == "emission" %} + + {% elif route == "network" %} + + + + + {% else %} + {% endif %} diff --git a/templates/network.html.tera b/templates/network.html.tera new file mode 100644 index 0000000..137f6cc --- /dev/null +++ b/templates/network.html.tera @@ -0,0 +1,217 @@ +{% extends "base" %} + +{% block content %} + + + +
+
+
+
PUBLIC NODES
+
+
+
+ +
+
+
+
+
+ NAME +
+
+
+
+
+
+ VERSION +
+
+
+
+
+
+ LATEST BLOCK +
+
+
+
+
+
+ BLOCK HASH +
+
+
+
+ + + {% for node in pub_nodes %} +
+
+
+
{{ node.name }}
+
+
+
+
+
{{ node.version }}
+
+
+
+
+
{{ node.height }}
+
+
+ +
+ {% endfor %} + +
+ + +
+ {% for node in pub_nodes %} +
+
+
+   +
{{ node.name }}
+
+
+
+
Version
+
{{ node.version }}
+
+
+
+
Latest Block
+
{{ node.height }}
+
+
+ +
+
+ {% endfor %} +
+
+
+ + +
+
+
+
REACHABLE NODES ({{ reach_len }})
+
+
+
+ +
+
+
+
+
+ ADDRESS +
+
+
+
+
+
+ USER AGENT +
+
+
+
+
+
+ ISP +
+
+
+
+
+
+ LOCATION +
+
+
+
+ + + {% for node in reach_nodes %} +
+
+
+
{{ node.address }}
+
+
+
+
+
{{ node.user_agent }}
+
+
+
+
+
{{ node.isp }}
+
+
+
+
+
{{ node.location }} {{ node.flag }}
+
+
+
+ {% endfor %} + +
+ + +
+ {% for node in reach_nodes %} +
+
+
+   +
{{ node.address }}
+
+
+
+
User Agent
+
{{ node.user_agent }}
+
+
+
+
ISP
+
{{ node.isp }}
+
+
+
+
Location
+
{{ node.location }} {{ node.flag }}
+
+
+
+ {% endfor %} +
+
+
+ + +
+ +{% endblock %}