sui_gql_client/queries/
latest_objects_version.rs

1use std::collections::HashMap;
2
3use af_sui_types::{Address, Version};
4use graphql_extract::extract;
5
6use super::fragments::PageInfoForward;
7use crate::queries::Error;
8use crate::{GraphQlClient, GraphQlResponseExt, missing_data, scalars, schema};
9
10#[derive(cynic::QueryVariables, Clone, Debug)]
11struct Variables<'a> {
12    filter: Option<ObjectFilter<'a>>,
13    after: Option<String>,
14    first: Option<i32>,
15}
16
17impl Variables<'_> {
18    fn next_variables(mut self, page_info: &PageInfoForward) -> Option<Self> {
19        let PageInfoForward {
20            has_next_page,
21            end_cursor,
22        } = page_info;
23        if *has_next_page {
24            self.after.clone_from(end_cursor);
25            Some(self)
26        } else {
27            None
28        }
29    }
30}
31
32#[derive(cynic::InputObject, Clone, Debug, Default)]
33struct ObjectFilter<'a> {
34    #[cynic(rename = "type")]
35    type_: Option<&'a scalars::TypeTag>,
36    owner: Option<&'a Address>,
37    object_ids: Option<&'a [Address]>,
38}
39
40pub async fn query<C: GraphQlClient>(
41    client: &C,
42    object_ids: &[Address],
43) -> super::Result<(u64, HashMap<Address, u64>), C> {
44    let vars = Variables {
45        after: None,
46        first: None,
47        filter: Some(ObjectFilter {
48            object_ids: Some(object_ids),
49            ..Default::default()
50        }),
51    };
52    let init = client
53        .query::<Query, _>(vars.clone())
54        .await
55        .map_err(Error::Client)?
56        .try_into_data()?;
57
58    extract!(init => {
59        checkpoint? {
60            sequence_number
61        }
62        objects {
63            nodes
64            page_info
65        }
66    });
67    let ckpt_num = sequence_number;
68    let init_nodes = nodes;
69
70    let mut next_vars = vars.next_variables(&page_info);
71    let mut pages = vec![];
72    while let Some(vars) = next_vars {
73        let Some(next_page) = client
74            .query::<QueryPage, _>(vars.clone())
75            .await
76            .map_err(Error::Client)?
77            .try_into_data()?
78        else {
79            break;
80        };
81        next_vars = vars.next_variables(&next_page.objects.page_info);
82        pages.push(next_page);
83    }
84
85    let mut raw_objs = HashMap::new();
86    let page_nodes = pages.into_iter().flat_map(|q| q.objects.nodes);
87    for object in init_nodes.into_iter().chain(page_nodes) {
88        let object_id = object.object_id;
89        let version = object.version;
90        raw_objs.insert(object_id, version);
91    }
92    // Ensure all input objects have versions
93    for id in object_ids {
94        raw_objs
95            .contains_key(id)
96            .then_some(())
97            .ok_or(missing_data!("Object version for {id}"))?;
98    }
99
100    Ok((ckpt_num, raw_objs))
101}
102
103#[derive(cynic::QueryFragment, Clone, Debug)]
104#[cynic(variables = "Variables")]
105struct Query {
106    checkpoint: Option<Checkpoint>,
107
108    #[arguments(filter: $filter, first: $first, after: $after)]
109    objects: ObjectConnection,
110}
111
112// =============================================================================
113//  Subsequent pages
114// =============================================================================
115
116#[derive(cynic::QueryFragment, Debug)]
117#[cynic(graphql_type = "Query", variables = "Variables")]
118struct QueryPage {
119    #[arguments(filter: $filter, first: $first, after: $after)]
120    objects: ObjectConnection,
121}
122
123// =============================================================================
124//  Inner query fragments
125// =============================================================================
126
127#[derive(cynic::QueryFragment, Debug, Clone)]
128struct Object {
129    version: Version,
130    #[cynic(rename = "address")]
131    object_id: Address,
132}
133
134#[derive(cynic::QueryFragment, Debug, Clone)]
135struct Checkpoint {
136    sequence_number: Version,
137}
138
139/// `ObjectConnection` where the `Object` fragment does take any parameters.
140#[derive(cynic::QueryFragment, Clone, Debug)]
141#[cynic(graphql_type = "ObjectConnection")]
142struct ObjectConnection {
143    nodes: Vec<Object>,
144    page_info: PageInfoForward,
145}
146
147#[cfg(test)]
148#[allow(clippy::unwrap_used)]
149#[test]
150fn gql_output() {
151    use cynic::QueryBuilder as _;
152
153    let ids = vec![
154        "0x4264c07a42f9d002c1244e43a1f0fa21c49e4a25c7202c597b8476ef6bb57113"
155            .parse()
156            .unwrap(),
157        "0x60d1a85f81172a7418206f4b16e1e07e40c91cf58783f63f18a25efc81442dcb"
158            .parse()
159            .unwrap(),
160    ];
161
162    let vars = Variables {
163        filter: Some(ObjectFilter {
164            object_ids: Some(&ids),
165            ..Default::default()
166        }),
167        after: None,
168        first: None,
169    };
170
171    let operation = Query::build(vars);
172    insta::assert_snapshot!(operation.query, @r###"
173    query Query($filter: ObjectFilter, $after: String, $first: Int) {
174      checkpoint {
175        sequenceNumber
176      }
177      objects(filter: $filter, first: $first, after: $after) {
178        nodes {
179          version
180          address
181        }
182        pageInfo {
183          hasNextPage
184          endCursor
185        }
186      }
187    }
188    "###);
189}
190
191#[cfg(test)]
192#[allow(clippy::unwrap_used)]
193#[test]
194fn page_gql_output() {
195    use cynic::QueryBuilder as _;
196    let ids = vec![
197        "0x4264c07a42f9d002c1244e43a1f0fa21c49e4a25c7202c597b8476ef6bb57113"
198            .parse()
199            .unwrap(),
200        "0x60d1a85f81172a7418206f4b16e1e07e40c91cf58783f63f18a25efc81442dcb"
201            .parse()
202            .unwrap(),
203    ];
204    let vars = Variables {
205        filter: Some(ObjectFilter {
206            object_ids: Some(&ids),
207            ..Default::default()
208        }),
209        after: None,
210        first: None,
211    };
212    let operation = QueryPage::build(vars);
213    insta::assert_snapshot!(operation.query, @r###"
214    query QueryPage($filter: ObjectFilter, $after: String, $first: Int) {
215      objects(filter: $filter, first: $first, after: $after) {
216        nodes {
217          version
218          address
219        }
220        pageInfo {
221          hasNextPage
222          endCursor
223        }
224      }
225    }
226    "###);
227}