Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
SELECT
stake_address
FROM
catalyst_id_for_stake_address
WHERE
catalyst_id = :catalyst_id
ALLOW FILTERING;
14 changes: 14 additions & 0 deletions catalyst-gateway/bin/src/db/index/queries/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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 =
Expand Down Expand Up @@ -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,
})
}

Expand Down Expand Up @@ -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
}
Expand Down
Original file line number Diff line number Diff line change
@@ -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<Session>) -> anyhow::Result<PreparedStatement> {
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<TypedRowStream<GetStakeAddressByCatIDQuery>> {
session
.execute_iter(PreparedSelectQuery::StakeAddressByCatalystId, params)
.await?
.rows_stream::<GetStakeAddressByCatIDQuery>()
.map_err(Into::into)
}
}
1 change: 1 addition & 0 deletions catalyst-gateway/bin/src/db/index/queries/rbac/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Original file line number Diff line number Diff line change
Expand Up @@ -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},
Expand Down Expand Up @@ -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<Cip19StakeAddress> {
// 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<CassandraSession>,
) -> Result<crate::db::types::DbStakeAddress, anyhow::Error> {
let params = GetStakeAddressByCatIDParams::new(token.catalyst_id().clone().into());
let mut results = GetStakeAddressByCatIDQuery::execute(&session, params).await?;

results
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure whether this will give us a valid stake_address that we want 🤔. Might require to build the registration chain and get the latest stake address. What do you think? @stanislav-tkach

Copy link
Member

@stanislav-tkach stanislav-tkach May 8, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think that you are right. Anyway, this logic can be greatly simplified when we have the RBAC cache, but we probably cannot wait till that?..

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is label as RC2 so I think so, would be okay to perform a build_reg_chain for now

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

so the (cat_id,stake_addr) in catalyst_id_for_stake_address table is not the latest?

Copy link
Member

@stanislav-tkach stanislav-tkach May 8, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is kind of the latest, but it can lie in several cases. For example:

  1. A root registration with A stake address and CID_1 Catalyst ID.
  2. A role 0 update that changes a stake address from A to B.

After processing these two registrations we would have two entries in the table: [(A, CID_1), (B, CID_1)]. A query with the A address returns CID_1, but that stake address is no longer used by this registration chain, so we should return "not found".

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@stanislav-tkach does this imply to the inverse also e.g here

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is label as RC2 so I think so, would be okay to perform a build_reg_chain for now

@bkioshn do we have any examples of this?

Copy link
Member

@stanislav-tkach stanislav-tkach May 8, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@stanislav-tkach does this imply to the inverse also e.g here

Sorry, I was confused. I described the situation with looking up a Catalyst ID by a stake address. Doing the reverse (to search a stake address with a Catalyst ID) would be simply inefficient using the catalyst_id_for_stake_address table. But there is an issue there too:

  1. A root registration with A stake address and CID_1 Catalyst ID.
  2. Another root registration with A stake address and CID_2 Catalyst ID.

It is allowed to "override" (or restart) a chain and in this case there once again would be multiple entries and it is impossible to tell which one is correct without building all of the chains. We cannot just use the latest one because it can be incorrect.

Sorry if all that is confusing (it is for me), but that is why we are trying to fix it with a different approach.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This will be resolved with the cache update to rbac registrations, if I am not mistaken.
It may be prudent to pause this and rebase it on that work, rather than try and fix those issues here.

Copy link
Contributor

@bkioshn bkioshn May 12, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry for late reply, @cong-or I have one that you can test
The scenario is

Registration 1: root registration
stake1 = stake_test1urmunkf4zd332vnucxmsqkddjhhr6a3ww7675phu7pvun0qvaqsqj
catid1 = preprod.cardano/HM_KavkvqjOjbX9ZeoEZd2mSgsjSJE9FKoVwwbdo04U

Registration 2: root registration
stake2 = stake_test1uzte0jry3w6rn3j63xhzcyn7fv5akggad66aaffyqsqkzxgm0mhky
catid1 = preprod.cardano/HM_KavkvqjOjbX9ZeoEZd2mSgsjSJE9FKoVwwbdo04U

Using catid1, you must get the stake address of registration 2

^ The above example will be invalid, I will generate a new one once the cache is ready

.try_next()
.await?
.map(|row| row.stake_address)
.ok_or_else(|| anyhow::anyhow!("No stake address found"))
}
57 changes: 57 additions & 0 deletions catalyst-gateway/bin/src/service/api/cardano/staking/mod.rs
Original file line number Diff line number Diff line change
@@ -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::{
Expand Down Expand Up @@ -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<Option<Cip19StakeAddress>>,
/// 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<Option<Network>>,
/// A time point at which the assets should be calculated.
/// If omitted latest slot number is used.
asat: Query<Option<cardano::query::AsAt>>,
/// 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
}
}
2 changes: 1 addition & 1 deletion catalyst-gateway/tests/api_tests/api/v1/cardano.py
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
Original file line number Diff line number Diff line change
@@ -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
Loading