From 76f2922238f9f063b9ef060b3aeed4a094ca88d0 Mon Sep 17 00:00:00 2001 From: Alex Woods Date: Fri, 24 Oct 2025 15:51:08 +0100 Subject: [PATCH 01/23] Implement vertical slice of txs endpoint - returns a tiny bit of transaction info given a tx hash --- Cargo.lock | 2 +- common/src/queries/transactions.rs | 15 ++++- modules/chain_store/src/chain_store.rs | 62 ++++++++++++++++++- modules/chain_store/src/stores/fjall.rs | 24 ++++++- modules/chain_store/src/stores/mod.rs | 6 ++ modules/rest_blockfrost/src/handlers/mod.rs | 1 + .../src/handlers/transactions.rs | 54 ++++++++++++++++ .../rest_blockfrost/src/handlers_config.rs | 7 +++ .../rest_blockfrost/src/rest_blockfrost.rs | 12 ++++ 9 files changed, 176 insertions(+), 7 deletions(-) create mode 100644 modules/rest_blockfrost/src/handlers/transactions.rs diff --git a/Cargo.lock b/Cargo.lock index ecb0c3de..83a6a4b5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1543,7 +1543,7 @@ dependencies = [ "heck", "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.107", ] [[package]] diff --git a/common/src/queries/transactions.rs b/common/src/queries/transactions.rs index 0bb5b407..a8d68814 100644 --- a/common/src/queries/transactions.rs +++ b/common/src/queries/transactions.rs @@ -1,6 +1,13 @@ +use crate::{BlockHash, TxHash}; + +pub const DEFAULT_TRANSACTIONS_QUERY_TOPIC: (&str, &str) = ( + "transactions-state-query-topic", + "cardano.query.transactions", +); + #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] pub enum TransactionsStateQuery { - GetTransactionInfo, + GetTransactionInfo { tx_hash: TxHash }, GetTransactionUTxOs, GetTransactionStakeCertificates, GetTransactionDelegationCertificates, @@ -35,7 +42,11 @@ pub enum TransactionsStateQueryResponse { } #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] -pub struct TransactionInfo {} +pub struct TransactionInfo { + pub hash: TxHash, + pub block: BlockHash, + pub block_height: u64, +} #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] pub struct TransactionUTxOs {} diff --git a/modules/chain_store/src/chain_store.rs b/modules/chain_store/src/chain_store.rs index d2cfd5e7..7029326d 100644 --- a/modules/chain_store/src/chain_store.rs +++ b/modules/chain_store/src/chain_store.rs @@ -11,10 +11,14 @@ use acropolis_common::{ DEFAULT_BLOCKS_QUERY_TOPIC, }, queries::misc::Order, + queries::transactions::{ + TransactionInfo, TransactionsStateQuery, TransactionsStateQueryResponse, + DEFAULT_TRANSACTIONS_QUERY_TOPIC, + }, state_history::{StateHistory, StateHistoryStore}, BechOrdAddress, BlockHash, GenesisDelegate, HeavyDelegate, TxHash, VRFKey, }; -use anyhow::{bail, Result}; +use anyhow::{anyhow, bail, Result}; use caryatid_sdk::{module, Context, Module}; use config::Config; use std::collections::{BTreeMap, HashMap}; @@ -22,7 +26,7 @@ use std::sync::Arc; use tokio::sync::Mutex; use tracing::error; -use crate::stores::{fjall::FjallStore, Block, Store}; +use crate::stores::{fjall::FjallStore, Block, Store, Tx}; const DEFAULT_BLOCKS_TOPIC: &str = "cardano.block.body"; const DEFAULT_PROTOCOL_PARAMETERS_TOPIC: &str = "cardano.protocol.parameters"; @@ -45,6 +49,9 @@ impl ChainStore { let block_queries_topic = config .get_string(DEFAULT_BLOCKS_QUERY_TOPIC.0) .unwrap_or(DEFAULT_BLOCKS_QUERY_TOPIC.1.to_string()); + let txs_queries_topic = config + .get_string(DEFAULT_TRANSACTIONS_QUERY_TOPIC.0) + .unwrap_or(DEFAULT_TRANSACTIONS_QUERY_TOPIC.1.to_string()); let store_type = config.get_string("store").unwrap_or(DEFAULT_STORE.to_string()); let store: Arc = match store_type.as_str() { @@ -80,6 +87,25 @@ impl ChainStore { } }); + let query_store = store.clone(); + context.handle(&txs_queries_topic, move |req| { + let query_store = query_store.clone(); + async move { + let Message::StateQuery(StateQuery::Transactions(query)) = req.as_ref() else { + return Arc::new(Message::StateQueryResponse( + StateQueryResponse::Transactions(TransactionsStateQueryResponse::Error( + "Invalid message for txs-state".into(), + )), + )); + }; + let res = Self::handle_txs_query(&query_store, &query) + .unwrap_or_else(|err| TransactionsStateQueryResponse::Error(err.to_string())); + Arc::new(Message::StateQueryResponse( + StateQueryResponse::Transactions(res), + )) + } + }); + let mut new_blocks_subscription = context.subscribe(&new_blocks_topic).await?; let mut params_subscription = context.subscribe(¶ms_topic).await?; context.run(async move { @@ -517,6 +543,38 @@ impl ChainStore { Ok(BlockInvolvedAddresses { addresses }) } + fn to_tx_info(tx: Tx) -> Result { + let block = pallas_traverse::MultiEraBlock::decode(&tx.block.bytes)?; + let txs = block.txs(); + let Some(tx) = txs.get(tx.index as usize) else { + return Err(anyhow!("Transaction not found in block for given index")); + }; + Ok(TransactionInfo { + hash: TxHash(*tx.hash()), + block: BlockHash(*block.hash()), + block_height: block.number(), + }) + } + + fn handle_txs_query( + store: &Arc, + query: &TransactionsStateQuery, + ) -> Result { + match query { + TransactionsStateQuery::GetTransactionInfo { tx_hash } => { + let Some(tx) = store.get_tx_by_hash(tx_hash.as_ref())? else { + return Ok(TransactionsStateQueryResponse::NotFound); + }; + Ok(TransactionsStateQueryResponse::TransactionInfo( + Self::to_tx_info(tx)?, + )) + } + _ => Ok(TransactionsStateQueryResponse::Error( + "Unimplemented".to_string(), + )), + } + } + fn handle_new_params(state: &mut State, message: Arc) -> Result<()> { match message.as_ref() { Message::Cardano((_, CardanoMessage::ProtocolParams(params))) => { diff --git a/modules/chain_store/src/stores/fjall.rs b/modules/chain_store/src/stores/fjall.rs index 5dc32e37..b275deb9 100644 --- a/modules/chain_store/src/stores/fjall.rs +++ b/modules/chain_store/src/stores/fjall.rs @@ -1,11 +1,11 @@ use std::{fs, path::Path, sync::Arc}; use acropolis_common::{BlockInfo, TxHash}; -use anyhow::Result; +use anyhow::{anyhow, Result}; use config::Config; use fjall::{Batch, Keyspace, Partition}; -use crate::stores::{Block, ExtraBlockData}; +use crate::stores::{Block, ExtraBlockData, Tx}; pub struct FjallStore { keyspace: Keyspace, @@ -92,6 +92,19 @@ impl super::Store for FjallStore { fn get_latest_block(&self) -> Result> { self.blocks.get_latest() } + + fn get_tx_by_hash(&self, hash: &[u8]) -> Result> { + let Some(block_ref) = self.txs.get_by_hash(hash)? else { + return Ok(None); + }; + let Some(block) = self.blocks.get_by_hash(block_ref.block_hash.as_ref())? else { + return Err(anyhow!("Referenced block not found")); + }; + Ok(Some(Tx { + block, + index: block_ref.index as u64, + })) + } } struct FjallBlockStore { @@ -220,6 +233,13 @@ impl FjallTXStore { let bytes = minicbor::to_vec(block_ref).expect("infallible"); batch.insert(&self.txs, hash.as_ref(), bytes); } + + fn get_by_hash(&self, hash: &[u8]) -> Result> { + let Some(block_ref) = self.txs.get(hash)? else { + return Ok(None); + }; + Ok(minicbor::decode(&block_ref)?) + } } #[derive(minicbor::Decode, minicbor::Encode)] diff --git a/modules/chain_store/src/stores/mod.rs b/modules/chain_store/src/stores/mod.rs index 866c6199..ccec028d 100644 --- a/modules/chain_store/src/stores/mod.rs +++ b/modules/chain_store/src/stores/mod.rs @@ -12,6 +12,7 @@ pub trait Store: Send + Sync { fn get_blocks_by_number_range(&self, min_number: u64, max_number: u64) -> Result>; fn get_block_by_epoch_slot(&self, epoch: u64, epoch_slot: u64) -> Result>; fn get_latest_block(&self) -> Result>; + fn get_tx_by_hash(&self, hash: &[u8]) -> Result>; } #[derive(Debug, PartialEq, Eq, minicbor::Decode, minicbor::Encode)] @@ -32,6 +33,11 @@ pub struct ExtraBlockData { pub timestamp: u64, } +pub struct Tx { + pub block: Block, + pub index: u64, +} + pub(crate) fn extract_tx_hashes(block: &[u8]) -> Result> { let block = pallas_traverse::MultiEraBlock::decode(block).context("could not decode block")?; Ok(block.txs().into_iter().map(|tx| TxHash(*tx.hash())).collect()) diff --git a/modules/rest_blockfrost/src/handlers/mod.rs b/modules/rest_blockfrost/src/handlers/mod.rs index dd53137e..c8765128 100644 --- a/modules/rest_blockfrost/src/handlers/mod.rs +++ b/modules/rest_blockfrost/src/handlers/mod.rs @@ -5,3 +5,4 @@ pub mod blocks; pub mod epochs; pub mod governance; pub mod pools; +pub mod transactions; diff --git a/modules/rest_blockfrost/src/handlers/transactions.rs b/modules/rest_blockfrost/src/handlers/transactions.rs new file mode 100644 index 00000000..c45d201c --- /dev/null +++ b/modules/rest_blockfrost/src/handlers/transactions.rs @@ -0,0 +1,54 @@ +//! REST handlers for Acropolis Blockfrost /txs endpoints +use acropolis_common::{ + messages::{Message, RESTResponse, StateQuery, StateQueryResponse}, + queries::{ + transactions::{TransactionsStateQuery, TransactionsStateQueryResponse}, + utils::rest_query_state, + }, + TxHash, +}; +use anyhow::{anyhow, Result}; +use caryatid_sdk::Context; +use hex::FromHex; +use std::sync::Arc; + +use crate::handlers_config::HandlersConfig; + +/// Handle `/txs/{hash}` +pub async fn handle_transactions_blockfrost( + context: Arc>, + params: Vec, + handlers_config: Arc, +) -> Result { + let param = match params.as_slice() { + [param] => param, + _ => return Ok(RESTResponse::with_text(400, "Invalid parameters")), + }; + + let tx_hash = match TxHash::from_hex(param) { + Ok(hash) => hash, + Err(_) => return Ok(RESTResponse::with_text(400, "Invalid transaction hash")), + }; + + let txs_info_msg = Arc::new(Message::StateQuery(StateQuery::Transactions( + TransactionsStateQuery::GetTransactionInfo { tx_hash }, + ))); + rest_query_state( + &context, + &handlers_config.transactions_query_topic, + txs_info_msg, + |message| match message { + Message::StateQueryResponse(StateQueryResponse::Transactions( + TransactionsStateQueryResponse::TransactionInfo(txs_info), + )) => Some(Ok(Some(txs_info))), + Message::StateQueryResponse(StateQueryResponse::Transactions( + TransactionsStateQueryResponse::NotFound, + )) => Some(Ok(None)), + Message::StateQueryResponse(StateQueryResponse::Transactions( + TransactionsStateQueryResponse::Error(e), + )) => Some(Err(anyhow!(e))), + _ => None, + }, + ) + .await +} diff --git a/modules/rest_blockfrost/src/handlers_config.rs b/modules/rest_blockfrost/src/handlers_config.rs index 029541d1..4444ab6b 100644 --- a/modules/rest_blockfrost/src/handlers_config.rs +++ b/modules/rest_blockfrost/src/handlers_config.rs @@ -10,6 +10,7 @@ use acropolis_common::queries::{ parameters::DEFAULT_PARAMETERS_QUERY_TOPIC, pools::DEFAULT_POOLS_QUERY_TOPIC, spdd::DEFAULT_SPDD_QUERY_TOPIC, + transactions::DEFAULT_TRANSACTIONS_QUERY_TOPIC, utxos::DEFAULT_UTXOS_QUERY_TOPIC, }; use config::Config; @@ -27,6 +28,7 @@ pub struct HandlersConfig { pub governance_query_topic: String, pub epochs_query_topic: String, pub spdd_query_topic: String, + pub transactions_query_topic: String, pub parameters_query_topic: String, pub utxos_query_topic: String, pub external_api_timeout: u64, @@ -71,6 +73,10 @@ impl From> for HandlersConfig { .get_string(DEFAULT_PARAMETERS_QUERY_TOPIC.0) .unwrap_or(DEFAULT_PARAMETERS_QUERY_TOPIC.1.to_string()); + let transactions_query_topic = config + .get_string(DEFAULT_TRANSACTIONS_QUERY_TOPIC.0) + .unwrap_or(DEFAULT_TRANSACTIONS_QUERY_TOPIC.1.to_string()); + let utxos_query_topic = config .get_string(DEFAULT_UTXOS_QUERY_TOPIC.0) .unwrap_or(DEFAULT_UTXOS_QUERY_TOPIC.1.to_string()); @@ -97,6 +103,7 @@ impl From> for HandlersConfig { governance_query_topic, epochs_query_topic, spdd_query_topic, + transactions_query_topic, parameters_query_topic, utxos_query_topic, external_api_timeout, diff --git a/modules/rest_blockfrost/src/rest_blockfrost.rs b/modules/rest_blockfrost/src/rest_blockfrost.rs index ac72e60a..404d81de 100644 --- a/modules/rest_blockfrost/src/rest_blockfrost.rs +++ b/modules/rest_blockfrost/src/rest_blockfrost.rs @@ -56,6 +56,7 @@ use handlers::{ handle_pool_votes_blockfrost, handle_pools_extended_retired_retiring_single_blockfrost, handle_pools_list_blockfrost, }, + transactions::handle_transactions_blockfrost, }; use crate::handlers_config::HandlersConfig; @@ -187,6 +188,9 @@ const DEFAULT_HANDLE_EPOCH_POOL_BLOCKS_TOPIC: (&str, &str) = ( "rest.get.epochs.*.blocks.*", ); +// Transactions topics +const DEFAULT_HANDLE_TRANSACTIONS_TOPIC: (&str, &str) = ("handle-transactions", "rest.get.txs.*"); + // Assets topics const DEFAULT_HANDLE_ASSETS_LIST_TOPIC: (&str, &str) = ("handle-topic-assets-list", "rest.get.assets"); @@ -641,6 +645,14 @@ impl BlockfrostREST { handle_address_transactions_blockfrost, ); + // Handler for /txs/{hash} + register_handler( + context.clone(), + DEFAULT_HANDLE_TRANSACTIONS_TOPIC, + handlers_config.clone(), + handle_transactions_blockfrost, + ); + Ok(()) } } From 943717644eddf56a017dcc355c28b338dc03e9f2 Mon Sep 17 00:00:00 2001 From: Alex Woods Date: Wed, 29 Oct 2025 18:05:04 +0000 Subject: [PATCH 02/23] Start fleshing out transaction info --- common/src/queries/transactions.rs | 60 +++++++++++++++++++++++-- modules/chain_store/src/chain_store.rs | 61 +++++++++++++++++++++++--- 2 files changed, 111 insertions(+), 10 deletions(-) diff --git a/common/src/queries/transactions.rs b/common/src/queries/transactions.rs index a8d68814..646d7e21 100644 --- a/common/src/queries/transactions.rs +++ b/common/src/queries/transactions.rs @@ -1,4 +1,6 @@ -use crate::{BlockHash, TxHash}; +use crate::{BlockHash, Lovelace, NativeAsset, TxHash}; +use serde::ser::{Serialize, SerializeStruct, Serializer}; +use serde_with::{DisplayFromStr, serde_as}; pub const DEFAULT_TRANSACTIONS_QUERY_TOPIC: (&str, &str) = ( "transactions-state-query-topic", @@ -41,11 +43,63 @@ pub enum TransactionsStateQueryResponse { Error(String), } +#[derive(Debug, Clone, serde::Deserialize)] +pub enum TransactionOutputAmount { + Lovelace(Lovelace), + Asset(NativeAsset), +} + +impl Serialize for TransactionOutputAmount { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + let mut state = serializer.serialize_struct("TransactionOutputAmount", 2)?; + match self { + TransactionOutputAmount::Lovelace(lovelace) => { + state.serialize_field("unit", "lovelace")?; + state.serialize_field("amount", &lovelace.to_string())?; + }, + TransactionOutputAmount::Asset(asset) => { + state.serialize_field("unit", &asset.name)?; + state.serialize_field("amount", &asset.amount.to_string())?; + }, + } + state.end() + } +} + +#[serde_as] #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] pub struct TransactionInfo { pub hash: TxHash, - pub block: BlockHash, - pub block_height: u64, + #[serde(rename = "block")] + pub block_hash: BlockHash, + #[serde(rename = "height")] + pub block_number: u64, + #[serde(rename = "time")] + pub block_time: u64, + pub slot: u64, + pub index: u64, + #[serde(rename = "order_amount")] + pub output_amounts: Vec, + #[serde(rename = "fees")] + #[serde_as(as = "DisplayFromStr")] + pub fee: u64, + pub deposit: u64, + pub size: u64, + pub invalid_before: Option, + pub invalid_after: Option, + pub utxo_count: u64, + pub withdrawal_count: u64, + pub mir_cert_count: u64, + pub delegation_count: u64, + pub stake_cert_count: u64, + pub pool_update_count: u64, + pub pool_retire_count: u64, + pub asset_mint_or_burn_count: u64, + pub redeemer_count: u64, + pub valid_contract: bool, } #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] diff --git a/modules/chain_store/src/chain_store.rs b/modules/chain_store/src/chain_store.rs index d6131eb0..b5b1f7be 100644 --- a/modules/chain_store/src/chain_store.rs +++ b/modules/chain_store/src/chain_store.rs @@ -12,11 +12,12 @@ use acropolis_common::{ }, queries::misc::Order, queries::transactions::{ - TransactionInfo, TransactionsStateQuery, TransactionsStateQueryResponse, - DEFAULT_TRANSACTIONS_QUERY_TOPIC, + TransactionInfo, TransactionOutputAmount, TransactionsStateQuery, + TransactionsStateQueryResponse, DEFAULT_TRANSACTIONS_QUERY_TOPIC, }, state_history::{StateHistory, StateHistoryStore}, - BechOrdAddress, BlockHash, GenesisDelegate, HeavyDelegate, TxHash, VRFKey, + AssetName, BechOrdAddress, BlockHash, GenesisDelegate, HeavyDelegate, NativeAsset, TxHash, + VRFKey, }; use anyhow::{anyhow, bail, Result}; use caryatid_sdk::{module, Context, Module}; @@ -539,13 +540,59 @@ impl ChainStore { fn to_tx_info(tx: Tx) -> Result { let block = pallas_traverse::MultiEraBlock::decode(&tx.block.bytes)?; let txs = block.txs(); - let Some(tx) = txs.get(tx.index as usize) else { + let Some(tx_decoded) = txs.get(tx.index as usize) else { return Err(anyhow!("Transaction not found in block for given index")); }; + let mut output_amounts = Vec::new(); + for output in tx_decoded.outputs() { + let value = output.value(); + let lovelace_amount = value.coin(); + if lovelace_amount != 0 { + output_amounts.push(TransactionOutputAmount::Lovelace(lovelace_amount)); + } + for policy in value.assets() { + for asset in policy.assets() { + if asset.is_output() { + output_amounts.push(TransactionOutputAmount::Asset(NativeAsset { + name: AssetName::new(asset.name()).ok_or(anyhow!("Bad asset name"))?, + amount: asset.output_coin().ok_or(anyhow!("No output amount"))?, + })); + } + } + } + } Ok(TransactionInfo { - hash: TxHash(*tx.hash()), - block: BlockHash(*block.hash()), - block_height: block.number(), + hash: TxHash(*tx_decoded.hash()), + block_hash: BlockHash(*block.hash()), + block_number: block.number(), + block_time: tx.block.extra.timestamp, + slot: block.slot(), + index: tx.index, + output_amounts, + fee: tx_decoded.fee().unwrap_or(0), + // TODO + deposit: 0, + size: tx_decoded.size() as u64, + invalid_before: tx_decoded.validity_start(), + // TODO + invalid_after: None, + utxo_count: tx_decoded.requires().len() as u64, + withdrawal_count: tx_decoded.withdrawals_sorted_set().len() as u64, + // TODO + mir_cert_count: 0, + // TODO + delegation_count: 0, + // TODO + stake_cert_count: 0, + // TODO + pool_update_count: 0, + // TODO + pool_retire_count: 0, + // TODO + asset_mint_or_burn_count: 0, + // TODO + redeemer_count: 0, + valid_contract: tx_decoded.is_valid(), }) } From b747f563cb7d529e07984a971e38a2fc37506eb3 Mon Sep 17 00:00:00 2001 From: Alex Woods Date: Fri, 31 Oct 2025 17:43:25 +0000 Subject: [PATCH 03/23] Fill in more of transaction info --- common/src/queries/transactions.rs | 6 +-- modules/chain_store/Cargo.toml | 1 + modules/chain_store/src/chain_store.rs | 55 +++++++++++++++++++------- 3 files changed, 44 insertions(+), 18 deletions(-) diff --git a/common/src/queries/transactions.rs b/common/src/queries/transactions.rs index 646d7e21..39f04e71 100644 --- a/common/src/queries/transactions.rs +++ b/common/src/queries/transactions.rs @@ -1,6 +1,6 @@ use crate::{BlockHash, Lovelace, NativeAsset, TxHash}; use serde::ser::{Serialize, SerializeStruct, Serializer}; -use serde_with::{DisplayFromStr, serde_as}; +use serde_with::{serde_as, DisplayFromStr}; pub const DEFAULT_TRANSACTIONS_QUERY_TOPIC: (&str, &str) = ( "transactions-state-query-topic", @@ -59,11 +59,11 @@ impl Serialize for TransactionOutputAmount { TransactionOutputAmount::Lovelace(lovelace) => { state.serialize_field("unit", "lovelace")?; state.serialize_field("amount", &lovelace.to_string())?; - }, + } TransactionOutputAmount::Asset(asset) => { state.serialize_field("unit", &asset.name)?; state.serialize_field("amount", &asset.amount.to_string())?; - }, + } } state.end() } diff --git a/modules/chain_store/Cargo.toml b/modules/chain_store/Cargo.toml index d4af79d6..f093754c 100644 --- a/modules/chain_store/Cargo.toml +++ b/modules/chain_store/Cargo.toml @@ -19,6 +19,7 @@ pallas-traverse = { workspace = true } tracing = "0.1.40" tokio.workspace = true imbl.workspace = true +pallas.workspace = true [dev-dependencies] tempfile = "3" diff --git a/modules/chain_store/src/chain_store.rs b/modules/chain_store/src/chain_store.rs index b5b1f7be..537b8584 100644 --- a/modules/chain_store/src/chain_store.rs +++ b/modules/chain_store/src/chain_store.rs @@ -22,6 +22,8 @@ use acropolis_common::{ use anyhow::{anyhow, bail, Result}; use caryatid_sdk::{module, Context, Module}; use config::Config; +use pallas::ledger::primitives::{alonzo, conway}; +use pallas_traverse::MultiEraCert; use std::collections::{BTreeMap, HashMap}; use std::sync::Arc; use tokio::sync::Mutex; @@ -561,6 +563,29 @@ impl ChainStore { } } } + let mut mir_cert_count = 0; + let mut delegation_count = 0; + let mut stake_cert_count = 0; + let mut pool_update_count = 0; + let mut pool_retire_count = 0; + for cert in tx_decoded.certs() { + match cert { + MultiEraCert::AlonzoCompatible(cert) => match cert.as_ref().as_ref() { + alonzo::Certificate::PoolRegistration { .. } => pool_update_count += 1, + alonzo::Certificate::PoolRetirement { .. } => pool_retire_count += 1, + alonzo::Certificate::MoveInstantaneousRewardsCert { .. } => mir_cert_count += 1, + _ => (), + }, + MultiEraCert::Conway(cert) => match cert.as_ref().as_ref() { + conway::Certificate::PoolRegistration { .. } => pool_update_count += 1, + conway::Certificate::PoolRetirement { .. } => pool_retire_count += 1, + conway::Certificate::StakeRegistration { .. } => stake_cert_count += 1, + conway::Certificate::StakeDelegation { .. } => delegation_count += 1, + _ => (), + }, + _ => (), + } + } Ok(TransactionInfo { hash: TxHash(*tx_decoded.hash()), block_hash: BlockHash(*block.hash()), @@ -569,29 +594,29 @@ impl ChainStore { slot: block.slot(), index: tx.index, output_amounts, + // TODO: None for byron - needs to look up input utxo values in other txs and subtract + // outputs value? fee: tx_decoded.fee().unwrap_or(0), // TODO deposit: 0, + // TODO reporting too many bytes (140) size: tx_decoded.size() as u64, invalid_before: tx_decoded.validity_start(), // TODO invalid_after: None, - utxo_count: tx_decoded.requires().len() as u64, + utxo_count: (tx_decoded.requires().len() + tx_decoded.produces().len()) as u64, withdrawal_count: tx_decoded.withdrawals_sorted_set().len() as u64, - // TODO - mir_cert_count: 0, - // TODO - delegation_count: 0, - // TODO - stake_cert_count: 0, - // TODO - pool_update_count: 0, - // TODO - pool_retire_count: 0, - // TODO - asset_mint_or_burn_count: 0, - // TODO - redeemer_count: 0, + mir_cert_count, + delegation_count, + stake_cert_count, + pool_update_count, + pool_retire_count, + asset_mint_or_burn_count: tx_decoded + .mints() + .iter() + .map(|p| p.assets().len()) + .sum::() as u64, + redeemer_count: tx_decoded.redeemers().len() as u64, valid_contract: tx_decoded.is_valid(), }) } From 47dd9a4495366d19e33341b1651c8a89f9f218ee Mon Sep 17 00:00:00 2001 From: Alex Woods Date: Wed, 5 Nov 2025 20:44:26 +0000 Subject: [PATCH 04/23] Add deposit to transaction info --- Cargo.lock | 10 ++ Cargo.toml | 3 +- cardano/Cargo.toml | 8 ++ cardano/src/lib.rs | 1 + cardano/src/transaction.rs | 31 ++++++ common/src/queries/transactions.rs | 12 +-- common/src/queries/utils.rs | 61 +++++++++++- modules/chain_store/src/chain_store.rs | 19 ++-- modules/rest_blockfrost/Cargo.toml | 1 + .../src/handlers/transactions.rs | 94 +++++++++++++++++-- 10 files changed, 212 insertions(+), 28 deletions(-) create mode 100644 cardano/Cargo.toml create mode 100644 cardano/src/lib.rs create mode 100644 cardano/src/transaction.rs diff --git a/Cargo.lock b/Cargo.lock index d237f4ca..e8f163b8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,6 +2,14 @@ # It is not intended for manual editing. version = 4 +[[package]] +name = "acropolis_cardano" +version = "0.1.0" +dependencies = [ + "acropolis_common", + "anyhow", +] + [[package]] name = "acropolis_codec" version = "0.1.0" @@ -139,6 +147,7 @@ dependencies = [ "hex", "imbl", "minicbor 0.26.5", + "pallas 0.33.0", "pallas-traverse 0.33.0", "tempfile", "tokio", @@ -306,6 +315,7 @@ dependencies = [ name = "acropolis_module_rest_blockfrost" version = "0.1.0" dependencies = [ + "acropolis_cardano", "acropolis_common", "anyhow", "async-trait", diff --git a/Cargo.toml b/Cargo.toml index 035c1edb..81d57d11 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3,6 +3,7 @@ [workspace] members = [ # Global message and common definitions + "cardano", "codec", "common", @@ -49,8 +50,6 @@ dashmap = "6.1.0" hex = "0.4" imbl = { version = "5.0.0", features = ["serde"] } pallas = "0.33.0" -pallas-addresses = "0.33.0" -pallas-crypto = "0.33.0" pallas-primitives = "0.33.0" pallas-traverse = "0.33.0" serde = { version = "1.0.214", features = ["derive"] } diff --git a/cardano/Cargo.toml b/cardano/Cargo.toml new file mode 100644 index 00000000..5f656dbf --- /dev/null +++ b/cardano/Cargo.toml @@ -0,0 +1,8 @@ +[package] +name = "acropolis_cardano" +version = "0.1.0" +edition = "2024" + +[dependencies] +acropolis_common = { version = "0.3.0", path = "../common" } +anyhow.workspace = true diff --git a/cardano/src/lib.rs b/cardano/src/lib.rs new file mode 100644 index 00000000..37f08066 --- /dev/null +++ b/cardano/src/lib.rs @@ -0,0 +1 @@ +pub mod transaction; diff --git a/cardano/src/transaction.rs b/cardano/src/transaction.rs new file mode 100644 index 00000000..e2987f8c --- /dev/null +++ b/cardano/src/transaction.rs @@ -0,0 +1,31 @@ +use acropolis_common::{Lovelace, protocol_params::ProtocolParams}; +use anyhow::{Error, anyhow}; + +pub fn calculate_transaction_fee( + recorded_fee: &Option, + inputs: &Vec, + outputs: &Vec, +) -> Lovelace { + match recorded_fee { + Some(fee) => *fee, + None => inputs.iter().sum::() - outputs.iter().sum::(), + } +} + +pub fn calculate_deposit( + pool_update_count: u64, + stake_cert_count: u64, + params: &ProtocolParams, +) -> Result { + match ¶ms.shelley { + Some(shelley) => Ok(stake_cert_count * shelley.protocol_params.key_deposit + + pool_update_count * shelley.protocol_params.pool_deposit), + None => { + if pool_update_count > 0 || stake_cert_count > 0 { + Err(anyhow!("No Shelley params, but deposits present")) + } else { + Ok(0) + } + } + } +} diff --git a/common/src/queries/transactions.rs b/common/src/queries/transactions.rs index 39f04e71..83abbf34 100644 --- a/common/src/queries/transactions.rs +++ b/common/src/queries/transactions.rs @@ -1,6 +1,6 @@ use crate::{BlockHash, Lovelace, NativeAsset, TxHash}; use serde::ser::{Serialize, SerializeStruct, Serializer}; -use serde_with::{serde_as, DisplayFromStr}; +use serde_with::serde_as; pub const DEFAULT_TRANSACTIONS_QUERY_TOPIC: (&str, &str) = ( "transactions-state-query-topic", @@ -73,20 +73,14 @@ impl Serialize for TransactionOutputAmount { #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] pub struct TransactionInfo { pub hash: TxHash, - #[serde(rename = "block")] pub block_hash: BlockHash, - #[serde(rename = "height")] pub block_number: u64, - #[serde(rename = "time")] pub block_time: u64, + pub epoch: u64, pub slot: u64, pub index: u64, - #[serde(rename = "order_amount")] pub output_amounts: Vec, - #[serde(rename = "fees")] - #[serde_as(as = "DisplayFromStr")] - pub fee: u64, - pub deposit: u64, + pub recorded_fee: Option, pub size: u64, pub invalid_before: Option, pub invalid_after: Option, diff --git a/common/src/queries/utils.rs b/common/src/queries/utils.rs index 20ce4f4e..975f119f 100644 --- a/common/src/queries/utils.rs +++ b/common/src/queries/utils.rs @@ -1,7 +1,7 @@ use anyhow::Result; use caryatid_sdk::Context; use serde::Serialize; -use std::sync::Arc; +use std::{future::Future, sync::Arc}; use crate::messages::{Message, RESTResponse}; @@ -22,6 +22,24 @@ where extractor(message) } +pub async fn query_state_async( + context: &Arc>, + topic: &str, + request_msg: Arc, + extractor: F, +) -> Result +where + F: FnOnce(Message) -> Fut, + Fut: Future>, +{ + // 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).await +} + /// The outer option in the extractor return value is whether the response was handled by F pub async fn rest_query_state( context: &Arc>, @@ -59,3 +77,44 @@ where )), } } + +pub async fn rest_query_state_async( + context: &Arc>, + topic: &str, + request_msg: Arc, + extractor: F, +) -> Result +where + F: FnOnce(Message) -> Fut, + Fut: Future, anyhow::Error>>>, + T: Serialize, +{ + let result = query_state_async( + context, + topic, + request_msg, + async |response| match extractor(response).await { + Some(response) => response, + None => Err(anyhow::anyhow!( + "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}"), + )), + } +} diff --git a/modules/chain_store/src/chain_store.rs b/modules/chain_store/src/chain_store.rs index 94bc91a4..60e14711 100644 --- a/modules/chain_store/src/chain_store.rs +++ b/modules/chain_store/src/chain_store.rs @@ -571,15 +571,21 @@ impl ChainStore { for cert in tx_decoded.certs() { match cert { MultiEraCert::AlonzoCompatible(cert) => match cert.as_ref().as_ref() { - alonzo::Certificate::PoolRegistration { .. } => pool_update_count += 1, + alonzo::Certificate::PoolRegistration { .. } => { + pool_update_count += 1; + } alonzo::Certificate::PoolRetirement { .. } => pool_retire_count += 1, alonzo::Certificate::MoveInstantaneousRewardsCert { .. } => mir_cert_count += 1, _ => (), }, MultiEraCert::Conway(cert) => match cert.as_ref().as_ref() { - conway::Certificate::PoolRegistration { .. } => pool_update_count += 1, + conway::Certificate::PoolRegistration { .. } => { + pool_update_count += 1; + } conway::Certificate::PoolRetirement { .. } => pool_retire_count += 1, - conway::Certificate::StakeRegistration { .. } => stake_cert_count += 1, + conway::Certificate::StakeRegistration { .. } => { + stake_cert_count += 1; + } conway::Certificate::StakeDelegation { .. } => delegation_count += 1, _ => (), }, @@ -591,14 +597,11 @@ impl ChainStore { block_hash: BlockHash::from(*block.hash()), block_number: block.number(), block_time: tx.block.extra.timestamp, + epoch: tx.block.extra.epoch, slot: block.slot(), index: tx.index, output_amounts, - // TODO: None for byron - needs to look up input utxo values in other txs and subtract - // outputs value? - fee: tx_decoded.fee().unwrap_or(0), - // TODO - deposit: 0, + recorded_fee: tx_decoded.fee(), // TODO reporting too many bytes (140) size: tx_decoded.size() as u64, invalid_before: tx_decoded.validity_start(), diff --git a/modules/rest_blockfrost/Cargo.toml b/modules/rest_blockfrost/Cargo.toml index a5982ef6..3e69c052 100644 --- a/modules/rest_blockfrost/Cargo.toml +++ b/modules/rest_blockfrost/Cargo.toml @@ -29,6 +29,7 @@ serde_json = { workspace = true } serde_with = { workspace = true } tokio = { workspace = true } tracing = { workspace = true } +acropolis_cardano = { version = "0.1.0", path = "../../cardano" } [lib] path = "src/rest_blockfrost.rs" diff --git a/modules/rest_blockfrost/src/handlers/transactions.rs b/modules/rest_blockfrost/src/handlers/transactions.rs index c45d201c..7b2bb5c6 100644 --- a/modules/rest_blockfrost/src/handlers/transactions.rs +++ b/modules/rest_blockfrost/src/handlers/transactions.rs @@ -1,19 +1,56 @@ //! REST handlers for Acropolis Blockfrost /txs endpoints +use acropolis_cardano::transaction::calculate_deposit; use acropolis_common::{ messages::{Message, RESTResponse, StateQuery, StateQueryResponse}, queries::{ - transactions::{TransactionsStateQuery, TransactionsStateQueryResponse}, - utils::rest_query_state, + parameters::{ParametersStateQuery, ParametersStateQueryResponse}, + transactions::{TransactionInfo, TransactionsStateQuery, TransactionsStateQueryResponse}, + utils::{query_state, rest_query_state_async}, }, - TxHash, + Lovelace, TxHash, }; use anyhow::{anyhow, Result}; use caryatid_sdk::Context; use hex::FromHex; +use serde::{ser::SerializeStruct, Serialize, Serializer}; use std::sync::Arc; use crate::handlers_config::HandlersConfig; +struct TxInfo(TransactionInfo, Lovelace, Lovelace); + +impl Serialize for TxInfo { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + let mut state = serializer.serialize_struct("TxInfo", 22)?; + state.serialize_field("hash", &self.0.hash)?; + state.serialize_field("block", &self.0.block_hash)?; + state.serialize_field("height", &self.0.block_number)?; + state.serialize_field("time", &self.0.block_time)?; + state.serialize_field("slot", &self.0.slot)?; + state.serialize_field("index", &self.0.index)?; + state.serialize_field("output_amount", &self.0.output_amounts)?; + state.serialize_field("fees", &self.1.to_string())?; + state.serialize_field("deposit", &self.2.to_string())?; + state.serialize_field("size", &self.0.size)?; + state.serialize_field("invalid_before", &self.0.invalid_before)?; + state.serialize_field("invalid_after", &self.0.invalid_after)?; + state.serialize_field("utxo_count", &self.0.utxo_count)?; + state.serialize_field("withdrawal_count", &self.0.withdrawal_count)?; + state.serialize_field("mir_cert_count", &self.0.mir_cert_count)?; + state.serialize_field("delegation_count", &self.0.delegation_count)?; + state.serialize_field("stake_cert_count", &self.0.stake_cert_count)?; + state.serialize_field("pool_update_count", &self.0.pool_update_count)?; + state.serialize_field("pool_retire_count", &self.0.pool_retire_count)?; + state.serialize_field("asset_mint_or_burn_count", &self.0.asset_mint_or_burn_count)?; + state.serialize_field("redeemer_count", &self.0.redeemer_count)?; + state.serialize_field("valid_contract", &self.0.valid_contract)?; + state.end() + } +} + /// Handle `/txs/{hash}` pub async fn handle_transactions_blockfrost( context: Arc>, @@ -33,14 +70,55 @@ pub async fn handle_transactions_blockfrost( let txs_info_msg = Arc::new(Message::StateQuery(StateQuery::Transactions( TransactionsStateQuery::GetTransactionInfo { tx_hash }, ))); - rest_query_state( - &context, - &handlers_config.transactions_query_topic, + rest_query_state_async( + &context.clone(), + &handlers_config.transactions_query_topic.clone(), txs_info_msg, - |message| match message { + async move |message| match message { Message::StateQueryResponse(StateQueryResponse::Transactions( TransactionsStateQueryResponse::TransactionInfo(txs_info), - )) => Some(Ok(Some(txs_info))), + )) => { + let params_msg = Arc::new(Message::StateQuery(StateQuery::Parameters( + ParametersStateQuery::GetEpochParameters { + epoch_number: txs_info.epoch, + }, + ))); + let params = match query_state( + &context, + &handlers_config.parameters_query_topic, + params_msg, + |message| match message { + Message::StateQueryResponse(StateQueryResponse::Parameters( + ParametersStateQueryResponse::EpochParameters(params), + )) => Ok(params), + Message::StateQueryResponse(StateQueryResponse::Parameters( + ParametersStateQueryResponse::NotFound, + )) => Err(anyhow!("Could not query parameters")), + Message::StateQueryResponse(StateQueryResponse::Parameters( + ParametersStateQueryResponse::Error(e), + )) => Err(anyhow!(e)), + _ => Err(anyhow!("Unexpected response")), + }, + ) + .await + { + Ok(params) => params, + Err(e) => return Some(Err(e)), + }; + let fee = match txs_info.recorded_fee { + Some(fee) => fee, + None => 0, // TODO: calc from outputs and inputs + }; + let deposit = match calculate_deposit( + txs_info.pool_update_count, + txs_info.stake_cert_count, + ¶ms, + ) { + Ok(deposit) => deposit, + Err(e) => return Some(Err(e)), + }; + Some(Ok(Some(TxInfo(txs_info, fee, deposit)))) + } Message::StateQueryResponse(StateQueryResponse::Transactions( TransactionsStateQueryResponse::NotFound, )) => Some(Ok(None)), From 22c319366e305cb65bacd4fc94ee5dc424e8568b Mon Sep 17 00:00:00 2001 From: Alex Woods Date: Fri, 7 Nov 2025 10:18:12 +0000 Subject: [PATCH 05/23] Stub out handler switching for all txs endpoints --- .../src/handlers/transactions.rs | 35 +++++++++++++++++-- 1 file changed, 32 insertions(+), 3 deletions(-) diff --git a/modules/rest_blockfrost/src/handlers/transactions.rs b/modules/rest_blockfrost/src/handlers/transactions.rs index 7b2bb5c6..fd15ccd1 100644 --- a/modules/rest_blockfrost/src/handlers/transactions.rs +++ b/modules/rest_blockfrost/src/handlers/transactions.rs @@ -57,16 +57,45 @@ pub async fn handle_transactions_blockfrost( params: Vec, handlers_config: Arc, ) -> Result { - let param = match params.as_slice() { - [param] => param, + let (tx_hash, param, param2) = match params.as_slice() { + [tx_hash] => (tx_hash, None, None), + [tx_hash, param] => (tx_hash, Some(param.as_str()), None), + [tx_hash, param, param2] => (tx_hash, Some(param.as_str()), Some(param2.as_str())), _ => return Ok(RESTResponse::with_text(400, "Invalid parameters")), }; - let tx_hash = match TxHash::from_hex(param) { + let tx_hash = match TxHash::from_hex(tx_hash) { Ok(hash) => hash, Err(_) => return Ok(RESTResponse::with_text(400, "Invalid transaction hash")), }; + match param { + None => handle_transaction_query(context, tx_hash, handlers_config).await, + Some("utxo") => Ok(RESTResponse::with_text(501, "Not implemented")), + Some("stakes") => Ok(RESTResponse::with_text(501, "Not implemented")), + Some("delegations") => Ok(RESTResponse::with_text(501, "Not implemented")), + Some("withdrawals") => Ok(RESTResponse::with_text(501, "Not implemented")), + Some("mirs") => Ok(RESTResponse::with_text(501, "Not implemented")), + Some("pool_updates") => Ok(RESTResponse::with_text(501, "Not implemented")), + Some("pool_retires") => Ok(RESTResponse::with_text(501, "Not implemented")), + Some("metadata") => match param2 { + None => Ok(RESTResponse::with_text(501, "Not implemented")), + Some("cbor") => Ok(RESTResponse::with_text(501, "Not implemented")), + _ => Ok(RESTResponse::with_text(400, "Invalid parameters")), + }, + Some("redeemers") => Ok(RESTResponse::with_text(501, "Not implemented")), + Some("required_signers") => Ok(RESTResponse::with_text(501, "Not implemented")), + Some("cbor") => Ok(RESTResponse::with_text(501, "Not implemented")), + _ => Ok(RESTResponse::with_text(400, "Invalid parameters")), + } +} + +/// Handle `/txs/{hash}` +async fn handle_transaction_query( + context: Arc>, + tx_hash: TxHash, + handlers_config: Arc, +) -> Result { let txs_info_msg = Arc::new(Message::StateQuery(StateQuery::Transactions( TransactionsStateQuery::GetTransactionInfo { tx_hash }, ))); From c6850cfa8aa427a5cfa80b8431a1a968068ce240 Mon Sep 17 00:00:00 2001 From: Alex Woods Date: Fri, 7 Nov 2025 14:37:19 +0000 Subject: [PATCH 06/23] Implement txs stakes endpoint --- cardano/src/transaction.rs | 4 +- common/src/queries/transactions.rs | 15 ++- modules/chain_store/src/chain_store.rs | 99 ++++++++++++++++--- .../src/handlers/transactions.rs | 58 ++++++++++- .../rest_blockfrost/src/rest_blockfrost.rs | 22 +++++ 5 files changed, 178 insertions(+), 20 deletions(-) diff --git a/cardano/src/transaction.rs b/cardano/src/transaction.rs index e2987f8c..f1747369 100644 --- a/cardano/src/transaction.rs +++ b/cardano/src/transaction.rs @@ -3,8 +3,8 @@ use anyhow::{Error, anyhow}; pub fn calculate_transaction_fee( recorded_fee: &Option, - inputs: &Vec, - outputs: &Vec, + inputs: &[Lovelace], + outputs: &[Lovelace], ) -> Lovelace { match recorded_fee { Some(fee) => *fee, diff --git a/common/src/queries/transactions.rs b/common/src/queries/transactions.rs index 78aec7e7..87e38d86 100644 --- a/common/src/queries/transactions.rs +++ b/common/src/queries/transactions.rs @@ -1,4 +1,4 @@ -use crate::{BlockHash, Lovelace, NativeAsset, TxHash}; +use crate::{BlockHash, Lovelace, NativeAsset, StakeAddress, TxHash}; use serde::ser::{Serialize, SerializeStruct, Serializer}; use serde_with::serde_as; @@ -12,7 +12,7 @@ use crate::queries::errors::QueryError; pub enum TransactionsStateQuery { GetTransactionInfo { tx_hash: TxHash }, GetTransactionUTxOs, - GetTransactionStakeCertificates, + GetTransactionStakeCertificates { tx_hash: TxHash }, GetTransactionDelegationCertificates, GetTransactionWithdrawals, GetTransactionMIRs, @@ -100,7 +100,16 @@ pub struct TransactionInfo { pub struct TransactionUTxOs {} #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] -pub struct TransactionStakeCertificates {} +pub struct TransactionStakeCertificate { + pub index: u64, + pub address: StakeAddress, + pub registration: bool, +} + +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] +pub struct TransactionStakeCertificates { + pub certificates: Vec, +} #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] pub struct TransactionDelegationCertificates {} diff --git a/modules/chain_store/src/chain_store.rs b/modules/chain_store/src/chain_store.rs index 501e53d8..07b047ef 100644 --- a/modules/chain_store/src/chain_store.rs +++ b/modules/chain_store/src/chain_store.rs @@ -1,6 +1,8 @@ mod stores; -use acropolis_codec::{block::map_to_block_issuer, map_parameters}; +use acropolis_codec::{ + block::map_to_block_issuer, map_parameters, map_parameters::map_stake_address, +}; use acropolis_common::queries::errors::QueryError; use acropolis_common::{ crypto::keyhash_224, @@ -13,12 +15,13 @@ use acropolis_common::{ }, queries::misc::Order, queries::transactions::{ - TransactionInfo, TransactionOutputAmount, TransactionsStateQuery, - TransactionsStateQueryResponse, DEFAULT_TRANSACTIONS_QUERY_TOPIC, + TransactionInfo, TransactionOutputAmount, TransactionStakeCertificate, + TransactionStakeCertificates, TransactionsStateQuery, TransactionsStateQueryResponse, + DEFAULT_TRANSACTIONS_QUERY_TOPIC, }, state_history::{StateHistory, StateHistoryStore}, - AssetName, BechOrdAddress, BlockHash, GenesisDelegate, HeavyDelegate, NativeAsset, PoolId, - TxHash, + AssetName, BechOrdAddress, BlockHash, GenesisDelegate, HeavyDelegate, NativeAsset, NetworkId, + PoolId, TxHash, }; use anyhow::{anyhow, bail, Result}; use caryatid_sdk::{module, Context, Module}; @@ -56,6 +59,8 @@ impl ChainStore { let txs_queries_topic = config .get_string(DEFAULT_TRANSACTIONS_QUERY_TOPIC.0) .unwrap_or(DEFAULT_TRANSACTIONS_QUERY_TOPIC.1.to_string()); + let network_id: NetworkId = + config.get_string("network-id").unwrap_or("mainnet".to_string()).into(); let store_type = config.get_string("store").unwrap_or(DEFAULT_STORE.to_string()); let store: Arc = match store_type.as_str() { @@ -100,6 +105,7 @@ impl ChainStore { let query_store = store.clone(); context.handle(&txs_queries_topic, move |req| { let query_store = query_store.clone(); + let network_id = network_id.clone(); async move { let Message::StateQuery(StateQuery::Transactions(query)) = req.as_ref() else { return Arc::new(Message::StateQueryResponse( @@ -108,11 +114,12 @@ impl ChainStore { )), )); }; - let res = Self::handle_txs_query(&query_store, &query).unwrap_or_else(|err| { - TransactionsStateQueryResponse::Error(QueryError::internal_error( - err.to_string(), - )) - }); + let res = + Self::handle_txs_query(&query_store, query, network_id).unwrap_or_else(|err| { + TransactionsStateQueryResponse::Error(QueryError::internal_error( + err.to_string(), + )) + }); Arc::new(Message::StateQueryResponse( StateQueryResponse::Transactions(res), )) @@ -571,7 +578,7 @@ impl ChainStore { Ok(BlockInvolvedAddresses { addresses }) } - fn to_tx_info(tx: Tx) -> Result { + fn to_tx_info(tx: &Tx) -> Result { let block = pallas_traverse::MultiEraBlock::decode(&tx.block.bytes)?; let txs = block.txs(); let Some(tx_decoded) = txs.get(tx.index as usize) else { @@ -608,6 +615,10 @@ impl ChainStore { } alonzo::Certificate::PoolRetirement { .. } => pool_retire_count += 1, alonzo::Certificate::MoveInstantaneousRewardsCert { .. } => mir_cert_count += 1, + alonzo::Certificate::StakeRegistration { .. } => { + stake_cert_count += 1; + } + alonzo::Certificate::StakeDelegation { .. } => delegation_count += 1, _ => (), }, MultiEraCert::Conway(cert) => match cert.as_ref().as_ref() { @@ -656,9 +667,59 @@ impl ChainStore { }) } + fn to_tx_stakes(tx: &Tx, network_id: NetworkId) -> Result> { + let block = pallas_traverse::MultiEraBlock::decode(&tx.block.bytes)?; + let txs = block.txs(); + let Some(tx_decoded) = txs.get(tx.index as usize) else { + return Err(anyhow!("Transaction not found in block for given index")); + }; + let mut certs = Vec::new(); + for (index, cert) in tx_decoded.certs().iter().enumerate() { + match cert { + MultiEraCert::AlonzoCompatible(cert) => match cert.as_ref().as_ref() { + alonzo::Certificate::StakeRegistration(cred) => { + certs.push(TransactionStakeCertificate { + index: index as u64, + address: map_stake_address(cred, network_id.clone()), + registration: true, + }); + } + alonzo::Certificate::StakeDeregistration(cred) => { + certs.push(TransactionStakeCertificate { + index: index as u64, + address: map_stake_address(cred, network_id.clone()), + registration: false, + }); + } + _ => (), + }, + MultiEraCert::Conway(cert) => match cert.as_ref().as_ref() { + conway::Certificate::StakeRegistration(cred) => { + certs.push(TransactionStakeCertificate { + index: index as u64, + address: map_stake_address(cred, network_id.clone()), + registration: true, + }); + } + conway::Certificate::StakeDeregistration(cred) => { + certs.push(TransactionStakeCertificate { + index: index as u64, + address: map_stake_address(cred, network_id.clone()), + registration: false, + }); + } + _ => (), + }, + _ => (), + } + } + Ok(certs) + } + fn handle_txs_query( store: &Arc, query: &TransactionsStateQuery, + network_id: NetworkId, ) -> Result { match query { TransactionsStateQuery::GetTransactionInfo { tx_hash } => { @@ -668,9 +729,23 @@ impl ChainStore { )); }; Ok(TransactionsStateQueryResponse::TransactionInfo( - Self::to_tx_info(tx)?, + Self::to_tx_info(&tx)?, )) } + TransactionsStateQuery::GetTransactionStakeCertificates { tx_hash } => { + let Some(tx) = store.get_tx_by_hash(tx_hash.as_ref())? else { + return Ok(TransactionsStateQueryResponse::Error( + QueryError::not_found("Transaction not found"), + )); + }; + Ok( + TransactionsStateQueryResponse::TransactionStakeCertificates( + TransactionStakeCertificates { + certificates: Self::to_tx_stakes(&tx, network_id)?, + }, + ), + ) + } _ => Ok(TransactionsStateQueryResponse::Error( QueryError::not_implemented("Unimplemented".to_string()), )), diff --git a/modules/rest_blockfrost/src/handlers/transactions.rs b/modules/rest_blockfrost/src/handlers/transactions.rs index ed380a7f..83744f63 100644 --- a/modules/rest_blockfrost/src/handlers/transactions.rs +++ b/modules/rest_blockfrost/src/handlers/transactions.rs @@ -4,7 +4,10 @@ use acropolis_common::{ messages::{Message, RESTResponse, StateQuery, StateQueryResponse}, queries::{ parameters::{ParametersStateQuery, ParametersStateQueryResponse}, - transactions::{TransactionInfo, TransactionsStateQuery, TransactionsStateQueryResponse}, + transactions::{ + TransactionInfo, TransactionStakeCertificate, TransactionsStateQuery, + TransactionsStateQueryResponse, + }, utils::{query_state, rest_query_state_async}, }, Lovelace, TxHash, @@ -12,7 +15,10 @@ use acropolis_common::{ use anyhow::{anyhow, Result}; use caryatid_sdk::Context; use hex::FromHex; -use serde::{ser::SerializeStruct, Serialize, Serializer}; +use serde::{ + ser::{Error, SerializeStruct}, + Serialize, Serializer, +}; use std::sync::Arc; use crate::handlers_config::HandlersConfig; @@ -72,7 +78,7 @@ pub async fn handle_transactions_blockfrost( match param { None => handle_transaction_query(context, tx_hash, handlers_config).await, Some("utxo") => Ok(RESTResponse::with_text(501, "Not implemented")), - Some("stakes") => Ok(RESTResponse::with_text(501, "Not implemented")), + Some("stakes") => handle_transaction_stakes_query(context, tx_hash, handlers_config).await, Some("delegations") => Ok(RESTResponse::with_text(501, "Not implemented")), Some("withdrawals") => Ok(RESTResponse::with_text(501, "Not implemented")), Some("mirs") => Ok(RESTResponse::with_text(501, "Not implemented")), @@ -153,3 +159,49 @@ async fn handle_transaction_query( ) .await } + +struct TxStake(TransactionStakeCertificate); + +impl Serialize for TxStake { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + let Ok(address) = self.0.address.to_string() else { + return Err(S::Error::custom("Can't stringify address")); + }; + let mut state = serializer.serialize_struct("TxStake", 3)?; + state.serialize_field("index", &self.0.index)?; + state.serialize_field("address", &address)?; + state.serialize_field("registration", &self.0.registration)?; + state.end() + } +} + +/// Handle `/txs/{hash}/stakes` +async fn handle_transaction_stakes_query( + context: Arc>, + tx_hash: TxHash, + handlers_config: Arc, +) -> Result { + let txs_info_msg = Arc::new(Message::StateQuery(StateQuery::Transactions( + TransactionsStateQuery::GetTransactionStakeCertificates { tx_hash }, + ))); + rest_query_state_async( + &context.clone(), + &handlers_config.transactions_query_topic.clone(), + txs_info_msg, + async move |message| match message { + Message::StateQueryResponse(StateQueryResponse::Transactions( + TransactionsStateQueryResponse::TransactionStakeCertificates(stakes), + )) => Some(Ok(Some( + stakes.certificates.into_iter().map(TxStake).collect::>(), + ))), + Message::StateQueryResponse(StateQueryResponse::Transactions( + TransactionsStateQueryResponse::Error(e), + )) => Some(Err(anyhow!(e))), + _ => None, + }, + ) + .await +} diff --git a/modules/rest_blockfrost/src/rest_blockfrost.rs b/modules/rest_blockfrost/src/rest_blockfrost.rs index bb296905..a7be8d51 100644 --- a/modules/rest_blockfrost/src/rest_blockfrost.rs +++ b/modules/rest_blockfrost/src/rest_blockfrost.rs @@ -217,6 +217,12 @@ const DEFAULT_HANDLE_EPOCH_POOL_BLOCKS_TOPIC: (&str, &str) = ( // Transactions topics const DEFAULT_HANDLE_TRANSACTIONS_TOPIC: (&str, &str) = ("handle-transactions", "rest.get.txs.*"); +const DEFAULT_HANDLE_TRANSACTIONS_SUB_TOPIC: (&str, &str) = + ("handle-transactions-sub", "rest.get.txs.*.*"); +const DEFAULT_HANDLE_TRANSACTIONS_METADATA_SUB_TOPIC: (&str, &str) = ( + "handle-transactions-metadata-sub", + "rest.get.txs.metadata.*", +); // Assets topics const DEFAULT_HANDLE_ASSETS_LIST_TOPIC: (&str, &str) = @@ -728,6 +734,22 @@ impl BlockfrostREST { handle_transactions_blockfrost, ); + // Handler for /txs/{hash}/* + register_handler( + context.clone(), + DEFAULT_HANDLE_TRANSACTIONS_SUB_TOPIC, + handlers_config.clone(), + handle_transactions_blockfrost, + ); + + // Handler for /txs/{hash}/*/* + register_handler( + context.clone(), + DEFAULT_HANDLE_TRANSACTIONS_METADATA_SUB_TOPIC, + handlers_config.clone(), + handle_transactions_blockfrost, + ); + Ok(()) } } From 1155ba08f8bea2ba6243a72da870f179525db646 Mon Sep 17 00:00:00 2001 From: Alex Woods Date: Fri, 7 Nov 2025 15:33:36 +0000 Subject: [PATCH 07/23] Implement txs delegations endpoint --- common/src/queries/transactions.rs | 16 ++++- modules/chain_store/src/chain_store.rs | 64 +++++++++++++++++-- .../src/handlers/transactions.rs | 55 +++++++++++++++- 3 files changed, 125 insertions(+), 10 deletions(-) diff --git a/common/src/queries/transactions.rs b/common/src/queries/transactions.rs index 87e38d86..2569e51c 100644 --- a/common/src/queries/transactions.rs +++ b/common/src/queries/transactions.rs @@ -1,4 +1,4 @@ -use crate::{BlockHash, Lovelace, NativeAsset, StakeAddress, TxHash}; +use crate::{BlockHash, Lovelace, NativeAsset, PoolId, StakeAddress, TxHash}; use serde::ser::{Serialize, SerializeStruct, Serializer}; use serde_with::serde_as; @@ -13,7 +13,7 @@ pub enum TransactionsStateQuery { GetTransactionInfo { tx_hash: TxHash }, GetTransactionUTxOs, GetTransactionStakeCertificates { tx_hash: TxHash }, - GetTransactionDelegationCertificates, + GetTransactionDelegationCertificates { tx_hash: TxHash }, GetTransactionWithdrawals, GetTransactionMIRs, GetTransactionPoolUpdateCertificates, @@ -112,7 +112,17 @@ pub struct TransactionStakeCertificates { } #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] -pub struct TransactionDelegationCertificates {} +pub struct TransactionDelegationCertificate { + pub index: u64, + pub address: StakeAddress, + pub pool: PoolId, + pub active_epoch: u64, +} + +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] +pub struct TransactionDelegationCertificates { + pub certificates: Vec, +} #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] pub struct TransactionWithdrawals {} diff --git a/modules/chain_store/src/chain_store.rs b/modules/chain_store/src/chain_store.rs index 07b047ef..717b11ae 100644 --- a/modules/chain_store/src/chain_store.rs +++ b/modules/chain_store/src/chain_store.rs @@ -1,7 +1,9 @@ mod stores; use acropolis_codec::{ - block::map_to_block_issuer, map_parameters, map_parameters::map_stake_address, + block::map_to_block_issuer, + map_parameters, + map_parameters::{map_stake_address, to_pool_id}, }; use acropolis_common::queries::errors::QueryError; use acropolis_common::{ @@ -15,9 +17,9 @@ use acropolis_common::{ }, queries::misc::Order, queries::transactions::{ - TransactionInfo, TransactionOutputAmount, TransactionStakeCertificate, - TransactionStakeCertificates, TransactionsStateQuery, TransactionsStateQueryResponse, - DEFAULT_TRANSACTIONS_QUERY_TOPIC, + TransactionDelegationCertificate, TransactionDelegationCertificates, TransactionInfo, + TransactionOutputAmount, TransactionStakeCertificate, TransactionStakeCertificates, + TransactionsStateQuery, TransactionsStateQueryResponse, DEFAULT_TRANSACTIONS_QUERY_TOPIC, }, state_history::{StateHistory, StateHistoryStore}, AssetName, BechOrdAddress, BlockHash, GenesisDelegate, HeavyDelegate, NativeAsset, NetworkId, @@ -716,6 +718,46 @@ impl ChainStore { Ok(certs) } + fn to_tx_delegations( + tx: &Tx, + network_id: NetworkId, + ) -> Result> { + let block = pallas_traverse::MultiEraBlock::decode(&tx.block.bytes)?; + let txs = block.txs(); + let Some(tx_decoded) = txs.get(tx.index as usize) else { + return Err(anyhow!("Transaction not found in block for given index")); + }; + let mut certs = Vec::new(); + for (index, cert) in tx_decoded.certs().iter().enumerate() { + match cert { + MultiEraCert::AlonzoCompatible(cert) => match cert.as_ref().as_ref() { + alonzo::Certificate::StakeDelegation(cred, pool_key_hash) => { + certs.push(TransactionDelegationCertificate { + index: index as u64, + address: map_stake_address(cred, network_id.clone()), + pool: to_pool_id(pool_key_hash), + active_epoch: tx.block.extra.epoch + 1, + }); + } + _ => (), + }, + MultiEraCert::Conway(cert) => match cert.as_ref().as_ref() { + conway::Certificate::StakeDelegation(cred, pool_key_hash) => { + certs.push(TransactionDelegationCertificate { + index: index as u64, + address: map_stake_address(cred, network_id.clone()), + pool: to_pool_id(pool_key_hash), + active_epoch: tx.block.extra.epoch + 1, + }); + } + _ => (), + }, + _ => (), + } + } + Ok(certs) + } + fn handle_txs_query( store: &Arc, query: &TransactionsStateQuery, @@ -746,6 +788,20 @@ impl ChainStore { ), ) } + TransactionsStateQuery::GetTransactionDelegationCertificates { tx_hash } => { + let Some(tx) = store.get_tx_by_hash(tx_hash.as_ref())? else { + return Ok(TransactionsStateQueryResponse::Error( + QueryError::not_found("Transaction not found"), + )); + }; + Ok( + TransactionsStateQueryResponse::TransactionDelegationCertificates( + TransactionDelegationCertificates { + certificates: Self::to_tx_delegations(&tx, network_id)?, + }, + ), + ) + } _ => Ok(TransactionsStateQueryResponse::Error( QueryError::not_implemented("Unimplemented".to_string()), )), diff --git a/modules/rest_blockfrost/src/handlers/transactions.rs b/modules/rest_blockfrost/src/handlers/transactions.rs index 83744f63..acfb72a0 100644 --- a/modules/rest_blockfrost/src/handlers/transactions.rs +++ b/modules/rest_blockfrost/src/handlers/transactions.rs @@ -5,8 +5,8 @@ use acropolis_common::{ queries::{ parameters::{ParametersStateQuery, ParametersStateQueryResponse}, transactions::{ - TransactionInfo, TransactionStakeCertificate, TransactionsStateQuery, - TransactionsStateQueryResponse, + TransactionDelegationCertificate, TransactionInfo, TransactionStakeCertificate, + TransactionsStateQuery, TransactionsStateQueryResponse, }, utils::{query_state, rest_query_state_async}, }, @@ -79,7 +79,9 @@ pub async fn handle_transactions_blockfrost( None => handle_transaction_query(context, tx_hash, handlers_config).await, Some("utxo") => Ok(RESTResponse::with_text(501, "Not implemented")), Some("stakes") => handle_transaction_stakes_query(context, tx_hash, handlers_config).await, - Some("delegations") => Ok(RESTResponse::with_text(501, "Not implemented")), + Some("delegations") => { + handle_transaction_delegations_query(context, tx_hash, handlers_config).await + } Some("withdrawals") => Ok(RESTResponse::with_text(501, "Not implemented")), Some("mirs") => Ok(RESTResponse::with_text(501, "Not implemented")), Some("pool_updates") => Ok(RESTResponse::with_text(501, "Not implemented")), @@ -205,3 +207,50 @@ async fn handle_transaction_stakes_query( ) .await } + +struct TxDelegation(TransactionDelegationCertificate); + +impl Serialize for TxDelegation { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + let Ok(address) = self.0.address.to_string() else { + return Err(S::Error::custom("Can't stringify address")); + }; + let mut state = serializer.serialize_struct("TxDelegation", 4)?; + state.serialize_field("index", &self.0.index)?; + state.serialize_field("address", &address)?; + state.serialize_field("pool_id", &self.0.pool.to_string())?; + state.serialize_field("active_epoch", &self.0.active_epoch)?; + state.end() + } +} + +/// Handle `/txs/{hash}/delegations` +async fn handle_transaction_delegations_query( + context: Arc>, + tx_hash: TxHash, + handlers_config: Arc, +) -> Result { + let txs_info_msg = Arc::new(Message::StateQuery(StateQuery::Transactions( + TransactionsStateQuery::GetTransactionDelegationCertificates { tx_hash }, + ))); + rest_query_state_async( + &context.clone(), + &handlers_config.transactions_query_topic.clone(), + txs_info_msg, + async move |message| match message { + Message::StateQueryResponse(StateQueryResponse::Transactions( + TransactionsStateQueryResponse::TransactionDelegationCertificates(delegations), + )) => Some(Ok(Some( + delegations.certificates.into_iter().map(TxDelegation).collect::>(), + ))), + Message::StateQueryResponse(StateQueryResponse::Transactions( + TransactionsStateQueryResponse::Error(e), + )) => Some(Err(anyhow!(e))), + _ => None, + }, + ) + .await +} From c0b1c1946330127f6403672cac41509bd0eb1e14 Mon Sep 17 00:00:00 2001 From: Alex Woods Date: Fri, 7 Nov 2025 19:17:02 +0000 Subject: [PATCH 08/23] Implement txs withdrawals endpoint --- common/src/queries/transactions.rs | 12 ++++- modules/chain_store/src/chain_store.rs | 51 +++++++++++++++---- .../src/handlers/transactions.rs | 51 ++++++++++++++++++- 3 files changed, 100 insertions(+), 14 deletions(-) diff --git a/common/src/queries/transactions.rs b/common/src/queries/transactions.rs index 2569e51c..172a2c30 100644 --- a/common/src/queries/transactions.rs +++ b/common/src/queries/transactions.rs @@ -14,7 +14,7 @@ pub enum TransactionsStateQuery { GetTransactionUTxOs, GetTransactionStakeCertificates { tx_hash: TxHash }, GetTransactionDelegationCertificates { tx_hash: TxHash }, - GetTransactionWithdrawals, + GetTransactionWithdrawals { tx_hash: TxHash }, GetTransactionMIRs, GetTransactionPoolUpdateCertificates, GetTransactionPoolRetirementCertificates, @@ -125,7 +125,15 @@ pub struct TransactionDelegationCertificates { } #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] -pub struct TransactionWithdrawals {} +pub struct TransactionWithdrawal { + pub address: StakeAddress, + pub amount: Lovelace, +} + +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] +pub struct TransactionWithdrawals { + pub withdrawals: Vec, +} #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] pub struct ScriptDatumJSON {} diff --git a/modules/chain_store/src/chain_store.rs b/modules/chain_store/src/chain_store.rs index 717b11ae..b3471c30 100644 --- a/modules/chain_store/src/chain_store.rs +++ b/modules/chain_store/src/chain_store.rs @@ -19,11 +19,12 @@ use acropolis_common::{ queries::transactions::{ TransactionDelegationCertificate, TransactionDelegationCertificates, TransactionInfo, TransactionOutputAmount, TransactionStakeCertificate, TransactionStakeCertificates, - TransactionsStateQuery, TransactionsStateQueryResponse, DEFAULT_TRANSACTIONS_QUERY_TOPIC, + TransactionWithdrawal, TransactionWithdrawals, TransactionsStateQuery, + TransactionsStateQueryResponse, DEFAULT_TRANSACTIONS_QUERY_TOPIC, }, state_history::{StateHistory, StateHistoryStore}, AssetName, BechOrdAddress, BlockHash, GenesisDelegate, HeavyDelegate, NativeAsset, NetworkId, - PoolId, TxHash, + PoolId, StakeAddress, TxHash, }; use anyhow::{anyhow, bail, Result}; use caryatid_sdk::{module, Context, Module}; @@ -730,8 +731,10 @@ impl ChainStore { let mut certs = Vec::new(); for (index, cert) in tx_decoded.certs().iter().enumerate() { match cert { - MultiEraCert::AlonzoCompatible(cert) => match cert.as_ref().as_ref() { - alonzo::Certificate::StakeDelegation(cred, pool_key_hash) => { + MultiEraCert::AlonzoCompatible(cert) => { + if let alonzo::Certificate::StakeDelegation(cred, pool_key_hash) = + cert.as_ref().as_ref() + { certs.push(TransactionDelegationCertificate { index: index as u64, address: map_stake_address(cred, network_id.clone()), @@ -739,10 +742,11 @@ impl ChainStore { active_epoch: tx.block.extra.epoch + 1, }); } - _ => (), - }, - MultiEraCert::Conway(cert) => match cert.as_ref().as_ref() { - conway::Certificate::StakeDelegation(cred, pool_key_hash) => { + } + MultiEraCert::Conway(cert) => { + if let conway::Certificate::StakeDelegation(cred, pool_key_hash) = + cert.as_ref().as_ref() + { certs.push(TransactionDelegationCertificate { index: index as u64, address: map_stake_address(cred, network_id.clone()), @@ -750,14 +754,29 @@ impl ChainStore { active_epoch: tx.block.extra.epoch + 1, }); } - _ => (), - }, + } _ => (), } } Ok(certs) } + fn to_tx_withdrawals(tx: &Tx) -> Result> { + let block = pallas_traverse::MultiEraBlock::decode(&tx.block.bytes)?; + let txs = block.txs(); + let Some(tx_decoded) = txs.get(tx.index as usize) else { + return Err(anyhow!("Transaction not found in block for given index")); + }; + let mut withdrawals = Vec::new(); + for (address, amount) in tx_decoded.withdrawals_sorted_set() { + withdrawals.push(TransactionWithdrawal { + address: StakeAddress::from_binary(address)?, + amount, + }); + } + Ok(withdrawals) + } + fn handle_txs_query( store: &Arc, query: &TransactionsStateQuery, @@ -802,6 +821,18 @@ impl ChainStore { ), ) } + TransactionsStateQuery::GetTransactionWithdrawals { tx_hash } => { + let Some(tx) = store.get_tx_by_hash(tx_hash.as_ref())? else { + return Ok(TransactionsStateQueryResponse::Error( + QueryError::not_found("Transaction not found"), + )); + }; + Ok(TransactionsStateQueryResponse::TransactionWithdrawals( + TransactionWithdrawals { + withdrawals: Self::to_tx_withdrawals(&tx)?, + }, + )) + } _ => Ok(TransactionsStateQueryResponse::Error( QueryError::not_implemented("Unimplemented".to_string()), )), diff --git a/modules/rest_blockfrost/src/handlers/transactions.rs b/modules/rest_blockfrost/src/handlers/transactions.rs index acfb72a0..577becc3 100644 --- a/modules/rest_blockfrost/src/handlers/transactions.rs +++ b/modules/rest_blockfrost/src/handlers/transactions.rs @@ -6,7 +6,7 @@ use acropolis_common::{ parameters::{ParametersStateQuery, ParametersStateQueryResponse}, transactions::{ TransactionDelegationCertificate, TransactionInfo, TransactionStakeCertificate, - TransactionsStateQuery, TransactionsStateQueryResponse, + TransactionWithdrawal, TransactionsStateQuery, TransactionsStateQueryResponse, }, utils::{query_state, rest_query_state_async}, }, @@ -82,7 +82,9 @@ pub async fn handle_transactions_blockfrost( Some("delegations") => { handle_transaction_delegations_query(context, tx_hash, handlers_config).await } - Some("withdrawals") => Ok(RESTResponse::with_text(501, "Not implemented")), + Some("withdrawals") => { + handle_transaction_withdrawals_query(context, tx_hash, handlers_config).await + } Some("mirs") => Ok(RESTResponse::with_text(501, "Not implemented")), Some("pool_updates") => Ok(RESTResponse::with_text(501, "Not implemented")), Some("pool_retires") => Ok(RESTResponse::with_text(501, "Not implemented")), @@ -254,3 +256,48 @@ async fn handle_transaction_delegations_query( ) .await } + +struct TxWithdrawal(TransactionWithdrawal); + +impl Serialize for TxWithdrawal { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + let Ok(address) = self.0.address.to_string() else { + return Err(S::Error::custom("Can't stringify address")); + }; + let mut state = serializer.serialize_struct("TxWithdrawal", 4)?; + state.serialize_field("address", &address)?; + state.serialize_field("amount", &self.0.amount.to_string())?; + state.end() + } +} + +/// Handle `/txs/{hash}/withdrawals` +async fn handle_transaction_withdrawals_query( + context: Arc>, + tx_hash: TxHash, + handlers_config: Arc, +) -> Result { + let txs_info_msg = Arc::new(Message::StateQuery(StateQuery::Transactions( + TransactionsStateQuery::GetTransactionWithdrawals { tx_hash }, + ))); + rest_query_state_async( + &context.clone(), + &handlers_config.transactions_query_topic.clone(), + txs_info_msg, + async move |message| match message { + Message::StateQueryResponse(StateQueryResponse::Transactions( + TransactionsStateQueryResponse::TransactionWithdrawals(withdrawals), + )) => Some(Ok(Some( + withdrawals.withdrawals.into_iter().map(TxWithdrawal).collect::>(), + ))), + Message::StateQueryResponse(StateQueryResponse::Transactions( + TransactionsStateQueryResponse::Error(e), + )) => Some(Err(anyhow!(e))), + _ => None, + }, + ) + .await +} From 129199e92ed77bd1417db0c312f1d31463874a3a Mon Sep 17 00:00:00 2001 From: Alex Woods Date: Wed, 12 Nov 2025 14:25:52 +0000 Subject: [PATCH 09/23] Add txs mirs endpoint --- common/src/queries/transactions.rs | 18 ++++- common/src/types.rs | 19 +++++ modules/chain_store/src/chain_store.rs | 71 ++++++++++++++++--- .../src/handlers/transactions.rs | 63 ++++++++++++++-- 4 files changed, 155 insertions(+), 16 deletions(-) diff --git a/common/src/queries/transactions.rs b/common/src/queries/transactions.rs index 172a2c30..002eca2e 100644 --- a/common/src/queries/transactions.rs +++ b/common/src/queries/transactions.rs @@ -1,4 +1,6 @@ -use crate::{BlockHash, Lovelace, NativeAsset, PoolId, StakeAddress, TxHash}; +use crate::{ + BlockHash, InstantaneousRewardSource, Lovelace, NativeAsset, PoolId, StakeAddress, TxHash, +}; use serde::ser::{Serialize, SerializeStruct, Serializer}; use serde_with::serde_as; @@ -15,7 +17,7 @@ pub enum TransactionsStateQuery { GetTransactionStakeCertificates { tx_hash: TxHash }, GetTransactionDelegationCertificates { tx_hash: TxHash }, GetTransactionWithdrawals { tx_hash: TxHash }, - GetTransactionMIRs, + GetTransactionMIRs { tx_hash: TxHash }, GetTransactionPoolUpdateCertificates, GetTransactionPoolRetirementCertificates, GetTransactionMetadata, @@ -139,7 +141,17 @@ pub struct TransactionWithdrawals { pub struct ScriptDatumJSON {} #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] -pub struct TransactionMIRs {} +pub struct TransactionMIR { + pub cert_index: u64, + pub pot: InstantaneousRewardSource, + pub address: StakeAddress, + pub amount: Lovelace, +} + +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] +pub struct TransactionMIRs { + pub mirs: Vec, +} #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] pub struct TransactionPoolUpdateCertificates {} diff --git a/common/src/types.rs b/common/src/types.rs index 2c4a3d84..7f157a79 100644 --- a/common/src/types.rs +++ b/common/src/types.rs @@ -712,6 +712,16 @@ pub enum Pot { Deposits, } +impl fmt::Display for Pot { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Pot::Reserves => write!(f, "reserves"), + Pot::Treasury => write!(f, "treasury"), + Pot::Deposits => write!(f, "deposits"), + } + } +} + /// Pot Delta - internal change of pot values at genesis / era boundaries #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] pub struct PotDelta { @@ -1070,6 +1080,15 @@ pub enum InstantaneousRewardSource { Treasury, } +impl fmt::Display for InstantaneousRewardSource { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + InstantaneousRewardSource::Reserves => write!(f, "reserves"), + InstantaneousRewardSource::Treasury => write!(f, "treasury"), + } + } +} + /// Target of a MIR #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] pub enum InstantaneousRewardTarget { diff --git a/modules/chain_store/src/chain_store.rs b/modules/chain_store/src/chain_store.rs index 74e1a041..02417af1 100644 --- a/modules/chain_store/src/chain_store.rs +++ b/modules/chain_store/src/chain_store.rs @@ -9,6 +9,12 @@ use acropolis_common::queries::errors::QueryError; use acropolis_common::{ crypto::keyhash_224, messages::{CardanoMessage, Message, StateQuery, StateQueryResponse}, + queries::transactions::{ + TransactionDelegationCertificate, TransactionDelegationCertificates, TransactionInfo, + TransactionMIR, TransactionMIRs, TransactionOutputAmount, TransactionStakeCertificate, + TransactionStakeCertificates, TransactionWithdrawal, TransactionWithdrawals, + TransactionsStateQuery, TransactionsStateQueryResponse, DEFAULT_TRANSACTIONS_QUERY_TOPIC, + }, queries::{ blocks::{ BlockHashes, BlockInfo, BlockInvolvedAddress, BlockInvolvedAddresses, BlockKey, @@ -18,15 +24,9 @@ use acropolis_common::{ }, misc::Order, }, - queries::transactions::{ - TransactionDelegationCertificate, TransactionDelegationCertificates, TransactionInfo, - TransactionOutputAmount, TransactionStakeCertificate, TransactionStakeCertificates, - TransactionWithdrawal, TransactionWithdrawals, TransactionsStateQuery, - TransactionsStateQueryResponse, DEFAULT_TRANSACTIONS_QUERY_TOPIC, - }, state_history::{StateHistory, StateHistoryStore}, - AssetName, BechOrdAddress, BlockHash, GenesisDelegate, HeavyDelegate, NativeAsset, NetworkId, - PoolId, StakeAddress, TxHash, + AssetName, BechOrdAddress, BlockHash, GenesisDelegate, HeavyDelegate, + InstantaneousRewardSource, NativeAsset, NetworkId, PoolId, StakeAddress, TxHash, }; use anyhow::{anyhow, bail, Result}; use caryatid_sdk::{module, Context, Module}; @@ -805,6 +805,49 @@ impl ChainStore { Ok(withdrawals) } + fn to_tx_mirs(tx: &Tx, network_id: NetworkId) -> Result> { + let block = pallas_traverse::MultiEraBlock::decode(&tx.block.bytes)?; + let txs = block.txs(); + let Some(tx_decoded) = txs.get(tx.index as usize) else { + return Err(anyhow!("Transaction not found in block for given index")); + }; + let mut certs = Vec::new(); + for (cert_index, cert) in tx_decoded.certs().iter().enumerate() { + match cert { + MultiEraCert::AlonzoCompatible(cert) => { + if let alonzo::Certificate::MoveInstantaneousRewardsCert(cert) = + cert.as_ref().as_ref() + { + match &cert.target { + alonzo::InstantaneousRewardTarget::StakeCredentials(creds) => { + for (cred, amount) in creds.clone().to_vec() { + certs.push(TransactionMIR { + cert_index: cert_index as u64, + pot: match cert.source { + alonzo::InstantaneousRewardSource::Reserves => { + InstantaneousRewardSource::Reserves + } + alonzo::InstantaneousRewardSource::Treasury => { + InstantaneousRewardSource::Treasury + } + }, + address: map_stake_address(&cred, network_id.clone()), + amount: amount as u64, + }); + } + } + alonzo::InstantaneousRewardTarget::OtherAccountingPot(coin) => { + // TODO + } + } + } + } + _ => (), + } + } + Ok(certs) + } + fn handle_txs_query( store: &Arc, query: &TransactionsStateQuery, @@ -861,6 +904,18 @@ impl ChainStore { }, )) } + TransactionsStateQuery::GetTransactionMIRs { tx_hash } => { + let Some(tx) = store.get_tx_by_hash(tx_hash.as_ref())? else { + return Ok(TransactionsStateQueryResponse::Error( + QueryError::not_found("Transaction not found"), + )); + }; + Ok(TransactionsStateQueryResponse::TransactionMIRs( + TransactionMIRs { + mirs: Self::to_tx_mirs(&tx, network_id)?, + }, + )) + } _ => Ok(TransactionsStateQueryResponse::Error( QueryError::not_implemented("Unimplemented".to_string()), )), diff --git a/modules/rest_blockfrost/src/handlers/transactions.rs b/modules/rest_blockfrost/src/handlers/transactions.rs index c691e6e3..d80e4947 100644 --- a/modules/rest_blockfrost/src/handlers/transactions.rs +++ b/modules/rest_blockfrost/src/handlers/transactions.rs @@ -7,8 +7,9 @@ use acropolis_common::{ errors::QueryError, parameters::{ParametersStateQuery, ParametersStateQueryResponse}, transactions::{ - TransactionDelegationCertificate, TransactionInfo, TransactionStakeCertificate, - TransactionWithdrawal, TransactionsStateQuery, TransactionsStateQueryResponse, + TransactionDelegationCertificate, TransactionInfo, TransactionMIR, + TransactionStakeCertificate, TransactionWithdrawal, TransactionsStateQuery, + TransactionsStateQueryResponse, }, utils::{query_state, rest_query_state_async}, }, @@ -73,7 +74,12 @@ pub async fn handle_transactions_blockfrost( let tx_hash = match TxHash::from_hex(tx_hash) { Ok(hash) => hash, - Err(_) => return Err(RESTError::invalid_param("transaction", "Invalid transaction hash")), + Err(_) => { + return Err(RESTError::invalid_param( + "transaction", + "Invalid transaction hash", + )) + } }; match param { @@ -86,7 +92,7 @@ pub async fn handle_transactions_blockfrost( Some("withdrawals") => { handle_transaction_withdrawals_query(context, tx_hash, handlers_config).await } - Some("mirs") => Ok(RESTResponse::with_text(501, "Not implemented")), + Some("mirs") => handle_transaction_mirs_query(context, tx_hash, handlers_config).await, Some("pool_updates") => Ok(RESTResponse::with_text(501, "Not implemented")), Some("pool_retires") => Ok(RESTResponse::with_text(501, "Not implemented")), Some("metadata") => match param2 { @@ -268,7 +274,7 @@ impl Serialize for TxWithdrawal { let Ok(address) = self.0.address.to_string() else { return Err(S::Error::custom("Can't stringify address")); }; - let mut state = serializer.serialize_struct("TxWithdrawal", 4)?; + let mut state = serializer.serialize_struct("TxWithdrawal", 2)?; state.serialize_field("address", &address)?; state.serialize_field("amount", &self.0.amount.to_string())?; state.end() @@ -302,3 +308,50 @@ async fn handle_transaction_withdrawals_query( ) .await } + +struct TxMIR(TransactionMIR); + +impl Serialize for TxMIR { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + let Ok(address) = self.0.address.to_string() else { + return Err(S::Error::custom("Can't stringify address")); + }; + let mut state = serializer.serialize_struct("TxMIR", 4)?; + state.serialize_field("cert_index", &self.0.cert_index)?; + state.serialize_field("pot", &self.0.pot.to_string().to_lowercase())?; + state.serialize_field("address", &address)?; + state.serialize_field("amount", &self.0.amount.to_string())?; + state.end() + } +} + +/// Handle `/txs/{hash}/mirs` +async fn handle_transaction_mirs_query( + context: Arc>, + tx_hash: TxHash, + handlers_config: Arc, +) -> Result { + let txs_info_msg = Arc::new(Message::StateQuery(StateQuery::Transactions( + TransactionsStateQuery::GetTransactionMIRs { tx_hash }, + ))); + rest_query_state_async( + &context.clone(), + &handlers_config.transactions_query_topic.clone(), + txs_info_msg, + async move |message| match message { + Message::StateQueryResponse(StateQueryResponse::Transactions( + TransactionsStateQueryResponse::TransactionMIRs(mirs), + )) => Some(Ok(Some( + mirs.mirs.into_iter().map(TxMIR).collect::>(), + ))), + Message::StateQueryResponse(StateQueryResponse::Transactions( + TransactionsStateQueryResponse::Error(e), + )) => Some(Err(e)), + _ => None, + }, + ) + .await +} From 95eeeaefbcf90ac05666080d91c60579c65d6a43 Mon Sep 17 00:00:00 2001 From: Alex Woods Date: Wed, 12 Nov 2025 17:52:02 +0000 Subject: [PATCH 10/23] WIP on txs pool_updates endpoint --- codec/src/map_parameters.rs | 2 +- common/src/queries/transactions.rs | 15 +++- modules/chain_store/src/chain_store.rs | 88 ++++++++++++++++++- .../src/handlers/transactions.rs | 62 ++++++++++++- 4 files changed, 156 insertions(+), 11 deletions(-) diff --git a/codec/src/map_parameters.rs b/codec/src/map_parameters.rs index 65d32b0d..6555009d 100644 --- a/codec/src/map_parameters.rs +++ b/codec/src/map_parameters.rs @@ -204,7 +204,7 @@ fn map_constitution(constitution: &conway::Constitution) -> Constitution { } /// Map a Pallas Relay to ours -fn map_relay(relay: &PallasRelay) -> Relay { +pub fn map_relay(relay: &PallasRelay) -> Relay { match relay { PallasRelay::SingleHostAddr(port, ipv4, ipv6) => Relay::SingleHostAddr(SingleHostAddr { port: match port { diff --git a/common/src/queries/transactions.rs b/common/src/queries/transactions.rs index 002eca2e..4d6c9b3f 100644 --- a/common/src/queries/transactions.rs +++ b/common/src/queries/transactions.rs @@ -1,5 +1,6 @@ use crate::{ - BlockHash, InstantaneousRewardSource, Lovelace, NativeAsset, PoolId, StakeAddress, TxHash, + BlockHash, InstantaneousRewardSource, Lovelace, NativeAsset, PoolId, PoolRegistration, + StakeAddress, TxHash, }; use serde::ser::{Serialize, SerializeStruct, Serializer}; use serde_with::serde_as; @@ -18,7 +19,7 @@ pub enum TransactionsStateQuery { GetTransactionDelegationCertificates { tx_hash: TxHash }, GetTransactionWithdrawals { tx_hash: TxHash }, GetTransactionMIRs { tx_hash: TxHash }, - GetTransactionPoolUpdateCertificates, + GetTransactionPoolUpdateCertificates { tx_hash: TxHash }, GetTransactionPoolRetirementCertificates, GetTransactionMetadata, GetTransactionMetadataCBOR, @@ -154,7 +155,15 @@ pub struct TransactionMIRs { } #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] -pub struct TransactionPoolUpdateCertificates {} +pub struct TransactionPoolUpdateCertificate { + pub cert_index: u64, + pub pool_reg: PoolRegistration, +} + +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] +pub struct TransactionPoolUpdateCertificates { + pub pool_updates: Vec, +} #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] pub struct TransactionPoolRetirementCertificates {} diff --git a/modules/chain_store/src/chain_store.rs b/modules/chain_store/src/chain_store.rs index 02417af1..e7b731e2 100644 --- a/modules/chain_store/src/chain_store.rs +++ b/modules/chain_store/src/chain_store.rs @@ -3,7 +3,7 @@ mod stores; use acropolis_codec::{ block::map_to_block_issuer, map_parameters, - map_parameters::{map_stake_address, to_pool_id}, + map_parameters::{map_relay, map_stake_address, to_hash, to_pool_id, to_vrf_key}, }; use acropolis_common::queries::errors::QueryError; use acropolis_common::{ @@ -11,7 +11,8 @@ use acropolis_common::{ messages::{CardanoMessage, Message, StateQuery, StateQueryResponse}, queries::transactions::{ TransactionDelegationCertificate, TransactionDelegationCertificates, TransactionInfo, - TransactionMIR, TransactionMIRs, TransactionOutputAmount, TransactionStakeCertificate, + TransactionMIR, TransactionMIRs, TransactionOutputAmount, TransactionPoolUpdateCertificate, + TransactionPoolUpdateCertificates, TransactionStakeCertificate, TransactionStakeCertificates, TransactionWithdrawal, TransactionWithdrawals, TransactionsStateQuery, TransactionsStateQueryResponse, DEFAULT_TRANSACTIONS_QUERY_TOPIC, }, @@ -26,12 +27,13 @@ use acropolis_common::{ }, state_history::{StateHistory, StateHistoryStore}, AssetName, BechOrdAddress, BlockHash, GenesisDelegate, HeavyDelegate, - InstantaneousRewardSource, NativeAsset, NetworkId, PoolId, StakeAddress, TxHash, + InstantaneousRewardSource, NativeAsset, NetworkId, PoolId, PoolMetadata, PoolRegistration, + Ratio, StakeAddress, StakeCredential, TxHash, }; use anyhow::{anyhow, bail, Result}; use caryatid_sdk::{module, Context, Module}; use config::Config; -use pallas::ledger::primitives::{alonzo, conway}; +use pallas::ledger::primitives::{alonzo, conway, Nullable}; use pallas_traverse::MultiEraCert; use std::collections::{BTreeMap, HashMap}; use std::sync::Arc; @@ -848,6 +850,70 @@ impl ChainStore { Ok(certs) } + fn to_tx_pool_updates( + tx: &Tx, + network_id: NetworkId, + ) -> Result> { + let block = pallas_traverse::MultiEraBlock::decode(&tx.block.bytes)?; + let txs = block.txs(); + let Some(tx_decoded) = txs.get(tx.index as usize) else { + return Err(anyhow!("Transaction not found in block for given index")); + }; + let mut certs = Vec::new(); + for (cert_index, cert) in tx_decoded.certs().iter().enumerate() { + match cert { + MultiEraCert::Conway(cert) => { + if let conway::Certificate::PoolRegistration { + operator, + vrf_keyhash, + pledge, + cost, + margin, + reward_account, + pool_owners, + relays, + pool_metadata, + } = cert.as_ref().as_ref() + { + certs.push(TransactionPoolUpdateCertificate { + cert_index: cert_index as u64, + pool_reg: PoolRegistration { + operator: to_pool_id(operator), + vrf_key_hash: to_vrf_key(vrf_keyhash), + pledge: *pledge, + cost: *cost, + margin: Ratio { + numerator: margin.numerator, + denominator: margin.denominator, + }, + reward_account: StakeAddress::from_binary(reward_account)?, + pool_owners: pool_owners + .into_iter() + .map(|v| { + StakeAddress::new( + StakeCredential::AddrKeyHash(to_hash(v)), + network_id.clone(), + ) + }) + .collect(), + relays: relays.iter().map(map_relay).collect(), + pool_metadata: match pool_metadata { + Nullable::Some(md) => Some(PoolMetadata { + url: md.url.clone(), + hash: md.hash.to_vec(), + }), + _ => None, + }, + }, + }); + } + } + _ => (), + } + } + Ok(certs) + } + fn handle_txs_query( store: &Arc, query: &TransactionsStateQuery, @@ -916,6 +982,20 @@ impl ChainStore { }, )) } + TransactionsStateQuery::GetTransactionPoolUpdateCertificates { tx_hash } => { + let Some(tx) = store.get_tx_by_hash(tx_hash.as_ref())? else { + return Ok(TransactionsStateQueryResponse::Error( + QueryError::not_found("Transaction not found"), + )); + }; + Ok( + TransactionsStateQueryResponse::TransactionPoolUpdateCertificates( + TransactionPoolUpdateCertificates { + pool_updates: Self::to_tx_pool_updates(&tx, network_id)?, + }, + ), + ) + } _ => Ok(TransactionsStateQueryResponse::Error( QueryError::not_implemented("Unimplemented".to_string()), )), diff --git a/modules/rest_blockfrost/src/handlers/transactions.rs b/modules/rest_blockfrost/src/handlers/transactions.rs index d80e4947..62d5913d 100644 --- a/modules/rest_blockfrost/src/handlers/transactions.rs +++ b/modules/rest_blockfrost/src/handlers/transactions.rs @@ -8,8 +8,8 @@ use acropolis_common::{ parameters::{ParametersStateQuery, ParametersStateQueryResponse}, transactions::{ TransactionDelegationCertificate, TransactionInfo, TransactionMIR, - TransactionStakeCertificate, TransactionWithdrawal, TransactionsStateQuery, - TransactionsStateQueryResponse, + TransactionPoolUpdateCertificate, TransactionStakeCertificate, TransactionWithdrawal, + TransactionsStateQuery, TransactionsStateQueryResponse, }, utils::{query_state, rest_query_state_async}, }, @@ -93,7 +93,9 @@ pub async fn handle_transactions_blockfrost( handle_transaction_withdrawals_query(context, tx_hash, handlers_config).await } Some("mirs") => handle_transaction_mirs_query(context, tx_hash, handlers_config).await, - Some("pool_updates") => Ok(RESTResponse::with_text(501, "Not implemented")), + Some("pool_updates") => { + handle_transaction_pool_updates_query(context, tx_hash, handlers_config).await + } Some("pool_retires") => Ok(RESTResponse::with_text(501, "Not implemented")), Some("metadata") => match param2 { None => Ok(RESTResponse::with_text(501, "Not implemented")), @@ -355,3 +357,57 @@ async fn handle_transaction_mirs_query( ) .await } + +struct TxPoolUpdateCertificate(TransactionPoolUpdateCertificate); + +impl Serialize for TxPoolUpdateCertificate { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + let Ok(reward_account) = self.0.pool_reg.reward_account.to_string() else { + return Err(S::Error::custom("Can't stringify reward account")); + }; + let mut state = serializer.serialize_struct("TxPoolUpdateCertificate", 4)?; + state.serialize_field("cert_index", &self.0.cert_index)?; + state.serialize_field("pool_id", &self.0.pool_reg.operator.to_string())?; + state.serialize_field("vrf_key", &self.0.pool_reg.vrf_key_hash.to_string())?; + state.serialize_field("pledge", &self.0.pool_reg.pledge.to_string())?; + state.serialize_field("margin_cost", &self.0.pool_reg.margin)?; + state.serialize_field("fixed_cost", &self.0.pool_reg.cost.to_string())?; + state.serialize_field("reward_account", &reward_account)?; + state.end() + } +} + +/// Handle `/txs/{hash}/pool_updates` +async fn handle_transaction_pool_updates_query( + context: Arc>, + tx_hash: TxHash, + handlers_config: Arc, +) -> Result { + let txs_info_msg = Arc::new(Message::StateQuery(StateQuery::Transactions( + TransactionsStateQuery::GetTransactionPoolUpdateCertificates { tx_hash }, + ))); + rest_query_state_async( + &context.clone(), + &handlers_config.transactions_query_topic.clone(), + txs_info_msg, + async move |message| match message { + Message::StateQueryResponse(StateQueryResponse::Transactions( + TransactionsStateQueryResponse::TransactionPoolUpdateCertificates(pool_updates), + )) => Some(Ok(Some( + pool_updates + .pool_updates + .into_iter() + .map(TxPoolUpdateCertificate) + .collect::>(), + ))), + Message::StateQueryResponse(StateQueryResponse::Transactions( + TransactionsStateQueryResponse::Error(e), + )) => Some(Err(e)), + _ => None, + }, + ) + .await +} From 8ee888261443026f735e663c6dae6802cf7f4d46 Mon Sep 17 00:00:00 2001 From: Alex Woods Date: Fri, 14 Nov 2025 13:12:06 +0000 Subject: [PATCH 11/23] Implement txs pool_updates endpoint --- codec/src/map_parameters.rs | 141 ++++++++++-------- common/src/types.rs | 5 +- modules/chain_store/src/chain_store.rs | 79 ++++++---- .../src/handlers/transactions.rs | 49 +++++- 4 files changed, 176 insertions(+), 98 deletions(-) diff --git a/codec/src/map_parameters.rs b/codec/src/map_parameters.rs index 6555009d..41f73efb 100644 --- a/codec/src/map_parameters.rs +++ b/codec/src/map_parameters.rs @@ -19,7 +19,10 @@ use acropolis_common::{ *, }; use pallas_primitives::conway::PseudoScript; -use std::collections::{HashMap, HashSet}; +use std::{ + collections::{HashMap, HashSet}, + net::{Ipv4Addr, Ipv6Addr}, +}; /// Map Pallas Network to our NetworkId pub fn map_network(network: addresses::Network) -> Result { @@ -212,11 +215,11 @@ pub fn map_relay(relay: &PallasRelay) -> Relay { _ => None, }, ipv4: match ipv4 { - Nullable::Some(ipv4) => ipv4.try_into().ok(), + Nullable::Some(ipv4) => <[u8; 4]>::try_from(ipv4).ok().map(Ipv4Addr::from), _ => None, }, ipv6: match ipv6 { - Nullable::Some(ipv6) => ipv6.try_into().ok(), + Nullable::Some(ipv6) => <[u8; 16]>::try_from(ipv6).ok().map(Ipv6Addr::from), _ => None, }, }), @@ -237,6 +240,53 @@ pub fn map_relay(relay: &PallasRelay) -> Relay { // Certificates // +pub fn to_pool_reg( + operator: &pallas_primitives::PoolKeyhash, + vrf_keyhash: &pallas_primitives::VrfKeyhash, + pledge: &pallas_primitives::Coin, + cost: &pallas_primitives::Coin, + margin: &pallas_primitives::UnitInterval, + reward_account: &pallas_primitives::RewardAccount, + pool_owners: &Vec, + relays: &Vec, + pool_metadata: &Nullable, + network_id: NetworkId, + force_reward_network_id: bool, +) -> Result { + Ok(PoolRegistration { + operator: to_pool_id(operator), + vrf_key_hash: to_vrf_key(vrf_keyhash), + pledge: *pledge, + cost: *cost, + margin: Ratio { + numerator: margin.numerator, + denominator: margin.denominator, + }, + reward_account: if force_reward_network_id { + StakeAddress::new( + StakeAddress::from_binary(reward_account)?.credential, + network_id.clone(), + ) + } else { + StakeAddress::from_binary(reward_account)? + }, + pool_owners: pool_owners + .iter() + .map(|v| { + StakeAddress::new(StakeCredential::AddrKeyHash(to_hash(v)), network_id.clone()) + }) + .collect(), + relays: relays.iter().map(map_relay).collect(), + pool_metadata: match pool_metadata { + Nullable::Some(md) => Some(PoolMetadata { + url: md.url.clone(), + hash: md.hash.to_vec(), + }), + _ => None, + }, + }) +} + /// Derive our TxCertificate from a Pallas Certificate pub fn map_certificate( cert: &MultiEraCert, @@ -277,34 +327,19 @@ pub fn map_certificate( relays, pool_metadata, } => Ok(TxCertificateWithPos { - cert: TxCertificate::PoolRegistration(PoolRegistration { - operator: to_pool_id(operator), - vrf_key_hash: to_vrf_key(vrf_keyhash), - pledge: *pledge, - cost: *cost, - margin: Ratio { - numerator: margin.numerator, - denominator: margin.denominator, - }, - reward_account: StakeAddress::from_binary(reward_account)?, - pool_owners: pool_owners - .iter() - .map(|v| { - StakeAddress::new( - StakeCredential::AddrKeyHash(to_hash(v)), - network_id.clone(), - ) - }) - .collect(), - relays: relays.iter().map(map_relay).collect(), - pool_metadata: match pool_metadata { - Nullable::Some(md) => Some(PoolMetadata { - url: md.url.clone(), - hash: md.hash.to_vec(), - }), - _ => None, - }, - }), + cert: TxCertificate::PoolRegistration(to_pool_reg( + operator, + vrf_keyhash, + pledge, + cost, + margin, + reward_account, + pool_owners, + relays, + pool_metadata, + network_id, + false, + )?), tx_identifier, cert_index: cert_index as u64, }), @@ -397,41 +432,23 @@ pub fn map_certificate( relays, pool_metadata, } => Ok(TxCertificateWithPos { - cert: TxCertificate::PoolRegistration(PoolRegistration { - operator: to_pool_id(operator), - vrf_key_hash: to_vrf_key(vrf_keyhash), - pledge: *pledge, - cost: *cost, - margin: Ratio { - numerator: margin.numerator, - denominator: margin.denominator, - }, + cert: TxCertificate::PoolRegistration(to_pool_reg( + operator, + vrf_keyhash, + pledge, + cost, + margin, + reward_account, + pool_owners, + relays, + pool_metadata, + network_id, // Force networkId - in mainnet epoch 208, one SPO (c63dab6d780a) uses // an e0 (testnet!) address, and this then fails to match their actual // reward account (e1). Feels like this should have been // a validation failure, but clearly wasn't! - reward_account: StakeAddress::new( - StakeAddress::from_binary(reward_account)?.credential, - network_id.clone(), - ), - pool_owners: pool_owners - .into_iter() - .map(|v| { - StakeAddress::new( - StakeCredential::AddrKeyHash(to_hash(v)), - network_id.clone(), - ) - }) - .collect(), - relays: relays.iter().map(map_relay).collect(), - pool_metadata: match pool_metadata { - Nullable::Some(md) => Some(PoolMetadata { - url: md.url.clone(), - hash: md.hash.to_vec(), - }), - _ => None, - }, - }), + true, + )?), tx_identifier, cert_index: cert_index as u64, }), diff --git a/common/src/types.rs b/common/src/types.rs index a0563486..9ab563ac 100644 --- a/common/src/types.rs +++ b/common/src/types.rs @@ -22,6 +22,7 @@ use std::{ collections::{HashMap, HashSet}, fmt, fmt::{Display, Formatter}, + net::{Ipv4Addr, Ipv6Addr}, ops::{AddAssign, Neg}, str::FromStr, }; @@ -915,10 +916,10 @@ pub struct SingleHostAddr { pub port: Option, /// Optional IPv4 address - pub ipv4: Option<[u8; 4]>, + pub ipv4: Option, /// Optional IPv6 address - pub ipv6: Option<[u8; 16]>, + pub ipv6: Option, } /// Relay hostname diff --git a/modules/chain_store/src/chain_store.rs b/modules/chain_store/src/chain_store.rs index e7b731e2..5907d3fc 100644 --- a/modules/chain_store/src/chain_store.rs +++ b/modules/chain_store/src/chain_store.rs @@ -3,7 +3,7 @@ mod stores; use acropolis_codec::{ block::map_to_block_issuer, map_parameters, - map_parameters::{map_relay, map_stake_address, to_hash, to_pool_id, to_vrf_key}, + map_parameters::{map_stake_address, to_pool_id, to_pool_reg}, }; use acropolis_common::queries::errors::QueryError; use acropolis_common::{ @@ -27,13 +27,12 @@ use acropolis_common::{ }, state_history::{StateHistory, StateHistoryStore}, AssetName, BechOrdAddress, BlockHash, GenesisDelegate, HeavyDelegate, - InstantaneousRewardSource, NativeAsset, NetworkId, PoolId, PoolMetadata, PoolRegistration, - Ratio, StakeAddress, StakeCredential, TxHash, + InstantaneousRewardSource, NativeAsset, NetworkId, PoolId, StakeAddress, TxHash, }; use anyhow::{anyhow, bail, Result}; use caryatid_sdk::{module, Context, Module}; use config::Config; -use pallas::ledger::primitives::{alonzo, conway, Nullable}; +use pallas::ledger::primitives::{alonzo, conway}; use pallas_traverse::MultiEraCert; use std::collections::{BTreeMap, HashMap}; use std::sync::Arc; @@ -862,6 +861,37 @@ impl ChainStore { let mut certs = Vec::new(); for (cert_index, cert) in tx_decoded.certs().iter().enumerate() { match cert { + MultiEraCert::AlonzoCompatible(cert) => { + if let alonzo::Certificate::PoolRegistration { + operator, + vrf_keyhash, + pledge, + cost, + margin, + reward_account, + pool_owners, + relays, + pool_metadata, + } = cert.as_ref().as_ref() + { + certs.push(TransactionPoolUpdateCertificate { + cert_index: cert_index as u64, + pool_reg: to_pool_reg( + operator, + vrf_keyhash, + pledge, + cost, + margin, + reward_account, + pool_owners, + relays, + pool_metadata, + network_id.clone(), + false, + )?, + }); + } + } MultiEraCert::Conway(cert) => { if let conway::Certificate::PoolRegistration { operator, @@ -877,34 +907,19 @@ impl ChainStore { { certs.push(TransactionPoolUpdateCertificate { cert_index: cert_index as u64, - pool_reg: PoolRegistration { - operator: to_pool_id(operator), - vrf_key_hash: to_vrf_key(vrf_keyhash), - pledge: *pledge, - cost: *cost, - margin: Ratio { - numerator: margin.numerator, - denominator: margin.denominator, - }, - reward_account: StakeAddress::from_binary(reward_account)?, - pool_owners: pool_owners - .into_iter() - .map(|v| { - StakeAddress::new( - StakeCredential::AddrKeyHash(to_hash(v)), - network_id.clone(), - ) - }) - .collect(), - relays: relays.iter().map(map_relay).collect(), - pool_metadata: match pool_metadata { - Nullable::Some(md) => Some(PoolMetadata { - url: md.url.clone(), - hash: md.hash.to_vec(), - }), - _ => None, - }, - }, + pool_reg: to_pool_reg( + operator, + vrf_keyhash, + pledge, + cost, + margin, + reward_account, + pool_owners, + relays, + pool_metadata, + network_id.clone(), + false, + )?, }); } } diff --git a/modules/rest_blockfrost/src/handlers/transactions.rs b/modules/rest_blockfrost/src/handlers/transactions.rs index 62d5913d..1295a441 100644 --- a/modules/rest_blockfrost/src/handlers/transactions.rs +++ b/modules/rest_blockfrost/src/handlers/transactions.rs @@ -13,7 +13,7 @@ use acropolis_common::{ }, utils::{query_state, rest_query_state_async}, }, - Lovelace, TxHash, + Lovelace, Relay, TxHash, }; use caryatid_sdk::Context; use hex::FromHex; @@ -358,6 +358,36 @@ async fn handle_transaction_mirs_query( .await } +struct TxRelay(Relay); + +impl Serialize for TxRelay { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + match &self.0 { + Relay::SingleHostAddr(addr) => { + let mut state = serializer.serialize_struct("TxRelay", 3)?; + state.serialize_field("ipv4", &addr.ipv4)?; + state.serialize_field("ipv6", &addr.ipv6)?; + state.serialize_field("port", &addr.port)?; + state.end() + } + Relay::SingleHostName(name) => { + let mut state = serializer.serialize_struct("TxRelay", 2)?; + state.serialize_field("dns", &name.dns_name)?; + state.serialize_field("port", &name.port)?; + state.end() + } + Relay::MultiHostName(name) => { + let mut state = serializer.serialize_struct("TxRelay", 1)?; + state.serialize_field("dns", &name.dns_name)?; + state.end() + } + } + } +} + struct TxPoolUpdateCertificate(TransactionPoolUpdateCertificate); impl Serialize for TxPoolUpdateCertificate { @@ -373,9 +403,24 @@ impl Serialize for TxPoolUpdateCertificate { state.serialize_field("pool_id", &self.0.pool_reg.operator.to_string())?; state.serialize_field("vrf_key", &self.0.pool_reg.vrf_key_hash.to_string())?; state.serialize_field("pledge", &self.0.pool_reg.pledge.to_string())?; - state.serialize_field("margin_cost", &self.0.pool_reg.margin)?; + state.serialize_field("margin_cost", &self.0.pool_reg.margin.to_f64())?; state.serialize_field("fixed_cost", &self.0.pool_reg.cost.to_string())?; state.serialize_field("reward_account", &reward_account)?; + state.serialize_field( + "owners", + &self + .0 + .pool_reg + .pool_owners + .iter() + .map(|o| o.to_string().unwrap_or("bad address".to_string())) + .collect::>(), + )?; + state.serialize_field("metadata", &self.0.pool_reg.pool_metadata)?; + state.serialize_field( + "relays", + &self.0.pool_reg.relays.clone().into_iter().map(TxRelay).collect::>(), + )?; state.end() } } From 6408af29b5fcb7d368619dae300a83d65fef8f72 Mon Sep 17 00:00:00 2001 From: Alex Woods Date: Fri, 14 Nov 2025 13:27:37 +0000 Subject: [PATCH 12/23] Add active epoch to txs pool_updates --- common/src/queries/transactions.rs | 1 + modules/chain_store/src/chain_store.rs | 2 ++ modules/rest_blockfrost/src/handlers/transactions.rs | 3 ++- 3 files changed, 5 insertions(+), 1 deletion(-) diff --git a/common/src/queries/transactions.rs b/common/src/queries/transactions.rs index 4d6c9b3f..20c5933b 100644 --- a/common/src/queries/transactions.rs +++ b/common/src/queries/transactions.rs @@ -158,6 +158,7 @@ pub struct TransactionMIRs { pub struct TransactionPoolUpdateCertificate { pub cert_index: u64, pub pool_reg: PoolRegistration, + pub active_epoch: u64, } #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] diff --git a/modules/chain_store/src/chain_store.rs b/modules/chain_store/src/chain_store.rs index 5907d3fc..068e8f0a 100644 --- a/modules/chain_store/src/chain_store.rs +++ b/modules/chain_store/src/chain_store.rs @@ -889,6 +889,7 @@ impl ChainStore { network_id.clone(), false, )?, + active_epoch: tx.block.extra.epoch + 1, }); } } @@ -920,6 +921,7 @@ impl ChainStore { network_id.clone(), false, )?, + active_epoch: tx.block.extra.epoch + 1, }); } } diff --git a/modules/rest_blockfrost/src/handlers/transactions.rs b/modules/rest_blockfrost/src/handlers/transactions.rs index 1295a441..1ceadb46 100644 --- a/modules/rest_blockfrost/src/handlers/transactions.rs +++ b/modules/rest_blockfrost/src/handlers/transactions.rs @@ -398,7 +398,7 @@ impl Serialize for TxPoolUpdateCertificate { let Ok(reward_account) = self.0.pool_reg.reward_account.to_string() else { return Err(S::Error::custom("Can't stringify reward account")); }; - let mut state = serializer.serialize_struct("TxPoolUpdateCertificate", 4)?; + let mut state = serializer.serialize_struct("TxPoolUpdateCertificate", 11)?; state.serialize_field("cert_index", &self.0.cert_index)?; state.serialize_field("pool_id", &self.0.pool_reg.operator.to_string())?; state.serialize_field("vrf_key", &self.0.pool_reg.vrf_key_hash.to_string())?; @@ -421,6 +421,7 @@ impl Serialize for TxPoolUpdateCertificate { "relays", &self.0.pool_reg.relays.clone().into_iter().map(TxRelay).collect::>(), )?; + state.serialize_field("active_epoch", &self.0.active_epoch)?; state.end() } } From b646955c2bf60522371ffdbc408dfa453290d6ec Mon Sep 17 00:00:00 2001 From: Alex Woods Date: Fri, 14 Nov 2025 14:26:02 +0000 Subject: [PATCH 13/23] Implement txs pool_retires endpoint --- common/src/queries/transactions.rs | 15 ++++- modules/chain_store/src/chain_store.rs | 65 +++++++++++++++++-- .../src/handlers/transactions.rs | 60 +++++++++++++++-- 3 files changed, 127 insertions(+), 13 deletions(-) diff --git a/common/src/queries/transactions.rs b/common/src/queries/transactions.rs index 20c5933b..69a00524 100644 --- a/common/src/queries/transactions.rs +++ b/common/src/queries/transactions.rs @@ -20,7 +20,7 @@ pub enum TransactionsStateQuery { GetTransactionWithdrawals { tx_hash: TxHash }, GetTransactionMIRs { tx_hash: TxHash }, GetTransactionPoolUpdateCertificates { tx_hash: TxHash }, - GetTransactionPoolRetirementCertificates, + GetTransactionPoolRetirementCertificates { tx_hash: TxHash }, GetTransactionMetadata, GetTransactionMetadataCBOR, GetTransactionRedeemers, @@ -118,7 +118,7 @@ pub struct TransactionStakeCertificates { pub struct TransactionDelegationCertificate { pub index: u64, pub address: StakeAddress, - pub pool: PoolId, + pub pool_id: PoolId, pub active_epoch: u64, } @@ -167,7 +167,16 @@ pub struct TransactionPoolUpdateCertificates { } #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] -pub struct TransactionPoolRetirementCertificates {} +pub struct TransactionPoolRetirementCertificate { + pub cert_index: u64, + pub pool_id: PoolId, + pub retirement_epoch: u64, +} + +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] +pub struct TransactionPoolRetirementCertificates { + pub pool_retirements: Vec, +} #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] pub struct TransactionMetadata {} diff --git a/modules/chain_store/src/chain_store.rs b/modules/chain_store/src/chain_store.rs index 068e8f0a..185413ad 100644 --- a/modules/chain_store/src/chain_store.rs +++ b/modules/chain_store/src/chain_store.rs @@ -11,10 +11,12 @@ use acropolis_common::{ messages::{CardanoMessage, Message, StateQuery, StateQueryResponse}, queries::transactions::{ TransactionDelegationCertificate, TransactionDelegationCertificates, TransactionInfo, - TransactionMIR, TransactionMIRs, TransactionOutputAmount, TransactionPoolUpdateCertificate, - TransactionPoolUpdateCertificates, TransactionStakeCertificate, - TransactionStakeCertificates, TransactionWithdrawal, TransactionWithdrawals, - TransactionsStateQuery, TransactionsStateQueryResponse, DEFAULT_TRANSACTIONS_QUERY_TOPIC, + TransactionMIR, TransactionMIRs, TransactionOutputAmount, + TransactionPoolRetirementCertificate, TransactionPoolRetirementCertificates, + TransactionPoolUpdateCertificate, TransactionPoolUpdateCertificates, + TransactionStakeCertificate, TransactionStakeCertificates, TransactionWithdrawal, + TransactionWithdrawals, TransactionsStateQuery, TransactionsStateQueryResponse, + DEFAULT_TRANSACTIONS_QUERY_TOPIC, }, queries::{ blocks::{ @@ -767,7 +769,7 @@ impl ChainStore { certs.push(TransactionDelegationCertificate { index: index as u64, address: map_stake_address(cred, network_id.clone()), - pool: to_pool_id(pool_key_hash), + pool_id: to_pool_id(pool_key_hash), active_epoch: tx.block.extra.epoch + 1, }); } @@ -779,7 +781,7 @@ impl ChainStore { certs.push(TransactionDelegationCertificate { index: index as u64, address: map_stake_address(cred, network_id.clone()), - pool: to_pool_id(pool_key_hash), + pool_id: to_pool_id(pool_key_hash), active_epoch: tx.block.extra.epoch + 1, }); } @@ -931,6 +933,43 @@ impl ChainStore { Ok(certs) } + fn to_tx_pool_retirements(tx: &Tx) -> Result> { + let block = pallas_traverse::MultiEraBlock::decode(&tx.block.bytes)?; + let txs = block.txs(); + let Some(tx_decoded) = txs.get(tx.index as usize) else { + return Err(anyhow!("Transaction not found in block for given index")); + }; + let mut certs = Vec::new(); + for (cert_index, cert) in tx_decoded.certs().iter().enumerate() { + match cert { + MultiEraCert::AlonzoCompatible(cert) => { + if let alonzo::Certificate::PoolRetirement(operator, epoch) = + cert.as_ref().as_ref() + { + certs.push(TransactionPoolRetirementCertificate { + cert_index: cert_index as u64, + pool_id: to_pool_id(operator), + retirement_epoch: *epoch, + }); + } + } + MultiEraCert::Conway(cert) => { + if let conway::Certificate::PoolRetirement(operator, epoch) = + cert.as_ref().as_ref() + { + certs.push(TransactionPoolRetirementCertificate { + cert_index: cert_index as u64, + pool_id: to_pool_id(operator), + retirement_epoch: *epoch, + }); + } + } + _ => (), + } + } + Ok(certs) + } + fn handle_txs_query( store: &Arc, query: &TransactionsStateQuery, @@ -1013,6 +1052,20 @@ impl ChainStore { ), ) } + TransactionsStateQuery::GetTransactionPoolRetirementCertificates { tx_hash } => { + let Some(tx) = store.get_tx_by_hash(tx_hash.as_ref())? else { + return Ok(TransactionsStateQueryResponse::Error( + QueryError::not_found("Transaction not found"), + )); + }; + Ok( + TransactionsStateQueryResponse::TransactionPoolRetirementCertificates( + TransactionPoolRetirementCertificates { + pool_retirements: Self::to_tx_pool_retirements(&tx)?, + }, + ), + ) + } _ => Ok(TransactionsStateQueryResponse::Error( QueryError::not_implemented("Unimplemented".to_string()), )), diff --git a/modules/rest_blockfrost/src/handlers/transactions.rs b/modules/rest_blockfrost/src/handlers/transactions.rs index 1ceadb46..3a6c0095 100644 --- a/modules/rest_blockfrost/src/handlers/transactions.rs +++ b/modules/rest_blockfrost/src/handlers/transactions.rs @@ -8,8 +8,9 @@ use acropolis_common::{ parameters::{ParametersStateQuery, ParametersStateQueryResponse}, transactions::{ TransactionDelegationCertificate, TransactionInfo, TransactionMIR, - TransactionPoolUpdateCertificate, TransactionStakeCertificate, TransactionWithdrawal, - TransactionsStateQuery, TransactionsStateQueryResponse, + TransactionPoolRetirementCertificate, TransactionPoolUpdateCertificate, + TransactionStakeCertificate, TransactionWithdrawal, TransactionsStateQuery, + TransactionsStateQueryResponse, }, utils::{query_state, rest_query_state_async}, }, @@ -96,7 +97,9 @@ pub async fn handle_transactions_blockfrost( Some("pool_updates") => { handle_transaction_pool_updates_query(context, tx_hash, handlers_config).await } - Some("pool_retires") => Ok(RESTResponse::with_text(501, "Not implemented")), + Some("pool_retires") => { + handle_transaction_pool_retires_query(context, tx_hash, handlers_config).await + } Some("metadata") => match param2 { None => Ok(RESTResponse::with_text(501, "Not implemented")), Some("cbor") => Ok(RESTResponse::with_text(501, "Not implemented")), @@ -232,7 +235,7 @@ impl Serialize for TxDelegation { let mut state = serializer.serialize_struct("TxDelegation", 4)?; state.serialize_field("index", &self.0.index)?; state.serialize_field("address", &address)?; - state.serialize_field("pool_id", &self.0.pool.to_string())?; + state.serialize_field("pool_id", &self.0.pool_id.to_string())?; state.serialize_field("active_epoch", &self.0.active_epoch)?; state.end() } @@ -457,3 +460,52 @@ async fn handle_transaction_pool_updates_query( ) .await } + +struct TxPoolRetirementCertificate(TransactionPoolRetirementCertificate); + +impl Serialize for TxPoolRetirementCertificate { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + let mut state = serializer.serialize_struct("TxPoolUpdateCertificate", 3)?; + state.serialize_field("cert_index", &self.0.cert_index)?; + state.serialize_field("pool_id", &self.0.pool_id.to_string())?; + state.serialize_field("retirement_epoch", &self.0.retirement_epoch)?; + state.end() + } +} + +/// Handle `/txs/{hash}/pool_retires` +async fn handle_transaction_pool_retires_query( + context: Arc>, + tx_hash: TxHash, + handlers_config: Arc, +) -> Result { + let txs_info_msg = Arc::new(Message::StateQuery(StateQuery::Transactions( + TransactionsStateQuery::GetTransactionPoolRetirementCertificates { tx_hash }, + ))); + rest_query_state_async( + &context.clone(), + &handlers_config.transactions_query_topic.clone(), + txs_info_msg, + async move |message| match message { + Message::StateQueryResponse(StateQueryResponse::Transactions( + TransactionsStateQueryResponse::TransactionPoolRetirementCertificates( + pool_retirements, + ), + )) => Some(Ok(Some( + pool_retirements + .pool_retirements + .into_iter() + .map(TxPoolRetirementCertificate) + .collect::>(), + ))), + Message::StateQueryResponse(StateQueryResponse::Transactions( + TransactionsStateQueryResponse::Error(e), + )) => Some(Err(e)), + _ => None, + }, + ) + .await +} From 9324b64b7659ca36d52af4f5ecba219c963e3244 Mon Sep 17 00:00:00 2001 From: Alex Woods Date: Fri, 14 Nov 2025 18:44:51 +0000 Subject: [PATCH 14/23] WIP txs metadata endpoint --- Cargo.lock | 29 ++--------- Cargo.toml | 1 + codec/src/map_parameters.rs | 16 ++++++ common/Cargo.toml | 2 +- common/src/lib.rs | 2 + common/src/metadata.rs | 33 +++++++++++++ common/src/queries/transactions.rs | 16 ++++-- modules/address_state/Cargo.toml | 2 +- modules/chain_store/Cargo.toml | 2 +- modules/chain_store/src/chain_store.rs | 49 ++++++++++++++++--- modules/historical_accounts_state/Cargo.toml | 2 +- .../src/handlers/transactions.rs | 34 +++++++++++-- 12 files changed, 143 insertions(+), 45 deletions(-) create mode 100644 common/src/metadata.rs diff --git a/Cargo.lock b/Cargo.lock index 8e8b355b..e408b0d9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -45,7 +45,7 @@ dependencies = [ "futures", "hex", "memmap2", - "minicbor 0.26.5", + "minicbor 0.25.1", "num-rational", "num-traits", "rayon", @@ -90,7 +90,7 @@ dependencies = [ "caryatid_sdk", "config", "fjall", - "minicbor 0.26.5", + "minicbor 0.25.1", "tempfile", "tokio", "tracing", @@ -156,7 +156,7 @@ dependencies = [ "config", "fjall", "hex", - "minicbor 0.26.5", + "minicbor 0.25.1", "pallas 0.33.0", "pallas-traverse 0.33.0", "tempfile", @@ -267,7 +267,7 @@ dependencies = [ "config", "fjall", "hex", - "minicbor 0.26.5", + "minicbor 0.25.1", "rayon", "tokio", "tracing", @@ -3575,16 +3575,6 @@ dependencies = [ "minicbor-derive 0.15.3", ] -[[package]] -name = "minicbor" -version = "0.26.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a309f581ade7597820083bc275075c4c6986e57e53f8d26f88507cfefc8c987" -dependencies = [ - "half 2.7.1", - "minicbor-derive 0.16.2", -] - [[package]] name = "minicbor" version = "2.1.1" @@ -3605,17 +3595,6 @@ dependencies = [ "syn 2.0.109", ] -[[package]] -name = "minicbor-derive" -version = "0.16.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a9882ef5c56df184b8ffc107fc6c61e33ee3a654b021961d790a78571bb9d67a" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.109", -] - [[package]] name = "minicbor-derive" version = "0.18.2" diff --git a/Cargo.toml b/Cargo.toml index 1f875661..f61c8ef0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -51,6 +51,7 @@ config = "0.15.11" dashmap = "6.1.0" hex = "0.4" imbl = { version = "5.0.0", features = ["serde"] } +minicbor = { version = "0.25.1", features = ["alloc", "std", "derive"] } opentelemetry = { version = "0.30.0", features = ["trace"] } opentelemetry-otlp = { version = "0.30.0", features = ["grpc-tonic", "trace", "tls"] } opentelemetry_sdk = { version = "0.30.0", features = ["rt-tokio"] } diff --git a/codec/src/map_parameters.rs b/codec/src/map_parameters.rs index 41f73efb..092c1f39 100644 --- a/codec/src/map_parameters.rs +++ b/codec/src/map_parameters.rs @@ -1136,3 +1136,19 @@ pub fn map_reference_script(script: &Option) -> Option< None => None, } } + +pub fn map_metadata(metadata: &pallas_primitives::Metadatum) -> Metadata { + match metadata { + pallas_primitives::Metadatum::Int(pallas_primitives::Int(i)) => { + Metadata::Int(MetadataInt(*i)) + } + pallas_primitives::Metadatum::Bytes(b) => Metadata::Bytes(b.to_vec()), + pallas_primitives::Metadatum::Text(s) => Metadata::Text(s.clone()), + pallas_primitives::Metadatum::Array(a) => { + Metadata::Array(a.iter().map(map_metadata).collect()) + } + pallas_primitives::Metadatum::Map(m) => { + Metadata::Map(m.iter().map(|(k, v)| (map_metadata(&k), map_metadata(&v))).collect()) + } + } +} diff --git a/common/Cargo.toml b/common/Cargo.toml index 4f44f198..a52c93cb 100644 --- a/common/Cargo.toml +++ b/common/Cargo.toml @@ -33,7 +33,7 @@ tempfile = "3" tokio = { workspace = true } tracing = { workspace = true } futures = "0.3.31" -minicbor = { version = "0.26.0", features = ["std", "half", "derive"] } +minicbor = { workspace = true, features = ["std", "half", "derive"] } num-traits = "0.2" dashmap = { workspace = true } rayon = "1.11.0" diff --git a/common/src/lib.rs b/common/src/lib.rs index 57889a9e..a6518b04 100644 --- a/common/src/lib.rs +++ b/common/src/lib.rs @@ -10,6 +10,7 @@ pub mod hash; pub mod ledger_state; pub mod math; pub mod messages; +pub mod metadata; pub mod params; pub mod protocol_params; pub mod queries; @@ -27,4 +28,5 @@ pub mod validation; // Flattened re-exports pub use self::address::*; +pub use self::metadata::*; pub use self::types::*; diff --git a/common/src/metadata.rs b/common/src/metadata.rs new file mode 100644 index 00000000..240670b8 --- /dev/null +++ b/common/src/metadata.rs @@ -0,0 +1,33 @@ +use minicbor::data::Int; +use serde::{Deserialize, Deserializer, Serialize, Serializer}; + +#[derive(Debug, Clone)] +pub struct MetadataInt(pub Int); + +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] +pub enum Metadata { + Int(MetadataInt), + Bytes(Vec), + Text(String), + Array(Vec), + Map(Vec<(Metadata, Metadata)>), +} + +impl Serialize for MetadataInt { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + serializer.serialize_i128(self.0.into()) + } +} + +impl<'a> Deserialize<'a> for MetadataInt { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'a>, + { + // TODO if this is ever used, i64 may not be enough! + Ok(MetadataInt(Int::from(i64::deserialize(deserializer)?))) + } +} diff --git a/common/src/queries/transactions.rs b/common/src/queries/transactions.rs index 69a00524..6f2b7980 100644 --- a/common/src/queries/transactions.rs +++ b/common/src/queries/transactions.rs @@ -1,6 +1,6 @@ use crate::{ - BlockHash, InstantaneousRewardSource, Lovelace, NativeAsset, PoolId, PoolRegistration, - StakeAddress, TxHash, + BlockHash, InstantaneousRewardSource, Lovelace, Metadata, NativeAsset, PoolId, + PoolRegistration, StakeAddress, TxHash, }; use serde::ser::{Serialize, SerializeStruct, Serializer}; use serde_with::serde_as; @@ -21,7 +21,7 @@ pub enum TransactionsStateQuery { GetTransactionMIRs { tx_hash: TxHash }, GetTransactionPoolUpdateCertificates { tx_hash: TxHash }, GetTransactionPoolRetirementCertificates { tx_hash: TxHash }, - GetTransactionMetadata, + GetTransactionMetadata { tx_hash: TxHash }, GetTransactionMetadataCBOR, GetTransactionRedeemers, GetTransactionRequiredSigners, @@ -179,7 +179,15 @@ pub struct TransactionPoolRetirementCertificates { } #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] -pub struct TransactionMetadata {} +pub struct TransactionMetadataItem { + pub label: String, + pub json_metadata: Metadata, +} + +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] +pub struct TransactionMetadata { + pub metadata: Vec, +} #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] pub struct TransactionMetadataCBOR {} diff --git a/modules/address_state/Cargo.toml b/modules/address_state/Cargo.toml index 980ed609..762c2b33 100644 --- a/modules/address_state/Cargo.toml +++ b/modules/address_state/Cargo.toml @@ -16,7 +16,7 @@ caryatid_sdk = { workspace = true } anyhow = { workspace = true } config = { workspace = true } fjall = "2.7.0" -minicbor = { version = "0.26.0", features = ["std", "derive"] } +minicbor = { workspace = true, features = ["std", "derive"] } tokio = { workspace = true } tracing = { workspace = true } diff --git a/modules/chain_store/Cargo.toml b/modules/chain_store/Cargo.toml index e8310c96..54af6fe6 100644 --- a/modules/chain_store/Cargo.toml +++ b/modules/chain_store/Cargo.toml @@ -14,7 +14,7 @@ anyhow = "1.0" config = "0.15.11" fjall = "2.7.0" hex = "0.4" -minicbor = { version = "0.26.0", features = ["std", "half", "derive"] } +minicbor = { workspace = true, features = ["std", "half", "derive"] } pallas-traverse = { workspace = true } tracing = "0.1.40" tokio.workspace = true diff --git a/modules/chain_store/src/chain_store.rs b/modules/chain_store/src/chain_store.rs index 185413ad..f0714cc0 100644 --- a/modules/chain_store/src/chain_store.rs +++ b/modules/chain_store/src/chain_store.rs @@ -3,7 +3,7 @@ mod stores; use acropolis_codec::{ block::map_to_block_issuer, map_parameters, - map_parameters::{map_stake_address, to_pool_id, to_pool_reg}, + map_parameters::{map_metadata, map_stake_address, to_pool_id, to_pool_reg}, }; use acropolis_common::queries::errors::QueryError; use acropolis_common::{ @@ -11,12 +11,12 @@ use acropolis_common::{ messages::{CardanoMessage, Message, StateQuery, StateQueryResponse}, queries::transactions::{ TransactionDelegationCertificate, TransactionDelegationCertificates, TransactionInfo, - TransactionMIR, TransactionMIRs, TransactionOutputAmount, - TransactionPoolRetirementCertificate, TransactionPoolRetirementCertificates, - TransactionPoolUpdateCertificate, TransactionPoolUpdateCertificates, - TransactionStakeCertificate, TransactionStakeCertificates, TransactionWithdrawal, - TransactionWithdrawals, TransactionsStateQuery, TransactionsStateQueryResponse, - DEFAULT_TRANSACTIONS_QUERY_TOPIC, + TransactionMIR, TransactionMIRs, TransactionMetadata, TransactionMetadataItem, + TransactionOutputAmount, TransactionPoolRetirementCertificate, + TransactionPoolRetirementCertificates, TransactionPoolUpdateCertificate, + TransactionPoolUpdateCertificates, TransactionStakeCertificate, + TransactionStakeCertificates, TransactionWithdrawal, TransactionWithdrawals, + TransactionsStateQuery, TransactionsStateQueryResponse, DEFAULT_TRANSACTIONS_QUERY_TOPIC, }, queries::{ blocks::{ @@ -35,7 +35,7 @@ use anyhow::{anyhow, bail, Result}; use caryatid_sdk::{module, Context, Module}; use config::Config; use pallas::ledger::primitives::{alonzo, conway}; -use pallas_traverse::MultiEraCert; +use pallas_traverse::{MultiEraCert, MultiEraMeta}; use std::collections::{BTreeMap, HashMap}; use std::sync::Arc; use tokio::sync::Mutex; @@ -970,6 +970,27 @@ impl ChainStore { Ok(certs) } + fn to_tx_metadata(tx: &Tx) -> Result> { + let block = pallas_traverse::MultiEraBlock::decode(&tx.block.bytes)?; + let txs = block.txs(); + let Some(tx_decoded) = txs.get(tx.index as usize) else { + return Err(anyhow!("Transaction not found in block for given index")); + }; + let mut items = Vec::new(); + match tx_decoded.metadata() { + MultiEraMeta::AlonzoCompatible(metadata) => { + for (label, datum) in &metadata.clone().to_vec() { + items.push(TransactionMetadataItem { + label: label.to_string(), + json_metadata: map_metadata(&datum), + }); + } + } + _ => (), + } + Ok(items) + } + fn handle_txs_query( store: &Arc, query: &TransactionsStateQuery, @@ -1066,6 +1087,18 @@ impl ChainStore { ), ) } + TransactionsStateQuery::GetTransactionMetadata { tx_hash } => { + let Some(tx) = store.get_tx_by_hash(tx_hash.as_ref())? else { + return Ok(TransactionsStateQueryResponse::Error( + QueryError::not_found("Transaction not found"), + )); + }; + Ok(TransactionsStateQueryResponse::TransactionMetadata( + TransactionMetadata { + metadata: Self::to_tx_metadata(&tx)?, + }, + )) + } _ => Ok(TransactionsStateQueryResponse::Error( QueryError::not_implemented("Unimplemented".to_string()), )), diff --git a/modules/historical_accounts_state/Cargo.toml b/modules/historical_accounts_state/Cargo.toml index 930c4713..8027ad47 100644 --- a/modules/historical_accounts_state/Cargo.toml +++ b/modules/historical_accounts_state/Cargo.toml @@ -15,7 +15,7 @@ caryatid_sdk = { workspace = true } anyhow = { workspace = true } config = { workspace = true } -minicbor = { version = "0.26.0", features = ["std", "derive"] } +minicbor = { workspace = true, features = ["std", "derive"] } hex = { workspace = true } tokio = { workspace = true } tracing = { workspace = true } diff --git a/modules/rest_blockfrost/src/handlers/transactions.rs b/modules/rest_blockfrost/src/handlers/transactions.rs index 3a6c0095..433f2a49 100644 --- a/modules/rest_blockfrost/src/handlers/transactions.rs +++ b/modules/rest_blockfrost/src/handlers/transactions.rs @@ -8,9 +8,9 @@ use acropolis_common::{ parameters::{ParametersStateQuery, ParametersStateQueryResponse}, transactions::{ TransactionDelegationCertificate, TransactionInfo, TransactionMIR, - TransactionPoolRetirementCertificate, TransactionPoolUpdateCertificate, - TransactionStakeCertificate, TransactionWithdrawal, TransactionsStateQuery, - TransactionsStateQueryResponse, + TransactionMetadataItem, TransactionPoolRetirementCertificate, + TransactionPoolUpdateCertificate, TransactionStakeCertificate, TransactionWithdrawal, + TransactionsStateQuery, TransactionsStateQueryResponse, }, utils::{query_state, rest_query_state_async}, }, @@ -101,7 +101,7 @@ pub async fn handle_transactions_blockfrost( handle_transaction_pool_retires_query(context, tx_hash, handlers_config).await } Some("metadata") => match param2 { - None => Ok(RESTResponse::with_text(501, "Not implemented")), + None => handle_transaction_metadata_query(context, tx_hash, handlers_config).await, Some("cbor") => Ok(RESTResponse::with_text(501, "Not implemented")), _ => Ok(RESTResponse::with_text(400, "Invalid parameters")), }, @@ -509,3 +509,29 @@ async fn handle_transaction_pool_retires_query( ) .await } + +/// Handle `/txs/{hash}/metadata` +async fn handle_transaction_metadata_query( + context: Arc>, + tx_hash: TxHash, + handlers_config: Arc, +) -> Result { + let txs_info_msg = Arc::new(Message::StateQuery(StateQuery::Transactions( + TransactionsStateQuery::GetTransactionMetadata { tx_hash }, + ))); + rest_query_state_async( + &context.clone(), + &handlers_config.transactions_query_topic.clone(), + txs_info_msg, + async move |message| match message { + Message::StateQueryResponse(StateQueryResponse::Transactions( + TransactionsStateQueryResponse::TransactionMetadata(metadata), + )) => Some(Ok(Some(metadata.metadata))), + Message::StateQueryResponse(StateQueryResponse::Transactions( + TransactionsStateQueryResponse::Error(e), + )) => Some(Err(e)), + _ => None, + }, + ) + .await +} From 98923033137aa69ec5f839342a5be4745b05d9ab Mon Sep 17 00:00:00 2001 From: Alex Woods Date: Wed, 19 Nov 2025 15:36:49 +0000 Subject: [PATCH 15/23] Add serialization for metadata in txs metadata endpoint --- common/src/metadata.rs | 6 ++ .../src/handlers/transactions.rs | 63 ++++++++++++++++++- 2 files changed, 66 insertions(+), 3 deletions(-) diff --git a/common/src/metadata.rs b/common/src/metadata.rs index 240670b8..4996ca9f 100644 --- a/common/src/metadata.rs +++ b/common/src/metadata.rs @@ -22,6 +22,12 @@ impl Serialize for MetadataInt { } } +impl ToString for MetadataInt { + fn to_string(&self) -> String { + self.0.to_string() + } +} + impl<'a> Deserialize<'a> for MetadataInt { fn deserialize(deserializer: D) -> Result where diff --git a/modules/rest_blockfrost/src/handlers/transactions.rs b/modules/rest_blockfrost/src/handlers/transactions.rs index 433f2a49..951b736e 100644 --- a/modules/rest_blockfrost/src/handlers/transactions.rs +++ b/modules/rest_blockfrost/src/handlers/transactions.rs @@ -14,12 +14,12 @@ use acropolis_common::{ }, utils::{query_state, rest_query_state_async}, }, - Lovelace, Relay, TxHash, + Lovelace, Metadata, Relay, TxHash, }; use caryatid_sdk::Context; use hex::FromHex; use serde::{ - ser::{Error, SerializeStruct}, + ser::{Error, SerializeMap, SerializeSeq, SerializeStruct}, Serialize, Serializer, }; use std::sync::Arc; @@ -510,6 +510,61 @@ async fn handle_transaction_pool_retires_query( .await } +struct TxMetadata(Metadata); + +impl Serialize for TxMetadata { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + match &self.0 { + Metadata::Int(i) => i.serialize(serializer), + Metadata::Bytes(b) => { + let h = hex::encode(b); + serializer.serialize_str(&h) + } + Metadata::Text(s) => s.serialize(serializer), + Metadata::Array(a) => { + let mut state = serializer.serialize_seq(Some(a.len()))?; + for i in a { + state.serialize_element(&TxMetadata(i.clone()))?; + } + state.end() + } + Metadata::Map(m) => { + let mut state = serializer.serialize_map(Some(m.len()))?; + for (k, v) in m { + match k { + Metadata::Int(i) => { + state.serialize_entry(&i.to_string(), &TxMetadata(v.clone()))? + } + Metadata::Bytes(b) => { + state.serialize_entry(&hex::encode(b), &TxMetadata(v.clone()))? + } + Metadata::Text(s) => state.serialize_entry(&s, &TxMetadata(v.clone()))?, + _ => return Err(S::Error::custom("Invalid key type in map")), + } + } + state.end() + } + } + } +} + +struct TxMetadataItem(TransactionMetadataItem); + +impl Serialize for TxMetadataItem { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + let mut state = serializer.serialize_struct("TxPoolUpdateCertificate", 2)?; + state.serialize_field("label", &self.0.label)?; + state.serialize_field("json_metadata", &TxMetadata(self.0.json_metadata.clone()))?; + state.end() + } +} + /// Handle `/txs/{hash}/metadata` async fn handle_transaction_metadata_query( context: Arc>, @@ -526,7 +581,9 @@ async fn handle_transaction_metadata_query( async move |message| match message { Message::StateQueryResponse(StateQueryResponse::Transactions( TransactionsStateQueryResponse::TransactionMetadata(metadata), - )) => Some(Ok(Some(metadata.metadata))), + )) => Some(Ok(Some( + metadata.metadata.into_iter().map(|i| TxMetadataItem(i)).collect::>(), + ))), Message::StateQueryResponse(StateQueryResponse::Transactions( TransactionsStateQueryResponse::Error(e), )) => Some(Err(e)), From 49c7782851e74d3ac0a9a1bf86e58f1369516cdc Mon Sep 17 00:00:00 2001 From: Alex Woods Date: Wed, 19 Nov 2025 18:25:59 +0000 Subject: [PATCH 16/23] Code tidy ups --- codec/src/map_parameters.rs | 8 +-- common/src/metadata.rs | 7 +- common/src/queries/transactions.rs | 11 +++- modules/chain_store/src/chain_store.rs | 64 +++++++++---------- .../src/handlers/transactions.rs | 8 +-- modules/rest_blockfrost/src/types.rs | 10 +-- 6 files changed, 52 insertions(+), 56 deletions(-) diff --git a/codec/src/map_parameters.rs b/codec/src/map_parameters.rs index 092c1f39..150609d3 100644 --- a/codec/src/map_parameters.rs +++ b/codec/src/map_parameters.rs @@ -239,7 +239,7 @@ pub fn map_relay(relay: &PallasRelay) -> Relay { // // Certificates // - +#[allow(clippy::too_many_arguments)] pub fn to_pool_reg( operator: &pallas_primitives::PoolKeyhash, vrf_keyhash: &pallas_primitives::VrfKeyhash, @@ -247,8 +247,8 @@ pub fn to_pool_reg( cost: &pallas_primitives::Coin, margin: &pallas_primitives::UnitInterval, reward_account: &pallas_primitives::RewardAccount, - pool_owners: &Vec, - relays: &Vec, + pool_owners: &[pallas_primitives::AddrKeyhash], + relays: &[pallas_primitives::Relay], pool_metadata: &Nullable, network_id: NetworkId, force_reward_network_id: bool, @@ -1148,7 +1148,7 @@ pub fn map_metadata(metadata: &pallas_primitives::Metadatum) -> Metadata { Metadata::Array(a.iter().map(map_metadata).collect()) } pallas_primitives::Metadatum::Map(m) => { - Metadata::Map(m.iter().map(|(k, v)| (map_metadata(&k), map_metadata(&v))).collect()) + Metadata::Map(m.iter().map(|(k, v)| (map_metadata(k), map_metadata(v))).collect()) } } } diff --git a/common/src/metadata.rs b/common/src/metadata.rs index 4996ca9f..62ba4ef0 100644 --- a/common/src/metadata.rs +++ b/common/src/metadata.rs @@ -1,5 +1,6 @@ use minicbor::data::Int; use serde::{Deserialize, Deserializer, Serialize, Serializer}; +use std::fmt; #[derive(Debug, Clone)] pub struct MetadataInt(pub Int); @@ -22,9 +23,9 @@ impl Serialize for MetadataInt { } } -impl ToString for MetadataInt { - fn to_string(&self) -> String { - self.0.to_string() +impl fmt::Display for MetadataInt { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "{}", self.0) } } diff --git a/common/src/queries/transactions.rs b/common/src/queries/transactions.rs index 6f2b7980..5c7ebb0f 100644 --- a/common/src/queries/transactions.rs +++ b/common/src/queries/transactions.rs @@ -28,6 +28,7 @@ pub enum TransactionsStateQuery { GetTransactionCBOR, } +#[allow(clippy::large_enum_variant)] #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] pub enum TransactionsStateQueryResponse { TransactionInfo(TransactionInfo), @@ -190,7 +191,15 @@ pub struct TransactionMetadata { } #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] -pub struct TransactionMetadataCBOR {} +pub struct TransactionMetadataItemCBOR { + pub label: String, + pub metadata: Vec, +} + +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] +pub struct TransactionMetadataCBOR { + pub metadata: Vec, +} #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] pub struct TransactionRedeemers {} diff --git a/modules/chain_store/src/chain_store.rs b/modules/chain_store/src/chain_store.rs index 27969677..701a0b83 100644 --- a/modules/chain_store/src/chain_store.rs +++ b/modules/chain_store/src/chain_store.rs @@ -962,36 +962,33 @@ impl ChainStore { }; let mut certs = Vec::new(); for (cert_index, cert) in tx_decoded.certs().iter().enumerate() { - match cert { - MultiEraCert::AlonzoCompatible(cert) => { - if let alonzo::Certificate::MoveInstantaneousRewardsCert(cert) = - cert.as_ref().as_ref() - { - match &cert.target { - alonzo::InstantaneousRewardTarget::StakeCredentials(creds) => { - for (cred, amount) in creds.clone().to_vec() { - certs.push(TransactionMIR { - cert_index: cert_index as u64, - pot: match cert.source { - alonzo::InstantaneousRewardSource::Reserves => { - InstantaneousRewardSource::Reserves - } - alonzo::InstantaneousRewardSource::Treasury => { - InstantaneousRewardSource::Treasury - } - }, - address: map_stake_address(&cred, network_id.clone()), - amount: amount as u64, - }); - } - } - alonzo::InstantaneousRewardTarget::OtherAccountingPot(coin) => { - // TODO + if let MultiEraCert::AlonzoCompatible(cert) = cert { + if let alonzo::Certificate::MoveInstantaneousRewardsCert(cert) = + cert.as_ref().as_ref() + { + match &cert.target { + alonzo::InstantaneousRewardTarget::StakeCredentials(creds) => { + for (cred, amount) in creds.clone().to_vec() { + certs.push(TransactionMIR { + cert_index: cert_index as u64, + pot: match cert.source { + alonzo::InstantaneousRewardSource::Reserves => { + InstantaneousRewardSource::Reserves + } + alonzo::InstantaneousRewardSource::Treasury => { + InstantaneousRewardSource::Treasury + } + }, + address: map_stake_address(&cred, network_id.clone()), + amount: amount as u64, + }); } } + alonzo::InstantaneousRewardTarget::OtherAccountingPot(_coin) => { + // TODO + } } } - _ => (), } } Ok(certs) @@ -1123,16 +1120,13 @@ impl ChainStore { return Err(anyhow!("Transaction not found in block for given index")); }; let mut items = Vec::new(); - match tx_decoded.metadata() { - MultiEraMeta::AlonzoCompatible(metadata) => { - for (label, datum) in &metadata.clone().to_vec() { - items.push(TransactionMetadataItem { - label: label.to_string(), - json_metadata: map_metadata(&datum), - }); - } + if let MultiEraMeta::AlonzoCompatible(metadata) = tx_decoded.metadata() { + for (label, datum) in &metadata.clone().to_vec() { + items.push(TransactionMetadataItem { + label: label.to_string(), + json_metadata: map_metadata(datum), + }); } - _ => (), } Ok(items) } diff --git a/modules/rest_blockfrost/src/handlers/transactions.rs b/modules/rest_blockfrost/src/handlers/transactions.rs index 951b736e..a70cf2a1 100644 --- a/modules/rest_blockfrost/src/handlers/transactions.rs +++ b/modules/rest_blockfrost/src/handlers/transactions.rs @@ -153,10 +153,8 @@ async fn handle_transaction_query( Ok(params) => params, Err(e) => return Some(Err(e)), }; - let fee = match txs_info.recorded_fee { - Some(fee) => fee, - None => 0, // TODO: calc from outputs and inputs - }; + // TODO: calc from outputs and inputs if recorded_fee is None + let fee = txs_info.recorded_fee.unwrap_or_default(); let deposit = match calculate_deposit( txs_info.pool_update_count, txs_info.stake_cert_count, @@ -582,7 +580,7 @@ async fn handle_transaction_metadata_query( Message::StateQueryResponse(StateQueryResponse::Transactions( TransactionsStateQueryResponse::TransactionMetadata(metadata), )) => Some(Ok(Some( - metadata.metadata.into_iter().map(|i| TxMetadataItem(i)).collect::>(), + metadata.metadata.into_iter().map(TxMetadataItem).collect::>(), ))), Message::StateQueryResponse(StateQueryResponse::Transactions( TransactionsStateQueryResponse::Error(e), diff --git a/modules/rest_blockfrost/src/types.rs b/modules/rest_blockfrost/src/types.rs index 405a798c..3ddf1d04 100644 --- a/modules/rest_blockfrost/src/types.rs +++ b/modules/rest_blockfrost/src/types.rs @@ -375,14 +375,8 @@ impl From for PoolRelayRest { match value { Relay::SingleHostAddr(s) => PoolRelayRest { - ipv4: s.ipv4.map(|bytes| { - let ipv4_addr = std::net::Ipv4Addr::from(bytes); - format!("{:?}", ipv4_addr) - }), - ipv6: s.ipv6.map(|bytes| { - let ipv6_addr = std::net::Ipv6Addr::from(bytes); - format!("{:?}", ipv6_addr) - }), + ipv4: s.ipv4.map(|addr| format!("{:?}", addr)), + ipv6: s.ipv6.map(|addr| format!("{:?}", addr)), dns: None, dns_srv: None, port: s.port.unwrap_or(default_port), From 3a9d651473def62c1074680c505bf0046430ba9a Mon Sep 17 00:00:00 2001 From: Alex Woods Date: Fri, 21 Nov 2025 09:59:21 +0000 Subject: [PATCH 17/23] Field naming, order and omission corrections --- common/src/queries/transactions.rs | 4 +-- .../src/handlers/transactions.rs | 28 +++++++++++++------ 2 files changed, 22 insertions(+), 10 deletions(-) diff --git a/common/src/queries/transactions.rs b/common/src/queries/transactions.rs index 5c7ebb0f..cf3cbd32 100644 --- a/common/src/queries/transactions.rs +++ b/common/src/queries/transactions.rs @@ -62,11 +62,11 @@ impl Serialize for TransactionOutputAmount { match self { TransactionOutputAmount::Lovelace(lovelace) => { state.serialize_field("unit", "lovelace")?; - state.serialize_field("amount", &lovelace.to_string())?; + state.serialize_field("quantity", &lovelace.to_string())?; } TransactionOutputAmount::Asset(asset) => { state.serialize_field("unit", &asset.name)?; - state.serialize_field("amount", &asset.amount.to_string())?; + state.serialize_field("quantity", &asset.amount.to_string())?; } } state.end() diff --git a/modules/rest_blockfrost/src/handlers/transactions.rs b/modules/rest_blockfrost/src/handlers/transactions.rs index a70cf2a1..baabb7f9 100644 --- a/modules/rest_blockfrost/src/handlers/transactions.rs +++ b/modules/rest_blockfrost/src/handlers/transactions.rs @@ -22,7 +22,10 @@ use serde::{ ser::{Error, SerializeMap, SerializeSeq, SerializeStruct}, Serialize, Serializer, }; -use std::sync::Arc; +use std::{ + net::{Ipv4Addr, Ipv6Addr}, + sync::Arc, +}; use crate::handlers_config::HandlersConfig; @@ -36,8 +39,8 @@ impl Serialize for TxInfo { let mut state = serializer.serialize_struct("TxInfo", 22)?; state.serialize_field("hash", &self.0.hash)?; state.serialize_field("block", &self.0.block_hash)?; - state.serialize_field("height", &self.0.block_number)?; - state.serialize_field("time", &self.0.block_time)?; + state.serialize_field("block_height", &self.0.block_number)?; + state.serialize_field("block_time", &self.0.block_time)?; state.serialize_field("slot", &self.0.slot)?; state.serialize_field("index", &self.0.index)?; state.serialize_field("output_amount", &self.0.output_amounts)?; @@ -45,7 +48,7 @@ impl Serialize for TxInfo { state.serialize_field("deposit", &self.2.to_string())?; state.serialize_field("size", &self.0.size)?; state.serialize_field("invalid_before", &self.0.invalid_before)?; - state.serialize_field("invalid_after", &self.0.invalid_after)?; + state.serialize_field("invalid_hereafter", &self.0.invalid_after)?; state.serialize_field("utxo_count", &self.0.utxo_count)?; state.serialize_field("withdrawal_count", &self.0.withdrawal_count)?; state.serialize_field("mir_cert_count", &self.0.mir_cert_count)?; @@ -185,7 +188,7 @@ impl Serialize for TxStake { return Err(S::Error::custom("Can't stringify address")); }; let mut state = serializer.serialize_struct("TxStake", 3)?; - state.serialize_field("index", &self.0.index)?; + state.serialize_field("cert_index", &self.0.index)?; state.serialize_field("address", &address)?; state.serialize_field("registration", &self.0.registration)?; state.end() @@ -231,7 +234,7 @@ impl Serialize for TxDelegation { return Err(S::Error::custom("Can't stringify address")); }; let mut state = serializer.serialize_struct("TxDelegation", 4)?; - state.serialize_field("index", &self.0.index)?; + state.serialize_field("cert_index", &self.0.index)?; state.serialize_field("address", &address)?; state.serialize_field("pool_id", &self.0.pool_id.to_string())?; state.serialize_field("active_epoch", &self.0.active_epoch)?; @@ -323,8 +326,8 @@ impl Serialize for TxMIR { return Err(S::Error::custom("Can't stringify address")); }; let mut state = serializer.serialize_struct("TxMIR", 4)?; - state.serialize_field("cert_index", &self.0.cert_index)?; state.serialize_field("pot", &self.0.pot.to_string().to_lowercase())?; + state.serialize_field("cert_index", &self.0.cert_index)?; state.serialize_field("address", &address)?; state.serialize_field("amount", &self.0.amount.to_string())?; state.end() @@ -371,18 +374,27 @@ impl Serialize for TxRelay { let mut state = serializer.serialize_struct("TxRelay", 3)?; state.serialize_field("ipv4", &addr.ipv4)?; state.serialize_field("ipv6", &addr.ipv6)?; + state.serialize_field("dns", &None::)?; + state.serialize_field("dns_srv", &None::)?; state.serialize_field("port", &addr.port)?; state.end() } Relay::SingleHostName(name) => { let mut state = serializer.serialize_struct("TxRelay", 2)?; + state.serialize_field("ipv4", &None::)?; + state.serialize_field("ipv6", &None::)?; state.serialize_field("dns", &name.dns_name)?; + state.serialize_field("dns_srv", &None::)?; state.serialize_field("port", &name.port)?; state.end() } Relay::MultiHostName(name) => { let mut state = serializer.serialize_struct("TxRelay", 1)?; + state.serialize_field("ipv4", &None::)?; + state.serialize_field("ipv6", &None::)?; state.serialize_field("dns", &name.dns_name)?; + state.serialize_field("dns_srv", &None::)?; + state.serialize_field("port", &None::)?; state.end() } } @@ -469,7 +481,7 @@ impl Serialize for TxPoolRetirementCertificate { let mut state = serializer.serialize_struct("TxPoolUpdateCertificate", 3)?; state.serialize_field("cert_index", &self.0.cert_index)?; state.serialize_field("pool_id", &self.0.pool_id.to_string())?; - state.serialize_field("retirement_epoch", &self.0.retirement_epoch)?; + state.serialize_field("retiring_epoch", &self.0.retirement_epoch)?; state.end() } } From 2ef19705f8a39d10004e16fc99deb8d90c253656 Mon Sep 17 00:00:00 2001 From: Alex Woods Date: Fri, 21 Nov 2025 10:36:12 +0000 Subject: [PATCH 18/23] Correct struct entry counts for TxRelay --- modules/rest_blockfrost/src/handlers/transactions.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/modules/rest_blockfrost/src/handlers/transactions.rs b/modules/rest_blockfrost/src/handlers/transactions.rs index baabb7f9..81c8185a 100644 --- a/modules/rest_blockfrost/src/handlers/transactions.rs +++ b/modules/rest_blockfrost/src/handlers/transactions.rs @@ -371,7 +371,7 @@ impl Serialize for TxRelay { { match &self.0 { Relay::SingleHostAddr(addr) => { - let mut state = serializer.serialize_struct("TxRelay", 3)?; + let mut state = serializer.serialize_struct("TxRelay", 5)?; state.serialize_field("ipv4", &addr.ipv4)?; state.serialize_field("ipv6", &addr.ipv6)?; state.serialize_field("dns", &None::)?; @@ -380,7 +380,7 @@ impl Serialize for TxRelay { state.end() } Relay::SingleHostName(name) => { - let mut state = serializer.serialize_struct("TxRelay", 2)?; + let mut state = serializer.serialize_struct("TxRelay", 5)?; state.serialize_field("ipv4", &None::)?; state.serialize_field("ipv6", &None::)?; state.serialize_field("dns", &name.dns_name)?; @@ -389,7 +389,7 @@ impl Serialize for TxRelay { state.end() } Relay::MultiHostName(name) => { - let mut state = serializer.serialize_struct("TxRelay", 1)?; + let mut state = serializer.serialize_struct("TxRelay", 5)?; state.serialize_field("ipv4", &None::)?; state.serialize_field("ipv6", &None::)?; state.serialize_field("dns", &name.dns_name)?; From 140f31e38aa5c9d006c53ae53a33e836199426f6 Mon Sep 17 00:00:00 2001 From: Alex Woods Date: Fri, 21 Nov 2025 10:52:34 +0000 Subject: [PATCH 19/23] Move specialised serialisation of TransactionOutputAmount to rest_blockfrost --- common/src/queries/transactions.rs | 25 +--------------- .../src/handlers/transactions.rs | 29 +++++++++++++++++-- 2 files changed, 28 insertions(+), 26 deletions(-) diff --git a/common/src/queries/transactions.rs b/common/src/queries/transactions.rs index cf3cbd32..88a9cf9b 100644 --- a/common/src/queries/transactions.rs +++ b/common/src/queries/transactions.rs @@ -2,8 +2,6 @@ use crate::{ BlockHash, InstantaneousRewardSource, Lovelace, Metadata, NativeAsset, PoolId, PoolRegistration, StakeAddress, TxHash, }; -use serde::ser::{Serialize, SerializeStruct, Serializer}; -use serde_with::serde_as; pub const DEFAULT_TRANSACTIONS_QUERY_TOPIC: (&str, &str) = ( "transactions-state-query-topic", @@ -47,33 +45,12 @@ pub enum TransactionsStateQueryResponse { Error(QueryError), } -#[derive(Debug, Clone, serde::Deserialize)] +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] pub enum TransactionOutputAmount { Lovelace(Lovelace), Asset(NativeAsset), } -impl Serialize for TransactionOutputAmount { - fn serialize(&self, serializer: S) -> Result - where - S: Serializer, - { - let mut state = serializer.serialize_struct("TransactionOutputAmount", 2)?; - match self { - TransactionOutputAmount::Lovelace(lovelace) => { - state.serialize_field("unit", "lovelace")?; - state.serialize_field("quantity", &lovelace.to_string())?; - } - TransactionOutputAmount::Asset(asset) => { - state.serialize_field("unit", &asset.name)?; - state.serialize_field("quantity", &asset.amount.to_string())?; - } - } - state.end() - } -} - -#[serde_as] #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] pub struct TransactionInfo { pub hash: TxHash, diff --git a/modules/rest_blockfrost/src/handlers/transactions.rs b/modules/rest_blockfrost/src/handlers/transactions.rs index 81c8185a..40640361 100644 --- a/modules/rest_blockfrost/src/handlers/transactions.rs +++ b/modules/rest_blockfrost/src/handlers/transactions.rs @@ -8,7 +8,7 @@ use acropolis_common::{ parameters::{ParametersStateQuery, ParametersStateQueryResponse}, transactions::{ TransactionDelegationCertificate, TransactionInfo, TransactionMIR, - TransactionMetadataItem, TransactionPoolRetirementCertificate, + TransactionMetadataItem, TransactionOutputAmount, TransactionPoolRetirementCertificate, TransactionPoolUpdateCertificate, TransactionStakeCertificate, TransactionWithdrawal, TransactionsStateQuery, TransactionsStateQueryResponse, }, @@ -43,7 +43,10 @@ impl Serialize for TxInfo { state.serialize_field("block_time", &self.0.block_time)?; state.serialize_field("slot", &self.0.slot)?; state.serialize_field("index", &self.0.index)?; - state.serialize_field("output_amount", &self.0.output_amounts)?; + state.serialize_field( + "output_amount", + &self.0.output_amounts.clone().into_iter().map(TxOutputAmount).collect::>(), + )?; state.serialize_field("fees", &self.1.to_string())?; state.serialize_field("deposit", &self.2.to_string())?; state.serialize_field("size", &self.0.size)?; @@ -63,6 +66,28 @@ impl Serialize for TxInfo { } } +struct TxOutputAmount(TransactionOutputAmount); + +impl Serialize for TxOutputAmount { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + let mut state = serializer.serialize_struct("TransactionOutputAmount", 2)?; + match &self.0 { + TransactionOutputAmount::Lovelace(lovelace) => { + state.serialize_field("unit", "lovelace")?; + state.serialize_field("quantity", &lovelace.to_string())?; + } + TransactionOutputAmount::Asset(asset) => { + state.serialize_field("unit", &asset.name)?; + state.serialize_field("quantity", &asset.amount.to_string())?; + } + } + state.end() + } +} + /// Handle `/txs/{hash}` pub async fn handle_transactions_blockfrost( context: Arc>, From 7228ea3680f0b70040313c64eb6ef1d4f9479539 Mon Sep 17 00:00:00 2001 From: Alex Woods Date: Fri, 28 Nov 2025 11:08:03 +0000 Subject: [PATCH 20/23] Hopefully more correct cert counts / stakes --- modules/chain_store/src/chain_store.rs | 43 +++++++++++++++++++++----- 1 file changed, 35 insertions(+), 8 deletions(-) diff --git a/modules/chain_store/src/chain_store.rs b/modules/chain_store/src/chain_store.rs index 9db194cd..614f8b13 100644 --- a/modules/chain_store/src/chain_store.rs +++ b/modules/chain_store/src/chain_store.rs @@ -807,29 +807,41 @@ impl ChainStore { let mut stake_cert_count = 0; let mut pool_update_count = 0; let mut pool_retire_count = 0; + // TODO: check counts use all correct certs for cert in tx_decoded.certs() { match cert { MultiEraCert::AlonzoCompatible(cert) => match cert.as_ref().as_ref() { + alonzo::Certificate::StakeRegistration { .. } => { + stake_cert_count += 1; + } + alonzo::Certificate::StakeDeregistration { .. } => { + stake_cert_count += 1; + } + alonzo::Certificate::StakeDelegation { .. } => delegation_count += 1, alonzo::Certificate::PoolRegistration { .. } => { pool_update_count += 1; } alonzo::Certificate::PoolRetirement { .. } => pool_retire_count += 1, alonzo::Certificate::MoveInstantaneousRewardsCert { .. } => mir_cert_count += 1, - alonzo::Certificate::StakeRegistration { .. } => { - stake_cert_count += 1; - } - alonzo::Certificate::StakeDelegation { .. } => delegation_count += 1, _ => (), }, MultiEraCert::Conway(cert) => match cert.as_ref().as_ref() { - conway::Certificate::PoolRegistration { .. } => { - pool_update_count += 1; - } - conway::Certificate::PoolRetirement { .. } => pool_retire_count += 1, conway::Certificate::StakeRegistration { .. } => { stake_cert_count += 1; } + conway::Certificate::StakeDeregistration { .. } => { + stake_cert_count += 1; + } conway::Certificate::StakeDelegation { .. } => delegation_count += 1, + conway::Certificate::PoolRegistration { .. } => { + pool_update_count += 1; + } + conway::Certificate::PoolRetirement { .. } => pool_retire_count += 1, + conway::Certificate::Reg { .. } => stake_cert_count += 1, + conway::Certificate::UnReg { .. } => stake_cert_count += 1, + conway::Certificate::StakeRegDeleg { .. } => delegation_count += 1, + conway::Certificate::VoteRegDeleg { .. } => delegation_count += 1, + conway::Certificate::StakeVoteRegDeleg { .. } => delegation_count += 1, _ => (), }, _ => (), @@ -874,6 +886,7 @@ impl ChainStore { return Err(anyhow!("Transaction not found in block for given index")); }; let mut certs = Vec::new(); + // TODO: check cert types for (index, cert) in tx_decoded.certs().iter().enumerate() { match cert { MultiEraCert::AlonzoCompatible(cert) => match cert.as_ref().as_ref() { @@ -908,6 +921,20 @@ impl ChainStore { registration: false, }); } + conway::Certificate::StakeRegDeleg(cred, _, _) => { + certs.push(TransactionStakeCertificate { + index: index as u64, + address: map_stake_address(cred, network_id.clone()), + registration: true, + }); + } + conway::Certificate::StakeVoteRegDeleg(cred, _, _, _) => { + certs.push(TransactionStakeCertificate { + index: index as u64, + address: map_stake_address(cred, network_id.clone()), + registration: true, + }); + } _ => (), }, _ => (), From eae8e68e247b825bb139b03f1723109783cdc5c6 Mon Sep 17 00:00:00 2001 From: Alex Woods Date: Fri, 28 Nov 2025 11:13:25 +0000 Subject: [PATCH 21/23] Hopefully more correct delegations --- modules/chain_store/src/chain_store.rs | 25 ++++++++++++++++++++----- 1 file changed, 20 insertions(+), 5 deletions(-) diff --git a/modules/chain_store/src/chain_store.rs b/modules/chain_store/src/chain_store.rs index 614f8b13..16c2176a 100644 --- a/modules/chain_store/src/chain_store.rs +++ b/modules/chain_store/src/chain_store.rs @@ -967,10 +967,8 @@ impl ChainStore { }); } } - MultiEraCert::Conway(cert) => { - if let conway::Certificate::StakeDelegation(cred, pool_key_hash) = - cert.as_ref().as_ref() - { + MultiEraCert::Conway(cert) => match cert.as_ref().as_ref() { + conway::Certificate::StakeDelegation(cred, pool_key_hash) => { certs.push(TransactionDelegationCertificate { index: index as u64, address: map_stake_address(cred, network_id.clone()), @@ -978,7 +976,24 @@ impl ChainStore { active_epoch: tx.block.extra.epoch + 1, }); } - } + conway::Certificate::StakeRegDeleg(cred, pool_key_hash, _) => { + certs.push(TransactionDelegationCertificate { + index: index as u64, + address: map_stake_address(cred, network_id.clone()), + pool_id: to_pool_id(pool_key_hash), + active_epoch: tx.block.extra.epoch + 1, + }); + } + conway::Certificate::StakeVoteRegDeleg(cred, pool_key_hash, _, _) => { + certs.push(TransactionDelegationCertificate { + index: index as u64, + address: map_stake_address(cred, network_id.clone()), + pool_id: to_pool_id(pool_key_hash), + active_epoch: tx.block.extra.epoch + 1, + }); + } + _ => (), + }, _ => (), } } From e8e6a287098cabc1396525eddfc6cc7f321b21be Mon Sep 17 00:00:00 2001 From: Alex Woods Date: Fri, 28 Nov 2025 11:42:08 +0000 Subject: [PATCH 22/23] Correct active_epoch of pool reg/update --- modules/chain_store/src/chain_store.rs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/modules/chain_store/src/chain_store.rs b/modules/chain_store/src/chain_store.rs index 16c2176a..d43c46e2 100644 --- a/modules/chain_store/src/chain_store.rs +++ b/modules/chain_store/src/chain_store.rs @@ -1096,7 +1096,8 @@ impl ChainStore { network_id.clone(), false, )?, - active_epoch: tx.block.extra.epoch + 1, + // Pool registration/updates become active after 2 epochs + active_epoch: tx.block.extra.epoch + 2, }); } } @@ -1128,7 +1129,8 @@ impl ChainStore { network_id.clone(), false, )?, - active_epoch: tx.block.extra.epoch + 1, + // Pool registration/updates become active after 2 epochs + active_epoch: tx.block.extra.epoch + 2, }); } } From 48ceddc6900331b17ccfaf59ce9159bacc3e89eb Mon Sep 17 00:00:00 2001 From: Alex Woods Date: Fri, 28 Nov 2025 11:55:26 +0000 Subject: [PATCH 23/23] Add TODO check comment for deposit amount --- modules/rest_blockfrost/src/handlers/transactions.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/modules/rest_blockfrost/src/handlers/transactions.rs b/modules/rest_blockfrost/src/handlers/transactions.rs index 40640361..11f53a84 100644 --- a/modules/rest_blockfrost/src/handlers/transactions.rs +++ b/modules/rest_blockfrost/src/handlers/transactions.rs @@ -88,7 +88,7 @@ impl Serialize for TxOutputAmount { } } -/// Handle `/txs/{hash}` +/// Handle `/txs/{hash}[/param][/param2]` pub async fn handle_transactions_blockfrost( context: Arc>, params: Vec, @@ -183,6 +183,7 @@ async fn handle_transaction_query( }; // TODO: calc from outputs and inputs if recorded_fee is None let fee = txs_info.recorded_fee.unwrap_or_default(); + // TODO: Check whether updates require a deposit or not let deposit = match calculate_deposit( txs_info.pool_update_count, txs_info.stake_cert_count,