explorerd/rpc/
mod.rs

1/* This file is part of DarkFi (https://dark.fi)
2 *
3 * Copyright (C) 2020-2025 Dyne.org foundation
4 *
5 * This program is free software: you can redistribute it and/or modify
6 * it under the terms of the GNU Affero General Public License as
7 * published by the Free Software Foundation, either version 3 of the
8 * License, or (at your option) any later version.
9 *
10 * This program is distributed in the hope that it will be useful,
11 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13 * GNU Affero General Public License for more details.
14 *
15 * You should have received a copy of the GNU Affero General Public License
16 * along with this program.  If not, see <https://www.gnu.org/licenses/>.
17 */
18
19use std::{collections::HashSet, sync::Arc, time::Instant};
20
21use async_trait::async_trait;
22use log::{debug, error, trace, warn};
23use smol::lock::{MutexGuard, RwLock};
24use tinyjson::JsonValue;
25use url::Url;
26
27use darkfi::{
28    error::RpcError,
29    rpc::{
30        client::RpcClient,
31        jsonrpc::{
32            validate_empty_params, ErrorCode, JsonError, JsonRequest, JsonResponse, JsonResult,
33        },
34        server::RequestHandler,
35    },
36    system::StoppableTaskPtr,
37    Error, Result,
38};
39
40use crate::{
41    error::{server_error, ExplorerdError},
42    Explorerd,
43};
44
45/// RPC block related requests
46pub mod blocks;
47
48/// RPC handlers for contract-related perations
49pub mod contracts;
50
51/// RPC handlers for blockchain statistics and metrics
52pub mod statistics;
53
54/// RPC handlers for transaction data, lookups, and processing
55pub mod transactions;
56
57#[async_trait]
58impl RequestHandler<()> for Explorerd {
59    /// Handles an incoming JSON-RPC request by executing the appropriate individual request handler
60    /// implementation based on the request's `method` field and using the provided parameters.
61    /// Supports methods across various categories, including block-related queries, contract interactions,
62    /// transaction lookups, statistical queries, and miscellaneous operations. If an invalid
63    /// method is requested, an appropriate error is returned.
64    ///
65    /// The function performs the error handling, allowing individual RPC method handlers to propagate
66    /// errors via the `?` operator. It ensures uniform translation of errors into JSON-RPC error responses.
67    /// Additionally, it handles the creation of `JsonResponse` or `JsonError` objects, enabling method
68    /// handlers to focus solely on core logic. Individual RPC handlers return a `JsonValue`, which this
69    /// function translates into the corresponding `JsonResult`.
70    ///
71    /// Unified logging is incorporated, so individual handlers only propagate the error
72    /// for it to be logged. Logs include detailed error information, such as method names, parameters,
73    /// and JSON-RPC errors, providing consistent and informative error trails for debugging.
74    ///
75    /// ## Example Log Message
76    /// ```
77    /// 05:11:02 [ERROR] RPC Request Failure: method: transactions.get_transactions_by_header_hash,
78    /// params: ["0x0222"], error: {"error":{"code":-32602,"message":"Invalid header hash: 0x0222"},
79    /// "id":1,"jsonrpc":"2.0"}
80    /// ```
81    async fn handle_request(&self, req: JsonRequest) -> JsonResult {
82        debug!(target: "explorerd::rpc", "--> {}", req.stringify().unwrap());
83
84        // Store method and params for later use
85        let method = req.method.as_str();
86        let params = &req.params;
87
88        // Handle ping case, as it returns a JsonResponse
89        if method == "ping" {
90            return self.pong(req.id, params.clone()).await
91        }
92
93        // Match all other methods
94        let result = match req.method.as_str() {
95            // =====================
96            // Blocks methods
97            // =====================
98            "blocks.get_last_n_blocks" => self.blocks_get_last_n_blocks(params).await,
99            "blocks.get_blocks_in_heights_range" => {
100                self.blocks_get_blocks_in_heights_range(params).await
101            }
102            "blocks.get_block_by_hash" => self.blocks_get_block_by_hash(params).await,
103
104            // =====================
105            // Transactions methods
106            // =====================
107            "transactions.get_transactions_by_header_hash" => {
108                self.transactions_get_transactions_by_header_hash(params).await
109            }
110            "transactions.get_transaction_by_hash" => {
111                self.transactions_get_transaction_by_hash(params).await
112            }
113
114            // =====================
115            // Statistics methods
116            // =====================
117            "statistics.get_basic_statistics" => self.statistics_get_basic_statistics(params).await,
118            "statistics.get_metric_statistics" => {
119                self.statistics_get_metric_statistics(params).await
120            }
121            "statistics.get_latest_metric_statistics" => {
122                self.statistics_get_latest_metric_statistics(params).await
123            }
124
125            // =====================
126            // Contract methods
127            // =====================
128            "contracts.get_native_contracts" => self.contracts_get_native_contracts(params).await,
129            "contracts.get_contract_source_code_paths" => {
130                self.contracts_get_contract_source_code_paths(params).await
131            }
132            "contracts.get_contract_source" => self.contracts_get_contract_source(params).await,
133
134            // =====================
135            // Miscellaneous methods
136            // =====================
137            "ping_darkfid" => self.ping_darkfid(params).await,
138
139            // TODO: add any other useful methods
140
141            // ==============
142            // Invalid method
143            // ==============
144            _ => Err(RpcError::MethodNotFound(method.to_string()).into()),
145        };
146
147        // Process the result of the individual request handler, handling success or errors and translating
148        // them into an appropriate `JsonResult`.
149        match result {
150            // Successfully completed the request
151            Ok(value) => JsonResponse::new(value, req.id).into(),
152
153            // Handle errors when processing parameters
154            Err(Error::RpcServerError(RpcError::InvalidJson(e))) => {
155                let json_error =
156                    JsonError::new(ErrorCode::InvalidParams, Some(e.to_string()), req.id);
157
158                // Log the parameter error
159                log_request_failure(&req.method, params, &json_error);
160
161                // Convert error to JsonResult
162                json_error.into()
163            }
164
165            // Handle server errors
166            Err(Error::RpcServerError(RpcError::ServerError(e))) => {
167                // Remove the extra '&' and reference directly from e
168                let json_error = match e.downcast_ref::<ExplorerdError>() {
169                    Some(e_expl) => {
170                        // Successfully downcast to ExplorerdRpcError; call the typed function
171                        server_error(e_expl, req.id, None)
172                    }
173                    None => {
174                        // Return InternalError with the logged details
175                        JsonError::new(ErrorCode::InternalError, Some(e.to_string()), req.id)
176                    }
177                };
178
179                // Log the server error
180                log_request_failure(&req.method, params, &json_error);
181
182                // Convert error to JsonResult
183                json_error.into()
184            }
185
186            // Catch-all for any other unexpected errors
187            Err(e) => {
188                // Return InternalError with the logged details
189                let json_error =
190                    JsonError::new(ErrorCode::InternalError, Some(e.to_string()), req.id);
191
192                // Log the unexpected error
193                log_request_failure(&req.method, params, &json_error);
194
195                // Convert error to JsonResult
196                json_error.into()
197            }
198        }
199    }
200
201    async fn connections_mut(&self) -> MutexGuard<'_, HashSet<StoppableTaskPtr>> {
202        self.rpc_connections.lock().await
203    }
204}
205
206/// A RPC client for interacting with a Darkfid JSON-RPC endpoint, enabling communication with Darkfid blockchain nodes.
207/// Supports connection management, request handling, and graceful shutdowns.
208/// Implemented for shared access across ownership boundaries using `Arc`, with connection state managed via an `RwLock`.
209pub struct DarkfidRpcClient {
210    /// JSON-RPC client used to communicate with the Darkfid daemon. A value of `None` indicates no active connection.
211    /// The `RwLock` allows the client to be shared across ownership boundaries while managing the connection state.
212    rpc_client: RwLock<Option<RpcClient>>,
213}
214
215impl DarkfidRpcClient {
216    /// Creates a new client with an inactive connection.
217    pub fn new() -> Self {
218        Self { rpc_client: RwLock::new(None) }
219    }
220
221    /// Checks if there is an active connection to Darkfid.
222    pub async fn connected(&self) -> Result<bool> {
223        Ok(self.rpc_client.read().await.is_some())
224    }
225
226    /// Establishes a connection to the Darkfid node, storing the resulting client if successful.
227    /// If already connected, logs a message and returns without connecting again.
228    pub async fn connect(&self, endpoint: Url, ex: Arc<smol::Executor<'static>>) -> Result<()> {
229        let mut rpc_client_guard = self.rpc_client.write().await;
230
231        if rpc_client_guard.is_some() {
232            warn!(target: "explorerd::rpc::connect", "Already connected to darkfid");
233            return Ok(());
234        }
235
236        *rpc_client_guard = Some(RpcClient::new(endpoint, ex).await?);
237        Ok(())
238    }
239
240    /// Closes the connection with the connected darkfid, returning if there is no active connection.
241    /// If the connection is stopped, sets `rpc_client` to `None`.
242    pub async fn stop(&self) -> Result<()> {
243        let mut rpc_client_guard = self.rpc_client.write().await;
244
245        // If there's an active connection, stop it and clear the reference
246        if let Some(ref rpc_client) = *rpc_client_guard {
247            rpc_client.stop().await;
248            *rpc_client_guard = None;
249            return Ok(());
250        }
251
252        // If there's no connection, log the message and do nothing
253        warn!(target: "explorerd::rpc::stop", "Not connected to darkfid, nothing to stop.");
254        Ok(())
255    }
256
257    /// Sends a request to the client's Darkfid JSON-RPC endpoint using the given method and parameters.
258    /// Returns the received response or an error if no active connection to Darkfid exists.
259    pub async fn request(&self, method: &str, params: &JsonValue) -> Result<JsonValue> {
260        let rpc_client_guard = self.rpc_client.read().await;
261
262        if let Some(ref rpc_client) = *rpc_client_guard {
263            debug!(target: "explorerd::rpc::request", "Executing request {method} with params: {params:?}");
264            let latency = Instant::now();
265            let req = JsonRequest::new(method, params.clone());
266            let rep = rpc_client.request(req).await?;
267            let latency = latency.elapsed();
268            trace!(target: "explorerd::rpc::request", "Got reply: {rep:?}");
269            debug!(target: "explorerd::rpc::request", "Latency: {latency:?}");
270            return Ok(rep);
271        };
272
273        Err(Error::Custom("Not connected, is the explorer running in no-sync mode?".to_string()))
274    }
275
276    /// Sends a ping request to the client's darkfid endpoint to verify connectivity,
277    /// returning `true` if the ping is successful or an error if the request fails.
278    async fn ping(&self) -> Result<bool> {
279        self.request("ping", &JsonValue::Array(vec![])).await?;
280        Ok(true)
281    }
282}
283
284impl Default for DarkfidRpcClient {
285    fn default() -> Self {
286        Self::new()
287    }
288}
289
290impl Explorerd {
291    // RPCAPI:
292    // Pings configured darkfid daemon for liveness.
293    // Returns `true` on success.
294    //
295    // **Example API Usage:**
296    // --> {"jsonrpc": "2.0", "method": "ping_darkfid", "params": [], "id": 1}
297    // <-- {"jsonrpc": "2.0", "result": true, "id": 1}
298    async fn ping_darkfid(&self, params: &JsonValue) -> Result<JsonValue> {
299        // Log the start of the operation
300        debug!(target: "explorerd::rpc::ping_darkfid", "Pinging darkfid daemon...");
301
302        // Validate that the parameters are empty
303        validate_empty_params(params)?;
304
305        // Attempt to ping the darkfid daemon
306        self.darkfid_client
307            .ping()
308            .await
309            .map_err(|e| ExplorerdError::PingDarkfidFailed(e.to_string()))?;
310
311        // Ping succeeded, return a successful Boolean(true) value
312        Ok(JsonValue::Boolean(true))
313    }
314}
315
316/// Auxiliary function that logs RPC request failures by generating a structured log message
317/// containing the provided `req_method`, `params`, and `error` details. Constructs a log target
318/// specific to the request method, formats the error message by stringifying the JSON parameters
319/// and error, and performs the log operation without returning a value.
320fn log_request_failure(req_method: &str, params: &JsonValue, error: &JsonError) {
321    // Generate the log target based on request
322    let log_target = format!("explorerd::rpc::handle_request::{req_method}");
323
324    // Stringify the params
325    let params_stringified = match params.stringify() {
326        Ok(params) => params,
327        Err(e) => format!("Failed to stringify params: {e:?}"),
328    };
329
330    // Stringfy the error
331    let error_stringified = match error.stringify() {
332        Ok(err_str) => err_str,
333        Err(e) => format!("Failed to stringify error: {e:?}"),
334    };
335
336    // Format the error message for the log
337    let error_message = format!("RPC Request Failure: method: {req_method}, params: {params_stringified}, error: {error_stringified}");
338
339    // Log the error
340    error!(target: &log_target, "{error_message}");
341}
342
343/// Test module for validating API functions within this `mod.rs` file. It ensures that the core API
344/// functions behave as expected and that they handle invalid parameters properly.
345#[cfg(test)]
346mod tests {
347    use tinyjson::JsonValue;
348
349    use darkfi::rpc::jsonrpc::JsonRequest;
350
351    use super::*;
352    use crate::{
353        error::ERROR_CODE_PING_DARKFID_FAILED,
354        test_utils::{setup, validate_empty_rpc_parameters},
355    };
356
357    #[test]
358    /// Validates the failure scenario of the `ping_darkfid` JSON-RPC method by sending a request
359    /// to a disconnected darkfid endpoint, ensuring the response is an error with the expected
360    /// code and message.
361    fn test_ping_darkfid_failure() {
362        smol::block_on(async {
363            // Set up the Explorerd instance
364            let explorerd = setup();
365
366            // Prepare a JSON-RPC request for `ping_darkfid`
367            let request = JsonRequest {
368                id: 1,
369                jsonrpc: "2.0",
370                method: "ping_darkfid".to_string(),
371                params: JsonValue::Array(vec![]),
372            };
373
374            // Call `handle_request` on the Explorerd instance
375            let response = explorerd.handle_request(request).await;
376
377            // Verify the response is a `JsonError` with the `PingFailed` error code
378            match response {
379                JsonResult::Error(actual_error) => {
380                    let expected_error_code = ERROR_CODE_PING_DARKFID_FAILED;
381                    let expected_error_msg = "Ping darkfid failed: Not connected, is the explorer running in no-sync mode?";
382                    assert_eq!(actual_error.error.code, expected_error_code);
383                    assert_eq!(actual_error.error.message, expected_error_msg);
384                }
385                _ => panic!("Expected a JSON object for the response, but got something else"),
386            }
387        });
388    }
389
390    /// Tests the `ping_darkfid` method to ensure it correctly handles cases where non-empty parameters
391    /// are supplied, returning an expected error response.
392    #[test]
393    fn test_ping_darkfid_empty_params() {
394        smol::block_on(async {
395            validate_empty_rpc_parameters(&setup(), "ping_darkfid").await;
396        });
397    }
398}