unified http api client

This commit is contained in:
Jędrzej Stuczyński
2023-10-03 10:16:33 +01:00
parent e8eb031fbd
commit e6eb83f350
30 changed files with 499 additions and 738 deletions
@@ -19,6 +19,7 @@ use nym_api_requests::models::{
use nym_network_defaults::NymNetworkDetails;
use url::Url;
pub use crate::nym_api::NymApiClientExt;
pub use nym_mixnet_contract_common::{
mixnode::MixNodeDetails, GatewayBond, IdentityKey, IdentityKeyRef, MixId,
};
@@ -177,7 +178,7 @@ impl<C, S> Client<C, S> {
}
pub fn change_nym_api(&mut self, new_endpoint: Url) {
self.nym_api.change_url(new_endpoint)
self.nym_api.change_base_url(new_endpoint)
}
pub async fn get_cached_mixnodes(&self) -> Result<Vec<MixNodeDetails>, ValidatorClientError> {
@@ -251,7 +252,7 @@ impl NymApiClient {
}
pub fn change_nym_api(&mut self, new_endpoint: Url) {
self.nym_api.change_url(new_endpoint);
self.nym_api.change_base_url(new_endpoint);
}
pub async fn get_cached_active_mixnodes(
@@ -1,20 +1,7 @@
// Copyright 2022-2023 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use http_api_client::HttpClientError;
use nym_api_requests::models::RequestError;
use thiserror::Error;
#[derive(Error, Debug)]
pub enum NymAPIError {
#[error("There was an issue with the REST request - {source}")]
ReqwestClientError {
#[from]
source: reqwest::Error,
},
#[error("Not found")]
NotFound,
#[error("Request failed with error message - {0}")]
GenericRequestFailure(String),
#[error("The nym API has failed to resolve our request. It returned status code {status} and additional error message: {}", error.message())]
ApiRequestFailure { status: u16, error: RequestError },
}
pub type NymAPIError = HttpClientError<RequestError>;
@@ -3,6 +3,8 @@
use crate::nym_api::error::NymAPIError;
use crate::nym_api::routes::{CORE_STATUS_COUNT, SINCE_ARG};
use async_trait::async_trait;
use http_api_client::{ApiClient, NO_PARAMS};
use nym_api_requests::coconut::{
BlindSignRequestBody, BlindedSignatureResponse, VerifyCredentialBody, VerifyCredentialResponse,
};
@@ -10,133 +12,29 @@ use nym_api_requests::models::{
ComputeRewardEstParam, GatewayBondAnnotated, GatewayCoreStatusResponse,
GatewayStatusReportResponse, GatewayUptimeHistoryResponse, InclusionProbabilityResponse,
MixNodeBondAnnotated, MixnodeCoreStatusResponse, MixnodeStatusReportResponse,
MixnodeStatusResponse, MixnodeUptimeHistoryResponse, RequestError, RewardEstimationResponse,
MixnodeStatusResponse, MixnodeUptimeHistoryResponse, RewardEstimationResponse,
StakeSaturationResponse, UptimeResponse,
};
use nym_mixnet_contract_common::mixnode::MixNodeDetails;
use nym_mixnet_contract_common::{GatewayBond, IdentityKeyRef, MixId};
use nym_name_service_common::response::NamesListResponse;
use nym_service_provider_directory_common::response::ServicesListResponse;
use reqwest::{Response, StatusCode};
use serde::{Deserialize, Serialize};
use url::Url;
pub mod error;
pub mod routes;
type PathSegments<'a> = &'a [&'a str];
type Params<'a, K, V> = &'a [(K, V)];
pub use http_api_client::Client;
const NO_PARAMS: Params<'_, &'_ str, &'_ str> = &[];
#[derive(Clone)]
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, NymAPIError>
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_nym_api<T, K, V>(
&self,
path: PathSegments<'_>,
params: Params<'_, K, V>,
) -> Result<T, NymAPIError>
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 if res.status() == StatusCode::NOT_FOUND {
Err(NymAPIError::NotFound)
} else {
Err(NymAPIError::GenericRequestFailure(res.text().await?))
}
}
// This works for endpoints returning Result<Json<T>, ErrorResponse>
async fn query_nym_api_fallible<T, K, V>(
&self,
path: PathSegments<'_>,
params: Params<'_, K, V>,
) -> Result<T, NymAPIError>
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(NymAPIError::ApiRequestFailure {
status: status.as_u16(),
error: request_error,
})
}
}
async fn post_nym_api<B, T, K, V>(
&self,
path: PathSegments<'_>,
params: Params<'_, K, V>,
json_body: &B,
) -> Result<T, NymAPIError>
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(NymAPIError::GenericRequestFailure(response.text().await?))
}
}
pub async fn get_mixnodes(&self) -> Result<Vec<MixNodeDetails>, NymAPIError> {
self.query_nym_api(&[routes::API_VERSION, routes::MIXNODES], NO_PARAMS)
#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
pub trait NymApiClientExt: ApiClient {
async fn get_mixnodes(&self) -> Result<Vec<MixNodeDetails>, NymAPIError> {
self.get_json(&[routes::API_VERSION, routes::MIXNODES], NO_PARAMS)
.await
}
pub async fn get_mixnodes_detailed(&self) -> Result<Vec<MixNodeBondAnnotated>, NymAPIError> {
self.query_nym_api(
async fn get_mixnodes_detailed(&self) -> Result<Vec<MixNodeBondAnnotated>, NymAPIError> {
self.get_json(
&[
routes::API_VERSION,
routes::STATUS,
@@ -148,8 +46,8 @@ impl Client {
.await
}
pub async fn get_gateways_detailed(&self) -> Result<Vec<GatewayBondAnnotated>, NymAPIError> {
self.query_nym_api(
async fn get_gateways_detailed(&self) -> Result<Vec<GatewayBondAnnotated>, NymAPIError> {
self.get_json(
&[
routes::API_VERSION,
routes::STATUS,
@@ -161,10 +59,10 @@ impl Client {
.await
}
pub async fn get_mixnodes_detailed_unfiltered(
async fn get_mixnodes_detailed_unfiltered(
&self,
) -> Result<Vec<MixNodeBondAnnotated>, NymAPIError> {
self.query_nym_api(
self.get_json(
&[
routes::API_VERSION,
routes::STATUS,
@@ -176,23 +74,21 @@ impl Client {
.await
}
pub async fn get_gateways(&self) -> Result<Vec<GatewayBond>, NymAPIError> {
self.query_nym_api(&[routes::API_VERSION, routes::GATEWAYS], NO_PARAMS)
async fn get_gateways(&self) -> Result<Vec<GatewayBond>, NymAPIError> {
self.get_json(&[routes::API_VERSION, routes::GATEWAYS], NO_PARAMS)
.await
}
pub async fn get_active_mixnodes(&self) -> Result<Vec<MixNodeDetails>, NymAPIError> {
self.query_nym_api(
async fn get_active_mixnodes(&self) -> Result<Vec<MixNodeDetails>, NymAPIError> {
self.get_json(
&[routes::API_VERSION, routes::MIXNODES, routes::ACTIVE],
NO_PARAMS,
)
.await
}
pub async fn get_active_mixnodes_detailed(
&self,
) -> Result<Vec<MixNodeBondAnnotated>, NymAPIError> {
self.query_nym_api(
async fn get_active_mixnodes_detailed(&self) -> Result<Vec<MixNodeBondAnnotated>, NymAPIError> {
self.get_json(
&[
routes::API_VERSION,
routes::STATUS,
@@ -205,19 +101,19 @@ impl Client {
.await
}
pub async fn get_rewarded_mixnodes(&self) -> Result<Vec<MixNodeDetails>, NymAPIError> {
self.query_nym_api(
async fn get_rewarded_mixnodes(&self) -> Result<Vec<MixNodeDetails>, NymAPIError> {
self.get_json(
&[routes::API_VERSION, routes::MIXNODES, routes::REWARDED],
NO_PARAMS,
)
.await
}
pub async fn get_mixnode_report(
async fn get_mixnode_report(
&self,
mix_id: MixId,
) -> Result<MixnodeStatusReportResponse, NymAPIError> {
self.query_nym_api(
self.get_json(
&[
routes::API_VERSION,
routes::STATUS,
@@ -230,11 +126,11 @@ impl Client {
.await
}
pub async fn get_gateway_report(
async fn get_gateway_report(
&self,
identity: IdentityKeyRef<'_>,
) -> Result<GatewayStatusReportResponse, NymAPIError> {
self.query_nym_api(
self.get_json(
&[
routes::API_VERSION,
routes::STATUS,
@@ -247,11 +143,11 @@ impl Client {
.await
}
pub async fn get_mixnode_history(
async fn get_mixnode_history(
&self,
mix_id: MixId,
) -> Result<MixnodeUptimeHistoryResponse, NymAPIError> {
self.query_nym_api(
self.get_json(
&[
routes::API_VERSION,
routes::STATUS,
@@ -264,11 +160,11 @@ impl Client {
.await
}
pub async fn get_gateway_history(
async fn get_gateway_history(
&self,
identity: IdentityKeyRef<'_>,
) -> Result<GatewayUptimeHistoryResponse, NymAPIError> {
self.query_nym_api(
self.get_json(
&[
routes::API_VERSION,
routes::STATUS,
@@ -281,10 +177,10 @@ impl Client {
.await
}
pub async fn get_rewarded_mixnodes_detailed(
async fn get_rewarded_mixnodes_detailed(
&self,
) -> Result<Vec<MixNodeBondAnnotated>, NymAPIError> {
self.query_nym_api(
self.get_json(
&[
routes::API_VERSION,
routes::STATUS,
@@ -297,13 +193,13 @@ impl Client {
.await
}
pub async fn get_gateway_core_status_count(
async fn get_gateway_core_status_count(
&self,
identity: IdentityKeyRef<'_>,
since: Option<i64>,
) -> Result<GatewayCoreStatusResponse, NymAPIError> {
if let Some(since) = since {
self.query_nym_api(
self.get_json(
&[
routes::API_VERSION,
routes::STATUS_ROUTES,
@@ -315,7 +211,7 @@ impl Client {
)
.await
} else {
self.query_nym_api(
self.get_json(
&[
routes::API_VERSION,
routes::STATUS_ROUTES,
@@ -328,13 +224,13 @@ impl Client {
}
}
pub async fn get_mixnode_core_status_count(
async fn get_mixnode_core_status_count(
&self,
mix_id: MixId,
since: Option<i64>,
) -> Result<MixnodeCoreStatusResponse, NymAPIError> {
if let Some(since) = since {
self.query_nym_api(
self.get_json(
&[
routes::API_VERSION,
routes::STATUS_ROUTES,
@@ -346,7 +242,7 @@ impl Client {
)
.await
} else {
self.query_nym_api(
self.get_json(
&[
routes::API_VERSION,
routes::STATUS_ROUTES,
@@ -360,11 +256,11 @@ impl Client {
}
}
pub async fn get_mixnode_status(
async fn get_mixnode_status(
&self,
mix_id: MixId,
) -> Result<MixnodeStatusResponse, NymAPIError> {
self.query_nym_api(
self.get_json(
&[
routes::API_VERSION,
routes::STATUS_ROUTES,
@@ -377,11 +273,11 @@ impl Client {
.await
}
pub async fn get_mixnode_reward_estimation(
async fn get_mixnode_reward_estimation(
&self,
mix_id: MixId,
) -> Result<RewardEstimationResponse, NymAPIError> {
self.query_nym_api_fallible(
self.get_json(
&[
routes::API_VERSION,
routes::STATUS_ROUTES,
@@ -394,12 +290,12 @@ impl Client {
.await
}
pub async fn compute_mixnode_reward_estimation(
async fn compute_mixnode_reward_estimation(
&self,
mix_id: MixId,
request_body: &ComputeRewardEstParam,
) -> Result<RewardEstimationResponse, NymAPIError> {
self.post_nym_api(
self.post_json(
&[
routes::API_VERSION,
routes::STATUS_ROUTES,
@@ -413,11 +309,11 @@ impl Client {
.await
}
pub async fn get_mixnode_stake_saturation(
async fn get_mixnode_stake_saturation(
&self,
mix_id: MixId,
) -> Result<StakeSaturationResponse, NymAPIError> {
self.query_nym_api_fallible(
self.get_json(
&[
routes::API_VERSION,
routes::STATUS_ROUTES,
@@ -430,11 +326,11 @@ impl Client {
.await
}
pub async fn get_mixnode_inclusion_probability(
async fn get_mixnode_inclusion_probability(
&self,
mix_id: MixId,
) -> Result<InclusionProbabilityResponse, NymAPIError> {
self.query_nym_api_fallible(
self.get_json(
&[
routes::API_VERSION,
routes::STATUS_ROUTES,
@@ -447,11 +343,8 @@ impl Client {
.await
}
pub async fn get_mixnode_avg_uptime(
&self,
mix_id: MixId,
) -> Result<UptimeResponse, NymAPIError> {
self.query_nym_api_fallible(
async fn get_mixnode_avg_uptime(&self, mix_id: MixId) -> Result<UptimeResponse, NymAPIError> {
self.get_json(
&[
routes::API_VERSION,
routes::STATUS_ROUTES,
@@ -464,11 +357,11 @@ impl Client {
.await
}
pub async fn blind_sign(
async fn blind_sign(
&self,
request_body: &BlindSignRequestBody,
) -> Result<BlindedSignatureResponse, NymAPIError> {
self.post_nym_api(
self.post_json(
&[
routes::API_VERSION,
routes::COCONUT_ROUTES,
@@ -481,11 +374,11 @@ impl Client {
.await
}
pub async fn verify_bandwidth_credential(
async fn verify_bandwidth_credential(
&self,
request_body: &VerifyCredentialBody,
) -> Result<VerifyCredentialResponse, NymAPIError> {
self.post_nym_api(
self.post_json(
&[
routes::API_VERSION,
routes::COCONUT_ROUTES,
@@ -498,118 +391,20 @@ impl Client {
.await
}
pub async fn get_service_providers(&self) -> Result<ServicesListResponse, NymAPIError> {
async fn get_service_providers(&self) -> Result<ServicesListResponse, NymAPIError> {
log::trace!("Getting service providers");
self.query_nym_api(&[routes::API_VERSION, routes::SERVICE_PROVIDERS], NO_PARAMS)
self.get_json(&[routes::API_VERSION, routes::SERVICE_PROVIDERS], NO_PARAMS)
.await
}
//pub async fn get_registered_names(&self) -> Result<Vec<NameEntry>, NymAPIError> {
pub async fn get_registered_names(&self) -> Result<NamesListResponse, NymAPIError> {
//async fn get_registered_names(&self) -> Result<Vec<NameEntry>, NymAPIError> {
async fn get_registered_names(&self) -> Result<NamesListResponse, NymAPIError> {
log::trace!("Getting registered names");
self.query_nym_api(&[routes::API_VERSION, routes::REGISTERED_NAMES], NO_PARAMS)
self.get_json(&[routes::API_VERSION, routes::REGISTERED_NAMES], NO_PARAMS)
.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()
);
}
}
#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
impl NymApiClientExt for Client {}