From 82a1d1b2e73787b57516d1f0ffc69c71bd99625b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vladim=C3=ADr=20Volek?= Date: Sun, 30 Aug 2026 16:09:40 +0200 Subject: [PATCH] feat(minibf): governance proposals withdrawals --- crates/minibf/src/error.rs | 20 ++ crates/minibf/src/lib.rs | 8 + crates/minibf/src/mapping.rs | 26 ++ crates/minibf/src/routes/governance.rs | 338 ++++++++++++++++++++++++- docs/content/apis/minibf.mdx | 168 ++++++------ 5 files changed, 473 insertions(+), 87 deletions(-) diff --git a/crates/minibf/src/error.rs b/crates/minibf/src/error.rs index a0984f64..e2361fb8 100644 --- a/crates/minibf/src/error.rs +++ b/crates/minibf/src/error.rs @@ -22,6 +22,8 @@ pub enum Error { InvalidXpub, InvalidDerivationRole, InvalidDerivationIndex, + InvalidCertIndex, + InvalidGovActionId, } #[derive(Serialize)] @@ -159,6 +161,24 @@ impl IntoResponse for Error { )), ) .into_response(), + Error::InvalidCertIndex => ( + StatusCode::BAD_REQUEST, + Json(ErrorBody::new( + 400, + "Bad Request", + "params/cert_index must be integer", + )), + ) + .into_response(), + Error::InvalidGovActionId => ( + StatusCode::BAD_REQUEST, + Json(ErrorBody::new( + 400, + "Bad Request", + "Invalid or malformed gov action id.", + )), + ) + .into_response(), } } } diff --git a/crates/minibf/src/lib.rs b/crates/minibf/src/lib.rs index ece21a8f..89f98505 100644 --- a/crates/minibf/src/lib.rs +++ b/crates/minibf/src/lib.rs @@ -621,6 +621,14 @@ where "/governance/proposals", get(routes::governance::proposals::), ) + .route( + "/governance/proposals/{tx_hash}/{cert_index}/withdrawals", + get(routes::governance::proposal_withdrawals::), + ) + .route( + "/governance/proposals/{gov_action_id}/withdrawals", + get(routes::governance::proposal_withdrawals_by_gov_action::), + ) .with_state(facade) .layer( trace::TraceLayer::new_for_http() diff --git a/crates/minibf/src/mapping.rs b/crates/minibf/src/mapping.rs index b6c9de20..be75865c 100644 --- a/crates/minibf/src/mapping.rs +++ b/crates/minibf/src/mapping.rs @@ -141,6 +141,32 @@ pub fn bech32_gov_action(tx: &Hash<32>, idx: u32) -> Result bech32(GOV_ACTION_HRP, [tx.as_slice(), &bytes[first..]].concat()) } +/// Read a CIP-129 governance action id back into the proposing tx hash and +/// the action index, the inverse of [`bech32_gov_action`]. +/// +/// The index is whatever big-endian bytes trail the hash, so both the +/// one-byte form Blockfrost writes for index 0 and the bare 32-byte form +/// explorers write for it resolve to the same proposal. +pub fn parse_gov_action_id(id: &str) -> Result<(Hash<32>, u32), StatusCode> { + let (hrp, payload) = bech32::decode(id).map_err(|_| StatusCode::BAD_REQUEST)?; + + if hrp != GOV_ACTION_HRP { + return Err(StatusCode::BAD_REQUEST); + } + + let Some((tx, idx)) = payload.split_at_checked(32) else { + return Err(StatusCode::BAD_REQUEST); + }; + + if idx.len() > 4 { + return Err(StatusCode::BAD_REQUEST); + } + + let idx = idx.iter().fold(0u32, |acc, byte| (acc << 8) | *byte as u32); + + Ok((Hash::from(tx), idx)) +} + pub fn asset_fingerprint(subject: &[u8]) -> Result { let mut hasher = pallas::crypto::hash::Hasher::<160>::new(); hasher.input(subject); diff --git a/crates/minibf/src/routes/governance.rs b/crates/minibf/src/routes/governance.rs index 545c1d81..ce09744c 100644 --- a/crates/minibf/src/routes/governance.rs +++ b/crates/minibf/src/routes/governance.rs @@ -3,7 +3,10 @@ use axum::{ http::StatusCode, Json, }; -use blockfrost_openapi::models::proposals_inner::{GovernanceType, ProposalsInner}; +use blockfrost_openapi::models::{ + proposal_withdrawals_inner::ProposalWithdrawalsInner, + proposals_inner::{GovernanceType, ProposalsInner}, +}; use dolos_cardano::{ model::{DRepState, FixedNamespace as _, ProposalAction, ProposalState}, pallas_extras, ChainSummary, PParamsSet, @@ -11,13 +14,17 @@ use dolos_cardano::{ use dolos_core::{ArchiveStore as _, BlockSlot, Domain, StateStore as _}; use pallas::{ crypto::hash::Hash, - ledger::{primitives::Epoch, traverse::MultiEraBlock}, + ledger::{ + addresses::Network, + primitives::{Coin, Epoch, StakeCredential}, + traverse::MultiEraBlock, + }, }; use std::collections::HashMap; use crate::{ error::Error, - mapping::{bech32, bech32_gov_action, IntoModel}, + mapping::{bech32, bech32_gov_action, parse_gov_action_id, stake_cred_to_address, IntoModel}, pagination::{Order, Pagination, PaginationParameters}, Facade, }; @@ -449,6 +456,118 @@ where Ok(Json(page)) } +/// One page of a proposal's treasury withdrawals, ordered the way the action +/// itself lists them. +/// +/// A proposal procedure carries its withdrawals as a map keyed by reward +/// account, so their on-chain order is the raw reward-account bytes ascending. +/// That is the order dolos keeps in the action, and the one Blockfrost's own +/// listing — by the db-sync row id the rows were inserted with — lands in. +fn withdrawals_page( + state: &ProposalState, + network: Network, + pagination: &Pagination, +) -> Result, StatusCode> { + let ProposalAction::TreasuryWithdrawal(withdrawals) = &state.action else { + return Ok(vec![]); + }; + + // desc is the whole ascending listing read backwards + let ordered: Box> = match pagination.order { + Order::Asc => Box::new(withdrawals.iter()), + Order::Desc => Box::new(withdrawals.iter().rev()), + }; + + ordered + .skip(pagination.from()) + .take(pagination.count) + .map(|(credential, amount)| { + let stake_address = stake_cred_to_address(credential, network) + .to_bech32() + .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?; + + Ok(ProposalWithdrawalsInner { + stake_address, + amount: amount.to_string(), + }) + }) + .collect() +} + +/// The withdrawals of the proposal `tx` proposed at action index `idx`. +/// +/// Blockfrost reads this off a join with db-sync's `treasury_withdrawal` +/// table and sends whatever rows come back, so a proposal it has never heard +/// of and a proposal that asks for no withdrawal are the same empty listing +/// rather than a 404. +fn read_withdrawals( + domain: &Facade, + tx: Hash<32>, + idx: u32, + pagination: &Pagination, +) -> Result, Error> { + let key = ProposalState::build_entity_key(tx, idx); + + let state = domain + .state() + .read_entity_typed::(ProposalState::NS, &key) + .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?; + + let Some(state) = state else { + return Ok(vec![]); + }; + + let network = domain.get_network_id()?; + + Ok(withdrawals_page(&state, network, pagination)?) +} + +/// `GET /governance/proposals/{tx_hash}/{cert_index}/withdrawals`: the +/// treasury payouts a withdrawal proposal asks for, oldest first. +pub async fn proposal_withdrawals( + Path((tx_hash, cert_index)): Path<(String, String)>, + Query(params): Query, + State(domain): State>, +) -> Result>, Error> +where + D: Domain + Clone + Send + Sync + 'static, +{ + let pagination = Pagination::try_from(params)?; + + let cert_index = cert_index + .parse::() + .map_err(|_| Error::InvalidCertIndex)?; + + // Blockfrost matches the hash as text against db-sync, so a malformed one + // is a listing that matches nothing rather than a bad request. + let Ok(tx) = tx_hash.parse::>() else { + return Ok(Json(vec![])); + }; + + let page = read_withdrawals(&domain, tx, cert_index, &pagination)?; + + Ok(Json(page)) +} + +/// `GET /governance/proposals/{gov_action_id}/withdrawals`: the same listing, +/// addressed by CIP-129 id instead of by tx hash and action index. +pub async fn proposal_withdrawals_by_gov_action( + Path(gov_action_id): Path, + Query(params): Query, + State(domain): State>, +) -> Result>, Error> +where + D: Domain + Clone + Send + Sync + 'static, +{ + let pagination = Pagination::try_from(params)?; + + let (tx, idx) = parse_gov_action_id(&gov_action_id).map_err(|_| Error::InvalidGovActionId)?; + + let page = read_withdrawals(&domain, tx, idx, &pagination)?; + + Ok(Json(page)) +} + #[cfg(test)] mod tests { use super::*; @@ -456,7 +575,8 @@ mod tests { use bech32::{Bech32, Hrp}; use dolos_testing::synthetic::SyntheticBlockConfig; use itertools::Itertools; - use pallas::ledger::primitives::conway::GovAction; + use pallas::{codec::utils::Bytes, ledger::primitives::conway::GovAction}; + use std::collections::BTreeMap; fn invalid_drep() -> &'static str { "not-a-drep" @@ -745,6 +865,216 @@ mod tests { } } + /// The three withdrawals of preview's `0e58f693…6590#0`, each as the raw + /// reward account the chain carries, the amount, and the bech32 address + /// Blockfrost returns for it. Listed in the order the API returns them. + fn withdrawal_vectors() -> Vec<(Vec, u64, &'static str)> { + vec![ + ( + hex::decode("e0788cf0519348fefaf3c721c5f5bd60b195b444fa0d8fb4512dc259be").unwrap(), + 2000, + "stake_test1upugeuz3jdy0a7hncusutadavzcetdzylgxcldz39hp9n0s0xy0n5", + ), + ( + hex::decode("e0ba149e2e2379097e65f0c03f2733d3103151e7f100d36dfdb01a0b22").unwrap(), + 1000, + "stake_test1uzapf83wydusjln97rqr7fen6vgrz5087yqdxm0akqdqkgstjz8g4", + ), + ( + hex::decode("e0f631370cc87882bf5e14ab72534caf2655d0a2a50a9a8a3820bb6f4a").unwrap(), + 3000, + "stake_test1urmrzdcvepug9067zj4hy56v4un9t59z559f4z3cyzak7js3z5t2t", + ), + ] + } + + fn expected_withdrawals() -> Vec<(String, String)> { + withdrawal_vectors() + .into_iter() + .map(|(_, amount, address)| (address.to_string(), amount.to_string())) + .collect() + } + + /// One tx proposing a treasury withdrawal at index 0 and an info action at + /// index 1, so the same fixture covers a proposal that pays out and one + /// that cannot. + fn withdrawal_app() -> TestApp { + let withdrawals: BTreeMap = withdrawal_vectors() + .into_iter() + .map(|(account, amount, _)| (Bytes::from(account), amount)) + .collect(); + + TestApp::new_with_cfg(SyntheticBlockConfig { + block_count: 1, + txs_per_block: 1, + gov_actions_by_block: vec![vec![vec![ + GovAction::TreasuryWithdrawals(withdrawals, None), + GovAction::Information, + ]]], + ..Default::default() + }) + } + + async fn get_withdrawals(app: &TestApp, path: &str) -> Vec<(String, String)> { + let (status, bytes) = app.get_bytes(path).await; + assert_eq!( + status, + StatusCode::OK, + "unexpected status {status} for {path} with body: {}", + String::from_utf8_lossy(&bytes) + ); + + let rows: Vec = + serde_json::from_slice(&bytes).expect("failed to parse withdrawals"); + + rows.into_iter() + .map(|x| (x.stake_address, x.amount)) + .collect() + } + + #[tokio::test] + async fn governance_proposal_withdrawals_happy_path() { + let app = withdrawal_app(); + let tx = tx_hash_of_block(&app, 0); + let path = format!("/governance/proposals/{tx}/0/withdrawals"); + + assert_eq!(get_withdrawals(&app, &path).await, expected_withdrawals()); + } + + #[tokio::test] + async fn governance_proposal_withdrawals_orders_and_paginates() { + let app = withdrawal_app(); + let tx = tx_hash_of_block(&app, 0); + let base = format!("/governance/proposals/{tx}/0/withdrawals"); + let expected = expected_withdrawals(); + + let desc = get_withdrawals(&app, &format!("{base}?order=desc")).await; + assert_eq!( + desc, + expected.iter().rev().cloned().collect_vec(), + "desc is the ascending listing read backwards" + ); + + let page = get_withdrawals(&app, &format!("{base}?count=2&page=2")).await; + assert_eq!(page, expected[2..].to_vec()); + + let page = get_withdrawals(&app, &format!("{base}?count=1&page=3&order=desc")).await; + assert_eq!(page, expected[..1].to_vec()); + + // a page past the end is empty, not an error + let page = get_withdrawals(&app, &format!("{base}?page=5")).await; + assert!(page.is_empty()); + } + + /// Blockfrost joins the proposal against db-sync's withdrawal table and + /// sends whatever comes back, so everything that names no withdrawal — + /// another action type, an unknown proposal, an unreadable hash — is the + /// same empty listing rather than a 404. + #[tokio::test] + async fn governance_proposal_withdrawals_without_rows() { + let app = withdrawal_app(); + let tx = tx_hash_of_block(&app, 0); + + // index 1 of the same tx is the info action + let path = format!("/governance/proposals/{tx}/1/withdrawals"); + assert!(get_withdrawals(&app, &path).await.is_empty()); + + // an index the tx never proposed at + let path = format!("/governance/proposals/{tx}/9/withdrawals"); + assert!(get_withdrawals(&app, &path).await.is_empty()); + + let missing = hex::encode([0u8; 32]); + let path = format!("/governance/proposals/{missing}/0/withdrawals"); + assert!(get_withdrawals(&app, &path).await.is_empty()); + + let path = "/governance/proposals/not-a-tx-hash/0/withdrawals"; + assert!(get_withdrawals(&app, path).await.is_empty()); + } + + #[tokio::test] + async fn governance_proposal_withdrawals_by_gov_action_id() { + let app = withdrawal_app(); + let tx: Hash<32> = tx_hash_of_block(&app, 0) + .parse() + .expect("failed to parse tx hash"); + + let id = bech32_gov_action(&tx, 0).unwrap(); + let path = format!("/governance/proposals/{id}/withdrawals"); + assert_eq!(get_withdrawals(&app, &path).await, expected_withdrawals()); + + // the same listing, paginated the same way + let page = get_withdrawals(&app, &format!("{path}?order=desc&count=1")).await; + assert_eq!(page, expected_withdrawals()[2..].to_vec()); + + // the bare-hash form explorers write for index 0 names the same + // proposal as the one-byte form Blockfrost writes + let minimal = bech32(bech32::Hrp::parse("gov_action").unwrap(), tx.as_slice()).unwrap(); + let path = format!("/governance/proposals/{minimal}/withdrawals"); + assert_eq!(get_withdrawals(&app, &path).await, expected_withdrawals()); + + // a well-formed id for a proposal nobody made + let id = bech32_gov_action(&Hash::from([0u8; 32]), 0).unwrap(); + let path = format!("/governance/proposals/{id}/withdrawals"); + assert!(get_withdrawals(&app, &path).await.is_empty()); + } + + #[tokio::test] + async fn governance_proposal_withdrawals_bad_request() { + let app = withdrawal_app(); + let tx = tx_hash_of_block(&app, 0); + let base = format!("/governance/proposals/{tx}/0/withdrawals"); + + assert_status(&app, &format!("{base}?count=0"), StatusCode::BAD_REQUEST).await; + assert_status( + &app, + &format!("{base}?order=sideways"), + StatusCode::BAD_REQUEST, + ) + .await; + + // the cert index is the only path part that has to be a number + let path = format!("/governance/proposals/{tx}/x/withdrawals"); + assert_status(&app, &path, StatusCode::BAD_REQUEST).await; + + for id in ["not-bech32", &missing_drep()] { + let path = format!("/governance/proposals/{id}/withdrawals"); + assert_status(&app, &path, StatusCode::BAD_REQUEST).await; + } + } + + #[tokio::test] + async fn governance_proposal_withdrawals_internal_error() { + let app = TestApp::new_with_fault(Some(TestFault::StateStoreError)); + let path = format!( + "/governance/proposals/{}/0/withdrawals", + hex::encode([1u8; 32]) + ); + assert_status(&app, &path, StatusCode::INTERNAL_SERVER_ERROR).await; + } + + /// CIP-129 ids read back into the proposal they name, including the + /// bare-hash form that omits the index byte for index 0. + #[test] + fn gov_action_id_round_trips() { + let tx: Hash<32> = "b2a591ac219ce6dcca5847e0248015209c7cb0436aa6bd6863d0c1f152a60bc5" + .parse() + .expect("failed to parse tx hash"); + + for idx in [0, 1, 255, 256, u32::MAX] { + let id = bech32_gov_action(&tx, idx).unwrap(); + assert_eq!(parse_gov_action_id(&id).unwrap(), (tx, idx), "{idx}"); + } + + let bare = bech32(bech32::Hrp::parse("gov_action").unwrap(), tx.as_slice()).unwrap(); + assert_eq!(parse_gov_action_id(&bare).unwrap(), (tx, 0)); + + // not bech32, wrong hrp, and a payload too short to hold a tx hash + assert!(parse_gov_action_id("not-bech32").is_err()); + assert!(parse_gov_action_id(&missing_drep()).is_err()); + let short = bech32(bech32::Hrp::parse("gov_action").unwrap(), [0u8; 31]).unwrap(); + assert!(parse_gov_action_id(&short).is_err()); + } + /// CIP-129: the id is the proposing tx hash with the action index /// trailing it. The first two vectors come from the Blockfrost spec kept /// in `crates/minibf/openapi.yaml`; the last one pins the minimal diff --git a/docs/content/apis/minibf.mdx b/docs/content/apis/minibf.mdx index 143b3198..19b90482 100644 --- a/docs/content/apis/minibf.mdx +++ b/docs/content/apis/minibf.mdx @@ -97,86 +97,88 @@ Check the [Configuration Schema](../configuration/schema) for detailed info on h Dolos provides many, but not all of the Blockfrost endpoints. The following list represent the endpoints currently supported by Dolos: -| Endpoint | Description | -|--------------------------------------------|----------------------------------------------------------| -| `/` | Service info (version, revision) | -| `/health` | Health check | -| `/health/clock` | Clock health | -| `/metrics` | Prometheus metrics | -| `/accounts/{stake_address}` | Get account information for a stake address | -| `/accounts/{stake_address}/addresses` | Get addresses for a stake address | -| `/accounts/{stake_address}/addresses/assets` | Get assets held across a stake address's addresses | -| `/accounts/{stake_address}/delegations` | Get delegations for a stake address | -| `/accounts/{stake_address}/registrations` | Get registrations for a stake address | -| `/accounts/{stake_address}/rewards` | Get rewards for a stake address | -| `/accounts/{stake_address}/transactions` | Get transactions for a stake address | -| `/accounts/{stake_address}/utxos` | Get UTXOs for a stake address | -| `/accounts/{stake_address}/withdrawals` | Get withdrawals for a stake address | -| `/addresses/{address}` | Get general information for a specific address | -| `/addresses/{address}/total` | Get lifetime received/sent totals for a specific address | -| `/addresses/{address}/transactions` | Get transactions for a specific address | -| `/addresses/{address}/txs` | Alias of `/addresses/{address}/transactions` | -| `/addresses/{address}/utxos` | Get UTXOs for a specific address | -| `/addresses/{address}/utxos/{asset}` | Get UTXOs for a specific address and asset | -| `/assets/policy/{policy_id}` | Get assets minted under a specific policy | -| `/assets/{subject}` | Get asset information | -| `/assets/{subject}/addresses` | Get addresses holding a specific asset | -| `/assets/{subject}/transactions` | Get transactions involving a specific asset | -| `/assets/{subject}/txs` | Alias of `/assets/{subject}/transactions` (hashes only) | -| `/blocks/latest` | Get latest block information | -| `/blocks/latest/txs` | Get transactions for the latest block | -| `/blocks/latest/txs/cbor` | Get transactions with CBOR data for the latest block | -| `/blocks/slot/{slot_number}` | Get block by slot number | -| `/blocks/{hash_or_number}` | Get block information | -| `/blocks/{hash_or_number}/addresses` | Get addresses in a specific block | -| `/blocks/{hash_or_number}/next` | Get next block | -| `/blocks/{hash_or_number}/previous` | Get previous block | -| `/blocks/{hash_or_number}/txs` | Get transactions for a specific block | -| `/blocks/{hash_or_number}/txs/cbor` | Get transactions with CBOR data for a specific block | -| `/epochs/latest` | Get latest epoch information | -| `/epochs/latest/parameters` | Get latest epoch parameters | -| `/epochs/{epoch}` | Get information for a specific epoch | -| `/epochs/{epoch}/blocks` | Get blocks in a specific epoch | -| `/epochs/{epoch}/blocks/{pool_id}` | Get blocks minted by a specific pool in a specific epoch | -| `/epochs/{epoch}/next` | Get the epochs following a specific epoch | -| `/epochs/{epoch}/parameters` | Get epoch parameters | -| `/epochs/{epoch}/previous` | Get the epochs preceding a specific epoch | -| `/epochs/{epoch}/stakes` | Get epoch stake distribution | -| `/epochs/{epoch}/stakes/{pool_id}` | Get epoch stake distribution for a specific pool | -| `/genesis` | Get genesis information | -| `/governance/dreps/{drep_id}` | Get DRep information | -| `/governance/proposals` | Get list of governance proposals | -| `/metadata/txs/labels/{label}` | Get metadata for transactions with a specific label | -| `/metadata/txs/labels/{label}/cbor` | Get CBOR metadata for transactions with a specific label | -| `/network` | Get network information | -| `/network/eras` | Get network eras information | -| `/pools` | Get list of registered pools | -| `/pools/extended` | Get extended pool information | -| `/pools/retired` | Get list of retired pools | -| `/pools/retiring` | Get list of retiring pools | -| `/pools/{id}` | Get information for a specific pool | -| `/pools/{id}/delegators` | Get delegators for a specific pool | -| `/pools/{id}/history` | Get history for a specific pool | -| `/pools/{id}/metadata` | Get metadata for a specific pool | -| `/pools/{id}/relays` | Get relays for a specific pool | -| `/pools/{id}/updates` | Get certificate updates for a specific pool | -| `/scripts/{script_hash}` | Get script information | -| `/scripts/{script_hash}/json` | Get script JSON | -| `/scripts/{script_hash}/cbor` | Get script CBOR | -| `/scripts/{script_hash}/utxos` | Get live UTxOs that hold the script as a reference | -| `/scripts/datum/{datum_hash}` | Get datum by hash | -| `/scripts/datum/{datum_hash}/cbor` | Get datum CBOR by hash | -| `/tx/submit` | Submit a transaction | -| `/txs/{tx_hash}` | Get transaction information | -| `/txs/{tx_hash}/cbor` | Get CBOR data for a transaction | -| `/txs/{tx_hash}/delegations` | Get delegations for a specific transaction | -| `/txs/{tx_hash}/metadata` | Get metadata for a specific transaction | -| `/txs/{tx_hash}/metadata/cbor` | Get CBOR metadata for a specific transaction | -| `/txs/{tx_hash}/mirs` | Get MIRs for a specific transaction | -| `/txs/{tx_hash}/pool_retires` | Get pool retirements for a specific transaction | -| `/txs/{tx_hash}/pool_updates` | Get pool updates for a specific transaction | -| `/txs/{tx_hash}/redeemers` | Get redeemers for a specific transaction | -| `/txs/{tx_hash}/required_signers` | Get required signers for a specific transaction | -| `/txs/{tx_hash}/stakes` | Get stakes for a specific transaction | -| `/txs/{tx_hash}/utxos` | Get UTXOs for a specific transaction | -| `/txs/{tx_hash}/withdrawals` | Get withdrawals for a specific transaction | +| Endpoint | Description | +| ---------------------------------------------------------- | -------------------------------------------------------- | +| `/` | Service info (version, revision) | +| `/health` | Health check | +| `/health/clock` | Clock health | +| `/metrics` | Prometheus metrics | +| `/accounts/{stake_address}` | Get account information for a stake address | +| `/accounts/{stake_address}/addresses` | Get addresses for a stake address | +| `/accounts/{stake_address}/addresses/assets` | Get assets held across a stake address's addresses | +| `/accounts/{stake_address}/delegations` | Get delegations for a stake address | +| `/accounts/{stake_address}/registrations` | Get registrations for a stake address | +| `/accounts/{stake_address}/rewards` | Get rewards for a stake address | +| `/accounts/{stake_address}/transactions` | Get transactions for a stake address | +| `/accounts/{stake_address}/utxos` | Get UTXOs for a stake address | +| `/accounts/{stake_address}/withdrawals` | Get withdrawals for a stake address | +| `/addresses/{address}` | Get general information for a specific address | +| `/addresses/{address}/total` | Get lifetime received/sent totals for a specific address | +| `/addresses/{address}/transactions` | Get transactions for a specific address | +| `/addresses/{address}/txs` | Alias of `/addresses/{address}/transactions` | +| `/addresses/{address}/utxos` | Get UTXOs for a specific address | +| `/addresses/{address}/utxos/{asset}` | Get UTXOs for a specific address and asset | +| `/assets/policy/{policy_id}` | Get assets minted under a specific policy | +| `/assets/{subject}` | Get asset information | +| `/assets/{subject}/addresses` | Get addresses holding a specific asset | +| `/assets/{subject}/transactions` | Get transactions involving a specific asset | +| `/assets/{subject}/txs` | Alias of `/assets/{subject}/transactions` (hashes only) | +| `/blocks/latest` | Get latest block information | +| `/blocks/latest/txs` | Get transactions for the latest block | +| `/blocks/latest/txs/cbor` | Get transactions with CBOR data for the latest block | +| `/blocks/slot/{slot_number}` | Get block by slot number | +| `/blocks/{hash_or_number}` | Get block information | +| `/blocks/{hash_or_number}/addresses` | Get addresses in a specific block | +| `/blocks/{hash_or_number}/next` | Get next block | +| `/blocks/{hash_or_number}/previous` | Get previous block | +| `/blocks/{hash_or_number}/txs` | Get transactions for a specific block | +| `/blocks/{hash_or_number}/txs/cbor` | Get transactions with CBOR data for a specific block | +| `/epochs/latest` | Get latest epoch information | +| `/epochs/latest/parameters` | Get latest epoch parameters | +| `/epochs/{epoch}` | Get information for a specific epoch | +| `/epochs/{epoch}/blocks` | Get blocks in a specific epoch | +| `/epochs/{epoch}/blocks/{pool_id}` | Get blocks minted by a specific pool in a specific epoch | +| `/epochs/{epoch}/next` | Get the epochs following a specific epoch | +| `/epochs/{epoch}/parameters` | Get epoch parameters | +| `/epochs/{epoch}/previous` | Get the epochs preceding a specific epoch | +| `/epochs/{epoch}/stakes` | Get epoch stake distribution | +| `/epochs/{epoch}/stakes/{pool_id}` | Get epoch stake distribution for a specific pool | +| `/genesis` | Get genesis information | +| `/governance/dreps/{drep_id}` | Get DRep information | +| `/governance/proposals` | Get list of governance proposals | +| `/governance/proposals/{gov_action_id}/withdrawals` | Get withdrawals of a proposal by CIP-129 id | +| `/governance/proposals/{tx_hash}/{cert_index}/withdrawals` | Get withdrawals of a specific proposal | +| `/metadata/txs/labels/{label}` | Get metadata for transactions with a specific label | +| `/metadata/txs/labels/{label}/cbor` | Get CBOR metadata for transactions with a specific label | +| `/network` | Get network information | +| `/network/eras` | Get network eras information | +| `/pools` | Get list of registered pools | +| `/pools/extended` | Get extended pool information | +| `/pools/retired` | Get list of retired pools | +| `/pools/retiring` | Get list of retiring pools | +| `/pools/{id}` | Get information for a specific pool | +| `/pools/{id}/delegators` | Get delegators for a specific pool | +| `/pools/{id}/history` | Get history for a specific pool | +| `/pools/{id}/metadata` | Get metadata for a specific pool | +| `/pools/{id}/relays` | Get relays for a specific pool | +| `/pools/{id}/updates` | Get certificate updates for a specific pool | +| `/scripts/{script_hash}` | Get script information | +| `/scripts/{script_hash}/json` | Get script JSON | +| `/scripts/{script_hash}/cbor` | Get script CBOR | +| `/scripts/{script_hash}/utxos` | Get live UTxOs that hold the script as a reference | +| `/scripts/datum/{datum_hash}` | Get datum by hash | +| `/scripts/datum/{datum_hash}/cbor` | Get datum CBOR by hash | +| `/tx/submit` | Submit a transaction | +| `/txs/{tx_hash}` | Get transaction information | +| `/txs/{tx_hash}/cbor` | Get CBOR data for a transaction | +| `/txs/{tx_hash}/delegations` | Get delegations for a specific transaction | +| `/txs/{tx_hash}/metadata` | Get metadata for a specific transaction | +| `/txs/{tx_hash}/metadata/cbor` | Get CBOR metadata for a specific transaction | +| `/txs/{tx_hash}/mirs` | Get MIRs for a specific transaction | +| `/txs/{tx_hash}/pool_retires` | Get pool retirements for a specific transaction | +| `/txs/{tx_hash}/pool_updates` | Get pool updates for a specific transaction | +| `/txs/{tx_hash}/redeemers` | Get redeemers for a specific transaction | +| `/txs/{tx_hash}/required_signers` | Get required signers for a specific transaction | +| `/txs/{tx_hash}/stakes` | Get stakes for a specific transaction | +| `/txs/{tx_hash}/utxos` | Get UTXOs for a specific transaction | +| `/txs/{tx_hash}/withdrawals` | Get withdrawals for a specific transaction |