Parcourir la source

explorerd: introduce `DarkfidRpcClient` for darkfid interactions

Refactored Darkfid JSON-RPC interaction methods into the new `DarkfidRpcClient` struct. This change consolidates functionality for reuse across ownership boundaries and introduces support for operating the explorer in no-sync mode.

Summary of Updates:
- Created `DarkfidRpcClient` and moved core methods like `ping` and block retrieval (`get_block_by_height`, `get_last_confirmed_block`) from `Explorerd` into it
- Updated `rpc_client` to use the `RwLock<Option<RpcClient>>` type for internally immutability and defer its creation to the `connect` method, allowing initialization without an active connection
- Added a `connect` method to establish connections to Darkfid on demand
- Updated the `stop` method to set the `RpcClient` to `None`
- Renamed and updated the `request` method to return an error when a request is made without an active connection to Darkfid
- Updated the `Explorerd` struct and implementation to integrate with `DarkfidRpcClient`
- Updated the `explorerd` binary to explicitly call `connect`

Design considerations:
- Changed `Explorerd.darkfid_client` to an `Arc` and used an `RwLock` for `DarkfidRpcClient.rpc_client` to enable shared access across ownership boundaries
- Updated `RpcClient` to be an `Option`, deferring initialization until `connect` is called. This allows startup without an active connection (`None`), supporting use cases like the explorer no-sync mode
- The `RwLock` in `DarkfidRpcClient` provides internal mutability for the connection state, ensuring that callers remain unaffected by implementation changes, such as adding a `connect` function to the `RpcClient` to delegate connection management and removing the need for `DarkfidRpcClient` to handle the connection state

Future considerations:
- The `RwLock` may be removed later by refactoring `RpcClient` for lazy connections with a `connect` method, but is kept for now to avoid wider system changes
- Plan to move `DarkfidRpcClient` to the Darkfi SDK, laying the foundation for an easy-to-use client to accelerate the creation of future Darkfi DApps
kalm il y a 1 an
Parent
commit
9e65d6aff2

+ 28 - 10
bin/explorer/explorerd/src/main.rs

@@ -25,17 +25,16 @@ use url::Url;
 
 use darkfi::{
     async_daemonize, cli_desc,
-    rpc::{
-        client::RpcClient,
-        server::{listen_and_serve, RequestHandler},
-    },
+    rpc::server::{listen_and_serve, RequestHandler},
     system::{StoppableTask, StoppableTaskPtr},
     util::path::get_config_path,
     Error, Result,
 };
 
 use crate::{
-    config::ExplorerNetworkConfig, rpc::blocks::subscribe_blocks, service::ExplorerService,
+    config::ExplorerNetworkConfig,
+    rpc::{blocks::subscribe_blocks, DarkfidRpcClient},
+    service::ExplorerService,
 };
 
 /// Configuration management across multiple networks (localnet, testnet, mainnet)
