use crate::consistency::ConsistentIndexCursor;
use crate::data::apys::calculate_apy;
use crate::data::{DataLoader, Db};
use crate::types::cursor::{JsonCursor, Page};
use async_graphql::connection::{Connection, CursorType, Edge};
use async_graphql::dataloader::Loader;
use std::collections::{BTreeMap, HashMap};
use sui_indexer::apis::GovernanceReadApi;
use sui_types::committee::EpochId;
use sui_types::sui_system_state::PoolTokenExchangeRate;
use sui_types::base_types::SuiAddress as NativeSuiAddress;
use super::big_int::BigInt;
use super::move_object::MoveObject;
use super::object::Object;
use super::owner::Owner;
use super::sui_address::SuiAddress;
use super::uint53::UInt53;
use super::validator_credentials::ValidatorCredentials;
use super::{address::Address, base64::Base64};
use crate::error::Error;
use async_graphql::*;
use sui_indexer::apis::governance_api::exchange_rates;
use sui_types::sui_system_state::sui_system_state_summary::SuiValidatorSummary as NativeSuiValidatorSummary;
#[derive(Clone, Debug)]
pub(crate) struct Validator {
pub validator_summary: NativeSuiValidatorSummary,
pub at_risk: Option<u64>,
pub report_records: Option<Vec<Address>>,
pub checkpoint_viewed_at: u64,
pub requested_for_epoch: u64,
}
type EpochStakeSubsidyStarted = u64;
#[async_trait::async_trait]
impl Loader<u64> for Db {
type Value = (
EpochStakeSubsidyStarted,
BTreeMap<NativeSuiAddress, Vec<(EpochId, PoolTokenExchangeRate)>>,
);
type Error = Error;
async fn load(
&self,
keys: &[u64],
) -> Result<
HashMap<
u64,
(
EpochStakeSubsidyStarted,
BTreeMap<NativeSuiAddress, Vec<(EpochId, PoolTokenExchangeRate)>>,
),
>,
Error,
> {
let latest_sui_system_state = self
.inner
.get_latest_sui_system_state()
.await
.map_err(|_| Error::Internal("Failed to fetch latest Sui system state".to_string()))?;
let governance_api = GovernanceReadApi::new(self.inner.clone());
let exchange_rates = exchange_rates(&governance_api, &latest_sui_system_state)
.await
.map_err(|e| Error::Internal(format!("Error fetching exchange rates. {e}")))?;
let mut results = BTreeMap::new();
let epoch_to_filter_out = if let Some(epoch) = keys.first() {
if epoch == &latest_sui_system_state.epoch {
*epoch - 1
} else {
*epoch
}
} else {
latest_sui_system_state.epoch - 1
};
for er in exchange_rates {
results.insert(
er.address,
er.rates
.into_iter()
.filter(|(epoch, _)| epoch <= &epoch_to_filter_out)
.collect(),
);
}
let requested_epoch = match keys.first() {
Some(x) => *x,
None => latest_sui_system_state.epoch,
};
let mut r = HashMap::new();
r.insert(
requested_epoch,
(latest_sui_system_state.stake_subsidy_start_epoch, results),
);
Ok(r)
}
}
type CAddr = JsonCursor<ConsistentIndexCursor>;
#[Object]
impl Validator {
async fn address(&self) -> Address {
Address {
address: SuiAddress::from(self.validator_summary.sui_address),
checkpoint_viewed_at: self.checkpoint_viewed_at,
}
}
async fn credentials(&self) -> Option<ValidatorCredentials> {
let v = &self.validator_summary;
let credentials = ValidatorCredentials {
protocol_pub_key: Some(Base64::from(v.protocol_pubkey_bytes.clone())),
network_pub_key: Some(Base64::from(v.network_pubkey_bytes.clone())),
worker_pub_key: Some(Base64::from(v.worker_pubkey_bytes.clone())),
proof_of_possession: Some(Base64::from(v.proof_of_possession_bytes.clone())),
net_address: Some(v.net_address.clone()),
p2p_address: Some(v.p2p_address.clone()),
primary_address: Some(v.primary_address.clone()),
worker_address: Some(v.worker_address.clone()),
};
Some(credentials)
}
async fn next_epoch_credentials(&self) -> Option<ValidatorCredentials> {
let v = &self.validator_summary;
let credentials = ValidatorCredentials {
protocol_pub_key: v
.next_epoch_protocol_pubkey_bytes
.as_ref()
.map(Base64::from),
network_pub_key: v.next_epoch_network_pubkey_bytes.as_ref().map(Base64::from),
worker_pub_key: v.next_epoch_worker_pubkey_bytes.as_ref().map(Base64::from),
proof_of_possession: v.next_epoch_proof_of_possession.as_ref().map(Base64::from),
net_address: v.next_epoch_net_address.clone(),
p2p_address: v.next_epoch_p2p_address.clone(),
primary_address: v.next_epoch_primary_address.clone(),
worker_address: v.next_epoch_worker_address.clone(),
};
Some(credentials)
}
async fn name(&self) -> Option<String> {
Some(self.validator_summary.name.clone())
}
async fn description(&self) -> Option<String> {
Some(self.validator_summary.description.clone())
}
async fn image_url(&self) -> Option<String> {
Some(self.validator_summary.image_url.clone())
}
async fn project_url(&self) -> Option<String> {
Some(self.validator_summary.project_url.clone())
}
async fn operation_cap(&self, ctx: &Context<'_>) -> Result<Option<MoveObject>> {
MoveObject::query(
ctx,
self.operation_cap_id(),
Object::latest_at(self.checkpoint_viewed_at),
)
.await
.extend()
}
#[graphql(
deprecation = "The staking pool is a wrapped object. Access its fields directly on the \
`Validator` type."
)]
async fn staking_pool(&self) -> Result<Option<MoveObject>> {
Ok(None)
}
async fn staking_pool_id(&self) -> SuiAddress {
self.validator_summary.staking_pool_id.into()
}
#[graphql(
deprecation = "The exchange object is a wrapped object. Access its dynamic fields through \
the `exchangeRatesTable` query."
)]
async fn exchange_rates(&self) -> Result<Option<MoveObject>> {
Ok(None)
}
async fn exchange_rates_table(&self) -> Result<Option<Owner>> {
Ok(Some(Owner {
address: self.validator_summary.exchange_rates_id.into(),
checkpoint_viewed_at: self.checkpoint_viewed_at,
root_version: None,
}))
}
async fn exchange_rates_size(&self) -> Option<UInt53> {
Some(self.validator_summary.exchange_rates_size.into())
}
async fn staking_pool_activation_epoch(&self) -> Option<UInt53> {
self.validator_summary
.staking_pool_activation_epoch
.map(UInt53::from)
}
async fn staking_pool_sui_balance(&self) -> Option<BigInt> {
Some(BigInt::from(
self.validator_summary.staking_pool_sui_balance,
))
}
async fn rewards_pool(&self) -> Option<BigInt> {
Some(BigInt::from(self.validator_summary.rewards_pool))
}
async fn pool_token_balance(&self) -> Option<BigInt> {
Some(BigInt::from(self.validator_summary.pool_token_balance))
}
async fn pending_stake(&self) -> Option<BigInt> {
Some(BigInt::from(self.validator_summary.pending_stake))
}
async fn pending_total_sui_withdraw(&self) -> Option<BigInt> {
Some(BigInt::from(
self.validator_summary.pending_total_sui_withdraw,
))
}
async fn pending_pool_token_withdraw(&self) -> Option<BigInt> {
Some(BigInt::from(
self.validator_summary.pending_pool_token_withdraw,
))
}
async fn voting_power(&self) -> Option<u64> {
Some(self.validator_summary.voting_power)
}
async fn gas_price(&self) -> Option<BigInt> {
Some(BigInt::from(self.validator_summary.gas_price))
}
async fn commission_rate(&self) -> Option<u64> {
Some(self.validator_summary.commission_rate)
}
async fn next_epoch_stake(&self) -> Option<BigInt> {
Some(BigInt::from(self.validator_summary.next_epoch_stake))
}
async fn next_epoch_gas_price(&self) -> Option<BigInt> {
Some(BigInt::from(self.validator_summary.next_epoch_gas_price))
}
async fn next_epoch_commission_rate(&self) -> Option<u64> {
Some(self.validator_summary.next_epoch_commission_rate)
}
async fn at_risk(&self) -> Option<UInt53> {
self.at_risk.map(UInt53::from)
}
async fn report_records(
&self,
ctx: &Context<'_>,
first: Option<u64>,
before: Option<CAddr>,
last: Option<u64>,
after: Option<CAddr>,
) -> Result<Connection<String, Address>> {
let page = Page::from_params(ctx.data_unchecked(), first, after, last, before)?;
let mut connection = Connection::new(false, false);
let Some(addresses) = &self.report_records else {
return Ok(connection);
};
let Some((prev, next, _, cs)) =
page.paginate_consistent_indices(addresses.len(), self.checkpoint_viewed_at)?
else {
return Ok(connection);
};
connection.has_previous_page = prev;
connection.has_next_page = next;
for c in cs {
connection.edges.push(Edge::new(
c.encode_cursor(),
Address {
address: addresses[c.ix].address,
checkpoint_viewed_at: c.c,
},
));
}
Ok(connection)
}
async fn apy(&self, ctx: &Context<'_>) -> Result<Option<u64>, Error> {
let DataLoader(loader) = ctx.data_unchecked();
let (stake_subsidy_start_epoch, exchange_rates) = loader
.load_one(self.requested_for_epoch)
.await?
.ok_or_else(|| Error::Internal("DataLoading exchange rates failed".to_string()))?;
let rates = exchange_rates
.get(&self.validator_summary.sui_address)
.ok_or_else(|| {
Error::Internal(format!(
"Failed to get the exchange rate for this validator address {} for requested epoch {}",
self.validator_summary.sui_address, self.requested_for_epoch
))
})?;
let avg_apy = Some(calculate_apy(stake_subsidy_start_epoch, rates));
Ok(avg_apy.map(|x| (x * 10000.0) as u64))
}
}
impl Validator {
pub fn operation_cap_id(&self) -> SuiAddress {
SuiAddress::from_array(**self.validator_summary.operation_cap_id)
}
}