Skip to main content

sui_rpc_api/grpc/v2/
render.rs

1// Copyright (c) Mysten Labs, Inc.
2// SPDX-License-Identifier: Apache-2.0
3
4use futures::FutureExt;
5use mysten_common::ZipDebugEqIteratorExt;
6use prost_types::Struct;
7use sui_rpc::{
8    field::FieldMaskTree,
9    merge::Merge,
10    proto::sui::rpc::v2::{
11        Bcs, Display, Event, ExecutedTransaction, Object, TransactionEffects, TransactionEvents,
12    },
13};
14use sui_types::full_checkpoint_content::ObjectSet;
15
16use crate::{RpcService, reader::DisplayStore};
17
18impl RpcService {
19    pub fn render_object_to_proto(
20        &self,
21        object: &sui_types::object::Object,
22        read_mask: &FieldMaskTree,
23        output_objects: &ObjectSet,
24    ) -> Object {
25        let mut message = Object::default();
26
27        if read_mask.contains(Object::JSON_FIELD) {
28            let move_object = object.data.try_as_move();
29            message.json = move_object.and_then(|m| {
30                self.render_json(&m.type_().clone().into(), m.contents(), output_objects)
31                    .map(Box::new)
32            });
33        }
34
35        if read_mask.contains(Object::DISPLAY_FIELD) {
36            message.display = self.render_object_display(object).map(Box::new);
37        }
38
39        message.merge(object, read_mask);
40
41        message
42    }
43
44    /// Render a Move value as JSON.
45    /// If output_objects is provided, packages from it will be checked first before the backing store.
46    pub fn render_json(
47        &self,
48        struct_tag: &move_core_types::language_storage::StructTag,
49        contents: &[u8],
50        output_objects: &ObjectSet,
51    ) -> Option<prost_types::Value> {
52        let mut budget = self.config.max_json_move_value_size();
53        self.render_json_with_budget(struct_tag, contents, output_objects, &mut budget)
54    }
55
56    /// Render a Move value as JSON, drawing the size budget from a caller-owned
57    /// counter. Endpoints that render many Move values in a single response
58    /// (e.g. every event in a checkpoint) share one budget across all renders
59    /// to bound the aggregate response memory rather than multiplying the
60    /// per-render limit by the number of items. The budget is updated in place
61    /// to reflect bytes consumed; once it reaches zero subsequent calls fail
62    /// fast and return `None`.
63    pub fn render_json_with_budget(
64        &self,
65        struct_tag: &move_core_types::language_storage::StructTag,
66        contents: &[u8],
67        output_objects: &ObjectSet,
68        size_budget: &mut usize,
69    ) -> Option<prost_types::Value> {
70        let layout = self
71            .reader
72            .inner()
73            .get_struct_layout_with_overlay(struct_tag, output_objects)
74            .ok()
75            .flatten()?;
76
77        sui_types::object::rpc_visitor::proto::ProtoVisitor::deserialize_value_with_budget(
78            contents,
79            &layout,
80            size_budget,
81        )
82        .map_err(|e| tracing::debug!("unable to convert move value to JSON: {e}"))
83        .ok()
84    }
85
86    pub fn render_object_display(&self, object: &sui_types::object::Object) -> Option<Display> {
87        let move_object = object.data.try_as_move()?;
88        let object_type = &move_object.type_().clone().into();
89        let contents = move_object.contents();
90
91        let limits = sui_display::v2::Limits {
92            max_depth: self.config.display().max_field_depth(),
93            max_nodes: self.config.display().max_format_nodes(),
94            max_loads: self.config.display().max_object_loads(),
95        };
96        let display_object = self.reader.get_display_object_v2_by_type(object_type)?;
97        let display_template =
98            sui_display::v2::Display::parse(limits, display_object.fields()).ok()?;
99
100        let layout = self
101            .reader
102            .inner()
103            .get_struct_layout(object_type)
104            .ok()
105            .flatten()?;
106
107        let root = sui_display::v2::OwnedSlice::new(layout, contents.to_owned());
108        let interpreter = sui_display::v2::Interpreter::new(root, DisplayStore::new(&self.reader));
109
110        let mut display = Display::default();
111
112        // The display api requires that the `sui_display::v2::Store` is async. We know that the
113        // Store implementation we are passing in here is fully synchronous, doing db access
114        // in-place and as such should never return Poll::Pending.
115        match display_template
116            .display::<prost_types::Value>(
117                self.config.display().max_move_value_depth(),
118                self.config.display().max_output_size(),
119                &interpreter,
120            )
121            .now_or_never()
122            .unwrap()
123        {
124            Ok(rendered) => {
125                let mut output = Struct::default();
126                let mut errors = Struct::default();
127
128                for (field, result) in rendered {
129                    match result {
130                        Ok(value) => {
131                            output.fields.insert(field, value);
132                        }
133                        Err(e) => {
134                            errors.fields.insert(field, e.to_string().into());
135                        }
136                    }
137                }
138
139                if !output.fields.is_empty() {
140                    display.set_output(output.fields);
141                }
142
143                if !errors.fields.is_empty() {
144                    display.set_errors(errors.fields);
145                }
146            }
147            Err(e) => {
148                display.set_errors(e.to_string());
149            }
150        }
151
152        Some(display)
153    }
154
155    pub fn render_events_to_proto(
156        &self,
157        events: &sui_types::effects::TransactionEvents,
158        mask: &FieldMaskTree,
159        output_objects: &ObjectSet,
160    ) -> TransactionEvents {
161        let mut message = TransactionEvents::default();
162
163        if mask.contains(TransactionEvents::BCS_FIELD) {
164            let mut bcs = Bcs::serialize(&events).unwrap();
165            bcs.name = Some("TransactionEvents".to_owned());
166            message.bcs = Some(bcs);
167        }
168
169        if mask.contains(TransactionEvents::DIGEST_FIELD) {
170            message.digest = Some(events.digest().to_string());
171        }
172
173        if let Some(event_mask) = mask.subtree(TransactionEvents::EVENTS_FIELD) {
174            message.events = events
175                .data
176                .iter()
177                .map(|event| self.render_event_to_proto(event, &event_mask, output_objects))
178                .collect();
179        }
180
181        message
182    }
183
184    pub fn render_event_to_proto(
185        &self,
186        event: &sui_types::event::Event,
187        mask: &FieldMaskTree,
188        output_objects: &ObjectSet,
189    ) -> Event {
190        let mut message = Event::default();
191
192        if mask.contains(Event::PACKAGE_ID_FIELD) {
193            message.set_package_id(event.package_id.to_canonical_string(true));
194        }
195
196        if mask.contains(Event::MODULE_FIELD) {
197            message.set_module(event.transaction_module.to_string());
198        }
199
200        if mask.contains(Event::SENDER_FIELD) {
201            message.sender = Some(event.sender.to_string());
202        }
203
204        if mask.contains(Event::EVENT_TYPE_FIELD) {
205            message.event_type = Some(event.type_.to_canonical_string(true));
206        }
207
208        if mask.contains(Event::CONTENTS_FIELD) {
209            let mut bcs = Bcs::from(event.contents.clone());
210            bcs.name = Some(event.type_.to_canonical_string(true));
211            message.contents = Some(bcs);
212        }
213
214        if mask.contains(Event::JSON_FIELD) {
215            message.json = self
216                .render_json(&event.type_, &event.contents, output_objects)
217                .map(Box::new);
218        }
219
220        message
221    }
222
223    // Renders clever error information in-place
224    pub fn render_clever_error(&self, effects: &mut TransactionEffects) {
225        use sui_rpc::proto::sui::rpc::v2::CleverError;
226        use sui_rpc::proto::sui::rpc::v2::MoveAbort;
227        use sui_rpc::proto::sui::rpc::v2::clever_error;
228        use sui_rpc::proto::sui::rpc::v2::execution_error::ErrorDetails;
229
230        let Some(move_abort) = effects
231            .status
232            .as_mut()
233            .and_then(|status| status.error.as_mut())
234            .and_then(|error| match &mut error.error_details {
235                Some(ErrorDetails::Abort(move_abort)) => Some(move_abort),
236                _ => None,
237            })
238        else {
239            return;
240        };
241
242        fn render(service: &RpcService, move_abort: &MoveAbort) -> Option<CleverError> {
243            let location = move_abort.location.as_ref()?;
244            let abort_code = move_abort.abort_code();
245            let package_id = location.package().parse::<sui_sdk_types::Address>().ok()?;
246            let module = location.module();
247
248            let package = {
249                let object = service.reader.inner().get_object(&package_id.into())?;
250                sui_package_resolver::Package::read_from_object(&object).ok()?
251            };
252
253            let clever_error = package.resolve_clever_error(module, abort_code)?;
254
255            let mut clever_error_message = CleverError::default();
256
257            match clever_error.error_info {
258                sui_package_resolver::ErrorConstants::None => {}
259                sui_package_resolver::ErrorConstants::Rendered {
260                    identifier,
261                    constant,
262                } => {
263                    clever_error_message.constant_name = Some(identifier);
264                    clever_error_message.value = Some(clever_error::Value::Rendered(constant));
265                }
266                sui_package_resolver::ErrorConstants::Raw { identifier, bytes } => {
267                    clever_error_message.constant_name = Some(identifier);
268                    clever_error_message.value = Some(clever_error::Value::Raw(bytes.into()));
269                }
270            }
271
272            clever_error_message.error_code = clever_error.error_code.map(Into::into);
273            clever_error_message.line_number = Some(clever_error.source_line_number.into());
274
275            Some(clever_error_message)
276        }
277
278        move_abort.clever_error = render(self, move_abort);
279    }
280
281    pub fn render_effects_to_proto(
282        &self,
283        effects: &sui_types::effects::TransactionEffects,
284        unchanged_loaded_runtime_objects: &[sui_types::storage::ObjectKey],
285        objects: &ObjectSet,
286        mask: &FieldMaskTree,
287    ) -> TransactionEffects {
288        // TODO consider inlining this function here to avoid needing to do the extra parsing below
289        let mut message = TransactionEffects::merge_from(effects, mask);
290
291        if mask.contains(TransactionEffects::UNCHANGED_LOADED_RUNTIME_OBJECTS_FIELD) {
292            message.unchanged_loaded_runtime_objects = unchanged_loaded_runtime_objects
293                .iter()
294                .map(Into::into)
295                .collect();
296        }
297
298        self.annotate_effects(&mut message, objects, mask);
299
300        message
301    }
302
303    /// Fill the effects fields that `TransactionEffects::merge_from` cannot: the
304    /// object types of changed and unchanged consensus objects, looked up in
305    /// `objects`, and the clever error of a Move abort.
306    pub fn annotate_effects(
307        &self,
308        effects: &mut TransactionEffects,
309        objects: &ObjectSet,
310        mask: &FieldMaskTree,
311    ) {
312        if mask.contains(TransactionEffects::CHANGED_OBJECTS_FIELD) {
313            for changed_object in effects.changed_objects.iter_mut() {
314                let Ok(object_id) = changed_object
315                    .object_id()
316                    .parse::<sui_types::base_types::ObjectID>()
317                else {
318                    continue;
319                };
320
321                if let Some(object) = objects.get(&sui_types::storage::ObjectKey(
322                    object_id,
323                    changed_object
324                        .input_version_opt()
325                        .unwrap_or_else(|| changed_object.output_version())
326                        .into(),
327                )) {
328                    changed_object.set_object_type(object_type_to_string(object.into()));
329                }
330            }
331        }
332
333        if mask.contains(TransactionEffects::UNCHANGED_CONSENSUS_OBJECTS_FIELD) {
334            for unchanged_consensus_object in effects.unchanged_consensus_objects.iter_mut() {
335                let Ok(object_id) = unchanged_consensus_object
336                    .object_id()
337                    .parse::<sui_types::base_types::ObjectID>()
338                else {
339                    continue;
340                };
341
342                if let Some(object) = objects.get(&sui_types::storage::ObjectKey(
343                    object_id,
344                    unchanged_consensus_object.version().into(),
345                )) {
346                    unchanged_consensus_object
347                        .set_object_type(object_type_to_string(object.into()));
348                }
349            }
350        }
351
352        // Try to render clever error info
353        self.render_clever_error(effects);
354    }
355
356    /// Fill the parts of an `ExecutedTransaction` merged from checkpoint data
357    /// that `merge_from` cannot: effects object types and clever errors (see
358    /// [`Self::annotate_effects`]) and event JSON, which needs the package
359    /// resolver. Event JSON draws from `json_budget`, which the caller shares
360    /// across every event in one response.
361    pub fn annotate_checkpoint_transaction(
362        &self,
363        message: &mut ExecutedTransaction,
364        transaction: &sui_types::full_checkpoint_content::ExecutedTransaction,
365        objects: &ObjectSet,
366        mask: &FieldMaskTree,
367        json_budget: &mut usize,
368    ) {
369        if let Some(effects_mask) = mask.subtree(ExecutedTransaction::EFFECTS_FIELD)
370            && let Some(effects) = message.effects.as_mut()
371        {
372            self.annotate_effects(effects, objects, &effects_mask);
373        }
374
375        if let Some(events_mask) = mask.subtree(ExecutedTransaction::EVENTS_FIELD)
376            && let Some(event_mask) = events_mask.subtree(TransactionEvents::EVENTS_FIELD)
377            && event_mask.contains(Event::JSON_FIELD)
378            && let Some(rendered) = message.events.as_mut()
379            && let Some(events) = &transaction.events
380        {
381            for (event_message, event) in rendered.events.iter_mut().zip_debug_eq(&events.data) {
382                event_message.json = self
383                    .render_json_with_budget(&event.type_, &event.contents, objects, json_budget)
384                    .map(Box::new);
385            }
386        }
387    }
388}
389
390fn object_type_to_string(object_type: sui_types::base_types::ObjectType) -> String {
391    match object_type {
392        sui_types::base_types::ObjectType::Package => "package".to_owned(),
393        sui_types::base_types::ObjectType::Struct(move_object_type) => {
394            move_object_type.to_canonical_string(true)
395        }
396    }
397}