3727370b9e
* Improved error propagation for fallible validator api queries * Updated changelog
593 lines
17 KiB
Rust
593 lines
17 KiB
Rust
// Copyright 2021-2022 - Nym Technologies SA <contact@nymtech.net>
|
|
// 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<K, V>(
|
|
&self,
|
|
path: PathSegments<'_>,
|
|
params: Params<'_, K, V>,
|
|
) -> Result<Response, ValidatorAPIError>
|
|
where
|
|
K: AsRef<str>,
|
|
V: AsRef<str>,
|
|
{
|
|
let url = create_api_url(&self.url, path, params);
|
|
Ok(self.reqwest_client.get(url).send().await?)
|
|
}
|
|
|
|
async fn query_validator_api<T, K, V>(
|
|
&self,
|
|
path: PathSegments<'_>,
|
|
params: Params<'_, K, V>,
|
|
) -> Result<T, ValidatorAPIError>
|
|
where
|
|
for<'a> T: Deserialize<'a>,
|
|
K: AsRef<str>,
|
|
V: AsRef<str>,
|
|
{
|
|
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<Json<T>, ErrorResponse>
|
|
async fn query_validator_api_fallible<T, K, V>(
|
|
&self,
|
|
path: PathSegments<'_>,
|
|
params: Params<'_, K, V>,
|
|
) -> Result<T, ValidatorAPIError>
|
|
where
|
|
for<'a> T: Deserialize<'a>,
|
|
K: AsRef<str>,
|
|
V: AsRef<str>,
|
|
{
|
|
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<B, T, K, V>(
|
|
&self,
|
|
path: PathSegments<'_>,
|
|
params: Params<'_, K, V>,
|
|
json_body: &B,
|
|
) -> Result<T, ValidatorAPIError>
|
|
where
|
|
B: Serialize + ?Sized,
|
|
for<'a> T: Deserialize<'a>,
|
|
K: AsRef<str>,
|
|
V: AsRef<str>,
|
|
{
|
|
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<Vec<MixNodeDetails>, ValidatorAPIError> {
|
|
self.query_validator_api(&[routes::API_VERSION, routes::MIXNODES], NO_PARAMS)
|
|
.await
|
|
}
|
|
|
|
pub async fn get_mixnodes_detailed(
|
|
&self,
|
|
) -> Result<Vec<MixNodeBondAnnotated>, ValidatorAPIError> {
|
|
self.query_validator_api(
|
|
&[routes::API_VERSION, routes::MIXNODES, routes::DETAILED],
|
|
NO_PARAMS,
|
|
)
|
|
.await
|
|
}
|
|
|
|
pub async fn get_gateways(&self) -> Result<Vec<GatewayBond>, ValidatorAPIError> {
|
|
self.query_validator_api(&[routes::API_VERSION, routes::GATEWAYS], NO_PARAMS)
|
|
.await
|
|
}
|
|
|
|
pub async fn get_active_mixnodes(&self) -> Result<Vec<MixNodeDetails>, ValidatorAPIError> {
|
|
self.query_validator_api(
|
|
&[routes::API_VERSION, routes::MIXNODES, routes::ACTIVE],
|
|
NO_PARAMS,
|
|
)
|
|
.await
|
|
}
|
|
|
|
pub async fn get_active_mixnodes_detailed(
|
|
&self,
|
|
) -> Result<Vec<MixNodeBondAnnotated>, 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<Vec<MixNodeDetails>, 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<MixnodeStatusReportResponse, ValidatorAPIError> {
|
|
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<GatewayStatusReportResponse, ValidatorAPIError> {
|
|
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<MixnodeUptimeHistoryResponse, ValidatorAPIError> {
|
|
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<GatewayUptimeHistoryResponse, ValidatorAPIError> {
|
|
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<Vec<MixNodeBondAnnotated>, 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<i64>,
|
|
) -> Result<GatewayCoreStatusResponse, ValidatorAPIError> {
|
|
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<i64>,
|
|
) -> Result<MixnodeCoreStatusResponse, ValidatorAPIError> {
|
|
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<MixnodeStatusResponse, ValidatorAPIError> {
|
|
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<RewardEstimationResponse, ValidatorAPIError> {
|
|
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<StakeSaturationResponse, ValidatorAPIError> {
|
|
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<InclusionProbabilityResponse, ValidatorAPIError> {
|
|
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<UptimeResponse, ValidatorAPIError> {
|
|
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<BlindedSignatureResponse, ValidatorAPIError> {
|
|
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<BlindedSignatureResponse, ValidatorAPIError> {
|
|
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<VerificationKeyResponse, ValidatorAPIError> {
|
|
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<CosmosAddressResponse, ValidatorAPIError> {
|
|
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<VerifyCredentialResponse, ValidatorAPIError> {
|
|
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<K: AsRef<str>, V: AsRef<str>>(
|
|
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()
|
|
);
|
|
}
|
|
}
|