diff --git a/stake-o-matic/src/validators_app.rs b/stake-o-matic/src/validators_app.rs new file mode 100644 index 0000000000..4d8a9b588c --- /dev/null +++ b/stake-o-matic/src/validators_app.rs @@ -0,0 +1,196 @@ +use serde::{Deserialize, Serialize}; +use std::collections::HashMap; + +#[allow(dead_code)] +#[derive(Debug)] +pub enum ClusterJson { + MainnetBeta, + Testnet, +} + +impl Default for ClusterJson { + fn default() -> Self { + Self::MainnetBeta + } +} + +impl AsRef for ClusterJson { + fn as_ref(&self) -> &str { + match self { + Self::MainnetBeta => "mainnet.json", + Self::Testnet => "testnet.json", + } + } +} + +const DEFAULT_BASE_URL: &str = "https://www.validators.app/api/v1/"; +const TOKEN_HTTP_HEADER_NAME: &str = "Token"; + +#[derive(Debug)] +pub struct ClientConfig { + pub base_url: String, + pub cluster: ClusterJson, + pub api_token: String, +} + +impl Default for ClientConfig { + fn default() -> Self { + Self { + base_url: DEFAULT_BASE_URL.to_string(), + cluster: ClusterJson::default(), + api_token: String::default(), + } + } +} + +#[derive(Debug)] +enum Endpoint { + Ping, + Validators, +} + +impl Endpoint { + fn with_cluster(path: &str, cluster: &ClusterJson) -> String { + format!("{}/{}", path, cluster.as_ref()) + } + pub fn path(&self, cluster: &ClusterJson) -> String { + match self { + Self::Ping => "ping.json".to_string(), + Self::Validators => Self::with_cluster("validators", cluster), + } + } +} + +#[derive(Debug, Deserialize, Serialize)] +struct PingResponse { + answer: String, +} + +#[derive(Debug, Deserialize, Serialize)] +pub struct ValidatorsResponseEntry { + pub account: Option, + pub active_stake: Option, + pub commission: Option, + pub created_at: Option, + pub data_center_concentration_score: Option, + pub data_center_host: Option, + pub data_center_key: Option, + pub delinquent: Option, + pub details: Option, + pub keybase_id: Option, + pub name: Option, + pub network: Option, + pub ping_time: Option, + pub published_information_score: Option, + pub root_distance_score: Option, + pub security_report_score: Option, + pub skipped_slot_percent: Option, + pub skipped_slot_score: Option, + pub skipped_slots: Option, + pub software_version: Option, + pub software_version_score: Option, + pub stake_concentration_score: Option, + pub total_score: Option, + pub updated_at: Option, + pub url: Option, + pub vote_account: Option, + pub vote_distance_score: Option, + pub www_url: Option, +} + +#[derive(Debug, Deserialize, Serialize)] +pub struct ValidatorsResponse(Vec); + +impl AsRef> for ValidatorsResponse { + fn as_ref(&self) -> &Vec { + &self.0 + } +} + +#[allow(dead_code)] +#[derive(Debug, Clone, Copy)] +pub enum SortKind { + Score, + Name, + Stake, +} + +impl std::fmt::Display for SortKind { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + match self { + Self::Score => write!(f, "score"), + Self::Name => write!(f, "name"), + Self::Stake => write!(f, "stake"), + } + } +} + +pub type Limit = u32; + +pub struct Client { + base_url: reqwest::Url, + cluster: ClusterJson, + api_token: String, + client: reqwest::blocking::Client, +} + +impl Client { + pub fn new>(api_token: T) -> Self { + let config = ClientConfig { + api_token: api_token.as_ref().to_string(), + ..ClientConfig::default() + }; + Self::new_with_config(config) + } + + pub fn new_with_config(config: ClientConfig) -> Self { + let ClientConfig { + base_url, + cluster, + api_token, + } = config; + Self { + base_url: reqwest::Url::parse(&base_url).unwrap(), + cluster, + api_token, + client: reqwest::blocking::Client::new(), + } + } + + fn request( + &self, + endpoint: Endpoint, + query: &HashMap, + ) -> reqwest::Result { + let url = self.base_url.join(&endpoint.path(&self.cluster)).unwrap(); + let request = self + .client + .get(url) + .header(TOKEN_HTTP_HEADER_NAME, &self.api_token) + .query(&query) + .build()?; + self.client.execute(request) + } + + #[allow(dead_code)] + pub fn ping(&self) -> reqwest::Result<()> { + let response = self.request(Endpoint::Ping, &HashMap::new())?; + response.json::().map(|_| ()) + } + + pub fn validators( + &self, + sort: Option, + limit: Option, + ) -> reqwest::Result { + let mut query = HashMap::new(); + if let Some(sort) = sort { + query.insert("sort".into(), sort.to_string()); + } + if let Some(limit) = limit { + query.insert("limit".into(), limit.to_string()); + } + let response = self.request(Endpoint::Validators, &query)?; + response.json::() + } +}