diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index 81162d27..5fe98519 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -1,21 +1,25 @@ # Pull Request Title + ## Description + ## Related Issue(s) + ## How was this tested? + ## Checklist + - [ ] My code builds and passes local tests - [ ] I added/updated tests for my changes, where applicable - [ ] I updated documentation (if applicable) - [ ] CI is green for this PR ## Impact / Side effects + ## Reviewer notes / Areas to focus + 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 20ce4f4e..897e61a2 100644 --- a/common/src/queries/utils.rs +++ b/common/src/queries/utils.rs @@ -1,61 +1,45 @@ -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; 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 message = Arc::try_unwrap(raw_msg).unwrap_or_else(|arc| (*arc).clone()); 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, request_msg: Arc, extractor: F, -) -> Result +) -> 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!( + let data = query_state(context, topic, request_msg, |response| { + extractor(response).ok_or_else(|| { + QueryError::internal_error(format!( "Unexpected response message type while calling {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}"), - )), - } + .await?; + + 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 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/rest_helper.rs b/common/src/rest_helper.rs index 135652cb..9eab49c3 100644 --- a/common/src/rest_helper.rs +++ b/common/src/rest_helper.rs @@ -1,6 +1,7 @@ //! Helper functions for REST handlers use crate::messages::{Message, RESTResponse}; +use crate::rest_error::RESTError; use anyhow::{anyhow, Result}; use caryatid_sdk::Context; use futures::future::Future; @@ -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/snapshot/NOTES.md b/common/src/snapshot/NOTES.md index 549a20a7..710ebd3f 100644 --- a/common/src/snapshot/NOTES.md +++ b/common/src/snapshot/NOTES.md @@ -1,7 +1,10 @@ # Bootstrapping from a Snapshot file + We can boot an Acropolis node either from geneis and replay all of the blocks up to some point, or we can boot from a snapshot file. This module provides the components -needed to boot from a snapshot file. See [snapshot_bootsrapper](../../../modules/snapshot_bootstrapper/src/snapshot_bootstrapper.rs) for the process that references and runs with these helpers. +needed to boot from a snapshot file. +See [snapshot_bootsrapper](../../../modules/snapshot_bootstrapper/src/snapshot_bootstrapper.rs) for the process that +references and runs with these helpers. Booting from a snapshot takes minutes instead of the hours it takes to boot from genesis. It also allows booting from a given epoch which allows one to create tests @@ -10,22 +13,27 @@ eras and will typically boot from Conway around epoch 305, 306, and 307. It take three epochs to have enough context to correctly calculate the rewards. The required data for boostrapping are: + - snapshot files (each has an associated epoch number and point) - nonces - headers ## Snapshot Files + The snapshots come from the Amaru project. In their words, "the snapshots we generated are different [from a Mithril snapshot]: they're -the actual ledger state; i.e. the in-memory state that is constructed by iterating over each block up to a specific point. So, it's all the UTxOs, the set of pending governance actions, the account balance, etc. +the actual ledger state; i.e. the in-memory state that is constructed by iterating over each block up to a specific +point. So, it's all the UTxOs, the set of pending governance actions, the account balance, etc. If you get this from a trusted source, you don't need to do any replay, you can just start up and load this from disk. -The format of these is completely non-standard; we just forked the haskell node and spit out whatever we needed to in CBOR." +The format of these is completely non-standard; we just forked the haskell node and spit out whatever we needed to in +CBOR." Snapshot files are referenced by their epoch number in the config.json file below. See [Amaru snapshot format](../../../docs/amaru-snapshot-structure.md) ## Configuration files + There is a path for each network bootstrap configuration file. Network Should be one of 'mainnet', 'preprod', 'preview' or 'testnet_' where `magic` is a 32-bits unsigned value denoting a particular testnet. @@ -43,7 +51,8 @@ a network name of `preview`, the expected layout for configuration files would b * `data/preview/nonces.json`: a list of `InitialNonces` values, * `data/preview/headers.json`: a list of `Point`s. -These files are loaded by [snapshot_bootsrapper](../../../modules/snapshot_bootstrapper/src/snapshot_bootstrapper.rs) during bootup. +These files are loaded by [snapshot_bootsrapper](../../../modules/snapshot_bootstrapper/src/snapshot_bootstrapper.rs) +during bootup. ## Bootstrapping sequence @@ -75,7 +84,8 @@ headers. Snapshot parsing is done while streaming the data to keep the memory footprint lower. As elements of the file are parsed, callbacks provide the data to the boostrapper which publishes the data on the message bus. -There are TODO markers in [snapshot_bootsrapper](../../../modules/snapshot_bootstrapper/src/snapshot_bootstrapper.rs) that show where to add the +There are TODO markers in [snapshot_bootsrapper](../../../modules/snapshot_bootstrapper/src/snapshot_bootstrapper.rs) +that show where to add the publishing of the parsed snapshot data. diff --git a/modules/drdd_state/src/rest.rs b/modules/drdd_state/src/rest.rs index 37822569..32310485 100644 --- a/modules/drdd_state/src/rest.rs +++ b/modules/drdd_state/src/rest.rs @@ -1,6 +1,6 @@ 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; @@ -17,16 +17,10 @@ struct DRDDResponse { pub async fn handle_drdd( state: Option>>, params: HashMap, -) -> 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", - )); - } - }; +) -> 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, @@ -36,10 +30,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 in epoch {}", epoch))); } }, None => locked.get_latest(), @@ -65,13 +56,8 @@ pub async fn handle_drdd( no_confidence: drdd.no_confidence, }; - 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}"), - )), - } + let body = serde_json::to_string(&response)?; + Ok(RESTResponse::with_json(200, &body)) } else { let response = DRDDResponse { dreps: HashMap::new(), @@ -79,12 +65,7 @@ pub async fn handle_drdd( no_confidence: 0, }; - 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", - )), - } + let body = serde_json::to_string(&response)?; + Ok(RESTResponse::with_json(200, &body)) } } diff --git a/modules/rest_blockfrost/src/handlers/accounts.rs b/modules/rest_blockfrost/src/handlers/accounts.rs index ffe61413..6f392973 100644 --- a/modules/rest_blockfrost/src/handlers/accounts.rs +++ b/modules/rest_blockfrost/src/handlers/accounts.rs @@ -15,9 +15,9 @@ use acropolis_common::queries::blocks::{ use acropolis_common::queries::errors::QueryError; use acropolis_common::queries::utils::query_state; use acropolis_common::queries::utxos::{UTxOStateQuery, UTxOStateQueryResponse}; +use acropolis_common::rest_error::RESTError; use acropolis_common::serialization::{Bech32Conversion, Bech32WithHrp}; use acropolis_common::{DRepChoice, Datum, ReferenceScript, StakeAddress}; -use anyhow::{anyhow, Result}; use blake2::{Blake2b512, Digest}; use caryatid_sdk::Context; @@ -40,15 +40,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, @@ -62,45 +61,31 @@ pub async fn handle_single_account_blockfrost( )) => Ok(None), 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::internal_error( + "Unexpected message type while retrieving account info", )), }, ) .await?; let Some(account) = account else { - return Ok(RESTResponse::with_text(404, "Account not found")); + return Err(RESTError::not_found("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}"), - )); - } - }, - 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 = 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}"), - )) - } - }, - None => None, - }; + 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, @@ -109,13 +94,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 @@ -123,11 +103,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( @@ -148,18 +125,16 @@ pub async fn handle_account_registrations_blockfrost( )) => Ok(None), 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::internal_error( + "Unexpected message type while retrieving account registrations", )), }, ) .await?; let Some(registrations) = registrations else { - return Ok(RESTResponse::with_text(404, "Account not found")); + return Err(RESTError::not_found("Account not found")); }; // Get TxHashes from TxIdentifiers @@ -177,11 +152,9 @@ pub async fn handle_account_registrations_blockfrost( )) => 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" + )) => Err(e), + _ => Err(QueryError::internal_error( + "Unexpected message type while resolving transaction hashes", )), }, ) @@ -191,9 +164,8 @@ pub async fn handle_account_registrations_blockfrost( 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", + return Err(RESTError::InternalServerError( + "Missing tx hash for registration".to_string(), )); }; @@ -203,13 +175,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 @@ -217,11 +184,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( @@ -242,18 +206,16 @@ pub async fn handle_account_delegations_blockfrost( )) => Ok(None), 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::internal_error( + "Unexpected message type while retrieving account delegations", )), }, ) .await?; let Some(delegations) = delegations else { - return Ok(RESTResponse::with_text(404, "Account not found")); + return Err(RESTError::not_found("Account not found")); }; // Get TxHashes from TxIdentifiers @@ -271,11 +233,9 @@ pub async fn handle_account_delegations_blockfrost( )) => 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" + )) => Err(e), + _ => Err(QueryError::internal_error( + "Unexpected message type while resolving transaction hashes", )), }, ) @@ -285,21 +245,13 @@ pub async fn handle_account_delegations_blockfrost( 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", + return Err(RESTError::InternalServerError( + "Missing tx hash for delegation".to_string(), )); }; - 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 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, @@ -309,13 +261,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 @@ -323,11 +270,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( @@ -348,18 +292,16 @@ pub async fn handle_account_mirs_blockfrost( )) => Ok(None), 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::internal_error( + "Unexpected message type while retrieving account mirs", )), }, ) .await?; let Some(mirs) = mirs else { - return Ok(RESTResponse::with_text(404, "Account not found")); + return Err(RESTError::not_found("Account not found")); }; // Get TxHashes from TxIdentifiers @@ -377,11 +319,9 @@ pub async fn handle_account_mirs_blockfrost( )) => 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" + )) => Err(e), + _ => Err(QueryError::internal_error( + "Unexpected message type while resolving transaction hashes", )), }, ) @@ -391,9 +331,8 @@ pub async fn handle_account_mirs_blockfrost( 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", + return Err(RESTError::InternalServerError( + "Missing tx hash for MIR record".to_string(), )); }; @@ -403,24 +342,16 @@ 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( @@ -441,18 +372,16 @@ pub async fn handle_account_withdrawals_blockfrost( )) => Ok(None), 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::internal_error( + "Unexpected message type while retrieving account withdrawals", )), }, ) .await?; let Some(withdrawals) = withdrawals else { - return Ok(RESTResponse::with_text(404, "Account not found")); + return Err(RESTError::not_found("Account not found")); }; // Get TxHashes from TxIdentifiers @@ -470,11 +399,9 @@ pub async fn handle_account_withdrawals_blockfrost( )) => 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" + )) => Err(e), + _ => Err(QueryError::internal_error( + "Unexpected message type while resolving transaction hashes", )), }, ) @@ -484,9 +411,8 @@ pub async fn handle_account_withdrawals_blockfrost( 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", + return Err(RESTError::InternalServerError( + "Missing tx hash for withdrawal".to_string(), )); }; @@ -496,24 +422,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( @@ -534,49 +452,36 @@ pub async fn handle_account_rewards_blockfrost( )) => Ok(None), 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::internal_error( + "Unexpected message type while retrieving account rewards", )), }, ) .await?; let Some(rewards) = rewards else { - return Ok(RESTResponse::with_text(404, "Account not found")); + return Err(RESTError::not_found("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| { + 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( @@ -597,45 +502,31 @@ pub async fn handle_account_addresses_blockfrost( )) => Ok(None), 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::internal_error( + "Unexpected message type while retrieving account addresses", )), }, ) .await?; let Some(addresses) = addresses else { - return Ok(RESTResponse::with_text(404, "Account not found")); + return Err(RESTError::not_found("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(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)) } /// Handle `/accounts/{stake_address}/addresses/assets` Blockfrost-compatible endpoint @@ -643,8 +534,8 @@ pub async fn handle_account_assets_blockfrost( _context: Arc>, _params: Vec, _handlers_config: Arc, -) -> Result { - Ok(RESTResponse::with_text(501, "Not implemented")) +) -> Result { + Err(RESTError::not_implemented("Account assets not implemented")) } /// Handle `/accounts/{stake_address}/addresses/total` Blockfrost-compatible endpoint @@ -652,8 +543,8 @@ pub async fn handle_account_totals_blockfrost( _context: Arc>, _params: Vec, _handlers_config: Arc, -) -> Result { - Ok(RESTResponse::with_text(501, "Not implemented")) +) -> Result { + Err(RESTError::not_implemented("Account totals not implemented")) } /// Handle `/accounts/{stake_address}/utxos` Blockfrost-compatible endpoint @@ -661,11 +552,8 @@ pub async fn handle_account_utxos_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)?; // Get addresses from historical accounts state let msg = Arc::new(Message::StateQuery(StateQuery::Accounts( @@ -679,16 +567,11 @@ pub async fn handle_account_utxos_blockfrost( Message::StateQueryResponse(StateQueryResponse::Accounts( AccountsStateQueryResponse::AccountAssociatedAddresses(addresses), )) => Ok(Some(addresses)), - Message::StateQueryResponse(StateQueryResponse::Accounts( - AccountsStateQueryResponse::Error(QueryError::NotFound { .. }), - )) => Ok(None), 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::internal_error( + "Unexpected message type while retrieving account addresses", )), }, ) @@ -712,11 +595,9 @@ pub async fn handle_account_utxos_blockfrost( )) => Ok(utxos), Message::StateQueryResponse(StateQueryResponse::Addresses( AddressStateQueryResponse::Error(e), - )) => Err(anyhow::anyhow!( - "Internal server error while retrieving account UTxOs: {e}" - )), - _ => Err(anyhow::anyhow!( - "Unexpected message type while retrieving account UTxOs" + )) => Err(e), + _ => Err(QueryError::internal_error( + "Unexpected message type while retrieving account UTxOs", )), }, ) @@ -739,11 +620,9 @@ pub async fn handle_account_utxos_blockfrost( )) => Ok(utxos), Message::StateQueryResponse(StateQueryResponse::Blocks( BlocksStateQueryResponse::Error(e), - )) => Err(anyhow::anyhow!( - "Internal server error while retrieving utxo hashes: {e}" - )), - _ => Err(anyhow::anyhow!( - "Unexpected message type while retrieving account UTxOs" + )) => Err(e), + _ => Err(QueryError::internal_error( + "Unexpected message type while retrieving account UTxOs", )), }, ) @@ -765,11 +644,9 @@ pub async fn handle_account_utxos_blockfrost( )) => Ok(utxos), Message::StateQueryResponse(StateQueryResponse::UTxOs( UTxOStateQueryResponse::Error(e), - )) => Err(anyhow::anyhow!( - "Internal server error while retrieving UTxO entries: {e}" - )), - _ => Err(anyhow::anyhow!( - "Unexpected message type while retrieving UTxO entries" + )) => Err(e), + _ => Err(QueryError::internal_error( + "Unexpected message type while retrieving UTxO entries", )), }, ) @@ -813,35 +690,26 @@ 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 { +fn parse_stake_address(params: &[String]) -> Result { let Some(stake_key) = params.first() else { - return Err(RESTResponse::with_text( - 400, - "Missing stake address parameter", - )); + return Err(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", "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), @@ -851,7 +719,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), diff --git a/modules/rest_blockfrost/src/handlers/addresses.rs b/modules/rest_blockfrost/src/handlers/addresses.rs index 21979392..490f6ef0 100644 --- a/modules/rest_blockfrost/src/handlers/addresses.rs +++ b/modules/rest_blockfrost/src/handlers/addresses.rs @@ -1,8 +1,8 @@ -use anyhow::Result; use std::sync::Arc; use crate::{handlers_config::HandlersConfig, types::AddressInfoREST}; use acropolis_common::queries::errors::QueryError; +use acropolis_common::rest_error::RESTError; use acropolis_common::{ messages::{Message, RESTResponse, StateQuery, StateQueryResponse}, queries::{ @@ -19,37 +19,28 @@ 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")); + 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())); } }; @@ -60,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, @@ -72,17 +63,19 @@ pub async fn handle_address_single_blockfrost( AddressStateQueryResponse::Error(QueryError::NotFound { .. }), )) => Ok(None), Message::StateQueryResponse(StateQueryResponse::Addresses( - AddressStateQueryResponse::Error(_), - )) => Err(anyhow::anyhow!("Address info storage disabled")), - - _ => Err(anyhow::anyhow!("Unexpected response")), + AddressStateQueryResponse::Error(e), + )) => Err(e), + _ => Err(QueryError::internal_error( + "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,19 +88,16 @@ 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)?; 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( 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,15 +107,13 @@ pub async fn handle_address_single_blockfrost( )) => Ok(balance), Message::StateQueryResponse(StateQueryResponse::UTxOs( UTxOStateQueryResponse::Error(e), - )) => Err(anyhow::anyhow!(format!("UTxO query error: {e}"))), - _ => Err(anyhow::anyhow!("Unexpected response")), + )) => Err(e), + _ => Err(QueryError::internal_error( + "Unexpected message type while retrieving UTxO sum", + )), }, ) - .await - { - Ok(address_balance) => address_balance, - Err(e) => return Ok(RESTResponse::with_text(500, &format!("Query failed: {e}"))), - }; + .await?; let rest_response = AddressInfoREST { address: address_str.to_string(), @@ -135,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,8 +132,8 @@ pub async fn handle_address_extended_blockfrost( _context: Arc>, _params: Vec, _handlers_config: Arc, -) -> Result { - Ok(RESTResponse::with_text(501, "Not implemented")) +) -> Result { + Err(RESTError::not_implemented("Address extended endpoint")) } /// Handle `/addresses/{address}/totals` Blockfrost-compatible endpoint @@ -158,8 +141,8 @@ pub async fn handle_address_totals_blockfrost( _context: Arc>, _params: Vec, _handlers_config: Arc, -) -> Result { - Ok(RESTResponse::with_text(501, "Not implemented")) +) -> Result { + Err(RESTError::not_implemented("Address totals endpoint")) } /// Handle `/addresses/{address}/utxos` Blockfrost-compatible endpoint @@ -167,8 +150,8 @@ pub async fn handle_address_utxos_blockfrost( _context: Arc>, _params: Vec, _handlers_config: Arc, -) -> Result { - Ok(RESTResponse::with_text(501, "Not implemented")) +) -> Result { + Err(RESTError::not_implemented("Address UTxOs endpoint")) } /// Handle `/addresses/{address}/utxos/{asset}` Blockfrost-compatible endpoint @@ -176,8 +159,8 @@ pub async fn handle_address_asset_utxos_blockfrost( _context: Arc>, _params: Vec, _handlers_config: Arc, -) -> Result { - Ok(RESTResponse::with_text(501, "Not implemented")) +) -> Result { + Err(RESTError::not_implemented("Address asset UTxOs endpoint")) } /// Handle `/addresses/{address}/transactions` Blockfrost-compatible endpoint @@ -185,6 +168,6 @@ pub async fn handle_address_transactions_blockfrost( _context: Arc>, _params: Vec, _handlers_config: Arc, -) -> Result { - Ok(RESTResponse::with_text(501, "Not implemented")) +) -> Result { + 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 cc4e1f02..b5b744df 100644 --- a/modules/rest_blockfrost/src/handlers/assets.rs +++ b/modules/rest_blockfrost/src/handlers/assets.rs @@ -6,6 +6,7 @@ use crate::{ }, }; use acropolis_common::queries::errors::QueryError; +use acropolis_common::rest_error::RESTError; use acropolis_common::{ messages::{Message, RESTResponse, StateQuery, StateQueryResponse}, queries::{ @@ -15,7 +16,6 @@ use acropolis_common::{ serialization::Bech32WithHrp, AssetMetadataStandard, AssetName, PolicyId, }; -use anyhow::Result; use blake2::{digest::consts::U20, Blake2b, Digest}; use caryatid_sdk::Context; use hex::FromHex; @@ -28,391 +28,291 @@ 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::internal_error( "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 (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)?; 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 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, 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}")) - } + )) => Ok((quantity, info)), Message::StateQueryResponse(StateQueryResponse::Assets( AssetsStateQueryResponse::Error(QueryError::NotFound { .. }), - )) => Ok(RESTResponse::with_text(404, "Asset not found")), + )) => Err(QueryError::not_found("Asset")), 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::internal_error( "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}"), - )), - } - } + )) => Ok(history), Message::StateQueryResponse(StateQueryResponse::Assets( AssetsStateQueryResponse::Error(QueryError::NotFound { .. }), - )) => Ok(RESTResponse::with_text(404, "Asset history not found")), + )) => Err(QueryError::not_found("Asset 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::internal_error( "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}")) - } + )) => Ok(txs), Message::StateQueryResponse(StateQueryResponse::Assets( AssetsStateQueryResponse::Error(QueryError::NotFound { .. }), - )) => Ok(RESTResponse::with_text(404, "Asset not found")), + )) => Err(QueryError::not_found("Asset")), 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::internal_error( "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::Error(QueryError::NotFound { .. }), - )) => Ok(RESTResponse::with_text(404, "Asset not found")), + )) => Err(QueryError::not_found("Asset")), 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::internal_error( "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 = + 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"))?; 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}")) - } + )) => Ok(assets), Message::StateQueryResponse(StateQueryResponse::Assets( AssetsStateQueryResponse::Error(QueryError::NotFound { .. }), - )) => Ok(RESTResponse::with_text(404, "Policy assets not found")), + )) => Err(QueryError::not_found("Policy 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::internal_error( "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)?; 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)) } @@ -567,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.code, 400); - assert_eq!(err.body, "Invalid hex string"); } #[test] @@ -577,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.code, 400); - assert_eq!(err.body, "Asset identifier must be at least 28 bytes"); } #[test] @@ -589,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.code, 400); - assert_eq!(err.body, "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 fbbf2158..9ca20222 100644 --- a/modules/rest_blockfrost/src/handlers/blocks.rs +++ b/modules/rest_blockfrost/src/handlers/blocks.rs @@ -1,7 +1,7 @@ //! REST handlers for Acropolis Blockfrost /blocks endpoints use crate::handlers_config::HandlersConfig; use crate::types::BlockInfoREST; -use acropolis_common::queries::errors::QueryError; +use acropolis_common::rest_error::RESTError; use acropolis_common::{ extract_strict_query_params, messages::{Message, RESTResponse, StateQuery, StateQueryResponse}, @@ -12,24 +12,28 @@ use acropolis_common::{ }, BlockHash, }; -use anyhow::{anyhow, Result}; use caryatid_sdk::Context; use std::collections::HashMap; use std::sync::Arc; -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::Error(QueryError::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::Error(QueryError::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::Error(QueryError::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::Error(QueryError::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::Error(QueryError::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::Error(QueryError::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::Error(QueryError::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::Error(QueryError::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 e2d3257a..b62b6791 100644 --- a/modules/rest_blockfrost/src/handlers/epochs.rs +++ b/modules/rest_blockfrost/src/handlers/epochs.rs @@ -5,6 +5,7 @@ use crate::{ }, }; use acropolis_common::queries::errors::QueryError; +use acropolis_common::rest_error::RESTError; use acropolis_common::serialization::Bech32Conversion; use acropolis_common::{ messages::{Message, RESTResponse, StateQuery, StateQueryResponse}, @@ -18,7 +19,6 @@ use acropolis_common::{ }, PoolId, }; -use anyhow::{anyhow, Result}; use caryatid_sdk::Context; use std::sync::Arc; @@ -26,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]; @@ -39,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", "invalid epoch number"))?; EpochsStateQuery::GetEpochInfo { epoch_number: parsed, } @@ -61,26 +54,28 @@ 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::internal_error( + "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::LatestEpoch(response) => response.epoch, + EpochsStateQueryResponse::EpochInfo(response) => response.epoch, EpochsStateQueryResponse::Error(QueryError::NotFound { .. }) => { - Err(anyhow!("Epoch not found")) + return Err(RESTError::not_found("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::Error(e) => { + return Err(e.into()); + } + _ => { + return Err(RESTError::unexpected_response( + "Unexpected message 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) @@ -97,7 +92,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::internal_error( "Unexpected message type while retrieving the latest total active stakes", )), }, @@ -116,14 +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), - _ => Err(anyhow::anyhow!( - "Unexpected message type while retrieving total active stakes for epoch: {epoch_number}", + 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}"), )), }, ) - .await? + .await? }; let mut response = EpochActivityRest::from(ea_message); @@ -134,15 +135,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)) } @@ -150,11 +143,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]; @@ -176,11 +168,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::internal_error( + "Unexpected message type while retrieving latest epoch", )), }, ) @@ -189,15 +179,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", "invalid epoch number"))?; query = ParametersStateQuery::GetEpochParameters { epoch_number: parsed, }; @@ -211,8 +195,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::internal_error( + "Unexpected message type while retrieving parameters", )), }, ) @@ -221,38 +205,26 @@ 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::Error(QueryError::NotFound { .. }) => Ok( - RESTResponse::with_text(404, "Protocol parameters not found for requested epoch"), + ParametersStateQueryResponse::Error(QueryError::NotFound { .. }) => Err( + RESTError::not_found("Protocol parameters not found for requested epoch"), ), - ParametersStateQueryResponse::Error(e) => Ok(RESTResponse::with_text(400, &e.to_string())), - _ => Ok(RESTResponse::with_text( - 500, + ParametersStateQueryResponse::Error(e) => Err(e.into()), + _ => Err(RESTError::unexpected_response( "Unexpected message type while retrieving parameters", )), } @@ -262,24 +234,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", "invalid epoch number"))?; let next_epochs_msg = Arc::new(Message::StateQuery(StateQuery::Epochs( EpochsStateQuery::GetNextEpochs { @@ -296,29 +261,18 @@ pub async fn handle_epoch_next_blockfrost( )) => Ok(response.epochs.into_iter().map(EpochActivityRest::from).collect::>()), Message::StateQueryResponse(StateQueryResponse::Epochs( EpochsStateQueryResponse::Error(QueryError::NotFound { .. }), - )) => Err(anyhow::anyhow!("Epoch not found")), + )) => Err(QueryError::not_found("Epoch")), Message::StateQueryResponse(StateQueryResponse::Epochs( EpochsStateQueryResponse::Error(e), - )) => Err(anyhow::anyhow!( - "Internal server error while retrieving next epochs: {e}" - )), - - _ => Err(anyhow::anyhow!( - "Unexpected message type while retrieving next epochs" + )) => Err(e), + _ => Err(QueryError::internal_error( + "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)) } @@ -326,24 +280,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", "invalid epoch number"))?; let previous_epochs_msg = Arc::new(Message::StateQuery(StateQuery::Epochs( EpochsStateQuery::GetPreviousEpochs { @@ -360,28 +307,18 @@ pub async fn handle_epoch_previous_blockfrost( )) => Ok(response.epochs.into_iter().map(EpochActivityRest::from).collect::>()), Message::StateQueryResponse(StateQueryResponse::Epochs( EpochsStateQueryResponse::Error(QueryError::NotFound { .. }), - )) => Err(anyhow::anyhow!("Epoch not found")), + )) => Err(QueryError::not_found("Epoch")), Message::StateQueryResponse(StateQueryResponse::Epochs( EpochsStateQueryResponse::Error(e), - )) => Err(anyhow::anyhow!( - "Internal server error while retrieving previous epochs: {e}" - )), - _ => Err(anyhow::anyhow!( - "Unexpected message type while retrieving previous epochs" + )) => Err(e), + _ => Err(QueryError::internal_error( + "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)) } @@ -389,24 +326,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", "invalid epoch number"))?; // Query latest epoch from epochs-state let latest_epoch_msg = Arc::new(Message::StateQuery(StateQuery::Epochs( @@ -420,15 +350,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::internal_error( + "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 @@ -447,68 +380,54 @@ 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::internal_error( + "Unexpected message type while retrieving SPDD by epoch", )), }, ) .await?; + let spdd_response = 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))?; + 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, 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 pool_id_str = ¶ms[1]; - 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 epoch_number = param + .parse::() + .map_err(|_| RESTError::invalid_param("epoch", "invalid epoch number"))?; + + 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( @@ -522,15 +441,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::internal_error( + "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 @@ -550,75 +472,61 @@ 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::internal_error( + "Unexpected message type while retrieving SPDD by epoch and pool", )), }, ) .await?; + let spdd_response = 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))?; + 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, }) }) - .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("Epoch total blocks endpoint")) } 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", "invalid epoch 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", "invalid Bech32 stake pool ID"))?; // query Pool's Blocks by epoch from spo-state let msg = Arc::new(Message::StateQuery(StateQuery::Pools( @@ -638,10 +546,8 @@ 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(anyhow::anyhow!("Unexpected message type")), + )) => Err(e), + _ => Err(QueryError::internal_error("Unexpected message type")), }, ) .await?; @@ -650,11 +556,6 @@ pub async fn handle_epoch_pool_blocks_blockfrost( // 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 1312bb14..528c4160 100644 --- a/modules/rest_blockfrost/src/handlers/governance.rs +++ b/modules/rest_blockfrost/src/handlers/governance.rs @@ -5,6 +5,7 @@ use crate::types::{ VoterRoleREST, }; use acropolis_common::queries::errors::QueryError; +use acropolis_common::rest_error::RESTError; use acropolis_common::{ messages::{Message, RESTResponse, StateQuery, StateQueryResponse}, queries::{ @@ -13,7 +14,6 @@ use acropolis_common::{ }, Credential, GovActionId, TxHash, Voter, }; -use anyhow::{anyhow, Result}; use caryatid_sdk::Context; use reqwest::Client; use serde_json::Value; @@ -23,46 +23,49 @@ 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 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: Vec = list + let response: Result, RESTError> = list .dreps .iter() .map(|cred| { Ok(DRepsListREST { - drep_id: cred.to_drep_bech32()?, + drep_id: cred + .to_drep_bech32() + .map_err(|e| RESTError::encoding_failed(&format!("DRep ID: {e}")))?, hex: hex::encode(cred.get_hash()), }) }) - .collect::>()?; + .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}"), - )), - } + 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 { .. }), - )) => Ok(RESTResponse::with_text(404, "No DReps found")), + )) => Err(RESTError::not_found("No DReps found")), Message::StateQueryResponse(StateQueryResponse::Governance( GovernanceStateQueryResponse::Error(e), - )) => Ok(RESTResponse::with_text(500, &format!("Query error: {e}"))), + )) => Err(e.into()), - _ => Ok(RESTResponse::with_text(500, "Unexpected message type")), + _ => Err(RESTError::unexpected_response("Unexpected message type")), } } @@ -70,20 +73,12 @@ pub async fn handle_single_drep_blockfrost( context: Arc>, params: Vec, handlers_config: Arc, -) -> Result { +) -> Result { let Some(drep_id) = params.first() else { - return Ok(RESTResponse::with_text(400, "Missing DRep ID parameter")); + return Err(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 { @@ -91,7 +86,11 @@ pub async fn handle_single_drep_blockfrost( }, ))); - let raw_msg = context.message_bus.request(&handlers_config.dreps_query_topic, msg).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 { @@ -106,8 +105,11 @@ pub async fn handle_single_drep_blockfrost( AccountsStateQuery::GetAccountsBalancesSum { stake_addresses }, ))); - let raw_sum = - context.message_bus.request(&handlers_config.accounts_query_topic, sum_msg).await?; + 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 { @@ -118,15 +120,13 @@ pub async fn handle_single_drep_blockfrost( Message::StateQueryResponse(StateQueryResponse::Accounts( AccountsStateQueryResponse::Error(e), )) => { - return Ok(RESTResponse::with_text( - 500, - &format!("Failed to sum balances: {e}"), - )); + return Err(RESTError::InternalServerError(format!( + "Failed to sum balances: {e}" + ))); } _ => { - return Ok(RESTResponse::with_text( - 500, + return Err(RESTError::unexpected_response( "Unexpected response from accounts-state", )); } @@ -144,24 +144,19 @@ pub async fn handle_single_drep_blockfrost( expired: response.info.expired, }; - 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 json = serde_json::to_string_pretty(&response)?; + Ok(RESTResponse::with_json(200, &json)) } Message::StateQueryResponse(StateQueryResponse::Governance( GovernanceStateQueryResponse::Error(QueryError::NotFound { .. }), - )) => Ok(RESTResponse::with_text(404, "DRep not found")), + )) => Err(RESTError::not_found("DRep not found")), Message::StateQueryResponse(StateQueryResponse::Governance( GovernanceStateQueryResponse::Error(e), - )) => Ok(RESTResponse::with_text(500, &e.to_string())), + )) => Err(e.into()), - _ => Ok(RESTResponse::with_text(500, "Unexpected message type")), + _ => Err(RESTError::unexpected_response("Unexpected message type")), } } @@ -169,15 +164,12 @@ pub async fn handle_drep_delegators_blockfrost( context: Arc>, params: Vec, handlers_config: Arc, -) -> Result { +) -> Result { let Some(drep_id) = params.first() else { - return Ok(RESTResponse::with_text(400, "Missing DRep ID parameter")); + return Err(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 { @@ -185,7 +177,11 @@ pub async fn handle_drep_delegators_blockfrost( }, ))); - let raw_msg = context.message_bus.request(&handlers_config.dreps_query_topic, msg).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 { @@ -198,20 +194,26 @@ pub async fn handle_drep_delegators_blockfrost( }, ))); - let raw_msg = - context.message_bus.request(&handlers_config.accounts_query_topic, msg).await?; + 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> = map + let response: Result, RESTError> = map .into_iter() .map(|(stake_address, amount)| { - let bech32 = stake_address - .to_string() - .map_err(|e| anyhow!("Failed to encode stake address {}", e))?; + let bech32 = stake_address.to_string().map_err(|e| { + RESTError::InternalServerError(format!( + "Failed to encode stake address: {}", + e + )) + })?; Ok(serde_json::json!({ "address": bech32, @@ -220,30 +222,16 @@ pub async fn handle_drep_delegators_blockfrost( }) .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}"), - )), - } + let response = response?; + let json = serde_json::to_string_pretty(&response)?; + Ok(RESTResponse::with_json(200, &json)) } Message::StateQueryResponse(StateQueryResponse::Accounts( AccountsStateQueryResponse::Error(e), - )) => Ok(RESTResponse::with_text( - 500, - &format!("Account state error: {e}"), - )), + )) => Err(e.into()), - _ => Ok(RESTResponse::with_text( - 500, + _ => Err(RESTError::unexpected_response( "Unexpected response from accounts-state", )), } @@ -251,31 +239,26 @@ pub async fn handle_drep_delegators_blockfrost( Message::StateQueryResponse(StateQueryResponse::Governance( GovernanceStateQueryResponse::Error(QueryError::NotFound { .. }), - )) => Ok(RESTResponse::with_text(404, "DRep not found")), + )) => Err(RESTError::not_found("DRep not found")), Message::StateQueryResponse(StateQueryResponse::Governance( - GovernanceStateQueryResponse::Error(_), - )) => Ok(RESTResponse::with_text( - 500, - "DRep delegator storage is disabled in config", - )), + GovernanceStateQueryResponse::Error(e), + )) => Err(e.into()), - _ => Ok(RESTResponse::with_text(500, "Unexpected message type")), + _ => Err(RESTError::unexpected_response("Unexpected message type")), } } + pub async fn handle_drep_metadata_blockfrost( context: Arc>, params: Vec, handlers_config: Arc, -) -> Result { +) -> Result { let Some(drep_id) = params.first() else { - return Ok(RESTResponse::with_text(400, "Missing DRep ID parameter")); + return Err(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 { @@ -283,81 +266,61 @@ pub async fn handle_drep_metadata_blockfrost( }, ))); - let raw_msg = context.message_bus.request(&handlers_config.dreps_query_topic, msg).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::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", - )), - } - } + )) => 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)) } - } + }, Message::StateQueryResponse(StateQueryResponse::Governance( GovernanceStateQueryResponse::Error(QueryError::NotFound { .. }), - )) => Ok(RESTResponse::with_text(404, "DRep metadata not found")), + )) => Err(RESTError::not_found("DRep metadata not found")), Message::StateQueryResponse(StateQueryResponse::Governance( - GovernanceStateQueryResponse::Error(_), - )) => Ok(RESTResponse::with_text( - 500, - "DRep metadata storage is disabled in config", - )), + GovernanceStateQueryResponse::Error(e), + )) => Err(e.into()), - _ => Ok(RESTResponse::with_text(500, "Unexpected message type")), + _ => Err(RESTError::unexpected_response("Unexpected message type")), } } @@ -365,15 +328,12 @@ pub async fn handle_drep_updates_blockfrost( context: Arc>, params: Vec, handlers_config: Arc, -) -> Result { +) -> Result { let Some(drep_id) = params.first() else { - return Ok(RESTResponse::with_text(400, "Missing DRep ID parameter")); + return Err(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 { @@ -398,27 +358,19 @@ pub async fn handle_drep_updates_blockfrost( }) .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}"), - )), - } + let json = serde_json::to_string_pretty(&response)?; + Ok(RESTResponse::with_json(200, &json)) } Message::StateQueryResponse(StateQueryResponse::Governance( GovernanceStateQueryResponse::Error(QueryError::NotFound { .. }), - )) => Ok(RESTResponse::with_text(404, "DRep not found")), + )) => Err(RESTError::not_found("DRep not found")), Message::StateQueryResponse(StateQueryResponse::Governance( - GovernanceStateQueryResponse::Error(_), - )) => Ok(RESTResponse::with_text( - 503, - "DRep updates storage is disabled in config", - )), + GovernanceStateQueryResponse::Error(e), + )) => Err(e.into()), - _ => Ok(RESTResponse::with_text(500, "Unexpected message type")), + _ => Err(RESTError::unexpected_response("Unexpected message type")), } } @@ -426,15 +378,12 @@ pub async fn handle_drep_votes_blockfrost( context: Arc>, params: Vec, handlers_config: Arc, -) -> Result { +) -> Result { let Some(drep_id) = params.first() else { - return Ok(RESTResponse::with_text(400, "Missing DRep ID parameter")); + return Err(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 { @@ -458,27 +407,19 @@ pub async fn handle_drep_votes_blockfrost( }) .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}"), - )), - } + let json = serde_json::to_string_pretty(&response)?; + Ok(RESTResponse::with_json(200, &json)) } Message::StateQueryResponse(StateQueryResponse::Governance( GovernanceStateQueryResponse::Error(QueryError::NotFound { .. }), - )) => Ok(RESTResponse::with_text(404, "DRep not found")), + )) => Err(RESTError::not_found("DRep not found")), Message::StateQueryResponse(StateQueryResponse::Governance( - GovernanceStateQueryResponse::Error(_), - )) => Ok(RESTResponse::with_text( - 503, - "DRep vote storage is disabled in config", - )), + GovernanceStateQueryResponse::Error(e), + )) => Err(e.into()), - _ => Ok(RESTResponse::with_text(500, "Unexpected message type")), + _ => Err(RESTError::unexpected_response("Unexpected message type")), } } @@ -486,7 +427,7 @@ 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, ))); @@ -505,30 +446,22 @@ pub async fn handle_proposals_list_blockfrost( 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 vec = props_bech32 + .map_err(|e| RESTError::encoding_failed(&format!("proposal IDs to Bech32: {e}")))?; + + let json = serde_json::to_string(&vec)?; + Ok(RESTResponse::with_json(200, &json)) } Message::StateQueryResponse(StateQueryResponse::Governance( GovernanceStateQueryResponse::Error(QueryError::NotFound { .. }), - )) => Ok(RESTResponse::with_text(404, "No proposals found")), + )) => Err(RESTError::not_found("No proposals found")), Message::StateQueryResponse(StateQueryResponse::Governance( GovernanceStateQueryResponse::Error(e), - )) => Ok(RESTResponse::with_text(500, &format!("Query error: {e}"))), + )) => Err(e.into()), - _ => Ok(RESTResponse::with_text(500, "Unexpected message type")), + _ => Err(RESTError::unexpected_response("Unexpected message type")), } } @@ -536,11 +469,8 @@ 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 }, @@ -552,23 +482,20 @@ pub async fn handle_single_proposal_blockfrost( 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 json = serde_json::to_string(&info)?; + Ok(RESTResponse::with_json(200, &json)) + } Message::StateQueryResponse(StateQueryResponse::Governance( GovernanceStateQueryResponse::Error(QueryError::NotFound { .. }), - )) => Ok(RESTResponse::with_text(404, "Proposal not found")), + )) => Err(RESTError::not_found("Proposal not found")), Message::StateQueryResponse(StateQueryResponse::Governance( GovernanceStateQueryResponse::Error(e), - )) => Ok(RESTResponse::with_text(500, &format!("Query error: {e}"))), + )) => Err(e.into()), - _ => Ok(RESTResponse::with_text(500, "Unexpected message type")), + _ => Err(RESTError::unexpected_response("Unexpected message type")), } } @@ -576,27 +503,24 @@ 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("Proposal parameters endpoint")) } 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("Proposal withdrawals endpoint")) } 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; @@ -635,24 +559,19 @@ pub async fn handle_proposal_votes_blockfrost( }); } - 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}"), - )), - } + let json = serde_json::to_string(&votes_list)?; + Ok(RESTResponse::with_json(200, &json)) } Message::StateQueryResponse(StateQueryResponse::Governance( GovernanceStateQueryResponse::Error(QueryError::NotFound { .. }), - )) => Ok(RESTResponse::with_text(404, "Proposal not found")), + )) => Err(RESTError::not_found("Proposal not found")), Message::StateQueryResponse(StateQueryResponse::Governance( GovernanceStateQueryResponse::Error(e), - )) => Ok(RESTResponse::with_text(500, &format!("Query error: {e}"))), + )) => Err(e.into()), - _ => Ok(RESTResponse::with_text(500, "Unexpected message type")), + _ => Err(RESTError::unexpected_response("Unexpected message type")), } } @@ -660,60 +579,36 @@ 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("Proposal metadata endpoint")) } -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)?; + 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 = 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 action_index = cert_index_str + .parse::() + .map_err(|_| RESTError::invalid_param("cert_index", "expected u8"))?; - Ok(Ok(GovActionId { + 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 4ddf256f..27b22c76 100644 --- a/modules/rest_blockfrost/src/handlers/pools.rs +++ b/modules/rest_blockfrost/src/handlers/pools.rs @@ -8,6 +8,7 @@ use crate::{ utils::{fetch_pool_metadata_as_bytes, verify_pool_metadata_hash, PoolMetadataJson}, }; use acropolis_common::queries::errors::QueryError; +use acropolis_common::rest_error::RESTError; use acropolis_common::serialization::Bech32Conversion; use acropolis_common::{ messages::{Message, RESTResponse, StateQuery, StateQueryResponse}, @@ -20,7 +21,6 @@ use acropolis_common::{ rest_helper::ToCheckedF64, PoolId, PoolRetirement, PoolUpdateAction, TxIdentifier, }; -use anyhow::Result; use caryatid_sdk::Context; use rust_decimal::Decimal; use std::{sync::Arc, time::Duration}; @@ -32,7 +32,7 @@ pub async fn handle_pools_list_blockfrost( context: Arc>, _params: Vec, handlers_config: Arc, -) -> Result { +) -> Result { // Prepare the message let msg = Arc::new(Message::StateQuery(StateQuery::Pools( PoolsStateQuery::GetPoolsList, @@ -40,7 +40,6 @@ pub async fn handle_pools_list_blockfrost( // 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()); @@ -52,33 +51,18 @@ pub async fn handle_pools_list_blockfrost( Message::StateQueryResponse(StateQueryResponse::Pools(PoolsStateQueryResponse::Error( e, ))) => { - return Ok(RESTResponse::with_text( - 500, - &format!("Internal server error while retrieving pools list: {e}"), - )); + return Err(e.into()); } - _ => return Ok(RESTResponse::with_text(500, "Unexpected message type")), + _ => return Err(RESTError::unexpected_response("Unexpected message type")), }; - let pool_ids = 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}"), - )), - } + 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)) } /// Handle `/pools/extended` `/pools/retired` `/pools/retiring` `/pools/{pool_id}` Blockfrost-compatible endpoint @@ -86,10 +70,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() { @@ -102,22 +86,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, @@ -132,11 +113,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::internal_error( + "Unexpected message type while retrieving pools list with info", )), }, ); @@ -155,11 +134,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::internal_error( + "Unexpected message type while retrieving latest epoch", )), }, ); @@ -176,7 +153,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::internal_error("Unexpected message type")), }, ); @@ -226,8 +206,8 @@ async fn handle_pools_extended_blockfrost( // if epoch_history is not enabled Ok(None) } - _ => Err(anyhow::anyhow!( - "Unexpected message type while retrieving pools active stakes" + _ => Err(QueryError::internal_error( + "Unexpected message type while retrieving pools active stakes", )), }, ); @@ -249,11 +229,8 @@ async fn handle_pools_extended_blockfrost( 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::internal_error("Unexpected message type")), }, ); @@ -271,7 +248,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::internal_error("Unexpected message type")), }, ); @@ -284,48 +264,37 @@ 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_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}")))?, + 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(); + + let pools_extened_rest = pools_extened_rest_results?; + let json = serde_json::to_string(&pools_extened_rest)?; + Ok(RESTResponse::with_json(200, &json)) } async fn handle_pools_retired_blockfrost( context: Arc>, handlers_config: Arc, -) -> Result { +) -> Result { // Get retired pools from spo-state let retired_pools_msg = Arc::new(Message::StateQuery(StateQuery::Pools( PoolsStateQuery::GetPoolsRetiredList, @@ -340,10 +309,8 @@ 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::internal_error("Unexpected message type")), }, ) .await?; @@ -359,19 +326,14 @@ async fn handle_pools_retired_blockfrost( }) .collect::>(); - 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 { +) -> Result { // Get retiring pools from spo-state let retiring_pools_msg = Arc::new(Message::StateQuery(StateQuery::Pools( PoolsStateQuery::GetPoolsRetiringList, @@ -386,10 +348,8 @@ 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::internal_error("Unexpected message type")), }, ) .await?; @@ -405,20 +365,15 @@ async fn handle_pools_retiring_blockfrost( }) .collect::>(); - 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 { @@ -436,13 +391,11 @@ async fn handle_pools_spo_blockfrost( )) => Ok(pool_info), Message::StateQueryResponse(StateQueryResponse::Pools( PoolsStateQueryResponse::Error(QueryError::NotFound { .. }), - )) => Err(anyhow::anyhow!("Pool Not found")), + )) => Err(QueryError::not_found("Pool")), 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::internal_error("Unexpected message type")), }, ); @@ -460,11 +413,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::internal_error( + "Unexpected message type while retrieving latest epoch", )), }, ); @@ -481,7 +432,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::internal_error("Unexpected message type")), }, ); @@ -497,7 +451,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::internal_error("Unexpected message type")), }, ); @@ -517,11 +474,11 @@ async fn handle_pools_spo_blockfrost( )) => Ok(Some(pool_updates)), Message::StateQueryResponse(StateQueryResponse::Pools( PoolsStateQueryResponse::Error(QueryError::NotFound { .. }), - )) => Err(anyhow::anyhow!("Pool Not found")), + )) => Err(QueryError::not_found("Pool")), Message::StateQueryResponse(StateQueryResponse::Pools( PoolsStateQueryResponse::Error(_e), )) => Ok(None), - _ => Err(anyhow::anyhow!("Unexpected message type")), + _ => Err(QueryError::internal_error("Unexpected message type")), }, ); @@ -539,7 +496,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::internal_error("Unexpected message type")), }, ); @@ -565,9 +525,10 @@ async fn handle_pools_spo_blockfrost( 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")); + 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 @@ -608,7 +569,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::internal_error("Unexpected message type")), }, ); @@ -630,7 +594,7 @@ async fn handle_pools_spo_blockfrost( Message::StateQueryResponse(StateQueryResponse::Pools( PoolsStateQueryResponse::Error(_e), )) => Ok(None), - _ => Err(anyhow::anyhow!("Unexpected message type")), + _ => Err(QueryError::internal_error("Unexpected message type")), }, ); @@ -652,10 +616,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::internal_error("Unexpected message type")), }, ); @@ -665,19 +627,23 @@ 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_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 = 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, @@ -705,30 +671,21 @@ 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 { +) -> Result { let Some(pool_id) = params.first() else { - return Ok(RESTResponse::with_text(400, "Missing pool ID parameter")); + return Err(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", "invalid Bech32 stake pool ID"))?; // get latest epoch from epochs-state let latest_epoch_info_msg = Arc::new(Message::StateQuery(StateQuery::Epochs( @@ -744,10 +701,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::internal_error("Unexpected message type")), }, ) .await?; @@ -766,12 +721,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::internal_error("Unexpected message type")), }, ) .await?; @@ -779,30 +731,21 @@ 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 { +) -> Result { let Some(pool_id) = params.first() else { - return Ok(RESTResponse::with_text(400, "Missing pool ID parameter")); + return Err(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", "invalid Bech32 stake pool ID"))?; let pool_metadata_msg = Arc::new(Message::StateQuery(StateQuery::Pools( PoolsStateQuery::GetPoolMetadata { pool_id: spo }, @@ -817,13 +760,11 @@ pub async fn handle_pool_metadata_blockfrost( )) => Ok(pool_metadata), Message::StateQueryResponse(StateQueryResponse::Pools( PoolsStateQueryResponse::Error(QueryError::NotFound { .. }), - )) => Err(anyhow::anyhow!("Not found")), + )) => Err(QueryError::not_found("Pool metadata")), 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::internal_error("Unexpected message type")), }, ) .await?; @@ -832,20 +773,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::InternalServerError(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_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", - )); - }; + 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(), @@ -858,30 +795,21 @@ 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 { +) -> Result { let Some(pool_id) = params.first() else { - return Ok(RESTResponse::with_text(400, "Missing pool ID parameter")); + return Err(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", "invalid Bech32 stake pool ID"))?; let pool_relay_msg = Arc::new(Message::StateQuery(StateQuery::Pools( PoolsStateQuery::GetPoolRelays { pool_id: spo }, @@ -897,43 +825,32 @@ pub async fn handle_pool_relays_blockfrost( )) => Ok(pool_relays), Message::StateQueryResponse(StateQueryResponse::Pools( PoolsStateQueryResponse::Error(QueryError::NotFound { .. }), - )) => Err(anyhow::anyhow!("Pool Relays Not found")), + )) => Err(QueryError::not_found("Pool Relays")), 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::internal_error("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 { +) -> Result { let Some(pool_id) = params.first() else { - return Ok(RESTResponse::with_text(400, "Missing pool ID parameter")); + return Err(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", "invalid Bech32 stake pool ID"))?; // Get Pool delegators from spo-state let pool_delegators_msg = Arc::new(Message::StateQuery(StateQuery::Pools( @@ -950,7 +867,7 @@ pub async fn handle_pool_delegators_blockfrost( )) => Ok(Some(pool_delegators.delegators)), Message::StateQueryResponse(StateQueryResponse::Pools( PoolsStateQueryResponse::Error(QueryError::NotFound { .. }), - )) => Err(anyhow::anyhow!("Pool Delegators Not found")), + )) => Err(QueryError::not_found("Pool Delegators")), Message::StateQueryResponse(StateQueryResponse::Pools( PoolsStateQueryResponse::Error(_e), )) => { @@ -958,7 +875,7 @@ pub async fn handle_pool_delegators_blockfrost( warn!("Fallback to query from accounts_state"); Ok(None) } - _ => Err(anyhow::anyhow!("Unexpected message type")), + _ => Err(QueryError::internal_error("Unexpected message type")), }, ) .await?; @@ -981,10 +898,8 @@ 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::internal_error("Unexpected message type")), }, ) .await?; @@ -994,39 +909,30 @@ pub async fn handle_pool_delegators_blockfrost( 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}"))?; + 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(), }); } - 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 { +) -> Result { let Some(pool_id) = params.first() else { - return Ok(RESTResponse::with_text(400, "Missing pool ID parameter")); + return Err(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", "invalid Bech32 stake pool ID"))?; // Get blocks by pool_id from spo_state let pool_blocks_msg = Arc::new(Message::StateQuery(StateQuery::Pools( @@ -1042,9 +948,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::internal_error("Unexpected message type")), }, ) .await?; @@ -1053,30 +959,21 @@ 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 { +) -> Result { let Some(pool_id) = params.first() else { - return Ok(RESTResponse::with_text(400, "Missing pool ID parameter")); + return Err(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", "invalid Bech32 stake pool ID"))?; // query from spo_state let pool_updates_msg = Arc::new(Message::StateQuery(StateQuery::Pools( @@ -1092,16 +989,15 @@ pub async fn handle_pool_updates_blockfrost( )) => Ok(pool_updates), Message::StateQueryResponse(StateQueryResponse::Pools( PoolsStateQueryResponse::Error(QueryError::NotFound { .. }), - )) => Err(anyhow::anyhow!("Pool not found")), + )) => Err(QueryError::not_found("Pool")), 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::internal_error("Unexpected message type")), }, ) .await?; + let pool_updates_rest = pool_updates .into_iter() .map(|u| PoolUpdateEventRest { @@ -1111,30 +1007,21 @@ 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 { +) -> Result { let Some(pool_id) = params.first() else { - return Ok(RESTResponse::with_text(400, "Missing pool ID parameter")); + return Err(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", "invalid Bech32 stake pool ID"))?; // query from spo_state let pool_votes_msg = Arc::new(Message::StateQuery(StateQuery::Pools( @@ -1150,10 +1037,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::internal_error("Unexpected message type")), }, ) .await?; @@ -1167,11 +1052,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 95815757..0465b65c 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,6 +11,7 @@ use anyhow::Result; use caryatid_sdk::{module, Context, Module}; use config::Config; use tracing::info; + mod cost_models; mod handlers; mod handlers_config; @@ -766,11 +768,10 @@ 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(); @@ -793,11 +794,10 @@ 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..adc0d21c 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::rest_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, { @@ -38,12 +39,7 @@ pub async fn handle_spdd( 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}" - ), - )), + Err(e) => Err(RESTError::from(e)), } } else { Ok(RESTResponse::with_json(200, "{}"))