/* 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 .
*/
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,
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,
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,
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,
txs: Vec,
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: i64, _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: i64, _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: i64, params: JsonValue) -> JsonResult {
let Some(params) = params.get::>() 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::().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: i64, params: JsonValue) -> JsonResult {
let Some(params) = params.get::>() 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::().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::().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: i64, params: JsonValue) -> JsonResult {
let Some(params) = params.get::>() 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::().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: i64, params: JsonValue) -> JsonResult {
let Some(params) = params.get::>() 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::().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: i64, _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: i64, params: JsonValue) -> JsonResult {
let Some(params) = params.get::>() 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::().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: i64, params: JsonValue) -> JsonResult {
let locked_filter = if let Some(params) = params.get::>() {
if !params.is_empty() {
params[0].get::().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 = 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: i64, _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: i64, _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 = 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 = 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()
}
}