iota_grpc_api/client/
checkpoint.rs

1// Copyright (c) 2025 IOTA Stiftung
2// SPDX-License-Identifier: Apache-2.0
3
4use futures::{Stream, StreamExt};
5use iota_grpc_types::{CertifiedCheckpointSummary, CheckpointData};
6use tonic::transport::Channel;
7
8use crate::checkpoint::checkpoint_service_client::CheckpointServiceClient;
9
10/// Enum representing the content of a checkpoint, either full data or summary.
11#[derive(Debug, Clone)]
12pub enum CheckpointContent {
13    Data(CheckpointData),
14    Summary(CertifiedCheckpointSummary),
15}
16
17/// Dedicated client for checkpoint-related gRPC operations.
18///
19/// This client handles all checkpoint service interactions including streaming
20/// checkpoints and querying epoch information.
21#[derive(Clone)]
22pub struct CheckpointClient {
23    client: CheckpointServiceClient<Channel>,
24}
25
26impl CheckpointClient {
27    /// Create a new CheckpointClient from a shared gRPC channel.
28    pub(super) fn new(channel: Channel) -> Self {
29        Self {
30            client: CheckpointServiceClient::new(channel),
31        }
32    }
33
34    /// Stream checkpoints with automatic deserialization.
35    ///
36    /// # Arguments
37    /// * `start_sequence_number` - Optional starting sequence number
38    /// * `end_sequence_number` - Optional ending sequence number
39    /// * `full` - Whether to stream full checkpoint data or just summaries
40    ///
41    /// # Returns
42    /// A stream of checkpoint content (either data or summaries)
43    pub async fn stream_checkpoints(
44        &mut self,
45        start_sequence_number: Option<u64>,
46        end_sequence_number: Option<u64>,
47        full: bool,
48    ) -> Result<impl Stream<Item = Result<CheckpointContent, tonic::Status>>, tonic::Status> {
49        let request = crate::checkpoint::CheckpointStreamRequest {
50            start_sequence_number,
51            end_sequence_number,
52            full,
53        };
54        let stream = self.client.stream_checkpoints(request).await?.into_inner();
55
56        Ok(stream.map(|result| {
57            result.and_then(|checkpoint| {
58                Self::deserialize_checkpoint(&checkpoint).map_err(|e| {
59                    tonic::Status::internal(format!("Failed to deserialize checkpoint: {e}"))
60                })
61            })
62        }))
63    }
64
65    /// Get the first checkpoint sequence number for a given epoch.
66    pub async fn get_epoch_first_checkpoint_sequence_number(
67        &mut self,
68        epoch: u64,
69    ) -> Result<u64, tonic::Status> {
70        let request = crate::checkpoint::EpochRequest { epoch };
71        let response = self
72            .client
73            .get_epoch_first_checkpoint_sequence_number(request)
74            .await?;
75        Ok(response.into_inner().sequence_number)
76    }
77
78    // ========================================
79    // Private Helper Methods
80    // ========================================
81
82    /// Deserialize checkpoint data based on the checkpoint type (full or
83    /// summary). Returns either checkpoint data or summary depending on the
84    /// checkpoint type.
85    fn deserialize_checkpoint(
86        checkpoint: &crate::checkpoint::Checkpoint,
87    ) -> anyhow::Result<CheckpointContent> {
88        let bcs_data = checkpoint
89            .bcs_data
90            .as_ref()
91            .ok_or_else(|| anyhow::anyhow!("Missing BCS data in checkpoint"))?;
92
93        if checkpoint.is_full {
94            let checkpoint_data = bcs_data.deserialize_into::<CheckpointData>()?;
95            Ok(CheckpointContent::Data(checkpoint_data))
96        } else {
97            let checkpoint_summary = bcs_data.deserialize_into::<CertifiedCheckpointSummary>()?;
98            Ok(CheckpointContent::Summary(checkpoint_summary))
99        }
100    }
101}