Browse Source

script/research/blockchain-explorer: added base transactions calls

skoupidi 2 years ago
parent
commit
968f0c7202

+ 1 - 1
script/research/blockchain-explorer/blocks.sql

@@ -1,4 +1,4 @@
--- Database block table definition.
+-- Database blocks table definition.
 -- We store data in a usable format.
 -- We store data in a usable format.
 CREATE TABLE IF NOT EXISTS blocks (
 CREATE TABLE IF NOT EXISTS blocks (
     -- Header hash identifier of the block
     -- Header hash identifier of the block

+ 3 - 3
script/research/blockchain-explorer/src/blocks.rs

@@ -30,7 +30,7 @@ use drk::{
 
 
 use crate::BlockchainExplorer;
 use crate::BlockchainExplorer;
 
 
-// Dtabase SQL table constant names. These have to represent the `block.sql`
+// Database SQL table constant names. These have to represent the `blocks.sql`
 // SQL schema.
 // SQL schema.
 pub const BLOCKS_TABLE: &str = "blocks";
 pub const BLOCKS_TABLE: &str = "blocks";
 
 
@@ -81,8 +81,8 @@ impl BlockRecord {
     }
     }
 }
 }
 
 
-impl From<BlockInfo> for BlockRecord {
-    fn from(block: BlockInfo) -> Self {
+impl From<&BlockInfo> for BlockRecord {
+    fn from(block: &BlockInfo) -> Self {
         Self {
         Self {
             header_hash: block.hash().to_string(),
             header_hash: block.hash().to_string(),
             version: block.header.version,
             version: block.header.version,

+ 11 - 2
script/research/blockchain-explorer/src/main.rs

@@ -47,10 +47,14 @@ mod error;
 mod rpc;
 mod rpc;
 mod rpc_blocks;
 mod rpc_blocks;
 use rpc_blocks::subscribe_blocks;
 use rpc_blocks::subscribe_blocks;
+mod rpc_transactions;
 
 
 /// Database functionality related to blocks
 /// Database functionality related to blocks
 mod blocks;
 mod blocks;
 
 
+/// Database functionality related to transactions
+mod transactions;
+
 const CONFIG_FILE: &str = "blockchain_explorer_config.toml";
 const CONFIG_FILE: &str = "blockchain_explorer_config.toml";
 const CONFIG_FILE_CONTENTS: &str = include_str!("../blockchain_explorer_config.toml");
 const CONFIG_FILE_CONTENTS: &str = include_str!("../blockchain_explorer_config.toml");
 
 
@@ -153,10 +157,15 @@ impl BlockchainExplorer {
         // Initialize all the database tables
         // Initialize all the database tables
         if let Err(e) = explorer.initialize_blocks().await {
         if let Err(e) = explorer.initialize_blocks().await {
             let err = format!("{e:?}");
             let err = format!("{e:?}");
-            error!(target: "blockchain-explorer", "Error initializing database tables: {err}");
+            error!(target: "blockchain-explorer", "Error initializing blocks database table: {err}");
+            return Err(Error::RusqliteError(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::RusqliteError(err))
             return Err(Error::RusqliteError(err))
         }
         }
-        // TODO: map transaction structure to their corresponding files with sql table and retrieval methods
+        // TODO: Map deployed contracts to their corresponding files with sql table and retrieval methods
 
 
         Ok(explorer)
         Ok(explorer)
     }
     }

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

@@ -49,18 +49,26 @@ impl RequestHandler for BlockchainExplorer {
             "ping" => self.pong(req.id, req.params).await,
             "ping" => self.pong(req.id, req.params).await,
             "ping_darkfid" => self.ping_darkfid(req.id, req.params).await,
             "ping_darkfid" => self.ping_darkfid(req.id, req.params).await,
 
 
-            // ==================
+            // =====================
             // Blocks methods
             // Blocks methods
-            // ==================
+            // =====================
             "blocks.get_last_n_blocks" => self.blocks_get_last_n_blocks(req.id, req.params).await,
             "blocks.get_last_n_blocks" => self.blocks_get_last_n_blocks(req.id, req.params).await,
             "blocks.get_blocks_in_heights_range" => {
             "blocks.get_blocks_in_heights_range" => {
                 self.blocks_get_blocks_in_heights_range(req.id, req.params).await
                 self.blocks_get_blocks_in_heights_range(req.id, req.params).await
             }
             }
             "blocks.get_block_by_hash" => self.blocks_get_block_by_hash(req.id, req.params).await,
             "blocks.get_block_by_hash" => self.blocks_get_block_by_hash(req.id, req.params).await,
 
 
+            // =====================
+            // Transactions methods
+            // =====================
+            "transactions.get_transactions_by_header_hash" => {
+                self.transactions_get_transactions_by_header_hash(req.id, req.params).await
+            }
+            "transactions.get_transaction_by_hash" => {
+                self.transactions_get_transaction_by_hash(req.id, req.params).await
+            }
+
             // TODO: add statistics retrieval method
             // TODO: add statistics retrieval method
-            // TODO: add transactions retrieval method by their block hash
-            // TODO: add transaction retrieval method by its hash
             // TODO: add any other usefull method
             // TODO: add any other usefull method
 
 
             // ==============
             // ==============

+ 33 - 15
script/research/blockchain-explorer/src/rpc_blocks.rs

@@ -56,11 +56,11 @@ impl BlockchainExplorer {
     }
     }
 
 
     /// Syncs the blockchain starting from the last synced block.
     /// Syncs the blockchain starting from the last synced block.
-    /// If reset flag is provided, all tables are reset, and start scanning from beginning.
+    /// 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) -> WalletDbResult<()> {
-        // Grab last scanned block height
+        // Grab last synced block height
         let mut height = self.last_block().await?;
         let mut height = self.last_block().await?;
-        // If last scanned block is genesis (0) or reset flag
+        // If last synced block is genesis (0) or reset flag
         // has been provided we reset, otherwise continue with
         // has been provided we reset, otherwise continue with
         // the next block height
         // the next block height
         if height == 0 || reset {
         if height == 0 || reset {
@@ -83,10 +83,10 @@ impl BlockchainExplorer {
             };
             };
             let last = *rep.get::<f64>().unwrap() as u32;
             let last = *rep.get::<f64>().unwrap() as u32;
 
 
-            info!(target: "blockchain-explorer::rpc_blocks::sync_blocks", "Requested to scan from block number: {height}");
+            info!(target: "blockchain-explorer::rpc_blocks::sync_blocks", "Requested to sync from block number: {height}");
             info!(target: "blockchain-explorer::rpc_blocks::sync_blocks", "Last known block number reported by darkfid: {last}");
             info!(target: "blockchain-explorer::rpc_blocks::sync_blocks", "Last known block number reported by darkfid: {last}");
 
 
-            // Already scanned last known block
+            // Already synced last known block
             if height > last {
             if height > last {
                 return Ok(())
                 return Ok(())
             }
             }
@@ -102,11 +102,20 @@ impl BlockchainExplorer {
                     }
                     }
                 };
                 };
 
 
-                if let Err(e) = self.put_block(&block.into()).await {
-                    error!(target: "blockchain-explorer::rpc_blocks::sync_blocks", "[sync_blocks] Scan block failed: {e:?}");
+                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)
                     return Err(WalletDbError::GenericError)
                 };
                 };
 
 
+                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)
+                    };
+                }
+
                 height += 1;
                 height += 1;
             }
             }
         }
         }
