1use std::collections::{BTreeMap, HashMap};
6
7use async_graphql::{
8 connection::{Connection, CursorType, Edge},
9 dataloader::Loader,
10 *,
11};
12use futures::TryFutureExt;
13use iota_indexer::apis::GovernanceReadApi;
14use iota_json_rpc::governance_api::median_apy_from_exchange_rates;
15use iota_types::{
16 base_types::IotaAddress as NativeIotaAddress,
17 committee::EpochId,
18 iota_system_state::{
19 PoolTokenExchangeRate,
20 iota_system_state_summary::IotaValidatorSummary as NativeIotaValidatorSummary,
21 },
22};
23
24use crate::{
25 consistency::ConsistentIndexCursor,
26 data::{DataLoader, Db},
27 error::Error,
28 types::{
29 address::Address,
30 base64::Base64,
31 big_int::BigInt,
32 cursor::{JsonCursor, Page},
33 iota_address::IotaAddress,
34 move_object::MoveObject,
35 object::Object,
36 owner::Owner,
37 system_state_summary::SystemStateSummaryView,
38 uint53::UInt53,
39 validator_credentials::ValidatorCredentials,
40 },
41};
42
43#[derive(Clone, Debug)]
44pub(crate) struct Validator {
45 pub validator_summary: NativeIotaValidatorSummary,
46 pub at_risk: Option<u64>,
47 pub report_records: Option<Vec<Address>>,
48 pub checkpoint_viewed_at: u64,
50 pub requested_for_epoch: u64,
53}
54
55impl Loader<u64> for Db {
62 type Value = BTreeMap<NativeIotaAddress, Vec<(EpochId, PoolTokenExchangeRate)>>;
63
64 type Error = Error;
65
66 async fn load(
67 &self,
68 keys: &[u64],
69 ) -> Result<
70 HashMap<u64, BTreeMap<NativeIotaAddress, Vec<(EpochId, PoolTokenExchangeRate)>>>,
71 Error,
72 > {
73 let latest_iota_system_state = self
74 .inner
75 .spawn_blocking(move |this| this.get_latest_iota_system_state())
76 .await
77 .map_err(|_| Error::Internal("Failed to fetch latest IOTA system state".to_string()))?;
78 let governance_api = GovernanceReadApi::new(self.inner.clone());
79
80 let (candidate_rates, pending_rates) = tokio::try_join!(
81 governance_api
82 .candidate_validators_exchange_rate(&latest_iota_system_state)
83 .map_err(|e| {
84 Error::Internal(format!(
85 "Error fetching candidate validators exchange rates. {e}"
86 ))
87 }),
88 governance_api
89 .pending_validators_exchange_rate()
90 .map_err(|e| {
91 Error::Internal(format!(
92 "Error fetching pending validators exchange rates. {e}"
93 ))
94 })
95 )?;
96
97 let mut exchange_rates = governance_api
98 .exchange_rates(&latest_iota_system_state)
99 .await
100 .map_err(|e| Error::Internal(format!("Error fetching exchange rates. {e}")))?;
101
102 exchange_rates.extend(candidate_rates.into_iter());
103 exchange_rates.extend(pending_rates.into_iter());
104
105 let mut results = BTreeMap::new();
106
107 let epoch_to_filter_out = if let Some(epoch) = keys.first() {
114 if epoch == &latest_iota_system_state.epoch() {
115 *epoch - 1
116 } else {
117 *epoch
118 }
119 } else {
120 latest_iota_system_state.epoch() - 1
121 };
122
123 for er in exchange_rates {
130 results.insert(
131 er.address,
132 er.rates
133 .into_iter()
134 .filter(|(epoch, _)| epoch <= &epoch_to_filter_out)
135 .collect(),
136 );
137 }
138
139 let requested_epoch = match keys.first() {
140 Some(x) => *x,
141 None => latest_iota_system_state.epoch(),
142 };
143
144 let mut r = HashMap::new();
145 r.insert(requested_epoch, results);
146
147 Ok(r)
148 }
149}
150
151type CAddr = JsonCursor<ConsistentIndexCursor>;
152
153#[Object]
154impl Validator {
155 async fn address(&self) -> Address {
157 Address {
158 address: IotaAddress::from(self.validator_summary.iota_address),
159 checkpoint_viewed_at: self.checkpoint_viewed_at,
160 }
161 }
162
163 async fn credentials(&self) -> Option<ValidatorCredentials> {
166 let v = &self.validator_summary;
167 let credentials = ValidatorCredentials {
168 authority_pub_key: Some(Base64::from(v.authority_pubkey_bytes.clone())),
169 network_pub_key: Some(Base64::from(v.network_pubkey_bytes.clone())),
170 protocol_pub_key: Some(Base64::from(v.protocol_pubkey_bytes.clone())),
171 proof_of_possession: Some(Base64::from(v.proof_of_possession_bytes.clone())),
172 net_address: Some(v.net_address.clone()),
173 p2p_address: Some(v.p2p_address.clone()),
174 primary_address: Some(v.primary_address.clone()),
175 };
176 Some(credentials)
177 }
178
179 async fn next_epoch_credentials(&self) -> Option<ValidatorCredentials> {
181 let v = &self.validator_summary;
182 let credentials = ValidatorCredentials {
183 authority_pub_key: v
184 .next_epoch_authority_pubkey_bytes
185 .as_ref()
186 .map(Base64::from),
187 network_pub_key: v.next_epoch_network_pubkey_bytes.as_ref().map(Base64::from),
188 protocol_pub_key: v
189 .next_epoch_protocol_pubkey_bytes
190 .as_ref()
191 .map(Base64::from),
192 proof_of_possession: v.next_epoch_proof_of_possession.as_ref().map(Base64::from),
193 net_address: v.next_epoch_net_address.clone(),
194 p2p_address: v.next_epoch_p2p_address.clone(),
195 primary_address: v.next_epoch_primary_address.clone(),
196 };
197 Some(credentials)
198 }
199
200 async fn name(&self) -> Option<String> {
202 Some(self.validator_summary.name.clone())
203 }
204
205 async fn description(&self) -> Option<String> {
207 Some(self.validator_summary.description.clone())
208 }
209
210 async fn image_url(&self) -> Option<String> {
212 Some(self.validator_summary.image_url.clone())
213 }
214
215 async fn project_url(&self) -> Option<String> {
217 Some(self.validator_summary.project_url.clone())
218 }
219
220 async fn operation_cap(&self, ctx: &Context<'_>) -> Result<Option<MoveObject>> {
225 MoveObject::query(
226 ctx,
227 self.operation_cap_id(),
228 Object::latest_at(self.checkpoint_viewed_at),
229 )
230 .await
231 .extend()
232 }
233
234 #[graphql(
237 deprecation = "The staking pool is a wrapped object. Access its fields directly on the \
238 `Validator` type."
239 )]
240 async fn staking_pool(&self) -> Result<Option<MoveObject>> {
241 Ok(None)
242 }
243
244 async fn staking_pool_id(&self) -> IotaAddress {
246 self.validator_summary.staking_pool_id.into()
247 }
248
249 #[graphql(
253 deprecation = "The exchange object is a wrapped object. Access its dynamic fields through \
254 the `exchangeRatesTable` query."
255 )]
256 async fn exchange_rates(&self) -> Result<Option<MoveObject>> {
257 Ok(None)
258 }
259
260 async fn exchange_rates_table(&self) -> Result<Option<Owner>> {
265 Ok(Some(Owner {
266 address: self.validator_summary.exchange_rates_id.into(),
267 checkpoint_viewed_at: self.checkpoint_viewed_at,
268 root_version: None,
269 }))
270 }
271
272 async fn exchange_rates_size(&self) -> Option<UInt53> {
274 Some(self.validator_summary.exchange_rates_size.into())
275 }
276
277 async fn staking_pool_activation_epoch(&self) -> Option<UInt53> {
279 self.validator_summary
280 .staking_pool_activation_epoch
281 .map(UInt53::from)
282 }
283
284 async fn staking_pool_iota_balance(&self) -> Option<BigInt> {
286 Some(BigInt::from(
287 self.validator_summary.staking_pool_iota_balance,
288 ))
289 }
290
291 async fn rewards_pool(&self) -> Option<BigInt> {
293 Some(BigInt::from(self.validator_summary.rewards_pool))
294 }
295
296 async fn pool_token_balance(&self) -> Option<BigInt> {
298 Some(BigInt::from(self.validator_summary.pool_token_balance))
299 }
300
301 async fn pending_stake(&self) -> Option<BigInt> {
303 Some(BigInt::from(self.validator_summary.pending_stake))
304 }
305
306 async fn pending_total_iota_withdraw(&self) -> Option<BigInt> {
309 Some(BigInt::from(
310 self.validator_summary.pending_total_iota_withdraw,
311 ))
312 }
313
314 async fn pending_pool_token_withdraw(&self) -> Option<BigInt> {
317 Some(BigInt::from(
318 self.validator_summary.pending_pool_token_withdraw,
319 ))
320 }
321
322 async fn voting_power(&self) -> Option<u64> {
325 Some(self.validator_summary.voting_power)
326 }
327
328 async fn gas_price(&self) -> Option<BigInt> {
332 Some(BigInt::from(self.validator_summary.gas_price))
333 }
334
335 async fn commission_rate(&self) -> Option<u64> {
337 Some(self.validator_summary.commission_rate)
338 }
339
340 async fn next_epoch_stake(&self) -> Option<BigInt> {
343 Some(BigInt::from(self.validator_summary.next_epoch_stake))
344 }
345
346 async fn next_epoch_gas_price(&self) -> Option<BigInt> {
348 Some(BigInt::from(self.validator_summary.next_epoch_gas_price))
349 }
350
351 async fn next_epoch_commission_rate(&self) -> Option<u64> {
353 Some(self.validator_summary.next_epoch_commission_rate)
354 }
355
356 async fn at_risk(&self) -> Option<UInt53> {
359 self.at_risk.map(UInt53::from)
360 }
361
362 async fn report_records(
364 &self,
365 ctx: &Context<'_>,
366 first: Option<u64>,
367 before: Option<CAddr>,
368 last: Option<u64>,
369 after: Option<CAddr>,
370 ) -> Result<Connection<String, Address>> {
371 let page = Page::from_params(ctx.data_unchecked(), first, after, last, before)?;
372
373 let mut connection = Connection::new(false, false);
374 let Some(addresses) = &self.report_records else {
375 return Ok(connection);
376 };
377
378 let Some((prev, next, _, cs)) =
379 page.paginate_consistent_indices(addresses.len(), self.checkpoint_viewed_at)?
380 else {
381 return Ok(connection);
382 };
383
384 connection.has_previous_page = prev;
385 connection.has_next_page = next;
386
387 for c in cs {
388 connection.edges.push(Edge::new(
389 c.encode_cursor(),
390 Address {
391 address: addresses[c.ix].address,
392 checkpoint_viewed_at: c.c,
393 },
394 ));
395 }
396
397 Ok(connection)
398 }
399
400 async fn apy(&self, ctx: &Context<'_>) -> Result<Option<u64>, Error> {
403 let DataLoader(loader) = ctx.data_unchecked();
404 let exchange_rates = loader
405 .load_one(self.requested_for_epoch)
406 .await?
407 .ok_or_else(|| Error::Internal("DataLoading exchange rates failed".to_string()))?;
408 let rates = exchange_rates
409 .get(&self.validator_summary.iota_address)
410 .ok_or_else(|| {
411 Error::Internal(format!(
412 "Failed to get the exchange rate for this validator address {} for requested epoch {}",
413 self.validator_summary.iota_address, self.requested_for_epoch
414 ))
415 })?
416 .iter()
417 .map(|(_, exchange_rate)| exchange_rate);
418
419 let avg_apy = Some(median_apy_from_exchange_rates(rates));
420
421 Ok(avg_apy.map(|x| (x * 10000.0) as u64))
422 }
423}
424
425impl Validator {
426 pub fn operation_cap_id(&self) -> IotaAddress {
427 IotaAddress::from_array(**self.validator_summary.operation_cap_id)
428 }
429}