From 3999d2506404923443708abf67562c9468ad6bf3 Mon Sep 17 00:00:00 2001
From: Omer Yacine <mariocynicys@gmail.com>
Date: Thu, 24 Oct 2024 22:09:16 +0200
Subject: [PATCH] refactor(dispatcher): use generic error placeholder for the
 ApiClient

---
 src/transport/client.rs         | 41 ++++----------------
 src/transport/client/helpers.rs | 16 ++++----
 src/transport/client/native.rs  | 57 +++++++++++++++++-----------
 src/transport/client/wasm.rs    | 67 +++++++++++++++++++++++----------
 src/transport/endpoints.rs      | 33 +++++++++-------
 5 files changed, 119 insertions(+), 95 deletions(-)

diff --git a/src/transport/client.rs b/src/transport/client.rs
index 0024302..023f112 100644
--- a/src/transport/client.rs
+++ b/src/transport/client.rs
@@ -4,7 +4,7 @@ use percent_encoding::{utf8_percent_encode, NON_ALPHANUMERIC};
 use serde_json::Value as JsonValue;
 use std::collections::HashMap;
 use thiserror::Error;
-use url::Url;
+use url::{ParseError, Url};
 
 #[cfg(not(target_arch = "wasm32"))] pub mod native;
 #[cfg(target_arch = "wasm32")] pub mod wasm;
