sui_gql_client/queries/
object_args.rs

1//! For requesting [`ObjectArg`]s from the server. Defines [`object_args!`](crate::object_args!).
2use af_sui_types::{Address, ObjectArg, Version};
3use bimap::BiMap;
4use futures::TryStreamExt as _;
5use itertools::Itertools as _;
6use sui_gql_schema::scalars;
7
8use super::fragments::ObjectFilterV2;
9use super::objects_flat::Variables;
10use crate::{GraphQlClient, GraphQlErrors, GraphQlResponseExt as _, schema};
11
12type Query = super::objects_flat::Query<Object>;
13
14// WARN: can't call this `Result`, otherwise it will mess with the code generated by the derive
15// macros in this module.
16type Res<T, C> = std::result::Result<T, Error<<C as GraphQlClient>::Error>>;
17
18#[derive(thiserror::Error, Debug)]
19pub enum Error<T> {
20    #[error(transparent)]
21    Client(T),
22    #[error(transparent)]
23    Server(#[from] GraphQlErrors),
24    #[error("No data in object args query response")]
25    NoData,
26    #[error("Response missing object args for pairs: {0:?}")]
27    MissingNamedArgs(Vec<(String, Address)>),
28}
29
30/// Turn a bijective map of names and object ids into one of names and object args.
31///
32/// Fails if the query response does not have the necessary data for the input map.
33pub(super) async fn query<C: GraphQlClient>(
34    client: &C,
35    mut names: BiMap<String, Address>,
36    page_size: Option<u32>,
37) -> Res<BiMap<String, ObjectArg>, C> {
38    let object_ids = names.right_values().cloned().collect_vec();
39    let filter = ObjectFilterV2 {
40        object_ids: Some(&object_ids),
41        type_: None,
42        owner: None,
43    };
44    let vars = Variables {
45        filter: Some(filter),
46        after: None,
47        first: page_size.map(|n| n as i32),
48    };
49
50    let mut stream = std::pin::pin!(super::stream::forward(client, vars, request));
51
52    let mut result = BiMap::new();
53
54    while let Some(arg) = stream.try_next().await? {
55        if let Some((key, _)) = names.remove_by_right(arg.id_borrowed()) {
56            result.insert(key, arg);
57        }
58    }
59
60    if !names.is_empty() {
61        return Err(Error::MissingNamedArgs(names.into_iter().collect()));
62    }
63
64    Ok(result)
65}
66
67async fn request<C: GraphQlClient>(
68    client: &C,
69    vars: Variables<'_>,
70) -> Res<super::stream::Page<impl Iterator<Item = Res<ObjectArg, C>> + 'static + use<C>>, C> {
71    let objects = client
72        .query::<Query, _>(vars)
73        .await
74        .map_err(Error::Client)?
75        .try_into_data()?
76        .ok_or(Error::NoData)?
77        .objects;
78
79    Ok(super::stream::Page {
80        info: objects.page_info.into(),
81        data: objects
82            .nodes
83            .into_iter()
84            .filter_map(Object::object_arg)
85            .map(Ok),
86    })
87}
88
89#[cfg(test)]
90#[allow(clippy::unwrap_used)]
91#[test]
92fn gql_output() {
93    use cynic::QueryBuilder as _;
94    let vars = Variables {
95        filter: None,
96        first: None,
97        after: None,
98    };
99    let operation = Query::build(vars);
100    insta::assert_snapshot!(operation.query, @r###"
101    query Query($filter: ObjectFilter, $after: String, $first: Int) {
102      objects(filter: $filter, first: $first, after: $after) {
103        nodes {
104          address
105          version
106          digest
107          owner {
108            __typename
109            ... on Immutable {
110              _
111            }
112            ... on Shared {
113              __typename
114              initialSharedVersion
115            }
116            ... on Parent {
117              __typename
118            }
119            ... on AddressOwner {
120              __typename
121            }
122          }
123        }
124        pageInfo {
125          hasNextPage
126          endCursor
127        }
128      }
129    }
130    "###);
131}
132
133// =============================================================================
134//  Macro helper
135// =============================================================================
136
137/// Query [ObjectArg]s and assign them to variables. Optionally, set the page size.
138///
139/// This will panic if the user specifies two different identifiers mapping to the same [Address].
140///
141/// The `mut` keyword here means we're requesting a mutable [ObjectArg::SharedObject].
142///
143/// # Example
144/// ```no_run
145/// # use color_eyre::Result;
146/// # use sui_gql_client::object_args;
147/// # use sui_gql_client::reqwest::ReqwestClient;
148/// # const SUI_GRAPHQL_SERVER_URL: &str = "https://sui-testnet.mystenlabs.com/graphql";
149/// # tokio_test::block_on(async {
150/// let client = ReqwestClient::new(
151///     reqwest::Client::default(),
152///     SUI_GRAPHQL_SERVER_URL.to_owned(),
153/// );
154/// object_args!({
155///     mut clearing_house: "0xe4a1c0bfc53a7c2941a433a9a681c942327278b402878e0c45280eecd098c3d1".parse()?,
156///     registry: "0x400e84251a6ce2192f69c1aa775d68bab7690e059578317bf9e844d40e07e04d".parse()?,
157/// } with { &client } paged by 10);
158/// # println!("{clearing_house:?}");
159/// # println!("{registry:?}");
160/// # Ok::<_, color_eyre::eyre::Error>(())
161/// # });
162/// ```
163#[macro_export]
164macro_rules! object_args {
165    // Optimization: 1 object arg only
166    (
167        { $name:ident: $object_id:expr_2021 $(,)? }
168        with { $client:expr_2021 }
169    ) => {
170        let $name = $crate::queries::GraphQlClientExt::object_arg($client, $object_id)
171            .await?;
172    };
173
174    (
175        { mut $name:ident: $object_id:expr_2021 $(,)? }
176        with { $client:expr_2021 }
177    ) => {
178        let $name = {
179            let mut oarg = $crate::queries::GraphQlClientExt::object_arg($client, $object_id)
180                .await?;
181            oarg.set_mutable(true)?;
182            oarg
183        };
184    };
185
186    (
187        {$($tt:tt)*}
188        with { $client:expr_2021 } $(paged by $page_size:expr_2021)?
189    ) => {
190        $crate::object_args!(@Names $($tt)*);
191        {
192            use $crate::queries::GraphQlClientExt as _;
193            let mut names = $crate::queries::BiMap::new();
194            $crate::object_args! { @Map names $($tt)* }
195            let mut oargs = $crate::queries::GraphQlClientExt::object_args(
196                $client,
197                names,
198                $crate::object_args!(@PageSize $($page_size)?)
199            ).await?;
200            $crate::object_args! { @Result oargs $($tt)* }
201        }
202    };
203
204    (@Names mut $name:ident: $_:expr_2021 $(, $($rest:tt)*)?) => {
205        $crate::object_args!(@Names $name: $_ $(, $($rest)*)?)
206    };
207
208    (@Names $name:ident: $_:expr_2021 $(, $($rest:tt)*)?) => {
209        let $name;
210        $crate::object_args!{ @Names $($($rest)*)? }
211    };
212
213    (@Names ) => {};
214
215    (@Map $map:ident mut $name:ident: $object_id:expr_2021 $(, $($rest:tt)*)?) => {
216        $crate::object_args! { @Map $map $name: $object_id $(, $($rest)*)? }
217    };
218
219    (@Map $map:ident $name:ident: $object_id:expr_2021 $(, $($rest:tt)*)?) => {
220        $map.insert(stringify!($name).to_owned(), $object_id);
221        $crate::object_args!{ @Map $map $($($rest)*)? }
222    };
223
224    (@Map $map:ident) => {};
225
226    (@Result $oargs:ident mut $name:ident: $_:expr_2021 $(, $($rest:tt)*)?) => {
227        let mut arg = $oargs
228            .remove_by_left(stringify!($name))
229            .expect("request_named_object_args should fail if any names are missing")
230            .1;
231        arg.set_mutable(true)?;
232        $name = arg;
233        $crate::object_args! {@Result $oargs $($($rest)*)?}
234    };
235
236    (@Result $oargs:ident $name:ident: $_:expr_2021 $(, $($rest:tt)*)?) => {
237        $name = $oargs
238            .remove_by_left(stringify!($name))
239            .expect("request_named_object_args should fail if any names are missing")
240            .1;
241        $crate::object_args! { @Result $oargs $($($rest)*)? }
242    };
243
244    (@Result $oargs:ident ) => {
245    };
246
247    (@PageSize $page_size:expr_2021) => { Some($page_size) };
248    (@PageSize) => { None };
249}
250
251// =============================================================================
252//  Inner query fragments
253// =============================================================================
254
255#[derive(cynic::QueryFragment, Debug)]
256struct Object {
257    #[cynic(rename = "address")]
258    object_id: Address,
259    version: Version,
260    digest: Option<scalars::Digest>,
261    owner: Option<ObjectOwner>,
262}
263
264impl Object {
265    /// Return the [ObjectArg] or none if missing data.
266    ///
267    /// For shared objects, `mutable` is set as `false`. Use [ObjectArg::set_mutable] if needed.
268    fn object_arg(self) -> Option<ObjectArg> {
269        let Self {
270            object_id,
271            version,
272            digest,
273            owner: Some(owner),
274        } = self
275        else {
276            return None;
277        };
278
279        build_object_arg_default(object_id, version, owner, digest)
280    }
281}
282
283pub(crate) fn build_object_arg_default(
284    id: Address,
285    version: Version,
286    owner: ObjectOwner,
287    digest: Option<scalars::Digest>,
288) -> Option<ObjectArg> {
289    Some(match owner {
290        ObjectOwner::Immutable(_) | ObjectOwner::Parent(_) | ObjectOwner::AddressOwner(_) => {
291            ObjectArg::ImmOrOwnedObject((id, version, digest?.0))
292        }
293        ObjectOwner::Shared(Shared {
294            initial_shared_version,
295            ..
296        }) => ObjectArg::SharedObject {
297            id,
298            initial_shared_version,
299            mutable: false,
300        },
301        ObjectOwner::Unknown => return None,
302    })
303}
304
305pub(super) fn build_oarg_set_mut(
306    object_id: Address,
307    version: Version,
308    owner: Option<ObjectOwner>,
309    digest: Option<scalars::Digest>,
310    mutable_: bool,
311) -> Option<ObjectArg> {
312    let mut oarg = build_object_arg_default(object_id, version, owner?, digest)?;
313    if let ObjectArg::SharedObject {
314        ref mut mutable, ..
315    } = oarg
316    {
317        *mutable = mutable_;
318    }
319    Some(oarg)
320}
321
322#[derive(cynic::InlineFragments, Debug)]
323pub(super) enum ObjectOwner {
324    #[allow(dead_code)]
325    Immutable(Immutable),
326
327    Shared(Shared),
328
329    #[allow(dead_code)]
330    Parent(Parent),
331
332    #[allow(dead_code)]
333    AddressOwner(AddressOwner),
334
335    #[cynic(fallback)]
336    Unknown,
337}
338
339#[derive(cynic::QueryFragment, Debug)]
340pub(super) struct Immutable {
341    #[cynic(rename = "_")]
342    __underscore: Option<bool>,
343}
344
345#[derive(cynic::QueryFragment, Debug)]
346pub(super) struct Shared {
347    __typename: String,
348    initial_shared_version: Version,
349}
350
351#[derive(cynic::QueryFragment, Debug)]
352pub(super) struct Parent {
353    __typename: String,
354}
355
356#[derive(cynic::QueryFragment, Debug)]
357pub(super) struct AddressOwner {
358    __typename: String,
359}