@@ -100,14 +99,18 @@ pub struct Explorerd {
     /// JSON-RPC connection tracker
     pub rpc_connections: Mutex<HashSet<StoppableTaskPtr>>,
     /// JSON-RPC client to execute requests to darkfid daemon
-    pub rpc_client: RpcClient,
+    pub darkfid_client: Arc<DarkfidRpcClient>,
+    /// Darkfi blockchain node endpoint to sync with when not in no-sync mode
+    darkfid_endpoint: Url,
+    /// A asynchronous executor used to create an RPC client when not in no-sync mode
+    executor: Arc<smol::Executor<'static>>,
 }
 
 impl Explorerd {
     /// Creates a new `BlockchainExplorer` instance.
     async fn new(db_path: String, endpoint: Url, ex: Arc<smol::Executor<'static>>) -> Result<Self> {
-        // Initialize rpc client
-        let rpc_client = RpcClient::new(endpoint.clone(), ex).await?;
+        // Initialize darkfid rpc client
+        let darkfid_client = Arc::new(DarkfidRpcClient::new());
         info!(target: "explorerd", "Connected to Darkfi node: {}", endpoint.to_string().trim_end_matches('/'));
 
         // Create explorer service
@@ -116,7 +119,19 @@ impl Explorerd {
         // Initialize the explorer service
         service.init().await?;
 
-        Ok(Self { rpc_connections: Mutex::new(HashSet::new()), rpc_client, service })
+        Ok(Self {
+            service,
+            rpc_connections: Mutex::new(HashSet::new()),
+            darkfid_client,
+            darkfid_endpoint: endpoint,
+            executor: ex,
+        })
+    }
+
+    /// Establishes a connection to the configured darkfid endpoint, returning a successful
+    /// result if the connection is successful, or an error otherwise.
+    async fn connect(&self) -> Result<()> {
+        self.darkfid_client.connect(self.darkfid_endpoint.clone(), self.executor.clone()).await
     }
 }
 
@@ -155,6 +170,9 @@ async fn realmain(args: Args, ex: Arc<smol::Executor<'static>>) -> Result<()> {
     );
     info!(target: "explorerd", "Started JSON-RPC server: {}", config.rpc.rpc_listen.to_string().trim_end_matches("/"));
 
+    // Connect the darkfid client
+    explorer.connect().await?;
+
     // Sync blocks
     info!(target: "explorerd", "Syncing blocks from darkfid...");
     if let Err(e) = explorer.sync_blocks(args.reset).await {
@@ -193,7 +211,7 @@ async fn realmain(args: Args, ex: Arc<smol::Executor<'static>>) -> Result<()> {
     subscriber_task.stop().await;
 
     info!(target: "explorerd", "Stopping JSON-RPC client...");
-    explorer.rpc_client.stop().await;
+    let _ = explorer.darkfid_client.stop().await;
 
     Ok(())
 }

+ 24 - 21
bin/explorer/explorerd/src/rpc/blocks.rs

@@ -37,13 +37,13 @@ use darkfi::{
 };
 use darkfi_serial::deserialize_async;
 
-use crate::{error::handle_database_error, Explorerd};
+use crate::{error::handle_database_error, rpc::DarkfidRpcClient, Explorerd};
 
-impl Explorerd {
-    // Queries darkfid for a block with given height.
-    async fn get_darkfid_block_by_height(&self, height: u32) -> Result<BlockInfo> {
+impl DarkfidRpcClient {
+    /// Retrieves a block from at a given height returning the corresponding [`BlockInfo`].
+    async fn get_block_by_height(&self, height: u32) -> Result<BlockInfo> {
         let params = self
-            .darkfid_daemon_request(
+            .request(
                 "blockchain.get_block",
                 &JsonValue::Array(vec![JsonValue::String(height.to_string())]),
             )
@@ -54,6 +54,19 @@ impl Explorerd {
         Ok(block)
     }
 
+    /// Retrieves the last confirmed block returning the block height and its header hash.
+    async fn get_last_confirmed_block(&self) -> Result<(u32, String)> {
+        let rep =
+            self.request("blockchain.last_confirmed_block", &JsonValue::Array(vec![])).await?;
+        let params = rep.get::<Vec<JsonValue>>().unwrap();
+        let height = *params[0].get::<f64>().unwrap() as u32;
+        let hash = params[1].get::<String>().unwrap().clone();
+
+        Ok((height, hash))
+    }
+}
+
+impl Explorerd {
     /// Synchronizes blocks between the explorer and a Darkfi blockchain node, ensuring
     /// the database remains consistent by syncing any missing or outdated blocks.
     ///
@@ -75,7 +88,8 @@ impl Explorerd {
         })?;
 
         // Grab the last confirmed block height and hash from the darkfi node
-        let (last_darkfid_height, last_darkfid_hash) = self.get_last_confirmed_block().await?;
+        let (last_darkfid_height, last_darkfid_hash) =
+            self.darkfid_client.get_last_confirmed_block().await?;
 
         // Initialize the current height to sync from, starting from genesis block if last sync block does not exist
         let (last_synced_height, last_synced_hash) = last_synced_block
@@ -130,7 +144,7 @@ impl Explorerd {
             let block_sync_start = Instant::now();
 
             // Retrieve the block from darkfi node by height
-            let block = match self.get_darkfid_block_by_height(current_height).await {
+            let block = match self.darkfid_client.get_block_by_height(current_height).await {
                 Ok(r) => r,
                 Err(e) => {
                     return Err(handle_database_error(
@@ -206,7 +220,7 @@ impl Explorerd {
             // Check if we found a synced block for current height being searched
             if let Some(synced_block) = synced_block {
                 // Fetch the block from darkfi node to check for a match
-                match self.get_darkfid_block_by_height(cur_height).await {
+                match self.darkfid_client.get_block_by_height(cur_height).await {
                     Ok(darkfid_block) => {
                         // If hashes match, we've found the point of divergence
                         if synced_block.header_hash == darkfid_block.hash().to_string() {
@@ -387,18 +401,6 @@ impl Explorerd {
             }
         }
     }
-
-    // Queries darkfid for last confirmed block.
-    async fn get_last_confirmed_block(&self) -> Result<(u32, String)> {
-        let rep = self
-            .darkfid_daemon_request("blockchain.last_confirmed_block", &JsonValue::Array(vec![]))
-            .await?;
-        let params = rep.get::<Vec<JsonValue>>().unwrap();
-        let height = *params[0].get::<f64>().unwrap() as u32;
-        let hash = params[1].get::<String>().unwrap().clone();
-
-        Ok((height, hash))
-    }
 }
 
 /// Subscribes to darkfid's JSON-RPC notification endpoint that serves
@@ -409,7 +411,8 @@ pub async fn subscribe_blocks(
     ex: Arc<smol::Executor<'static>>,
 ) -> Result<(StoppableTaskPtr, StoppableTaskPtr)> {
     // Grab last confirmed block
-    let (last_darkfid_height, last_darkfid_hash) = explorer.get_last_confirmed_block().await?;
+    let (last_darkfid_height, last_darkfid_hash) =
+        explorer.darkfid_client.get_last_confirmed_block().await?;
 
     // Grab last synced block
     let (mut height, hash) = match explorer.service.last_block() {

+ 98 - 21
bin/explorer/explorerd/src/rpc/mod.rs

@@ -16,20 +16,22 @@
  * along with this program.  If not, see <https://www.gnu.org/licenses/>.
  */
 
-use std::{collections::HashSet, time::Instant};
+use std::{collections::HashSet, sync::Arc, time::Instant};
 
 use async_trait::async_trait;
-use log::{debug, error, trace};
-use smol::lock::MutexGuard;
+use log::{debug, error, trace, warn};
+use smol::lock::{MutexGuard, RwLock};
 use tinyjson::JsonValue;
+use url::Url;
 
 use darkfi::{
     rpc::{
+        client::RpcClient,
         jsonrpc::{ErrorCode, JsonError, JsonRequest, JsonResponse, JsonResult},
         server::RequestHandler,
     },
     system::StoppableTaskPtr,
-    Result,
+    Error, Result,
 };
 
 use crate::{
@@ -117,6 +119,97 @@ impl RequestHandler<()> for Explorerd {
     }
 }
 
+/// A RPC client for interacting with a Darkfid JSON-RPC endpoint, enabling communication with Darkfid blockchain nodes.
+/// Supports connection management, request handling, and graceful shutdowns.
+/// Implemented for shared access across ownership boundaries using `Arc`, with connection state managed via an `RwLock`.
+pub struct DarkfidRpcClient {
+    /// JSON-RPC client used to communicate with the Darkfid daemon. A value of `None` indicates no active connection.
+    /// The `RwLock` allows the client to be shared across ownership boundaries while managing the connection state.
+    rpc_client: RwLock<Option<RpcClient>>,
+}
+
+impl DarkfidRpcClient {
+    /// Creates a new client with an inactive connection.
+    pub fn new() -> Self {
+        Self { rpc_client: RwLock::new(None) }
+    }
+
+    /// Checks if there is an active connection to Darkfid.
+    pub async fn connected(&self) -> Result<bool> {
+        Ok(self.rpc_client.read().await.is_some())
+    }
+
+    /// Establishes a connection to the Darkfid node, storing the resulting client if successful.
+    /// If already connected, logs a message and returns without connecting again.
+    pub async fn connect(&self, endpoint: Url, ex: Arc<smol::Executor<'static>>) -> Result<()> {
+        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.");
+            return Ok(());
+        }
+
+        *rpc_client_guard = Some(RpcClient::new(endpoint, ex).await?);
+        Ok(())
+    }
+
+    /// Closes the connection with the connected darkfid, returning if there is no active connection.
+    /// If the connection is stopped, sets `rpc_client` to `None`.
+    pub async fn stop(&self) -> Result<()> {
+        let mut rpc_client_guard = self.rpc_client.write().await;
+
+        // If there's an active connection, stop it and clear the reference
+        if let Some(ref rpc_client) = *rpc_client_guard {
+            rpc_client.stop().await;
+            *rpc_client_guard = None;
+            return Ok(());
+        }
+
+        // If there's no connection, log the message and do nothing
+        warn!(target: "explorerd::rpc::stop", "Not connected to darkfid, nothing to stop.");
+        Ok(())
+    }
+
+    /// Sends a request to the client's Darkfid JSON-RPC endpoint using the given method and parameters.
+    /// Returns the received response or an error if no active connection to Darkfid exists.
+    pub async fn request(&self, method: &str, params: &JsonValue) -> Result<JsonValue> {
+        let rpc_client_guard = self.rpc_client.read().await;
+
+        if let Some(ref rpc_client) = *rpc_client_guard {
+            debug!(target: "explorerd::rpc::request", "Executing request {} with params: {:?}", method, params);
+            let latency = Instant::now();
+            let req = JsonRequest::new(method, params.clone());
+            let rep = rpc_client.request(req).await?;
+            let latency = latency.elapsed();
+            trace!(target: "explorerd::rpc::request", "Got reply: {:?}", rep);
+            debug!(target: "explorerd::rpc::request", "Latency: {:?}", latency);
+            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(),
+        ))
+    }
+
+    /// 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);
+        }
+
+        Ok(true)
+    }
+}
+
+impl Default for DarkfidRpcClient {
+    fn default() -> Self {
+        Self::new()
+    }
+}
+
 impl Explorerd {
     // RPCAPI:
     // Pings configured darkfid daemon for liveness.
@@ -126,26 +219,10 @@ impl Explorerd {
     // <-- {"jsonrpc": "2.0", "result": "true", "id": 1}
     async fn ping_darkfid(&self, id: u16, _params: JsonValue) -> JsonResult {
         debug!(target: "explorerd::rpc::ping_darkfid", "Pinging darkfid daemon...");
-        if let Err(e) = self.darkfid_daemon_request("ping", &JsonValue::Array(vec![])).await {
+        if let Err(e) = self.darkfid_client.ping().await {
             error!(target: "explorerd::rpc::ping_darkfid", "Failed to ping darkfid daemon: {}", e);
             return server_error(RpcError::PingFailed, id, None)
         }
         JsonResponse::new(JsonValue::Boolean(true), id).into()
     }
-
-    /// Auxiliary function to execute a request towards the configured darkfid daemon JSON-RPC endpoint.
-    pub async fn darkfid_daemon_request(
-        &self,
-        method: &str,
-        params: &JsonValue,
-    ) -> Result<JsonValue> {
-        debug!(target: "explorerd::rpc::darkfid_daemon_request", "Executing request {} with params: {:?}", method, params);
-        let latency = Instant::now();
-        let req = JsonRequest::new(method, params.clone());
-        let rep = self.rpc_client.request(req).await?;
-        let latency = latency.elapsed();
-        trace!(target: "explorerd::rpc::darkfid_daemon_request", "Got reply: {:?}", rep);
-        debug!(target: "explorerd::rpc::darkfid_daemon_request", "Latency: {:?}", latency);
-        Ok(rep)
-    }
 }