iota_grpc_client/api/ledger/
epochs.rs

1// Copyright (c) 2026 IOTA Stiftung
2// SPDX-License-Identifier: Apache-2.0
3
4//! High-level API for epoch queries.
5
6use iota_grpc_types::{
7    field::FieldMask,
8    v1::{epoch::Epoch, ledger_service::GetEpochRequest},
9};
10
11use crate::{
12    Client,
13    api::{
14        GET_EPOCH_READ_MASK, MetadataEnvelope, Result, TryFromProtoError, field_mask_with_default,
15    },
16};
17
18impl Client {
19    /// Get epoch information.
20    ///
21    /// Returns the [`Epoch`] proto type with fields populated according to the
22    /// `read_mask`.
23    ///
24    /// # Parameters
25    ///
26    /// * `epoch` - The epoch to query. If `None`, returns the current epoch.
27    /// * `read_mask` - Optional field mask specifying which fields to include.
28    ///   If `None`, uses [`GET_EPOCH_READ_MASK`].
29    ///
30    /// # Available Read Mask Fields
31    ///
32    /// ## Epoch Fields
33    /// - `epoch` - the epoch number
34    /// - `committee` - the validator committee for this epoch
35    /// - `bcs_system_state` - the BCS-encoded system state at the beginning of
36    ///   the epoch for past epochs or the current system state for the current
37    ///   epoch, which can be used for historical state queries or to get the
38    ///   current state respectively
39    ///
40    /// ## Checkpoint Fields
41    /// - `first_checkpoint` - the first checkpoint included in the epoch
42    /// - `last_checkpoint` - the last checkpoint included in the epoch, which
43    ///   may be unavailable for the current epoch if it has not ended yet
44    ///
45    /// ## Timing Fields
46    /// - `start` - the timestamp of the first checkpoint included in the epoch
47    /// - `end` - the timestamp of the last checkpoint included in the epoch,
48    ///   which may be unavailable for the current epoch if it has not ended yet
49    ///
50    /// ## Gas Fields
51    /// - `reference_gas_price` - the reference gas price during the epoch,
52    ///   denominated in NANOS
53    ///
54    /// ## Protocol Configuration Fields
55    /// - `protocol_config` - the protocol configuration during the epoch
56    ///   - `protocol_config.protocol_version` - the protocol version during the
57    ///     epoch
58    ///   - `protocol_config.feature_flags` - the individual protocol feature
59    ///     flags during the epoch (use `protocol_config.feature_flags.<key>` to
60    ///     filter specific flags)
61    ///   - `protocol_config.attributes` - the individual protocol attributes
62    ///     during the epoch (use `protocol_config.attributes.<key>` to filter
63    ///     specific attributes)
64    ///
65    ///   > **Note:** Other than for all other fields, wildcards don't work for
66    ///   > `protocol_config.feature_flags` and `protocol_config.attributes`
67    ///   > since they are maps (`protocol_config` is not enough). If you want
68    ///   > all entries, you must specify the map directly, or single entries of
69    ///   > it by name.
70    ///   > (e.g. `protocol_config.feature_flags` to get all entries, or
71    ///   > `protocol_config.feature_flags.zklogin_auth` to get a single flag)
72    ///
73    /// # Example
74    ///
75    /// ```no_run
76    /// # use iota_grpc_client::Client;
77    /// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
78    /// let client = Client::connect("http://localhost:9000").await?;
79    ///
80    /// // Get current epoch with default fields
81    /// let epoch = client.get_epoch(None, None).await?;
82    /// println!("Epoch: {:?}", epoch.body().epoch);
83    ///
84    /// // Get specific epoch with custom fields
85    /// let epoch = client
86    ///     .get_epoch(Some(0), Some("epoch,reference_gas_price,first_checkpoint"))
87    ///     .await?;
88    ///
89    /// // Get all feature flags for the current epoch
90    /// let epoch = client
91    ///     .get_epoch(None, Some("protocol_config.feature_flags"))
92    ///     .await?
93    ///     .into_inner();
94    /// let flags = epoch.protocol_config.unwrap().feature_flags.unwrap().flags;
95    ///
96    /// // Get a single named feature flag
97    /// let epoch = client
98    ///     .get_epoch(None, Some("protocol_config.feature_flags.zklogin_auth"))
99    ///     .await?;
100    ///
101    /// // Get all protocol attributes for the current epoch
102    /// let epoch = client
103    ///     .get_epoch(None, Some("protocol_config.attributes"))
104    ///     .await?
105    ///     .into_inner();
106    /// let attributes = epoch
107    ///     .protocol_config
108    ///     .unwrap()
109    ///     .attributes
110    ///     .unwrap()
111    ///     .attributes;
112    ///
113    /// // Get a single named attribute
114    /// let epoch = client
115    ///     .get_epoch(None, Some("protocol_config.attributes.max_tx_gas"))
116    ///     .await?;
117    /// # Ok(())
118    /// # }
119    /// ```
120    pub async fn get_epoch(
121        &self,
122        epoch: Option<u64>,
123        read_mask: Option<&str>,
124    ) -> Result<MetadataEnvelope<Epoch>> {
125        let mut request = GetEpochRequest::default()
126            .with_read_mask(field_mask_with_default(read_mask, GET_EPOCH_READ_MASK));
127
128        if let Some(epoch) = epoch {
129            request = request.with_epoch(epoch);
130        }
131
132        let mut client = self.ledger_service_client();
133        let response = client.get_epoch(request).await?;
134
135        MetadataEnvelope::from(response).try_map(|r| {
136            r.epoch
137                .ok_or_else(|| TryFromProtoError::missing("epoch").into())
138        })
139    }
140
141    /// Get the reference gas price for the current epoch.
142    ///
143    /// # Example
144    ///
145    /// ```no_run
146    /// # use iota_grpc_client::Client;
147    /// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
148    /// let client = Client::connect("http://localhost:9000").await?;
149    /// let gas_price = client.get_reference_gas_price().await?.into_inner();
150    /// println!("Reference gas price: {gas_price} NANOS");
151    /// # Ok(())
152    /// # }
153    /// ```
154    pub async fn get_reference_gas_price(&self) -> Result<MetadataEnvelope<u64>> {
155        self.get_epoch_field("reference_gas_price", |e| e.reference_gas_price)
156            .await
157    }
158
159    /// Internal helper to fetch a single field from the current epoch.
160    async fn get_epoch_field<T>(
161        &self,
162        field: &str,
163        extractor: impl FnOnce(Epoch) -> Option<T>,
164    ) -> Result<MetadataEnvelope<T>> {
165        // Current epoch (no epoch field set)
166        let request = GetEpochRequest::default().with_read_mask(FieldMask {
167            paths: vec![field.to_string()],
168        });
169
170        let mut client = self.ledger_service_client();
171        let response = client.get_epoch(request).await?;
172
173        MetadataEnvelope::from(response).try_map(|r| {
174            r.epoch
175                .and_then(extractor)
176                .ok_or_else(|| TryFromProtoError::missing(field).into())
177        })
178    }
179}