@@ -244,20 +253,20 @@ pub async fn subscribe_blocks(
         .darkfid_daemon_request("blockchain.last_known_block", &JsonValue::Array(vec![]))
         .darkfid_daemon_request("blockchain.last_known_block", &JsonValue::Array(vec![]))
         .await?;
         .await?;
     let last_known = *rep.get::<f64>().unwrap() as u32;
     let last_known = *rep.get::<f64>().unwrap() as u32;
-    let last_scanned = match explorer.last_block().await {
+    let last_synced = match explorer.last_block().await {
         Ok(l) => l,
         Ok(l) => l,
         Err(e) => {
         Err(e) => {
             return Err(Error::RusqliteError(format!(
             return Err(Error::RusqliteError(format!(
-                "[subscribe_blocks] Retrieving last scanned block failed: {e:?}"
+                "[subscribe_blocks] Retrieving last synced block failed: {e:?}"
             )))
             )))
         }
         }
     };
     };
 
 
-    if last_known != last_scanned {
-        warn!(target: "blockchain-explorer::rpc_blocks::subscribe_blocks", "Warning: Last scanned block is not the last known block.");
-        warn!(target: "blockchain-explorer::rpc_blocks::subscribe_blocks", "You should first fully scan the blockchain, and then subscribe");
+    if last_known != last_synced {
+        warn!(target: "blockchain-explorer::rpc_blocks::subscribe_blocks", "Warning: Last synced block is not the last known block.");
+        warn!(target: "blockchain-explorer::rpc_blocks::subscribe_blocks", "You should first fully sync the blockchain, and then subscribe");
         return Err(Error::RusqliteError(
         return Err(Error::RusqliteError(
-            "[subscribe_blocks] Blockchain not fully scanned".to_string(),
+            "[subscribe_blocks] Blockchain not fully synced".to_string(),
         ))
         ))
     }
     }
 
 
@@ -332,11 +341,20 @@ pub async fn subscribe_blocks(
                             info!(target: "blockchain-explorer::rpc_blocks::subscribe_blocks", "=======================================");
                             info!(target: "blockchain-explorer::rpc_blocks::subscribe_blocks", "=======================================");
 
 
                             info!(target: "blockchain-explorer::rpc_blocks::subscribe_blocks", "Deserialized successfully. Storring block...");
                             info!(target: "blockchain-explorer::rpc_blocks::subscribe_blocks", "Deserialized successfully. Storring block...");
-                            if let Err(e) = explorer.put_block(&block_data.into()).await {
+                            if let Err(e) = explorer.put_block(&(&block_data).into()).await {
                                 return Err(Error::RusqliteError(format!(
                                 return Err(Error::RusqliteError(format!(
-                                    "[subscribe_blocks] Scanning block failed: {e:?}"
+                                    "[subscribe_blocks] Insert 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::RusqliteError(format!(
+                                        "[subscribe_blocks] Insert block transaction failed: {e:?}"
+                                    )))
+                                };
+                            }
                         }
                         }
                     }
                     }
 
 

+ 101 - 0
script/research/blockchain-explorer/src/rpc_transactions.rs

@@ -0,0 +1,101 @@
+/* This file is part of DarkFi (https://dark.fi)
+ *
+ * Copyright (C) 2020-2024 Dyne.org foundation
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program.  If not, see <https://www.gnu.org/licenses/>.
+ */
+
+use log::error;
+use tinyjson::JsonValue;
+
+use darkfi::rpc::jsonrpc::{
+    ErrorCode::{InternalError, InvalidParams},
+    JsonError, JsonResponse, JsonResult,
+};
+
+use crate::BlockchainExplorer;
+
+impl BlockchainExplorer {
+    // RPCAPI:
+    // Queries the database to retrieve the transactions corresponding to the provided block header hash.
+    // Returns the readable transactions upon success.
+    //
+    // **Params:**
+    // * `array[0]`: `String` Block header hash
+    //
+    // **Returns:**
+    // * Array of `TransactionRecord` encoded into a JSON.
+    //
+    // --> {"jsonrpc": "2.0", "method": "transactions.get_transactions_by_header_hash", "params": ["5cc...2f9"], "id": 1}
+    // <-- {"jsonrpc": "2.0", "result": {...}, "id": 1}
+    pub async fn transactions_get_transactions_by_header_hash(
+        &self,
+        id: u16,
+        params: JsonValue,
+    ) -> JsonResult {
+        let params = params.get::<Vec<JsonValue>>().unwrap();
+        if params.len() != 1 || !params[0].is_string() {
+            return JsonError::new(InvalidParams, None, id).into()
+        }
+
+        let header_hash = params[0].get::<String>().unwrap();
+        let transactions = match self.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);
+                return JsonError::new(InternalError, None, id).into()
+            }
+        };
+
+        let mut ret = vec![];
+        for transaction in transactions {
+            ret.push(transaction.to_json_array());
+        }
+        JsonResponse::new(JsonValue::Array(ret), id).into()
+    }
+
+    // RPCAPI:
+    // Queries the database to retrieve the transaction corresponding to the provided hash.
+    // Returns the readable transaction upon success.
+    //
+    // **Params:**
+    // * `array[0]`: `String` Transaction hash
+    //
+    // **Returns:**
+    // * `TransactionRecord` encoded into a JSON.
+    //
+    // --> {"jsonrpc": "2.0", "method": "transactions.get_transaction_by_hash", "params": ["7e7...b4d"], "id": 1}
+    // <-- {"jsonrpc": "2.0", "result": {...}, "id": 1}
+    pub async fn transactions_get_transaction_by_hash(
+        &self,
+        id: u16,
+        params: JsonValue,
+    ) -> JsonResult {
+        let params = params.get::<Vec<JsonValue>>().unwrap();
+        if params.len() != 1 || !params[0].is_string() {
+            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,
+            Err(e) => {
+                error!(target: "blockchain-explorer::rpc_transactions::transactions_get_transaction_by_hash", "Failed fetching transaction: {}", e);
+                return JsonError::new(InternalError, None, id).into()
+            }
+        };
+
+        JsonResponse::new(transaction.to_json_array(), id).into()
+    }
+}

