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
Generated
+14 -16
View File
@@ -3862,6 +3862,17 @@ dependencies = [
"itoa",
]
[[package]]
name = "http-api-client"
version = "0.1.0"
dependencies = [
"async-trait",
"reqwest",
"serde",
"serde_json",
"thiserror",
]
[[package]]
name = "http-body"
version = "0.4.5"
@@ -5550,13 +5561,14 @@ dependencies = [
name = "mix-fetch-wasm"
version = "1.2.0-rc.10"
dependencies = [
"async-trait",
"futures",
"http-api-client",
"js-sys",
"nym-ordered-buffer",
"nym-service-providers-common",
"nym-socks5-requests",
"rand 0.7.3",
"reqwest",
"serde",
"serde-wasm-bindgen",
"thiserror",
@@ -6613,21 +6625,6 @@ dependencies = [
"serde",
]
[[package]]
name = "nym-http-requests"
version = "0.1.0"
dependencies = [
"bytecodec",
"bytes",
"http",
"httpcodec",
"nym-ordered-buffer",
"nym-service-providers-common",
"nym-socks5-requests",
"thiserror",
"url",
]
[[package]]
name = "nym-inclusion-probability"
version = "0.1.0"
@@ -7401,6 +7398,7 @@ dependencies = [
"eyre",
"flate2",
"futures",
"http-api-client",
"itertools",
"log",
"nym-api-requests",
+1 -1
View File
@@ -46,7 +46,7 @@ members = [
"common/crypto",
"common/dkg",
"common/execute",
"common/http-requests",
"common/http-api-client",
"common/inclusion-probability",
"common/ledger",
"common/mixnode-common",
@@ -24,6 +24,7 @@ nym-service-provider-directory-common = { path = "../../cosmwasm-smart-contracts
serde = { workspace = true, features = ["derive"] }
serde_json = { workspace = true }
reqwest = { workspace = true, features = ["json"] }
http-api-client = { path = "../../../common/http-api-client"}
thiserror = { workspace = true }
log = { workspace = true }
url = { workspace = true, features = ["serde"] }
@@ -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 {}
@@ -1,11 +1,11 @@
// Copyright 2022 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use clap::Parser;
use comfy_table::Table;
use crate::context::QueryClientWithNyxd;
use crate::utils::{pretty_cosmwasm_coin, show_error};
use clap::Parser;
use comfy_table::Table;
use nym_validator_client::client::NymApiClientExt;
#[derive(Debug, Parser)]
pub struct Args {
@@ -1,11 +1,11 @@
// Copyright 2022 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use clap::Parser;
use comfy_table::Table;
use crate::context::QueryClientWithNyxd;
use crate::utils::{pretty_decimal_with_denom, show_error};
use clap::Parser;
use comfy_table::Table;
use nym_validator_client::client::NymApiClientExt;
#[derive(Debug, Parser)]
pub struct Args {
@@ -1,12 +1,12 @@
// Copyright 2023 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use clap::Parser;
use comfy_table::Table;
use nym_validator_client::nym_api::error::NymAPIError;
use crate::context::QueryClientWithNyxd;
use crate::utils::show_error;
use clap::Parser;
use comfy_table::Table;
use nym_validator_client::client::NymApiClientExt;
use nym_validator_client::nym_api::error::NymAPIError;
#[derive(Debug, Parser)]
pub struct Args {
@@ -1,12 +1,12 @@
// Copyright 2023 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use clap::Parser;
use comfy_table::Table;
use nym_validator_client::nym_api::error::NymAPIError;
use crate::context::QueryClientWithNyxd;
use crate::utils::show_error;
use clap::Parser;
use comfy_table::Table;
use nym_validator_client::client::NymApiClientExt;
use nym_validator_client::nym_api::error::NymAPIError;
#[derive(Debug, Parser)]
pub struct Args {
+18
View File
@@ -0,0 +1,18 @@
[package]
name = "http-api-client"
version = "0.1.0"
authors.workspace = true
repository.workspace = true
homepage.workspace = true
documentation.workspace = true
edition.workspace = true
license.workspace = true
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
async-trait = { workspace = true }
reqwest = { workspace = true, features = ["json"] }
serde = { workspace = true }
serde_json = { workspace = true }
thiserror = { workspace = true }
+315
View File
@@ -0,0 +1,315 @@
// Copyright 2023 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use async_trait::async_trait;
use reqwest::{IntoUrl, Response, StatusCode, Url};
use serde::de::DeserializeOwned;
use serde::{Deserialize, Serialize};
use std::fmt::Display;
use thiserror::Error;
pub type PathSegments<'a> = &'a [&'a str];
pub type Params<'a, K, V> = &'a [(K, V)];
pub const NO_PARAMS: Params<'_, &'_ str, &'_ str> = &[];
#[derive(Debug, Error)]
pub enum HttpClientError<E: Display = String> {
#[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("failed to resolve request. status code: {status}, additional error message: {error}")]
EndpointFailure { status: u16, error: E },
}
/// A simple extendable client wrapper for http request with extra url sanitization.
#[derive(Debug, Clone)]
pub struct Client {
base_url: Url,
reqwest_client: reqwest::Client,
}
impl Client {
pub fn new(base_url: Url) -> Self {
Client {
base_url,
reqwest_client: reqwest::Client::new(),
}
}
pub fn new_url<U, E>(url: U) -> Result<Self, HttpClientError<E>>
where
U: IntoUrl,
E: Display,
{
Ok(Self::new(url.into_url()?))
}
pub fn change_base_url(&mut self, new_url: Url) {
self.base_url = new_url
}
pub fn current_url(&self) -> &Url {
&self.base_url
}
async fn send_get_request<K, V, E>(
&self,
path: PathSegments<'_>,
params: Params<'_, K, V>,
) -> Result<Response, HttpClientError<E>>
where
K: AsRef<str>,
V: AsRef<str>,
E: Display,
{
let url = sanitize_url(&self.base_url, path, params);
Ok(self.reqwest_client.get(url).send().await?)
}
async fn send_post_request<B, K, V, E>(
&self,
path: PathSegments<'_>,
params: Params<'_, K, V>,
json_body: &B,
) -> Result<Response, HttpClientError<E>>
where
B: Serialize + ?Sized,
K: AsRef<str>,
V: AsRef<str>,
E: Display,
{
let url = sanitize_url(&self.base_url, path, params);
Ok(self.reqwest_client.post(url).json(json_body).send().await?)
}
pub async fn get_json_endpoint<T, K, V, E>(
&self,
path: PathSegments<'_>,
params: Params<'_, K, V>,
) -> Result<T, HttpClientError<E>>
where
for<'a> T: Deserialize<'a>,
K: AsRef<str>,
V: AsRef<str>,
E: Display + DeserializeOwned,
{
let res = self.send_get_request(path, params).await?;
parse_response(res).await
}
pub async fn post_json_endpoint<B, T, K, V, E>(
&self,
path: PathSegments<'_>,
params: Params<'_, K, V>,
json_body: &B,
) -> Result<T, HttpClientError<E>>
where
B: Serialize + ?Sized,
for<'a> T: Deserialize<'a>,
K: AsRef<str>,
V: AsRef<str>,
E: Display + DeserializeOwned,
{
let res = self.send_post_request(path, params, json_body).await?;
parse_response(res).await
}
}
// define those methods on the trait for nicer extensions (and not having to type the thing twice)
#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
pub trait ApiClient {
async fn get_json<T, K, V, E>(
&self,
path: PathSegments<'_>,
params: Params<'_, K, V>,
) -> Result<T, HttpClientError<E>>
where
for<'a> T: Deserialize<'a>,
K: AsRef<str> + Sync,
V: AsRef<str> + Sync,
E: Display + DeserializeOwned;
async fn post_json<B, T, K, V, E>(
&self,
path: PathSegments<'_>,
params: Params<'_, K, V>,
json_body: &B,
) -> Result<T, HttpClientError<E>>
where
B: Serialize + ?Sized + Sync,
for<'a> T: Deserialize<'a>,
K: AsRef<str> + Sync,
V: AsRef<str> + Sync,
E: Display + DeserializeOwned;
}
#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
impl ApiClient for Client {
async fn get_json<T, K, V, E>(
&self,
path: PathSegments<'_>,
params: Params<'_, K, V>,
) -> Result<T, HttpClientError<E>>
where
for<'a> T: Deserialize<'a>,
K: AsRef<str> + Sync,
V: AsRef<str> + Sync,
E: Display + DeserializeOwned,
{
self.get_json_endpoint(path, params).await
}
async fn post_json<B, T, K, V, E>(
&self,
path: PathSegments<'_>,
params: Params<'_, K, V>,
json_body: &B,
) -> Result<T, HttpClientError<E>>
where
B: Serialize + ?Sized + Sync,
for<'a> T: Deserialize<'a>,
K: AsRef<str> + Sync,
V: AsRef<str> + Sync,
E: Display + DeserializeOwned,
{
self.post_json_endpoint(path, params, json_body).await
}
}
// utility function that should solve the double slash problem in API urls forever.
pub fn sanitize_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
}
async fn parse_response<T, E>(res: Response) -> Result<T, HttpClientError<E>>
where
T: DeserializeOwned,
E: DeserializeOwned + Display,
{
let status = res.status();
if res.status().is_success() {
Ok(res.json().await?)
} else if res.status() == StatusCode::NOT_FOUND {
Err(HttpClientError::NotFound)
} else {
let plaintext = res.text().await?;
if let Ok(request_error) = serde_json::from_str(&plaintext) {
Err(HttpClientError::EndpointFailure {
status: status.as_u16(),
error: request_error,
})
} else {
Err(HttpClientError::GenericRequestFailure(plaintext))
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn sanitizing_urls() {
let base_url: Url = "http://foomp.com".parse().unwrap();
// works with 1 segment
assert_eq!(
"http://foomp.com/foo",
sanitize_url(&base_url, &["foo"], NO_PARAMS).as_str()
);
// works with 2 segments
assert_eq!(
"http://foomp.com/foo/bar",
sanitize_url(&base_url, &["foo", "bar"], NO_PARAMS).as_str()
);
// works with leading slash
assert_eq!(
"http://foomp.com/foo",
sanitize_url(&base_url, &["/foo"], NO_PARAMS).as_str()
);
assert_eq!(
"http://foomp.com/foo/bar",
sanitize_url(&base_url, &["/foo", "bar"], NO_PARAMS).as_str()
);
assert_eq!(
"http://foomp.com/foo/bar",
sanitize_url(&base_url, &["foo", "/bar"], NO_PARAMS).as_str()
);
// works with trailing slash
assert_eq!(
"http://foomp.com/foo",
sanitize_url(&base_url, &["foo/"], NO_PARAMS).as_str()
);
assert_eq!(
"http://foomp.com/foo/bar",
sanitize_url(&base_url, &["foo/", "bar"], NO_PARAMS).as_str()
);
assert_eq!(
"http://foomp.com/foo/bar",
sanitize_url(&base_url, &["foo", "bar/"], NO_PARAMS).as_str()
);
// works with both leading and trailing slash
assert_eq!(
"http://foomp.com/foo",
sanitize_url(&base_url, &["/foo/"], NO_PARAMS).as_str()
);
assert_eq!(
"http://foomp.com/foo/bar",
sanitize_url(&base_url, &["/foo/", "/bar/"], NO_PARAMS).as_str()
);
// adds params
assert_eq!(
"http://foomp.com/foo/bar?foomp=baz",
sanitize_url(&base_url, &["foo", "bar"], &[("foomp", "baz")]).as_str()
);
assert_eq!(
"http://foomp.com/foo/bar?arg1=val1&arg2=val2",
sanitize_url(
&base_url,
&["/foo/", "/bar/"],
&[("arg1", "val1"), ("arg2", "val2")]
)
.as_str()
);
}
}
-19
View File
@@ -1,19 +0,0 @@
[package]
name = "nym-http-requests"
version = "0.1.0"
description = "Helper library for sending HTTP requesters over the Nym mixnet"
edition = { workspace = true }
authors = { workspace = true }
license = { workspace = true }
repository = { workspace = true }
[dependencies]
nym-socks5-requests = { path = "../socks5/requests" }
nym-ordered-buffer = { path = "../socks5/ordered-buffer" }
nym-service-providers-common = { path = "../../service-providers/common" }
bytecodec = "0.4.15"
httpcodec = "0.2.3"
bytes = "1"
http = "0.2.9"
thiserror = { workspace = true }
url = "2"
-23
View File
@@ -1,23 +0,0 @@
use std::io;
use thiserror::Error;
#[derive(Debug, Error)]
pub enum MixHttpRequestError {
#[error("invalid Socks5 response")]
InvalidSocks5Response,
#[error("the received Socks5 response was empty")]
EmptySocks5Response,
#[error("bytecodec Error: {0}")]
ByteCodecError(#[from] bytecodec::Error),
#[error("Url parse error: {0}")]
UrlParseError(#[from] url::ParseError),
#[error("could not resolve socket address from the provided URL")]
SocketAddrResolveError {
#[source]
source: io::Error,
},
}
-63
View File
@@ -1,63 +0,0 @@
// Copyright 2021 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use bytecodec::bytes::BytesEncoder;
use bytecodec::io::IoEncodeExt;
use bytecodec::Encode;
use httpcodec::{BodyEncoder, Request, RequestEncoder};
pub mod error;
pub mod socks;
pub fn encode_http_request_as_string(
request: Request<Vec<u8>>,
) -> Result<String, error::MixHttpRequestError> {
// Encode HTTP request as bytes
let mut encoder = RequestEncoder::new(BodyEncoder::new(BytesEncoder::new()));
encoder.start_encoding(request)?;
let mut buf = Vec::new();
encoder.encode_all(&mut buf)?;
Ok(String::from_utf8_lossy(&buf).to_string())
}
#[cfg(test)]
mod http_requests_tests {
use super::*;
use httpcodec::{HeaderField, HttpVersion, Method, RequestTarget};
fn create_http_get_request() -> Request<Vec<u8>> {
let mut request = Request::new(
Method::new("GET").unwrap(),
RequestTarget::new("/.wellknown/wallet/validators.json").unwrap(),
HttpVersion::V1_1,
b"".to_vec(),
);
let mut headers = request.header_mut();
headers.add_field(HeaderField::new("Host", "nymtech.net").unwrap());
request
}
#[test]
fn http_request_ok() {
// Encode HTTP request as bytes
let request = create_http_get_request();
let mut encoder = RequestEncoder::new(BodyEncoder::new(BytesEncoder::new()));
encoder.start_encoding(request).unwrap();
let mut buf = Vec::new();
encoder.encode_all(&mut buf).unwrap();
let body_as_string = String::from_utf8(buf).unwrap();
// replace newlines with \r\n
let expected = r"GET /.wellknown/wallet/validators.json HTTP/1.1
Host: nymtech.net
Content-Length: 0
"
.replace('\n', "\r\n");
assert_eq!(expected, body_as_string);
}
}
-214
View File
@@ -1,214 +0,0 @@
// Copyright 2023 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use crate::error;
use bytecodec::bytes::BytesEncoder;
use bytecodec::bytes::RemainingBytesDecoder;
use bytecodec::io::IoEncodeExt;
use bytecodec::{DecodeExt, Encode};
use httpcodec::{BodyDecoder, ResponseDecoder};
use httpcodec::{BodyEncoder, Request, RequestEncoder};
use nym_service_providers_common::interface::ProviderInterfaceVersion;
use nym_socks5_requests::{SocketData, Socks5ProtocolVersion, Socks5ProviderRequest};
pub fn encode_http_request_as_socks_send_request(
provider_interface: ProviderInterfaceVersion,
socks5_protocol: Socks5ProtocolVersion,
conn_id: u64,
request: Request<Vec<u8>>,
seq: Option<u64>,
local_closed: bool,
) -> Result<nym_socks5_requests::Socks5ProviderRequest, error::MixHttpRequestError> {
// Encode HTTP request as bytes
let mut encoder = RequestEncoder::new(BodyEncoder::new(BytesEncoder::new()));
encoder.start_encoding(request)?;
let mut buf = Vec::new();
encoder.encode_all(&mut buf)?;
// Wrap it as SOCKS send request
let request_content = nym_socks5_requests::request::Socks5Request::new_send(
socks5_protocol,
SocketData::new(seq.unwrap_or_default(), conn_id, local_closed, buf),
);
// and wrap it in provider request
Ok(Socks5ProviderRequest::new_provider_data(
provider_interface,
request_content,
))
}
#[derive(Debug)]
pub struct MixHttpResponse {
// pub connection_id: u64,
// #[deprecated]
// pub is_closed: bool,
pub http_response: httpcodec::Response<Vec<u8>>,
// #[deprecated]
// pub seq: u64,
}
impl MixHttpResponse {
pub fn try_from_bytes(b: &[u8]) -> Result<MixHttpResponse, error::MixHttpRequestError> {
if b.is_empty() {
Err(error::MixHttpRequestError::EmptySocks5Response)
} else {
let mut decoder = ResponseDecoder::<BodyDecoder<RemainingBytesDecoder>>::default();
let http_response = decoder.decode_from_bytes(b)?;
Ok(MixHttpResponse { http_response })
}
}
}
// impl TryFrom<Socks5Response> for MixHttpResponse {
// type Error = error::MixHttpRequestError;
//
// fn try_from(value: Socks5Response) -> Result<Self, Self::Error> {
// if let Socks5ResponseContent::NetworkData { content } = value.content {
// content.try_into()
// } else {
// Err(error::MixHttpRequestError::InvalidSocks5Response)
// }
// }
// }
//
// impl TryFrom<SocketData> for MixHttpResponse {
// type Error = error::MixHttpRequestError;
//
// fn try_from(value: SocketData) -> Result<Self, Self::Error> {
// if value.data.is_empty() {
// Err(error::MixHttpRequestError::EmptySocks5Response)
// } else {
// let mut decoder = ResponseDecoder::<BodyDecoder<RemainingBytesDecoder>>::default();
// let http_response = decoder.decode_from_bytes(value.data.as_ref())?;
//
// Ok(MixHttpResponse {
// connection_id: value.header.connection_id,
// is_closed: value.header.local_socket_closed,
// http_response,
// seq: value.header.seq,
// })
// }
// }
// }
// pub fn decode_socks_response_as_http_response(
// socks5_response: Socks5Response,
// ) -> Result<MixHttpResponse, error::MixHttpRequestError> {
// socks5_response.try_into()
// }
//
// #[cfg(test)]
// mod http_requests_tests {
// use super::*;
// use httpcodec::{HeaderField, HttpVersion, Method, RequestTarget};
// use nym_service_providers_common::interface::Serializable;
// use nym_socks5_requests::Socks5Response;
//
// fn create_http_get_request() -> Request<Vec<u8>> {
// let mut request = Request::new(
// Method::new("GET").unwrap(),
// RequestTarget::new("/.wellknown/wallet/validators.json").unwrap(),
// HttpVersion::V1_1,
// b"".to_vec(),
// );
// let mut headers = request.header_mut();
// headers.add_field(HeaderField::new("Host", "nymtech.net").unwrap());
//
// request
// }
//
// fn create_socks5_request_buffer() -> Vec<u8> {
// let request = create_http_get_request();
// let socks5_request = encode_http_request_as_socks_send_request(
// ProviderInterfaceVersion::new_current(),
// Socks5ProtocolVersion::new_current(),
// 99u64,
// request,
// Some(42u64),
// true,
// )
// .unwrap();
// socks5_request.into_bytes()
// }
//
// #[test]
// fn request_http_request_content_ok() {
// let buffer = create_socks5_request_buffer();
//
// // HTTP request string content is as expected
// assert_eq!(
// [71u8, 69u8, 84u8, 32u8, 47u8, 46u8, 119u8, 101u8],
// buffer[19..27]
// );
// }
//
// /// This test will fail if the framing of the request buffer changes, e.g. when OrderedMessage
// /// changes to have the `index` value as a field, instead of packed with the `data`
// #[test]
// fn request_size_as_expected_ok() {
// let buffer = create_socks5_request_buffer();
// // println!("{:?}", buffer) // uncomment and run `cargo test -- --nocapture` to view
//
// assert_eq!(108, buffer.len()); // version set to SOCKS5
// }
//
// #[test]
// fn request_socks5_headers_ok() {
// let buffer = create_socks5_request_buffer();
//
// assert_eq!(5u8, buffer[0]); // version set to SOCKS5
// assert_eq!(1u8, buffer[1]); // type is SEND
// assert_eq!(99u8, buffer[9]); // ConnectionId is correct
// assert_eq!(1u8, buffer[10]); // local_closed is true
// }
//
// #[test]
// fn request_ordered_message_ok() {
// let buffer = create_socks5_request_buffer();
//
// // OrderedMessage index is correct
// assert_eq!(42u8, buffer[18]);
// }
//
// fn create_socks_response() -> Socks5Response {
// // HTTP response is just a string
// let http_response_string = "HTTP/1.1 200 OK\r\nServer: foo/0.0.1\r\n\r\n";
//
// let data = http_response_string.as_bytes().to_vec();
//
// // wrap in `NetworkData`, then Socks5Response
// Socks5Response::new(
// Socks5ProtocolVersion::new_current(),
// Socks5ResponseContent::NetworkData {
// content: SocketData::new(42, 99u64, false, data),
// },
// )
// }
//
// /// This test will fail is anything in the framing of the socks5_response byte
// /// representation changes
// #[test]
// fn response_byte_size_is_as_expected() {
// let socks5_response = create_socks_response();
// let buf = socks5_response.into_bytes();
//
// assert_eq!(57, buf.len());
// }
//
// #[test]
// fn response_parses() {
// unimplemented!()
// // let socks5_response = create_socks_response();
// // let response = decode_socks_response_as_http_response(socks5_response).unwrap();
// //
// // assert_eq!(42u64, response.seq); // OrderedMessage index as expected
// // assert_eq!(HttpVersion::V1_1, response.http_response.http_version());
// // assert_eq!(200u16, response.http_response.status_code().as_u16());
// // assert_eq!(
// // "foo/0.0.1",
// // response.http_response.header().get_field("Server").unwrap()
// // );
// }
// }
+1
View File
@@ -6,6 +6,7 @@ use crate::helpers::best_effort_small_dec_to_f64;
use crate::mix_node::models::EconomicDynamicsStats;
use nym_contracts_common::truncate_decimal;
use nym_mixnet_contract_common::MixId;
use nym_validator_client::client::NymApiClientExt;
pub(crate) async fn retrieve_mixnode_econ_stats(
client: &ThreadsafeValidatorClient,
+7
View File
@@ -11,6 +11,7 @@ use nym_mixnet_contract_common::{
};
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use std::fmt::{Display, Formatter};
use std::{fmt, time::Duration};
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
@@ -30,6 +31,12 @@ impl RequestError {
}
}
impl Display for RequestError {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
self.message.fmt(f)
}
}
#[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
#[cfg_attr(feature = "generate-ts", derive(ts_rs::TS))]
#[cfg_attr(
+12
View File
@@ -2964,6 +2964,17 @@ dependencies = [
"itoa 1.0.9",
]
[[package]]
name = "http-api-client"
version = "0.1.0"
dependencies = [
"async-trait",
"reqwest",
"serde",
"serde_json",
"thiserror",
]
[[package]]
name = "http-body"
version = "0.4.5"
@@ -4634,6 +4645,7 @@ dependencies = [
"eyre",
"flate2",
"futures",
"http-api-client",
"itertools",
"log",
"nym-api-requests",
@@ -8,6 +8,7 @@ use nym_bin_common::version_checker::is_minor_version_compatible;
use nym_config::defaults::var_names::NYM_API;
use nym_contracts_common::types::Percent;
use nym_topology::gateway;
use nym_validator_client::client::NymApiClientExt;
use nym_validator_client::nym_api::Client as ApiClient;
use std::str::FromStr;
use url::Url;
@@ -0,0 +1,6 @@
// Copyright 2023 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
pub struct Client {
//
}
@@ -7,8 +7,13 @@ use nym_crypto::asymmetric::identity;
use serde::Serialize;
use std::ops::Deref;
#[cfg(feature = "client")]
pub mod client;
pub mod v1;
#[cfg(feature = "client")]
pub use client::Client;
#[derive(Debug, Clone, Serialize)]
#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
#[cfg_attr(feature = "openapi", aliases(SignedHostInformation = SignedData<HostInformation>))]
+12
View File
@@ -2390,6 +2390,17 @@ dependencies = [
"itoa 1.0.9",
]
[[package]]
name = "http-api-client"
version = "0.1.0"
dependencies = [
"async-trait",
"reqwest",
"serde",
"serde_json",
"thiserror",
]
[[package]]
name = "http-body"
version = "0.4.5"
@@ -3434,6 +3445,7 @@ dependencies = [
"eyre",
"flate2",
"futures",
"http-api-client",
"itertools",
"log",
"nym-api-requests",
@@ -14,6 +14,7 @@ use nym_types::currency::DecCoin;
use nym_types::gateway::GatewayBond;
use nym_types::mixnode::{MixNodeCostParams, MixNodeDetails};
use nym_types::transaction::TransactionExecuteResult;
use nym_validator_client::client::NymApiClientExt;
use nym_validator_client::nyxd::contract_traits::{MixnetQueryClient, MixnetSigningClient};
use nym_validator_client::nyxd::Fee;
use serde::{Deserialize, Serialize};
@@ -13,6 +13,7 @@ use nym_types::deprecated::{
use nym_types::mixnode::MixNodeCostParams;
use nym_types::pending_events::PendingEpochEvent;
use nym_types::transaction::TransactionExecuteResult;
use nym_validator_client::client::NymApiClientExt;
use nym_validator_client::nyxd::contract_traits::{
MixnetQueryClient, MixnetSigningClient, NymContractsProvider, PagedMixnetQueryClient,
};
@@ -7,6 +7,7 @@ use crate::state::WalletState;
use nym_mixnet_contract_common::{
reward_params::Performance, Coin, IdentityKeyRef, MixId, Percent,
};
use nym_validator_client::client::NymApiClientExt;
use nym_validator_client::models::{
ComputeRewardEstParam, GatewayCoreStatusResponse, GatewayStatusReportResponse,
InclusionProbabilityResponse, MixnodeCoreStatusResponse, MixnodeStatusResponse,
+2 -1
View File
@@ -14,10 +14,10 @@ rust-version = "1.70"
crate-type = ["cdylib", "rlib"]
[dependencies]
async-trait = { workspace = true }
futures = { workspace = true }
js-sys = { workspace = true }
rand = { version = "0.7.3", features = ["wasm-bindgen"] }
reqwest = { workspace = true }
serde = { workspace = true, features = ["derive"] }
serde-wasm-bindgen = { workspace = true }
tokio = { workspace = true, features = ["sync"] }
@@ -27,6 +27,7 @@ wasm-bindgen-futures = { workspace = true }
thiserror = { workspace = true }
tsify = { workspace = true, features = ["js"] }
http-api-client = { path = "../../common/http-api-client" }
nym-socks5-requests = { path = "../../common/socks5/requests" }
nym-ordered-buffer = { path = "../../common/socks5/ordered-buffer" }
nym-service-providers-common = { path = "../../service-providers/common" }
Binary file not shown.
+14 -98
View File
@@ -4,10 +4,13 @@
// this exists inside mix-fetch rather than being made into repo-wide library since it's a temporary solution
// until the API is finalised and endpoints are moved to nym-api
use reqwest::{IntoUrl, Response, StatusCode};
use async_trait::async_trait;
use http_api_client::{ApiClient, HttpClientError, NO_PARAMS};
use serde::Deserialize;
use thiserror::Error;
use url::Url;
pub use http_api_client::Client;
pub type HarbourMasterApiError = HttpClientError;
mod routes {
pub const API_VERSION: &str = "v1";
@@ -17,79 +20,12 @@ mod routes {
pub const NEW: &str = "new";
}
// most of it is copied from the nym-api client
type PathSegments<'a> = &'a [&'a str];
type Params<'a, K, V> = &'a [(K, V)];
const NO_PARAMS: Params<'_, &'_ str, &'_ str> = &[];
#[derive(Debug, Error)]
pub enum HarbourMasterApiError {
#[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),
}
pub struct Client {
url: Url,
reqwest_client: reqwest::Client,
}
impl Client {
pub fn new<U: IntoUrl>(url: U) -> Result<Self, HarbourMasterApiError> {
let reqwest_client = reqwest::Client::new();
Ok(Self {
url: url.into_url()?,
reqwest_client,
})
}
async fn send_get_request<K, V>(
&self,
path: PathSegments<'_>,
params: Params<'_, K, V>,
) -> Result<Response, HarbourMasterApiError>
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_harbourmaster<T, K, V>(
&self,
path: PathSegments<'_>,
params: Params<'_, K, V>,
) -> Result<T, HarbourMasterApiError>
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(HarbourMasterApiError::NotFound)
} else {
Err(HarbourMasterApiError::GenericRequestFailure(
res.text().await?,
))
}
}
#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
pub trait HarbourMasterApiClientExt: ApiClient {
// since it's a temporary thing don't worry about paging.
pub async fn get_services_new(&self) -> Result<PagedResult<ServiceNew>, HarbourMasterApiError> {
self.query_harbourmaster(
async fn get_services_new(&self) -> Result<PagedResult<ServiceNew>, HarbourMasterApiError> {
self.get_json(
&[routes::API_VERSION, routes::SERVICES, routes::NEW],
NO_PARAMS,
)
@@ -97,29 +33,9 @@ impl Client {
}
}
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);
}
drop(path_segments);
if !params.is_empty() {
url.query_pairs_mut().extend_pairs(params);
}
url
}
#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
impl HarbourMasterApiClientExt for Client {}
// https://gitlab.nymte.ch/nym/shipyard-test-and-earn/-/blob/main/harbour-master/src/http/mod.rs#L13
#[derive(Debug, Deserialize)]
+2 -1
View File
@@ -3,6 +3,7 @@
use crate::error::MixFetchError;
use crate::harbourmaster;
use crate::harbourmaster::HarbourMasterApiClientExt;
use rand::seq::SliceRandom;
use rand::thread_rng;
use std::collections::HashMap;
@@ -23,7 +24,7 @@ pub(crate) async fn get_network_requester(
return Ok(sp);
}
let client = harbourmaster::Client::new(HARBOUR_MASTER)?;
let client = harbourmaster::Client::new_url(HARBOUR_MASTER)?;
let providers = client.get_services_new().await?;
console_log!(
"obtained list of {} service providers on the network",