diff --git a/common/http-api-client/src/lib.rs b/common/http-api-client/src/lib.rs
index f1068bb562..50930463da 100644
--- a/common/http-api-client/src/lib.rs
+++ b/common/http-api-client/src/lib.rs
@@ -138,7 +138,12 @@
pub use reqwest::{IntoUrl, StatusCode};
+use crate::path::RequestPath;
use async_trait::async_trait;
+use bytes::Bytes;
+use http::header::CONTENT_TYPE;
+use http::HeaderMap;
+use mime::Mime;
use reqwest::header::HeaderValue;
use reqwest::{RequestBuilder, Response};
use serde::de::DeserializeOwned;
@@ -149,10 +154,6 @@ use thiserror::Error;
use tracing::{debug, instrument, warn};
use url::Url;
-use bytes::Bytes;
-use http::header::CONTENT_TYPE;
-use http::HeaderMap;
-use mime::Mime;
#[cfg(not(target_arch = "wasm32"))]
use std::net::SocketAddr;
#[cfg(not(target_arch = "wasm32"))]
@@ -163,6 +164,8 @@ pub use user_agent::UserAgent;
#[cfg(not(target_arch = "wasm32"))]
mod dns;
+mod path;
+
#[cfg(not(target_arch = "wasm32"))]
pub use dns::{HickoryDnsError, HickoryDnsResolver};
@@ -454,14 +457,15 @@ impl Client {
#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
pub trait ApiClientCore {
/// Create an HTTP request using the host configured in this client.
- fn create_request(
+ fn create_request
(
&self,
method: reqwest::Method,
- path: PathSegments<'_>,
+ path: P,
params: Params<'_, K, V>,
json_body: Option<&B>,
) -> RequestBuilder
where
+ P: RequestPath,
B: Serialize + ?Sized,
K: AsRef,
V: AsRef;
@@ -512,7 +516,7 @@ pub trait ApiClientCore {
};
let params: Vec<(String, String)> = standin_url.query_pairs().into_owned().collect();
- self.create_request(method, &path, ¶ms, json_body)
+ self.create_request(method, path.as_slice(), ¶ms, json_body)
}
/// Send a created HTTP request.
@@ -525,14 +529,15 @@ pub trait ApiClientCore {
E: Display;
/// Create and send a created HTTP request.
- async fn send_request(
+ async fn send_request(
&self,
method: reqwest::Method,
- path: PathSegments<'_>,
+ path: P,
params: Params<'_, K, V>,
json_body: Option<&B>,
) -> Result>
where
+ P: RequestPath + Send + Sync,
B: Serialize + ?Sized + Sync,
K: AsRef + Sync,
V: AsRef + Sync,
@@ -547,14 +552,15 @@ pub trait ApiClientCore {
#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
impl ApiClientCore for Client {
#[instrument(level = "debug", skip_all, fields(path=?path))]
- fn create_request(
+ fn create_request(
&self,
method: reqwest::Method,
- path: PathSegments<'_>,
+ path: P,
params: Params<'_, K, V>,
json_body: Option<&B>,
) -> RequestBuilder
where
+ P: RequestPath,
B: Serialize + ?Sized,
K: AsRef,
V: AsRef,
@@ -597,12 +603,9 @@ impl ApiClientCore for Client {
#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
pub trait ApiClient: ApiClientCore {
/// Create an HTTP GET Request with the provided path and parameters
- fn create_get_request(
- &self,
- path: PathSegments<'_>,
- params: Params<'_, K, V>,
- ) -> RequestBuilder
+ fn create_get_request(&self, path: P, params: Params<'_, K, V>) -> RequestBuilder
where
+ P: RequestPath,
K: AsRef,
V: AsRef,
{
@@ -610,13 +613,14 @@ pub trait ApiClient: ApiClientCore {
}
/// Create an HTTP POST Request with the provided path, parameters, and json body
- fn create_post_request(
+ fn create_post_request(
&self,
- path: PathSegments<'_>,
+ path: P,
params: Params<'_, K, V>,
json_body: &B,
) -> RequestBuilder
where
+ P: RequestPath,
B: Serialize + ?Sized,
K: AsRef,
V: AsRef,
@@ -625,12 +629,9 @@ pub trait ApiClient: ApiClientCore {
}
/// Create an HTTP DELETE Request with the provided path and parameters
- fn create_delete_request(
- &self,
- path: PathSegments<'_>,
- params: Params<'_, K, V>,
- ) -> RequestBuilder
+ fn create_delete_request(&self, path: P, params: Params<'_, K, V>) -> RequestBuilder
where
+ P: RequestPath,
K: AsRef,
V: AsRef,
{
@@ -638,13 +639,14 @@ pub trait ApiClient: ApiClientCore {
}
/// Create an HTTP PATCH Request with the provided path, parameters, and json body
- fn create_patch_request(
+ fn create_patch_request(
&self,
- path: PathSegments<'_>,
+ path: P,
params: Params<'_, K, V>,
json_body: &B,
) -> RequestBuilder
where
+ P: RequestPath,
B: Serialize + ?Sized,
K: AsRef,
V: AsRef,
@@ -654,12 +656,13 @@ pub trait ApiClient: ApiClientCore {
/// Create and send an HTTP GET Request with the provided path and parameters
#[instrument(level = "debug", skip_all, fields(path=?path))]
- async fn send_get_request(
+ async fn send_get_request(
&self,
- path: PathSegments<'_>,
+ path: P,
params: Params<'_, K, V>,
) -> Result>
where
+ P: RequestPath + Send + Sync,
K: AsRef + Sync,
V: AsRef + Sync,
E: Display,
@@ -669,13 +672,14 @@ pub trait ApiClient: ApiClientCore {
}
/// Create and send an HTTP POST Request with the provided path, parameters, and json data
- async fn send_post_request(
+ async fn send_post_request(
&self,
- path: PathSegments<'_>,
+ path: P,
params: Params<'_, K, V>,
json_body: &B,
) -> Result>
where
+ P: RequestPath + Send + Sync,
B: Serialize + ?Sized + Sync,
K: AsRef + Sync,
V: AsRef + Sync,
@@ -686,12 +690,13 @@ pub trait ApiClient: ApiClientCore {
}
/// Create and send an HTTP DELETE Request with the provided path and parameters
- async fn send_delete_request(
+ async fn send_delete_request(
&self,
- path: PathSegments<'_>,
+ path: P,
params: Params<'_, K, V>,
) -> Result>
where
+ P: RequestPath + Send + Sync,
K: AsRef + Sync,
V: AsRef + Sync,
E: Display,
@@ -701,13 +706,14 @@ pub trait ApiClient: ApiClientCore {
}
/// Create and send an HTTP PATCH Request with the provided path, parameters, and json data
- async fn send_patch_request(
+ async fn send_patch_request(
&self,
- path: PathSegments<'_>,
+ path: P,
params: Params<'_, K, V>,
json_body: &B,
) -> Result>
where
+ P: RequestPath + Send + Sync,
B: Serialize + ?Sized + Sync,
K: AsRef + Sync,
V: AsRef + Sync,
@@ -722,12 +728,13 @@ pub trait ApiClient: ApiClientCore {
/// into the provided type `T`.
#[instrument(level = "debug", skip_all, fields(path=?path))]
// TODO: deprecate in favour of get_response that works based on mime type in the response
- async fn get_json(
+ async fn get_json(
&self,
- path: PathSegments<'_>,
+ path: P,
params: Params<'_, K, V>,
) -> Result>
where
+ P: RequestPath + Send + Sync,
for<'a> T: Deserialize<'a>,
K: AsRef + Sync,
V: AsRef + Sync,
@@ -739,12 +746,13 @@ pub trait ApiClient: ApiClientCore {
/// 'get' data from the segment-defined path, e.g. `["api", "v1", "mixnodes"]`, with tuple
/// defined key-value parameters, e.g. `[("since", "12345")]`. Attempt to parse the response
/// into the provided type `T` based on the content type header
- async fn get_response(
+ async fn get_response(
&self,
- path: PathSegments<'_>,
+ path: P,
params: Params<'_, K, V>,
) -> Result>
where
+ P: RequestPath + Send + Sync,
for<'a> T: Deserialize<'a>,
K: AsRef + Sync,
V: AsRef + Sync,
@@ -759,13 +767,14 @@ pub trait ApiClient: ApiClientCore {
/// 'post' json data to the segment-defined path, e.g. `["api", "v1", "mixnodes"]`, with tuple
/// defined key-value parameters, e.g. `[("since", "12345")]`. Attempt to parse the response
/// into the provided type `T`.
- async fn post_json(
+ async fn post_json(
&self,
- path: PathSegments<'_>,
+ path: P,
params: Params<'_, K, V>,
json_body: &B,
) -> Result>
where
+ P: RequestPath + Send + Sync,
B: Serialize + ?Sized + Sync,
for<'a> T: Deserialize<'a>,
K: AsRef + Sync,
@@ -781,12 +790,13 @@ pub trait ApiClient: ApiClientCore {
/// 'delete' json data from the segment-defined path, e.g. `["api", "v1", "mixnodes"]`, with
/// tuple defined key-value parameters, e.g. `[("since", "12345")]`. Attempt to parse the
/// response into the provided type `T`.
- async fn delete_json(
+ async fn delete_json(
&self,
- path: PathSegments<'_>,
+ path: P,
params: Params<'_, K, V>,
) -> Result>
where
+ P: RequestPath + Send + Sync,
for<'a> T: Deserialize<'a>,
K: AsRef + Sync,
V: AsRef + Sync,
@@ -801,13 +811,14 @@ pub trait ApiClient: ApiClientCore {
/// 'patch' json data at the segment-defined path, e.g. `["api", "v1", "mixnodes"]`, with tuple
/// defined key-value parameters, e.g. `[("since", "12345")]`. Attempt to parse the response
/// into the provided type `T`.
- async fn patch_json(
+ async fn patch_json(
&self,
- path: PathSegments<'_>,
+ path: P,
params: Params<'_, K, V>,
json_body: &B,
) -> Result>
where
+ P: RequestPath + Send + Sync,
B: Serialize + ?Sized + Sync,
for<'a> T: Deserialize<'a>,
K: AsRef + Sync,
@@ -890,7 +901,7 @@ impl ApiClient for C where C: ApiClientCore + Sync {}
/// utility function that should solve the double slash problem in API urls forever.
fn sanitize_url, V: AsRef>(
base: &Url,
- segments: PathSegments<'_>,
+ request_path: impl RequestPath,
params: Params<'_, K, V>,
) -> Url {
let mut url = base.clone();
@@ -900,10 +911,7 @@ fn sanitize_url, V: AsRef>(
path_segments.pop_if_empty();
- for segment in segments {
- let segment = segment.strip_prefix('/').unwrap_or(segment);
- let segment = segment.strip_suffix('/').unwrap_or(segment);
-
+ for segment in request_path.to_sanitized_segments() {
path_segments.push(segment);
}
@@ -1048,6 +1056,18 @@ mod tests {
fn sanitizing_urls() {
let base_url: Url = "http://foomp.com".parse().unwrap();
+ // works with a full string
+ assert_eq!(
+ "http://foomp.com/foo/bar",
+ sanitize_url(&base_url, "/foo//bar/", NO_PARAMS).as_str()
+ );
+
+ // (and leading slash doesn't matter)
+ assert_eq!(
+ "http://foomp.com/foo/bar",
+ sanitize_url(&base_url, "foo//bar/", NO_PARAMS).as_str()
+ );
+
// works with 1 segment
assert_eq!(
"http://foomp.com/foo",
diff --git a/common/http-api-client/src/path.rs b/common/http-api-client/src/path.rs
new file mode 100644
index 0000000000..b7aeea5e47
--- /dev/null
+++ b/common/http-api-client/src/path.rs
@@ -0,0 +1,59 @@
+// Copyright 2025 - Nym Technologies SA
+// SPDX-License-Identifier: Apache-2.0
+
+use std::fmt::Debug;
+
+/// Collection of URL Path Segments
+pub type PathSegments<'a> = &'a [&'a str];
+
+fn sanitize_fragment(segment: &str) -> &str {
+ segment.trim_matches(|c: char| c.is_whitespace() || c == '/')
+}
+
+pub trait RequestPath: Debug {
+ fn to_sanitized_segments(&self) -> Vec<&str>;
+}
+
+macro_rules! impl_stringified_sanitized_segments {
+ ($frag_iter:expr) => {{
+ let mut path_segments = Vec::new();
+
+ for segment in $frag_iter {
+ if !segment.is_empty() {
+ path_segments.push(sanitize_fragment(segment));
+ }
+ }
+
+ path_segments
+ }};
+}
+
+impl RequestPath for PathSegments<'_> {
+ fn to_sanitized_segments(&self) -> Vec<&str> {
+ impl_stringified_sanitized_segments!(self.iter())
+ }
+}
+
+impl RequestPath for &[&str; N] {
+ fn to_sanitized_segments(&self) -> Vec<&str> {
+ impl_stringified_sanitized_segments!(self.iter())
+ }
+}
+
+impl RequestPath for &str {
+ fn to_sanitized_segments(&self) -> Vec<&str> {
+ impl_stringified_sanitized_segments!(self.split('/'))
+ }
+}
+
+impl RequestPath for String {
+ fn to_sanitized_segments(&self) -> Vec<&str> {
+ impl_stringified_sanitized_segments!(self.split('/'))
+ }
+}
+
+impl RequestPath for &String {
+ fn to_sanitized_segments(&self) -> Vec<&str> {
+ impl_stringified_sanitized_segments!(self.split('/'))
+ }
+}
diff --git a/nym-wallet/Cargo.lock b/nym-wallet/Cargo.lock
index 211d475f05..0cdfe77705 100644
--- a/nym-wallet/Cargo.lock
+++ b/nym-wallet/Cargo.lock
@@ -415,11 +415,14 @@ version = "0.4.22"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "59a194f9d963d8099596278594b3107448656ba73831c9d8c783e613ce86da64"
dependencies = [
+ "brotli",
"flate2",
"futures-core",
"memchr",
"pin-project-lite",
"tokio",
+ "zstd",
+ "zstd-safe",
]
[[package]]
@@ -940,6 +943,8 @@ version = "1.2.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1fcb57c740ae1daf453ae85f16e37396f672b039e00d9d866e07ddb24e328e3a"
dependencies = [
+ "jobserver",
+ "libc",
"shlex",
]
@@ -3529,6 +3534,16 @@ version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8eaf4bc02d17cbdd7ff4c7438cafcdf7fb9a4613313ad11b4f8fefe7d3fa0130"
+[[package]]
+name = "jobserver"
+version = "0.1.33"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "38f262f097c174adebe41eb73d66ae9c06b2844fb0da69969647bbddd9b0538a"
+dependencies = [
+ "getrandom 0.3.2",
+ "libc",
+]
+
[[package]]
name = "jpeg-decoder"
version = "0.3.1"
@@ -9270,6 +9285,34 @@ dependencies = [
"memchr",
]
+[[package]]
+name = "zstd"
+version = "0.13.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e91ee311a569c327171651566e07972200e76fcfe2242a4fa446149a3881c08a"
+dependencies = [
+ "zstd-safe",
+]
+
+[[package]]
+name = "zstd-safe"
+version = "7.2.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8f49c4d5f0abb602a93fb8736af2a4f4dd9512e36f7f570d66e65ff867ed3b9d"
+dependencies = [
+ "zstd-sys",
+]
+
+[[package]]
+name = "zstd-sys"
+version = "2.0.15+zstd.1.5.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "eb81183ddd97d0c74cedf1d50d85c8d08c1b8b68ee863bdee9e706eedba1a237"
+dependencies = [
+ "cc",
+ "pkg-config",
+]
+
[[package]]
name = "zvariant"
version = "5.4.0"