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