From 5f60353aed42826f9ea154bf915641f056d0555b Mon Sep 17 00:00:00 2001 From: gonzalezzfelipe Date: Thu, 6 Feb 2025 15:17:01 -0300 Subject: [PATCH] Add several block endpoints --- src/serve/minibf/mod.rs | 8 ++ .../routes/blocks/hash_or_number/addresses.rs | 99 +++++++++++++++ .../routes/blocks/hash_or_number/mod.rs | 33 +++++ .../routes/blocks/hash_or_number/next.rs | 53 ++++++++ .../routes/blocks/hash_or_number/previous.rs | 55 +++++++++ .../routes/blocks/hash_or_number/txs.rs | 35 ++++++ src/serve/minibf/routes/blocks/latest/mod.rs | 39 +++--- src/serve/minibf/routes/blocks/mod.rs | 116 ++++++++++++++++-- src/serve/minibf/routes/blocks/slot/mod.rs | 1 + .../minibf/routes/blocks/slot/slot_number.rs | 43 +++++++ 10 files changed, 446 insertions(+), 36 deletions(-) create mode 100644 src/serve/minibf/routes/blocks/hash_or_number/addresses.rs create mode 100644 src/serve/minibf/routes/blocks/hash_or_number/mod.rs create mode 100644 src/serve/minibf/routes/blocks/hash_or_number/next.rs create mode 100644 src/serve/minibf/routes/blocks/hash_or_number/previous.rs create mode 100644 src/serve/minibf/routes/blocks/hash_or_number/txs.rs create mode 100644 src/serve/minibf/routes/blocks/slot/mod.rs create mode 100644 src/serve/minibf/routes/blocks/slot/slot_number.rs diff --git a/src/serve/minibf/mod.rs b/src/serve/minibf/mod.rs index 866ca42..5021298 100644 --- a/src/serve/minibf/mod.rs +++ b/src/serve/minibf/mod.rs @@ -40,10 +40,18 @@ pub async fn serve( .mount( "/", routes![ + // Addresses routes::addresses::address::utxo::route, routes::addresses::address::utxo::asset::route, + // Blocks routes::blocks::latest::route, routes::blocks::latest::txs::route, + routes::blocks::hash_or_number::route, + routes::blocks::hash_or_number::addresses::route, + routes::blocks::hash_or_number::next::route, + routes::blocks::hash_or_number::previous::route, + routes::blocks::hash_or_number::txs::route, + routes::blocks::slot::slot_number::route, ], ) .launch() diff --git a/src/serve/minibf/routes/blocks/hash_or_number/addresses.rs b/src/serve/minibf/routes/blocks/hash_or_number/addresses.rs new file mode 100644 index 0000000..293bc2c --- /dev/null +++ b/src/serve/minibf/routes/blocks/hash_or_number/addresses.rs @@ -0,0 +1,99 @@ +use itertools::Itertools; +use pallas::ledger::traverse::MultiEraBlock; +use rocket::{get, http::Status, State}; +use serde::{Deserialize, Serialize}; +use std::collections::HashMap; + +use crate::{ + state::LedgerStore, + wal::{redb::WalStore, ReadUtils, WalReader}, +}; + +#[derive(Debug, Clone, Serialize, Deserialize, Default)] +pub struct BlockAddress { + address: String, + transactions: Vec, +} + +#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq, Eq, Hash)] +pub struct BlockAddressTx { + tx_hash: String, +} + +#[get("/blocks//addresses", rank = 2)] +pub fn route( + hash_or_number: String, + wal: &State, + ledger: &State, +) -> Result>, Status> { + let maybe_raw = wal + .crawl_from(None) + .map_err(|_| Status::ServiceUnavailable)? + .into_blocks() + .find(|maybe_raw| match maybe_raw { + Some(raw) => match MultiEraBlock::decode(&raw.body) { + Ok(block) => { + block.hash().to_string() == hash_or_number + || block.number().to_string() == hash_or_number + } + Err(_) => false, + }, + None => false, + }); + + match maybe_raw { + Some(Some(raw)) => { + let block = MultiEraBlock::decode(&raw.body).map_err(|_| Status::ServiceUnavailable)?; + let mut addresses = HashMap::new(); + + for tx in block.txs() { + // Handle inputs + let utxos = ledger + .get_utxos(tx.inputs().iter().map(Into::into).collect()) + .map_err(|_| Status::ServiceUnavailable)?; + + for (_, eracbor) in utxos { + let parsed = + pallas::ledger::traverse::MultiEraOutput::decode(eracbor.0, &eracbor.1) + .map_err(|_| Status::InternalServerError)?; + let address = parsed + .address() + .map_err(|_| Status::ServiceUnavailable)? + .to_bech32() + .map_err(|_| Status::ServiceUnavailable)?; + let tx_hash = tx.hash().to_string(); + addresses + .entry(address.to_string()) + .or_insert_with(Vec::new) + .push(BlockAddressTx { tx_hash }); + } + + // Handle outputs + for output in tx.outputs() { + let address = output + .address() + .map_err(|_| Status::ServiceUnavailable)? + .to_bech32() + .map_err(|_| Status::ServiceUnavailable)?; + let tx_hash = tx.hash().to_string(); + addresses + .entry(address.to_string()) + .or_insert_with(Vec::new) + .push(BlockAddressTx { tx_hash }); + } + } + + Ok(rocket::serde::json::Json( + addresses + .into_iter() + .sorted_by_key(|(address, _)| address.clone()) + .map(|(address, transactions)| BlockAddress { + address, + transactions: transactions.into_iter().unique().collect(), + }) + .collect(), + )) + } + _ => Err(Status::NotFound), + } +} diff --git a/src/serve/minibf/routes/blocks/hash_or_number/mod.rs b/src/serve/minibf/routes/blocks/hash_or_number/mod.rs new file mode 100644 index 0000000..d7d841c --- /dev/null +++ b/src/serve/minibf/routes/blocks/hash_or_number/mod.rs @@ -0,0 +1,33 @@ +use pallas::ledger::traverse::wellknown::GenesisValues; +use rocket::{get, http::Status, State}; +use std::sync::Arc; + +use crate::{ledger::pparams::Genesis, wal::redb::WalStore}; + +use super::Block; + +pub mod addresses; +pub mod next; +pub mod previous; +pub mod txs; + +#[get("/blocks/", rank = 2)] +pub fn route( + hash_or_number: String, + genesis: &State>, + wal: &State, +) -> Result, Status> { + let Some(magic) = genesis.shelley.network_magic else { + return Err(Status::ServiceUnavailable); + }; + + let Some(values) = GenesisValues::from_magic(magic as u64) else { + return Err(Status::ServiceUnavailable); + }; + + let block = Block::find_in_wal(wal, &hash_or_number, &values)?; + match block { + Some(block) => Ok(rocket::serde::json::Json(block)), + None => Err(Status::NotFound), + } +} diff --git a/src/serve/minibf/routes/blocks/hash_or_number/next.rs b/src/serve/minibf/routes/blocks/hash_or_number/next.rs new file mode 100644 index 0000000..4666d19 --- /dev/null +++ b/src/serve/minibf/routes/blocks/hash_or_number/next.rs @@ -0,0 +1,53 @@ +use pallas::ledger::traverse::{wellknown::GenesisValues, MultiEraBlock}; +use rocket::{get, http::Status, State}; +use std::sync::Arc; + +use crate::{ + ledger::pparams::Genesis, + wal::{redb::WalStore, ReadUtils, WalReader}, +}; + +use super::Block; + +#[get("/blocks//next", rank = 2)] +pub fn route( + hash_or_number: String, + genesis: &State>, + wal: &State, +) -> Result, Status> { + let Some(magic) = genesis.shelley.network_magic else { + return Err(Status::ServiceUnavailable); + }; + + let Some(values) = GenesisValues::from_magic(magic as u64) else { + return Err(Status::ServiceUnavailable); + }; + + let iterator = wal + .crawl_from(None) + .map_err(|_| Status::ServiceUnavailable)? + .into_blocks(); + + let mut prev = None; + for raw in iterator.flatten() { + let block = MultiEraBlock::decode(&raw.body).map_err(|_| Status::ServiceUnavailable)?; + if block.hash().to_string() == hash_or_number + || block.number().to_string() == hash_or_number + { + break; + } else { + prev = Some(raw.hash.to_string()); + } + } + match prev { + Some(block) => { + match Block::find_in_wal(wal, &block, &values) + .map_err(|_| Status::ServiceUnavailable)? + { + Some(block) => Ok(rocket::serde::json::Json(block)), + None => Err(Status::NotFound), + } + } + None => Err(Status::NotFound), + } +} diff --git a/src/serve/minibf/routes/blocks/hash_or_number/previous.rs b/src/serve/minibf/routes/blocks/hash_or_number/previous.rs new file mode 100644 index 0000000..50d24a4 --- /dev/null +++ b/src/serve/minibf/routes/blocks/hash_or_number/previous.rs @@ -0,0 +1,55 @@ +use pallas::ledger::traverse::{wellknown::GenesisValues, MultiEraBlock}; +use rocket::{get, http::Status, State}; +use std::sync::Arc; + +use crate::{ + ledger::pparams::Genesis, + wal::{redb::WalStore, ReadUtils, WalReader}, +}; + +use super::Block; + +#[get("/blocks//previous", rank = 2)] +pub fn route( + hash_or_number: String, + genesis: &State>, + wal: &State, +) -> Result, Status> { + let Some(magic) = genesis.shelley.network_magic else { + return Err(Status::ServiceUnavailable); + }; + + let Some(values) = GenesisValues::from_magic(magic as u64) else { + return Err(Status::ServiceUnavailable); + }; + + // Reversed iterator + let iterator = wal + .crawl_from(None) + .map_err(|_| Status::ServiceUnavailable)? + .rev() + .into_blocks(); + + let mut next = None; + for raw in iterator.flatten() { + let block = MultiEraBlock::decode(&raw.body).map_err(|_| Status::ServiceUnavailable)?; + if block.hash().to_string() == hash_or_number + || block.number().to_string() == hash_or_number + { + break; + } else { + next = Some(raw.hash.to_string()); + } + } + match next { + Some(block) => { + match Block::find_in_wal(wal, &block, &values) + .map_err(|_| Status::ServiceUnavailable)? + { + Some(block) => Ok(rocket::serde::json::Json(block)), + None => Err(Status::NotFound), + } + } + None => Err(Status::NotFound), + } +} diff --git a/src/serve/minibf/routes/blocks/hash_or_number/txs.rs b/src/serve/minibf/routes/blocks/hash_or_number/txs.rs new file mode 100644 index 0000000..38b2eb8 --- /dev/null +++ b/src/serve/minibf/routes/blocks/hash_or_number/txs.rs @@ -0,0 +1,35 @@ +use pallas::ledger::traverse::MultiEraBlock; +use rocket::{get, http::Status, State}; + +use crate::wal::{redb::WalStore, ReadUtils, WalReader}; + +#[get("/blocks//txs", rank = 2)] +pub fn route( + hash_or_number: String, + wal: &State, +) -> Result>, Status> { + let maybe_raw = wal + .crawl_from(None) + .map_err(|_| Status::ServiceUnavailable)? + .into_blocks() + .find(|maybe_raw| match maybe_raw { + Some(raw) => match MultiEraBlock::decode(&raw.body) { + Ok(block) => { + block.hash().to_string() == hash_or_number + || block.number().to_string() == hash_or_number + } + Err(_) => false, + }, + None => false, + }); + + match maybe_raw { + Some(Some(raw)) => { + let block = MultiEraBlock::decode(&raw.body).map_err(|_| Status::ServiceUnavailable)?; + Ok(rocket::serde::json::Json( + block.txs().iter().map(|tx| tx.hash().to_string()).collect(), + )) + } + _ => Err(Status::NotFound), + } +} diff --git a/src/serve/minibf/routes/blocks/latest/mod.rs b/src/serve/minibf/routes/blocks/latest/mod.rs index a0d43cd..2d8d649 100644 --- a/src/serve/minibf/routes/blocks/latest/mod.rs +++ b/src/serve/minibf/routes/blocks/latest/mod.rs @@ -1,6 +1,6 @@ pub mod txs; -use pallas::ledger::traverse::{wellknown::GenesisValues, MultiEraBlock}; +use pallas::ledger::traverse::wellknown::GenesisValues; use rocket::{get, http::Status, State}; use std::sync::Arc; @@ -10,39 +10,32 @@ use crate::{ wal::{redb::WalStore, WalReader}, }; -#[get("/blocks/latest")] +#[get("/blocks/latest", rank = 1)] pub fn route( genesis: &State>, wal: &State, ) -> Result, Status> { + let Some(magic) = genesis.shelley.network_magic else { + return Err(Status::ServiceUnavailable); + }; + + let Some(values) = GenesisValues::from_magic(magic as u64) else { + return Err(Status::ServiceUnavailable); + }; + let tip = wal.find_tip().map_err(|_| Status::ServiceUnavailable)?; match tip { None => Err(Status::ServiceUnavailable), Some((_, point)) => { - let raw_block = wal + let raw = wal .read_block(&point) .map_err(|_| Status::ServiceUnavailable)?; - let block = - MultiEraBlock::decode(&raw_block.body).map_err(|_| Status::ServiceUnavailable)?; - - let Some(magic) = genesis.shelley.network_magic else { - return Err(Status::ServiceUnavailable); - }; - let Some(values) = GenesisValues::from_magic(magic as u64) else { - return Err(Status::ServiceUnavailable); - }; - let (epoch, epoch_slot) = block.epoch(&values); - Ok(rocket::serde::json::Json(Block { - slot: Some(block.slot()), - hash: block.hash().to_string(), - tx_count: block.tx_count() as u64, - size: block.size() as u64, - epoch: Some(epoch), - epoch_slot: Some(epoch_slot), - height: Some(block.number()), - ..Default::default() - })) + match Block::find_in_wal(wal, &raw.hash.to_string(), &values) { + Ok(Some(block)) => Ok(rocket::serde::json::Json(block)), + Ok(None) => Err(Status::NotFound), + Err(_) => Err(Status::ServiceUnavailable), + } } } } diff --git a/src/serve/minibf/routes/blocks/mod.rs b/src/serve/minibf/routes/blocks/mod.rs index 6f8b49e..d52cb01 100644 --- a/src/serve/minibf/routes/blocks/mod.rs +++ b/src/serve/minibf/routes/blocks/mod.rs @@ -1,9 +1,12 @@ -use pallas::ledger::traverse::MultiEraBlock; +use pallas::ledger::traverse::{wellknown::GenesisValues, MultiEraBlock}; +use rocket::http::Status; use serde::{Deserialize, Serialize}; -use crate::wal::RawBlock; +use crate::wal::{redb::WalStore, ReadUtils, WalReader}; +pub mod hash_or_number; pub mod latest; +pub mod slot; #[derive(Debug, Clone, Serialize, Deserialize, Default)] pub struct Block { @@ -11,7 +14,6 @@ pub struct Block { pub hash: String, pub tx_count: u64, pub size: u64, - pub time: u64, pub height: Option, pub epoch: Option, @@ -27,16 +29,104 @@ pub struct Block { pub confirmations: u64, } -impl From<&RawBlock> for Block { - fn from(raw_block: &RawBlock) -> Self { - let block = MultiEraBlock::decode(&raw_block.body).unwrap(); - Self { - slot: Some(block.slot()), - hash: block.hash().to_string(), - tx_count: block.tx_count() as u64, - size: block.size() as u64, - // height: Some(block.epoch(genesis) as u64), - ..Default::default() +impl Block { + pub fn find_in_wal( + wal: &WalStore, + hash_or_number: &str, + genesis: &GenesisValues, + ) -> Result, Status> { + let iterator = wal + .crawl_from(None) + .map_err(|_| Status::ServiceUnavailable)? + .into_blocks(); + + let mut curr = None; + let mut next = None; + let mut confirmations = 0; + + // Scan the iterator, if found set the current block and continue to set next and count + // confirmations. + for value in iterator { + if curr.is_none() { + if let Some(raw) = value { + let block = + MultiEraBlock::decode(&raw.body).map_err(|_| Status::ServiceUnavailable)?; + if block.hash().to_string() == hash_or_number + || block.number().to_string() == hash_or_number + { + curr = Some(raw.body); + } + } + } else { + confirmations += 1; + if next.is_none() { + if let Some(raw) = value { + next = Some( + MultiEraBlock::decode(&raw.body) + .map_err(|_| Status::ServiceUnavailable)? + .hash() + .to_string(), + ); + } + } + } + } + match curr { + Some(bytes) => { + // Decode the block due to lifetime headaches. + let block = + MultiEraBlock::decode(&bytes).map_err(|_| Status::ServiceUnavailable)?; + + let header = block.header(); + let prev = header.previous_hash().map(|h| h.to_string()); + let block_vrf = match header.vrf_vkey() { + Some(v) => Some( + bech32::encode::(bech32::Hrp::parse("vrf_vk").unwrap(), v) + .map_err(|_| Status::ServiceUnavailable)?, + ), + None => None, + }; + let (epoch, epoch_slot) = block.epoch(genesis); + Ok(Some(Self { + slot: Some(block.slot()), + hash: block.hash().to_string(), + tx_count: block.tx_count() as u64, + size: block.body_size().unwrap_or(0) as u64, + epoch: Some(epoch), + epoch_slot: Some(epoch_slot), + height: Some(block.number()), + previous_block: prev.clone(), + next_block: next.clone(), + confirmations, + block_vrf, + output: match block.tx_count() { + 0 => None, + _ => Some( + block + .txs() + .iter() + .map(|tx| { + tx.outputs().iter().map(|o| o.value().coin()).sum::() + }) + .sum::() + .to_string(), + ), + }, + fees: match block.tx_count() { + 0 => None, + _ => Some( + block + .txs() + .iter() + .map(|tx| tx.fee().unwrap_or(0)) + .sum::() + .to_string(), + ), + }, + ..Default::default() + })) + } + _ => Err(Status::ServiceUnavailable), } } } diff --git a/src/serve/minibf/routes/blocks/slot/mod.rs b/src/serve/minibf/routes/blocks/slot/mod.rs new file mode 100644 index 0000000..a616ede --- /dev/null +++ b/src/serve/minibf/routes/blocks/slot/mod.rs @@ -0,0 +1 @@ +pub mod slot_number; diff --git a/src/serve/minibf/routes/blocks/slot/slot_number.rs b/src/serve/minibf/routes/blocks/slot/slot_number.rs new file mode 100644 index 0000000..2c7b562 --- /dev/null +++ b/src/serve/minibf/routes/blocks/slot/slot_number.rs @@ -0,0 +1,43 @@ +use pallas::ledger::traverse::wellknown::GenesisValues; +use rocket::{get, http::Status, State}; +use std::sync::Arc; + +use crate::{ + ledger::pparams::Genesis, + serve::minibf::routes::blocks::Block, + wal::{redb::WalStore, ReadUtils, WalReader}, +}; + +#[get("/blocks/slot/")] +pub fn route( + slot_number: u64, + genesis: &State>, + wal: &State, +) -> Result, Status> { + let Some(magic) = genesis.shelley.network_magic else { + return Err(Status::ServiceUnavailable); + }; + + let Some(values) = GenesisValues::from_magic(magic as u64) else { + return Err(Status::ServiceUnavailable); + }; + + let point = wal + .crawl_from(None) + .map_err(|_| Status::ServiceUnavailable)? + .filter_forward() + .into_blocks() + .find(|maybe_block| match maybe_block { + Some(block) => block.slot == slot_number, + None => false, + }); + + match point { + Some(Some(raw)) => match Block::find_in_wal(wal, &raw.hash.to_string(), &values) { + Ok(Some(block)) => Ok(rocket::serde::json::Json(block)), + Ok(None) => Err(Status::NotFound), + Err(_) => Err(Status::ServiceUnavailable), + }, + _ => Err(Status::NotFound), + } +}