From 199e5ad9358f644f49398aefd0778273c98a189c Mon Sep 17 00:00:00 2001 From: Matthew Hounslow Date: Wed, 5 Nov 2025 11:29:52 -0800 Subject: [PATCH 01/20] Refactor: Introduce standardized `RESTError` and `QueryError` types for consistent error handling across modules, replacing raw strings or `anyhow::Error` --- common/src/app_error.rs | 220 ++++++++ common/src/lib.rs | 1 + common/src/queries/accounts.rs | 4 +- common/src/queries/addresses.rs | 4 +- common/src/queries/blocks.rs | 5 +- common/src/queries/errors.rs | 105 ++++ common/src/queries/mod.rs | 1 + common/src/queries/utils.rs | 43 +- common/src/queries/utxos.rs | 4 +- common/src/rest_helper.rs | 37 +- common/src/serialization.rs | 18 +- modules/accounts_state/src/accounts_state.rs | 55 +- modules/address_state/src/address_state.rs | 37 +- modules/drdd_state/src/rest.rs | 24 +- .../rest_blockfrost/src/handlers/accounts.rs | 498 +++++------------- .../rest_blockfrost/src/handlers/addresses.rs | 44 +- .../rest_blockfrost/src/rest_blockfrost.rs | 29 +- modules/spdd_state/src/rest.rs | 21 +- 18 files changed, 615 insertions(+), 535 deletions(-) create mode 100644 common/src/app_error.rs create mode 100644 common/src/queries/errors.rs diff --git a/common/src/app_error.rs b/common/src/app_error.rs new file mode 100644 index 00000000..60824903 --- /dev/null +++ b/common/src/app_error.rs @@ -0,0 +1,220 @@ +use crate::queries::errors::QueryError; +use anyhow::Error as AnyhowError; +use caryatid_module_rest_server::messages::RESTResponse; +use std::fmt; +use std::num::ParseIntError; + +/// Standard error types for the application +#[derive(Debug)] +pub enum RESTError { + BadRequest(String), + NotFound(String), + InternalServerError(String), + NotImplemented(String), +} + +impl RESTError { + /// Get the HTTP status code for this error + pub fn status_code(&self) -> u16 { + match self { + RESTError::BadRequest(_) => 400, + RESTError::NotFound(_) => 404, + RESTError::InternalServerError(_) => 500, + RESTError::NotImplemented(_) => 501, + } + } + + /// Get the error message + pub fn message(&self) -> String { + match self { + RESTError::BadRequest(msg) => msg.clone(), + RESTError::NotFound(msg) => msg.clone(), + RESTError::InternalServerError(msg) => msg.clone(), + RESTError::NotImplemented(msg) => msg.clone(), + } + } + + /// Parameter missing error + pub fn param_missing(param_name: &str) -> Self { + RESTError::BadRequest(format!("{} parameter is missing", param_name)) + } + + /// Feature hasn't been implemented error + pub fn not_implemented(feature: &str) -> Self { + RESTError::NotImplemented(format!("{} not yet implemented", feature)) + } + + /// Storage disabled error + pub fn storage_disabled(storage_type: &str) -> Self { + RESTError::NotImplemented(format!("{} storage is disabled in config", storage_type)) + } + + /// Invalid hex string error + pub fn invalid_hex() -> Self { + RESTError::BadRequest("Invalid hex string".to_string()) + } + + /// Invalid parameter error + pub fn invalid_param(param_name: &str, reason: &str) -> Self { + RESTError::BadRequest(format!("Invalid {}: {}", param_name, reason)) + } + + /// Resource wasn't found error + pub fn not_found(resource: &str) -> Self { + RESTError::NotFound(format!("{} not found", resource)) + } + + /// Unexpected response error + pub fn unexpected_response(context: &str) -> Self { + RESTError::InternalServerError(format!("Unexpected response while {}", context)) + } + + /// Query failed error + pub fn query_failed(error: impl fmt::Display) -> Self { + RESTError::InternalServerError(format!("Query failed: {}", error)) + } + + /// Serialization failed error + pub fn serialization_failed(what: &str, error: impl fmt::Display) -> Self { + RESTError::InternalServerError(format!("Failed to serialize {}: {}", what, error)) + } + + /// Encoding failed error + pub fn encoding_failed(what: &str) -> Self { + RESTError::InternalServerError(format!("Failed to encode {}", what)) + } +} + +impl fmt::Display for RESTError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}", self.message()) + } +} + +impl std::error::Error for RESTError {} + +/// Convert RESTError to RESTResponse +impl From for RESTResponse { + fn from(error: RESTError) -> Self { + RESTResponse::with_text(error.status_code(), &error.message()) + } +} + +/// Convert anyhow::Error to RESTError (default to 500) +impl From for RESTError { + fn from(error: AnyhowError) -> Self { + RESTError::InternalServerError(error.to_string()) + } +} + +/// Convert ParseIntError to RESTError (400 Bad Request) +impl From for RESTError { + fn from(error: ParseIntError) -> Self { + RESTError::BadRequest(error.to_string()) + } +} + +/// Convert hex decode errors to RESTError (400 Bad Request) +impl From for RESTError { + fn from(error: hex::FromHexError) -> Self { + RESTError::BadRequest(format!("Invalid hex string: {}", error)) + } +} + +/// Convert bech32 decode errors to RESTError (400 Bad Request) +impl From for RESTError { + fn from(error: bech32::DecodeError) -> Self { + RESTError::BadRequest(format!("Invalid bech32 encoding: {}", error)) + } +} + +/// Convert bech32 encode errors to RESTError (500 Internal Server Error) +impl From for RESTError { + fn from(error: bech32::EncodeError) -> Self { + RESTError::InternalServerError(format!("Failed to encode bech32: {}", error)) + } +} + +/// Convert serde_json errors to RESTError (500 Internal Server Error) +impl From for RESTError { + fn from(error: serde_json::Error) -> Self { + RESTError::InternalServerError(format!("JSON serialization failed: {}", error)) + } +} + +impl From for RESTError { + fn from(error: QueryError) -> Self { + match error { + QueryError::NotFound { resource } => RESTError::NotFound(resource), + QueryError::QueryFailed { message } => RESTError::InternalServerError(message), + QueryError::StorageDisabled { storage_type } => { + RESTError::NotImplemented(format!("{} storage is disabled in config", storage_type)) + } + QueryError::InvalidRequest { message } => RESTError::BadRequest(message), + } + } +} + +pub type AppResult = Result; + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_bad_request_error() { + let error = RESTError::BadRequest("Invalid parameter".to_string()); + assert_eq!(error.status_code(), 400); + assert_eq!(error.message(), "Invalid parameter"); + } + + #[test] + fn test_not_found_error() { + let error = RESTError::NotFound("Account not found".to_string()); + assert_eq!(error.status_code(), 404); + assert_eq!(error.message(), "Account not found"); + } + + #[test] + fn test_internal_error() { + let error = RESTError::InternalServerError("Database connection failed".to_string()); + assert_eq!(error.status_code(), 500); + assert_eq!(error.message(), "Database connection failed"); + } + + #[test] + fn test_from_anyhow() { + let anyhow_err = anyhow::anyhow!("Something went wrong"); + let app_error = RESTError::from(anyhow_err); + assert_eq!(app_error.status_code(), 500); + } + + #[test] + fn test_from_parse_int_error() { + let result: Result = "not_a_number".parse(); + let app_error: RESTError = result.unwrap_err().into(); + assert_eq!(app_error.status_code(), 400); + } + + #[test] + fn test_from_hex_error() { + let result = hex::decode("not_hex_gg"); + let app_error: RESTError = result.unwrap_err().into(); + assert_eq!(app_error.status_code(), 400); + } + + #[test] + fn test_to_rest_response() { + let error = RESTError::BadRequest("Invalid stake address".to_string()); + let response: RESTResponse = error.into(); + assert_eq!(response.code, 400); + assert_eq!(response.body, "Invalid stake address"); + } + + #[test] + fn test_convenience_constructors() { + assert_eq!(RESTError::invalid_hex().status_code(), 400); + assert_eq!(RESTError::not_found("Asset").message(), "Asset not found"); + assert_eq!(RESTError::not_implemented("Feature").status_code(), 501); + } +} diff --git a/common/src/lib.rs b/common/src/lib.rs index 0ca5a58a..839af3e4 100644 --- a/common/src/lib.rs +++ b/common/src/lib.rs @@ -22,6 +22,7 @@ pub mod stake_addresses; pub mod state_history; pub mod types; pub mod validation; +pub mod app_error; // Flattened re-exports pub use self::address::*; diff --git a/common/src/queries/accounts.rs b/common/src/queries/accounts.rs index 80213455..2fdaa55f 100644 --- a/common/src/queries/accounts.rs +++ b/common/src/queries/accounts.rs @@ -3,6 +3,7 @@ use std::collections::HashMap; use crate::{ DRepChoice, PoolId, PoolLiveStakeInfo, RewardType, ShelleyAddress, StakeAddress, TxIdentifier, }; +use crate::queries::errors::QueryError; pub const DEFAULT_ACCOUNTS_QUERY_TOPIC: (&str, &str) = ("accounts-state-query-topic", "cardano.query.accounts"); @@ -81,8 +82,7 @@ pub enum AccountsStateQueryResponse { DrepDelegators(DrepDelegators), AccountsDrepDelegationsMap(HashMap>), - NotFound, - Error(String), + Error(QueryError), } #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] diff --git a/common/src/queries/addresses.rs b/common/src/queries/addresses.rs index 01985834..3bc3d0e8 100644 --- a/common/src/queries/addresses.rs +++ b/common/src/queries/addresses.rs @@ -1,4 +1,5 @@ use crate::{Address, AddressTotals, TxIdentifier, UTxOIdentifier}; +use crate::queries::errors::QueryError; pub const DEFAULT_ADDRESS_QUERY_TOPIC: (&str, &str) = ("address-state-query-topic", "cardano.query.address"); @@ -15,6 +16,5 @@ pub enum AddressStateQueryResponse { AddressTotals(AddressTotals), AddressUTxOs(Vec), AddressTransactions(Vec), - NotFound, - Error(String), + Error(QueryError) } diff --git a/common/src/queries/blocks.rs b/common/src/queries/blocks.rs index b196a1ae..62c5b1da 100644 --- a/common/src/queries/blocks.rs +++ b/common/src/queries/blocks.rs @@ -7,6 +7,7 @@ use cryptoxide::hashing::blake2b::Blake2b; use serde::ser::{Serialize, SerializeStruct, Serializer}; use serde_with::{hex::Hex, serde_as}; use std::collections::HashMap; +use crate::queries::errors::QueryError; pub const DEFAULT_BLOCKS_QUERY_TOPIC: (&str, &str) = ("blocks-state-query-topic", "cardano.query.blocks"); @@ -90,8 +91,8 @@ pub enum BlocksStateQueryResponse { BlockInvolvedAddresses(BlockInvolvedAddresses), BlockHashes(BlockHashes), TransactionHashes(TransactionHashes), - NotFound, - Error(String), + + Error(QueryError), } #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] diff --git a/common/src/queries/errors.rs b/common/src/queries/errors.rs new file mode 100644 index 00000000..4c333c2f --- /dev/null +++ b/common/src/queries/errors.rs @@ -0,0 +1,105 @@ +use caryatid_module_rest_server::messages::RESTResponse; +use serde::{Deserialize, Serialize}; +use std::fmt; + +/// Common error type for all state query responses +#[derive(Debug, Clone, Serialize, Deserialize)] +pub enum QueryError { + /// The requested resource was not found + NotFound { + resource: String + }, + + /// An error occurred while processing the query + QueryFailed { + message: String + }, + + /// Storage backend is disabled in configuration + StorageDisabled { + storage_type: String + }, + + /// Invalid request parameters + InvalidRequest { + message: String + }, + + /// One or more resources in a batch query were not found + PartialNotFound { + message: String, + }, + + /// Query variant is not implemented yet + NotImplemented { + query: String, + }, +} + +impl QueryError { + pub fn not_found(resource: impl Into) -> Self { + Self::NotFound { + resource: resource.into() + } + } + + pub fn query_failed(message: impl Into) -> Self { + Self::QueryFailed { + message: message.into() + } + } + + pub fn storage_disabled(storage_type: impl Into) -> Self { + Self::StorageDisabled { + storage_type: storage_type.into() + } + } + + pub fn invalid_request(message: impl Into) -> Self { + Self::InvalidRequest { + message: message.into() + } + } + + pub fn partial_not_found(message: impl Into) -> Self { + Self::PartialNotFound { + message: message.into() + } + } + + pub fn not_implemented(query: impl Into) -> Self { + Self::NotImplemented { + query: query.into() + } + } +} + +impl fmt::Display for QueryError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::NotFound { resource } => write!(f, "Not found: {}", resource), + Self::QueryFailed { message } => write!(f, "Query failed: {}", message), + Self::StorageDisabled { storage_type } => write!(f, "{} storage is not enabled", storage_type), + Self::InvalidRequest { message } => write!(f, "Invalid request: {}", message), + Self::PartialNotFound { message } => write!(f, "Partial result: {}", message), + Self::NotImplemented { query } => write!(f, "Query not implemented: {}", query), + } + } +} + +/// Convert QueryError to RESTResponse with appropriate status codes +/// Not entirely sure where this should go +impl From for RESTResponse { + fn from(error: QueryError) -> Self { + match &error { + QueryError::NotFound { .. } => RESTResponse::with_text(404, &error.to_string()), + QueryError::QueryFailed { .. } => RESTResponse::with_text(500, &error.to_string()), + QueryError::StorageDisabled { .. } => RESTResponse::with_text(501, &error.to_string()), + QueryError::NotImplemented { .. } => RESTResponse::with_text(501, &error.to_string()), + QueryError::InvalidRequest { .. } => RESTResponse::with_text(400, &error.to_string()), + QueryError::PartialNotFound { .. } => RESTResponse::with_text(206, &error.to_string()), + } + } +} + +impl std::error::Error for QueryError {} diff --git a/common/src/queries/mod.rs b/common/src/queries/mod.rs index a03a82ae..9c6664d4 100644 --- a/common/src/queries/mod.rs +++ b/common/src/queries/mod.rs @@ -20,6 +20,7 @@ pub mod spdd; pub mod transactions; pub mod utils; pub mod utxos; +pub mod errors; pub fn get_query_topic(context: Arc>, topic: (&str, &str)) -> String { context.config.get_string(topic.0).unwrap_or_else(|_| topic.1.to_string()) diff --git a/common/src/queries/utils.rs b/common/src/queries/utils.rs index 20ce4f4e..d057ba83 100644 --- a/common/src/queries/utils.rs +++ b/common/src/queries/utils.rs @@ -4,18 +4,22 @@ use serde::Serialize; use std::sync::Arc; use crate::messages::{Message, RESTResponse}; +use crate::queries::errors::QueryError; pub async fn query_state( context: &Arc>, topic: &str, request_msg: Arc, extractor: F, -) -> Result +) -> Result where - F: FnOnce(Message) -> Result, + F: FnOnce(Message) -> Result, { - // build message to query - let raw_msg = context.message_bus.request(topic, request_msg).await?; + let raw_msg = context + .message_bus + .request(topic, request_msg) + .await + .map_err(|e| QueryError::query_failed(e.to_string()))?; let message = Arc::try_unwrap(raw_msg).unwrap_or_else(|arc| (*arc).clone()); @@ -30,32 +34,25 @@ pub async fn rest_query_state( extractor: F, ) -> Result where - F: FnOnce(Message) -> Option, anyhow::Error>>, + F: FnOnce(Message) -> Option>, T: Serialize, { let result = query_state(context, topic, request_msg, |response| { match extractor(response) { - Some(response) => response, - None => Err(anyhow::anyhow!( - "Unexpected response message type while calling {topic}" - )), + Some(result) => result, + None => Err(QueryError::query_failed(format!( + "Unexpected response message type from {topic}" + ))), } }) .await; + match result { - Ok(result) => match result { - Some(result) => match serde_json::to_string(&result) { - Ok(json) => Ok(RESTResponse::with_json(200, &json)), - Err(e) => Ok(RESTResponse::with_text( - 500, - &format!("Internal server error while calling {topic}: {e}"), - )), - }, - None => Ok(RESTResponse::with_text(404, "Not found")), - }, - Err(e) => Ok(RESTResponse::with_text( - 500, - &format!("Internal server error while calling {topic}: {e}"), - )), + Ok(data) => { + let json = serde_json::to_string_pretty(&data) + .map_err(|e| QueryError::query_failed(format!("JSON serialization failed: {e}")))?; + Ok(RESTResponse::with_json(200, &json)) + } + Err(query_error) => Ok(query_error.into()), } } diff --git a/common/src/queries/utxos.rs b/common/src/queries/utxos.rs index bb75c4c0..80d9a2ec 100644 --- a/common/src/queries/utxos.rs +++ b/common/src/queries/utxos.rs @@ -1,4 +1,5 @@ use crate::{UTxOIdentifier, Value}; +use crate::queries::errors::QueryError; pub const DEFAULT_UTXOS_QUERY_TOPIC: (&str, &str) = ("utxo-state-query-topic", "cardano.query.utxos"); @@ -13,6 +14,5 @@ pub enum UTxOStateQuery { #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] pub enum UTxOStateQueryResponse { UTxOsSum(Value), - NotFound, - Error(String), + Error(QueryError), } diff --git a/common/src/rest_helper.rs b/common/src/rest_helper.rs index 135652cb..b1b34a96 100644 --- a/common/src/rest_helper.rs +++ b/common/src/rest_helper.rs @@ -1,5 +1,6 @@ //! Helper functions for REST handlers +use crate::app_error::RESTError; use crate::messages::{Message, RESTResponse}; use anyhow::{anyhow, Result}; use caryatid_sdk::Context; @@ -17,7 +18,7 @@ pub fn handle_rest( ) -> JoinHandle<()> where F: Fn() -> Fut + Send + Sync + Clone + 'static, - Fut: Future> + Send + 'static, + Fut: Future> + Send + 'static, { context.handle(topic, move |message: Arc| { let handler = handler.clone(); @@ -25,12 +26,7 @@ where let response = match message.as_ref() { Message::RESTRequest(request) => { info!("REST received {} {}", request.method, request.path); - match handler().await { - Ok(response) => response, - Err(error) => { - RESTResponse::with_text(500, &format!("{error:?}").to_string()) - } - } + handler().await.unwrap_or_else(|error| error.into()) } _ => { error!("Unexpected message type {:?}", message); @@ -51,7 +47,7 @@ pub fn handle_rest_with_path_parameter( ) -> JoinHandle<()> where F: Fn(&[&str]) -> Fut + Send + Sync + Clone + 'static, - Fut: Future> + Send + 'static, + Fut: Future> + Send + 'static, { let topic_owned = topic.to_string(); context.handle(topic, move |message: Arc| { @@ -65,12 +61,7 @@ where extract_params_from_topic_and_path(&topic_owned, &request.path_elements); let params_slice: Vec<&str> = params_vec.iter().map(|s| s.as_str()).collect(); - match handler(¶ms_slice).await { - Ok(response) => response, - Err(error) => { - RESTResponse::with_text(500, &format!("{error:?}").to_string()) - } - } + handler(¶ms_slice).await.unwrap_or_else(|error| error.into()) } _ => { error!("Unexpected message type {:?}", message); @@ -83,7 +74,7 @@ where }) } -// Handle a REST request with query parameters +/// Handle a REST request with query parameters pub fn handle_rest_with_query_parameters( context: Arc>, topic: &str, @@ -91,7 +82,7 @@ pub fn handle_rest_with_query_parameters( ) -> JoinHandle<()> where F: Fn(HashMap) -> Fut + Send + Sync + Clone + 'static, - Fut: Future> + Send + 'static, + Fut: Future> + Send + 'static, { context.handle(topic, move |message: Arc| { let handler = handler.clone(); @@ -99,10 +90,7 @@ where let response = match message.as_ref() { Message::RESTRequest(request) => { let params = request.query_parameters.clone(); - match handler(params).await { - Ok(response) => response, - Err(error) => RESTResponse::with_text(500, &format!("{error:?}")), - } + handler(params).await.unwrap_or_else(|error| error.into()) } _ => RESTResponse::with_text(500, "Unexpected message in REST request"), }; @@ -112,7 +100,7 @@ where }) } -// Handle a REST request with path and query parameters +/// Handle a REST request with path and query parameters pub fn handle_rest_with_path_and_query_parameters( context: Arc>, topic: &str, @@ -120,7 +108,7 @@ pub fn handle_rest_with_path_and_query_parameters( ) -> JoinHandle<()> where F: Fn(&[&str], HashMap) -> Fut + Send + Sync + Clone + 'static, - Fut: Future> + Send + 'static, + Fut: Future> + Send + 'static, { let topic_owned = topic.to_string(); context.handle(topic, move |message: Arc| { @@ -133,10 +121,7 @@ where extract_params_from_topic_and_path(&topic_owned, &request.path_elements); let params_slice: Vec<&str> = params_vec.iter().map(|s| s.as_str()).collect(); let query_params = request.query_parameters.clone(); - match handler(¶ms_slice, query_params).await { - Ok(response) => response, - Err(error) => RESTResponse::with_text(500, &format!("{error:?}")), - } + handler(¶ms_slice, query_params).await.unwrap_or_else(|error| error.into()) } _ => RESTResponse::with_text(500, "Unexpected message in REST request"), }; diff --git a/common/src/serialization.rs b/common/src/serialization.rs index 709cfdf6..b2ec1c0a 100644 --- a/common/src/serialization.rs +++ b/common/src/serialization.rs @@ -3,8 +3,10 @@ use std::marker::PhantomData; use crate::PoolId; use anyhow::anyhow; use bech32::{Bech32, Hrp}; -use serde::{ser::SerializeMap, Deserialize, Serializer}; +use serde::{ser::SerializeMap, Deserialize, Serialize, Serializer}; use serde_with::{ser::SerializeAsWrap, DeserializeAs, SerializeAs}; +use caryatid_module_rest_server::messages::RESTResponse; +use crate::app_error::RESTError; pub struct SerializeMapAs(std::marker::PhantomData<(KAs, VAs)>); @@ -158,3 +160,17 @@ impl Bech32WithHrp for [u8] { Ok(data.to_vec()) } } + +/// Helper to serialize a result to JSON REST response +pub fn serialize_to_json_response(data: &T) -> Result { + let json = serde_json::to_string_pretty(data)?; + Ok(RESTResponse::with_json(200, &json)) +} + +/// Convert a Result to a RESTResponse, handling errors gracefully +pub fn to_rest_response(result: Result) -> RESTResponse { + match result { + Ok(data) => serialize_to_json_response(&data).unwrap_or_else(|e| e.into()), + Err(e) => e.into(), + } +} \ No newline at end of file diff --git a/modules/accounts_state/src/accounts_state.rs b/modules/accounts_state/src/accounts_state.rs index 82630f03..b172414e 100644 --- a/modules/accounts_state/src/accounts_state.rs +++ b/modules/accounts_state/src/accounts_state.rs @@ -32,6 +32,7 @@ mod verifier; use acropolis_common::queries::accounts::{ AccountInfo, AccountsStateQuery, AccountsStateQueryResponse, }; +use acropolis_common::queries::errors::QueryError; use verifier::Verifier; use crate::spo_distribution_store::SPDDStore; @@ -484,9 +485,9 @@ impl AccountsState { async move { let Message::StateQuery(StateQuery::Accounts(query)) = message.as_ref() else { return Arc::new(Message::StateQueryResponse(StateQueryResponse::Accounts( - AccountsStateQueryResponse::Error( - "Invalid message for accounts-state".into(), - ), + AccountsStateQueryResponse::Error(QueryError::invalid_request( + "Invalid message for accounts-state", + )), ))); }; @@ -495,26 +496,30 @@ impl AccountsState { Some(s) => Some(s.lock().await), None => None, }; + let state = match guard.current() { Some(s) => s, None => { return Arc::new(Message::StateQueryResponse( - StateQueryResponse::Accounts(AccountsStateQueryResponse::NotFound), + StateQueryResponse::Accounts(AccountsStateQueryResponse::Error( + QueryError::not_found("Current state"), + )), )); } }; let response = match query { AccountsStateQuery::GetAccountInfo { account } => { - if let Some(account) = state.get_stake_state(account) { - AccountsStateQueryResponse::AccountInfo(AccountInfo { + match state.get_stake_state(account) { + Some(account) => AccountsStateQueryResponse::AccountInfo(AccountInfo { utxo_value: account.utxo_value, rewards: account.rewards, delegated_spo: account.delegated_spo, delegated_drep: account.delegated_drep.clone(), - }) - } else { - AccountsStateQueryResponse::NotFound + }), + None => AccountsStateQueryResponse::Error(QueryError::not_found( + format!("Account {}", account), + )), } } @@ -548,7 +553,7 @@ impl AccountsState { AccountsStateQueryResponse::AccountsDrepDelegationsMap(map) } None => AccountsStateQueryResponse::Error( - "Error retrieving DRep delegations map".to_string(), + QueryError::partial_not_found("One or more accounts not found"), ), } } @@ -563,7 +568,7 @@ impl AccountsState { match state.get_accounts_utxo_values_map(stake_addresses) { Some(map) => AccountsStateQueryResponse::AccountsUtxoValuesMap(map), None => AccountsStateQueryResponse::Error( - "One or more accounts not found".to_string(), + QueryError::partial_not_found("One or more accounts not found"), ), } } @@ -572,7 +577,7 @@ impl AccountsState { match state.get_accounts_utxo_values_sum(stake_addresses) { Some(sum) => AccountsStateQueryResponse::AccountsUtxoValuesSum(sum), None => AccountsStateQueryResponse::Error( - "One or more accounts not found".to_string(), + QueryError::partial_not_found("One or more accounts not found"), ), } } @@ -581,7 +586,7 @@ impl AccountsState { match state.get_accounts_balances_map(stake_addresses) { Some(map) => AccountsStateQueryResponse::AccountsBalancesMap(map), None => AccountsStateQueryResponse::Error( - "One or more accounts not found".to_string(), + QueryError::partial_not_found("One or more accounts not found"), ), } } @@ -596,7 +601,7 @@ impl AccountsState { match state.get_account_balances_sum(stake_addresses) { Some(sum) => AccountsStateQueryResponse::AccountsBalancesSum(sum), None => AccountsStateQueryResponse::Error( - "One or more accounts not found".to_string(), + QueryError::partial_not_found("One or more accounts not found"), ), } } @@ -604,11 +609,13 @@ impl AccountsState { AccountsStateQuery::GetSPDDByEpoch { epoch } => match spdd_store_guard { Some(spdd_store) => match spdd_store.query_by_epoch(*epoch) { Ok(result) => AccountsStateQueryResponse::SPDDByEpoch(result), - Err(e) => AccountsStateQueryResponse::Error(e.to_string()), + Err(e) => AccountsStateQueryResponse::Error(QueryError::query_failed( + e.to_string(), + )), }, - None => AccountsStateQueryResponse::Error( - "SPDD store is not enabled".to_string(), - ), + None => { + AccountsStateQueryResponse::Error(QueryError::storage_disabled("SPDD")) + } }, AccountsStateQuery::GetSPDDByEpochAndPool { epoch, pool_id } => { @@ -618,19 +625,21 @@ impl AccountsState { Ok(result) => { AccountsStateQueryResponse::SPDDByEpochAndPool(result) } - Err(e) => AccountsStateQueryResponse::Error(e.to_string()), + Err(e) => AccountsStateQueryResponse::Error( + QueryError::query_failed(e.to_string()), + ), } } None => AccountsStateQueryResponse::Error( - "SPDD store is not enabled".to_string(), + QueryError::storage_disabled("SPDD"), ), } } - _ => AccountsStateQueryResponse::Error(format!( - "Unimplemented query variant: {:?}", + _ => AccountsStateQueryResponse::Error(QueryError::not_implemented(format!( + "{:?}", query - )), + ))), }; Arc::new(Message::StateQueryResponse(StateQueryResponse::Accounts( diff --git a/modules/address_state/src/address_state.rs b/modules/address_state/src/address_state.rs index 4a5b5f4f..a56165e0 100644 --- a/modules/address_state/src/address_state.rs +++ b/modules/address_state/src/address_state.rs @@ -4,6 +4,11 @@ use std::sync::Arc; +use crate::{ + immutable_address_store::ImmutableAddressStore, + state::{AddressStorageConfig, State}, +}; +use acropolis_common::queries::errors::QueryError; use acropolis_common::{ messages::{CardanoMessage, Message, StateQuery, StateQueryResponse}, queries::addresses::{ @@ -16,11 +21,6 @@ use caryatid_sdk::{module, Context, Module, Subscription}; use config::Config; use tokio::sync::{mpsc, Mutex}; use tracing::{error, info}; - -use crate::{ - immutable_address_store::ImmutableAddressStore, - state::{AddressStorageConfig, State}, -}; mod immutable_address_store; mod state; mod volatile_addresses; @@ -194,15 +194,14 @@ impl AddressState { let state_mutex = Arc::new(Mutex::new(state)); let state_run = state_mutex.clone(); - // Query handler context.handle(&address_query_topic, move |message| { let state_mutex = state_mutex.clone(); async move { let Message::StateQuery(StateQuery::Addresses(query)) = message.as_ref() else { return Arc::new(Message::StateQueryResponse(StateQueryResponse::Addresses( - AddressStateQueryResponse::Error( - "Invalid message for address-state".into(), - ), + AddressStateQueryResponse::Error(QueryError::invalid_request( + "Invalid message for address-state", + )), ))); }; @@ -211,21 +210,31 @@ impl AddressState { AddressStateQuery::GetAddressUTxOs { address } => { match state.get_address_utxos(address).await { Ok(Some(utxos)) => AddressStateQueryResponse::AddressUTxOs(utxos), - Ok(None) => AddressStateQueryResponse::NotFound, - Err(e) => AddressStateQueryResponse::Error(e.to_string()), + Ok(None) => AddressStateQueryResponse::Error(QueryError::not_found( + "Address not found", + )), + Err(e) => AddressStateQueryResponse::Error(QueryError::query_failed( + e.to_string(), + )), } } AddressStateQuery::GetAddressTransactions { address } => { match state.get_address_transactions(address).await { Ok(Some(txs)) => AddressStateQueryResponse::AddressTransactions(txs), - Ok(None) => AddressStateQueryResponse::NotFound, - Err(e) => AddressStateQueryResponse::Error(e.to_string()), + Ok(None) => AddressStateQueryResponse::Error(QueryError::not_found( + "Address not found", + )), + Err(e) => AddressStateQueryResponse::Error(QueryError::query_failed( + e.to_string(), + )), } } AddressStateQuery::GetAddressTotals { address } => { match state.get_address_totals(address).await { Ok(totals) => AddressStateQueryResponse::AddressTotals(totals), - Err(e) => AddressStateQueryResponse::Error(e.to_string()), + Err(e) => AddressStateQueryResponse::Error(QueryError::query_failed( + e.to_string(), + )), } } }; diff --git a/modules/drdd_state/src/rest.rs b/modules/drdd_state/src/rest.rs index 37822569..91334109 100644 --- a/modules/drdd_state/src/rest.rs +++ b/modules/drdd_state/src/rest.rs @@ -1,4 +1,5 @@ use crate::state::State; +use acropolis_common::app_error::RESTError; use acropolis_common::{extract_strict_query_params, messages::RESTResponse, DRepCredential}; use anyhow::Result; use serde::Serialize; @@ -17,14 +18,11 @@ struct DRDDResponse { pub async fn handle_drdd( state: Option>>, params: HashMap, -) -> Result { +) -> Result { let locked = match state.as_ref() { Some(state) => state.lock().await, None => { - return Ok(RESTResponse::with_text( - 503, - "DRDD storage is disabled by configuration", - )); + return Err(RESTError::storage_disabled("DRDD")); } }; @@ -36,10 +34,7 @@ pub async fn handle_drdd( Some(epoch) => match locked.get_epoch(epoch) { Some(drdd) => Some(drdd), None => { - return Ok(RESTResponse::with_text( - 404, - &format!("DRDD not found for epoch {}", epoch), - )); + return Err(RESTError::not_found(&format!("DRDD for epoch {}", epoch))); } }, None => locked.get_latest(), @@ -67,9 +62,9 @@ pub async fn handle_drdd( match serde_json::to_string(&response) { Ok(body) => Ok(RESTResponse::with_json(200, &body)), - Err(e) => Ok(RESTResponse::with_text( - 500, - &format!("Internal server error retrieving DRep delegation distribution: {e}"), + Err(e) => Err(RESTError::serialization_failed( + "DRep delegation distribution", + e, )), } } else { @@ -81,10 +76,7 @@ pub async fn handle_drdd( match serde_json::to_string(&response) { Ok(body) => Ok(RESTResponse::with_json(200, &body)), - Err(_) => Ok(RESTResponse::with_text( - 500, - "Internal server error serializing empty DRDD response", - )), + Err(e) => Err(RESTError::serialization_failed("empty DRDD response", e)), } } } diff --git a/modules/rest_blockfrost/src/handlers/accounts.rs b/modules/rest_blockfrost/src/handlers/accounts.rs index 1bb82a6e..3758e8b8 100644 --- a/modules/rest_blockfrost/src/handlers/accounts.rs +++ b/modules/rest_blockfrost/src/handlers/accounts.rs @@ -1,23 +1,24 @@ //! REST handlers for Acropolis Blockfrost /accounts endpoints use std::sync::Arc; +use crate::handlers_config::HandlersConfig; +use crate::types::{ + AccountAddressREST, AccountRewardREST, AccountWithdrawalREST, DelegationUpdateREST, + RegistrationUpdateREST, +}; +use acropolis_common::app_error::RESTError; use acropolis_common::messages::{Message, RESTResponse, StateQuery, StateQueryResponse}; use acropolis_common::queries::accounts::{AccountsStateQuery, AccountsStateQueryResponse}; use acropolis_common::queries::blocks::{ BlocksStateQuery, BlocksStateQueryResponse, TransactionHashes, }; +use acropolis_common::queries::errors::QueryError; use acropolis_common::queries::utils::query_state; use acropolis_common::serialization::{Bech32Conversion, Bech32WithHrp}; -use acropolis_common::{DRepChoice, StakeAddress}; +use acropolis_common::{DRepChoice, StakeAddress, TxHash}; use anyhow::{anyhow, Result}; use caryatid_sdk::Context; -use crate::handlers_config::HandlersConfig; -use crate::types::{ - AccountAddressREST, AccountRewardREST, AccountWithdrawalREST, DelegationUpdateREST, - RegistrationUpdateREST, -}; - #[derive(serde::Serialize)] pub struct StakeAccountRest { pub utxo_value: u64, @@ -37,15 +38,14 @@ pub async fn handle_single_account_blockfrost( context: Arc>, params: Vec, handlers_config: Arc, -) -> Result { - let account = match parse_stake_address(¶ms) { - Ok(addr) => addr, - Err(resp) => return Ok(resp), - }; +) -> Result { + let account = parse_stake_address(¶ms)?; + // Prepare the message let msg = Arc::new(Message::StateQuery(StateQuery::Accounts( AccountsStateQuery::GetAccountInfo { account }, ))); + let account = query_state( &context, &handlers_config.accounts_query_topic, @@ -53,49 +53,26 @@ pub async fn handle_single_account_blockfrost( |message| match message { Message::StateQueryResponse(StateQueryResponse::Accounts( AccountsStateQueryResponse::AccountInfo(account), - )) => Ok(Some(account)), - Message::StateQueryResponse(StateQueryResponse::Accounts( - AccountsStateQueryResponse::NotFound, - )) => Ok(None), + )) => Ok(account), Message::StateQueryResponse(StateQueryResponse::Accounts( AccountsStateQueryResponse::Error(e), - )) => Err(anyhow::anyhow!( - "Internal server error while retrieving account info: {e}" - )), - _ => Err(anyhow::anyhow!( - "Unexpected message type while retrieving account info" - )), + )) => Err(e), + _ => Err(QueryError::query_failed("Unexpected response type")), }, ) .await?; - let Some(account) = account else { - return Ok(RESTResponse::with_text(404, "Account not found")); - }; - let delegated_spo = match &account.delegated_spo { - Some(spo) => match spo.to_bech32() { - Ok(val) => Some(val), - Err(e) => { - return Ok(RESTResponse::with_text( - 500, - &format!("Internal server error while mapping SPO: {e}"), - )); - } - }, + Some(spo) => { + Some(spo.to_bech32().map_err(|e| RESTError::encoding_failed(&format!("SPO: {e}")))?) + } None => None, }; let delegated_drep = match &account.delegated_drep { - Some(drep) => match map_drep_choice(drep) { - Ok(val) => Some(val), - Err(e) => { - return Ok(RESTResponse::with_text( - 500, - &format!("Internal server error while mapping dRep: {e}"), - )) - } - }, + Some(drep) => Some( + map_drep_choice(drep).map_err(|e| RESTError::encoding_failed(&format!("dRep: {e}")))?, + ), None => None, }; @@ -106,13 +83,8 @@ pub async fn handle_single_account_blockfrost( delegated_drep, }; - match serde_json::to_string_pretty(&rest_response) { - Ok(json) => Ok(RESTResponse::with_json(200, &json)), - Err(e) => Ok(RESTResponse::with_text( - 500, - &format!("Internal server error while retrieving account info: {e}"), - )), - } + let json = serde_json::to_string_pretty(&rest_response)?; + Ok(RESTResponse::with_json(200, &json)) } /// Handle `/accounts/{stake_address}/registrations` Blockfrost-compatible endpoint @@ -120,11 +92,8 @@ pub async fn handle_account_registrations_blockfrost( context: Arc>, params: Vec, handlers_config: Arc, -) -> Result { - let account = match parse_stake_address(¶ms) { - Ok(addr) => addr, - Err(resp) => return Ok(resp), - }; +) -> Result { + let account = parse_stake_address(¶ms)?; // Prepare the message let msg = Arc::new(Message::StateQuery(StateQuery::Accounts( @@ -139,60 +108,25 @@ pub async fn handle_account_registrations_blockfrost( |message| match message { Message::StateQueryResponse(StateQueryResponse::Accounts( AccountsStateQueryResponse::AccountRegistrationHistory(registrations), - )) => Ok(Some(registrations)), - Message::StateQueryResponse(StateQueryResponse::Accounts( - AccountsStateQueryResponse::NotFound, - )) => Ok(None), + )) => Ok(registrations), Message::StateQueryResponse(StateQueryResponse::Accounts( AccountsStateQueryResponse::Error(e), - )) => Err(anyhow::anyhow!( - "Internal server error while retrieving account registrations: {e}" - )), - _ => Err(anyhow::anyhow!( - "Unexpected message type while retrieving account registrations" - )), + )) => Err(e), + _ => Err(QueryError::query_failed("Unexpected response type")), }, ) .await?; - let Some(registrations) = registrations else { - return Ok(RESTResponse::with_text(404, "Account not found")); - }; - // Get TxHashes from TxIdentifiers let tx_ids: Vec<_> = registrations.iter().map(|r| r.tx_identifier).collect(); - let msg = Arc::new(Message::StateQuery(StateQuery::Blocks( - BlocksStateQuery::GetTransactionHashes { tx_ids }, - ))); - let tx_hashes = query_state( - &context, - &handlers_config.blocks_query_topic, - msg, - |message| match message { - Message::StateQueryResponse(StateQueryResponse::Blocks( - BlocksStateQueryResponse::TransactionHashes(TransactionHashes { tx_hashes }), - )) => Ok(tx_hashes), - Message::StateQueryResponse(StateQueryResponse::Blocks( - BlocksStateQueryResponse::Error(e), - )) => Err(anyhow::anyhow!( - "Internal server error while resolving transaction hashes: {e}" - )), - _ => Err(anyhow::anyhow!( - "Unexpected message type while resolving transaction hashes" - )), - }, - ) - .await?; + let tx_hashes = get_transaction_hashes(&context, &handlers_config, tx_ids).await?; let mut rest_response = Vec::new(); for r in registrations { - let Some(tx_hash) = tx_hashes.get(&r.tx_identifier) else { - return Ok(RESTResponse::with_text( - 500, - "Missing tx hash for registration", - )); - }; + let tx_hash = tx_hashes.get(&r.tx_identifier).ok_or_else(|| { + RESTError::InternalServerError("Missing tx hash for registration".to_string()) + })?; rest_response.push(RegistrationUpdateREST { tx_hash: hex::encode(tx_hash), @@ -200,13 +134,8 @@ pub async fn handle_account_registrations_blockfrost( }); } - match serde_json::to_string_pretty(&rest_response) { - Ok(json) => Ok(RESTResponse::with_json(200, &json)), - Err(e) => Ok(RESTResponse::with_text( - 500, - &format!("Internal server error while serializing registration history: {e}"), - )), - } + let json = serde_json::to_string_pretty(&rest_response)?; + Ok(RESTResponse::with_json(200, &json)) } /// Handle `/accounts/{stake_address}/delegations` Blockfrost-compatible endpoint @@ -214,11 +143,8 @@ pub async fn handle_account_delegations_blockfrost( context: Arc>, params: Vec, handlers_config: Arc, -) -> Result { - let account = match parse_stake_address(¶ms) { - Ok(addr) => addr, - Err(resp) => return Ok(resp), - }; +) -> Result { + let account = parse_stake_address(¶ms)?; // Prepare the message let msg = Arc::new(Message::StateQuery(StateQuery::Accounts( @@ -233,70 +159,28 @@ pub async fn handle_account_delegations_blockfrost( |message| match message { Message::StateQueryResponse(StateQueryResponse::Accounts( AccountsStateQueryResponse::AccountDelegationHistory(delegations), - )) => Ok(Some(delegations)), - Message::StateQueryResponse(StateQueryResponse::Accounts( - AccountsStateQueryResponse::NotFound, - )) => Ok(None), + )) => Ok(delegations), Message::StateQueryResponse(StateQueryResponse::Accounts( AccountsStateQueryResponse::Error(e), - )) => Err(anyhow::anyhow!( - "Internal server error while retrieving account delegations: {e}" - )), - _ => Err(anyhow::anyhow!( - "Unexpected message type while retrieving account delegations" - )), + )) => Err(e), + _ => Err(QueryError::query_failed("Unexpected response type")), }, ) .await?; - let Some(delegations) = delegations else { - return Ok(RESTResponse::with_text(404, "Account not found")); - }; - // Get TxHashes from TxIdentifiers let tx_ids: Vec<_> = delegations.iter().map(|r| r.tx_identifier).collect(); - let msg = Arc::new(Message::StateQuery(StateQuery::Blocks( - BlocksStateQuery::GetTransactionHashes { tx_ids }, - ))); - let tx_hashes = query_state( - &context, - &handlers_config.blocks_query_topic, - msg, - |message| match message { - Message::StateQueryResponse(StateQueryResponse::Blocks( - BlocksStateQueryResponse::TransactionHashes(TransactionHashes { tx_hashes }), - )) => Ok(tx_hashes), - Message::StateQueryResponse(StateQueryResponse::Blocks( - BlocksStateQueryResponse::Error(e), - )) => Err(anyhow::anyhow!( - "Internal server error while resolving transaction hashes: {e}" - )), - _ => Err(anyhow::anyhow!( - "Unexpected message type while resolving transaction hashes" - )), - }, - ) - .await?; + let tx_hashes = get_transaction_hashes(&context, &handlers_config, tx_ids).await?; let mut rest_response = Vec::new(); for r in delegations { - let Some(tx_hash) = tx_hashes.get(&r.tx_identifier) else { - return Ok(RESTResponse::with_text( - 500, - "Missing tx hash for delegation", - )); - }; - - let pool_id = match r.pool.to_bech32() { - Ok(p) => p, - Err(e) => { - return Ok(RESTResponse::with_text( - 500, - &format!("Failed to encode pool ID: {e}"), - )); - } - }; + let tx_hash = tx_hashes.get(&r.tx_identifier).ok_or_else(|| { + RESTError::InternalServerError("Missing tx hash for delegation".to_string()) + })?; + + let pool_id = + r.pool.to_bech32().map_err(|e| RESTError::encoding_failed(&format!("pool ID: {e}")))?; rest_response.push(DelegationUpdateREST { active_epoch: r.active_epoch, @@ -306,13 +190,8 @@ pub async fn handle_account_delegations_blockfrost( }); } - match serde_json::to_string_pretty(&rest_response) { - Ok(json) => Ok(RESTResponse::with_json(200, &json)), - Err(e) => Ok(RESTResponse::with_text( - 500, - &format!("Internal server error while serializing delegation history: {e}"), - )), - } + let json = serde_json::to_string_pretty(&rest_response)?; + Ok(RESTResponse::with_json(200, &json)) } /// Handle `/accounts/{stake_address}/mirs` Blockfrost-compatible endpoint @@ -320,11 +199,8 @@ pub async fn handle_account_mirs_blockfrost( context: Arc>, params: Vec, handlers_config: Arc, -) -> Result { - let account = match parse_stake_address(¶ms) { - Ok(addr) => addr, - Err(resp) => return Ok(resp), - }; +) -> Result { + let account = parse_stake_address(¶ms)?; // Prepare the message let msg = Arc::new(Message::StateQuery(StateQuery::Accounts( @@ -339,60 +215,25 @@ pub async fn handle_account_mirs_blockfrost( |message| match message { Message::StateQueryResponse(StateQueryResponse::Accounts( AccountsStateQueryResponse::AccountMIRHistory(mirs), - )) => Ok(Some(mirs)), - Message::StateQueryResponse(StateQueryResponse::Accounts( - AccountsStateQueryResponse::NotFound, - )) => Ok(None), + )) => Ok(mirs), Message::StateQueryResponse(StateQueryResponse::Accounts( AccountsStateQueryResponse::Error(e), - )) => Err(anyhow::anyhow!( - "Internal server error while retrieving account mirs: {e}" - )), - _ => Err(anyhow::anyhow!( - "Unexpected message type while retrieving account mirs" - )), + )) => Err(e), + _ => Err(QueryError::query_failed("Unexpected response type")), }, ) .await?; - let Some(mirs) = mirs else { - return Ok(RESTResponse::with_text(404, "Account not found")); - }; - // Get TxHashes from TxIdentifiers let tx_ids: Vec<_> = mirs.iter().map(|r| r.tx_identifier).collect(); - let msg = Arc::new(Message::StateQuery(StateQuery::Blocks( - BlocksStateQuery::GetTransactionHashes { tx_ids }, - ))); - let tx_hashes = query_state( - &context, - &handlers_config.blocks_query_topic, - msg, - |message| match message { - Message::StateQueryResponse(StateQueryResponse::Blocks( - BlocksStateQueryResponse::TransactionHashes(TransactionHashes { tx_hashes }), - )) => Ok(tx_hashes), - Message::StateQueryResponse(StateQueryResponse::Blocks( - BlocksStateQueryResponse::Error(e), - )) => Err(anyhow::anyhow!( - "Internal server error while resolving transaction hashes: {e}" - )), - _ => Err(anyhow::anyhow!( - "Unexpected message type while resolving transaction hashes" - )), - }, - ) - .await?; + let tx_hashes = get_transaction_hashes(&context, &handlers_config, tx_ids).await?; let mut rest_response = Vec::new(); for r in mirs { - let Some(tx_hash) = tx_hashes.get(&r.tx_identifier) else { - return Ok(RESTResponse::with_text( - 500, - "Missing tx hash for MIR record", - )); - }; + let tx_hash = tx_hashes.get(&r.tx_identifier).ok_or_else(|| { + RESTError::InternalServerError("Missing tx hash for MIR record".to_string()) + })?; rest_response.push(AccountWithdrawalREST { tx_hash: hex::encode(tx_hash), @@ -400,28 +241,20 @@ pub async fn handle_account_mirs_blockfrost( }); } - match serde_json::to_string_pretty(&rest_response) { - Ok(json) => Ok(RESTResponse::with_json(200, &json)), - Err(e) => Ok(RESTResponse::with_text( - 500, - &format!("Internal server error while serializing MIR history: {e}"), - )), - } + let json = serde_json::to_string_pretty(&rest_response)?; + Ok(RESTResponse::with_json(200, &json)) } pub async fn handle_account_withdrawals_blockfrost( context: Arc>, params: Vec, handlers_config: Arc, -) -> Result { - let account = match parse_stake_address(¶ms) { - Ok(addr) => addr, - Err(resp) => return Ok(resp), - }; +) -> Result { + let account = parse_stake_address(¶ms)?; // Prepare the message let msg = Arc::new(Message::StateQuery(StateQuery::Accounts( - AccountsStateQuery::GetAccountRegistrationHistory { account }, + AccountsStateQuery::GetAccountWithdrawalHistory { account }, ))); // Get withdrawals from historical accounts state @@ -432,60 +265,25 @@ pub async fn handle_account_withdrawals_blockfrost( |message| match message { Message::StateQueryResponse(StateQueryResponse::Accounts( AccountsStateQueryResponse::AccountWithdrawalHistory(withdrawals), - )) => Ok(Some(withdrawals)), - Message::StateQueryResponse(StateQueryResponse::Accounts( - AccountsStateQueryResponse::NotFound, - )) => Ok(None), + )) => Ok(withdrawals), Message::StateQueryResponse(StateQueryResponse::Accounts( AccountsStateQueryResponse::Error(e), - )) => Err(anyhow::anyhow!( - "Internal server error while retrieving account withdrawals: {e}" - )), - _ => Err(anyhow::anyhow!( - "Unexpected message type while retrieving account withdrawals" - )), + )) => Err(e), + _ => Err(QueryError::query_failed("Unexpected response type")), }, ) .await?; - let Some(withdrawals) = withdrawals else { - return Ok(RESTResponse::with_text(404, "Account not found")); - }; - // Get TxHashes from TxIdentifiers let tx_ids: Vec<_> = withdrawals.iter().map(|r| r.tx_identifier).collect(); - let msg = Arc::new(Message::StateQuery(StateQuery::Blocks( - BlocksStateQuery::GetTransactionHashes { tx_ids }, - ))); - let tx_hashes = query_state( - &context, - &handlers_config.blocks_query_topic, - msg, - |message| match message { - Message::StateQueryResponse(StateQueryResponse::Blocks( - BlocksStateQueryResponse::TransactionHashes(TransactionHashes { tx_hashes }), - )) => Ok(tx_hashes), - Message::StateQueryResponse(StateQueryResponse::Blocks( - BlocksStateQueryResponse::Error(e), - )) => Err(anyhow::anyhow!( - "Internal server error while resolving transaction hashes: {e}" - )), - _ => Err(anyhow::anyhow!( - "Unexpected message type while resolving transaction hashes" - )), - }, - ) - .await?; + let tx_hashes = get_transaction_hashes(&context, &handlers_config, tx_ids).await?; let mut rest_response = Vec::new(); for w in withdrawals { - let Some(tx_hash) = tx_hashes.get(&w.tx_identifier) else { - return Ok(RESTResponse::with_text( - 500, - "Missing tx hash for withdrawal", - )); - }; + let tx_hash = tx_hashes.get(&w.tx_identifier).ok_or_else(|| { + RESTError::InternalServerError("Missing tx hash for withdrawal".to_string()) + })?; rest_response.push(AccountWithdrawalREST { tx_hash: hex::encode(tx_hash), @@ -493,24 +291,16 @@ pub async fn handle_account_withdrawals_blockfrost( }); } - match serde_json::to_string_pretty(&rest_response) { - Ok(json) => Ok(RESTResponse::with_json(200, &json)), - Err(e) => Ok(RESTResponse::with_text( - 500, - &format!("Internal server error while serializing withdrawal history: {e}"), - )), - } + let json = serde_json::to_string_pretty(&rest_response)?; + Ok(RESTResponse::with_json(200, &json)) } pub async fn handle_account_rewards_blockfrost( context: Arc>, params: Vec, handlers_config: Arc, -) -> Result { - let account = match parse_stake_address(¶ms) { - Ok(addr) => addr, - Err(resp) => return Ok(resp), - }; +) -> Result { + let account = parse_stake_address(¶ms)?; // Prepare the message let msg = Arc::new(Message::StateQuery(StateQuery::Accounts( @@ -525,55 +315,33 @@ pub async fn handle_account_rewards_blockfrost( |message| match message { Message::StateQueryResponse(StateQueryResponse::Accounts( AccountsStateQueryResponse::AccountRewardHistory(rewards), - )) => Ok(Some(rewards)), - Message::StateQueryResponse(StateQueryResponse::Accounts( - AccountsStateQueryResponse::NotFound, - )) => Ok(None), + )) => Ok(rewards), Message::StateQueryResponse(StateQueryResponse::Accounts( AccountsStateQueryResponse::Error(e), - )) => Err(anyhow::anyhow!( - "Internal server error while retrieving account rewards: {e}" - )), - _ => Err(anyhow::anyhow!( - "Unexpected message type while retrieving account rewards" - )), + )) => Err(e), + _ => Err(QueryError::query_failed("Unexpected response type")), }, ) .await?; - let Some(rewards) = rewards else { - return Ok(RESTResponse::with_text(404, "Account not found")); - }; - - let rest_response = - match rewards.iter().map(|r| r.try_into()).collect::, _>>() { - Ok(v) => v, - Err(e) => { - return Ok(RESTResponse::with_text( - 500, - &format!("Failed to convert reward entry: {e}"), - )) - } - }; - - match serde_json::to_string_pretty(&rest_response) { - Ok(json) => Ok(RESTResponse::with_json(200, &json)), - Err(e) => Ok(RESTResponse::with_text( - 500, - &format!("Internal server error while serializing reward history: {e}"), - )), - } + let rest_response = rewards + .iter() + .map(|r| r.try_into()) + .collect::, _>>() + .map_err(|e: anyhow::Error| { + RESTError::InternalServerError(format!("Failed to convert reward entry: {e}")) + })?; + + let json = serde_json::to_string_pretty(&rest_response)?; + Ok(RESTResponse::with_json(200, &json)) } pub async fn handle_account_addresses_blockfrost( context: Arc>, params: Vec, handlers_config: Arc, -) -> Result { - let account = match parse_stake_address(¶ms) { - Ok(addr) => addr, - Err(resp) => return Ok(resp), - }; +) -> Result { + let account = parse_stake_address(¶ms)?; // Prepare the message let msg = Arc::new(Message::StateQuery(StateQuery::Accounts( @@ -588,67 +356,38 @@ pub async fn handle_account_addresses_blockfrost( |message| match message { Message::StateQueryResponse(StateQueryResponse::Accounts( AccountsStateQueryResponse::AccountAssociatedAddresses(addresses), - )) => Ok(Some(addresses)), - Message::StateQueryResponse(StateQueryResponse::Accounts( - AccountsStateQueryResponse::NotFound, - )) => Ok(None), + )) => Ok(addresses), Message::StateQueryResponse(StateQueryResponse::Accounts( AccountsStateQueryResponse::Error(e), - )) => Err(anyhow::anyhow!( - "Internal server error while retrieving account addresses: {e}" - )), - _ => Err(anyhow::anyhow!( - "Unexpected message type while retrieving account addresses" - )), + )) => Err(e), + _ => Err(QueryError::query_failed("Unexpected response type")), }, ) .await?; - let Some(addresses) = addresses else { - return Ok(RESTResponse::with_text(404, "Account not found")); - }; - - let rest_response = match addresses + let rest_response = addresses .iter() .map(|r| { - Ok::<_, anyhow::Error>(AccountAddressREST { - address: r.to_string().map_err(|e| anyhow!("invalid address: {e}"))?, + Ok::<_, RESTError>(AccountAddressREST { + address: r + .to_string() + .map_err(|e| RESTError::InternalServerError(format!("Invalid address: {e}")))?, }) }) - .collect::, _>>() - { - Ok(v) => v, - Err(e) => { - return Ok(RESTResponse::with_text( - 500, - &format!("Failed to convert address entry: {e}"), - )); - } - }; + .collect::, RESTError>>()?; - match serde_json::to_string_pretty(&rest_response) { - Ok(json) => Ok(RESTResponse::with_json(200, &json)), - Err(e) => Ok(RESTResponse::with_text( - 500, - &format!("Internal server error while serializing addresses: {e}"), - )), - } + let json = serde_json::to_string_pretty(&rest_response)?; + Ok(RESTResponse::with_json(200, &json)) } -fn parse_stake_address(params: &[String]) -> Result { - let Some(stake_key) = params.first() else { - return Err(RESTResponse::with_text( - 400, - "Missing stake address parameter", - )); - }; +fn parse_stake_address(params: &[String]) -> Result { + let stake_key = params.first().ok_or_else(|| RESTError::param_missing("stake address"))?; - StakeAddress::from_string(stake_key).map_err(|_| { - RESTResponse::with_text(400, &format!("Not a valid stake address: {stake_key}")) - }) + StakeAddress::from_string(stake_key) + .map_err(|_| RESTError::invalid_param("stake address", stake_key)) } -fn map_drep_choice(drep: &DRepChoice) -> Result { +fn map_drep_choice(drep: &DRepChoice) -> Result { match drep { DRepChoice::Key(hash) => { let val = hash @@ -680,3 +419,32 @@ fn map_drep_choice(drep: &DRepChoice) -> Result { }), } } + +/// Helper to fetch transaction hashes (used by multiple handlers) +async fn get_transaction_hashes( + context: &Arc>, + handlers_config: &Arc, + tx_ids: Vec, +) -> Result, RESTError> { + let msg = Arc::new(Message::StateQuery(StateQuery::Blocks( + BlocksStateQuery::GetTransactionHashes { tx_ids }, + ))); + + let result = query_state( + context, + &handlers_config.blocks_query_topic, + msg, + |message| match message { + Message::StateQueryResponse(StateQueryResponse::Blocks( + BlocksStateQueryResponse::TransactionHashes(TransactionHashes { tx_hashes }), + )) => Ok(tx_hashes), + Message::StateQueryResponse(StateQueryResponse::Blocks( + BlocksStateQueryResponse::Error(e), + )) => Err(e), + _ => Err(QueryError::query_failed("Unexpected response type")), + }, + ) + .await?; + + Ok(result) +} diff --git a/modules/rest_blockfrost/src/handlers/addresses.rs b/modules/rest_blockfrost/src/handlers/addresses.rs index 8a7741bd..3f520b52 100644 --- a/modules/rest_blockfrost/src/handlers/addresses.rs +++ b/modules/rest_blockfrost/src/handlers/addresses.rs @@ -1,6 +1,9 @@ use anyhow::Result; use std::sync::Arc; +use crate::{handlers_config::HandlersConfig, types::AddressInfoREST}; +use acropolis_common::app_error::RESTError; +use acropolis_common::queries::errors::QueryError; use acropolis_common::{ messages::{Message, RESTResponse, StateQuery, StateQueryResponse}, queries::{ @@ -12,14 +15,12 @@ use acropolis_common::{ }; use caryatid_sdk::Context; -use crate::{handlers_config::HandlersConfig, types::AddressInfoREST}; - /// Handle `/addresses/{address}` Blockfrost-compatible endpoint pub async fn handle_address_single_blockfrost( context: Arc>, params: Vec, handlers_config: Arc, -) -> Result { +) -> Result { let [address_str] = ¶ms[..] else { return Ok(RESTResponse::with_text(400, "Missing address parameter")); }; @@ -69,15 +70,12 @@ pub async fn handle_address_single_blockfrost( AddressStateQueryResponse::AddressUTxOs(utxo_identifiers), )) => Ok(Some(utxo_identifiers)), - Message::StateQueryResponse(StateQueryResponse::Addresses( - AddressStateQueryResponse::NotFound, - )) => Ok(None), - - Message::StateQueryResponse(StateQueryResponse::Addresses( - AddressStateQueryResponse::Error(_), - )) => Err(anyhow::anyhow!("Address info storage disabled")), - - _ => Err(anyhow::anyhow!("Unexpected response")), + Message::StateQueryResponse(StateQueryResponse::UTxOs( + UTxOStateQueryResponse::Error(e), + )) => Err(e.into()), + _ => Err(QueryError::query_failed( + "Unexpected response from addresses query", + )), }, ) .await; @@ -97,8 +95,9 @@ pub async fn handle_address_single_blockfrost( script: is_script, }; - let json = serde_json::to_string_pretty(&rest_response) - .map_err(|e| anyhow::anyhow!("JSON serialization error: {e}"))?; + let json = serde_json::to_string_pretty(&rest_response).map_err(|e| { + RESTError::InternalServerError(format!("JSON serialization error: {e}")) + })?; return Ok(RESTResponse::with_json(200, &json)); } @@ -109,7 +108,7 @@ pub async fn handle_address_single_blockfrost( UTxOStateQuery::GetUTxOsSum { utxo_identifiers }, ))); - let address_balance = match query_state( + let address_balance = query_state( &context, &handlers_config.utxos_query_topic, utxos_query_msg, @@ -117,20 +116,15 @@ pub async fn handle_address_single_blockfrost( Message::StateQueryResponse(StateQueryResponse::UTxOs( UTxOStateQueryResponse::UTxOsSum(balance), )) => Ok(balance), - Message::StateQueryResponse(StateQueryResponse::UTxOs( - UTxOStateQueryResponse::NotFound, - )) => Err(anyhow::anyhow!("UTxOs not found")), Message::StateQueryResponse(StateQueryResponse::UTxOs( UTxOStateQueryResponse::Error(e), - )) => Err(anyhow::anyhow!(format!("UTxO query error: {e}"))), - _ => Err(anyhow::anyhow!("Unexpected response")), + )) => Err(e.into()), + _ => Err(QueryError::query_failed( + "Unexpected response querying UTxOs", + )), }, ) - .await - { - Ok(address_balance) => address_balance, - Err(e) => return Ok(RESTResponse::with_text(500, &format!("Query failed: {e}"))), - }; + .await?; // Let middleware handle the error let rest_response = AddressInfoREST { address: address_str.to_string(), diff --git a/modules/rest_blockfrost/src/rest_blockfrost.rs b/modules/rest_blockfrost/src/rest_blockfrost.rs index 2cdb5570..701d8add 100644 --- a/modules/rest_blockfrost/src/rest_blockfrost.rs +++ b/modules/rest_blockfrost/src/rest_blockfrost.rs @@ -10,6 +10,8 @@ use anyhow::Result; use caryatid_sdk::{module, Context, Module}; use config::Config; use tracing::info; +use acropolis_common::app_error::RESTError; + mod cost_models; mod handlers; mod handlers_config; @@ -17,10 +19,9 @@ mod types; mod utils; use handlers::{ accounts::{ - handle_account_addresses_blockfrost, handle_account_delegations_blockfrost, - handle_account_mirs_blockfrost, handle_account_registrations_blockfrost, - handle_account_rewards_blockfrost, handle_account_withdrawals_blockfrost, - handle_single_account_blockfrost, + handle_account_delegations_blockfrost, handle_account_mirs_blockfrost, + handle_account_registrations_blockfrost, handle_account_rewards_blockfrost, + handle_account_withdrawals_blockfrost, handle_single_account_blockfrost, }, addresses::{ handle_address_asset_utxos_blockfrost, handle_address_extended_blockfrost, @@ -86,10 +87,6 @@ const DEFAULT_HANDLE_ACCOUNT_REWARDS_TOPIC: (&str, &str) = ( "handle-topic-account-rewards", "rest.get.accounts.*.rewards", ); -const DEFAULT_HANDLE_ACCOUNT_ADDRESSES_TOPIC: (&str, &str) = ( - "handle-topic-account-addresses", - "rest.get.accounts.*.addresses", -); // Blocks topics const DEFAULT_HANDLE_BLOCKS_LATEST_HASH_NUMBER_TOPIC: (&str, &str) = @@ -316,14 +313,6 @@ impl BlockfrostREST { handle_account_rewards_blockfrost, ); - // Handler for /accounts/{stake_address}/addresses - register_handler( - context.clone(), - DEFAULT_HANDLE_ACCOUNT_ADDRESSES_TOPIC, - handlers_config.clone(), - handle_account_addresses_blockfrost, - ); - // Handler for /blocks/latest, /blocks/{hash_or_number} register_handler( context.clone(), @@ -731,11 +720,11 @@ fn register_handler( + Sync + Clone + 'static, - Fut: Future> + Send + 'static, + Fut: Future> + Send + 'static, { let topic_name = context.config.get_string(topic.0).unwrap_or_else(|_| topic.1.to_string()); - tracing::info!("Creating request handler on '{}'", topic_name); + info!("Creating request handler on '{}'", topic_name); handle_rest_with_path_parameter(context.clone(), &topic_name, move |params| { let context = context.clone(); @@ -758,11 +747,11 @@ fn register_handler_with_query( + Sync + Clone + 'static, - Fut: Future> + Send + 'static, + Fut: Future> + Send + 'static, { let topic_name = context.config.get_string(topic.0).unwrap_or_else(|_| topic.1.to_string()); - tracing::info!("Creating request handler on '{}'", topic_name); + info!("Creating request handler on '{}'", topic_name); handle_rest_with_path_and_query_parameters( context.clone(), diff --git a/modules/spdd_state/src/rest.rs b/modules/spdd_state/src/rest.rs index 330a8f8a..1c27ac9d 100644 --- a/modules/spdd_state/src/rest.rs +++ b/modules/spdd_state/src/rest.rs @@ -1,4 +1,5 @@ use crate::state::State; +use acropolis_common::app_error::RESTError; use acropolis_common::serialization::Bech32Conversion; use acropolis_common::DelegatedStake; use acropolis_common::{extract_strict_query_params, messages::RESTResponse}; @@ -10,7 +11,7 @@ use tokio::sync::Mutex; pub async fn handle_spdd( state: Arc>, params: HashMap, -) -> Result { +) -> Result { let locked = state.lock().await; extract_strict_query_params!(params, { @@ -21,10 +22,7 @@ pub async fn handle_spdd( Some(epoch) => match locked.get_epoch(epoch) { Some(spdd) => Some(spdd), None => { - return Ok(RESTResponse::with_text( - 404, - &format!("SPDD not found for epoch {}", epoch), - )); + return Err(RESTError::not_found(&format!("SPDD for epoch {}", epoch))); } }, None => locked.get_latest(), @@ -36,15 +34,10 @@ pub async fn handle_spdd( .map(|(k, v)| (k.to_bech32().unwrap_or_else(|_| hex::encode(k)), *v)) .collect(); - match serde_json::to_string(&spdd) { - Ok(body) => Ok(RESTResponse::with_json(200, &body)), - Err(e) => Ok(RESTResponse::with_text( - 500, - &format!( - "Internal server error retrieving stake pool delegation distribution: {e}" - ), - )), - } + let body = + serde_json::to_string(&spdd).map_err(|e| RESTError::serialization_failed("SPDD", e))?; + + Ok(RESTResponse::with_json(200, &body)) } else { Ok(RESTResponse::with_json(200, "{}")) } From abac0ae23037b18331ee2e6662d34db1b98aceaf Mon Sep 17 00:00:00 2001 From: Matthew Hounslow Date: Wed, 5 Nov 2025 12:06:33 -0800 Subject: [PATCH 02/20] Refactor: Adjust `QueryError` to `RESTError` conversion, remove redundant `RESTResponse` implementation --- common/src/app_error.rs | 2 ++ common/src/queries/errors.rs | 15 --------------- 2 files changed, 2 insertions(+), 15 deletions(-) diff --git a/common/src/app_error.rs b/common/src/app_error.rs index 60824903..938940fd 100644 --- a/common/src/app_error.rs +++ b/common/src/app_error.rs @@ -146,11 +146,13 @@ impl From for RESTError { fn from(error: QueryError) -> Self { match error { QueryError::NotFound { resource } => RESTError::NotFound(resource), + QueryError::PartialNotFound { message, .. } => RESTError::NotFound(message), QueryError::QueryFailed { message } => RESTError::InternalServerError(message), QueryError::StorageDisabled { storage_type } => { RESTError::NotImplemented(format!("{} storage is disabled in config", storage_type)) } QueryError::InvalidRequest { message } => RESTError::BadRequest(message), + QueryError::NotImplemented { query } => RESTError::NotImplemented(query), } } } diff --git a/common/src/queries/errors.rs b/common/src/queries/errors.rs index 4c333c2f..1581a594 100644 --- a/common/src/queries/errors.rs +++ b/common/src/queries/errors.rs @@ -87,19 +87,4 @@ impl fmt::Display for QueryError { } } -/// Convert QueryError to RESTResponse with appropriate status codes -/// Not entirely sure where this should go -impl From for RESTResponse { - fn from(error: QueryError) -> Self { - match &error { - QueryError::NotFound { .. } => RESTResponse::with_text(404, &error.to_string()), - QueryError::QueryFailed { .. } => RESTResponse::with_text(500, &error.to_string()), - QueryError::StorageDisabled { .. } => RESTResponse::with_text(501, &error.to_string()), - QueryError::NotImplemented { .. } => RESTResponse::with_text(501, &error.to_string()), - QueryError::InvalidRequest { .. } => RESTResponse::with_text(400, &error.to_string()), - QueryError::PartialNotFound { .. } => RESTResponse::with_text(206, &error.to_string()), - } - } -} - impl std::error::Error for QueryError {} From 67409e47ff536d7cbbe74130930d053c78e2f8cb Mon Sep 17 00:00:00 2001 From: Matthew Hounslow Date: Wed, 5 Nov 2025 12:15:54 -0800 Subject: [PATCH 03/20] Add handler for `/accounts/{stake_address}/addresses` endpoint back --- .../rest_blockfrost/src/rest_blockfrost.rs | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/modules/rest_blockfrost/src/rest_blockfrost.rs b/modules/rest_blockfrost/src/rest_blockfrost.rs index 701d8add..d34feaae 100644 --- a/modules/rest_blockfrost/src/rest_blockfrost.rs +++ b/modules/rest_blockfrost/src/rest_blockfrost.rs @@ -19,9 +19,10 @@ mod types; mod utils; use handlers::{ accounts::{ - handle_account_delegations_blockfrost, handle_account_mirs_blockfrost, - handle_account_registrations_blockfrost, handle_account_rewards_blockfrost, - handle_account_withdrawals_blockfrost, handle_single_account_blockfrost, + handle_account_addresses_blockfrost, handle_account_delegations_blockfrost, + handle_account_mirs_blockfrost, handle_account_registrations_blockfrost, + handle_account_rewards_blockfrost, handle_account_withdrawals_blockfrost, + handle_single_account_blockfrost, }, addresses::{ handle_address_asset_utxos_blockfrost, handle_address_extended_blockfrost, @@ -87,6 +88,10 @@ const DEFAULT_HANDLE_ACCOUNT_REWARDS_TOPIC: (&str, &str) = ( "handle-topic-account-rewards", "rest.get.accounts.*.rewards", ); +const DEFAULT_HANDLE_ACCOUNT_ADDRESSES_TOPIC: (&str, &str) = ( + "handle-topic-account-addresses", + "rest.get.accounts.*.addresses", +); // Blocks topics const DEFAULT_HANDLE_BLOCKS_LATEST_HASH_NUMBER_TOPIC: (&str, &str) = @@ -313,6 +318,14 @@ impl BlockfrostREST { handle_account_rewards_blockfrost, ); + // Handler for /accounts/{stake_address}/addresses + register_handler( + context.clone(), + DEFAULT_HANDLE_ACCOUNT_ADDRESSES_TOPIC, + handlers_config.clone(), + handle_account_addresses_blockfrost, + ); + // Handler for /blocks/latest, /blocks/{hash_or_number} register_handler( context.clone(), From 150cd6961aec1dcf01bfb043b0dca05305a1ce79 Mon Sep 17 00:00:00 2001 From: Matthew Hounslow Date: Wed, 5 Nov 2025 13:00:54 -0800 Subject: [PATCH 04/20] Refactor: Replace `app_error::RESTError` with `rest_error::RESTError` --- common/src/lib.rs | 2 +- common/src/queries/errors.rs | 1 - common/src/queries/utils.rs | 34 ++++++++----------- common/src/{app_error.rs => rest_error.rs} | 0 common/src/rest_helper.rs | 2 +- common/src/serialization.rs | 10 +----- modules/drdd_state/src/rest.rs | 2 +- .../rest_blockfrost/src/handlers/accounts.rs | 10 ++++-- .../rest_blockfrost/src/handlers/addresses.rs | 2 +- .../rest_blockfrost/src/rest_blockfrost.rs | 2 +- modules/spdd_state/src/rest.rs | 2 +- 11 files changed, 28 insertions(+), 39 deletions(-) rename common/src/{app_error.rs => rest_error.rs} (100%) diff --git a/common/src/lib.rs b/common/src/lib.rs index 839af3e4..c0d2bb14 100644 --- a/common/src/lib.rs +++ b/common/src/lib.rs @@ -22,7 +22,7 @@ pub mod stake_addresses; pub mod state_history; pub mod types; pub mod validation; -pub mod app_error; +pub mod rest_error; // Flattened re-exports pub use self::address::*; diff --git a/common/src/queries/errors.rs b/common/src/queries/errors.rs index 1581a594..6caa1095 100644 --- a/common/src/queries/errors.rs +++ b/common/src/queries/errors.rs @@ -1,4 +1,3 @@ -use caryatid_module_rest_server::messages::RESTResponse; use serde::{Deserialize, Serialize}; use std::fmt; diff --git a/common/src/queries/utils.rs b/common/src/queries/utils.rs index d057ba83..57f51ae6 100644 --- a/common/src/queries/utils.rs +++ b/common/src/queries/utils.rs @@ -1,11 +1,13 @@ -use anyhow::Result; use caryatid_sdk::Context; use serde::Serialize; use std::sync::Arc; use crate::messages::{Message, RESTResponse}; use crate::queries::errors::QueryError; +use crate::rest_error::RESTError; +/// Query state and get typed result or QueryError +/// This is the low-level building block for handlers that need to do more processing pub async fn query_state( context: &Arc>, topic: &str, @@ -26,33 +28,25 @@ where extractor(message) } -/// The outer option in the extractor return value is whether the response was handled by F +/// Query state and return a REST response directly +/// This is a convenience function for simple handlers that just fetch and serialize data pub async fn rest_query_state( context: &Arc>, topic: &str, request_msg: Arc, extractor: F, -) -> Result +) -> Result where F: FnOnce(Message) -> Option>, T: Serialize, { - let result = query_state(context, topic, request_msg, |response| { - match extractor(response) { - Some(result) => result, - None => Err(QueryError::query_failed(format!( - "Unexpected response message type from {topic}" - ))), - } + let data = query_state(context, topic, request_msg, |response| { + extractor(response).unwrap_or_else(|| Err(QueryError::query_failed(format!( + "Unexpected response message type from {topic}" + )))) }) - .await; + .await?; // QueryError auto-converts to RESTError via From trait - match result { - Ok(data) => { - let json = serde_json::to_string_pretty(&data) - .map_err(|e| QueryError::query_failed(format!("JSON serialization failed: {e}")))?; - Ok(RESTResponse::with_json(200, &json)) - } - Err(query_error) => Ok(query_error.into()), - } -} + let json = serde_json::to_string_pretty(&data)?; // Uses From for RESTError + Ok(RESTResponse::with_json(200, &json)) +} \ No newline at end of file diff --git a/common/src/app_error.rs b/common/src/rest_error.rs similarity index 100% rename from common/src/app_error.rs rename to common/src/rest_error.rs diff --git a/common/src/rest_helper.rs b/common/src/rest_helper.rs index b1b34a96..847a5765 100644 --- a/common/src/rest_helper.rs +++ b/common/src/rest_helper.rs @@ -1,6 +1,6 @@ //! Helper functions for REST handlers -use crate::app_error::RESTError; +use crate::rest_error::RESTError; use crate::messages::{Message, RESTResponse}; use anyhow::{anyhow, Result}; use caryatid_sdk::Context; diff --git a/common/src/serialization.rs b/common/src/serialization.rs index b2ec1c0a..fcacfd0f 100644 --- a/common/src/serialization.rs +++ b/common/src/serialization.rs @@ -6,7 +6,7 @@ use bech32::{Bech32, Hrp}; use serde::{ser::SerializeMap, Deserialize, Serialize, Serializer}; use serde_with::{ser::SerializeAsWrap, DeserializeAs, SerializeAs}; use caryatid_module_rest_server::messages::RESTResponse; -use crate::app_error::RESTError; +use crate::rest_error::RESTError; pub struct SerializeMapAs(std::marker::PhantomData<(KAs, VAs)>); @@ -166,11 +166,3 @@ pub fn serialize_to_json_response(data: &T) -> Result(result: Result) -> RESTResponse { - match result { - Ok(data) => serialize_to_json_response(&data).unwrap_or_else(|e| e.into()), - Err(e) => e.into(), - } -} \ No newline at end of file diff --git a/modules/drdd_state/src/rest.rs b/modules/drdd_state/src/rest.rs index 91334109..6aa7ffaf 100644 --- a/modules/drdd_state/src/rest.rs +++ b/modules/drdd_state/src/rest.rs @@ -1,5 +1,5 @@ use crate::state::State; -use acropolis_common::app_error::RESTError; +use acropolis_common::rest_error::RESTError; use acropolis_common::{extract_strict_query_params, messages::RESTResponse, DRepCredential}; use anyhow::Result; use serde::Serialize; diff --git a/modules/rest_blockfrost/src/handlers/accounts.rs b/modules/rest_blockfrost/src/handlers/accounts.rs index 3758e8b8..320c3bed 100644 --- a/modules/rest_blockfrost/src/handlers/accounts.rs +++ b/modules/rest_blockfrost/src/handlers/accounts.rs @@ -6,7 +6,6 @@ use crate::types::{ AccountAddressREST, AccountRewardREST, AccountWithdrawalREST, DelegationUpdateREST, RegistrationUpdateREST, }; -use acropolis_common::app_error::RESTError; use acropolis_common::messages::{Message, RESTResponse, StateQuery, StateQueryResponse}; use acropolis_common::queries::accounts::{AccountsStateQuery, AccountsStateQueryResponse}; use acropolis_common::queries::blocks::{ @@ -14,6 +13,7 @@ use acropolis_common::queries::blocks::{ }; use acropolis_common::queries::errors::QueryError; use acropolis_common::queries::utils::query_state; +use acropolis_common::rest_error::RESTError; use acropolis_common::serialization::{Bech32Conversion, Bech32WithHrp}; use acropolis_common::{DRepChoice, StakeAddress, TxHash}; use anyhow::{anyhow, Result}; @@ -57,7 +57,9 @@ pub async fn handle_single_account_blockfrost( Message::StateQueryResponse(StateQueryResponse::Accounts( AccountsStateQueryResponse::Error(e), )) => Err(e), - _ => Err(QueryError::query_failed("Unexpected response type")), + _ => Err(QueryError::query_failed( + "Unexpected message type while retrieving account info", + )), }, ) .await?; @@ -112,7 +114,9 @@ pub async fn handle_account_registrations_blockfrost( Message::StateQueryResponse(StateQueryResponse::Accounts( AccountsStateQueryResponse::Error(e), )) => Err(e), - _ => Err(QueryError::query_failed("Unexpected response type")), + _ => Err(QueryError::query_failed( + "Unexpected message type while retrieving account registrations", + )), }, ) .await?; diff --git a/modules/rest_blockfrost/src/handlers/addresses.rs b/modules/rest_blockfrost/src/handlers/addresses.rs index 3f520b52..8442fb3f 100644 --- a/modules/rest_blockfrost/src/handlers/addresses.rs +++ b/modules/rest_blockfrost/src/handlers/addresses.rs @@ -2,7 +2,7 @@ use anyhow::Result; use std::sync::Arc; use crate::{handlers_config::HandlersConfig, types::AddressInfoREST}; -use acropolis_common::app_error::RESTError; +use acropolis_common::rest_error::RESTError; use acropolis_common::queries::errors::QueryError; use acropolis_common::{ messages::{Message, RESTResponse, StateQuery, StateQueryResponse}, diff --git a/modules/rest_blockfrost/src/rest_blockfrost.rs b/modules/rest_blockfrost/src/rest_blockfrost.rs index d34feaae..2ae0137c 100644 --- a/modules/rest_blockfrost/src/rest_blockfrost.rs +++ b/modules/rest_blockfrost/src/rest_blockfrost.rs @@ -10,7 +10,7 @@ use anyhow::Result; use caryatid_sdk::{module, Context, Module}; use config::Config; use tracing::info; -use acropolis_common::app_error::RESTError; +use acropolis_common::rest_error::RESTError; mod cost_models; mod handlers; diff --git a/modules/spdd_state/src/rest.rs b/modules/spdd_state/src/rest.rs index 1c27ac9d..d677bf94 100644 --- a/modules/spdd_state/src/rest.rs +++ b/modules/spdd_state/src/rest.rs @@ -1,5 +1,5 @@ use crate::state::State; -use acropolis_common::app_error::RESTError; +use acropolis_common::rest_error::RESTError; use acropolis_common::serialization::Bech32Conversion; use acropolis_common::DelegatedStake; use acropolis_common::{extract_strict_query_params, messages::RESTResponse}; From fecb9c3ffb9073816e75935238e9ed35f45acf69 Mon Sep 17 00:00:00 2001 From: Matthew Hounslow Date: Wed, 5 Nov 2025 14:05:22 -0800 Subject: [PATCH 05/20] Refactor: Remove `/drdd` and `/spdd` REST endpoints and associated handlers, streamline `RESTError` methods for flexibility and consistency across modules --- common/src/rest_error.rs | 54 ++++++------- modules/accounts_state/src/accounts_state.rs | 4 +- modules/drdd_state/src/drdd_state.rs | 8 -- modules/drdd_state/src/rest.rs | 82 -------------------- modules/spdd_state/src/rest.rs | 44 ----------- modules/spdd_state/src/spdd_state.rs | 6 -- 6 files changed, 26 insertions(+), 172 deletions(-) delete mode 100644 modules/drdd_state/src/rest.rs delete mode 100644 modules/spdd_state/src/rest.rs diff --git a/common/src/rest_error.rs b/common/src/rest_error.rs index 938940fd..80bfb3ef 100644 --- a/common/src/rest_error.rs +++ b/common/src/rest_error.rs @@ -39,14 +39,9 @@ impl RESTError { RESTError::BadRequest(format!("{} parameter is missing", param_name)) } - /// Feature hasn't been implemented error - pub fn not_implemented(feature: &str) -> Self { - RESTError::NotImplemented(format!("{} not yet implemented", feature)) - } - - /// Storage disabled error - pub fn storage_disabled(storage_type: &str) -> Self { - RESTError::NotImplemented(format!("{} storage is disabled in config", storage_type)) + /// Invalid parameter error + pub fn invalid_param(param_name: &str, reason: &str) -> Self { + RESTError::BadRequest(format!("Invalid {}: {}", param_name, reason)) } /// Invalid hex string error @@ -54,24 +49,29 @@ impl RESTError { RESTError::BadRequest("Invalid hex string".to_string()) } - /// Invalid parameter error - pub fn invalid_param(param_name: &str, reason: &str) -> Self { - RESTError::BadRequest(format!("Invalid {}: {}", param_name, reason)) + /// Resource not found error (flexible message) + pub fn not_found(message: &str) -> Self { + RESTError::NotFound(message.to_string()) + } + + /// Feature not implemented error (flexible message) + pub fn not_implemented(message: &str) -> Self { + RESTError::NotImplemented(message.to_string()) } - /// Resource wasn't found error - pub fn not_found(resource: &str) -> Self { - RESTError::NotFound(format!("{} not found", resource)) + /// Storage disabled error + pub fn storage_disabled(storage_type: &str) -> Self { + RESTError::NotImplemented(format!("{} storage is disabled in config", storage_type)) } - /// Unexpected response error - pub fn unexpected_response(context: &str) -> Self { - RESTError::InternalServerError(format!("Unexpected response while {}", context)) + /// Unexpected response error (flexible message) + pub fn unexpected_response(message: &str) -> Self { + RESTError::InternalServerError(message.to_string()) } - /// Query failed error - pub fn query_failed(error: impl fmt::Display) -> Self { - RESTError::InternalServerError(format!("Query failed: {}", error)) + /// Query failed error (flexible message) + pub fn query_failed(message: &str) -> Self { + RESTError::InternalServerError(message.to_string()) } /// Serialization failed error @@ -142,14 +142,15 @@ impl From for RESTError { } } +/// Convert QueryError to RESTError impl From for RESTError { fn from(error: QueryError) -> Self { match error { QueryError::NotFound { resource } => RESTError::NotFound(resource), - QueryError::PartialNotFound { message, .. } => RESTError::NotFound(message), + QueryError::PartialNotFound { message } => RESTError::BadRequest(message), QueryError::QueryFailed { message } => RESTError::InternalServerError(message), QueryError::StorageDisabled { storage_type } => { - RESTError::NotImplemented(format!("{} storage is disabled in config", storage_type)) + RESTError::storage_disabled(&storage_type) } QueryError::InvalidRequest { message } => RESTError::BadRequest(message), QueryError::NotImplemented { query } => RESTError::NotImplemented(query), @@ -212,11 +213,4 @@ mod tests { assert_eq!(response.code, 400); assert_eq!(response.body, "Invalid stake address"); } - - #[test] - fn test_convenience_constructors() { - assert_eq!(RESTError::invalid_hex().status_code(), 400); - assert_eq!(RESTError::not_found("Asset").message(), "Asset not found"); - assert_eq!(RESTError::not_implemented("Feature").status_code(), 501); - } -} +} \ No newline at end of file diff --git a/modules/accounts_state/src/accounts_state.rs b/modules/accounts_state/src/accounts_state.rs index b172414e..80a9c74d 100644 --- a/modules/accounts_state/src/accounts_state.rs +++ b/modules/accounts_state/src/accounts_state.rs @@ -553,7 +553,7 @@ impl AccountsState { AccountsStateQueryResponse::AccountsDrepDelegationsMap(map) } None => AccountsStateQueryResponse::Error( - QueryError::partial_not_found("One or more accounts not found"), + QueryError::query_failed("Error retrieving DRep delegations map"), ), } } @@ -637,7 +637,7 @@ impl AccountsState { } _ => AccountsStateQueryResponse::Error(QueryError::not_implemented(format!( - "{:?}", + "Unimplemented query variant: {:?}", query ))), }; diff --git a/modules/drdd_state/src/drdd_state.rs b/modules/drdd_state/src/drdd_state.rs index 056b8495..05c03bd7 100644 --- a/modules/drdd_state/src/drdd_state.rs +++ b/modules/drdd_state/src/drdd_state.rs @@ -12,8 +12,6 @@ use tokio::sync::Mutex; use tracing::{error, info, info_span, Instrument}; mod state; use state::State; -mod rest; -use rest::handle_drdd; const DEFAULT_SUBSCRIBE_TOPIC: &str = "cardano.drep.distribution"; const DEFAULT_HANDLE_DRDD_TOPIC: (&str, &str) = ("handle-topic-drdd", "rest.get.drdd"); @@ -106,12 +104,6 @@ impl DRDDState { None }; - // Register /drdd REST endpoint - handle_rest_with_query_parameters(context.clone(), &handle_drdd_topic, move |params| { - let state_rest = state_opt.clone(); - handle_drdd(state_rest.clone(), params) - }); - Ok(()) } } diff --git a/modules/drdd_state/src/rest.rs b/modules/drdd_state/src/rest.rs deleted file mode 100644 index 6aa7ffaf..00000000 --- a/modules/drdd_state/src/rest.rs +++ /dev/null @@ -1,82 +0,0 @@ -use crate::state::State; -use acropolis_common::rest_error::RESTError; -use acropolis_common::{extract_strict_query_params, messages::RESTResponse, DRepCredential}; -use anyhow::Result; -use serde::Serialize; -use std::{collections::HashMap, sync::Arc}; -use tokio::sync::Mutex; - -// Response struct for DRDD -#[derive(Serialize)] -struct DRDDResponse { - dreps: HashMap, - abstain: u64, - no_confidence: u64, -} - -/// Handles /drdd -pub async fn handle_drdd( - state: Option>>, - params: HashMap, -) -> Result { - let locked = match state.as_ref() { - Some(state) => state.lock().await, - None => { - return Err(RESTError::storage_disabled("DRDD")); - } - }; - - extract_strict_query_params!(params, { - "epoch" => epoch: Option, - }); - - let drdd_opt = match epoch { - Some(epoch) => match locked.get_epoch(epoch) { - Some(drdd) => Some(drdd), - None => { - return Err(RESTError::not_found(&format!("DRDD for epoch {}", epoch))); - } - }, - None => locked.get_latest(), - }; - - if let Some(drdd) = drdd_opt { - let dreps: HashMap = drdd - .dreps - .iter() - .map(|(k, v)| { - let key = k.to_drep_bech32().unwrap_or_else(|_| match k { - DRepCredential::AddrKeyHash(bytes) | DRepCredential::ScriptHash(bytes) => { - hex::encode(bytes) - } - }); - (key, *v) - }) - .collect(); - - let response = DRDDResponse { - dreps, - abstain: drdd.abstain, - no_confidence: drdd.no_confidence, - }; - - match serde_json::to_string(&response) { - Ok(body) => Ok(RESTResponse::with_json(200, &body)), - Err(e) => Err(RESTError::serialization_failed( - "DRep delegation distribution", - e, - )), - } - } else { - let response = DRDDResponse { - dreps: HashMap::new(), - abstain: 0, - no_confidence: 0, - }; - - match serde_json::to_string(&response) { - Ok(body) => Ok(RESTResponse::with_json(200, &body)), - Err(e) => Err(RESTError::serialization_failed("empty DRDD response", e)), - } - } -} diff --git a/modules/spdd_state/src/rest.rs b/modules/spdd_state/src/rest.rs deleted file mode 100644 index d677bf94..00000000 --- a/modules/spdd_state/src/rest.rs +++ /dev/null @@ -1,44 +0,0 @@ -use crate::state::State; -use acropolis_common::rest_error::RESTError; -use acropolis_common::serialization::Bech32Conversion; -use acropolis_common::DelegatedStake; -use acropolis_common::{extract_strict_query_params, messages::RESTResponse}; -use anyhow::Result; -use std::{collections::HashMap, sync::Arc}; -use tokio::sync::Mutex; - -/// Handles /spdd -pub async fn handle_spdd( - state: Arc>, - params: HashMap, -) -> Result { - let locked = state.lock().await; - - extract_strict_query_params!(params, { - "epoch" => epoch: Option, - }); - - let spdd_opt = match epoch { - Some(epoch) => match locked.get_epoch(epoch) { - Some(spdd) => Some(spdd), - None => { - return Err(RESTError::not_found(&format!("SPDD for epoch {}", epoch))); - } - }, - None => locked.get_latest(), - }; - - if let Some(spdd) = spdd_opt { - let spdd: HashMap = spdd - .iter() - .map(|(k, v)| (k.to_bech32().unwrap_or_else(|_| hex::encode(k)), *v)) - .collect(); - - let body = - serde_json::to_string(&spdd).map_err(|e| RESTError::serialization_failed("SPDD", e))?; - - Ok(RESTResponse::with_json(200, &body)) - } else { - Ok(RESTResponse::with_json(200, "{}")) - } -} diff --git a/modules/spdd_state/src/spdd_state.rs b/modules/spdd_state/src/spdd_state.rs index 97848468..4f8cd77d 100644 --- a/modules/spdd_state/src/spdd_state.rs +++ b/modules/spdd_state/src/spdd_state.rs @@ -53,12 +53,6 @@ impl SPDDState { let state_opt = if store_spdd { let state = Arc::new(Mutex::new(State::new())); - // Register /spdd REST endpoint - let state_rest = state.clone(); - handle_rest_with_query_parameters(context.clone(), &handle_spdd_topic, move |params| { - handle_spdd(state_rest.clone(), params) - }); - // Subscribe for spdd messages from accounts_state let state_handler = state.clone(); let mut message_subscription = context.subscribe(&subscribe_topic).await?; From 87a9a48c78ce4e9eb573ba07ef0a2b821848d71f Mon Sep 17 00:00:00 2001 From: Matthew Hounslow Date: Wed, 5 Nov 2025 14:21:29 -0800 Subject: [PATCH 06/20] Refactor: Improve error handling by replacing raw strings with `QueryError` --- common/src/queries/blocks.rs | 1 + modules/chain_store/src/chain_store.rs | 62 ++++++++++++++----- .../src/historical_accounts_state.rs | 59 +++++++++++++----- .../rest_blockfrost/src/handlers/assets.rs | 20 +++--- 4 files changed, 97 insertions(+), 45 deletions(-) diff --git a/common/src/queries/blocks.rs b/common/src/queries/blocks.rs index 62c5b1da..f7e5bd65 100644 --- a/common/src/queries/blocks.rs +++ b/common/src/queries/blocks.rs @@ -7,6 +7,7 @@ use cryptoxide::hashing::blake2b::Blake2b; use serde::ser::{Serialize, SerializeStruct, Serializer}; use serde_with::{hex::Hex, serde_as}; use std::collections::HashMap; +use std::path::Display; use crate::queries::errors::QueryError; pub const DEFAULT_BLOCKS_QUERY_TOPIC: (&str, &str) = diff --git a/modules/chain_store/src/chain_store.rs b/modules/chain_store/src/chain_store.rs index a358b895..456c2621 100644 --- a/modules/chain_store/src/chain_store.rs +++ b/modules/chain_store/src/chain_store.rs @@ -1,6 +1,8 @@ mod stores; +use crate::stores::{fjall::FjallStore, Block, Store}; use acropolis_codec::{block::map_to_block_issuer, map_parameters}; +use acropolis_common::queries::errors::QueryError; use acropolis_common::{ crypto::keyhash_224, messages::{CardanoMessage, Message, StateQuery, StateQueryResponse}, @@ -22,8 +24,6 @@ use std::sync::Arc; use tokio::sync::Mutex; use tracing::error; -use crate::stores::{fjall::FjallStore, Block, Store}; - const DEFAULT_BLOCKS_TOPIC: &str = "cardano.block.available"; const DEFAULT_PROTOCOL_PARAMETERS_TOPIC: &str = "cardano.protocol.parameters"; const DEFAULT_STORE: &str = "fjall"; @@ -66,16 +66,24 @@ impl ChainStore { async move { let Message::StateQuery(StateQuery::Blocks(query)) = req.as_ref() else { return Arc::new(Message::StateQueryResponse(StateQueryResponse::Blocks( - BlocksStateQueryResponse::Error("Invalid message for blocks-state".into()), + BlocksStateQueryResponse::Error( + QueryError::invalid_request("Invalid message for blocks-state") + ), ))); }; let Some(state) = query_history.lock().await.current().cloned() else { return Arc::new(Message::StateQueryResponse(StateQueryResponse::Blocks( - BlocksStateQueryResponse::Error("unitialised state".to_string()), + BlocksStateQueryResponse::Error( + QueryError::query_failed("Uninitialized state") + ), ))); }; let res = Self::handle_blocks_query(&query_store, &state, query) - .unwrap_or_else(|err| BlocksStateQueryResponse::Error(err.to_string())); + .unwrap_or_else(|err| { + BlocksStateQueryResponse::Error( + QueryError::query_failed(err.to_string()) + ) + }); Arc::new(Message::StateQueryResponse(StateQueryResponse::Blocks(res))) } }); @@ -132,42 +140,54 @@ impl ChainStore { match query { BlocksStateQuery::GetLatestBlock => { let Some(block) = store.get_latest_block()? else { - return Ok(BlocksStateQueryResponse::NotFound); + return Ok(BlocksStateQueryResponse::Error(QueryError::not_found( + "Latest block not found", + ))); }; let info = Self::to_block_info(block, store, state, true)?; Ok(BlocksStateQueryResponse::LatestBlock(info)) } BlocksStateQuery::GetLatestBlockTransactions { limit, skip, order } => { let Some(block) = store.get_latest_block()? else { - return Ok(BlocksStateQueryResponse::NotFound); + return Ok(BlocksStateQueryResponse::Error(QueryError::not_found( + "Latest block not found", + ))); }; let txs = Self::to_block_transactions(block, limit, skip, order)?; Ok(BlocksStateQueryResponse::LatestBlockTransactions(txs)) } BlocksStateQuery::GetLatestBlockTransactionsCBOR { limit, skip, order } => { let Some(block) = store.get_latest_block()? else { - return Ok(BlocksStateQueryResponse::NotFound); + return Ok(BlocksStateQueryResponse::Error(QueryError::not_found( + "Latest block not found", + ))); }; let txs = Self::to_block_transactions_cbor(block, limit, skip, order)?; Ok(BlocksStateQueryResponse::LatestBlockTransactionsCBOR(txs)) } BlocksStateQuery::GetBlockInfo { block_key } => { let Some(block) = Self::get_block_by_key(store, block_key)? else { - return Ok(BlocksStateQueryResponse::NotFound); + return Ok(BlocksStateQueryResponse::Error(QueryError::not_found( + &format!("Block {:?} not found", block_key), + ))); }; let info = Self::to_block_info(block, store, state, false)?; Ok(BlocksStateQueryResponse::BlockInfo(info)) } BlocksStateQuery::GetBlockBySlot { slot } => { let Some(block) = store.get_block_by_slot(*slot)? else { - return Ok(BlocksStateQueryResponse::NotFound); + return Ok(BlocksStateQueryResponse::Error(QueryError::not_found( + &format!("Block at slot {} not found", slot), + ))); }; let info = Self::to_block_info(block, store, state, false)?; Ok(BlocksStateQueryResponse::BlockBySlot(info)) } BlocksStateQuery::GetBlockByEpochSlot { epoch, slot } => { let Some(block) = store.get_block_by_epoch_slot(*epoch, *slot)? else { - return Ok(BlocksStateQueryResponse::NotFound); + return Ok(BlocksStateQueryResponse::Error(QueryError::not_found( + &format!("Block at epoch {} slot {} not found", epoch, slot), + ))); }; let info = Self::to_block_info(block, store, state, false)?; Ok(BlocksStateQueryResponse::BlockByEpochSlot(info)) @@ -183,7 +203,9 @@ impl ChainStore { })); } let Some(block) = Self::get_block_by_key(store, block_key)? else { - return Ok(BlocksStateQueryResponse::NotFound); + return Ok(BlocksStateQueryResponse::Error(QueryError::not_found( + &format!("Block {:?} not found", block_key), + ))); }; let number = match block_key { BlockKey::Number(number) => *number, @@ -208,7 +230,9 @@ impl ChainStore { })); } let Some(block) = Self::get_block_by_key(store, block_key)? else { - return Ok(BlocksStateQueryResponse::NotFound); + return Ok(BlocksStateQueryResponse::Error(QueryError::not_found( + &format!("Block {:?} not found", block_key), + ))); }; let number = match block_key { BlockKey::Number(number) => *number, @@ -233,7 +257,9 @@ impl ChainStore { order, } => { let Some(block) = Self::get_block_by_key(store, block_key)? else { - return Ok(BlocksStateQueryResponse::NotFound); + return Ok(BlocksStateQueryResponse::Error(QueryError::not_found( + &format!("Block {:?} not found", block_key), + ))); }; let txs = Self::to_block_transactions(block, limit, skip, order)?; Ok(BlocksStateQueryResponse::BlockTransactions(txs)) @@ -245,7 +271,9 @@ impl ChainStore { order, } => { let Some(block) = Self::get_block_by_key(store, block_key)? else { - return Ok(BlocksStateQueryResponse::NotFound); + return Ok(BlocksStateQueryResponse::Error(QueryError::not_found( + &format!("Block {:?} not found", block_key), + ))); }; let txs = Self::to_block_transactions_cbor(block, limit, skip, order)?; Ok(BlocksStateQueryResponse::BlockTransactionsCBOR(txs)) @@ -256,7 +284,9 @@ impl ChainStore { skip, } => { let Some(block) = Self::get_block_by_key(store, block_key)? else { - return Ok(BlocksStateQueryResponse::NotFound); + return Ok(BlocksStateQueryResponse::Error(QueryError::not_found( + &format!("Block {:?} not found", block_key), + ))); }; let addresses = Self::to_block_involved_addresses(block, limit, skip)?; Ok(BlocksStateQueryResponse::BlockInvolvedAddresses(addresses)) diff --git a/modules/historical_accounts_state/src/historical_accounts_state.rs b/modules/historical_accounts_state/src/historical_accounts_state.rs index db200fb4..7435a605 100644 --- a/modules/historical_accounts_state/src/historical_accounts_state.rs +++ b/modules/historical_accounts_state/src/historical_accounts_state.rs @@ -4,6 +4,7 @@ use acropolis_common::queries::accounts::{ AccountsStateQuery, AccountsStateQueryResponse, DEFAULT_HISTORICAL_ACCOUNTS_QUERY_TOPIC, }; +use acropolis_common::queries::errors::QueryError; use acropolis_common::{ messages::{CardanoMessage, Message, StateQuery, StateQueryResponse}, BlockInfo, BlockStatus, @@ -301,9 +302,9 @@ impl HistoricalAccountsState { async move { let Message::StateQuery(StateQuery::Accounts(query)) = message.as_ref() else { return Arc::new(Message::StateQueryResponse(StateQueryResponse::Accounts( - AccountsStateQueryResponse::Error( - "Invalid message for accounts-state".into(), - ), + AccountsStateQueryResponse::Error(QueryError::invalid_request( + "Invalid message for accounts-state", + )), ))); }; @@ -315,8 +316,12 @@ impl HistoricalAccountsState { registrations, ) } - Ok(None) => AccountsStateQueryResponse::NotFound, - Err(e) => AccountsStateQueryResponse::Error(e.to_string()), + Ok(None) => AccountsStateQueryResponse::Error(QueryError::not_found( + format!("Account {}", account), + )), + Err(e) => AccountsStateQueryResponse::Error(QueryError::query_failed( + format!("Failed to get registration history: {}", e), + )), } } AccountsStateQuery::GetAccountDelegationHistory { account } => { @@ -324,15 +329,23 @@ impl HistoricalAccountsState { Ok(Some(delegations)) => { AccountsStateQueryResponse::AccountDelegationHistory(delegations) } - Ok(None) => AccountsStateQueryResponse::NotFound, - Err(e) => AccountsStateQueryResponse::Error(e.to_string()), + Ok(None) => AccountsStateQueryResponse::Error(QueryError::not_found( + format!("Account {}", account), + )), + Err(e) => AccountsStateQueryResponse::Error(QueryError::query_failed( + format!("Failed to get delegation history: {}", e), + )), } } AccountsStateQuery::GetAccountMIRHistory { account } => { match state.lock().await.get_mir_history(account).await { Ok(Some(mirs)) => AccountsStateQueryResponse::AccountMIRHistory(mirs), - Ok(None) => AccountsStateQueryResponse::NotFound, - Err(e) => AccountsStateQueryResponse::Error(e.to_string()), + Ok(None) => AccountsStateQueryResponse::Error(QueryError::not_found( + format!("Account {}", account), + )), + Err(e) => AccountsStateQueryResponse::Error(QueryError::query_failed( + format!("Failed to get MIR history: {}", e), + )), } } AccountsStateQuery::GetAccountWithdrawalHistory { account } => { @@ -340,8 +353,12 @@ impl HistoricalAccountsState { Ok(Some(withdrawals)) => { AccountsStateQueryResponse::AccountWithdrawalHistory(withdrawals) } - Ok(None) => AccountsStateQueryResponse::NotFound, - Err(e) => AccountsStateQueryResponse::Error(e.to_string()), + Ok(None) => AccountsStateQueryResponse::Error(QueryError::not_found( + format!("Account {}", account), + )), + Err(e) => AccountsStateQueryResponse::Error(QueryError::query_failed( + format!("Failed to get withdrawal history: {}", e), + )), } } AccountsStateQuery::GetAccountRewardHistory { account } => { @@ -349,8 +366,12 @@ impl HistoricalAccountsState { Ok(Some(rewards)) => { AccountsStateQueryResponse::AccountRewardHistory(rewards) } - Ok(None) => AccountsStateQueryResponse::NotFound, - Err(e) => AccountsStateQueryResponse::Error(e.to_string()), + Ok(None) => AccountsStateQueryResponse::Error(QueryError::not_found( + format!("Account {}", account), + )), + Err(e) => AccountsStateQueryResponse::Error(QueryError::query_failed( + format!("Failed to get reward history: {}", e), + )), } } AccountsStateQuery::GetAccountAssociatedAddresses { account } => { @@ -358,14 +379,18 @@ impl HistoricalAccountsState { Ok(Some(addresses)) => { AccountsStateQueryResponse::AccountAssociatedAddresses(addresses) } - Ok(None) => AccountsStateQueryResponse::NotFound, - Err(e) => AccountsStateQueryResponse::Error(e.to_string()), + Ok(None) => AccountsStateQueryResponse::Error(QueryError::not_found( + format!("Account {}", account), + )), + Err(e) => AccountsStateQueryResponse::Error(QueryError::query_failed( + format!("Failed to get associated addresses: {}", e), + )), } } - _ => AccountsStateQueryResponse::Error(format!( + _ => AccountsStateQueryResponse::Error(QueryError::not_implemented(format!( "Unimplemented query variant: {:?}", query - )), + ))), }; Arc::new(Message::StateQueryResponse(StateQueryResponse::Accounts( diff --git a/modules/rest_blockfrost/src/handlers/assets.rs b/modules/rest_blockfrost/src/handlers/assets.rs index ce96f370..d0bc20d7 100644 --- a/modules/rest_blockfrost/src/handlers/assets.rs +++ b/modules/rest_blockfrost/src/handlers/assets.rs @@ -170,14 +170,14 @@ pub async fn handle_asset_history_blockfrost( AssetsStateQuery::GetAssetHistory { policy, name }, ))); - let response = match query_state( + let response = query_state( &context, &handlers_config.assets_query_topic, asset_query_msg, |message| match message { Message::StateQueryResponse(StateQueryResponse::Assets( - AssetsStateQueryResponse::AssetHistory(history), - )) => { + AssetsStateQueryResponse::AssetHistory(history), + )) => { let rest_history: Vec = history.iter().map(Into::into).collect(); match serde_json::to_string_pretty(&rest_history) { @@ -189,11 +189,11 @@ pub async fn handle_asset_history_blockfrost( } } Message::StateQueryResponse(StateQueryResponse::Assets( - AssetsStateQueryResponse::NotFound, - )) => Ok(RESTResponse::with_text(404, "Asset history not found")), + AssetsStateQueryResponse::NotFound, + )) => Ok(RESTResponse::with_text(404, "Asset history not found")), Message::StateQueryResponse(StateQueryResponse::Assets( - AssetsStateQueryResponse::Error(_), - )) => Ok(RESTResponse::with_text( + AssetsStateQueryResponse::Error(_), + )) => Ok(RESTResponse::with_text( 501, "Asset history storage is disabled in config", )), @@ -203,11 +203,7 @@ pub async fn handle_asset_history_blockfrost( )), }, ) - .await - { - Ok(rest) => rest, - Err(e) => RESTResponse::with_text(500, &format!("Query failed: {e}")), - }; + .await.unwrap_or_else(|e| RESTResponse::with_text(500, &format!("Query failed: {e}"))); Ok(response) } From 8f28bd8a38686f8edd83463088e3f8f3c2488888 Mon Sep 17 00:00:00 2001 From: Matthew Hounslow Date: Wed, 5 Nov 2025 15:19:05 -0800 Subject: [PATCH 07/20] Refactor: Reorder imports and adjust formatting for improved readability in `accounts_state.rs` and `accounts.rs` --- common/src/queries/accounts.rs | 2 +- modules/accounts_state/src/accounts_state.rs | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/common/src/queries/accounts.rs b/common/src/queries/accounts.rs index 2fdaa55f..c3e05b8d 100644 --- a/common/src/queries/accounts.rs +++ b/common/src/queries/accounts.rs @@ -1,9 +1,9 @@ use std::collections::HashMap; +use crate::queries::errors::QueryError; use crate::{ DRepChoice, PoolId, PoolLiveStakeInfo, RewardType, ShelleyAddress, StakeAddress, TxIdentifier, }; -use crate::queries::errors::QueryError; pub const DEFAULT_ACCOUNTS_QUERY_TOPIC: (&str, &str) = ("accounts-state-query-topic", "cardano.query.accounts"); diff --git a/modules/accounts_state/src/accounts_state.rs b/modules/accounts_state/src/accounts_state.rs index 80a9c74d..76fa7551 100644 --- a/modules/accounts_state/src/accounts_state.rs +++ b/modules/accounts_state/src/accounts_state.rs @@ -552,9 +552,9 @@ impl AccountsState { Some(map) => { AccountsStateQueryResponse::AccountsDrepDelegationsMap(map) } - None => AccountsStateQueryResponse::Error( - QueryError::query_failed("Error retrieving DRep delegations map"), - ), + None => AccountsStateQueryResponse::Error(QueryError::query_failed( + "Error retrieving DRep delegations map", + )), } } From 571d63d614cfa26adf50e96fe5147ae607ec0984 Mon Sep 17 00:00:00 2001 From: Matthew Hounslow Date: Wed, 5 Nov 2025 15:20:17 -0800 Subject: [PATCH 08/20] Refactor: Standardize error handling with `QueryError` integration, improve code consistency, and reintroduce `/drdd` and `/spdd` REST endpoints --- common/src/lib.rs | 2 +- common/src/queries/addresses.rs | 2 +- common/src/queries/assets.rs | 4 +- common/src/queries/blocks.rs | 3 +- common/src/queries/epochs.rs | 4 +- common/src/queries/errors.rs | 40 +- common/src/queries/governance.rs | 15 +- common/src/queries/mempool.rs | 5 +- common/src/queries/metadata.rs | 5 +- common/src/queries/mod.rs | 2 +- common/src/queries/network.rs | 5 +- common/src/queries/parameters.rs | 4 +- common/src/queries/pools.rs | 4 +- common/src/queries/scripts.rs | 5 +- common/src/queries/spdd.rs | 5 +- common/src/queries/transactions.rs | 5 +- common/src/queries/utils.rs | 12 +- common/src/queries/utxos.rs | 2 +- common/src/rest_error.rs | 2 +- common/src/rest_helper.rs | 2 +- common/src/serialization.rs | 4 +- common/src/types.rs | 1 + modules/assets_state/src/assets_state.rs | 121 ++- modules/chain_store/src/chain_store.rs | 20 +- modules/drdd_state/src/drdd_state.rs | 8 + modules/drdd_state/src/rest.rs | 74 ++ modules/drep_state/src/drep_state.rs | 106 +- modules/epochs_state/src/epochs_state.rs | 29 +- .../governance_state/src/governance_state.rs | 19 +- .../parameters_state/src/parameters_state.rs | 19 +- .../rest_blockfrost/src/handlers/addresses.rs | 12 +- .../rest_blockfrost/src/handlers/assets.rs | 385 +++----- .../rest_blockfrost/src/handlers/blocks.rs | 199 ++-- .../rest_blockfrost/src/handlers/epochs.rs | 380 +++----- .../src/handlers/governance.rs | 912 +++++++----------- modules/rest_blockfrost/src/handlers/pools.rs | 646 +++++-------- .../rest_blockfrost/src/rest_blockfrost.rs | 2 +- modules/spdd_state/src/rest.rs | 47 + modules/spdd_state/src/spdd_state.rs | 14 +- modules/spo_state/src/spo_state.rs | 110 ++- modules/utxo_state/src/utxo_state.rs | 9 +- 41 files changed, 1489 insertions(+), 1756 deletions(-) create mode 100644 modules/drdd_state/src/rest.rs create mode 100644 modules/spdd_state/src/rest.rs diff --git a/common/src/lib.rs b/common/src/lib.rs index c0d2bb14..b3d7dc53 100644 --- a/common/src/lib.rs +++ b/common/src/lib.rs @@ -15,6 +15,7 @@ pub mod protocol_params; pub mod queries; pub mod rational_number; pub mod resolver; +pub mod rest_error; pub mod rest_helper; pub mod serialization; pub mod snapshot; @@ -22,7 +23,6 @@ pub mod stake_addresses; pub mod state_history; pub mod types; pub mod validation; -pub mod rest_error; // Flattened re-exports pub use self::address::*; diff --git a/common/src/queries/addresses.rs b/common/src/queries/addresses.rs index 3bc3d0e8..28030f01 100644 --- a/common/src/queries/addresses.rs +++ b/common/src/queries/addresses.rs @@ -1,5 +1,5 @@ -use crate::{Address, AddressTotals, TxIdentifier, UTxOIdentifier}; use crate::queries::errors::QueryError; +use crate::{Address, AddressTotals, TxIdentifier, UTxOIdentifier}; pub const DEFAULT_ADDRESS_QUERY_TOPIC: (&str, &str) = ("address-state-query-topic", "cardano.query.address"); diff --git a/common/src/queries/assets.rs b/common/src/queries/assets.rs index 2234af46..ba549cee 100644 --- a/common/src/queries/assets.rs +++ b/common/src/queries/assets.rs @@ -1,3 +1,4 @@ +use crate::queries::errors::QueryError; use crate::{ AssetAddressEntry, AssetInfoRecord, AssetMintRecord, AssetName, PolicyAsset, PolicyId, TxIdentifier, @@ -36,6 +37,5 @@ pub enum AssetsStateQueryResponse { AssetAddresses(AssetAddresses), AssetTransactions(AssetTransactions), PolicyIdAssets(PolicyAssets), - NotFound, - Error(String), + Error(QueryError), } diff --git a/common/src/queries/blocks.rs b/common/src/queries/blocks.rs index f7e5bd65..5f32a9b2 100644 --- a/common/src/queries/blocks.rs +++ b/common/src/queries/blocks.rs @@ -1,3 +1,4 @@ +use crate::queries::errors::QueryError; use crate::{ queries::misc::Order, serialization::{Bech32Conversion, Bech32WithHrp}, @@ -7,8 +8,6 @@ use cryptoxide::hashing::blake2b::Blake2b; use serde::ser::{Serialize, SerializeStruct, Serializer}; use serde_with::{hex::Hex, serde_as}; use std::collections::HashMap; -use std::path::Display; -use crate::queries::errors::QueryError; pub const DEFAULT_BLOCKS_QUERY_TOPIC: (&str, &str) = ("blocks-state-query-topic", "cardano.query.blocks"); diff --git a/common/src/queries/epochs.rs b/common/src/queries/epochs.rs index 8b3abee5..572a6cb3 100644 --- a/common/src/queries/epochs.rs +++ b/common/src/queries/epochs.rs @@ -1,3 +1,4 @@ +use crate::queries::errors::QueryError; use crate::{messages::EpochActivityMessage, protocol_params::ProtocolParams, PoolId}; pub const DEFAULT_EPOCHS_QUERY_TOPIC: (&str, &str) = @@ -24,8 +25,7 @@ pub enum EpochsStateQueryResponse { EpochStakeDistributionByPool(EpochStakeDistributionByPool), LatestEpochBlocksMintedByPool(u64), - NotFound, - Error(String), + Error(QueryError), } #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] diff --git a/common/src/queries/errors.rs b/common/src/queries/errors.rs index 6caa1095..1cdd21c3 100644 --- a/common/src/queries/errors.rs +++ b/common/src/queries/errors.rs @@ -5,70 +5,58 @@ use std::fmt; #[derive(Debug, Clone, Serialize, Deserialize)] pub enum QueryError { /// The requested resource was not found - NotFound { - resource: String - }, + NotFound { resource: String }, /// An error occurred while processing the query - QueryFailed { - message: String - }, + QueryFailed { message: String }, /// Storage backend is disabled in configuration - StorageDisabled { - storage_type: String - }, + StorageDisabled { storage_type: String }, /// Invalid request parameters - InvalidRequest { - message: String - }, + InvalidRequest { message: String }, /// One or more resources in a batch query were not found - PartialNotFound { - message: String, - }, + PartialNotFound { message: String }, /// Query variant is not implemented yet - NotImplemented { - query: String, - }, + NotImplemented { query: String }, } impl QueryError { pub fn not_found(resource: impl Into) -> Self { Self::NotFound { - resource: resource.into() + resource: resource.into(), } } pub fn query_failed(message: impl Into) -> Self { Self::QueryFailed { - message: message.into() + message: message.into(), } } pub fn storage_disabled(storage_type: impl Into) -> Self { Self::StorageDisabled { - storage_type: storage_type.into() + storage_type: storage_type.into(), } } pub fn invalid_request(message: impl Into) -> Self { Self::InvalidRequest { - message: message.into() + message: message.into(), } } pub fn partial_not_found(message: impl Into) -> Self { Self::PartialNotFound { - message: message.into() + message: message.into(), } } pub fn not_implemented(query: impl Into) -> Self { Self::NotImplemented { - query: query.into() + query: query.into(), } } } @@ -78,7 +66,9 @@ impl fmt::Display for QueryError { match self { Self::NotFound { resource } => write!(f, "Not found: {}", resource), Self::QueryFailed { message } => write!(f, "Query failed: {}", message), - Self::StorageDisabled { storage_type } => write!(f, "{} storage is not enabled", storage_type), + Self::StorageDisabled { storage_type } => { + write!(f, "{} storage is not enabled", storage_type) + } Self::InvalidRequest { message } => write!(f, "Invalid request: {}", message), Self::PartialNotFound { message } => write!(f, "Partial result: {}", message), Self::NotImplemented { query } => write!(f, "Query not implemented: {}", query), diff --git a/common/src/queries/governance.rs b/common/src/queries/governance.rs index 074ae38b..da35ed8a 100644 --- a/common/src/queries/governance.rs +++ b/common/src/queries/governance.rs @@ -1,5 +1,6 @@ use std::collections::HashMap; +use crate::queries::errors::QueryError; use crate::{ Anchor, DRepCredential, GovActionId, Lovelace, ProposalProcedure, StakeAddress, TxHash, TxIdentifier, Vote, Voter, VotingProcedure, @@ -41,8 +42,7 @@ pub enum GovernanceStateQueryResponse { ProposalWithdrawals(ProposalWithdrawals), ProposalVotes(ProposalVotes), ProposalMetadata(ProposalMetadata), - NotFound, - Error(String), + Error(QueryError), } #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] @@ -124,14 +124,3 @@ pub struct ProposalVotes { #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] pub struct ProposalMetadata {} - -pub fn handle_governance_query_result( - result: anyhow::Result>, - mapper: impl FnOnce(T) -> GovernanceStateQueryResponse, -) -> GovernanceStateQueryResponse { - match result { - Ok(Some(val)) => mapper(val), - Ok(None) => GovernanceStateQueryResponse::NotFound, - Err(e) => GovernanceStateQueryResponse::Error(e.to_string()), - } -} diff --git a/common/src/queries/mempool.rs b/common/src/queries/mempool.rs index 83d62b7e..4eda6e0b 100644 --- a/common/src/queries/mempool.rs +++ b/common/src/queries/mempool.rs @@ -1,3 +1,5 @@ +use crate::queries::errors::QueryError; + #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] pub enum MempoolStateQuery { GetMempoolList, @@ -10,8 +12,7 @@ pub enum MempoolStateQueryResponse { MempoolList(MempoolList), MempoolTransaction(MempoolTransaction), MempoolTransactionByAddress(MempoolTransactionByAddress), - NotFound, - Error(String), + Error(QueryError), } #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] diff --git a/common/src/queries/metadata.rs b/common/src/queries/metadata.rs index 91a5ecda..5c4654e0 100644 --- a/common/src/queries/metadata.rs +++ b/common/src/queries/metadata.rs @@ -1,3 +1,5 @@ +use crate::queries::errors::QueryError; + #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] pub enum MetadataStateQuery { GetMetadataLabels, @@ -10,8 +12,7 @@ pub enum MetadataStateQueryResponse { MetadataLabels(MetadataLabels), TransactionMetadataJSON(TransactionMetadataJSON), TransactionMetadataCBOR(TransactionMetadataCBOR), - NotFound, - Error(String), + Error(QueryError), } #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] diff --git a/common/src/queries/mod.rs b/common/src/queries/mod.rs index 9c6664d4..2e7fa2b3 100644 --- a/common/src/queries/mod.rs +++ b/common/src/queries/mod.rs @@ -7,6 +7,7 @@ pub mod addresses; pub mod assets; pub mod blocks; pub mod epochs; +pub mod errors; pub mod governance; pub mod ledger; pub mod mempool; @@ -20,7 +21,6 @@ pub mod spdd; pub mod transactions; pub mod utils; pub mod utxos; -pub mod errors; pub fn get_query_topic(context: Arc>, topic: (&str, &str)) -> String { context.config.get_string(topic.0).unwrap_or_else(|_| topic.1.to_string()) diff --git a/common/src/queries/network.rs b/common/src/queries/network.rs index 78d5432d..fc039c26 100644 --- a/common/src/queries/network.rs +++ b/common/src/queries/network.rs @@ -1,3 +1,5 @@ +use crate::queries::errors::QueryError; + #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] pub enum NetworkStateQuery { GetNetworkInformation, @@ -8,8 +10,7 @@ pub enum NetworkStateQuery { pub enum NetworkStateQueryResponse { NetworkInformation(NetworkInformation), EraSummary(EraSummary), - NotFound, - Error(String), + Error(QueryError), } #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] diff --git a/common/src/queries/parameters.rs b/common/src/queries/parameters.rs index 03e26408..c7a418f8 100644 --- a/common/src/queries/parameters.rs +++ b/common/src/queries/parameters.rs @@ -1,4 +1,5 @@ use crate::protocol_params::ProtocolParams; +use crate::queries::errors::QueryError; pub const DEFAULT_PARAMETERS_QUERY_TOPIC: (&str, &str) = ("parameters-state-query-topic", "cardano.query.parameters"); @@ -16,8 +17,7 @@ pub enum ParametersStateQueryResponse { EpochParameters(ProtocolParams), NetworkName(String), - NotFound, - Error(String), + Error(QueryError), } #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] diff --git a/common/src/queries/pools.rs b/common/src/queries/pools.rs index bb3e9d7d..5bc75ddf 100644 --- a/common/src/queries/pools.rs +++ b/common/src/queries/pools.rs @@ -1,3 +1,4 @@ +use crate::queries::errors::QueryError; use crate::{ queries::governance::VoteRecord, rational_number::RationalNumber, PoolEpochState, PoolId, PoolMetadata, PoolRegistration, PoolRetirement, PoolUpdateEvent, Relay, StakeAddress, @@ -77,8 +78,7 @@ pub enum PoolsStateQueryResponse { BlocksByPoolAndEpoch(Vec), PoolUpdates(Vec), PoolVotes(Vec), - NotFound, - Error(String), + Error(QueryError), } #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] diff --git a/common/src/queries/scripts.rs b/common/src/queries/scripts.rs index c4524597..4e159666 100644 --- a/common/src/queries/scripts.rs +++ b/common/src/queries/scripts.rs @@ -1,3 +1,5 @@ +use crate::queries::errors::QueryError; + #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] pub enum ScriptsStateQuery { GetScriptsList, @@ -18,8 +20,7 @@ pub enum ScriptsStateQueryResponse { ScriptRedeemers(ScriptRedeemers), ScriptDatumJSON(ScriptDatumJSON), ScriptDatumCBOR(ScriptDatumCBOR), - NotFound, - Error(String), + Error(QueryError), } #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] diff --git a/common/src/queries/spdd.rs b/common/src/queries/spdd.rs index 000d813f..ed6e2127 100644 --- a/common/src/queries/spdd.rs +++ b/common/src/queries/spdd.rs @@ -1,3 +1,5 @@ +use crate::queries::errors::QueryError; + pub const DEFAULT_SPDD_QUERY_TOPIC: (&str, &str) = ("spdd-state-query-topic", "cardano.query.spdd"); #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] @@ -9,6 +11,5 @@ pub enum SPDDStateQuery { pub enum SPDDStateQueryResponse { EpochTotalActiveStakes(u64), - NotFound, - Error(String), + Error(QueryError), } diff --git a/common/src/queries/transactions.rs b/common/src/queries/transactions.rs index 0bb5b407..ec1283c4 100644 --- a/common/src/queries/transactions.rs +++ b/common/src/queries/transactions.rs @@ -1,3 +1,5 @@ +use crate::queries::errors::QueryError; + #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] pub enum TransactionsStateQuery { GetTransactionInfo, @@ -30,8 +32,7 @@ pub enum TransactionsStateQueryResponse { TransactionRedeemers(TransactionRedeemers), TransactionRequiredSigners(TransactionRequiredSigners), TransactionCBOR(TransactionCBOR), - NotFound, - Error(String), + Error(QueryError), } #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] diff --git a/common/src/queries/utils.rs b/common/src/queries/utils.rs index 57f51ae6..b629e0d3 100644 --- a/common/src/queries/utils.rs +++ b/common/src/queries/utils.rs @@ -41,12 +41,14 @@ where T: Serialize, { let data = query_state(context, topic, request_msg, |response| { - extractor(response).unwrap_or_else(|| Err(QueryError::query_failed(format!( - "Unexpected response message type from {topic}" - )))) + extractor(response).unwrap_or_else(|| { + Err(QueryError::query_failed(format!( + "Unexpected response message type from {topic}" + ))) + }) }) - .await?; // QueryError auto-converts to RESTError via From trait + .await?; // QueryError auto-converts to RESTError via From trait let json = serde_json::to_string_pretty(&data)?; // Uses From for RESTError Ok(RESTResponse::with_json(200, &json)) -} \ No newline at end of file +} diff --git a/common/src/queries/utxos.rs b/common/src/queries/utxos.rs index 80d9a2ec..cbcd8a71 100644 --- a/common/src/queries/utxos.rs +++ b/common/src/queries/utxos.rs @@ -1,5 +1,5 @@ -use crate::{UTxOIdentifier, Value}; use crate::queries::errors::QueryError; +use crate::{UTxOIdentifier, Value}; pub const DEFAULT_UTXOS_QUERY_TOPIC: (&str, &str) = ("utxo-state-query-topic", "cardano.query.utxos"); diff --git a/common/src/rest_error.rs b/common/src/rest_error.rs index 80bfb3ef..64f72eb6 100644 --- a/common/src/rest_error.rs +++ b/common/src/rest_error.rs @@ -213,4 +213,4 @@ mod tests { assert_eq!(response.code, 400); assert_eq!(response.body, "Invalid stake address"); } -} \ No newline at end of file +} diff --git a/common/src/rest_helper.rs b/common/src/rest_helper.rs index 847a5765..9eab49c3 100644 --- a/common/src/rest_helper.rs +++ b/common/src/rest_helper.rs @@ -1,7 +1,7 @@ //! Helper functions for REST handlers -use crate::rest_error::RESTError; use crate::messages::{Message, RESTResponse}; +use crate::rest_error::RESTError; use anyhow::{anyhow, Result}; use caryatid_sdk::Context; use futures::future::Future; diff --git a/common/src/serialization.rs b/common/src/serialization.rs index fcacfd0f..ece26c7f 100644 --- a/common/src/serialization.rs +++ b/common/src/serialization.rs @@ -1,12 +1,12 @@ use std::marker::PhantomData; +use crate::rest_error::RESTError; use crate::PoolId; use anyhow::anyhow; use bech32::{Bech32, Hrp}; +use caryatid_module_rest_server::messages::RESTResponse; use serde::{ser::SerializeMap, Deserialize, Serialize, Serializer}; use serde_with::{ser::SerializeAsWrap, DeserializeAs, SerializeAs}; -use caryatid_module_rest_server::messages::RESTResponse; -use crate::rest_error::RESTError; pub struct SerializeMapAs(std::marker::PhantomData<(KAs, VAs)>); diff --git a/common/src/types.rs b/common/src/types.rs index 41ecc62e..5c583aa0 100644 --- a/common/src/types.rs +++ b/common/src/types.rs @@ -251,6 +251,7 @@ impl fmt::Display for RewardType { } pub type PolicyId = [u8; 28]; + pub type NativeAssets = Vec<(PolicyId, Vec)>; pub type NativeAssetsDelta = Vec<(PolicyId, Vec)>; pub type NativeAssetsMap = HashMap>; diff --git a/modules/assets_state/src/assets_state.rs b/modules/assets_state/src/assets_state.rs index 240277cd..6605ab9a 100644 --- a/modules/assets_state/src/assets_state.rs +++ b/modules/assets_state/src/assets_state.rs @@ -8,7 +8,10 @@ use crate::{ }; use acropolis_common::{ messages::{CardanoMessage, Message, StateQuery, StateQueryResponse}, - queries::assets::{AssetsStateQuery, AssetsStateQueryResponse, DEFAULT_ASSETS_QUERY_TOPIC}, + queries::{ + assets::{AssetsStateQuery, AssetsStateQueryResponse, DEFAULT_ASSETS_QUERY_TOPIC}, + errors::QueryError, + }, state_history::{StateHistory, StateHistoryStore}, BlockInfo, BlockStatus, }; @@ -224,7 +227,7 @@ impl AssetsState { } } - // Get configuration flags and topis + // Get configuration flags and topics let storage_config = AssetsStorageConfig { store_assets: get_bool_flag(&config, DEFAULT_STORE_ASSETS), store_info: get_bool_flag(&config, DEFAULT_STORE_INFO), @@ -258,7 +261,7 @@ impl AssetsState { let assets_query_topic = get_string_flag(&config, DEFAULT_ASSETS_QUERY_TOPIC); info!("Creating asset query handler on '{assets_query_topic}'"); - // Initalize state history + // Initialize state history let history = Arc::new(Mutex::new(StateHistory::::new( "AssetsState", StateHistoryStore::default_block_store(), @@ -279,7 +282,9 @@ impl AssetsState { async move { let Message::StateQuery(StateQuery::Assets(query)) = message.as_ref() else { return Arc::new(Message::StateQueryResponse(StateQueryResponse::Assets( - AssetsStateQueryResponse::Error("Invalid message for assets-state".into()), + AssetsStateQueryResponse::Error(QueryError::invalid_request( + "Invalid message for assets-state", + )), ))); }; @@ -293,7 +298,9 @@ impl AssetsState { let reg = registry.lock().await; match state.get_assets_list(®) { Ok(list) => AssetsStateQueryResponse::AssetsList(list), - Err(e) => AssetsStateQueryResponse::Error(e.to_string()), + Err(e) => AssetsStateQueryResponse::Error(QueryError::query_failed( + e.to_string(), + )), } } AssetsStateQuery::GetAssetInfo { policy, name } => { @@ -301,16 +308,28 @@ impl AssetsState { match reg.lookup_id(policy, name) { Some(asset_id) => match state.get_asset_info(&asset_id, ®) { Ok(Some(info)) => AssetsStateQueryResponse::AssetInfo(info), - Ok(None) => AssetsStateQueryResponse::NotFound, - Err(e) => AssetsStateQueryResponse::Error(e.to_string()), + Ok(None) => { + AssetsStateQueryResponse::Error(QueryError::not_found(format!( + "Asset {}:{}", + hex::encode(policy), + hex::encode(name.as_slice()) + ))) + } + Err(e) => AssetsStateQueryResponse::Error( + QueryError::query_failed(e.to_string()), + ), }, None => { if state.config.store_info && state.config.store_assets { - AssetsStateQueryResponse::NotFound + AssetsStateQueryResponse::Error(QueryError::not_found(format!( + "Asset {}:{}", + hex::encode(policy), + hex::encode(name.as_slice()) + ))) } else { - AssetsStateQueryResponse::Error( - "asset info storage disabled in config".to_string(), - ) + AssetsStateQueryResponse::Error(QueryError::storage_disabled( + "asset info", + )) } } } @@ -322,16 +341,28 @@ impl AssetsState { Ok(Some(history)) => { AssetsStateQueryResponse::AssetHistory(history) } - Ok(None) => AssetsStateQueryResponse::NotFound, - Err(e) => AssetsStateQueryResponse::Error(e.to_string()), + Ok(None) => { + AssetsStateQueryResponse::Error(QueryError::not_found(format!( + "Asset history for {}:{}", + hex::encode(policy), + hex::encode(name.as_slice()) + ))) + } + Err(e) => AssetsStateQueryResponse::Error( + QueryError::query_failed(e.to_string()), + ), }, None => { if state.config.store_history { - AssetsStateQueryResponse::NotFound + AssetsStateQueryResponse::Error(QueryError::not_found(format!( + "Asset history for {}:{}", + hex::encode(policy), + hex::encode(name.as_slice()) + ))) } else { - AssetsStateQueryResponse::Error( - "asset history storage disabled in config".to_string(), - ) + AssetsStateQueryResponse::Error(QueryError::storage_disabled( + "asset history", + )) } } } @@ -343,16 +374,28 @@ impl AssetsState { Ok(Some(addresses)) => { AssetsStateQueryResponse::AssetAddresses(addresses) } - Ok(None) => AssetsStateQueryResponse::NotFound, - Err(e) => AssetsStateQueryResponse::Error(e.to_string()), + Ok(None) => { + AssetsStateQueryResponse::Error(QueryError::not_found(format!( + "Asset addresses for {}:{}", + hex::encode(policy), + hex::encode(name.as_slice()) + ))) + } + Err(e) => AssetsStateQueryResponse::Error( + QueryError::query_failed(e.to_string()), + ), }, None => { if state.config.store_addresses { - AssetsStateQueryResponse::NotFound + AssetsStateQueryResponse::Error(QueryError::not_found(format!( + "Asset addresses for {}:{}", + hex::encode(policy), + hex::encode(name.as_slice()) + ))) } else { - AssetsStateQueryResponse::Error( - "asset addresses storage disabled in config".to_string(), - ) + AssetsStateQueryResponse::Error(QueryError::storage_disabled( + "asset addresses", + )) } } } @@ -362,16 +405,28 @@ impl AssetsState { match reg.lookup_id(policy, name) { Some(asset_id) => match state.get_asset_transactions(&asset_id) { Ok(Some(txs)) => AssetsStateQueryResponse::AssetTransactions(txs), - Ok(None) => AssetsStateQueryResponse::NotFound, - Err(e) => AssetsStateQueryResponse::Error(e.to_string()), + Ok(None) => { + AssetsStateQueryResponse::Error(QueryError::not_found(format!( + "Asset transactions for {}:{}", + hex::encode(policy), + hex::encode(name.as_slice()) + ))) + } + Err(e) => AssetsStateQueryResponse::Error( + QueryError::query_failed(e.to_string()), + ), }, None => { if state.config.store_transactions.is_enabled() { - AssetsStateQueryResponse::NotFound + AssetsStateQueryResponse::Error(QueryError::not_found(format!( + "Asset transactions for {}:{}", + hex::encode(policy), + hex::encode(name.as_slice()) + ))) } else { - AssetsStateQueryResponse::Error( - "asset transactions storage disabled in config".to_string(), - ) + AssetsStateQueryResponse::Error(QueryError::storage_disabled( + "asset transactions", + )) } } } @@ -380,8 +435,12 @@ impl AssetsState { let reg = registry.lock().await; match state.get_policy_assets(policy, ®) { Ok(Some(assets)) => AssetsStateQueryResponse::PolicyIdAssets(assets), - Ok(None) => AssetsStateQueryResponse::NotFound, - Err(e) => AssetsStateQueryResponse::Error(e.to_string()), + Ok(None) => AssetsStateQueryResponse::Error(QueryError::not_found( + format!("Assets for policy {}", hex::encode(policy)), + )), + Err(e) => AssetsStateQueryResponse::Error(QueryError::query_failed( + e.to_string(), + )), } } }; diff --git a/modules/chain_store/src/chain_store.rs b/modules/chain_store/src/chain_store.rs index 456c2621..9753e8c4 100644 --- a/modules/chain_store/src/chain_store.rs +++ b/modules/chain_store/src/chain_store.rs @@ -66,23 +66,21 @@ impl ChainStore { async move { let Message::StateQuery(StateQuery::Blocks(query)) = req.as_ref() else { return Arc::new(Message::StateQueryResponse(StateQueryResponse::Blocks( - BlocksStateQueryResponse::Error( - QueryError::invalid_request("Invalid message for blocks-state") - ), + BlocksStateQueryResponse::Error(QueryError::invalid_request( + "Invalid message for blocks-state", + )), ))); }; let Some(state) = query_history.lock().await.current().cloned() else { return Arc::new(Message::StateQueryResponse(StateQueryResponse::Blocks( - BlocksStateQueryResponse::Error( - QueryError::query_failed("Uninitialized state") - ), + BlocksStateQueryResponse::Error(QueryError::query_failed( + "Uninitialized state", + )), ))); }; - let res = Self::handle_blocks_query(&query_store, &state, query) - .unwrap_or_else(|err| { - BlocksStateQueryResponse::Error( - QueryError::query_failed(err.to_string()) - ) + let res = + Self::handle_blocks_query(&query_store, &state, query).unwrap_or_else(|err| { + BlocksStateQueryResponse::Error(QueryError::query_failed(err.to_string())) }); Arc::new(Message::StateQueryResponse(StateQueryResponse::Blocks(res))) } diff --git a/modules/drdd_state/src/drdd_state.rs b/modules/drdd_state/src/drdd_state.rs index 05c03bd7..056b8495 100644 --- a/modules/drdd_state/src/drdd_state.rs +++ b/modules/drdd_state/src/drdd_state.rs @@ -12,6 +12,8 @@ use tokio::sync::Mutex; use tracing::{error, info, info_span, Instrument}; mod state; use state::State; +mod rest; +use rest::handle_drdd; const DEFAULT_SUBSCRIBE_TOPIC: &str = "cardano.drep.distribution"; const DEFAULT_HANDLE_DRDD_TOPIC: (&str, &str) = ("handle-topic-drdd", "rest.get.drdd"); @@ -104,6 +106,12 @@ impl DRDDState { None }; + // Register /drdd REST endpoint + handle_rest_with_query_parameters(context.clone(), &handle_drdd_topic, move |params| { + let state_rest = state_opt.clone(); + handle_drdd(state_rest.clone(), params) + }); + Ok(()) } } diff --git a/modules/drdd_state/src/rest.rs b/modules/drdd_state/src/rest.rs new file mode 100644 index 00000000..f69d0a48 --- /dev/null +++ b/modules/drdd_state/src/rest.rs @@ -0,0 +1,74 @@ +use crate::state::State; +use acropolis_common::rest_error::RESTError; +use acropolis_common::{extract_strict_query_params, messages::RESTResponse, DRepCredential}; +use serde::Serialize; +use std::{collections::HashMap, sync::Arc}; +use tokio::sync::Mutex; + +// Response struct for DRDD +#[derive(Serialize)] +struct DRDDResponse { + dreps: HashMap, + abstain: u64, + no_confidence: u64, +} + +/// Handles /drdd +pub async fn handle_drdd( + state: Option>>, + params: HashMap, +) -> Result { + let state_arc = state.as_ref().ok_or_else(|| RESTError::storage_disabled("DRDD"))?; + + let locked = state_arc.lock().await; + + extract_strict_query_params!(params, { + "epoch" => epoch: Option, + }); + + let drdd_opt = match epoch { + Some(epoch) => match locked.get_epoch(epoch) { + Some(drdd) => Some(drdd), + None => { + return Err(RESTError::not_found(&format!( + "DRDD not found for epoch {}", + epoch + ))); + } + }, + None => locked.get_latest(), + }; + + if let Some(drdd) = drdd_opt { + let dreps: HashMap = drdd + .dreps + .iter() + .map(|(k, v)| { + let key = k.to_drep_bech32().unwrap_or_else(|_| match k { + DRepCredential::AddrKeyHash(bytes) | DRepCredential::ScriptHash(bytes) => { + hex::encode(bytes) + } + }); + (key, *v) + }) + .collect(); + + let response = DRDDResponse { + dreps, + abstain: drdd.abstain, + no_confidence: drdd.no_confidence, + }; + + let body = serde_json::to_string(&response)?; + Ok(RESTResponse::with_json(200, &body)) + } else { + let response = DRDDResponse { + dreps: HashMap::new(), + abstain: 0, + no_confidence: 0, + }; + + let body = serde_json::to_string(&response)?; + Ok(RESTResponse::with_json(200, &body)) + } +} diff --git a/modules/drep_state/src/drep_state.rs b/modules/drep_state/src/drep_state.rs index c1b14cf1..b600ff96 100644 --- a/modules/drep_state/src/drep_state.rs +++ b/modules/drep_state/src/drep_state.rs @@ -1,6 +1,7 @@ //! Acropolis DRep State module for Caryatid //! Accepts certificate events and derives the DRep State in memory +use acropolis_common::queries::errors::QueryError; use acropolis_common::{ messages::{CardanoMessage, Message, StateQuery, StateQueryResponse}, queries::governance::{ @@ -265,9 +266,9 @@ impl DRepState { async move { let Message::StateQuery(StateQuery::Governance(query)) = message.as_ref() else { return Arc::new(Message::StateQueryResponse(StateQueryResponse::Governance( - GovernanceStateQueryResponse::Error( - "Invalid message for governance-state".into(), - ), + GovernanceStateQueryResponse::Error(QueryError::invalid_request( + "Invalid message for governance-state", + )), ))); }; @@ -279,7 +280,9 @@ impl DRepState { let dreps = state.list(); GovernanceStateQueryResponse::DRepsList(DRepsList { dreps }) } - None => GovernanceStateQueryResponse::Error("No current DRep state".into()), + None => GovernanceStateQueryResponse::Error(QueryError::query_failed( + "No current DRep state", + )), }, GovernanceStateQuery::GetDRepInfoWithDelegators { drep_credential } => { match locked.current() { @@ -303,19 +306,28 @@ impl DRepState { ) } - Ok(None) => GovernanceStateQueryResponse::NotFound, - Err(msg) => { - GovernanceStateQueryResponse::Error(msg.to_string()) - } + Ok(None) => GovernanceStateQueryResponse::Error( + QueryError::not_found(format!( + "DRep delegators for {:?}", + drep_credential + )), + ), + Err(msg) => GovernanceStateQueryResponse::Error( + QueryError::query_failed(msg), + ), } } - Ok(None) => GovernanceStateQueryResponse::NotFound, - Err(msg) => GovernanceStateQueryResponse::Error(msg.to_string()), + Ok(None) => GovernanceStateQueryResponse::Error( + QueryError::not_found(format!("DRep {:?}", drep_credential)), + ), + Err(msg) => GovernanceStateQueryResponse::Error( + QueryError::query_failed(msg), + ), }, - None => { - GovernanceStateQueryResponse::Error("No current state".to_string()) - } + None => GovernanceStateQueryResponse::Error(QueryError::query_failed( + "No current state", + )), } } GovernanceStateQuery::GetDRepDelegators { drep_credential } => { @@ -328,12 +340,18 @@ impl DRepState { }, ) } - Ok(None) => GovernanceStateQueryResponse::NotFound, - Err(msg) => GovernanceStateQueryResponse::Error(msg.to_string()), + Ok(None) => { + GovernanceStateQueryResponse::Error(QueryError::not_found( + format!("DRep delegators for {:?}", drep_credential), + )) + } + Err(msg) => GovernanceStateQueryResponse::Error( + QueryError::query_failed(msg), + ), }, - None => { - GovernanceStateQueryResponse::Error("No current state".to_string()) - } + None => GovernanceStateQueryResponse::Error(QueryError::query_failed( + "No current state", + )), } } GovernanceStateQuery::GetDRepMetadata { drep_credential } => { @@ -342,12 +360,18 @@ impl DRepState { Ok(Some(anchor)) => GovernanceStateQueryResponse::DRepMetadata( Some(Some(anchor.clone())), ), - Ok(None) => GovernanceStateQueryResponse::NotFound, - Err(msg) => GovernanceStateQueryResponse::Error(msg.to_string()), + Ok(None) => { + GovernanceStateQueryResponse::Error(QueryError::not_found( + format!("DRep metadata for {:?}", drep_credential), + )) + } + Err(msg) => GovernanceStateQueryResponse::Error( + QueryError::query_failed(msg), + ), }, - None => { - GovernanceStateQueryResponse::Error("No current state".to_string()) - } + None => GovernanceStateQueryResponse::Error(QueryError::query_failed( + "No current state", + )), } } @@ -359,12 +383,18 @@ impl DRepState { updates: updates.to_vec(), }) } - Ok(None) => GovernanceStateQueryResponse::NotFound, - Err(msg) => GovernanceStateQueryResponse::Error(msg.to_string()), + Ok(None) => { + GovernanceStateQueryResponse::Error(QueryError::not_found( + format!("DRep updates for {:?}", drep_credential), + )) + } + Err(msg) => GovernanceStateQueryResponse::Error( + QueryError::query_failed(msg), + ), }, - None => { - GovernanceStateQueryResponse::Error("No current state".to_string()) - } + None => GovernanceStateQueryResponse::Error(QueryError::query_failed( + "No current state", + )), } } GovernanceStateQuery::GetDRepVotes { drep_credential } => { @@ -375,17 +405,23 @@ impl DRepState { votes: votes.to_vec(), }) } - Ok(None) => GovernanceStateQueryResponse::NotFound, - Err(msg) => GovernanceStateQueryResponse::Error(msg.to_string()), + Ok(None) => { + GovernanceStateQueryResponse::Error(QueryError::not_found( + format!("DRep votes for {:?}", drep_credential), + )) + } + Err(msg) => GovernanceStateQueryResponse::Error( + QueryError::query_failed(msg), + ), }, - None => { - GovernanceStateQueryResponse::Error("No current state".to_string()) - } + None => GovernanceStateQueryResponse::Error(QueryError::query_failed( + "No current state", + )), } } - _ => GovernanceStateQueryResponse::Error(format!( + _ => GovernanceStateQueryResponse::Error(QueryError::invalid_request(format!( "Unimplemented governance query: {query:?}" - )), + ))), }; Arc::new(Message::StateQueryResponse(StateQueryResponse::Governance( response, diff --git a/modules/epochs_state/src/epochs_state.rs b/modules/epochs_state/src/epochs_state.rs index c7f12d42..3ed96862 100644 --- a/modules/epochs_state/src/epochs_state.rs +++ b/modules/epochs_state/src/epochs_state.rs @@ -1,6 +1,7 @@ //! Acropolis epochs state module for Caryatid //! Unpacks block bodies to get transaction fees +use acropolis_common::queries::errors::QueryError; use acropolis_common::{ messages::{CardanoMessage, Message, StateQuery, StateQueryResponse}, queries::epochs::{ @@ -259,7 +260,9 @@ impl EpochsState { async move { let Message::StateQuery(StateQuery::Epochs(query)) = message.as_ref() else { return Arc::new(Message::StateQueryResponse(StateQueryResponse::Epochs( - EpochsStateQueryResponse::Error("Invalid message for epochs-state".into()), + EpochsStateQueryResponse::Error(QueryError::query_failed( + "Invalid message for epochs-state", + )), ))); }; @@ -276,9 +279,11 @@ impl EpochsState { Ok(Some(epoch_info)) => { EpochsStateQueryResponse::EpochInfo(EpochInfo { epoch: epoch_info }) } - Ok(None) => EpochsStateQueryResponse::NotFound, + Ok(None) => EpochsStateQueryResponse::Error(QueryError::not_found( + format!("Epoch {}", epoch_number), + )), Err(_) => EpochsStateQueryResponse::Error( - "Historical epoch storage is disabled".to_string(), + QueryError::storage_disabled("historical epoch"), ), } } @@ -286,7 +291,10 @@ impl EpochsState { EpochsStateQuery::GetNextEpochs { epoch_number } => { let current_epoch = state.get_epoch_info(); if *epoch_number > current_epoch.epoch { - EpochsStateQueryResponse::NotFound + EpochsStateQueryResponse::Error(QueryError::not_found(format!( + "Epoch {} is in the future", + epoch_number + ))) } else { match epochs_history.get_next_epochs(*epoch_number) { Ok(mut epochs) => { @@ -297,7 +305,7 @@ impl EpochsState { EpochsStateQueryResponse::NextEpochs(NextEpochs { epochs }) } Err(_) => EpochsStateQueryResponse::Error( - "Historical epoch storage is disabled".to_string(), + QueryError::storage_disabled("historical epoch"), ), } } @@ -306,7 +314,10 @@ impl EpochsState { EpochsStateQuery::GetPreviousEpochs { epoch_number } => { let current_epoch = state.get_epoch_info(); if *epoch_number > current_epoch.epoch { - EpochsStateQueryResponse::NotFound + EpochsStateQueryResponse::Error(QueryError::not_found(format!( + "Epoch {} is in the future", + epoch_number + ))) } else { match epochs_history.get_previous_epochs(*epoch_number) { Ok(epochs) => { @@ -315,7 +326,7 @@ impl EpochsState { }) } Err(_) => EpochsStateQueryResponse::Error( - "Historical epoch storage is disabled".to_string(), + QueryError::storage_disabled("historical epoch"), ), } } @@ -327,10 +338,10 @@ impl EpochsState { ) } - _ => EpochsStateQueryResponse::Error(format!( + _ => EpochsStateQueryResponse::Error(QueryError::invalid_request(format!( "Unimplemented query variant: {:?}", query - )), + ))), }; Arc::new(Message::StateQueryResponse(StateQueryResponse::Epochs( response, diff --git a/modules/governance_state/src/governance_state.rs b/modules/governance_state/src/governance_state.rs index f9034e11..58ee8839 100644 --- a/modules/governance_state/src/governance_state.rs +++ b/modules/governance_state/src/governance_state.rs @@ -1,6 +1,7 @@ //! Acropolis Governance State module for Caryatid //! Accepts certificate events and derives the Governance State in memory +use acropolis_common::queries::errors::QueryError; use acropolis_common::{ messages::{ CardanoMessage, DRepStakeDistributionMessage, GovernanceProceduresMessage, Message, @@ -180,9 +181,9 @@ impl GovernanceState { async move { let Message::StateQuery(StateQuery::Governance(query)) = message.as_ref() else { return Arc::new(Message::StateQueryResponse(StateQueryResponse::Governance( - GovernanceStateQueryResponse::Error( - "Invalid message for governance-state".into(), - ), + GovernanceStateQueryResponse::Error(QueryError::query_failed( + "Invalid message for governance-state", + )), ))); }; @@ -201,7 +202,9 @@ impl GovernanceState { procedure: proc.clone(), }) } - None => GovernanceStateQueryResponse::NotFound, + None => GovernanceStateQueryResponse::Error(QueryError::not_found( + format!("Proposal not found {}", proposal), + )), } } GovernanceStateQuery::GetProposalVotes { proposal } => { @@ -209,12 +212,14 @@ impl GovernanceState { Ok(votes) => { GovernanceStateQueryResponse::ProposalVotes(ProposalVotes { votes }) } - Err(_) => GovernanceStateQueryResponse::NotFound, + Err(_) => GovernanceStateQueryResponse::Error(QueryError::not_found( + format!("Proposal not found {}", proposal), + )), } } - _ => GovernanceStateQueryResponse::Error(format!( + _ => GovernanceStateQueryResponse::Error(QueryError::not_implemented(format!( "Unimplemented governance query: {query:?}" - )), + ))), }; Arc::new(Message::StateQueryResponse(StateQueryResponse::Governance( diff --git a/modules/parameters_state/src/parameters_state.rs b/modules/parameters_state/src/parameters_state.rs index 3aea07ed..7d96e2db 100644 --- a/modules/parameters_state/src/parameters_state.rs +++ b/modules/parameters_state/src/parameters_state.rs @@ -1,6 +1,7 @@ //! Acropolis Parameter State module for Caryatid //! Accepts certificate events and derives the Governance State in memory +use acropolis_common::queries::errors::QueryError; use acropolis_common::{ messages::{CardanoMessage, Message, ProtocolParamsMessage, StateQuery, StateQueryResponse}, queries::parameters::{ @@ -15,6 +16,7 @@ use config::Config; use std::sync::Arc; use tokio::sync::Mutex; use tracing::{error, info, info_span, Instrument}; + mod alonzo_genesis; mod genesis_params; mod parameters_updater; @@ -173,9 +175,9 @@ impl ParametersState { async move { let Message::StateQuery(StateQuery::Parameters(query)) = message.as_ref() else { return Arc::new(Message::StateQueryResponse(StateQueryResponse::Parameters( - ParametersStateQueryResponse::Error( - "Invalid message for parameters-state".into(), - ), + ParametersStateQueryResponse::Error(QueryError::query_failed( + "Invalid message for parameters-state", + )), ))); }; @@ -188,16 +190,17 @@ impl ParametersState { } ParametersStateQuery::GetEpochParameters { epoch_number } => { if !store_history { - ParametersStateQueryResponse::Error( - "Historical protocol parameter storage disabled by config" - .to_string(), - ) + ParametersStateQueryResponse::Error(QueryError::storage_disabled( + "Historical protocol parameter", + )) } else { match lock.get_at_or_before(*epoch_number) { Some(state) => ParametersStateQueryResponse::EpochParameters( state.current_params.get_params(), ), - None => ParametersStateQueryResponse::NotFound, + None => ParametersStateQueryResponse::Error(QueryError::not_found( + format!("Epoch {} not found in history", epoch_number), + )), } } } diff --git a/modules/rest_blockfrost/src/handlers/addresses.rs b/modules/rest_blockfrost/src/handlers/addresses.rs index 8442fb3f..7be16088 100644 --- a/modules/rest_blockfrost/src/handlers/addresses.rs +++ b/modules/rest_blockfrost/src/handlers/addresses.rs @@ -2,8 +2,8 @@ use anyhow::Result; use std::sync::Arc; use crate::{handlers_config::HandlersConfig, types::AddressInfoREST}; -use acropolis_common::rest_error::RESTError; use acropolis_common::queries::errors::QueryError; +use acropolis_common::rest_error::RESTError; use acropolis_common::{ messages::{Message, RESTResponse, StateQuery, StateQueryResponse}, queries::{ @@ -148,7 +148,7 @@ pub async fn handle_address_extended_blockfrost( _context: Arc>, _params: Vec, _handlers_config: Arc, -) -> Result { +) -> Result { Ok(RESTResponse::with_text(501, "Not implemented")) } @@ -157,7 +157,7 @@ pub async fn handle_address_totals_blockfrost( _context: Arc>, _params: Vec, _handlers_config: Arc, -) -> Result { +) -> Result { Ok(RESTResponse::with_text(501, "Not implemented")) } @@ -166,7 +166,7 @@ pub async fn handle_address_utxos_blockfrost( _context: Arc>, _params: Vec, _handlers_config: Arc, -) -> Result { +) -> Result { Ok(RESTResponse::with_text(501, "Not implemented")) } @@ -175,7 +175,7 @@ pub async fn handle_address_asset_utxos_blockfrost( _context: Arc>, _params: Vec, _handlers_config: Arc, -) -> Result { +) -> Result { Ok(RESTResponse::with_text(501, "Not implemented")) } @@ -184,6 +184,6 @@ pub async fn handle_address_transactions_blockfrost( _context: Arc>, _params: Vec, _handlers_config: Arc, -) -> Result { +) -> Result { Ok(RESTResponse::with_text(501, "Not implemented")) } diff --git a/modules/rest_blockfrost/src/handlers/assets.rs b/modules/rest_blockfrost/src/handlers/assets.rs index d0bc20d7..2914725f 100644 --- a/modules/rest_blockfrost/src/handlers/assets.rs +++ b/modules/rest_blockfrost/src/handlers/assets.rs @@ -5,16 +5,14 @@ use crate::{ PolicyAssetRest, }, }; +use acropolis_common::queries::assets::{AssetsStateQuery, AssetsStateQueryResponse}; +use acropolis_common::rest_error::RESTError; use acropolis_common::{ messages::{Message, RESTResponse, StateQuery, StateQueryResponse}, - queries::{ - assets::{AssetsStateQuery, AssetsStateQueryResponse}, - utils::query_state, - }, + queries::{errors::QueryError, utils::query_state}, serialization::Bech32WithHrp, AssetMetadataStandard, AssetName, PolicyId, }; -use anyhow::Result; use blake2::{digest::consts::U20, Blake2b, Digest}; use caryatid_sdk::Context; use hex::FromHex; @@ -27,390 +25,277 @@ pub async fn handle_assets_list_blockfrost( context: Arc>, _params: Vec, handlers_config: Arc, -) -> Result { +) -> Result { let assets_list_msg = Arc::new(Message::StateQuery(StateQuery::Assets( AssetsStateQuery::GetAssetsList, ))); - let response = query_state( + let assets = query_state( &context, &handlers_config.assets_query_topic, assets_list_msg, |message| match message { Message::StateQueryResponse(StateQueryResponse::Assets( AssetsStateQueryResponse::AssetsList(assets), - )) => { - let rest_assets: Vec = assets.iter().map(Into::into).collect(); - serde_json::to_string_pretty(&rest_assets) - .map(|json| RESTResponse::with_json(200, &json)) - .map_err(|e| anyhow::anyhow!("Failed to serialize assets list: {e}")) - } + )) => Ok(assets), Message::StateQueryResponse(StateQueryResponse::Assets( - AssetsStateQueryResponse::Error(_), - )) => Ok(RESTResponse::with_text( - 500, - "Asset storage is disabled in config", - )), - _ => Ok(RESTResponse::with_text( - 500, + AssetsStateQueryResponse::Error(e), + )) => Err(e), + _ => Err(QueryError::query_failed( "Unexpected response while retrieving asset list", )), }, ) - .await; + .await?; - match response { - Ok(rest) => Ok(rest), - Err(e) => Ok(RESTResponse::with_text(500, &format!("Query failed: {e}"))), - } + let rest_assets: Vec = assets.iter().map(Into::into).collect(); + let json = serde_json::to_string_pretty(&rest_assets)?; + Ok(RESTResponse::with_json(200, &json)) } pub async fn handle_asset_single_blockfrost( context: Arc>, params: Vec, handlers_config: Arc, -) -> Result { +) -> Result { let asset = params[0].clone(); - let asset_query_msg = match split_policy_and_asset(&asset) { - Ok((policy, name)) => Arc::new(Message::StateQuery(StateQuery::Assets( - AssetsStateQuery::GetAssetInfo { policy, name }, - ))), - Err(resp) => return Ok(resp), - }; + let (policy, name) = split_policy_and_asset(&asset)?; + + let asset_query_msg = Arc::new(Message::StateQuery(StateQuery::Assets( + AssetsStateQuery::GetAssetInfo { policy, name }, + ))); let (policy_str, name_str) = asset.split_at(56); - let Ok(bytes) = hex::decode(&asset) else { - return Ok(RESTResponse::with_text(400, "Invalid asset hex")); - }; + let bytes = hex::decode(&asset).map_err(|_| RESTError::invalid_hex())?; + let mut hasher = Blake2b::::new(); hasher.update(&bytes); let hash: Vec = hasher.finalize().to_vec(); - let Ok(fingerprint) = hash.to_bech32_with_hrp("asset") else { - return Ok(RESTResponse::with_text( - 500, - "Failed to encode asset fingerprint", - )); - }; + let fingerprint = hash + .to_bech32_with_hrp("asset") + .map_err(|e| RESTError::encoding_failed(&format!("asset fingerprint: {}", e)))?; + let off_chain_metadata = fetch_asset_metadata(&asset, &handlers_config.offchain_token_registry_url).await; let policy_id = policy_str.to_string(); let asset_name = name_str.to_string(); - let response = query_state( + let (quantity, info) = query_state( &context, &handlers_config.assets_query_topic, asset_query_msg, - move |message| match message { - Message::StateQueryResponse(StateQueryResponse::Assets( - AssetsStateQueryResponse::AssetInfo((quantity, info)), - )) => { - let (onchain_metadata_json, onchain_metadata_extra, cip68_version) = info - .onchain_metadata - .as_ref() - .map(|raw_meta| normalize_onchain_metadata(raw_meta.as_slice())) - .unwrap_or((None, None, None)); - - let onchain_metadata_standard = cip68_version.or(info.metadata_standard); - - // TODO: Query transaction_state once implemented to fetch inital_mint_tx_hash based on TxIdentifier - let response = AssetInfoRest { - asset, - policy_id, - asset_name, - fingerprint, - quantity: quantity.to_string(), - initial_mint_tx_hash: "transaction_state not yet implemented".to_string(), - mint_or_burn_count: info.mint_or_burn_count, - onchain_metadata: onchain_metadata_json, - onchain_metadata_standard, - onchain_metadata_extra, - metadata: off_chain_metadata, - }; - - serde_json::to_string_pretty(&response) - .map(|json| RESTResponse::with_json(200, &json)) - .map_err(|e| anyhow::anyhow!("Failed to serialize asset info: {e}")) - } + |message| match message { Message::StateQueryResponse(StateQueryResponse::Assets( - AssetsStateQueryResponse::NotFound, - )) => Ok(RESTResponse::with_text(404, "Asset not found")), + AssetsStateQueryResponse::AssetInfo(data), + )) => Ok(data), Message::StateQueryResponse(StateQueryResponse::Assets( - AssetsStateQueryResponse::Error(_), - )) => Ok(RESTResponse::with_text( - 501, - "Asset info storage disabled in config", - )), - _ => Ok(RESTResponse::with_text( - 500, + AssetsStateQueryResponse::Error(e), + )) => Err(e), + _ => Err(QueryError::query_failed( "Unexpected response while retrieving asset info", )), }, ) - .await; + .await?; + + let (onchain_metadata_json, onchain_metadata_extra, cip68_version) = info + .onchain_metadata + .as_ref() + .map(|raw_meta| normalize_onchain_metadata(raw_meta.as_slice())) + .unwrap_or((None, None, None)); + + let onchain_metadata_standard = cip68_version.or(info.metadata_standard); + + // TODO: Query transaction_state once implemented to fetch inital_mint_tx_hash based on TxIdentifier + let response = AssetInfoRest { + asset, + policy_id, + asset_name, + fingerprint, + quantity: quantity.to_string(), + initial_mint_tx_hash: "transaction_state not yet implemented".to_string(), + mint_or_burn_count: info.mint_or_burn_count, + onchain_metadata: onchain_metadata_json, + onchain_metadata_standard, + onchain_metadata_extra, + metadata: off_chain_metadata, + }; - match response { - Ok(rest) => Ok(rest), - Err(e) => Ok(RESTResponse::with_text(500, &format!("Query failed: {e}"))), - } + let json = serde_json::to_string_pretty(&response)?; + Ok(RESTResponse::with_json(200, &json)) } pub async fn handle_asset_history_blockfrost( context: Arc>, params: Vec, handlers_config: Arc, -) -> Result { - let (policy, name) = match split_policy_and_asset(¶ms[0]) { - Ok(pair) => pair, - Err(resp) => return Ok(resp), - }; +) -> Result { + let (policy, name) = split_policy_and_asset(¶ms[0])?; let asset_query_msg = Arc::new(Message::StateQuery(StateQuery::Assets( AssetsStateQuery::GetAssetHistory { policy, name }, ))); - let response = query_state( + let history = query_state( &context, &handlers_config.assets_query_topic, asset_query_msg, |message| match message { Message::StateQueryResponse(StateQueryResponse::Assets( - AssetsStateQueryResponse::AssetHistory(history), - )) => { - let rest_history: Vec = - history.iter().map(Into::into).collect(); - match serde_json::to_string_pretty(&rest_history) { - Ok(json) => Ok(RESTResponse::with_json(200, &json)), - Err(e) => Ok(RESTResponse::with_text( - 500, - &format!("Failed to serialize asset history: {e}"), - )), - } - } - Message::StateQueryResponse(StateQueryResponse::Assets( - AssetsStateQueryResponse::NotFound, - )) => Ok(RESTResponse::with_text(404, "Asset history not found")), + AssetsStateQueryResponse::AssetHistory(history), + )) => Ok(history), Message::StateQueryResponse(StateQueryResponse::Assets( - AssetsStateQueryResponse::Error(_), - )) => Ok(RESTResponse::with_text( - 501, - "Asset history storage is disabled in config", - )), - _ => Ok(RESTResponse::with_text( - 500, + AssetsStateQueryResponse::Error(e), + )) => Err(e), + _ => Err(QueryError::query_failed( "Unexpected response while retrieving asset history", )), }, ) - .await.unwrap_or_else(|e| RESTResponse::with_text(500, &format!("Query failed: {e}"))); + .await?; - Ok(response) + let rest_history: Vec = history.iter().map(Into::into).collect(); + let json = serde_json::to_string_pretty(&rest_history)?; + Ok(RESTResponse::with_json(200, &json)) } pub async fn handle_asset_transactions_blockfrost( context: Arc>, params: Vec, handlers_config: Arc, -) -> Result { - let (policy, name) = match split_policy_and_asset(¶ms[0]) { - Ok(pair) => pair, - Err(resp) => return Ok(resp), - }; +) -> Result { + let (policy, name) = split_policy_and_asset(¶ms[0])?; let asset_query_msg = Arc::new(Message::StateQuery(StateQuery::Assets( AssetsStateQuery::GetAssetTransactions { policy, name }, ))); - let response = query_state( + let txs = query_state( &context, &handlers_config.assets_query_topic, asset_query_msg, |message| match message { Message::StateQueryResponse(StateQueryResponse::Assets( AssetsStateQueryResponse::AssetTransactions(txs), - )) => { - // TODO: Query transaction_state once implemented to fetch tx_hash and block_time using TxIdentifier - let rest_txs: Vec = txs - .iter() - .map(|identifier| AssetTransactionRest { - tx_hash: "transaction_state not yet implemented".to_string(), - tx_index: identifier.tx_index(), - block_height: identifier.block_number(), - block_time: "transaction_state not yet implemented".to_string(), - }) - .collect(); - - serde_json::to_string_pretty(&rest_txs) - .map(|json| RESTResponse::with_json(200, &json)) - .map_err(|e| anyhow::anyhow!("Failed to serialize asset transactions: {e}")) - } - Message::StateQueryResponse(StateQueryResponse::Assets( - AssetsStateQueryResponse::NotFound, - )) => Ok(RESTResponse::with_text(404, "Asset not found")), + )) => Ok(txs), Message::StateQueryResponse(StateQueryResponse::Assets( - AssetsStateQueryResponse::Error(_), - )) => Ok(RESTResponse::with_text( - 501, - "Asset transactions storage is disabled in config", - )), - _ => Ok(RESTResponse::with_text( - 500, + AssetsStateQueryResponse::Error(e), + )) => Err(e), + _ => Err(QueryError::query_failed( "Unexpected response while retrieving asset transactions", )), }, ) - .await; - - match response { - Ok(rest) => Ok(rest), - Err(e) => Ok(RESTResponse::with_text(500, &format!("Query failed: {e}"))), - } + .await?; + + // TODO: Query transaction_state once implemented to fetch tx_hash and block_time using TxIdentifier + let rest_txs: Vec = txs + .iter() + .map(|identifier| AssetTransactionRest { + tx_hash: "transaction_state not yet implemented".to_string(), + tx_index: identifier.tx_index(), + block_height: identifier.block_number(), + block_time: "transaction_state not yet implemented".to_string(), + }) + .collect(); + + let json = serde_json::to_string_pretty(&rest_txs)?; + Ok(RESTResponse::with_json(200, &json)) } pub async fn handle_asset_addresses_blockfrost( context: Arc>, params: Vec, handlers_config: Arc, -) -> Result { - let (policy, name) = match split_policy_and_asset(¶ms[0]) { - Ok(pair) => pair, - Err(resp) => return Ok(resp), - }; +) -> Result { + let (policy, name) = split_policy_and_asset(¶ms[0])?; let asset_query_msg = Arc::new(Message::StateQuery(StateQuery::Assets( AssetsStateQuery::GetAssetAddresses { policy, name }, ))); - let response = query_state( + let addresses = query_state( &context, &handlers_config.assets_query_topic, asset_query_msg, |message| match message { Message::StateQueryResponse(StateQueryResponse::Assets( AssetsStateQueryResponse::AssetAddresses(addresses), - )) => { - let rest_addrs: Result, _> = - addresses.iter().map(AssetAddressRest::try_from).collect(); - - match rest_addrs { - Ok(rest_addrs) => match serde_json::to_string_pretty(&rest_addrs) { - Ok(json) => Ok(RESTResponse::with_json(200, &json)), - Err(e) => Ok(RESTResponse::with_text( - 500, - &format!("Failed to serialize asset addresses: {e}"), - )), - }, - Err(e) => Ok(RESTResponse::with_text( - 500, - &format!("Failed to convert address entry: {e}"), - )), - } - } + )) => Ok(addresses), Message::StateQueryResponse(StateQueryResponse::Assets( - AssetsStateQueryResponse::NotFound, - )) => Ok(RESTResponse::with_text(404, "Asset not found")), - Message::StateQueryResponse(StateQueryResponse::Assets( - AssetsStateQueryResponse::Error(_), - )) => Ok(RESTResponse::with_text( - 501, - "Asset addresses storage is disabled in config", - )), - _ => Ok(RESTResponse::with_text( - 500, + AssetsStateQueryResponse::Error(e), + )) => Err(e), + _ => Err(QueryError::query_failed( "Unexpected response while retrieving asset addresses", )), }, ) - .await; + .await?; - match response { - Ok(rest) => Ok(rest), - Err(e) => Ok(RESTResponse::with_text(500, &format!("Query failed: {e}"))), - } + let rest_addrs: Vec = + addresses.iter().map(AssetAddressRest::try_from).collect::, _>>().map_err( + |e| RESTError::InternalServerError(format!("Failed to convert address entry: {}", e)), + )?; + + let json = serde_json::to_string_pretty(&rest_addrs)?; + Ok(RESTResponse::with_json(200, &json)) } pub async fn handle_policy_assets_blockfrost( context: Arc>, params: Vec, handlers_config: Arc, -) -> Result { - let policy: PolicyId = match <[u8; 28]>::from_hex(¶ms[0]) { - Ok(bytes) => bytes, - Err(_) => { - return Ok(RESTResponse::with_text(400, "Invalid policy_id parameter")); - } - }; +) -> Result { + let policy: PolicyId = <[u8; 28]>::from_hex(¶ms[0]) + .map_err(|_| RESTError::invalid_param("policy_id", "invalid hex format"))?; let asset_query_msg = Arc::new(Message::StateQuery(StateQuery::Assets( AssetsStateQuery::GetPolicyIdAssets { policy }, ))); - let response = query_state( + let assets = query_state( &context, &handlers_config.assets_query_topic, asset_query_msg, |message| match message { Message::StateQueryResponse(StateQueryResponse::Assets( AssetsStateQueryResponse::PolicyIdAssets(assets), - )) => { - let rest_assets: Vec = assets.iter().map(Into::into).collect(); - serde_json::to_string_pretty(&rest_assets) - .map(|json| RESTResponse::with_json(200, &json)) - .map_err(|e| anyhow::anyhow!("Failed to serialize assets list: {e}")) - } - Message::StateQueryResponse(StateQueryResponse::Assets( - AssetsStateQueryResponse::NotFound, - )) => Ok(RESTResponse::with_text(404, "Policy assets not found")), + )) => Ok(assets), Message::StateQueryResponse(StateQueryResponse::Assets( - AssetsStateQueryResponse::Error(_), - )) => Ok(RESTResponse::with_text( - 501, - "Indexing by policy is disabled in config", - )), - _ => Ok(RESTResponse::with_text( - 500, + AssetsStateQueryResponse::Error(e), + )) => Err(e), + _ => Err(QueryError::query_failed( "Unexpected response while retrieving policy assets", )), }, ) - .await; + .await?; - match response { - Ok(rest) => Ok(rest), - Err(e) => Ok(RESTResponse::with_text(500, &format!("Query failed: {e}"))), - } + let rest_assets: Vec = assets.iter().map(Into::into).collect(); + let json = serde_json::to_string_pretty(&rest_assets)?; + Ok(RESTResponse::with_json(200, &json)) } -fn split_policy_and_asset(hex_str: &str) -> Result<(PolicyId, AssetName), RESTResponse> { - let decoded = match hex::decode(hex_str) { - Ok(bytes) => bytes, - Err(_) => return Err(RESTResponse::with_text(400, "Invalid hex string")), - }; +fn split_policy_and_asset(hex_str: &str) -> Result<(PolicyId, AssetName), RESTError> { + let decoded = hex::decode(hex_str).map_err(|_| RESTError::invalid_hex())?; if decoded.len() < 28 { - return Err(RESTResponse::with_text( - 400, - "Asset identifier must be at least 28 bytes", + return Err(RESTError::BadRequest( + "Asset identifier must be at least 28 bytes".to_string(), )); } let (policy_part, asset_part) = decoded.split_at(28); - let policy_id: PolicyId = match policy_part.try_into() { - Ok(arr) => arr, - Err(_) => return Err(RESTResponse::with_text(400, "Policy id must be 28 bytes")), - }; + let policy_id: PolicyId = policy_part + .try_into() + .map_err(|_| RESTError::BadRequest("Policy id must be 28 bytes".to_string()))?; - let asset_name = match AssetName::new(asset_part) { - Some(asset_name) => asset_name, - None => { - return Err(RESTResponse::with_text( - 400, - "Asset name must be less than 32 bytes", - )) - } - }; + let asset_name = AssetName::new(asset_part).ok_or_else(|| { + RESTError::BadRequest("Asset name must be less than 32 bytes".to_string()) + })?; Ok((policy_id, asset_name)) } @@ -566,8 +451,8 @@ mod tests { let result = split_policy_and_asset("zzzz"); assert!(result.is_err()); let err = result.unwrap_err(); - assert_eq!(err.code, 400); - assert_eq!(err.body, "Invalid hex string"); + assert_eq!(err.status_code(), 400); + assert_eq!(err.message(), "Invalid hex string"); } #[test] @@ -576,8 +461,8 @@ mod tests { let result = split_policy_and_asset(&hex_str); assert!(result.is_err()); let err = result.unwrap_err(); - assert_eq!(err.code, 400); - assert_eq!(err.body, "Asset identifier must be at least 28 bytes"); + assert_eq!(err.status_code(), 400); + assert_eq!(err.message(), "Asset identifier must be at least 28 bytes"); } #[test] @@ -588,8 +473,8 @@ mod tests { let result = split_policy_and_asset(&hex_str); assert!(result.is_err()); let err = result.unwrap_err(); - assert_eq!(err.code, 400); - assert_eq!(err.body, "Asset name must be less than 32 bytes"); + assert_eq!(err.status_code(), 400); + assert_eq!(err.message(), "Asset name must be less than 32 bytes"); } #[test] diff --git a/modules/rest_blockfrost/src/handlers/blocks.rs b/modules/rest_blockfrost/src/handlers/blocks.rs index 205fdab5..9ca20222 100644 --- a/modules/rest_blockfrost/src/handlers/blocks.rs +++ b/modules/rest_blockfrost/src/handlers/blocks.rs @@ -1,4 +1,7 @@ //! REST handlers for Acropolis Blockfrost /blocks endpoints +use crate::handlers_config::HandlersConfig; +use crate::types::BlockInfoREST; +use acropolis_common::rest_error::RESTError; use acropolis_common::{ extract_strict_query_params, messages::{Message, RESTResponse, StateQuery, StateQueryResponse}, @@ -9,27 +12,28 @@ use acropolis_common::{ }, BlockHash, }; -use anyhow::{anyhow, Result}; use caryatid_sdk::Context; use std::collections::HashMap; use std::sync::Arc; -use crate::handlers_config::HandlersConfig; -use crate::types::BlockInfoREST; - -fn parse_block_key(key: &str) -> Result { +fn parse_block_key(key: &str) -> Result { match key.len() { - 64 => match hex::decode(key) { - Ok(key) => match BlockHash::try_from(key) { - Ok(block_hash) => Ok(BlockKey::Hash(block_hash)), - Err(_) => Err(anyhow::Error::msg("Invalid block hash")), - }, - Err(error) => Err(error.into()), - }, - _ => match key.parse::() { - Ok(key) => Ok(BlockKey::Number(key)), - Err(error) => Err(error.into()), - }, + 64 => { + let bytes = hex::decode(key) + .map_err(|_| RESTError::invalid_param("block", "invalid hex format"))?; + let block_hash = BlockHash::try_from(bytes) + .map_err(|_| RESTError::invalid_param("block", "invalid block hash"))?; + Ok(BlockKey::Hash(block_hash)) + } + _ => { + let number = key.parse::().map_err(|_| { + RESTError::invalid_param( + "block", + "must be a valid block number or 64-character hex hash", + ) + })?; + Ok(BlockKey::Number(number)) + } } } @@ -38,10 +42,10 @@ pub async fn handle_blocks_latest_hash_number_blockfrost( context: Arc>, params: Vec, handlers_config: Arc, -) -> Result { +) -> Result { let param = match params.as_slice() { [param] => param, - _ => return Ok(RESTResponse::with_text(400, "Invalid parameters")), + _ => return Err(RESTError::BadRequest("Invalid parameters".to_string())), }; match param.as_str() { @@ -54,7 +58,7 @@ pub async fn handle_blocks_latest_hash_number_blockfrost( async fn handle_blocks_latest_blockfrost( context: Arc>, handlers_config: Arc, -) -> Result { +) -> Result { let blocks_latest_msg = Arc::new(Message::StateQuery(StateQuery::Blocks( BlocksStateQuery::GetLatestBlock, ))); @@ -65,10 +69,10 @@ async fn handle_blocks_latest_blockfrost( |message| match message { Message::StateQueryResponse(StateQueryResponse::Blocks( BlocksStateQueryResponse::LatestBlock(blocks_latest), - )) => Some(Ok(Some(BlockInfoREST(blocks_latest)))), + )) => Some(Ok(BlockInfoREST(blocks_latest))), Message::StateQueryResponse(StateQueryResponse::Blocks( BlocksStateQueryResponse::Error(e), - )) => Some(Err(anyhow!(e))), + )) => Some(Err(e)), _ => None, }, ) @@ -80,11 +84,8 @@ async fn handle_blocks_hash_number_blockfrost( context: Arc>, hash_or_number: &str, handlers_config: Arc, -) -> Result { - let block_key = match parse_block_key(hash_or_number) { - Ok(block_key) => block_key, - Err(error) => return Err(error), - }; +) -> Result { + let block_key = parse_block_key(hash_or_number)?; let block_info_msg = Arc::new(Message::StateQuery(StateQuery::Blocks( BlocksStateQuery::GetBlockInfo { block_key }, @@ -96,13 +97,10 @@ async fn handle_blocks_hash_number_blockfrost( |message| match message { Message::StateQueryResponse(StateQueryResponse::Blocks( BlocksStateQueryResponse::BlockInfo(block_info), - )) => Some(Ok(Some(BlockInfoREST(block_info)))), - Message::StateQueryResponse(StateQueryResponse::Blocks( - BlocksStateQueryResponse::NotFound, - )) => Some(Ok(None)), + )) => Some(Ok(BlockInfoREST(block_info))), Message::StateQueryResponse(StateQueryResponse::Blocks( BlocksStateQueryResponse::Error(e), - )) => Some(Err(anyhow!(e))), + )) => Some(Err(e)), _ => None, }, ) @@ -115,10 +113,10 @@ pub async fn handle_blocks_latest_hash_number_transactions_blockfrost( params: Vec, query_params: HashMap, handlers_config: Arc, -) -> Result { +) -> Result { let param = match params.as_slice() { [param] => param, - _ => return Ok(RESTResponse::with_text(400, "Invalid parameters")), + _ => return Err(RESTError::BadRequest("Invalid parameters".to_string())), }; extract_strict_query_params!(query_params, { @@ -162,7 +160,7 @@ async fn handle_blocks_latest_transactions_blockfrost( skip: u64, order: Order, handlers_config: Arc, -) -> Result { +) -> Result { let blocks_latest_txs_msg = Arc::new(Message::StateQuery(StateQuery::Blocks( BlocksStateQuery::GetLatestBlockTransactions { limit, skip, order }, ))); @@ -173,10 +171,10 @@ async fn handle_blocks_latest_transactions_blockfrost( |message| match message { Message::StateQueryResponse(StateQueryResponse::Blocks( BlocksStateQueryResponse::LatestBlockTransactions(blocks_txs), - )) => Some(Ok(Some(blocks_txs))), + )) => Some(Ok(blocks_txs)), Message::StateQueryResponse(StateQueryResponse::Blocks( BlocksStateQueryResponse::Error(e), - )) => Some(Err(anyhow!(e))), + )) => Some(Err(e)), _ => None, }, ) @@ -191,11 +189,8 @@ async fn handle_blocks_hash_number_transactions_blockfrost( skip: u64, order: Order, handlers_config: Arc, -) -> Result { - let block_key = match parse_block_key(hash_or_number) { - Ok(block_key) => block_key, - Err(error) => return Err(error), - }; +) -> Result { + let block_key = parse_block_key(hash_or_number)?; let block_txs_msg = Arc::new(Message::StateQuery(StateQuery::Blocks( BlocksStateQuery::GetBlockTransactions { @@ -212,13 +207,10 @@ async fn handle_blocks_hash_number_transactions_blockfrost( |message| match message { Message::StateQueryResponse(StateQueryResponse::Blocks( BlocksStateQueryResponse::BlockTransactions(block_txs), - )) => Some(Ok(Some(block_txs))), - Message::StateQueryResponse(StateQueryResponse::Blocks( - BlocksStateQueryResponse::NotFound, - )) => Some(Ok(None)), + )) => Some(Ok(block_txs)), Message::StateQueryResponse(StateQueryResponse::Blocks( BlocksStateQueryResponse::Error(e), - )) => Some(Err(anyhow!(e))), + )) => Some(Err(e)), _ => None, }, ) @@ -231,10 +223,10 @@ pub async fn handle_blocks_latest_hash_number_transactions_cbor_blockfrost( params: Vec, query_params: HashMap, handlers_config: Arc, -) -> Result { +) -> Result { let param = match params.as_slice() { [param] => param, - _ => return Ok(RESTResponse::with_text(400, "Invalid parameters")), + _ => return Err(RESTError::BadRequest("Invalid parameters".to_string())), }; extract_strict_query_params!(query_params, { @@ -278,7 +270,7 @@ async fn handle_blocks_latest_transactions_cbor_blockfrost( skip: u64, order: Order, handlers_config: Arc, -) -> Result { +) -> Result { let blocks_latest_txs_msg = Arc::new(Message::StateQuery(StateQuery::Blocks( BlocksStateQuery::GetLatestBlockTransactionsCBOR { limit, skip, order }, ))); @@ -289,10 +281,10 @@ async fn handle_blocks_latest_transactions_cbor_blockfrost( |message| match message { Message::StateQueryResponse(StateQueryResponse::Blocks( BlocksStateQueryResponse::LatestBlockTransactionsCBOR(blocks_txs), - )) => Some(Ok(Some(blocks_txs))), + )) => Some(Ok(blocks_txs)), Message::StateQueryResponse(StateQueryResponse::Blocks( BlocksStateQueryResponse::Error(e), - )) => Some(Err(anyhow!(e))), + )) => Some(Err(e)), _ => None, }, ) @@ -307,11 +299,8 @@ async fn handle_blocks_hash_number_transactions_cbor_blockfrost( skip: u64, order: Order, handlers_config: Arc, -) -> Result { - let block_key = match parse_block_key(hash_or_number) { - Ok(block_key) => block_key, - Err(error) => return Err(error), - }; +) -> Result { + let block_key = parse_block_key(hash_or_number)?; let block_txs_cbor_msg = Arc::new(Message::StateQuery(StateQuery::Blocks( BlocksStateQuery::GetBlockTransactionsCBOR { @@ -328,13 +317,10 @@ async fn handle_blocks_hash_number_transactions_cbor_blockfrost( |message| match message { Message::StateQueryResponse(StateQueryResponse::Blocks( BlocksStateQueryResponse::BlockTransactionsCBOR(block_txs_cbor), - )) => Some(Ok(Some(block_txs_cbor))), - Message::StateQueryResponse(StateQueryResponse::Blocks( - BlocksStateQueryResponse::NotFound, - )) => Some(Ok(None)), + )) => Some(Ok(block_txs_cbor)), Message::StateQueryResponse(StateQueryResponse::Blocks( BlocksStateQueryResponse::Error(e), - )) => Some(Err(anyhow!(e))), + )) => Some(Err(e)), _ => None, }, ) @@ -347,16 +333,13 @@ pub async fn handle_blocks_hash_number_next_blockfrost( params: Vec, query_params: HashMap, handlers_config: Arc, -) -> Result { +) -> Result { let param = match params.as_slice() { [param] => param, - _ => return Ok(RESTResponse::with_text(400, "Invalid parameters")), + _ => return Err(RESTError::BadRequest("Invalid parameters".to_string())), }; - let block_key = match parse_block_key(param) { - Ok(block_key) => block_key, - Err(error) => return Err(error), - }; + let block_key = parse_block_key(param)?; extract_strict_query_params!(query_params, { "count" => limit: Option, @@ -379,13 +362,10 @@ pub async fn handle_blocks_hash_number_next_blockfrost( |message| match message { Message::StateQueryResponse(StateQueryResponse::Blocks( BlocksStateQueryResponse::NextBlocks(blocks_next), - )) => Some(Ok(Some(blocks_next))), - Message::StateQueryResponse(StateQueryResponse::Blocks( - BlocksStateQueryResponse::NotFound, - )) => Some(Ok(None)), + )) => Some(Ok(blocks_next)), Message::StateQueryResponse(StateQueryResponse::Blocks( BlocksStateQueryResponse::Error(e), - )) => Some(Err(anyhow!(e))), + )) => Some(Err(e)), _ => None, }, ) @@ -398,16 +378,13 @@ pub async fn handle_blocks_hash_number_previous_blockfrost( params: Vec, query_params: HashMap, handlers_config: Arc, -) -> Result { +) -> Result { let param = match params.as_slice() { [param] => param, - _ => return Ok(RESTResponse::with_text(400, "Invalid parameters")), + _ => return Err(RESTError::BadRequest("Invalid parameters".to_string())), }; - let block_key = match parse_block_key(param) { - Ok(block_key) => block_key, - Err(error) => return Err(error), - }; + let block_key = parse_block_key(param)?; extract_strict_query_params!(query_params, { "count" => limit: Option, @@ -430,13 +407,10 @@ pub async fn handle_blocks_hash_number_previous_blockfrost( |message| match message { Message::StateQueryResponse(StateQueryResponse::Blocks( BlocksStateQueryResponse::PreviousBlocks(blocks_previous), - )) => Some(Ok(Some(blocks_previous))), - Message::StateQueryResponse(StateQueryResponse::Blocks( - BlocksStateQueryResponse::NotFound, - )) => Some(Ok(None)), + )) => Some(Ok(blocks_previous)), Message::StateQueryResponse(StateQueryResponse::Blocks( BlocksStateQueryResponse::Error(e), - )) => Some(Err(anyhow!(e))), + )) => Some(Err(e)), _ => None, }, ) @@ -448,16 +422,15 @@ pub async fn handle_blocks_slot_blockfrost( context: Arc>, params: Vec, handlers_config: Arc, -) -> Result { +) -> Result { let slot = match params.as_slice() { [param] => param, - _ => return Ok(RESTResponse::with_text(400, "Invalid parameters")), + _ => return Err(RESTError::BadRequest("Invalid parameters".to_string())), }; - let slot = match slot.parse::() { - Ok(slot) => slot, - Err(error) => return Err(error.into()), - }; + let slot = slot + .parse::() + .map_err(|_| RESTError::invalid_param("slot", "must be a valid number"))?; let block_slot_msg = Arc::new(Message::StateQuery(StateQuery::Blocks( BlocksStateQuery::GetBlockBySlot { slot }, @@ -469,13 +442,10 @@ pub async fn handle_blocks_slot_blockfrost( |message| match message { Message::StateQueryResponse(StateQueryResponse::Blocks( BlocksStateQueryResponse::BlockBySlot(block_info), - )) => Some(Ok(Some(block_info))), - Message::StateQueryResponse(StateQueryResponse::Blocks( - BlocksStateQueryResponse::NotFound, - )) => Some(Ok(None)), + )) => Some(Ok(block_info)), Message::StateQueryResponse(StateQueryResponse::Blocks( BlocksStateQueryResponse::Error(e), - )) => Some(Err(anyhow!(e))), + )) => Some(Err(e)), _ => None, }, ) @@ -487,21 +457,19 @@ pub async fn handle_blocks_epoch_slot_blockfrost( context: Arc>, params: Vec, handlers_config: Arc, -) -> Result { +) -> Result { let (epoch, slot) = match params.as_slice() { [param1, param2] => (param1, param2), - _ => return Ok(RESTResponse::with_text(400, "Invalid parameters")), + _ => return Err(RESTError::BadRequest("Invalid parameters".to_string())), }; - let epoch = match epoch.parse::() { - Ok(epoch) => epoch, - Err(error) => return Err(error.into()), - }; + let epoch = epoch + .parse::() + .map_err(|_| RESTError::invalid_param("epoch", "must be a valid number"))?; - let slot = match slot.parse::() { - Ok(slot) => slot, - Err(error) => return Err(error.into()), - }; + let slot = slot + .parse::() + .map_err(|_| RESTError::invalid_param("slot", "must be a valid number"))?; let block_epoch_slot_msg = Arc::new(Message::StateQuery(StateQuery::Blocks( BlocksStateQuery::GetBlockByEpochSlot { epoch, slot }, @@ -513,13 +481,10 @@ pub async fn handle_blocks_epoch_slot_blockfrost( |message| match message { Message::StateQueryResponse(StateQueryResponse::Blocks( BlocksStateQueryResponse::BlockByEpochSlot(block_info), - )) => Some(Ok(Some(block_info))), - Message::StateQueryResponse(StateQueryResponse::Blocks( - BlocksStateQueryResponse::NotFound, - )) => Some(Ok(None)), + )) => Some(Ok(block_info)), Message::StateQueryResponse(StateQueryResponse::Blocks( BlocksStateQueryResponse::Error(e), - )) => Some(Err(anyhow!(e))), + )) => Some(Err(e)), _ => None, }, ) @@ -532,16 +497,13 @@ pub async fn handle_blocks_hash_number_addresses_blockfrost( params: Vec, query_params: HashMap, handlers_config: Arc, -) -> Result { +) -> Result { let param = match params.as_slice() { [param] => param, - _ => return Ok(RESTResponse::with_text(400, "Invalid parameters")), + _ => return Err(RESTError::BadRequest("Invalid parameters".to_string())), }; - let block_key = match parse_block_key(param) { - Ok(block_key) => block_key, - Err(error) => return Err(error), - }; + let block_key = parse_block_key(param)?; extract_strict_query_params!(query_params, { "count" => limit: Option, @@ -564,13 +526,10 @@ pub async fn handle_blocks_hash_number_addresses_blockfrost( |message| match message { Message::StateQueryResponse(StateQueryResponse::Blocks( BlocksStateQueryResponse::BlockInvolvedAddresses(block_addresses), - )) => Some(Ok(Some(block_addresses))), - Message::StateQueryResponse(StateQueryResponse::Blocks( - BlocksStateQueryResponse::NotFound, - )) => Some(Ok(None)), + )) => Some(Ok(block_addresses)), Message::StateQueryResponse(StateQueryResponse::Blocks( BlocksStateQueryResponse::Error(e), - )) => Some(Err(anyhow!(e))), + )) => Some(Err(e)), _ => None, }, ) diff --git a/modules/rest_blockfrost/src/handlers/epochs.rs b/modules/rest_blockfrost/src/handlers/epochs.rs index 74f57bdc..285ebce4 100644 --- a/modules/rest_blockfrost/src/handlers/epochs.rs +++ b/modules/rest_blockfrost/src/handlers/epochs.rs @@ -4,12 +4,14 @@ use crate::{ EpochActivityRest, ProtocolParamsRest, SPDDByEpochAndPoolItemRest, SPDDByEpochItemRest, }, }; +use acropolis_common::rest_error::RESTError; use acropolis_common::serialization::Bech32Conversion; use acropolis_common::{ messages::{Message, RESTResponse, StateQuery, StateQueryResponse}, queries::{ accounts::{AccountsStateQuery, AccountsStateQueryResponse}, epochs::{EpochsStateQuery, EpochsStateQueryResponse}, + errors::QueryError, parameters::{ParametersStateQuery, ParametersStateQueryResponse}, pools::{PoolsStateQuery, PoolsStateQueryResponse}, spdd::{SPDDStateQuery, SPDDStateQueryResponse}, @@ -17,7 +19,6 @@ use acropolis_common::{ }, PoolId, }; -use anyhow::{anyhow, Result}; use caryatid_sdk::Context; use std::sync::Arc; @@ -25,11 +26,10 @@ pub async fn handle_epoch_info_blockfrost( context: Arc>, params: Vec, handlers_config: Arc, -) -> Result { +) -> Result { if params.len() != 1 { - return Ok(RESTResponse::with_text( - 400, - "Expected one parameter: 'latest' or an epoch number", + return Err(RESTError::BadRequest( + "Expected one parameter: 'latest' or an epoch number".to_string(), )); } let param = ¶ms[0]; @@ -38,15 +38,9 @@ pub async fn handle_epoch_info_blockfrost( let query = if param == "latest" { EpochsStateQuery::GetLatestEpoch } else { - let parsed = match param.parse::() { - Ok(num) => num, - Err(_) => { - return Ok(RESTResponse::with_text( - 400, - "Invalid epoch number parameter", - )); - } - }; + let parsed = param + .parse::() + .map_err(|_| RESTError::invalid_param("epoch", "must be a valid number"))?; EpochsStateQuery::GetEpochInfo { epoch_number: parsed, } @@ -60,24 +54,23 @@ pub async fn handle_epoch_info_blockfrost( epoch_info_msg, |message| match message { Message::StateQueryResponse(StateQueryResponse::Epochs(response)) => Ok(response), - _ => Err(anyhow!( - "Unexpected message type while retrieving latest epoch" + _ => Err(QueryError::query_failed( + "Unexpected message type while retrieving latest epoch", )), }, ) .await?; let ea_message = match epoch_info_response { - EpochsStateQueryResponse::LatestEpoch(response) => Ok(response.epoch), - EpochsStateQueryResponse::EpochInfo(response) => Ok(response.epoch), - EpochsStateQueryResponse::NotFound => Err(anyhow!("Epoch not found")), - EpochsStateQueryResponse::Error(e) => Err(anyhow!( - "Internal server error while retrieving epoch info: {e}" - )), - _ => Err(anyhow!( - "Unexpected message type while retrieving epoch info" - )), - }?; + EpochsStateQueryResponse::LatestEpoch(response) => response.epoch, + EpochsStateQueryResponse::EpochInfo(response) => response.epoch, + EpochsStateQueryResponse::Error(e) => return Err(e.into()), + _ => { + return Err(RESTError::unexpected_response( + "Unexpected response type while retrieving epoch info", + )) + } + }; let epoch_number = ea_message.epoch; // For the latest epoch, query accounts-state for the stake pool delegation distribution (SPDD) @@ -94,7 +87,10 @@ pub async fn handle_epoch_info_blockfrost( Message::StateQueryResponse(StateQueryResponse::Accounts( AccountsStateQueryResponse::ActiveStakes(total_active_stake), )) => Ok(total_active_stake), - _ => Err(anyhow::anyhow!( + Message::StateQueryResponse(StateQueryResponse::Accounts( + AccountsStateQueryResponse::Error(e), + )) => Err(e), + _ => Err(QueryError::query_failed( "Unexpected message type while retrieving the latest total active stakes", )), }, @@ -115,9 +111,13 @@ pub async fn handle_epoch_info_blockfrost( Message::StateQueryResponse(StateQueryResponse::SPDD( SPDDStateQueryResponse::EpochTotalActiveStakes(total_active_stakes), )) => Ok(total_active_stakes), - _ => Err(anyhow::anyhow!( - "Unexpected message type while retrieving total active stakes for epoch: {epoch_number}", - )), + Message::StateQueryResponse(StateQueryResponse::SPDD( + SPDDStateQueryResponse::Error(e), + )) => Err(e), + _ => Err(QueryError::query_failed(&format!( + "Unexpected message type while retrieving total active stakes for epoch: {}", + epoch_number + ))), }, ) .await? @@ -131,15 +131,7 @@ pub async fn handle_epoch_info_blockfrost( response.active_stake = Some(total_active_stakes); } - let json = match serde_json::to_string_pretty(&response) { - Ok(j) => j, - Err(e) => { - return Ok(RESTResponse::with_text( - 500, - &format!("Internal server error while retrieving latest epoch: {e}"), - )); - } - }; + let json = serde_json::to_string_pretty(&response)?; Ok(RESTResponse::with_json(200, &json)) } @@ -147,11 +139,10 @@ pub async fn handle_epoch_params_blockfrost( context: Arc>, params: Vec, handlers_config: Arc, -) -> Result { +) -> Result { if params.len() != 1 { - return Ok(RESTResponse::with_text( - 400, - "Expected one parameter: 'latest' or an epoch number", + return Err(RESTError::BadRequest( + "Expected one parameter: 'latest' or an epoch number".to_string(), )); } let param = ¶ms[0]; @@ -173,11 +164,9 @@ pub async fn handle_epoch_params_blockfrost( )) => Ok(res.epoch.epoch), Message::StateQueryResponse(StateQueryResponse::Epochs( EpochsStateQueryResponse::Error(e), - )) => Err(anyhow::anyhow!( - "Internal server error while retrieving latest epoch: {e}" - )), - _ => Err(anyhow::anyhow!( - "Unexpected message type while retrieving latest epoch" + )) => Err(e), + _ => Err(QueryError::query_failed( + "Unexpected message type while retrieving latest epoch", )), }, ) @@ -186,15 +175,9 @@ pub async fn handle_epoch_params_blockfrost( if param == "latest" { query = ParametersStateQuery::GetLatestEpochParameters; } else { - let parsed = match param.parse::() { - Ok(num) => num, - Err(_) => { - return Ok(RESTResponse::with_text( - 400, - "Invalid epoch number parameter", - )); - } - }; + let parsed = param + .parse::() + .map_err(|_| RESTError::invalid_param("epoch", "must be a valid number"))?; query = ParametersStateQuery::GetEpochParameters { epoch_number: parsed, }; @@ -208,8 +191,8 @@ pub async fn handle_epoch_params_blockfrost( parameters_msg, |message| match message { Message::StateQueryResponse(StateQueryResponse::Parameters(resp)) => Ok(resp), - _ => Err(anyhow::anyhow!( - "Unexpected message type while retrieving parameters" + _ => Err(QueryError::query_failed( + "Unexpected message type while retrieving parameters", )), }, ) @@ -218,40 +201,24 @@ pub async fn handle_epoch_params_blockfrost( match parameters_response { ParametersStateQueryResponse::LatestEpochParameters(params) => { let rest = ProtocolParamsRest::from((latest_epoch, params)); - match serde_json::to_string_pretty(&rest) { - Ok(json) => Ok(RESTResponse::with_json(200, &json)), - Err(e) => Ok(RESTResponse::with_text( - 500, - &format!("Failed to serialize parameters: {e}"), - )), - } + let json = serde_json::to_string_pretty(&rest)?; + Ok(RESTResponse::with_json(200, &json)) } ParametersStateQueryResponse::EpochParameters(params) => { let epoch = epoch_number.expect("epoch_number must exist for EpochParameters"); if epoch > latest_epoch { - return Ok(RESTResponse::with_text( - 404, + return Err(RESTError::not_found( "Protocol parameters not found for requested epoch", )); } let rest = ProtocolParamsRest::from((epoch, params)); - match serde_json::to_string_pretty(&rest) { - Ok(json) => Ok(RESTResponse::with_json(200, &json)), - Err(e) => Ok(RESTResponse::with_text( - 500, - &format!("Failed to serialize parameters: {e}"), - )), - } + let json = serde_json::to_string_pretty(&rest)?; + Ok(RESTResponse::with_json(200, &json)) } - ParametersStateQueryResponse::NotFound => Ok(RESTResponse::with_text( - 404, - "Protocol parameters not found for requested epoch", - )), - ParametersStateQueryResponse::Error(msg) => Ok(RESTResponse::with_text(400, &msg)), - _ => Ok(RESTResponse::with_text( - 500, - "Unexpected message type while retrieving parameters", + ParametersStateQueryResponse::Error(e) => Err(e.into()), + _ => Err(RESTError::unexpected_response( + "Unexpected response type while retrieving parameters", )), } } @@ -260,24 +227,17 @@ pub async fn handle_epoch_next_blockfrost( context: Arc>, params: Vec, handlers_config: Arc, -) -> Result { +) -> Result { if params.len() != 1 { - return Ok(RESTResponse::with_text( - 400, - "Expected one parameter: an epoch number", + return Err(RESTError::BadRequest( + "Expected one parameter: an epoch number".to_string(), )); } let param = ¶ms[0]; - let parsed = match param.parse::() { - Ok(num) => num, - Err(_) => { - return Ok(RESTResponse::with_text( - 400, - "Invalid epoch number parameter", - )); - } - }; + let parsed = param + .parse::() + .map_err(|_| RESTError::invalid_param("epoch", "must be a valid number"))?; let next_epochs_msg = Arc::new(Message::StateQuery(StateQuery::Epochs( EpochsStateQuery::GetNextEpochs { @@ -294,28 +254,15 @@ pub async fn handle_epoch_next_blockfrost( )) => Ok(response.epochs.into_iter().map(EpochActivityRest::from).collect::>()), Message::StateQueryResponse(StateQueryResponse::Epochs( EpochsStateQueryResponse::Error(e), - )) => Err(anyhow::anyhow!( - "Internal server error while retrieving next epochs: {e}" - )), - Message::StateQueryResponse(StateQueryResponse::Epochs( - EpochsStateQueryResponse::NotFound, - )) => Err(anyhow::anyhow!("Epoch not found")), - _ => Err(anyhow::anyhow!( - "Unexpected message type while retrieving next epochs" + )) => Err(e), + _ => Err(QueryError::query_failed( + "Unexpected message type while retrieving next epochs", )), }, ) .await?; - let json = match serde_json::to_string_pretty(&next_epochs) { - Ok(j) => j, - Err(e) => { - return Ok(RESTResponse::with_text( - 500, - &format!("Failed to serialize epoch info: {e}"), - )); - } - }; + let json = serde_json::to_string_pretty(&next_epochs)?; Ok(RESTResponse::with_json(200, &json)) } @@ -323,24 +270,17 @@ pub async fn handle_epoch_previous_blockfrost( context: Arc>, params: Vec, handlers_config: Arc, -) -> Result { +) -> Result { if params.len() != 1 { - return Ok(RESTResponse::with_text( - 400, - "Expected one parameter: an epoch number", + return Err(RESTError::BadRequest( + "Expected one parameter: an epoch number".to_string(), )); } let param = ¶ms[0]; - let parsed = match param.parse::() { - Ok(num) => num, - Err(_) => { - return Ok(RESTResponse::with_text( - 400, - "Invalid epoch number parameter", - )); - } - }; + let parsed = param + .parse::() + .map_err(|_| RESTError::invalid_param("epoch", "must be a valid number"))?; let previous_epochs_msg = Arc::new(Message::StateQuery(StateQuery::Epochs( EpochsStateQuery::GetPreviousEpochs { @@ -357,28 +297,15 @@ pub async fn handle_epoch_previous_blockfrost( )) => Ok(response.epochs.into_iter().map(EpochActivityRest::from).collect::>()), Message::StateQueryResponse(StateQueryResponse::Epochs( EpochsStateQueryResponse::Error(e), - )) => Err(anyhow::anyhow!( - "Internal server error while retrieving previous epochs: {e}" - )), - Message::StateQueryResponse(StateQueryResponse::Epochs( - EpochsStateQueryResponse::NotFound, - )) => Err(anyhow::anyhow!("Epoch not found")), - _ => Err(anyhow::anyhow!( - "Unexpected message type while retrieving previous epochs" + )) => Err(e), + _ => Err(QueryError::query_failed( + "Unexpected message type while retrieving previous epochs", )), }, ) .await?; - let json = match serde_json::to_string_pretty(&previous_epochs) { - Ok(j) => j, - Err(e) => { - return Ok(RESTResponse::with_text( - 500, - &format!("Failed to serialize epoch info: {e}"), - )); - } - }; + let json = serde_json::to_string_pretty(&previous_epochs)?; Ok(RESTResponse::with_json(200, &json)) } @@ -386,24 +313,17 @@ pub async fn handle_epoch_total_stakes_blockfrost( context: Arc>, params: Vec, handlers_config: Arc, -) -> Result { +) -> Result { if params.len() != 1 { - return Ok(RESTResponse::with_text( - 400, - "Expected one parameter: an epoch number", + return Err(RESTError::BadRequest( + "Expected one parameter: an epoch number".to_string(), )); } let param = ¶ms[0]; - let epoch_number = match param.parse::() { - Ok(num) => num, - Err(_) => { - return Ok(RESTResponse::with_text( - 400, - "Invalid epoch number parameter", - )); - } - }; + let epoch_number = param + .parse::() + .map_err(|_| RESTError::invalid_param("epoch", "must be a valid number"))?; // Query latest epoch from epochs-state let latest_epoch_msg = Arc::new(Message::StateQuery(StateQuery::Epochs( @@ -417,15 +337,18 @@ pub async fn handle_epoch_total_stakes_blockfrost( Message::StateQueryResponse(StateQueryResponse::Epochs( EpochsStateQueryResponse::LatestEpoch(res), )) => Ok(res.epoch.epoch), - _ => Err(anyhow::anyhow!( - "Unexpected message type while retrieving latest epoch" + Message::StateQueryResponse(StateQueryResponse::Epochs( + EpochsStateQueryResponse::Error(e), + )) => Err(e), + _ => Err(QueryError::query_failed( + "Unexpected message type while retrieving latest epoch", )), }, ) .await?; if epoch_number > latest_epoch { - return Ok(RESTResponse::with_text(404, "Epoch not found")); + return Err(RESTError::not_found("Epoch not found")); } // Query SPDD by epoch from accounts-state @@ -444,68 +367,55 @@ pub async fn handle_epoch_total_stakes_blockfrost( )) => Ok(res), Message::StateQueryResponse(StateQueryResponse::Accounts( AccountsStateQueryResponse::Error(e), - )) => Err(anyhow::anyhow!( - "Internal server error while retrieving SPDD by epoch: {e}" - )), - _ => Err(anyhow::anyhow!( - "Unexpected message type while retrieving SPDD by epoch" + )) => Err(e), + _ => Err(QueryError::query_failed( + "Unexpected message type while retrieving SPDD by epoch", )), }, ) .await?; - let spdd_response = spdd + + let spdd_response: Vec = spdd .into_iter() .map(|(pool_id, stake_address, amount)| { let bech32 = stake_address .to_string() - .map_err(|e| anyhow::anyhow!("Failed to convert stake address to string {}", e))?; + .map_err(|e| RESTError::encoding_failed(&format!("stake address: {}", e)))?; Ok(SPDDByEpochItemRest { pool_id, stake_address: bech32, amount, }) }) - .collect::>>()?; + .collect::, RESTError>>()?; - match serde_json::to_string_pretty(&spdd_response) { - Ok(json) => Ok(RESTResponse::with_json(200, &json)), - Err(e) => Ok(RESTResponse::with_text( - 500, - &format!("Failed to serialize SPDD by epoch: {e}"), - )), - } + let json = serde_json::to_string_pretty(&spdd_response)?; + Ok(RESTResponse::with_json(200, &json)) } pub async fn handle_epoch_pool_stakes_blockfrost( context: Arc>, params: Vec, handlers_config: Arc, -) -> Result { +) -> Result { if params.len() != 2 { - return Ok(RESTResponse::with_text( - 400, - "Expected two parameters: an epoch number and a pool ID", + return Err(RESTError::BadRequest( + "Expected two parameters: an epoch number and a pool ID".to_string(), )); } let param = ¶ms[0]; let pool_id = ¶ms[1]; - let epoch_number = match param.parse::() { - Ok(num) => num, - Err(_) => { - return Ok(RESTResponse::with_text( - 400, - "Invalid epoch number parameter", - )); - } - }; + let epoch_number = param + .parse::() + .map_err(|_| RESTError::invalid_param("epoch", "must be a valid number"))?; - let Ok(pool_id) = PoolId::from_bech32(pool_id) else { - return Ok(RESTResponse::with_text( - 400, - &format!("Invalid Bech32 stake pool ID: {pool_id}"), - )); - }; + let pool_id = PoolId::from_bech32(pool_id).map_err(|_| { + RESTError::invalid_param( + "pool_id", + &format!("Invalid Bech32 stake pool ID: {}", pool_id), + ) + })?; // Query latest epoch from epochs-state let latest_epoch_msg = Arc::new(Message::StateQuery(StateQuery::Epochs( @@ -519,15 +429,18 @@ pub async fn handle_epoch_pool_stakes_blockfrost( Message::StateQueryResponse(StateQueryResponse::Epochs( EpochsStateQueryResponse::LatestEpoch(res), )) => Ok(res.epoch.epoch), - _ => Err(anyhow::anyhow!( - "Unexpected message type while retrieving latest epoch" + Message::StateQueryResponse(StateQueryResponse::Epochs( + EpochsStateQueryResponse::Error(e), + )) => Err(e), + _ => Err(QueryError::query_failed( + "Unexpected message type while retrieving latest epoch", )), }, ) .await?; if epoch_number > latest_epoch { - return Ok(RESTResponse::with_text(404, "Epoch not found")); + return Err(RESTError::not_found("Epoch not found")); } // Query SPDD by epoch and pool from accounts-state @@ -547,75 +460,62 @@ pub async fn handle_epoch_pool_stakes_blockfrost( )) => Ok(res), Message::StateQueryResponse(StateQueryResponse::Accounts( AccountsStateQueryResponse::Error(e), - )) => Err(anyhow::anyhow!( - "Internal server error while retrieving SPDD by epoch and pool: {e}" - )), - _ => Err(anyhow::anyhow!( - "Unexpected message type while retrieving SPDD by epoch and pool" + )) => Err(e), + _ => Err(QueryError::query_failed( + "Unexpected message type while retrieving SPDD by epoch and pool", )), }, ) .await?; - let spdd_response = spdd + + let spdd_response: Vec = spdd .into_iter() .map(|(stake_address, amount)| { let bech32 = stake_address .to_string() - .map_err(|e| anyhow::anyhow!("Failed to convert stake address to string {}", e))?; + .map_err(|e| RESTError::encoding_failed(&format!("stake address: {}", e)))?; Ok(SPDDByEpochAndPoolItemRest { stake_address: bech32, amount, }) }) - .collect::>>()?; + .collect::, RESTError>>()?; - match serde_json::to_string_pretty(&spdd_response) { - Ok(json) => Ok(RESTResponse::with_json(200, &json)), - Err(e) => Ok(RESTResponse::with_text( - 500, - &format!("Failed to serialize SPDD by epoch and pool: {e}"), - )), - } + let json = serde_json::to_string_pretty(&spdd_response)?; + Ok(RESTResponse::with_json(200, &json)) } pub async fn handle_epoch_total_blocks_blockfrost( _context: Arc>, _params: Vec, _handlers_config: Arc, -) -> Result { - Ok(RESTResponse::with_text(501, "Not implemented")) +) -> Result { + Err(RESTError::not_implemented("Endpoint not yet implemented")) } pub async fn handle_epoch_pool_blocks_blockfrost( context: Arc>, params: Vec, handlers_config: Arc, -) -> Result { +) -> Result { if params.len() != 2 { - return Ok(RESTResponse::with_text( - 400, - "Expected two parameters: an epoch number and a pool ID", + return Err(RESTError::BadRequest( + "Expected two parameters: an epoch number and a pool ID".to_string(), )); } let epoch_number_param = ¶ms[0]; let pool_id_param = ¶ms[1]; - let epoch_number = match epoch_number_param.parse::() { - Ok(num) => num, - Err(_) => { - return Ok(RESTResponse::with_text( - 400, - "Invalid epoch number parameter", - )); - } - }; + let epoch_number = epoch_number_param + .parse::() + .map_err(|_| RESTError::invalid_param("epoch", "must be a valid number"))?; - let Ok(spo) = PoolId::from_bech32(pool_id_param) else { - return Ok(RESTResponse::with_text( - 400, - &format!("Invalid Bech32 stake pool ID: {pool_id_param}"), - )); - }; + let spo = PoolId::from_bech32(pool_id_param).map_err(|_| { + RESTError::invalid_param( + "pool_id", + &format!("Invalid Bech32 stake pool ID: {}", pool_id_param), + ) + })?; // query Pool's Blocks by epoch from spo-state let msg = Arc::new(Message::StateQuery(StateQuery::Pools( @@ -635,10 +535,10 @@ pub async fn handle_epoch_pool_blocks_blockfrost( )) => Ok(blocks), Message::StateQueryResponse(StateQueryResponse::Pools( PoolsStateQueryResponse::Error(e), - )) => Err(anyhow::anyhow!( - "Internal server error while retrieving pool block hashes by epoch: {e}" + )) => Err(e), + _ => Err(QueryError::query_failed( + "Unexpected message type while retrieving pool block hashes by epoch", )), - _ => Err(anyhow::anyhow!("Unexpected message type")), }, ) .await?; @@ -646,12 +546,6 @@ pub async fn handle_epoch_pool_blocks_blockfrost( // NOTE: // Need to query chain_store // to get block_hash for each block height - - match serde_json::to_string_pretty(&blocks) { - Ok(json) => Ok(RESTResponse::with_json(200, &json)), - Err(e) => Ok(RESTResponse::with_text( - 500, - &format!("Internal server error while retrieving pool block hashes by epoch: {e}"), - )), - } + let json = serde_json::to_string_pretty(&blocks)?; + Ok(RESTResponse::with_json(200, &json)) } diff --git a/modules/rest_blockfrost/src/handlers/governance.rs b/modules/rest_blockfrost/src/handlers/governance.rs index 20440e46..dad92282 100644 --- a/modules/rest_blockfrost/src/handlers/governance.rs +++ b/modules/rest_blockfrost/src/handlers/governance.rs @@ -4,15 +4,17 @@ use crate::types::{ DRepInfoREST, DRepMetadataREST, DRepUpdateREST, DRepVoteREST, DRepsListREST, ProposalVoteREST, VoterRoleREST, }; +use acropolis_common::rest_error::RESTError; use acropolis_common::{ messages::{Message, RESTResponse, StateQuery, StateQueryResponse}, queries::{ accounts::{AccountsStateQuery, AccountsStateQueryResponse}, + errors::QueryError, governance::{GovernanceStateQuery, GovernanceStateQueryResponse}, + utils::query_state, }, Credential, GovActionId, TxHash, Voter, }; -use anyhow::{anyhow, Result}; use caryatid_sdk::Context; use reqwest::Client; use serde_json::Value; @@ -22,67 +24,52 @@ pub async fn handle_dreps_list_blockfrost( context: Arc>, _params: Vec, handlers_config: Arc, -) -> Result { +) -> Result { let msg = Arc::new(Message::StateQuery(StateQuery::Governance( GovernanceStateQuery::GetDRepsList, ))); - let raw_msg = context.message_bus.request(&handlers_config.dreps_query_topic, msg).await?; - let message = Arc::try_unwrap(raw_msg).unwrap_or_else(|arc| (*arc).clone()); - match message { - Message::StateQueryResponse(StateQueryResponse::Governance( - GovernanceStateQueryResponse::DRepsList(list), - )) => { - let response: Vec = list - .dreps - .iter() - .map(|cred| { - Ok(DRepsListREST { - drep_id: cred.to_drep_bech32()?, - hex: hex::encode(cred.get_hash()), - }) - }) - .collect::>()?; - - match serde_json::to_string_pretty(&response) { - Ok(json) => Ok(RESTResponse::with_json(200, &json)), - Err(e) => Ok(RESTResponse::with_text( - 500, - &format!("Failed to serialize response: {e}"), - )), - } - } - - Message::StateQueryResponse(StateQueryResponse::Governance( - GovernanceStateQueryResponse::Error(e), - )) => Ok(RESTResponse::with_text(500, &format!("Query error: {e}"))), - - Message::StateQueryResponse(StateQueryResponse::Governance( - GovernanceStateQueryResponse::NotFound, - )) => Ok(RESTResponse::with_text(404, "No DReps found")), - - _ => Ok(RESTResponse::with_text(500, "Unexpected message type")), - } + let list = query_state( + &context, + &handlers_config.dreps_query_topic, + msg, + |message| match message { + Message::StateQueryResponse(StateQueryResponse::Governance( + GovernanceStateQueryResponse::DRepsList(list), + )) => Ok(list), + Message::StateQueryResponse(StateQueryResponse::Governance( + GovernanceStateQueryResponse::Error(e), + )) => Err(e), + _ => Err(QueryError::query_failed("Unexpected message type")), + }, + ) + .await?; + + let response: Vec = list + .dreps + .iter() + .map(|cred| { + Ok(DRepsListREST { + drep_id: cred + .to_drep_bech32() + .map_err(|e| RESTError::encoding_failed(&format!("DRep ID: {}", e)))?, + hex: hex::encode(cred.get_hash()), + }) + }) + .collect::, RESTError>>()?; + + let json = serde_json::to_string_pretty(&response)?; + Ok(RESTResponse::with_json(200, &json)) } pub async fn handle_single_drep_blockfrost( context: Arc>, params: Vec, handlers_config: Arc, -) -> Result { - let Some(drep_id) = params.first() else { - return Ok(RESTResponse::with_text(400, "Missing DRep ID parameter")); - }; +) -> Result { + let drep_id = params.first().ok_or_else(|| RESTError::param_missing("drep_id"))?; - let credential = match Credential::from_drep_bech32(drep_id) { - Ok(c) => c, - Err(e) => { - return Ok(RESTResponse::with_text( - 400, - &format!("Invalid Bech32 DRep ID: {drep_id}. Error: {e}"), - )); - } - }; + let credential = parse_drep_credential(drep_id)?; let msg = Arc::new(Message::StateQuery(StateQuery::Governance( GovernanceStateQuery::GetDRepInfoWithDelegators { @@ -90,93 +77,71 @@ pub async fn handle_single_drep_blockfrost( }, ))); - let raw_msg = context.message_bus.request(&handlers_config.dreps_query_topic, msg).await?; - let message = Arc::try_unwrap(raw_msg).unwrap_or_else(|arc| (*arc).clone()); - - match message { - Message::StateQueryResponse(StateQueryResponse::Governance( - GovernanceStateQueryResponse::DRepInfoWithDelegators(response), - )) => { - let active = !response.info.retired && !response.info.expired; - - let stake_addresses = response.delegators.clone(); - - let sum_msg = Arc::new(Message::StateQuery(StateQuery::Accounts( - AccountsStateQuery::GetAccountsBalancesSum { stake_addresses }, - ))); - - let raw_sum = - context.message_bus.request(&handlers_config.accounts_query_topic, sum_msg).await?; - let sum_response = Arc::try_unwrap(raw_sum).unwrap_or_else(|arc| (*arc).clone()); - - let amount = match sum_response { - Message::StateQueryResponse(StateQueryResponse::Accounts( - AccountsStateQueryResponse::AccountsBalancesSum(sum), - )) => sum.to_string(), - - Message::StateQueryResponse(StateQueryResponse::Accounts( - AccountsStateQueryResponse::Error(e), - )) => { - return Ok(RESTResponse::with_text( - 500, - &format!("Failed to sum balances: {e}"), - )); - } - - _ => { - return Ok(RESTResponse::with_text( - 500, - "Unexpected response from accounts-state", - )); - } - }; - - let response = DRepInfoREST { - drep_id: drep_id.to_string(), - hex: hex::encode(credential.get_hash()), - amount, - active, - active_epoch: response.info.active_epoch, - has_script: matches!(credential, Credential::ScriptHash(_)), - last_active_epoch: response.info.last_active_epoch, - retired: response.info.retired, - expired: response.info.expired, - }; + let response = query_state( + &context, + &handlers_config.dreps_query_topic, + msg, + |message| match message { + Message::StateQueryResponse(StateQueryResponse::Governance( + GovernanceStateQueryResponse::DRepInfoWithDelegators(response), + )) => Ok(response), + Message::StateQueryResponse(StateQueryResponse::Governance( + GovernanceStateQueryResponse::Error(e), + )) => Err(e), + _ => Err(QueryError::query_failed("Unexpected message type")), + }, + ) + .await?; - match serde_json::to_string_pretty(&response) { - Ok(json) => Ok(RESTResponse::with_json(200, &json)), - Err(e) => Ok(RESTResponse::with_text( - 500, - &format!("Failed to serialize DRep info: {e}"), - )), - } - } + let active = !response.info.retired && !response.info.expired; + let stake_addresses = response.delegators.clone(); - Message::StateQueryResponse(StateQueryResponse::Governance( - GovernanceStateQueryResponse::NotFound, - )) => Ok(RESTResponse::with_text(404, "DRep not found")), + let sum_msg = Arc::new(Message::StateQuery(StateQuery::Accounts( + AccountsStateQuery::GetAccountsBalancesSum { stake_addresses }, + ))); - Message::StateQueryResponse(StateQueryResponse::Governance( - GovernanceStateQueryResponse::Error(e), - )) => Ok(RESTResponse::with_text(500, &e.to_string())), + let amount = query_state( + &context, + &handlers_config.accounts_query_topic, + sum_msg, + |message| match message { + Message::StateQueryResponse(StateQueryResponse::Accounts( + AccountsStateQueryResponse::AccountsBalancesSum(sum), + )) => Ok(sum.to_string()), + Message::StateQueryResponse(StateQueryResponse::Accounts( + AccountsStateQueryResponse::Error(e), + )) => Err(e), + _ => Err(QueryError::query_failed( + "Unexpected response from accounts-state", + )), + }, + ) + .await?; + + let response = DRepInfoREST { + drep_id: drep_id.to_string(), + hex: hex::encode(credential.get_hash()), + amount, + active, + active_epoch: response.info.active_epoch, + has_script: matches!(credential, Credential::ScriptHash(_)), + last_active_epoch: response.info.last_active_epoch, + retired: response.info.retired, + expired: response.info.expired, + }; - _ => Ok(RESTResponse::with_text(500, "Unexpected message type")), - } + let json = serde_json::to_string_pretty(&response)?; + Ok(RESTResponse::with_json(200, &json)) } pub async fn handle_drep_delegators_blockfrost( context: Arc>, params: Vec, handlers_config: Arc, -) -> Result { - let Some(drep_id) = params.first() else { - return Ok(RESTResponse::with_text(400, "Missing DRep ID parameter")); - }; +) -> Result { + let drep_id = params.first().ok_or_else(|| RESTError::param_missing("drep_id"))?; - let credential = match parse_drep_credential(drep_id) { - Ok(c) => c, - Err(resp) => return Ok(resp), - }; + let credential = parse_drep_credential(drep_id)?; let msg = Arc::new(Message::StateQuery(StateQuery::Governance( GovernanceStateQuery::GetDRepDelegators { @@ -184,97 +149,72 @@ pub async fn handle_drep_delegators_blockfrost( }, ))); - let raw_msg = context.message_bus.request(&handlers_config.dreps_query_topic, msg).await?; - let message = Arc::try_unwrap(raw_msg).unwrap_or_else(|arc| (*arc).clone()); - - match message { - Message::StateQueryResponse(StateQueryResponse::Governance( - GovernanceStateQueryResponse::DRepDelegators(delegators), - )) => { - let msg = Arc::new(Message::StateQuery(StateQuery::Accounts( - AccountsStateQuery::GetAccountsUtxoValuesMap { - stake_addresses: delegators.addresses.clone(), - }, - ))); - - let raw_msg = - context.message_bus.request(&handlers_config.accounts_query_topic, msg).await?; - let message = Arc::try_unwrap(raw_msg).unwrap_or_else(|arc| (*arc).clone()); - - match message { - Message::StateQueryResponse(StateQueryResponse::Accounts( - AccountsStateQueryResponse::AccountsUtxoValuesMap(map), - )) => { - let response: Result> = map - .into_iter() - .map(|(stake_address, amount)| { - let bech32 = stake_address - .to_string() - .map_err(|e| anyhow!("Failed to encode stake address {}", e))?; - - Ok(serde_json::json!({ - "address": bech32, - "amount": amount.to_string(), - })) - }) - .collect(); - - match response { - Ok(response) => match serde_json::to_string_pretty(&response) { - Ok(json) => Ok(RESTResponse::with_json(200, &json)), - Err(e) => Ok(RESTResponse::with_text( - 500, - &format!("Failed to serialize DRep delegators: {e}"), - )), - }, - Err(e) => Ok(RESTResponse::with_text( - 500, - &format!("Internal error: {e}"), - )), - } - } - - Message::StateQueryResponse(StateQueryResponse::Accounts( - AccountsStateQueryResponse::Error(e), - )) => Ok(RESTResponse::with_text( - 500, - &format!("Account state error: {e}"), - )), - - _ => Ok(RESTResponse::with_text( - 500, - "Unexpected response from accounts-state", - )), - } - } - - Message::StateQueryResponse(StateQueryResponse::Governance( - GovernanceStateQueryResponse::NotFound, - )) => Ok(RESTResponse::with_text(404, "DRep not found")), + let delegators = query_state( + &context, + &handlers_config.dreps_query_topic, + msg, + |message| match message { + Message::StateQueryResponse(StateQueryResponse::Governance( + GovernanceStateQueryResponse::DRepDelegators(delegators), + )) => Ok(delegators), + Message::StateQueryResponse(StateQueryResponse::Governance( + GovernanceStateQueryResponse::Error(e), + )) => Err(e), + _ => Err(QueryError::query_failed("Unexpected message type")), + }, + ) + .await?; - Message::StateQueryResponse(StateQueryResponse::Governance( - GovernanceStateQueryResponse::Error(_), - )) => Ok(RESTResponse::with_text( - 500, - "DRep delegator storage is disabled in config", - )), + let msg = Arc::new(Message::StateQuery(StateQuery::Accounts( + AccountsStateQuery::GetAccountsUtxoValuesMap { + stake_addresses: delegators.addresses.clone(), + }, + ))); - _ => Ok(RESTResponse::with_text(500, "Unexpected message type")), - } + let map = query_state( + &context, + &handlers_config.accounts_query_topic, + msg, + |message| match message { + Message::StateQueryResponse(StateQueryResponse::Accounts( + AccountsStateQueryResponse::AccountsUtxoValuesMap(map), + )) => Ok(map), + Message::StateQueryResponse(StateQueryResponse::Accounts( + AccountsStateQueryResponse::Error(e), + )) => Err(e), + _ => Err(QueryError::query_failed( + "Unexpected response from accounts-state", + )), + }, + ) + .await?; + + let response: Vec = map + .into_iter() + .map(|(stake_address, amount)| { + let bech32 = stake_address + .to_string() + .map_err(|e| RESTError::encoding_failed(&format!("stake address: {}", e)))?; + + Ok(serde_json::json!({ + "address": bech32, + "amount": amount.to_string(), + })) + }) + .collect::, RESTError>>()?; + + let json = serde_json::to_string_pretty(&response)?; + Ok(RESTResponse::with_json(200, &json)) } + pub async fn handle_drep_metadata_blockfrost( context: Arc>, params: Vec, handlers_config: Arc, -) -> Result { - let Some(drep_id) = params.first() else { - return Ok(RESTResponse::with_text(400, "Missing DRep ID parameter")); - }; +) -> Result { + let drep_id = params.first().ok_or_else(|| RESTError::param_missing("drep_id"))?; - let credential = match parse_drep_credential(drep_id) { - Ok(c) => c, - Err(resp) => return Ok(resp), - }; + let credential = parse_drep_credential(drep_id)?; let msg = Arc::new(Message::StateQuery(StateQuery::Governance( GovernanceStateQuery::GetDRepMetadata { @@ -282,81 +222,62 @@ pub async fn handle_drep_metadata_blockfrost( }, ))); - let raw_msg = context.message_bus.request(&handlers_config.dreps_query_topic, msg).await?; - let message = Arc::try_unwrap(raw_msg).unwrap_or_else(|arc| (*arc).clone()); - - match message { - Message::StateQueryResponse(StateQueryResponse::Governance( - GovernanceStateQueryResponse::DRepMetadata(metadata), - )) => { - match metadata { - None => { - // metadata feature disabled - Ok(RESTResponse::with_text( - 500, - "DRep metadata storage is disabled in config", - )) - } - Some(None) => { - // enabled, but nothing stored for this DRep - Ok(RESTResponse::with_text(404, "DRep metadata not found")) - } - Some(Some(anchor)) => { - // enabled + stored → fetch the JSON - match Client::new().get(&anchor.url).send().await { - Ok(resp) => match resp.bytes().await { - Ok(raw_bytes) => match serde_json::from_slice::(&raw_bytes) { - Ok(json) => { - let bytes_hex = format!("\\x{}", hex::encode(&raw_bytes)); - - let response = DRepMetadataREST { - drep_id: drep_id.to_string(), - hex: hex::encode(credential.get_hash()), - url: anchor.url.clone(), - hash: hex::encode(anchor.data_hash.clone()), - json_metadata: json, - bytes: bytes_hex, - }; - - match serde_json::to_string_pretty(&response) { - Ok(json) => Ok(RESTResponse::with_json(200, &json)), - Err(e) => Ok(RESTResponse::with_text( - 500, - &format!("Failed to serialize DRep metadata: {e}"), - )), - } - } - Err(_) => Ok(RESTResponse::with_text( - 500, - "Invalid JSON from DRep metadata URL", - )), - }, - Err(_) => Ok(RESTResponse::with_text( - 500, - "Failed to read bytes from DRep metadata URL", - )), - }, - Err(_) => Ok(RESTResponse::with_text( - 500, - "Failed to fetch DRep metadata URL", - )), - } - } - } - } - - Message::StateQueryResponse(StateQueryResponse::Governance( - GovernanceStateQueryResponse::NotFound, - )) => Ok(RESTResponse::with_text(404, "DRep metadata not found")), + let metadata = query_state( + &context, + &handlers_config.dreps_query_topic, + msg, + |message| match message { + Message::StateQueryResponse(StateQueryResponse::Governance( + GovernanceStateQueryResponse::DRepMetadata(metadata), + )) => Ok(metadata), + Message::StateQueryResponse(StateQueryResponse::Governance( + GovernanceStateQueryResponse::Error(e), + )) => Err(e), + _ => Err(QueryError::query_failed("Unexpected message type")), + }, + ) + .await?; - Message::StateQueryResponse(StateQueryResponse::Governance( - GovernanceStateQueryResponse::Error(_), - )) => Ok(RESTResponse::with_text( - 500, - "DRep metadata storage is disabled in config", - )), + match metadata { + None => { + // metadata feature disabled + Err(RESTError::storage_disabled("DRep metadata")) + } + Some(None) => { + // enabled, but nothing stored for this DRep + Err(RESTError::not_found("DRep metadata not found")) + } + Some(Some(anchor)) => { + // enabled + stored → fetch the JSON + let resp = Client::new().get(&anchor.url).send().await.map_err(|e| { + RESTError::query_failed(&format!("Failed to fetch DRep metadata URL: {}", e)) + })?; + + let raw_bytes = resp.bytes().await.map_err(|e| { + RESTError::query_failed(&format!( + "Failed to read bytes from DRep metadata URL: {}", + e + )) + })?; + + let json: Value = serde_json::from_slice(&raw_bytes).map_err(|_| { + RESTError::BadRequest("Invalid JSON from DRep metadata URL".to_string()) + })?; + + let bytes_hex = format!("\\x{}", hex::encode(&raw_bytes)); + + let response = DRepMetadataREST { + drep_id: drep_id.to_string(), + hex: hex::encode(credential.get_hash()), + url: anchor.url.clone(), + hash: hex::encode(anchor.data_hash.clone()), + json_metadata: json, + bytes: bytes_hex, + }; - _ => Ok(RESTResponse::with_text(500, "Unexpected message type")), + let json = serde_json::to_string_pretty(&response)?; + Ok(RESTResponse::with_json(200, &json)) + } } } @@ -364,15 +285,10 @@ pub async fn handle_drep_updates_blockfrost( context: Arc>, params: Vec, handlers_config: Arc, -) -> Result { - let Some(drep_id) = params.first() else { - return Ok(RESTResponse::with_text(400, "Missing DRep ID parameter")); - }; +) -> Result { + let drep_id = params.first().ok_or_else(|| RESTError::param_missing("drep_id"))?; - let credential = match parse_drep_credential(drep_id) { - Ok(c) => c, - Err(resp) => return Ok(resp), - }; + let credential = parse_drep_credential(drep_id)?; let msg = Arc::new(Message::StateQuery(StateQuery::Governance( GovernanceStateQuery::GetDRepUpdates { @@ -380,60 +296,44 @@ pub async fn handle_drep_updates_blockfrost( }, ))); - let raw_msg = context.message_bus.request(&handlers_config.dreps_query_topic, msg).await?; - let message = Arc::try_unwrap(raw_msg).unwrap_or_else(|arc| (*arc).clone()); - - match message { - Message::StateQueryResponse(StateQueryResponse::Governance( - GovernanceStateQueryResponse::DRepUpdates(list), - )) => { - let response: Vec = list - .updates - .iter() - .map(|event| DRepUpdateREST { - tx_hash: "TxHash lookup not yet implemented".to_string(), - cert_index: event.cert_index, - action: event.action.clone(), - }) - .collect(); - - match serde_json::to_string_pretty(&response) { - Ok(json) => Ok(RESTResponse::with_json(200, &json)), - Err(e) => Ok(RESTResponse::with_text( - 500, - &format!("Failed to serialize DRep updates: {e}"), - )), - } - } - - Message::StateQueryResponse(StateQueryResponse::Governance( - GovernanceStateQueryResponse::Error(_), - )) => Ok(RESTResponse::with_text( - 503, - "DRep updates storage is disabled in config", - )), - - Message::StateQueryResponse(StateQueryResponse::Governance( - GovernanceStateQueryResponse::NotFound, - )) => Ok(RESTResponse::with_text(404, "DRep not found")), - - _ => Ok(RESTResponse::with_text(500, "Unexpected message type")), - } + let list = query_state( + &context, + &handlers_config.dreps_query_topic, + msg, + |message| match message { + Message::StateQueryResponse(StateQueryResponse::Governance( + GovernanceStateQueryResponse::DRepUpdates(list), + )) => Ok(list), + Message::StateQueryResponse(StateQueryResponse::Governance( + GovernanceStateQueryResponse::Error(e), + )) => Err(e), + _ => Err(QueryError::query_failed("Unexpected message type")), + }, + ) + .await?; + + let response: Vec = list + .updates + .iter() + .map(|event| DRepUpdateREST { + tx_hash: "TxHash lookup not yet implemented".to_string(), + cert_index: event.cert_index, + action: event.action.clone(), + }) + .collect(); + + let json = serde_json::to_string_pretty(&response)?; + Ok(RESTResponse::with_json(200, &json)) } pub async fn handle_drep_votes_blockfrost( context: Arc>, params: Vec, handlers_config: Arc, -) -> Result { - let Some(drep_id) = params.first() else { - return Ok(RESTResponse::with_text(400, "Missing DRep ID parameter")); - }; +) -> Result { + let drep_id = params.first().ok_or_else(|| RESTError::param_missing("drep_id"))?; - let credential = match parse_drep_credential(drep_id) { - Ok(c) => c, - Err(resp) => return Ok(resp), - }; + let credential = parse_drep_credential(drep_id)?; let msg = Arc::new(Message::StateQuery(StateQuery::Governance( GovernanceStateQuery::GetDRepVotes { @@ -441,161 +341,129 @@ pub async fn handle_drep_votes_blockfrost( }, ))); - let raw_msg = context.message_bus.request(&handlers_config.dreps_query_topic, msg).await?; - let message = Arc::try_unwrap(raw_msg).unwrap_or_else(|arc| (*arc).clone()); - match message { - Message::StateQueryResponse(StateQueryResponse::Governance( - GovernanceStateQueryResponse::DRepVotes(votes), - )) => { - let response: Vec<_> = votes - .votes - .iter() - .map(|vote| DRepVoteREST { - tx_hash: hex::encode(vote.tx_hash), - cert_index: vote.vote_index, - vote: vote.vote.clone(), - }) - .collect(); - - match serde_json::to_string_pretty(&response) { - Ok(json) => Ok(RESTResponse::with_json(200, &json)), - Err(e) => Ok(RESTResponse::with_text( - 500, - &format!("Failed to serialize DRep votes: {e}"), - )), - } - } - - Message::StateQueryResponse(StateQueryResponse::Governance( - GovernanceStateQueryResponse::NotFound, - )) => Ok(RESTResponse::with_text(404, "DRep not found")), - - Message::StateQueryResponse(StateQueryResponse::Governance( - GovernanceStateQueryResponse::Error(_), - )) => Ok(RESTResponse::with_text( - 503, - "DRep vote storage is disabled in config", - )), - - _ => Ok(RESTResponse::with_text(500, "Unexpected message type")), - } + let votes = query_state( + &context, + &handlers_config.dreps_query_topic, + msg, + |message| match message { + Message::StateQueryResponse(StateQueryResponse::Governance( + GovernanceStateQueryResponse::DRepVotes(votes), + )) => Ok(votes), + Message::StateQueryResponse(StateQueryResponse::Governance( + GovernanceStateQueryResponse::Error(e), + )) => Err(e), + _ => Err(QueryError::query_failed("Unexpected message type")), + }, + ) + .await?; + + let response: Vec<_> = votes + .votes + .iter() + .map(|vote| DRepVoteREST { + tx_hash: hex::encode(vote.tx_hash), + cert_index: vote.vote_index, + vote: vote.vote.clone(), + }) + .collect(); + + let json = serde_json::to_string_pretty(&response)?; + Ok(RESTResponse::with_json(200, &json)) } pub async fn handle_proposals_list_blockfrost( context: Arc>, _params: Vec, handlers_config: Arc, -) -> Result { +) -> Result { let msg = Arc::new(Message::StateQuery(StateQuery::Governance( GovernanceStateQuery::GetProposalsList, ))); - let raw_msg = context.message_bus.request(&handlers_config.governance_query_topic, msg).await?; - let message = Arc::try_unwrap(raw_msg).unwrap_or_else(|arc| (*arc).clone()); - - match message { - Message::StateQueryResponse(StateQueryResponse::Governance( - GovernanceStateQueryResponse::ProposalsList(list), - )) => { - if list.proposals.is_empty() { - return Ok(RESTResponse::with_json(200, "[]")); - } - - let props_bech32: Result, _> = - list.proposals.iter().map(|id| id.to_bech32()).collect(); - - match props_bech32 { - Ok(vec) => match serde_json::to_string(&vec) { - Ok(json) => Ok(RESTResponse::with_json(200, &json)), - Err(e) => Ok(RESTResponse::with_text( - 500, - &format!("Failed to serialize proposals list: {e}"), - )), - }, - Err(e) => Ok(RESTResponse::with_text( - 500, - &format!("Failed to convert proposal IDs to Bech32: {e}"), - )), - } - } + let list = query_state( + &context, + &handlers_config.governance_query_topic, + msg, + |message| match message { + Message::StateQueryResponse(StateQueryResponse::Governance( + GovernanceStateQueryResponse::ProposalsList(list), + )) => Ok(list), + Message::StateQueryResponse(StateQueryResponse::Governance( + GovernanceStateQueryResponse::Error(e), + )) => Err(e), + _ => Err(QueryError::query_failed("Unexpected message type")), + }, + ) + .await?; - Message::StateQueryResponse(StateQueryResponse::Governance( - GovernanceStateQueryResponse::Error(e), - )) => Ok(RESTResponse::with_text(500, &format!("Query error: {e}"))), + if list.proposals.is_empty() { + return Ok(RESTResponse::with_json(200, "[]")); + } - Message::StateQueryResponse(StateQueryResponse::Governance( - GovernanceStateQueryResponse::NotFound, - )) => Ok(RESTResponse::with_text(404, "No proposals found")), + let props_bech32: Vec = list + .proposals + .iter() + .map(|id| id.to_bech32()) + .collect::, _>>() + .map_err(|e| RESTError::encoding_failed(&format!("proposal IDs: {}", e)))?; - _ => Ok(RESTResponse::with_text(500, "Unexpected message type")), - } + let json = serde_json::to_string(&props_bech32)?; + Ok(RESTResponse::with_json(200, &json)) } pub async fn handle_single_proposal_blockfrost( context: Arc>, params: Vec, handlers_config: Arc, -) -> Result { - let proposal = match parse_gov_action_id(¶ms)? { - Ok(id) => id, - Err(resp) => return Ok(resp), - }; +) -> Result { + let proposal = parse_gov_action_id(¶ms)?; let msg = Arc::new(Message::StateQuery(StateQuery::Governance( GovernanceStateQuery::GetProposalInfo { proposal }, ))); - let raw_msg = context.message_bus.request(&handlers_config.governance_query_topic, msg).await?; - let message = Arc::try_unwrap(raw_msg).unwrap_or_else(|arc| (*arc).clone()); - - match message { - Message::StateQueryResponse(StateQueryResponse::Governance( - GovernanceStateQueryResponse::ProposalInfo(info), - )) => match serde_json::to_string(&info) { - Ok(json) => Ok(RESTResponse::with_json(200, &json)), - Err(e) => Ok(RESTResponse::with_text( - 500, - &format!("Failed to serialize proposal info: {e}"), - )), + let info = query_state( + &context, + &handlers_config.governance_query_topic, + msg, + |message| match message { + Message::StateQueryResponse(StateQueryResponse::Governance( + GovernanceStateQueryResponse::ProposalInfo(info), + )) => Ok(info), + Message::StateQueryResponse(StateQueryResponse::Governance( + GovernanceStateQueryResponse::Error(e), + )) => Err(e), + _ => Err(QueryError::query_failed("Unexpected message type")), }, + ) + .await?; - Message::StateQueryResponse(StateQueryResponse::Governance( - GovernanceStateQueryResponse::NotFound, - )) => Ok(RESTResponse::with_text(404, "Proposal not found")), - - Message::StateQueryResponse(StateQueryResponse::Governance( - GovernanceStateQueryResponse::Error(e), - )) => Ok(RESTResponse::with_text(500, &format!("Query error: {e}"))), - - _ => Ok(RESTResponse::with_text(500, "Unexpected message type")), - } + let json = serde_json::to_string(&info)?; + Ok(RESTResponse::with_json(200, &json)) } pub async fn handle_proposal_parameters_blockfrost( _context: Arc>, _params: Vec, _handlers_config: Arc, -) -> Result { - Ok(RESTResponse::with_text(501, "Not implemented")) +) -> Result { + Err(RESTError::not_implemented("Endpoint not yet implemented")) } pub async fn handle_proposal_withdrawals_blockfrost( _context: Arc>, _params: Vec, _handlers_config: Arc, -) -> Result { - Ok(RESTResponse::with_text(501, "Not implemented")) +) -> Result { + Err(RESTError::not_implemented("Endpoint not yet implemented")) } pub async fn handle_proposal_votes_blockfrost( context: Arc>, params: Vec, handlers_config: Arc, -) -> Result { - let proposal = match parse_gov_action_id(¶ms)? { - Ok(id) => id, - Err(resp) => return Ok(resp), - }; +) -> Result { + let proposal = parse_gov_action_id(¶ms)?; let tx_hash = hex::encode(proposal.transaction_id); let cert_index = proposal.action_index; @@ -604,115 +472,85 @@ pub async fn handle_proposal_votes_blockfrost( GovernanceStateQuery::GetProposalVotes { proposal }, ))); - let raw_msg = context.message_bus.request(&handlers_config.governance_query_topic, msg).await?; - let message = Arc::try_unwrap(raw_msg).unwrap_or_else(|arc| (*arc).clone()); - - match message { - Message::StateQueryResponse(StateQueryResponse::Governance( - GovernanceStateQueryResponse::ProposalVotes(votes), - )) => { - let mut votes_list = Vec::new(); - - for (voter, (_, voting_proc)) in votes.votes { - let voter_role = match voter { - Voter::ConstitutionalCommitteeKey(_) - | Voter::ConstitutionalCommitteeScript(_) => { - VoterRoleREST::ConstitutionalCommittee - } - Voter::DRepKey(_) | Voter::DRepScript(_) => VoterRoleREST::Drep, - Voter::StakePoolKey(_) => VoterRoleREST::Spo, - }; - - let voter_str = voter.to_string(); - - votes_list.push(ProposalVoteREST { - tx_hash: tx_hash.clone(), - cert_index, - voter_role, - voter: voter_str, - vote: voting_proc.vote, - }); - } - - match serde_json::to_string(&votes_list) { - Ok(json) => Ok(RESTResponse::with_json(200, &json)), - Err(e) => Ok(RESTResponse::with_text( - 500, - &format!("Internal server error while retrieving proposal votes: {e}"), - )), - } - } - - Message::StateQueryResponse(StateQueryResponse::Governance( - GovernanceStateQueryResponse::NotFound, - )) => Ok(RESTResponse::with_text(404, "Proposal not found")), + let votes = query_state( + &context, + &handlers_config.governance_query_topic, + msg, + |message| match message { + Message::StateQueryResponse(StateQueryResponse::Governance( + GovernanceStateQueryResponse::ProposalVotes(votes), + )) => Ok(votes), + Message::StateQueryResponse(StateQueryResponse::Governance( + GovernanceStateQueryResponse::Error(e), + )) => Err(e), + _ => Err(QueryError::query_failed("Unexpected message type")), + }, + ) + .await?; - Message::StateQueryResponse(StateQueryResponse::Governance( - GovernanceStateQueryResponse::Error(e), - )) => Ok(RESTResponse::with_text(500, &format!("Query error: {e}"))), + let mut votes_list = Vec::new(); - _ => Ok(RESTResponse::with_text(500, "Unexpected message type")), + for (voter, (_, voting_proc)) in votes.votes { + let voter_role = match voter { + Voter::ConstitutionalCommitteeKey(_) | Voter::ConstitutionalCommitteeScript(_) => { + VoterRoleREST::ConstitutionalCommittee + } + Voter::DRepKey(_) | Voter::DRepScript(_) => VoterRoleREST::Drep, + Voter::StakePoolKey(_) => VoterRoleREST::Spo, + }; + + let voter_str = voter.to_string(); + + votes_list.push(ProposalVoteREST { + tx_hash: tx_hash.clone(), + cert_index, + voter_role, + voter: voter_str, + vote: voting_proc.vote, + }); } + + let json = serde_json::to_string(&votes_list)?; + Ok(RESTResponse::with_json(200, &json)) } pub async fn handle_proposal_metadata_blockfrost( _context: Arc>, _params: Vec, _handlers_config: Arc, -) -> Result { - Ok(RESTResponse::with_text(501, "Not implemented")) +) -> Result { + Err(RESTError::not_implemented("Endpoint not yet implemented")) } -pub fn parse_gov_action_id(params: &[String]) -> Result> { +pub fn parse_gov_action_id(params: &[String]) -> Result { if params.len() != 2 { - return Ok(Err(RESTResponse::with_text( - 400, - "Expected two parameters: tx_hash/cert_index", - ))); + return Err(RESTError::BadRequest( + "Expected two parameters: tx_hash/cert_index".to_string(), + )); } let tx_hash_hex = ¶ms[0]; let cert_index_str = ¶ms[1]; - let transaction_id: TxHash = match hex::decode(tx_hash_hex) { - Ok(bytes) => match bytes.as_slice().try_into() { - Ok(arr) => arr, - Err(_) => { - return Ok(Err(RESTResponse::with_text( - 400, - "Invalid tx_hash length, must be 32 bytes", - ))); - } - }, - Err(e) => { - return Ok(Err(RESTResponse::with_text( - 400, - &format!("Invalid hex tx_hash: {e}"), - ))); - } - }; + let bytes = hex::decode(tx_hash_hex) + .map_err(|e| RESTError::invalid_param("tx_hash", &format!("invalid hex: {}", e)))?; - let action_index = match cert_index_str.parse::() { - Ok(i) => i, - Err(e) => { - return Ok(Err(RESTResponse::with_text( - 400, - &format!("Invalid cert_index, expected u8: {e}"), - ))); - } - }; + let transaction_id: TxHash = bytes + .as_slice() + .try_into() + .map_err(|_| RESTError::invalid_param("tx_hash", "must be 32 bytes"))?; - Ok(Ok(GovActionId { + let action_index = cert_index_str + .parse::() + .map_err(|e| RESTError::invalid_param("cert_index", &format!("expected u8: {}", e)))?; + + Ok(GovActionId { transaction_id, action_index, - })) + }) } -fn parse_drep_credential(drep_id: &str) -> Result { - Credential::from_drep_bech32(drep_id).map_err(|e| { - RESTResponse::with_text( - 400, - &format!("Invalid Bech32 DRep ID: {drep_id}. Error: {e}"), - ) - }) +fn parse_drep_credential(drep_id: &str) -> Result { + Credential::from_drep_bech32(drep_id) + .map_err(|e| RESTError::invalid_param("drep_id", &format!("Invalid Bech32 DRep ID: {}", e))) } diff --git a/modules/rest_blockfrost/src/handlers/pools.rs b/modules/rest_blockfrost/src/handlers/pools.rs index 4b14d116..e5b42675 100644 --- a/modules/rest_blockfrost/src/handlers/pools.rs +++ b/modules/rest_blockfrost/src/handlers/pools.rs @@ -7,19 +7,20 @@ use crate::{ types::{PoolEpochStateRest, PoolExtendedRest, PoolMetadataRest, PoolRetirementRest}, utils::{fetch_pool_metadata_as_bytes, verify_pool_metadata_hash, PoolMetadataJson}, }; +use acropolis_common::queries::utils::query_state; +use acropolis_common::rest_error::RESTError; use acropolis_common::serialization::Bech32Conversion; use acropolis_common::{ messages::{Message, RESTResponse, StateQuery, StateQueryResponse}, queries::{ accounts::{AccountsStateQuery, AccountsStateQueryResponse}, epochs::{EpochsStateQuery, EpochsStateQueryResponse}, + errors::QueryError, pools::{PoolsStateQuery, PoolsStateQueryResponse}, - utils::query_state, }, rest_helper::ToCheckedF64, PoolId, PoolRetirement, PoolUpdateAction, TxIdentifier, }; -use anyhow::Result; use caryatid_sdk::Context; use rust_decimal::Decimal; use std::{sync::Arc, time::Duration}; @@ -31,53 +32,35 @@ pub async fn handle_pools_list_blockfrost( context: Arc>, _params: Vec, handlers_config: Arc, -) -> Result { - // Prepare the message +) -> Result { let msg = Arc::new(Message::StateQuery(StateQuery::Pools( PoolsStateQuery::GetPoolsList, ))); - // Send message via message bus - let raw = context.message_bus.request(&handlers_config.pools_query_topic, msg).await?; - - // Unwrap and match - let message = Arc::try_unwrap(raw).unwrap_or_else(|arc| (*arc).clone()); - - let pool_operators = match message { - Message::StateQueryResponse(StateQueryResponse::Pools( - PoolsStateQueryResponse::PoolsList(pool_operators), - )) => pool_operators, - - Message::StateQueryResponse(StateQueryResponse::Pools(PoolsStateQueryResponse::Error( - e, - ))) => { - return Ok(RESTResponse::with_text( - 500, - &format!("Internal server error while retrieving pools list: {e}"), - )); - } - - _ => return Ok(RESTResponse::with_text(500, "Unexpected message type")), - }; + let pool_operators = query_state( + &context, + &handlers_config.pools_query_topic, + msg, + |message| match message { + Message::StateQueryResponse(StateQueryResponse::Pools( + PoolsStateQueryResponse::PoolsList(pool_operators), + )) => Ok(pool_operators), + Message::StateQueryResponse(StateQueryResponse::Pools( + PoolsStateQueryResponse::Error(e), + )) => Err(e), + _ => Err(QueryError::query_failed("Unexpected message type")), + }, + ) + .await?; - let pool_ids = pool_operators + let pool_ids: Vec = pool_operators .iter() .map(|operator| operator.to_bech32()) - .collect::, _>>(); - - match pool_ids { - Ok(pool_ids) => match serde_json::to_string(&pool_ids) { - Ok(json) => Ok(RESTResponse::with_json(200, &json)), - Err(e) => Ok(RESTResponse::with_text( - 500, - &format!("Internal server error while retrieving pools list: {e}"), - )), - }, - Err(e) => Ok(RESTResponse::with_text( - 500, - &format!("Internal server error while retrieving pools list: {e}"), - )), - } + .collect::, _>>() + .map_err(|e| RESTError::encoding_failed(&format!("pool IDs: {}", e)))?; + + let json = serde_json::to_string(&pool_ids)?; + Ok(RESTResponse::with_json(200, &json)) } /// Handle `/pools/extended` `/pools/retired` `/pools/retiring` `/pools/{pool_id}` Blockfrost-compatible endpoint @@ -85,10 +68,10 @@ pub async fn handle_pools_extended_retired_retiring_single_blockfrost( context: Arc>, params: Vec, handlers_config: Arc, -) -> Result { +) -> Result { let param = match params.as_slice() { [param] => param, - _ => return Ok(RESTResponse::with_text(400, "Invalid parameters")), + _ => return Err(RESTError::BadRequest("Invalid parameters".to_string())), }; match param.as_str() { @@ -101,22 +84,19 @@ pub async fn handle_pools_extended_retired_retiring_single_blockfrost( "retiring" => { handle_pools_retiring_blockfrost(context.clone(), handlers_config.clone()).await } - _ => match PoolId::from_bech32(param) { - Ok(pool_id) => { - handle_pools_spo_blockfrost(context.clone(), pool_id, handlers_config.clone()).await - } - Err(e) => Ok(RESTResponse::with_text( - 400, - &format!("Invalid Bech32 stake pool ID: {param}. Error: {e}"), - )), - }, + _ => { + let pool_id = PoolId::from_bech32(param).map_err(|e| { + RESTError::invalid_param("pool_id", &format!("Invalid Bech32 stake pool ID: {}", e)) + })?; + handle_pools_spo_blockfrost(context.clone(), pool_id, handlers_config.clone()).await + } } } async fn handle_pools_extended_blockfrost( context: Arc>, handlers_config: Arc, -) -> Result { +) -> Result { // Get pools info from spo-state let pools_list_with_info_msg = Arc::new(Message::StateQuery(StateQuery::Pools( PoolsStateQuery::GetPoolsListWithInfo, @@ -131,11 +111,9 @@ async fn handle_pools_extended_blockfrost( )) => Ok(pools_list_with_info.pools), Message::StateQueryResponse(StateQueryResponse::Pools( PoolsStateQueryResponse::Error(e), - )) => Err(anyhow::anyhow!( - "Internal server error while retrieving pools list: {e}" - )), - _ => Err(anyhow::anyhow!( - "Unexpected message type while retrieving pools list with info" + )) => Err(e), + _ => Err(QueryError::query_failed( + "Unexpected message type while retrieving pools list with info", )), }, ); @@ -154,11 +132,9 @@ async fn handle_pools_extended_blockfrost( )) => Ok(res.epoch), Message::StateQueryResponse(StateQueryResponse::Epochs( EpochsStateQueryResponse::Error(e), - )) => Err(anyhow::anyhow!( - "Internal server error while retrieving latest epoch: {e}" - )), - _ => Err(anyhow::anyhow!( - "Unexpected message type while retrieving latest epoch" + )) => Err(e), + _ => Err(QueryError::query_failed( + "Unexpected message type while retrieving latest epoch", )), }, ); @@ -175,7 +151,10 @@ async fn handle_pools_extended_blockfrost( Message::StateQueryResponse(StateQueryResponse::Accounts( AccountsStateQueryResponse::OptimalPoolSizing(res), )) => Ok(res), - _ => Err(anyhow::anyhow!("Unexpected message type")), + Message::StateQueryResponse(StateQueryResponse::Accounts( + AccountsStateQueryResponse::Error(e), + )) => Err(e), + _ => Err(QueryError::query_failed("Unexpected message type")), }, ); @@ -196,7 +175,7 @@ async fn handle_pools_extended_blockfrost( // check optimal_pool_sizing is Some let Some(optimal_pool_sizing) = optimal_pool_sizing else { - // if it is before Shelly Era + // if it is before Shelley Era return Ok(RESTResponse::with_json(200, "[]")); }; @@ -220,13 +199,13 @@ async fn handle_pools_extended_blockfrost( PoolsStateQueryResponse::PoolsActiveStakes(active_stakes), )) => Ok(Some(active_stakes)), Message::StateQueryResponse(StateQueryResponse::Pools( - PoolsStateQueryResponse::Error(_e), + PoolsStateQueryResponse::Error(_), )) => { // if epoch_history is not enabled Ok(None) } - _ => Err(anyhow::anyhow!( - "Unexpected message type while retrieving pools active stakes" + _ => Err(QueryError::query_failed( + "Unexpected message type while retrieving pools active stakes", )), }, ); @@ -245,14 +224,10 @@ async fn handle_pools_extended_blockfrost( Message::StateQueryResponse(StateQueryResponse::Accounts( AccountsStateQueryResponse::PoolsLiveStakes(pools_live_stakes), )) => Ok(pools_live_stakes), - Message::StateQueryResponse(StateQueryResponse::Accounts( AccountsStateQueryResponse::Error(e), - )) => Err(anyhow::anyhow!( - "Internal server error while retrieving pools live stakes: {e}" - )), - - _ => Err(anyhow::anyhow!("Unexpected message type")), + )) => Err(e), + _ => Err(QueryError::query_failed("Unexpected message type")), }, ); @@ -270,7 +245,10 @@ async fn handle_pools_extended_blockfrost( Message::StateQueryResponse(StateQueryResponse::Pools( PoolsStateQueryResponse::PoolsTotalBlocksMinted(total_blocks_minted), )) => Ok(total_blocks_minted), - _ => Err(anyhow::anyhow!("Unexpected message type")), + Message::StateQueryResponse(StateQueryResponse::Pools( + PoolsStateQueryResponse::Error(e), + )) => Err(e), + _ => Err(QueryError::query_failed("Unexpected message type")), }, ); @@ -283,49 +261,36 @@ async fn handle_pools_extended_blockfrost( let pools_live_stakes = pools_live_stakes?; let total_blocks_minted = total_blocks_minted?; - let pools_extened_rest_results: Result, anyhow::Error> = - pools_list_with_info - .iter() - .enumerate() - .map(|(i, (pool_operator, pool_registration))| { - Ok(PoolExtendedRest { - pool_id: pool_operator.to_bech32()?, - hex: pool_operator.to_vec(), - active_stake: pools_active_stakes - .as_ref() - .map(|active_stakes| active_stakes[i]), - live_stake: pools_live_stakes[i], - blocks_minted: total_blocks_minted[i], - live_saturation: Decimal::from(pools_live_stakes[i]) - * Decimal::from(optimal_pool_sizing.nopt) - / Decimal::from(optimal_pool_sizing.total_supply), - declared_pledge: pool_registration.pledge, - margin_cost: pool_registration.margin.to_f32(), - fixed_cost: pool_registration.cost, - }) + let pools_extended_rest: Vec = pools_list_with_info + .iter() + .enumerate() + .map(|(i, (pool_operator, pool_registration))| { + Ok(PoolExtendedRest { + pool_id: pool_operator + .to_bech32() + .map_err(|e| RESTError::encoding_failed(&format!("pool ID: {}", e)))?, + hex: pool_operator.to_vec(), + active_stake: pools_active_stakes.as_ref().map(|active_stakes| active_stakes[i]), + live_stake: pools_live_stakes[i], + blocks_minted: total_blocks_minted[i], + live_saturation: Decimal::from(pools_live_stakes[i]) + * Decimal::from(optimal_pool_sizing.nopt) + / Decimal::from(optimal_pool_sizing.total_supply), + declared_pledge: pool_registration.pledge, + margin_cost: pool_registration.margin.to_f32(), + fixed_cost: pool_registration.cost, }) - .collect(); - - match pools_extened_rest_results { - Ok(pools_extened_rest) => match serde_json::to_string(&pools_extened_rest) { - Ok(json) => Ok(RESTResponse::with_json(200, &json)), - Err(e) => Ok(RESTResponse::with_text( - 500, - &format!("Internal server error while extended retrieving pools list: {e}"), - )), - }, - Err(e) => Ok(RESTResponse::with_text( - 500, - &format!("Internal server error while extended retrieving pools list: {e}"), - )), - } + }) + .collect::, RESTError>>()?; + + let json = serde_json::to_string(&pools_extended_rest)?; + Ok(RESTResponse::with_json(200, &json)) } async fn handle_pools_retired_blockfrost( context: Arc>, handlers_config: Arc, -) -> Result { - // Get retired pools from spo-state +) -> Result { let retired_pools_msg = Arc::new(Message::StateQuery(StateQuery::Pools( PoolsStateQuery::GetPoolsRetiredList, ))); @@ -339,39 +304,32 @@ async fn handle_pools_retired_blockfrost( )) => Ok(retired_pools), Message::StateQueryResponse(StateQueryResponse::Pools( PoolsStateQueryResponse::Error(e), - )) => Err(anyhow::anyhow!( - "Internal server error while retrieving retired pools: {e}" - )), - _ => Err(anyhow::anyhow!("Unexpected message type")), + )) => Err(e), + _ => Err(QueryError::query_failed("Unexpected message type")), }, ) .await?; - let retired_pools_rest = retired_pools + let retired_pools_rest: Vec = retired_pools .iter() - .filter_map(|PoolRetirement { operator, epoch }| { - let pool_id = operator.to_bech32().ok()?; - Some(PoolRetirementRest { - pool_id, + .map(|PoolRetirement { operator, epoch }| { + Ok(PoolRetirementRest { + pool_id: operator + .to_bech32() + .map_err(|e| RESTError::encoding_failed(&format!("pool ID: {}", e)))?, epoch: *epoch, }) }) - .collect::>(); + .collect::, RESTError>>()?; - match serde_json::to_string(&retired_pools_rest) { - Ok(json) => Ok(RESTResponse::with_json(200, &json)), - Err(e) => Ok(RESTResponse::with_text( - 500, - &format!("Internal server error while retrieving retired pools: {e}"), - )), - } + let json = serde_json::to_string(&retired_pools_rest)?; + Ok(RESTResponse::with_json(200, &json)) } async fn handle_pools_retiring_blockfrost( context: Arc>, handlers_config: Arc, -) -> Result { - // Get retiring pools from spo-state +) -> Result { let retiring_pools_msg = Arc::new(Message::StateQuery(StateQuery::Pools( PoolsStateQuery::GetPoolsRetiringList, ))); @@ -385,39 +343,33 @@ async fn handle_pools_retiring_blockfrost( )) => Ok(retiring_pools), Message::StateQueryResponse(StateQueryResponse::Pools( PoolsStateQueryResponse::Error(e), - )) => Err(anyhow::anyhow!( - "Internal server error while retrieving retiring pools: {e}" - )), - _ => Err(anyhow::anyhow!("Unexpected message type")), + )) => Err(e), + _ => Err(QueryError::query_failed("Unexpected message type")), }, ) .await?; - let retiring_pools_rest = retiring_pools + let retiring_pools_rest: Vec = retiring_pools .iter() - .filter_map(|PoolRetirement { operator, epoch }| { - let pool_id = operator.to_bech32().ok()?; - Some(PoolRetirementRest { - pool_id, + .map(|PoolRetirement { operator, epoch }| { + Ok(PoolRetirementRest { + pool_id: operator + .to_bech32() + .map_err(|e| RESTError::encoding_failed(&format!("pool ID: {}", e)))?, epoch: *epoch, }) }) - .collect::>(); + .collect::, RESTError>>()?; - match serde_json::to_string(&retiring_pools_rest) { - Ok(json) => Ok(RESTResponse::with_json(200, &json)), - Err(e) => Ok(RESTResponse::with_text( - 500, - &format!("Internal server error while retrieving retiring pools: {e}"), - )), - } + let json = serde_json::to_string(&retiring_pools_rest)?; + Ok(RESTResponse::with_json(200, &json)) } async fn handle_pools_spo_blockfrost( context: Arc>, pool_operator: PoolId, handlers_config: Arc, -) -> Result { +) -> Result { // Get PoolRegistration from spo state let pool_info_msg = Arc::new(Message::StateQuery(StateQuery::Pools( PoolsStateQuery::GetPoolInfo { @@ -433,15 +385,10 @@ async fn handle_pools_spo_blockfrost( Message::StateQueryResponse(StateQueryResponse::Pools( PoolsStateQueryResponse::PoolInfo(pool_info), )) => Ok(pool_info), - Message::StateQueryResponse(StateQueryResponse::Pools( - PoolsStateQueryResponse::NotFound, - )) => Err(anyhow::anyhow!("Pool Not found")), Message::StateQueryResponse(StateQueryResponse::Pools( PoolsStateQueryResponse::Error(e), - )) => Err(anyhow::anyhow!( - "Internal server error while retrieving pool info: {e}" - )), - _ => Err(anyhow::anyhow!("Unexpected message type")), + )) => Err(e), + _ => Err(QueryError::query_failed("Unexpected message type")), }, ); @@ -459,11 +406,9 @@ async fn handle_pools_spo_blockfrost( )) => Ok(res.epoch), Message::StateQueryResponse(StateQueryResponse::Epochs( EpochsStateQueryResponse::Error(e), - )) => Err(anyhow::anyhow!( - "Internal server error while retrieving latest epoch: {e}" - )), - _ => Err(anyhow::anyhow!( - "Unexpected message type while retrieving latest epoch" + )) => Err(e), + _ => Err(QueryError::query_failed( + "Unexpected message type while retrieving latest epoch", )), }, ); @@ -480,7 +425,10 @@ async fn handle_pools_spo_blockfrost( Message::StateQueryResponse(StateQueryResponse::Accounts( AccountsStateQueryResponse::PoolLiveStake(res), )) => Ok(res), - _ => Err(anyhow::anyhow!("Unexpected message type")), + Message::StateQueryResponse(StateQueryResponse::Accounts( + AccountsStateQueryResponse::Error(e), + )) => Err(e), + _ => Err(QueryError::query_failed("Unexpected message type")), }, ); @@ -496,7 +444,10 @@ async fn handle_pools_spo_blockfrost( Message::StateQueryResponse(StateQueryResponse::Accounts( AccountsStateQueryResponse::OptimalPoolSizing(res), )) => Ok(res), - _ => Err(anyhow::anyhow!("Unexpected message type")), + Message::StateQueryResponse(StateQueryResponse::Accounts( + AccountsStateQueryResponse::Error(e), + )) => Err(e), + _ => Err(QueryError::query_failed("Unexpected message type")), }, ); @@ -515,12 +466,9 @@ async fn handle_pools_spo_blockfrost( PoolsStateQueryResponse::PoolUpdates(pool_updates), )) => Ok(Some(pool_updates)), Message::StateQueryResponse(StateQueryResponse::Pools( - PoolsStateQueryResponse::NotFound, - )) => Err(anyhow::anyhow!("Pool Not found")), - Message::StateQueryResponse(StateQueryResponse::Pools( - PoolsStateQueryResponse::Error(_e), + PoolsStateQueryResponse::Error(_), )) => Ok(None), - _ => Err(anyhow::anyhow!("Unexpected message type")), + _ => Err(QueryError::query_failed("Unexpected message type")), }, ); @@ -538,7 +486,10 @@ async fn handle_pools_spo_blockfrost( Message::StateQueryResponse(StateQueryResponse::Pools( PoolsStateQueryResponse::PoolTotalBlocksMinted(total_blocks_minted), )) => Ok(total_blocks_minted), - _ => Err(anyhow::anyhow!("Unexpected message type")), + Message::StateQueryResponse(StateQueryResponse::Pools( + PoolsStateQueryResponse::Error(e), + )) => Err(e), + _ => Err(QueryError::query_failed("Unexpected message type")), }, ); @@ -563,10 +514,11 @@ async fn handle_pools_spo_blockfrost( let live_stakes_info = live_stakes_info?; let total_blocks_minted = total_blocks_minted?; let Some(optimal_pool_sizing) = optimal_pool_sizing? else { - // if it is before Shelly Era - return Ok(RESTResponse::with_json(404, "Pool Not Found")); + // if it is before Shelley Era + return Err(RESTError::not_found("Pool not found")); }; let pool_updates = pool_updates?; + // TODO: Query TxHash from chainstore module for registrations and retirements let _registrations: Option> = pool_updates.as_ref().map(|updates| { updates @@ -607,7 +559,10 @@ async fn handle_pools_spo_blockfrost( Message::StateQueryResponse(StateQueryResponse::Epochs( EpochsStateQueryResponse::LatestEpochBlocksMintedByPool(blocks_minted), )) => Ok(blocks_minted), - _ => Err(anyhow::anyhow!("Unexpected message type")), + Message::StateQueryResponse(StateQueryResponse::Epochs( + EpochsStateQueryResponse::Error(e), + )) => Err(e), + _ => Err(QueryError::query_failed("Unexpected message type")), }, ); @@ -627,9 +582,9 @@ async fn handle_pools_spo_blockfrost( PoolsStateQueryResponse::PoolActiveStakeInfo(res), )) => Ok(Some(res)), Message::StateQueryResponse(StateQueryResponse::Pools( - PoolsStateQueryResponse::Error(_e), + PoolsStateQueryResponse::Error(_), )) => Ok(None), - _ => Err(anyhow::anyhow!("Unexpected message type")), + _ => Err(QueryError::query_failed("Unexpected message type")), }, ); @@ -651,10 +606,8 @@ async fn handle_pools_spo_blockfrost( )) => Ok(res), Message::StateQueryResponse(StateQueryResponse::Accounts( AccountsStateQueryResponse::Error(e), - )) => Err(anyhow::anyhow!( - "Internal server error while retrieving live pledge: {e}" - )), - _ => Err(anyhow::anyhow!("Unexpected message type")), + )) => Err(e), + _ => Err(QueryError::query_failed("Unexpected message type")), }, ); @@ -664,19 +617,22 @@ async fn handle_pools_spo_blockfrost( let active_stakes_info = active_stakes_info?; let live_pledge = live_pledge?; - let pool_id = pool_info.operator.to_bech32()?; - let reward_account = pool_info.reward_account.get_credential().to_stake_bech32(); - let Ok(reward_account) = reward_account else { - return Ok(RESTResponse::with_text(404, "Invalid Reward Account")); - }; - let pool_owners = pool_info + let pool_id = pool_info + .operator + .to_bech32() + .map_err(|e| RESTError::encoding_failed(&format!("pool ID: {}", e)))?; + let reward_account = pool_info + .reward_account + .get_credential() + .to_stake_bech32() + .map_err(|e| RESTError::encoding_failed(&format!("reward account: {}", e)))?; + let pool_owners: Vec = pool_info .pool_owners .iter() .map(|owner| owner.get_credential().to_stake_bech32()) - .collect::, _>>(); - let Ok(pool_owners) = pool_owners else { - return Ok(RESTResponse::with_text(404, "Invalid Pool Owners")); - }; + .collect::, _>>() + .map_err(|e| RESTError::encoding_failed(&format!("pool owners: {}", e)))?; + let pool_info_rest: PoolInfoRest = PoolInfoRest { pool_id, hex: *pool_info.operator, @@ -704,30 +660,23 @@ async fn handle_pools_spo_blockfrost( retirement: "TxHash lookup not yet implemented".to_string(), }; - match serde_json::to_string(&pool_info_rest) { - Ok(json) => Ok(RESTResponse::with_json(200, &json)), - Err(e) => Ok(RESTResponse::with_text( - 500, - &format!("Internal server error while retrieving pool info: {e}"), - )), - } + let json = serde_json::to_string(&pool_info_rest)?; + Ok(RESTResponse::with_json(200, &json)) } pub async fn handle_pool_history_blockfrost( context: Arc>, params: Vec, handlers_config: Arc, -) -> Result { - let Some(pool_id) = params.first() else { - return Ok(RESTResponse::with_text(400, "Missing pool ID parameter")); - }; +) -> Result { + let pool_id = params.first().ok_or_else(|| RESTError::param_missing("pool_id"))?; - let Ok(spo) = PoolId::from_bech32(pool_id) else { - return Ok(RESTResponse::with_text( - 400, - &format!("Invalid Bech32 stake pool ID: {pool_id}"), - )); - }; + let spo = PoolId::from_bech32(pool_id).map_err(|_| { + RESTError::invalid_param( + "pool_id", + &format!("Invalid Bech32 stake pool ID: {}", pool_id), + ) + })?; // get latest epoch from epochs-state let latest_epoch_info_msg = Arc::new(Message::StateQuery(StateQuery::Epochs( @@ -743,10 +692,8 @@ pub async fn handle_pool_history_blockfrost( )) => Ok(res.epoch), Message::StateQueryResponse(StateQueryResponse::Epochs( EpochsStateQueryResponse::Error(e), - )) => Err(anyhow::anyhow!( - "Internal server error while retrieving latest epoch: {e}" - )), - _ => Err(anyhow::anyhow!("Unexpected message type")), + )) => Err(e), + _ => Err(QueryError::query_failed("Unexpected message type")), }, ) .await?; @@ -765,12 +712,9 @@ pub async fn handle_pool_history_blockfrost( PoolsStateQueryResponse::PoolHistory(pool_history), )) => Ok(pool_history.into_iter().map(|state| state.into()).collect()), Message::StateQueryResponse(StateQueryResponse::Pools( - PoolsStateQueryResponse::Error(_e), - )) => { - // when pool epoch history is not enabled - Err(anyhow::anyhow!("Pool Epoch History is not enabled.")) - } - _ => Err(anyhow::anyhow!("Unexpected message type")), + PoolsStateQueryResponse::Error(e), + )) => Err(e), + _ => Err(QueryError::query_failed("Unexpected message type")), }, ) .await?; @@ -778,30 +722,23 @@ pub async fn handle_pool_history_blockfrost( // remove epoch state whose epoch is greater than or equal to latest_epoch pool_history.retain(|state| state.epoch < latest_epoch); - match serde_json::to_string(&pool_history) { - Ok(json) => Ok(RESTResponse::with_json(200, &json)), - Err(e) => Ok(RESTResponse::with_text( - 500, - &format!("Internal server error while retrieving pool history: {e}"), - )), - } + let json = serde_json::to_string(&pool_history)?; + Ok(RESTResponse::with_json(200, &json)) } pub async fn handle_pool_metadata_blockfrost( context: Arc>, params: Vec, handlers_config: Arc, -) -> Result { - let Some(pool_id) = params.first() else { - return Ok(RESTResponse::with_text(400, "Missing pool ID parameter")); - }; +) -> Result { + let pool_id = params.first().ok_or_else(|| RESTError::param_missing("pool_id"))?; - let Ok(spo) = PoolId::from_bech32(pool_id) else { - return Ok(RESTResponse::with_text( - 400, - &format!("Invalid Bech32 stake pool ID: {pool_id}"), - )); - }; + let spo = PoolId::from_bech32(pool_id).map_err(|_| { + RESTError::invalid_param( + "pool_id", + &format!("Invalid Bech32 stake pool ID: {}", pool_id), + ) + })?; let pool_metadata_msg = Arc::new(Message::StateQuery(StateQuery::Pools( PoolsStateQuery::GetPoolMetadata { pool_id: spo }, @@ -814,15 +751,10 @@ pub async fn handle_pool_metadata_blockfrost( Message::StateQueryResponse(StateQueryResponse::Pools( PoolsStateQueryResponse::PoolMetadata(pool_metadata), )) => Ok(pool_metadata), - Message::StateQueryResponse(StateQueryResponse::Pools( - PoolsStateQueryResponse::NotFound, - )) => Err(anyhow::anyhow!("Not found")), Message::StateQueryResponse(StateQueryResponse::Pools( PoolsStateQueryResponse::Error(e), - )) => Err(anyhow::anyhow!( - "Internal server error while retrieving pool metadata: {e}" - )), - _ => Err(anyhow::anyhow!("Unexpected message type")), + )) => Err(e), + _ => Err(QueryError::query_failed("Unexpected message type")), }, ) .await?; @@ -831,20 +763,16 @@ pub async fn handle_pool_metadata_blockfrost( pool_metadata.url.clone(), Duration::from_secs(handlers_config.external_api_timeout), ) - .await?; + .await + .map_err(|e| RESTError::query_failed(&format!("Failed to fetch pool metadata: {}", e)))?; - // Verify hash of the fetched pool metadata, matches with the metadata hash provided by PoolRegistration - if let Err(e) = verify_pool_metadata_hash(&pool_metadata_bytes, &pool_metadata.hash) { - return Ok(RESTResponse::with_text(404, &e)); - } + // Verify hash of the fetched pool metadata + verify_pool_metadata_hash(&pool_metadata_bytes, &pool_metadata.hash) + .map_err(|e| RESTError::not_found(&e))?; - // Convert bytes into an understandable PoolMetadata structure - let Ok(pool_metadata_json) = PoolMetadataJson::try_from(pool_metadata_bytes) else { - return Ok(RESTResponse::with_text( - 400, - "Failed PoolMetadata Json conversion", - )); - }; + // Convert bytes into PoolMetadata structure + let pool_metadata_json = PoolMetadataJson::try_from(pool_metadata_bytes) + .map_err(|_| RESTError::BadRequest("Failed PoolMetadata Json conversion".to_string()))?; let pool_metadata_rest = PoolMetadataRest { pool_id: pool_id.to_string(), @@ -857,30 +785,23 @@ pub async fn handle_pool_metadata_blockfrost( homepage: pool_metadata_json.homepage, }; - match serde_json::to_string(&pool_metadata_rest) { - Ok(json) => Ok(RESTResponse::with_json(200, &json)), - Err(e) => Ok(RESTResponse::with_text( - 500, - &format!("Internal server error while retrieving pool metadata: {e}"), - )), - } + let json = serde_json::to_string(&pool_metadata_rest)?; + Ok(RESTResponse::with_json(200, &json)) } pub async fn handle_pool_relays_blockfrost( context: Arc>, params: Vec, handlers_config: Arc, -) -> Result { - let Some(pool_id) = params.first() else { - return Ok(RESTResponse::with_text(400, "Missing pool ID parameter")); - }; +) -> Result { + let pool_id = params.first().ok_or_else(|| RESTError::param_missing("pool_id"))?; - let Ok(spo) = PoolId::from_bech32(pool_id) else { - return Ok(RESTResponse::with_text( - 400, - &format!("Invalid Bech32 stake pool ID: {pool_id}"), - )); - }; + let spo = PoolId::from_bech32(pool_id).map_err(|_| { + RESTError::invalid_param( + "pool_id", + &format!("Invalid Bech32 stake pool ID: {}", pool_id), + ) + })?; let pool_relay_msg = Arc::new(Message::StateQuery(StateQuery::Pools( PoolsStateQuery::GetPoolRelays { pool_id: spo }, @@ -894,45 +815,33 @@ pub async fn handle_pool_relays_blockfrost( Message::StateQueryResponse(StateQueryResponse::Pools( PoolsStateQueryResponse::PoolRelays(pool_relays), )) => Ok(pool_relays), - Message::StateQueryResponse(StateQueryResponse::Pools( - PoolsStateQueryResponse::NotFound, - )) => Err(anyhow::anyhow!("Pool Relays Not found")), Message::StateQueryResponse(StateQueryResponse::Pools( PoolsStateQueryResponse::Error(e), - )) => Err(anyhow::anyhow!( - "Internal server error while retrieving pool relays: {e}" - )), - _ => Err(anyhow::anyhow!("Unexpected message type")), + )) => Err(e), + _ => Err(QueryError::query_failed("Unexpected message type")), }, ) .await?; let relays_in_rest = pool_relays.into_iter().map(|r| r.into()).collect::>(); - match serde_json::to_string(&relays_in_rest) { - Ok(json) => Ok(RESTResponse::with_json(200, &json)), - Err(e) => Ok(RESTResponse::with_text( - 500, - &format!("Internal server error while retrieving pool relays: {e}"), - )), - } + let json = serde_json::to_string(&relays_in_rest)?; + Ok(RESTResponse::with_json(200, &json)) } pub async fn handle_pool_delegators_blockfrost( context: Arc>, params: Vec, handlers_config: Arc, -) -> Result { - let Some(pool_id) = params.first() else { - return Ok(RESTResponse::with_text(400, "Missing pool ID parameter")); - }; +) -> Result { + let pool_id = params.first().ok_or_else(|| RESTError::param_missing("pool_id"))?; - let Ok(spo) = PoolId::from_bech32(pool_id) else { - return Ok(RESTResponse::with_text( - 400, - &format!("Invalid Bech32 stake pool ID: {pool_id}"), - )); - }; + let spo = PoolId::from_bech32(pool_id).map_err(|_| { + RESTError::invalid_param( + "pool_id", + &format!("Invalid Bech32 stake pool ID: {}", pool_id), + ) + })?; // Get Pool delegators from spo-state let pool_delegators_msg = Arc::new(Message::StateQuery(StateQuery::Pools( @@ -948,16 +857,13 @@ pub async fn handle_pool_delegators_blockfrost( PoolsStateQueryResponse::PoolDelegators(pool_delegators), )) => Ok(Some(pool_delegators.delegators)), Message::StateQueryResponse(StateQueryResponse::Pools( - PoolsStateQueryResponse::NotFound, - )) => Err(anyhow::anyhow!("Pool Delegators Not found")), - Message::StateQueryResponse(StateQueryResponse::Pools( - PoolsStateQueryResponse::Error(_e), + PoolsStateQueryResponse::Error(_), )) => { // store-stake-addresses is not enabled warn!("Fallback to query from accounts_state"); Ok(None) } - _ => Err(anyhow::anyhow!("Unexpected message type")), + _ => Err(QueryError::query_failed("Unexpected message type")), }, ) .await?; @@ -970,7 +876,7 @@ pub async fn handle_pool_delegators_blockfrost( let pool_delegators_msg = Arc::new(Message::StateQuery(StateQuery::Accounts( AccountsStateQuery::GetPoolDelegators { pool_operator: spo }, ))); - let pool_delegators = query_state( + query_state( &context, &handlers_config.accounts_query_topic, pool_delegators_msg, @@ -980,52 +886,43 @@ pub async fn handle_pool_delegators_blockfrost( )) => Ok(pool_delegators.delegators), Message::StateQueryResponse(StateQueryResponse::Accounts( AccountsStateQueryResponse::Error(e), - )) => Err(anyhow::anyhow!( - "Error while retrieving pool delegators from accounts_state: {e}" - )), - _ => Err(anyhow::anyhow!("Unexpected message type")), + )) => Err(e), + _ => Err(QueryError::query_failed("Unexpected message type")), }, ) - .await?; - pool_delegators + .await? } }; - let mut delegators_rest = Vec::::new(); - for (stake_address, l) in pool_delegators { - let bech32 = stake_address - .to_string() - .map_err(|e| anyhow::anyhow!("Invalid stake address in pool delegators: {e}"))?; - delegators_rest.push(PoolDelegatorRest { - address: bech32, - live_stake: l.to_string(), - }); - } + let delegators_rest: Vec = pool_delegators + .into_iter() + .map(|(stake_address, l)| { + Ok(PoolDelegatorRest { + address: stake_address + .to_string() + .map_err(|e| RESTError::encoding_failed(&format!("stake address: {}", e)))?, + live_stake: l.to_string(), + }) + }) + .collect::, RESTError>>()?; - match serde_json::to_string(&delegators_rest) { - Ok(json) => Ok(RESTResponse::with_json(200, &json)), - Err(e) => Ok(RESTResponse::with_text( - 500, - &format!("Internal server error while retrieving pool delegators: {e}"), - )), - } + let json = serde_json::to_string(&delegators_rest)?; + Ok(RESTResponse::with_json(200, &json)) } pub async fn handle_pool_blocks_blockfrost( context: Arc>, params: Vec, handlers_config: Arc, -) -> Result { - let Some(pool_id) = params.first() else { - return Ok(RESTResponse::with_text(400, "Missing pool ID parameter")); - }; +) -> Result { + let pool_id = params.first().ok_or_else(|| RESTError::param_missing("pool_id"))?; - let Ok(spo) = PoolId::from_bech32(pool_id) else { - return Ok(RESTResponse::with_text( - 400, - &format!("Invalid Bech32 stake pool ID: {pool_id}"), - )); - }; + let spo = PoolId::from_bech32(pool_id).map_err(|_| { + RESTError::invalid_param( + "pool_id", + &format!("Invalid Bech32 stake pool ID: {}", pool_id), + ) + })?; // Get blocks by pool_id from spo_state let pool_blocks_msg = Arc::new(Message::StateQuery(StateQuery::Pools( @@ -1041,9 +938,9 @@ pub async fn handle_pool_blocks_blockfrost( PoolsStateQueryResponse::BlocksByPool(pool_blocks), )) => Ok(pool_blocks), Message::StateQueryResponse(StateQueryResponse::Pools( - PoolsStateQueryResponse::Error(_), - )) => Err(anyhow::anyhow!("Blocks are not enabled")), - _ => Err(anyhow::anyhow!("Unexpected message type")), + PoolsStateQueryResponse::Error(e), + )) => Err(e), + _ => Err(QueryError::query_failed("Unexpected message type")), }, ) .await?; @@ -1052,30 +949,23 @@ pub async fn handle_pool_blocks_blockfrost( // Need to query chain_store // to get block_hash for each block height - match serde_json::to_string_pretty(&pool_blocks) { - Ok(json) => Ok(RESTResponse::with_json(200, &json)), - Err(e) => Ok(RESTResponse::with_text( - 500, - &format!("Internal server error while retrieving pool blocks: {e}"), - )), - } + let json = serde_json::to_string_pretty(&pool_blocks)?; + Ok(RESTResponse::with_json(200, &json)) } pub async fn handle_pool_updates_blockfrost( context: Arc>, params: Vec, handlers_config: Arc, -) -> Result { - let Some(pool_id) = params.first() else { - return Ok(RESTResponse::with_text(400, "Missing pool ID parameter")); - }; +) -> Result { + let pool_id = params.first().ok_or_else(|| RESTError::param_missing("pool_id"))?; - let Ok(spo) = PoolId::from_bech32(pool_id) else { - return Ok(RESTResponse::with_text( - 400, - &format!("Invalid Bech32 stake pool ID: {pool_id}"), - )); - }; + let spo = PoolId::from_bech32(pool_id).map_err(|_| { + RESTError::invalid_param( + "pool_id", + &format!("Invalid Bech32 stake pool ID: {}", pool_id), + ) + })?; // query from spo_state let pool_updates_msg = Arc::new(Message::StateQuery(StateQuery::Pools( @@ -1089,18 +979,14 @@ pub async fn handle_pool_updates_blockfrost( Message::StateQueryResponse(StateQueryResponse::Pools( PoolsStateQueryResponse::PoolUpdates(pool_updates), )) => Ok(pool_updates), - Message::StateQueryResponse(StateQueryResponse::Pools( - PoolsStateQueryResponse::NotFound, - )) => Err(anyhow::anyhow!("Pool not found")), Message::StateQueryResponse(StateQueryResponse::Pools( PoolsStateQueryResponse::Error(e), - )) => Err(anyhow::anyhow!( - "Internal server error while retrieving pool updates: {e}" - )), - _ => Err(anyhow::anyhow!("Unexpected message type")), + )) => Err(e), + _ => Err(QueryError::query_failed("Unexpected message type")), }, ) .await?; + let pool_updates_rest = pool_updates .into_iter() .map(|u| PoolUpdateEventRest { @@ -1110,30 +996,23 @@ pub async fn handle_pool_updates_blockfrost( }) .collect::>(); - match serde_json::to_string(&pool_updates_rest) { - Ok(json) => Ok(RESTResponse::with_json(200, &json)), - Err(e) => Ok(RESTResponse::with_text( - 500, - &format!("Internal server error while retrieving pool updates: {e}"), - )), - } + let json = serde_json::to_string(&pool_updates_rest)?; + Ok(RESTResponse::with_json(200, &json)) } pub async fn handle_pool_votes_blockfrost( context: Arc>, params: Vec, handlers_config: Arc, -) -> Result { - let Some(pool_id) = params.first() else { - return Ok(RESTResponse::with_text(400, "Missing pool ID parameter")); - }; +) -> Result { + let pool_id = params.first().ok_or_else(|| RESTError::param_missing("pool_id"))?; - let Ok(spo) = PoolId::from_bech32(pool_id) else { - return Ok(RESTResponse::with_text( - 400, - &format!("Invalid Bech32 stake pool ID: {pool_id}"), - )); - }; + let spo = PoolId::from_bech32(pool_id).map_err(|_| { + RESTError::invalid_param( + "pool_id", + &format!("Invalid Bech32 stake pool ID: {}", pool_id), + ) + })?; // query from spo_state let pool_votes_msg = Arc::new(Message::StateQuery(StateQuery::Pools( @@ -1149,10 +1028,8 @@ pub async fn handle_pool_votes_blockfrost( )) => Ok(pool_votes), Message::StateQueryResponse(StateQueryResponse::Pools( PoolsStateQueryResponse::Error(e), - )) => Err(anyhow::anyhow!( - "Internal server error while retrieving pool votes: {e}" - )), - _ => Err(anyhow::anyhow!("Unexpected message type")), + )) => Err(e), + _ => Err(QueryError::query_failed("Unexpected message type")), }, ) .await?; @@ -1166,11 +1043,6 @@ pub async fn handle_pool_votes_blockfrost( }) .collect::>(); - match serde_json::to_string(&pool_votes_rest) { - Ok(json) => Ok(RESTResponse::with_json(200, &json)), - Err(e) => Ok(RESTResponse::with_text( - 500, - &format!("Internal server error while retrieving pool votes: {e}"), - )), - } + let json = serde_json::to_string(&pool_votes_rest)?; + Ok(RESTResponse::with_json(200, &json)) } diff --git a/modules/rest_blockfrost/src/rest_blockfrost.rs b/modules/rest_blockfrost/src/rest_blockfrost.rs index 2ae0137c..4744455e 100644 --- a/modules/rest_blockfrost/src/rest_blockfrost.rs +++ b/modules/rest_blockfrost/src/rest_blockfrost.rs @@ -2,6 +2,7 @@ use std::{collections::HashMap, future::Future, sync::Arc}; +use acropolis_common::rest_error::RESTError; use acropolis_common::{ messages::{Message, RESTResponse}, rest_helper::{handle_rest_with_path_and_query_parameters, handle_rest_with_path_parameter}, @@ -10,7 +11,6 @@ use anyhow::Result; use caryatid_sdk::{module, Context, Module}; use config::Config; use tracing::info; -use acropolis_common::rest_error::RESTError; mod cost_models; mod handlers; diff --git a/modules/spdd_state/src/rest.rs b/modules/spdd_state/src/rest.rs new file mode 100644 index 00000000..adc0d21c --- /dev/null +++ b/modules/spdd_state/src/rest.rs @@ -0,0 +1,47 @@ +use crate::state::State; +use acropolis_common::rest_error::RESTError; +use acropolis_common::serialization::Bech32Conversion; +use acropolis_common::DelegatedStake; +use acropolis_common::{extract_strict_query_params, messages::RESTResponse}; +use anyhow::Result; +use std::{collections::HashMap, sync::Arc}; +use tokio::sync::Mutex; + +/// Handles /spdd +pub async fn handle_spdd( + state: Arc>, + params: HashMap, +) -> Result { + let locked = state.lock().await; + + extract_strict_query_params!(params, { + "epoch" => epoch: Option, + }); + + let spdd_opt = match epoch { + Some(epoch) => match locked.get_epoch(epoch) { + Some(spdd) => Some(spdd), + None => { + return Ok(RESTResponse::with_text( + 404, + &format!("SPDD not found for epoch {}", epoch), + )); + } + }, + None => locked.get_latest(), + }; + + if let Some(spdd) = spdd_opt { + let spdd: HashMap = spdd + .iter() + .map(|(k, v)| (k.to_bech32().unwrap_or_else(|_| hex::encode(k)), *v)) + .collect(); + + match serde_json::to_string(&spdd) { + Ok(body) => Ok(RESTResponse::with_json(200, &body)), + Err(e) => Err(RESTError::from(e)), + } + } else { + Ok(RESTResponse::with_json(200, "{}")) + } +} diff --git a/modules/spdd_state/src/spdd_state.rs b/modules/spdd_state/src/spdd_state.rs index 4f8cd77d..b43c2cb1 100644 --- a/modules/spdd_state/src/spdd_state.rs +++ b/modules/spdd_state/src/spdd_state.rs @@ -1,5 +1,6 @@ //! Acropolis SPDD state module for Caryatid //! Stores historical stake pool delegation distributions +use acropolis_common::queries::errors::QueryError; use acropolis_common::{ messages::{CardanoMessage, Message, StateQuery, StateQueryResponse}, queries::spdd::{SPDDStateQuery, SPDDStateQueryResponse, DEFAULT_SPDD_QUERY_TOPIC}, @@ -11,6 +12,7 @@ use config::Config; use std::sync::Arc; use tokio::sync::Mutex; use tracing::{error, info, info_span, Instrument}; + mod state; use state::State; mod rest; @@ -53,6 +55,12 @@ impl SPDDState { let state_opt = if store_spdd { let state = Arc::new(Mutex::new(State::new())); + // Register /spdd REST endpoint + let state_rest = state.clone(); + handle_rest_with_query_parameters(context.clone(), &handle_spdd_topic, move |params| { + handle_spdd(state_rest.clone(), params) + }); + // Subscribe for spdd messages from accounts_state let state_handler = state.clone(); let mut message_subscription = context.subscribe(&subscribe_topic).await?; @@ -118,12 +126,14 @@ impl SPDDState { async move { let Message::StateQuery(StateQuery::SPDD(query)) = message.as_ref() else { return Arc::new(Message::StateQueryResponse(StateQueryResponse::SPDD( - SPDDStateQueryResponse::Error("Invalid message for spdd-state".into()), + SPDDStateQueryResponse::Error(QueryError::query_failed( + "Invalid message for spdd-state", + )), ))); }; let Some(state) = state else { return Arc::new(Message::StateQueryResponse(StateQueryResponse::SPDD( - SPDDStateQueryResponse::Error("SPDD storage is NOT enabled".into()), + SPDDStateQueryResponse::Error(QueryError::storage_disabled("SPDD")), ))); }; let state = state.lock().await; diff --git a/modules/spo_state/src/spo_state.rs b/modules/spo_state/src/spo_state.rs index 5ae1b45d..08ab8438 100644 --- a/modules/spo_state/src/spo_state.rs +++ b/modules/spo_state/src/spo_state.rs @@ -1,6 +1,7 @@ //! Acropolis SPO state module for Caryatid //! Accepts certificate events and derives the SPO state in memory +use acropolis_common::queries::errors::QueryError; use acropolis_common::{ ledger_state::SPOState as LedgerSPOState, messages::{ @@ -498,7 +499,9 @@ impl SPOState { async move { let Message::StateQuery(StateQuery::Pools(query)) = message.as_ref() else { return Arc::new(Message::StateQueryResponse(StateQueryResponse::Pools( - PoolsStateQueryResponse::Error("Invalid message for pools-state".into()), + PoolsStateQueryResponse::Error(QueryError::invalid_request( + "Invalid message for pools-state", + )), ))); }; @@ -507,14 +510,15 @@ impl SPOState { let response = match query { // NOTE: // For now, we only store active pools - // But we need to store retired pool's information also + // But we need to store retired pool's information also // for BF's compatibility - PoolsStateQuery::GetPoolInfo { pool_id } => { - match state.get(pool_id) { - Some(pool) => PoolsStateQueryResponse::PoolInfo(pool.clone()), - None => PoolsStateQueryResponse::NotFound, - } - } + PoolsStateQuery::GetPoolInfo { pool_id } => match state.get(pool_id) { + Some(pool) => PoolsStateQueryResponse::PoolInfo(pool.clone()), + None => PoolsStateQueryResponse::Error(QueryError::not_found(&format!( + "Pool {}", + pool_id + ))), + }, PoolsStateQuery::GetPoolsList => { PoolsStateQueryResponse::PoolsList(state.list_pool_operators()) @@ -544,7 +548,9 @@ impl SPOState { .unwrap_or(RationalNumber::from(0)), }) } else { - PoolsStateQueryResponse::Error("Epochs history is not enabled".into()) + PoolsStateQueryResponse::Error(QueryError::storage_disabled( + "epochs history", + )) } } @@ -559,14 +565,16 @@ impl SPOState { active_stakes.unwrap_or(vec![0; pools_operators.len()]), ) } else { - PoolsStateQueryResponse::Error("Epochs history is not enabled".into()) + PoolsStateQueryResponse::Error(QueryError::storage_disabled( + "epochs history", + )) } } - PoolsStateQuery::GetPoolsTotalBlocksMinted { - pools_operators - } => { - PoolsStateQueryResponse::PoolsTotalBlocksMinted(state.get_total_blocks_minted_by_pools(pools_operators)) + PoolsStateQuery::GetPoolsTotalBlocksMinted { pools_operators } => { + PoolsStateQueryResponse::PoolsTotalBlocksMinted( + state.get_total_blocks_minted_by_pools(pools_operators), + ) } PoolsStateQuery::GetPoolHistory { pool_id } => { @@ -575,9 +583,9 @@ impl SPOState { epochs_history.get_pool_history(pool_id).unwrap_or_default(); PoolsStateQueryResponse::PoolHistory(history) } else { - PoolsStateQueryResponse::Error( - "Pool Epoch history is not enabled".into(), - ) + PoolsStateQueryResponse::Error(QueryError::storage_disabled( + "pool epoch history", + )) } } @@ -591,9 +599,9 @@ impl SPOState { let retired_pools = retired_pools_history.get_retired_pools(); PoolsStateQueryResponse::PoolsRetiredList(retired_pools) } else { - PoolsStateQueryResponse::Error( - "Pool retirement history is not enabled".into(), - ) + PoolsStateQueryResponse::Error(QueryError::storage_disabled( + "pool retirement history", + )) } } @@ -602,7 +610,10 @@ impl SPOState { if let Some(pool_metadata) = pool_metadata { PoolsStateQueryResponse::PoolMetadata(pool_metadata) } else { - PoolsStateQueryResponse::NotFound + PoolsStateQueryResponse::Error(QueryError::not_found(&format!( + "Pool metadata for {}", + pool_id + ))) } } @@ -611,42 +622,64 @@ impl SPOState { if let Some(relays) = pool_relays { PoolsStateQueryResponse::PoolRelays(relays) } else { - PoolsStateQueryResponse::NotFound + PoolsStateQueryResponse::Error(QueryError::not_found(&format!( + "Pool relays for {}", + pool_id + ))) } } PoolsStateQuery::GetPoolDelegators { pool_id } => { - if state.is_historical_delegators_enabled() && state.is_stake_address_enabled() { + if state.is_historical_delegators_enabled() + && state.is_stake_address_enabled() + { let pool_delegators = state.get_pool_delegators(pool_id); if let Some(pool_delegators) = pool_delegators { PoolsStateQueryResponse::PoolDelegators(PoolDelegators { delegators: pool_delegators, }) } else { - PoolsStateQueryResponse::NotFound + PoolsStateQueryResponse::Error(QueryError::not_found(&format!( + "Pool delegators for {}", + pool_id + ))) } } else { - PoolsStateQueryResponse::Error("Pool delegators are not enabled or stake addresses are not enabled".into()) + PoolsStateQueryResponse::Error(QueryError::storage_disabled( + "pool delegators or stake addresses", + )) } } PoolsStateQuery::GetPoolTotalBlocksMinted { pool_id } => { - PoolsStateQueryResponse::PoolTotalBlocksMinted(state.get_total_blocks_minted_by_pool(pool_id)) + PoolsStateQueryResponse::PoolTotalBlocksMinted( + state.get_total_blocks_minted_by_pool(pool_id), + ) } PoolsStateQuery::GetBlocksByPool { pool_id } => { if state.is_historical_blocks_enabled() { - PoolsStateQueryResponse::BlocksByPool(state.get_blocks_by_pool(pool_id).unwrap_or_default()) + PoolsStateQueryResponse::BlocksByPool( + state.get_blocks_by_pool(pool_id).unwrap_or_default(), + ) } else { - PoolsStateQueryResponse::Error("Blocks are not enabled".into()) + PoolsStateQueryResponse::Error(QueryError::storage_disabled( + "historical blocks", + )) } } PoolsStateQuery::GetBlocksByPoolAndEpoch { pool_id, epoch } => { if state.is_historical_blocks_enabled() { - PoolsStateQueryResponse::BlocksByPoolAndEpoch(state.get_blocks_by_pool_and_epoch(pool_id, *epoch).unwrap_or_default()) + PoolsStateQueryResponse::BlocksByPoolAndEpoch( + state + .get_blocks_by_pool_and_epoch(pool_id, *epoch) + .unwrap_or_default(), + ) } else { - PoolsStateQueryResponse::Error("Blocks are not enabled".into()) + PoolsStateQueryResponse::Error(QueryError::storage_disabled( + "historical blocks", + )) } } @@ -656,18 +689,27 @@ impl SPOState { if let Some(pool_updates) = pool_updates { PoolsStateQueryResponse::PoolUpdates(pool_updates) } else { - PoolsStateQueryResponse::NotFound + PoolsStateQueryResponse::Error(QueryError::not_found(&format!( + "Pool updates for {}", + pool_id + ))) } } else { - PoolsStateQueryResponse::Error("Pool updates are not enabled".into()) + PoolsStateQueryResponse::Error(QueryError::storage_disabled( + "pool updates", + )) } } PoolsStateQuery::GetPoolVotes { pool_id } => { if state.is_historical_votes_enabled() { - PoolsStateQueryResponse::PoolVotes(state.get_pool_votes(pool_id).unwrap_or_default()) + PoolsStateQueryResponse::PoolVotes( + state.get_pool_votes(pool_id).unwrap_or_default(), + ) } else { - PoolsStateQueryResponse::Error("Pool votes are not enabled".into()) + PoolsStateQueryResponse::Error(QueryError::storage_disabled( + "pool votes", + )) } } }; diff --git a/modules/utxo_state/src/utxo_state.rs b/modules/utxo_state/src/utxo_state.rs index 842f2458..4a67514d 100644 --- a/modules/utxo_state/src/utxo_state.rs +++ b/modules/utxo_state/src/utxo_state.rs @@ -7,6 +7,7 @@ use acropolis_common::{ }; use caryatid_sdk::{module, Context, Module}; +use acropolis_common::queries::errors::QueryError; use anyhow::{anyhow, Result}; use config::Config; use std::sync::Arc; @@ -112,7 +113,9 @@ impl UTXOState { async move { let Message::StateQuery(StateQuery::UTxOs(query)) = message.as_ref() else { return Arc::new(Message::StateQueryResponse(StateQueryResponse::UTxOs( - UTxOStateQueryResponse::Error("Invalid message for utxo-state".into()), + UTxOStateQueryResponse::Error(QueryError::query_failed( + "Invalid message for utxo-state", + )), ))); }; @@ -121,7 +124,9 @@ impl UTXOState { UTxOStateQuery::GetUTxOsSum { utxo_identifiers } => { match state.get_utxos_sum(utxo_identifiers).await { Ok(balance) => UTxOStateQueryResponse::UTxOsSum(balance), - Err(e) => UTxOStateQueryResponse::Error(e.to_string()), + Err(e) => UTxOStateQueryResponse::Error(QueryError::query_failed( + format!("Fetching UTxO sum failed: {}", e.to_string()), + )), } } }; From 4b07f5918721f4e5802be17413b9a175e374d060 Mon Sep 17 00:00:00 2001 From: Matthew Hounslow Date: Wed, 5 Nov 2025 15:35:28 -0800 Subject: [PATCH 09/20] Refactor: Simplify `QueryError` variants, remove unused `PartialNotFound` and `QueryFailed`, standardize error handling with `internal_error`, and update method signatures across modules --- common/src/queries/errors.rs | 18 ++----- common/src/queries/utils.rs | 11 ++-- common/src/rest_error.rs | 38 ++++--------- modules/accounts_state/src/accounts_state.rs | 14 ++--- modules/address_state/src/address_state.rs | 16 +++--- modules/assets_state/src/assets_state.rs | 12 ++--- modules/chain_store/src/chain_store.rs | 4 +- modules/drep_state/src/drep_state.rs | 24 ++++----- modules/epochs_state/src/epochs_state.rs | 2 +- .../governance_state/src/governance_state.rs | 2 +- .../src/historical_accounts_state.rs | 12 ++--- .../parameters_state/src/parameters_state.rs | 2 +- .../rest_blockfrost/src/handlers/accounts.rs | 16 +++--- .../rest_blockfrost/src/handlers/addresses.rs | 4 +- .../rest_blockfrost/src/handlers/assets.rs | 12 ++--- .../rest_blockfrost/src/handlers/epochs.rs | 24 ++++----- .../src/handlers/governance.rs | 22 ++++---- modules/rest_blockfrost/src/handlers/pools.rs | 54 +++++++++---------- modules/spdd_state/src/spdd_state.rs | 2 +- modules/utxo_state/src/utxo_state.rs | 4 +- 20 files changed, 133 insertions(+), 160 deletions(-) diff --git a/common/src/queries/errors.rs b/common/src/queries/errors.rs index 1cdd21c3..9b4879df 100644 --- a/common/src/queries/errors.rs +++ b/common/src/queries/errors.rs @@ -8,7 +8,7 @@ pub enum QueryError { NotFound { resource: String }, /// An error occurred while processing the query - QueryFailed { message: String }, + Internal { message: String }, /// Storage backend is disabled in configuration StorageDisabled { storage_type: String }, @@ -16,9 +16,6 @@ pub enum QueryError { /// Invalid request parameters InvalidRequest { message: String }, - /// One or more resources in a batch query were not found - PartialNotFound { message: String }, - /// Query variant is not implemented yet NotImplemented { query: String }, } @@ -30,8 +27,8 @@ impl QueryError { } } - pub fn query_failed(message: impl Into) -> Self { - Self::QueryFailed { + pub fn internal_error(message: impl Into) -> Self { + Self::Internal { message: message.into(), } } @@ -48,12 +45,6 @@ impl QueryError { } } - pub fn partial_not_found(message: impl Into) -> Self { - Self::PartialNotFound { - message: message.into(), - } - } - pub fn not_implemented(query: impl Into) -> Self { Self::NotImplemented { query: query.into(), @@ -65,12 +56,11 @@ impl fmt::Display for QueryError { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { Self::NotFound { resource } => write!(f, "Not found: {}", resource), - Self::QueryFailed { message } => write!(f, "Query failed: {}", message), + Self::Internal { message } => write!(f, "Query failed: {}", message), Self::StorageDisabled { storage_type } => { write!(f, "{} storage is not enabled", storage_type) } Self::InvalidRequest { message } => write!(f, "Invalid request: {}", message), - Self::PartialNotFound { message } => write!(f, "Partial result: {}", message), Self::NotImplemented { query } => write!(f, "Query not implemented: {}", query), } } diff --git a/common/src/queries/utils.rs b/common/src/queries/utils.rs index b629e0d3..6e620da7 100644 --- a/common/src/queries/utils.rs +++ b/common/src/queries/utils.rs @@ -21,15 +21,14 @@ where .message_bus .request(topic, request_msg) .await - .map_err(|e| QueryError::query_failed(e.to_string()))?; + .map_err(|e| QueryError::internal_error(e.to_string()))?; let message = Arc::try_unwrap(raw_msg).unwrap_or_else(|arc| (*arc).clone()); extractor(message) } -/// Query state and return a REST response directly -/// This is a convenience function for simple handlers that just fetch and serialize data +/// The outer option in the extractor return value is whether the response was handled by F pub async fn rest_query_state( context: &Arc>, topic: &str, @@ -42,13 +41,13 @@ where { let data = query_state(context, topic, request_msg, |response| { extractor(response).unwrap_or_else(|| { - Err(QueryError::query_failed(format!( + Err(QueryError::internal_error(format!( "Unexpected response message type from {topic}" ))) }) }) - .await?; // QueryError auto-converts to RESTError via From trait + .await?; - let json = serde_json::to_string_pretty(&data)?; // Uses From for RESTError + let json = serde_json::to_string_pretty(&data)?; Ok(RESTResponse::with_json(200, &json)) } diff --git a/common/src/rest_error.rs b/common/src/rest_error.rs index 64f72eb6..25a21107 100644 --- a/common/src/rest_error.rs +++ b/common/src/rest_error.rs @@ -2,7 +2,6 @@ use crate::queries::errors::QueryError; use anyhow::Error as AnyhowError; use caryatid_module_rest_server::messages::RESTResponse; use std::fmt; -use std::num::ParseIntError; /// Standard error types for the application #[derive(Debug)] @@ -49,12 +48,12 @@ impl RESTError { RESTError::BadRequest("Invalid hex string".to_string()) } - /// Resource not found error (flexible message) + /// Resource not found error pub fn not_found(message: &str) -> Self { RESTError::NotFound(message.to_string()) } - /// Feature not implemented error (flexible message) + /// Feature not implemented error pub fn not_implemented(message: &str) -> Self { RESTError::NotImplemented(message.to_string()) } @@ -64,21 +63,16 @@ impl RESTError { RESTError::NotImplemented(format!("{} storage is disabled in config", storage_type)) } - /// Unexpected response error (flexible message) + /// Unexpected response error pub fn unexpected_response(message: &str) -> Self { RESTError::InternalServerError(message.to_string()) } - /// Query failed error (flexible message) + /// Query failed error pub fn query_failed(message: &str) -> Self { RESTError::InternalServerError(message.to_string()) } - /// Serialization failed error - pub fn serialization_failed(what: &str, error: impl fmt::Display) -> Self { - RESTError::InternalServerError(format!("Failed to serialize {}: {}", what, error)) - } - /// Encoding failed error pub fn encoding_failed(what: &str) -> Self { RESTError::InternalServerError(format!("Failed to encode {}", what)) @@ -107,12 +101,12 @@ impl From for RESTError { } } -/// Convert ParseIntError to RESTError (400 Bad Request) -impl From for RESTError { - fn from(error: ParseIntError) -> Self { - RESTError::BadRequest(error.to_string()) - } -} +// /// Convert ParseIntError to RESTError (400 Bad Request) +// impl From for RESTError { +// fn from(error: ParseIntError) -> Self { +// RESTError::BadRequest(error.to_string()) +// } +// } /// Convert hex decode errors to RESTError (400 Bad Request) impl From for RESTError { @@ -147,8 +141,7 @@ impl From for RESTError { fn from(error: QueryError) -> Self { match error { QueryError::NotFound { resource } => RESTError::NotFound(resource), - QueryError::PartialNotFound { message } => RESTError::BadRequest(message), - QueryError::QueryFailed { message } => RESTError::InternalServerError(message), + QueryError::Internal { message } => RESTError::InternalServerError(message), QueryError::StorageDisabled { storage_type } => { RESTError::storage_disabled(&storage_type) } @@ -158,8 +151,6 @@ impl From for RESTError { } } -pub type AppResult = Result; - #[cfg(test)] mod tests { use super::*; @@ -192,13 +183,6 @@ mod tests { assert_eq!(app_error.status_code(), 500); } - #[test] - fn test_from_parse_int_error() { - let result: Result = "not_a_number".parse(); - let app_error: RESTError = result.unwrap_err().into(); - assert_eq!(app_error.status_code(), 400); - } - #[test] fn test_from_hex_error() { let result = hex::decode("not_hex_gg"); diff --git a/modules/accounts_state/src/accounts_state.rs b/modules/accounts_state/src/accounts_state.rs index 76fa7551..1c0aed0e 100644 --- a/modules/accounts_state/src/accounts_state.rs +++ b/modules/accounts_state/src/accounts_state.rs @@ -552,7 +552,7 @@ impl AccountsState { Some(map) => { AccountsStateQueryResponse::AccountsDrepDelegationsMap(map) } - None => AccountsStateQueryResponse::Error(QueryError::query_failed( + None => AccountsStateQueryResponse::Error(QueryError::internal_error( "Error retrieving DRep delegations map", )), } @@ -568,7 +568,7 @@ impl AccountsState { match state.get_accounts_utxo_values_map(stake_addresses) { Some(map) => AccountsStateQueryResponse::AccountsUtxoValuesMap(map), None => AccountsStateQueryResponse::Error( - QueryError::partial_not_found("One or more accounts not found"), + QueryError::not_found("One or more accounts not found"), ), } } @@ -577,7 +577,7 @@ impl AccountsState { match state.get_accounts_utxo_values_sum(stake_addresses) { Some(sum) => AccountsStateQueryResponse::AccountsUtxoValuesSum(sum), None => AccountsStateQueryResponse::Error( - QueryError::partial_not_found("One or more accounts not found"), + QueryError::not_found("One or more accounts not found"), ), } } @@ -586,7 +586,7 @@ impl AccountsState { match state.get_accounts_balances_map(stake_addresses) { Some(map) => AccountsStateQueryResponse::AccountsBalancesMap(map), None => AccountsStateQueryResponse::Error( - QueryError::partial_not_found("One or more accounts not found"), + QueryError::not_found("One or more accounts not found"), ), } } @@ -601,7 +601,7 @@ impl AccountsState { match state.get_account_balances_sum(stake_addresses) { Some(sum) => AccountsStateQueryResponse::AccountsBalancesSum(sum), None => AccountsStateQueryResponse::Error( - QueryError::partial_not_found("One or more accounts not found"), + QueryError::not_found("One or more accounts not found"), ), } } @@ -609,7 +609,7 @@ impl AccountsState { AccountsStateQuery::GetSPDDByEpoch { epoch } => match spdd_store_guard { Some(spdd_store) => match spdd_store.query_by_epoch(*epoch) { Ok(result) => AccountsStateQueryResponse::SPDDByEpoch(result), - Err(e) => AccountsStateQueryResponse::Error(QueryError::query_failed( + Err(e) => AccountsStateQueryResponse::Error(QueryError::internal_error( e.to_string(), )), }, @@ -626,7 +626,7 @@ impl AccountsState { AccountsStateQueryResponse::SPDDByEpochAndPool(result) } Err(e) => AccountsStateQueryResponse::Error( - QueryError::query_failed(e.to_string()), + QueryError::internal_error(e.to_string()), ), } } diff --git a/modules/address_state/src/address_state.rs b/modules/address_state/src/address_state.rs index a56165e0..fd1e5979 100644 --- a/modules/address_state/src/address_state.rs +++ b/modules/address_state/src/address_state.rs @@ -4,11 +4,6 @@ use std::sync::Arc; -use crate::{ - immutable_address_store::ImmutableAddressStore, - state::{AddressStorageConfig, State}, -}; -use acropolis_common::queries::errors::QueryError; use acropolis_common::{ messages::{CardanoMessage, Message, StateQuery, StateQueryResponse}, queries::addresses::{ @@ -21,6 +16,11 @@ use caryatid_sdk::{module, Context, Module, Subscription}; use config::Config; use tokio::sync::{mpsc, Mutex}; use tracing::{error, info}; +use acropolis_common::queries::errors::QueryError; +use crate::{ + immutable_address_store::ImmutableAddressStore, + state::{AddressStorageConfig, State}, +}; mod immutable_address_store; mod state; mod volatile_addresses; @@ -213,7 +213,7 @@ impl AddressState { Ok(None) => AddressStateQueryResponse::Error(QueryError::not_found( "Address not found", )), - Err(e) => AddressStateQueryResponse::Error(QueryError::query_failed( + Err(e) => AddressStateQueryResponse::Error(QueryError::internal_error( e.to_string(), )), } @@ -224,7 +224,7 @@ impl AddressState { Ok(None) => AddressStateQueryResponse::Error(QueryError::not_found( "Address not found", )), - Err(e) => AddressStateQueryResponse::Error(QueryError::query_failed( + Err(e) => AddressStateQueryResponse::Error(QueryError::internal_error( e.to_string(), )), } @@ -232,7 +232,7 @@ impl AddressState { AddressStateQuery::GetAddressTotals { address } => { match state.get_address_totals(address).await { Ok(totals) => AddressStateQueryResponse::AddressTotals(totals), - Err(e) => AddressStateQueryResponse::Error(QueryError::query_failed( + Err(e) => AddressStateQueryResponse::Error(QueryError::internal_error( e.to_string(), )), } diff --git a/modules/assets_state/src/assets_state.rs b/modules/assets_state/src/assets_state.rs index 6605ab9a..8c6e401e 100644 --- a/modules/assets_state/src/assets_state.rs +++ b/modules/assets_state/src/assets_state.rs @@ -298,7 +298,7 @@ impl AssetsState { let reg = registry.lock().await; match state.get_assets_list(®) { Ok(list) => AssetsStateQueryResponse::AssetsList(list), - Err(e) => AssetsStateQueryResponse::Error(QueryError::query_failed( + Err(e) => AssetsStateQueryResponse::Error(QueryError::internal_error( e.to_string(), )), } @@ -316,7 +316,7 @@ impl AssetsState { ))) } Err(e) => AssetsStateQueryResponse::Error( - QueryError::query_failed(e.to_string()), + QueryError::internal_error(e.to_string()), ), }, None => { @@ -349,7 +349,7 @@ impl AssetsState { ))) } Err(e) => AssetsStateQueryResponse::Error( - QueryError::query_failed(e.to_string()), + QueryError::internal_error(e.to_string()), ), }, None => { @@ -382,7 +382,7 @@ impl AssetsState { ))) } Err(e) => AssetsStateQueryResponse::Error( - QueryError::query_failed(e.to_string()), + QueryError::internal_error(e.to_string()), ), }, None => { @@ -413,7 +413,7 @@ impl AssetsState { ))) } Err(e) => AssetsStateQueryResponse::Error( - QueryError::query_failed(e.to_string()), + QueryError::internal_error(e.to_string()), ), }, None => { @@ -438,7 +438,7 @@ impl AssetsState { Ok(None) => AssetsStateQueryResponse::Error(QueryError::not_found( format!("Assets for policy {}", hex::encode(policy)), )), - Err(e) => AssetsStateQueryResponse::Error(QueryError::query_failed( + Err(e) => AssetsStateQueryResponse::Error(QueryError::internal_error( e.to_string(), )), } diff --git a/modules/chain_store/src/chain_store.rs b/modules/chain_store/src/chain_store.rs index 9753e8c4..60c1ef16 100644 --- a/modules/chain_store/src/chain_store.rs +++ b/modules/chain_store/src/chain_store.rs @@ -73,14 +73,14 @@ impl ChainStore { }; let Some(state) = query_history.lock().await.current().cloned() else { return Arc::new(Message::StateQueryResponse(StateQueryResponse::Blocks( - BlocksStateQueryResponse::Error(QueryError::query_failed( + BlocksStateQueryResponse::Error(QueryError::internal_error( "Uninitialized state", )), ))); }; let res = Self::handle_blocks_query(&query_store, &state, query).unwrap_or_else(|err| { - BlocksStateQueryResponse::Error(QueryError::query_failed(err.to_string())) + BlocksStateQueryResponse::Error(QueryError::internal_error(err.to_string())) }); Arc::new(Message::StateQueryResponse(StateQueryResponse::Blocks(res))) } diff --git a/modules/drep_state/src/drep_state.rs b/modules/drep_state/src/drep_state.rs index b600ff96..16cc79bd 100644 --- a/modules/drep_state/src/drep_state.rs +++ b/modules/drep_state/src/drep_state.rs @@ -280,7 +280,7 @@ impl DRepState { let dreps = state.list(); GovernanceStateQueryResponse::DRepsList(DRepsList { dreps }) } - None => GovernanceStateQueryResponse::Error(QueryError::query_failed( + None => GovernanceStateQueryResponse::Error(QueryError::internal_error( "No current DRep state", )), }, @@ -313,7 +313,7 @@ impl DRepState { )), ), Err(msg) => GovernanceStateQueryResponse::Error( - QueryError::query_failed(msg), + QueryError::internal_error(msg), ), } } @@ -322,10 +322,10 @@ impl DRepState { QueryError::not_found(format!("DRep {:?}", drep_credential)), ), Err(msg) => GovernanceStateQueryResponse::Error( - QueryError::query_failed(msg), + QueryError::internal_error(msg), ), }, - None => GovernanceStateQueryResponse::Error(QueryError::query_failed( + None => GovernanceStateQueryResponse::Error(QueryError::internal_error( "No current state", )), } @@ -346,10 +346,10 @@ impl DRepState { )) } Err(msg) => GovernanceStateQueryResponse::Error( - QueryError::query_failed(msg), + QueryError::internal_error(msg), ), }, - None => GovernanceStateQueryResponse::Error(QueryError::query_failed( + None => GovernanceStateQueryResponse::Error(QueryError::internal_error( "No current state", )), } @@ -366,10 +366,10 @@ impl DRepState { )) } Err(msg) => GovernanceStateQueryResponse::Error( - QueryError::query_failed(msg), + QueryError::internal_error(msg), ), }, - None => GovernanceStateQueryResponse::Error(QueryError::query_failed( + None => GovernanceStateQueryResponse::Error(QueryError::internal_error( "No current state", )), } @@ -389,10 +389,10 @@ impl DRepState { )) } Err(msg) => GovernanceStateQueryResponse::Error( - QueryError::query_failed(msg), + QueryError::internal_error(msg), ), }, - None => GovernanceStateQueryResponse::Error(QueryError::query_failed( + None => GovernanceStateQueryResponse::Error(QueryError::internal_error( "No current state", )), } @@ -411,10 +411,10 @@ impl DRepState { )) } Err(msg) => GovernanceStateQueryResponse::Error( - QueryError::query_failed(msg), + QueryError::internal_error(msg), ), }, - None => GovernanceStateQueryResponse::Error(QueryError::query_failed( + None => GovernanceStateQueryResponse::Error(QueryError::internal_error( "No current state", )), } diff --git a/modules/epochs_state/src/epochs_state.rs b/modules/epochs_state/src/epochs_state.rs index 3ed96862..83ecbe26 100644 --- a/modules/epochs_state/src/epochs_state.rs +++ b/modules/epochs_state/src/epochs_state.rs @@ -260,7 +260,7 @@ impl EpochsState { async move { let Message::StateQuery(StateQuery::Epochs(query)) = message.as_ref() else { return Arc::new(Message::StateQueryResponse(StateQueryResponse::Epochs( - EpochsStateQueryResponse::Error(QueryError::query_failed( + EpochsStateQueryResponse::Error(QueryError::internal_error( "Invalid message for epochs-state", )), ))); diff --git a/modules/governance_state/src/governance_state.rs b/modules/governance_state/src/governance_state.rs index 58ee8839..2510be84 100644 --- a/modules/governance_state/src/governance_state.rs +++ b/modules/governance_state/src/governance_state.rs @@ -181,7 +181,7 @@ impl GovernanceState { async move { let Message::StateQuery(StateQuery::Governance(query)) = message.as_ref() else { return Arc::new(Message::StateQueryResponse(StateQueryResponse::Governance( - GovernanceStateQueryResponse::Error(QueryError::query_failed( + GovernanceStateQueryResponse::Error(QueryError::internal_error( "Invalid message for governance-state", )), ))); diff --git a/modules/historical_accounts_state/src/historical_accounts_state.rs b/modules/historical_accounts_state/src/historical_accounts_state.rs index 7435a605..a56e1d4f 100644 --- a/modules/historical_accounts_state/src/historical_accounts_state.rs +++ b/modules/historical_accounts_state/src/historical_accounts_state.rs @@ -319,7 +319,7 @@ impl HistoricalAccountsState { Ok(None) => AccountsStateQueryResponse::Error(QueryError::not_found( format!("Account {}", account), )), - Err(e) => AccountsStateQueryResponse::Error(QueryError::query_failed( + Err(e) => AccountsStateQueryResponse::Error(QueryError::internal_error( format!("Failed to get registration history: {}", e), )), } @@ -332,7 +332,7 @@ impl HistoricalAccountsState { Ok(None) => AccountsStateQueryResponse::Error(QueryError::not_found( format!("Account {}", account), )), - Err(e) => AccountsStateQueryResponse::Error(QueryError::query_failed( + Err(e) => AccountsStateQueryResponse::Error(QueryError::internal_error( format!("Failed to get delegation history: {}", e), )), } @@ -343,7 +343,7 @@ impl HistoricalAccountsState { Ok(None) => AccountsStateQueryResponse::Error(QueryError::not_found( format!("Account {}", account), )), - Err(e) => AccountsStateQueryResponse::Error(QueryError::query_failed( + Err(e) => AccountsStateQueryResponse::Error(QueryError::internal_error( format!("Failed to get MIR history: {}", e), )), } @@ -356,7 +356,7 @@ impl HistoricalAccountsState { Ok(None) => AccountsStateQueryResponse::Error(QueryError::not_found( format!("Account {}", account), )), - Err(e) => AccountsStateQueryResponse::Error(QueryError::query_failed( + Err(e) => AccountsStateQueryResponse::Error(QueryError::internal_error( format!("Failed to get withdrawal history: {}", e), )), } @@ -369,7 +369,7 @@ impl HistoricalAccountsState { Ok(None) => AccountsStateQueryResponse::Error(QueryError::not_found( format!("Account {}", account), )), - Err(e) => AccountsStateQueryResponse::Error(QueryError::query_failed( + Err(e) => AccountsStateQueryResponse::Error(QueryError::internal_error( format!("Failed to get reward history: {}", e), )), } @@ -382,7 +382,7 @@ impl HistoricalAccountsState { Ok(None) => AccountsStateQueryResponse::Error(QueryError::not_found( format!("Account {}", account), )), - Err(e) => AccountsStateQueryResponse::Error(QueryError::query_failed( + Err(e) => AccountsStateQueryResponse::Error(QueryError::internal_error( format!("Failed to get associated addresses: {}", e), )), } diff --git a/modules/parameters_state/src/parameters_state.rs b/modules/parameters_state/src/parameters_state.rs index 7d96e2db..a64eed7a 100644 --- a/modules/parameters_state/src/parameters_state.rs +++ b/modules/parameters_state/src/parameters_state.rs @@ -175,7 +175,7 @@ impl ParametersState { async move { let Message::StateQuery(StateQuery::Parameters(query)) = message.as_ref() else { return Arc::new(Message::StateQueryResponse(StateQueryResponse::Parameters( - ParametersStateQueryResponse::Error(QueryError::query_failed( + ParametersStateQueryResponse::Error(QueryError::internal_error( "Invalid message for parameters-state", )), ))); diff --git a/modules/rest_blockfrost/src/handlers/accounts.rs b/modules/rest_blockfrost/src/handlers/accounts.rs index 320c3bed..10a26987 100644 --- a/modules/rest_blockfrost/src/handlers/accounts.rs +++ b/modules/rest_blockfrost/src/handlers/accounts.rs @@ -57,7 +57,7 @@ pub async fn handle_single_account_blockfrost( Message::StateQueryResponse(StateQueryResponse::Accounts( AccountsStateQueryResponse::Error(e), )) => Err(e), - _ => Err(QueryError::query_failed( + _ => Err(QueryError::internal_error( "Unexpected message type while retrieving account info", )), }, @@ -114,7 +114,7 @@ pub async fn handle_account_registrations_blockfrost( Message::StateQueryResponse(StateQueryResponse::Accounts( AccountsStateQueryResponse::Error(e), )) => Err(e), - _ => Err(QueryError::query_failed( + _ => Err(QueryError::internal_error( "Unexpected message type while retrieving account registrations", )), }, @@ -167,7 +167,7 @@ pub async fn handle_account_delegations_blockfrost( Message::StateQueryResponse(StateQueryResponse::Accounts( AccountsStateQueryResponse::Error(e), )) => Err(e), - _ => Err(QueryError::query_failed("Unexpected response type")), + _ => Err(QueryError::internal_error("Unexpected response type")), }, ) .await?; @@ -223,7 +223,7 @@ pub async fn handle_account_mirs_blockfrost( Message::StateQueryResponse(StateQueryResponse::Accounts( AccountsStateQueryResponse::Error(e), )) => Err(e), - _ => Err(QueryError::query_failed("Unexpected response type")), + _ => Err(QueryError::internal_error("Unexpected response type")), }, ) .await?; @@ -273,7 +273,7 @@ pub async fn handle_account_withdrawals_blockfrost( Message::StateQueryResponse(StateQueryResponse::Accounts( AccountsStateQueryResponse::Error(e), )) => Err(e), - _ => Err(QueryError::query_failed("Unexpected response type")), + _ => Err(QueryError::internal_error("Unexpected response type")), }, ) .await?; @@ -323,7 +323,7 @@ pub async fn handle_account_rewards_blockfrost( Message::StateQueryResponse(StateQueryResponse::Accounts( AccountsStateQueryResponse::Error(e), )) => Err(e), - _ => Err(QueryError::query_failed("Unexpected response type")), + _ => Err(QueryError::internal_error("Unexpected response type")), }, ) .await?; @@ -364,7 +364,7 @@ pub async fn handle_account_addresses_blockfrost( Message::StateQueryResponse(StateQueryResponse::Accounts( AccountsStateQueryResponse::Error(e), )) => Err(e), - _ => Err(QueryError::query_failed("Unexpected response type")), + _ => Err(QueryError::internal_error("Unexpected response type")), }, ) .await?; @@ -445,7 +445,7 @@ async fn get_transaction_hashes( Message::StateQueryResponse(StateQueryResponse::Blocks( BlocksStateQueryResponse::Error(e), )) => Err(e), - _ => Err(QueryError::query_failed("Unexpected response type")), + _ => Err(QueryError::internal_error("Unexpected response type")), }, ) .await?; diff --git a/modules/rest_blockfrost/src/handlers/addresses.rs b/modules/rest_blockfrost/src/handlers/addresses.rs index 7be16088..142db74a 100644 --- a/modules/rest_blockfrost/src/handlers/addresses.rs +++ b/modules/rest_blockfrost/src/handlers/addresses.rs @@ -73,7 +73,7 @@ pub async fn handle_address_single_blockfrost( Message::StateQueryResponse(StateQueryResponse::UTxOs( UTxOStateQueryResponse::Error(e), )) => Err(e.into()), - _ => Err(QueryError::query_failed( + _ => Err(QueryError::internal_error( "Unexpected response from addresses query", )), }, @@ -119,7 +119,7 @@ pub async fn handle_address_single_blockfrost( Message::StateQueryResponse(StateQueryResponse::UTxOs( UTxOStateQueryResponse::Error(e), )) => Err(e.into()), - _ => Err(QueryError::query_failed( + _ => Err(QueryError::internal_error( "Unexpected response querying UTxOs", )), }, diff --git a/modules/rest_blockfrost/src/handlers/assets.rs b/modules/rest_blockfrost/src/handlers/assets.rs index 2914725f..4383e6e2 100644 --- a/modules/rest_blockfrost/src/handlers/assets.rs +++ b/modules/rest_blockfrost/src/handlers/assets.rs @@ -41,7 +41,7 @@ pub async fn handle_assets_list_blockfrost( Message::StateQueryResponse(StateQueryResponse::Assets( AssetsStateQueryResponse::Error(e), )) => Err(e), - _ => Err(QueryError::query_failed( + _ => Err(QueryError::internal_error( "Unexpected response while retrieving asset list", )), }, @@ -93,7 +93,7 @@ pub async fn handle_asset_single_blockfrost( Message::StateQueryResponse(StateQueryResponse::Assets( AssetsStateQueryResponse::Error(e), )) => Err(e), - _ => Err(QueryError::query_failed( + _ => Err(QueryError::internal_error( "Unexpected response while retrieving asset info", )), }, @@ -149,7 +149,7 @@ pub async fn handle_asset_history_blockfrost( Message::StateQueryResponse(StateQueryResponse::Assets( AssetsStateQueryResponse::Error(e), )) => Err(e), - _ => Err(QueryError::query_failed( + _ => Err(QueryError::internal_error( "Unexpected response while retrieving asset history", )), }, @@ -183,7 +183,7 @@ pub async fn handle_asset_transactions_blockfrost( Message::StateQueryResponse(StateQueryResponse::Assets( AssetsStateQueryResponse::Error(e), )) => Err(e), - _ => Err(QueryError::query_failed( + _ => Err(QueryError::internal_error( "Unexpected response while retrieving asset transactions", )), }, @@ -227,7 +227,7 @@ pub async fn handle_asset_addresses_blockfrost( Message::StateQueryResponse(StateQueryResponse::Assets( AssetsStateQueryResponse::Error(e), )) => Err(e), - _ => Err(QueryError::query_failed( + _ => Err(QueryError::internal_error( "Unexpected response while retrieving asset addresses", )), }, @@ -266,7 +266,7 @@ pub async fn handle_policy_assets_blockfrost( Message::StateQueryResponse(StateQueryResponse::Assets( AssetsStateQueryResponse::Error(e), )) => Err(e), - _ => Err(QueryError::query_failed( + _ => Err(QueryError::internal_error( "Unexpected response while retrieving policy assets", )), }, diff --git a/modules/rest_blockfrost/src/handlers/epochs.rs b/modules/rest_blockfrost/src/handlers/epochs.rs index 285ebce4..cfd980a0 100644 --- a/modules/rest_blockfrost/src/handlers/epochs.rs +++ b/modules/rest_blockfrost/src/handlers/epochs.rs @@ -54,7 +54,7 @@ pub async fn handle_epoch_info_blockfrost( epoch_info_msg, |message| match message { Message::StateQueryResponse(StateQueryResponse::Epochs(response)) => Ok(response), - _ => Err(QueryError::query_failed( + _ => Err(QueryError::internal_error( "Unexpected message type while retrieving latest epoch", )), }, @@ -90,7 +90,7 @@ pub async fn handle_epoch_info_blockfrost( Message::StateQueryResponse(StateQueryResponse::Accounts( AccountsStateQueryResponse::Error(e), )) => Err(e), - _ => Err(QueryError::query_failed( + _ => Err(QueryError::internal_error( "Unexpected message type while retrieving the latest total active stakes", )), }, @@ -114,7 +114,7 @@ pub async fn handle_epoch_info_blockfrost( Message::StateQueryResponse(StateQueryResponse::SPDD( SPDDStateQueryResponse::Error(e), )) => Err(e), - _ => Err(QueryError::query_failed(&format!( + _ => Err(QueryError::internal_error(&format!( "Unexpected message type while retrieving total active stakes for epoch: {}", epoch_number ))), @@ -165,7 +165,7 @@ pub async fn handle_epoch_params_blockfrost( Message::StateQueryResponse(StateQueryResponse::Epochs( EpochsStateQueryResponse::Error(e), )) => Err(e), - _ => Err(QueryError::query_failed( + _ => Err(QueryError::internal_error( "Unexpected message type while retrieving latest epoch", )), }, @@ -191,7 +191,7 @@ pub async fn handle_epoch_params_blockfrost( parameters_msg, |message| match message { Message::StateQueryResponse(StateQueryResponse::Parameters(resp)) => Ok(resp), - _ => Err(QueryError::query_failed( + _ => Err(QueryError::internal_error( "Unexpected message type while retrieving parameters", )), }, @@ -255,7 +255,7 @@ pub async fn handle_epoch_next_blockfrost( Message::StateQueryResponse(StateQueryResponse::Epochs( EpochsStateQueryResponse::Error(e), )) => Err(e), - _ => Err(QueryError::query_failed( + _ => Err(QueryError::internal_error( "Unexpected message type while retrieving next epochs", )), }, @@ -298,7 +298,7 @@ pub async fn handle_epoch_previous_blockfrost( Message::StateQueryResponse(StateQueryResponse::Epochs( EpochsStateQueryResponse::Error(e), )) => Err(e), - _ => Err(QueryError::query_failed( + _ => Err(QueryError::internal_error( "Unexpected message type while retrieving previous epochs", )), }, @@ -340,7 +340,7 @@ pub async fn handle_epoch_total_stakes_blockfrost( Message::StateQueryResponse(StateQueryResponse::Epochs( EpochsStateQueryResponse::Error(e), )) => Err(e), - _ => Err(QueryError::query_failed( + _ => Err(QueryError::internal_error( "Unexpected message type while retrieving latest epoch", )), }, @@ -368,7 +368,7 @@ pub async fn handle_epoch_total_stakes_blockfrost( Message::StateQueryResponse(StateQueryResponse::Accounts( AccountsStateQueryResponse::Error(e), )) => Err(e), - _ => Err(QueryError::query_failed( + _ => Err(QueryError::internal_error( "Unexpected message type while retrieving SPDD by epoch", )), }, @@ -432,7 +432,7 @@ pub async fn handle_epoch_pool_stakes_blockfrost( Message::StateQueryResponse(StateQueryResponse::Epochs( EpochsStateQueryResponse::Error(e), )) => Err(e), - _ => Err(QueryError::query_failed( + _ => Err(QueryError::internal_error( "Unexpected message type while retrieving latest epoch", )), }, @@ -461,7 +461,7 @@ pub async fn handle_epoch_pool_stakes_blockfrost( Message::StateQueryResponse(StateQueryResponse::Accounts( AccountsStateQueryResponse::Error(e), )) => Err(e), - _ => Err(QueryError::query_failed( + _ => Err(QueryError::internal_error( "Unexpected message type while retrieving SPDD by epoch and pool", )), }, @@ -536,7 +536,7 @@ pub async fn handle_epoch_pool_blocks_blockfrost( Message::StateQueryResponse(StateQueryResponse::Pools( PoolsStateQueryResponse::Error(e), )) => Err(e), - _ => Err(QueryError::query_failed( + _ => Err(QueryError::internal_error( "Unexpected message type while retrieving pool block hashes by epoch", )), }, diff --git a/modules/rest_blockfrost/src/handlers/governance.rs b/modules/rest_blockfrost/src/handlers/governance.rs index dad92282..e9a8ef12 100644 --- a/modules/rest_blockfrost/src/handlers/governance.rs +++ b/modules/rest_blockfrost/src/handlers/governance.rs @@ -40,7 +40,7 @@ pub async fn handle_dreps_list_blockfrost( Message::StateQueryResponse(StateQueryResponse::Governance( GovernanceStateQueryResponse::Error(e), )) => Err(e), - _ => Err(QueryError::query_failed("Unexpected message type")), + _ => Err(QueryError::internal_error("Unexpected message type")), }, ) .await?; @@ -88,7 +88,7 @@ pub async fn handle_single_drep_blockfrost( Message::StateQueryResponse(StateQueryResponse::Governance( GovernanceStateQueryResponse::Error(e), )) => Err(e), - _ => Err(QueryError::query_failed("Unexpected message type")), + _ => Err(QueryError::internal_error("Unexpected message type")), }, ) .await?; @@ -111,7 +111,7 @@ pub async fn handle_single_drep_blockfrost( Message::StateQueryResponse(StateQueryResponse::Accounts( AccountsStateQueryResponse::Error(e), )) => Err(e), - _ => Err(QueryError::query_failed( + _ => Err(QueryError::internal_error( "Unexpected response from accounts-state", )), }, @@ -160,7 +160,7 @@ pub async fn handle_drep_delegators_blockfrost( Message::StateQueryResponse(StateQueryResponse::Governance( GovernanceStateQueryResponse::Error(e), )) => Err(e), - _ => Err(QueryError::query_failed("Unexpected message type")), + _ => Err(QueryError::internal_error("Unexpected message type")), }, ) .await?; @@ -182,7 +182,7 @@ pub async fn handle_drep_delegators_blockfrost( Message::StateQueryResponse(StateQueryResponse::Accounts( AccountsStateQueryResponse::Error(e), )) => Err(e), - _ => Err(QueryError::query_failed( + _ => Err(QueryError::internal_error( "Unexpected response from accounts-state", )), }, @@ -233,7 +233,7 @@ pub async fn handle_drep_metadata_blockfrost( Message::StateQueryResponse(StateQueryResponse::Governance( GovernanceStateQueryResponse::Error(e), )) => Err(e), - _ => Err(QueryError::query_failed("Unexpected message type")), + _ => Err(QueryError::internal_error("Unexpected message type")), }, ) .await?; @@ -307,7 +307,7 @@ pub async fn handle_drep_updates_blockfrost( Message::StateQueryResponse(StateQueryResponse::Governance( GovernanceStateQueryResponse::Error(e), )) => Err(e), - _ => Err(QueryError::query_failed("Unexpected message type")), + _ => Err(QueryError::internal_error("Unexpected message type")), }, ) .await?; @@ -352,7 +352,7 @@ pub async fn handle_drep_votes_blockfrost( Message::StateQueryResponse(StateQueryResponse::Governance( GovernanceStateQueryResponse::Error(e), )) => Err(e), - _ => Err(QueryError::query_failed("Unexpected message type")), + _ => Err(QueryError::internal_error("Unexpected message type")), }, ) .await?; @@ -391,7 +391,7 @@ pub async fn handle_proposals_list_blockfrost( Message::StateQueryResponse(StateQueryResponse::Governance( GovernanceStateQueryResponse::Error(e), )) => Err(e), - _ => Err(QueryError::query_failed("Unexpected message type")), + _ => Err(QueryError::internal_error("Unexpected message type")), }, ) .await?; @@ -433,7 +433,7 @@ pub async fn handle_single_proposal_blockfrost( Message::StateQueryResponse(StateQueryResponse::Governance( GovernanceStateQueryResponse::Error(e), )) => Err(e), - _ => Err(QueryError::query_failed("Unexpected message type")), + _ => Err(QueryError::internal_error("Unexpected message type")), }, ) .await?; @@ -483,7 +483,7 @@ pub async fn handle_proposal_votes_blockfrost( Message::StateQueryResponse(StateQueryResponse::Governance( GovernanceStateQueryResponse::Error(e), )) => Err(e), - _ => Err(QueryError::query_failed("Unexpected message type")), + _ => Err(QueryError::internal_error("Unexpected message type")), }, ) .await?; diff --git a/modules/rest_blockfrost/src/handlers/pools.rs b/modules/rest_blockfrost/src/handlers/pools.rs index e5b42675..3f6555d0 100644 --- a/modules/rest_blockfrost/src/handlers/pools.rs +++ b/modules/rest_blockfrost/src/handlers/pools.rs @@ -48,7 +48,7 @@ pub async fn handle_pools_list_blockfrost( Message::StateQueryResponse(StateQueryResponse::Pools( PoolsStateQueryResponse::Error(e), )) => Err(e), - _ => Err(QueryError::query_failed("Unexpected message type")), + _ => Err(QueryError::internal_error("Unexpected message type")), }, ) .await?; @@ -112,7 +112,7 @@ async fn handle_pools_extended_blockfrost( Message::StateQueryResponse(StateQueryResponse::Pools( PoolsStateQueryResponse::Error(e), )) => Err(e), - _ => Err(QueryError::query_failed( + _ => Err(QueryError::internal_error( "Unexpected message type while retrieving pools list with info", )), }, @@ -133,7 +133,7 @@ async fn handle_pools_extended_blockfrost( Message::StateQueryResponse(StateQueryResponse::Epochs( EpochsStateQueryResponse::Error(e), )) => Err(e), - _ => Err(QueryError::query_failed( + _ => Err(QueryError::internal_error( "Unexpected message type while retrieving latest epoch", )), }, @@ -154,7 +154,7 @@ async fn handle_pools_extended_blockfrost( Message::StateQueryResponse(StateQueryResponse::Accounts( AccountsStateQueryResponse::Error(e), )) => Err(e), - _ => Err(QueryError::query_failed("Unexpected message type")), + _ => Err(QueryError::internal_error("Unexpected message type")), }, ); @@ -204,7 +204,7 @@ async fn handle_pools_extended_blockfrost( // if epoch_history is not enabled Ok(None) } - _ => Err(QueryError::query_failed( + _ => Err(QueryError::internal_error( "Unexpected message type while retrieving pools active stakes", )), }, @@ -227,7 +227,7 @@ async fn handle_pools_extended_blockfrost( Message::StateQueryResponse(StateQueryResponse::Accounts( AccountsStateQueryResponse::Error(e), )) => Err(e), - _ => Err(QueryError::query_failed("Unexpected message type")), + _ => Err(QueryError::internal_error("Unexpected message type")), }, ); @@ -248,7 +248,7 @@ async fn handle_pools_extended_blockfrost( Message::StateQueryResponse(StateQueryResponse::Pools( PoolsStateQueryResponse::Error(e), )) => Err(e), - _ => Err(QueryError::query_failed("Unexpected message type")), + _ => Err(QueryError::internal_error("Unexpected message type")), }, ); @@ -305,7 +305,7 @@ async fn handle_pools_retired_blockfrost( Message::StateQueryResponse(StateQueryResponse::Pools( PoolsStateQueryResponse::Error(e), )) => Err(e), - _ => Err(QueryError::query_failed("Unexpected message type")), + _ => Err(QueryError::internal_error("Unexpected message type")), }, ) .await?; @@ -344,7 +344,7 @@ async fn handle_pools_retiring_blockfrost( Message::StateQueryResponse(StateQueryResponse::Pools( PoolsStateQueryResponse::Error(e), )) => Err(e), - _ => Err(QueryError::query_failed("Unexpected message type")), + _ => Err(QueryError::internal_error("Unexpected message type")), }, ) .await?; @@ -388,7 +388,7 @@ async fn handle_pools_spo_blockfrost( Message::StateQueryResponse(StateQueryResponse::Pools( PoolsStateQueryResponse::Error(e), )) => Err(e), - _ => Err(QueryError::query_failed("Unexpected message type")), + _ => Err(QueryError::internal_error("Unexpected message type")), }, ); @@ -407,7 +407,7 @@ async fn handle_pools_spo_blockfrost( Message::StateQueryResponse(StateQueryResponse::Epochs( EpochsStateQueryResponse::Error(e), )) => Err(e), - _ => Err(QueryError::query_failed( + _ => Err(QueryError::internal_error( "Unexpected message type while retrieving latest epoch", )), }, @@ -428,7 +428,7 @@ async fn handle_pools_spo_blockfrost( Message::StateQueryResponse(StateQueryResponse::Accounts( AccountsStateQueryResponse::Error(e), )) => Err(e), - _ => Err(QueryError::query_failed("Unexpected message type")), + _ => Err(QueryError::internal_error("Unexpected message type")), }, ); @@ -447,7 +447,7 @@ async fn handle_pools_spo_blockfrost( Message::StateQueryResponse(StateQueryResponse::Accounts( AccountsStateQueryResponse::Error(e), )) => Err(e), - _ => Err(QueryError::query_failed("Unexpected message type")), + _ => Err(QueryError::internal_error("Unexpected message type")), }, ); @@ -468,7 +468,7 @@ async fn handle_pools_spo_blockfrost( Message::StateQueryResponse(StateQueryResponse::Pools( PoolsStateQueryResponse::Error(_), )) => Ok(None), - _ => Err(QueryError::query_failed("Unexpected message type")), + _ => Err(QueryError::internal_error("Unexpected message type")), }, ); @@ -489,7 +489,7 @@ async fn handle_pools_spo_blockfrost( Message::StateQueryResponse(StateQueryResponse::Pools( PoolsStateQueryResponse::Error(e), )) => Err(e), - _ => Err(QueryError::query_failed("Unexpected message type")), + _ => Err(QueryError::internal_error("Unexpected message type")), }, ); @@ -562,7 +562,7 @@ async fn handle_pools_spo_blockfrost( Message::StateQueryResponse(StateQueryResponse::Epochs( EpochsStateQueryResponse::Error(e), )) => Err(e), - _ => Err(QueryError::query_failed("Unexpected message type")), + _ => Err(QueryError::internal_error("Unexpected message type")), }, ); @@ -584,7 +584,7 @@ async fn handle_pools_spo_blockfrost( Message::StateQueryResponse(StateQueryResponse::Pools( PoolsStateQueryResponse::Error(_), )) => Ok(None), - _ => Err(QueryError::query_failed("Unexpected message type")), + _ => Err(QueryError::internal_error("Unexpected message type")), }, ); @@ -607,7 +607,7 @@ async fn handle_pools_spo_blockfrost( Message::StateQueryResponse(StateQueryResponse::Accounts( AccountsStateQueryResponse::Error(e), )) => Err(e), - _ => Err(QueryError::query_failed("Unexpected message type")), + _ => Err(QueryError::internal_error("Unexpected message type")), }, ); @@ -693,7 +693,7 @@ pub async fn handle_pool_history_blockfrost( Message::StateQueryResponse(StateQueryResponse::Epochs( EpochsStateQueryResponse::Error(e), )) => Err(e), - _ => Err(QueryError::query_failed("Unexpected message type")), + _ => Err(QueryError::internal_error("Unexpected message type")), }, ) .await?; @@ -714,7 +714,7 @@ pub async fn handle_pool_history_blockfrost( Message::StateQueryResponse(StateQueryResponse::Pools( PoolsStateQueryResponse::Error(e), )) => Err(e), - _ => Err(QueryError::query_failed("Unexpected message type")), + _ => Err(QueryError::internal_error("Unexpected message type")), }, ) .await?; @@ -754,7 +754,7 @@ pub async fn handle_pool_metadata_blockfrost( Message::StateQueryResponse(StateQueryResponse::Pools( PoolsStateQueryResponse::Error(e), )) => Err(e), - _ => Err(QueryError::query_failed("Unexpected message type")), + _ => Err(QueryError::internal_error("Unexpected message type")), }, ) .await?; @@ -818,7 +818,7 @@ pub async fn handle_pool_relays_blockfrost( Message::StateQueryResponse(StateQueryResponse::Pools( PoolsStateQueryResponse::Error(e), )) => Err(e), - _ => Err(QueryError::query_failed("Unexpected message type")), + _ => Err(QueryError::internal_error("Unexpected message type")), }, ) .await?; @@ -863,7 +863,7 @@ pub async fn handle_pool_delegators_blockfrost( warn!("Fallback to query from accounts_state"); Ok(None) } - _ => Err(QueryError::query_failed("Unexpected message type")), + _ => Err(QueryError::internal_error("Unexpected message type")), }, ) .await?; @@ -887,7 +887,7 @@ pub async fn handle_pool_delegators_blockfrost( Message::StateQueryResponse(StateQueryResponse::Accounts( AccountsStateQueryResponse::Error(e), )) => Err(e), - _ => Err(QueryError::query_failed("Unexpected message type")), + _ => Err(QueryError::internal_error("Unexpected message type")), }, ) .await? @@ -940,7 +940,7 @@ pub async fn handle_pool_blocks_blockfrost( Message::StateQueryResponse(StateQueryResponse::Pools( PoolsStateQueryResponse::Error(e), )) => Err(e), - _ => Err(QueryError::query_failed("Unexpected message type")), + _ => Err(QueryError::internal_error("Unexpected message type")), }, ) .await?; @@ -982,7 +982,7 @@ pub async fn handle_pool_updates_blockfrost( Message::StateQueryResponse(StateQueryResponse::Pools( PoolsStateQueryResponse::Error(e), )) => Err(e), - _ => Err(QueryError::query_failed("Unexpected message type")), + _ => Err(QueryError::internal_error("Unexpected message type")), }, ) .await?; @@ -1029,7 +1029,7 @@ pub async fn handle_pool_votes_blockfrost( Message::StateQueryResponse(StateQueryResponse::Pools( PoolsStateQueryResponse::Error(e), )) => Err(e), - _ => Err(QueryError::query_failed("Unexpected message type")), + _ => Err(QueryError::internal_error("Unexpected message type")), }, ) .await?; diff --git a/modules/spdd_state/src/spdd_state.rs b/modules/spdd_state/src/spdd_state.rs index b43c2cb1..02d635fa 100644 --- a/modules/spdd_state/src/spdd_state.rs +++ b/modules/spdd_state/src/spdd_state.rs @@ -126,7 +126,7 @@ impl SPDDState { async move { let Message::StateQuery(StateQuery::SPDD(query)) = message.as_ref() else { return Arc::new(Message::StateQueryResponse(StateQueryResponse::SPDD( - SPDDStateQueryResponse::Error(QueryError::query_failed( + SPDDStateQueryResponse::Error(QueryError::internal_error( "Invalid message for spdd-state", )), ))); diff --git a/modules/utxo_state/src/utxo_state.rs b/modules/utxo_state/src/utxo_state.rs index 4a67514d..2e09a466 100644 --- a/modules/utxo_state/src/utxo_state.rs +++ b/modules/utxo_state/src/utxo_state.rs @@ -113,7 +113,7 @@ impl UTXOState { async move { let Message::StateQuery(StateQuery::UTxOs(query)) = message.as_ref() else { return Arc::new(Message::StateQueryResponse(StateQueryResponse::UTxOs( - UTxOStateQueryResponse::Error(QueryError::query_failed( + UTxOStateQueryResponse::Error(QueryError::internal_error( "Invalid message for utxo-state", )), ))); @@ -124,7 +124,7 @@ impl UTXOState { UTxOStateQuery::GetUTxOsSum { utxo_identifiers } => { match state.get_utxos_sum(utxo_identifiers).await { Ok(balance) => UTxOStateQueryResponse::UTxOsSum(balance), - Err(e) => UTxOStateQueryResponse::Error(QueryError::query_failed( + Err(e) => UTxOStateQueryResponse::Error(QueryError::internal_error( format!("Fetching UTxO sum failed: {}", e.to_string()), )), } From 50941ab9c6919688dcaed7c9f7630b74a198a0f0 Mon Sep 17 00:00:00 2001 From: Matthew Hounslow Date: Wed, 5 Nov 2025 15:39:48 -0800 Subject: [PATCH 10/20] Refactor: Remove redundant trailing commas, reorder imports, and improve error handling formatting across modules --- common/src/queries/accounts.rs | 1 - common/src/queries/addresses.rs | 2 +- common/src/queries/blocks.rs | 1 - common/src/queries/epochs.rs | 1 - common/src/queries/parameters.rs | 1 - common/src/queries/spdd.rs | 1 - modules/accounts_state/src/accounts_state.rs | 30 ++++++------ modules/address_state/src/address_state.rs | 10 ++-- modules/drep_state/src/drep_state.rs | 30 ++++++------ .../src/historical_accounts_state.rs | 48 ++++++++++++------- 10 files changed, 66 insertions(+), 59 deletions(-) diff --git a/common/src/queries/accounts.rs b/common/src/queries/accounts.rs index c3e05b8d..37ca4535 100644 --- a/common/src/queries/accounts.rs +++ b/common/src/queries/accounts.rs @@ -81,7 +81,6 @@ pub enum AccountsStateQueryResponse { // DReps-related responses DrepDelegators(DrepDelegators), AccountsDrepDelegationsMap(HashMap>), - Error(QueryError), } diff --git a/common/src/queries/addresses.rs b/common/src/queries/addresses.rs index 28030f01..92128963 100644 --- a/common/src/queries/addresses.rs +++ b/common/src/queries/addresses.rs @@ -16,5 +16,5 @@ pub enum AddressStateQueryResponse { AddressTotals(AddressTotals), AddressUTxOs(Vec), AddressTransactions(Vec), - Error(QueryError) + Error(QueryError), } diff --git a/common/src/queries/blocks.rs b/common/src/queries/blocks.rs index 5f32a9b2..a0c9cd34 100644 --- a/common/src/queries/blocks.rs +++ b/common/src/queries/blocks.rs @@ -91,7 +91,6 @@ pub enum BlocksStateQueryResponse { BlockInvolvedAddresses(BlockInvolvedAddresses), BlockHashes(BlockHashes), TransactionHashes(TransactionHashes), - Error(QueryError), } diff --git a/common/src/queries/epochs.rs b/common/src/queries/epochs.rs index 572a6cb3..c3348049 100644 --- a/common/src/queries/epochs.rs +++ b/common/src/queries/epochs.rs @@ -24,7 +24,6 @@ pub enum EpochsStateQueryResponse { EpochStakeDistribution(EpochStakeDistribution), EpochStakeDistributionByPool(EpochStakeDistributionByPool), LatestEpochBlocksMintedByPool(u64), - Error(QueryError), } diff --git a/common/src/queries/parameters.rs b/common/src/queries/parameters.rs index c7a418f8..958848bd 100644 --- a/common/src/queries/parameters.rs +++ b/common/src/queries/parameters.rs @@ -16,7 +16,6 @@ pub enum ParametersStateQueryResponse { LatestEpochParameters(ProtocolParams), EpochParameters(ProtocolParams), NetworkName(String), - Error(QueryError), } diff --git a/common/src/queries/spdd.rs b/common/src/queries/spdd.rs index ed6e2127..2c43829d 100644 --- a/common/src/queries/spdd.rs +++ b/common/src/queries/spdd.rs @@ -10,6 +10,5 @@ pub enum SPDDStateQuery { #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] pub enum SPDDStateQueryResponse { EpochTotalActiveStakes(u64), - Error(QueryError), } diff --git a/modules/accounts_state/src/accounts_state.rs b/modules/accounts_state/src/accounts_state.rs index 1c0aed0e..006c22fb 100644 --- a/modules/accounts_state/src/accounts_state.rs +++ b/modules/accounts_state/src/accounts_state.rs @@ -567,27 +567,27 @@ impl AccountsState { AccountsStateQuery::GetAccountsUtxoValuesMap { stake_addresses } => { match state.get_accounts_utxo_values_map(stake_addresses) { Some(map) => AccountsStateQueryResponse::AccountsUtxoValuesMap(map), - None => AccountsStateQueryResponse::Error( - QueryError::not_found("One or more accounts not found"), - ), + None => AccountsStateQueryResponse::Error(QueryError::not_found( + "One or more accounts not found", + )), } } AccountsStateQuery::GetAccountsUtxoValuesSum { stake_addresses } => { match state.get_accounts_utxo_values_sum(stake_addresses) { Some(sum) => AccountsStateQueryResponse::AccountsUtxoValuesSum(sum), - None => AccountsStateQueryResponse::Error( - QueryError::not_found("One or more accounts not found"), - ), + None => AccountsStateQueryResponse::Error(QueryError::not_found( + "One or more accounts not found", + )), } } AccountsStateQuery::GetAccountsBalancesMap { stake_addresses } => { match state.get_accounts_balances_map(stake_addresses) { Some(map) => AccountsStateQueryResponse::AccountsBalancesMap(map), - None => AccountsStateQueryResponse::Error( - QueryError::not_found("One or more accounts not found"), - ), + None => AccountsStateQueryResponse::Error(QueryError::not_found( + "One or more accounts not found", + )), } } @@ -600,18 +600,18 @@ impl AccountsState { AccountsStateQuery::GetAccountsBalancesSum { stake_addresses } => { match state.get_account_balances_sum(stake_addresses) { Some(sum) => AccountsStateQueryResponse::AccountsBalancesSum(sum), - None => AccountsStateQueryResponse::Error( - QueryError::not_found("One or more accounts not found"), - ), + None => AccountsStateQueryResponse::Error(QueryError::not_found( + "One or more accounts not found", + )), } } AccountsStateQuery::GetSPDDByEpoch { epoch } => match spdd_store_guard { Some(spdd_store) => match spdd_store.query_by_epoch(*epoch) { Ok(result) => AccountsStateQueryResponse::SPDDByEpoch(result), - Err(e) => AccountsStateQueryResponse::Error(QueryError::internal_error( - e.to_string(), - )), + Err(e) => AccountsStateQueryResponse::Error( + QueryError::internal_error(e.to_string()), + ), }, None => { AccountsStateQueryResponse::Error(QueryError::storage_disabled("SPDD")) diff --git a/modules/address_state/src/address_state.rs b/modules/address_state/src/address_state.rs index fd1e5979..7c391563 100644 --- a/modules/address_state/src/address_state.rs +++ b/modules/address_state/src/address_state.rs @@ -4,6 +4,11 @@ use std::sync::Arc; +use crate::{ + immutable_address_store::ImmutableAddressStore, + state::{AddressStorageConfig, State}, +}; +use acropolis_common::queries::errors::QueryError; use acropolis_common::{ messages::{CardanoMessage, Message, StateQuery, StateQueryResponse}, queries::addresses::{ @@ -16,11 +21,6 @@ use caryatid_sdk::{module, Context, Module, Subscription}; use config::Config; use tokio::sync::{mpsc, Mutex}; use tracing::{error, info}; -use acropolis_common::queries::errors::QueryError; -use crate::{ - immutable_address_store::ImmutableAddressStore, - state::{AddressStorageConfig, State}, -}; mod immutable_address_store; mod state; mod volatile_addresses; diff --git a/modules/drep_state/src/drep_state.rs b/modules/drep_state/src/drep_state.rs index 16cc79bd..236f5cd3 100644 --- a/modules/drep_state/src/drep_state.rs +++ b/modules/drep_state/src/drep_state.rs @@ -325,9 +325,9 @@ impl DRepState { QueryError::internal_error(msg), ), }, - None => GovernanceStateQueryResponse::Error(QueryError::internal_error( - "No current state", - )), + None => GovernanceStateQueryResponse::Error( + QueryError::internal_error("No current state"), + ), } } GovernanceStateQuery::GetDRepDelegators { drep_credential } => { @@ -349,9 +349,9 @@ impl DRepState { QueryError::internal_error(msg), ), }, - None => GovernanceStateQueryResponse::Error(QueryError::internal_error( - "No current state", - )), + None => GovernanceStateQueryResponse::Error( + QueryError::internal_error("No current state"), + ), } } GovernanceStateQuery::GetDRepMetadata { drep_credential } => { @@ -369,9 +369,9 @@ impl DRepState { QueryError::internal_error(msg), ), }, - None => GovernanceStateQueryResponse::Error(QueryError::internal_error( - "No current state", - )), + None => GovernanceStateQueryResponse::Error( + QueryError::internal_error("No current state"), + ), } } @@ -392,9 +392,9 @@ impl DRepState { QueryError::internal_error(msg), ), }, - None => GovernanceStateQueryResponse::Error(QueryError::internal_error( - "No current state", - )), + None => GovernanceStateQueryResponse::Error( + QueryError::internal_error("No current state"), + ), } } GovernanceStateQuery::GetDRepVotes { drep_credential } => { @@ -414,9 +414,9 @@ impl DRepState { QueryError::internal_error(msg), ), }, - None => GovernanceStateQueryResponse::Error(QueryError::internal_error( - "No current state", - )), + None => GovernanceStateQueryResponse::Error( + QueryError::internal_error("No current state"), + ), } } _ => GovernanceStateQueryResponse::Error(QueryError::invalid_request(format!( diff --git a/modules/historical_accounts_state/src/historical_accounts_state.rs b/modules/historical_accounts_state/src/historical_accounts_state.rs index a56e1d4f..b24e3b6e 100644 --- a/modules/historical_accounts_state/src/historical_accounts_state.rs +++ b/modules/historical_accounts_state/src/historical_accounts_state.rs @@ -319,9 +319,11 @@ impl HistoricalAccountsState { Ok(None) => AccountsStateQueryResponse::Error(QueryError::not_found( format!("Account {}", account), )), - Err(e) => AccountsStateQueryResponse::Error(QueryError::internal_error( - format!("Failed to get registration history: {}", e), - )), + Err(e) => { + AccountsStateQueryResponse::Error(QueryError::internal_error( + format!("Failed to get registration history: {}", e), + )) + } } } AccountsStateQuery::GetAccountDelegationHistory { account } => { @@ -332,9 +334,11 @@ impl HistoricalAccountsState { Ok(None) => AccountsStateQueryResponse::Error(QueryError::not_found( format!("Account {}", account), )), - Err(e) => AccountsStateQueryResponse::Error(QueryError::internal_error( - format!("Failed to get delegation history: {}", e), - )), + Err(e) => { + AccountsStateQueryResponse::Error(QueryError::internal_error( + format!("Failed to get delegation history: {}", e), + )) + } } } AccountsStateQuery::GetAccountMIRHistory { account } => { @@ -343,9 +347,11 @@ impl HistoricalAccountsState { Ok(None) => AccountsStateQueryResponse::Error(QueryError::not_found( format!("Account {}", account), )), - Err(e) => AccountsStateQueryResponse::Error(QueryError::internal_error( - format!("Failed to get MIR history: {}", e), - )), + Err(e) => { + AccountsStateQueryResponse::Error(QueryError::internal_error( + format!("Failed to get MIR history: {}", e), + )) + } } } AccountsStateQuery::GetAccountWithdrawalHistory { account } => { @@ -356,9 +362,11 @@ impl HistoricalAccountsState { Ok(None) => AccountsStateQueryResponse::Error(QueryError::not_found( format!("Account {}", account), )), - Err(e) => AccountsStateQueryResponse::Error(QueryError::internal_error( - format!("Failed to get withdrawal history: {}", e), - )), + Err(e) => { + AccountsStateQueryResponse::Error(QueryError::internal_error( + format!("Failed to get withdrawal history: {}", e), + )) + } } } AccountsStateQuery::GetAccountRewardHistory { account } => { @@ -369,9 +377,11 @@ impl HistoricalAccountsState { Ok(None) => AccountsStateQueryResponse::Error(QueryError::not_found( format!("Account {}", account), )), - Err(e) => AccountsStateQueryResponse::Error(QueryError::internal_error( - format!("Failed to get reward history: {}", e), - )), + Err(e) => { + AccountsStateQueryResponse::Error(QueryError::internal_error( + format!("Failed to get reward history: {}", e), + )) + } } } AccountsStateQuery::GetAccountAssociatedAddresses { account } => { @@ -382,9 +392,11 @@ impl HistoricalAccountsState { Ok(None) => AccountsStateQueryResponse::Error(QueryError::not_found( format!("Account {}", account), )), - Err(e) => AccountsStateQueryResponse::Error(QueryError::internal_error( - format!("Failed to get associated addresses: {}", e), - )), + Err(e) => { + AccountsStateQueryResponse::Error(QueryError::internal_error( + format!("Failed to get associated addresses: {}", e), + )) + } } } _ => AccountsStateQueryResponse::Error(QueryError::not_implemented(format!( From 6aa66decefb38cce609521cbaf560de4c5ff9d3c Mon Sep 17 00:00:00 2001 From: Matthew Hounslow Date: Wed, 5 Nov 2025 15:57:17 -0800 Subject: [PATCH 11/20] fix: clippy with error string display --- common/src/address.rs | 17 ++++------------- modules/chain_store/src/chain_store.rs | 16 ++++++++-------- .../rest_blockfrost/src/handlers/addresses.rs | 4 ++-- modules/rest_blockfrost/src/handlers/epochs.rs | 2 +- modules/spo_state/src/spo_state.rs | 10 +++++----- modules/utxo_state/src/utxo_state.rs | 2 +- 6 files changed, 21 insertions(+), 30 deletions(-) diff --git a/common/src/address.rs b/common/src/address.rs index 9fb8b03a..f770d2c1 100644 --- a/common/src/address.rs +++ b/common/src/address.rs @@ -148,10 +148,12 @@ pub struct ShelleyAddressPointer { serde::Deserialize, minicbor::Encode, minicbor::Decode, + Default, )] pub enum ShelleyAddressDelegationPart { /// No delegation (enterprise addresses) #[n(0)] + #[default] None, /// Delegation to stake key @@ -167,12 +169,6 @@ pub enum ShelleyAddressDelegationPart { Pointer(#[n(0)] ShelleyAddressPointer), } -impl Default for ShelleyAddressDelegationPart { - fn default() -> Self { - Self::None - } -} - /// A Shelley-era address #[derive( Debug, @@ -619,20 +615,15 @@ impl Default for StakeAddress { } /// A Cardano address -#[derive(Debug, Clone, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)] +#[derive(Debug, Clone, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize, Default)] pub enum Address { + #[default] None, Byron(ByronAddress), Shelley(ShelleyAddress), Stake(StakeAddress), } -impl Default for Address { - fn default() -> Self { - Self::None - } -} - impl Address { /// Get the stake pointer if there is one pub fn get_pointer(&self) -> Option { diff --git a/modules/chain_store/src/chain_store.rs b/modules/chain_store/src/chain_store.rs index 60c1ef16..9de2320c 100644 --- a/modules/chain_store/src/chain_store.rs +++ b/modules/chain_store/src/chain_store.rs @@ -166,7 +166,7 @@ impl ChainStore { BlocksStateQuery::GetBlockInfo { block_key } => { let Some(block) = Self::get_block_by_key(store, block_key)? else { return Ok(BlocksStateQueryResponse::Error(QueryError::not_found( - &format!("Block {:?} not found", block_key), + format!("Block {:?} not found", block_key), ))); }; let info = Self::to_block_info(block, store, state, false)?; @@ -175,7 +175,7 @@ impl ChainStore { BlocksStateQuery::GetBlockBySlot { slot } => { let Some(block) = store.get_block_by_slot(*slot)? else { return Ok(BlocksStateQueryResponse::Error(QueryError::not_found( - &format!("Block at slot {} not found", slot), + format!("Block at slot {} not found", slot), ))); }; let info = Self::to_block_info(block, store, state, false)?; @@ -184,7 +184,7 @@ impl ChainStore { BlocksStateQuery::GetBlockByEpochSlot { epoch, slot } => { let Some(block) = store.get_block_by_epoch_slot(*epoch, *slot)? else { return Ok(BlocksStateQueryResponse::Error(QueryError::not_found( - &format!("Block at epoch {} slot {} not found", epoch, slot), + format!("Block at epoch {} slot {} not found", epoch, slot), ))); }; let info = Self::to_block_info(block, store, state, false)?; @@ -202,7 +202,7 @@ impl ChainStore { } let Some(block) = Self::get_block_by_key(store, block_key)? else { return Ok(BlocksStateQueryResponse::Error(QueryError::not_found( - &format!("Block {:?} not found", block_key), + format!("Block {:?} not found", block_key), ))); }; let number = match block_key { @@ -229,7 +229,7 @@ impl ChainStore { } let Some(block) = Self::get_block_by_key(store, block_key)? else { return Ok(BlocksStateQueryResponse::Error(QueryError::not_found( - &format!("Block {:?} not found", block_key), + format!("Block {:?} not found", block_key), ))); }; let number = match block_key { @@ -256,7 +256,7 @@ impl ChainStore { } => { let Some(block) = Self::get_block_by_key(store, block_key)? else { return Ok(BlocksStateQueryResponse::Error(QueryError::not_found( - &format!("Block {:?} not found", block_key), + format!("Block {:?} not found", block_key), ))); }; let txs = Self::to_block_transactions(block, limit, skip, order)?; @@ -270,7 +270,7 @@ impl ChainStore { } => { let Some(block) = Self::get_block_by_key(store, block_key)? else { return Ok(BlocksStateQueryResponse::Error(QueryError::not_found( - &format!("Block {:?} not found", block_key), + format!("Block {:?} not found", block_key), ))); }; let txs = Self::to_block_transactions_cbor(block, limit, skip, order)?; @@ -283,7 +283,7 @@ impl ChainStore { } => { let Some(block) = Self::get_block_by_key(store, block_key)? else { return Ok(BlocksStateQueryResponse::Error(QueryError::not_found( - &format!("Block {:?} not found", block_key), + format!("Block {:?} not found", block_key), ))); }; let addresses = Self::to_block_involved_addresses(block, limit, skip)?; diff --git a/modules/rest_blockfrost/src/handlers/addresses.rs b/modules/rest_blockfrost/src/handlers/addresses.rs index 142db74a..6c14e364 100644 --- a/modules/rest_blockfrost/src/handlers/addresses.rs +++ b/modules/rest_blockfrost/src/handlers/addresses.rs @@ -72,7 +72,7 @@ pub async fn handle_address_single_blockfrost( Message::StateQueryResponse(StateQueryResponse::UTxOs( UTxOStateQueryResponse::Error(e), - )) => Err(e.into()), + )) => Err(e), _ => Err(QueryError::internal_error( "Unexpected response from addresses query", )), @@ -118,7 +118,7 @@ pub async fn handle_address_single_blockfrost( )) => Ok(balance), Message::StateQueryResponse(StateQueryResponse::UTxOs( UTxOStateQueryResponse::Error(e), - )) => Err(e.into()), + )) => Err(e), _ => Err(QueryError::internal_error( "Unexpected response querying UTxOs", )), diff --git a/modules/rest_blockfrost/src/handlers/epochs.rs b/modules/rest_blockfrost/src/handlers/epochs.rs index cfd980a0..b63e72be 100644 --- a/modules/rest_blockfrost/src/handlers/epochs.rs +++ b/modules/rest_blockfrost/src/handlers/epochs.rs @@ -114,7 +114,7 @@ pub async fn handle_epoch_info_blockfrost( Message::StateQueryResponse(StateQueryResponse::SPDD( SPDDStateQueryResponse::Error(e), )) => Err(e), - _ => Err(QueryError::internal_error(&format!( + _ => Err(QueryError::internal_error(format!( "Unexpected message type while retrieving total active stakes for epoch: {}", epoch_number ))), diff --git a/modules/spo_state/src/spo_state.rs b/modules/spo_state/src/spo_state.rs index 08ab8438..17b0f527 100644 --- a/modules/spo_state/src/spo_state.rs +++ b/modules/spo_state/src/spo_state.rs @@ -514,7 +514,7 @@ impl SPOState { // for BF's compatibility PoolsStateQuery::GetPoolInfo { pool_id } => match state.get(pool_id) { Some(pool) => PoolsStateQueryResponse::PoolInfo(pool.clone()), - None => PoolsStateQueryResponse::Error(QueryError::not_found(&format!( + None => PoolsStateQueryResponse::Error(QueryError::not_found(format!( "Pool {}", pool_id ))), @@ -610,7 +610,7 @@ impl SPOState { if let Some(pool_metadata) = pool_metadata { PoolsStateQueryResponse::PoolMetadata(pool_metadata) } else { - PoolsStateQueryResponse::Error(QueryError::not_found(&format!( + PoolsStateQueryResponse::Error(QueryError::not_found(format!( "Pool metadata for {}", pool_id ))) @@ -622,7 +622,7 @@ impl SPOState { if let Some(relays) = pool_relays { PoolsStateQueryResponse::PoolRelays(relays) } else { - PoolsStateQueryResponse::Error(QueryError::not_found(&format!( + PoolsStateQueryResponse::Error(QueryError::not_found(format!( "Pool relays for {}", pool_id ))) @@ -639,7 +639,7 @@ impl SPOState { delegators: pool_delegators, }) } else { - PoolsStateQueryResponse::Error(QueryError::not_found(&format!( + PoolsStateQueryResponse::Error(QueryError::not_found(format!( "Pool delegators for {}", pool_id ))) @@ -689,7 +689,7 @@ impl SPOState { if let Some(pool_updates) = pool_updates { PoolsStateQueryResponse::PoolUpdates(pool_updates) } else { - PoolsStateQueryResponse::Error(QueryError::not_found(&format!( + PoolsStateQueryResponse::Error(QueryError::not_found(format!( "Pool updates for {}", pool_id ))) diff --git a/modules/utxo_state/src/utxo_state.rs b/modules/utxo_state/src/utxo_state.rs index 2e09a466..a6361305 100644 --- a/modules/utxo_state/src/utxo_state.rs +++ b/modules/utxo_state/src/utxo_state.rs @@ -125,7 +125,7 @@ impl UTXOState { match state.get_utxos_sum(utxo_identifiers).await { Ok(balance) => UTxOStateQueryResponse::UTxOsSum(balance), Err(e) => UTxOStateQueryResponse::Error(QueryError::internal_error( - format!("Fetching UTxO sum failed: {}", e.to_string()), + format!("Fetching UTxO sum failed: {e}"), )), } } From 1bc9e65ff2d054aadfe857a1c2bf7d564e0d3e1c Mon Sep 17 00:00:00 2001 From: Matthew Hounslow Date: Wed, 5 Nov 2025 16:14:13 -0800 Subject: [PATCH 12/20] Refactor: Migrate to `thiserror` for `RESTError` and `QueryError` --- common/src/queries/errors.rs | 25 +++++++--------------- common/src/queries/utils.rs | 4 +--- common/src/rest_error.rs | 40 +++++++++++++++--------------------- 3 files changed, 24 insertions(+), 45 deletions(-) diff --git a/common/src/queries/errors.rs b/common/src/queries/errors.rs index 9b4879df..8fa135c5 100644 --- a/common/src/queries/errors.rs +++ b/common/src/queries/errors.rs @@ -1,22 +1,27 @@ use serde::{Deserialize, Serialize}; -use std::fmt; +use thiserror::Error; /// Common error type for all state query responses -#[derive(Debug, Clone, Serialize, Deserialize)] +#[derive(Debug, Clone, Error, Serialize, Deserialize)] pub enum QueryError { /// The requested resource was not found + #[error("Not found: {resource}")] NotFound { resource: String }, /// An error occurred while processing the query + #[error("Internal error: {message}")] Internal { message: String }, /// Storage backend is disabled in configuration + #[error("{storage_type} storage is not enabled")] StorageDisabled { storage_type: String }, /// Invalid request parameters + #[error("Invalid request: {message}")] InvalidRequest { message: String }, /// Query variant is not implemented yet + #[error("Query not implemented: {query}")] NotImplemented { query: String }, } @@ -51,19 +56,3 @@ impl QueryError { } } } - -impl fmt::Display for QueryError { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match self { - Self::NotFound { resource } => write!(f, "Not found: {}", resource), - Self::Internal { message } => write!(f, "Query failed: {}", message), - Self::StorageDisabled { storage_type } => { - write!(f, "{} storage is not enabled", storage_type) - } - Self::InvalidRequest { message } => write!(f, "Invalid request: {}", message), - Self::NotImplemented { query } => write!(f, "Query not implemented: {}", query), - } - } -} - -impl std::error::Error for QueryError {} diff --git a/common/src/queries/utils.rs b/common/src/queries/utils.rs index 6e620da7..8d4de100 100644 --- a/common/src/queries/utils.rs +++ b/common/src/queries/utils.rs @@ -6,8 +6,6 @@ use crate::messages::{Message, RESTResponse}; use crate::queries::errors::QueryError; use crate::rest_error::RESTError; -/// Query state and get typed result or QueryError -/// This is the low-level building block for handlers that need to do more processing pub async fn query_state( context: &Arc>, topic: &str, @@ -21,7 +19,7 @@ where .message_bus .request(topic, request_msg) .await - .map_err(|e| QueryError::internal_error(e.to_string()))?; + .map_err(|e| QueryError::internal_error(format!("Failed to query '{topic}': {e:#}")))?; let message = Arc::try_unwrap(raw_msg).unwrap_or_else(|arc| (*arc).clone()); diff --git a/common/src/rest_error.rs b/common/src/rest_error.rs index 25a21107..66218ce9 100644 --- a/common/src/rest_error.rs +++ b/common/src/rest_error.rs @@ -1,14 +1,21 @@ use crate::queries::errors::QueryError; use anyhow::Error as AnyhowError; use caryatid_module_rest_server::messages::RESTResponse; -use std::fmt; +use thiserror::Error; -/// Standard error types for the application -#[derive(Debug)] +/// Standard REST error types +#[derive(Debug, Error)] pub enum RESTError { + #[error("{0}")] BadRequest(String), + + #[error("{0}")] NotFound(String), + + #[error("{0}")] InternalServerError(String), + + #[error("{0}")] NotImplemented(String), } @@ -24,12 +31,12 @@ impl RESTError { } /// Get the error message - pub fn message(&self) -> String { + pub fn message(&self) -> &str { match self { - RESTError::BadRequest(msg) => msg.clone(), - RESTError::NotFound(msg) => msg.clone(), - RESTError::InternalServerError(msg) => msg.clone(), - RESTError::NotImplemented(msg) => msg.clone(), + RESTError::BadRequest(msg) => msg, + RESTError::NotFound(msg) => msg, + RESTError::InternalServerError(msg) => msg, + RESTError::NotImplemented(msg) => msg, } } @@ -79,18 +86,10 @@ impl RESTError { } } -impl fmt::Display for RESTError { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "{}", self.message()) - } -} - -impl std::error::Error for RESTError {} - /// Convert RESTError to RESTResponse impl From for RESTResponse { fn from(error: RESTError) -> Self { - RESTResponse::with_text(error.status_code(), &error.message()) + RESTResponse::with_text(error.status_code(), error.message()) } } @@ -101,13 +100,6 @@ impl From for RESTError { } } -// /// Convert ParseIntError to RESTError (400 Bad Request) -// impl From for RESTError { -// fn from(error: ParseIntError) -> Self { -// RESTError::BadRequest(error.to_string()) -// } -// } - /// Convert hex decode errors to RESTError (400 Bad Request) impl From for RESTError { fn from(error: hex::FromHexError) -> Self { From a20f65eac8a2f222ac877e29fc67c3378b070908 Mon Sep 17 00:00:00 2001 From: Matthew Hounslow Date: Fri, 7 Nov 2025 11:16:57 -0800 Subject: [PATCH 13/20] Refactor: Try to revert to simple conversion of errors to as exact as I can --- common/src/rest_error.rs | 2 +- common/src/types.rs | 1 - modules/drdd_state/src/rest.rs | 2 +- .../rest_blockfrost/src/handlers/accounts.rs | 283 +++++-- .../rest_blockfrost/src/handlers/addresses.rs | 76 +- .../rest_blockfrost/src/handlers/assets.rs | 58 +- .../rest_blockfrost/src/handlers/epochs.rs | 102 +-- .../src/handlers/governance.rs | 746 ++++++++++-------- modules/rest_blockfrost/src/handlers/pools.rs | 247 +++--- .../rest_blockfrost/src/rest_blockfrost.rs | 2 - 10 files changed, 850 insertions(+), 669 deletions(-) diff --git a/common/src/rest_error.rs b/common/src/rest_error.rs index 66218ce9..047322c5 100644 --- a/common/src/rest_error.rs +++ b/common/src/rest_error.rs @@ -42,7 +42,7 @@ impl RESTError { /// Parameter missing error pub fn param_missing(param_name: &str) -> Self { - RESTError::BadRequest(format!("{} parameter is missing", param_name)) + RESTError::BadRequest(format!("Missing {} parameter", param_name)) } /// Invalid parameter error diff --git a/common/src/types.rs b/common/src/types.rs index 5c583aa0..41ecc62e 100644 --- a/common/src/types.rs +++ b/common/src/types.rs @@ -251,7 +251,6 @@ impl fmt::Display for RewardType { } pub type PolicyId = [u8; 28]; - pub type NativeAssets = Vec<(PolicyId, Vec)>; pub type NativeAssetsDelta = Vec<(PolicyId, Vec)>; pub type NativeAssetsMap = HashMap>; diff --git a/modules/drdd_state/src/rest.rs b/modules/drdd_state/src/rest.rs index f69d0a48..d8e635af 100644 --- a/modules/drdd_state/src/rest.rs +++ b/modules/drdd_state/src/rest.rs @@ -31,7 +31,7 @@ pub async fn handle_drdd( Some(drdd) => Some(drdd), None => { return Err(RESTError::not_found(&format!( - "DRDD not found for epoch {}", + "DRDD in epoch {}", epoch ))); } diff --git a/modules/rest_blockfrost/src/handlers/accounts.rs b/modules/rest_blockfrost/src/handlers/accounts.rs index 10a26987..9d1ac475 100644 --- a/modules/rest_blockfrost/src/handlers/accounts.rs +++ b/modules/rest_blockfrost/src/handlers/accounts.rs @@ -15,8 +15,7 @@ use acropolis_common::queries::errors::QueryError; use acropolis_common::queries::utils::query_state; use acropolis_common::rest_error::RESTError; use acropolis_common::serialization::{Bech32Conversion, Bech32WithHrp}; -use acropolis_common::{DRepChoice, StakeAddress, TxHash}; -use anyhow::{anyhow, Result}; +use acropolis_common::{DRepChoice, StakeAddress}; use caryatid_sdk::Context; #[derive(serde::Serialize)] @@ -53,7 +52,10 @@ pub async fn handle_single_account_blockfrost( |message| match message { Message::StateQueryResponse(StateQueryResponse::Accounts( AccountsStateQueryResponse::AccountInfo(account), - )) => Ok(account), + )) => Ok(Some(account)), + Message::StateQueryResponse(StateQueryResponse::Accounts( + AccountsStateQueryResponse::Error(QueryError::NotFound { .. }), + )) => Ok(None), Message::StateQueryResponse(StateQueryResponse::Accounts( AccountsStateQueryResponse::Error(e), )) => Err(e), @@ -64,19 +66,23 @@ pub async fn handle_single_account_blockfrost( ) .await?; - let delegated_spo = match &account.delegated_spo { - Some(spo) => { - Some(spo.to_bech32().map_err(|e| RESTError::encoding_failed(&format!("SPO: {e}")))?) - } - None => None, + let Some(account) = account else { + return Err(RESTError::not_found("Account not found")); }; - let delegated_drep = match &account.delegated_drep { - Some(drep) => Some( - map_drep_choice(drep).map_err(|e| RESTError::encoding_failed(&format!("dRep: {e}")))?, - ), - None => None, - }; + let delegated_spo = account + .delegated_spo + .as_ref() + .map(|spo| spo.to_bech32()) + .transpose() + .map_err(|e| RESTError::encoding_failed(&format!("SPO: {e}")))?; + + let delegated_drep = account + .delegated_drep + .as_ref() + .map(map_drep_choice) + .transpose() + .map_err(|e| RESTError::encoding_failed(&format!("dRep: {e}")))?; let rest_response = StakeAccountRest { utxo_value: account.utxo_value, @@ -110,10 +116,13 @@ pub async fn handle_account_registrations_blockfrost( |message| match message { Message::StateQueryResponse(StateQueryResponse::Accounts( AccountsStateQueryResponse::AccountRegistrationHistory(registrations), - )) => Ok(registrations), + )) => Ok(Some(registrations)), + Message::StateQueryResponse(StateQueryResponse::Accounts( + AccountsStateQueryResponse::Error(QueryError::NotFound { .. }), + )) => Ok(None), Message::StateQueryResponse(StateQueryResponse::Accounts( AccountsStateQueryResponse::Error(e), - )) => Err(e), + )) => Err(e.into()), _ => Err(QueryError::internal_error( "Unexpected message type while retrieving account registrations", )), @@ -121,16 +130,41 @@ pub async fn handle_account_registrations_blockfrost( ) .await?; + let Some(registrations) = registrations else { + return Err(RESTError::not_found("Account not found")); + }; + // Get TxHashes from TxIdentifiers let tx_ids: Vec<_> = registrations.iter().map(|r| r.tx_identifier).collect(); - let tx_hashes = get_transaction_hashes(&context, &handlers_config, tx_ids).await?; + let msg = Arc::new(Message::StateQuery(StateQuery::Blocks( + BlocksStateQuery::GetTransactionHashes { tx_ids }, + ))); + let tx_hashes = query_state( + &context, + &handlers_config.blocks_query_topic, + msg, + |message| match message { + Message::StateQueryResponse(StateQueryResponse::Blocks( + BlocksStateQueryResponse::TransactionHashes(TransactionHashes { tx_hashes }), + )) => Ok(tx_hashes), + Message::StateQueryResponse(StateQueryResponse::Blocks( + BlocksStateQueryResponse::Error(e), + )) => Err(e.into()), + _ => Err(QueryError::internal_error( + "Unexpected message type while resolving transaction hashes", + )), + }, + ) + .await?; let mut rest_response = Vec::new(); for r in registrations { - let tx_hash = tx_hashes.get(&r.tx_identifier).ok_or_else(|| { - RESTError::InternalServerError("Missing tx hash for registration".to_string()) - })?; + let Some(tx_hash) = tx_hashes.get(&r.tx_identifier) else { + return Err(RESTError::InternalServerError( + "Missing tx hash for registration".to_string(), + )); + }; rest_response.push(RegistrationUpdateREST { tx_hash: hex::encode(tx_hash), @@ -163,25 +197,55 @@ pub async fn handle_account_delegations_blockfrost( |message| match message { Message::StateQueryResponse(StateQueryResponse::Accounts( AccountsStateQueryResponse::AccountDelegationHistory(delegations), - )) => Ok(delegations), + )) => Ok(Some(delegations)), + Message::StateQueryResponse(StateQueryResponse::Accounts( + AccountsStateQueryResponse::Error(QueryError::NotFound { .. }), + )) => Ok(None), Message::StateQueryResponse(StateQueryResponse::Accounts( AccountsStateQueryResponse::Error(e), - )) => Err(e), - _ => Err(QueryError::internal_error("Unexpected response type")), + )) => Err(e.into()), + _ => Err(QueryError::internal_error( + "Unexpected message type while retrieving account delegations", + )), }, ) .await?; + let Some(delegations) = delegations else { + return Err(RESTError::not_found("Account not found")); + }; + // Get TxHashes from TxIdentifiers let tx_ids: Vec<_> = delegations.iter().map(|r| r.tx_identifier).collect(); - let tx_hashes = get_transaction_hashes(&context, &handlers_config, tx_ids).await?; + let msg = Arc::new(Message::StateQuery(StateQuery::Blocks( + BlocksStateQuery::GetTransactionHashes { tx_ids }, + ))); + let tx_hashes = query_state( + &context, + &handlers_config.blocks_query_topic, + msg, + |message| match message { + Message::StateQueryResponse(StateQueryResponse::Blocks( + BlocksStateQueryResponse::TransactionHashes(TransactionHashes { tx_hashes }), + )) => Ok(tx_hashes), + Message::StateQueryResponse(StateQueryResponse::Blocks( + BlocksStateQueryResponse::Error(e), + )) => Err(e.into()), + _ => Err(QueryError::internal_error( + "Unexpected message type while resolving transaction hashes", + )), + }, + ) + .await?; let mut rest_response = Vec::new(); for r in delegations { - let tx_hash = tx_hashes.get(&r.tx_identifier).ok_or_else(|| { - RESTError::InternalServerError("Missing tx hash for delegation".to_string()) - })?; + let Some(tx_hash) = tx_hashes.get(&r.tx_identifier) else { + return Err(RESTError::InternalServerError( + "Missing tx hash for delegation".to_string(), + )); + }; let pool_id = r.pool.to_bech32().map_err(|e| RESTError::encoding_failed(&format!("pool ID: {e}")))?; @@ -219,25 +283,55 @@ pub async fn handle_account_mirs_blockfrost( |message| match message { Message::StateQueryResponse(StateQueryResponse::Accounts( AccountsStateQueryResponse::AccountMIRHistory(mirs), - )) => Ok(mirs), + )) => Ok(Some(mirs)), + Message::StateQueryResponse(StateQueryResponse::Accounts( + AccountsStateQueryResponse::Error(QueryError::NotFound { .. }), + )) => Ok(None), Message::StateQueryResponse(StateQueryResponse::Accounts( AccountsStateQueryResponse::Error(e), - )) => Err(e), - _ => Err(QueryError::internal_error("Unexpected response type")), + )) => Err(e.into()), + _ => Err(QueryError::internal_error( + "Unexpected message type while retrieving account mirs", + )), }, ) .await?; + let Some(mirs) = mirs else { + return Err(RESTError::not_found("Account not found")); + }; + // Get TxHashes from TxIdentifiers let tx_ids: Vec<_> = mirs.iter().map(|r| r.tx_identifier).collect(); - let tx_hashes = get_transaction_hashes(&context, &handlers_config, tx_ids).await?; + let msg = Arc::new(Message::StateQuery(StateQuery::Blocks( + BlocksStateQuery::GetTransactionHashes { tx_ids }, + ))); + let tx_hashes = query_state( + &context, + &handlers_config.blocks_query_topic, + msg, + |message| match message { + Message::StateQueryResponse(StateQueryResponse::Blocks( + BlocksStateQueryResponse::TransactionHashes(TransactionHashes { tx_hashes }), + )) => Ok(tx_hashes), + Message::StateQueryResponse(StateQueryResponse::Blocks( + BlocksStateQueryResponse::Error(e), + )) => Err(e.into()), + _ => Err(QueryError::internal_error( + "Unexpected message type while resolving transaction hashes", + )), + }, + ) + .await?; let mut rest_response = Vec::new(); for r in mirs { - let tx_hash = tx_hashes.get(&r.tx_identifier).ok_or_else(|| { - RESTError::InternalServerError("Missing tx hash for MIR record".to_string()) - })?; + let Some(tx_hash) = tx_hashes.get(&r.tx_identifier) else { + return Err(RESTError::InternalServerError( + "Missing tx hash for MIR record".to_string(), + )); + }; rest_response.push(AccountWithdrawalREST { tx_hash: hex::encode(tx_hash), @@ -258,7 +352,7 @@ pub async fn handle_account_withdrawals_blockfrost( // Prepare the message let msg = Arc::new(Message::StateQuery(StateQuery::Accounts( - AccountsStateQuery::GetAccountWithdrawalHistory { account }, + AccountsStateQuery::GetAccountRegistrationHistory { account }, ))); // Get withdrawals from historical accounts state @@ -269,25 +363,55 @@ pub async fn handle_account_withdrawals_blockfrost( |message| match message { Message::StateQueryResponse(StateQueryResponse::Accounts( AccountsStateQueryResponse::AccountWithdrawalHistory(withdrawals), - )) => Ok(withdrawals), + )) => Ok(Some(withdrawals)), + Message::StateQueryResponse(StateQueryResponse::Accounts( + AccountsStateQueryResponse::Error(QueryError::NotFound { .. }), + )) => Ok(None), Message::StateQueryResponse(StateQueryResponse::Accounts( AccountsStateQueryResponse::Error(e), - )) => Err(e), - _ => Err(QueryError::internal_error("Unexpected response type")), + )) => Err(e.into()), + _ => Err(QueryError::internal_error( + "Unexpected message type while retrieving account withdrawals", + )), }, ) .await?; + let Some(withdrawals) = withdrawals else { + return Err(RESTError::not_found("Account not found")); + }; + // Get TxHashes from TxIdentifiers let tx_ids: Vec<_> = withdrawals.iter().map(|r| r.tx_identifier).collect(); - let tx_hashes = get_transaction_hashes(&context, &handlers_config, tx_ids).await?; + let msg = Arc::new(Message::StateQuery(StateQuery::Blocks( + BlocksStateQuery::GetTransactionHashes { tx_ids }, + ))); + let tx_hashes = query_state( + &context, + &handlers_config.blocks_query_topic, + msg, + |message| match message { + Message::StateQueryResponse(StateQueryResponse::Blocks( + BlocksStateQueryResponse::TransactionHashes(TransactionHashes { tx_hashes }), + )) => Ok(tx_hashes), + Message::StateQueryResponse(StateQueryResponse::Blocks( + BlocksStateQueryResponse::Error(e), + )) => Err(e.into()), + _ => Err(QueryError::internal_error( + "Unexpected message type while resolving transaction hashes", + )), + }, + ) + .await?; let mut rest_response = Vec::new(); for w in withdrawals { - let tx_hash = tx_hashes.get(&w.tx_identifier).ok_or_else(|| { - RESTError::InternalServerError("Missing tx hash for withdrawal".to_string()) - })?; + let Some(tx_hash) = tx_hashes.get(&w.tx_identifier) else { + return Err(RESTError::InternalServerError( + "Missing tx hash for withdrawal".to_string(), + )); + }; rest_response.push(AccountWithdrawalREST { tx_hash: hex::encode(tx_hash), @@ -319,20 +443,29 @@ pub async fn handle_account_rewards_blockfrost( |message| match message { Message::StateQueryResponse(StateQueryResponse::Accounts( AccountsStateQueryResponse::AccountRewardHistory(rewards), - )) => Ok(rewards), + )) => Ok(Some(rewards)), + Message::StateQueryResponse(StateQueryResponse::Accounts( + AccountsStateQueryResponse::Error(QueryError::NotFound { .. }), + )) => Ok(None), Message::StateQueryResponse(StateQueryResponse::Accounts( AccountsStateQueryResponse::Error(e), - )) => Err(e), - _ => Err(QueryError::internal_error("Unexpected response type")), + )) => Err(e.into()), + _ => Err(QueryError::internal_error( + "Unexpected message type while retrieving account rewards", + )), }, ) .await?; + let Some(rewards) = rewards else { + return Err(RESTError::not_found("Account not found")); + }; + let rest_response = rewards .iter() .map(|r| r.try_into()) .collect::, _>>() - .map_err(|e: anyhow::Error| { + .map_err(|e| { RESTError::InternalServerError(format!("Failed to convert reward entry: {e}")) })?; @@ -360,19 +493,28 @@ pub async fn handle_account_addresses_blockfrost( |message| match message { Message::StateQueryResponse(StateQueryResponse::Accounts( AccountsStateQueryResponse::AccountAssociatedAddresses(addresses), - )) => Ok(addresses), + )) => Ok(Some(addresses)), + Message::StateQueryResponse(StateQueryResponse::Accounts( + AccountsStateQueryResponse::Error(QueryError::NotFound { .. }), + )) => Ok(None), Message::StateQueryResponse(StateQueryResponse::Accounts( AccountsStateQueryResponse::Error(e), - )) => Err(e), - _ => Err(QueryError::internal_error("Unexpected response type")), + )) => Err(e.into()), + _ => Err(QueryError::internal_error( + "Unexpected message type while retrieving account addresses", + )), }, ) .await?; + let Some(addresses) = addresses else { + return Err(RESTError::not_found("Account not found")); + }; + let rest_response = addresses .iter() .map(|r| { - Ok::<_, RESTError>(AccountAddressREST { + Ok(AccountAddressREST { address: r .to_string() .map_err(|e| RESTError::InternalServerError(format!("Invalid address: {e}")))?, @@ -385,19 +527,21 @@ pub async fn handle_account_addresses_blockfrost( } fn parse_stake_address(params: &[String]) -> Result { - let stake_key = params.first().ok_or_else(|| RESTError::param_missing("stake address"))?; + let Some(stake_key) = params.first() else { + return Err(RESTError::param_missing("stake address")); + }; StakeAddress::from_string(stake_key) - .map_err(|_| RESTError::invalid_param("stake address", stake_key)) + .map_err(|_| RESTError::invalid_param("stake address", "not a valid stake address")) } -fn map_drep_choice(drep: &DRepChoice) -> Result { +fn map_drep_choice(drep: &DRepChoice) -> Result { match drep { DRepChoice::Key(hash) => { let val = hash .to_vec() .to_bech32_with_hrp("drep") - .map_err(|e| anyhow!("Bech32 encoding failed for DRep Key: {e}"))?; + .map_err(|e| RESTError::encoding_failed(&format!("DRep Key: {e}")))?; Ok(DRepChoiceRest { drep_type: "Key".to_string(), value: Some(val), @@ -407,7 +551,7 @@ fn map_drep_choice(drep: &DRepChoice) -> Result { let val = hash .to_vec() .to_bech32_with_hrp("drep_script") - .map_err(|e| anyhow!("Bech32 encoding failed for DRep Script: {e}"))?; + .map_err(|e| RESTError::encoding_failed(&format!("DRep Script: {e}")))?; Ok(DRepChoiceRest { drep_type: "Script".to_string(), value: Some(val), @@ -423,32 +567,3 @@ fn map_drep_choice(drep: &DRepChoice) -> Result { }), } } - -/// Helper to fetch transaction hashes (used by multiple handlers) -async fn get_transaction_hashes( - context: &Arc>, - handlers_config: &Arc, - tx_ids: Vec, -) -> Result, RESTError> { - let msg = Arc::new(Message::StateQuery(StateQuery::Blocks( - BlocksStateQuery::GetTransactionHashes { tx_ids }, - ))); - - let result = query_state( - context, - &handlers_config.blocks_query_topic, - msg, - |message| match message { - Message::StateQueryResponse(StateQueryResponse::Blocks( - BlocksStateQueryResponse::TransactionHashes(TransactionHashes { tx_hashes }), - )) => Ok(tx_hashes), - Message::StateQueryResponse(StateQueryResponse::Blocks( - BlocksStateQueryResponse::Error(e), - )) => Err(e), - _ => Err(QueryError::internal_error("Unexpected response type")), - }, - ) - .await?; - - Ok(result) -} diff --git a/modules/rest_blockfrost/src/handlers/addresses.rs b/modules/rest_blockfrost/src/handlers/addresses.rs index 6c14e364..490f6ef0 100644 --- a/modules/rest_blockfrost/src/handlers/addresses.rs +++ b/modules/rest_blockfrost/src/handlers/addresses.rs @@ -1,4 +1,3 @@ -use anyhow::Result; use std::sync::Arc; use crate::{handlers_config::HandlersConfig, types::AddressInfoREST}; @@ -22,35 +21,26 @@ pub async fn handle_address_single_blockfrost( handlers_config: Arc, ) -> Result { let [address_str] = ¶ms[..] else { - return Ok(RESTResponse::with_text(400, "Missing address parameter")); + return Err(RESTError::param_missing("address")); }; let (address, stake_address) = match Address::from_string(address_str) { Ok(Address::None) | Ok(Address::Stake(_)) => { - return Ok(RESTResponse::with_text( - 400, - &format!("Invalid address '{address_str}'"), + return Err(RESTError::invalid_param( + "address", + "must be a payment address", )); } Ok(Address::Byron(byron)) => (Address::Byron(byron), None), Ok(Address::Shelley(shelley)) => { - let stake_addr = match shelley.stake_address_string() { - Ok(stake_addr) => stake_addr, - Err(e) => { - return Ok(RESTResponse::with_text( - 400, - &format!("Invalid address '{address_str}': {e}"), - )); - } - }; + let stake_addr = shelley + .stake_address_string() + .map_err(|e| RESTError::invalid_param("address", &e.to_string()))?; (Address::Shelley(shelley), stake_addr) } Err(e) => { - return Ok(RESTResponse::with_text( - 400, - &format!("Invalid address '{}': {e}", params[0]), - )); + return Err(RESTError::invalid_param("address", &e.to_string())); } }; @@ -61,7 +51,7 @@ pub async fn handle_address_single_blockfrost( AddressStateQuery::GetAddressUTxOs { address }, ))); - let utxo_query_result = query_state( + let utxo_identifiers = query_state( &context, &handlers_config.addresses_query_topic, address_query_msg, @@ -69,20 +59,23 @@ pub async fn handle_address_single_blockfrost( Message::StateQueryResponse(StateQueryResponse::Addresses( AddressStateQueryResponse::AddressUTxOs(utxo_identifiers), )) => Ok(Some(utxo_identifiers)), - - Message::StateQueryResponse(StateQueryResponse::UTxOs( - UTxOStateQueryResponse::Error(e), + Message::StateQueryResponse(StateQueryResponse::Addresses( + AddressStateQueryResponse::Error(QueryError::NotFound { .. }), + )) => Ok(None), + Message::StateQueryResponse(StateQueryResponse::Addresses( + AddressStateQueryResponse::Error(e), )) => Err(e), _ => Err(QueryError::internal_error( - "Unexpected response from addresses query", + "Unexpected message type while retrieving address UTxOs", )), }, ) - .await; + .await?; - let utxo_identifiers = match utxo_query_result { - Ok(Some(utxo_identifiers)) => utxo_identifiers, - Ok(None) => { + let utxo_identifiers = match utxo_identifiers { + Some(identifiers) => identifiers, + None => { + // Empty address - return zero balance (Blockfrost behavior) let rest_response = AddressInfoREST { address: address_str.to_string(), amount: Value { @@ -95,13 +88,9 @@ pub async fn handle_address_single_blockfrost( script: is_script, }; - let json = serde_json::to_string_pretty(&rest_response).map_err(|e| { - RESTError::InternalServerError(format!("JSON serialization error: {e}")) - })?; - + let json = serde_json::to_string_pretty(&rest_response)?; return Ok(RESTResponse::with_json(200, &json)); } - Err(e) => return Ok(RESTResponse::with_text(500, &format!("Query failed: {e}"))), }; let utxos_query_msg = Arc::new(Message::StateQuery(StateQuery::UTxOs( @@ -120,11 +109,11 @@ pub async fn handle_address_single_blockfrost( UTxOStateQueryResponse::Error(e), )) => Err(e), _ => Err(QueryError::internal_error( - "Unexpected response querying UTxOs", + "Unexpected message type while retrieving UTxO sum", )), }, ) - .await?; // Let middleware handle the error + .await?; let rest_response = AddressInfoREST { address: address_str.to_string(), @@ -134,13 +123,8 @@ pub async fn handle_address_single_blockfrost( script: is_script, }; - match serde_json::to_string_pretty(&rest_response) { - Ok(json) => Ok(RESTResponse::with_json(200, &json)), - Err(e) => Ok(RESTResponse::with_text( - 500, - &format!("Internal server error while retrieving address info: {e}"), - )), - } + let json = serde_json::to_string_pretty(&rest_response)?; + Ok(RESTResponse::with_json(200, &json)) } /// Handle `/addresses/{address}/extended` Blockfrost-compatible endpoint @@ -149,7 +133,7 @@ pub async fn handle_address_extended_blockfrost( _params: Vec, _handlers_config: Arc, ) -> Result { - Ok(RESTResponse::with_text(501, "Not implemented")) + Err(RESTError::not_implemented("Address extended endpoint")) } /// Handle `/addresses/{address}/totals` Blockfrost-compatible endpoint @@ -158,7 +142,7 @@ pub async fn handle_address_totals_blockfrost( _params: Vec, _handlers_config: Arc, ) -> Result { - Ok(RESTResponse::with_text(501, "Not implemented")) + Err(RESTError::not_implemented("Address totals endpoint")) } /// Handle `/addresses/{address}/utxos` Blockfrost-compatible endpoint @@ -167,7 +151,7 @@ pub async fn handle_address_utxos_blockfrost( _params: Vec, _handlers_config: Arc, ) -> Result { - Ok(RESTResponse::with_text(501, "Not implemented")) + Err(RESTError::not_implemented("Address UTxOs endpoint")) } /// Handle `/addresses/{address}/utxos/{asset}` Blockfrost-compatible endpoint @@ -176,7 +160,7 @@ pub async fn handle_address_asset_utxos_blockfrost( _params: Vec, _handlers_config: Arc, ) -> Result { - Ok(RESTResponse::with_text(501, "Not implemented")) + Err(RESTError::not_implemented("Address asset UTxOs endpoint")) } /// Handle `/addresses/{address}/transactions` Blockfrost-compatible endpoint @@ -185,5 +169,5 @@ pub async fn handle_address_transactions_blockfrost( _params: Vec, _handlers_config: Arc, ) -> Result { - Ok(RESTResponse::with_text(501, "Not implemented")) + Err(RESTError::not_implemented("Address transactions endpoint")) } diff --git a/modules/rest_blockfrost/src/handlers/assets.rs b/modules/rest_blockfrost/src/handlers/assets.rs index 4383e6e2..b5b744df 100644 --- a/modules/rest_blockfrost/src/handlers/assets.rs +++ b/modules/rest_blockfrost/src/handlers/assets.rs @@ -5,11 +5,14 @@ use crate::{ PolicyAssetRest, }, }; -use acropolis_common::queries::assets::{AssetsStateQuery, AssetsStateQueryResponse}; +use acropolis_common::queries::errors::QueryError; use acropolis_common::rest_error::RESTError; use acropolis_common::{ messages::{Message, RESTResponse, StateQuery, StateQueryResponse}, - queries::{errors::QueryError, utils::query_state}, + queries::{ + assets::{AssetsStateQuery, AssetsStateQueryResponse}, + utils::query_state, + }, serialization::Bech32WithHrp, AssetMetadataStandard, AssetName, PolicyId, }; @@ -61,20 +64,15 @@ pub async fn handle_asset_single_blockfrost( let asset = params[0].clone(); let (policy, name) = split_policy_and_asset(&asset)?; - let asset_query_msg = Arc::new(Message::StateQuery(StateQuery::Assets( - AssetsStateQuery::GetAssetInfo { policy, name }, - ))); - let (policy_str, name_str) = asset.split_at(56); - let bytes = hex::decode(&asset).map_err(|_| RESTError::invalid_hex())?; - + let bytes = hex::decode(&asset)?; let mut hasher = Blake2b::::new(); hasher.update(&bytes); let hash: Vec = hasher.finalize().to_vec(); let fingerprint = hash .to_bech32_with_hrp("asset") - .map_err(|e| RESTError::encoding_failed(&format!("asset fingerprint: {}", e)))?; + .map_err(|e| RESTError::encoding_failed(&format!("asset fingerprint: {e}")))?; let off_chain_metadata = fetch_asset_metadata(&asset, &handlers_config.offchain_token_registry_url).await; @@ -82,14 +80,21 @@ pub async fn handle_asset_single_blockfrost( let policy_id = policy_str.to_string(); let asset_name = name_str.to_string(); + let asset_query_msg = Arc::new(Message::StateQuery(StateQuery::Assets( + AssetsStateQuery::GetAssetInfo { policy, name }, + ))); + let (quantity, info) = query_state( &context, &handlers_config.assets_query_topic, asset_query_msg, - |message| match message { + move |message| match message { + Message::StateQueryResponse(StateQueryResponse::Assets( + AssetsStateQueryResponse::AssetInfo((quantity, info)), + )) => Ok((quantity, info)), Message::StateQueryResponse(StateQueryResponse::Assets( - AssetsStateQueryResponse::AssetInfo(data), - )) => Ok(data), + AssetsStateQueryResponse::Error(QueryError::NotFound { .. }), + )) => Err(QueryError::not_found("Asset")), Message::StateQueryResponse(StateQueryResponse::Assets( AssetsStateQueryResponse::Error(e), )) => Err(e), @@ -146,6 +151,9 @@ pub async fn handle_asset_history_blockfrost( Message::StateQueryResponse(StateQueryResponse::Assets( AssetsStateQueryResponse::AssetHistory(history), )) => Ok(history), + Message::StateQueryResponse(StateQueryResponse::Assets( + AssetsStateQueryResponse::Error(QueryError::NotFound { .. }), + )) => Err(QueryError::not_found("Asset history")), Message::StateQueryResponse(StateQueryResponse::Assets( AssetsStateQueryResponse::Error(e), )) => Err(e), @@ -180,6 +188,9 @@ pub async fn handle_asset_transactions_blockfrost( Message::StateQueryResponse(StateQueryResponse::Assets( AssetsStateQueryResponse::AssetTransactions(txs), )) => Ok(txs), + Message::StateQueryResponse(StateQueryResponse::Assets( + AssetsStateQueryResponse::Error(QueryError::NotFound { .. }), + )) => Err(QueryError::not_found("Asset")), Message::StateQueryResponse(StateQueryResponse::Assets( AssetsStateQueryResponse::Error(e), )) => Err(e), @@ -224,6 +235,9 @@ pub async fn handle_asset_addresses_blockfrost( Message::StateQueryResponse(StateQueryResponse::Assets( AssetsStateQueryResponse::AssetAddresses(addresses), )) => Ok(addresses), + Message::StateQueryResponse(StateQueryResponse::Assets( + AssetsStateQueryResponse::Error(QueryError::NotFound { .. }), + )) => Err(QueryError::not_found("Asset")), Message::StateQueryResponse(StateQueryResponse::Assets( AssetsStateQueryResponse::Error(e), )) => Err(e), @@ -234,9 +248,9 @@ pub async fn handle_asset_addresses_blockfrost( ) .await?; - let rest_addrs: Vec = + let rest_addrs = addresses.iter().map(AssetAddressRest::try_from).collect::, _>>().map_err( - |e| RESTError::InternalServerError(format!("Failed to convert address entry: {}", e)), + |e| RESTError::InternalServerError(format!("Failed to convert address entry: {e}")), )?; let json = serde_json::to_string_pretty(&rest_addrs)?; @@ -249,7 +263,7 @@ pub async fn handle_policy_assets_blockfrost( handlers_config: Arc, ) -> Result { let policy: PolicyId = <[u8; 28]>::from_hex(¶ms[0]) - .map_err(|_| RESTError::invalid_param("policy_id", "invalid hex format"))?; + .map_err(|_| RESTError::invalid_param("policy_id", "invalid hex"))?; let asset_query_msg = Arc::new(Message::StateQuery(StateQuery::Assets( AssetsStateQuery::GetPolicyIdAssets { policy }, @@ -263,6 +277,9 @@ pub async fn handle_policy_assets_blockfrost( Message::StateQueryResponse(StateQueryResponse::Assets( AssetsStateQueryResponse::PolicyIdAssets(assets), )) => Ok(assets), + Message::StateQueryResponse(StateQueryResponse::Assets( + AssetsStateQueryResponse::Error(QueryError::NotFound { .. }), + )) => Err(QueryError::not_found("Policy assets")), Message::StateQueryResponse(StateQueryResponse::Assets( AssetsStateQueryResponse::Error(e), )) => Err(e), @@ -279,7 +296,7 @@ pub async fn handle_policy_assets_blockfrost( } fn split_policy_and_asset(hex_str: &str) -> Result<(PolicyId, AssetName), RESTError> { - let decoded = hex::decode(hex_str).map_err(|_| RESTError::invalid_hex())?; + let decoded = hex::decode(hex_str)?; if decoded.len() < 28 { return Err(RESTError::BadRequest( @@ -450,9 +467,6 @@ mod tests { fn invalid_hex_string() { let result = split_policy_and_asset("zzzz"); assert!(result.is_err()); - let err = result.unwrap_err(); - assert_eq!(err.status_code(), 400); - assert_eq!(err.message(), "Invalid hex string"); } #[test] @@ -460,9 +474,6 @@ mod tests { let hex_str = hex::encode([1u8, 2, 3]); let result = split_policy_and_asset(&hex_str); assert!(result.is_err()); - let err = result.unwrap_err(); - assert_eq!(err.status_code(), 400); - assert_eq!(err.message(), "Asset identifier must be at least 28 bytes"); } #[test] @@ -472,9 +483,6 @@ mod tests { let hex_str = hex::encode(bytes); let result = split_policy_and_asset(&hex_str); assert!(result.is_err()); - let err = result.unwrap_err(); - assert_eq!(err.status_code(), 400); - assert_eq!(err.message(), "Asset name must be less than 32 bytes"); } #[test] diff --git a/modules/rest_blockfrost/src/handlers/epochs.rs b/modules/rest_blockfrost/src/handlers/epochs.rs index b63e72be..b62b6791 100644 --- a/modules/rest_blockfrost/src/handlers/epochs.rs +++ b/modules/rest_blockfrost/src/handlers/epochs.rs @@ -4,6 +4,7 @@ use crate::{ EpochActivityRest, ProtocolParamsRest, SPDDByEpochAndPoolItemRest, SPDDByEpochItemRest, }, }; +use acropolis_common::queries::errors::QueryError; use acropolis_common::rest_error::RESTError; use acropolis_common::serialization::Bech32Conversion; use acropolis_common::{ @@ -11,7 +12,6 @@ use acropolis_common::{ queries::{ accounts::{AccountsStateQuery, AccountsStateQueryResponse}, epochs::{EpochsStateQuery, EpochsStateQueryResponse}, - errors::QueryError, parameters::{ParametersStateQuery, ParametersStateQueryResponse}, pools::{PoolsStateQuery, PoolsStateQueryResponse}, spdd::{SPDDStateQuery, SPDDStateQueryResponse}, @@ -40,7 +40,7 @@ pub async fn handle_epoch_info_blockfrost( } else { let parsed = param .parse::() - .map_err(|_| RESTError::invalid_param("epoch", "must be a valid number"))?; + .map_err(|_| RESTError::invalid_param("epoch", "invalid epoch number"))?; EpochsStateQuery::GetEpochInfo { epoch_number: parsed, } @@ -64,11 +64,16 @@ pub async fn handle_epoch_info_blockfrost( let ea_message = match epoch_info_response { EpochsStateQueryResponse::LatestEpoch(response) => response.epoch, EpochsStateQueryResponse::EpochInfo(response) => response.epoch, - EpochsStateQueryResponse::Error(e) => return Err(e.into()), + EpochsStateQueryResponse::Error(QueryError::NotFound { .. }) => { + return Err(RESTError::not_found("Epoch not found")); + } + EpochsStateQueryResponse::Error(e) => { + return Err(e.into()); + } _ => { return Err(RESTError::unexpected_response( - "Unexpected response type while retrieving epoch info", - )) + "Unexpected message type while retrieving epoch info", + )); } }; let epoch_number = ea_message.epoch; @@ -109,18 +114,17 @@ pub async fn handle_epoch_info_blockfrost( total_active_stakes_msg, |message| match message { Message::StateQueryResponse(StateQueryResponse::SPDD( - SPDDStateQueryResponse::EpochTotalActiveStakes(total_active_stakes), - )) => Ok(total_active_stakes), + SPDDStateQueryResponse::EpochTotalActiveStakes(total_active_stakes), + )) => Ok(total_active_stakes), Message::StateQueryResponse(StateQueryResponse::SPDD( - SPDDStateQueryResponse::Error(e), - )) => Err(e), - _ => Err(QueryError::internal_error(format!( - "Unexpected message type while retrieving total active stakes for epoch: {}", - epoch_number - ))), + SPDDStateQueryResponse::Error(e), + )) => Err(e), + _ => Err(QueryError::internal_error( + format!("Unexpected message type while retrieving total active stakes for epoch: {epoch_number}"), + )), }, ) - .await? + .await? }; let mut response = EpochActivityRest::from(ea_message); @@ -177,7 +181,7 @@ pub async fn handle_epoch_params_blockfrost( } else { let parsed = param .parse::() - .map_err(|_| RESTError::invalid_param("epoch", "must be a valid number"))?; + .map_err(|_| RESTError::invalid_param("epoch", "invalid epoch number"))?; query = ParametersStateQuery::GetEpochParameters { epoch_number: parsed, }; @@ -216,9 +220,12 @@ pub async fn handle_epoch_params_blockfrost( let json = serde_json::to_string_pretty(&rest)?; Ok(RESTResponse::with_json(200, &json)) } + ParametersStateQueryResponse::Error(QueryError::NotFound { .. }) => Err( + RESTError::not_found("Protocol parameters not found for requested epoch"), + ), ParametersStateQueryResponse::Error(e) => Err(e.into()), _ => Err(RESTError::unexpected_response( - "Unexpected response type while retrieving parameters", + "Unexpected message type while retrieving parameters", )), } } @@ -237,7 +244,7 @@ pub async fn handle_epoch_next_blockfrost( let parsed = param .parse::() - .map_err(|_| RESTError::invalid_param("epoch", "must be a valid number"))?; + .map_err(|_| RESTError::invalid_param("epoch", "invalid epoch number"))?; let next_epochs_msg = Arc::new(Message::StateQuery(StateQuery::Epochs( EpochsStateQuery::GetNextEpochs { @@ -252,6 +259,9 @@ pub async fn handle_epoch_next_blockfrost( Message::StateQueryResponse(StateQueryResponse::Epochs( EpochsStateQueryResponse::NextEpochs(response), )) => Ok(response.epochs.into_iter().map(EpochActivityRest::from).collect::>()), + Message::StateQueryResponse(StateQueryResponse::Epochs( + EpochsStateQueryResponse::Error(QueryError::NotFound { .. }), + )) => Err(QueryError::not_found("Epoch")), Message::StateQueryResponse(StateQueryResponse::Epochs( EpochsStateQueryResponse::Error(e), )) => Err(e), @@ -280,7 +290,7 @@ pub async fn handle_epoch_previous_blockfrost( let parsed = param .parse::() - .map_err(|_| RESTError::invalid_param("epoch", "must be a valid number"))?; + .map_err(|_| RESTError::invalid_param("epoch", "invalid epoch number"))?; let previous_epochs_msg = Arc::new(Message::StateQuery(StateQuery::Epochs( EpochsStateQuery::GetPreviousEpochs { @@ -295,6 +305,9 @@ pub async fn handle_epoch_previous_blockfrost( Message::StateQueryResponse(StateQueryResponse::Epochs( EpochsStateQueryResponse::PreviousEpochs(response), )) => Ok(response.epochs.into_iter().map(EpochActivityRest::from).collect::>()), + Message::StateQueryResponse(StateQueryResponse::Epochs( + EpochsStateQueryResponse::Error(QueryError::NotFound { .. }), + )) => Err(QueryError::not_found("Epoch")), Message::StateQueryResponse(StateQueryResponse::Epochs( EpochsStateQueryResponse::Error(e), )) => Err(e), @@ -323,7 +336,7 @@ pub async fn handle_epoch_total_stakes_blockfrost( let epoch_number = param .parse::() - .map_err(|_| RESTError::invalid_param("epoch", "must be a valid number"))?; + .map_err(|_| RESTError::invalid_param("epoch", "invalid epoch number"))?; // Query latest epoch from epochs-state let latest_epoch_msg = Arc::new(Message::StateQuery(StateQuery::Epochs( @@ -375,12 +388,15 @@ pub async fn handle_epoch_total_stakes_blockfrost( ) .await?; - let spdd_response: Vec = spdd + let spdd_response = spdd .into_iter() .map(|(pool_id, stake_address, amount)| { - let bech32 = stake_address - .to_string() - .map_err(|e| RESTError::encoding_failed(&format!("stake address: {}", e)))?; + let bech32 = stake_address.to_string().map_err(|e| { + RESTError::InternalServerError(format!( + "Failed to convert stake address to string: {}", + e + )) + })?; Ok(SPDDByEpochItemRest { pool_id, stake_address: bech32, @@ -404,18 +420,14 @@ pub async fn handle_epoch_pool_stakes_blockfrost( )); } let param = ¶ms[0]; - let pool_id = ¶ms[1]; + let pool_id_str = ¶ms[1]; let epoch_number = param .parse::() - .map_err(|_| RESTError::invalid_param("epoch", "must be a valid number"))?; + .map_err(|_| RESTError::invalid_param("epoch", "invalid epoch number"))?; - let pool_id = PoolId::from_bech32(pool_id).map_err(|_| { - RESTError::invalid_param( - "pool_id", - &format!("Invalid Bech32 stake pool ID: {}", pool_id), - ) - })?; + let pool_id = PoolId::from_bech32(pool_id_str) + .map_err(|_| RESTError::invalid_param("pool_id", "invalid Bech32 stake pool ID"))?; // Query latest epoch from epochs-state let latest_epoch_msg = Arc::new(Message::StateQuery(StateQuery::Epochs( @@ -468,12 +480,15 @@ pub async fn handle_epoch_pool_stakes_blockfrost( ) .await?; - let spdd_response: Vec = spdd + let spdd_response = spdd .into_iter() .map(|(stake_address, amount)| { - let bech32 = stake_address - .to_string() - .map_err(|e| RESTError::encoding_failed(&format!("stake address: {}", e)))?; + let bech32 = stake_address.to_string().map_err(|e| { + RESTError::InternalServerError(format!( + "Failed to convert stake address to string: {}", + e + )) + })?; Ok(SPDDByEpochAndPoolItemRest { stake_address: bech32, amount, @@ -490,7 +505,7 @@ pub async fn handle_epoch_total_blocks_blockfrost( _params: Vec, _handlers_config: Arc, ) -> Result { - Err(RESTError::not_implemented("Endpoint not yet implemented")) + Err(RESTError::not_implemented("Epoch total blocks endpoint")) } pub async fn handle_epoch_pool_blocks_blockfrost( @@ -508,14 +523,10 @@ pub async fn handle_epoch_pool_blocks_blockfrost( let epoch_number = epoch_number_param .parse::() - .map_err(|_| RESTError::invalid_param("epoch", "must be a valid number"))?; + .map_err(|_| RESTError::invalid_param("epoch", "invalid epoch number"))?; - let spo = PoolId::from_bech32(pool_id_param).map_err(|_| { - RESTError::invalid_param( - "pool_id", - &format!("Invalid Bech32 stake pool ID: {}", pool_id_param), - ) - })?; + let spo = PoolId::from_bech32(pool_id_param) + .map_err(|_| RESTError::invalid_param("pool_id", "invalid Bech32 stake pool ID"))?; // query Pool's Blocks by epoch from spo-state let msg = Arc::new(Message::StateQuery(StateQuery::Pools( @@ -536,9 +547,7 @@ pub async fn handle_epoch_pool_blocks_blockfrost( Message::StateQueryResponse(StateQueryResponse::Pools( PoolsStateQueryResponse::Error(e), )) => Err(e), - _ => Err(QueryError::internal_error( - "Unexpected message type while retrieving pool block hashes by epoch", - )), + _ => Err(QueryError::internal_error("Unexpected message type")), }, ) .await?; @@ -546,6 +555,7 @@ pub async fn handle_epoch_pool_blocks_blockfrost( // NOTE: // Need to query chain_store // to get block_hash for each block height + let json = serde_json::to_string_pretty(&blocks)?; Ok(RESTResponse::with_json(200, &json)) } diff --git a/modules/rest_blockfrost/src/handlers/governance.rs b/modules/rest_blockfrost/src/handlers/governance.rs index e9a8ef12..528c4160 100644 --- a/modules/rest_blockfrost/src/handlers/governance.rs +++ b/modules/rest_blockfrost/src/handlers/governance.rs @@ -4,14 +4,13 @@ use crate::types::{ DRepInfoREST, DRepMetadataREST, DRepUpdateREST, DRepVoteREST, DRepsListREST, ProposalVoteREST, VoterRoleREST, }; +use acropolis_common::queries::errors::QueryError; use acropolis_common::rest_error::RESTError; use acropolis_common::{ messages::{Message, RESTResponse, StateQuery, StateQueryResponse}, queries::{ accounts::{AccountsStateQuery, AccountsStateQueryResponse}, - errors::QueryError, governance::{GovernanceStateQuery, GovernanceStateQueryResponse}, - utils::query_state, }, Credential, GovActionId, TxHash, Voter, }; @@ -29,37 +28,45 @@ pub async fn handle_dreps_list_blockfrost( GovernanceStateQuery::GetDRepsList, ))); - let list = query_state( - &context, - &handlers_config.dreps_query_topic, - msg, - |message| match message { - Message::StateQueryResponse(StateQueryResponse::Governance( - GovernanceStateQueryResponse::DRepsList(list), - )) => Ok(list), - Message::StateQueryResponse(StateQueryResponse::Governance( - GovernanceStateQueryResponse::Error(e), - )) => Err(e), - _ => Err(QueryError::internal_error("Unexpected message type")), - }, - ) - .await?; - - let response: Vec = list - .dreps - .iter() - .map(|cred| { - Ok(DRepsListREST { - drep_id: cred - .to_drep_bech32() - .map_err(|e| RESTError::encoding_failed(&format!("DRep ID: {}", e)))?, - hex: hex::encode(cred.get_hash()), - }) - }) - .collect::, RESTError>>()?; - - let json = serde_json::to_string_pretty(&response)?; - Ok(RESTResponse::with_json(200, &json)) + let raw_msg = context + .message_bus + .request(&handlers_config.dreps_query_topic, msg) + .await + .map_err(|e| RESTError::InternalServerError(format!("Message bus error: {e}")))?; + let message = Arc::try_unwrap(raw_msg).unwrap_or_else(|arc| (*arc).clone()); + + match message { + Message::StateQueryResponse(StateQueryResponse::Governance( + GovernanceStateQueryResponse::DRepsList(list), + )) => { + let response: Result, RESTError> = list + .dreps + .iter() + .map(|cred| { + Ok(DRepsListREST { + drep_id: cred + .to_drep_bech32() + .map_err(|e| RESTError::encoding_failed(&format!("DRep ID: {e}")))?, + hex: hex::encode(cred.get_hash()), + }) + }) + .collect(); + + let response = response?; + let json = serde_json::to_string_pretty(&response)?; + Ok(RESTResponse::with_json(200, &json)) + } + + Message::StateQueryResponse(StateQueryResponse::Governance( + GovernanceStateQueryResponse::Error(QueryError::NotFound { .. }), + )) => Err(RESTError::not_found("No DReps found")), + + Message::StateQueryResponse(StateQueryResponse::Governance( + GovernanceStateQueryResponse::Error(e), + )) => Err(e.into()), + + _ => Err(RESTError::unexpected_response("Unexpected message type")), + } } pub async fn handle_single_drep_blockfrost( @@ -67,7 +74,9 @@ pub async fn handle_single_drep_blockfrost( params: Vec, handlers_config: Arc, ) -> Result { - let drep_id = params.first().ok_or_else(|| RESTError::param_missing("drep_id"))?; + let Some(drep_id) = params.first() else { + return Err(RESTError::param_missing("DRep ID")); + }; let credential = parse_drep_credential(drep_id)?; @@ -77,61 +86,78 @@ pub async fn handle_single_drep_blockfrost( }, ))); - let response = query_state( - &context, - &handlers_config.dreps_query_topic, - msg, - |message| match message { - Message::StateQueryResponse(StateQueryResponse::Governance( - GovernanceStateQueryResponse::DRepInfoWithDelegators(response), - )) => Ok(response), - Message::StateQueryResponse(StateQueryResponse::Governance( - GovernanceStateQueryResponse::Error(e), - )) => Err(e), - _ => Err(QueryError::internal_error("Unexpected message type")), - }, - ) - .await?; + let raw_msg = context + .message_bus + .request(&handlers_config.dreps_query_topic, msg) + .await + .map_err(|e| RESTError::InternalServerError(format!("Message bus error: {e}")))?; + let message = Arc::try_unwrap(raw_msg).unwrap_or_else(|arc| (*arc).clone()); + + match message { + Message::StateQueryResponse(StateQueryResponse::Governance( + GovernanceStateQueryResponse::DRepInfoWithDelegators(response), + )) => { + let active = !response.info.retired && !response.info.expired; + + let stake_addresses = response.delegators.clone(); + + let sum_msg = Arc::new(Message::StateQuery(StateQuery::Accounts( + AccountsStateQuery::GetAccountsBalancesSum { stake_addresses }, + ))); + + let raw_sum = context + .message_bus + .request(&handlers_config.accounts_query_topic, sum_msg) + .await + .map_err(|e| RESTError::InternalServerError(format!("Message bus error: {e}")))?; + let sum_response = Arc::try_unwrap(raw_sum).unwrap_or_else(|arc| (*arc).clone()); + + let amount = match sum_response { + Message::StateQueryResponse(StateQueryResponse::Accounts( + AccountsStateQueryResponse::AccountsBalancesSum(sum), + )) => sum.to_string(), + + Message::StateQueryResponse(StateQueryResponse::Accounts( + AccountsStateQueryResponse::Error(e), + )) => { + return Err(RESTError::InternalServerError(format!( + "Failed to sum balances: {e}" + ))); + } + + _ => { + return Err(RESTError::unexpected_response( + "Unexpected response from accounts-state", + )); + } + }; - let active = !response.info.retired && !response.info.expired; - let stake_addresses = response.delegators.clone(); + let response = DRepInfoREST { + drep_id: drep_id.to_string(), + hex: hex::encode(credential.get_hash()), + amount, + active, + active_epoch: response.info.active_epoch, + has_script: matches!(credential, Credential::ScriptHash(_)), + last_active_epoch: response.info.last_active_epoch, + retired: response.info.retired, + expired: response.info.expired, + }; - let sum_msg = Arc::new(Message::StateQuery(StateQuery::Accounts( - AccountsStateQuery::GetAccountsBalancesSum { stake_addresses }, - ))); + let json = serde_json::to_string_pretty(&response)?; + Ok(RESTResponse::with_json(200, &json)) + } - let amount = query_state( - &context, - &handlers_config.accounts_query_topic, - sum_msg, - |message| match message { - Message::StateQueryResponse(StateQueryResponse::Accounts( - AccountsStateQueryResponse::AccountsBalancesSum(sum), - )) => Ok(sum.to_string()), - Message::StateQueryResponse(StateQueryResponse::Accounts( - AccountsStateQueryResponse::Error(e), - )) => Err(e), - _ => Err(QueryError::internal_error( - "Unexpected response from accounts-state", - )), - }, - ) - .await?; - - let response = DRepInfoREST { - drep_id: drep_id.to_string(), - hex: hex::encode(credential.get_hash()), - amount, - active, - active_epoch: response.info.active_epoch, - has_script: matches!(credential, Credential::ScriptHash(_)), - last_active_epoch: response.info.last_active_epoch, - retired: response.info.retired, - expired: response.info.expired, - }; + Message::StateQueryResponse(StateQueryResponse::Governance( + GovernanceStateQueryResponse::Error(QueryError::NotFound { .. }), + )) => Err(RESTError::not_found("DRep not found")), - let json = serde_json::to_string_pretty(&response)?; - Ok(RESTResponse::with_json(200, &json)) + Message::StateQueryResponse(StateQueryResponse::Governance( + GovernanceStateQueryResponse::Error(e), + )) => Err(e.into()), + + _ => Err(RESTError::unexpected_response("Unexpected message type")), + } } pub async fn handle_drep_delegators_blockfrost( @@ -139,7 +165,9 @@ pub async fn handle_drep_delegators_blockfrost( params: Vec, handlers_config: Arc, ) -> Result { - let drep_id = params.first().ok_or_else(|| RESTError::param_missing("drep_id"))?; + let Some(drep_id) = params.first() else { + return Err(RESTError::param_missing("DRep ID")); + }; let credential = parse_drep_credential(drep_id)?; @@ -149,62 +177,76 @@ pub async fn handle_drep_delegators_blockfrost( }, ))); - let delegators = query_state( - &context, - &handlers_config.dreps_query_topic, - msg, - |message| match message { - Message::StateQueryResponse(StateQueryResponse::Governance( - GovernanceStateQueryResponse::DRepDelegators(delegators), - )) => Ok(delegators), - Message::StateQueryResponse(StateQueryResponse::Governance( - GovernanceStateQueryResponse::Error(e), - )) => Err(e), - _ => Err(QueryError::internal_error("Unexpected message type")), - }, - ) - .await?; + let raw_msg = context + .message_bus + .request(&handlers_config.dreps_query_topic, msg) + .await + .map_err(|e| RESTError::InternalServerError(format!("Message bus error: {e}")))?; + let message = Arc::try_unwrap(raw_msg).unwrap_or_else(|arc| (*arc).clone()); + + match message { + Message::StateQueryResponse(StateQueryResponse::Governance( + GovernanceStateQueryResponse::DRepDelegators(delegators), + )) => { + let msg = Arc::new(Message::StateQuery(StateQuery::Accounts( + AccountsStateQuery::GetAccountsUtxoValuesMap { + stake_addresses: delegators.addresses.clone(), + }, + ))); + + let raw_msg = context + .message_bus + .request(&handlers_config.accounts_query_topic, msg) + .await + .map_err(|e| RESTError::InternalServerError(format!("Message bus error: {e}")))?; + let message = Arc::try_unwrap(raw_msg).unwrap_or_else(|arc| (*arc).clone()); + + match message { + Message::StateQueryResponse(StateQueryResponse::Accounts( + AccountsStateQueryResponse::AccountsUtxoValuesMap(map), + )) => { + let response: Result, RESTError> = map + .into_iter() + .map(|(stake_address, amount)| { + let bech32 = stake_address.to_string().map_err(|e| { + RESTError::InternalServerError(format!( + "Failed to encode stake address: {}", + e + )) + })?; + + Ok(serde_json::json!({ + "address": bech32, + "amount": amount.to_string(), + })) + }) + .collect(); + + let response = response?; + let json = serde_json::to_string_pretty(&response)?; + Ok(RESTResponse::with_json(200, &json)) + } + + Message::StateQueryResponse(StateQueryResponse::Accounts( + AccountsStateQueryResponse::Error(e), + )) => Err(e.into()), + + _ => Err(RESTError::unexpected_response( + "Unexpected response from accounts-state", + )), + } + } - let msg = Arc::new(Message::StateQuery(StateQuery::Accounts( - AccountsStateQuery::GetAccountsUtxoValuesMap { - stake_addresses: delegators.addresses.clone(), - }, - ))); + Message::StateQueryResponse(StateQueryResponse::Governance( + GovernanceStateQueryResponse::Error(QueryError::NotFound { .. }), + )) => Err(RESTError::not_found("DRep not found")), - let map = query_state( - &context, - &handlers_config.accounts_query_topic, - msg, - |message| match message { - Message::StateQueryResponse(StateQueryResponse::Accounts( - AccountsStateQueryResponse::AccountsUtxoValuesMap(map), - )) => Ok(map), - Message::StateQueryResponse(StateQueryResponse::Accounts( - AccountsStateQueryResponse::Error(e), - )) => Err(e), - _ => Err(QueryError::internal_error( - "Unexpected response from accounts-state", - )), - }, - ) - .await?; - - let response: Vec = map - .into_iter() - .map(|(stake_address, amount)| { - let bech32 = stake_address - .to_string() - .map_err(|e| RESTError::encoding_failed(&format!("stake address: {}", e)))?; - - Ok(serde_json::json!({ - "address": bech32, - "amount": amount.to_string(), - })) - }) - .collect::, RESTError>>()?; - - let json = serde_json::to_string_pretty(&response)?; - Ok(RESTResponse::with_json(200, &json)) + Message::StateQueryResponse(StateQueryResponse::Governance( + GovernanceStateQueryResponse::Error(e), + )) => Err(e.into()), + + _ => Err(RESTError::unexpected_response("Unexpected message type")), + } } pub async fn handle_drep_metadata_blockfrost( @@ -212,7 +254,9 @@ pub async fn handle_drep_metadata_blockfrost( params: Vec, handlers_config: Arc, ) -> Result { - let drep_id = params.first().ok_or_else(|| RESTError::param_missing("drep_id"))?; + let Some(drep_id) = params.first() else { + return Err(RESTError::param_missing("DRep ID")); + }; let credential = parse_drep_credential(drep_id)?; @@ -222,62 +266,61 @@ pub async fn handle_drep_metadata_blockfrost( }, ))); - let metadata = query_state( - &context, - &handlers_config.dreps_query_topic, - msg, - |message| match message { - Message::StateQueryResponse(StateQueryResponse::Governance( - GovernanceStateQueryResponse::DRepMetadata(metadata), - )) => Ok(metadata), - Message::StateQueryResponse(StateQueryResponse::Governance( - GovernanceStateQueryResponse::Error(e), - )) => Err(e), - _ => Err(QueryError::internal_error("Unexpected message type")), + let raw_msg = context + .message_bus + .request(&handlers_config.dreps_query_topic, msg) + .await + .map_err(|e| RESTError::InternalServerError(format!("Message bus error: {e}")))?; + let message = Arc::try_unwrap(raw_msg).unwrap_or_else(|arc| (*arc).clone()); + + match message { + Message::StateQueryResponse(StateQueryResponse::Governance( + GovernanceStateQueryResponse::DRepMetadata(metadata), + )) => match metadata { + None => Err(RESTError::storage_disabled("DRep metadata")), + Some(None) => Err(RESTError::not_found("DRep metadata not found")), + Some(Some(anchor)) => { + let resp = Client::new().get(&anchor.url).send().await.map_err(|_| { + RESTError::InternalServerError("Failed to fetch DRep metadata URL".to_string()) + })?; + + let raw_bytes = resp.bytes().await.map_err(|_| { + RESTError::InternalServerError( + "Failed to read bytes from DRep metadata URL".to_string(), + ) + })?; + + let json = serde_json::from_slice::(&raw_bytes).map_err(|_| { + RESTError::InternalServerError( + "Invalid JSON from DRep metadata URL".to_string(), + ) + })?; + + let bytes_hex = format!("\\x{}", hex::encode(&raw_bytes)); + + let response = DRepMetadataREST { + drep_id: drep_id.to_string(), + hex: hex::encode(credential.get_hash()), + url: anchor.url.clone(), + hash: hex::encode(anchor.data_hash.clone()), + json_metadata: json, + bytes: bytes_hex, + }; + + let json = serde_json::to_string_pretty(&response)?; + Ok(RESTResponse::with_json(200, &json)) + } }, - ) - .await?; - match metadata { - None => { - // metadata feature disabled - Err(RESTError::storage_disabled("DRep metadata")) - } - Some(None) => { - // enabled, but nothing stored for this DRep - Err(RESTError::not_found("DRep metadata not found")) - } - Some(Some(anchor)) => { - // enabled + stored → fetch the JSON - let resp = Client::new().get(&anchor.url).send().await.map_err(|e| { - RESTError::query_failed(&format!("Failed to fetch DRep metadata URL: {}", e)) - })?; - - let raw_bytes = resp.bytes().await.map_err(|e| { - RESTError::query_failed(&format!( - "Failed to read bytes from DRep metadata URL: {}", - e - )) - })?; - - let json: Value = serde_json::from_slice(&raw_bytes).map_err(|_| { - RESTError::BadRequest("Invalid JSON from DRep metadata URL".to_string()) - })?; - - let bytes_hex = format!("\\x{}", hex::encode(&raw_bytes)); - - let response = DRepMetadataREST { - drep_id: drep_id.to_string(), - hex: hex::encode(credential.get_hash()), - url: anchor.url.clone(), - hash: hex::encode(anchor.data_hash.clone()), - json_metadata: json, - bytes: bytes_hex, - }; + Message::StateQueryResponse(StateQueryResponse::Governance( + GovernanceStateQueryResponse::Error(QueryError::NotFound { .. }), + )) => Err(RESTError::not_found("DRep metadata not found")), - let json = serde_json::to_string_pretty(&response)?; - Ok(RESTResponse::with_json(200, &json)) - } + Message::StateQueryResponse(StateQueryResponse::Governance( + GovernanceStateQueryResponse::Error(e), + )) => Err(e.into()), + + _ => Err(RESTError::unexpected_response("Unexpected message type")), } } @@ -286,7 +329,9 @@ pub async fn handle_drep_updates_blockfrost( params: Vec, handlers_config: Arc, ) -> Result { - let drep_id = params.first().ok_or_else(|| RESTError::param_missing("drep_id"))?; + let Some(drep_id) = params.first() else { + return Err(RESTError::param_missing("DRep ID")); + }; let credential = parse_drep_credential(drep_id)?; @@ -296,34 +341,37 @@ pub async fn handle_drep_updates_blockfrost( }, ))); - let list = query_state( - &context, - &handlers_config.dreps_query_topic, - msg, - |message| match message { - Message::StateQueryResponse(StateQueryResponse::Governance( - GovernanceStateQueryResponse::DRepUpdates(list), - )) => Ok(list), - Message::StateQueryResponse(StateQueryResponse::Governance( - GovernanceStateQueryResponse::Error(e), - )) => Err(e), - _ => Err(QueryError::internal_error("Unexpected message type")), - }, - ) - .await?; - - let response: Vec = list - .updates - .iter() - .map(|event| DRepUpdateREST { - tx_hash: "TxHash lookup not yet implemented".to_string(), - cert_index: event.cert_index, - action: event.action.clone(), - }) - .collect(); - - let json = serde_json::to_string_pretty(&response)?; - Ok(RESTResponse::with_json(200, &json)) + let raw_msg = context.message_bus.request(&handlers_config.dreps_query_topic, msg).await?; + let message = Arc::try_unwrap(raw_msg).unwrap_or_else(|arc| (*arc).clone()); + + match message { + Message::StateQueryResponse(StateQueryResponse::Governance( + GovernanceStateQueryResponse::DRepUpdates(list), + )) => { + let response: Vec = list + .updates + .iter() + .map(|event| DRepUpdateREST { + tx_hash: "TxHash lookup not yet implemented".to_string(), + cert_index: event.cert_index, + action: event.action.clone(), + }) + .collect(); + + let json = serde_json::to_string_pretty(&response)?; + Ok(RESTResponse::with_json(200, &json)) + } + + Message::StateQueryResponse(StateQueryResponse::Governance( + GovernanceStateQueryResponse::Error(QueryError::NotFound { .. }), + )) => Err(RESTError::not_found("DRep not found")), + + Message::StateQueryResponse(StateQueryResponse::Governance( + GovernanceStateQueryResponse::Error(e), + )) => Err(e.into()), + + _ => Err(RESTError::unexpected_response("Unexpected message type")), + } } pub async fn handle_drep_votes_blockfrost( @@ -331,7 +379,9 @@ pub async fn handle_drep_votes_blockfrost( params: Vec, handlers_config: Arc, ) -> Result { - let drep_id = params.first().ok_or_else(|| RESTError::param_missing("drep_id"))?; + let Some(drep_id) = params.first() else { + return Err(RESTError::param_missing("DRep ID")); + }; let credential = parse_drep_credential(drep_id)?; @@ -341,34 +391,36 @@ pub async fn handle_drep_votes_blockfrost( }, ))); - let votes = query_state( - &context, - &handlers_config.dreps_query_topic, - msg, - |message| match message { - Message::StateQueryResponse(StateQueryResponse::Governance( - GovernanceStateQueryResponse::DRepVotes(votes), - )) => Ok(votes), - Message::StateQueryResponse(StateQueryResponse::Governance( - GovernanceStateQueryResponse::Error(e), - )) => Err(e), - _ => Err(QueryError::internal_error("Unexpected message type")), - }, - ) - .await?; - - let response: Vec<_> = votes - .votes - .iter() - .map(|vote| DRepVoteREST { - tx_hash: hex::encode(vote.tx_hash), - cert_index: vote.vote_index, - vote: vote.vote.clone(), - }) - .collect(); - - let json = serde_json::to_string_pretty(&response)?; - Ok(RESTResponse::with_json(200, &json)) + let raw_msg = context.message_bus.request(&handlers_config.dreps_query_topic, msg).await?; + let message = Arc::try_unwrap(raw_msg).unwrap_or_else(|arc| (*arc).clone()); + match message { + Message::StateQueryResponse(StateQueryResponse::Governance( + GovernanceStateQueryResponse::DRepVotes(votes), + )) => { + let response: Vec<_> = votes + .votes + .iter() + .map(|vote| DRepVoteREST { + tx_hash: hex::encode(vote.tx_hash), + cert_index: vote.vote_index, + vote: vote.vote.clone(), + }) + .collect(); + + let json = serde_json::to_string_pretty(&response)?; + Ok(RESTResponse::with_json(200, &json)) + } + + Message::StateQueryResponse(StateQueryResponse::Governance( + GovernanceStateQueryResponse::Error(QueryError::NotFound { .. }), + )) => Err(RESTError::not_found("DRep not found")), + + Message::StateQueryResponse(StateQueryResponse::Governance( + GovernanceStateQueryResponse::Error(e), + )) => Err(e.into()), + + _ => Err(RESTError::unexpected_response("Unexpected message type")), + } } pub async fn handle_proposals_list_blockfrost( @@ -380,35 +432,37 @@ pub async fn handle_proposals_list_blockfrost( GovernanceStateQuery::GetProposalsList, ))); - let list = query_state( - &context, - &handlers_config.governance_query_topic, - msg, - |message| match message { - Message::StateQueryResponse(StateQueryResponse::Governance( - GovernanceStateQueryResponse::ProposalsList(list), - )) => Ok(list), - Message::StateQueryResponse(StateQueryResponse::Governance( - GovernanceStateQueryResponse::Error(e), - )) => Err(e), - _ => Err(QueryError::internal_error("Unexpected message type")), - }, - ) - .await?; + let raw_msg = context.message_bus.request(&handlers_config.governance_query_topic, msg).await?; + let message = Arc::try_unwrap(raw_msg).unwrap_or_else(|arc| (*arc).clone()); - if list.proposals.is_empty() { - return Ok(RESTResponse::with_json(200, "[]")); - } + match message { + Message::StateQueryResponse(StateQueryResponse::Governance( + GovernanceStateQueryResponse::ProposalsList(list), + )) => { + if list.proposals.is_empty() { + return Ok(RESTResponse::with_json(200, "[]")); + } + + let props_bech32: Result, _> = + list.proposals.iter().map(|id| id.to_bech32()).collect(); - let props_bech32: Vec = list - .proposals - .iter() - .map(|id| id.to_bech32()) - .collect::, _>>() - .map_err(|e| RESTError::encoding_failed(&format!("proposal IDs: {}", e)))?; + let vec = props_bech32 + .map_err(|e| RESTError::encoding_failed(&format!("proposal IDs to Bech32: {e}")))?; - let json = serde_json::to_string(&props_bech32)?; - Ok(RESTResponse::with_json(200, &json)) + let json = serde_json::to_string(&vec)?; + Ok(RESTResponse::with_json(200, &json)) + } + + Message::StateQueryResponse(StateQueryResponse::Governance( + GovernanceStateQueryResponse::Error(QueryError::NotFound { .. }), + )) => Err(RESTError::not_found("No proposals found")), + + Message::StateQueryResponse(StateQueryResponse::Governance( + GovernanceStateQueryResponse::Error(e), + )) => Err(e.into()), + + _ => Err(RESTError::unexpected_response("Unexpected message type")), + } } pub async fn handle_single_proposal_blockfrost( @@ -422,24 +476,27 @@ pub async fn handle_single_proposal_blockfrost( GovernanceStateQuery::GetProposalInfo { proposal }, ))); - let info = query_state( - &context, - &handlers_config.governance_query_topic, - msg, - |message| match message { - Message::StateQueryResponse(StateQueryResponse::Governance( - GovernanceStateQueryResponse::ProposalInfo(info), - )) => Ok(info), - Message::StateQueryResponse(StateQueryResponse::Governance( - GovernanceStateQueryResponse::Error(e), - )) => Err(e), - _ => Err(QueryError::internal_error("Unexpected message type")), - }, - ) - .await?; + let raw_msg = context.message_bus.request(&handlers_config.governance_query_topic, msg).await?; + let message = Arc::try_unwrap(raw_msg).unwrap_or_else(|arc| (*arc).clone()); + + match message { + Message::StateQueryResponse(StateQueryResponse::Governance( + GovernanceStateQueryResponse::ProposalInfo(info), + )) => { + let json = serde_json::to_string(&info)?; + Ok(RESTResponse::with_json(200, &json)) + } + + Message::StateQueryResponse(StateQueryResponse::Governance( + GovernanceStateQueryResponse::Error(QueryError::NotFound { .. }), + )) => Err(RESTError::not_found("Proposal not found")), - let json = serde_json::to_string(&info)?; - Ok(RESTResponse::with_json(200, &json)) + Message::StateQueryResponse(StateQueryResponse::Governance( + GovernanceStateQueryResponse::Error(e), + )) => Err(e.into()), + + _ => Err(RESTError::unexpected_response("Unexpected message type")), + } } pub async fn handle_proposal_parameters_blockfrost( @@ -447,7 +504,7 @@ pub async fn handle_proposal_parameters_blockfrost( _params: Vec, _handlers_config: Arc, ) -> Result { - Err(RESTError::not_implemented("Endpoint not yet implemented")) + Err(RESTError::not_implemented("Proposal parameters endpoint")) } pub async fn handle_proposal_withdrawals_blockfrost( @@ -455,7 +512,7 @@ pub async fn handle_proposal_withdrawals_blockfrost( _params: Vec, _handlers_config: Arc, ) -> Result { - Err(RESTError::not_implemented("Endpoint not yet implemented")) + Err(RESTError::not_implemented("Proposal withdrawals endpoint")) } pub async fn handle_proposal_votes_blockfrost( @@ -472,46 +529,50 @@ pub async fn handle_proposal_votes_blockfrost( GovernanceStateQuery::GetProposalVotes { proposal }, ))); - let votes = query_state( - &context, - &handlers_config.governance_query_topic, - msg, - |message| match message { - Message::StateQueryResponse(StateQueryResponse::Governance( - GovernanceStateQueryResponse::ProposalVotes(votes), - )) => Ok(votes), - Message::StateQueryResponse(StateQueryResponse::Governance( - GovernanceStateQueryResponse::Error(e), - )) => Err(e), - _ => Err(QueryError::internal_error("Unexpected message type")), - }, - ) - .await?; + let raw_msg = context.message_bus.request(&handlers_config.governance_query_topic, msg).await?; + let message = Arc::try_unwrap(raw_msg).unwrap_or_else(|arc| (*arc).clone()); + + match message { + Message::StateQueryResponse(StateQueryResponse::Governance( + GovernanceStateQueryResponse::ProposalVotes(votes), + )) => { + let mut votes_list = Vec::new(); + + for (voter, (_, voting_proc)) in votes.votes { + let voter_role = match voter { + Voter::ConstitutionalCommitteeKey(_) + | Voter::ConstitutionalCommitteeScript(_) => { + VoterRoleREST::ConstitutionalCommittee + } + Voter::DRepKey(_) | Voter::DRepScript(_) => VoterRoleREST::Drep, + Voter::StakePoolKey(_) => VoterRoleREST::Spo, + }; + + let voter_str = voter.to_string(); + + votes_list.push(ProposalVoteREST { + tx_hash: tx_hash.clone(), + cert_index, + voter_role, + voter: voter_str, + vote: voting_proc.vote, + }); + } - let mut votes_list = Vec::new(); + let json = serde_json::to_string(&votes_list)?; + Ok(RESTResponse::with_json(200, &json)) + } - for (voter, (_, voting_proc)) in votes.votes { - let voter_role = match voter { - Voter::ConstitutionalCommitteeKey(_) | Voter::ConstitutionalCommitteeScript(_) => { - VoterRoleREST::ConstitutionalCommittee - } - Voter::DRepKey(_) | Voter::DRepScript(_) => VoterRoleREST::Drep, - Voter::StakePoolKey(_) => VoterRoleREST::Spo, - }; - - let voter_str = voter.to_string(); - - votes_list.push(ProposalVoteREST { - tx_hash: tx_hash.clone(), - cert_index, - voter_role, - voter: voter_str, - vote: voting_proc.vote, - }); - } + Message::StateQueryResponse(StateQueryResponse::Governance( + GovernanceStateQueryResponse::Error(QueryError::NotFound { .. }), + )) => Err(RESTError::not_found("Proposal not found")), + + Message::StateQueryResponse(StateQueryResponse::Governance( + GovernanceStateQueryResponse::Error(e), + )) => Err(e.into()), - let json = serde_json::to_string(&votes_list)?; - Ok(RESTResponse::with_json(200, &json)) + _ => Err(RESTError::unexpected_response("Unexpected message type")), + } } pub async fn handle_proposal_metadata_blockfrost( @@ -519,7 +580,7 @@ pub async fn handle_proposal_metadata_blockfrost( _params: Vec, _handlers_config: Arc, ) -> Result { - Err(RESTError::not_implemented("Endpoint not yet implemented")) + Err(RESTError::not_implemented("Proposal metadata endpoint")) } pub fn parse_gov_action_id(params: &[String]) -> Result { @@ -532,17 +593,14 @@ pub fn parse_gov_action_id(params: &[String]) -> Result let tx_hash_hex = ¶ms[0]; let cert_index_str = ¶ms[1]; - let bytes = hex::decode(tx_hash_hex) - .map_err(|e| RESTError::invalid_param("tx_hash", &format!("invalid hex: {}", e)))?; - - let transaction_id: TxHash = bytes - .as_slice() - .try_into() - .map_err(|_| RESTError::invalid_param("tx_hash", "must be 32 bytes"))?; + let bytes = hex::decode(tx_hash_hex)?; + let transaction_id: TxHash = bytes.as_slice().try_into().map_err(|_| { + RESTError::invalid_param("tx_hash", "invalid tx_hash length, must be 32 bytes") + })?; let action_index = cert_index_str .parse::() - .map_err(|e| RESTError::invalid_param("cert_index", &format!("expected u8: {}", e)))?; + .map_err(|_| RESTError::invalid_param("cert_index", "expected u8"))?; Ok(GovActionId { transaction_id, @@ -552,5 +610,5 @@ pub fn parse_gov_action_id(params: &[String]) -> Result fn parse_drep_credential(drep_id: &str) -> Result { Credential::from_drep_bech32(drep_id) - .map_err(|e| RESTError::invalid_param("drep_id", &format!("Invalid Bech32 DRep ID: {}", e))) + .map_err(|e| RESTError::invalid_param("drep_id", &format!("invalid Bech32 DRep ID: {e}"))) } diff --git a/modules/rest_blockfrost/src/handlers/pools.rs b/modules/rest_blockfrost/src/handlers/pools.rs index 3f6555d0..27b22c76 100644 --- a/modules/rest_blockfrost/src/handlers/pools.rs +++ b/modules/rest_blockfrost/src/handlers/pools.rs @@ -7,7 +7,7 @@ use crate::{ types::{PoolEpochStateRest, PoolExtendedRest, PoolMetadataRest, PoolRetirementRest}, utils::{fetch_pool_metadata_as_bytes, verify_pool_metadata_hash, PoolMetadataJson}, }; -use acropolis_common::queries::utils::query_state; +use acropolis_common::queries::errors::QueryError; use acropolis_common::rest_error::RESTError; use acropolis_common::serialization::Bech32Conversion; use acropolis_common::{ @@ -15,8 +15,8 @@ use acropolis_common::{ queries::{ accounts::{AccountsStateQuery, AccountsStateQueryResponse}, epochs::{EpochsStateQuery, EpochsStateQueryResponse}, - errors::QueryError, pools::{PoolsStateQuery, PoolsStateQueryResponse}, + utils::query_state, }, rest_helper::ToCheckedF64, PoolId, PoolRetirement, PoolUpdateAction, TxIdentifier, @@ -33,32 +33,34 @@ pub async fn handle_pools_list_blockfrost( _params: Vec, handlers_config: Arc, ) -> Result { + // Prepare the message let msg = Arc::new(Message::StateQuery(StateQuery::Pools( PoolsStateQuery::GetPoolsList, ))); - let pool_operators = query_state( - &context, - &handlers_config.pools_query_topic, - msg, - |message| match message { - Message::StateQueryResponse(StateQueryResponse::Pools( - PoolsStateQueryResponse::PoolsList(pool_operators), - )) => Ok(pool_operators), - Message::StateQueryResponse(StateQueryResponse::Pools( - PoolsStateQueryResponse::Error(e), - )) => Err(e), - _ => Err(QueryError::internal_error("Unexpected message type")), - }, - ) - .await?; + // Send message via message bus + let raw = context.message_bus.request(&handlers_config.pools_query_topic, msg).await?; + // Unwrap and match + let message = Arc::try_unwrap(raw).unwrap_or_else(|arc| (*arc).clone()); - let pool_ids: Vec = pool_operators - .iter() - .map(|operator| operator.to_bech32()) - .collect::, _>>() - .map_err(|e| RESTError::encoding_failed(&format!("pool IDs: {}", e)))?; + let pool_operators = match message { + Message::StateQueryResponse(StateQueryResponse::Pools( + PoolsStateQueryResponse::PoolsList(pool_operators), + )) => pool_operators, + + Message::StateQueryResponse(StateQueryResponse::Pools(PoolsStateQueryResponse::Error( + e, + ))) => { + return Err(e.into()); + } + + _ => return Err(RESTError::unexpected_response("Unexpected message type")), + }; + let pool_ids: Result, _> = + pool_operators.iter().map(|operator| operator.to_bech32()).collect(); + + let pool_ids = pool_ids.map_err(|e| RESTError::encoding_failed(&format!("pool IDs: {e}")))?; let json = serde_json::to_string(&pool_ids)?; Ok(RESTResponse::with_json(200, &json)) } @@ -86,7 +88,7 @@ pub async fn handle_pools_extended_retired_retiring_single_blockfrost( } _ => { let pool_id = PoolId::from_bech32(param).map_err(|e| { - RESTError::invalid_param("pool_id", &format!("Invalid Bech32 stake pool ID: {}", e)) + RESTError::invalid_param("pool_id", &format!("invalid Bech32 stake pool ID: {e}")) })?; handle_pools_spo_blockfrost(context.clone(), pool_id, handlers_config.clone()).await } @@ -175,7 +177,7 @@ async fn handle_pools_extended_blockfrost( // check optimal_pool_sizing is Some let Some(optimal_pool_sizing) = optimal_pool_sizing else { - // if it is before Shelley Era + // if it is before Shelly Era return Ok(RESTResponse::with_json(200, "[]")); }; @@ -199,7 +201,7 @@ async fn handle_pools_extended_blockfrost( PoolsStateQueryResponse::PoolsActiveStakes(active_stakes), )) => Ok(Some(active_stakes)), Message::StateQueryResponse(StateQueryResponse::Pools( - PoolsStateQueryResponse::Error(_), + PoolsStateQueryResponse::Error(_e), )) => { // if epoch_history is not enabled Ok(None) @@ -224,6 +226,7 @@ async fn handle_pools_extended_blockfrost( Message::StateQueryResponse(StateQueryResponse::Accounts( AccountsStateQueryResponse::PoolsLiveStakes(pools_live_stakes), )) => Ok(pools_live_stakes), + Message::StateQueryResponse(StateQueryResponse::Accounts( AccountsStateQueryResponse::Error(e), )) => Err(e), @@ -261,14 +264,14 @@ async fn handle_pools_extended_blockfrost( let pools_live_stakes = pools_live_stakes?; let total_blocks_minted = total_blocks_minted?; - let pools_extended_rest: Vec = pools_list_with_info + let pools_extened_rest_results: Result, RESTError> = pools_list_with_info .iter() .enumerate() .map(|(i, (pool_operator, pool_registration))| { Ok(PoolExtendedRest { pool_id: pool_operator .to_bech32() - .map_err(|e| RESTError::encoding_failed(&format!("pool ID: {}", e)))?, + .map_err(|e| RESTError::encoding_failed(&format!("pool ID: {e}")))?, hex: pool_operator.to_vec(), active_stake: pools_active_stakes.as_ref().map(|active_stakes| active_stakes[i]), live_stake: pools_live_stakes[i], @@ -281,9 +284,10 @@ async fn handle_pools_extended_blockfrost( fixed_cost: pool_registration.cost, }) }) - .collect::, RESTError>>()?; + .collect(); - let json = serde_json::to_string(&pools_extended_rest)?; + let pools_extened_rest = pools_extened_rest_results?; + let json = serde_json::to_string(&pools_extened_rest)?; Ok(RESTResponse::with_json(200, &json)) } @@ -291,6 +295,7 @@ async fn handle_pools_retired_blockfrost( context: Arc>, handlers_config: Arc, ) -> Result { + // Get retired pools from spo-state let retired_pools_msg = Arc::new(Message::StateQuery(StateQuery::Pools( PoolsStateQuery::GetPoolsRetiredList, ))); @@ -310,17 +315,16 @@ async fn handle_pools_retired_blockfrost( ) .await?; - let retired_pools_rest: Vec = retired_pools + let retired_pools_rest = retired_pools .iter() - .map(|PoolRetirement { operator, epoch }| { - Ok(PoolRetirementRest { - pool_id: operator - .to_bech32() - .map_err(|e| RESTError::encoding_failed(&format!("pool ID: {}", e)))?, + .filter_map(|PoolRetirement { operator, epoch }| { + let pool_id = operator.to_bech32().ok()?; + Some(PoolRetirementRest { + pool_id, epoch: *epoch, }) }) - .collect::, RESTError>>()?; + .collect::>(); let json = serde_json::to_string(&retired_pools_rest)?; Ok(RESTResponse::with_json(200, &json)) @@ -330,6 +334,7 @@ async fn handle_pools_retiring_blockfrost( context: Arc>, handlers_config: Arc, ) -> Result { + // Get retiring pools from spo-state let retiring_pools_msg = Arc::new(Message::StateQuery(StateQuery::Pools( PoolsStateQuery::GetPoolsRetiringList, ))); @@ -349,17 +354,16 @@ async fn handle_pools_retiring_blockfrost( ) .await?; - let retiring_pools_rest: Vec = retiring_pools + let retiring_pools_rest = retiring_pools .iter() - .map(|PoolRetirement { operator, epoch }| { - Ok(PoolRetirementRest { - pool_id: operator - .to_bech32() - .map_err(|e| RESTError::encoding_failed(&format!("pool ID: {}", e)))?, + .filter_map(|PoolRetirement { operator, epoch }| { + let pool_id = operator.to_bech32().ok()?; + Some(PoolRetirementRest { + pool_id, epoch: *epoch, }) }) - .collect::, RESTError>>()?; + .collect::>(); let json = serde_json::to_string(&retiring_pools_rest)?; Ok(RESTResponse::with_json(200, &json)) @@ -385,6 +389,9 @@ async fn handle_pools_spo_blockfrost( Message::StateQueryResponse(StateQueryResponse::Pools( PoolsStateQueryResponse::PoolInfo(pool_info), )) => Ok(pool_info), + Message::StateQueryResponse(StateQueryResponse::Pools( + PoolsStateQueryResponse::Error(QueryError::NotFound { .. }), + )) => Err(QueryError::not_found("Pool")), Message::StateQueryResponse(StateQueryResponse::Pools( PoolsStateQueryResponse::Error(e), )) => Err(e), @@ -466,7 +473,10 @@ async fn handle_pools_spo_blockfrost( PoolsStateQueryResponse::PoolUpdates(pool_updates), )) => Ok(Some(pool_updates)), Message::StateQueryResponse(StateQueryResponse::Pools( - PoolsStateQueryResponse::Error(_), + PoolsStateQueryResponse::Error(QueryError::NotFound { .. }), + )) => Err(QueryError::not_found("Pool")), + Message::StateQueryResponse(StateQueryResponse::Pools( + PoolsStateQueryResponse::Error(_e), )) => Ok(None), _ => Err(QueryError::internal_error("Unexpected message type")), }, @@ -514,8 +524,8 @@ async fn handle_pools_spo_blockfrost( let live_stakes_info = live_stakes_info?; let total_blocks_minted = total_blocks_minted?; let Some(optimal_pool_sizing) = optimal_pool_sizing? else { - // if it is before Shelley Era - return Err(RESTError::not_found("Pool not found")); + // if it is before Shelly Era + return Err(RESTError::not_found("Pool Not Found")); }; let pool_updates = pool_updates?; @@ -582,7 +592,7 @@ async fn handle_pools_spo_blockfrost( PoolsStateQueryResponse::PoolActiveStakeInfo(res), )) => Ok(Some(res)), Message::StateQueryResponse(StateQueryResponse::Pools( - PoolsStateQueryResponse::Error(_), + PoolsStateQueryResponse::Error(_e), )) => Ok(None), _ => Err(QueryError::internal_error("Unexpected message type")), }, @@ -620,18 +630,19 @@ async fn handle_pools_spo_blockfrost( let pool_id = pool_info .operator .to_bech32() - .map_err(|e| RESTError::encoding_failed(&format!("pool ID: {}", e)))?; + .map_err(|e| RESTError::encoding_failed(&format!("pool ID: {e}")))?; let reward_account = pool_info .reward_account .get_credential() .to_stake_bech32() - .map_err(|e| RESTError::encoding_failed(&format!("reward account: {}", e)))?; - let pool_owners: Vec = pool_info + .map_err(|e| RESTError::encoding_failed(&format!("reward account: {e}")))?; + + let pool_owners = pool_info .pool_owners .iter() .map(|owner| owner.get_credential().to_stake_bech32()) - .collect::, _>>() - .map_err(|e| RESTError::encoding_failed(&format!("pool owners: {}", e)))?; + .collect::, _>>() + .map_err(|e| RESTError::encoding_failed(&format!("pool owners: {e}")))?; let pool_info_rest: PoolInfoRest = PoolInfoRest { pool_id, @@ -669,14 +680,12 @@ pub async fn handle_pool_history_blockfrost( params: Vec, handlers_config: Arc, ) -> Result { - let pool_id = params.first().ok_or_else(|| RESTError::param_missing("pool_id"))?; + let Some(pool_id) = params.first() else { + return Err(RESTError::param_missing("pool ID")); + }; - let spo = PoolId::from_bech32(pool_id).map_err(|_| { - RESTError::invalid_param( - "pool_id", - &format!("Invalid Bech32 stake pool ID: {}", pool_id), - ) - })?; + let spo = PoolId::from_bech32(pool_id) + .map_err(|_| RESTError::invalid_param("pool_id", "invalid Bech32 stake pool ID"))?; // get latest epoch from epochs-state let latest_epoch_info_msg = Arc::new(Message::StateQuery(StateQuery::Epochs( @@ -731,14 +740,12 @@ pub async fn handle_pool_metadata_blockfrost( params: Vec, handlers_config: Arc, ) -> Result { - let pool_id = params.first().ok_or_else(|| RESTError::param_missing("pool_id"))?; + let Some(pool_id) = params.first() else { + return Err(RESTError::param_missing("pool ID")); + }; - let spo = PoolId::from_bech32(pool_id).map_err(|_| { - RESTError::invalid_param( - "pool_id", - &format!("Invalid Bech32 stake pool ID: {}", pool_id), - ) - })?; + let spo = PoolId::from_bech32(pool_id) + .map_err(|_| RESTError::invalid_param("pool_id", "invalid Bech32 stake pool ID"))?; let pool_metadata_msg = Arc::new(Message::StateQuery(StateQuery::Pools( PoolsStateQuery::GetPoolMetadata { pool_id: spo }, @@ -751,6 +758,9 @@ pub async fn handle_pool_metadata_blockfrost( Message::StateQueryResponse(StateQueryResponse::Pools( PoolsStateQueryResponse::PoolMetadata(pool_metadata), )) => Ok(pool_metadata), + Message::StateQueryResponse(StateQueryResponse::Pools( + PoolsStateQueryResponse::Error(QueryError::NotFound { .. }), + )) => Err(QueryError::not_found("Pool metadata")), Message::StateQueryResponse(StateQueryResponse::Pools( PoolsStateQueryResponse::Error(e), )) => Err(e), @@ -764,13 +774,13 @@ pub async fn handle_pool_metadata_blockfrost( Duration::from_secs(handlers_config.external_api_timeout), ) .await - .map_err(|e| RESTError::query_failed(&format!("Failed to fetch pool metadata: {}", e)))?; + .map_err(|e| RESTError::InternalServerError(format!("Failed to fetch pool metadata: {e}")))?; - // Verify hash of the fetched pool metadata + // Verify hash of the fetched pool metadata, matches with the metadata hash provided by PoolRegistration verify_pool_metadata_hash(&pool_metadata_bytes, &pool_metadata.hash) .map_err(|e| RESTError::not_found(&e))?; - // Convert bytes into PoolMetadata structure + // Convert bytes into an understandable PoolMetadata structure let pool_metadata_json = PoolMetadataJson::try_from(pool_metadata_bytes) .map_err(|_| RESTError::BadRequest("Failed PoolMetadata Json conversion".to_string()))?; @@ -794,14 +804,12 @@ pub async fn handle_pool_relays_blockfrost( params: Vec, handlers_config: Arc, ) -> Result { - let pool_id = params.first().ok_or_else(|| RESTError::param_missing("pool_id"))?; + let Some(pool_id) = params.first() else { + return Err(RESTError::param_missing("pool ID")); + }; - let spo = PoolId::from_bech32(pool_id).map_err(|_| { - RESTError::invalid_param( - "pool_id", - &format!("Invalid Bech32 stake pool ID: {}", pool_id), - ) - })?; + let spo = PoolId::from_bech32(pool_id) + .map_err(|_| RESTError::invalid_param("pool_id", "invalid Bech32 stake pool ID"))?; let pool_relay_msg = Arc::new(Message::StateQuery(StateQuery::Pools( PoolsStateQuery::GetPoolRelays { pool_id: spo }, @@ -815,6 +823,9 @@ pub async fn handle_pool_relays_blockfrost( Message::StateQueryResponse(StateQueryResponse::Pools( PoolsStateQueryResponse::PoolRelays(pool_relays), )) => Ok(pool_relays), + Message::StateQueryResponse(StateQueryResponse::Pools( + PoolsStateQueryResponse::Error(QueryError::NotFound { .. }), + )) => Err(QueryError::not_found("Pool Relays")), Message::StateQueryResponse(StateQueryResponse::Pools( PoolsStateQueryResponse::Error(e), )) => Err(e), @@ -834,14 +845,12 @@ pub async fn handle_pool_delegators_blockfrost( params: Vec, handlers_config: Arc, ) -> Result { - let pool_id = params.first().ok_or_else(|| RESTError::param_missing("pool_id"))?; + let Some(pool_id) = params.first() else { + return Err(RESTError::param_missing("pool ID")); + }; - let spo = PoolId::from_bech32(pool_id).map_err(|_| { - RESTError::invalid_param( - "pool_id", - &format!("Invalid Bech32 stake pool ID: {}", pool_id), - ) - })?; + let spo = PoolId::from_bech32(pool_id) + .map_err(|_| RESTError::invalid_param("pool_id", "invalid Bech32 stake pool ID"))?; // Get Pool delegators from spo-state let pool_delegators_msg = Arc::new(Message::StateQuery(StateQuery::Pools( @@ -857,7 +866,10 @@ pub async fn handle_pool_delegators_blockfrost( PoolsStateQueryResponse::PoolDelegators(pool_delegators), )) => Ok(Some(pool_delegators.delegators)), Message::StateQueryResponse(StateQueryResponse::Pools( - PoolsStateQueryResponse::Error(_), + PoolsStateQueryResponse::Error(QueryError::NotFound { .. }), + )) => Err(QueryError::not_found("Pool Delegators")), + Message::StateQueryResponse(StateQueryResponse::Pools( + PoolsStateQueryResponse::Error(_e), )) => { // store-stake-addresses is not enabled warn!("Fallback to query from accounts_state"); @@ -876,7 +888,7 @@ pub async fn handle_pool_delegators_blockfrost( let pool_delegators_msg = Arc::new(Message::StateQuery(StateQuery::Accounts( AccountsStateQuery::GetPoolDelegators { pool_operator: spo }, ))); - query_state( + let pool_delegators = query_state( &context, &handlers_config.accounts_query_topic, pool_delegators_msg, @@ -890,21 +902,21 @@ pub async fn handle_pool_delegators_blockfrost( _ => Err(QueryError::internal_error("Unexpected message type")), }, ) - .await? + .await?; + pool_delegators } }; - let delegators_rest: Vec = pool_delegators - .into_iter() - .map(|(stake_address, l)| { - Ok(PoolDelegatorRest { - address: stake_address - .to_string() - .map_err(|e| RESTError::encoding_failed(&format!("stake address: {}", e)))?, - live_stake: l.to_string(), - }) - }) - .collect::, RESTError>>()?; + let mut delegators_rest = Vec::::new(); + for (stake_address, l) in pool_delegators { + let bech32 = stake_address.to_string().map_err(|e| { + RESTError::InternalServerError(format!("Invalid stake address in pool delegators: {e}")) + })?; + delegators_rest.push(PoolDelegatorRest { + address: bech32, + live_stake: l.to_string(), + }); + } let json = serde_json::to_string(&delegators_rest)?; Ok(RESTResponse::with_json(200, &json)) @@ -915,14 +927,12 @@ pub async fn handle_pool_blocks_blockfrost( params: Vec, handlers_config: Arc, ) -> Result { - let pool_id = params.first().ok_or_else(|| RESTError::param_missing("pool_id"))?; + let Some(pool_id) = params.first() else { + return Err(RESTError::param_missing("pool ID")); + }; - let spo = PoolId::from_bech32(pool_id).map_err(|_| { - RESTError::invalid_param( - "pool_id", - &format!("Invalid Bech32 stake pool ID: {}", pool_id), - ) - })?; + let spo = PoolId::from_bech32(pool_id) + .map_err(|_| RESTError::invalid_param("pool_id", "invalid Bech32 stake pool ID"))?; // Get blocks by pool_id from spo_state let pool_blocks_msg = Arc::new(Message::StateQuery(StateQuery::Pools( @@ -958,14 +968,12 @@ pub async fn handle_pool_updates_blockfrost( params: Vec, handlers_config: Arc, ) -> Result { - let pool_id = params.first().ok_or_else(|| RESTError::param_missing("pool_id"))?; + let Some(pool_id) = params.first() else { + return Err(RESTError::param_missing("pool ID")); + }; - let spo = PoolId::from_bech32(pool_id).map_err(|_| { - RESTError::invalid_param( - "pool_id", - &format!("Invalid Bech32 stake pool ID: {}", pool_id), - ) - })?; + let spo = PoolId::from_bech32(pool_id) + .map_err(|_| RESTError::invalid_param("pool_id", "invalid Bech32 stake pool ID"))?; // query from spo_state let pool_updates_msg = Arc::new(Message::StateQuery(StateQuery::Pools( @@ -979,6 +987,9 @@ pub async fn handle_pool_updates_blockfrost( Message::StateQueryResponse(StateQueryResponse::Pools( PoolsStateQueryResponse::PoolUpdates(pool_updates), )) => Ok(pool_updates), + Message::StateQueryResponse(StateQueryResponse::Pools( + PoolsStateQueryResponse::Error(QueryError::NotFound { .. }), + )) => Err(QueryError::not_found("Pool")), Message::StateQueryResponse(StateQueryResponse::Pools( PoolsStateQueryResponse::Error(e), )) => Err(e), @@ -1005,14 +1016,12 @@ pub async fn handle_pool_votes_blockfrost( params: Vec, handlers_config: Arc, ) -> Result { - let pool_id = params.first().ok_or_else(|| RESTError::param_missing("pool_id"))?; - - let spo = PoolId::from_bech32(pool_id).map_err(|_| { - RESTError::invalid_param( - "pool_id", - &format!("Invalid Bech32 stake pool ID: {}", pool_id), - ) - })?; + let Some(pool_id) = params.first() else { + return Err(RESTError::param_missing("pool ID")); + }; + + let spo = PoolId::from_bech32(pool_id) + .map_err(|_| RESTError::invalid_param("pool_id", "invalid Bech32 stake pool ID"))?; // query from spo_state let pool_votes_msg = Arc::new(Message::StateQuery(StateQuery::Pools( diff --git a/modules/rest_blockfrost/src/rest_blockfrost.rs b/modules/rest_blockfrost/src/rest_blockfrost.rs index 4744455e..998811d9 100644 --- a/modules/rest_blockfrost/src/rest_blockfrost.rs +++ b/modules/rest_blockfrost/src/rest_blockfrost.rs @@ -736,7 +736,6 @@ fn register_handler( Fut: Future> + Send + 'static, { let topic_name = context.config.get_string(topic.0).unwrap_or_else(|_| topic.1.to_string()); - info!("Creating request handler on '{}'", topic_name); handle_rest_with_path_parameter(context.clone(), &topic_name, move |params| { @@ -763,7 +762,6 @@ fn register_handler_with_query( Fut: Future> + Send + 'static, { let topic_name = context.config.get_string(topic.0).unwrap_or_else(|_| topic.1.to_string()); - info!("Creating request handler on '{}'", topic_name); handle_rest_with_path_and_query_parameters( From 8606303498586aa852a3f50b6552adcaddbb9aea Mon Sep 17 00:00:00 2001 From: Matthew Hounslow Date: Fri, 7 Nov 2025 11:17:12 -0800 Subject: [PATCH 14/20] Refactor: Try to revert to simple conversion of errors to as exact as I can --- modules/drdd_state/src/rest.rs | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/modules/drdd_state/src/rest.rs b/modules/drdd_state/src/rest.rs index d8e635af..32310485 100644 --- a/modules/drdd_state/src/rest.rs +++ b/modules/drdd_state/src/rest.rs @@ -30,10 +30,7 @@ pub async fn handle_drdd( Some(epoch) => match locked.get_epoch(epoch) { Some(drdd) => Some(drdd), None => { - return Err(RESTError::not_found(&format!( - "DRDD in epoch {}", - epoch - ))); + return Err(RESTError::not_found(&format!("DRDD in epoch {}", epoch))); } }, None => locked.get_latest(), From 662b0536ccaa7ce14dfea58d174580c8d0e943de Mon Sep 17 00:00:00 2001 From: Matthew Hounslow Date: Fri, 7 Nov 2025 11:21:50 -0800 Subject: [PATCH 15/20] Remove old serialize method --- common/src/serialization.rs | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/common/src/serialization.rs b/common/src/serialization.rs index ece26c7f..709cfdf6 100644 --- a/common/src/serialization.rs +++ b/common/src/serialization.rs @@ -1,11 +1,9 @@ use std::marker::PhantomData; -use crate::rest_error::RESTError; use crate::PoolId; use anyhow::anyhow; use bech32::{Bech32, Hrp}; -use caryatid_module_rest_server::messages::RESTResponse; -use serde::{ser::SerializeMap, Deserialize, Serialize, Serializer}; +use serde::{ser::SerializeMap, Deserialize, Serializer}; use serde_with::{ser::SerializeAsWrap, DeserializeAs, SerializeAs}; pub struct SerializeMapAs(std::marker::PhantomData<(KAs, VAs)>); @@ -160,9 +158,3 @@ impl Bech32WithHrp for [u8] { Ok(data.to_vec()) } } - -/// Helper to serialize a result to JSON REST response -pub fn serialize_to_json_response(data: &T) -> Result { - let json = serde_json::to_string_pretty(data)?; - Ok(RESTResponse::with_json(200, &json)) -} From 189edfb07e07b8b49799829f439f79c36a522124 Mon Sep 17 00:00:00 2001 From: Matthew Hounslow Date: Fri, 7 Nov 2025 11:36:56 -0800 Subject: [PATCH 16/20] fix: Clippy removing redundant `into` calls in state query responses --- common/src/queries/utils.rs | 2 +- .../rest_blockfrost/src/handlers/accounts.rs | 20 +++++++++---------- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/common/src/queries/utils.rs b/common/src/queries/utils.rs index 8d4de100..6c013c5f 100644 --- a/common/src/queries/utils.rs +++ b/common/src/queries/utils.rs @@ -40,7 +40,7 @@ where let data = query_state(context, topic, request_msg, |response| { extractor(response).unwrap_or_else(|| { Err(QueryError::internal_error(format!( - "Unexpected response message type from {topic}" + "Unexpected response message type while calling {topic}" ))) }) }) diff --git a/modules/rest_blockfrost/src/handlers/accounts.rs b/modules/rest_blockfrost/src/handlers/accounts.rs index 9d1ac475..bf6f2a00 100644 --- a/modules/rest_blockfrost/src/handlers/accounts.rs +++ b/modules/rest_blockfrost/src/handlers/accounts.rs @@ -122,7 +122,7 @@ pub async fn handle_account_registrations_blockfrost( )) => Ok(None), Message::StateQueryResponse(StateQueryResponse::Accounts( AccountsStateQueryResponse::Error(e), - )) => Err(e.into()), + )) => Err(e), _ => Err(QueryError::internal_error( "Unexpected message type while retrieving account registrations", )), @@ -149,7 +149,7 @@ pub async fn handle_account_registrations_blockfrost( )) => Ok(tx_hashes), Message::StateQueryResponse(StateQueryResponse::Blocks( BlocksStateQueryResponse::Error(e), - )) => Err(e.into()), + )) => Err(e), _ => Err(QueryError::internal_error( "Unexpected message type while resolving transaction hashes", )), @@ -203,7 +203,7 @@ pub async fn handle_account_delegations_blockfrost( )) => Ok(None), Message::StateQueryResponse(StateQueryResponse::Accounts( AccountsStateQueryResponse::Error(e), - )) => Err(e.into()), + )) => Err(e), _ => Err(QueryError::internal_error( "Unexpected message type while retrieving account delegations", )), @@ -230,7 +230,7 @@ pub async fn handle_account_delegations_blockfrost( )) => Ok(tx_hashes), Message::StateQueryResponse(StateQueryResponse::Blocks( BlocksStateQueryResponse::Error(e), - )) => Err(e.into()), + )) => Err(e), _ => Err(QueryError::internal_error( "Unexpected message type while resolving transaction hashes", )), @@ -289,7 +289,7 @@ pub async fn handle_account_mirs_blockfrost( )) => Ok(None), Message::StateQueryResponse(StateQueryResponse::Accounts( AccountsStateQueryResponse::Error(e), - )) => Err(e.into()), + )) => Err(e), _ => Err(QueryError::internal_error( "Unexpected message type while retrieving account mirs", )), @@ -316,7 +316,7 @@ pub async fn handle_account_mirs_blockfrost( )) => Ok(tx_hashes), Message::StateQueryResponse(StateQueryResponse::Blocks( BlocksStateQueryResponse::Error(e), - )) => Err(e.into()), + )) => Err(e), _ => Err(QueryError::internal_error( "Unexpected message type while resolving transaction hashes", )), @@ -369,7 +369,7 @@ pub async fn handle_account_withdrawals_blockfrost( )) => Ok(None), Message::StateQueryResponse(StateQueryResponse::Accounts( AccountsStateQueryResponse::Error(e), - )) => Err(e.into()), + )) => Err(e), _ => Err(QueryError::internal_error( "Unexpected message type while retrieving account withdrawals", )), @@ -396,7 +396,7 @@ pub async fn handle_account_withdrawals_blockfrost( )) => Ok(tx_hashes), Message::StateQueryResponse(StateQueryResponse::Blocks( BlocksStateQueryResponse::Error(e), - )) => Err(e.into()), + )) => Err(e), _ => Err(QueryError::internal_error( "Unexpected message type while resolving transaction hashes", )), @@ -449,7 +449,7 @@ pub async fn handle_account_rewards_blockfrost( )) => Ok(None), Message::StateQueryResponse(StateQueryResponse::Accounts( AccountsStateQueryResponse::Error(e), - )) => Err(e.into()), + )) => Err(e), _ => Err(QueryError::internal_error( "Unexpected message type while retrieving account rewards", )), @@ -499,7 +499,7 @@ pub async fn handle_account_addresses_blockfrost( )) => Ok(None), Message::StateQueryResponse(StateQueryResponse::Accounts( AccountsStateQueryResponse::Error(e), - )) => Err(e.into()), + )) => Err(e), _ => Err(QueryError::internal_error( "Unexpected message type while retrieving account addresses", )), From c7cc0f5fcb42927cf3205b20a84ad2286c2e11f2 Mon Sep 17 00:00:00 2001 From: Matthew Hounslow Date: Fri, 7 Nov 2025 14:55:17 -0800 Subject: [PATCH 17/20] Refactor: Simplify error handling in queries and implement conversion from `anyhow::Error` to `QueryError` --- common/src/queries/errors.rs | 8 ++++++++ common/src/queries/utils.rs | 9 ++------- 2 files changed, 10 insertions(+), 7 deletions(-) diff --git a/common/src/queries/errors.rs b/common/src/queries/errors.rs index 8fa135c5..059b1825 100644 --- a/common/src/queries/errors.rs +++ b/common/src/queries/errors.rs @@ -56,3 +56,11 @@ impl QueryError { } } } + +impl From for QueryError { + fn from(err: anyhow::Error) -> Self { + Self::Internal { + message: err.to_string(), + } + } +} diff --git a/common/src/queries/utils.rs b/common/src/queries/utils.rs index 6c013c5f..93ef9652 100644 --- a/common/src/queries/utils.rs +++ b/common/src/queries/utils.rs @@ -15,12 +15,7 @@ pub async fn query_state( where F: FnOnce(Message) -> Result, { - let raw_msg = context - .message_bus - .request(topic, request_msg) - .await - .map_err(|e| QueryError::internal_error(format!("Failed to query '{topic}': {e:#}")))?; - + let raw_msg = context.message_bus.request(topic, request_msg).await?; let message = Arc::try_unwrap(raw_msg).unwrap_or_else(|arc| (*arc).clone()); extractor(message) @@ -38,7 +33,7 @@ where T: Serialize, { let data = query_state(context, topic, request_msg, |response| { - extractor(response).unwrap_or_else(|| { + extractor(response).unwrap_or_else(|e| { Err(QueryError::internal_error(format!( "Unexpected response message type while calling {topic}" ))) From bd4198ae7c8616b9f1774482451d4c17cbe77378 Mon Sep 17 00:00:00 2001 From: Matthew Hounslow Date: Fri, 7 Nov 2025 15:13:09 -0800 Subject: [PATCH 18/20] fix: clippy again --- common/src/queries/utils.rs | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/common/src/queries/utils.rs b/common/src/queries/utils.rs index 93ef9652..b41793cc 100644 --- a/common/src/queries/utils.rs +++ b/common/src/queries/utils.rs @@ -33,11 +33,9 @@ where T: Serialize, { let data = query_state(context, topic, request_msg, |response| { - extractor(response).unwrap_or_else(|e| { - Err(QueryError::internal_error(format!( - "Unexpected response message type while calling {topic}" - ))) - }) + extractor(response).ok_or_else(Err(QueryError::internal_error(format!( + "Unexpected response message type while calling {topic}" + )))) }) .await?; From 6047608ec7899f01010c1ee288e1acd313df4970 Mon Sep 17 00:00:00 2001 From: Matthew Hounslow Date: Fri, 7 Nov 2025 15:32:09 -0800 Subject: [PATCH 19/20] fix: Improve error handling in `rest_query_state` by simplifying extractor error conversion --- common/src/queries/utils.rs | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/common/src/queries/utils.rs b/common/src/queries/utils.rs index b41793cc..897e61a2 100644 --- a/common/src/queries/utils.rs +++ b/common/src/queries/utils.rs @@ -21,7 +21,6 @@ where extractor(message) } -/// The outer option in the extractor return value is whether the response was handled by F pub async fn rest_query_state( context: &Arc>, topic: &str, @@ -33,9 +32,11 @@ where T: Serialize, { let data = query_state(context, topic, request_msg, |response| { - extractor(response).ok_or_else(Err(QueryError::internal_error(format!( - "Unexpected response message type while calling {topic}" - )))) + extractor(response).ok_or_else(|| { + QueryError::internal_error(format!( + "Unexpected response message type while calling {topic}" + )) + })? }) .await?; From 43007b651b6348ab3d6b006102ec4bdcd0b31a30 Mon Sep 17 00:00:00 2001 From: Matthew Hounslow Date: Fri, 7 Nov 2025 15:35:44 -0800 Subject: [PATCH 20/20] fix: Simplify JSON serialization and improve error handling in account endpoints --- .../rest_blockfrost/src/handlers/accounts.rs | 22 +++++-------------- 1 file changed, 6 insertions(+), 16 deletions(-) diff --git a/modules/rest_blockfrost/src/handlers/accounts.rs b/modules/rest_blockfrost/src/handlers/accounts.rs index e04ffd87..6f392973 100644 --- a/modules/rest_blockfrost/src/handlers/accounts.rs +++ b/modules/rest_blockfrost/src/handlers/accounts.rs @@ -525,13 +525,8 @@ pub async fn handle_account_addresses_blockfrost( }) .collect::, RESTError>>()?; - match serde_json::to_string_pretty(&rest_response) { - Ok(json) => Ok(RESTResponse::with_json(200, &json)), - Err(e) => Ok(RESTResponse::with_text( - 500, - &format!("Internal server error while serializing addresses: {e}"), - )), - } + let json = serde_json::to_string_pretty(&rest_response)?; + Ok(RESTResponse::with_json(200, &json)) } /// Handle `/accounts/{stake_address}/addresses/assets` Blockfrost-compatible endpoint @@ -540,7 +535,7 @@ pub async fn handle_account_assets_blockfrost( _params: Vec, _handlers_config: Arc, ) -> Result { - Ok(RESTResponse::with_text(501, "Not implemented")) + Err(RESTError::not_implemented("Account assets not implemented")) } /// Handle `/accounts/{stake_address}/addresses/total` Blockfrost-compatible endpoint @@ -549,7 +544,7 @@ pub async fn handle_account_totals_blockfrost( _params: Vec, _handlers_config: Arc, ) -> Result { - Ok(RESTResponse::with_text(501, "Not implemented")) + Err(RESTError::not_implemented("Account totals not implemented")) } /// Handle `/accounts/{stake_address}/utxos` Blockfrost-compatible endpoint @@ -695,13 +690,8 @@ pub async fn handle_account_utxos_blockfrost( }) } - match serde_json::to_string_pretty(&rest_response) { - Ok(json) => Ok(RESTResponse::with_json(200, &json)), - Err(e) => Ok(RESTResponse::with_text( - 500, - &format!("Internal server error while serializing addresses: {e}"), - )), - } + let json = serde_json::to_string_pretty(&rest_response)?; + Ok(RESTResponse::with_json(200, &json)) } fn parse_stake_address(params: &[String]) -> Result {