Sfoglia il codice sorgente

explorerd/rpc: streamline parameter processing, result handling, error handling, and logging

Refactored the RPC layer to unify parameter extraction/validation, error handling, result construction, and request failure logging. These updates simplify the rpc layer, reduce individual method handler code by approximately 20%, enhance error context, and aim to improve the overall user and developer experience.

### Key Improvements:

Streamlined Error Handling:
- Unified error handling within the `handle_request` function enables RPC method handlers to use the `?` operator to return errors for unified processing. This streamlines error handling and provides consistent translation of `JsonError` responses.

Parameter Parsing and Validation:
- Incorporated use of new `jsonrpc` utilities for streamlined parameter extraction and validation. This eliminates parameter boilerplate extract and validation logic across handlers, reduces the risk of inconsistencies, and establishes a unified parameter processing approach for all RPC methods.

Simplified `JsonResult` Construction:
- Replaced direct `JsonResult` construction in handlers with a unified approach. Handlers now return a `JsonValue` wrapped in a `darkfi::Result`, simplifying implementation by removing the need to construct `JsonResponse`/`JsonError` within individual RPC method handlers.

Enhanced Error Context:
- Added detailed error information, including parameter names, indices, and values that caused validation failures. These enhancements make it easier to pinpoint the root causes of errors, benefiting both developers and API users.

Unified Logging:
- Added consolidated logging for RPC request failures in the `handle_request` function. Errors are logged with details like RPC method name, parameters, and the JSON-RPC error returned back to the caller, ensuring consistent and informative reporting.

### Highlight:

Cleaner and More Consistent Code:
By consolidating error handling, result construction, and parameter processing with the new `jsonrpc` utilities, RPC method handlers are now more concise. These changes reduce the code required for implementation, allowing developers to focus on core RPC logic and service integration.

### Module Update Details:

mod.rs:
- Refactored the `handle_request` method to streamline JSON-RPC request handling
- Unified error processing, logging, and result transformation using `JsonResult`
- Reorganized JSON-RPC methods in the match block into logical order (blocks, transactions, statistics, contracts, then miscellaneous)
- Added a utility function for failure logging, capturing method names, parameters, and error details

blocks.rs: contracts.rs, statistics.rs, transactions.rs:
- Updated individual modules to align with the refactored `handle_request` logic
- Updated numeric parameters to be processed as `JsonValue::Number` instead of strings
kalm 1 anno fa
parent
commit
db23d77da9

+ 40 - 78
bin/explorer/explorerd/src/rpc/blocks.rs

@@ -16,15 +16,12 @@
  * along with this program.  If not, see <https://www.gnu.org/licenses/>.
  */
 
-use log::error;
 use tinyjson::JsonValue;
 
 use darkfi::{
     blockchain::BlockInfo,
-    rpc::jsonrpc::{
-        ErrorCode::{InternalError, InvalidParams, ParseError},
-        JsonError, JsonResponse, JsonResult,
-    },
+    error::RpcError,
+    rpc::jsonrpc::{parse_json_array_number, parse_json_array_string},
     util::encoding::base64,
     Result,
 };