@@ -25,46 +25,21 @@ use reqwest::Error as ReqwestError;
 pub trait ApiClient: Clone {
     type Request;
     type Response;
+    type Error;
     type Conf;
 
-    async fn new(conf: Self::Conf) -> Result<Self, ApiClientError>
+    async fn new(conf: Self::Conf) -> Result<Self, Self::Error>
     where
         Self: Sized;
 
-    fn process_schema(&self, schema: EndpointSchema) -> Result<Self::Request, ApiClientError>;
-
-    fn to_data_request<R: SiaApiRequest>(&self, request: R) -> Result<Self::Request, ApiClientError> {
-        self.process_schema(request.to_endpoint_schema()?)
-    }
+    fn to_data_request<R: SiaApiRequest>(&self, request: R) -> Result<Self::Request, Self::Error>;
 
     // TODO this can have a default implementation if an associated type can provide .execute()
     // eg self.client().execute(request).await.map_err(Self::ClientError)
-    async fn execute_request(&self, request: Self::Request) -> Result<Self::Response, ApiClientError>;
+    async fn execute_request(&self, request: Self::Request) -> Result<Self::Response, Self::Error>;
 
     // TODO default implementation should be possible if Execute::Response is a serde deserializable type
-    async fn dispatcher<R: SiaApiRequest>(&self, request: R) -> Result<R::Response, ApiClientError>;
-}
-
-#[derive(Debug, Error)]
-pub enum ApiClientError {
-    #[error("BuildError error: {0}")]
-    BuildError(String),
-    #[error("FixmePlaceholder error: {0}")]
-    FixmePlaceholder(String), // FIXME this entire enum needs refactoring to not use client-specific error types
-    #[error("UrlParse error: {0}")]
-    UrlParse(#[from] url::ParseError),
-    #[error("UnexpectedHttpStatus error: status:{status} body:{body}")]
-    UnexpectedHttpStatus { status: http::StatusCode, body: String },
-    #[error("Serde error: {0}")]
-    Serde(#[from] serde_json::Error),
-    #[error("UnexpectedEmptyResponse error: {expected_type}")]
-    UnexpectedEmptyResponse { expected_type: String },
-    #[error("WasmFetchError error: {0}")]
-    #[cfg(target_arch = "wasm32")]
-    WasmFetchError(#[from] FetchError),
-    #[error("ReqwestError error: {0}")]
-    #[cfg(not(target_arch = "wasm32"))]
-    ReqwestError(#[from] ReqwestError), // FIXME remove this; it should be generalized enough to not need arch-specific error types
+    async fn dispatcher<R: SiaApiRequest>(&self, request: R) -> Result<R::Response, Self::Error>;
 }
 
 // Not all client implementations will have an exact equivalent of HTTP methods
@@ -149,7 +124,7 @@ pub enum Body {
 
 impl EndpointSchema {
     // Safely build the URL using percent-encoding for path params
-    pub fn build_url(&self, base_url: &Url) -> Result<Url, ApiClientError> {
+    pub fn build_url(&self, base_url: &Url) -> Result<Url, ParseError> {
         let mut path = self.path_schema.to_string();
 
         // Replace placeholders in the path with encoded values if path_params are provided
@@ -161,7 +136,7 @@ impl EndpointSchema {
         }
 
         // Combine base_url with the constructed path
-        let mut url = base_url.join(&path).map_err(ApiClientError::UrlParse)?;
+        let mut url = base_url.join(&path)?;
 
         // Add query parameters if any
         if let Some(query_params) = &self.query_params {
diff --git a/src/transport/client/helpers.rs b/src/transport/client/helpers.rs
index 37ae54c..77fe399 100644
--- a/src/transport/client/helpers.rs
+++ b/src/transport/client/helpers.rs
@@ -1,4 +1,4 @@
-use super::{ApiClient, ApiClientError};
+use super::ApiClient;
 use crate::transport::endpoints::{AddressBalanceRequest, AddressBalanceResponse, ConsensusTipRequest,
                                   GetAddressUtxosRequest};
 use crate::types::{Address, Currency, PublicKey, SiacoinElement, SpendPolicy, V2TransactionBuilder};
@@ -6,13 +6,13 @@ use async_trait::async_trait;
 use thiserror::Error;
 
 #[derive(Debug, Error)]
-pub enum ApiClientHelpersError {
+pub enum ApiClientHelpersError<E> {
     #[error(
         "ApiClientHelpersError::SelectOutputs: insufficent amount, available: {available:?} required: {required:?}"
     )]
     SelectOutputs { available: Currency, required: Currency },
     #[error("ApiClientHelpersError::ApiClientError: {0}")]
-    ApiClientError(#[from] ApiClientError),
+    ApiClientError(#[from] E),
 }
 
 /// Helper methods for the ApiClient trait
@@ -20,11 +20,11 @@ pub enum ApiClientHelpersError {
 /// This crate is focused on catering to the Komodo Defi Framework integration
 #[async_trait]
 pub trait ApiClientHelpers: ApiClient {
-    async fn current_height(&self) -> Result<u64, ApiClientError> {
+    async fn current_height(&self) -> Result<u64, Self::Error> {
         Ok(self.dispatcher(ConsensusTipRequest).await?.height)
     }
 
-    async fn address_balance(&self, address: Address) -> Result<AddressBalanceResponse, ApiClientError> {
+    async fn address_balance(&self, address: Address) -> Result<AddressBalanceResponse, Self::Error> {
         self.dispatcher(AddressBalanceRequest { address }).await
     }
 
@@ -33,7 +33,7 @@ pub trait ApiClientHelpers: ApiClient {
         address: &Address,
         limit: Option<i64>,
         offset: Option<i64>,
-    ) -> Result<Vec<SiacoinElement>, ApiClientError> {
+    ) -> Result<Vec<SiacoinElement>, Self::Error> {
         self.dispatcher(GetAddressUtxosRequest {
             address: address.clone(),
             limit,
@@ -60,7 +60,7 @@ pub trait ApiClientHelpers: ApiClient {
         &self,
         address: &Address,
         total_amount: Currency,
-    ) -> Result<(Vec<SiacoinElement>, Currency), ApiClientHelpersError> {
+    ) -> Result<(Vec<SiacoinElement>, Currency), ApiClientHelpersError<Self::Error>> {
         let mut unspent_outputs = self.get_unspent_outputs(address, None, None).await?;
 
         // Sort outputs from largest to smallest
@@ -106,7 +106,7 @@ pub trait ApiClientHelpers: ApiClient {
         tx_builder: &mut V2TransactionBuilder,
         public_key: &PublicKey,
         miner_fee: Currency,
-    ) -> Result<(), ApiClientHelpersError> {
+    ) -> Result<(), ApiClientHelpersError<Self::Error>> {
         let address = public_key.address();
         let outputs_total: Currency = tx_builder.siacoin_outputs.iter().map(|output| output.value).sum();
 
diff --git a/src/transport/client/native.rs b/src/transport/client/native.rs
index f894568..6f60968 100644
--- a/src/transport/client/native.rs
+++ b/src/transport/client/native.rs
@@ -1,13 +1,14 @@
-use crate::transport::endpoints::{ConsensusTipRequest, SiaApiRequest};
+use crate::transport::endpoints::{ConsensusTipRequest, EndpointSchemaError, SiaApiRequest};
 use async_trait::async_trait;
 use base64::engine::general_purpose::STANDARD as BASE64;
 use base64::Engine;
-use http::header::{HeaderMap, HeaderValue, AUTHORIZATION};
+use http::header::{HeaderMap, HeaderValue, InvalidHeaderValue, AUTHORIZATION};
 use reqwest::Client as ReqwestClient;
 use serde::Deserialize;
-use url::Url;
+use thiserror::Error;
+use url::{ParseError, Url};
 
-use crate::transport::client::{ApiClient, ApiClientError, ApiClientHelpers, Body as ClientBody, EndpointSchema};
+use crate::transport::client::{ApiClient, ApiClientHelpers, Body as ClientBody};
 use core::time::Duration;
 
 #[derive(Clone)]
@@ -16,6 +17,22 @@ pub struct NativeClient {
     pub base_url: Url,
 }
 
+#[derive(Debug, Error)]
+pub enum ClientError {
+    #[error("Client initialization error: {0}")]
+    InitializationError(#[from] InvalidHeaderValue),
+    #[error("Reqwest error: {0}")]
+    ReqwestError(#[from] reqwest::Error),
+    #[error("Url parse error: {0}")]
+    UrlParseError(#[from] ParseError),
+    #[error("Endpoint schema creation error: {0}")]
+    EndpointError(#[from] EndpointSchemaError),
+    #[error("Unexpected empty resposne, expected: {expected_type}")]
+    UnexpectedEmptyResponse { expected_type: String },
+    #[error("Unexpected HTTP status: [status: {status} body: {body}]")]
+    UnexpectedHttpStatus { status: http::StatusCode, body: String },
+}
+
 #[derive(Clone, Debug, Deserialize)]
 pub struct Conf {
     pub server_url: Url,
@@ -29,15 +46,16 @@ pub struct Conf {
 impl ApiClient for NativeClient {
     type Request = reqwest::Request;
     type Response = reqwest::Response;
+    type Error = ClientError;
     type Conf = Conf;
 
-    async fn new(conf: Self::Conf) -> Result<Self, ApiClientError> {
+    async fn new(conf: Self::Conf) -> Result<Self, Self::Error> {
         let mut headers = HeaderMap::new();
         if let Some(password) = &conf.password {
             let auth_value = format!("Basic {}", BASE64.encode(format!(":{}", password)));
             headers.insert(
                 AUTHORIZATION,
-                HeaderValue::from_str(&auth_value).map_err(|e| ApiClientError::BuildError(e.to_string()))?,
+                HeaderValue::from_str(&auth_value).map_err(ClientError::InitializationError)?,
             );
         }
         let timeout = conf.timeout.unwrap_or(10);
@@ -45,7 +63,7 @@ impl ApiClient for NativeClient {
             .default_headers(headers)
             .timeout(Duration::from_secs(timeout))
             .build()
-            .map_err(ApiClientError::ReqwestError)?;
+            .map_err(ClientError::ReqwestError)?;
 
         let ret = NativeClient {
             client,
@@ -56,43 +74,40 @@ impl ApiClient for NativeClient {
         Ok(ret)
     }
 
-    fn process_schema(&self, schema: EndpointSchema) -> Result<Self::Request, ApiClientError> {
-        let url = schema.build_url(&self.base_url)?;
+    fn to_data_request<R: SiaApiRequest>(&self, request: R) -> Result<Self::Request, Self::Error> {
+        let schema = request.to_endpoint_schema().map_err(ClientError::EndpointError)?;
+        let url = schema.build_url(&self.base_url).map_err(ClientError::UrlParseError)?;
         let req = match schema.body {
             ClientBody::None => self.client.request(schema.method.into(), url).build(),
             ClientBody::Utf8(body) => self.client.request(schema.method.into(), url).body(body).build(),
             ClientBody::Json(body) => self.client.request(schema.method.into(), url).json(&body).build(),
             ClientBody::Bytes(body) => self.client.request(schema.method.into(), url).body(body).build(),
         }
-        .map_err(ApiClientError::ReqwestError)?;
+        .map_err(ClientError::ReqwestError)?;
         Ok(req)
     }
 
-    async fn execute_request(&self, request: Self::Request) -> Result<Self::Response, ApiClientError> {
-        self.client.execute(request).await.map_err(ApiClientError::ReqwestError)
+    async fn execute_request(&self, request: Self::Request) -> Result<Self::Response, Self::Error> {
+        self.client.execute(request).await.map_err(ClientError::ReqwestError)
     }
 
-    async fn dispatcher<R: SiaApiRequest>(&self, request: R) -> Result<R::Response, ApiClientError> {
+    async fn dispatcher<R: SiaApiRequest>(&self, request: R) -> Result<R::Response, Self::Error> {
         let request = self.to_data_request(request)?;
 
         // Execute the request using reqwest client
-        let response = self
-            .client
-            .execute(request)
-            .await
-            .map_err(ApiClientError::ReqwestError)?;
+        let response = self.execute_request(request).await?;
 
         // Check the response status and return the appropriate result
         match response.status() {
             reqwest::StatusCode::OK => Ok(response
                 .json::<R::Response>()
                 .await
-                .map_err(ApiClientError::ReqwestError)?),
+                .map_err(ClientError::ReqwestError)?),
             reqwest::StatusCode::NO_CONTENT => {
                 if let Some(resp_type) = R::is_empty_response() {
                     Ok(resp_type)
                 } else {
-                    Err(ApiClientError::UnexpectedEmptyResponse {
+                    Err(ClientError::UnexpectedEmptyResponse {
                         expected_type: std::any::type_name::<R::Response>().to_string(),
                     })
                 }
@@ -106,7 +121,7 @@ impl ApiClient for NativeClient {
                     .map_err(|e| format!("Failed to retrieve body: {}", e))
                     .unwrap_or_else(|e| e);
 
-                Err(ApiClientError::UnexpectedHttpStatus { status, body })
+                Err(ClientError::UnexpectedHttpStatus { status, body })
             },
         }
     }
diff --git a/src/transport/client/wasm.rs b/src/transport/client/wasm.rs
index 9ecfea2..a3fbb90 100644
--- a/src/transport/client/wasm.rs
+++ b/src/transport/client/wasm.rs
@@ -1,21 +1,42 @@
-use crate::transport::client::{ApiClient, ApiClientError, ApiClientHelpers, Body, EndpointSchema, SchemaMethod};
-use crate::transport::endpoints::{ConsensusTipRequest, SiaApiRequest};
+use crate::transport::client::{ApiClient, ApiClientHelpers, Body, SchemaMethod};
+use crate::transport::endpoints::{ConsensusTipRequest, EndpointSchemaError, SiaApiRequest};
 
 use async_trait::async_trait;
 use http::StatusCode;
 use serde::Deserialize;
 use std::collections::HashMap;
-use url::Url;
+use thiserror::Error;
+use url::{ParseError, Url};
 
 pub mod wasm_fetch;
+use crate::transport::client::wasm::wasm_fetch::FetchError;
 use wasm_fetch::{Body as FetchBody, FetchMethod, FetchRequest, FetchResponse};
 
 #[derive(Clone)]
-pub struct Client {
+pub struct WasmClient {
     pub base_url: Url,
     pub headers: HashMap<String, String>,
 }
 
+#[derive(Debug, Error)]
+pub enum ClientError {
+    #[error("Request error: {0}")]
+    WasmFetchError(#[from] FetchError),
+    #[error("Url parse error: {0}")]
+    UrlParseError(#[from] ParseError),
+    #[error("Endpoint schema creation error: {0}")]
+    EndpointError(#[from] EndpointSchemaError),
+    #[error("Deserialization error: {0}")]
+    DeserializationError(#[from] serde_json::Error),
+    #[error("Unexpected empty resposne, expected: {expected_type}")]
+    UnexpectedEmptyResponse { expected_type: String },
+    #[error("Unexpected HTTP status: [status: {status} body: {body}]")]
+    UnexpectedHttpStatus { status: http::StatusCode, body: String },
+    // FIXME: Remove this error type.
+    #[error("FixmePlaceholder error: {0}")]
+    FixmePlaceholder(String),
+}
+
 #[derive(Clone, Debug, Deserialize)]
 pub struct Conf {
     pub server_url: Url,
@@ -24,13 +45,14 @@ pub struct Conf {
 }
 
 #[async_trait]
-impl ApiClient for Client {
+impl ApiClient for WasmClient {
     type Request = FetchRequest;
     type Response = FetchResponse;
+    type Error = ClientError;
     type Conf = Conf;
 
-    async fn new(conf: Self::Conf) -> Result<Self, ApiClientError> {
-        let client = Client {
+    async fn new(conf: Self::Conf) -> Result<Self, ClientError> {
+        let client = WasmClient {
             base_url: conf.server_url,
             headers: conf.headers,
         };
@@ -39,12 +61,13 @@ impl ApiClient for Client {
         Ok(client)
     }
 
-    fn process_schema(&self, schema: EndpointSchema) -> Result<Self::Request, ApiClientError> {
+    fn to_data_request<R: SiaApiRequest>(&self, request: R) -> Result<Self::Request, Self::Error> {
+        let schema = request.to_endpoint_schema().map_err(ClientError::EndpointError)?;
         let url = schema.build_url(&self.base_url)?;
         let method = match schema.method {
             SchemaMethod::Get => FetchMethod::Get,
             SchemaMethod::Post => FetchMethod::Post,
-            _ => return Err(ApiClientError::FixmePlaceholder("Unsupported method".to_string())),
+            _ => return Err(ClientError::FixmePlaceholder("Unsupported method".to_string())),
         };
         let body = match schema.body {
             Body::Utf8(body) => Some(FetchBody::Utf8(body)),
@@ -60,15 +83,15 @@ impl ApiClient for Client {
         })
     }
 
-    async fn execute_request(&self, request: Self::Request) -> Result<Self::Response, ApiClientError> {
+    async fn execute_request(&self, request: Self::Request) -> Result<Self::Response, ClientError> {
         request
             .execute()
             .await
-            .map_err(|e| ApiClientError::FixmePlaceholder(format!("FIXME {}", e)))
+            .map_err(|e| ClientError::FixmePlaceholder(format!("FIXME {}", e)))
     }
 
     // Dispatcher function that converts the request and handles execution
-    async fn dispatcher<R: SiaApiRequest>(&self, request: R) -> Result<R::Response, ApiClientError> {
+    async fn dispatcher<R: SiaApiRequest>(&self, request: R) -> Result<R::Response, ClientError> {
         let request = self.to_data_request(request)?; // Convert request to data request
 
         // Execute the request
@@ -77,10 +100,14 @@ impl ApiClient for Client {
         match response.status {
             StatusCode::OK => {
                 let response_body = match response.body {
-                    Some(FetchBody::Json(body)) => serde_json::from_value(body).map_err(ApiClientError::Serde)?,
-                    Some(FetchBody::Utf8(body)) => serde_json::from_str(&body).map_err(ApiClientError::Serde)?,
+                    Some(FetchBody::Json(body)) => {
+                        serde_json::from_value(body).map_err(ClientError::DeserializationError)?
+                    },
+                    Some(FetchBody::Utf8(body)) => {
+                        serde_json::from_str(&body).map_err(ClientError::DeserializationError)?
+                    },
                     _ => {
-                        return Err(ApiClientError::FixmePlaceholder(
+                        return Err(ClientError::FixmePlaceholder(
                             "Unsupported body type in response".to_string(),
                         ))
                     },
@@ -91,7 +118,7 @@ impl ApiClient for Client {
                 if let Some(resp_type) = R::is_empty_response() {
                     Ok(resp_type)
                 } else {
-                    Err(ApiClientError::UnexpectedEmptyResponse {
+                    Err(ClientError::UnexpectedEmptyResponse {
                         expected_type: std::any::type_name::<R::Response>().to_string(),
                     })
                 }
@@ -103,7 +130,7 @@ impl ApiClient for Client {
                     .map(|b| format!("{}", b)) // Use Display trait to format Body
                     .unwrap_or_else(|| "".to_string()); // If body is None, use an empty string
 
-                Err(ApiClientError::UnexpectedHttpStatus { status, body })
+                Err(ClientError::UnexpectedHttpStatus { status, body })
             },
         }
     }
@@ -113,7 +140,7 @@ impl ApiClient for Client {
 // Just this is needed to implement the `ApiClientHelpers` trait
 // unless custom implementations for the traits methods are needed
 #[async_trait]
-impl ApiClientHelpers for Client {}
+impl ApiClientHelpers for WasmClient {}
 
 #[cfg(all(target_arch = "wasm32", test))]
 mod wasm_tests {
@@ -132,7 +159,7 @@ mod wasm_tests {
     async fn test_sia_wasm_client_client_error() {
         use crate::transport::endpoints::TxpoolBroadcastRequest;
         use crate::types::V2Transaction;
-        let client = Client::new(CONF.clone()).await.unwrap();
+        let client = WasmClient::new(CONF.clone()).await.unwrap();
 
         let tx_str = r#"
         {
@@ -186,7 +213,7 @@ mod wasm_tests {
             v2transactions: vec![tx],
         };
         match client.dispatcher(req).await.expect_err("Expected HTTP 400 error") {
-            ApiClientError::UnexpectedHttpStatus {
+            ClientError::UnexpectedHttpStatus {
                 status: StatusCode::BAD_REQUEST,
                 body: _,
             } => (),
diff --git a/src/transport/endpoints.rs b/src/transport/endpoints.rs
index d55104b..21e2aba 100644
--- a/src/transport/endpoints.rs
+++ b/src/transport/endpoints.rs
@@ -1,8 +1,9 @@
-use crate::transport::client::{ApiClientError, Body, EndpointSchema, EndpointSchemaBuilder, SchemaMethod};
+use crate::transport::client::{Body, EndpointSchema, EndpointSchemaBuilder, SchemaMethod};
 use crate::types::{Address, BlockID, Currency, Event, Hash256, SiacoinElement, V1Transaction, V2Transaction};
 use serde::de::DeserializeOwned;
 use serde::{Deserialize, Serialize};
 use std::collections::HashMap;
+use thiserror::Error;
 
 const ENDPOINT_ADDRESSES_BALANCE: &str = "api/addresses/{address}/balance";
 const ENDPOINT_ADDRESSES_EVENTS: &str = "api/addresses/{address}/events";
@@ -14,13 +15,19 @@ const ENDPOINT_TXPOOL_FEE: &str = "api/txpool/fee";
 const ENDPOINT_TXPOOL_TRANSACTIONS: &str = "api/txpool/transactions";
 const ENDPOINT_DEBUG_MINE: &str = "api/debug/mine";
 
+#[derive(Debug, Error)]
+pub enum EndpointSchemaError {
+    #[error("Serialization error: {0}")]
+    SerializationError(#[from] serde_json::Error),
+}
+
 pub trait SiaApiRequest: Send {
     type Response: DeserializeOwned;
 
     // Applicable for requests that return HTTP 204 No Content
     fn is_empty_response() -> Option<Self::Response> { None }
 
-    fn to_endpoint_schema(&self) -> Result<EndpointSchema, ApiClientError>;
+    fn to_endpoint_schema(&self) -> Result<EndpointSchema, EndpointSchemaError>;
 }
 
 /// Represents the request-response pair for fetching the current consensus tip of the Sia network.
@@ -47,7 +54,7 @@ pub struct ConsensusTipRequest;
 impl SiaApiRequest for ConsensusTipRequest {
     type Response = ConsensusTipResponse;
 
-    fn to_endpoint_schema(&self) -> Result<EndpointSchema, ApiClientError> {
+    fn to_endpoint_schema(&self) -> Result<EndpointSchema, EndpointSchemaError> {
         Ok(EndpointSchemaBuilder::new(ENDPOINT_CONSENSUS_TIP.to_owned(), SchemaMethod::Get).build())
     }
 }
@@ -89,7 +96,7 @@ pub struct AddressBalanceRequest {
 impl SiaApiRequest for AddressBalanceRequest {
     type Response = AddressBalanceResponse;
 
-    fn to_endpoint_schema(&self) -> Result<EndpointSchema, ApiClientError> {
+    fn to_endpoint_schema(&self) -> Result<EndpointSchema, EndpointSchemaError> {
         let mut path_params = HashMap::new();
         path_params.insert("address".to_owned(), self.address.to_string());
 
@@ -136,7 +143,7 @@ pub struct GetEventRequest {
 impl SiaApiRequest for GetEventRequest {
     type Response = Event;
 
-    fn to_endpoint_schema(&self) -> Result<EndpointSchema, ApiClientError> {
+    fn to_endpoint_schema(&self) -> Result<EndpointSchema, EndpointSchemaError> {
         // Create the path_params HashMap to substitute {txid} in the path schema
         let mut path_params = HashMap::new();
         path_params.insert("txid".to_owned(), self.txid.to_string());
@@ -180,7 +187,7 @@ pub struct AddressesEventsRequest {
 impl SiaApiRequest for AddressesEventsRequest {
     type Response = Vec<Event>;
 
-    fn to_endpoint_schema(&self) -> Result<EndpointSchema, ApiClientError> {
+    fn to_endpoint_schema(&self) -> Result<EndpointSchema, EndpointSchemaError> {
         let mut path_params = HashMap::new();
         path_params.insert("address".to_owned(), self.address.to_string());
 
@@ -235,7 +242,7 @@ pub struct GetAddressUtxosRequest {
 impl SiaApiRequest for GetAddressUtxosRequest {
     type Response = Vec<SiacoinElement>;
 
-    fn to_endpoint_schema(&self) -> Result<EndpointSchema, ApiClientError> {
+    fn to_endpoint_schema(&self) -> Result<EndpointSchema, EndpointSchemaError> {
         let mut path_params = HashMap::new();
         path_params.insert("address".to_owned(), self.address.to_string());
 
@@ -299,9 +306,9 @@ impl SiaApiRequest for TxpoolBroadcastRequest {
 
     fn is_empty_response() -> Option<Self::Response> { Some(EmptyResponse) }
 
-    fn to_endpoint_schema(&self) -> Result<EndpointSchema, ApiClientError> {
+    fn to_endpoint_schema(&self) -> Result<EndpointSchema, EndpointSchemaError> {
         // Serialize the transactions into a JSON body
-        let body = serde_json::to_value(self).map_err(ApiClientError::Serde)?;
+        let body = serde_json::to_value(self).map_err(EndpointSchemaError::SerializationError)?;
         let body = body.to_string();
         Ok(
             EndpointSchemaBuilder::new(ENDPOINT_TXPOOL_BROADCAST.to_owned(), SchemaMethod::Post)
@@ -343,7 +350,7 @@ pub struct TxpoolFeeResponse(pub Currency);
 impl SiaApiRequest for TxpoolFeeRequest {
     type Response = TxpoolFeeResponse;
 
-    fn to_endpoint_schema(&self) -> Result<EndpointSchema, ApiClientError> {
+    fn to_endpoint_schema(&self) -> Result<EndpointSchema, EndpointSchemaError> {
         Ok(
             EndpointSchemaBuilder::new(ENDPOINT_TXPOOL_FEE.to_owned(), SchemaMethod::Get).build(), // No path_params, query_params, or body needed for this request
         )
@@ -374,7 +381,7 @@ impl SiaApiRequest for TxpoolTransactionsRequest {
 
     fn is_empty_response() -> Option<Self::Response> { Some(EmptyResponse) }
 
-    fn to_endpoint_schema(&self) -> Result<EndpointSchema, ApiClientError> {
+    fn to_endpoint_schema(&self) -> Result<EndpointSchema, EndpointSchemaError> {
         Ok(
             EndpointSchemaBuilder::new(ENDPOINT_TXPOOL_TRANSACTIONS.to_owned(), SchemaMethod::Get).build(), // No path_params, query_params, or body needed for this request
         )
@@ -414,9 +421,9 @@ impl SiaApiRequest for DebugMineRequest {
 
     fn is_empty_response() -> Option<Self::Response> { Some(EmptyResponse) }
 
-    fn to_endpoint_schema(&self) -> Result<EndpointSchema, ApiClientError> {
+    fn to_endpoint_schema(&self) -> Result<EndpointSchema, EndpointSchemaError> {
         // Serialize the request into a JSON string
-        let body = serde_json::to_string(self).map_err(ApiClientError::Serde)?;
+        let body = serde_json::to_string(self).map_err(EndpointSchemaError::SerializationError)?;
         Ok(
             EndpointSchemaBuilder::new(ENDPOINT_DEBUG_MINE.to_owned(), SchemaMethod::Post)
                 .body(Body::Utf8(body)) // Set the JSON body for the POST request