From e6eb83f350fbe8dca7d14492dca459ea63f63c06 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C4=99drzej=20Stuczy=C5=84ski?= Date: Tue, 3 Oct 2023 10:16:33 +0100 Subject: [PATCH] unified http api client --- Cargo.lock | 30 +- Cargo.toml | 2 +- .../client-libs/validator-client/Cargo.toml | 1 + .../validator-client/src/client.rs | 5 +- .../validator-client/src/nym_api/error.rs | 23 +- .../validator-client/src/nym_api/mod.rs | 331 ++++-------------- .../mixnet/query/query_all_gateways.rs | 6 +- .../mixnet/query/query_all_mixnodes.rs | 6 +- .../validator/mixnet/query/query_all_names.rs | 8 +- .../query/query_all_service_providers.rs | 8 +- common/http-api-client/Cargo.toml | 18 + common/http-api-client/src/lib.rs | 315 +++++++++++++++++ common/http-requests/Cargo.toml | 19 - common/http-requests/src/error.rs | 23 -- common/http-requests/src/lib.rs | 63 ---- common/http-requests/src/socks.rs | 214 ----------- explorer-api/src/mix_node/econ_stats.rs | 1 + nym-api/nym-api-requests/src/models.rs | 7 + nym-connect/desktop/Cargo.lock | 12 + .../src/operations/directory/gateways.rs | 1 + nym-node/nym-node-requests/src/api/client.rs | 6 + nym-node/nym-node-requests/src/api/mod.rs | 5 + nym-wallet/Cargo.lock | 12 + .../src-tauri/src/operations/mixnet/bond.rs | 1 + .../src/operations/mixnet/delegate.rs | 1 + .../src/operations/nym_api/status.rs | 1 + wasm/mix-fetch/Cargo.toml | 3 +- wasm/mix-fetch/go-mix-conn/build/go_conn.wasm | Bin 8527995 -> 8527995 bytes wasm/mix-fetch/src/harbourmaster.rs | 112 +----- wasm/mix-fetch/src/helpers.rs | 3 +- 30 files changed, 499 insertions(+), 738 deletions(-) create mode 100644 common/http-api-client/Cargo.toml create mode 100644 common/http-api-client/src/lib.rs delete mode 100644 common/http-requests/Cargo.toml delete mode 100644 common/http-requests/src/error.rs delete mode 100644 common/http-requests/src/lib.rs delete mode 100644 common/http-requests/src/socks.rs create mode 100644 nym-node/nym-node-requests/src/api/client.rs diff --git a/Cargo.lock b/Cargo.lock index 20367b6860..98aae9c4ed 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -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", diff --git a/Cargo.toml b/Cargo.toml index d9fb1194be..b4499e56a3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -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", diff --git a/common/client-libs/validator-client/Cargo.toml b/common/client-libs/validator-client/Cargo.toml index 4b3533ef7e..e16345f446 100644 --- a/common/client-libs/validator-client/Cargo.toml +++ b/common/client-libs/validator-client/Cargo.toml @@ -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"] } diff --git a/common/client-libs/validator-client/src/client.rs b/common/client-libs/validator-client/src/client.rs index c5ae83a6bf..b6be937bcc 100644 --- a/common/client-libs/validator-client/src/client.rs +++ b/common/client-libs/validator-client/src/client.rs @@ -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 Client { } 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, 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( diff --git a/common/client-libs/validator-client/src/nym_api/error.rs b/common/client-libs/validator-client/src/nym_api/error.rs index 34053daeaf..554a0826d4 100644 --- a/common/client-libs/validator-client/src/nym_api/error.rs +++ b/common/client-libs/validator-client/src/nym_api/error.rs @@ -1,20 +1,7 @@ +// Copyright 2022-2023 - Nym Technologies SA +// 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; diff --git a/common/client-libs/validator-client/src/nym_api/mod.rs b/common/client-libs/validator-client/src/nym_api/mod.rs index a7d2f1c1c1..6b0ce6b731 100644 --- a/common/client-libs/validator-client/src/nym_api/mod.rs +++ b/common/client-libs/validator-client/src/nym_api/mod.rs @@ -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( - &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_nym_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 if res.status() == StatusCode::NOT_FOUND { - Err(NymAPIError::NotFound) - } else { - Err(NymAPIError::GenericRequestFailure(res.text().await?)) - } - } - - // This works for endpoints returning Result, ErrorResponse> - async fn query_nym_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(NymAPIError::ApiRequestFailure { - status: status.as_u16(), - error: request_error, - }) - } - } - - async fn post_nym_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(NymAPIError::GenericRequestFailure(response.text().await?)) - } - } - - pub async fn get_mixnodes(&self) -> Result, 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, NymAPIError> { + self.get_json(&[routes::API_VERSION, routes::MIXNODES], NO_PARAMS) .await } - pub async fn get_mixnodes_detailed(&self) -> Result, NymAPIError> { - self.query_nym_api( + async fn get_mixnodes_detailed(&self) -> Result, NymAPIError> { + self.get_json( &[ routes::API_VERSION, routes::STATUS, @@ -148,8 +46,8 @@ impl Client { .await } - pub async fn get_gateways_detailed(&self) -> Result, NymAPIError> { - self.query_nym_api( + async fn get_gateways_detailed(&self) -> Result, 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, 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, NymAPIError> { - self.query_nym_api(&[routes::API_VERSION, routes::GATEWAYS], NO_PARAMS) + async fn get_gateways(&self) -> Result, NymAPIError> { + self.get_json(&[routes::API_VERSION, routes::GATEWAYS], NO_PARAMS) .await } - pub async fn get_active_mixnodes(&self) -> Result, NymAPIError> { - self.query_nym_api( + async fn get_active_mixnodes(&self) -> Result, NymAPIError> { + self.get_json( &[routes::API_VERSION, routes::MIXNODES, routes::ACTIVE], NO_PARAMS, ) .await } - pub async fn get_active_mixnodes_detailed( - &self, - ) -> Result, NymAPIError> { - self.query_nym_api( + async fn get_active_mixnodes_detailed(&self) -> Result, NymAPIError> { + self.get_json( &[ routes::API_VERSION, routes::STATUS, @@ -205,19 +101,19 @@ impl Client { .await } - pub async fn get_rewarded_mixnodes(&self) -> Result, NymAPIError> { - self.query_nym_api( + async fn get_rewarded_mixnodes(&self) -> Result, 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 { - 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 { - 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 { - 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 { - 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, 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, ) -> Result { 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, ) -> Result { 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 { - 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 { - 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 { - 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 { - 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 { - 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 { - self.query_nym_api_fallible( + async fn get_mixnode_avg_uptime(&self, mix_id: MixId) -> Result { + 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 { - 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 { - 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 { + async fn get_service_providers(&self) -> Result { 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, NymAPIError> { - pub async fn get_registered_names(&self) -> Result { + //async fn get_registered_names(&self) -> Result, NymAPIError> { + async fn get_registered_names(&self) -> Result { 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, 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() - ); - } -} +#[cfg_attr(target_arch = "wasm32", async_trait(?Send))] +#[cfg_attr(not(target_arch = "wasm32"), async_trait)] +impl NymApiClientExt for Client {} diff --git a/common/commands/src/validator/mixnet/query/query_all_gateways.rs b/common/commands/src/validator/mixnet/query/query_all_gateways.rs index 17f1d4d48d..16f0e44285 100644 --- a/common/commands/src/validator/mixnet/query/query_all_gateways.rs +++ b/common/commands/src/validator/mixnet/query/query_all_gateways.rs @@ -1,11 +1,11 @@ // Copyright 2022 - Nym Technologies SA // 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 { diff --git a/common/commands/src/validator/mixnet/query/query_all_mixnodes.rs b/common/commands/src/validator/mixnet/query/query_all_mixnodes.rs index 3b23f2092e..9bb20cc8d4 100644 --- a/common/commands/src/validator/mixnet/query/query_all_mixnodes.rs +++ b/common/commands/src/validator/mixnet/query/query_all_mixnodes.rs @@ -1,11 +1,11 @@ // Copyright 2022 - Nym Technologies SA // 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 { diff --git a/common/commands/src/validator/mixnet/query/query_all_names.rs b/common/commands/src/validator/mixnet/query/query_all_names.rs index 5a17cad30d..fa75752c6c 100644 --- a/common/commands/src/validator/mixnet/query/query_all_names.rs +++ b/common/commands/src/validator/mixnet/query/query_all_names.rs @@ -1,12 +1,12 @@ // Copyright 2023 - Nym Technologies SA // 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 { diff --git a/common/commands/src/validator/mixnet/query/query_all_service_providers.rs b/common/commands/src/validator/mixnet/query/query_all_service_providers.rs index e13f483f93..74ef07a637 100644 --- a/common/commands/src/validator/mixnet/query/query_all_service_providers.rs +++ b/common/commands/src/validator/mixnet/query/query_all_service_providers.rs @@ -1,12 +1,12 @@ // Copyright 2023 - Nym Technologies SA // 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 { diff --git a/common/http-api-client/Cargo.toml b/common/http-api-client/Cargo.toml new file mode 100644 index 0000000000..ce30d3fa42 --- /dev/null +++ b/common/http-api-client/Cargo.toml @@ -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 } diff --git a/common/http-api-client/src/lib.rs b/common/http-api-client/src/lib.rs new file mode 100644 index 0000000000..969794a675 --- /dev/null +++ b/common/http-api-client/src/lib.rs @@ -0,0 +1,315 @@ +// Copyright 2023 - Nym Technologies SA +// 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 { + #[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(url: U) -> Result> + 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( + &self, + path: PathSegments<'_>, + params: Params<'_, K, V>, + ) -> Result> + where + K: AsRef, + V: AsRef, + E: Display, + { + let url = sanitize_url(&self.base_url, path, params); + Ok(self.reqwest_client.get(url).send().await?) + } + + async fn send_post_request( + &self, + path: PathSegments<'_>, + params: Params<'_, K, V>, + json_body: &B, + ) -> Result> + where + B: Serialize + ?Sized, + K: AsRef, + V: AsRef, + 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( + &self, + path: PathSegments<'_>, + params: Params<'_, K, V>, + ) -> Result> + where + for<'a> T: Deserialize<'a>, + K: AsRef, + V: AsRef, + E: Display + DeserializeOwned, + { + let res = self.send_get_request(path, params).await?; + parse_response(res).await + } + + pub async fn post_json_endpoint( + &self, + path: PathSegments<'_>, + params: Params<'_, K, V>, + json_body: &B, + ) -> Result> + where + B: Serialize + ?Sized, + for<'a> T: Deserialize<'a>, + K: AsRef, + V: AsRef, + 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( + &self, + path: PathSegments<'_>, + params: Params<'_, K, V>, + ) -> Result> + where + for<'a> T: Deserialize<'a>, + K: AsRef + Sync, + V: AsRef + Sync, + E: Display + DeserializeOwned; + + async fn post_json( + &self, + path: PathSegments<'_>, + params: Params<'_, K, V>, + json_body: &B, + ) -> Result> + where + B: Serialize + ?Sized + Sync, + for<'a> T: Deserialize<'a>, + K: AsRef + Sync, + V: AsRef + 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( + &self, + path: PathSegments<'_>, + params: Params<'_, K, V>, + ) -> Result> + where + for<'a> T: Deserialize<'a>, + K: AsRef + Sync, + V: AsRef + Sync, + E: Display + DeserializeOwned, + { + self.get_json_endpoint(path, params).await + } + + async fn post_json( + &self, + path: PathSegments<'_>, + params: Params<'_, K, V>, + json_body: &B, + ) -> Result> + where + B: Serialize + ?Sized + Sync, + for<'a> T: Deserialize<'a>, + K: AsRef + Sync, + V: AsRef + 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, 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 +} + +async fn parse_response(res: Response) -> Result> +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() + ); + } +} diff --git a/common/http-requests/Cargo.toml b/common/http-requests/Cargo.toml deleted file mode 100644 index 43cd636e9d..0000000000 --- a/common/http-requests/Cargo.toml +++ /dev/null @@ -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" diff --git a/common/http-requests/src/error.rs b/common/http-requests/src/error.rs deleted file mode 100644 index 180a4fd44a..0000000000 --- a/common/http-requests/src/error.rs +++ /dev/null @@ -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, - }, -} diff --git a/common/http-requests/src/lib.rs b/common/http-requests/src/lib.rs deleted file mode 100644 index 1905396279..0000000000 --- a/common/http-requests/src/lib.rs +++ /dev/null @@ -1,63 +0,0 @@ -// Copyright 2021 - Nym Technologies SA -// 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>, -) -> Result { - // 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> { - 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); - } -} diff --git a/common/http-requests/src/socks.rs b/common/http-requests/src/socks.rs deleted file mode 100644 index 952ff1fc52..0000000000 --- a/common/http-requests/src/socks.rs +++ /dev/null @@ -1,214 +0,0 @@ -// Copyright 2023 - Nym Technologies SA -// 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>, - seq: Option, - local_closed: bool, -) -> Result { - // 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>, - // #[deprecated] - // pub seq: u64, -} - -impl MixHttpResponse { - pub fn try_from_bytes(b: &[u8]) -> Result { - if b.is_empty() { - Err(error::MixHttpRequestError::EmptySocks5Response) - } else { - let mut decoder = ResponseDecoder::>::default(); - let http_response = decoder.decode_from_bytes(b)?; - - Ok(MixHttpResponse { http_response }) - } - } -} - -// impl TryFrom for MixHttpResponse { -// type Error = error::MixHttpRequestError; -// -// fn try_from(value: Socks5Response) -> Result { -// if let Socks5ResponseContent::NetworkData { content } = value.content { -// content.try_into() -// } else { -// Err(error::MixHttpRequestError::InvalidSocks5Response) -// } -// } -// } -// -// impl TryFrom for MixHttpResponse { -// type Error = error::MixHttpRequestError; -// -// fn try_from(value: SocketData) -> Result { -// if value.data.is_empty() { -// Err(error::MixHttpRequestError::EmptySocks5Response) -// } else { -// let mut decoder = ResponseDecoder::>::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 { -// 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> { -// 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 { -// 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() -// // ); -// } -// } diff --git a/explorer-api/src/mix_node/econ_stats.rs b/explorer-api/src/mix_node/econ_stats.rs index 66a4676376..0b1c901a40 100644 --- a/explorer-api/src/mix_node/econ_stats.rs +++ b/explorer-api/src/mix_node/econ_stats.rs @@ -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, diff --git a/nym-api/nym-api-requests/src/models.rs b/nym-api/nym-api-requests/src/models.rs index 319d49edd1..86866f7da6 100644 --- a/nym-api/nym-api-requests/src/models.rs +++ b/nym-api/nym-api-requests/src/models.rs @@ -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( diff --git a/nym-connect/desktop/Cargo.lock b/nym-connect/desktop/Cargo.lock index 9f5c999744..b0f36b0cfe 100644 --- a/nym-connect/desktop/Cargo.lock +++ b/nym-connect/desktop/Cargo.lock @@ -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", diff --git a/nym-connect/desktop/src-tauri/src/operations/directory/gateways.rs b/nym-connect/desktop/src-tauri/src/operations/directory/gateways.rs index 8c00fa21b5..e6c4d2403b 100644 --- a/nym-connect/desktop/src-tauri/src/operations/directory/gateways.rs +++ b/nym-connect/desktop/src-tauri/src/operations/directory/gateways.rs @@ -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; diff --git a/nym-node/nym-node-requests/src/api/client.rs b/nym-node/nym-node-requests/src/api/client.rs new file mode 100644 index 0000000000..1fe2fea684 --- /dev/null +++ b/nym-node/nym-node-requests/src/api/client.rs @@ -0,0 +1,6 @@ +// Copyright 2023 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +pub struct Client { + // +} diff --git a/nym-node/nym-node-requests/src/api/mod.rs b/nym-node/nym-node-requests/src/api/mod.rs index 90d3c85a13..5c3efbc73d 100644 --- a/nym-node/nym-node-requests/src/api/mod.rs +++ b/nym-node/nym-node-requests/src/api/mod.rs @@ -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))] diff --git a/nym-wallet/Cargo.lock b/nym-wallet/Cargo.lock index 4e45b25c7d..da568b7427 100644 --- a/nym-wallet/Cargo.lock +++ b/nym-wallet/Cargo.lock @@ -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", diff --git a/nym-wallet/src-tauri/src/operations/mixnet/bond.rs b/nym-wallet/src-tauri/src/operations/mixnet/bond.rs index 896b62175a..1cf3a7bd69 100644 --- a/nym-wallet/src-tauri/src/operations/mixnet/bond.rs +++ b/nym-wallet/src-tauri/src/operations/mixnet/bond.rs @@ -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}; diff --git a/nym-wallet/src-tauri/src/operations/mixnet/delegate.rs b/nym-wallet/src-tauri/src/operations/mixnet/delegate.rs index 18bd72551f..1a47f7bee9 100644 --- a/nym-wallet/src-tauri/src/operations/mixnet/delegate.rs +++ b/nym-wallet/src-tauri/src/operations/mixnet/delegate.rs @@ -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, }; diff --git a/nym-wallet/src-tauri/src/operations/nym_api/status.rs b/nym-wallet/src-tauri/src/operations/nym_api/status.rs index 8ca13d5776..096615b3b5 100644 --- a/nym-wallet/src-tauri/src/operations/nym_api/status.rs +++ b/nym-wallet/src-tauri/src/operations/nym_api/status.rs @@ -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, diff --git a/wasm/mix-fetch/Cargo.toml b/wasm/mix-fetch/Cargo.toml index da79e729bb..5f4b96c9ff 100644 --- a/wasm/mix-fetch/Cargo.toml +++ b/wasm/mix-fetch/Cargo.toml @@ -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" } diff --git a/wasm/mix-fetch/go-mix-conn/build/go_conn.wasm b/wasm/mix-fetch/go-mix-conn/build/go_conn.wasm index 0066e6d4fdaa888c7eef83431a7037d063ba6a1b..a0bcade9b5a7c9a020f94c76dbfde034c944fd64 100755 GIT binary patch delta 599 zcmWN=*HRM!7>40dv4Fkzf*lo--Ay(d6h%=`P*6f>mYA|>CIL)B^R;mBjUGDVojBuo z>g8|`ocKR8&(^;m9b5KiPimx?U2F2L+lj0 z#BQ-ibc!yqSL_q}#Q||p91@4c5ph%;6UW5~aZ;QTr^Oj@R-6;(#RYLuToT>lvbZ9y zifiJ!xFK$e9&t(HLVn5q2_N)E&ceB3n>-T@*yZNU8 delta 599 zcmWN=*HRM!7>40dv4Fkzf*lpIyQzz!2r4BA0-=Z{F4;|pNeCT+uZ4qe^w1ga#2Lp^ zFNb^J#Q&LjcK-cn+Oa>|#(S#y_~u4_J=v%X4wn}b;mG7btWsZ@8l7KGRcd}TF}4|& zriPNSu|}#dJ~$Hj)1^cyKa-#H>Y3O=u96+N@p zpV%)Bh-T3u4vItKus9-)ieuuqI3Z4oQ{uEZBhHF*;=H&ZE{aRyvbZ9yiff`(To*UQ zO>s-y7I(y5(I)PR`{IFU7abxd9N`L2#6?0RMM|VaM);yrJQR;amv}6mh;H#z^oU;Z zOgtAaM4#vv17c8Q#gKR@hQ)|@B}T=V7#BG)AtuF?m=>?ajCdp7ig)6@$cqnu2wH2| zzz^IgDin)FC*!1@bP%NdL>MItVd#3vu#kxdg_Jw%x6H1Vt6}q6VYy?aT#LFq$BVZ+ ze!J)9T&FXU>~y{F+qJ;X+JX&jWQ(?B=WN-2v=v*mH9K$XcEK*%CA(}_?5bU}>vqF7 W?56!>v7hZ1`_+E?yItG*_4_|WRr = &'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(url: U) -> Result { - let reqwest_client = reqwest::Client::new(); - Ok(Self { - url: url.into_url()?, - reqwest_client, - }) - } - - 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_harbourmaster( - &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 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, HarbourMasterApiError> { - self.query_harbourmaster( + async fn get_services_new(&self) -> Result, HarbourMasterApiError> { + self.get_json( &[routes::API_VERSION, routes::SERVICES, routes::NEW], NO_PARAMS, ) @@ -97,29 +33,9 @@ impl Client { } } -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); - } - 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)] diff --git a/wasm/mix-fetch/src/helpers.rs b/wasm/mix-fetch/src/helpers.rs index 0ba09bacea..92144807ec 100644 --- a/wasm/mix-fetch/src/helpers.rs +++ b/wasm/mix-fetch/src/helpers.rs @@ -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",