llms.txt
Skip to main content

Object Display

Objects on Sui are explicit in their structure and behavior and can be displayed in an understandable way. However, to support richer metadata for clients, there's a standard and efficient way of "describing" them to the client - the Display object, registered in the system Display Registry defined in the Sui Framework.

Background

Historically, there were different attempts to agree on a standard structure of an object so it can be displayed in a user interface. One of the approaches was to define certain fields in the object struct which, when present, would be used in the UI. This approach was not flexible enough and required developers to define the same fields in every object, and sometimes the fields did not make sense for the object.

/// An attempt to standardize the object structure for display.
public struct CounterWithDisplay has key {
id: UID,
/// If this field is present it will be displayed in the UI as `name`.
name: String,
/// If this field is present it will be displayed in the UI as `description`.
description: String,
// ...
image: String,
/// Actual fields of the object.
counter: u64,
// ...
}

If any of the fields contained static data, it would be duplicated in every object. And, since Move does not have interfaces, it is not possible to know if an object has a specific field without "manually" checking the object's type, which makes the client fetching more complex.

Object Display

To address these issues, Sui introduces a standard way of describing an object for display. Instead of defining fields in the object struct, the display metadata is stored in a separate object - Display<T> - which is associated with the type T. This way, the display metadata is not duplicated, and it is easy to extend and maintain.

Another important feature of Sui Display is the ability to define templates and use object fields in those templates. Not only does it allow for a more flexible display, but it also frees the developer from the need to define the same fields with the same names and types in every object.

The Object Display is natively supported by the Sui Full Node, and the client can fetch the display metadata for any object if the object type has a Display associated with it.

Display Registry

For every type T there is exactly one Display<T>, and it lives at a predictable address. Both properties come from the Display Registry - a system shared object located at the reserved address 0xd (see Reserved Addresses). When a display is created, its object ID is derived from the registry's UID and the type T. As a result, anyone - including RPCs and other clients - can compute the ID of Display<T> offline and fetch it directly, without scanning events or querying historical data.

module sui::display_registry;

/// The root of display, to enable derivation of addresses.
/// The address is system-generated at `0xd`.
public struct DisplayRegistry has key { id: UID }

/// Holds the display values for the type `T`.
public struct Display<phantom T> has key {
id: UID,
/// All the (key,value) entries for a given display object.
fields: VecMap<String, String>,
/// ID of the `DisplayCap` managing this display. `None` for
/// migrated V1 displays until the capability is claimed.
cap_id: Option<ID>,
}

/// The capability object that is used to manage the display.
public struct DisplayCap<phantom T> has key, store { id: UID }

The Display<T> object itself is shared, and the authority over it is represented by a separate owned object - the DisplayCap<T> capability. The holder of the capability can set, unset, or clear the display fields at any time, and the changes apply globally without the need to update every object. The capability can be transferred to another account, or built into an application with custom metadata-management functionality.

Creating a Display

A new Display is created with one of two functions, both taking a mutable reference to the DisplayRegistry and returning the Display<T> together with its DisplayCap<T>:

  • display_registry::new<T> - takes an Internal Permit, and hence can only be called from the module that defines T;
  • display_registry::new_with_publisher<T> - takes the Publisher object, for cases when the display is created outside of the defining module.

Because the registry is a shared object, it cannot be accessed in the module initializer - the display is created by a separate, one-time call right after the package is published:

module book::arena;

use std::string::String;
use sui::display_registry::{Self, DisplayRegistry, DisplayCap};

/// Some object which will be displayed.
public struct Hero has key {
id: UID,
class: String,
level: u64,
}

/// Creates the `Display<Hero>`. Call it exactly once, right after publishing:
/// the registry holds a single `Display` per type, so a second call aborts.
/// It is an `entry` function rather than a `public` one, so that a later
/// package upgrade can remove it - upgrade rules freeze `public` functions,
/// but not `entry` ones.
entry fun create_display(registry: &mut DisplayRegistry, ctx: &mut TxContext) {
let (mut display, cap) = display_registry::new<Hero>(
registry,
internal::permit(),
ctx,
);

display.set(&cap, "name", "{class} (lvl. {level})");
display.set(&cap, "description", "One of the greatest heroes of all time. Join us!");
display.set(&cap, "link", "https://example.com/hero/{id}");
display.set(&cap, "image_url", "https://example.com/hero/{class}.jpg");

// Share the `Display` so clients can find it, and send the capability to
// the publisher, who keeps it to update the fields later.
display.share();
transfer::public_transfer(cap, ctx.sender());
}

The set calls define the template fields, and share finalizes the creation by sharing the Display object; the DisplayCap is then transferred to the publisher, who keeps it to update the fields later. Note that the function is defined as entry rather than public: a one-time setup function is best kept out of the package's public API, so that a later upgrade can remove it - upgrade compatibility rules freeze public function signatures, but not entry ones.

Standard Fields

