mod.rs 16 KB

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