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}