Skip to main content

iota_sdk_graphql_client/api/
move_view_call.rs

1// Copyright (c) 2026 IOTA Stiftung
2// SPDX-License-Identifier: Apache-2.0
3
4use cynic::QueryBuilder;
5use iota_transaction_builder::types::MoveTypes;
6use iota_types::{Address, ObjectId, ObjectReference, TypeTag};
7
8use crate::{
9    GraphQLClient,
10    api::define_query,
11    error::GraphQLResult,
12    query_types::{MoveViewCallArgs, MoveViewCallQueryFragment, MoveViewResult},
13};
14
15define_query! {
16    /// Query for [`GraphQLClient::move_view_call_json`]. Await it to send the
17    /// request.
18    pub struct MoveViewCallJsonQuery {
19        client: GraphQLClient,
20        function_name: String,
21        type_arguments: Option<Vec<String>>,
22        arguments: Option<Vec<serde_json::Value>>,
23    }
24    output: GraphQLResult<MoveViewResult>;
25}
26
27impl MoveViewCallJsonQuery {
28    /// Set the type arguments of the Move function.
29    pub fn type_arguments(mut self, type_arguments: Vec<String>) -> Self {
30        self.type_arguments = Some(type_arguments);
31        self
32    }
33
34    /// Set the arguments passed into the Move function, in JSON format.
35    pub fn arguments(mut self, arguments: Vec<serde_json::Value>) -> Self {
36        self.arguments = Some(arguments);
37        self
38    }
39
40    async fn send(self) -> GraphQLResult<MoveViewResult> {
41        let operation = MoveViewCallQueryFragment::build(MoveViewCallArgs {
42            function_name: self.function_name,
43            type_arguments: self.type_arguments,
44            arguments: self.arguments,
45        });
46        let response = self.client.run_query(&operation).await?;
47
48        Ok(response.move_view_call)
49    }
50}
51
52define_query! {
53    /// Query for [`GraphQLClient::move_view_call`]. Await it to send the
54    /// request.
55    pub struct MoveViewCallQuery {
56        client: GraphQLClient,
57        function_name: String,
58        type_arguments: Option<Vec<TypeTag>>,
59        arguments: Option<Vec<serde_json::Value>>,
60    }
61    output: GraphQLResult<MoveViewResult>;
62}
63
64impl MoveViewCallQuery {
65    /// Set the type arguments of the Move function.
66    pub fn type_arguments(mut self, type_arguments: Vec<TypeTag>) -> Self {
67        self.type_arguments = Some(type_arguments);
68        self
69    }
70
71    /// Set the type arguments of the Move function from Rust types, e.g.
72    /// `generics::<(u64, String)>()`.
73    pub fn generics<G: MoveTypes>(mut self) -> Self {
74        self.type_arguments = Some(G::type_tags());
75        self
76    }
77
78    /// Set the typed arguments passed into the Move function, replacing the
79    /// ones set so far. A single argument is wrapped in a list or tuple.
80    pub fn arguments(mut self, arguments: impl MoveViewArgList) -> Self {
81        self.arguments = Some(arguments.to_json_vec());
82        self
83    }
84
85    /// Append a single typed argument passed into the Move function.
86    ///
87    /// A collection is appended as one argument, i.e. a Move vector; use
88    /// [`arguments`](Self::arguments) to pass a collection as the whole
89    /// argument list.
90    pub fn argument(mut self, argument: impl MoveViewArg) -> Self {
91        self.arguments
92            .get_or_insert_default()
93            .push(argument.to_json());
94        self
95    }
96
97    async fn send(self) -> GraphQLResult<MoveViewResult> {
98        let type_arguments = self
99            .type_arguments
100            .map(|tags| tags.into_iter().map(|t| t.to_string()).collect());
101        MoveViewCallJsonQuery {
102            client: self.client,
103            function_name: self.function_name,
104            type_arguments,
105            arguments: self.arguments,
106        }
107        .await
108    }
109}
110
111/// The fully qualified name of a Move function, as
112/// `<package_id>::<module_name>::<function_name>`.
113fn function_name(package: ObjectId, module: &str, function: &str) -> String {
114    format!("{package}::{module}::{function}")
115}
116
117impl GraphQLClient {
118    /// Execute a Move View Function with raw JSON arguments.
119    ///
120    /// This is an alternative to [`GraphQLClient::move_view_call`] that accepts
121    /// raw JSON values instead of typed arguments.
122    ///
123    /// A View Function is a function in a Move module with a return type that
124    /// does not alter the state of the ledger. When using this interface,
125    /// no transactions are submitted to the network for inclusion into the
126    /// ledger.
127    ///
128    /// The function is `function` in the module `module` of the package
129    /// `package`. Set its type arguments with
130    /// [`type_arguments`](MoveViewCallJsonQuery::type_arguments) and its JSON
131    /// arguments with [`arguments`](MoveViewCallJsonQuery::arguments).
132    ///
133    /// Resolves to a `MoveViewResult` containing either execution results
134    /// (return values) or an error.
135    pub fn move_view_call_json(
136        &self,
137        package: impl Into<ObjectId>,
138        module: impl Into<String>,
139        function: impl Into<String>,
140    ) -> MoveViewCallJsonQuery {
141        MoveViewCallJsonQuery {
142            client: self.clone(),
143            function_name: function_name(package.into(), &module.into(), &function.into()),
144            type_arguments: None,
145            arguments: None,
146        }
147    }
148
149    /// Execute a Move View Function.
150    ///
151    /// A View Function is a function in a Move module with a return type that
152    /// does not alter the state of the ledger. When using this interface,
153    /// no transactions are submitted to the network for inclusion into the
154    /// ledger.
155    ///
156    /// This method allows calling nearly any Move function with a return type
157    /// and any arguments. The function's result values are provided and
158    /// decoded using the appropriate Move type, then formatted in JSON.
159    ///
160    /// The use of this interface does not require signature checks (even for
161    /// functions that take Owned Objects as input) or gas coins, as it does
162    /// not alter ledger state. Spam attacks are dealt with at the RPC level
163    /// rather than execution level.
164    ///
165    /// The function is `function` in the module `module` of the package
166    /// `package`. Set its typed arguments with
167    /// [`arguments`](MoveViewCallQuery::arguments) and its type arguments
168    /// with [`type_arguments`](MoveViewCallQuery::type_arguments).
169    ///
170    /// # Example
171    /// ```rust,ignore
172    /// // The `view_demo` package published on testnet, and the shared
173    /// // `view_demo::shop::Shop` created when it was published.
174    /// let package = ObjectId::from_str(
175    ///     "0x533074f8e22e8ce1330d7e9d67c18966abb5a3d58dc2e2deea50e50bea4e87f4",
176    /// )?;
177    /// let shop = ObjectId::from_str(
178    ///     "0x9d5ce0da7531d56ffecced5efb7e19ccad0e191071041267cc8134a3e5a6cd20",
179    /// )?;
180    ///
181    /// // Single argument: wrap in a list or tuple
182    /// let result = client
183    ///     .move_view_call(package, "shop", "total_revenue")
184    ///     .arguments((shop,))
185    ///     .await?;
186    ///
187    /// // Or append it
188    /// let result = client
189    ///     .move_view_call(package, "shop", "total_revenue")
190    ///     .argument(shop)
191    ///     .await?;
192    /// ```
193    ///
194    /// Resolves to a `MoveViewResult` containing either execution results
195    /// (return values) or an error.
196    pub fn move_view_call(
197        &self,
198        package: impl Into<ObjectId>,
199        module: impl Into<String>,
200        function: impl Into<String>,
201    ) -> MoveViewCallQuery {
202        MoveViewCallQuery {
203            client: self.clone(),
204            function_name: function_name(package.into(), &module.into(), &function.into()),
205            type_arguments: None,
206            arguments: None,
207        }
208    }
209}
210
211/// A trait which defines a single argument for a Move View Function call.
212#[diagnostic::on_unimplemented(message = "Provided value is not a valid Move view argument.")]
213pub trait MoveViewArg {
214    /// Convert this argument to a JSON value for the GraphQL API.
215    fn to_json(self) -> serde_json::Value;
216}
217
218// Macro for types that convert to JSON Number
219macro_rules! impl_move_view_arg_number {
220    ($($ty:ty),* $(,)?) => {
221        $(
222            impl MoveViewArg for $ty {
223                fn to_json(self) -> serde_json::Value {
224                    serde_json::Value::Number(self.into())
225                }
226            }
227
228            impl MoveViewArg for &$ty {
229                fn to_json(self) -> serde_json::Value {
230                    (*self).to_json()
231                }
232            }
233        )*
234    };
235}
236
237// Macro for types that convert to JSON String via to_string()
238macro_rules! impl_move_view_arg_string {
239    ($($ty:ty),* $(,)?) => {
240        $(
241            impl MoveViewArg for $ty {
242                fn to_json(self) -> serde_json::Value {
243                    serde_json::Value::String(self.to_string())
244                }
245            }
246
247            impl MoveViewArg for &$ty {
248                fn to_json(self) -> serde_json::Value {
249                    (*self).to_json()
250                }
251            }
252        )*
253    };
254}
255
256impl MoveViewArg for bool {
257    fn to_json(self) -> serde_json::Value {
258        serde_json::Value::Bool(self)
259    }
260}
261
262impl MoveViewArg for &bool {
263    fn to_json(self) -> serde_json::Value {
264        (*self).to_json()
265    }
266}
267
268impl_move_view_arg_number!(u8, u16, u32);
269
270// u64 and u128 must be represented as strings in JSON to avoid precision loss
271impl_move_view_arg_string!(u64, u128, ObjectId, Address);
272
273impl MoveViewArg for &str {
274    fn to_json(self) -> serde_json::Value {
275        serde_json::Value::String((*self).to_owned())
276    }
277}
278
279impl MoveViewArg for String {
280    fn to_json(self) -> serde_json::Value {
281        serde_json::Value::String(self)
282    }
283}
284
285impl MoveViewArg for &String {
286    fn to_json(self) -> serde_json::Value {
287        self.as_str().to_json()
288    }
289}
290
291impl MoveViewArg for ObjectReference {
292    fn to_json(self) -> serde_json::Value {
293        serde_json::Value::String(self.object_id.to_string())
294    }
295}
296
297impl MoveViewArg for &ObjectReference {
298    fn to_json(self) -> serde_json::Value {
299        serde_json::Value::String(self.object_id.to_string())
300    }
301}
302
303// Collection implementations
304impl<T: MoveViewArg> MoveViewArg for Vec<T> {
305    fn to_json(self) -> serde_json::Value {
306        serde_json::Value::Array(self.into_iter().map(|v| v.to_json()).collect())
307    }
308}
309
310impl<T> MoveViewArg for &[T]
311where
312    for<'a> &'a T: MoveViewArg,
313{
314    fn to_json(self) -> serde_json::Value {
315        serde_json::Value::Array(self.iter().map(|v| v.to_json()).collect())
316    }
317}
318
319impl<const N: usize, T: MoveViewArg> MoveViewArg for [T; N] {
320    fn to_json(self) -> serde_json::Value {
321        serde_json::Value::Array(self.into_iter().map(|v| v.to_json()).collect())
322    }
323}
324
325impl<T: MoveViewArg> MoveViewArg for Option<T> {
326    fn to_json(self) -> serde_json::Value {
327        match self {
328            Some(v) => v.to_json(),
329            None => serde_json::Value::Null,
330        }
331    }
332}
333
334// Smart pointer implementations
335impl<T> MoveViewArg for std::sync::Arc<T>
336where
337    for<'a> &'a T: MoveViewArg,
338{
339    fn to_json(self) -> serde_json::Value {
340        self.as_ref().to_json()
341    }
342}
343
344impl<T> MoveViewArg for Box<T>
345where
346    for<'a> &'a T: MoveViewArg,
347{
348    fn to_json(self) -> serde_json::Value {
349        self.as_ref().to_json()
350    }
351}
352
353// Allow passing raw JSON values
354impl MoveViewArg for serde_json::Value {
355    fn to_json(self) -> serde_json::Value {
356        self
357    }
358}
359
360/// A trait which defines a list of arguments for a Move View Function call.
361#[diagnostic::on_unimplemented(
362    message = "Provided value is not a valid list of Move view arguments.",
363    note = "Expected a tuple, vector, array, or slice of types that implement `MoveViewArg`."
364)]
365pub trait MoveViewArgList: sealed::Sealed {
366    /// Convert the arguments to a vector of JSON values.
367    fn to_json_vec(self) -> Vec<serde_json::Value>;
368}
369
370// Single element tuple implementation
371impl<T: MoveViewArg> MoveViewArgList for (T,) {
372    fn to_json_vec(self) -> Vec<serde_json::Value> {
373        vec![self.0.to_json()]
374    }
375}
376
377impl<T: MoveViewArg> MoveViewArgList for Vec<T> {
378    fn to_json_vec(self) -> Vec<serde_json::Value> {
379        self.into_iter().map(|v| v.to_json()).collect()
380    }
381}
382
383impl<const N: usize, T: MoveViewArg> MoveViewArgList for [T; N] {
384    fn to_json_vec(self) -> Vec<serde_json::Value> {
385        self.into_iter().map(|v| v.to_json()).collect()
386    }
387}
388
389impl<T> MoveViewArgList for &[T]
390where
391    for<'a> &'a T: MoveViewArg,
392{
393    fn to_json_vec(self) -> Vec<serde_json::Value> {
394        self.iter().map(|v| v.to_json()).collect()
395    }
396}
397
398// Tuple implementations using a macro
399macro_rules! impl_move_view_args_tuple {
400    ($(($n:tt, $T:ident)),*) => {
401        impl<$($T),+> sealed::Sealed for ($($T),+) {}
402
403        impl<$($T),+> MoveViewArgList for ($($T),+)
404        where $($T: MoveViewArg),+
405        {
406            fn to_json_vec(self) -> Vec<serde_json::Value> {
407                vec![
408                    $(
409                        self.$n.to_json()
410                    ),+
411                ]
412            }
413        }
414    };
415}
416
417variadics_please::all_tuples_enumerated!(impl_move_view_args_tuple, 2, 15, T);
418
419mod sealed {
420    pub trait Sealed {}
421
422    impl<T> Sealed for (T,) {}
423    impl<T> Sealed for Vec<T> {}
424    impl<const N: usize, T> Sealed for [T; N] {}
425    impl<T> Sealed for &[T] {}
426}
427
428#[cfg(all(test, not(target_arch = "wasm32")))]
429mod tests {
430    use iota_types::{Address, ObjectId, TypeTag};
431
432    use crate::test_utils::sent_variables;
433
434    #[tokio::test]
435    async fn move_view_calls_send_the_function_type_arguments_and_arguments() {
436        let vars = sent_variables("MoveViewCallQueryFragment", |client| async move {
437            let _ = client
438                .move_view_call(Address::FRAMEWORK, "coin", "value")
439                .arguments((21u64,))
440                .type_arguments(vec![TypeTag::U64])
441                .await;
442        })
443        .await;
444        assert_eq!(
445            vars["functionName"],
446            format!("{}::coin::value", ObjectId::from(Address::FRAMEWORK))
447        );
448        assert_eq!(vars["typeArguments"], serde_json::json!(["u64"]));
449        assert_eq!(vars["arguments"], serde_json::json!(["21"]));
450
451        let vars = sent_variables("MoveViewCallQueryFragment", |client| async move {
452            let _ = client
453                .move_view_call_json(Address::FRAMEWORK, "coin", "value")
454                .type_arguments(vec!["u64".to_owned()])
455                .arguments(vec![serde_json::json!("21")])
456                .await;
457        })
458        .await;
459        assert_eq!(
460            vars["functionName"],
461            format!("{}::coin::value", ObjectId::from(Address::FRAMEWORK))
462        );
463        assert_eq!(vars["typeArguments"], serde_json::json!(["u64"]));
464        assert_eq!(vars["arguments"], serde_json::json!(["21"]));
465
466        let vars = sent_variables("MoveViewCallQueryFragment", |client| async move {
467            let _ = client
468                .move_view_call(Address::FRAMEWORK, "coin", "value")
469                .await;
470        })
471        .await;
472        assert!(vars["typeArguments"].is_null());
473        assert!(vars["arguments"].is_null());
474
475        let vars = sent_variables("MoveViewCallQueryFragment", |client| async move {
476            let _ = client
477                .move_view_call_json(Address::FRAMEWORK, "coin", "value")
478                .await;
479        })
480        .await;
481        assert!(vars["typeArguments"].is_null());
482        assert!(vars["arguments"].is_null());
483    }
484
485    #[tokio::test]
486    async fn move_view_calls_append_arguments_and_take_type_arguments_from_generics() {
487        let vars = sent_variables("MoveViewCallQueryFragment", |client| async move {
488            let _ = client
489                .move_view_call(Address::FRAMEWORK, "coin", "value")
490                .arguments((1u8, "a"))
491                .argument(u64::MAX)
492                .argument(vec![2u8, 3])
493                .generics::<(u64, String)>()
494                .await;
495        })
496        .await;
497        assert_eq!(
498            vars["arguments"],
499            serde_json::json!([1, "a", u64::MAX.to_string(), [2, 3]])
500        );
501        assert_eq!(
502            vars["typeArguments"],
503            serde_json::json!(["u64", "vector<u8>"])
504        );
505
506        let vars = sent_variables("MoveViewCallQueryFragment", |client| async move {
507            let _ = client
508                .move_view_call(Address::FRAMEWORK, "coin", "value")
509                .argument(21u64)
510                .await;
511        })
512        .await;
513        assert_eq!(vars["arguments"], serde_json::json!(["21"]));
514    }
515}