+ 200 - 0
script/research/blockchain-explorer/src/transactions.rs

@@ -0,0 +1,200 @@
+/* This file is part of DarkFi (https://dark.fi)
+ *
+ * Copyright (C) 2020-2024 Dyne.org foundation
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program.  If not, see <https://www.gnu.org/licenses/>.
+ */
+
+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 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";
+
+#[derive(Debug, Clone)]
+/// Structure representing a `TRANSACTIONS_TABLE` record.
+pub struct TransactionRecord {
+    /// Transaction hash identifier
+    pub transaction_hash: String,
+    /// Header hash identifier of the block this transaction was included in
+    pub header_hash: String,
+    // TODO: Split the payload into a more easily readable fields
+    /// Transaction payload
+    pub payload: Transaction,
+}
+
+impl TransactionRecord {
+    /// Auxiliary function to convert a `TransactionRecord` into a `JsonValue` array.
+    pub fn to_json_array(&self) -> JsonValue {
+        let mut ret = vec![];
+        ret.push(JsonValue::String(self.transaction_hash.clone()));
+        ret.push(JsonValue::String(self.header_hash.clone()));
+        ret.push(JsonValue::String(format!("{:?}", self.payload)));
+        JsonValue::Array(ret)
+    }
+}
+
+impl From<(&String, &Transaction)> for TransactionRecord {
+    fn from((header_hash, transaction): (&String, &Transaction)) -> Self {
+        Self {
+            transaction_hash: transaction.hash().to_string(),
+            header_hash: header_hash.clone(),
+            payload: transaction.clone(),
+        }
+    }
+}
+
+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)?;
+
+        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::RusqliteError(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 })
+    }
+
+    /// 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::RusqliteError(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)
+    }
+
+    /// Fetch all transactions from the database for the given block header hash.
+    pub fn get_transactions_by_header_hash(
+        &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,
+            Err(e) => {
+                return Err(Error::RusqliteError(format!(
+                    "[get_transactions_by_header_hash] 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)
+    }
+
+    /// 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::RusqliteError(format!(
+                    "[get_transaction_by_hash] Transaction retrieval failed: {e:?}"
+                )))
+            }
+        };
+
+        self.parse_transaction_record(&row)
+    }
+}

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

@@ -0,0 +1,14 @@
+-- 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
+);
+