// Copyright 2021-2022 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 use crate::validator_api::error::ValidatorAPIError; use crate::validator_api::routes::{CORE_STATUS_COUNT, SINCE_ARG}; use mixnet_contract_common::mixnode::MixNodeDetails; use mixnet_contract_common::{GatewayBond, IdentityKeyRef, MixId}; use reqwest::Response; use serde::{Deserialize, Serialize}; use url::Url; use validator_api_requests::coconut::{ BlindSignRequestBody, BlindedSignatureResponse, CosmosAddressResponse, VerificationKeyResponse, VerifyCredentialBody, VerifyCredentialResponse, }; use validator_api_requests::models::{ GatewayCoreStatusResponse, GatewayStatusReportResponse, GatewayUptimeHistoryResponse, InclusionProbabilityResponse, MixNodeBondAnnotated, MixnodeCoreStatusResponse, MixnodeStatusReportResponse, MixnodeStatusResponse, MixnodeUptimeHistoryResponse, RequestError, RewardEstimationResponse, StakeSaturationResponse, UptimeResponse, }; pub mod error; pub mod routes; type PathSegments<'a> = &'a [&'a str]; type Params<'a, K, V> = &'a [(K, V)]; const NO_PARAMS: Params<'_, &'_ str, &'_ str> = &[]; pub struct Client { url: Url, reqwest_client: reqwest::Client, } impl Client { pub fn new(url: Url) -> Self { let reqwest_client = reqwest::Client::new(); Self { url, reqwest_client, } } pub fn change_url(&mut self, new_url: Url) { self.url = new_url } pub fn current_url(&self) -> &Url { &self.url } async fn send_get_request( &self, path: PathSegments<'_>, params: Params<'_, K, V>, ) -> Result where K: AsRef, V: AsRef, { let url = create_api_url(&self.url, path, params); Ok(self.reqwest_client.get(url).send().await?) } async fn query_validator_api( &self, path: PathSegments<'_>, params: Params<'_, K, V>, ) -> Result where for<'a> T: Deserialize<'a>, K: AsRef, V: AsRef, { let res = self.send_get_request(path, params).await?; if res.status().is_success() { Ok(res.json().await?) } else { Err(ValidatorAPIError::GenericRequestFailure(res.text().await?)) } } // This works for endpoints returning Result, ErrorResponse> async fn query_validator_api_fallible( &self, path: PathSegments<'_>, params: Params<'_, K, V>, ) -> Result where for<'a> T: Deserialize<'a>, K: AsRef, V: AsRef, { let res = self.send_get_request(path, params).await?; let status = res.status(); if res.status().is_success() { Ok(res.json().await?) } else { let request_error: RequestError = res.json().await?; Err(ValidatorAPIError::ApiRequestFailure { status: status.as_u16(), error: request_error, }) } } async fn post_validator_api( &self, path: PathSegments<'_>, params: Params<'_, K, V>, json_body: &B, ) -> Result where B: Serialize + ?Sized, for<'a> T: Deserialize<'a>, K: AsRef, V: AsRef, { let url = create_api_url(&self.url, path, params); let response = self.reqwest_client.post(url).json(json_body).send().await?; if response.status().is_success() { Ok(response.json().await?) } else { Err(ValidatorAPIError::GenericRequestFailure( response.text().await?, )) } } pub async fn get_mixnodes(&self) -> Result, ValidatorAPIError> { self.query_validator_api(&[routes::API_VERSION, routes::MIXNODES], NO_PARAMS) .await } pub async fn get_mixnodes_detailed( &self, ) -> Result, ValidatorAPIError> { self.query_validator_api( &[routes::API_VERSION, routes::MIXNODES, routes::DETAILED], NO_PARAMS, ) .await } pub async fn get_gateways(&self) -> Result, ValidatorAPIError> { self.query_validator_api(&[routes::API_VERSION, routes::GATEWAYS], NO_PARAMS) .await } pub async fn get_active_mixnodes(&self) -> Result, ValidatorAPIError> { self.query_validator_api( &[routes::API_VERSION, routes::MIXNODES, routes::ACTIVE], NO_PARAMS, ) .await } pub async fn get_active_mixnodes_detailed( &self, ) -> Result, ValidatorAPIError> { self.query_validator_api( &[ routes::API_VERSION, routes::MIXNODES, routes::ACTIVE, routes::DETAILED, ], NO_PARAMS, ) .await } pub async fn get_rewarded_mixnodes(&self) -> Result, ValidatorAPIError> { self.query_validator_api( &[routes::API_VERSION, routes::MIXNODES, routes::REWARDED], NO_PARAMS, ) .await } pub async fn get_mixnode_report( &self, mix_id: MixId, ) -> Result { self.query_validator_api( &[ routes::API_VERSION, routes::STATUS, routes::MIXNODE, &mix_id.to_string(), routes::REPORT, ], NO_PARAMS, ) .await } pub async fn get_gateway_report( &self, identity: IdentityKeyRef<'_>, ) -> Result { self.query_validator_api( &[ routes::API_VERSION, routes::STATUS, routes::GATEWAY, identity, routes::REPORT, ], NO_PARAMS, ) .await } pub async fn get_mixnode_history( &self, mix_id: MixId, ) -> Result { self.query_validator_api( &[ routes::API_VERSION, routes::STATUS, routes::MIXNODE, &mix_id.to_string(), routes::HISTORY, ], NO_PARAMS, ) .await } pub async fn get_gateway_history( &self, identity: IdentityKeyRef<'_>, ) -> Result { self.query_validator_api( &[ routes::API_VERSION, routes::STATUS, routes::GATEWAY, identity, routes::HISTORY, ], NO_PARAMS, ) .await } pub async fn get_rewarded_mixnodes_detailed( &self, ) -> Result, ValidatorAPIError> { self.query_validator_api( &[ routes::API_VERSION, routes::MIXNODES, routes::REWARDED, routes::DETAILED, ], NO_PARAMS, ) .await } pub async fn get_gateway_core_status_count( &self, identity: IdentityKeyRef<'_>, since: Option, ) -> Result { if let Some(since) = since { self.query_validator_api( &[ routes::API_VERSION, routes::STATUS_ROUTES, routes::GATEWAY, identity, CORE_STATUS_COUNT, ], &[(SINCE_ARG, since.to_string())], ) .await } else { self.query_validator_api( &[ routes::API_VERSION, routes::STATUS_ROUTES, routes::GATEWAY, identity, ], NO_PARAMS, ) .await } } pub async fn get_mixnode_core_status_count( &self, mix_id: MixId, since: Option, ) -> Result { if let Some(since) = since { self.query_validator_api( &[ routes::API_VERSION, routes::STATUS_ROUTES, routes::MIXNODE, &mix_id.to_string(), CORE_STATUS_COUNT, ], &[(SINCE_ARG, since.to_string())], ) .await } else { self.query_validator_api( &[ routes::API_VERSION, routes::STATUS_ROUTES, routes::MIXNODE, &mix_id.to_string(), CORE_STATUS_COUNT, ], NO_PARAMS, ) .await } } pub async fn get_mixnode_status( &self, mix_id: MixId, ) -> Result { self.query_validator_api( &[ routes::API_VERSION, routes::STATUS_ROUTES, routes::MIXNODE, &mix_id.to_string(), routes::STATUS, ], NO_PARAMS, ) .await } pub async fn get_mixnode_reward_estimation( &self, mix_id: MixId, ) -> Result { self.query_validator_api_fallible( &[ routes::API_VERSION, routes::STATUS_ROUTES, routes::MIXNODE, &mix_id.to_string(), routes::REWARD_ESTIMATION, ], NO_PARAMS, ) .await } pub async fn get_mixnode_stake_saturation( &self, mix_id: MixId, ) -> Result { self.query_validator_api_fallible( &[ routes::API_VERSION, routes::STATUS_ROUTES, routes::MIXNODE, &mix_id.to_string(), routes::STAKE_SATURATION, ], NO_PARAMS, ) .await } pub async fn get_mixnode_inclusion_probability( &self, mix_id: MixId, ) -> Result { self.query_validator_api_fallible( &[ routes::API_VERSION, routes::STATUS_ROUTES, routes::MIXNODE, &mix_id.to_string(), routes::INCLUSION_CHANCE, ], NO_PARAMS, ) .await } pub async fn get_mixnode_avg_uptime( &self, mix_id: MixId, ) -> Result { self.query_validator_api_fallible( &[ routes::API_VERSION, routes::STATUS_ROUTES, routes::MIXNODE, &mix_id.to_string(), routes::AVG_UPTIME, ], NO_PARAMS, ) .await } pub async fn blind_sign( &self, request_body: &BlindSignRequestBody, ) -> Result { self.post_validator_api( &[ routes::API_VERSION, routes::COCONUT_ROUTES, routes::BANDWIDTH, routes::COCONUT_BLIND_SIGN, ], NO_PARAMS, request_body, ) .await } pub async fn partial_bandwidth_credential( &self, request_body: &str, ) -> Result { self.post_validator_api( &[ routes::API_VERSION, routes::COCONUT_ROUTES, routes::BANDWIDTH, routes::COCONUT_PARTIAL_BANDWIDTH_CREDENTIAL, ], NO_PARAMS, request_body, ) .await } pub async fn get_coconut_verification_key( &self, ) -> Result { self.query_validator_api( &[ routes::API_VERSION, routes::COCONUT_ROUTES, routes::BANDWIDTH, routes::COCONUT_VERIFICATION_KEY, ], NO_PARAMS, ) .await } pub async fn get_cosmos_address(&self) -> Result { self.query_validator_api( &[ routes::API_VERSION, routes::COCONUT_ROUTES, routes::BANDWIDTH, routes::COCONUT_COSMOS_ADDRESS, ], NO_PARAMS, ) .await } pub async fn verify_bandwidth_credential( &self, request_body: &VerifyCredentialBody, ) -> Result { self.post_validator_api( &[ routes::API_VERSION, routes::COCONUT_ROUTES, routes::BANDWIDTH, routes::COCONUT_VERIFY_BANDWIDTH_CREDENTIAL, ], NO_PARAMS, request_body, ) .await } } // utility function that should solve the double slash problem in validator API forever. fn create_api_url, V: AsRef>( base: &Url, segments: PathSegments<'_>, params: Params<'_, K, V>, ) -> Url { let mut url = base.clone(); let mut path_segments = url .path_segments_mut() .expect("provided validator url does not have a base!"); for segment in segments { let segment = segment.strip_prefix('/').unwrap_or(segment); let segment = segment.strip_suffix('/').unwrap_or(segment); path_segments.push(segment); } // I don't understand why compiler couldn't figure out that it's no longer used // and can be dropped drop(path_segments); if !params.is_empty() { url.query_pairs_mut().extend_pairs(params); } url } #[cfg(test)] mod tests { use super::*; #[test] fn creating_api_path() { let base_url: Url = "http://foomp.com".parse().unwrap(); // works with 1 segment assert_eq!( "http://foomp.com/foo", create_api_url(&base_url, &["foo"], NO_PARAMS).as_str() ); // works with 2 segments assert_eq!( "http://foomp.com/foo/bar", create_api_url(&base_url, &["foo", "bar"], NO_PARAMS).as_str() ); // works with leading slash assert_eq!( "http://foomp.com/foo", create_api_url(&base_url, &["/foo"], NO_PARAMS).as_str() ); assert_eq!( "http://foomp.com/foo/bar", create_api_url(&base_url, &["/foo", "bar"], NO_PARAMS).as_str() ); assert_eq!( "http://foomp.com/foo/bar", create_api_url(&base_url, &["foo", "/bar"], NO_PARAMS).as_str() ); // works with trailing slash assert_eq!( "http://foomp.com/foo", create_api_url(&base_url, &["foo/"], NO_PARAMS).as_str() ); assert_eq!( "http://foomp.com/foo/bar", create_api_url(&base_url, &["foo/", "bar"], NO_PARAMS).as_str() ); assert_eq!( "http://foomp.com/foo/bar", create_api_url(&base_url, &["foo", "bar/"], NO_PARAMS).as_str() ); // works with both leading and trailing slash assert_eq!( "http://foomp.com/foo", create_api_url(&base_url, &["/foo/"], NO_PARAMS).as_str() ); assert_eq!( "http://foomp.com/foo/bar", create_api_url(&base_url, &["/foo/", "/bar/"], NO_PARAMS).as_str() ); // adds params assert_eq!( "http://foomp.com/foo/bar?foomp=baz", create_api_url(&base_url, &["foo", "bar"], &[("foomp", "baz")]).as_str() ); assert_eq!( "http://foomp.com/foo/bar?arg1=val1&arg2=val2", create_api_url( &base_url, &["/foo/", "/bar/"], &[("arg1", "val1"), ("arg2", "val2")] ) .as_str() ); } }