| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734 |
- /* This file is part of DarkFi (https://dark.fi)
- *
- * Copyright (C) 2020-2026 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 std::collections::HashMap;
- use darkfi::{
- blockchain::{header_store::PowData, BlockInfo},
- rpc::jsonrpc::{
- ErrorCode::{InternalError, InvalidParams},
- JsonError, JsonResponse, JsonResult,
- },
- tx::Transaction,
- util::{encoding::base64, parse::encode_base10},
- };
- use darkfi_money_contract::MoneyFunction;
- use darkfi_sdk::crypto::contract_id::MONEY_CONTRACT_ID;
- use darkfi_serial::{deserialize_async, serialize_async};
- use monero::{consensus::encode::Encodable, VarInt};
- use tiny_keccak::{Hasher, Keccak};
- use tinyjson::JsonValue;
- use crate::{DifficultyIndex, Explorer};
- struct ContractCallInfo {
- contract_id: String,
- contract_tag: Option<String>,
- func: String,
- size: u64,
- }
- impl ContractCallInfo {
- fn new(contract_id: String, func: String, size: u64) -> Self {
- let contract_tag = match contract_id.as_str() {
- "BZHKGQ26bzmBithTQYTJtjo2QdCqpkR9tjSBopT4yf4o" => Some("Money".to_string()),
- "Fd8kfCuqU8BoFFp6GcXv5pC8XXRkBK7gUPQX5XDz7iXj" => Some("DAO".to_string()),
- "EJs7oEjKkvCeEVCmpRsd6fEoTGCFJ7WKUBfmAjwaegN" => Some("Deployoor".to_string()),
- _ => None,
- };
- Self { contract_id, contract_tag, func, size }
- }
- fn to_json(&self) -> JsonValue {
- let tag = match &self.contract_tag {
- Some(t) => JsonValue::String(t.clone()),
- None => JsonValue::Null,
- };
- JsonValue::Object(HashMap::from([
- ("contract_id".to_string(), JsonValue::String(self.contract_id.clone())),
- ("contract_tag".to_string(), tag),
- ("func".to_string(), JsonValue::String(self.func.clone())),
- ("size".to_string(), JsonValue::Number(self.size as f64)),
- ]))
- }
- }
- struct TransactionInfo {
- hash: String,
- calls: Vec<ContractCallInfo>,
- fee: u64,
- size: u64,
- }
- impl TransactionInfo {
- async fn new(tx: &Transaction) -> Self {
- let mut fee = 0;
- let mut calls = Vec::with_capacity(tx.calls.len());
- for call in &tx.calls {
- let func = call.data.data[0];
- if call.data.contract_id == *MONEY_CONTRACT_ID && func == MoneyFunction::FeeV1 as u8 {
- fee = deserialize_async(&call.data.data[1..9]).await.unwrap();
- }
- calls.push(ContractCallInfo::new(
- call.data.contract_id.to_string(),
- format!("0x{:02x}", func),
- call.data.data.len() as u64,
- ));
- }
- Self {
- hash: tx.hash().to_string(),
- calls,
- fee,
- size: serialize_async(tx).await.len() as u64,
- }
- }
- fn to_json(&self) -> JsonValue {
- let calls = self.calls.iter().map(|c| c.to_json()).collect();
- JsonValue::Object(HashMap::from([
- ("hash".to_string(), JsonValue::String(self.hash.clone())),
- ("calls".to_string(), JsonValue::Array(calls)),
- ("fee".to_string(), JsonValue::String(encode_base10(self.fee, 8))),
- ("size".to_string(), JsonValue::Number(self.size as f64)),
- ]))
- }
- }
- /// Full transaction info for the get_tx RPC endpoint
- struct ExplTxInfo {
- hash: String,
- from_block: u64,
- confirmations: u64,
- fee: u64,
- size: u64,
- n_calls: u64,
- calls: Vec<ContractCallInfo>,
- raw: String,
- }
- impl ExplTxInfo {
- async fn new(tx: &Transaction, block_height: u64, current_height: u64) -> Self {
- let mut fee = 0;
- let mut calls = Vec::with_capacity(tx.calls.len());
- for call in &tx.calls {
- let func = call.data.data[0];
- if call.data.contract_id == *MONEY_CONTRACT_ID && func == MoneyFunction::FeeV1 as u8 {
- fee = deserialize_async(&call.data.data[1..9]).await.unwrap();
- }
- calls.push(ContractCallInfo::new(
- call.data.contract_id.to_string(),
- format!("0x{:02x}", func),
- call.data.data.len() as u64,
- ));
- }
- let raw_bytes = serialize_async(tx).await;
- let confirmations =
- if current_height >= block_height { current_height - block_height + 1 } else { 0 };
- Self {
- hash: tx.hash().to_string(),
- from_block: block_height,
- confirmations,
- fee,
- size: raw_bytes.len() as u64,
- n_calls: tx.calls.len() as u64,
- calls,
- raw: base64::encode(&raw_bytes),
- }
- }
- fn to_json(&self) -> JsonValue {
- JsonValue::Object(HashMap::from([
- ("hash".to_string(), JsonValue::String(self.hash.clone())),
- ("from_block".to_string(), JsonValue::Number(self.from_block as f64)),
- ("confirmations".to_string(), JsonValue::Number(self.confirmations as f64)),
- ("fee".to_string(), JsonValue::String(encode_base10(self.fee, 8))),
- ("size".to_string(), JsonValue::Number(self.size as f64)),
- ("n_calls".to_string(), JsonValue::Number(self.n_calls as f64)),
- (
- "calls".to_string(),
- JsonValue::Array(self.calls.iter().map(|c| c.to_json()).collect()),
- ),
- ("raw".to_string(), JsonValue::String(self.raw.clone())),
- ]))
- }
- }
- struct ExplBlockInfo {
- height: u64,
- hash: String,
- version: u8,
- previous_hash: String,
- nonce: u64,
- timestamp: u64,
- transactions_root: String,
- state_root: String,
- size: u64,
- difficulty: u64,
- cumulative: u64,
- powtype: String,
- monero_hash: Option<String>,
- txs: Vec<TransactionInfo>,
- coinbase: CoinbaseInfo,
- }
- impl ExplBlockInfo {
- async fn new(block: &BlockInfo, diff: &DifficultyIndex) -> Self {
- let mut monero_hash = None;
- let powtype = match &block.header.pow_data {
- PowData::DarkFi => "DarkFi".to_string(),
- PowData::Monero(powdata) => {
- // Calculate the Monero block header hash
- let mut blockhashing_blob = powdata.to_block_hashing_blob();
- // Monero prefixes a VarInt of the blob len before getting the
- // block hash but doesn't do this when getting the PoW hash :)
- let mut header = vec![];
- VarInt(blockhashing_blob.len() as u64).consensus_encode(&mut header).unwrap();
- header.append(&mut blockhashing_blob);
- let mut keccak = Keccak::v256();
- keccak.update(&header);
- let mut hash = [0u8; 32];
- keccak.finalize(&mut hash);
- monero_hash = Some(hex::encode(hash));
- "Monero".to_string()
- }
- };
- let mut txs = Vec::with_capacity(block.txs.len());
- for tx in &block.txs {
- txs.push(TransactionInfo::new(tx).await);
- }
- let coinbase = CoinbaseInfo::new(&block.txs[block.txs.len() - 1]).await;
- Self {
- height: block.header.height as u64,
- hash: block.header.hash().to_string(),
- version: block.header.version,
- previous_hash: block.header.previous.to_string(),
- nonce: block.header.nonce as u64,
- timestamp: block.header.timestamp.inner(),
- transactions_root: block.header.transactions_root.to_string(),
- state_root: hex::encode(block.header.state_root),
- size: serialize_async(block).await.len() as u64,
- difficulty: diff.difficulty,
- cumulative: diff.cumulative,
- powtype,
- monero_hash,
- txs,
- coinbase,
- }
- }
- fn to_json(&self) -> JsonValue {
- let monero_hash = if let Some(hash) = &self.monero_hash {
- JsonValue::String(hash.to_string())
- } else {
- JsonValue::Null
- };
- JsonValue::Object(HashMap::from([
- ("height".to_string(), JsonValue::Number(self.height as f64)),
- ("hash".to_string(), JsonValue::String(self.hash.clone())),
- ("version".to_string(), JsonValue::Number(self.version as f64)),
- ("previous_hash".to_string(), JsonValue::String(self.previous_hash.clone())),
- ("nonce".to_string(), JsonValue::Number(self.nonce as f64)),
- ("timestamp".to_string(), JsonValue::Number(self.timestamp as f64)),
- ("transactions_root".to_string(), JsonValue::String(self.transactions_root.clone())),
- ("state_root".to_string(), JsonValue::String(self.state_root.clone())),
- ("size".to_string(), JsonValue::Number(self.size as f64)),
- ("difficulty".to_string(), JsonValue::Number(self.difficulty as f64)),
- ("cumulative".to_string(), JsonValue::Number(self.cumulative as f64)),
- ("powtype".to_string(), JsonValue::String(self.powtype.clone())),
- ("monero_hash".to_string(), monero_hash),
- ("txs".to_string(), JsonValue::Array(self.txs.iter().map(|t| t.to_json()).collect())),
- ("coinbase".to_string(), self.coinbase.to_json()),
- ]))
- }
- }
- struct CoinbaseInfo {
- hash: String,
- reward: u64,
- size: u64,
- }
- impl CoinbaseInfo {
- async fn new(tx: &Transaction) -> Self {
- Self {
- hash: tx.hash().to_string(),
- reward: 0,
- size: serialize_async(tx).await.len() as u64,
- }
- }
- fn to_json(&self) -> JsonValue {
- JsonValue::Object(HashMap::from([
- ("hash".to_string(), JsonValue::String(self.hash.clone())),
- ("reward".to_string(), JsonValue::Number(self.reward as f64)),
- ("size".to_string(), JsonValue::Number(self.size as f64)),
- ]))
- }
- }
- impl Explorer {
- pub async fn rpc_current_difficulty(&self, id: u16, _params: JsonValue) -> JsonResult {
- // Get latest height
- let Ok(Some(height)) = self.get_height() else {
- return JsonError::new(InternalError, None, id).into()
- };
- let Ok(Some(difficulty)) = self.get_difficulty(height) else {
- return JsonError::new(InternalError, None, id).into()
- };
- JsonResponse::new(
- JsonValue::Array(vec![
- (difficulty.difficulty as f64).into(),
- (difficulty.cumulative as f64).into(),
- ]),
- id,
- )
- .into()
- }
- pub async fn rpc_current_height(&self, id: u16, _params: JsonValue) -> JsonResult {
- let Ok(Some(height)) = self.get_height() else {
- return JsonError::new(InternalError, None, id).into()
- };
- JsonResponse::new(JsonValue::Number(height as f64), id).into()
- }
- pub async fn rpc_latest_blocks(&self, id: u16, params: JsonValue) -> JsonResult {
- let Some(params) = params.get::<Vec<JsonValue>>() else {
- return JsonError::new(InvalidParams, None, id).into()
- };
- if params.len() != 1 || !params[0].is_number() {
- return JsonError::new(InvalidParams, None, id).into()
- }
- let n_blocks = *params[0].get::<f64>().unwrap() as u64;
- let Ok(Some(height)) = self.get_height() else {
- return JsonError::new(InternalError, None, id).into()
- };
- // Calculate how many blocks we can actually return
- let start_height = height.saturating_sub(n_blocks.saturating_sub(1));
- let mut blocks = Vec::with_capacity((height - start_height + 1) as usize);
- for h in (start_height..=height).rev() {
- let Ok(Some((header, tx_count, size))) = self.get_block_summary(h).await else {
- return JsonError::new(InternalError, None, id).into()
- };
- let powtype = match header.pow_data {
- PowData::DarkFi => "DarkFi".to_string(),
- PowData::Monero(_) => "Monero".to_string(),
- };
- blocks.push(JsonValue::Object(HashMap::from([
- ("height".to_string(), JsonValue::Number(header.height as f64)),
- ("size".to_string(), JsonValue::Number(size as f64)),
- ("n_txs".to_string(), JsonValue::Number(tx_count as f64)),
- ("timestamp".to_string(), JsonValue::Number(header.timestamp.inner() as f64)),
- ("powtype".to_string(), JsonValue::String(powtype)),
- ("hash".to_string(), JsonValue::String(header.hash().to_string())),
- ])));
- }
- JsonResponse::new(JsonValue::Array(blocks), id).into()
- }
- pub async fn rpc_get_block(&self, id: u16, params: JsonValue) -> JsonResult {
- let Some(params) = params.get::<Vec<JsonValue>>() else {
- return JsonError::new(InvalidParams, None, id).into()
- };
- if params.len() != 1 {
- return JsonError::new(InvalidParams, None, id).into()
- }
- let height = if params[0].is_string() {
- let Ok(hash) = hex::decode(params[0].get::<String>().unwrap()) else {
- return JsonError::new(InvalidParams, None, id).into()
- };
- let Ok(Some(height_bytes)) = self.header_indices.get(hash) else {
- return JsonError::new(InternalError, None, id).into()
- };
- let height_bytes = height_bytes.to_vec();
- u64::from_le_bytes(height_bytes.try_into().unwrap())
- } else if params[0].is_number() {
- *params[0].get::<f64>().unwrap() as u64
- } else {
- return JsonError::new(InvalidParams, None, id).into()
- };
- let Ok(Some(block)) = self.get_block(height).await else {
- return JsonError::new(InternalError, None, id).into()
- };
- let Ok(Some(diff)) = self.get_difficulty(height) else {
- return JsonError::new(InternalError, None, id).into()
- };
- let info = ExplBlockInfo::new(&block, &diff).await;
- JsonResponse::new(info.to_json(), id).into()
- }
- pub async fn rpc_get_tx(&self, id: u16, params: JsonValue) -> JsonResult {
- let Some(params) = params.get::<Vec<JsonValue>>() else {
- return JsonError::new(InvalidParams, None, id).into()
- };
- if params.len() != 1 || !params[0].is_string() {
- return JsonError::new(InvalidParams, None, id).into()
- }
- let tx_hash_str = params[0].get::<String>().unwrap();
- // Get current height for confirmations calculation
- let Ok(Some(current_height)) = self.get_height() else {
- return JsonError::new(InternalError, None, id).into()
- };
- // Get transaction by hash
- let Ok(Some((tx, block_height))) = self.get_tx_by_hash_str(tx_hash_str).await else {
- return JsonError::new(InternalError, None, id).into()
- };
- let info = ExplTxInfo::new(&tx, block_height, current_height).await;
- JsonResponse::new(info.to_json(), id).into()
- }
- /// Search for a block or transaction by hash.
- /// Returns `{"type": "block", "height": N}` or `{"type": "tx"}` depending on what was found.
- pub async fn rpc_search(&self, id: u16, params: JsonValue) -> JsonResult {
- let Some(params) = params.get::<Vec<JsonValue>>() else {
- return JsonError::new(InvalidParams, None, id).into()
- };
- if params.len() != 1 || !params[0].is_string() {
- return JsonError::new(InvalidParams, None, id).into()
- }
- let query = params[0].get::<String>().unwrap();
- // Try to decode as hex
- let Ok(hash_bytes) = hex::decode(query) else {
- return JsonError::new(InvalidParams, None, id).into()
- };
- // Try block hash first (serialized blake3 hash)
- if let Ok(Some(height_bytes)) = self.header_indices.get(&hash_bytes) {
- let height = u64::from_le_bytes(height_bytes.as_ref().try_into().unwrap_or([0u8; 8]));
- return JsonResponse::new(
- JsonValue::Object(HashMap::from([
- ("type".to_string(), JsonValue::String("block".to_string())),
- ("height".to_string(), JsonValue::Number(height as f64)),
- ])),
- id,
- )
- .into()
- }
- // Try transaction hash (32 bytes)
- if hash_bytes.len() == 32 {
- let mut tx_hash = [0u8; 32];
- tx_hash.copy_from_slice(&hash_bytes);
- if self.tx_indices.get(tx_hash).ok().flatten().is_some() {
- return JsonResponse::new(
- JsonValue::Object(HashMap::from([(
- "type".to_string(),
- JsonValue::String("tx".to_string()),
- )])),
- id,
- )
- .into()
- }
- }
- // Not found
- JsonError::new(InternalError, Some("Not found".to_string()), id).into()
- }
- /// Calculate the current network hashrate.
- /// Hashrate = difficulty / average_block_time
- /// We use the last N blocks to smooth out variance.
- pub async fn rpc_get_hashrate(&self, id: u16, _params: JsonValue) -> JsonResult {
- const BLOCKS_TO_AVERAGE: u64 = 30;
- let Ok(Some(height)) = self.get_height() else {
- return JsonError::new(InternalError, None, id).into()
- };
- if height < 2 {
- return JsonResponse::new(JsonValue::Number(0.0), id).into()
- }
- let start_height = height.saturating_sub(BLOCKS_TO_AVERAGE);
- // Get timestamps from start and end blocks
- let Ok(Some(start_header)) = self.get_header(start_height).await else {
- return JsonError::new(InternalError, None, id).into()
- };
- let Ok(Some(end_header)) = self.get_header(height).await else {
- return JsonError::new(InternalError, None, id).into()
- };
- let time_diff = end_header.timestamp.inner() as f64 - start_header.timestamp.inner() as f64;
- let blocks_mined = (height - start_height) as f64;
- if time_diff <= 0.0 || blocks_mined <= 0.0 {
- return JsonResponse::new(JsonValue::Number(0.0), id).into()
- }
- // Get current difficulty
- let Ok(Some(diff)) = self.get_difficulty(height) else {
- return JsonError::new(InternalError, None, id).into()
- };
- // Average block time in seconds
- let avg_block_time = time_diff / blocks_mined;
- // Hashrate = difficulty / block_time
- // This approximates hashes per second needed to find a block at current difficulty
- let hashrate = (diff.difficulty as f64) / avg_block_time;
- JsonResponse::new(JsonValue::Number(hashrate), id).into()
- }
- /// Get contract information by ID.
- /// Params: `[contract_id: String]`
- /// Returns: `{ contract_id, locked, wasm_size, deploy_block, deploy_tx }`
- pub async fn rpc_get_contract(&self, id: u16, params: JsonValue) -> JsonResult {
- let Some(params) = params.get::<Vec<JsonValue>>() else {
- return JsonError::new(InvalidParams, None, id).into()
- };
- if params.len() != 1 || !params[0].is_string() {
- return JsonError::new(InvalidParams, None, id).into()
- }
- let contract_id_str = params[0].get::<String>().unwrap();
- let Ok(Some(contract)) = self.get_contract(contract_id_str).await else {
- return JsonError::new(InternalError, Some("Contract not found".to_string()), id).into()
- };
- JsonResponse::new(
- JsonValue::Object(HashMap::from([
- ("contract_id".to_string(), JsonValue::String(contract.contract_id.to_string())),
- ("locked".to_string(), JsonValue::Boolean(contract.locked)),
- ("wasm_size".to_string(), JsonValue::Number(contract.wasm_size as f64)),
- ("deploy_block".to_string(), JsonValue::Number(contract.deploy_block as f64)),
- ("deploy_tx".to_string(), JsonValue::String(hex::encode(contract.deploy_tx_hash))),
- ])),
- id,
- )
- .into()
- }
- /// List all contracts.
- /// Params: `[locked_filter: bool | null] (optional)`
- /// Returns: Array of contract objects
- pub async fn rpc_list_contracts(&self, id: u16, params: JsonValue) -> JsonResult {
- let locked_filter = if let Some(params) = params.get::<Vec<JsonValue>>() {
- if !params.is_empty() {
- params[0].get::<bool>().copied()
- } else {
- None
- }
- } else {
- None
- };
- let Ok(contracts) = self.list_contracts(locked_filter).await else {
- return JsonError::new(InternalError, None, id).into()
- };
- let contracts_json: Vec<JsonValue> = contracts
- .iter()
- .map(|c| {
- JsonValue::Object(HashMap::from([
- ("contract_id".to_string(), JsonValue::String(c.contract_id.to_string())),
- ("locked".to_string(), JsonValue::Boolean(c.locked)),
- ("wasm_size".to_string(), JsonValue::Number(c.wasm_size as f64)),
- ("deploy_block".to_string(), JsonValue::Number(c.deploy_block as f64)),
- ("deploy_tx".to_string(), JsonValue::String(hex::encode(c.deploy_tx_hash))),
- ]))
- })
- .collect();
- JsonResponse::new(JsonValue::Array(contracts_json), id).into()
- }
- /// Get contract count.
- /// Returns: Number of contracts
- pub async fn rpc_contract_count(&self, id: u16, _params: JsonValue) -> JsonResult {
- let Ok(count) = self.get_contract_count() else {
- return JsonError::new(InternalError, None, id).into()
- };
- JsonResponse::new(JsonValue::Number(count as f64), id).into()
- }
- /// Get blockchain statistics from stored data.
- /// Returns daily stats, monthly growth, and tx per block stats.
- pub async fn rpc_get_stats(&self, id: u16, _params: JsonValue) -> JsonResult {
- // Get daily stats from sled
- let daily_stats = match self.get_all_daily_stats().await {
- Ok(stats) => stats,
- Err(_) => return JsonError::new(InternalError, None, id).into(),
- };
- // Get monthly stats from sled
- let monthly_stats = match self.get_all_monthly_stats().await {
- Ok(stats) => stats,
- Err(_) => return JsonError::new(InternalError, None, id).into(),
- };
- // Convert daily stats to JSON (for graph)
- let daily_json: Vec<JsonValue> = daily_stats
- .iter()
- .map(|(day, stats)| {
- let avg_tx = if stats.block_count > 0 {
- stats.user_tx_count as f64 / stats.block_count as f64
- } else {
- 0.0
- };
- JsonValue::Object(HashMap::from([
- ("day".to_string(), JsonValue::Number(*day as f64)),
- ("avg_tx".to_string(), JsonValue::Number(avg_tx)),
- ("block_count".to_string(), JsonValue::Number(stats.block_count as f64)),
- ("user_tx_count".to_string(), JsonValue::Number(stats.user_tx_count as f64)),
- ("total_size".to_string(), JsonValue::Number(stats.total_size as f64)),
- ]))
- })
- .collect();
- // Convert monthly stats to JSON with cumulative
- let mut cumulative: u64 = 0;
- let monthly_json: Vec<JsonValue> = monthly_stats
- .iter()
- .map(|(year, month, stats)| {
- cumulative += stats.total_size;
- JsonValue::Object(HashMap::from([
- ("year".to_string(), JsonValue::Number(*year as f64)),
- ("month".to_string(), JsonValue::Number(*month as f64)),
- (
- "size_mb".to_string(),
- JsonValue::Number(stats.total_size as f64 / 1_048_576.0),
- ),
- (
- "cumulative_mb".to_string(),
- JsonValue::Number(cumulative as f64 / 1_048_576.0),
- ),
- ("block_count".to_string(), JsonValue::Number(stats.block_count as f64)),
- ]))
- })
- .collect();
- // Calculate tx per block stats for time periods
- // Get current day
- let current_day = if let Some((day, _)) = daily_stats.last() { *day } else { 0 };
- fn calc_period_stats(
- daily_stats: &[(u64, crate::db::DailyStats)],
- from_day: u64,
- to_day: u64,
- ) -> (f64, f64, u64, u64) {
- let mut total_blocks: u64 = 0;
- let mut total_user_tx: u64 = 0;
- let mut empty_blocks: u64 = 0;
- for (day, stats) in daily_stats {
- if *day >= from_day && *day <= to_day {
- total_blocks += stats.block_count;
- total_user_tx += stats.user_tx_count;
- // A block is "empty" if it has 0 user transactions
- // We approximate empty blocks as: blocks where avg user_tx < 1
- // But we don't have per-block data, so we estimate
- if stats.block_count > 0 && stats.user_tx_count == 0 {
- empty_blocks += stats.block_count;
- }
- }
- }
- let avg_tx =
- if total_blocks > 0 { total_user_tx as f64 / total_blocks as f64 } else { 0.0 };
- let empty_pct = if total_blocks > 0 {
- empty_blocks as f64 / total_blocks as f64 * 100.0
- } else {
- 0.0
- };
- (avg_tx, empty_pct, total_user_tx, total_blocks)
- }
- let (avg_day, empty_day, total_day, blocks_day) =
- calc_period_stats(&daily_stats, current_day, current_day);
- let (avg_week, empty_week, total_week, blocks_week) =
- calc_period_stats(&daily_stats, current_day.saturating_sub(6), current_day);
- let (avg_month, empty_month, total_month, blocks_month) =
- calc_period_stats(&daily_stats, current_day.saturating_sub(29), current_day);
- let (avg_year, empty_year, total_year, blocks_year) =
- calc_period_stats(&daily_stats, current_day.saturating_sub(364), current_day);
- fn stats_to_json(avg: f64, empty_pct: f64, total: u64, block_count: u64) -> JsonValue {
- JsonValue::Object(HashMap::from([
- ("avg_tx".to_string(), JsonValue::Number(avg)),
- ("empty_pct".to_string(), JsonValue::Number(empty_pct)),
- ("total_tx".to_string(), JsonValue::Number(total as f64)),
- ("block_count".to_string(), JsonValue::Number(block_count as f64)),
- ]))
- }
- let tx_per_block = JsonValue::Object(HashMap::from([
- ("last_day".to_string(), stats_to_json(avg_day, empty_day, total_day, blocks_day)),
- ("last_week".to_string(), stats_to_json(avg_week, empty_week, total_week, blocks_week)),
- (
- "last_month".to_string(),
- stats_to_json(avg_month, empty_month, total_month, blocks_month),
- ),
- ("last_year".to_string(), stats_to_json(avg_year, empty_year, total_year, blocks_year)),
- ]));
- JsonResponse::new(
- JsonValue::Object(HashMap::from([
- ("daily_stats".to_string(), JsonValue::Array(daily_json)),
- ("monthly_growth".to_string(), JsonValue::Array(monthly_json)),
- ("tx_per_block".to_string(), tx_per_block),
- ])),
- id,
- )
- .into()
- }
- }
|