Skip to main content

iota_sdk_graphql_client/api/
package.rs

1// Copyright (c) Mysten Labs, Inc.
2// Modifications Copyright (c) 2026 IOTA Stiftung
3// SPDX-License-Identifier: Apache-2.0
4
5//! Package API implementation.
6
7use cynic::QueryBuilder;
8use futures::Stream;
9use iota_types::{Address, MovePackage, Object, Version};
10
11use crate::{
12    GraphQLClient, MoveFunction, MoveModule, Page,
13    api::define_query,
14    error::GraphQLResult,
15    pagination::{PaginationFilter, PaginationFilterResponse},
16    query_types::{
17        LatestPackageQueryFragment, MovePackageVersionFilter, NormalizedMoveFunctionQueryArgs,
18        NormalizedMoveFunctionQueryFragment, NormalizedMoveModuleQueryArgs,
19        NormalizedMoveModuleQueryFragment, PackageArgs, PackageCheckpointFilter,
20        PackageQueryFragment, PackageVersionsArgs, PackageVersionsQueryFragment, PackagesQueryArgs,
21        PackagesQueryFragment,
22    },
23    streams::stream_paginated_query,
24};
25
26define_query! {
27    /// Query for [`GraphQLClient::package_versions`]. Await it to send the
28    /// request.
29    #[derive(Clone)]
30    pub struct ListPackageVersionsQuery {
31        client: GraphQLClient,
32        address: Address,
33        pagination: PaginationFilter,
34        after_version: Option<Version>,
35        before_version: Option<Version>,
36    }
37    output: GraphQLResult<Page<MovePackage>>;
38}
39
40impl ListPackageVersionsQuery {
41    /// Set the page to fetch.
42    pub fn pagination(mut self, pagination: PaginationFilter) -> Self {
43        self.pagination = pagination;
44        self
45    }
46
47    /// Stream every item, page by page, starting at the pagination's cursor
48    /// and in its direction, with its limit as the page size.
49    pub fn stream(self) -> impl Stream<Item = GraphQLResult<MovePackage>> + Unpin {
50        let pagination = self.pagination.clone();
51        stream_paginated_query(move |page| self.clone().pagination(page).send(), pagination)
52    }
53
54    /// Only return versions after this one.
55    pub fn after_version(mut self, after_version: Version) -> Self {
56        self.after_version = Some(after_version);
57        self
58    }
59
60    /// Only return versions before this one.
61    pub fn before_version(mut self, before_version: Version) -> Self {
62        self.before_version = Some(before_version);
63        self
64    }
65
66    fn operation<'a>(
67        address: Address,
68        after_version: Option<Version>,
69        before_version: Option<Version>,
70        pagination: &'a PaginationFilterResponse,
71    ) -> cynic::Operation<PackageVersionsQueryFragment, PackageVersionsArgs<'a>> {
72        PackageVersionsQueryFragment::build(PackageVersionsArgs {
73            address,
74            after: pagination.after.as_deref(),
75            before: pagination.before.as_deref(),
76            first: pagination.first,
77            last: pagination.last,
78            filter: Some(MovePackageVersionFilter {
79                after_version: after_version.map(|v| v.as_u64()),
80                before_version: before_version.map(|v| v.as_u64()),
81            }),
82        })
83    }
84
85    async fn send(self) -> GraphQLResult<Page<MovePackage>> {
86        let Self {
87            client,
88            pagination,
89            address,
90            after_version,
91            before_version,
92        } = self;
93        let pagination = client.pagination_filter(pagination).await;
94        let response = client
95            .run_query(&Self::operation(
96                address,
97                after_version,
98                before_version,
99                &pagination,
100            ))
101            .await?;
102
103        let pc = response.package_versions;
104        let page_info = pc.page_info;
105        let bcs = pc
106            .nodes
107            .iter()
108            .map(|p| &p.bcs)
109            .filter_map(|b64| b64.as_ref().map(|b| crate::base64::decode(b.0.as_str())))
110            .collect::<crate::error::GraphQLResult<Vec<_>>>()?;
111        let packages = bcs
112            .iter()
113            .map(|b| {
114                Ok(bcs::from_bytes::<Object>(b)
115                    .map_err(iota_types::BcsError::new)?
116                    .data
117                    .into_package())
118            })
119            .collect::<Result<Vec<_>, iota_types::BcsError>>()?;
120
121        Ok(Page::new(page_info, packages))
122    }
123}
124
125define_query! {
126    /// Query for [`GraphQLClient::packages`]. Await it to send the request.
127    #[derive(Clone)]
128    pub struct ListPackagesQuery {
129        client: GraphQLClient,
130        pagination: PaginationFilter,
131        after_checkpoint: Option<u64>,
132        before_checkpoint: Option<u64>,
133    }
134    output: GraphQLResult<Page<MovePackage>>;
135}
136
137impl ListPackagesQuery {
138    /// Set the page to fetch.
139    pub fn pagination(mut self, pagination: PaginationFilter) -> Self {
140        self.pagination = pagination;
141        self
142    }
143
144    /// Stream every item, page by page, starting at the pagination's cursor
145    /// and in its direction, with its limit as the page size.
146    pub fn stream(self) -> impl Stream<Item = GraphQLResult<MovePackage>> + Unpin {
147        let pagination = self.pagination.clone();
148        stream_paginated_query(move |page| self.clone().pagination(page).send(), pagination)
149    }
150
151    /// Only return packages published after this checkpoint.
152    pub fn after_checkpoint(mut self, after_checkpoint: u64) -> Self {
153        self.after_checkpoint = Some(after_checkpoint);
154        self
155    }
156
157    /// Only return packages published before this checkpoint.
158    pub fn before_checkpoint(mut self, before_checkpoint: u64) -> Self {
159        self.before_checkpoint = Some(before_checkpoint);
160        self
161    }
162
163    fn operation<'a>(
164        after_checkpoint: Option<u64>,
165        before_checkpoint: Option<u64>,
166        pagination: &'a PaginationFilterResponse,
167    ) -> cynic::Operation<PackagesQueryFragment, PackagesQueryArgs<'a>> {
168        PackagesQueryFragment::build(PackagesQueryArgs {
169            after: pagination.after.as_deref(),
170            before: pagination.before.as_deref(),
171            first: pagination.first,
172            last: pagination.last,
173            filter: Some(PackageCheckpointFilter {
174                after_checkpoint,
175                before_checkpoint,
176            }),
177        })
178    }
179
180    async fn send(self) -> GraphQLResult<Page<MovePackage>> {
181        let Self {
182            client,
183            pagination,
184            after_checkpoint,
185            before_checkpoint,
186        } = self;
187        let pagination = client.pagination_filter(pagination).await;
188        let response = client
189            .run_query(&Self::operation(
190                after_checkpoint,
191                before_checkpoint,
192                &pagination,
193            ))
194            .await?;
195
196        let pc = response.packages;
197        let page_info = pc.page_info;
198        let bcs = pc
199            .nodes
200            .iter()
201            .map(|p| &p.bcs)
202            .filter_map(|b64| b64.as_ref().map(|b| crate::base64::decode(b.0.as_str())))
203            .collect::<crate::error::GraphQLResult<Vec<_>>>()?;
204        let packages = bcs
205            .iter()
206            .map(|b| {
207                Ok(bcs::from_bytes::<Object>(b)
208                    .map_err(iota_types::BcsError::new)?
209                    .data
210                    .into_package())
211            })
212            .collect::<Result<Vec<_>, iota_types::BcsError>>()?;
213
214        Ok(Page::new(page_info, packages))
215    }
216}
217
218define_query! {
219    /// Query for [`GraphQLClient::normalized_move_module`]. Await it to send
220    /// the request.
221    pub struct GetNormalizedMoveModuleQuery {
222        client: GraphQLClient,
223        package: Address,
224        module: String,
225        version: Option<Version>,
226        enums_pagination: PaginationFilter,
227        friends_pagination: PaginationFilter,
228        functions_pagination: PaginationFilter,
229        structs_pagination: PaginationFilter,
230    }
231    output: GraphQLResult<Option<MoveModule>>;
232}
233
234struct ModulePagination {
235    enums: PaginationFilterResponse,
236    friends: PaginationFilterResponse,
237    functions: PaginationFilterResponse,
238    structs: PaginationFilterResponse,
239}
240
241impl GetNormalizedMoveModuleQuery {
242    /// Set the package version.
243    pub fn version(mut self, version: Version) -> Self {
244        self.version = Some(version);
245        self
246    }
247
248    /// Set the page of the module's enums to fetch.
249    pub fn enums_pagination(mut self, pagination: PaginationFilter) -> Self {
250        self.enums_pagination = pagination;
251        self
252    }
253
254    /// Set the page of the module's friends to fetch.
255    pub fn friends_pagination(mut self, pagination: PaginationFilter) -> Self {
256        self.friends_pagination = pagination;
257        self
258    }
259
260    /// Set the page of the module's functions to fetch.
261    pub fn functions_pagination(mut self, pagination: PaginationFilter) -> Self {
262        self.functions_pagination = pagination;
263        self
264    }
265
266    /// Set the page of the module's structs to fetch.
267    pub fn structs_pagination(mut self, pagination: PaginationFilter) -> Self {
268        self.structs_pagination = pagination;
269        self
270    }
271
272    fn operation<'a>(
273        package: Address,
274        module: &'a str,
275        version: Option<Version>,
276        pagination: &'a ModulePagination,
277    ) -> cynic::Operation<NormalizedMoveModuleQueryFragment, NormalizedMoveModuleQueryArgs<'a>>
278    {
279        let ModulePagination {
280            enums,
281            friends,
282            functions,
283            structs,
284        } = pagination;
285        NormalizedMoveModuleQueryFragment::build(NormalizedMoveModuleQueryArgs {
286            package,
287            module,
288            version: version.map(|v| v.as_u64()),
289            after_enums: enums.after.as_deref(),
290            after_functions: functions.after.as_deref(),
291            after_structs: structs.after.as_deref(),
292            after_friends: friends.after.as_deref(),
293            before_enums: enums.before.as_deref(),
294            before_functions: functions.before.as_deref(),
295            before_structs: structs.before.as_deref(),
296            before_friends: friends.before.as_deref(),
297            first_enums: enums.first,
298            first_functions: functions.first,
299            first_structs: structs.first,
300            first_friends: friends.first,
301            last_enums: enums.last,
302            last_functions: functions.last,
303            last_structs: structs.last,
304            last_friends: friends.last,
305        })
306    }
307
308    async fn send(self) -> GraphQLResult<Option<MoveModule>> {
309        let Self {
310            client,
311            package,
312            module,
313            version,
314            enums_pagination,
315            friends_pagination,
316            functions_pagination,
317            structs_pagination,
318        } = self;
319        let pagination = ModulePagination {
320            enums: client.pagination_filter(enums_pagination).await,
321            friends: client.pagination_filter(friends_pagination).await,
322            functions: client.pagination_filter(functions_pagination).await,
323            structs: client.pagination_filter(structs_pagination).await,
324        };
325        let response = client
326            .run_query(&Self::operation(package, &module, version, &pagination))
327            .await?;
328
329        Ok(response.package.and_then(|p| p.module).map(Into::into))
330    }
331}
332
333define_query! {
334    /// Query for [`GraphQLClient::package`]. Await it to send the request.
335    pub struct GetPackageQuery {
336        client: GraphQLClient,
337        address: Address,
338        version: Option<Version>,
339    }
340    output: GraphQLResult<Option<MovePackage>>;
341}
342
343impl GetPackageQuery {
344    /// Set the package version. Without it, the package is loaded from the
345    /// given address.
346    pub fn version(mut self, version: Version) -> Self {
347        self.version = Some(version);
348        self
349    }
350
351    async fn send(self) -> GraphQLResult<Option<MovePackage>> {
352        let operation = PackageQueryFragment::build(PackageArgs {
353            address: self.address,
354            version: self.version.map(|v| v.as_u64()),
355        });
356
357        let response = self.client.run_query(&operation).await?;
358
359        Ok(response
360            .package
361            .and_then(|x| x.bcs)
362            .map(|bcs| crate::base64::decode(bcs.0.as_str()))
363            .transpose()?
364            .map(|bcs| bcs::from_bytes::<Object>(&bcs).map_err(iota_types::BcsError::new))
365            .transpose()?
366            .map(|obj| obj.data.into_package()))
367    }
368}
369
370define_query! {
371    /// Query for [`GraphQLClient::normalized_move_function`]. Await it to send
372    /// the request.
373    pub struct GetNormalizedMoveFunctionQuery {
374        client: GraphQLClient,
375        package: Address,
376        module: String,
377        function: String,
378        version: Option<Version>,
379    }
380    output: GraphQLResult<Option<MoveFunction>>;
381}
382
383impl GetNormalizedMoveFunctionQuery {
384    /// Set the package version. Without it, the package at the given address
385    /// is used.
386    pub fn version(mut self, version: Version) -> Self {
387        self.version = Some(version);
388        self
389    }
390
391    async fn send(self) -> GraphQLResult<Option<MoveFunction>> {
392        let operation =
393            NormalizedMoveFunctionQueryFragment::build(NormalizedMoveFunctionQueryArgs {
394                address: self.package,
395                module: &self.module,
396                function: &self.function,
397                version: self.version.map(|v| v.as_u64()),
398            });
399        let response = self.client.run_query(&operation).await?;
400
401        Ok(response
402            .package
403            .and_then(|p| p.module)
404            .and_then(|m| m.function)
405            .map(Into::into))
406    }
407}
408
409define_query! {
410    /// Query for [`GraphQLClient::package_latest`]. Await it to send the
411    /// request.
412    pub struct GetPackageLatestQuery {
413        client: GraphQLClient,
414        address: Address,
415    }
416    output: GraphQLResult<Option<MovePackage>>;
417}
418
419impl GetPackageLatestQuery {
420    async fn send(self) -> GraphQLResult<Option<MovePackage>> {
421        let operation = LatestPackageQueryFragment::build(PackageArgs {
422            address: self.address,
423            version: None,
424        });
425
426        let response = self.client.run_query(&operation).await?;
427
428        Ok(response
429            .latest_package
430            .and_then(|x| x.bcs)
431            .map(|bcs| crate::base64::decode(&bcs.0))
432            .transpose()?
433            .map(|bcs| bcs::from_bytes::<Object>(&bcs).map_err(iota_types::BcsError::new))
434            .transpose()?
435            .map(|obj| obj.data.into_package()))
436    }
437}
438
439impl GraphQLClient {
440    /// The package corresponding to the given address (at the optionally given
441    /// version). When no version is given, the package is loaded directly
442    /// from the address given. Otherwise, the address is translated before
443    /// loading to point to the package whose original ID matches
444    /// the package at address, but whose version is version. For non-system
445    /// packages, this might result in a different address than address
446    /// because different versions of a package, introduced by upgrades,
447    /// exist at distinct addresses.
448    ///
449    /// Note that this interpretation of version is different from a historical
450    /// object read (the interpretation of version for the object query).
451    pub fn package(&self, address: Address) -> GetPackageQuery {
452        GetPackageQuery {
453            client: self.clone(),
454            address,
455            version: None,
456        }
457    }
458
459    /// Fetch all versions of package at address (packages that share this
460    /// package's original ID), optionally bounding the versions exclusively
461    /// with [`ListPackageVersionsQuery::after_version`] and
462    /// [`ListPackageVersionsQuery::before_version`].
463    pub fn package_versions(&self, address: Address) -> ListPackageVersionsQuery {
464        ListPackageVersionsQuery {
465            client: self.clone(),
466            address,
467            pagination: PaginationFilter::default(),
468            after_version: None,
469            before_version: None,
470        }
471    }
472
473    /// Fetch the latest version of the package at address.
474    /// This corresponds to the package with the highest version that shares its
475    /// original ID with the package at address.
476    pub fn package_latest(&self, address: Address) -> GetPackageLatestQuery {
477        GetPackageLatestQuery {
478            client: self.clone(),
479            address,
480        }
481    }
482
483    /// The Move packages that exist in the network, optionally bounded
484    /// exclusively with [`ListPackagesQuery::after_checkpoint`] and
485    /// [`ListPackagesQuery::before_checkpoint`].
486    ///
487    /// This query returns all versions of a given user package that appear
488    /// between the specified checkpoints, but only records the latest
489    /// versions of system packages.
490    pub fn packages(&self) -> ListPackagesQuery {
491        ListPackagesQuery {
492            client: self.clone(),
493            pagination: PaginationFilter::default(),
494            after_checkpoint: None,
495            before_checkpoint: None,
496        }
497    }
498
499    /// Return the normalized Move function data for the provided package,
500    /// module, and function.
501    pub fn normalized_move_function(
502        &self,
503        package: Address,
504        module: impl Into<String>,
505        function: impl Into<String>,
506    ) -> GetNormalizedMoveFunctionQuery {
507        GetNormalizedMoveFunctionQuery {
508            client: self.clone(),
509            package,
510            module: module.into(),
511            function: function.into(),
512            version: None,
513        }
514    }
515
516    /// Return the normalized Move module data for the provided module.
517    // TODO: do we want to self paginate everything and return all the data, or keep
518    // pagination options?
519    pub fn normalized_move_module(
520        &self,
521        package: Address,
522        module: impl Into<String>,
523    ) -> GetNormalizedMoveModuleQuery {
524        GetNormalizedMoveModuleQuery {
525            client: self.clone(),
526            package,
527            module: module.into(),
528            version: None,
529            enums_pagination: PaginationFilter::default(),
530            friends_pagination: PaginationFilter::default(),
531            functions_pagination: PaginationFilter::default(),
532            structs_pagination: PaginationFilter::default(),
533        }
534    }
535}
536
537#[cfg(all(test, not(target_arch = "wasm32")))]
538mod tests {
539    use iota_types::{Address, Version};
540
541    use crate::{
542        Direction, PaginationFilter,
543        test_utils::{
544            assert_backward_page, assert_forward_page, backward_page, forward_page, sent_variables,
545            test_client,
546        },
547    };
548
549    #[tokio::test]
550    async fn package_latest_sends_the_address_without_a_version() {
551        let vars = sent_variables("LatestPackageQueryFragment", |client| async move {
552            let _ = client.package_latest(Address::FRAMEWORK).await;
553        })
554        .await;
555        assert_eq!(vars["address"], Address::FRAMEWORK.to_string());
556        assert!(vars["version"].is_null());
557    }
558
559    #[tokio::test]
560    async fn package_sends_the_address_and_version() {
561        let vars = sent_variables("PackageQueryFragment", |client| async move {
562            let _ = client
563                .package(Address::FRAMEWORK)
564                .version(Version::from_u64(3))
565                .await;
566        })
567        .await;
568        assert_eq!(vars["address"], Address::FRAMEWORK.to_string());
569        assert_eq!(vars["version"], 3);
570
571        let vars = sent_variables("PackageQueryFragment", |client| async move {
572            let _ = client.package(Address::FRAMEWORK).await;
573        })
574        .await;
575        assert!(vars["version"].is_null());
576    }
577
578    #[tokio::test]
579    async fn normalized_move_function_sends_the_function_and_version() {
580        let vars = sent_variables("NormalizedMoveFunctionQueryFragment", |client| async move {
581            let _ = client
582                .normalized_move_function(Address::FRAMEWORK, "coin", "value")
583                .version(Version::from_u64(3))
584                .await;
585        })
586        .await;
587        assert_eq!(vars["address"], Address::FRAMEWORK.to_string());
588        assert_eq!(vars["module"], "coin");
589        assert_eq!(vars["function"], "value");
590        assert_eq!(vars["version"], 3);
591
592        let vars = sent_variables("NormalizedMoveFunctionQueryFragment", |client| async move {
593            let _ = client
594                .normalized_move_function(Address::FRAMEWORK, "coin", "value")
595                .await;
596        })
597        .await;
598        assert!(vars["version"].is_null());
599    }
600
601    #[tokio::test]
602    async fn package_versions_sends_the_address_versions_and_pagination() {
603        let vars = sent_variables("PackageVersionsQueryFragment", |client| async move {
604            let _ = client
605                .package_versions(Address::FRAMEWORK)
606                .after_version(Version::from_u64(2))
607                .before_version(Version::from_u64(5))
608                .pagination(backward_page())
609                .await;
610        })
611        .await;
612        assert_eq!(vars["address"], Address::FRAMEWORK.to_string());
613        assert_eq!(vars["filter"]["afterVersion"], 2);
614        assert_eq!(vars["filter"]["beforeVersion"], 5);
615        assert_backward_page(&vars);
616
617        let vars = sent_variables("PackageVersionsQueryFragment", |client| async move {
618            let _ = client
619                .package_versions(Address::FRAMEWORK)
620                .after_version(Version::from_u64(2))
621                .before_version(Version::from_u64(5))
622                .pagination(forward_page())
623                .await;
624        })
625        .await;
626        assert_forward_page(&vars);
627    }
628
629    #[tokio::test]
630    async fn packages_sends_the_checkpoints_and_pagination() {
631        let vars = sent_variables("PackagesQueryFragment", |client| async move {
632            let _ = client
633                .packages()
634                .after_checkpoint(2)
635                .before_checkpoint(5)
636                .pagination(backward_page())
637                .await;
638        })
639        .await;
640        assert_eq!(vars["filter"]["afterCheckpoint"], 2);
641        assert_eq!(vars["filter"]["beforeCheckpoint"], 5);
642        assert_backward_page(&vars);
643
644        let vars = sent_variables("PackagesQueryFragment", |client| async move {
645            let _ = client
646                .packages()
647                .after_checkpoint(2)
648                .before_checkpoint(5)
649                .pagination(forward_page())
650                .await;
651        })
652        .await;
653        assert_forward_page(&vars);
654    }
655
656    #[tokio::test]
657    async fn normalized_move_module_sends_each_pagination_to_its_field() {
658        let page = |direction, cursor: &str, limit| PaginationFilter {
659            direction,
660            cursor: Some(cursor.to_owned()),
661            limit: Some(limit),
662        };
663        let vars = sent_variables("NormalizedMoveModuleQueryFragment", |client| async move {
664            let _ = client
665                .normalized_move_module(Address::FRAMEWORK, "coin")
666                .version(Version::from_u64(4))
667                .enums_pagination(page(Direction::Forward, "enums", 1))
668                .friends_pagination(page(Direction::Backward, "friends", 2))
669                .functions_pagination(page(Direction::Forward, "functions", 3))
670                .structs_pagination(page(Direction::Backward, "structs", 5))
671                .await;
672        })
673        .await;
674        assert_eq!(vars["package"], Address::FRAMEWORK.to_string());
675        assert_eq!(vars["module"], "coin");
676        assert_eq!(vars["version"], 4);
677        for (member, direction, limit) in [
678            ("Enums", Direction::Forward, 1),
679            ("Friends", Direction::Backward, 2),
680            ("Functions", Direction::Forward, 3),
681            ("Structs", Direction::Backward, 5),
682        ] {
683            let cursor = member.to_lowercase();
684            let (used, unused) = match direction {
685                Direction::Forward => (("after", "first"), ("before", "last")),
686                Direction::Backward => (("before", "last"), ("after", "first")),
687            };
688            assert_eq!(vars[format!("{}{member}", used.0)], cursor);
689            assert_eq!(vars[format!("{}{member}", used.1)], limit);
690            assert!(vars[format!("{}{member}", unused.0)].is_null());
691            assert!(vars[format!("{}{member}", unused.1)].is_null());
692        }
693    }
694
695    #[tokio::test]
696    async fn test_package() {
697        let client = test_client();
698        client
699            .package(Address::FRAMEWORK)
700            .await
701            .map_err(|e| {
702                format!(
703                    "Package query failed for {} network. Error: {e}",
704                    client.rpc_server()
705                )
706            })
707            .unwrap()
708            .unwrap();
709    }
710
711    #[tokio::test]
712    async fn test_latest_package_query() {
713        let client = test_client();
714        client
715            .package_latest(Address::FRAMEWORK)
716            .await
717            .map_err(|e| {
718                format!(
719                    "Latest package query failed for {} network. Error: {e}",
720                    client.rpc_server()
721                )
722            })
723            .unwrap()
724            .unwrap();
725    }
726
727    #[tokio::test]
728    async fn test_packages_query() {
729        let client = test_client();
730        let packages = client
731            .packages()
732            .await
733            .map_err(|e| {
734                format!(
735                    "Packages query failed for {} network. Error: {e}",
736                    client.rpc_server()
737                )
738            })
739            .unwrap();
740
741        assert!(
742            !packages.is_empty(),
743            "Packages query returned no data for {} network",
744            client.rpc_server()
745        );
746    }
747}