diff --git a/catalyst-gateway/bin/src/db/index/queries/cql/get_stake_address_from_cat_id.cql b/catalyst-gateway/bin/src/db/index/queries/cql/get_stake_address_from_cat_id.cql new file mode 100644 index 000000000000..a0c674b91989 --- /dev/null +++ b/catalyst-gateway/bin/src/db/index/queries/cql/get_stake_address_from_cat_id.cql @@ -0,0 +1,7 @@ +SELECT + stake_address +FROM + catalyst_id_for_stake_address +WHERE + catalyst_id = :catalyst_id +ALLOW FILTERING; \ No newline at end of file diff --git a/catalyst-gateway/bin/src/db/index/queries/mod.rs b/catalyst-gateway/bin/src/db/index/queries/mod.rs index f2de302702ab..90daf615db74 100644 --- a/catalyst-gateway/bin/src/db/index/queries/mod.rs +++ b/catalyst-gateway/bin/src/db/index/queries/mod.rs @@ -12,6 +12,7 @@ use std::{fmt::Debug, sync::Arc}; use anyhow::bail; use crossbeam_skiplist::SkipMap; +use rbac::get_stake_address_from_catalyst_id; use registrations::{ get_all_invalids::GetAllInvalidRegistrationsQuery, get_all_registrations::GetAllRegistrationsQuery, get_from_stake_addr::GetRegistrationQuery, @@ -99,6 +100,8 @@ pub(crate) enum PreparedSelectQuery { CatalystIdByTransactionId, /// Get Catalyst ID by stake address. CatalystIdByStakeAddress, + /// Get stake address by Catalyst ID . + StakeAddressByCatalystId, /// Get RBAC registrations by Catalyst ID. RbacRegistrationsByCatalystId, /// Get invalid RBAC registrations by Catalyst ID. @@ -164,6 +167,8 @@ pub(crate) struct PreparedQueries { sync_status_insert: PreparedStatement, /// Get Catalyst ID by stake address. catalyst_id_by_stake_address_query: PreparedStatement, + /// Get stake address by Catalyst ID. + stake_address_by_catalyst_id_query: PreparedStatement, /// Get Catalyst ID by transaction ID. catalyst_id_by_transaction_id_query: PreparedStatement, /// Get RBAC registrations by Catalyst ID. @@ -211,6 +216,11 @@ impl PreparedQueries { let sync_status_insert = SyncStatusInsertQuery::prepare(session.clone()).await?; let catalyst_id_by_stake_address_query = get_catalyst_id_from_stake_address::Query::prepare(session.clone()).await?; + let stake_address_by_catalyst_id_query = + get_stake_address_from_catalyst_id::GetStakeAddressByCatIDQuery::prepare( + session.clone(), + ) + .await?; let catalyst_id_by_transaction_id_query = get_catalyst_id_from_transaction_id::Query::prepare(session.clone()).await?; let rbac_registrations_by_catalyst_id_query = @@ -267,6 +277,7 @@ impl PreparedQueries { catalyst_id_by_transaction_id_query, get_all_registrations_query: get_all_registrations_query?, get_all_invalid_registrations_query: get_all_invalid_registrations_query?, + stake_address_by_catalyst_id_query, }) } @@ -370,6 +381,9 @@ impl PreparedQueries { PreparedSelectQuery::GetAllInvalidRegistrations => { &self.get_all_invalid_registrations_query }, + PreparedSelectQuery::StakeAddressByCatalystId => { + &self.stake_address_by_catalyst_id_query + }, }; session_execute_iter(session, prepared_stmt, params).await } diff --git a/catalyst-gateway/bin/src/db/index/queries/rbac/get_stake_address_from_catalyst_id.rs b/catalyst-gateway/bin/src/db/index/queries/rbac/get_stake_address_from_catalyst_id.rs new file mode 100644 index 000000000000..eb1b35525524 --- /dev/null +++ b/catalyst-gateway/bin/src/db/index/queries/rbac/get_stake_address_from_catalyst_id.rs @@ -0,0 +1,61 @@ +//! Get stake address by Catalyst ID. + +use std::sync::Arc; + +use scylla::{ + prepared_statement::PreparedStatement, statement::Consistency, + transport::iterator::TypedRowStream, DeserializeRow, SerializeRow, Session, +}; +use tracing::error; + +use crate::db::{ + index::{ + queries::{PreparedQueries, PreparedSelectQuery}, + session::CassandraSession, + }, + types::{DbCatalystId, DbStakeAddress}, +}; + +/// Get stake address from cat id +const QUERY: &str = include_str!("../cql/get_stake_address_from_cat_id.cql"); + +/// Get stake address by Catalyst ID query params. +#[derive(SerializeRow)] +pub(crate) struct GetStakeAddressByCatIDParams { + /// A Catalyst ID. + pub catalyst_id: DbCatalystId, +} + +impl GetStakeAddressByCatIDParams { + /// Creates a new [`GetStakeAddressByCatIDParams`]. + pub(crate) fn new(catalyst_id: DbCatalystId) -> Self { + Self { catalyst_id } + } +} + +/// Get Catalyst ID by stake address query. +#[derive(Debug, Clone, DeserializeRow)] +pub(crate) struct GetStakeAddressByCatIDQuery { + /// Stake address from Catalyst ID for. + pub(crate) stake_address: DbStakeAddress, +} + +impl GetStakeAddressByCatIDQuery { + /// Prepares a get Catalyst ID by stake address query. + pub(crate) async fn prepare(session: Arc) -> anyhow::Result { + PreparedQueries::prepare(session, QUERY, Consistency::All, true) + .await + .inspect_err(|e| error!(error=%e, "Failed to prepare get stake address by Catalyst ID")) + } + + /// Executes a get stake address by Catalyst ID query. + pub(crate) async fn execute( + session: &CassandraSession, params: GetStakeAddressByCatIDParams, + ) -> anyhow::Result> { + session + .execute_iter(PreparedSelectQuery::StakeAddressByCatalystId, params) + .await? + .rows_stream::() + .map_err(Into::into) + } +} diff --git a/catalyst-gateway/bin/src/db/index/queries/rbac/mod.rs b/catalyst-gateway/bin/src/db/index/queries/rbac/mod.rs index 20273a494269..e8db00b3bdf1 100644 --- a/catalyst-gateway/bin/src/db/index/queries/rbac/mod.rs +++ b/catalyst-gateway/bin/src/db/index/queries/rbac/mod.rs @@ -4,3 +4,4 @@ pub(crate) mod get_catalyst_id_from_stake_address; pub(crate) mod get_catalyst_id_from_transaction_id; pub(crate) mod get_rbac_invalid_registrations; pub(crate) mod get_rbac_registrations; +pub(crate) mod get_stake_address_from_catalyst_id; diff --git a/catalyst-gateway/bin/src/service/api/cardano/staking/assets_get.rs b/catalyst-gateway/bin/src/service/api/cardano/staking/assets_get.rs index 2bb2c988e58f..1010dee10a72 100644 --- a/catalyst-gateway/bin/src/service/api/cardano/staking/assets_get.rs +++ b/catalyst-gateway/bin/src/service/api/cardano/staking/assets_get.rs @@ -12,19 +12,25 @@ use tracing::debug; use crate::{ db::index::{ - queries::staked_ada::{ - get_assets_by_stake_address::{ - GetAssetsByStakeAddressParams, GetAssetsByStakeAddressQuery, + queries::{ + rbac::get_stake_address_from_catalyst_id::{ + GetStakeAddressByCatIDParams, GetStakeAddressByCatIDQuery, }, - get_txi_by_txn_hash::{GetTxiByTxnHashesQuery, GetTxiByTxnHashesQueryParams}, - get_txo_by_stake_address::{ - GetTxoByStakeAddressQuery, GetTxoByStakeAddressQueryParams, + staked_ada::{ + get_assets_by_stake_address::{ + GetAssetsByStakeAddressParams, GetAssetsByStakeAddressQuery, + }, + get_txi_by_txn_hash::{GetTxiByTxnHashesQuery, GetTxiByTxnHashesQueryParams}, + get_txo_by_stake_address::{ + GetTxoByStakeAddressQuery, GetTxoByStakeAddressQueryParams, + }, + update_txo_spent::{UpdateTxoSpentQuery, UpdateTxoSpentQueryParams}, }, - update_txo_spent::{UpdateTxoSpentQuery, UpdateTxoSpentQueryParams}, }, session::{CassandraSession, CassandraSessionError}, }, service::common::{ + auth::rbac::token::CatalystRBACTokenV1, objects::cardano::{ network::Network, stake_info::{FullStakeInfo, StakeInfo, StakedTxoAssetInfo}, @@ -347,3 +353,45 @@ fn build_stake_info(mut txo_state: TxoAssetsState, slot_num: SlotNo) -> anyhow:: Ok(stake_info) } + +/// Retrieves the CIP-19 stake address associated with a given Catalyst RBAC token. +pub async fn get_stake_address_from_cat_id( + token: CatalystRBACTokenV1, +) -> anyhow::Result { + // Attempt to acquire sessions + let volatile_session = + CassandraSession::get(false).ok_or(CassandraSessionError::FailedAcquiringSession)?; + let persistent_session = + CassandraSession::get(true).ok_or(CassandraSessionError::FailedAcquiringSession)?; + + // Try getting the stake address from the volatile session + let stake_address = match get_stake_address_for_token(token.clone(), volatile_session).await { + Ok(address) => address, + Err(_) => { + match get_stake_address_for_token(token, persistent_session).await { + Ok(address) => address, + Err(_) => { + return Err(anyhow::anyhow!( + "Cannot get stake addr from volatile or persistent session" + )) + }, + } + }, + }; + + Cip19StakeAddress::try_from(stake_address.to_string()) +} + +/// Fetches the stake address for a given Catalyst token. +async fn get_stake_address_for_token( + token: CatalystRBACTokenV1, session: Arc, +) -> Result { + let params = GetStakeAddressByCatIDParams::new(token.catalyst_id().clone().into()); + let mut results = GetStakeAddressByCatIDQuery::execute(&session, params).await?; + + results + .try_next() + .await? + .map(|row| row.stake_address) + .ok_or_else(|| anyhow::anyhow!("No stake address found")) +} diff --git a/catalyst-gateway/bin/src/service/api/cardano/staking/mod.rs b/catalyst-gateway/bin/src/service/api/cardano/staking/mod.rs index 397e14f88f10..fceab04ff995 100644 --- a/catalyst-gateway/bin/src/service/api/cardano/staking/mod.rs +++ b/catalyst-gateway/bin/src/service/api/cardano/staking/mod.rs @@ -1,9 +1,11 @@ //! Cardano Staking API Endpoints. +use assets_get::{get_stake_address_from_cat_id, Responses}; use poem_openapi::{ param::{Path, Query}, OpenApi, }; +use tracing::debug; use crate::service::{ common::{ @@ -58,4 +60,59 @@ impl Api { )) .await } + + /// Get staked assets v2. + /// + /// This endpoint returns the total Cardano's staked assets to the corresponded + /// user's stake address. + #[oai( + path = "/v2/cardano/assets/", + method = "get", + operation_id = "stakedAssetsGetVersion2", + transform = "schema_version_validation" + )] + async fn staked_ada_get_v2( + &self, + /// The stake address of the user. + /// Should be a valid Bech32 encoded address followed by the https://cips.cardano.org/cip/CIP-19/#stake-addresses. + stake_address: Query>, + /// Cardano network type. + /// If omitted network type is identified from the stake address. + /// If specified it must be correspondent to the network type encoded in the stake + /// address. + /// As `preprod` and `preview` network types in the stake address encoded as a + /// `testnet`, to specify `preprod` or `preview` network type use this + /// query parameter. + network: Query>, + /// A time point at which the assets should be calculated. + /// If omitted latest slot number is used. + asat: Query>, + /// No Authorization required, but Token permitted. + auth: NoneOrRBAC, + ) -> assets_get::AllResponses { + let stake_address = match stake_address.0 { + Some(addr) => addr, + None => { + if let NoneOrRBAC::RBAC(token) = auth { + match get_stake_address_from_cat_id(token.into()).await { + Ok(addr) => addr, + Err(err) => { + debug!("Cannot obtain stake addr from cat id {err}"); + return Responses::NotFound.into(); + }, + } + } else { + debug!("No Stake address or RBAC token present"); + return Responses::NotFound.into(); + } + }, + }; + + Box::pin(assets_get::endpoint( + stake_address, + network.0, + SlotNo::into_option(asat.0), + )) + .await + } } diff --git a/catalyst-gateway/tests/api_tests/api/v1/cardano.py b/catalyst-gateway/tests/api_tests/api/v1/cardano.py index 07de81ca712f..7d38c1ccf59c 100644 --- a/catalyst-gateway/tests/api_tests/api/v1/cardano.py +++ b/catalyst-gateway/tests/api_tests/api/v1/cardano.py @@ -6,7 +6,7 @@ # cardano assets GET def assets(stake_address: str, slot_no: int, token: str): - url = f"{URL}/assets/{stake_address}?asat=SLOT:{slot_no}" + url = f"{URL}/assets?stake_address={stake_address}?asat=SLOT:{slot_no}" headers = { "Authorization": f"Bearer {token}", "Content-Type": "application/json", diff --git a/catalyst-gateway/tests/api_tests/hurl/get_cardano_assets.hurl b/catalyst-gateway/tests/api_tests/hurl/get_cardano_assets.hurl index 92bfc53e2eff..4df0bfa6c8bd 100644 --- a/catalyst-gateway/tests/api_tests/hurl/get_cardano_assets.hurl +++ b/catalyst-gateway/tests/api_tests/hurl/get_cardano_assets.hurl @@ -1,15 +1,15 @@ # Get staked ADA amount: zero assets -GET http://localhost:3030/api/draft/cardano/assets/stake_test1ursne3ndzr4kz8gmhmstu5026erayrnqyj46nqkkfcn0ufss2t7vt +GET http://localhost:3030/api/draft/cardano/assets?stake_address=stake_test1ursne3ndzr4kz8gmhmstu5026erayrnqyj46nqkkfcn0ufss2t7vt HTTP 200 {"persistent":{"ada_amount":9809147618,"native_tokens":[],"slot_number":76323283},"volatile":{"ada_amount":0,"native_tokens":[],"slot_number":0}} # Get staked ADA amount: single asset -GET http://localhost:3030/api/draft/cardano/assets/stake_test1uq7cnze6az9f8ffjrvkxx4ad77jz088frkhzupxcc7y4x8q5x808s +GET http://localhost:3030/api/draft/cardano/assets?stake_address=stake_test1uq7cnze6az9f8ffjrvkxx4ad77jz088frkhzupxcc7y4x8q5x808s HTTP 200 {"persistent":{"ada_amount":8870859858,"native_tokens":[{"amount":3,"asset_name":"GoldRelic","policy_hash":"0x2862c9b33e98096107e2d8b8c072070834db9c91c0d2f3743e75df65"}],"slot_number":76572358},"volatile":{"ada_amount":0,"native_tokens":[],"slot_number":0}} # Get staked ADA amount: multiple assets -GET http://localhost:3030/api/draft/cardano/assets/stake_test1ur66dds0pkf3j5tu7py9tqf7savpv7pgc5g3dd74xy0x2vsldf2mx +GET http://localhost:3030/api/draft/cardano/assets?stake_address=stake_test1ur66dds0pkf3j5tu7py9tqf7savpv7pgc5g3dd74xy0x2vsldf2mx HTTP 200 [Asserts] jsonpath "$.persistent.native_tokens" count == 9 \ No newline at end of file