added network page

This commit is contained in:
aglkm
2026-03-18 20:52:41 +03:00
parent a0840ded3d
commit 8d2743831a
9 changed files with 514 additions and 30 deletions
+66 -2
View File
@@ -192,7 +192,8 @@ pub struct ExplorerConfig {
pub foreign_api_secret: String,
pub coingecko_api: String,
pub public_api: String,
pub external_nodes: Vec<String>,
pub stats_source: Vec<String>,
pub public_nodes: Vec<String>,
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<PublicNode>,
pub reach_nodes: Vec<ConnectedNode>,
pub conn_nodes: Vec<ConnectedNode>,
}
impl NetStats {
pub fn new() -> NetStats {
NetStats {
pub_nodes: Vec::new(),
reach_nodes: Vec::new(),
conn_nodes: Vec::new(),
}
}
}
+12 -3
View File
@@ -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) => {},
+32 -12
View File
@@ -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<Arc<Mutex<NetStats>>>) -> 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="<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::<Block>::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::<Block>::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()
+128 -8
View File
@@ -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<Mutex<Dashboard>>) -> Result<(), anyhow:
// Collecting: inbound, outbound, user_agent.
pub async fn get_connected_peers(dashboard: Arc<Mutex<Dashboard>>, statistics: Arc<Mutex<Statistics>>) -> 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<Mutex<Dashboard>>, statistics: Arc<Mutex<Statistics>>,
netstats: Arc<Mutex<NetStats>>) -> Result<(), anyhow::Error> {
let mut peers = HashMap::new();
let mut addrs = Vec::new();
let mut connected_nodes = Vec::<ConnectedNode>::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<Mutex<Dashboard>>, 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<Mutex<Dashboard>>, 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<Mutex<Dashboard>>) -> Result<(),
Ok(())
}
// Get public nodes data
pub async fn get_pubnodes_stats(netstats: Arc<Mutex<NetStats>>) -> Result<(), anyhow::Error> {
let mut nodes = Vec::<PublicNode>::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<Mutex<NetStats>>) -> Result<(), anyhow::Error> {
let conn_nodes = get_conn_nodes(netstats.clone());
let mut reach_nodes = Vec::<ConnectedNode>::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<Mutex<NetStats>>) -> Vec<ConnectedNode> {
let nstats = netstats.lock().unwrap();
nstats.conn_nodes.clone()
}
+7 -3
View File
@@ -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<Mutex<Dashboard>>, blocks: Arc<Mutex<Vec<Block>>>,
txns: Arc<Mutex<Transactions>>, stats: Arc<Mutex<Statistics>>) -> Result<(), anyhow::Error> {
txns: Arc<Mutex<Transactions>>, stats: Arc<Mutex<Statistics>>,
netstats: Arc<Mutex<NetStats>>) -> 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<Mutex<Dashboard>>, txns: Arc<Mutex<Transactions>>, stats: Arc<Mutex<Statistics>>) -> Result<(), anyhow::Error> {
pub async fn stats(dash: Arc<Mutex<Dashboard>>, txns: Arc<Mutex<Transactions>>, stats: Arc<Mutex<Statistics>>, netstats: Arc<Mutex<NetStats>>) -> 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();