The fields that are supported most widely are:

  • name - A name for the object. The name is displayed when users view the object.
  • description - A description for the object. The description is displayed when users view the object.
  • link - A link to the object to use in an application.
  • image_url - A URL or a blob with the image for the object.
  • thumbnail_url - A URL to a smaller image to use in wallets, explorers, and other products as a preview.
  • project_url - A link to a website associated with the object or creator.
  • creator - A string that indicates the object creator.

Please refer to the Sui Documentation for the most up-to-date list of supported fields.

While there's a standard set of fields, the Display object does not enforce them. The developer can define any fields they need, and the client can use them as they see fit. Some applications may require additional fields and omit others, and the Display is flexible enough to support them.

Template Syntax

Every value in a Display is a format string - a mix of literal text and expressions delimited by { and }. The simplest expression is a field path: {path} is replaced with the value of the field at that path, where the path is a dot-separated list of field names starting from the object being displayed. To output a literal brace, double it - {{ becomes {.

/// Some common metadata for objects.
public struct Metadata has store {
name: String,
description: String,
published_at: u64
}

/// The type with nested Metadata field.
public struct LittlePony has key, store {
id: UID,
image_url: String,
metadata: Metadata
}

The Display for the type LittlePony above could be defined as follows:

{
"name": "Just a pony",
"image_url": "{image_url}",
"description": "{metadata.description}"
}

A field path is only the most basic expression. The full form of an expression has three parts - a chain that navigates into the data, an optional list of fallbacks separated by |, and an optional transform prefixed with : that controls how the value is rendered:

{ chain | fallback | ... : transform }

The following sections walk through the parts of this syntax that come up most often. For the complete grammar - literals, struct and enum values, derived-object access, and the exhaustive transform list - see the Object Display Syntax reference.

Vector and Map Indexing

A chain can index into a vector or a VecMap with square brackets. Numeric indices always carry a type suffix - 0u64, not 0 - and another field's value can be used as the index:

{items[0u64]}           first element of the `items` vector
{items[idx]} use the `idx` field's value as the index
{scores[6u32]} look up the key `6u32` in a VecMap, returns its value

Dynamic Field Access

Templates can reach beyond the object's own fields and load dynamic fields from storage. The -> operator loads a dynamic field, => loads a dynamic object field, and the key goes in brackets:

{parent->['color']}     dynamic field with the string key 'color'
{parent->['color'].x} read field `x` on the loaded value
{parent=>['hat']} dynamic object field (the value is a full object)

Because each load reads from storage, they are budgeted: a template may perform at most 8 object loads by default, with -> costing one and => costing two.

Transforms

By default a value is rendered as a human-readable string. A transform after : changes that - useful for values that are not plain text, such as byte vectors or timestamps:

TransformEffect
str (default)Human-readable string; UTF-8 for String and vector<u8>.
hexLowercase, zero-padded hexadecimal.
base64Base64-encoded bytes; accepts url and nopad modifiers.
bcsBCS-serialized value, then Base64-encoded - for aggregate types.
jsonStructured JSON value; only when it is the whole format string.
timestamp (ts)A numeric value read as Unix milliseconds, formatted ISO 8601.
urlLike str, but percent-encodes reserved URL characters.
{amount:hex}                    render `amount` as hex
{created_at:ts} "2023-04-12T17:00:00Z"
{metadata:json} emit the whole struct as JSON

Fallbacks

If a chain evaluates to null - a missing field, an out-of-bounds index, or a None Option - the next chain after | is tried. A string literal in single quotes makes a convenient default:

{display_name | name | 'Anonymous'}

If every alternative is null, the whole format string evaluates to null and the field is omitted from the result.

Migrating from V1 to V2

The registry-backed Display described on this page is the second version of the standard - Display V2. The original one - V1, implemented in the sui::display module - predates the registry: V1 Display<T> objects were owned rather than shared, could only be created with the Publisher object, and were discovered through events. Any number of V1 displays could exist for the same type, and full nodes used the most recently updated one. V2 replaces event-based discovery with derivation from the registry, and reduces "any number of displays" to exactly one per type.

Existing V1 displays were migrated to V2 automatically by a system migration: for every type with a V1 display, there is already a shared Display<T> with the same fields and with cap_id set to none. To manage such a display, the creator claims its DisplayCap in one of two ways:

  • claim - consumes the legacy V1 Display object as the proof of authority over the type, destroying it in the process;
  • claim_with_publisher - uses the Publisher object instead; the leftover V1 object can then be destroyed with delete_legacy.
use sui::display::Display as LegacyDisplay;

/// Claim the `DisplayCap` for the system-migrated `Display<Hero>`, giving up
/// the legacy V1 `Display` object, which is destroyed in the process.
public fun claim_display_cap(
display: &mut display_registry::Display<Hero>,
legacy: LegacyDisplay<Hero>,
ctx: &mut TxContext,
): DisplayCap<Hero> {
display.claim(legacy, ctx)
}

For a V1 display that was created after the system migration took place, the display_registry::migrate_v1_to_v2 function performs the migration directly: it creates the V2 Display, copies the fields from the legacy object, destroys it, and returns the new display together with its capability.

Further Reading

llms.txt