Просмотр исходного кода

explorer: transition from SQL to sled-based implementation

This commit request introduces a sled-backed Block Explorer, replacing the SQL-based implementation.

Summary of updates:
- Transitioned the block explorer from SQL to a sled-based database implementation
- Introduced BlockExplorerDb struct that handles explorer database operations
- Established sled-based darkfid block synchronization for startup and subscription sync
- Removed rusqlite dependency
- Removed SQL create table scripts
- Updated app.py to handle not found as empty results instead of a thrown exception when calling rpc.get_block_transactions
kalm 1 год назад
Родитель
Сommit
6a8743b667

+ 1 - 1
script/research/blockchain-explorer/Cargo.toml

@@ -18,7 +18,7 @@ drk = {path = "../../../bin/drk"}
 
 # Misc
 log = "0.4.22"
-rusqlite = {version = "0.32.1", features = ["sqlcipher"]}
+sled-overlay = {version = "0.1.4"}
 
 # JSON-RPC
 async-trait = "0.1.83"

+ 0 - 21
script/research/blockchain-explorer/blocks.sql

@@ -1,21 +0,0 @@
--- Database blocks table definition.
--- We store data in a usable format.
-CREATE TABLE IF NOT EXISTS blocks (
-    -- Header hash identifier of the block
-    header_hash TEXT PRIMARY KEY NOT NULL,
-    -- Block version
-    version INTEGER NOT NULL,
-    -- Previous block hash
-    previous TEXT NOT NULL,
-    -- Block height
-    height INTEGER NOT NULL,
-    -- Block creation timestamp
-    timestamp INTEGER NOT NULL,
-    -- The block's nonce. This value changes arbitrarily with mining.
-    nonce INTEGER NOT NULL,
-    -- Merkle tree root of the transactions hashes contained in this block
-    root TEXT NOT NULL,
-    -- Block producer signature
-    signature BLOB NOT NULL
-);
-

+ 4 - 4
script/research/blockchain-explorer/site/app.py

@@ -31,11 +31,11 @@ async def index():
 @app.route('/search', methods=['GET', 'POST'])
 async def search():
     search_hash = request.args.get('search_hash', '')
-    try:
-        block = await rpc.get_block(search_hash)
-        transactions = await rpc.get_block_transactions(search_hash)
+    block = await rpc.get_block(search_hash)
+    transactions = await rpc.get_block_transactions(search_hash)
+    if transactions:
         return render_template('block.html', block=block, transactions=transactions)
-    except Exception:
+    else:
         transaction = await rpc.get_transaction(search_hash)
         return render_template('transaction.html', transaction=transaction)
 

+ 85 - 283
script/research/blockchain-explorer/src/blocks.rs

@@ -16,36 +16,22 @@
  * along with this program.  If not, see <https://www.gnu.org/licenses/>.
  */
 
-use log::info;
-use rusqlite::types::Value;
+use log::{debug, info};
 use tinyjson::JsonValue;
 
