Skip to main content

iota_sdk_graphql_client/api/
checkpoints.rs

1// Copyright (c) Mysten Labs, Inc.
2// Modifications Copyright (c) 2026 IOTA Stiftung
3// SPDX-License-Identifier: Apache-2.0
4
5//! Checkpoints API implementation.
6
7use cynic::QueryBuilder;
8use futures::Stream;
9use iota_types::{CheckpointDigest, CheckpointSequenceNumber, CheckpointSummary};
10
11use crate::{
12    GraphQLClient,
13    api::define_query,
14    error::GraphQLResult,
15    pagination::{Page, PaginationFilter, PaginationFilterResponse},
16    query_types::{
17        CheckpointArgs, CheckpointId, CheckpointQueryFragment, CheckpointTotalTxQueryFragment,
18        CheckpointsArgs, CheckpointsQueryFragment,
19    },
20    streams::stream_paginated_query,
21};
22
23define_query! {
24    /// Query for [`GraphQLClient::checkpoints`]. Await it to send the request.
25    #[derive(Clone)]
26    pub struct ListCheckpointsQuery {
27        client: GraphQLClient,
28        pagination: PaginationFilter,
29    }
30    output: GraphQLResult<Page<CheckpointSummary>>;
31}
32
33impl ListCheckpointsQuery {
34    /// Set the page to fetch.
35    pub fn pagination(mut self, pagination: PaginationFilter) -> Self {
36        self.pagination = pagination;
37        self
38    }
39
40    /// Stream every item, page by page, starting at the pagination's cursor
41    /// and in its direction, with its limit as the page size.
42    /// Without a cursor this fetches every checkpoint, which may take many
43    /// requests.
44    pub fn stream(self) -> impl Stream<Item = GraphQLResult<CheckpointSummary>> + Unpin {
45        let pagination = self.pagination.clone();
46        stream_paginated_query(move |page| self.clone().pagination(page).send(), pagination)
47    }
48
49    fn operation<'a>(
50        pagination: &'a PaginationFilterResponse,
51    ) -> cynic::Operation<CheckpointsQueryFragment, CheckpointsArgs<'a>> {
52        CheckpointsQueryFragment::build(CheckpointsArgs {
53            after: pagination.after.as_deref(),
54            before: pagination.before.as_deref(),
55            first: pagination.first,
56            last: pagination.last,
57        })
58    }
59
60    async fn send(self) -> GraphQLResult<Page<CheckpointSummary>> {
61        let Self { client, pagination } = self;
62        let pagination = client.pagination_filter(pagination).await;
63        let response = client.run_query(&Self::operation(&pagination)).await?;
64
65        let cc = response.checkpoints;
66        let page_info = cc.page_info;
67        let nodes = cc
68            .nodes
69            .into_iter()
70            .map(|c| c.try_into())
71            .collect::<GraphQLResult<Vec<_>>>()?;
72
73        Ok(Page::new(page_info, nodes))
74    }
75}
76
77define_query! {
78    /// Query for [`GraphQLClient::checkpoint`],
79    /// [`GraphQLClient::checkpoint_by_digest`] and
80    /// [`GraphQLClient::checkpoint_by_sequence_number`]. Await it to send the
81    /// request.
82    pub struct GetCheckpointQuery {
83        client: GraphQLClient,
84        digest: Option<CheckpointDigest>,
85        sequence_number: Option<u64>,
86    }
87    output: GraphQLResult<Option<CheckpointSummary>>;
88}
89
90impl GetCheckpointQuery {
91    async fn send(self) -> GraphQLResult<Option<CheckpointSummary>> {
92        let operation = CheckpointQueryFragment::build(CheckpointArgs {
93            id: CheckpointId {
94                digest: self.digest.map(|d| d.to_string()),
95                sequence_number: self.sequence_number,
96            },
97        });
98        let response = self.client.run_query(&operation).await?;
99
100        response.checkpoint.map(|c| c.try_into()).transpose()
101    }
102}
103
104define_query! {
105    /// Query for [`GraphQLClient::latest_checkpoint_sequence_number`]. Await
106    /// it to send the request.
107    pub struct GetLatestCheckpointSequenceNumberQuery {
108        client: GraphQLClient,
109    }
110    output: GraphQLResult<Option<CheckpointSequenceNumber>>;
111}
112
113impl GetLatestCheckpointSequenceNumberQuery {
114    async fn send(self) -> GraphQLResult<Option<CheckpointSequenceNumber>> {
115        Ok(self.client.checkpoint().await?.map(|c| c.sequence_number))
116    }
117}
118
119define_query! {
120    /// Query for [`GraphQLClient::total_transaction_blocks`],
121    /// [`GraphQLClient::total_transaction_blocks_by_digest`] and
122    /// [`GraphQLClient::total_transaction_blocks_by_sequence_number`]. Await it
123    /// to send the request.
124    pub struct GetTotalTransactionBlocksQuery {
125        client: GraphQLClient,
126        digest: Option<String>,
127        sequence_number: Option<u64>,
128    }
129    output: GraphQLResult<Option<u64>>;
130}
131
132impl GetTotalTransactionBlocksQuery {
133    async fn send(self) -> GraphQLResult<Option<u64>> {
134        let operation = CheckpointTotalTxQueryFragment::build(CheckpointArgs {
135            id: CheckpointId {
136                digest: self.digest,
137                sequence_number: self.sequence_number,
138            },
139        });
140        let response = self.client.run_query(&operation).await?;
141
142        Ok(response
143            .checkpoint
144            .and_then(|c| c.network_total_transactions))
145    }
146}
147
148impl GraphQLClient {
149    /// Get the [`CheckpointSummary`] of the last known checkpoint.
150    pub fn checkpoint(&self) -> GetCheckpointQuery {
151        GetCheckpointQuery {
152            client: self.clone(),
153            digest: None,
154            sequence_number: None,
155        }
156    }
157
158    /// Get the [`CheckpointSummary`] for the given checkpoint digest.
159    pub fn checkpoint_by_digest(&self, digest: CheckpointDigest) -> GetCheckpointQuery {
160        GetCheckpointQuery {
161            client: self.clone(),
162            digest: Some(digest),
163            sequence_number: None,
164        }
165    }
166
167    /// Get the [`CheckpointSummary`] for the given checkpoint sequence number.
168    pub fn checkpoint_by_sequence_number(&self, sequence_number: u64) -> GetCheckpointQuery {
169        GetCheckpointQuery {
170            client: self.clone(),
171            digest: None,
172            sequence_number: Some(sequence_number),
173        }
174    }
175
176    /// Get a page of [`CheckpointSummary`].
177    pub fn checkpoints(&self) -> ListCheckpointsQuery {
178        ListCheckpointsQuery {
179            client: self.clone(),
180            pagination: PaginationFilter::default(),
181        }
182    }
183
184    /// Return the sequence number of the latest checkpoint that has been
185    /// executed.
186    pub fn latest_checkpoint_sequence_number(&self) -> GetLatestCheckpointSequenceNumberQuery {
187        GetLatestCheckpointSequenceNumberQuery {
188            client: self.clone(),
189        }
190    }
191
192    /// The total number of transaction blocks in the network by the end of the
193    /// provided checkpoint digest.
194    pub fn total_transaction_blocks_by_digest(
195        &self,
196        digest: CheckpointDigest,
197    ) -> GetTotalTransactionBlocksQuery {
198        GetTotalTransactionBlocksQuery {
199            client: self.clone(),
200            digest: Some(digest.to_string()),
201            sequence_number: None,
202        }
203    }
204
205    /// The total number of transaction blocks in the network by the end of the
206    /// provided checkpoint sequence number.
207    pub fn total_transaction_blocks_by_sequence_number(
208        &self,
209        sequence_number: u64,
210    ) -> GetTotalTransactionBlocksQuery {
211        GetTotalTransactionBlocksQuery {
212            client: self.clone(),
213            digest: None,
214            sequence_number: Some(sequence_number),
215        }
216    }
217
218    /// The total number of transaction blocks in the network by the end of the
219    /// last known checkpoint.
220    pub fn total_transaction_blocks(&self) -> GetTotalTransactionBlocksQuery {
221        GetTotalTransactionBlocksQuery {
222            client: self.clone(),
223            digest: None,
224            sequence_number: None,
225        }
226    }
227}
228
229#[cfg(all(test, not(target_arch = "wasm32")))]
230mod tests {
231    use iota_types::CheckpointDigest;
232
233    use crate::test_utils::{
234        assert_backward_page, assert_forward_page, backward_page, forward_page, sent_variables,
235        test_client,
236    };
237
238    #[tokio::test]
239    async fn total_transaction_blocks_send_the_checkpoint() {
240        let request = sent_variables("CheckpointTotalTxQueryFragment", |client| async move {
241            let _ = client.total_transaction_blocks_by_sequence_number(7).await;
242        })
243        .await;
244        let vars = &request;
245        assert_eq!(vars["id"]["sequenceNumber"], 7);
246        assert!(vars["id"]["digest"].is_null());
247
248        let digest = CheckpointDigest::ZERO;
249        let vars = sent_variables("CheckpointTotalTxQueryFragment", |client| async move {
250            let _ = client.total_transaction_blocks_by_digest(digest).await;
251        })
252        .await;
253        assert_eq!(vars["id"]["digest"], digest.to_string());
254        assert!(vars["id"]["sequenceNumber"].is_null());
255
256        let request = sent_variables("CheckpointTotalTxQueryFragment", |client| async move {
257            let _ = client.total_transaction_blocks().await;
258        })
259        .await;
260        assert!(request["id"]["digest"].is_null());
261        assert!(request["id"]["sequenceNumber"].is_null());
262
263        let request = sent_variables("CheckpointQueryFragment", |client| async move {
264            let _ = client.latest_checkpoint_sequence_number().await;
265        })
266        .await;
267        assert!(request["id"]["digest"].is_null());
268        assert!(request["id"]["sequenceNumber"].is_null());
269    }
270
271    #[tokio::test]
272    async fn checkpoint_sends_the_digest_or_sequence_number() {
273        let vars = sent_variables("CheckpointQueryFragment", |client| async move {
274            let _ = client.checkpoint_by_sequence_number(7).await;
275        })
276        .await;
277        assert_eq!(vars["id"]["sequenceNumber"], 7);
278        assert!(vars["id"]["digest"].is_null());
279
280        let digest = CheckpointDigest::ZERO;
281        let vars = sent_variables("CheckpointQueryFragment", |client| async move {
282            let _ = client.checkpoint_by_digest(digest).await;
283        })
284        .await;
285        assert_eq!(vars["id"]["digest"], digest.to_string());
286        assert!(vars["id"]["sequenceNumber"].is_null());
287
288        let vars = sent_variables("CheckpointQueryFragment", |client| async move {
289            let _ = client.checkpoint().await;
290        })
291        .await;
292        assert!(vars["id"]["digest"].is_null());
293        assert!(vars["id"]["sequenceNumber"].is_null());
294    }
295
296    #[tokio::test]
297    async fn checkpoints_sends_the_pagination() {
298        let vars = sent_variables("CheckpointsQueryFragment", |client| async move {
299            let _ = client.checkpoints().pagination(backward_page()).await;
300        })
301        .await;
302        assert_backward_page(&vars);
303
304        let vars = sent_variables("CheckpointsQueryFragment", |client| async move {
305            let _ = client.checkpoints().pagination(forward_page()).await;
306        })
307        .await;
308        assert_forward_page(&vars);
309    }
310
311    #[test]
312    fn checkpoints_query_forwards_pagination_arguments() {
313        use cynic::QueryBuilder;
314
315        use crate::query_types::{CheckpointsArgs, CheckpointsQueryFragment};
316
317        let operation = CheckpointsQueryFragment::build(CheckpointsArgs {
318            first: Some(10),
319            after: None,
320            last: None,
321            before: None,
322        });
323        for arg in ["first:", "after:", "last:", "before:"] {
324            assert!(
325                operation.query.contains(arg),
326                "checkpoints query is missing the `{arg}` argument:\n{}",
327                operation.query
328            );
329        }
330    }
331
332    #[tokio::test]
333    async fn test_checkpoint_query() {
334        let client = test_client();
335        client
336            .checkpoint()
337            .await
338            .map_err(|e| {
339                format!(
340                    "Checkpoint query failed for {} network: Error: {e}",
341                    client.rpc_server()
342                )
343            })
344            .unwrap()
345            .unwrap();
346    }
347
348    #[tokio::test]
349    async fn test_checkpoints_query() {
350        let client = test_client();
351        let cs = client
352            .checkpoints()
353            .await
354            .map_err(|e| {
355                format!(
356                    "Checkpoints query failed for {} network: Error {e}",
357                    client.rpc_server()
358                )
359            })
360            .unwrap();
361
362        assert!(
363            !cs.is_empty(),
364            "Checkpoints query returned no data for {} network",
365            client.rpc_server()
366        );
367    }
368
369    #[tokio::test]
370    async fn test_latest_checkpoint_sequence_number_query() {
371        let client = test_client();
372        client
373            .latest_checkpoint_sequence_number()
374            .await
375            .map_err(|e| {
376                format!(
377                    "Latest checkpoint sequence number query failed for {} network: Error {e}",
378                    client.rpc_server()
379                )
380            })
381            .unwrap()
382            .unwrap();
383    }
384
385    #[tokio::test]
386    async fn test_total_transaction_blocks() {
387        let client = test_client();
388        let total_transaction_blocks = client
389            .total_transaction_blocks()
390            .await
391            .map_err(|e| {
392                format!(
393                    "Total transaction blocks query failed for {} network. Error: {e}",
394                    client.rpc_server()
395                )
396            })
397            .unwrap()
398            .unwrap();
399        assert!(total_transaction_blocks > 0);
400
401        let checkpoint_sequence_number = client
402            .latest_checkpoint_sequence_number()
403            .await
404            .map_err(|e| {
405                format!(
406                    "Latest checkpoint sequence number query failed for {} network. Error: {e}",
407                    client.rpc_server()
408                )
409            })
410            .unwrap()
411            .unwrap();
412        let total_transaction_blocks_by_sequence_number = client
413            .total_transaction_blocks_by_sequence_number(checkpoint_sequence_number)
414            .await
415            .unwrap()
416            .unwrap();
417        assert!(
418            total_transaction_blocks_by_sequence_number >= total_transaction_blocks,
419            "expected at least {total_transaction_blocks} transaction blocks, found {total_transaction_blocks_by_sequence_number}"
420        );
421
422        let checkpoint = client
423            .checkpoint_by_sequence_number(checkpoint_sequence_number)
424            .await
425            .unwrap()
426            .unwrap();
427
428        let total_transaction_blocks_by_digest = client
429            .total_transaction_blocks_by_digest(checkpoint.digest())
430            .await
431            .unwrap()
432            .unwrap();
433        assert_eq!(
434            total_transaction_blocks_by_sequence_number,
435            total_transaction_blocks_by_digest
436        );
437    }
438}