1use std::{
6 collections::{BTreeMap, BTreeSet, HashMap},
7 ops::RangeInclusive,
8};
9
10use async_graphql::{
11 connection::{Connection, CursorType, Edge},
12 dataloader::Loader,
13 *,
14};
15use diesel::{ExpressionMethods, OptionalExtension, QueryDsl};
16use fastcrypto::encoding::{Base58, Encoding};
17use iota_indexer::{
18 models::checkpoints::StoredCheckpoint, pruning::CommitterTables, schema::checkpoints,
19};
20use iota_sdk_types::{CheckpointDigest, CheckpointSummary};
21use serde::{Deserialize, Serialize};
22
23use crate::{
24 config::DEFAULT_PAGE_SIZE,
25 connection::ScanConnection,
26 consistency::Checkpointed,
27 data::{Conn, DataLoader, Db, DbConnection, QueryExecutor},
28 error::Error,
29 types::{
30 base64::Base64,
31 cursor::{self, Page, ScanLimited, Target},
32 date_time::DateTime,
33 digest::Digest,
34 epoch::Epoch,
35 gas::GasCostSummary,
36 transaction_block::{self, TransactionBlock, TransactionBlockFilter},
37 uint53::UInt53,
38 },
39};
40
41#[derive(Default, InputObject)]
44pub(crate) struct CheckpointId {
45 pub digest: Option<Digest>,
46 pub sequence_number: Option<UInt53>,
47}
48
49#[derive(Copy, Clone, Hash, Eq, PartialEq, Debug)]
52struct SeqNumKey {
53 pub sequence_number: u64,
54 pub digest: Option<Digest>,
58 pub checkpoint_viewed_at: u64,
59}
60
61#[derive(Copy, Clone, Hash, Eq, PartialEq, Debug)]
64struct DigestKey {
65 pub digest: Digest,
66 pub checkpoint_viewed_at: u64,
67}
68
69#[derive(Clone)]
70pub(crate) struct Checkpoint {
71 pub stored: StoredCheckpoint,
74 pub checkpoint_viewed_at: u64,
76}
77
78pub(crate) type Cursor = cursor::JsonCursor<CheckpointCursor>;
79
80#[derive(Serialize, Deserialize, Clone, PartialEq, Eq)]
84pub(crate) struct CheckpointCursor {
85 #[serde(rename = "c")]
87 pub checkpoint_viewed_at: UInt53,
88 #[serde(rename = "s")]
89 pub sequence_number: UInt53,
90}
91
92#[Object]
95impl Checkpoint {
96 #[graphql(complexity = 0)]
101 async fn digest(&self) -> Result<String> {
102 Ok(self.digest_impl().extend()?.to_base58())
103 }
104
105 #[graphql(complexity = 0)]
112 async fn bcs(&self) -> Result<Option<Base64>> {
113 if self.stored.content_digest.is_none() || self.stored.version_specific_data.is_none() {
114 return Ok(None);
115 }
116 let summary = CheckpointSummary::try_from(self.stored.clone())
117 .map_err(|e| Error::Internal(format!("Failed to rebuild checkpoint summary: {e}")))
118 .extend()?;
119 let bytes = bcs::to_bytes(&summary)
120 .map_err(|e| Error::Internal(format!("Failed to serialize checkpoint summary: {e}")))
121 .extend()?;
122 Ok(Some(Base64::from(bytes)))
123 }
124
125 #[graphql(complexity = 0)]
128 async fn sequence_number(&self) -> Result<UInt53> {
129 UInt53::try_from(self.sequence_number_impl()).extend()
130 }
131
132 #[graphql(complexity = 0)]
136 async fn timestamp(&self) -> Result<DateTime> {
137 DateTime::from_ms(self.stored.timestamp_ms).extend()
138 }
139
140 #[graphql(complexity = 0)]
143 async fn validator_signatures(&self) -> Base64 {
144 Base64::from(&self.stored.validator_signature)
145 }
146
147 #[graphql(complexity = 0)]
149 async fn previous_checkpoint_digest(&self) -> Option<String> {
150 self.stored
151 .previous_checkpoint_digest
152 .as_ref()
153 .map(Base58::encode)
154 }
155
156 #[graphql(complexity = 0)]
159 async fn network_total_transactions(&self) -> Result<Option<UInt53>> {
160 Ok(Some(
161 UInt53::try_from(self.network_total_transactions_impl()).extend()?,
162 ))
163 }
164
165 #[graphql(complexity = 0)]
170 async fn rolling_gas_summary(&self) -> Option<GasCostSummary> {
171 Some(GasCostSummary {
172 computation_cost: self.stored.computation_cost as u64,
173 computation_cost_burned: self.stored.computation_cost_burned(),
174 storage_cost: self.stored.storage_cost as u64,
175 storage_rebate: self.stored.storage_rebate as u64,
176 non_refundable_storage_fee: self.stored.non_refundable_storage_fee as u64,
177 })
178 }
179
180 async fn epoch(&self, ctx: &Context<'_>) -> Result<Option<Epoch>> {
182 Epoch::query(
183 ctx,
184 Some(self.stored.epoch as u64),
185 self.checkpoint_viewed_at,
186 )
187 .await
188 .extend()
189 }
190
191 #[graphql(
220 complexity = "first.or(last).unwrap_or(DEFAULT_PAGE_SIZE as u64) as usize * child_complexity"
221 )]
222 async fn transaction_blocks(
223 &self,
224 ctx: &Context<'_>,
225 first: Option<u64>,
226 after: Option<transaction_block::Cursor>,
227 last: Option<u64>,
228 before: Option<transaction_block::Cursor>,
229 filter: Option<TransactionBlockFilter>,
230 #[graphql(
231 deprecation = "`scanLimit` will be removed with v1.38, along with the support for combining complex filters."
232 )]
233 scan_limit: Option<u64>,
234 ) -> Result<ScanConnection<String, TransactionBlock>> {
235 let page = Page::from_params(ctx.data_unchecked(), first, after, last, before)?;
236
237 let Some(filter) = filter
238 .unwrap_or_default()
239 .intersect(TransactionBlockFilter {
240 at_checkpoint: Some(UInt53::try_from(self.stored.sequence_number as u64).extend()?),
241 ..Default::default()
242 })
243 else {
244 return Ok(ScanConnection::new(false, false));
245 };
246
247 TransactionBlock::paginate(ctx, page, filter, self.checkpoint_viewed_at, scan_limit)
248 .await
249 .extend()
250 }
251}
252
253impl CheckpointId {
254 pub(crate) fn by_seq_num(seq_num: u64) -> Result<Self, Error> {
255 Ok(CheckpointId {
256 sequence_number: Some(seq_num.try_into()?),
257 digest: None,
258 })
259 }
260}
261
262impl Checkpoint {
263 pub(crate) fn sequence_number_impl(&self) -> u64 {
264 self.stored.sequence_number as u64
265 }
266
267 pub(crate) fn network_total_transactions_impl(&self) -> u64 {
268 self.stored.network_total_transactions as u64
269 }
270
271 pub(crate) fn digest_impl(&self) -> Result<CheckpointDigest, Error> {
272 CheckpointDigest::from_bytes(self.stored.checkpoint_digest.clone())
273 .map_err(|e| Error::Internal(format!("Failed to deserialize checkpoint digest: {e}")))
274 }
275
276 pub(crate) async fn query(
280 ctx: &Context<'_>,
281 filter: CheckpointId,
282 checkpoint_viewed_at: u64,
283 ) -> Result<Option<Self>, Error> {
284 match filter {
285 CheckpointId {
286 sequence_number: Some(sequence_number),
287 digest,
288 } => {
289 let DataLoader(dl) = ctx.data_unchecked();
290 dl.load_one(SeqNumKey {
291 sequence_number: sequence_number.into(),
292 digest,
293 checkpoint_viewed_at,
294 })
295 .await
296 }
297
298 CheckpointId {
299 sequence_number: None,
300 digest: Some(digest),
301 } => {
302 let DataLoader(dl) = ctx.data_unchecked();
303 dl.load_one(DigestKey {
304 digest,
305 checkpoint_viewed_at,
306 })
307 .await
308 }
309
310 CheckpointId {
311 sequence_number: None,
312 digest: None,
313 } => Checkpoint::query_latest_at(ctx.data_unchecked(), checkpoint_viewed_at).await,
314 }
315 }
316
317 async fn query_latest_at(db: &Db, checkpoint_viewed_at: u64) -> Result<Option<Self>, Error> {
321 use checkpoints::dsl;
322
323 let stored: Option<StoredCheckpoint> = db
324 .execute(move |conn| {
325 conn.first(move || {
326 dsl::checkpoints
327 .filter(dsl::sequence_number.le(checkpoint_viewed_at as i64))
328 .order_by(dsl::sequence_number.desc())
329 })
330 .optional()
331 })
332 .await
333 .map_err(|e| Error::Internal(format!("Failed to fetch checkpoint: {e}")))?;
334
335 Ok(stored.map(|stored| Checkpoint {
336 stored,
337 checkpoint_viewed_at,
338 }))
339 }
340
341 pub(crate) fn query_timestamp(
345 conn: &mut Conn<'_>,
346 seq_num: u64,
347 ) -> Result<u64, diesel::result::Error> {
348 use checkpoints::dsl;
349
350 let stored: i64 = conn.first(|| {
351 dsl::checkpoints
352 .select(dsl::timestamp_ms)
353 .filter(dsl::sequence_number.eq(seq_num as i64))
354 })?;
355
356 Ok(stored as u64)
357 }
358
359 async fn pagination_range(
368 db: &Db,
369 filter: Option<u64>,
370 checkpoint_viewed_at: u64,
371 ) -> Result<Option<(u64, u64)>, Error> {
372 let Some(epoch) = filter else {
373 return Ok(Some((0, checkpoint_viewed_at)));
374 };
375
376 let row: Option<(i64, Option<i64>)> = db
377 .execute(move |conn| {
378 use iota_indexer::schema::epochs::dsl as e;
379 conn.first(move || {
380 e::epochs
381 .select((e::first_checkpoint_id, e::last_checkpoint_id))
382 .filter(e::epoch.eq(epoch as i64))
383 })
384 .optional()
385 })
386 .await
387 .map_err(|err| Error::Internal(format!("Failed to fetch epoch range: {err}")))?;
388
389 Ok(row.map(|(first, last)| {
390 let hi = last
391 .map(|l| std::cmp::min(l as u64, checkpoint_viewed_at))
392 .unwrap_or(checkpoint_viewed_at);
393 (first as u64, hi)
394 }))
395 }
396
397 pub(crate) async fn paginate(
415 db: &Db,
416 page: Page<Cursor>,
417 filter: Option<u64>,
418 checkpoint_viewed_at: u64,
419 ) -> Result<Connection<String, Checkpoint>, Error> {
420 let cursor_viewed_at = page.validate_cursor_consistency()?;
421 let checkpoint_viewed_at = cursor_viewed_at.unwrap_or(checkpoint_viewed_at);
422
423 if page.limit() == 0 {
424 return Ok(Connection::new(false, false));
425 }
426
427 let Some((mut absolute_lo_incl, absolute_hi_incl)) =
428 Self::pagination_range(db, filter, checkpoint_viewed_at).await?
429 else {
430 return Ok(Connection::new(false, false));
431 };
432
433 if !db.inner.is_fallback_enabled() {
435 if let Some(lowest_unpruned_cp) = db
436 .inner
437 .watermark_cache()
438 .get_lowest_available_cp_for_tables(&[CommitterTables::Checkpoints])
439 .map(|w| w as u64)
440 {
441 absolute_lo_incl = absolute_lo_incl.max(lowest_unpruned_cp);
442 }
443 }
444
445 let available_range = absolute_lo_incl..=absolute_hi_incl;
446
447 if available_range.is_empty() {
448 return Err(Error::DataPruned(
449 "all checkpoints in the requested range have been pruned".into(),
450 ));
451 }
452
453 let Some(page_range) = page.narrow_to_available_range(&available_range)? else {
454 return Ok(Connection::new(false, false));
455 };
456
457 let limit = page.limit();
459 let picked_seqs: Vec<u64> = if page.is_from_front() {
460 page_range.take(limit).collect()
461 } else {
462 page_range.rev().take(limit).collect()
463 };
464 let mut all_rows: Vec<StoredCheckpoint> = db
465 .inner
466 .get_stored_checkpoints_by_seqs_with_fallback(picked_seqs.clone())
467 .await
468 .map_err(|err| Error::Internal(format!("Failed to fetch checkpoints: {err}")))?
469 .into_iter()
470 .flatten()
471 .collect();
472 all_rows.sort_by_key(|s| s.sequence_number);
473
474 if all_rows.len() < picked_seqs.len() {
478 let picked: BTreeSet<u64> = picked_seqs.iter().copied().collect();
479 let returned: BTreeSet<u64> =
480 all_rows.iter().map(|r| r.sequence_number as u64).collect();
481 let misses: Vec<u64> = picked.difference(&returned).copied().collect();
482 return Err(Error::Internal(format!(
483 "checkpoints {misses:?} expected to be available but not found"
484 )));
485 }
486
487 let fetched_lo = all_rows.first().expect("checked non-empty").sequence_number as u64;
488 let fetched_hi = all_rows.last().expect("checked non-empty").sequence_number as u64;
489 let has_prev = fetched_lo > absolute_lo_incl;
490 let has_next = fetched_hi < absolute_hi_incl;
491
492 let mut conn = Connection::new(has_prev, has_next);
493 for stored in all_rows {
494 let cursor = stored.cursor(checkpoint_viewed_at).encode_cursor();
495 conn.edges.push(Edge::new(
496 cursor,
497 Checkpoint {
498 stored,
499 checkpoint_viewed_at,
500 },
501 ));
502 }
503
504 Ok(conn)
505 }
506}
507
508impl Target<Cursor> for StoredCheckpoint {
509 fn cursor(&self, checkpoint_viewed_at: u64) -> Cursor {
510 Cursor::new(CheckpointCursor {
511 checkpoint_viewed_at: UInt53::new_unchecked(checkpoint_viewed_at),
512 sequence_number: UInt53::new_unchecked(self.sequence_number as u64),
513 })
514 }
515}
516
517impl Checkpointed for Cursor {
518 fn checkpoint_viewed_at(&self) -> u64 {
519 self.checkpoint_viewed_at.into()
520 }
521}
522
523impl ScanLimited for Cursor {}
524
525impl Page<Cursor> {
526 fn narrow_to_available_range(
534 &self,
535 available: &RangeInclusive<u64>,
536 ) -> Result<Option<RangeInclusive<u64>>, Error> {
537 let lo = *available.start();
538 let hi = *available.end();
539
540 let after = self
541 .after()
542 .map(|c| ("after", u64::from(c.sequence_number)));
543 let before = self
544 .before()
545 .map(|c| ("before", u64::from(c.sequence_number)));
546
547 if let (Some((_, after_seq)), Some((_, before_seq))) = (after, before) {
549 if after_seq > before_seq {
550 return Ok(None);
551 }
552 }
553
554 if let Some((name, seq)) = after.or(before) {
557 if seq < lo {
558 return Err(Error::DataPruned(format!(
559 "`{name}` cursor (seq {seq}) is below the available range {available:?}"
560 )));
561 }
562 }
563 if let Some((name, seq)) = before.or(after) {
564 if seq > hi {
565 return Err(Error::Client(format!(
566 "`{name}` cursor (seq {seq}) is above the available range {available:?}"
567 )));
568 }
569 }
570
571 let page_lo = after.map(|(_, seq)| seq.saturating_add(1)).unwrap_or(lo);
573 let page_hi = before.map(|(_, seq)| seq.saturating_sub(1)).unwrap_or(hi);
574 let range = page_lo..=page_hi;
575 if range.is_empty() {
576 Ok(None)
577 } else {
578 Ok(Some(range))
579 }
580 }
581}
582
583impl Loader<SeqNumKey> for Db {
584 type Value = Checkpoint;
585 type Error = Error;
586
587 async fn load(&self, keys: &[SeqNumKey]) -> Result<HashMap<SeqNumKey, Checkpoint>, Error> {
588 let seqs: Vec<u64> = keys
590 .iter()
591 .filter(|key| key.checkpoint_viewed_at >= key.sequence_number)
592 .map(|key| key.sequence_number)
593 .collect();
594
595 let rows = self
596 .inner
597 .get_stored_checkpoints_by_seqs_with_fallback(seqs.clone())
598 .await
599 .map_err(|e| Error::Internal(format!("Failed to fetch checkpoints: {e}")))?;
600
601 let checkpoint_id_to_stored: BTreeMap<u64, StoredCheckpoint> = seqs
602 .into_iter()
603 .zip(rows)
604 .filter_map(|(seq, row)| row.map(|stored| (seq, stored)))
605 .collect();
606
607 Ok(keys
608 .iter()
609 .filter_map(|key| {
610 let stored = checkpoint_id_to_stored.get(&key.sequence_number).cloned()?;
611 let checkpoint = Checkpoint {
612 stored,
613 checkpoint_viewed_at: key.checkpoint_viewed_at,
614 };
615
616 let digest = &checkpoint.stored.checkpoint_digest;
617 if matches!(key.digest, Some(d) if d.as_slice() != digest) {
618 None
619 } else {
620 Some((*key, checkpoint))
621 }
622 })
623 .collect())
624 }
625}
626
627impl Loader<DigestKey> for Db {
628 type Value = Checkpoint;
629 type Error = Error;
630
631 async fn load(&self, keys: &[DigestKey]) -> Result<HashMap<DigestKey, Checkpoint>, Error> {
632 let digests: Vec<CheckpointDigest> = keys.iter().map(|key| key.digest.into()).collect();
633
634 let rows = self
635 .inner
636 .get_stored_checkpoints_by_digests_with_fallback(digests.clone())
637 .await
638 .map_err(|e| Error::Internal(format!("Failed to fetch checkpoints: {e}")))?;
639
640 let checkpoint_id_to_stored: BTreeMap<Vec<u8>, StoredCheckpoint> = digests
641 .into_iter()
642 .zip(rows)
643 .filter_map(|(digest, row)| row.map(|stored| (digest.bytes().to_vec(), stored)))
644 .collect();
645
646 Ok(keys
647 .iter()
648 .filter_map(|key| {
649 let DigestKey {
650 digest,
651 checkpoint_viewed_at,
652 } = *key;
653
654 let stored = checkpoint_id_to_stored.get(digest.as_slice()).cloned()?;
655
656 let checkpoint = Checkpoint {
657 stored,
658 checkpoint_viewed_at,
659 };
660
661 let seq_num = checkpoint.stored.sequence_number as u64;
665 (checkpoint_viewed_at >= seq_num).then_some((*key, checkpoint))
666 })
667 .collect())
668 }
669}