From 5a14f87272959fdb640d13d4283219ff48e39ae3 Mon Sep 17 00:00:00 2001 From: Felipe Rosa Date: Thu, 31 Oct 2024 11:17:48 -0300 Subject: [PATCH] feat: RBAC chain root and registrations queries (#1024) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: RBAC chain root and registrations queries * chore: PR changes * chore: fmt * chore: fix checks * chore: fix openapi lints * chore: fix types * chore: fix fmt * chore: fix openapi lints * chore: regenerate openapi sdk --------- Co-authored-by: Steven Johnson Co-authored-by: Oleksandr Prokhorenko Co-authored-by: JoaquĆ­n Rosales --- .../db/index/queries/cql/get_chain_root.cql | 3 + .../cql/get_registrations_by_chain_root.cql | 3 + .../queries/cql/get_role0_key_chain_root.cql | 3 + .../bin/src/db/index/queries/mod.rs | 31 ++- .../db/index/queries/rbac/get_chain_root.rs | 72 +++++++ .../index/queries/rbac/get_registrations.rs | 73 +++++++ .../queries/rbac/get_role0_chain_root.rs | 72 +++++++ .../bin/src/db/index/queries/rbac/mod.rs | 4 + .../bin/src/service/api/cardano/mod.rs | 51 +++++ .../api/cardano/rbac/chain_root_get.rs | 79 ++++++++ .../bin/src/service/api/cardano/rbac/mod.rs | 5 + .../api/cardano/rbac/registrations_get.rs | 94 +++++++++ .../api/cardano/rbac/role0_chain_root_get.rs | 84 +++++++++ .../cat_gateway_api.models.swagger.dart | 178 ++++++++++++++++++ .../cat_gateway_api.models.swagger.g.dart | 45 +++++ .../cat_gateway_api.swagger.chopper.dart | 38 ++++ .../cat_gateway_api.swagger.dart | 49 +++++ 17 files changed, 883 insertions(+), 1 deletion(-) create mode 100644 catalyst-gateway/bin/src/db/index/queries/cql/get_chain_root.cql create mode 100644 catalyst-gateway/bin/src/db/index/queries/cql/get_registrations_by_chain_root.cql create mode 100644 catalyst-gateway/bin/src/db/index/queries/cql/get_role0_key_chain_root.cql create mode 100644 catalyst-gateway/bin/src/db/index/queries/rbac/get_chain_root.rs create mode 100644 catalyst-gateway/bin/src/db/index/queries/rbac/get_registrations.rs create mode 100644 catalyst-gateway/bin/src/db/index/queries/rbac/get_role0_chain_root.rs create mode 100644 catalyst-gateway/bin/src/db/index/queries/rbac/mod.rs create mode 100644 catalyst-gateway/bin/src/service/api/cardano/rbac/chain_root_get.rs create mode 100644 catalyst-gateway/bin/src/service/api/cardano/rbac/mod.rs create mode 100644 catalyst-gateway/bin/src/service/api/cardano/rbac/registrations_get.rs create mode 100644 catalyst-gateway/bin/src/service/api/cardano/rbac/role0_chain_root_get.rs diff --git a/catalyst-gateway/bin/src/db/index/queries/cql/get_chain_root.cql b/catalyst-gateway/bin/src/db/index/queries/cql/get_chain_root.cql new file mode 100644 index 00000000000..2c9b9121cea --- /dev/null +++ b/catalyst-gateway/bin/src/db/index/queries/cql/get_chain_root.cql @@ -0,0 +1,3 @@ +SELECT chain_root +FROM chain_root_for_stake_addr +WHERE stake_addr = :stake_address diff --git a/catalyst-gateway/bin/src/db/index/queries/cql/get_registrations_by_chain_root.cql b/catalyst-gateway/bin/src/db/index/queries/cql/get_registrations_by_chain_root.cql new file mode 100644 index 00000000000..1630453b5b1 --- /dev/null +++ b/catalyst-gateway/bin/src/db/index/queries/cql/get_registrations_by_chain_root.cql @@ -0,0 +1,3 @@ +SELECT transaction_id +FROM RBAC509_registration +WHERE chain_root = :chain_root diff --git a/catalyst-gateway/bin/src/db/index/queries/cql/get_role0_key_chain_root.cql b/catalyst-gateway/bin/src/db/index/queries/cql/get_role0_key_chain_root.cql new file mode 100644 index 00000000000..82fe1224164 --- /dev/null +++ b/catalyst-gateway/bin/src/db/index/queries/cql/get_role0_key_chain_root.cql @@ -0,0 +1,3 @@ +SELECT chain_root +FROM chain_root_for_role0_key +WHERE role0_key = :role0_key diff --git a/catalyst-gateway/bin/src/db/index/queries/mod.rs b/catalyst-gateway/bin/src/db/index/queries/mod.rs index e9f761d1a3e..afb5e79cafc 100644 --- a/catalyst-gateway/bin/src/db/index/queries/mod.rs +++ b/catalyst-gateway/bin/src/db/index/queries/mod.rs @@ -2,6 +2,7 @@ //! //! This improves query execution time. +pub(crate) mod rbac; pub(crate) mod registrations; pub(crate) mod staked_ada; pub(crate) mod sync_status; @@ -10,6 +11,10 @@ use std::{fmt::Debug, sync::Arc}; use anyhow::{bail, Context}; use crossbeam_skiplist::SkipMap; +use rbac::{ + get_chain_root::GetChainRootQuery, get_registrations::GetRegistrationsByChainRootQuery, + get_role0_chain_root::GetRole0ChainRootQuery, +}; use registrations::{ get_from_stake_addr::GetRegistrationQuery, get_from_stake_hash::GetStakeAddrQuery, get_from_vote_key::GetStakeAddrFromVoteKeyQuery, get_invalid::GetInvalidRegistrationQuery, @@ -84,6 +89,12 @@ pub(crate) enum PreparedSelectQuery { StakeAddrFromStakeHash, /// Get stake addr from vote key StakeAddrFromVoteKey, + /// Get chain root by stake address + ChainRootByStakeAddress, + /// Get registrations by chain root + RegistrationsByChainRoot, + /// Get chain root by role0 key + ChainRootByRole0Key, } /// All prepared UPSERT query statements (inserts/updates a single value of data). @@ -139,6 +150,12 @@ pub(crate) struct PreparedQueries { invalid_registrations_from_stake_addr_query: PreparedStatement, /// Insert Sync Status update. sync_status_insert: PreparedStatement, + /// Get chain root by stake address + chain_root_by_stake_address_query: PreparedStatement, + /// Get registrations by chain root + registrations_by_chain_root_query: PreparedStatement, + /// Get chain root by role0 key + chain_root_by_role0_key_query: PreparedStatement, } /// An individual query response that can fail @@ -171,7 +188,11 @@ impl PreparedQueries { let stake_addr_from_stake_hash = GetStakeAddrQuery::prepare(session.clone()).await; let stake_addr_from_vote_key = GetStakeAddrFromVoteKeyQuery::prepare(session.clone()).await; let invalid_registrations = GetInvalidRegistrationQuery::prepare(session.clone()).await; - let sync_status_insert = SyncStatusInsertQuery::prepare(session).await; + let sync_status_insert = SyncStatusInsertQuery::prepare(session.clone()).await; + let chain_root_by_stake_address = GetChainRootQuery::prepare(session.clone()).await; + let registrations_by_chain_root = + GetRegistrationsByChainRootQuery::prepare(session.clone()).await; + let chain_root_by_role0_key = GetRole0ChainRootQuery::prepare(session).await; let ( txo_insert_queries, @@ -216,6 +237,9 @@ impl PreparedQueries { stake_addr_from_vote_key_query: stake_addr_from_vote_key?, invalid_registrations_from_stake_addr_query: invalid_registrations?, sync_status_insert: sync_status_insert?, + chain_root_by_stake_address_query: chain_root_by_stake_address?, + registrations_by_chain_root_query: registrations_by_chain_root?, + chain_root_by_role0_key_query: chain_root_by_role0_key?, }) } @@ -301,6 +325,11 @@ impl PreparedQueries { PreparedSelectQuery::InvalidRegistrationsFromStakeAddr => { &self.invalid_registrations_from_stake_addr_query }, + PreparedSelectQuery::ChainRootByStakeAddress => &self.chain_root_by_stake_address_query, + PreparedSelectQuery::RegistrationsByChainRoot => { + &self.registrations_by_chain_root_query + }, + PreparedSelectQuery::ChainRootByRole0Key => &self.chain_root_by_role0_key_query, }; session diff --git a/catalyst-gateway/bin/src/db/index/queries/rbac/get_chain_root.rs b/catalyst-gateway/bin/src/db/index/queries/rbac/get_chain_root.rs new file mode 100644 index 00000000000..d0160164f6a --- /dev/null +++ b/catalyst-gateway/bin/src/db/index/queries/rbac/get_chain_root.rs @@ -0,0 +1,72 @@ +//! Get chain root by stake address. +use std::sync::Arc; + +use scylla::{ + prepared_statement::PreparedStatement, transport::iterator::TypedRowIterator, SerializeRow, + Session, +}; +use tracing::error; + +use crate::db::index::{ + queries::{PreparedQueries, PreparedSelectQuery}, + session::CassandraSession, +}; + +/// Get get chain root by stake address query string. +const GET_CHAIN_ROOT: &str = include_str!("../cql/get_chain_root.cql"); + +/// Get chain root by stake address query params. +#[derive(SerializeRow)] +pub(crate) struct GetChainRootQueryParams { + /// Stake address to get the chain root for. + pub(crate) stake_address: Vec, +} + +/// Get chain root by stake address query row result +// TODO: https://github.com/input-output-hk/catalyst-voices/issues/828 +// The macro uses expect to signal an error in deserializing values. +#[allow(clippy::expect_used)] +mod result { + use scylla::FromRow; + + /// Get chain root query result. + #[derive(FromRow)] + pub(crate) struct GetChainRootQuery { + /// Chain root for the queries stake address. + pub(crate) chain_root: Vec, + } +} + +/// Get chain root by stake address query. +pub(crate) struct GetChainRootQuery; + +impl GetChainRootQuery { + /// Prepares a get chain root by stake address query. + pub(crate) async fn prepare(session: Arc) -> anyhow::Result { + let get_chain_root_by_stake_address_query = PreparedQueries::prepare( + session, + GET_CHAIN_ROOT, + scylla::statement::Consistency::All, + true, + ) + .await; + + if let Err(ref error) = get_chain_root_by_stake_address_query { + error!(error=%error, "Failed to prepare get chain root by stake address query"); + }; + + get_chain_root_by_stake_address_query + } + + /// Executes a get chain root by stake address query. + pub(crate) async fn execute( + session: &CassandraSession, params: GetChainRootQueryParams, + ) -> anyhow::Result> { + let iter = session + .execute_iter(PreparedSelectQuery::ChainRootByStakeAddress, params) + .await? + .into_typed::(); + + Ok(iter) + } +} diff --git a/catalyst-gateway/bin/src/db/index/queries/rbac/get_registrations.rs b/catalyst-gateway/bin/src/db/index/queries/rbac/get_registrations.rs new file mode 100644 index 00000000000..c3f10087571 --- /dev/null +++ b/catalyst-gateway/bin/src/db/index/queries/rbac/get_registrations.rs @@ -0,0 +1,73 @@ +//! Get registrations by chain root. +use std::sync::Arc; + +use scylla::{ + prepared_statement::PreparedStatement, transport::iterator::TypedRowIterator, SerializeRow, + Session, +}; +use tracing::error; + +use crate::db::index::{ + queries::{PreparedQueries, PreparedSelectQuery}, + session::CassandraSession, +}; + +/// Get get chain root by stake address query string. +const GET_REGISTRATIONS_BY_CHAIN_ROOT_CQL: &str = + include_str!("../cql/get_registrations_by_chain_root.cql"); + +/// Get chain root by stake address query params. +#[derive(SerializeRow)] +pub(crate) struct GetRegistrationsByChainRootQueryParams { + /// Chain root to get registrations for. + pub(crate) chain_root: Vec, +} + +/// Get registrations by chain root query row result +// TODO: https://github.com/input-output-hk/catalyst-voices/issues/828 +// The macro uses expect to signal an error in deserializing values. +#[allow(clippy::expect_used)] +mod result { + use scylla::FromRow; + + /// Get chain root query result. + #[derive(FromRow)] + pub(crate) struct GetRegistrationsByChainRootQuery { + /// Registration transaction id. + pub(crate) transaction_id: Vec, + } +} + +/// Get chain root by stake address query. +pub(crate) struct GetRegistrationsByChainRootQuery; + +impl GetRegistrationsByChainRootQuery { + /// Prepares a get registrations by chain root query. + pub(crate) async fn prepare(session: Arc) -> anyhow::Result { + let get_registrations_by_chain_root_query = PreparedQueries::prepare( + session, + GET_REGISTRATIONS_BY_CHAIN_ROOT_CQL, + scylla::statement::Consistency::All, + true, + ) + .await; + + if let Err(ref error) = get_registrations_by_chain_root_query { + error!(error=%error, "Failed to prepare get registrations by chain root query"); + }; + + get_registrations_by_chain_root_query + } + + /// Executes a get registrations by chain root query. + pub(crate) async fn execute( + session: &CassandraSession, params: GetRegistrationsByChainRootQueryParams, + ) -> anyhow::Result> { + let iter = session + .execute_iter(PreparedSelectQuery::RegistrationsByChainRoot, params) + .await? + .into_typed::(); + + Ok(iter) + } +} diff --git a/catalyst-gateway/bin/src/db/index/queries/rbac/get_role0_chain_root.rs b/catalyst-gateway/bin/src/db/index/queries/rbac/get_role0_chain_root.rs new file mode 100644 index 00000000000..387c2fcb61d --- /dev/null +++ b/catalyst-gateway/bin/src/db/index/queries/rbac/get_role0_chain_root.rs @@ -0,0 +1,72 @@ +//! Get chain root by role0 key. +use std::sync::Arc; + +use scylla::{ + prepared_statement::PreparedStatement, transport::iterator::TypedRowIterator, SerializeRow, + Session, +}; +use tracing::error; + +use crate::db::index::{ + queries::{PreparedQueries, PreparedSelectQuery}, + session::CassandraSession, +}; + +/// Get chain root by role0 key query string. +const GET_ROLE0_KEY_CHAIN_ROOT_CQL: &str = include_str!("../cql/get_role0_key_chain_root.cql"); + +/// Get chain root by role0 key query params. +#[derive(SerializeRow)] +pub(crate) struct GetRole0ChainRootQueryParams { + /// Role0 key to get the chain root for. + pub(crate) role0_key: Vec, +} + +/// Get chain root by role0 key query row result +// TODO: https://github.com/input-output-hk/catalyst-voices/issues/828 +// The macro uses expect to signal an error in deserializing values. +#[allow(clippy::expect_used)] +mod result { + use scylla::FromRow; + + /// Get role0 key chain root query result. + #[derive(FromRow)] + pub(crate) struct GetRole0ChainRootQuery { + /// Chain root. + pub(crate) chain_root: Vec, + } +} + +/// Get chain root by role0 key query. +pub(crate) struct GetRole0ChainRootQuery; + +impl GetRole0ChainRootQuery { + /// Prepares a get chain root by role0 key query. + pub(crate) async fn prepare(session: Arc) -> anyhow::Result { + let get_chain_root_by_role0_key_query = PreparedQueries::prepare( + session, + GET_ROLE0_KEY_CHAIN_ROOT_CQL, + scylla::statement::Consistency::All, + true, + ) + .await; + + if let Err(ref error) = get_chain_root_by_role0_key_query { + error!(error=%error, "Failed to prepare get chain root by role0 key query"); + }; + + get_chain_root_by_role0_key_query + } + + /// Executes a get chain root role0 key query. + pub(crate) async fn execute( + session: &CassandraSession, params: GetRole0ChainRootQueryParams, + ) -> anyhow::Result> { + let iter = session + .execute_iter(PreparedSelectQuery::ChainRootByRole0Key, params) + .await? + .into_typed::(); + + Ok(iter) + } +} diff --git a/catalyst-gateway/bin/src/db/index/queries/rbac/mod.rs b/catalyst-gateway/bin/src/db/index/queries/rbac/mod.rs new file mode 100644 index 00000000000..bbcf0756147 --- /dev/null +++ b/catalyst-gateway/bin/src/db/index/queries/rbac/mod.rs @@ -0,0 +1,4 @@ +//! RBAC queries. +pub(crate) mod get_chain_root; +pub(crate) mod get_registrations; +pub(crate) mod get_role0_chain_root; diff --git a/catalyst-gateway/bin/src/service/api/cardano/mod.rs b/catalyst-gateway/bin/src/service/api/cardano/mod.rs index 9f7efd52847..fcdf2d7d0a1 100644 --- a/catalyst-gateway/bin/src/service/api/cardano/mod.rs +++ b/catalyst-gateway/bin/src/service/api/cardano/mod.rs @@ -15,6 +15,7 @@ use crate::service::{ mod cip36; mod date_time_to_slot_number_get; +mod rbac; mod registration_get; mod staked_ada_get; mod sync_state_get; @@ -188,4 +189,54 @@ impl CardanoApi { ) -> cip36::MultipleRegistrationResponse { cip36::get_associated_vote_key_registrations(vote_key.0, true).await } + + #[oai( + path = "/draft/rbac/chain_root/:stake_address", + method = "get", + operation_id = "rbacChainRootGet" + )] + /// Get RBAC chain root + /// + /// This endpoint returns the RBAC certificate chain root for a given stake address. + async fn rbac_chain_root_get( + &self, + /// Stake address to get the chain root for. + Path(stake_address): Path, + ) -> rbac::chain_root_get::AllResponses { + rbac::chain_root_get::endpoint(stake_address).await + } + + #[oai( + path = "/draft/rbac/registrations/:chain_root", + method = "get", + operation_id = "rbacRegistrations" + )] + /// Get registrations by RBAC chain root + /// + /// This endpoint returns the registrations for a given chain root. + async fn rbac_registrations_get( + &self, + /// Chain root to get the registrations for. + #[oai(validator(max_length = 66, min_length = 64, pattern = "0x[0-9a-f]{64}"))] + Path(chain_root): Path, + ) -> rbac::registrations_get::AllResponses { + rbac::registrations_get::endpoint(chain_root).await + } + + #[oai( + path = "/draft/rbac/role0_chain_root/:role0_key", + method = "get", + operation_id = "rbacRole0KeyChainRoot" + )] + /// Get RBAC chain root for a given role0 key. + /// + /// This endpoint returns the RBAC certificate chain root for a given role 0 key. + async fn rbac_role0_key_chain_root( + &self, + /// Role0 key to get the chain root for. + #[oai(validator(min_length = 34, max_length = 34, pattern = "0x[0-9a-f]{32}"))] + Path(role0_key): Path, + ) -> rbac::role0_chain_root_get::AllResponses { + rbac::role0_chain_root_get::endpoint(role0_key).await + } } diff --git a/catalyst-gateway/bin/src/service/api/cardano/rbac/chain_root_get.rs b/catalyst-gateway/bin/src/service/api/cardano/rbac/chain_root_get.rs new file mode 100644 index 00000000000..7bf6e451f50 --- /dev/null +++ b/catalyst-gateway/bin/src/service/api/cardano/rbac/chain_root_get.rs @@ -0,0 +1,79 @@ +//! Implementation of the GET `/rbac/chain_root` endpoint. +use der_parser::asn1_rs::ToDer; +use futures::StreamExt as _; +use poem_openapi::{payload::Json, ApiResponse, Object}; +use tracing::error; + +use crate::{ + db::index::{ + queries::rbac::get_chain_root::{GetChainRootQuery, GetChainRootQueryParams}, + session::CassandraSession, + }, + service::common::{responses::WithErrorResponses, types::cardano::address::Cip19StakeAddress}, +}; + +/// GET RBAC chain root response. +#[derive(Object)] +pub(crate) struct Response { + /// RBAC certificate chain root. + #[oai(validator(max_length = 66, min_length = 64, pattern = "0x[0-9a-f]{64}"))] + chain_root: String, +} + +/// Endpoint responses. +#[derive(ApiResponse)] +pub(crate) enum Responses { + /// Success returns the chain root hash. + #[oai(status = 200)] + Ok(Json), + /// No chain root found for the given stake address. + #[oai(status = 404)] + NotFound, + /// Internal server error. + #[oai(status = 500)] + InternalServerError, +} + +pub(crate) type AllResponses = WithErrorResponses; + +/// Get chain root endpoint. +pub(crate) async fn endpoint(stake_address: Cip19StakeAddress) -> AllResponses { + let Some(session) = CassandraSession::get(true) else { + error!("Failed to acquire db session"); + return Responses::InternalServerError.into(); + }; + + let Ok(stake_address) = stake_address.to_der_vec() else { + error!("Failed to create stake address vec"); + return Responses::InternalServerError.into(); + }; + + let query_res = + GetChainRootQuery::execute(&session, GetChainRootQueryParams { stake_address }).await; + + match query_res { + Ok(mut row_iter) => { + if let Some(row_res) = row_iter.next().await { + let row = match row_res { + Ok(row) => row, + Err(err) => { + error!(error = ?err, "Failed to parse get chain root by stake address query row"); + return Responses::InternalServerError.into(); + }, + }; + + let res = Response { + chain_root: format!("0x{}", hex::encode(row.chain_root)), + }; + + Responses::Ok(Json(res)).into() + } else { + Responses::NotFound.into() + } + }, + Err(err) => { + error!(error = ?err, "Failed to execute get chain root by stake address query"); + Responses::InternalServerError.into() + }, + } +} diff --git a/catalyst-gateway/bin/src/service/api/cardano/rbac/mod.rs b/catalyst-gateway/bin/src/service/api/cardano/rbac/mod.rs new file mode 100644 index 00000000000..5be171318f4 --- /dev/null +++ b/catalyst-gateway/bin/src/service/api/cardano/rbac/mod.rs @@ -0,0 +1,5 @@ +//! RBAC endpoints. + +pub(crate) mod chain_root_get; +pub(crate) mod registrations_get; +pub(crate) mod role0_chain_root_get; diff --git a/catalyst-gateway/bin/src/service/api/cardano/rbac/registrations_get.rs b/catalyst-gateway/bin/src/service/api/cardano/rbac/registrations_get.rs new file mode 100644 index 00000000000..f130703d897 --- /dev/null +++ b/catalyst-gateway/bin/src/service/api/cardano/rbac/registrations_get.rs @@ -0,0 +1,94 @@ +//! Implementation of the GET `/rbac/registrations` endpoint. +use futures::StreamExt as _; +use poem_openapi::{payload::Json, ApiResponse, Object}; +use tracing::error; + +use crate::{ + db::index::{ + queries::rbac::get_registrations::{ + GetRegistrationsByChainRootQuery, GetRegistrationsByChainRootQueryParams, + }, + session::CassandraSession, + }, + service::common::{objects::cardano::hash::Hash, responses::WithErrorResponses}, +}; + +/// GET RBAC registrations by chain root response list item. +#[derive(Object)] +pub(crate) struct RbacRegistration { + /// Registration transaction hash. + tx_hash: Hash, +} + +/// GET RBAC registrations by chain root response. +#[derive(Object)] +pub(crate) struct RbacRegistrationsResponse { + /// Registrations by RBAC chain root. + #[oai(validator(max_items = "100000"))] + registrations: Vec, +} + +/// Endpoint responses. +#[derive(ApiResponse)] +pub(crate) enum Responses { + /// Success returns a list of registration transaction ids. + #[oai(status = 200)] + Ok(Json), + /// Response for bad requests. + #[oai(status = 400)] + BadRequest, + /// Internal server error. + #[oai(status = 500)] + InternalServerError, +} + +pub(crate) type AllResponses = WithErrorResponses; + +/// Get chain root endpoint. +pub(crate) async fn endpoint(chain_root: String) -> AllResponses { + let Some(session) = CassandraSession::get(true) else { + error!("Failed to acquire db session"); + return Responses::InternalServerError.into(); + }; + + let Ok(decoded_chain_root) = hex::decode(chain_root) else { + return Responses::BadRequest.into(); + }; + + let query_res = GetRegistrationsByChainRootQuery::execute( + &session, + GetRegistrationsByChainRootQueryParams { + chain_root: decoded_chain_root, + }, + ) + .await; + + let mut row_iter = match query_res { + Ok(row_iter) => row_iter, + Err(err) => { + error!( + error = ?err, + "Failed to execute get registrations by chain root query" + ); + return Responses::InternalServerError.into(); + }, + }; + + let mut registrations = Vec::new(); + while let Some(row_res) = row_iter.next().await { + let row = match row_res { + Ok(row) => row, + Err(err) => { + error!(error = ?err, "Failed to parse get registrations by chain root query row"); + return Responses::InternalServerError.into(); + }, + }; + + let item = RbacRegistration { + tx_hash: row.transaction_id.into(), + }; + registrations.push(item); + } + + Responses::Ok(Json(RbacRegistrationsResponse { registrations })).into() +} diff --git a/catalyst-gateway/bin/src/service/api/cardano/rbac/role0_chain_root_get.rs b/catalyst-gateway/bin/src/service/api/cardano/rbac/role0_chain_root_get.rs new file mode 100644 index 00000000000..37a6b1b7a1c --- /dev/null +++ b/catalyst-gateway/bin/src/service/api/cardano/rbac/role0_chain_root_get.rs @@ -0,0 +1,84 @@ +//! Implementation of the GET `/rbac/role0_chain_root` endpoint. +use futures::StreamExt as _; +use poem_openapi::{payload::Json, ApiResponse, Object}; +use tracing::error; + +use crate::{ + db::index::{ + queries::rbac::get_role0_chain_root::{ + GetRole0ChainRootQuery, GetRole0ChainRootQueryParams, + }, + session::CassandraSession, + }, + service::common::responses::WithErrorResponses, +}; + +/// GET RBAC chain root response. +#[derive(Object)] +pub(crate) struct RbacRole0ChainRootResponse { + /// RBAC certificate chain root. + #[oai(validator(max_length = 66, min_length = 64, pattern = "0x[0-9a-f]{64}"))] + chain_root: String, +} + +/// Endpoint responses. +#[derive(ApiResponse)] +pub(crate) enum Responses { + /// Success returns the chain root hash. + #[oai(status = 200)] + Ok(Json), + /// No chain root found for the given stake address. + #[oai(status = 404)] + NotFound, + /// Response for bad requests. + #[oai(status = 400)] + BadRequest, + /// Internal server error. + #[oai(status = 500)] + InternalServerError, +} + +pub(crate) type AllResponses = WithErrorResponses; + +/// Get chain root for role0 key endpoint. +pub(crate) async fn endpoint(role0_key: String) -> AllResponses { + let Some(session) = CassandraSession::get(true) else { + error!("Failed to acquire db session"); + return Responses::InternalServerError.into(); + }; + + let Ok(decoded_role0_key) = hex::decode(role0_key) else { + return Responses::BadRequest.into(); + }; + + let query_res = GetRole0ChainRootQuery::execute(&session, GetRole0ChainRootQueryParams { + role0_key: decoded_role0_key, + }) + .await; + + match query_res { + Ok(mut row_iter) => { + if let Some(row_res) = row_iter.next().await { + let row = match row_res { + Ok(row) => row, + Err(err) => { + error!(error = ?err, "Failed to parse get chain root by role0 key query row"); + return Responses::InternalServerError.into(); + }, + }; + + let res = RbacRole0ChainRootResponse { + chain_root: format!("0x{}", hex::encode(row.chain_root)), + }; + + Responses::Ok(Json(res)).into() + } else { + Responses::NotFound.into() + } + }, + Err(err) => { + error!(error = ?err, "Failed to execute get chain root by role0 key query"); + Responses::InternalServerError.into() + }, + } +} diff --git a/catalyst_voices/packages/catalyst_voices_services/lib/generated/catalyst_gateway/cat_gateway_api.models.swagger.dart b/catalyst_voices/packages/catalyst_voices_services/lib/generated/catalyst_gateway/cat_gateway_api.models.swagger.dart index 78b908c4e7b..925538f3504 100644 --- a/catalyst_voices/packages/catalyst_voices_services/lib/generated/catalyst_gateway/cat_gateway_api.models.swagger.dart +++ b/catalyst_voices/packages/catalyst_voices_services/lib/generated/catalyst_gateway/cat_gateway_api.models.swagger.dart @@ -1148,6 +1148,140 @@ extension $InvalidRegistrationsReportExtension on InvalidRegistrationsReport { } } +@JsonSerializable(explicitToJson: true) +class RbacRegistration { + const RbacRegistration({ + required this.txHash, + }); + + factory RbacRegistration.fromJson(Map json) => + _$RbacRegistrationFromJson(json); + + static const toJsonFactory = _$RbacRegistrationToJson; + Map toJson() => _$RbacRegistrationToJson(this); + + @JsonKey(name: 'tx_hash') + final String txHash; + static const fromJsonFactory = _$RbacRegistrationFromJson; + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other is RbacRegistration && + (identical(other.txHash, txHash) || + const DeepCollectionEquality().equals(other.txHash, txHash))); + } + + @override + String toString() => jsonEncode(this); + + @override + int get hashCode => + const DeepCollectionEquality().hash(txHash) ^ runtimeType.hashCode; +} + +extension $RbacRegistrationExtension on RbacRegistration { + RbacRegistration copyWith({String? txHash}) { + return RbacRegistration(txHash: txHash ?? this.txHash); + } + + RbacRegistration copyWithWrapped({Wrapped? txHash}) { + return RbacRegistration( + txHash: (txHash != null ? txHash.value : this.txHash)); + } +} + +@JsonSerializable(explicitToJson: true) +class RbacRegistrationsResponse { + const RbacRegistrationsResponse({ + required this.registrations, + }); + + factory RbacRegistrationsResponse.fromJson(Map json) => + _$RbacRegistrationsResponseFromJson(json); + + static const toJsonFactory = _$RbacRegistrationsResponseToJson; + Map toJson() => _$RbacRegistrationsResponseToJson(this); + + @JsonKey(name: 'registrations', defaultValue: []) + final List registrations; + static const fromJsonFactory = _$RbacRegistrationsResponseFromJson; + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other is RbacRegistrationsResponse && + (identical(other.registrations, registrations) || + const DeepCollectionEquality() + .equals(other.registrations, registrations))); + } + + @override + String toString() => jsonEncode(this); + + @override + int get hashCode => + const DeepCollectionEquality().hash(registrations) ^ runtimeType.hashCode; +} + +extension $RbacRegistrationsResponseExtension on RbacRegistrationsResponse { + RbacRegistrationsResponse copyWith({List? registrations}) { + return RbacRegistrationsResponse( + registrations: registrations ?? this.registrations); + } + + RbacRegistrationsResponse copyWithWrapped( + {Wrapped>? registrations}) { + return RbacRegistrationsResponse( + registrations: + (registrations != null ? registrations.value : this.registrations)); + } +} + +@JsonSerializable(explicitToJson: true) +class RbacRole0ChainRootResponse { + const RbacRole0ChainRootResponse({ + required this.chainRoot, + }); + + factory RbacRole0ChainRootResponse.fromJson(Map json) => + _$RbacRole0ChainRootResponseFromJson(json); + + static const toJsonFactory = _$RbacRole0ChainRootResponseToJson; + Map toJson() => _$RbacRole0ChainRootResponseToJson(this); + + @JsonKey(name: 'chain_root') + final String chainRoot; + static const fromJsonFactory = _$RbacRole0ChainRootResponseFromJson; + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other is RbacRole0ChainRootResponse && + (identical(other.chainRoot, chainRoot) || + const DeepCollectionEquality() + .equals(other.chainRoot, chainRoot))); + } + + @override + String toString() => jsonEncode(this); + + @override + int get hashCode => + const DeepCollectionEquality().hash(chainRoot) ^ runtimeType.hashCode; +} + +extension $RbacRole0ChainRootResponseExtension on RbacRole0ChainRootResponse { + RbacRole0ChainRootResponse copyWith({String? chainRoot}) { + return RbacRole0ChainRootResponse(chainRoot: chainRoot ?? this.chainRoot); + } + + RbacRole0ChainRootResponse copyWithWrapped({Wrapped? chainRoot}) { + return RbacRole0ChainRootResponse( + chainRoot: (chainRoot != null ? chainRoot.value : this.chainRoot)); + } +} + @JsonSerializable(explicitToJson: true) class RegistrationInfo { const RegistrationInfo({ @@ -1299,6 +1433,50 @@ extension $RejectedFragmentExtension on RejectedFragment { } } +@JsonSerializable(explicitToJson: true) +class Response$ { + const Response$({ + required this.chainRoot, + }); + + factory Response$.fromJson(Map json) => + _$Response$FromJson(json); + + static const toJsonFactory = _$Response$ToJson; + Map toJson() => _$Response$ToJson(this); + + @JsonKey(name: 'chain_root') + final String chainRoot; + static const fromJsonFactory = _$Response$FromJson; + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other is Response$ && + (identical(other.chainRoot, chainRoot) || + const DeepCollectionEquality() + .equals(other.chainRoot, chainRoot))); + } + + @override + String toString() => jsonEncode(this); + + @override + int get hashCode => + const DeepCollectionEquality().hash(chainRoot) ^ runtimeType.hashCode; +} + +extension $Response$Extension on Response$ { + Response$ copyWith({String? chainRoot}) { + return Response$(chainRoot: chainRoot ?? this.chainRoot); + } + + Response$ copyWithWrapped({Wrapped? chainRoot}) { + return Response$( + chainRoot: (chainRoot != null ? chainRoot.value : this.chainRoot)); + } +} + @JsonSerializable(explicitToJson: true) class Sentry { const Sentry({ diff --git a/catalyst_voices/packages/catalyst_voices_services/lib/generated/catalyst_gateway/cat_gateway_api.models.swagger.g.dart b/catalyst_voices/packages/catalyst_voices_services/lib/generated/catalyst_gateway/cat_gateway_api.models.swagger.g.dart index 1a365de3563..47971eb963a 100644 --- a/catalyst_voices/packages/catalyst_voices_services/lib/generated/catalyst_gateway/cat_gateway_api.models.swagger.g.dart +++ b/catalyst_voices/packages/catalyst_voices_services/lib/generated/catalyst_gateway/cat_gateway_api.models.swagger.g.dart @@ -286,6 +286,43 @@ Map _$InvalidRegistrationsReportToJson( 'cip36': instance.cip36, }; +RbacRegistration _$RbacRegistrationFromJson(Map json) => + RbacRegistration( + txHash: json['tx_hash'] as String, + ); + +Map _$RbacRegistrationToJson(RbacRegistration instance) => + { + 'tx_hash': instance.txHash, + }; + +RbacRegistrationsResponse _$RbacRegistrationsResponseFromJson( + Map json) => + RbacRegistrationsResponse( + registrations: (json['registrations'] as List?) + ?.map((e) => RbacRegistration.fromJson(e as Map)) + .toList() ?? + [], + ); + +Map _$RbacRegistrationsResponseToJson( + RbacRegistrationsResponse instance) => + { + 'registrations': instance.registrations.map((e) => e.toJson()).toList(), + }; + +RbacRole0ChainRootResponse _$RbacRole0ChainRootResponseFromJson( + Map json) => + RbacRole0ChainRootResponse( + chainRoot: json['chain_root'] as String, + ); + +Map _$RbacRole0ChainRootResponseToJson( + RbacRole0ChainRootResponse instance) => + { + 'chain_root': instance.chainRoot, + }; + RegistrationInfo _$RegistrationInfoFromJson(Map json) => RegistrationInfo( rewardsAddress: json['rewards_address'] as String, @@ -317,6 +354,14 @@ Map _$RejectedFragmentToJson(RejectedFragment instance) => 'reason': reasonRejectedToJson(instance.reason), }; +Response$ _$Response$FromJson(Map json) => Response$( + chainRoot: json['chain_root'] as String, + ); + +Map _$Response$ToJson(Response$ instance) => { + 'chain_root': instance.chainRoot, + }; + Sentry _$SentryFromJson(Map json) => Sentry( dsn: json['dsn'] as String, release: json['release'] as String?, diff --git a/catalyst_voices/packages/catalyst_voices_services/lib/generated/catalyst_gateway/cat_gateway_api.swagger.chopper.dart b/catalyst_voices/packages/catalyst_voices_services/lib/generated/catalyst_gateway/cat_gateway_api.swagger.chopper.dart index d73ee5f5736..fc8c32af62d 100644 --- a/catalyst_voices/packages/catalyst_voices_services/lib/generated/catalyst_gateway/cat_gateway_api.swagger.chopper.dart +++ b/catalyst_voices/packages/catalyst_voices_services/lib/generated/catalyst_gateway/cat_gateway_api.swagger.chopper.dart @@ -195,6 +195,44 @@ final class _$CatGatewayApi extends CatGatewayApi { return client.send($request); } + @override + Future> _apiDraftRbacChainRootStakeAddressGet( + {required String? stakeAddress}) { + final Uri $url = Uri.parse('/api/draft/rbac/chain_root/${stakeAddress}'); + final Request $request = Request( + 'GET', + $url, + client.baseUrl, + ); + return client.send($request); + } + + @override + Future> + _apiDraftRbacRegistrationsChainRootGet({required String? chainRoot}) { + final Uri $url = Uri.parse('/api/draft/rbac/registrations/${chainRoot}'); + final Request $request = Request( + 'GET', + $url, + client.baseUrl, + ); + return client + .send($request); + } + + @override + Future> + _apiDraftRbacRole0ChainRootRole0KeyGet({required String? role0Key}) { + final Uri $url = Uri.parse('/api/draft/rbac/role0_chain_root/${role0Key}'); + final Request $request = Request( + 'GET', + $url, + client.baseUrl, + ); + return client + .send($request); + } + @override Future> _apiDraftConfigFrontendGet() { final Uri $url = Uri.parse('/api/draft/config/frontend'); diff --git a/catalyst_voices/packages/catalyst_voices_services/lib/generated/catalyst_gateway/cat_gateway_api.swagger.dart b/catalyst_voices/packages/catalyst_voices_services/lib/generated/catalyst_gateway/cat_gateway_api.swagger.dart index cef66d8404e..be78ca77291 100644 --- a/catalyst_voices/packages/catalyst_voices_services/lib/generated/catalyst_gateway/cat_gateway_api.swagger.dart +++ b/catalyst_voices/packages/catalyst_voices_services/lib/generated/catalyst_gateway/cat_gateway_api.swagger.dart @@ -254,6 +254,55 @@ abstract class CatGatewayApi extends ChopperService { _apiDraftCardanoCip36LatestRegistrationVoteKeyGet( {@Query('vote_key') required String? voteKey}); + ///Get RBAC chain root + ///@param stake_address Stake address to get the chain root for. + Future> apiDraftRbacChainRootStakeAddressGet( + {required String? stakeAddress}) { + generatedMapping.putIfAbsent(Response$, () => Response$.fromJsonFactory); + + return _apiDraftRbacChainRootStakeAddressGet(stakeAddress: stakeAddress); + } + + ///Get RBAC chain root + ///@param stake_address Stake address to get the chain root for. + @Get(path: '/api/draft/rbac/chain_root/{stake_address}') + Future> _apiDraftRbacChainRootStakeAddressGet( + {@Path('stake_address') required String? stakeAddress}); + + ///Get registrations by RBAC chain root + ///@param chain_root Chain root to get the registrations for. + Future> + apiDraftRbacRegistrationsChainRootGet({required String? chainRoot}) { + generatedMapping.putIfAbsent(RbacRegistrationsResponse, + () => RbacRegistrationsResponse.fromJsonFactory); + + return _apiDraftRbacRegistrationsChainRootGet(chainRoot: chainRoot); + } + + ///Get registrations by RBAC chain root + ///@param chain_root Chain root to get the registrations for. + @Get(path: '/api/draft/rbac/registrations/{chain_root}') + Future> + _apiDraftRbacRegistrationsChainRootGet( + {@Path('chain_root') required String? chainRoot}); + + ///Get RBAC chain root for a given role0 key. + ///@param role0_key Role0 key to get the chain root for. + Future> + apiDraftRbacRole0ChainRootRole0KeyGet({required String? role0Key}) { + generatedMapping.putIfAbsent(RbacRole0ChainRootResponse, + () => RbacRole0ChainRootResponse.fromJsonFactory); + + return _apiDraftRbacRole0ChainRootRole0KeyGet(role0Key: role0Key); + } + + ///Get RBAC chain root for a given role0 key. + ///@param role0_key Role0 key to get the chain root for. + @Get(path: '/api/draft/rbac/role0_chain_root/{role0_key}') + Future> + _apiDraftRbacRole0ChainRootRole0KeyGet( + {@Path('role0_key') required String? role0Key}); + ///Get the configuration for the frontend. Future> apiDraftConfigFrontendGet() { generatedMapping.putIfAbsent(