Skip to content

Commit

Permalink
[Display] RPC support for fetching and rendering Display object (#8663)
Browse files Browse the repository at this point in the history
## Description 

This PR adds the RPC support for
#7668 and added e2e TS tests

Todos(in separate PRs)

- [ ] merge the display endpoint into the[ new queryObjects
API](https://www.notion.so/mystenlabs/RFC-GetObjects-API-Refactoring-27ff6b8d6ffc42c8b492c1efa368b70a?pvs=4)
- [ ] Handle struct that has no Display defined `Overall display should
be treated as Debug and Display traits in Rust. If we don’t have Display
defined, we show default debug (on the FE/Apps), if there’s Display, we
show things nicely.` I'll implement this when we implement the new
queryObjects API
- [ ] Add rust based tests


## Test Plan 

How did you test the new or updated feature?

Added e2e TS tests to cover the following test cases
- Escape syntax (e.g., `\{name\}` should return `{name}` instead of
`Alice` even if there's a field called Alice)
- Nested fields
- multiple fields
- option::some and option::none
- no template value
- sui address
- UID
- `sui::url::Url` type



---
If your changes are not user-facing and not a breaking change, you can
skip the following section. Otherwise, please indicate what changed, and
then add to the Release Notes section as highlighted during the release
process.

### Type of Change (Check all that apply)

- [ ] user-visible impact
- [ ] breaking change for a client SDKs
- [ ] breaking change for FNs (FN binary must upgrade)
- [ ] breaking change for validators or node operators (must upgrade
binaries)
- [ ] breaking change for on-chain data layout
- [ ] necessitate either a data wipe or data migration

### Release notes
  • Loading branch information
666lcz authored Feb 28, 2023
1 parent bf545c7 commit 4593333
Show file tree
Hide file tree
Showing 16 changed files with 582 additions and 9 deletions.
5 changes: 5 additions & 0 deletions .changeset/silver-geckos-look.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@mysten/sui.js": patch
---

Add optional parameter for filtering object by type in getOwnedObjectsByAddress
7 changes: 7 additions & 0 deletions crates/sui-indexer/src/apis/read_api.rs
Original file line number Diff line number Diff line change
Expand Up @@ -213,6 +213,13 @@ impl ReadApiServer for ReadApi {
async fn get_raw_object(&self, object_id: ObjectID) -> RpcResult<GetRawObjectDataResponse> {
self.fullnode.get_raw_object(object_id).await
}

async fn get_display_deprecated(
&self,
object_id: ObjectID,
) -> RpcResult<BTreeMap<String, String>> {
self.fullnode.get_display_deprecated(object_id).await
}
}

impl SuiRpcModule for ReadApi {
Expand Down
9 changes: 9 additions & 0 deletions crates/sui-json-rpc/src/api/read.rs
Original file line number Diff line number Diff line change
Expand Up @@ -201,4 +201,13 @@ pub trait ReadApi {
/// the id of the object
object_id: ObjectID,
) -> RpcResult<GetRawObjectDataResponse>;

// TODO: this will be replaced by the new queryObjects API
/// Return the Display string of a object
#[method(name = "getDisplayDeprecated")]
async fn get_display_deprecated(
&self,
/// the id of the object
object_id: ObjectID,
) -> RpcResult<BTreeMap<String, String>>;
}
205 changes: 201 additions & 4 deletions crates/sui-json-rpc/src/read_api.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,12 @@ use async_trait::async_trait;
use jsonrpsee::core::RpcResult;
use move_binary_format::normalized::{Module as NormalizedModule, Type};
use move_core_types::identifier::Identifier;
use move_core_types::language_storage::StructTag;
use move_core_types::value::{MoveStruct, MoveValue};
use std::collections::BTreeMap;
use std::sync::Arc;
use sui_types::collection_types::VecMap;
use sui_types::display::{DisplayCreatedEvent, DisplayObject};
use sui_types::intent::{AppId, Intent, IntentMessage, IntentScope, IntentVersion};
use tap::TapFallible;

Expand All @@ -17,9 +21,9 @@ use jsonrpsee::RpcModule;
use sui_core::authority::AuthorityState;
use sui_json_rpc_types::{
Checkpoint, CheckpointId, DynamicFieldPage, GetObjectDataResponse, GetPastObjectDataResponse,
GetRawObjectDataResponse, MoveFunctionArgType, ObjectValueKind, Page,
SuiMoveNormalizedFunction, SuiMoveNormalizedModule, SuiMoveNormalizedStruct, SuiObjectInfo,
SuiTransactionEffects, SuiTransactionResponse, TransactionsPage,
GetRawObjectDataResponse, MoveFunctionArgType, ObjectValueKind, Page, SuiEvent,
SuiMoveNormalizedFunction, SuiMoveNormalizedModule, SuiMoveNormalizedStruct, SuiMoveStruct,
SuiMoveValue, SuiObjectInfo, SuiTransactionEffects, SuiTransactionResponse, TransactionsPage,
};
use sui_open_rpc::Module;
use sui_types::base_types::SequenceNumber;
Expand All @@ -32,7 +36,7 @@ use sui_types::messages_checkpoint::{
};
use sui_types::move_package::normalize_modules;
use sui_types::object::{Data, ObjectRead};
use sui_types::query::TransactionQuery;
use sui_types::query::{EventQuery, TransactionQuery};

use sui_types::dynamic_field::DynamicFieldName;
use tracing::debug;
Expand All @@ -41,6 +45,8 @@ use crate::api::cap_page_limit;
use crate::error::Error;
use crate::SuiRpcModule;

const MAX_DISPLAY_NESTED_LEVEL: usize = 10;

// An implementation of the read portion of the JSON-RPC interface intended for use in
// Fullnodes.
pub struct ReadApi {
Expand Down Expand Up @@ -372,6 +378,15 @@ impl ReadApiServer for ReadApi {
.map_err(|e| anyhow!("{e}"))?
.try_into()?)
}

async fn get_display_deprecated(
&self,
object_id: ObjectID,
) -> RpcResult<BTreeMap<String, String>> {
let (object_type, move_struct) = get_object_type_and_struct(self, object_id).await?;
let display_object = get_display_object(self, object_type).await?;
Ok(get_rendered_fields(display_object.fields, &move_struct).map_err(|e| anyhow!("{e}"))?)
}
}

impl SuiRpcModule for ReadApi {
Expand All @@ -384,6 +399,84 @@ impl SuiRpcModule for ReadApi {
}
}

async fn get_display_object(
fullnode_api: &ReadApi,
object_type: StructTag,
) -> RpcResult<DisplayObject> {
let display_object_id = get_display_object_id(fullnode_api, object_type).await?;
if let ObjectRead::Exists(_, display_object, _) = fullnode_api
.state
.get_object_read(&display_object_id)
.await
.map_err(|e| anyhow!("Failed to fetch display object {display_object_id}: {e}"))?
{
let move_object = display_object
.data
.try_as_move()
.ok_or_else(|| anyhow!("Failed to extract Move object from {display_object_id}"))?;
Ok(bcs::from_bytes::<DisplayObject>(move_object.contents())
.map_err(|e| anyhow!("Failed to deserialize DisplayObject {display_object_id}: {e}"))?)
} else {
Err(anyhow!("Display object {display_object_id} does not exist"))?
}
}

async fn get_display_object_id(
fullnode_api: &ReadApi,
object_type: StructTag,
) -> RpcResult<ObjectID> {
let display_created_event = fullnode_api
.state
.get_events(
EventQuery::MoveEvent(DisplayCreatedEvent::type_(&object_type).to_string()),
/* cursor */ None,
/* limit */ 1,
/* descending */ false,
)
.await?;
if display_created_event.is_empty() {
return Err(anyhow!(
"Failed to find DisplayCreated event for {object_type}"
))?;
}
if let SuiEvent::MoveEvent { bcs, .. } = display_created_event[0].clone().1.event {
let display_object_id = bcs::from_bytes::<DisplayCreatedEvent>(&bcs)
.map_err(|e| anyhow!("Failed to deserialize DisplayCreatedEvent: {e}"))?
.id
.bytes;
Ok(display_object_id)
} else {
Err(anyhow!("Failed to extract display object id from event"))?
}
}

async fn get_object_type_and_struct(
fullnode_api: &ReadApi,
object_id: ObjectID,
) -> RpcResult<(StructTag, MoveStruct)> {
let object_read = fullnode_api
.state
.get_object_read(&object_id)
.await
.map_err(|e| anyhow!("Failed to fetch {object_id}: {e}"))?;
if let ObjectRead::Exists(_, o, layout) = object_read {
let layout = layout.ok_or_else(|| anyhow!("Failed to extract layout"))?;
let object_type = o
.type_()
.ok_or_else(|| anyhow!("Failed to extract object type"))?
.clone();
let move_struct = o
.data
.try_as_move()
.ok_or_else(|| anyhow!("Failed to extract Move object from {object_id}"))?
.to_move_struct(&layout)
.map_err(|err| anyhow!("{err}"))?;
Ok((object_type, move_struct))
} else {
Err(anyhow!("Object {object_id} does not exist"))?
}
}

pub async fn get_move_module(
fullnode_api: &ReadApi,
package: ObjectID,
Expand Down Expand Up @@ -433,3 +526,107 @@ pub fn get_transaction_data_and_digest(
let txn_digest = TransactionDigest::new(sha3_hash(&intent_msg.value));
Ok((intent_msg.value, txn_digest))
}

pub fn get_rendered_fields(
fields: VecMap<String, String>,
move_struct: &MoveStruct,
) -> RpcResult<BTreeMap<String, String>> {
let sui_move_value: SuiMoveValue = MoveValue::Struct(move_struct.clone()).into();
if let SuiMoveValue::Struct(move_struct) = sui_move_value {
return fields
.contents
.iter()
.map(|entry| match parse_template(&entry.value, &move_struct) {
Ok(value) => Ok((entry.key.clone(), value)),
Err(e) => Err(e),
})
.collect::<RpcResult<BTreeMap<_, _>>>();
}
Err(anyhow!("Failed to parse move struct"))?
}

fn parse_template(template: &str, move_struct: &SuiMoveStruct) -> RpcResult<String> {
let mut output = template.to_string();
let mut var_name = String::new();
let mut in_braces = false;
let mut escaped = false;

for ch in template.chars() {
match ch {
'\\' => {
escaped = true;
continue;
}
'{' if !escaped => {
in_braces = true;
var_name.clear();
}
'}' if !escaped => {
in_braces = false;
let value = get_value_from_move_struct(move_struct, &var_name)?;
output = output.replace(&format!("{{{}}}", var_name), &value.to_string());
}
_ if !escaped => {
if in_braces {
var_name.push(ch);
}
}
_ => {}
}
escaped = false;
}

Ok(output.replace('\\', ""))
}

fn get_value_from_move_struct(move_struct: &SuiMoveStruct, var_name: &str) -> RpcResult<String> {
let parts: Vec<&str> = var_name.split('.').collect();
if parts.is_empty() {
return Err(anyhow!("Display template value cannot be empty"))?;
}
if parts.len() > MAX_DISPLAY_NESTED_LEVEL {
return Err(anyhow!(
"Display template value nested depth cannot exist {}",
MAX_DISPLAY_NESTED_LEVEL
))?;
}
let mut current_value = &SuiMoveValue::Struct(move_struct.clone());
// iterate over the parts and try to access the corresponding field
for part in parts {
match current_value {
SuiMoveValue::Struct(move_struct) => {
if let SuiMoveStruct::WithTypes { type_: _, fields }
| SuiMoveStruct::WithFields(fields) = move_struct
{
if let Some(value) = fields.get(part) {
current_value = value;
} else {
return Err(anyhow!(
"Field value {} cannot be found in struct",
var_name
))?;
}
} else {
return Err(anyhow!(
"Unexpected move struct type for field {}",
var_name
))?;
}
}
_ => return Err(anyhow!("Unexpected move value type for field {}", var_name))?,
}
}

match current_value {
SuiMoveValue::Option(move_option) => match move_option.as_ref() {
Some(move_value) => Ok(move_value.to_string()),
None => Ok("".to_string()),
},
SuiMoveValue::Vector(_) => Err(anyhow!(
"Vector is not supported as a Display value {}",
var_name
))?,

_ => Ok(current_value.to_string()),
}
}
29 changes: 29 additions & 0 deletions crates/sui-open-rpc/spec/openrpc.json
Original file line number Diff line number Diff line change
Expand Up @@ -823,6 +823,35 @@
}
}
},
{
"name": "sui_getDisplayDeprecated",
"tags": [
{
"name": "Read API"
}
],
"description": "Return the Display string of a object",
"params": [
{
"name": "object_id",
"description": "the id of the object",
"required": true,
"schema": {
"$ref": "#/components/schemas/ObjectID"
}
}
],
"result": {
"name": "BTreeMap<String,String>",
"required": true,
"schema": {
"type": "object",
"additionalProperties": {
"type": "string"
}
}
}
},
{
"name": "sui_getDynamicFieldObject",
"tags": [
Expand Down
4 changes: 2 additions & 2 deletions crates/sui-types/src/collection_types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,8 @@ pub struct VecMap<K, V> {
/// Rust version of the Move sui::vec_map::Entry type
#[derive(Debug, Serialize, Deserialize, Clone, Eq, PartialEq, JsonSchema)]
pub struct Entry<K, V> {
key: K,
value: V,
pub key: K,
pub value: V,
}

/// Rust version of the Move sui::vec_set::VecSet type
Expand Down
39 changes: 39 additions & 0 deletions crates/sui-types/src/display.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
// Copyright (c) Mysten Labs, Inc.
// SPDX-License-Identifier: Apache-2.0

use crate::collection_types::VecMap;
use crate::id::{ID, UID};
use crate::SUI_FRAMEWORK_ADDRESS;
use move_core_types::ident_str;
use move_core_types::identifier::IdentStr;
use move_core_types::language_storage::StructTag;
use serde::Deserialize;

pub const DISPLAY_MODULE_NAME: &IdentStr = ident_str!("display");
pub const DISPLAY_CREATED_EVENT_NAME: &IdentStr = ident_str!("DisplayCreated");

// TODO: add tests to keep in sync
/// Rust version of the Move sui::display::Display type
#[derive(Debug, Deserialize, Clone, Eq, PartialEq)]
pub struct DisplayObject {
pub id: UID,
pub fields: VecMap<String, String>,
pub version: u16,
}

#[derive(Deserialize, Debug)]
pub struct DisplayCreatedEvent {
// The Object ID of Display Object
pub id: ID,
}

impl DisplayCreatedEvent {
pub fn type_(inner: &StructTag) -> StructTag {
StructTag {
address: SUI_FRAMEWORK_ADDRESS,
name: DISPLAY_CREATED_EVENT_NAME.to_owned(),
module: DISPLAY_MODULE_NAME.to_owned(),
type_params: vec![inner.clone().into()],
}
}
}
1 change: 1 addition & 0 deletions crates/sui-types/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ pub mod collection_types;
pub mod committee;
pub mod crypto;
pub mod digests;
pub mod display;
pub mod dynamic_field;
pub mod event;
pub mod filter;
Expand Down
Loading

0 comments on commit 4593333

Please sign in to comment.