//A copy of http-client from logos-blockchain //ToDo: replace with dependency use std::{fmt::Debug, sync::Arc}; use reqwest::{Client, ClientBuilder, RequestBuilder, StatusCode, Url}; use serde::{Serialize, de::DeserializeOwned}; use futures::{Stream, StreamExt as _}; pub mod paths; pub mod structs; use nomos_core::{mantle::SignedMantleTx}; use crate::{paths::{CRYPTARCHIA_INFO, CRYPTARCHIA_LIB_STREAM, STORAGE_BLOCK}, structs::{Block, BlockInfo, info::CryptarchiaInfo}}; #[derive(thiserror::Error, Debug)] pub enum Error { #[error("Internal server error: {0}")] Server(String), #[error("Failed to execute request: {0}")] Client(String), #[error(transparent)] Request(#[from] reqwest::Error), #[error(transparent)] Url(#[from] url::ParseError), } #[derive(Clone)] pub struct BasicAuthCredentials { username: String, password: Option, } impl BasicAuthCredentials { #[must_use] pub const fn new(username: String, password: Option) -> Self { Self { username, password } } } #[derive(Clone)] pub struct CommonHttpClient { client: Arc, basic_auth: Option, } impl CommonHttpClient { #[must_use] pub fn new(basic_auth: Option) -> Self { let client = ClientBuilder::new() .build() .expect("Client from default settings should be able to build"); Self { client: Arc::new(client), basic_auth, } } #[must_use] pub fn new_with_client(client: Client, basic_auth: Option) -> Self { Self { client: Arc::new(client), basic_auth, } } pub async fn post(&self, request_url: Url, request_body: &Req) -> Result where Req: Serialize + ?Sized + Send + Sync, Res: DeserializeOwned + Send + Sync, { let request = self.client.post(request_url).json(request_body); self.execute_request::(request).await } pub async fn get( &self, request_url: Url, request_body: Option<&Req>, ) -> Result where Req: Serialize + ?Sized + Send + Sync, Res: DeserializeOwned + Send + Sync, { let mut request = self.client.get(request_url); if let Some(request_body) = request_body { request = request.json(request_body); } self.execute_request::(request).await } async fn execute_request( &self, mut request: RequestBuilder, ) -> Result { if let Some(basic_auth) = &self.basic_auth { request = request.basic_auth(&basic_auth.username, basic_auth.password.as_deref()); } let response = request.send().await.map_err(Error::Request)?; let status = response.status(); let body = response.text().await.map_err(Error::Request)?; match status { StatusCode::OK => serde_json::from_str(&body) .map_err(|e| Error::Server(format!("Failed to parse response: {e}"))), StatusCode::INTERNAL_SERVER_ERROR => Err(Error::Server(body)), _ => Err(Error::Server(format!( "Unexpected response [{status}]: {body}", ))), } } pub async fn get_lib_stream( &self, base_url: Url, ) -> Result, Error> { let request_url = base_url .join(CRYPTARCHIA_LIB_STREAM.trim_start_matches('/')) .map_err(Error::Url)?; let mut request = self.client.get(request_url); if let Some(basic_auth) = &self.basic_auth { request = request.basic_auth(&basic_auth.username, basic_auth.password.as_deref()); } let response = request.send().await.map_err(Error::Request)?; let status = response.status(); let lib_stream = response.bytes_stream().filter_map(async |item| { let bytes = item.ok()?; serde_json::from_slice::(&bytes).ok() }); match status { StatusCode::OK => Ok(lib_stream), StatusCode::INTERNAL_SERVER_ERROR => Err(Error::Server("Error".to_owned())), _ => Err(Error::Server(format!("Unexpected response [{status}]",))), } } pub async fn get_block_by_id( &self, base_url: Url, header_id: HeaderId, ) -> Result>, Error> where HeaderId: Serialize + Send + Sync, { let request_url = base_url .join(STORAGE_BLOCK.trim_start_matches('/')) .map_err(Error::Url)?; self.post(request_url, &header_id).await } /// Get consensus info (tip, height, etc.) pub async fn consensus_info(&self, base_url: Url) -> Result { let request_url = base_url .join(CRYPTARCHIA_INFO.trim_start_matches('/')) .map_err(Error::Url)?; self.get::<(), CryptarchiaInfo>(request_url, None).await } /// Get a block by its header ID pub async fn get_block( &self, base_url: Url, header_id: HeaderId, ) -> Result>, Error> { let request_url = base_url .join(STORAGE_BLOCK.trim_start_matches('/')) .map_err(Error::Url)?; self.post(request_url, &header_id).await } }