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}