Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 61b58c2e5e | |||
| b2e3d5f3a3 |
Generated
+16
-1
@@ -766,7 +766,7 @@ dependencies = [
|
|||||||
name = "config"
|
name = "config"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"handlebars",
|
"handlebars 3.5.5",
|
||||||
"humantime-serde",
|
"humantime-serde",
|
||||||
"network-defaults",
|
"network-defaults",
|
||||||
"serde",
|
"serde",
|
||||||
@@ -1582,6 +1582,7 @@ name = "explorer-api"
|
|||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"chrono",
|
"chrono",
|
||||||
|
"handlebars 4.1.3",
|
||||||
"humantime-serde",
|
"humantime-serde",
|
||||||
"isocountry",
|
"isocountry",
|
||||||
"log",
|
"log",
|
||||||
@@ -2227,6 +2228,20 @@ dependencies = [
|
|||||||
"serde_json",
|
"serde_json",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "handlebars"
|
||||||
|
version = "4.1.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "66b09e2322d20d14bc2572401ce7c1d60b4748580a76c230ed9c1f8938f0c833"
|
||||||
|
dependencies = [
|
||||||
|
"log",
|
||||||
|
"pest",
|
||||||
|
"pest_derive",
|
||||||
|
"quick-error 2.0.1",
|
||||||
|
"serde",
|
||||||
|
"serde_json",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "hashbrown"
|
name = "hashbrown"
|
||||||
version = "0.11.2"
|
version = "0.11.2"
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ okapi = { version = "0.6.0-alpha-1", features = ["derive_json_schema"] }
|
|||||||
rocket_okapi = "0.7.0-alpha-1"
|
rocket_okapi = "0.7.0-alpha-1"
|
||||||
log = "0.4.0"
|
log = "0.4.0"
|
||||||
pretty_env_logger = "0.4.0"
|
pretty_env_logger = "0.4.0"
|
||||||
|
handlebars = "4.1.3"
|
||||||
|
|
||||||
mixnet-contract = { path = "../common/mixnet-contract" }
|
mixnet-contract = { path = "../common/mixnet-contract" }
|
||||||
network-defaults = { path = "../common/network-defaults" }
|
network-defaults = { path = "../common/network-defaults" }
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ use reqwest::Error as ReqwestError;
|
|||||||
use crate::country_statistics::country_nodes_distribution::CountryNodesDistribution;
|
use crate::country_statistics::country_nodes_distribution::CountryNodesDistribution;
|
||||||
use crate::mix_nodes::{GeoLocation, Location};
|
use crate::mix_nodes::{GeoLocation, Location};
|
||||||
use crate::state::ExplorerApiStateContext;
|
use crate::state::ExplorerApiStateContext;
|
||||||
|
use std::env;
|
||||||
|
|
||||||
pub mod country_nodes_distribution;
|
pub mod country_nodes_distribution;
|
||||||
pub mod http;
|
pub mod http;
|
||||||
@@ -18,18 +19,21 @@ impl CountryStatistics {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn start(mut self) {
|
pub(crate) fn start(mut self) {
|
||||||
info!("Spawning task runner...");
|
if env::var("DEV_MODE").is_err() {
|
||||||
tokio::spawn(async move {
|
info!("Spawning task runner...");
|
||||||
let mut interval_timer = tokio::time::interval(std::time::Duration::from_secs(60 * 60));
|
tokio::spawn(async move {
|
||||||
loop {
|
let mut interval_timer =
|
||||||
// wait for the next interval tick
|
tokio::time::interval(std::time::Duration::from_secs(60 * 60));
|
||||||
interval_timer.tick().await;
|
loop {
|
||||||
|
// wait for the next interval tick
|
||||||
|
interval_timer.tick().await;
|
||||||
|
|
||||||
info!("Running task...");
|
info!("Running task...");
|
||||||
self.calculate_nodes_per_country().await;
|
self.calculate_nodes_per_country().await;
|
||||||
info!("Done");
|
info!("Done");
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Retrieves the current list of mixnodes from the validators and calculates how many nodes are in each country
|
/// Retrieves the current list of mixnodes from the validators and calculates how many nodes are in each country
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ use rocket_okapi::swagger_ui::make_swagger_ui;
|
|||||||
use crate::country_statistics::http::country_statistics_make_default_routes;
|
use crate::country_statistics::http::country_statistics_make_default_routes;
|
||||||
use crate::http::swagger::get_docs;
|
use crate::http::swagger::get_docs;
|
||||||
use crate::mix_node::http::mix_node_make_default_routes;
|
use crate::mix_node::http::mix_node_make_default_routes;
|
||||||
|
use crate::mix_node::templates::Templates;
|
||||||
use crate::ping::http::ping_make_default_routes;
|
use crate::ping::http::ping_make_default_routes;
|
||||||
use crate::state::ExplorerApiStateContext;
|
use crate::state::ExplorerApiStateContext;
|
||||||
|
|
||||||
@@ -29,6 +30,8 @@ pub(crate) fn start(state: ExplorerApiStateContext) {
|
|||||||
.to_cors()
|
.to_cors()
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
|
let templates = Templates::new();
|
||||||
|
|
||||||
let config = rocket::config::Config::release_default();
|
let config = rocket::config::Config::release_default();
|
||||||
rocket::build()
|
rocket::build()
|
||||||
.configure(config)
|
.configure(config)
|
||||||
@@ -38,6 +41,7 @@ pub(crate) fn start(state: ExplorerApiStateContext) {
|
|||||||
.mount("/swagger", make_swagger_ui(&get_docs()))
|
.mount("/swagger", make_swagger_ui(&get_docs()))
|
||||||
.register("/", catchers![not_found])
|
.register("/", catchers![not_found])
|
||||||
.manage(state)
|
.manage(state)
|
||||||
|
.manage(templates)
|
||||||
.attach(cors)
|
.attach(cors)
|
||||||
.launch()
|
.launch()
|
||||||
.await
|
.await
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
use reqwest::Error as ReqwestError;
|
use reqwest::Error as ReqwestError;
|
||||||
|
use rocket::response::content::Html;
|
||||||
use rocket::serde::json::Json;
|
use rocket::serde::json::Json;
|
||||||
use rocket::{Route, State};
|
use rocket::{Route, State};
|
||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
@@ -6,11 +7,12 @@ use serde::Serialize;
|
|||||||
use mixnet_contract::{Addr, Coin, Layer, MixNode};
|
use mixnet_contract::{Addr, Coin, Layer, MixNode};
|
||||||
|
|
||||||
use crate::mix_node::models::{NodeDescription, NodeStats};
|
use crate::mix_node::models::{NodeDescription, NodeStats};
|
||||||
|
use crate::mix_node::templates::{PreviewTemplateData, Templates};
|
||||||
use crate::mix_nodes::{get_mixnode_delegations, Location};
|
use crate::mix_nodes::{get_mixnode_delegations, Location};
|
||||||
use crate::state::ExplorerApiStateContext;
|
use crate::state::ExplorerApiStateContext;
|
||||||
|
|
||||||
pub fn mix_node_make_default_routes() -> Vec<Route> {
|
pub fn mix_node_make_default_routes() -> Vec<Route> {
|
||||||
routes_with_openapi![get_delegations, get_description, get_stats, list]
|
routes_with_openapi![get_delegations, get_description, get_stats, list, preview]
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug, Serialize, JsonSchema)]
|
#[derive(Clone, Debug, Serialize, JsonSchema)]
|
||||||
@@ -51,6 +53,34 @@ pub(crate) async fn list(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[openapi(tag = "mix_node")]
|
||||||
|
#[get("/<pubkey>/preview")]
|
||||||
|
pub(crate) async fn preview(
|
||||||
|
pubkey: &str,
|
||||||
|
templates: &State<Templates>,
|
||||||
|
state: &State<ExplorerApiStateContext>,
|
||||||
|
) -> Html<String> {
|
||||||
|
match get_mixnode_description(pubkey, state).await {
|
||||||
|
Some(node_description) => {
|
||||||
|
// use handlebars to render an HTML output for an OpenGraph / Twitter preview - this is
|
||||||
|
// used in social media apps / messenger apps that show previews for links
|
||||||
|
match templates.render_preview(PreviewTemplateData {
|
||||||
|
title: node_description.name,
|
||||||
|
description: node_description.description,
|
||||||
|
url: format!(
|
||||||
|
"https://testnet-milhon-explorer.nymtech.net/nym/mixnodes/{}",
|
||||||
|
pubkey
|
||||||
|
),
|
||||||
|
image_url: String::from("https://media2.giphy.com/media/pwyW4XDmtqjG8/200.gif?cid=dda24d50ae15e44c38783edc824618df68645c6af2592b28&rid=200.gif&ct=g"),
|
||||||
|
}) {
|
||||||
|
Ok(r) => Html(r),
|
||||||
|
Err(_e) => Html(String::from("Oh no, something went wrong!")),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None => Html(String::from("Sorry, mix node not found")),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[openapi(tag = "mix_node")]
|
#[openapi(tag = "mix_node")]
|
||||||
#[get("/<pubkey>/delegations")]
|
#[get("/<pubkey>/delegations")]
|
||||||
pub(crate) async fn get_delegations(pubkey: &str) -> Json<Vec<mixnet_contract::Delegation>> {
|
pub(crate) async fn get_delegations(pubkey: &str) -> Json<Vec<mixnet_contract::Delegation>> {
|
||||||
@@ -63,6 +93,13 @@ pub(crate) async fn get_description(
|
|||||||
pubkey: &str,
|
pubkey: &str,
|
||||||
state: &State<ExplorerApiStateContext>,
|
state: &State<ExplorerApiStateContext>,
|
||||||
) -> Option<Json<NodeDescription>> {
|
) -> Option<Json<NodeDescription>> {
|
||||||
|
get_mixnode_description(pubkey, state).await.map(Json)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn get_mixnode_description(
|
||||||
|
pubkey: &str,
|
||||||
|
state: &State<ExplorerApiStateContext>,
|
||||||
|
) -> Option<NodeDescription> {
|
||||||
match state
|
match state
|
||||||
.inner
|
.inner
|
||||||
.mix_node_cache
|
.mix_node_cache
|
||||||
@@ -72,7 +109,7 @@ pub(crate) async fn get_description(
|
|||||||
{
|
{
|
||||||
Some(cache_value) => {
|
Some(cache_value) => {
|
||||||
trace!("Returning cached value for {}", pubkey);
|
trace!("Returning cached value for {}", pubkey);
|
||||||
Some(Json(cache_value))
|
Some(cache_value)
|
||||||
}
|
}
|
||||||
None => {
|
None => {
|
||||||
trace!("No valid cache value for {}", pubkey);
|
trace!("No valid cache value for {}", pubkey);
|
||||||
@@ -91,7 +128,7 @@ pub(crate) async fn get_description(
|
|||||||
.mix_node_cache
|
.mix_node_cache
|
||||||
.set_description(pubkey, response.clone())
|
.set_description(pubkey, response.clone())
|
||||||
.await;
|
.await;
|
||||||
Some(Json(response))
|
Some(response)
|
||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
error!(
|
error!(
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
mod cache;
|
mod cache;
|
||||||
pub(crate) mod http;
|
pub(crate) mod http;
|
||||||
pub(crate) mod models;
|
pub(crate) mod models;
|
||||||
|
pub(crate) mod templates;
|
||||||
|
|||||||
@@ -0,0 +1,73 @@
|
|||||||
|
use handlebars::{Handlebars, RenderError};
|
||||||
|
use serde::Serialize;
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub(crate) struct Templates {
|
||||||
|
handlebars: Handlebars<'static>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Templates {
|
||||||
|
pub(crate) fn new() -> Self {
|
||||||
|
let mut handlebars = Handlebars::new();
|
||||||
|
|
||||||
|
assert!(handlebars
|
||||||
|
.register_template_string("preview", PREVIEW_TEMPLATE)
|
||||||
|
.is_ok());
|
||||||
|
|
||||||
|
Templates { handlebars }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn render_preview(&self, data: PreviewTemplateData) -> Result<String, RenderError> {
|
||||||
|
self.handlebars.render("preview", &data)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Serialize, JsonSchema)]
|
||||||
|
pub(crate) struct PreviewTemplateData {
|
||||||
|
pub(crate) title: String,
|
||||||
|
pub(crate) description: String,
|
||||||
|
pub(crate) url: String,
|
||||||
|
pub(crate) image_url: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
const PREVIEW_TEMPLATE: &str = r#"<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
|
|
||||||
|
<title>{{ title }}</title>
|
||||||
|
<meta name="description" content="{{ description }}">
|
||||||
|
|
||||||
|
<meta property="og:type" content="article">
|
||||||
|
<meta property="og:url" content="{{ url }}">
|
||||||
|
<meta property="og:title" content="{{ title }}">
|
||||||
|
<meta property="og:description" content="{{ description }}">
|
||||||
|
<meta property="og:image" content="{{ image_url }}">
|
||||||
|
|
||||||
|
<meta name="twitter:card" value="summary_large_image">
|
||||||
|
<meta name="twitter:title" value="{{ title }}">
|
||||||
|
<meta name="twitter:description" value="{{ description }}">
|
||||||
|
<meta name="twitter:image" value="{{ image_url }}">
|
||||||
|
<meta name="twitter:site" value="@nymtech">
|
||||||
|
|
||||||
|
<meta http-equiv="refresh" content="0;url={{url}}" />
|
||||||
|
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol";
|
||||||
|
}
|
||||||
|
.meme {
|
||||||
|
padding-top: 20px;
|
||||||
|
}
|
||||||
|
.meme > img {
|
||||||
|
max-width: 200px;
|
||||||
|
max-height: 200px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<h1>{{title}}</h1>
|
||||||
|
<div>{{description}}<div>
|
||||||
|
</body>
|
||||||
|
</html>"#;
|
||||||
Reference in New Issue
Block a user