From 6e9366058916c58e8d4e773aee55073b92484dea Mon Sep 17 00:00:00 2001 From: William Hankins Date: Thu, 23 Oct 2025 20:20:45 +0000 Subject: [PATCH 1/5] feat: reg, deleg, and mir REST handlers Signed-off-by: William Hankins --- .../src/historical_accounts_state.rs | 3 + .../rest_blockfrost/src/handlers/accounts.rs | 347 +++++++++++++++++- .../rest_blockfrost/src/handlers_config.rs | 8 +- .../rest_blockfrost/src/rest_blockfrost.rs | 42 ++- modules/rest_blockfrost/src/types.rs | 20 + 5 files changed, 401 insertions(+), 19 deletions(-) diff --git a/modules/historical_accounts_state/src/historical_accounts_state.rs b/modules/historical_accounts_state/src/historical_accounts_state.rs index fb957d60..cb052599 100644 --- a/modules/historical_accounts_state/src/historical_accounts_state.rs +++ b/modules/historical_accounts_state/src/historical_accounts_state.rs @@ -212,10 +212,13 @@ impl HistoricalAccountsState { (state.immutable.clone(), state.config.clone()) }; + info!("sending persist for epoch {}", current_block.epoch); if let Err(e) = persist_tx.send((current_block.epoch as u32, store, cfg)).await { panic!("persistence worker crashed: {e}"); } + + info!("persist send completed"); } } diff --git a/modules/rest_blockfrost/src/handlers/accounts.rs b/modules/rest_blockfrost/src/handlers/accounts.rs index 7ae84995..d17ac7c9 100644 --- a/modules/rest_blockfrost/src/handlers/accounts.rs +++ b/modules/rest_blockfrost/src/handlers/accounts.rs @@ -3,6 +3,9 @@ use std::sync::Arc; use acropolis_common::messages::{Message, RESTResponse, StateQuery, StateQueryResponse}; use acropolis_common::queries::accounts::{AccountsStateQuery, AccountsStateQueryResponse}; +use acropolis_common::queries::blocks::{ + BlocksStateQuery, BlocksStateQueryResponse, TransactionHashes, +}; use acropolis_common::queries::utils::query_state; use acropolis_common::serialization::Bech32WithHrp; use acropolis_common::{DRepChoice, StakeAddress}; @@ -10,6 +13,7 @@ use anyhow::{anyhow, Result}; use caryatid_sdk::Context; use crate::handlers_config::HandlersConfig; +use crate::types::{AccountWithdrawalREST, DelegationUpdateREST, RegistrationUpdateREST}; #[derive(serde::Serialize)] pub struct StakeAccountRest { @@ -31,24 +35,10 @@ pub async fn handle_single_account_blockfrost( params: Vec, handlers_config: Arc, ) -> Result { - let Some(stake_key) = params.get(0) else { - return Ok(RESTResponse::with_text( - 400, - "Missing stake address parameter", - )); - }; - - // Convert Bech32 stake address to StakeAddress - let stake_address = match StakeAddress::from_string(&stake_key) { + let stake_address = match parse_stake_address(¶ms) { Ok(addr) => addr, - _ => { - return Ok(RESTResponse::with_text( - 400, - &format!("Not a valid stake address: {stake_key}"), - )); - } + Err(resp) => return Ok(resp), }; - // Prepare the message let msg = Arc::new(Message::StateQuery(StateQuery::Accounts( AccountsStateQuery::GetAccountInfo { stake_address }, @@ -115,7 +105,7 @@ pub async fn handle_single_account_blockfrost( delegated_drep, }; - match serde_json::to_string(&rest_response) { + match serde_json::to_string_pretty(&rest_response) { Ok(json) => Ok(RESTResponse::with_json(200, &json)), Err(e) => Ok(RESTResponse::with_text( 500, @@ -124,6 +114,329 @@ pub async fn handle_single_account_blockfrost( } } +/// Handle `/accounts/{stake_address}/registrations` Blockfrost-compatible endpoint +pub async fn handle_account_registrations_blockfrost( + context: Arc>, + params: Vec, + handlers_config: Arc, +) -> Result { + let stake_address = match parse_stake_address(¶ms) { + Ok(addr) => addr, + Err(resp) => return Ok(resp), + }; + + // Prepare the message + let msg = Arc::new(Message::StateQuery(StateQuery::Accounts( + AccountsStateQuery::GetAccountRegistrationHistory { + account: stake_address, + }, + ))); + + // Get registrations from historical accounts state + let registrations = query_state( + &context, + &handlers_config.historical_accounts_query_topic, + msg, + |message| match message { + Message::StateQueryResponse(StateQueryResponse::Accounts( + AccountsStateQueryResponse::AccountRegistrationHistory(registrations), + )) => Ok(registrations), + Message::StateQueryResponse(StateQueryResponse::Accounts( + AccountsStateQueryResponse::NotFound, + )) => { + return Err(anyhow::anyhow!("Account not found")); + } + Message::StateQueryResponse(StateQueryResponse::Accounts( + AccountsStateQueryResponse::Error(e), + )) => { + return Err(anyhow::anyhow!( + "Internal server error while retrieving account info: {e}" + )); + } + _ => { + return Err(anyhow::anyhow!( + "Unexpected message type while retrieving account info" + )) + } + }, + ) + .await?; + + // Get TxHashes from TxIdentifiers + let tx_ids: Vec<_> = registrations.iter().map(|r| r.tx_identifier.clone()).collect(); + let msg = Arc::new(Message::StateQuery(StateQuery::Blocks( + BlocksStateQuery::GetTransactionHashes { tx_ids }, + ))); + let tx_hashes = query_state( + &context, + &handlers_config.blocks_query_topic, + msg, + |message| match message { + Message::StateQueryResponse(StateQueryResponse::Blocks( + BlocksStateQueryResponse::TransactionHashes(TransactionHashes { tx_hashes }), + )) => Ok(tx_hashes), + Message::StateQueryResponse(StateQueryResponse::Blocks( + BlocksStateQueryResponse::Error(e), + )) => Err(anyhow::anyhow!( + "Internal server error while resolving transaction hashes: {e}" + )), + _ => Err(anyhow::anyhow!( + "Unexpected message type while resolving transaction hashes" + )), + }, + ) + .await?; + + let mut rest_response = Vec::new(); + + for r in registrations { + let Some(tx_hash) = tx_hashes.get(&r.tx_identifier) else { + return Ok(RESTResponse::with_text( + 500, + "Missing tx hash for registration", + )); + }; + + rest_response.push(RegistrationUpdateREST { + tx_hash: hex::encode(tx_hash), + action: if r.deregistered { + "deregistered".to_string() + } else { + "registered".to_string() + }, + }); + } + + 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 account registration history: {e}"), + )), + } +} + +/// Handle `/accounts/{stake_address}/delegations` Blockfrost-compatible endpoint +pub async fn handle_account_delegations_blockfrost( + context: Arc>, + params: Vec, + handlers_config: Arc, +) -> Result { + let stake_address = match parse_stake_address(¶ms) { + Ok(addr) => addr, + Err(resp) => return Ok(resp), + }; + + // Prepare the message + let msg = Arc::new(Message::StateQuery(StateQuery::Accounts( + AccountsStateQuery::GetAccountDelegationHistory { + account: stake_address, + }, + ))); + + // Get delegations from historical accounts state + let delegations = query_state( + &context, + &handlers_config.historical_accounts_query_topic, + msg, + |message| match message { + Message::StateQueryResponse(StateQueryResponse::Accounts( + AccountsStateQueryResponse::AccountDelegationHistory(delegations), + )) => Ok(delegations), + Message::StateQueryResponse(StateQueryResponse::Accounts( + AccountsStateQueryResponse::NotFound, + )) => { + return Err(anyhow::anyhow!("Account not found")); + } + Message::StateQueryResponse(StateQueryResponse::Accounts( + AccountsStateQueryResponse::Error(e), + )) => { + return Err(anyhow::anyhow!( + "Internal server error while retrieving account info: {e}" + )); + } + _ => { + return Err(anyhow::anyhow!( + "Unexpected message type while retrieving account info" + )) + } + }, + ) + .await?; + + // Get TxHashes from TxIdentifiers + let tx_ids: Vec<_> = delegations.iter().map(|r| r.tx_identifier.clone()).collect(); + let msg = Arc::new(Message::StateQuery(StateQuery::Blocks( + BlocksStateQuery::GetTransactionHashes { tx_ids }, + ))); + let tx_hashes = query_state( + &context, + &handlers_config.blocks_query_topic, + msg, + |message| match message { + Message::StateQueryResponse(StateQueryResponse::Blocks( + BlocksStateQueryResponse::TransactionHashes(TransactionHashes { tx_hashes }), + )) => Ok(tx_hashes), + Message::StateQueryResponse(StateQueryResponse::Blocks( + BlocksStateQueryResponse::Error(e), + )) => Err(anyhow::anyhow!( + "Internal server error while resolving transaction hashes: {e}" + )), + _ => Err(anyhow::anyhow!( + "Unexpected message type while resolving transaction hashes" + )), + }, + ) + .await?; + + let mut rest_response = Vec::new(); + + for r in delegations { + let Some(tx_hash) = tx_hashes.get(&r.tx_identifier) else { + return Ok(RESTResponse::with_text( + 500, + "Missing tx hash for delegation", + )); + }; + + let pool_id = match r.pool.to_bech32_with_hrp("pool") { + Ok(p) => p, + Err(e) => { + return Ok(RESTResponse::with_text( + 500, + &format!("Failed to encode pool ID: {e}"), + )); + } + }; + + rest_response.push(DelegationUpdateREST { + active_epoch: r.active_epoch, + tx_hash: hex::encode(tx_hash), + amount: r.amount.to_string(), + pool_id, + }); + } + + 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 account delegation history: {e}"), + )), + } +} + +/// Handle `/accounts/{stake_address}/mirs` Blockfrost-compatible endpoint +pub async fn handle_account_mirs_blockfrost( + context: Arc>, + params: Vec, + handlers_config: Arc, +) -> Result { + let stake_address = match parse_stake_address(¶ms) { + Ok(addr) => addr, + Err(resp) => return Ok(resp), + }; + + // Prepare the message + let msg = Arc::new(Message::StateQuery(StateQuery::Accounts( + AccountsStateQuery::GetAccountMIRHistory { + account: stake_address, + }, + ))); + + // Get delegations from historical accounts state + let mirs = query_state( + &context, + &handlers_config.historical_accounts_query_topic, + msg, + |message| match message { + Message::StateQueryResponse(StateQueryResponse::Accounts( + AccountsStateQueryResponse::AccountMIRHistory(mirs), + )) => Ok(mirs), + Message::StateQueryResponse(StateQueryResponse::Accounts( + AccountsStateQueryResponse::NotFound, + )) => { + return Err(anyhow::anyhow!("Account not found")); + } + Message::StateQueryResponse(StateQueryResponse::Accounts( + AccountsStateQueryResponse::Error(e), + )) => { + return Err(anyhow::anyhow!( + "Internal server error while retrieving account info: {e}" + )); + } + _ => { + return Err(anyhow::anyhow!( + "Unexpected message type while retrieving account info" + )) + } + }, + ) + .await?; + + // Get TxHashes from TxIdentifiers + let tx_ids: Vec<_> = mirs.iter().map(|r| r.tx_identifier.clone()).collect(); + let msg = Arc::new(Message::StateQuery(StateQuery::Blocks( + BlocksStateQuery::GetTransactionHashes { tx_ids }, + ))); + let tx_hashes = query_state( + &context, + &handlers_config.blocks_query_topic, + msg, + |message| match message { + Message::StateQueryResponse(StateQueryResponse::Blocks( + BlocksStateQueryResponse::TransactionHashes(TransactionHashes { tx_hashes }), + )) => Ok(tx_hashes), + Message::StateQueryResponse(StateQueryResponse::Blocks( + BlocksStateQueryResponse::Error(e), + )) => Err(anyhow::anyhow!( + "Internal server error while resolving transaction hashes: {e}" + )), + _ => Err(anyhow::anyhow!( + "Unexpected message type while resolving transaction hashes" + )), + }, + ) + .await?; + + let mut rest_response = Vec::new(); + + for r in mirs { + let Some(tx_hash) = tx_hashes.get(&r.tx_identifier) else { + return Ok(RESTResponse::with_text( + 500, + "Missing tx hash for MIR record", + )); + }; + + rest_response.push(AccountWithdrawalREST { + tx_hash: hex::encode(tx_hash), + amount: r.amount.to_string(), + }); + } + + 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}"), + )), + } +} + +fn parse_stake_address(params: &[String]) -> Result { + let Some(stake_key) = params.first() else { + return Err(RESTResponse::with_text( + 400, + "Missing stake address parameter", + )); + }; + + StakeAddress::from_string(stake_key).map_err(|_| { + RESTResponse::with_text(400, &format!("Not a valid stake address: {stake_key}")) + }) +} + fn map_drep_choice(drep: &DRepChoice) -> Result { match drep { DRepChoice::Key(hash) => { diff --git a/modules/rest_blockfrost/src/handlers_config.rs b/modules/rest_blockfrost/src/handlers_config.rs index 029541d1..837bfb06 100644 --- a/modules/rest_blockfrost/src/handlers_config.rs +++ b/modules/rest_blockfrost/src/handlers_config.rs @@ -1,7 +1,7 @@ use std::sync::Arc; use acropolis_common::queries::{ - accounts::DEFAULT_ACCOUNTS_QUERY_TOPIC, + accounts::{DEFAULT_ACCOUNTS_QUERY_TOPIC, DEFAULT_HISTORICAL_ACCOUNTS_QUERY_TOPIC}, addresses::DEFAULT_ADDRESS_QUERY_TOPIC, assets::{DEFAULT_ASSETS_QUERY_TOPIC, DEFAULT_OFFCHAIN_TOKEN_REGISTRY_URL}, blocks::DEFAULT_BLOCKS_QUERY_TOPIC, @@ -19,6 +19,7 @@ const DEFAULT_EXTERNAL_API_TIMEOUT: (&str, i64) = ("external_api_timeout", 3); / #[derive(Clone)] pub struct HandlersConfig { pub accounts_query_topic: String, + pub historical_accounts_query_topic: String, pub addresses_query_topic: String, pub assets_query_topic: String, pub blocks_query_topic: String, @@ -39,6 +40,10 @@ impl From> for HandlersConfig { .get_string(DEFAULT_ACCOUNTS_QUERY_TOPIC.0) .unwrap_or(DEFAULT_ACCOUNTS_QUERY_TOPIC.1.to_string()); + let historical_accounts_query_topic = config + .get_string(DEFAULT_HISTORICAL_ACCOUNTS_QUERY_TOPIC.0) + .unwrap_or(DEFAULT_HISTORICAL_ACCOUNTS_QUERY_TOPIC.1.to_string()); + let addresses_query_topic = config .get_string(DEFAULT_ADDRESS_QUERY_TOPIC.0) .unwrap_or(DEFAULT_ADDRESS_QUERY_TOPIC.1.to_string()); @@ -89,6 +94,7 @@ impl From> for HandlersConfig { Self { accounts_query_topic, + historical_accounts_query_topic, addresses_query_topic, assets_query_topic, blocks_query_topic, diff --git a/modules/rest_blockfrost/src/rest_blockfrost.rs b/modules/rest_blockfrost/src/rest_blockfrost.rs index ac72e60a..66e4763d 100644 --- a/modules/rest_blockfrost/src/rest_blockfrost.rs +++ b/modules/rest_blockfrost/src/rest_blockfrost.rs @@ -58,11 +58,27 @@ use handlers::{ }, }; -use crate::handlers_config::HandlersConfig; +use crate::{ + handlers::accounts::{ + handle_account_delegations_blockfrost, handle_account_mirs_blockfrost, + handle_account_registrations_blockfrost, + }, + handlers_config::HandlersConfig, +}; // Accounts topics const DEFAULT_HANDLE_SINGLE_ACCOUNT_TOPIC: (&str, &str) = ("handle-topic-account-single", "rest.get.accounts.*"); +const DEFAULT_HANDLE_ACCOUNT_REGISTRATIONS_TOPIC: (&str, &str) = ( + "handle-topic-account-registrations", + "rest.get.accounts.*.registrations", +); +const DEFAULT_HANDLE_ACCOUNT_DELEGATIONS_TOPIC: (&str, &str) = ( + "handle-topic-account-delegations", + "rest.get.accounts.*.delegations", +); +const DEFAULT_HANDLE_ACCOUNT_MIRS_TOPIC: (&str, &str) = + ("handle-topic-account-mirs", "rest.get.accounts.*.mirs"); // Blocks topics const DEFAULT_HANDLE_BLOCKS_LATEST_HASH_NUMBER_TOPIC: (&str, &str) = @@ -249,6 +265,30 @@ impl BlockfrostREST { handle_single_account_blockfrost, ); + // Handler for /accounts/{stake_address}/registrations + register_handler( + context.clone(), + DEFAULT_HANDLE_ACCOUNT_REGISTRATIONS_TOPIC, + handlers_config.clone(), + handle_account_registrations_blockfrost, + ); + + // Handler for /accounts/{stake_address}/delegations + register_handler( + context.clone(), + DEFAULT_HANDLE_ACCOUNT_DELEGATIONS_TOPIC, + handlers_config.clone(), + handle_account_delegations_blockfrost, + ); + + // Handler for /accounts/{stake_address}/mirs + register_handler( + context.clone(), + DEFAULT_HANDLE_ACCOUNT_MIRS_TOPIC, + handlers_config.clone(), + handle_account_mirs_blockfrost, + ); + // Handler for /blocks/latest, /blocks/{hash_or_number} register_handler( context.clone(), diff --git a/modules/rest_blockfrost/src/types.rs b/modules/rest_blockfrost/src/types.rs index b4cd3364..bfcf8fbf 100644 --- a/modules/rest_blockfrost/src/types.rs +++ b/modules/rest_blockfrost/src/types.rs @@ -854,3 +854,23 @@ impl From for AmountList { Self(out) } } + +#[derive(Serialize)] +pub struct RegistrationUpdateREST { + pub tx_hash: String, + pub action: String, +} + +#[derive(Serialize)] +pub struct DelegationUpdateREST { + pub active_epoch: u32, + pub tx_hash: String, + pub amount: String, + pub pool_id: String, +} + +#[derive(Serialize)] +pub struct AccountWithdrawalREST { + pub tx_hash: String, + pub amount: String, +} From eabd1fa5cfa29d8964102a57a101283f92e5dc01 Mon Sep 17 00:00:00 2001 From: William Hankins Date: Mon, 27 Oct 2025 23:00:28 +0000 Subject: [PATCH 2/5] fix: impl Display on RegistrationStatus Signed-off-by: William Hankins --- common/src/queries/accounts.rs | 9 +++++++++ modules/rest_blockfrost/src/handlers/accounts.rs | 6 +----- 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/common/src/queries/accounts.rs b/common/src/queries/accounts.rs index 979425ce..be573a13 100644 --- a/common/src/queries/accounts.rs +++ b/common/src/queries/accounts.rs @@ -131,6 +131,15 @@ pub enum RegistrationStatus { Deregistered, } +impl std::fmt::Display for RegistrationStatus { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + RegistrationStatus::Registered => write!(f, "registered"), + RegistrationStatus::Deregistered => write!(f, "deregistered"), + } + } +} + #[derive( Debug, Clone, serde::Serialize, serde::Deserialize, minicbor::Decode, minicbor::Encode, )] diff --git a/modules/rest_blockfrost/src/handlers/accounts.rs b/modules/rest_blockfrost/src/handlers/accounts.rs index d17ac7c9..a96e6ecb 100644 --- a/modules/rest_blockfrost/src/handlers/accounts.rs +++ b/modules/rest_blockfrost/src/handlers/accounts.rs @@ -199,11 +199,7 @@ pub async fn handle_account_registrations_blockfrost( rest_response.push(RegistrationUpdateREST { tx_hash: hex::encode(tx_hash), - action: if r.deregistered { - "deregistered".to_string() - } else { - "registered".to_string() - }, + action: r.status.to_string(), }); } From 931188a889c8947cf35817bf636c09b89e7acf37 Mon Sep 17 00:00:00 2001 From: William Hankins Date: Mon, 27 Oct 2025 23:30:41 +0000 Subject: [PATCH 3/5] fix: remove excessive persistence logging Signed-off-by: William Hankins --- .../src/historical_accounts_state.rs | 3 --- .../src/immutable_historical_account_store.rs | 15 ++++++++------- 2 files changed, 8 insertions(+), 10 deletions(-) diff --git a/modules/historical_accounts_state/src/historical_accounts_state.rs b/modules/historical_accounts_state/src/historical_accounts_state.rs index cb052599..fb957d60 100644 --- a/modules/historical_accounts_state/src/historical_accounts_state.rs +++ b/modules/historical_accounts_state/src/historical_accounts_state.rs @@ -212,13 +212,10 @@ impl HistoricalAccountsState { (state.immutable.clone(), state.config.clone()) }; - info!("sending persist for epoch {}", current_block.epoch); if let Err(e) = persist_tx.send((current_block.epoch as u32, store, cfg)).await { panic!("persistence worker crashed: {e}"); } - - info!("persist send completed"); } } diff --git a/modules/historical_accounts_state/src/immutable_historical_account_store.rs b/modules/historical_accounts_state/src/immutable_historical_account_store.rs index bea59143..96ed17b6 100644 --- a/modules/historical_accounts_state/src/immutable_historical_account_store.rs +++ b/modules/historical_accounts_state/src/immutable_historical_account_store.rs @@ -9,7 +9,7 @@ use fjall::{Keyspace, Partition, PartitionCreateOptions}; use minicbor::{decode, to_vec}; use rayon::iter::{IntoParallelIterator, ParallelIterator}; use tokio::sync::Mutex; -use tracing::{debug, error, info}; +use tracing::{debug, error}; use crate::state::{AccountEntry, ActiveStakeHistory, HistoricalAccountsConfig, RewardHistory}; @@ -61,10 +61,14 @@ impl ImmutableHistoricalAccountStore { /// and addresses into their respective Fjall partitions for an entire epoch. /// Skips any partitions that have already stored the given epoch. /// All writes are batched and committed atomically, preventing on-disk corruption in case of failure. - pub async fn persist_epoch(&self, epoch: u32, config: &HistoricalAccountsConfig) -> Result<()> { + pub async fn persist_epoch( + &self, + epoch: u32, + config: &HistoricalAccountsConfig, + ) -> Result { if !config.any_enabled() { debug!("no persistence needed for epoch {epoch} (disabled)",); - return Ok(()); + return Ok(0); } let drained_blocks = { @@ -135,10 +139,7 @@ impl ImmutableHistoricalAccountStore { } match batch.commit() { - Ok(_) => { - info!("committed {change_count} account changes for epoch {epoch}"); - Ok(()) - } + Ok(_) => Ok(change_count), Err(e) => { error!("batch commit failed for epoch {epoch}: {e}"); Err(e.into()) From f39c6d430748f254f0139c17f57bbdda06652220 Mon Sep 17 00:00:00 2001 From: William Hankins Date: Tue, 28 Oct 2025 17:05:27 +0000 Subject: [PATCH 4/5] fix: cleanup comments and error responses Signed-off-by: William Hankins --- .../rest_blockfrost/src/handlers/accounts.rs | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/modules/rest_blockfrost/src/handlers/accounts.rs b/modules/rest_blockfrost/src/handlers/accounts.rs index a96e6ecb..4be43567 100644 --- a/modules/rest_blockfrost/src/handlers/accounts.rs +++ b/modules/rest_blockfrost/src/handlers/accounts.rs @@ -78,7 +78,7 @@ pub async fn handle_single_account_blockfrost( Err(e) => { return Ok(RESTResponse::with_text( 500, - &format!("Internal server error while retrieving stake address: {e}"), + &format!("Internal server error while mapping SPO: {e}"), )); } }, @@ -91,7 +91,7 @@ pub async fn handle_single_account_blockfrost( Err(e) => { return Ok(RESTResponse::with_text( 500, - &format!("Internal server error while retrieving stake address: {e}"), + &format!("Internal server error while mapping dRep: {e}"), )) } }, @@ -109,7 +109,7 @@ pub async fn handle_single_account_blockfrost( Ok(json) => Ok(RESTResponse::with_json(200, &json)), Err(e) => Ok(RESTResponse::with_text( 500, - &format!("Internal server error while retrieving DRep delegation distribution: {e}"), + &format!("Internal server error while retrieving account info: {e}"), )), } } @@ -207,7 +207,7 @@ pub async fn handle_account_registrations_blockfrost( Ok(json) => Ok(RESTResponse::with_json(200, &json)), Err(e) => Ok(RESTResponse::with_text( 500, - &format!("Internal server error while serializing account registration history: {e}"), + &format!("Internal server error while serializing registration history: {e}"), )), } } @@ -317,7 +317,7 @@ pub async fn handle_account_delegations_blockfrost( Ok(json) => Ok(RESTResponse::with_json(200, &json)), Err(e) => Ok(RESTResponse::with_text( 500, - &format!("Internal server error while serializing account delegation history: {e}"), + &format!("Internal server error while serializing delegation history: {e}"), )), } } @@ -328,19 +328,17 @@ pub async fn handle_account_mirs_blockfrost( params: Vec, handlers_config: Arc, ) -> Result { - let stake_address = match parse_stake_address(¶ms) { + let account = match parse_stake_address(¶ms) { Ok(addr) => addr, Err(resp) => return Ok(resp), }; // Prepare the message let msg = Arc::new(Message::StateQuery(StateQuery::Accounts( - AccountsStateQuery::GetAccountMIRHistory { - account: stake_address, - }, + AccountsStateQuery::GetAccountMIRHistory { account }, ))); - // Get delegations from historical accounts state + // Get MIRs from historical accounts state let mirs = query_state( &context, &handlers_config.historical_accounts_query_topic, From 5eb101acaeea61d3ad4a0814d13be444f106fd82 Mon Sep 17 00:00:00 2001 From: William Hankins Date: Wed, 29 Oct 2025 00:11:40 +0000 Subject: [PATCH 5/5] fix: return 404 on NotFound for accounts endpoints Signed-off-by: William Hankins --- .../src/historical_accounts_state.rs | 9 +- .../src/immutable_historical_account_store.rs | 10 +- .../historical_accounts_state/src/state.rs | 61 ++++++---- .../rest_blockfrost/src/handlers/accounts.rs | 104 ++++++++---------- 4 files changed, 98 insertions(+), 86 deletions(-) diff --git a/modules/historical_accounts_state/src/historical_accounts_state.rs b/modules/historical_accounts_state/src/historical_accounts_state.rs index fb957d60..b64fa5cb 100644 --- a/modules/historical_accounts_state/src/historical_accounts_state.rs +++ b/modules/historical_accounts_state/src/historical_accounts_state.rs @@ -321,25 +321,28 @@ impl HistoricalAccountsState { let response = match query { AccountsStateQuery::GetAccountRegistrationHistory { account } => { match state.lock().await.get_registration_history(&account).await { - Ok(registrations) => { + Ok(Some(registrations)) => { AccountsStateQueryResponse::AccountRegistrationHistory( registrations, ) } + Ok(None) => AccountsStateQueryResponse::NotFound, Err(e) => AccountsStateQueryResponse::Error(e.to_string()), } } AccountsStateQuery::GetAccountDelegationHistory { account } => { match state.lock().await.get_delegation_history(&account).await { - Ok(delegations) => { + Ok(Some(delegations)) => { AccountsStateQueryResponse::AccountDelegationHistory(delegations) } + Ok(None) => AccountsStateQueryResponse::NotFound, Err(e) => AccountsStateQueryResponse::Error(e.to_string()), } } AccountsStateQuery::GetAccountMIRHistory { account } => { match state.lock().await.get_mir_history(&account).await { - Ok(mirs) => AccountsStateQueryResponse::AccountMIRHistory(mirs), + Ok(Some(mirs)) => AccountsStateQueryResponse::AccountMIRHistory(mirs), + Ok(None) => AccountsStateQueryResponse::NotFound, Err(e) => AccountsStateQueryResponse::Error(e.to_string()), } } diff --git a/modules/historical_accounts_state/src/immutable_historical_account_store.rs b/modules/historical_accounts_state/src/immutable_historical_account_store.rs index 96ed17b6..ae6fbd0e 100644 --- a/modules/historical_accounts_state/src/immutable_historical_account_store.rs +++ b/modules/historical_accounts_state/src/immutable_historical_account_store.rs @@ -66,16 +66,16 @@ impl ImmutableHistoricalAccountStore { epoch: u32, config: &HistoricalAccountsConfig, ) -> Result { - if !config.any_enabled() { - debug!("no persistence needed for epoch {epoch} (disabled)",); - return Ok(0); - } - let drained_blocks = { let mut pending = self.pending.lock().await; std::mem::take(&mut *pending) }; + if !config.any_enabled() { + debug!("no persistence needed for epoch {epoch} (disabled)",); + return Ok(0); + } + let mut batch = self.keyspace.batch(); let mut change_count = 0; diff --git a/modules/historical_accounts_state/src/state.rs b/modules/historical_accounts_state/src/state.rs index 96f1fbdf..89690432 100644 --- a/modules/historical_accounts_state/src/state.rs +++ b/modules/historical_accounts_state/src/state.rs @@ -249,41 +249,58 @@ impl State { pub async fn get_registration_history( &self, account: &StakeAddress, - ) -> Result> { - let mut registrations = - self.immutable.get_registration_history(&account).await?.unwrap_or_default(); + ) -> Result>> { + let immutable = self.immutable.get_registration_history(&account).await?; - self.merge_volatile_history( - &account, - |e| e.registration_history.as_ref(), - &mut registrations, - ); + let mut volatile = Vec::new(); + self.merge_volatile_history(&account, |e| e.registration_history.as_ref(), &mut volatile); - Ok(registrations) + match immutable { + Some(mut registrations) => { + registrations.extend(volatile); + Ok(Some(registrations)) + } + None if volatile.is_empty() => Ok(None), + None => Ok(Some(volatile)), + } } pub async fn get_delegation_history( &self, account: &StakeAddress, - ) -> Result> { - let mut delegations = - self.immutable.get_delegation_history(&account).await?.unwrap_or_default(); + ) -> Result>> { + let immutable = self.immutable.get_delegation_history(&account).await?; - self.merge_volatile_history( - &account, - |e| e.delegation_history.as_ref(), - &mut delegations, - ); + let mut volatile = Vec::new(); + self.merge_volatile_history(&account, |e| e.delegation_history.as_ref(), &mut volatile); - Ok(delegations) + match immutable { + Some(mut delegations) => { + delegations.extend(volatile); + Ok(Some(delegations)) + } + None if volatile.is_empty() => Ok(None), + None => Ok(Some(volatile)), + } } - pub async fn get_mir_history(&self, account: &StakeAddress) -> Result> { - let mut mirs = self.immutable.get_mir_history(&account).await?.unwrap_or_default(); + pub async fn get_mir_history( + &self, + account: &StakeAddress, + ) -> Result>> { + let immutable = self.immutable.get_mir_history(&account).await?; - self.merge_volatile_history(&account, |e| e.mir_history.as_ref(), &mut mirs); + let mut volatile = Vec::new(); + self.merge_volatile_history(&account, |e| e.mir_history.as_ref(), &mut volatile); - Ok(mirs) + match immutable { + Some(mut mirs) => { + mirs.extend(volatile); + Ok(Some(mirs)) + } + None if volatile.is_empty() => Ok(None), + None => Ok(Some(volatile)), + } } pub async fn _get_withdrawal_history( diff --git a/modules/rest_blockfrost/src/handlers/accounts.rs b/modules/rest_blockfrost/src/handlers/accounts.rs index 4be43567..f4784615 100644 --- a/modules/rest_blockfrost/src/handlers/accounts.rs +++ b/modules/rest_blockfrost/src/handlers/accounts.rs @@ -50,28 +50,26 @@ pub async fn handle_single_account_blockfrost( |message| match message { Message::StateQueryResponse(StateQueryResponse::Accounts( AccountsStateQueryResponse::AccountInfo(account), - )) => Ok(account), + )) => Ok(Some(account)), Message::StateQueryResponse(StateQueryResponse::Accounts( AccountsStateQueryResponse::NotFound, - )) => { - return Err(anyhow::anyhow!("Account not found")); - } + )) => Ok(None), Message::StateQueryResponse(StateQueryResponse::Accounts( AccountsStateQueryResponse::Error(e), - )) => { - return Err(anyhow::anyhow!( - "Internal server error while retrieving account info: {e}" - )); - } - _ => { - return Err(anyhow::anyhow!( - "Unexpected message type while retrieving account info" - )) - } + )) => Err(anyhow::anyhow!( + "Internal server error while retrieving account info: {e}" + )), + _ => Err(anyhow::anyhow!( + "Unexpected message type while retrieving account info" + )), }, ) .await?; + let Some(account) = account else { + return Ok(RESTResponse::with_text(404, "Account not found")); + }; + let delegated_spo = match &account.delegated_spo { Some(spo) => match spo.to_bech32_with_hrp("pool") { Ok(val) => Some(val), @@ -140,28 +138,26 @@ pub async fn handle_account_registrations_blockfrost( |message| match message { Message::StateQueryResponse(StateQueryResponse::Accounts( AccountsStateQueryResponse::AccountRegistrationHistory(registrations), - )) => Ok(registrations), + )) => Ok(Some(registrations)), Message::StateQueryResponse(StateQueryResponse::Accounts( AccountsStateQueryResponse::NotFound, - )) => { - return Err(anyhow::anyhow!("Account not found")); - } + )) => Ok(None), Message::StateQueryResponse(StateQueryResponse::Accounts( AccountsStateQueryResponse::Error(e), - )) => { - return Err(anyhow::anyhow!( - "Internal server error while retrieving account info: {e}" - )); - } - _ => { - return Err(anyhow::anyhow!( - "Unexpected message type while retrieving account info" - )) - } + )) => Err(anyhow::anyhow!( + "Internal server error while retrieving account info: {e}" + )), + _ => Err(anyhow::anyhow!( + "Unexpected message type while retrieving account info" + )), }, ) .await?; + let Some(registrations) = registrations else { + return Ok(RESTResponse::with_text(404, "Account not found")); + }; + // Get TxHashes from TxIdentifiers let tx_ids: Vec<_> = registrations.iter().map(|r| r.tx_identifier.clone()).collect(); let msg = Arc::new(Message::StateQuery(StateQuery::Blocks( @@ -238,28 +234,26 @@ pub async fn handle_account_delegations_blockfrost( |message| match message { Message::StateQueryResponse(StateQueryResponse::Accounts( AccountsStateQueryResponse::AccountDelegationHistory(delegations), - )) => Ok(delegations), + )) => Ok(Some(delegations)), Message::StateQueryResponse(StateQueryResponse::Accounts( AccountsStateQueryResponse::NotFound, - )) => { - return Err(anyhow::anyhow!("Account not found")); - } + )) => Ok(None), Message::StateQueryResponse(StateQueryResponse::Accounts( AccountsStateQueryResponse::Error(e), - )) => { - return Err(anyhow::anyhow!( - "Internal server error while retrieving account info: {e}" - )); - } - _ => { - return Err(anyhow::anyhow!( - "Unexpected message type while retrieving account info" - )) - } + )) => Err(anyhow::anyhow!( + "Internal server error while retrieving account info: {e}" + )), + _ => Err(anyhow::anyhow!( + "Unexpected message type while retrieving account info" + )), }, ) .await?; + let Some(delegations) = delegations else { + return Ok(RESTResponse::with_text(404, "Account not found")); + }; + // Get TxHashes from TxIdentifiers let tx_ids: Vec<_> = delegations.iter().map(|r| r.tx_identifier.clone()).collect(); let msg = Arc::new(Message::StateQuery(StateQuery::Blocks( @@ -346,28 +340,26 @@ pub async fn handle_account_mirs_blockfrost( |message| match message { Message::StateQueryResponse(StateQueryResponse::Accounts( AccountsStateQueryResponse::AccountMIRHistory(mirs), - )) => Ok(mirs), + )) => Ok(Some(mirs)), Message::StateQueryResponse(StateQueryResponse::Accounts( AccountsStateQueryResponse::NotFound, - )) => { - return Err(anyhow::anyhow!("Account not found")); - } + )) => Ok(None), Message::StateQueryResponse(StateQueryResponse::Accounts( AccountsStateQueryResponse::Error(e), - )) => { - return Err(anyhow::anyhow!( - "Internal server error while retrieving account info: {e}" - )); - } - _ => { - return Err(anyhow::anyhow!( - "Unexpected message type while retrieving account info" - )) - } + )) => Err(anyhow::anyhow!( + "Internal server error while retrieving account info: {e}" + )), + _ => Err(anyhow::anyhow!( + "Unexpected message type while retrieving account info" + )), }, ) .await?; + let Some(mirs) = mirs else { + return Ok(RESTResponse::with_text(404, "Account not found")); + }; + // Get TxHashes from TxIdentifiers let tx_ids: Vec<_> = mirs.iter().map(|r| r.tx_identifier.clone()).collect(); let msg = Arc::new(Message::StateQuery(StateQuery::Blocks(