@@ -70,36 +67,23 @@ impl Explorerd {
     // **Returns:**
     // * Array of `BlockRecord` encoded into a JSON.
     //
-    // --> {"jsonrpc": "2.0", "method": "blocks.get_last_n_blocks", "params": ["10"], "id": 1}
+    // **Example API Usage:**
+    // --> {"jsonrpc": "2.0", "method": "blocks.get_last_n_blocks", "params": [10], "id": 1}
     // <-- {"jsonrpc": "2.0", "result": {...}, "id": 1}
-    pub async fn blocks_get_last_n_blocks(&self, id: u16, params: JsonValue) -> JsonResult {
-        let params = params.get::<Vec<JsonValue>>().unwrap();
-        if params.len() != 1 || !params[0].is_string() {
-            return JsonError::new(InvalidParams, None, id).into()
-        }
+    pub async fn blocks_get_last_n_blocks(&self, params: &JsonValue) -> Result<JsonValue> {
+        // Extract the number of last blocks to fetch
+        let num_last_blocks = parse_json_array_number("num_last_blocks", 0, params)? as usize;
+
+        // Fetch the blocks
+        let blocks_result = self.service.get_last_n(num_last_blocks)?;
 
-        // Extract the number of last blocks to retrieve from parameters
-        let n = match params[0].get::<String>().unwrap().parse::<usize>() {
-            Ok(v) => v,
-            Err(_) => return JsonError::new(ParseError, None, id).into(),
-        };
-
-        // Fetch the blocks and handle potential errors
-        let blocks_result = match self.service.get_last_n(n) {
-            Ok(blocks) => blocks,
-            Err(e) => {
-                error!(target: "explorerd::rpc_blocks::blocks_get_last_n_blocks", "Failed fetching blocks: {}", e);
-                return JsonError::new(InternalError, None, id).into();
-            }
-        };
-
-        // Transform blocks to json and return result
+        // Transform blocks to `JsonValue`
         if blocks_result.is_empty() {
-            JsonResponse::new(JsonValue::Array(vec![]), id).into()
+            Ok(JsonValue::Array(vec![]))
         } else {
             let json_blocks: Vec<JsonValue> =
                 blocks_result.into_iter().map(|block| block.to_json_array()).collect();
-            JsonResponse::new(JsonValue::Array(json_blocks), id).into()
+            Ok(JsonValue::Array(json_blocks))
         }
     }
 
@@ -114,48 +98,37 @@ impl Explorerd {
     // **Returns:**
     // * Array of `BlockRecord` encoded into a JSON.
     //
-    // --> {"jsonrpc": "2.0", "method": "blocks.get_blocks_in_heights_range", "params": ["10", "15"], "id": 1}
+    // **Example API Usage:**
+    // --> {"jsonrpc": "2.0", "method": "blocks.get_blocks_in_heights_range", "params": [10, 15], "id": 1}
     // <-- {"jsonrpc": "2.0", "result": {...}, "id": 1}
     pub async fn blocks_get_blocks_in_heights_range(
         &self,
-        id: u16,
-        params: JsonValue,
-    ) -> JsonResult {
-        let params = params.get::<Vec<JsonValue>>().unwrap();
-        if params.len() != 2 || !params[0].is_string() || !params[1].is_string() {
-            return JsonError::new(InvalidParams, None, id).into()
-        }
+        params: &JsonValue,
+    ) -> Result<JsonValue> {
+        // Extract the start range
+        let start = parse_json_array_number("start", 0, params)? as u32;
 
-        let start = match params[0].get::<String>().unwrap().parse::<u32>() {
-            Ok(v) => v,
-            Err(_) => return JsonError::new(ParseError, None, id).into(),
-        };
-
-        let end = match params[1].get::<String>().unwrap().parse::<u32>() {
-            Ok(v) => v,
-            Err(_) => return JsonError::new(ParseError, None, id).into(),
-        };
+        // Extract the end range
+        let end = parse_json_array_number("end", 1, params)? as u32;
 
+        // Validate for valid range
         if start > end {
-            return JsonError::new(ParseError, None, id).into()
+            return Err(RpcError::InvalidJson(format!(
+                "Invalid range: start ({start}) cannot be greater than end ({end})"
+            ))
+            .into());
         }
 
-        // Fetch the blocks and handle potential errors
-        let blocks_result = match self.service.get_by_range(start, end) {
-            Ok(blocks) => blocks,
-            Err(e) => {
-                error!(target: "explorerd::rpc_blocks::blocks_get_blocks_in_height_range", "Failed fetching blocks: {}", e);
-                return JsonError::new(InternalError, None, id).into();
-            }
-        };
+        // Fetch the blocks
+        let blocks_result = self.service.get_by_range(start, end)?;
 
-        // Transform blocks to json and return result
+        // Transform blocks to `JsonValue` and return result
         if blocks_result.is_empty() {
-            JsonResponse::new(JsonValue::Array(vec![]), id).into()
+            Ok(JsonValue::Array(vec![]))
         } else {
             let json_blocks: Vec<JsonValue> =
                 blocks_result.into_iter().map(|block| block.to_json_array()).collect();
-            JsonResponse::new(JsonValue::Array(json_blocks), id).into()
+            Ok(JsonValue::Array(json_blocks))
         }
     }
 
@@ -169,28 +142,17 @@ impl Explorerd {
     // **Returns:**
     // * `BlockRecord` encoded into a JSON.
     //
+    // **Example API Usage:**
     // --> {"jsonrpc": "2.0", "method": "blocks.get_block_by_hash", "params": ["5cc...2f9"], "id": 1}
     // <-- {"jsonrpc": "2.0", "result": {...}, "id": 1}
-    pub async fn blocks_get_block_by_hash(&self, id: u16, params: JsonValue) -> JsonResult {
-        let params = params.get::<Vec<JsonValue>>().unwrap();
-        if params.len() != 1 || !params[0].is_string() {
-            return JsonError::new(InvalidParams, None, id).into()
-        }
-
-        // Extract header hash from params, returning error if not provided
-        let header_hash = match params[0].get::<String>() {
-            Some(hash) => hash,
-            None => return JsonError::new(InvalidParams, None, id).into(),
-        };
-
-        // Fetch and transform block to json, handling any errors and returning the result
-        match self.service.get_block_by_hash(header_hash) {
-            Ok(Some(block)) => JsonResponse::new(block.to_json_array(), id).into(),
-            Ok(None) => JsonResponse::new(JsonValue::Array(vec![]), id).into(),
-            Err(e) => {
-                error!(target: "explorerd::rpc_blocks", "Failed fetching block: {:?}", e);
-                JsonError::new(InternalError, None, id).into()
-            }
+    pub async fn blocks_get_block_by_hash(&self, params: &JsonValue) -> Result<JsonValue> {
+        // Extract header hash
+        let header_hash = parse_json_array_string("header_hash", 0, params)?;
+
+        // Fetch and transform block to `JsonValue`
+        match self.service.get_block_by_hash(&header_hash)? {
+            Some(block) => Ok(block.to_json_array()),
+            None => Ok(JsonValue::Array(vec![])),
         }
     }
 }

+ 44 - 84
bin/explorer/explorerd/src/rpc/contracts.rs

@@ -18,16 +18,15 @@
 
 use std::str::FromStr;
 
-use log::error;
 use tinyjson::JsonValue;
 
-use darkfi::rpc::jsonrpc::{
-    ErrorCode::{InternalError, InvalidParams},
-    JsonError, JsonResponse, JsonResult,
+use darkfi::{
+    rpc::jsonrpc::{parse_json_array_string, validate_empty_params},
+    Result,
 };
 use darkfi_sdk::crypto::ContractId;
 
-use crate::Explorerd;
+use crate::{error::ExplorerdError, Explorerd};
 
 impl Explorerd {
     // RPCAPI:
@@ -40,33 +39,25 @@ impl Explorerd {
     // **Returns:**
     // * Array of `ContractRecord`s encoded into a JSON.
     //
+    // **Example API Usage:**
     // --> {"jsonrpc": "2.0", "method": "contracts.get_native_contracts", "params": ["5cc...2f9"], "id": 1}
     // <-- {"jsonrpc": "2.0", "result": ["BZHKGQ26bzmBithTQYTJtjo2QdCqpkR9tjSBopT4yf4o", "Money Contract", "The money contract..."], "id": 1}
-    pub async fn contracts_get_native_contracts(&self, id: u16, params: JsonValue) -> JsonResult {
-        // Ensure that the parameters are empty
-        let params = params.get::<Vec<JsonValue>>().unwrap();
-        if !params.is_empty() {
-            return JsonError::new(InvalidParams, None, id).into()
-        }
+    pub async fn contracts_get_native_contracts(&self, params: &JsonValue) -> Result<JsonValue> {
+        // Validate that no parameters are provided
+        validate_empty_params(params)?;
 
-        // Retrieve native contracts and handle potential errors
-        let contract_records = match self.service.get_native_contracts() {
-            Ok(v) => v,
-            Err(e) => {
-                error!(target: "explorerd::rpc_contracts::contracts_get_native_contracts", "Failed fetching native contracts: {}", e);
-                return JsonError::new(InternalError, None, id).into()
-            }
-        };
+        // Retrieve native contracts
+        let contract_records = self.service.get_native_contracts()?;
 
-        // Transform contract records into a JSON array and return the result
+        // Transform contract records into a JSON array and return result
         if contract_records.is_empty() {
-            JsonResponse::new(JsonValue::Array(vec![]), id).into()
+            Ok(JsonValue::Array(vec![]))
         } else {
             let json_blocks: Vec<JsonValue> = contract_records
                 .into_iter()
                 .map(|contract_record| contract_record.to_json_array())
                 .collect();
-            JsonResponse::new(JsonValue::Array(json_blocks), id).into()
+            Ok(JsonValue::Array(json_blocks))
         }
     }
 
@@ -80,41 +71,27 @@ impl Explorerd {
     // **Returns:**
     // * `JsonArray` containing source code paths for the specified Contract ID.
     //
-    // Example Call:
+    // **Example API Usage:**
     // --> {"jsonrpc": "2.0", "method": "contracts.get_contract_source_code_paths", "params": ["BZHKGQ26bzmBithTQYTJtjo2QdCqpkR9tjSBopT4yf4o"], "id": 1}
     // <-- {"jsonrpc": "2.0", "result": ["path/to/source1.rs", "path/to/source2.rs"], "id": 1}
     pub async fn contracts_get_contract_source_code_paths(
         &self,
-        id: u16,
-        params: JsonValue,
-    ) -> JsonResult {
-        // Validate that a single required parameter is provided and is of type String
-        let params = params.get::<Vec<JsonValue>>().unwrap();
-        if params.len() != 1 || !params[0].is_string() {
-            return JsonError::new(InvalidParams, None, id).into()
-        }
+        params: &JsonValue,
+    ) -> Result<JsonValue> {
+        // Extract contract ID
+        let contact_id_str = parse_json_array_string("contract_id", 0, params)?;
 
-        // Validate the provided contract ID and convert it into a ContractId object
-        let contact_id_str = params[0].get::<String>().unwrap();
-        let contract_id = match ContractId::from_str(contact_id_str) {
-            Ok(contract_id) => contract_id,
-            Err(e) => return JsonError::new(InternalError, Some(e.to_string()), id).into(),
-        };
-
-        // Retrieve source code paths for the contract, transform them into a JsonResponse, and return the result
-        match self.service.get_contract_source_paths(&contract_id) {
-            Ok(paths) => {
-                let transformed_paths =
-                    paths.iter().map(|path| JsonValue::String(path.clone())).collect();
-                JsonResponse::new(JsonValue::Array(transformed_paths), id).into()
-            }
-            Err(e) => {
-                error!(
-                    target: "explorerd::rpc_contracts::contracts_get_contract_source_code_paths",
-                    "Failed fetching contract source code paths: {e:?}");
-                JsonError::new(InternalError, None, id).into()
-            }
-        }
+        // Convert the contract string to a `ContractId` instance
+        let contract_id = ContractId::from_str(&contact_id_str)
+            .map_err(|_| ExplorerdError::InvalidContractId(contact_id_str))?;
+
+        // Retrieve source code paths for the contract
+        let paths = self.service.get_contract_source_paths(&contract_id)?;
+
+        // Tranform found paths into `JsonValues`
+        let json_value_paths = paths.iter().map(|path| JsonValue::String(path.clone())).collect();
+
+        Ok(JsonValue::Array(json_value_paths))
     }
 
     // RPCAPI:
@@ -128,41 +105,24 @@ impl Explorerd {
     // **Returns:**
     // * `String` containing the content of the contract source file.
     //
-    // Example Call:
+    // **Example API Usage:**
     // --> {"jsonrpc": "2.0", "method": "contracts.get_contract_source", "params": ["BZHKGQ26bzmBithTQYTJtjo2QdCqpkR9tjSBopT4yf4o", "client/lib.rs"], "id": 1}
     // <-- {"jsonrpc": "2.0", "result": "/* This file is ...", "id": 1}
-    pub async fn contracts_get_contract_source(&self, id: u16, params: JsonValue) -> JsonResult {
-        // Validate that the required parameters are provided
-        let params = params.get::<Vec<JsonValue>>().unwrap();
-        if params.len() != 2 || !params[0].is_string() || !params[1].is_string() {
-            return JsonError::new(InvalidParams, None, id).into()
-        }
+    pub async fn contracts_get_contract_source(&self, params: &JsonValue) -> Result<JsonValue> {
+        // Extract the contract ID
+        let contact_id_str = parse_json_array_string("contract_id", 0, params)?;
+
+        // Convert the contract string to a `ContractId` instance
+        let contract_id = ContractId::from_str(&contact_id_str)
+            .map_err(|_| ExplorerdError::InvalidContractId(contact_id_str))?;
+
+        // Extract the source path
+        let source_path = parse_json_array_string("source_path", 1, params)?;
 
-        // Validate and extract the provided Contract ID
-        let contact_id_str = params[0].get::<String>().unwrap();
-        let contract_id = match ContractId::from_str(contact_id_str) {
-            Ok(contract_id) => contract_id,
-            Err(e) => return JsonError::new(InternalError, Some(e.to_string()), id).into(),
-        };
-
-        // Extract the provided source path
-        let source_path = params[1].get::<String>().unwrap();
-
-        // Retrieve the contract source code, transform it into a JsonResponse, and return the result
-        match self.service.get_contract_source_content(&contract_id, source_path) {
-            Ok(Some(source_file)) => JsonResponse::new(JsonValue::String(source_file), id).into(),
-            Ok(None) => {
-                let empty_value =
-                    JsonValue::from(std::collections::HashMap::<String, JsonValue>::new());
-                JsonResponse::new(empty_value, id).into()
-            }
-            Err(e) => {
-                error!(
-                    target: "explorerd::rpc_contracts::contracts_get_contract_source",
-                    "Failed fetching contract source code: {}", e
-                );
-                JsonError::new(InternalError, None, id).into()
-            }
+        // Retrieve the contract source code, transform it into a `JsonValue`, and return the result
+        match self.service.get_contract_source_content(&contract_id, &source_path)? {
+            Some(source_file) => Ok(JsonValue::String(source_file)),
+            None => Ok(JsonValue::from(std::collections::HashMap::<String, JsonValue>::new())),
         }
     }
 }

+ 160 - 47
bin/explorer/explorerd/src/rpc/mod.rs

@@ -25,9 +25,12 @@ use tinyjson::JsonValue;
 use url::Url;
 
 use darkfi::{
+    error::RpcError,
     rpc::{
         client::RpcClient,
-        jsonrpc::{ErrorCode, JsonError, JsonRequest, JsonResponse, JsonResult},
+        jsonrpc::{
+            validate_empty_params, ErrorCode, JsonError, JsonRequest, JsonResponse, JsonResult,
+        },
         server::RequestHandler,
     },
     system::StoppableTaskPtr,
@@ -53,64 +56,145 @@ pub mod transactions;
 
 #[async_trait]
 impl RequestHandler<()> for Explorerd {
+    /// Handles an incoming JSON-RPC request by executing the appropriate individual request handler
+    /// implementation based on the request's `method` field and using the provided parameters.
+    /// Supports methods across various categories, including block-related queries, contract interactions,
+    /// transaction lookups, statistical queries, and miscellaneous operations. If an invalid
+    /// method is requested, an appropriate error is returned.
+    ///
+    /// The function performs the error handling, allowing individual RPC method handlers to propagate
+    /// errors via the `?` operator. It ensures uniform translation of errors into JSON-RPC error responses.
+    /// Additionally, it handles the creation of `JsonResponse` or `JsonError` objects, enabling method
+    /// handlers to focus solely on core logic. Individual RPC handlers return a `JsonValue`, which this
+    /// function translates into the corresponding `JsonResult`.
+    ///
+    /// Unified logging is incorporated, so individual handlers only propagate the error
+    /// for it to be logged. Logs include detailed error information, such as method names, parameters,
+    /// and JSON-RPC errors, providing consistent and informative error trails for debugging.
+    ///
+    /// ## Example Log Message
+    /// ```
+    /// 05:11:02 [ERROR] RPC Request Failure: method: transactions.get_transactions_by_header_hash,
+    /// params: ["0x0222"], error: {"error":{"code":-32602,"message":"Invalid header hash: 0x0222"},
+    /// "id":1,"jsonrpc":"2.0"}
+    /// ```
     async fn handle_request(&self, req: JsonRequest) -> JsonResult {
         debug!(target: "explorerd::rpc", "--> {}", req.stringify().unwrap());
 
-        match req.method.as_str() {
-            // =====================
-            // Miscellaneous methods
-            // =====================
-            "ping" => self.pong(req.id, req.params).await,
-            "ping_darkfid" => self.ping_darkfid(req.id, req.params).await,
+        // Store method and params for later use
+        let method = req.method.as_str();
+        let params = &req.params;
 
+        // Handle ping case, as it returns a JsonResponse
+        if method == "ping" {
+            return self.pong(req.id, params.clone()).await
+        }
+
+        // Match all other methods
+        let result = match req.method.as_str() {
             // =====================
             // Blocks methods
             // =====================
-            "blocks.get_last_n_blocks" => self.blocks_get_last_n_blocks(req.id, req.params).await,
+            "blocks.get_last_n_blocks" => self.blocks_get_last_n_blocks(params).await,
             "blocks.get_blocks_in_heights_range" => {
-                self.blocks_get_blocks_in_heights_range(req.id, req.params).await
-            }
-            "blocks.get_block_by_hash" => self.blocks_get_block_by_hash(req.id, req.params).await,
-
-            // =====================
-            // Contract methods
-            // =====================
-            "contracts.get_native_contracts" => {
-                self.contracts_get_native_contracts(req.id, req.params).await
-            }
-            "contracts.get_contract_source_code_paths" => {
-                self.contracts_get_contract_source_code_paths(req.id, req.params).await
-            }
-            "contracts.get_contract_source" => {
-                self.contracts_get_contract_source(req.id, req.params).await
+                self.blocks_get_blocks_in_heights_range(params).await
             }
+            "blocks.get_block_by_hash" => self.blocks_get_block_by_hash(params).await,
 
             // =====================
             // Transactions methods
             // =====================
             "transactions.get_transactions_by_header_hash" => {
-                self.transactions_get_transactions_by_header_hash(req.id, req.params).await
+                self.transactions_get_transactions_by_header_hash(params).await
             }
             "transactions.get_transaction_by_hash" => {
-                self.transactions_get_transaction_by_hash(req.id, req.params).await
+                self.transactions_get_transaction_by_hash(params).await
             }
 
             // =====================
             // Statistics methods
             // =====================
-            "statistics.get_basic_statistics" => {
-                self.statistics_get_basic_statistics(req.id, req.params).await
-            }
+            "statistics.get_basic_statistics" => self.statistics_get_basic_statistics(params).await,
             "statistics.get_metric_statistics" => {
-                self.statistics_get_metric_statistics(req.id, req.params).await
+                self.statistics_get_metric_statistics(params).await
+            }
+            "statistics.get_latest_metric_statistics" => {
+                self.statistics_get_latest_metric_statistics(params).await
             }
 
+            // =====================
+            // Contract methods
+            // =====================
+            "contracts.get_native_contracts" => self.contracts_get_native_contracts(params).await,
+            "contracts.get_contract_source_code_paths" => {
+                self.contracts_get_contract_source_code_paths(params).await
+            }
+            "contracts.get_contract_source" => self.contracts_get_contract_source(params).await,
+
+            // =====================
+            // Miscellaneous methods
+            // =====================
+            "ping_darkfid" => self.ping_darkfid(params).await,
+
             // TODO: add any other useful methods
 
             // ==============
             // Invalid method
             // ==============
-            _ => JsonError::new(ErrorCode::MethodNotFound, None, req.id).into(),
+            _ => Err(RpcError::MethodNotFound(method.to_string()).into()),
+        };
+
+        // Process the result of the individual request handler, handling success or errors and translating
+        // them into an appropriate `JsonResult`.
+        match result {
+            // Successfully completed the request
+            Ok(value) => JsonResponse::new(value, req.id).into(),
+
+            // Handle errors when processing parameters
+            Err(Error::RpcServerError(RpcError::InvalidJson(e))) => {
+                let json_error =
+                    JsonError::new(ErrorCode::InvalidParams, Some(e.to_string()), req.id);
+
+                // Log the parameter error
+                log_request_failure(&req.method, params, &json_error);
+
+                // Convert error to JsonResult
+                json_error.into()
+            }
+
+            // Handle server errors
+            Err(Error::RpcServerError(RpcError::ServerError(e))) => {
+                // Remove the extra '&' and reference directly from e
+                let json_error = match e.downcast_ref::<ExplorerdError>() {
+                    Some(e_expl) => {
+                        // Successfully downcast to ExplorerdRpcError; call the typed function
+                        server_error(e_expl, req.id, None)
+                    }
+                    None => {
+                        // Return InternalError with the logged details
+                        JsonError::new(ErrorCode::InternalError, Some(e.to_string()), req.id)
+                    }
+                };
+
+                // Log the server error
+                log_request_failure(&req.method, params, &json_error);
+
+                // Convert error to JsonResult
+                json_error.into()
+            }
+
+            // Catch-all for any other unexpected errors
+            Err(e) => {
+                // Return InternalError with the logged details
+                let json_error =
+                    JsonError::new(ErrorCode::InternalError, Some(e.to_string()), req.id);
+
+                // Log the unexpected error
+                log_request_failure(&req.method, params, &json_error);
+
+                // Convert error to JsonResult
+                json_error.into()
+            }
         }
     }
 
@@ -145,7 +229,7 @@ impl DarkfidRpcClient {
         let mut rpc_client_guard = self.rpc_client.write().await;
 
         if rpc_client_guard.is_some() {
-            warn!(target: "explorerd::rpc::connect", "Already connected to darkfid.");
+            warn!(target: "explorerd::rpc::connect", "Already connected to darkfid");
             return Ok(());
         }
 
@@ -186,20 +270,13 @@ impl DarkfidRpcClient {
             return Ok(rep);
         };
 
-        error!(target: "explorerd::rpc::request", "Not connected to darkfid.");
-        Err(Error::Custom(
-            "Not connected to darkfid. Is the explorer running in no-sync mode?".to_string(),
-        ))
+        Err(Error::Custom("Not connected, is the explorer running in no-sync mode?".to_string()))
     }
 
     /// Sends a ping request to the client's darkfid endpoint to verify connectivity,
     /// returning `true` if the ping is successful or an error if the request fails.
     async fn ping(&self) -> Result<bool> {
-        if let Err(e) = self.request("ping", &JsonValue::Array(vec![])).await {
-            error!(target: "explorerd::rpc::ping", "Failed to ping darkfid daemon: {}", e);
-            return Err(e);
-        }
-
+        self.request("ping", &JsonValue::Array(vec![])).await?;
         Ok(true)
     }
 }
@@ -215,14 +292,50 @@ impl Explorerd {
     // Pings configured darkfid daemon for liveness.
     // Returns `true` on success.
     //
+    // **Example API Usage:**
     // --> {"jsonrpc": "2.0", "method": "ping_darkfid", "params": [], "id": 1}
-    // <-- {"jsonrpc": "2.0", "result": "true", "id": 1}
-    async fn ping_darkfid(&self, id: u16, _params: JsonValue) -> JsonResult {
+    // <-- {"jsonrpc": "2.0", "result": true, "id": 1}
+    async fn ping_darkfid(&self, params: &JsonValue) -> Result<JsonValue> {
+        // Log the start of the operation
         debug!(target: "explorerd::rpc::ping_darkfid", "Pinging darkfid daemon...");
-        if let Err(e) = self.darkfid_client.ping().await {
-            error!(target: "explorerd::rpc::ping_darkfid", "Failed to ping darkfid daemon: {}", e);
-            return server_error(&ExplorerdError::PingDarkfidFailed(e.to_string()), id, None).into()
-        }
-        JsonResponse::new(JsonValue::Boolean(true), id).into()
+
+        // Validate that the parameters are empty
+        validate_empty_params(params)?;
+
+        // Attempt to ping the darkfid daemon
+        self.darkfid_client
+            .ping()
+            .await
+            .map_err(|e| ExplorerdError::PingDarkfidFailed(e.to_string()))?;
+
+        // Ping succeeded, return a successful Boolean(true) value
+        Ok(JsonValue::Boolean(true))
     }
 }
+
+/// Auxiliary function that logs RPC request failures by generating a structured log message
+/// containing the provided `req_method`, `params`, and `error` details. Constructs a log target
+/// specific to the request method, formats the error message by stringifying the JSON parameters
+/// and error, and performs the log operation without returning a value.
+fn log_request_failure(req_method: &str, params: &JsonValue, error: &JsonError) {
+    // Generate the log target based on request
+    let log_target = format!("explorerd::rpc::handle_request::{}", req_method);
+
+    // Stringify the params
+    let params_stringified = match params.stringify() {
+        Ok(params) => params,
+        Err(e) => format!("Failed to stringify params: {:?}", e),
+    };
+
+    // Stringfy the error
+    let error_stringified = match error.stringify() {
+        Ok(err_str) => err_str,
+        Err(e) => format!("Failed to stringify error: {:?}", e),
+    };
+
+    // Format the error message for the log
+    let error_message = format!("RPC Request Failure: method: {req_method}, params: {params_stringified}, error: {error_stringified}");
+
+    // Log the error
+    error!(target: &log_target, "{}", error_message);
+}

+ 31 - 57
bin/explorer/explorerd/src/rpc/statistics.rs

@@ -18,13 +18,9 @@
 
 use std::vec::Vec;
 
-use log::error;
 use tinyjson::JsonValue;
 
-use darkfi::rpc::jsonrpc::{
-    ErrorCode::{InternalError, InvalidParams},
-    JsonError, JsonResponse, JsonResult,
-};
+use darkfi::{rpc::jsonrpc::validate_empty_params, Result};
 
 use crate::Explorerd;
 
@@ -39,26 +35,18 @@ impl Explorerd {
     // **Returns:**
     // * `BaseStatistics` encoded into a JSON.
     //
+    // **Example API Usage:**
     // --> {"jsonrpc": "2.0", "method": "statistics.get_basic_statistics", "params": [], "id": 1}
     // <-- {"jsonrpc": "2.0", "result": {...}, "id": 1}
-    pub async fn statistics_get_basic_statistics(&self, id: u16, params: JsonValue) -> JsonResult {
-        // Validate to ensure parameters are empty
-        let params = params.get::<Vec<JsonValue>>().unwrap();
-        if !params.is_empty() {
-            return JsonError::new(InvalidParams, None, id).into()
-        }
+    pub async fn statistics_get_basic_statistics(&self, params: &JsonValue) -> Result<JsonValue> {
+        // Validate that no parameters are provided
+        validate_empty_params(params)?;
 
-        // Fetch `BaseStatistics`, transform to `JsonResult`, and return results
-        match self.service.get_base_statistics() {
-            Ok(Some(statistics)) => JsonResponse::new(statistics.to_json_array(), id).into(),
-            Ok(None) => JsonResponse::new(JsonValue::Array(vec![]), id).into(),
-            Err(e) => {
-                error!(
-                    target: "explorerd::rpc_statistics::statistics_get_basic_statistics",
-                    "Failed fetching basic statistics: {}", e
-                );
-                JsonError::new(InternalError, None, id).into()
-            }
+        // Attempt to retrieve base statistics; if found, convert to a JSON array,
+        // otherwise return an empty JSON array
+        match self.service.get_base_statistics()? {
+            Some(statistics) => Ok(statistics.to_json_array()),
+            None => Ok(JsonValue::Array(vec![])),
         }
     }
 
@@ -72,26 +60,20 @@ impl Explorerd {
     // **Returns:**
     // * `MetricsStatistics` array encoded into a JSON.
     //
+    // **Example API Usage:**
     // --> {"jsonrpc": "2.0", "method": "statistics.get_metric_statistics", "params": [], "id": 1}
     // <-- {"jsonrpc": "2.0", "result": {...}, "id": 1}
-    pub async fn statistics_get_metric_statistics(&self, id: u16, params: JsonValue) -> JsonResult {
-        // Validate to ensure parameters are empty
-        let params = params.get::<Vec<JsonValue>>().unwrap();
-        if !params.is_empty() {
-            return JsonError::new(InvalidParams, None, id).into()
-        }
-        // Fetch metric statistics and return results
-        let metrics = match self.service.get_metrics_statistics().await {
-            Ok(v) => v,
-            Err(e) => {
-                error!(target: "explorerd::rpc_statistics::statistics_get_metric_statistics", "Failed fetching metric statistics: {}", e);
-                return JsonError::new(InternalError, None, id).into()
-            }
-        };
+    pub async fn statistics_get_metric_statistics(&self, params: &JsonValue) -> Result<JsonValue> {
+        // Validate that no parameters are provided
+        validate_empty_params(params)?;
+
+        // Retrieve metric statistics
+        let statistics = self.service.get_metrics_statistics().await?;
 
-        // Transform statistics to JsonResponse and return result
-        let metrics_json: Vec<JsonValue> = metrics.iter().map(|m| m.to_json_array()).collect();
-        JsonResponse::new(JsonValue::Array(metrics_json), id).into()
+        // Convert each metric statistic into a JSON array, returning the collected array
+        let statistics_json: Vec<JsonValue> =
+            statistics.iter().map(|m| m.to_json_array()).collect();
+        Ok(JsonValue::Array(statistics_json))
     }
 
     // RPCAPI:
@@ -104,28 +86,20 @@ impl Explorerd {
     // **Returns:**
     // * `MetricsStatistics` encoded into a JSON.
     //
+    // **Example API Usage:**
     // --> {"jsonrpc": "2.0", "method": "statistics.get_latest_metric_statistics", "params": [], "id": 1}
     // <-- {"jsonrpc": "2.0", "result": {...}, "id": 1}
     pub async fn statistics_get_latest_metric_statistics(
         &self,
-        id: u16,
-        params: JsonValue,
-    ) -> JsonResult {
-        // Validate to ensure parameters are empty
-        let params = params.get::<Vec<JsonValue>>().unwrap();
-        if !params.is_empty() {
-            return JsonError::new(InvalidParams, None, id).into()
-        }
-        // Fetch metric statistics and return results
-        let metrics = match self.service.get_latest_metrics_statistics().await {
-            Ok(v) => v,
-            Err(e) => {
-                error!(target: "explorerd::rpc_statistics::statistics_get_latest_metric_statistics", "Failed fetching metric statistics: {}", e);
-                return JsonError::new(InternalError, None, id).into()
-            }
-        };
+        params: &JsonValue,
+    ) -> Result<JsonValue> {
+        // Validate that no parameters are provided
+        validate_empty_params(params)?;
+
+        // Retrieve the latest statistics
+        let statistics = self.service.get_latest_metrics_statistics().await?;
 
-        // Transform statistics to JsonResponse and return result
-        JsonResponse::new(metrics.to_json_array(), id).into()
+        // Convert the retrieved metrics into a JSON array and return it
+        Ok(statistics.to_json_array())
     }
 }

+ 24 - 47
bin/explorer/explorerd/src/rpc/transactions.rs

@@ -16,16 +16,12 @@
  * along with this program.  If not, see <https://www.gnu.org/licenses/>.
  */
 
-use log::error;
 use tinyjson::JsonValue;
 
-use darkfi::rpc::jsonrpc::{
-    ErrorCode::{InternalError, InvalidParams},
-    JsonError, JsonResponse, JsonResult,
-};
+use darkfi::{rpc::jsonrpc::parse_json_array_string, Result};
 use darkfi_sdk::tx::TransactionHash;
 
-use crate::Explorerd;
+use crate::{error::ExplorerdError, Explorerd};
 
 impl Explorerd {
     // RPCAPI:
@@ -38,32 +34,21 @@ impl Explorerd {
     // **Returns:**
     // * Array of `TransactionRecord` encoded into a JSON.
     //
+    // **Example API Usage:**
     // --> {"jsonrpc": "2.0", "method": "transactions.get_transactions_by_header_hash", "params": ["5cc...2f9"], "id": 1}
     // <-- {"jsonrpc": "2.0", "result": {...}, "id": 1}
     pub async fn transactions_get_transactions_by_header_hash(
         &self,
-        id: u16,
-        params: JsonValue,
-    ) -> JsonResult {
-        let params = params.get::<Vec<JsonValue>>().unwrap();
-        if params.len() != 1 || !params[0].is_string() {
-            return JsonError::new(InvalidParams, None, id).into()
-        }
+        params: &JsonValue,
+    ) -> Result<JsonValue> {
+        // Extract header hash
+        let header_hash = parse_json_array_string("header_hash", 0, params)?;
 
-        let header_hash = params[0].get::<String>().unwrap();
-        let transactions = match self.service.get_transactions_by_header_hash(header_hash) {
-            Ok(v) => v,
-            Err(e) => {
-                error!(target: "explorerd::rpc_transactions::transactions_get_transaction_by_header_hash", "Failed fetching block transactions: {}", e);
-                return JsonError::new(InternalError, None, id).into()
-            }
-        };
+        // Retrieve transactions by header hash
+        let transactions = self.service.get_transactions_by_header_hash(&header_hash)?;
 
-        let mut ret = vec![];
-        for transaction in transactions {
-            ret.push(transaction.to_json_array());
-        }
-        JsonResponse::new(JsonValue::Array(ret), id).into()
+        // Convert transactions into a JSON array, return result
+        Ok(JsonValue::Array(transactions.iter().map(|tx| tx.to_json_array()).collect()))
     }
 
     // RPCAPI:
@@ -76,33 +61,25 @@ impl Explorerd {
     // **Returns:**
     // * `TransactionRecord` encoded into a JSON.
     //
+    // **Example API Usage:**
     // --> {"jsonrpc": "2.0", "method": "transactions.get_transaction_by_hash", "params": ["7e7...b4d"], "id": 1}
     // <-- {"jsonrpc": "2.0", "result": {...}, "id": 1}
     pub async fn transactions_get_transaction_by_hash(
         &self,
-        id: u16,
-        params: JsonValue,
-    ) -> JsonResult {
-        let params = params.get::<Vec<JsonValue>>().unwrap();
-        if params.len() != 1 || !params[0].is_string() {
-            return JsonError::new(InvalidParams, None, id).into()
-        }
+        params: &JsonValue,
+    ) -> Result<JsonValue> {
+        // Extract transaction hash
+        let tx_hash_str = parse_json_array_string("tx_hash", 0, params)?;
 
-        // Validate provided hash and store it for later use
-        let tx_hash_str = params[0].get::<String>().unwrap();
-        let tx_hash = match tx_hash_str.parse::<TransactionHash>() {
-            Ok(hash) => hash,
-            Err(e) => return JsonError::new(InternalError, Some(e.to_string()), id).into(),
-        };
+        // Convert the provided hash into a `TransactionHash` instance
+        let tx_hash = tx_hash_str
+            .parse::<TransactionHash>()
+            .map_err(|_| ExplorerdError::InvalidTxHash(tx_hash_str.to_string()))?;
 
-        // Retrieve transaction by hash and return result
-        match self.service.get_transaction_by_hash(&tx_hash) {
-            Ok(Some(transaction)) => JsonResponse::new(transaction.to_json_array(), id).into(),
-            Ok(None) => JsonResponse::new(JsonValue::Array(vec![]), id).into(),
-            Err(e) => {
-                error!(target: "explorerd::rpc_transactions::transactions_get_transaction_by_hash", "Failed fetching transaction: {}", e);
-                JsonError::new(InternalError, None, id).into()
-            }
+        // Retrieve the transaction by its hash, returning the result as a JsonValue array
+        match self.service.get_transaction_by_hash(&tx_hash)? {
+            Some(transaction) => Ok(transaction.to_json_array()),
+            None => Ok(JsonValue::Array(vec![])),
         }
     }
 }