Skip to main content

iota_sdk_graphql_client/api/
network.rs

1// Copyright (c) Mysten Labs, Inc.
2// Modifications Copyright (c) 2026 IOTA Stiftung
3// SPDX-License-Identifier: Apache-2.0
4
5//! Network API implementation.
6
7use cynic::QueryBuilder;
8use futures::Stream;
9
10use crate::{
11    GraphQLClient,
12    api::define_query,
13    error::GraphQLResult,
14    pagination::{Page, PaginationFilter, PaginationFilterResponse},
15    query_types::{
16        ActiveValidatorsArgs, ActiveValidatorsQueryFragment, ChainIdentifierQueryFragment,
17        EpochArgs, EpochSummaryQueryFragment, ProtocolConfigQueryFragment, ProtocolConfigs,
18        ProtocolVersionArgs, Validator,
19    },
20    streams::stream_paginated_query,
21};
22
23define_query! {
24    /// Query for [`GraphQLClient::chain_id`]. Await it to send the request.
25    pub struct GetChainIdQuery {
26        client: GraphQLClient,
27    }
28    output: GraphQLResult<String>;
29}
30
31impl GetChainIdQuery {
32    fn operation(&self) -> cynic::Operation<ChainIdentifierQueryFragment, ()> {
33        ChainIdentifierQueryFragment::build(())
34    }
35
36    async fn send(self) -> GraphQLResult<String> {
37        let response = self.client.run_query(&self.operation()).await?;
38
39        Ok(response.chain_identifier)
40    }
41}
42
43define_query! {
44    /// Query for [`GraphQLClient::active_validators`]. Await it to send the
45    /// request.
46    #[derive(Clone)]
47    pub struct ListActiveValidatorsQuery {
48        client: GraphQLClient,
49        epoch_number: Option<u64>,
50        pagination: PaginationFilter,
51    }
52    output: GraphQLResult<Page<Validator>>;
53}
54
55impl ListActiveValidatorsQuery {
56    /// Set the epoch number. Defaults to the current epoch.
57    pub fn epoch_number(mut self, epoch_number: u64) -> Self {
58        self.epoch_number = Some(epoch_number);
59        self
60    }
61
62    /// Set the page to fetch.
63    pub fn pagination(mut self, pagination: PaginationFilter) -> Self {
64        self.pagination = pagination;
65        self
66    }
67
68    /// Stream every item, page by page, starting at the pagination's cursor
69    /// and in its direction, with its limit as the page size.
70    pub fn stream(self) -> impl Stream<Item = GraphQLResult<Validator>> + Unpin {
71        let pagination = self.pagination.clone();
72        stream_paginated_query(move |page| self.clone().pagination(page).send(), pagination)
73    }
74
75    fn operation<'a>(
76        epoch_number: Option<u64>,
77        pagination: &'a PaginationFilterResponse,
78    ) -> cynic::Operation<ActiveValidatorsQueryFragment, ActiveValidatorsArgs<'a>> {
79        ActiveValidatorsQueryFragment::build(ActiveValidatorsArgs {
80            id: epoch_number,
81            after: pagination.after.as_deref(),
82            before: pagination.before.as_deref(),
83            first: pagination.first,
84            last: pagination.last,
85        })
86    }
87
88    async fn send(self) -> GraphQLResult<Page<Validator>> {
89        let Self {
90            client,
91            pagination,
92            epoch_number,
93        } = self;
94        let pagination = client.pagination_filter(pagination).await;
95        let response = client
96            .run_query(&Self::operation(epoch_number, &pagination))
97            .await?;
98
99        if let Some(validators) = response.epoch.and_then(|v| v.validator_set) {
100            let page_info = validators.active_validators.page_info;
101            Ok(Page::new(page_info, validators.active_validators.nodes))
102        } else {
103            Ok(Page::new_empty())
104        }
105    }
106}
107
108define_query! {
109    /// Query for [`GraphQLClient::reference_gas_price`]. Await it to send the
110    /// request.
111    pub struct GetReferenceGasPriceQuery {
112        client: GraphQLClient,
113        epoch: Option<u64>,
114    }
115    output: GraphQLResult<Option<u64>>;
116}
117
118impl GetReferenceGasPriceQuery {
119    /// Set the epoch number. Defaults to the last known epoch.
120    pub fn epoch_number(mut self, epoch_number: u64) -> Self {
121        self.epoch = Some(epoch_number);
122        self
123    }
124
125    async fn send(self) -> GraphQLResult<Option<u64>> {
126        let operation = EpochSummaryQueryFragment::build(EpochArgs { id: self.epoch });
127        let response = self.client.run_query(&operation).await?;
128
129        response
130            .epoch
131            .and_then(|e| e.reference_gas_price)
132            .map(|x| x.try_into())
133            .transpose()
134    }
135}
136
137define_query! {
138    /// Query for [`GraphQLClient::protocol_config`]. Await it to send the
139    /// request.
140    pub struct GetProtocolConfigQuery {
141        client: GraphQLClient,
142        version: Option<u64>,
143    }
144    output: GraphQLResult<ProtocolConfigs>;
145}
146
147impl GetProtocolConfigQuery {
148    /// Set the protocol version. Defaults to the latest version.
149    pub fn version(mut self, version: u64) -> Self {
150        self.version = Some(version);
151        self
152    }
153
154    async fn send(self) -> GraphQLResult<ProtocolConfigs> {
155        let operation =
156            ProtocolConfigQueryFragment::build(ProtocolVersionArgs { id: self.version });
157        let response = self.client.run_query(&operation).await?;
158        Ok(response.protocol_config)
159    }
160}
161
162impl GraphQLClient {
163    /// Get the chain identifier.
164    pub fn chain_id(&self) -> GetChainIdQuery {
165        GetChainIdQuery {
166            client: self.clone(),
167        }
168    }
169
170    /// Get the reference gas price. Defaults to the last known epoch.
171    ///
172    /// This will resolve to `Ok(None)` if the epoch requested is not available
173    /// in the GraphQL service (e.g., due to pruning).
174    pub fn reference_gas_price(&self) -> GetReferenceGasPriceQuery {
175        GetReferenceGasPriceQuery {
176            client: self.clone(),
177            epoch: None,
178        }
179    }
180
181    /// Get the protocol configuration. Defaults to the latest version.
182    pub fn protocol_config(&self) -> GetProtocolConfigQuery {
183        GetProtocolConfigQuery {
184            client: self.clone(),
185            version: None,
186        }
187    }
188
189    /// Get the list of active validators, including related metadata.
190    pub fn active_validators(&self) -> ListActiveValidatorsQuery {
191        ListActiveValidatorsQuery {
192            client: self.clone(),
193            epoch_number: None,
194            pagination: PaginationFilter::default(),
195        }
196    }
197}
198
199#[cfg(all(test, not(target_arch = "wasm32")))]
200mod tests {
201    use crate::{
202        GraphQLClient,
203        test_utils::{
204            assert_backward_page, assert_forward_page, backward_page, forward_page, sent_variables,
205            test_client,
206        },
207    };
208
209    #[tokio::test]
210    async fn active_validators_sends_the_epoch_and_pagination() {
211        let vars = sent_variables("ActiveValidatorsQueryFragment", |client| async move {
212            let _ = client
213                .active_validators()
214                .epoch_number(3)
215                .pagination(backward_page())
216                .await;
217        })
218        .await;
219        assert_eq!(vars["id"], 3);
220        assert_backward_page(&vars);
221
222        let vars = sent_variables("ActiveValidatorsQueryFragment", |client| async move {
223            let _ = client
224                .active_validators()
225                .epoch_number(3)
226                .pagination(forward_page())
227                .await;
228        })
229        .await;
230        assert_forward_page(&vars);
231    }
232
233    #[test]
234    fn chain_id_builds_the_chain_identifier_operation() {
235        let operation = GraphQLClient::new_localnet()
236            .unwrap()
237            .chain_id()
238            .operation();
239        assert_eq!(
240            operation.operation_name.as_deref(),
241            Some("ChainIdentifierQueryFragment")
242        );
243        assert!(operation.query.contains("chainIdentifier"));
244    }
245
246    #[tokio::test]
247    async fn test_chain_id() {
248        let client = test_client();
249        let chain_id = client.chain_id().await.unwrap();
250        assert!(!chain_id.is_empty());
251    }
252
253    #[tokio::test]
254    async fn reference_gas_price_and_protocol_config_send_their_input() {
255        let vars = sent_variables("EpochSummaryQueryFragment", |client| async move {
256            let _ = client.reference_gas_price().epoch_number(3).await;
257        })
258        .await;
259        assert_eq!(vars["id"], 3);
260
261        let vars = sent_variables("ProtocolConfigQueryFragment", |client| async move {
262            let _ = client.protocol_config().version(50).await;
263        })
264        .await;
265        assert_eq!(vars["id"], 50);
266
267        let vars = sent_variables("EpochSummaryQueryFragment", |client| async move {
268            let _ = client.reference_gas_price().await;
269        })
270        .await;
271        assert!(vars["id"].is_null());
272
273        let vars = sent_variables("ProtocolConfigQueryFragment", |client| async move {
274            let _ = client.protocol_config().await;
275        })
276        .await;
277        assert!(vars["id"].is_null());
278    }
279
280    #[tokio::test]
281    async fn test_reference_gas_price_query() {
282        let client = test_client();
283        client
284            .reference_gas_price()
285            .await
286            .map_err(|e| {
287                format!(
288                    "Reference gas price query failed for {} network: Error: {e}",
289                    client.rpc_server()
290                )
291            })
292            .unwrap()
293            .unwrap();
294    }
295
296    #[tokio::test]
297    async fn test_protocol_config_query() {
298        let client = test_client();
299        client
300            .protocol_config()
301            .await
302            .map_err(|e| {
303                format!(
304                    "Protocol config query failed for {} network: Error: {e}",
305                    client.rpc_server()
306                )
307            })
308            .unwrap();
309
310        // test specific version
311        let pc = client
312            .protocol_config()
313            .version(50)
314            .await
315            .map_err(|e| {
316                format!(
317                    "Protocol config query failed for {} network: Error: {e}",
318                    client.rpc_server()
319                )
320            })
321            .unwrap();
322        assert_eq!(
323            pc.protocol_version,
324            50,
325            "Protocol version query mismatch for {} network. Expected: 50, received: {}",
326            client.rpc_server(),
327            pc.protocol_version
328        );
329    }
330
331    #[tokio::test]
332    async fn test_active_validators() {
333        let client = test_client();
334        let av = client
335            .active_validators()
336            .await
337            .map_err(|e| {
338                format!(
339                    "Active validators query failed for {} network: Error: {e}",
340                    client.rpc_server()
341                )
342            })
343            .unwrap();
344
345        assert!(
346            !av.is_empty(),
347            "Active validators query returned no data for {} network",
348            client.rpc_server()
349        );
350    }
351}