explorerd/
main.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, path::Path, sync::Arc};
20
21use log::{error, info};
22use smol::{lock::Mutex, stream::StreamExt};
23use structopt_toml::{serde::Deserialize, structopt::StructOpt, StructOptToml};
24use url::Url;
25
26use darkfi::{
27    async_daemonize, cli_desc,
28    rpc::server::{listen_and_serve, RequestHandler},
29    system::{StoppableTask, StoppableTaskPtr},
30    util::path::get_config_path,
31    Error, Result,
32};
33
34use crate::{
35    config::ExplorerNetworkConfig,
36    rpc::DarkfidRpcClient,
37    service::{sync::subscribe_sync_blocks, ExplorerService},
38};
39
40/// Configuration management across multiple networks (localnet, testnet, mainnet)
41mod config;
42
43/// Manages JSON-RPC interactions for the explorer
44mod rpc;
45
46/// Core logic for block synchronization, chain data access, metadata storage/retrieval,
47/// and statistics computation
48mod service;
49
50/// Manages persistent storage for blockchain, contracts, metrics, and metadata
51mod store;
52
53/// Crate errors
54mod error;
55
56/// Test utilities used for unit and integration testing
57#[cfg(test)]
58mod test_utils;
59
60const CONFIG_FILE: &str = "explorerd_config.toml";
61const CONFIG_FILE_CONTENTS: &str = include_str!("../explorerd_config.toml");
62
63#[derive(Clone, Debug, Deserialize, StructOpt, StructOptToml)]
64#[serde(default)]
65#[structopt(name = "explorerd", about = cli_desc!())]
66struct Args {
67    #[structopt(short, long)]
68    /// Configuration file to use
69    config: Option<String>,
70
71    #[structopt(short, long, default_value = "testnet")]
72    /// Explorer network (localnet, testnet, mainnet)
73    network: String,
74
75    #[structopt(long)]
76    /// Reset the database and start syncing from first block
77    reset: bool,
78
79    #[structopt(short, long)]
80    /// Set log file to output to
81    log: Option<String>,
82
83    #[structopt(short, parse(from_occurrences))]
84    /// Increase verbosity (-vvv supported)
85    verbose: u8,
86
87    #[structopt(short, long)]
88    /// Disable synchronization and connections to `darkfid`, operating solely
89    /// on the local explorer database without attempting to connect or sync.
90    /// If not specified, the application will attempt to connect and sync by default.
91    no_sync: bool,
92}
93
94/// Defines a daemon structure responsible for handling incoming JSON-RPC requests and delegating them
95/// to the backend layer for processing. It provides a JSON-RPC interface for managing operations related to
96/// blocks, transactions, contracts, and metrics.
97///
98/// Upon startup, the daemon initializes a background task to handle incoming JSON-RPC requests.
99/// This includes processing operations related to blocks, transactions, contracts, and metrics by
100/// delegating them to the backend and returning appropriate RPC responses. Additionally, the daemon
101/// synchronizes blocks from the `darkfid` daemon into the explorer database and subscribes
102/// to new blocks, ensuring that the local database remains updated in real-time.
103pub struct Explorerd {
104    /// Explorer service instance
105    pub service: ExplorerService,
106    /// JSON-RPC connection tracker
107    pub rpc_connections: Mutex<HashSet<StoppableTaskPtr>>,
108    /// JSON-RPC client to execute requests to darkfid daemon
109    pub darkfid_client: Arc<DarkfidRpcClient>,
110    /// Darkfi blockchain node endpoint to sync with when not in no-sync mode
111    darkfid_endpoint: Url,
112    /// A asynchronous executor used to create an RPC client when not in no-sync mode
113    executor: Arc<smol::Executor<'static>>,
114}
115
116impl Explorerd {
117    /// Creates a new `BlockchainExplorer` instance.
118    async fn new(
119        db_path: String,
120        darkfid_endpoint: Url,
121        ex: Arc<smol::Executor<'static>>,
122    ) -> Result<Self> {
123        // Initialize darkfid rpc client
124        let darkfid_client = Arc::new(DarkfidRpcClient::new());
125
126        // Create explorer service
127        let service = ExplorerService::new(db_path, darkfid_client.clone())?;
128
129        // Initialize the explorer service
130        service.init().await?;
131
132        Ok(Self {
133            service,
134            rpc_connections: Mutex::new(HashSet::new()),
135            darkfid_client,
136            darkfid_endpoint,
137            executor: ex,
138        })
139    }
140
141    /// Establishes a connection to the configured darkfid endpoint, returning a successful
142    /// result if the connection is successful, or an error otherwise.
143    async fn connect(&self) -> Result<()> {
144        self.darkfid_client.connect(self.darkfid_endpoint.clone(), self.executor.clone()).await
145    }
146}
147
148async_daemonize!(realmain);
149async fn realmain(args: Args, ex: Arc<smol::Executor<'static>>) -> Result<()> {
150    info!(target: "explorerd", "Initializing DarkFi blockchain explorer node...");
151
152    // Resolve the configuration path
153    let config_path = get_config_path(args.config.clone(), CONFIG_FILE)?;
154
155    // Get explorer network configuration
156    let config: ExplorerNetworkConfig = (&config_path, &args.network).try_into()?;
157
158    // Initialize the explorer daemon instance
159    let explorer =
160        Explorerd::new(config.database.clone(), config.endpoint.clone(), ex.clone()).await?;
161    let explorer = Arc::new(explorer);
162    info!(target: "explorerd", "Node initialized successfully!");
163
164    // JSON-RPC server
165    // Here we create a task variable so we can manually close the task later.
166    let rpc_task = StoppableTask::new();
167    let explorer_ = explorer.clone();
168    rpc_task.clone().start(
169        listen_and_serve(config.rpc.clone().into(), explorer.clone(), None, ex.clone()),
170        |res| async move {
171            match res {
172                Ok(()) | Err(Error::RpcServerStopped) => explorer_.stop_connections().await,
173                Err(e) => {
174                    error!(target: "explorerd", "Failed starting sync JSON-RPC server: {}", e)
175                }
176            }
177        },
178        Error::RpcServerStopped,
179        ex.clone(),
180    );
181    info!(target: "explorerd", "Started JSON-RPC server: {}", config.rpc.rpc_listen.to_string().trim_end_matches("/"));
182
183    // Declare task variables optional in case we are in no-sync mode
184    let mut subscriber_task = None;
185    let mut listener_task = None;
186
187    // Do not sync when in no-sync mode
188    if !args.no_sync {
189        explorer.connect().await?;
190
191        // Sync blocks
192        info!(target: "explorerd", "Syncing blocks from darkfid...");
193        if let Err(e) = explorer.service.sync_blocks(args.reset).await {
194            let error_message = format!("Error syncing blocks: {:?}", e);
195            error!(target: "explorerd", "{error_message}");
196            return Err(Error::DatabaseError(error_message));
197        }
198
199        // Subscribe blocks
200        info!(target: "explorerd", "Subscribing to new blocks...");
201        match subscribe_sync_blocks(explorer.clone(), config.endpoint.clone(), ex.clone()).await {
202            Ok((sub_task, lst_task)) => {
203                subscriber_task = Some(sub_task);
204                listener_task = Some(lst_task);
205            }
206            Err(e) => {
207                let error_message = format!("Error setting up blocks subscriber: {:?}", e);
208                error!(target: "explorerd", "{error_message}");
209                return Err(Error::DatabaseError(error_message));
210            }
211        };
212    }
213
214    log_started_banner(explorer.clone(), &config, &args, &config_path, args.no_sync);
215    info!(target: "explorerd::", "All is good. Waiting for block notifications...");
216
217    // Signal handling for graceful termination.
218    let (signals_handler, signals_task) = SignalHandler::new(ex)?;
219    signals_handler.wait_termination(signals_task).await?;
220    info!(target: "explorerd", "Caught termination signal, cleaning up and exiting...");
221
222    info!(target: "explorerd", "Stopping JSON-RPC server...");
223    rpc_task.stop().await;
224
225    // Stop darkfid listener task if it exists
226    if let Some(task) = listener_task {
227        info!(target: "explorerd", "Stopping darkfid listener...");
228        task.stop().await;
229    }
230
231    // Stop darkfid subscribe task if it exists
232    if let Some(task) = subscriber_task {
233        info!(target: "explorerd", "Stopping darkfid subscriber...");
234        task.stop().await;
235    }
236
237    info!(target: "explorerd", "Stopping JSON-RPC client...");
238    let _ = explorer.darkfid_client.stop().await;
239
240    Ok(())
241}
242
243/// Logs a banner displaying the startup details of the DarkFi Explorer Node.
244fn log_started_banner(
245    explorer: Arc<Explorerd>,
246    config: &ExplorerNetworkConfig,
247    args: &Args,
248    config_path: &Path,
249    no_sync: bool,
250) {
251    // Generate the `connected_node` string based on sync mode
252    let connected_node = if no_sync {
253        "Not connected".to_string()
254    } else {
255        config.endpoint.to_string().trim_end_matches('/').to_string()
256    };
257
258    // Log the banner
259    info!(target: "explorerd", "========================================================================================");
260    info!(target: "explorerd", "                   Started DarkFi Explorer Node{}                                        ",
261        if no_sync { " (No-Sync Mode)" } else { "" });
262    info!(target: "explorerd", "========================================================================================");
263    info!(target: "explorerd", "  - Network: {}", args.network);
264    info!(target: "explorerd", "  - JSON-RPC Endpoint: {}", config.rpc.rpc_listen.to_string().trim_end_matches('/'));
265    info!(target: "explorerd", "  - Database: {}", config.database);
266    info!(target: "explorerd", "  - Configuration: {}", config_path.to_str().unwrap_or("Error: configuration path not found!"));
267    info!(target: "explorerd", "  - Reset Blocks: {}", if args.reset { "Yes" } else { "No" });
268    info!(target: "explorerd", "~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~");
269    info!(target: "explorerd", "  - Synced Blocks: {}", explorer.service.db.blockchain.len());
270    info!(target: "explorerd", "  - Synced Transactions: {}", explorer.service.db.blockchain.len());
271    info!(target: "explorerd", "  - Connected Darkfi Node: {}", connected_node);
272    info!(target: "explorerd", "========================================================================================");
273}