Skip to main content

iota_sdk_graphql_client/api/
objects.rs

1// Copyright (c) Mysten Labs, Inc.
2// Modifications Copyright (c) 2026 IOTA Stiftung
3// SPDX-License-Identifier: Apache-2.0
4
5//! Objects API implementation.
6
7use cynic::QueryBuilder;
8use futures::Stream;
9use iota_types::{Object, ObjectId, Version};
10
11use crate::{
12    GraphQLClient,
13    api::define_query,
14    error::GraphQLResult,
15    pagination::{Page, PaginationFilter, PaginationFilterResponse},
16    query_types::{
17        MoveObjectContentsBcsQueryFragment, MoveObjectContentsJsonQueryFragment, ObjectFilter,
18        ObjectQueryArgs, ObjectQueryFragment, ObjectsQueryArgs, ObjectsQueryFragment,
19    },
20    streams::stream_paginated_query,
21};
22
23define_query! {
24    /// Query for [`GraphQLClient::objects`]. Await it to send the request.
25    #[derive(Clone)]
26    pub struct ListObjectsQuery {
27        client: GraphQLClient,
28        filter: Option<ObjectFilter>,
29        pagination: PaginationFilter,
30    }
31    output: GraphQLResult<Page<Object>>;
32}
33
34impl ListObjectsQuery {
35    pub(crate) fn new(client: GraphQLClient) -> Self {
36        Self {
37            client,
38            filter: None,
39            pagination: PaginationFilter::default(),
40        }
41    }
42
43    /// Only return the objects that match `filter`.
44    pub fn filter(mut self, filter: ObjectFilter) -> Self {
45        self.filter = Some(filter);
46        self
47    }
48
49    /// Set the page to fetch.
50    pub fn pagination(mut self, pagination: PaginationFilter) -> Self {
51        self.pagination = pagination;
52        self
53    }
54
55    /// Stream every item, page by page, starting at the pagination's cursor
56    /// and in its direction, with its limit as the page size.
57    pub fn stream(self) -> impl Stream<Item = GraphQLResult<Object>> + Unpin {
58        let pagination = self.pagination.clone();
59        stream_paginated_query(move |page| self.clone().pagination(page).send(), pagination)
60    }
61
62    fn operation(
63        filter: Option<ObjectFilter>,
64        pagination: PaginationFilterResponse,
65    ) -> cynic::Operation<ObjectsQueryFragment, ObjectsQueryArgs> {
66        ObjectsQueryFragment::build(ObjectsQueryArgs {
67            after: pagination.after,
68            before: pagination.before,
69            filter,
70            first: pagination.first,
71            last: pagination.last,
72        })
73    }
74
75    async fn send(self) -> GraphQLResult<Page<Object>> {
76        let Self {
77            client,
78            pagination,
79            filter,
80        } = self;
81        let pagination = client.pagination_filter(pagination).await;
82        let response = client
83            .run_query(&Self::operation(filter, pagination))
84            .await?;
85
86        let oc = response.objects;
87        let page_info = oc.page_info;
88        let bcs = oc
89            .nodes
90            .iter()
91            .map(|o| &o.bcs)
92            .filter_map(|b64| b64.as_ref().map(|b| crate::base64::decode(b.0.as_str())))
93            .collect::<crate::error::GraphQLResult<Vec<_>>>()?;
94        let objects = bcs
95            .iter()
96            .map(|b| bcs::from_bytes::<iota_types::Object>(b).map_err(iota_types::BcsError::new))
97            .collect::<Result<Vec<_>, iota_types::BcsError>>()?;
98
99        Ok(Page::new(page_info, objects))
100    }
101}
102
103define_query! {
104    /// Query for [`GraphQLClient::object`]. Await it to send the request.
105    pub struct GetObjectQuery {
106        client: GraphQLClient,
107        object_id: ObjectId,
108        version: Option<Version>,
109    }
110    output: GraphQLResult<Option<Object>>;
111}
112
113impl GetObjectQuery {
114    /// Set the object version. Defaults to the latest version.
115    pub fn version(mut self, version: Version) -> Self {
116        self.version = Some(version);
117        self
118    }
119
120    async fn send(self) -> GraphQLResult<Option<Object>> {
121        let operation = ObjectQueryFragment::build(ObjectQueryArgs {
122            object_id: self.object_id,
123            version: self.version.map(|v| v.as_u64()),
124        });
125
126        let response = self.client.run_query(&operation).await?;
127
128        let obj = response.object;
129        let bcs = obj
130            .and_then(|o| o.bcs)
131            .map(|bcs| crate::base64::decode(bcs.0.as_str()))
132            .transpose()?;
133
134        let object = bcs
135            .map(|b| bcs::from_bytes::<iota_types::Object>(&b).map_err(iota_types::BcsError::new))
136            .transpose()?;
137
138        Ok(object)
139    }
140}
141
142define_query! {
143    /// Query for [`GraphQLClient::move_object_contents`]. Await it to send the
144    /// request.
145    pub struct GetMoveObjectContentsQuery {
146        client: GraphQLClient,
147        object_id: ObjectId,
148        version: Option<Version>,
149    }
150    output: GraphQLResult<Option<serde_json::Value>>;
151}
152
153impl GetMoveObjectContentsQuery {
154    /// Set the object version. Defaults to the latest version.
155    pub fn version(mut self, version: Version) -> Self {
156        self.version = Some(version);
157        self
158    }
159
160    async fn send(self) -> GraphQLResult<Option<serde_json::Value>> {
161        let operation = MoveObjectContentsJsonQueryFragment::build(ObjectQueryArgs {
162            object_id: self.object_id,
163            version: self.version.map(|v| v.as_u64()),
164        });
165
166        let response = self.client.run_query(&operation).await?;
167
168        Ok(response
169            .object
170            .and_then(|o| o.as_move_object)
171            .and_then(|o| o.contents)
172            .map(|mv| mv.json))
173    }
174}
175
176define_query! {
177    /// Query for [`GraphQLClient::move_object_contents_bcs`]. Await it to send
178    /// the request.
179    pub struct GetMoveObjectContentsBcsQuery {
180        client: GraphQLClient,
181        object_id: ObjectId,
182        version: Option<Version>,
183    }
184    output: GraphQLResult<Option<Vec<u8>>>;
185}
186
187impl GetMoveObjectContentsBcsQuery {
188    /// Set the object version. Defaults to the latest version.
189    pub fn version(mut self, version: Version) -> Self {
190        self.version = Some(version);
191        self
192    }
193
194    async fn send(self) -> GraphQLResult<Option<Vec<u8>>> {
195        let operation = MoveObjectContentsBcsQueryFragment::build(ObjectQueryArgs {
196            object_id: self.object_id,
197            version: self.version.map(|v| v.as_u64()),
198        });
199
200        let response = self.client.run_query(&operation).await?;
201
202        response
203            .object
204            .and_then(|o| o.as_move_object)
205            .and_then(|o| o.contents)
206            .map(|bcs| crate::base64::decode(bcs.bcs.0.as_str()))
207            .transpose()
208    }
209}
210
211define_query! {
212    /// Query for [`GraphQLClient::object_bcs`]. Await it to send the request.
213    pub struct GetObjectBcsQuery {
214        client: GraphQLClient,
215        object_id: ObjectId,
216    }
217    output: GraphQLResult<Option<Vec<u8>>>;
218}
219
220impl GetObjectBcsQuery {
221    async fn send(self) -> GraphQLResult<Option<Vec<u8>>> {
222        let operation = ObjectQueryFragment::build(ObjectQueryArgs {
223            object_id: self.object_id,
224            version: None,
225        });
226
227        let response = self.client.run_query(&operation).await?;
228
229        response
230            .object
231            .and_then(|o| o.bcs.map(|bcs| crate::base64::decode(bcs.0.as_str())))
232            .transpose()
233    }
234}
235
236impl GraphQLClient {
237    /// Return an object based on the provided [`Address`](iota_types::Address).
238    ///
239    /// If the object does not exist (e.g., due to pruning), this will resolve
240    /// to `Ok(None)`. Similarly, if this is not an object but an address, it
241    /// will resolve to `Ok(None)`.
242    pub fn object(&self, object_id: ObjectId) -> GetObjectQuery {
243        GetObjectQuery {
244            client: self.clone(),
245            object_id,
246            version: None,
247        }
248    }
249
250    /// Return a page of objects.
251    ///
252    /// Use [`ListObjectsQuery::filter`] together with
253    /// [`ObjectFilter::with_owner`] to get the objects owned by an address.
254    ///
255    /// # Example
256    ///
257    /// ```rust,ignore
258    /// let filter = ObjectFilter::default().with_owner(Address::from_str("test").unwrap());
259    ///
260    /// let owned_objects = client.objects().filter(filter).await;
261    /// ```
262    pub fn objects(&self) -> ListObjectsQuery {
263        ListObjectsQuery::new(self.clone())
264    }
265
266    /// Return the object's bcs content [`Vec<u8>`] based on the provided
267    /// [`Address`](iota_types::Address).
268    pub fn object_bcs(&self, object_id: ObjectId) -> GetObjectBcsQuery {
269        GetObjectBcsQuery {
270            client: self.clone(),
271            object_id,
272        }
273    }
274
275    /// Return the contents JSON of an object that is a Move object.
276    ///
277    /// If the object does not exist (e.g., due to pruning), this will resolve
278    /// to `Ok(None)`. Similarly, if this is not an object but an address, it
279    /// will resolve to `Ok(None)`.
280    pub fn move_object_contents(&self, object_id: ObjectId) -> GetMoveObjectContentsQuery {
281        GetMoveObjectContentsQuery {
282            client: self.clone(),
283            object_id,
284            version: None,
285        }
286    }
287
288    /// Return the BCS of an object that is a Move object.
289    ///
290    /// If the object does not exist (e.g., due to pruning), this will resolve
291    /// to `Ok(None)`. Similarly, if this is not an object but an address, it
292    /// will resolve to `Ok(None)`.
293    pub fn move_object_contents_bcs(&self, object_id: ObjectId) -> GetMoveObjectContentsBcsQuery {
294        GetMoveObjectContentsBcsQuery {
295            client: self.clone(),
296            object_id,
297            version: None,
298        }
299    }
300}
301
302#[cfg(all(test, not(target_arch = "wasm32")))]
303mod tests {
304    use iota_types::{Address, ObjectId, Version};
305
306    use crate::{
307        query_types::ObjectFilter,
308        test_utils::{
309            assert_backward_page, assert_forward_page, backward_page, forward_page, sent_variables,
310            test_client,
311        },
312    };
313
314    #[tokio::test]
315    async fn object_bcs_sends_the_object_id_without_a_version() {
316        let vars = sent_variables("ObjectQueryFragment", |client| async move {
317            let _ = client.object_bcs(ObjectId::SYSTEM_STATE).await;
318        })
319        .await;
320        assert_eq!(vars["objectId"], ObjectId::SYSTEM_STATE.to_string());
321        assert!(vars["version"].is_null());
322    }
323
324    #[tokio::test]
325    async fn object_queries_send_the_object_id_and_version() {
326        let vars = sent_variables("ObjectQueryFragment", |client| async move {
327            let _ = client
328                .object(ObjectId::SYSTEM_STATE)
329                .version(Version::from_u64(3))
330                .await;
331        })
332        .await;
333        assert_eq!(vars["objectId"], ObjectId::SYSTEM_STATE.to_string());
334        assert_eq!(vars["version"], 3);
335
336        let vars = sent_variables("MoveObjectContentsJsonQueryFragment", |client| async move {
337            let _ = client
338                .move_object_contents(ObjectId::SYSTEM_STATE)
339                .version(Version::from_u64(4))
340                .await;
341        })
342        .await;
343        assert_eq!(vars["objectId"], ObjectId::SYSTEM_STATE.to_string());
344        assert_eq!(vars["version"], 4);
345
346        let vars = sent_variables("MoveObjectContentsBcsQueryFragment", |client| async move {
347            let _ = client
348                .move_object_contents_bcs(ObjectId::SYSTEM_STATE)
349                .version(Version::from_u64(5))
350                .await;
351        })
352        .await;
353        assert_eq!(vars["objectId"], ObjectId::SYSTEM_STATE.to_string());
354        assert_eq!(vars["version"], 5);
355
356        let vars = sent_variables("ObjectQueryFragment", |client| async move {
357            let _ = client.object(ObjectId::SYSTEM_STATE).await;
358        })
359        .await;
360        assert!(vars["version"].is_null());
361
362        let vars = sent_variables("MoveObjectContentsJsonQueryFragment", |client| async move {
363            let _ = client.move_object_contents(ObjectId::SYSTEM_STATE).await;
364        })
365        .await;
366        assert!(vars["version"].is_null());
367
368        let vars = sent_variables("MoveObjectContentsBcsQueryFragment", |client| async move {
369            let _ = client
370                .move_object_contents_bcs(ObjectId::SYSTEM_STATE)
371                .await;
372        })
373        .await;
374        assert!(vars["version"].is_null());
375    }
376
377    #[tokio::test]
378    async fn objects_sends_the_filter_and_pagination() {
379        let vars = sent_variables("ObjectsQueryFragment", |client| async move {
380            let _ = client
381                .objects()
382                .filter(ObjectFilter::default().with_owner(Address::FRAMEWORK))
383                .pagination(backward_page())
384                .await;
385        })
386        .await;
387        assert_eq!(vars["filter"]["owner"], Address::FRAMEWORK.to_string());
388        assert_backward_page(&vars);
389
390        let vars = sent_variables("ObjectsQueryFragment", |client| async move {
391            let _ = client
392                .objects()
393                .filter(ObjectFilter::default().with_owner(Address::FRAMEWORK))
394                .pagination(forward_page())
395                .await;
396        })
397        .await;
398        assert_forward_page(&vars);
399    }
400
401    #[tokio::test]
402    async fn test_objects_query() {
403        let client = test_client();
404        let objects = client
405            .objects()
406            .await
407            .map_err(|e| {
408                format!(
409                    "Objects query failed for {} network: Error {e}",
410                    client.rpc_server()
411                )
412            })
413            .unwrap();
414        assert!(
415            !objects.is_empty(),
416            "Objects query returned no data for {} network",
417            client.rpc_server()
418        );
419    }
420
421    #[tokio::test]
422    async fn test_object_query() {
423        let client = test_client();
424        client
425            .object(ObjectId::SYSTEM_STATE)
426            .await
427            .map_err(|e| {
428                format!(
429                    "Object query failed for {} network: Error {e}",
430                    client.rpc_server()
431                )
432            })
433            .unwrap()
434            .unwrap();
435    }
436
437    #[tokio::test]
438    async fn test_object_bcs_query() {
439        let client = test_client();
440        client
441            .object_bcs(ObjectId::SYSTEM_STATE)
442            .await
443            .map_err(|e| {
444                format!(
445                    "Object bcs query failed for {} network: Error {e}",
446                    client.rpc_server()
447                )
448            })
449            .unwrap()
450            .unwrap();
451    }
452}