sui_indexer_alt_jsonrpc/api/objects/
response.rsuse std::{collections::BTreeMap, fmt::Write};
use anyhow::{bail, Context as _};
use futures::future::OptionFuture;
use move_core_types::{annotated_value::MoveTypeLayout, language_storage::StructTag};
use sui_display::v1::Format;
use sui_json_rpc_types::{
DisplayFieldsResponse, SuiData, SuiObjectData, SuiObjectDataOptions, SuiObjectResponse,
SuiParsedData, SuiPastObjectResponse, SuiRawData,
};
use sui_types::{
base_types::{ObjectID, ObjectType, SequenceNumber},
display::DisplayVersionUpdatedEvent,
error::SuiObjectResponseError,
object::{Data, Object},
TypeTag,
};
use tokio::join;
use crate::{
context::Context,
data::{displays::DisplayKey, object_info::LatestObjectInfoKey, objects::load_latest},
error::{rpc_bail, InternalContext, RpcError},
};
pub(super) async fn live_object(
ctx: &Context,
object_id: ObjectID,
options: &SuiObjectDataOptions,
) -> Result<SuiObjectResponse, RpcError> {
let Some(info) = ctx
.pg_loader()
.load_one(LatestObjectInfoKey(object_id))
.await
.context("Failed to load object ownership information from store")?
else {
return Ok(SuiObjectResponse::new_with_error(
SuiObjectResponseError::NotExists { object_id },
));
};
if info.owner_kind.is_none() {
return Ok(SuiObjectResponse::new_with_error(
SuiObjectResponseError::NotExists { object_id },
));
}
latest_object(ctx, object_id, options).await
}
pub(super) async fn latest_object(
ctx: &Context,
object_id: ObjectID,
options: &SuiObjectDataOptions,
) -> Result<SuiObjectResponse, RpcError> {
let object = load_latest(ctx, object_id)
.await
.context("Failed to load latest object")?
.context("Could not find latest content for live object")?;
Ok(SuiObjectResponse::new_with_data(
object_data_with_options(ctx, object, options).await?,
))
}
pub(super) async fn past_object(
ctx: &Context,
object_id: ObjectID,
version: SequenceNumber,
options: &SuiObjectDataOptions,
) -> Result<SuiPastObjectResponse, RpcError> {
let Some(object) = ctx
.kv_loader()
.load_one_object(object_id, version.value())
.await
.context("Failed to load object from store")?
else {
return Ok(SuiPastObjectResponse::VersionNotFound(object_id, version));
};
Ok(SuiPastObjectResponse::VersionFound(
object_data_with_options(ctx, object, options).await?,
))
}
pub(crate) async fn object_data_with_options(
ctx: &Context,
object: Object,
options: &SuiObjectDataOptions,
) -> Result<SuiObjectData, RpcError> {
let type_ = options.show_type.then(|| ObjectType::from(&object));
let owner = options.show_owner.then(|| object.owner().clone());
let previous_transaction = options
.show_previous_transaction
.then(|| object.previous_transaction);
let storage_rebate = options.show_storage_rebate.then(|| object.storage_rebate);
let content: OptionFuture<_> = options
.show_content
.then(|| object_data::<SuiParsedData>(ctx, &object))
.into();
let bcs: OptionFuture<_> = options
.show_bcs
.then(|| object_data::<SuiRawData>(ctx, &object))
.into();
let display: OptionFuture<_> = options.show_display.then(|| display(ctx, &object)).into();
let (content, bcs, display) = join!(content, bcs, display);
let content = content
.transpose()
.internal_context("Failed to deserialize object content")?;
let bcs = bcs
.transpose()
.internal_context("Failed to deserialize object to BCS")?;
Ok(SuiObjectData {
object_id: object.id(),
version: object.version(),
digest: object.digest(),
type_,
owner,
previous_transaction,
storage_rebate,
display,
content,
bcs,
})
}
async fn object_data<D: SuiData>(ctx: &Context, object: &Object) -> Result<D, RpcError> {
Ok(match object.data.clone() {
Data::Package(move_package) => D::try_from_package(move_package)?,
Data::Move(move_object) => {
let type_: TypeTag = move_object.type_().clone().into();
let MoveTypeLayout::Struct(layout) = ctx
.package_resolver()
.type_layout(type_.clone())
.await
.with_context(|| {
format!(
"Failed to resolve type layout for {}",
type_.to_canonical_display(true)
)
})?
else {
rpc_bail!(
"Type {} is not a struct",
type_.to_canonical_display(true)
);
};
D::try_from_object(move_object, *layout)?
}
})
}
async fn display(ctx: &Context, object: &Object) -> DisplayFieldsResponse {
let fields = match display_fields(ctx, object).await {
Ok(fields) => fields,
Err(e) => {
return DisplayFieldsResponse {
data: None,
error: Some(SuiObjectResponseError::DisplayError {
error: format!("{e:#}"),
}),
}
}
};
let mut field_values = BTreeMap::new();
let mut field_errors = String::new();
let mut prefix = "";
for (name, value) in fields {
match value {
Ok(value) => {
field_values.insert(name, value);
}
Err(e) => {
write!(field_errors, "{prefix}Error for field {name:?}: {e:#}").unwrap();
prefix = "; ";
}
}
}
DisplayFieldsResponse {
data: Some(field_values),
error: (!field_errors.is_empty()).then_some(SuiObjectResponseError::DisplayError {
error: field_errors,
}),
}
}
async fn display_fields(
ctx: &Context,
object: &Object,
) -> anyhow::Result<BTreeMap<String, anyhow::Result<String>>> {
let Some(object) = object.data.try_as_move() else {
bail!("Display is only supported for Move objects");
};
let config = &ctx.config().objects;
let type_: StructTag = object.type_().clone().into();
let layout = ctx.package_resolver().type_layout(type_.clone().into());
let display = ctx.pg_loader().load_one(DisplayKey(type_.clone()));
let (layout, display) = join!(layout, display);
let layout = layout.context("Failed to resolve type layout")?;
let Some(stored) = display.context("Failed to load Display format")? else {
bail!(
"Display format not found for {}",
type_.to_canonical_display(true)
);
};
let event: DisplayVersionUpdatedEvent =
bcs::from_bytes(&stored.display).context("Failed to deserialize Display format")?;
let format = Format::parse(config.max_display_field_depth, &event.fields)?;
format.display(config.max_display_output_size, object.contents(), &layout)
}