Skip to main content

iota_sdk_graphql_client/api/
move_objects.rs

1// Copyright (c) 2026 IOTA Stiftung
2// SPDX-License-Identifier: Apache-2.0
3
4//! Typed object queries, decoding each object into a Move-type mirror paired
5//! with its object reference.
6
7use std::marker::PhantomData;
8
9use futures::Stream;
10use iota_move_types::MoveObject;
11use iota_types::{Address, ObjectId, ObjectReference};
12
13use crate::{
14    GraphQLClient, ListObjectsQuery,
15    api::define_query,
16    error::GraphQLResult,
17    pagination::{Page, PaginationFilter},
18    query_types::ObjectFilter,
19    streams::stream_paginated_query,
20};
21
22/// An object of the Move type `T`, decoded into `T`.
23#[derive(Clone, Debug)]
24pub struct OwnedMoveObject<T> {
25    object_ref: ObjectReference,
26    object: T,
27}
28
29impl<T> OwnedMoveObject<T> {
30    /// Get the object's reference.
31    pub fn object_ref(&self) -> ObjectReference {
32        self.object_ref
33    }
34
35    /// Get the object's contents.
36    pub fn object(&self) -> &T {
37        &self.object
38    }
39
40    /// Consume the object and return its contents.
41    pub fn into_object(self) -> T {
42        self.object
43    }
44}
45
46/// Filter for the typed object queries.
47///
48/// [`ObjectFilter`] without its type field: the Move type comes from the type
49/// parameter, so there is no second place to set it and nothing to disagree
50/// about.
51#[derive(Clone, Debug, Default, Eq, PartialEq)]
52pub struct MoveObjectFilter {
53    /// Filter by the address owning the object.
54    owner: Option<Address>,
55    /// Filter by object ids.
56    object_ids: Option<Vec<ObjectId>>,
57}
58
59impl MoveObjectFilter {
60    /// Filter by the address owning the object.
61    pub fn with_owner(mut self, owner: Address) -> Self {
62        self.owner = Some(owner);
63        self
64    }
65
66    /// Filter by object ids.
67    pub fn with_object_ids(mut self, object_ids: Vec<ObjectId>) -> Self {
68        self.object_ids = Some(object_ids);
69        self
70    }
71
72    /// Widen into an [`ObjectFilter`] pinned to `T`'s Move type.
73    fn into_object_filter<T: MoveObject>(self) -> ObjectFilter {
74        ObjectFilter {
75            type_tag: Some(T::struct_tag().to_string()),
76            owner: self.owner,
77            object_ids: self.object_ids,
78        }
79    }
80}
81
82define_query! {
83    /// Query for [`GraphQLClient::move_objects`]. Await it to send the request.
84    #[derive(Clone)]
85    pub struct ListMoveObjectsQuery<T: MoveObject> {
86        client: GraphQLClient,
87        filter: Option<MoveObjectFilter>,
88        pagination: PaginationFilter,
89        _marker: PhantomData<fn() -> T>,
90    }
91    output: GraphQLResult<Page<OwnedMoveObject<T>>>;
92}
93
94impl<T: MoveObject> ListMoveObjectsQuery<T> {
95    /// Only return the objects that match `filter`.
96    pub fn filter(mut self, filter: MoveObjectFilter) -> Self {
97        self.filter = Some(filter);
98        self
99    }
100
101    /// Set the page to fetch.
102    pub fn pagination(mut self, pagination: PaginationFilter) -> Self {
103        self.pagination = pagination;
104        self
105    }
106
107    /// Stream every item, page by page, starting at the pagination's cursor
108    /// and in its direction, with its limit as the page size.
109    pub fn stream(self) -> impl Stream<Item = GraphQLResult<OwnedMoveObject<T>>> + Unpin
110    where
111        T: Clone + Unpin,
112    {
113        let pagination = self.pagination.clone();
114        stream_paginated_query(move |page| self.clone().pagination(page).send(), pagination)
115    }
116
117    fn objects_query(self) -> ListObjectsQuery {
118        ListObjectsQuery::new(self.client)
119            .filter(self.filter.unwrap_or_default().into_object_filter::<T>())
120            .pagination(self.pagination)
121    }
122
123    async fn send(self) -> GraphQLResult<Page<OwnedMoveObject<T>>> {
124        let page = self.objects_query().await?;
125        let (page_info, objects) = page.into_parts();
126        let decoded = objects
127            .iter()
128            .map(|object| {
129                Ok(OwnedMoveObject {
130                    object_ref: object.object_ref(),
131                    object: T::try_from(object)?,
132                })
133            })
134            .collect::<GraphQLResult<Vec<_>>>()?;
135        Ok(Page::new(page_info, decoded))
136    }
137}
138
139impl GraphQLClient {
140    /// Return a page of objects of the Move type `T`, decoded into `T` and
141    /// paired with their object references.
142    ///
143    /// The type filter is derived from `T`, so unlike
144    /// [`GraphQLClient::objects`] this needs no type string and no separate
145    /// decode step.
146    ///
147    /// # Errors
148    ///
149    /// Returns an error if any object in the page fails to decode. The query
150    /// filters on `T`'s exact type, so a failure means the on-chain type has
151    /// moved out from under the mirror rather than that one object is odd —
152    /// yielding the rest of the page would hide that.
153    ///
154    /// # Example
155    ///
156    /// ```rust,ignore
157    /// let staked: Page<OwnedMoveObject<StakedIota>> = client
158    ///     .move_objects()
159    ///     .filter(MoveObjectFilter::default().with_owner(address))
160    ///     .await?;
161    /// ```
162    pub fn move_objects<T: MoveObject>(&self) -> ListMoveObjectsQuery<T> {
163        ListMoveObjectsQuery {
164            client: self.clone(),
165            filter: None,
166            pagination: PaginationFilter::default(),
167            _marker: PhantomData,
168        }
169    }
170}
171
172#[cfg(all(test, not(target_arch = "wasm32")))]
173mod tests {
174    use futures::StreamExt;
175    use iota_move_types::iota_framework::{coin::Coin, iota::IOTA};
176
177    use super::*;
178    use crate::test_utils::{
179        assert_backward_page, assert_forward_page, backward_page, forward_page, sent_variables,
180        test_client,
181    };
182
183    #[tokio::test]
184    async fn move_objects_sends_the_type_filter_and_pagination() {
185        let vars = sent_variables("ObjectsQueryFragment", |client| async move {
186            let _ = client
187                .move_objects::<Coin<IOTA>>()
188                .filter(MoveObjectFilter::default().with_owner(Address::STD))
189                .pagination(backward_page())
190                .await;
191        })
192        .await;
193        assert_eq!(vars["filter"]["owner"], Address::STD.to_string());
194        assert_eq!(
195            vars["filter"]["type"],
196            Coin::<IOTA>::struct_tag().to_string()
197        );
198        assert_backward_page(&vars);
199
200        let vars = sent_variables("ObjectsQueryFragment", |client| async move {
201            let _ = client
202                .move_objects::<Coin<IOTA>>()
203                .filter(MoveObjectFilter::default().with_owner(Address::STD))
204                .pagination(forward_page())
205                .await;
206        })
207        .await;
208        assert_forward_page(&vars);
209    }
210
211    #[tokio::test]
212    async fn test_move_objects_query() {
213        let client = test_client();
214        let coins = client
215            .move_objects::<Coin<IOTA>>()
216            .await
217            .map_err(|e| {
218                format!(
219                    "Move objects query failed for {} network: Error {e}",
220                    client.rpc_server()
221                )
222            })
223            .unwrap();
224        assert!(
225            !coins.is_empty(),
226            "Move objects query returned no data for {} network",
227            client.rpc_server()
228        );
229    }
230
231    /// The type filter comes from `T`, so a page reached this way holds only
232    /// objects of that type — every one of them decoded, or the call failed.
233    #[tokio::test]
234    async fn test_move_objects_stream() {
235        let client = test_client();
236        let mut stream = client.move_objects::<Coin<IOTA>>().stream();
237        stream
238            .next()
239            .await
240            .unwrap_or_else(|| {
241                panic!(
242                    "Move objects stream yielded nothing for {} network",
243                    client.rpc_server()
244                )
245            })
246            .map_err(|e| {
247                format!(
248                    "Move objects stream failed for {} network: Error {e}",
249                    client.rpc_server()
250                )
251            })
252            .unwrap();
253    }
254}