Skip to main content

iota_sdk_graphql_client/api/
dynamic_fields.rs

1// Copyright (c) Mysten Labs, Inc.
2// Modifications Copyright (c) 2026 IOTA Stiftung
3// SPDX-License-Identifier: Apache-2.0
4
5//! Dynamic Fields API implementation.
6
7use base64ct::Encoding;
8use cynic::QueryBuilder;
9use futures::Stream;
10use iota_types::{Address, TypeTag};
11
12use crate::{
13    DynamicFieldOutput, GraphQLClient, NameValue,
14    api::define_query,
15    error::GraphQLResult,
16    pagination::{Page, PaginationFilter, PaginationFilterResponse},
17    query_types::{
18        Base64, DynamicFieldArgs, DynamicFieldConnectionArgs, DynamicFieldName,
19        DynamicFieldQueryFragment, DynamicFieldsOwnerQueryFragment,
20        DynamicObjectFieldQueryFragment,
21    },
22    streams::stream_paginated_query,
23};
24
25define_query! {
26    /// Query for [`GraphQLClient::dynamic_fields`]. Await it to send the
27    /// request.
28    #[derive(Clone)]
29    pub struct ListDynamicFieldsQuery {
30        client: GraphQLClient,
31        address: Address,
32        pagination: PaginationFilter,
33    }
34    output: GraphQLResult<Page<DynamicFieldOutput>>;
35}
36
37impl ListDynamicFieldsQuery {
38    /// Set the page to fetch.
39    pub fn pagination(mut self, pagination: PaginationFilter) -> Self {
40        self.pagination = pagination;
41        self
42    }
43
44    /// Stream every item, page by page, starting at the pagination's cursor
45    /// and in its direction, with its limit as the page size.
46    pub fn stream(self) -> impl Stream<Item = GraphQLResult<DynamicFieldOutput>> + Unpin {
47        let pagination = self.pagination.clone();
48        stream_paginated_query(move |page| self.clone().pagination(page).send(), pagination)
49    }
50
51    fn operation<'a>(
52        address: Address,
53        pagination: &'a PaginationFilterResponse,
54    ) -> cynic::Operation<DynamicFieldsOwnerQueryFragment, DynamicFieldConnectionArgs<'a>> {
55        DynamicFieldsOwnerQueryFragment::build(DynamicFieldConnectionArgs {
56            address,
57            after: pagination.after.as_deref(),
58            before: pagination.before.as_deref(),
59            first: pagination.first,
60            last: pagination.last,
61        })
62    }
63
64    async fn send(self) -> GraphQLResult<Page<DynamicFieldOutput>> {
65        let Self {
66            client,
67            pagination,
68            address,
69        } = self;
70        let pagination = client.pagination_filter(pagination).await;
71        let response = client
72            .run_query(&Self::operation(address, &pagination))
73            .await?;
74
75        let DynamicFieldsOwnerQueryFragment { owner: Some(dfs) } = response else {
76            return Ok(Page::new_empty());
77        };
78
79        Ok(Page::new(
80            dfs.dynamic_fields.page_info,
81            dfs.dynamic_fields
82                .nodes
83                .into_iter()
84                .map(TryInto::try_into)
85                .collect::<GraphQLResult<Vec<_>>>()?,
86        ))
87    }
88}
89
90fn dynamic_field_name(type_tag: TypeTag, name: impl Into<NameValue>) -> DynamicFieldName {
91    DynamicFieldName {
92        type_tag: type_tag.to_string(),
93        bcs: Base64(base64ct::Base64::encode_string(&name.into().0)),
94    }
95}
96
97define_query! {
98    /// Query for [`GraphQLClient::dynamic_field`]. Await it to send the
99    /// request.
100    pub struct GetDynamicFieldQuery {
101        client: GraphQLClient,
102        address: Address,
103        name: DynamicFieldName,
104    }
105    output: GraphQLResult<Option<DynamicFieldOutput>>;
106}
107
108impl GetDynamicFieldQuery {
109    async fn send(self) -> GraphQLResult<Option<DynamicFieldOutput>> {
110        let operation = DynamicFieldQueryFragment::build(DynamicFieldArgs {
111            address: self.address,
112            name: self.name,
113        });
114
115        let response = self.client.run_query(&operation).await?;
116
117        let result = response
118            .owner
119            .and_then(|o| o.dynamic_field)
120            .map(|df| df.try_into())
121            .transpose()?;
122
123        Ok(result)
124    }
125}
126
127define_query! {
128    /// Query for [`GraphQLClient::dynamic_object_field`]. Await it to send the
129    /// request.
130    pub struct GetDynamicObjectFieldQuery {
131        client: GraphQLClient,
132        address: Address,
133        name: DynamicFieldName,
134    }
135    output: GraphQLResult<Option<DynamicFieldOutput>>;
136}
137
138impl GetDynamicObjectFieldQuery {
139    async fn send(self) -> GraphQLResult<Option<DynamicFieldOutput>> {
140        let operation = DynamicObjectFieldQueryFragment::build(DynamicFieldArgs {
141            address: self.address,
142            name: self.name,
143        });
144
145        let response = self.client.run_query(&operation).await?;
146
147        let result: Option<DynamicFieldOutput> = response
148            .owner
149            .and_then(|o| o.dynamic_object_field)
150            .map(|df| df.try_into())
151            .transpose()?;
152        Ok(result)
153    }
154}
155
156impl GraphQLClient {
157    /// Access a dynamic field on an object using its name. Names are arbitrary
158    /// Move values whose type have copy, drop, and store, and are specified
159    /// using their type, and their BCS contents, Base64 encoded.
160    ///
161    /// The `name` argument can be either a [`BcsName`](crate::BcsName) for
162    /// passing raw bcs bytes or a type that implements Serialize.
163    ///
164    /// This returns [`DynamicFieldOutput`] which contains the name, the value
165    /// as json, and object.
166    ///
167    /// # Example
168    /// ```rust,ignore
169    /// 
170    /// let client = iota_graphql_client::GraphQLClient::new_testnet().unwrap();
171    /// let address = ObjectId::system().into();
172    /// let df = client.dynamic_field_with_name(address, "u64", 2u64).await.unwrap();
173    ///
174    /// # alternatively, pass in the bcs bytes
175    /// let bcs = base64ct::Base64::decode_vec("AgAAAAAAAAA=").unwrap();
176    /// let df = client.dynamic_field(address, "u64", BcsName(bcs)).await.unwrap();
177    /// ```
178    pub fn dynamic_field(
179        &self,
180        address: Address,
181        type_tag: TypeTag,
182        name: impl Into<NameValue>,
183    ) -> GetDynamicFieldQuery {
184        GetDynamicFieldQuery {
185            client: self.clone(),
186            address,
187            name: dynamic_field_name(type_tag, name),
188        }
189    }
190
191    /// Access a dynamic object field on an object using its name. Names are
192    /// arbitrary Move values whose type have copy, drop, and store, and are
193    /// specified using their type, and their BCS contents, Base64 encoded.
194    ///
195    /// The `name` argument can be either a [`BcsName`](crate::BcsName) for
196    /// passing raw bcs bytes or a type that implements Serialize.
197    ///
198    /// This returns [`DynamicFieldOutput`] which contains the name, the value
199    /// as json, and object.
200    pub fn dynamic_object_field(
201        &self,
202        address: Address,
203        type_tag: TypeTag,
204        name: impl Into<NameValue>,
205    ) -> GetDynamicObjectFieldQuery {
206        GetDynamicObjectFieldQuery {
207            client: self.clone(),
208            address,
209            name: dynamic_field_name(type_tag, name),
210        }
211    }
212
213    /// Get a page of dynamic fields for the provided address. Note that this
214    /// will also fetch dynamic fields on wrapped objects.
215    pub fn dynamic_fields(&self, address: Address) -> ListDynamicFieldsQuery {
216        ListDynamicFieldsQuery {
217            client: self.clone(),
218            address,
219            pagination: PaginationFilter::default(),
220        }
221    }
222}
223
224#[cfg(all(test, not(target_arch = "wasm32")))]
225mod tests {
226    use base64ct::Encoding;
227    use iota_types::{ObjectId, TypeTag};
228
229    use crate::{
230        BcsName,
231        test_utils::{
232            assert_backward_page, assert_forward_page, backward_page, forward_page, sent_variables,
233            test_client,
234        },
235    };
236
237    #[tokio::test]
238    async fn dynamic_field_getters_send_the_address_and_name() {
239        let address = ObjectId::SYSTEM_STATE.into();
240        let vars = sent_variables("DynamicFieldQueryFragment", |client| async move {
241            let _ = client.dynamic_field(address, TypeTag::U64, 2u64).await;
242        })
243        .await;
244        assert_eq!(vars["address"], address.to_string());
245        assert_eq!(vars["name"]["type"], "u64");
246        assert_eq!(vars["name"]["bcs"], "AgAAAAAAAAA=");
247
248        let vars = sent_variables("DynamicObjectFieldQueryFragment", |client| async move {
249            let _ = client
250                .dynamic_object_field(address, TypeTag::U64, 2u64)
251                .await;
252        })
253        .await;
254        assert_eq!(vars["address"], address.to_string());
255        assert_eq!(vars["name"]["type"], "u64");
256        assert_eq!(vars["name"]["bcs"], "AgAAAAAAAAA=");
257    }
258
259    #[tokio::test]
260    async fn dynamic_fields_sends_the_address_and_pagination() {
261        let address = ObjectId::SYSTEM_STATE.into();
262        let vars = sent_variables("DynamicFieldsOwnerQueryFragment", |client| async move {
263            let _ = client
264                .dynamic_fields(address)
265                .pagination(backward_page())
266                .await;
267        })
268        .await;
269        assert_eq!(vars["address"], address.to_string());
270        assert_backward_page(&vars);
271
272        let vars = sent_variables("DynamicFieldsOwnerQueryFragment", |client| async move {
273            let _ = client
274                .dynamic_fields(address)
275                .pagination(forward_page())
276                .await;
277        })
278        .await;
279        assert_forward_page(&vars);
280    }
281
282    #[tokio::test]
283    async fn test_dynamic_field_query() {
284        let client = test_client();
285        let bcs = base64ct::Base64::decode_vec("AgAAAAAAAAA=").unwrap();
286        client
287            .dynamic_field(ObjectId::SYSTEM_STATE.into(), TypeTag::U64, BcsName(bcs))
288            .await
289            .map_err(|e| {
290                format!(
291                    "Dynamic field query failed for {} network. Error: {e}",
292                    client.rpc_server()
293                )
294            })
295            .unwrap();
296
297        client
298            .dynamic_field(ObjectId::SYSTEM_STATE.into(), TypeTag::U64, 2u64)
299            .await
300            .map_err(|e| {
301                format!(
302                    "Dynamic field query failed for {} network. Error: {e}",
303                    client.rpc_server()
304                )
305            })
306            .unwrap();
307    }
308
309    #[tokio::test]
310    async fn test_dynamic_fields_query() {
311        let client = test_client();
312        client
313            .dynamic_fields(ObjectId::SYSTEM_STATE.into())
314            .await
315            .map_err(|e| {
316                format!(
317                    "Dynamic fields query failed for {} network. Error: {e}",
318                    client.rpc_server()
319                )
320            })
321            .unwrap();
322    }
323}