diff --git a/CHANGELOG.md b/CHANGELOG.md index f02981df..83116ac2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,7 +3,8 @@ ### Added - Gateway: per-relay `blockfrost_gateway_relay_healthy`, `blockfrost_gateway_relay_data_node_up`, and `blockfrost_gateway_relay_info` metrics in `GET /metrics` (and the same data points in `GET /stats`) -- New endpoints proxied to the data node: `/accounts/{stake_address}/utxos`, `/addresses/{address}`, and `/blocks/slot/{slot_number}` +- New endpoints proxied to the data node: `/accounts/{stake_address}/utxos`, `/accounts/{stake_address}/transactions`, `/addresses/{address}`, `/blocks/slot/{slot_number}`, `/pools/retiring`, `/blocks/{hash_or_number}/txs/cbor`, and `/blocks/latest/txs/cbor` +- `/accounts/{stake_address}/*` endpoints now accept the CIP-19 credential forms (`stake_vk`, `stake_vkh`, `script`) in place of a canonical stake address, resolving them via the data node - `--max-response-body-bytes` to configure the maximum proxied response body size (default 10 MiB) ### Fixed diff --git a/crates/api_provider/src/types.rs b/crates/api_provider/src/types.rs index 76081bff..43f76005 100644 --- a/crates/api_provider/src/types.rs +++ b/crates/api_provider/src/types.rs @@ -9,6 +9,7 @@ use blockfrost_openapi::models::{ account_mir_content_inner::AccountMirContentInner, account_registration_content_inner::AccountRegistrationContentInner, account_reward_content_inner::AccountRewardContentInner, + account_transactions_content_inner::AccountTransactionsContentInner, account_utxo_content_inner::AccountUtxoContentInner, account_withdrawal_content_inner::AccountWithdrawalContentInner, address_content::AddressContent, address_content_extended::AddressContentExtended, @@ -18,7 +19,8 @@ use blockfrost_openapi::models::{ asset_addresses_inner::AssetAddressesInner, asset_history_inner::AssetHistoryInner, asset_policy_inner::AssetPolicyInner, asset_transactions_inner::AssetTransactionsInner, assets_inner::AssetsInner, block_content::BlockContent, - block_content_addresses_inner::BlockContentAddressesInner, drep::Drep, + block_content_addresses_inner::BlockContentAddressesInner, + block_content_txs_cbor_inner::BlockContentTxsCborInner, drep::Drep, drep_delegators_inner::DrepDelegatorsInner, drep_metadata::DrepMetadata, drep_updates_inner::DrepUpdatesInner, drep_votes_inner::DrepVotesInner, epoch_content::EpochContent, epoch_param_content::EpochParamContent, @@ -65,6 +67,7 @@ pub type AccountsHistoryResponse = Vec; pub type AccountsMirResponse = Vec; pub type AccountsUtxosResponse = Vec; pub type AccountsWithdrawalsResponse = Vec; +pub type AccountsTransactionsResponse = Vec; // addresses pub type AddressesResponse = AddressContent; @@ -87,6 +90,7 @@ pub type BlocksSingleResponse = BlockContent; pub type BlocksResponse = Vec; pub type BlocksAddressesExtendedResponse = Vec; pub type BlocksAddressesContentResponse = BlockContentAddressesInner; +pub type BlocksTxsCborResponse = Vec; // epochs pub type EpochsParamResponse = EpochParamContent; diff --git a/crates/data_node/src/api/accounts.rs b/crates/data_node/src/api/accounts.rs index d16505ee..ded21c9e 100644 --- a/crates/data_node/src/api/accounts.rs +++ b/crates/data_node/src/api/accounts.rs @@ -1,7 +1,8 @@ use crate::client::DataNode; use bf_api_provider::types::{ AccountsAddressesResponse, AccountsDelegationsResponse, AccountsRegistrationsResponse, - AccountsResponse, AccountsRewardsResponse, AccountsUtxosResponse, AccountsWithdrawalsResponse, + AccountsResponse, AccountsRewardsResponse, AccountsTransactionsResponse, AccountsUtxosResponse, + AccountsWithdrawalsResponse, }; use bf_common::{pagination::Pagination, types::ApiResult}; @@ -81,4 +82,14 @@ impl DataNodeAccounts<'_> { self.inner.client.get(&path, Some(pagination)).await } + + pub async fn transactions( + &self, + stake_address: &str, + pagination: &Pagination, + ) -> ApiResult { + let path = format!("accounts/{stake_address}/transactions"); + + self.inner.client.get(&path, Some(pagination)).await + } } diff --git a/crates/data_node/src/api/blocks.rs b/crates/data_node/src/api/blocks.rs index 719ea5d3..20723a7b 100644 --- a/crates/data_node/src/api/blocks.rs +++ b/crates/data_node/src/api/blocks.rs @@ -1,5 +1,5 @@ use crate::client::DataNode; -use bf_api_provider::types::{BlocksResponse, BlocksSingleResponse}; +use bf_api_provider::types::{BlocksResponse, BlocksSingleResponse, BlocksTxsCborResponse}; use bf_common::{pagination::Pagination, types::ApiResult}; pub struct DataNodeBlocks<'a> { @@ -21,6 +21,16 @@ impl DataNodeBlocks<'_> { self.inner.client.get("blocks/latest/txs", None).await } + pub async fn latest_txs_cbor( + &self, + pagination: &Pagination, + ) -> ApiResult { + self.inner + .client + .get("blocks/latest/txs/cbor", Some(pagination)) + .await + } + pub async fn by(&self, hash_or_number: &str) -> ApiResult { let path = format!("blocks/{hash_or_number}"); @@ -37,6 +47,16 @@ impl DataNodeBlocks<'_> { self.inner.client.get(&path, Some(pagination)).await } + pub async fn txs_cbor( + &self, + hash_or_number: &str, + pagination: &Pagination, + ) -> ApiResult { + let path = format!("blocks/{hash_or_number}/txs/cbor"); + + self.inner.client.get(&path, Some(pagination)).await + } + pub async fn previous( &self, hash_or_number: &str, diff --git a/crates/data_node/src/api/pools.rs b/crates/data_node/src/api/pools.rs index 9b461d9d..a0b09b52 100644 --- a/crates/data_node/src/api/pools.rs +++ b/crates/data_node/src/api/pools.rs @@ -1,7 +1,7 @@ use crate::client::DataNode; use bf_api_provider::types::{ PoolsDelegatorsResponse, PoolsHistoryResponse, PoolsListExtendedResponse, - PoolsMetadataResponse, PoolsSingleResponse, + PoolsMetadataResponse, PoolsRetiresResponse, PoolsSingleResponse, }; use bf_common::{pagination::Pagination, types::ApiResult}; @@ -23,6 +23,13 @@ impl DataNodePools<'_> { .await } + pub async fn retiring(&self, pagination: &Pagination) -> ApiResult { + self.inner + .client + .get("pools/retiring", Some(pagination)) + .await + } + pub async fn delegators( &self, pool_id: &str, diff --git a/crates/integration_tests/tests/data/supported_endpoints.json b/crates/integration_tests/tests/data/supported_endpoints.json index cdd1c022..1a41cc3c 100644 --- a/crates/integration_tests/tests/data/supported_endpoints.json +++ b/crates/integration_tests/tests/data/supported_endpoints.json @@ -16,9 +16,11 @@ "/accounts/{stake_address}/registrations", "/accounts/{stake_address}/withdrawals", "/accounts/{stake_address}/utxos", + "/accounts/{stake_address}/transactions", "/accounts/{stake_address}", "/assets/{asset}", "/pools/extended", + "/pools/retiring", "/pools/{pool_id}", "/pools/{pool_id}/metadata", "/pools/{pool_id}/delegators", @@ -34,7 +36,9 @@ "/blocks/{hash_or_number}/next", "/blocks/{hash_or_number}/previous", "/blocks/{hash_or_number}/txs", + "/blocks/{hash_or_number}/txs/cbor", "/blocks/latest/txs", + "/blocks/latest/txs/cbor", "/blocks/slot/{slot_number}", "/txs/{hash}/delegations", "/epochs/{number}/parameters", diff --git a/crates/platform/src/addresses.rs b/crates/platform/src/addresses.rs index 08b65f07..3f02d745 100644 --- a/crates/platform/src/addresses.rs +++ b/crates/platform/src/addresses.rs @@ -72,19 +72,24 @@ impl fmt::Display for AddressType { } pub fn is_stake_address_valid(input: &str, network: &Network) -> Result { - let (hrp, _) = bech32::decode(input).map_err(|_| BlockfrostError::invalid_stake_address())?; - let prefix_str = match hrp.as_str() { - "stake" => Ok("stake"), - "stake_test" => Ok("stake_test"), - _ => Err(BlockfrostError::invalid_stake_address()), - }?; + let (hrp, data) = + bech32::decode(input).map_err(|_| BlockfrostError::invalid_stake_address())?; - match network { - Network::Mainnet if prefix_str == "stake" => Ok(true), - Network::Preprod | Network::Preview | Network::Custom if prefix_str == "stake_test" => { - Ok(true) - }, - _ => Ok(false), + match hrp.as_str() { + // Canonical stake addresses carry the network in their header, so they + // must match the configured network. + "stake" => Ok(*network == Network::Mainnet), + "stake_test" => Ok(matches!( + network, + Network::Preprod | Network::Preview | Network::Custom + )), + // CIP-19 stake credentials (verification key, key hash or script hash) + // are network-agnostic; the data node resolves them against its own + // network. `stake_vkh` and `script` are 28-byte credentials, while + // `stake_vk` is a 32-byte Ed25519 verification key. + "stake_vkh" | "script" => Ok(data.len() == 28), + "stake_vk" => Ok(data.len() == 32), + _ => Err(BlockfrostError::invalid_stake_address()), } } @@ -250,6 +255,24 @@ mod tests { Network::Custom, false )] + #[case( + "CIP-19 stake_vkh credential is network-agnostic", + "stake_vkh18ncsfsl5ngpn0u3edp30mym5e0c795msf37yrhlm03hkjdcyt7g", + Network::Preview, + true + )] + #[case( + "CIP-19 stake_vk verification key is network-agnostic", + "stake_vk1px4j0r2fk7ux5p23shz8f3y5y2qam7s954rgf3lg5merqcj6aetsft99wu", + Network::Mainnet, + true + )] + #[case( + "CIP-19 script credential is network-agnostic", + "script1prjh5fpchv23u4ufyy7gs27tjjmqp0dtz2vwhkxvmvay6ehj87m", + Network::Preview, + true + )] fn test_validate_stake_address( #[case] description: &str, #[case] input: &str, diff --git a/crates/platform/src/api/accounts/stake_address.rs b/crates/platform/src/api/accounts/stake_address.rs index acf69515..e5221d13 100644 --- a/crates/platform/src/api/accounts/stake_address.rs +++ b/crates/platform/src/api/accounts/stake_address.rs @@ -5,5 +5,6 @@ pub mod mirs; pub mod registrations; pub mod rewards; pub mod root; +pub mod transactions; pub mod utxos; pub mod withdrawals; diff --git a/crates/platform/src/api/accounts/stake_address/transactions.rs b/crates/platform/src/api/accounts/stake_address/transactions.rs new file mode 100644 index 00000000..96d8a0ee --- /dev/null +++ b/crates/platform/src/api/accounts/stake_address/transactions.rs @@ -0,0 +1,25 @@ +use crate::{ + accounts::{AccountData, AccountsPath}, + server::state::AppState, +}; +use axum::extract::{Path, Query, State}; +use bf_api_provider::types::AccountsTransactionsResponse; +use bf_common::{ + pagination::{Pagination, PaginationQuery}, + types::ApiResult, +}; + +pub async fn route( + Path(path): Path, + State(state): State, + Query(pagination_query): Query, +) -> ApiResult { + let account = AccountData::from_account_path(path.stake_address, &state.config.network)?; + let pagination = Pagination::from_query(pagination_query)?; + let data_node = state.data_node()?; + + data_node + .accounts() + .transactions(&account.stake_address, &pagination) + .await +} diff --git a/crates/platform/src/api/blocks/hash_or_number/txs.rs b/crates/platform/src/api/blocks/hash_or_number/txs.rs index e87370f4..a708b0b5 100644 --- a/crates/platform/src/api/blocks/hash_or_number/txs.rs +++ b/crates/platform/src/api/blocks/hash_or_number/txs.rs @@ -1,19 +1,2 @@ -use crate::blocks::{BlockData, BlocksPath}; -use crate::{api::ApiResult, server::state::AppState}; -use axum::extract::{Path, Query, State}; -use bf_common::pagination::{Pagination, PaginationQuery}; - -pub async fn route( - State(state): State, - Query(pagination_query): Query, - Path(blocks_path): Path, -) -> ApiResult> { - let block_data = BlockData::from_string(blocks_path.hash_or_number)?; - let pagination = Pagination::from_query(pagination_query)?; - let data_node = state.data_node()?; - - data_node - .blocks() - .txs(&block_data.hash_or_number, &pagination) - .await -} +pub mod cbor; +pub mod root; diff --git a/crates/platform/src/api/blocks/hash_or_number/txs/cbor.rs b/crates/platform/src/api/blocks/hash_or_number/txs/cbor.rs new file mode 100644 index 00000000..078d4cef --- /dev/null +++ b/crates/platform/src/api/blocks/hash_or_number/txs/cbor.rs @@ -0,0 +1,20 @@ +use crate::blocks::{BlockData, BlocksPath}; +use crate::{api::ApiResult, server::state::AppState}; +use axum::extract::{Path, Query, State}; +use bf_api_provider::types::BlocksTxsCborResponse; +use bf_common::pagination::{Pagination, PaginationQuery}; + +pub async fn route( + State(state): State, + Query(pagination_query): Query, + Path(blocks_path): Path, +) -> ApiResult { + let block_data = BlockData::from_string(blocks_path.hash_or_number)?; + let pagination = Pagination::from_query(pagination_query)?; + let data_node = state.data_node()?; + + data_node + .blocks() + .txs_cbor(&block_data.hash_or_number, &pagination) + .await +} diff --git a/crates/platform/src/api/blocks/hash_or_number/txs/root.rs b/crates/platform/src/api/blocks/hash_or_number/txs/root.rs new file mode 100644 index 00000000..e87370f4 --- /dev/null +++ b/crates/platform/src/api/blocks/hash_or_number/txs/root.rs @@ -0,0 +1,19 @@ +use crate::blocks::{BlockData, BlocksPath}; +use crate::{api::ApiResult, server::state::AppState}; +use axum::extract::{Path, Query, State}; +use bf_common::pagination::{Pagination, PaginationQuery}; + +pub async fn route( + State(state): State, + Query(pagination_query): Query, + Path(blocks_path): Path, +) -> ApiResult> { + let block_data = BlockData::from_string(blocks_path.hash_or_number)?; + let pagination = Pagination::from_query(pagination_query)?; + let data_node = state.data_node()?; + + data_node + .blocks() + .txs(&block_data.hash_or_number, &pagination) + .await +} diff --git a/crates/platform/src/api/blocks/latest/txs.rs b/crates/platform/src/api/blocks/latest/txs.rs index efb670a1..a708b0b5 100644 --- a/crates/platform/src/api/blocks/latest/txs.rs +++ b/crates/platform/src/api/blocks/latest/txs.rs @@ -1,8 +1,2 @@ -use crate::{api::ApiResult, server::state::AppState}; -use axum::extract::State; - -pub async fn route(State(state): State) -> ApiResult> { - let data_node = state.data_node()?; - - data_node.blocks().latest_txs().await -} +pub mod cbor; +pub mod root; diff --git a/crates/platform/src/api/blocks/latest/txs/cbor.rs b/crates/platform/src/api/blocks/latest/txs/cbor.rs new file mode 100644 index 00000000..f5581e7b --- /dev/null +++ b/crates/platform/src/api/blocks/latest/txs/cbor.rs @@ -0,0 +1,14 @@ +use crate::{api::ApiResult, server::state::AppState}; +use axum::extract::{Query, State}; +use bf_api_provider::types::BlocksTxsCborResponse; +use bf_common::pagination::{Pagination, PaginationQuery}; + +pub async fn route( + State(state): State, + Query(pagination_query): Query, +) -> ApiResult { + let pagination = Pagination::from_query(pagination_query)?; + let data_node = state.data_node()?; + + data_node.blocks().latest_txs_cbor(&pagination).await +} diff --git a/crates/platform/src/api/blocks/latest/txs/root.rs b/crates/platform/src/api/blocks/latest/txs/root.rs new file mode 100644 index 00000000..efb670a1 --- /dev/null +++ b/crates/platform/src/api/blocks/latest/txs/root.rs @@ -0,0 +1,8 @@ +use crate::{api::ApiResult, server::state::AppState}; +use axum::extract::State; + +pub async fn route(State(state): State) -> ApiResult> { + let data_node = state.data_node()?; + + data_node.blocks().latest_txs().await +} diff --git a/crates/platform/src/api/pools/retiring.rs b/crates/platform/src/api/pools/retiring.rs index 5217d72d..631cb7e1 100644 --- a/crates/platform/src/api/pools/retiring.rs +++ b/crates/platform/src/api/pools/retiring.rs @@ -1,6 +1,14 @@ -use crate::{BlockfrostError, api::ApiResult}; +use crate::{api::ApiResult, server::state::AppState}; +use axum::extract::{Query, State}; use bf_api_provider::types::PoolsRetiresResponse; +use bf_common::pagination::{Pagination, PaginationQuery}; -pub async fn route() -> ApiResult { - Err(BlockfrostError::not_found()) +pub async fn route( + State(state): State, + Query(pagination_query): Query, +) -> ApiResult { + let data_node = state.data_node()?; + let pagination = Pagination::from_query(pagination_query)?; + + data_node.pools().retiring(&pagination).await } diff --git a/crates/platform/src/server/routes/hidden.rs b/crates/platform/src/server/routes/hidden.rs index f9b263f6..27a4ffdc 100644 --- a/crates/platform/src/server/routes/hidden.rs +++ b/crates/platform/src/server/routes/hidden.rs @@ -25,6 +25,7 @@ pub fn get_hidden_api_routes(enable_metrics: bool) -> Router { .route("/accounts/{stake_address}/addresses/assets", get(accounts::stake_address::addresses::assets::route)) .route("/accounts/{stake_address}/addresses/total", get(accounts::stake_address::addresses::total::route)) .route("/accounts/{stake_address}/utxos", get(accounts::stake_address::utxos::route)) + .route("/accounts/{stake_address}/transactions", get(accounts::stake_address::transactions::route)) // addresses .route("/addresses/{address}", get(addresses::address::root::route)) @@ -47,12 +48,14 @@ pub fn get_hidden_api_routes(enable_metrics: bool) -> Router { .route("/blocks/epoch/{epoch_number}/slot/{slot_number}", get(blocks::epoch::epoch_number::slot::slot_number::route)) .route("/blocks/slot/{slot_number}", get(blocks::slot::slot_number::route)) .route("/blocks/latest", get(blocks::latest::root::route)) - .route("/blocks/latest/txs", get(blocks::latest::txs::route)) + .route("/blocks/latest/txs", get(blocks::latest::txs::root::route)) + .route("/blocks/latest/txs/cbor", get(blocks::latest::txs::cbor::route)) .route("/blocks/{hash_or_number}", get(blocks::hash_or_number::root::route)) .route("/blocks/{hash_or_number}/addresses", get(blocks::hash_or_number::addresses::route)) .route("/blocks/{hash_or_number}/next", get(blocks::hash_or_number::next::route)) .route("/blocks/{hash_or_number}/previous", get(blocks::hash_or_number::previous::route)) - .route("/blocks/{hash_or_number}/txs", get(blocks::hash_or_number::txs::route)) + .route("/blocks/{hash_or_number}/txs", get(blocks::hash_or_number::txs::root::route)) + .route("/blocks/{hash_or_number}/txs/cbor", get(blocks::hash_or_number::txs::cbor::route)) // epochs .route("/epochs/latest", get(epochs::latest::root::route))