-use darkfi::{blockchain::BlockInfo, Error, Result};
-use darkfi_sdk::crypto::schnorr::Signature;
-use darkfi_serial::{deserialize, serialize};
-use drk::{
-    convert_named_params,
-    error::{WalletDbError, WalletDbResult},
+use darkfi::{
+    blockchain::{
+        BlockInfo, BlockchainOverlay, HeaderHash, SLED_BLOCK_DIFFICULTY_TREE,
+        SLED_BLOCK_ORDER_TREE, SLED_BLOCK_TREE,
+    },
+    Error, Result,
 };
+use darkfi_sdk::crypto::schnorr::Signature;
 
-use crate::BlockchainExplorer;
-
-// Database SQL table constant names. These have to represent the `blocks.sql`
-// SQL schema.
-pub const BLOCKS_TABLE: &str = "blocks";
-
-// BLOCKS_TABLE
-pub const BLOCKS_COL_HEADER_HASH: &str = "header_hash";
-pub const BLOCKS_COL_VERSION: &str = "version";
-pub const BLOCKS_COL_PREVIOUS: &str = "previous";
-pub const BLOCKS_COL_HEIGHT: &str = "height";
-pub const BLOCKS_COL_TIMESTAMP: &str = "timestamp";
-pub const BLOCKS_COL_NONCE: &str = "nonce";
-pub const BLOCKS_COL_ROOT: &str = "root";
-pub const BLOCKS_COL_SIGNATURE: &str = "signature";
+use crate::ExplorerDb;
 
 #[derive(Debug, Clone)]
-/// Structure representing a `BLOCKS_TABLE` record.
+/// Structure representing a block record.
 pub struct BlockRecord {
     /// Header hash identifier of the block
     pub header_hash: String,
@@ -96,148 +82,63 @@ impl From<&BlockInfo> for BlockRecord {
     }
 }
 
-impl BlockchainExplorer {
-    /// Initialize database with blocks tables.
-    pub async fn initialize_blocks(&self) -> WalletDbResult<()> {
-        // Initialize blocks database schema
-        let database_schema = include_str!("../blocks.sql");
-        self.database.exec_batch_sql(database_schema)?;
+impl ExplorerDb {
+    /// Resets blocks in the database by clearing all block related trees, returning an Ok result on success.
+    pub fn reset_blocks(&self) -> Result<()> {
+        let db = &self.blockchain.sled_db;
+        // Initialize block related trees to reset
+        let trees_to_reset = [SLED_BLOCK_TREE, SLED_BLOCK_ORDER_TREE, SLED_BLOCK_DIFFICULTY_TREE];
+
+        // Iterate over each tree and remove its entries
+        for tree_name in &trees_to_reset {
+            let tree = db.open_tree(tree_name)?;
+            tree.clear()?;
+            let tree_name_str = std::str::from_utf8(tree_name)?;
+            info!(target: "blockchain-explorer::blocks", "Successfully reset block tree: {tree_name_str}");
+        }
 
         Ok(())
     }
 
-    /// Reset blocks table in the database.
-    pub fn reset_blocks(&self) -> WalletDbResult<()> {
-        info!(target: "blockchain-explorer::blocks::reset_blocks", "Resetting blocks...");
-        let query = format!("DELETE FROM {};", BLOCKS_TABLE);
-        self.database.exec_sql(&query, &[])
-    }
-
-    /// Import given block into the database.
-    pub async fn put_block(&self, block: &BlockRecord) -> Result<()> {
-        let query = format!(
-            "INSERT OR REPLACE INTO {} ({}, {}, {}, {}, {}, {}, {}, {}) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8);",
-            BLOCKS_TABLE,
-            BLOCKS_COL_HEADER_HASH,
-            BLOCKS_COL_VERSION,
-            BLOCKS_COL_PREVIOUS,
-            BLOCKS_COL_HEIGHT,
-            BLOCKS_COL_TIMESTAMP,
-            BLOCKS_COL_NONCE,
-            BLOCKS_COL_ROOT,
-            BLOCKS_COL_SIGNATURE
-        );
-
-        if let Err(e) = self.database.exec_sql(
-            &query,
-            rusqlite::params![
-                block.header_hash,
-                block.version,
-                block.previous,
-                block.height,
-                block.timestamp,
-                block.nonce,
-                block.root,
-                serialize(&block.signature),
-            ],
-        ) {
-            return Err(Error::DatabaseError(format!("[put_block] Block insert failed: {e:?}")))
-        };
-
+    /// Adds a block to the block explorer database.
+    pub async fn put_block(&self, block: &BlockInfo) -> Result<()> {
+        let blockchain_overlay = BlockchainOverlay::new(&self.blockchain)?;
+        // Add the synced block and commit the changes
+        let _ = blockchain_overlay.lock().unwrap().add_block(block)?;
+        blockchain_overlay.lock().unwrap().overlay.lock().unwrap().apply()?;
+        debug!(target:"blockchain_explorer::blocks::put_block", "Added block {:?}", block);
         Ok(())
     }
 
-    /// Auxiliary function to parse a `BLOCKS_TABLE` record.
-    fn parse_block_record(&self, row: &[Value]) -> Result<BlockRecord> {
-        let Value::Text(ref header_hash) = row[0] else {
-            return Err(Error::ParseFailed("[parse_block_record] Header hash parsing failed"))
-        };
-        let header_hash = header_hash.clone();
-
-        let Value::Integer(version) = row[1] else {
-            return Err(Error::ParseFailed("[parse_block_record] Version parsing failed"))
-        };
-        let Ok(version) = u8::try_from(version) else {
-            return Err(Error::ParseFailed("[parse_block_record] Version parsing failed"))
-        };
-
-        let Value::Text(ref previous) = row[2] else {
-            return Err(Error::ParseFailed("[parse_block_record] Previous parsing failed"))
-        };
-        let previous = previous.clone();
-
-        let Value::Integer(height) = row[3] else {
-            return Err(Error::ParseFailed("[parse_block_record] Height parsing failed"))
-        };
-        let Ok(height) = u32::try_from(height) else {
-            return Err(Error::ParseFailed("[parse_block_record] Height parsing failed"))
-        };
-
-        let Value::Integer(timestamp) = row[4] else {
-            return Err(Error::ParseFailed("[parse_block_record] Timestamp parsing failed"))
-        };
-        let Ok(timestamp) = u64::try_from(timestamp) else {
-            return Err(Error::ParseFailed("[parse_block_record] Timestamp parsing failed"))
-        };
-
-        let Value::Integer(nonce) = row[5] else {
-            return Err(Error::ParseFailed("[parse_block_record] Nonce parsing failed"))
-        };
-        let Ok(nonce) = u64::try_from(nonce) else {
-            return Err(Error::ParseFailed("[parse_block_record] Nonce parsing failed"))
-        };
-
-        let Value::Text(ref root) = row[6] else {
-            return Err(Error::ParseFailed("[parse_block_record] Root parsing failed"))
-        };
-        let root = root.clone();
-
-        let Value::Blob(ref signature_bytes) = row[7] else {
-            return Err(Error::ParseFailed(
-                "[parse_block_record] Signature bytes bytes parsing failed",
-            ))
-        };
-        let signature = deserialize(signature_bytes)?;
-
-        Ok(BlockRecord {
-            header_hash,
-            version,
-            previous,
-            height,
-            timestamp,
-            nonce,
-            root,
-            signature,
-        })
+    /// Provides the total block count.
+    pub fn get_block_count(&self) -> usize {
+        self.blockchain.len()
     }
 
     /// Fetch all known blocks from the database.
     pub fn get_blocks(&self) -> Result<Vec<BlockRecord>> {
-        let rows = match self.database.query_multiple(BLOCKS_TABLE, &[], &[]) {
-            Ok(r) => r,
-            Err(e) => {
-                return Err(Error::DatabaseError(format!(
-                    "[get_blocks] Blocks retrieval failed: {e:?}"
-                )))
-            }
-        };
+        // Fetch blocks and handle any errors encountered
+        let blocks = &self.blockchain.get_all().map_err(|e| {
+            Error::DatabaseError(format!("[get_blocks] Block retrieval failed: {e:?}"))
+        })?;
 
-        let mut blocks = Vec::with_capacity(rows.len());
-        for row in rows {
-            blocks.push(self.parse_block_record(&row)?);
-        }
+        // Transform the found blocks into a vector of block records
+        let block_records: Vec<BlockRecord> = blocks.iter().map(BlockRecord::from).collect();
 
-        Ok(blocks)
+        Ok(block_records)
     }
 
-    /// Fetch a block given its header hash.
-    pub fn get_block_by_hash(&self, header_hash: &str) -> Result<BlockRecord> {
-        let row = match self.database.query_single(
-            BLOCKS_TABLE,
-            &[],
-            convert_named_params! {(BLOCKS_COL_HEADER_HASH, header_hash)},
-        ) {
-            Ok(r) => r,
+    /// Fetch a block given its header hash from the database.
+    pub fn get_block_by_hash(&self, header_hash: &str) -> Result<Option<BlockRecord>> {
+        // Parse header hash, returning an error if parsing fails
+        let header_hash = header_hash
+            .parse::<HeaderHash>()
+            .map_err(|_| Error::ParseFailed("[get_block_by_hash] Invalid header hash"))?;
+
+        // Fetch all blocks by hash and handle encountered errors
+        let blocks = match self.blockchain.get_blocks_by_hash(&[header_hash]) {
+            Ok(blocks) => blocks,
+            Err(Error::BlockNotFound(_)) => return Ok(None),
             Err(e) => {
                 return Err(Error::DatabaseError(format!(
                     "[get_block_by_hash] Block retrieval failed: {e:?}"
@@ -245,152 +146,53 @@ impl BlockchainExplorer {
             }
         };
 
-        self.parse_block_record(&row)
-    }
-
-    /// Fetch last block height from the database.
-    pub async fn last_block(&self) -> WalletDbResult<(u32, String)> {
-        // First we prepare the query
-        let query = format!(
-            "SELECT {}, {} FROM {} ORDER BY {} DESC LIMIT 1;",
-            BLOCKS_COL_HEADER_HASH, BLOCKS_COL_HEIGHT, BLOCKS_TABLE, BLOCKS_COL_HEIGHT
-        );
-        let Ok(conn) = self.database.conn.lock() else {
-            return Err(WalletDbError::FailedToAquireLock)
-        };
-        let Ok(mut stmt) = conn.prepare(&query) else {
-            return Err(WalletDbError::QueryPreparationFailed)
-        };
-
-        // Execute the query using provided params
-        let Ok(mut rows) = stmt.query([]) else { return Err(WalletDbError::QueryExecutionFailed) };
-
-        // Check if row exists
-        let Ok(next) = rows.next() else { return Err(WalletDbError::QueryExecutionFailed) };
-        let row = match next {
-            Some(row_result) => row_result,
-            None => return Ok((0_u32, "".to_string())),
-        };
-
-        // Parse returned values
-        let Ok(value) = row.get(0) else { return Err(WalletDbError::ParseColumnValueError) };
-        let Value::Text(ref header_hash) = value else {
-            return Err(WalletDbError::ParseColumnValueError)
-        };
-        let header_hash = header_hash.clone();
+        // Transform found block to a BlockRecord
+        let block = Some(BlockRecord::from(&blocks[0]));
 
-        let Ok(value) = row.get(1) else { return Err(WalletDbError::ParseColumnValueError) };
-        let Value::Integer(height) = value else {
-            return Err(WalletDbError::ParseColumnValueError)
-        };
-        let Ok(height) = u32::try_from(height) else {
-            return Err(WalletDbError::ParseColumnValueError)
-        };
-
-        Ok((height, header_hash))
+        Ok(block)
     }
 
-    /// Auxiliary function to parse a `BLOCKS_TABLE` query rows into block records.
-    fn parse_blocks_query_rows(&self, rows: &mut rusqlite::Rows) -> Result<Vec<BlockRecord>> {
-        // Loop over returned rows and parse them
-        let mut records = vec![];
-        loop {
-            // Check if an error occured
-            let row = match rows.next() {
-                Ok(r) => r,
-                Err(_) => {
-                    return Err(Error::DatabaseError(format!(
-                        "[get_last_n_blocks] {}",
-                        WalletDbError::QueryExecutionFailed
-                    )))
-                }
-            };
-
-            // Check if no row was returned
-            let row = match row {
-                Some(r) => r,
-                None => break,
-            };
+    /// Fetch the last block from the database.
+    pub fn last_block(&self) -> Result<Option<(u32, String)>> {
+        let block_store = &self.blockchain.blocks;
 
-            // Grab row returned values
-            let mut row_values = vec![];
-            let mut idx = 0;
-            loop {
-                let Ok(value) = row.get(idx) else { break };
-                row_values.push(value);
-                idx += 1;
-            }
-            records.push(row_values);
+        // Return None result when no blocks exist
+        if block_store.is_empty() {
+            return Ok(None);
         }
 
-        // Parse the records into blocks
-        let mut blocks = Vec::with_capacity(records.len());
-        for record in records {
-            blocks.push(self.parse_block_record(&record)?);
-        }
+        // Blocks exist, retrieve last block
+        let (height, header_hash) = block_store.get_last().map_err(|e| {
+            Error::DatabaseError(format!("[last_block] Block retrieval failed: {e:?}"))
+        })?;
 
-        Ok(blocks)
+        // Convert header hash to a string and return result
+        Ok(Some((height, header_hash.to_string())))
     }
 
-    /// Fetch last N blocks from the database.
-    pub fn get_last_n_blocks(&self, n: u16) -> Result<Vec<BlockRecord>> {
-        // First we prepare the query
-        let query = format!(
-            "SELECT * FROM {} ORDER BY {} DESC LIMIT {};",
-            BLOCKS_TABLE, BLOCKS_COL_HEIGHT, n
-        );
-        let Ok(conn) = self.database.conn.lock() else {
-            return Err(Error::DatabaseError(format!(
-                "[get_last_n_blocks] {}",
-                WalletDbError::FailedToAquireLock
-            )))
-        };
-        let Ok(mut stmt) = conn.prepare(&query) else {
-            return Err(Error::DatabaseError(format!(
-                "[get_last_n_blocks] {}",
-                WalletDbError::QueryPreparationFailed
-            )))
-        };
+    /// Fetch the last N blocks from the database.
+    pub fn get_last_n(&self, n: usize) -> Result<Vec<BlockRecord>> {
+        // Fetch the last n blocks and handle any errors encountered
+        let blocks_result = &self.blockchain.get_last_n(n).map_err(|e| {
+            Error::DatabaseError(format!("[get_last_n] Block retrieval failed: {e:?}"))
+        })?;
 
-        // Execute the query using provided params
-        let Ok(mut rows) = stmt.query([]) else {
-            return Err(Error::DatabaseError(format!(
-                "[get_last_n_blocks] {}",
-                WalletDbError::QueryExecutionFailed
-            )))
-        };
+        // Transform the found blocks into a vector of block records
+        let block_records: Vec<BlockRecord> = blocks_result.iter().map(BlockRecord::from).collect();
 
-        self.parse_blocks_query_rows(&mut rows)
+        Ok(block_records)
     }
 
-    /// Fetch last N blocks from the database.
-    pub fn get_blocks_in_heights_range(&self, start: u32, end: u32) -> Result<Vec<BlockRecord>> {
-        // First we prepare the query
-        let query = format!(
-            "SELECT * FROM {} WHERE {} >= {} AND {} <= {} ORDER BY {} ASC;",
-            BLOCKS_TABLE, BLOCKS_COL_HEIGHT, start, BLOCKS_COL_HEIGHT, end, BLOCKS_COL_HEIGHT
-        );
-        let Ok(conn) = self.database.conn.lock() else {
-            return Err(Error::DatabaseError(format!(
-                "[get_blocks_in_height_range] {}",
-                WalletDbError::FailedToAquireLock
-            )))
-        };
-        let Ok(mut stmt) = conn.prepare(&query) else {
-            return Err(Error::DatabaseError(format!(
-                "[get_blocks_in_height_range] {}",
-                WalletDbError::QueryPreparationFailed
-            )))
-        };
+    /// Fetch blocks within a specified range from the database.
+    pub fn get_by_range(&self, start: u32, end: u32) -> Result<Vec<BlockRecord>> {
+        // Fetch blocks in the specified range and handle any errors encountered
+        let blocks_result = &self.blockchain.get_by_range(start, end).map_err(|e| {
+            Error::DatabaseError(format!("[get_by_range]: Block retrieval failed: {e:?}"))
+        })?;
 
-        // Execute the query using provided params
-        let Ok(mut rows) = stmt.query([]) else {
-            return Err(Error::DatabaseError(format!(
-                "[get_blocks_in_height_range] {}",
-                WalletDbError::QueryExecutionFailed
-            )))
-        };
+        // Transform the found blocks into a vector of block records
+        let block_records: Vec<BlockRecord> = blocks_result.iter().map(BlockRecord::from).collect();
 
-        self.parse_blocks_query_rows(&mut rows)
+        Ok(block_records)
     }
 }

+ 54 - 101
script/research/blockchain-explorer/src/main.rs

@@ -16,20 +16,18 @@
  * along with this program.  If not, see <https://www.gnu.org/licenses/>.
  */
 
-use std::{
-    collections::HashSet,
-    fs,
-    io::{stdin, stdout, Write},
-    sync::Arc,
-};
+use std::{collections::HashSet, sync::Arc};
 
 use log::{error, info};
+use sled_overlay::sled;
 use smol::{lock::Mutex, stream::StreamExt};
 use structopt_toml::{serde::Deserialize, structopt::StructOpt, StructOptToml};
 use url::Url;
 
 use darkfi::{
-    async_daemonize, cli_desc,
+    async_daemonize,
+    blockchain::Blockchain,
+    cli_desc,
     rpc::{
         client::RpcClient,
         server::{listen_and_serve, RequestHandler},
@@ -38,7 +36,6 @@ use darkfi::{
     util::path::expand_path,
     Error, Result,
 };
-use drk::walletdb::{WalletDb, WalletPtr};
 
 /// Crate errors
 mod error;
@@ -64,7 +61,7 @@ const CONFIG_FILE_CONTENTS: &str = include_str!("../blockchain_explorer_config.t
 
 #[derive(Clone, Debug, Deserialize, StructOpt, StructOptToml)]
 #[serde(default)]
-#[structopt(name = "blockcahin-explorer", about = cli_desc!())]
+#[structopt(name = "blockchain-explorer", about = cli_desc!())]
 struct Args {
     #[structopt(short, long)]
     /// Configuration file to use
@@ -79,12 +76,7 @@ struct Args {
     db_path: String,
 
     #[structopt(long)]
-    /// Password for the daemon database.
-    /// If it's not present, daemon will prompt the user for it.
-    db_pass: Option<String>,
-
-    #[structopt(long)]
-    /// Reset the databae and start syncing from first block
+    /// Reset the database and start syncing from first block
     reset: bool,
 
     #[structopt(short, long, default_value = "tcp://127.0.0.1:8340")]
@@ -92,7 +84,7 @@ struct Args {
     endpoint: Url,
 
     #[structopt(short, long)]
-    /// Set log file to ouput into
+    /// Set log file to output into
     log: Option<String>,
 
     #[structopt(short, parse(from_occurrences))]
@@ -100,94 +92,59 @@ struct Args {
     verbose: u8,
 }
 
+/// Structure represents the explorer database backed by a sled DB connection.
+pub struct ExplorerDb {
+    /// Main pointer to the sled db connection
+    pub sled_db: sled::Db,
+    /// Explorer darkfid blockchain copy
+    pub blockchain: Blockchain,
+}
+
+impl ExplorerDb {
+    /// Creates a new `BlockExplorerDb` instance
+    pub fn new(db_path: String) -> Result<ExplorerDb> {
+        let db_path = expand_path(db_path.as_str())?;
+        let sled_db = sled::open(&db_path)?;
+        let blockchain = Blockchain::new(&sled_db)?;
+        info!(target: "blockchain-explorer", "Initialized explorer database {}, block count: {}", db_path.display(), blockchain.len());
+        Ok(ExplorerDb { sled_db, blockchain })
+    }
+}
+
 /// Daemon structure
-pub struct BlockchainExplorer {
-    /// Daemon database operations handler
-    pub database: WalletPtr,
+pub struct Explorerd {
+    /// Explorer database instance
+    pub db: ExplorerDb,
     /// JSON-RPC connection tracker
     pub rpc_connections: Mutex<HashSet<StoppableTaskPtr>>,
     /// JSON-RPC client to execute requests to darkfid daemon
     pub rpc_client: RpcClient,
 }
 
-impl BlockchainExplorer {
-    async fn new(
-        db_path: String,
-        db_pass: Option<String>,
-        endpoint: Url,
-        ex: Arc<smol::Executor<'static>>,
-    ) -> Result<Self> {
-        // Grab password
-        let db_pass = match db_pass {
-            Some(pass) => pass,
-            None => {
-                let mut pass = String::new();
-                while pass.trim().is_empty() {
-                    info!(target: "blockchain-explorer", "Provide database passsword:");
-                    stdout().flush()?;
-                    stdin().read_line(&mut pass).unwrap_or(0);
-                }
-                pass.trim().to_string()
-            }
-        };
+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?;
+        info!(target: "explorerd", "Created rpc client: {:?}", endpoint.clone());
 
-        // Script kiddies protection
-        if db_pass == "changeme" {
-            error!(target: "blockchain-explorer", "Please don't use default database password...");
-            return Err(Error::ParseFailed("Default database password usage"))
-        }
-
-        // Initialize database
-        let db_path = expand_path(&db_path)?;
-        if !db_path.exists() {
-            if let Some(parent) = db_path.parent() {
-                fs::create_dir_all(parent)?;
-            }
-        }
-        let database = match WalletDb::new(Some(db_path), Some(&db_pass)) {
-            Ok(w) => w,
-            Err(e) => {
-                let err = format!("{e:?}");
-                error!(target: "blockchain-explorer", "Error initializing database: {err}");
-                return Err(Error::DatabaseError(err))
-            }
-        };
+        // Initialize explorer database
+        let explorer_db = ExplorerDb::new(db_path)?;
 
-        // Initialize rpc client
-        let rpc_client = RpcClient::new(endpoint, ex).await?;
-
-        let explorer = Self { database, rpc_connections: Mutex::new(HashSet::new()), rpc_client };
-
-        // Initialize all the database tables
-        if let Err(e) = explorer.initialize_blocks().await {
-            let err = format!("{e:?}");
-            error!(target: "blockchain-explorer", "Error initializing blocks database table: {err}");
-            return Err(Error::DatabaseError(err))
-        }
-        if let Err(e) = explorer.initialize_transactions().await {
-            let err = format!("{e:?}");
-            error!(target: "blockchain-explorer", "Error initializing transactions database table: {err}");
-            return Err(Error::DatabaseError(err))
-        }
-        // TODO: Map deployed contracts to their corresponding files with sql table and retrieval methods
-
-        Ok(explorer)
+        Ok(Self { rpc_connections: Mutex::new(HashSet::new()), rpc_client, db: explorer_db })
     }
 }
 
 async_daemonize!(realmain);
 async fn realmain(args: Args, ex: Arc<smol::Executor<'static>>) -> Result<()> {
     info!(target: "blockchain-explorer", "Initializing DarkFi blockchain explorer node...");
-    let explorer =
-        BlockchainExplorer::new(args.db_path, args.db_pass, args.endpoint.clone(), ex.clone())
-            .await?;
+    let explorer = Explorerd::new(args.db_path, args.endpoint.clone(), ex.clone()).await?;
     let explorer = Arc::new(explorer);
     info!(target: "blockchain-explorer", "Node initialized successfully!");
 
     // JSON-RPC server
     info!(target: "blockchain-explorer", "Starting JSON-RPC server");
-    // Here we create a task variable so we can manually close the
-    // task later.
+    // Here we create a task variable so we can manually close the task later.
     let rpc_task = StoppableTask::new();
     let explorer_ = explorer.clone();
     rpc_task.clone().start(
@@ -205,26 +162,22 @@ async fn realmain(args: Args, ex: Arc<smol::Executor<'static>>) -> Result<()> {
     // Sync blocks
     info!(target: "blockchain-explorer", "Syncing blocks from darkfid...");
     if let Err(e) = explorer.sync_blocks(args.reset).await {
-        let err = format!("{e:?}");
-        error!(target: "blockchain-explorer", "Error syncing blocks: {err}");
-        return Err(Error::DatabaseError(err))
+        let error_message = format!("Error syncing blocks: {:?}", e);
+        error!(target: "blockchain-explorer", "{error_message}");
+        return Err(Error::DatabaseError(error_message));
     }
 
+    // Subscribe blocks
     info!(target: "blockchain-explorer", "Subscribing to new blocks...");
-    let (subscriber_task, listener_task) = match subscribe_blocks(
-        explorer.clone(),
-        args.endpoint,
-        ex.clone(),
-    )
-    .await
-    {
-        Ok(pair) => pair,
-        Err(e) => {
-            let err = format!("{e:?}");
-            error!(target: "blockchain-explorer", "Error while setting up blocks subscriber: {err}");
-            return Err(Error::DatabaseError(err))
-        }
-    };
+    let (subscriber_task, listener_task) =
+        match subscribe_blocks(explorer.clone(), args.endpoint, ex.clone()).await {
+            Ok(pair) => pair,
+            Err(e) => {
+                let error_message = format!("Error setting up blocks subscriber: {:?}", e);
+                error!(target: "blockchain-explorer", "{error_message}");
+                return Err(Error::DatabaseError(error_message));
+            }
+        };
 
     // Signal handling for graceful termination.
     let (signals_handler, signals_task) = SignalHandler::new(ex)?;

+ 4 - 4
script/research/blockchain-explorer/src/rpc.rs

@@ -34,11 +34,11 @@ use darkfi::{
 
 use crate::{
     error::{server_error, RpcError},
-    BlockchainExplorer,
+    Explorerd,
 };
 
 #[async_trait]
-impl RequestHandler for BlockchainExplorer {
+impl RequestHandler for Explorerd {
     async fn handle_request(&self, req: JsonRequest) -> JsonResult {
         debug!(target: "blockchain-explorer::rpc", "--> {}", req.stringify().unwrap());
 
@@ -75,7 +75,7 @@ impl RequestHandler for BlockchainExplorer {
                 self.statistics_get_basic_statistics(req.id, req.params).await
             }
 
-            // TODO: add any other usefull methods
+            // TODO: add any other useful methods
 
             // ==============
             // Invalid method
@@ -89,7 +89,7 @@ impl RequestHandler for BlockchainExplorer {
     }
 }
 
-impl BlockchainExplorer {
+impl Explorerd {
     // RPCAPI:
     // Pings configured darkfid daemon for liveness.
     // Returns `true` on success.

+ 70 - 61
script/research/blockchain-explorer/src/rpc_blocks.rs

@@ -36,11 +36,10 @@ use darkfi::{
     Error, Result,
 };
 use darkfi_serial::deserialize_async;
-use drk::error::{WalletDbError, WalletDbResult};
 
-use crate::BlockchainExplorer;
+use crate::Explorerd;
 
-impl BlockchainExplorer {
+impl Explorerd {
     // Queries darkfid for a block with given height.
     async fn get_block_by_height(&self, height: u32) -> Result<BlockInfo> {
         let params = self
@@ -57,14 +56,23 @@ impl BlockchainExplorer {
 
     /// Syncs the blockchain starting from the last synced block.
     /// If reset flag is provided, all tables are reset, and start syncing from beginning.
-    pub async fn sync_blocks(&self, reset: bool) -> WalletDbResult<()> {
+    pub async fn sync_blocks(&self, reset: bool) -> Result<()> {
         // Grab last synced block height
-        let (mut height, _) = self.last_block().await?;
+        let mut height = match self.db.last_block() {
+            Ok(Some((height, _))) => height,
+            Ok(None) => 0,
+            Err(e) => {
+                return Err(Error::DatabaseError(format!(
+                    "[sync_blocks] Retrieving last synced block failed: {:?}",
+                    e
+                )));
+            }
+        };
         // If last synced block is genesis (0) or reset flag
         // has been provided we reset, otherwise continue with
         // the next block height
         if height == 0 || reset {
-            self.reset_blocks()?;
+            self.db.reset_blocks()?;
             height = 0;
         } else {
             height += 1;
@@ -77,8 +85,9 @@ impl BlockchainExplorer {
             {
                 Ok(r) => r,
                 Err(e) => {
-                    error!(target: "blockchain-explorer::rpc_blocks::sync_blocks", "[sync_blocks] RPC client request failed: {e:?}");
-                    return Err(WalletDbError::GenericError)
+                    let error_message = format!("[sync_blocks] RPC client request failed: {:?}", e);
+                    error!(target: "blockchain-explorer::rpc_blocks::sync_blocks", "{}", error_message);
+                    return Err(Error::DatabaseError(error_message));
                 }
             };
             let last = *rep.get::<f64>().unwrap() as u32;
@@ -92,29 +101,23 @@ impl BlockchainExplorer {
             }
 
             while height <= last {
-                info!(target: "blockchain-explorer::rpc_blocks::sync_blocks", "Requesting block {height}... ");
-
                 let block = match self.get_block_by_height(height).await {
                     Ok(r) => r,
                     Err(e) => {
-                        error!(target: "blockchain-explorer::rpc_blocks::sync_blocks", "[sync_blocks] RPC client request failed: {e:?}");
-                        return Err(WalletDbError::GenericError)
+                        let error_message =
+                            format!("[sync_blocks] RPC client request failed: {:?}", e);
+                        error!(target: "blockchain-explorer::rpc_blocks::sync_blocks", "{}", error_message);
+                        return Err(Error::DatabaseError(error_message));
                     }
                 };
 
-                if let Err(e) = self.put_block(&(&block).into()).await {
-                    error!(target: "blockchain-explorer::rpc_blocks::sync_blocks", "[sync_blocks] Insert block failed: {e:?}");
-                    return Err(WalletDbError::GenericError)
+                if let Err(e) = self.db.put_block(&block).await {
+                    let error_message = format!("[sync_blocks] Put block failed: {:?}", e);
+                    error!(target: "blockchain-explorer::rpc_blocks::sync_blocks", "{}", error_message);
+                    return Err(Error::DatabaseError(error_message));
                 };
 
-                let block_hash = block.hash().to_string();
-                for transaction in block.txs {
-                    if let Err(e) = self.put_transaction(&(&block_hash, &transaction).into()).await
-                    {
-                        error!(target: "blockchain-explorer::rpc_blocks::sync_blocks", "[sync_blocks] Insert block transaction failed: {e:?}");
-                        return Err(WalletDbError::GenericError)
-                    };
-                }
+                info!(target: "blockchain-explorer::rpc_blocks::sync_blocks", "Synced block {height}");
 
                 height += 1;
             }
@@ -139,24 +142,29 @@ impl BlockchainExplorer {
             return JsonError::new(InvalidParams, None, id).into()
         }
 
-        let n = match params[0].get::<String>().unwrap().parse::<u16>() {
+        // Extract the number of last blocks to retrieve from parameters
+        let n = match params[0].get::<String>().unwrap().parse::<usize>() {
             Ok(v) => v,
             Err(_) => return JsonError::new(ParseError, None, id).into(),
         };
 
-        let blocks = match self.get_last_n_blocks(n) {
-            Ok(v) => v,
+        // Fetch the blocks and handle potential errors
+        let blocks_result = match self.db.get_last_n(n) {
+            Ok(blocks) => blocks,
             Err(e) => {
                 error!(target: "blockchain-explorer::rpc_blocks::blocks_get_last_n_blocks", "Failed fetching blocks: {}", e);
-                return JsonError::new(InternalError, None, id).into()
+                return JsonError::new(InternalError, None, id).into();
             }
         };
 
-        let mut ret = vec![];
-        for block in blocks {
-            ret.push(block.to_json_array());
+        // Transform blocks to json and return result
+        if blocks_result.is_empty() {
+            JsonResponse::new(JsonValue::Array(vec![]), id).into()
+        } else {
+            let json_blocks: Vec<JsonValue> =
+                blocks_result.into_iter().map(|block| block.to_json_array()).collect();
+            JsonResponse::new(JsonValue::Array(json_blocks), id).into()
         }
-        JsonResponse::new(JsonValue::Array(ret), id).into()
     }
 
     // RPCAPI:
@@ -196,19 +204,23 @@ impl BlockchainExplorer {
             return JsonError::new(ParseError, None, id).into()
         }
 
-        let blocks = match self.get_blocks_in_heights_range(start, end) {
-            Ok(v) => v,
+        // Fetch the blocks and handle potential errors
+        let blocks_result = match self.db.get_by_range(start, end) {
+            Ok(blocks) => blocks,
             Err(e) => {
                 error!(target: "blockchain-explorer::rpc_blocks::blocks_get_blocks_in_height_range", "Failed fetching blocks: {}", e);
-                return JsonError::new(InternalError, None, id).into()
+                return JsonError::new(InternalError, None, id).into();
             }
         };
 
-        let mut ret = vec![];
-        for block in blocks {
-            ret.push(block.to_json_array());
+        // Transform blocks to json and return result
+        if blocks_result.is_empty() {
+            JsonResponse::new(JsonValue::Array(vec![]), id).into()
+        } else {
+            let json_blocks: Vec<JsonValue> =
+                blocks_result.into_iter().map(|block| block.to_json_array()).collect();
+            JsonResponse::new(JsonValue::Array(json_blocks), id).into()
         }
-        JsonResponse::new(JsonValue::Array(ret), id).into()
     }
 
     // RPCAPI:
@@ -229,23 +241,28 @@ impl BlockchainExplorer {
             return JsonError::new(InvalidParams, None, id).into()
         }
 
-        let header_hash = params[0].get::<String>().unwrap();
-        let block = match self.get_block_by_hash(header_hash) {
-            Ok(v) => v,
-            Err(e) => {
-                error!(target: "blockchain-explorer::rpc_blocks::blocks_get_get_block_by_hash", "Failed fetching block: {}", e);
-                return JsonError::new(InternalError, None, id).into()
-            }
+        // Extract header hash from params, returning error if not provided
+        let header_hash = match params[0].get::<String>() {
+            Some(hash) => hash,
+            None => return JsonError::new(InvalidParams, None, id).into(),
         };
 
-        JsonResponse::new(block.to_json_array(), id).into()
+        // Fetch and transform block to json, handling any errors and returning the result
+        match self.db.get_block_by_hash(header_hash) {
+            Ok(Some(block)) => JsonResponse::new(block.to_json_array(), id).into(),
+            Ok(None) => JsonResponse::new(JsonValue::Array(vec![]), id).into(),
+            Err(e) => {
+                error!(target: "blockchain-explorer::rpc_blocks", "Failed fetching block: {:?}", e);
+                JsonError::new(InternalError, None, id).into()
+            }
+        }
     }
 }
 
 /// Subscribes to darkfid's JSON-RPC notification endpoint that serves
 /// new finalized blocks. Upon receiving them, store them to the database.
 pub async fn subscribe_blocks(
-    explorer: Arc<BlockchainExplorer>,
+    explorer: Arc<Explorerd>,
     endpoint: Url,
     ex: Arc<smol::Executor<'static>>,
 ) -> Result<(StoppableTaskPtr, StoppableTaskPtr)> {
@@ -253,8 +270,9 @@ pub async fn subscribe_blocks(
         .darkfid_daemon_request("blockchain.last_known_block", &JsonValue::Array(vec![]))
         .await?;
     let last_known = *rep.get::<f64>().unwrap() as u32;
-    let (last_synced, _) = match explorer.last_block().await {
-        Ok(l) => l,
+    let last_synced = match explorer.db.last_block() {
+        Ok(Some((height, _))) => height,
+        Ok(None) => 0,
         Err(e) => {
             return Err(Error::DatabaseError(format!(
                 "[subscribe_blocks] Retrieving last synced block failed: {e:?}"
@@ -340,21 +358,12 @@ pub async fn subscribe_blocks(
                             info!(target: "blockchain-explorer::rpc_blocks::subscribe_blocks", "Block header: {header_hash}");
                             info!(target: "blockchain-explorer::rpc_blocks::subscribe_blocks", "=======================================");
 
-                            info!(target: "blockchain-explorer::rpc_blocks::subscribe_blocks", "Deserialized successfully. Storring block...");
-                            if let Err(e) = explorer.put_block(&(&block_data).into()).await {
+                            info!(target: "blockchain-explorer::rpc_blocks::subscribe_blocks", "Deserialized successfully. Storing block...");
+                            if let Err(e) = explorer.db.put_block(&block_data).await {
                                 return Err(Error::DatabaseError(format!(
-                                    "[subscribe_blocks] Insert block failed: {e:?}"
+                                    "[subscribe_blocks] Put block failed: {e:?}"
                                 )))
                             }
-
-                            let block_hash = block_data.hash().to_string();
-                            for transaction in block_data.txs {
-                                if let Err(e) = explorer.put_transaction(&(&block_hash, &transaction).into()).await {
-                                    return Err(Error::DatabaseError(format!(
-                                        "[subscribe_blocks] Insert block transaction failed: {e:?}"
-                                    )))
-                                };
-                            }
                         }
                     }
 

+ 13 - 9
script/research/blockchain-explorer/src/rpc_statistics.rs

@@ -24,9 +24,9 @@ use darkfi::rpc::jsonrpc::{
     JsonError, JsonResponse, JsonResult,
 };
 
-use crate::BlockchainExplorer;
+use crate::Explorerd;
 
-impl BlockchainExplorer {
+impl Explorerd {
     // RPCAPI:
     // Queries the database to retrieve current basic statistics.
     // Returns the readable transaction upon success.
@@ -40,19 +40,23 @@ impl BlockchainExplorer {
     // --> {"jsonrpc": "2.0", "method": "statistics.get_basic_statistics", "params": [], "id": 1}
     // <-- {"jsonrpc": "2.0", "result": {...}, "id": 1}
     pub async fn statistics_get_basic_statistics(&self, id: u16, params: JsonValue) -> JsonResult {
+        // Validate to ensure parameters are empty
         let params = params.get::<Vec<JsonValue>>().unwrap();
         if !params.is_empty() {
             return JsonError::new(InvalidParams, None, id).into()
         }
 
-        let base_statistics = match self.get_base_statistics().await {
-            Ok(v) => v,
+        // Fetch base statistics and return results
+        match self.db.get_base_statistics() {
+            Ok(Some(statistics)) => JsonResponse::new(statistics.to_json_array(), id).into(),
+            Ok(None) => JsonResponse::new(JsonValue::Array(vec![]), id).into(),
             Err(e) => {
-                error!(target: "blockchain-explorer::rpc_statistics::statistics_get_basic_statistics", "Failed fetching basic statistics: {}", e);
-                return JsonError::new(InternalError, None, id).into()
+                error!(
+                    target: "blockchain-explorer::rpc_statistics::statistics_get_basic_statistics",
+                    "Failed fetching basic statistics: {}", e
+                );
+                JsonError::new(InternalError, None, id).into()
             }
-        };
-
-        JsonResponse::new(base_statistics.to_json_array(), id).into()
+        }
     }
 }

+ 17 - 10
script/research/blockchain-explorer/src/rpc_transactions.rs

@@ -23,10 +23,11 @@ use darkfi::rpc::jsonrpc::{
     ErrorCode::{InternalError, InvalidParams},
     JsonError, JsonResponse, JsonResult,
 };
+use darkfi_sdk::tx::TransactionHash;
 
-use crate::BlockchainExplorer;
+use crate::Explorerd;
 
-impl BlockchainExplorer {
+impl Explorerd {
     // RPCAPI:
     // Queries the database to retrieve the transactions corresponding to the provided block header hash.
     // Returns the readable transactions upon success.
@@ -50,7 +51,7 @@ impl BlockchainExplorer {
         }
 
         let header_hash = params[0].get::<String>().unwrap();
-        let transactions = match self.get_transactions_by_header_hash(header_hash) {
+        let transactions = match self.db.get_transactions_by_header_hash(header_hash) {
             Ok(v) => v,
             Err(e) => {
                 error!(target: "blockchain-explorer::rpc_transactions::transactions_get_transaction_by_header_hash", "Failed fetching block transactions: {}", e);
@@ -87,15 +88,21 @@ impl BlockchainExplorer {
             return JsonError::new(InvalidParams, None, id).into()
         }
 
-        let transaction_hash = params[0].get::<String>().unwrap();
-        let transaction = match self.get_transaction_by_hash(transaction_hash) {
-            Ok(v) => v,
+        // Validate provided hash and store it for later use
+        let tx_hash_str = params[0].get::<String>().unwrap();
+        let tx_hash = match tx_hash_str.parse::<TransactionHash>() {
+            Ok(hash) => hash,
+            Err(e) => return JsonError::new(InternalError, Some(e.to_string()), id).into(),
+        };
+
+        // Retrieve transaction by hash and return result
+        match self.db.get_transaction_by_hash(&tx_hash) {
+            Ok(Some(transaction)) => JsonResponse::new(transaction.to_json_array(), id).into(),
+            Ok(None) => JsonResponse::new(JsonValue::Array(vec![]), id).into(),
             Err(e) => {
                 error!(target: "blockchain-explorer::rpc_transactions::transactions_get_transaction_by_hash", "Failed fetching transaction: {}", e);
-                return JsonError::new(InternalError, None, id).into()
+                JsonError::new(InternalError, None, id).into()
             }
-        };
-
-        JsonResponse::new(transaction.to_json_array(), id).into()
+        }
     }
 }

+ 23 - 45
script/research/blockchain-explorer/src/statistics.rs

@@ -16,13 +16,12 @@
  * along with this program.  If not, see <https://www.gnu.org/licenses/>.
  */
 
-use rusqlite::types::Value;
 use tinyjson::JsonValue;
 
+use darkfi::{Error, Result};
 use darkfi_sdk::blockchain::block_epoch;
-use drk::error::{WalletDbError, WalletDbResult};
 
-use crate::{blocks::BLOCKS_TABLE, transactions::TRANSACTIONS_TABLE, BlockchainExplorer};
+use crate::ExplorerDb;
 
 #[derive(Debug, Clone)]
 /// Structure representing basic statistic extracted from the database.
@@ -34,9 +33,9 @@ pub struct BaseStatistics {
     /// Blockchains' last block hash
     pub last_block: String,
     /// Blockchain total blocks
-    pub total_blocks: u64,
+    pub total_blocks: usize,
     /// Blockchain total transactions
-    pub total_txs: u64,
+    pub total_txs: usize,
 }
 
 impl BaseStatistics {
@@ -52,45 +51,24 @@ impl BaseStatistics {
     }
 }
 
-impl BlockchainExplorer {
-    /// Fetch total rows count of given table from the database.
-    pub async fn get_table_count(&self, table: &str) -> WalletDbResult<u64> {
-        // First we prepare the query
-        let query = format!("SELECT COUNT() FROM {};", table);
-        let Ok(conn) = self.database.conn.lock() else {
-            return Err(WalletDbError::FailedToAquireLock)
-        };
-        let Ok(mut stmt) = conn.prepare(&query) else {
-            return Err(WalletDbError::QueryPreparationFailed)
-        };
-
-        // Execute the query using provided params
-        let Ok(mut rows) = stmt.query([]) else { return Err(WalletDbError::QueryExecutionFailed) };
-
-        // Check if row exists
-        let Ok(next) = rows.next() else { return Err(WalletDbError::QueryExecutionFailed) };
-        let row = match next {
-            Some(row_result) => row_result,
-            None => return Ok(0_u64),
-        };
-
-        // Parse returned value
-        let Ok(count) = row.get(0) else { return Err(WalletDbError::ParseColumnValueError) };
-        let Value::Integer(count) = count else { return Err(WalletDbError::ParseColumnValueError) };
-        let Ok(count) = u64::try_from(count) else {
-            return Err(WalletDbError::ParseColumnValueError)
-        };
-
-        Ok(count)
-    }
-
-    /// Fetch current database basic statistic.
-    pub async fn get_base_statistics(&self) -> WalletDbResult<BaseStatistics> {
-        let (height, last_block) = self.last_block().await?;
-        let epoch = block_epoch(height);
-        let total_blocks = self.get_table_count(BLOCKS_TABLE).await?;
-        let total_txs = self.get_table_count(TRANSACTIONS_TABLE).await?;
-
-        Ok(BaseStatistics { height, epoch, last_block, total_blocks, total_txs })
+impl ExplorerDb {
+    /// Fetch current database basic statistics.
+    pub fn get_base_statistics(&self) -> Result<Option<BaseStatistics>> {
+        let last_block = self.last_block();
+        Ok(last_block
+            // Throw database error if last_block retrievals fails
+            .map_err(|e| {
+                Error::DatabaseError(format!(
+                    "[get_base_statistics] Retrieving last block failed: {:?}",
+                    e
+                ))
+            })?
+            // Calculate base statistics and return result
+            .map(|(height, header_hash)| {
+                let epoch = block_epoch(height);
+                let total_blocks = self.get_block_count();
+                let total_txs = self.get_transaction_count();
+                BaseStatistics { height, epoch, last_block: header_hash, total_blocks, total_txs }
+            }))
     }
 }

+ 100 - 114
script/research/blockchain-explorer/src/transactions.rs

@@ -17,23 +17,19 @@
  */
 
 use log::info;
-use rusqlite::types::Value;
 use tinyjson::JsonValue;
 
-use darkfi::{tx::Transaction, Error, Result};
-use darkfi_serial::{deserialize, serialize};
-use drk::{convert_named_params, error::WalletDbResult};
+use darkfi::{
+    blockchain::{
+        HeaderHash, SLED_PENDING_TX_ORDER_TREE, SLED_PENDING_TX_TREE, SLED_TX_LOCATION_TREE,
+        SLED_TX_TREE,
+    },
+    tx::Transaction,
+    Error, Result,
+};
+use darkfi_sdk::tx::TransactionHash;
 
-use crate::BlockchainExplorer;
-
-// Database SQL table constant names. These have to represent the `transactions.sql`
-// SQL schema.
-pub const TRANSACTIONS_TABLE: &str = "transactions";
-
-// TRANSACTIONS_TABLE
-pub const TRANSACTIONS_COL_TRANSACTION_HASH: &str = "transaction_hash";
-pub const TRANSACTIONS_COL_HEADER_HASH: &str = "header_hash";
-pub const TRANSACTIONS_COL_PAYLOAD: &str = "payload";
+use crate::ExplorerDb;
 
 #[derive(Debug, Clone)]
 /// Structure representing a `TRANSACTIONS_TABLE` record.
@@ -68,90 +64,43 @@ impl From<(&String, &Transaction)> for TransactionRecord {
     }
 }
 
-impl BlockchainExplorer {
-    /// Initialize database with transactions tables.
-    pub async fn initialize_transactions(&self) -> WalletDbResult<()> {
-        // Initialize transactions database schema
-        let database_schema = include_str!("../transactions.sql");
-        self.database.exec_batch_sql(database_schema)?;
+impl ExplorerDb {
+    /// Resets transactions in the database by clearing transaction-related trees, returning an Ok result on success.
+    pub fn reset_transactions(&self) -> Result<()> {
+        // Initialize transaction trees to reset
+        let trees_to_reset =
+            [SLED_TX_TREE, SLED_TX_LOCATION_TREE, SLED_PENDING_TX_TREE, SLED_PENDING_TX_ORDER_TREE];
+
+        // Iterate over each associated transaction tree and delete its contents
+        for tree_name in &trees_to_reset {
+            let tree = &self.blockchain.sled_db.open_tree(tree_name)?;
+            tree.clear()?;
+            let tree_name_str = std::str::from_utf8(tree_name)?;
+            info!(target: "blockchain-explorer::blocks", "Successfully reset transaction tree: {tree_name_str}");
+        }
 
         Ok(())
     }
 
-    /// Reset transactions table in the database.
-    pub fn reset_transactions(&self) -> WalletDbResult<()> {
-        info!(target: "blockchain-explorer::transactions::reset_transactions", "Resetting transactions...");
-        let query = format!("DELETE FROM {};", TRANSACTIONS_TABLE);
-        self.database.exec_sql(&query, &[])
-    }
-
-    /// Import given transaction into the database.
-    pub async fn put_transaction(&self, transaction: &TransactionRecord) -> Result<()> {
-        let query = format!(
-            "INSERT OR REPLACE INTO {} ({}, {}, {}) VALUES (?1, ?2, ?3);",
-            TRANSACTIONS_TABLE,
-            TRANSACTIONS_COL_TRANSACTION_HASH,
-            TRANSACTIONS_COL_HEADER_HASH,
-            TRANSACTIONS_COL_PAYLOAD
-        );
-
-        if let Err(e) = self.database.exec_sql(
-            &query,
-            rusqlite::params![
-                transaction.transaction_hash,
-                transaction.header_hash,
-                serialize(&transaction.payload),
-            ],
-        ) {
-            return Err(Error::DatabaseError(format!(
-                "[put_transaction] Transaction insert failed: {e:?}"
-            )))
-        };
-
-        Ok(())
-    }
-
-    /// Auxiliary function to parse a `TRANSACTIONS_TABLE` record.
-    fn parse_transaction_record(&self, row: &[Value]) -> Result<TransactionRecord> {
-        let Value::Text(ref transaction_hash) = row[0] else {
-            return Err(Error::ParseFailed(
-                "[parse_transaction_record] Transaction hash parsing failed",
-            ))
-        };
-        let transaction_hash = transaction_hash.clone();
-
-        let Value::Text(ref header_hash) = row[1] else {
-            return Err(Error::ParseFailed("[parse_transaction_record] Header hash parsing failed"))
-        };
-        let header_hash = header_hash.clone();
-
-        let Value::Blob(ref payload_bytes) = row[2] else {
-            return Err(Error::ParseFailed(
-                "[parse_transaction_record] Payload bytes bytes parsing failed",
-            ))
-        };
-        let payload = deserialize(payload_bytes)?;
-
-        Ok(TransactionRecord { transaction_hash, header_hash, payload })
+    /// Provides the transaction count of all the transactions in the explorer database.
+    pub fn get_transaction_count(&self) -> usize {
+        self.blockchain.txs_len()
     }
 
     /// Fetch all known transactions from the database.
     pub fn get_transactions(&self) -> Result<Vec<TransactionRecord>> {
-        let rows = match self.database.query_multiple(TRANSACTIONS_TABLE, &[], &[]) {
-            Ok(r) => r,
-            Err(e) => {
-                return Err(Error::DatabaseError(format!(
-                    "[get_transactions] Transactions retrieval failed: {e:?}"
-                )))
-            }
-        };
-
-        let mut transactions = Vec::with_capacity(rows.len());
-        for row in rows {
-            transactions.push(self.parse_transaction_record(&row)?);
-        }
-
-        Ok(transactions)
+        // Retrieve all transactions and handle any errors encountered
+        let transactions_result = self.blockchain.transactions.get_all().map_err(|e| {
+            Error::DatabaseError(format!("[get_transactions] Trxs retrieval: {e:?}"))
+        })?;
+
+        // Transform the found transactions into a vector of transaction records
+        let transaction_records: Vec<TransactionRecord> = transactions_result
+            .iter()
+            .map(|(tx_hash, tx)| TransactionRecord::from((&tx_hash.as_string(), tx)))
+            .collect();
+
+        Ok(transaction_records)
     }
 
     /// Fetch all transactions from the database for the given block header hash.
@@ -159,42 +108,79 @@ impl BlockchainExplorer {
         &self,
         header_hash: &str,
     ) -> Result<Vec<TransactionRecord>> {
-        let rows = match self.database.query_multiple(
-            TRANSACTIONS_TABLE,
-            &[],
-            convert_named_params! {(TRANSACTIONS_COL_HEADER_HASH, header_hash)},
-        ) {
-            Ok(r) => r,
+        // Parse header hash, returning an error if parsing fails
+        let header_hash = header_hash
+            .parse::<HeaderHash>()
+            .map_err(|_| Error::ParseFailed("[get_transactions_by_header_hash] Invalid hash"))?;
+
+        // Fetch all blocks by hash and handle encountered errors
+        let blocks = match self.blockchain.get_blocks_by_hash(&[header_hash]) {
+            Ok(blocks) => blocks,
+            Err(Error::BlockNotFound(_)) => return Ok(vec![]),
             Err(e) => {
                 return Err(Error::DatabaseError(format!(
-                    "[get_transactions_by_header_hash] Transactions retrieval failed: {e:?}"
+                    "[get_transactions_by_header_hash] Block retrieval failed: {e:?}"
                 )))
             }
         };
 
-        let mut transactions = Vec::with_capacity(rows.len());
-        for row in rows {
-            transactions.push(self.parse_transaction_record(&row)?);
-        }
+        // Transform block transactions into transaction records
+        let tx_records = {
+            let block = &blocks[0];
+            block
+                .txs
+                .iter()
+                .map(|tx| TransactionRecord::from((&block.header.hash().as_string(), tx)))
+                .collect::<Vec<TransactionRecord>>()
+        };
 
-        Ok(transactions)
+        Ok(tx_records)
     }
 
     /// Fetch a transaction given its header hash.
-    pub fn get_transaction_by_hash(&self, transaction_hash: &str) -> Result<TransactionRecord> {
-        let row = match self.database.query_single(
-            TRANSACTIONS_TABLE,
-            &[],
-            convert_named_params! {(TRANSACTIONS_COL_TRANSACTION_HASH, transaction_hash)},
-        ) {
-            Ok(r) => r,
-            Err(e) => {
-                return Err(Error::DatabaseError(format!(
-                    "[get_transaction_by_hash] Transaction retrieval failed: {e:?}"
-                )))
+    pub fn get_transaction_by_hash(
+        &self,
+        tx_hash: &TransactionHash,
+    ) -> Result<Option<TransactionRecord>> {
+        let tx_store = &self.blockchain.transactions;
+
+        // Attempt to retrieve the transaction using the provided hash handling any potential errors
+        let txs = tx_store.get(&[*tx_hash], false).map_err(|e| {
+            Error::DatabaseError(format!(
+                "[get_transaction_by_hash] Transaction retrieval failed: {e:?}"
+            ))
+        })?;
+
+        // Match on the fetched transactions to process the result
+        let tx_record = match &txs[0] {
+            Some(tx) => {
+                // Retrieve the location of the transaction to obtain its header hash
+                let locations = tx_store.get_location(&[*tx_hash], false).map_err(|e| {
+                    Error::DatabaseError(format!(
+                        "[get_transaction_by_hash] Location retrieval failed: {e:?}"
+                    ))
+                })?;
+
+                // Unwrap the first location since we know it exists for a valid transaction
+                let (block_height, _) = locations[0].unwrap();
+
+                // Retrieve the block corresponding to the transaction's height
+                let block_data =
+                    &self.blockchain.blocks.get_order(&[block_height], false).map_err(|e| {
+                        Error::DatabaseError(format!(
+                            "[get_transaction_by_hash] Block retrieval failed: {e:?}"
+                        ))
+                    })?;
+
+                // Unwrap the block since we are assured it exists due to stored location
+                let header_hash = block_data[0].unwrap();
+
+                // Transform the transaction into a TransactionRecord
+                Some(TransactionRecord::from((&header_hash.as_string(), tx)))
             }
+            None => None,
         };
 
-        self.parse_transaction_record(&row)
+        Ok(tx_record)
     }
 }

+ 0 - 14
script/research/blockchain-explorer/transactions.sql

@@ -1,14 +0,0 @@
--- Database transactions table definition.
--- We store data in a usable format.
-CREATE TABLE IF NOT EXISTS transactions (
-    -- Transaction hash identifier
-    transaction_hash TEXT PRIMARY KEY NOT NULL,
-    -- Header hash identifier of the block this transaction was included in
-    header_hash TEXT NOT NULL,
-    -- TODO: Split the payload into a more easily readable fields
-    -- Transaction payload
-    payload BLOB NOT NULL,
-
-    FOREIGN KEY(header_hash) REFERENCES blocks(header_hash) ON DELETE CASCADE ON UPDATE CASCADE
-);
-