iota_graphql_rpc/types/
move_module.rs

1// Copyright (c) Mysten Labs, Inc.
2// Modifications Copyright (c) 2024 IOTA Stiftung
3// SPDX-License-Identifier: Apache-2.0
4
5use async_graphql::{
6    connection::{Connection, CursorType, Edge},
7    *,
8};
9use iota_package_resolver::Module as ParsedMoveModule;
10use move_disassembler::disassembler::Disassembler;
11use move_ir_types::location::Loc;
12
13use crate::{
14    consistency::{ConsistentIndexCursor, ConsistentNamedCursor},
15    error::Error,
16    types::{
17        base64::Base64,
18        cursor::{JsonCursor, Page},
19        datatype::MoveDatatype,
20        iota_address::IotaAddress,
21        move_enum::MoveEnum,
22        move_function::MoveFunction,
23        move_package::MovePackage,
24        move_struct::MoveStruct,
25    },
26};
27
28#[derive(Clone)]
29pub(crate) struct MoveModule {
30    pub storage_id: IotaAddress,
31    pub native: Vec<u8>,
32    pub parsed: ParsedMoveModule,
33    /// The checkpoint sequence number this was viewed at.
34    pub checkpoint_viewed_at: u64,
35}
36
37pub(crate) type CFriend = JsonCursor<ConsistentIndexCursor>;
38pub(crate) type CStruct = JsonCursor<ConsistentNamedCursor>;
39pub(crate) type CFunction = JsonCursor<ConsistentNamedCursor>;
40
41/// Represents a module in Move, a library that defines struct types
42/// and functions that operate on these types.
43#[Object]
44impl MoveModule {
45    /// The package that this Move module was defined in
46    async fn package(&self, ctx: &Context<'_>) -> Result<MovePackage> {
47        MovePackage::query(
48            ctx,
49            self.storage_id,
50            MovePackage::by_id_at(self.checkpoint_viewed_at),
51        )
52        .await
53        .extend()?
54        .ok_or_else(|| {
55            Error::Internal(format!(
56                "Cannot load package for module {}::{}",
57                self.storage_id,
58                self.parsed.name(),
59            ))
60        })
61        .extend()
62    }
63
64    /// The module's (unqualified) name.
65    async fn name(&self) -> &str {
66        self.parsed.name()
67    }
68
69    /// Format version of this module's bytecode.
70    async fn file_format_version(&self) -> u32 {
71        self.parsed.bytecode().version
72    }
73
74    /// Modules that this module considers friends (these modules can access
75    /// `public(friend)` functions from this module).
76    async fn friends(
77        &self,
78        ctx: &Context<'_>,
79        first: Option<u64>,
80        after: Option<CFriend>,
81        last: Option<u64>,
82        before: Option<CFriend>,
83    ) -> Result<Connection<String, MoveModule>> {
84        let page = Page::from_params(ctx.data_unchecked(), first, after, last, before)?;
85        let bytecode = self.parsed.bytecode();
86
87        let mut connection = Connection::new(false, false);
88        let Some((prev, next, checkpoint_viewed_at, cs)) = page
89            .paginate_consistent_indices(bytecode.friend_decls.len(), self.checkpoint_viewed_at)?
90        else {
91            return Ok(connection);
92        };
93
94        connection.has_previous_page = prev;
95        connection.has_next_page = next;
96
97        let runtime_id = *bytecode.self_id().address();
98        let Some(package) = MovePackage::query(
99            ctx,
100            self.storage_id,
101            MovePackage::by_id_at(checkpoint_viewed_at),
102        )
103        .await
104        .extend()?
105        else {
106            return Err(Error::Internal(format!(
107                "Failed to load package for module: {}",
108                self.storage_id,
109            ))
110            .extend());
111        };
112
113        // Select `friend_decls[lo..hi]` using iterators to enumerate before taking a
114        // sub-sequence from it, to get pairs `(i, friend_decls[i])`.
115        for c in cs {
116            let decl = &bytecode.friend_decls[c.ix];
117            let friend_pkg = bytecode.address_identifier_at(decl.address);
118            let friend_mod = bytecode.identifier_at(decl.name);
119
120            if friend_pkg != &runtime_id {
121                return Err(Error::Internal(format!(
122                    "Friend module of {} from a different package: {}::{}",
123                    runtime_id.to_canonical_display(/* with_prefix */ true),
124                    friend_pkg.to_canonical_display(/* with_prefix */ true),
125                    friend_mod,
126                ))
127                .extend());
128            }
129
130            let Some(friend) = package.module_impl(friend_mod.as_str()).extend()? else {
131                return Err(Error::Internal(format!(
132                    "Failed to load friend module of {}::{}: {}",
133                    self.storage_id,
134                    self.parsed.name(),
135                    friend_mod,
136                ))
137                .extend());
138            };
139
140            connection.edges.push(Edge::new(c.encode_cursor(), friend));
141        }
142
143        Ok(connection)
144    }
145
146    /// Look-up the definition of a struct defined in this module, by its name.
147    #[graphql(name = "struct")]
148    async fn struct_(&self, name: String) -> Result<Option<MoveStruct>> {
149        self.struct_impl(name).extend()
150    }
151
152    /// Iterate through the structs defined in this module.
153    async fn structs(
154        &self,
155        ctx: &Context<'_>,
156        first: Option<u64>,
157        after: Option<CStruct>,
158        last: Option<u64>,
159        before: Option<CStruct>,
160    ) -> Result<Option<Connection<String, MoveStruct>>> {
161        let page = Page::from_params(ctx.data_unchecked(), first, after, last, before)?;
162        let after = page.after().map(|a| a.name.as_str());
163        let before = page.before().map(|b| b.name.as_str());
164        let struct_range = self.parsed.structs(after, before);
165
166        let cursor_viewed_at = page.validate_cursor_consistency()?;
167        let checkpoint_viewed_at = cursor_viewed_at.unwrap_or(self.checkpoint_viewed_at);
168
169        let mut connection = Connection::new(false, false);
170        let struct_names = if page.is_from_front() {
171            struct_range.take(page.limit()).collect()
172        } else {
173            let mut names: Vec<_> = struct_range.rev().take(page.limit()).collect();
174            names.reverse();
175            names
176        };
177
178        connection.has_previous_page = struct_names
179            .first()
180            .is_some_and(|fst| self.parsed.structs(None, Some(fst)).next().is_some());
181
182        connection.has_next_page = struct_names
183            .last()
184            .is_some_and(|lst| self.parsed.structs(Some(lst), None).next().is_some());
185
186        for name in struct_names {
187            let Some(struct_) = self.struct_impl(name.to_string()).extend()? else {
188                return Err(Error::Internal(format!(
189                    "Cannot deserialize struct {name} in module {}::{}",
190                    self.storage_id,
191                    self.parsed.name(),
192                )))
193                .extend();
194            };
195
196            let cursor = JsonCursor::new(ConsistentNamedCursor {
197                name: name.to_string(),
198                c: checkpoint_viewed_at,
199            })
200            .encode_cursor();
201            connection.edges.push(Edge::new(cursor, struct_));
202        }
203
204        if connection.edges.is_empty() {
205            Ok(None)
206        } else {
207            Ok(Some(connection))
208        }
209    }
210
211    /// Look-up the definition of a enum defined in this module, by its name.
212    #[graphql(name = "enum")]
213    async fn enum_(&self, name: String) -> Result<Option<MoveEnum>> {
214        self.enum_impl(name).extend()
215    }
216
217    /// Iterate through the enums defined in this module.
218    async fn enums(
219        &self,
220        ctx: &Context<'_>,
221        first: Option<u64>,
222        after: Option<CStruct>,
223        last: Option<u64>,
224        before: Option<CStruct>,
225    ) -> Result<Option<Connection<String, MoveEnum>>> {
226        let page = Page::from_params(ctx.data_unchecked(), first, after, last, before)?;
227        let after = page.after().map(|a| a.name.as_str());
228        let before = page.before().map(|b| b.name.as_str());
229        let enum_range = self.parsed.enums(after, before);
230
231        let cursor_viewed_at = page.validate_cursor_consistency()?;
232        let checkpoint_viewed_at = cursor_viewed_at.unwrap_or(self.checkpoint_viewed_at);
233
234        let mut connection = Connection::new(false, false);
235        let enum_names = if page.is_from_front() {
236            enum_range.take(page.limit()).collect()
237        } else {
238            let mut names: Vec<_> = enum_range.rev().take(page.limit()).collect();
239            names.reverse();
240            names
241        };
242
243        connection.has_previous_page = enum_names
244            .first()
245            .is_some_and(|fst| self.parsed.enums(None, Some(fst)).next().is_some());
246
247        connection.has_next_page = enum_names
248            .last()
249            .is_some_and(|lst| self.parsed.enums(Some(lst), None).next().is_some());
250
251        for name in enum_names {
252            let Some(enum_) = self.enum_impl(name.to_string()).extend()? else {
253                return Err(Error::Internal(format!(
254                    "Cannot deserialize enum {name} in module {}::{}",
255                    self.storage_id,
256                    self.parsed.name(),
257                )))
258                .extend();
259            };
260
261            let cursor = JsonCursor::new(ConsistentNamedCursor {
262                name: name.to_string(),
263                c: checkpoint_viewed_at,
264            })
265            .encode_cursor();
266            connection.edges.push(Edge::new(cursor, enum_));
267        }
268
269        if connection.edges.is_empty() {
270            Ok(None)
271        } else {
272            Ok(Some(connection))
273        }
274    }
275
276    /// Look-up the definition of a datatype (struct or enum) defined in this
277    /// module, by its name.
278    async fn datatype(&self, name: String) -> Result<Option<MoveDatatype>> {
279        match self.struct_impl(name.clone()) {
280            Ok(Some(s)) => Ok(Some(MoveDatatype::Struct(s))),
281            Ok(None) => self
282                .enum_impl(name)
283                .map(|x| x.map(MoveDatatype::Enum))
284                .extend(),
285            Err(e) => Err(e.into()),
286        }
287    }
288
289    /// Iterate through the datatypes (enmums and structs) defined in this
290    /// module.
291    async fn datatypes(
292        &self,
293        ctx: &Context<'_>,
294        first: Option<u64>,
295        after: Option<CStruct>,
296        last: Option<u64>,
297        before: Option<CStruct>,
298    ) -> Result<Option<Connection<String, MoveDatatype>>> {
299        let page = Page::from_params(ctx.data_unchecked(), first, after, last, before)?;
300        let after = page.after().map(|a| a.name.as_str());
301        let before = page.before().map(|b| b.name.as_str());
302        let datatype_range = self.parsed.datatypes(after, before);
303
304        let cursor_viewed_at = page.validate_cursor_consistency()?;
305        let checkpoint_viewed_at = cursor_viewed_at.unwrap_or(self.checkpoint_viewed_at);
306
307        let mut connection = Connection::new(false, false);
308        let datatype_names = if page.is_from_front() {
309            datatype_range.take(page.limit()).collect()
310        } else {
311            let mut names: Vec<_> = datatype_range.rev().take(page.limit()).collect();
312            names.reverse();
313            names
314        };
315
316        connection.has_previous_page = datatype_names
317            .first()
318            .is_some_and(|fst| self.parsed.datatypes(None, Some(fst)).next().is_some());
319
320        connection.has_next_page = datatype_names
321            .last()
322            .is_some_and(|lst| self.parsed.datatypes(Some(lst), None).next().is_some());
323
324        for name in datatype_names {
325            let datatype = match self.struct_impl(name.to_string()) {
326                Ok(None) => self
327                    .enum_impl(name.to_string())
328                    .map(|x| x.map(MoveDatatype::Enum))
329                    .extend()?,
330                Ok(Some(s)) => Some(MoveDatatype::Struct(s)),
331                Err(e) => return Err(e.into()),
332            }
333            .ok_or_else(|| {
334                Error::Internal(format!(
335                    "Cannot deserialize datatype {name} in module {}::{}",
336                    self.storage_id,
337                    self.parsed.name(),
338                ))
339            })?;
340
341            let cursor = JsonCursor::new(ConsistentNamedCursor {
342                name: name.to_string(),
343                c: checkpoint_viewed_at,
344            })
345            .encode_cursor();
346            connection.edges.push(Edge::new(cursor, datatype));
347        }
348
349        if connection.edges.is_empty() {
350            Ok(None)
351        } else {
352            Ok(Some(connection))
353        }
354    }
355
356    /// Look-up the signature of a function defined in this module, by its name.
357    async fn function(&self, name: String) -> Result<Option<MoveFunction>> {
358        self.function_impl(name).extend()
359    }
360
361    /// Iterate through the signatures of functions defined in this module.
362    async fn functions(
363        &self,
364        ctx: &Context<'_>,
365        first: Option<u64>,
366        after: Option<CFunction>,
367        last: Option<u64>,
368        before: Option<CFunction>,
369    ) -> Result<Option<Connection<String, MoveFunction>>> {
370        let page = Page::from_params(ctx.data_unchecked(), first, after, last, before)?;
371        let after = page.after().map(|a| a.name.as_str());
372        let before = page.before().map(|b| b.name.as_str());
373        let function_range = self.parsed.functions(after, before);
374
375        let cursor_viewed_at = page.validate_cursor_consistency()?;
376        let checkpoint_viewed_at = cursor_viewed_at.unwrap_or(self.checkpoint_viewed_at);
377
378        let mut connection = Connection::new(false, false);
379        let function_names = if page.is_from_front() {
380            function_range.take(page.limit()).collect()
381        } else {
382            let mut names: Vec<_> = function_range.rev().take(page.limit()).collect();
383            names.reverse();
384            names
385        };
386
387        connection.has_previous_page = function_names
388            .first()
389            .is_some_and(|fst| self.parsed.functions(None, Some(fst)).next().is_some());
390
391        connection.has_next_page = function_names
392            .last()
393            .is_some_and(|lst| self.parsed.functions(Some(lst), None).next().is_some());
394
395        for name in function_names {
396            let Some(function) = self.function_impl(name.to_string()).extend()? else {
397                return Err(Error::Internal(format!(
398                    "Cannot deserialize function {name} in module {}::{}",
399                    self.storage_id,
400                    self.parsed.name(),
401                )))
402                .extend();
403            };
404
405            let cursor = JsonCursor::new(ConsistentNamedCursor {
406                name: name.to_string(),
407                c: checkpoint_viewed_at,
408            })
409            .encode_cursor();
410            connection.edges.push(Edge::new(cursor, function));
411        }
412
413        if connection.edges.is_empty() {
414            Ok(None)
415        } else {
416            Ok(Some(connection))
417        }
418    }
419
420    /// The Base64 encoded bcs serialization of the module.
421    async fn bytes(&self) -> Option<Base64> {
422        Some(Base64::from(self.native.clone()))
423    }
424
425    /// Textual representation of the module's bytecode.
426    async fn disassembly(&self) -> Result<Option<String>> {
427        Ok(Some(
428            Disassembler::from_module(self.parsed.bytecode(), Loc::invalid())
429                .map_err(|e| Error::Internal(format!("Error creating disassembler: {e}")))
430                .extend()?
431                .disassemble()
432                .map_err(|e| Error::Internal(format!("Error creating disassembly: {e}")))
433                .extend()?,
434        ))
435    }
436}
437
438impl MoveModule {
439    fn struct_impl(&self, name: String) -> Result<Option<MoveStruct>, Error> {
440        let def = match self.parsed.struct_def(&name) {
441            Ok(Some(def)) => def,
442            Ok(None) => return Ok(None),
443            Err(e) => return Err(Error::Internal(e.to_string())),
444        };
445
446        MoveStruct::new(
447            self.parsed.name().to_string(),
448            name,
449            def,
450            self.checkpoint_viewed_at,
451        )
452        .map(Option::Some)
453    }
454
455    fn enum_impl(&self, name: String) -> Result<Option<MoveEnum>, Error> {
456        let def = match self.parsed.enum_def(&name) {
457            Ok(Some(def)) => def,
458            Ok(None) => return Ok(None),
459            Err(e) => return Err(Error::Internal(e.to_string())),
460        };
461
462        MoveEnum::new(
463            self.parsed.name().to_string(),
464            name,
465            def,
466            self.checkpoint_viewed_at,
467        )
468        .map(Option::Some)
469    }
470
471    pub(crate) fn function_impl(&self, name: String) -> Result<Option<MoveFunction>, Error> {
472        let def = match self.parsed.function_def(&name) {
473            Ok(Some(def)) => def,
474            Ok(None) => return Ok(None),
475            Err(e) => return Err(Error::Internal(e.to_string())),
476        };
477
478        Ok(Some(MoveFunction::new(
479            self.storage_id,
480            self.parsed.name().to_string(),
481            name,
482            def,
483            self.checkpoint_viewed_at,
484        )))
485    }
486
487    pub(crate) async fn query(
488        ctx: &Context<'_>,
489        address: IotaAddress,
490        name: &str,
491        checkpoint_viewed_at: u64,
492    ) -> Result<Option<Self>, Error> {
493        let Some(package) =
494            MovePackage::query(ctx, address, MovePackage::by_id_at(checkpoint_viewed_at)).await?
495        else {
496            return Ok(None);
497        };
498
499        package.module_impl(name)
500    }
501}