[NFT] Object Display proposal (Accepted)

Object Display

This is a start of a conversation around Object Display proposal.
The questions it answers are:

  • How objects should be displayed offchain?
  • Should there be a (metadata) standard for displayable objects?
  • Can we solve display problem for both utility objects (AdminCap) and art objects (DevnetNFT)?
  • Is there a way to upgrade display of all objects in a centralized way?
  • How to we solve data duplication in scenarios where objects don’t contain unique information (imagine Weapon Skins - image stays the same for X objects)?

Current state and background

Currently, every developer, if they wish to have their objects displayed nicely, is forced to add certain properties into their Move definitions. Such as “name” which has to be of type “String” (results in a name in the wallet) and “url” with type “Url” (treated as an image for an object).

struct MyNFT has key {
 // ... custom app-specific fields ...
 name: String,
 url: Url
}

This approach is dead simple, but forces application developers to populate their assets with possibly repetitive data which usually results in a higher gas cost for creates / updates and a higher storage pricing. Additionally, if the Sui ecosystem agrees to add another property (say, “background_url”), packages published before that change would require republish / upgrades - which gets extremely expensive and logistically complicated in a decentralized system.

We believe that the main flow in this solution and similar ones is that we’re trying to solve “offchain” display with “onchain” object definitions. And by doing so, we lock ourselves in static “standards”, preemptive optimizations and inefficient practices in Move (and Smart Contracts in general).

Dynamic Configuration

With the recent Publisher Object addition, it has become possible to prove that the account has published the type (and the package where this type is defined). Practically speaking, we opened a door for a centralized configuration that is specific for a type.

This improvement finally allowed us to propose the sui::display module which aims to unify the way display properties are set; and the publisher (creator) of a package will have a tool and the power to define how their objects should look like.

So what is a Display object

It is an object with a very simple structure:

struct Display<phantom T: key> has key {
    id: UID,
    /// Contains `key => value` map with strings
    fields: VecMap<String, String>,
}

The fields property of the object contains a set of string keys and corresponding string values, which publisher can set at any time. They will contain the off-chain display configuration for the type T specified in the type signature: Display<T> (you can read it as “display-for-T”). Only publisher of the type T has the permission (unlocked with the Publisher Object) to create a new Display<T> and modify its fields.

Example: Alice publishes a new application - Capys; in her module she defines a type Capy and in the module initializer she creates a Publisher Object. Using that Publisher Object she can call the function display::create_display<T> where T = Capy. Done. The Display object for the type Capy is created.

How to use a Display object

Once Display Object is created, its fields can be set. For example, a pair "description" => "My Lovely Capy" could be used as a key and a value. Later, for example, a pair "project_url" => "https://capy.art/" could be added to the Display Object. There’s no limit to how many pairs could be added to the fields (the only limitation is the max size of a Sui Object).

The fields inside the Display should be treated as a “description” of a type, so if there’s a Display<Capy>, we expect that all Capy objects should have the same “description” and the same “project_url” field (and all the other possible fields defined in the Display - as long as they’re supported by the ecosystem).

// JSON example of Display<0x....::capy::Capy> contents
{
    "description": "My Lovely Capy",
    "project_url": "https://capy.art/"
}

However, most of the objects on Sui should contain some unique data (for example, an ID; or for an Art Objects - image URL), and if we were to solve the off-chain Display problem, we need to have a way of using the unique data stored in every Sui object and use them in the display definition. For that we suggest a string interpolation functionality.

Display pattern syntax

Every Sui object has at least one field (the required one is id of type UID), and these fields (and their values) often meant to affect the way the object is displayed. Let’s look at this example:

module my_bear_game::bears {
    struct Bear has key {
        id: UID,
        level: u8,
        ipfs: Url,
        name: String
    }
}

When showing this object in a wallet or in an explorer or in a marketplace application, we want to see most if not all of the properties defined here. But how do achieve that in a way that aligns with other objects? The field names might differ from application to application, the content might be different, lastly, we may want to have more fields - static ones - for example a “project_url” which will be the same for every Bear but we don’t want to store it in every object of Bear type - it’s simply expensive and inconvenient.

For all such cases we formed a pattern syntax for the Display. Simply put, it will be possible to insert a field of an object into a template string.

// JSON example of Display<my_bear_game::bears::Bear>
{
    "name": "{name} (Level: {level})",
    "image_url": "ipfs://{ipfs}/",
    "description": "A bear. One of many",
    "url": "https://sui-bears-game.xyz/bears/{id}",
    "project_url": "https://sui-bears-game.xyz/",
}

In the example above you can see that some fields contain expressions inside curly braces: {...}. They are meant to be substituted with the actual values of a specific object. So, for a Bear { name: "Billy", level: 10, ... } the display “name” would be "name" => "Billy (Level: 10)", and if the properties changed, the result would also change.

However, it’s up to the creator to decide which properties to use and how to use them. In this dummy bear game some properties are static: such as “project_url” and “description”, and some use templates.

Another example that we find important to show here is an utility Object, display of which can also be configured by the publisher. Even though, currently, most of the objects of that kind are displayed rather poorly.

module marketplacers::market {
    // Everyone can create a new Market; and AdminCap grants the
    // permission to withdraw profits (eg from trading fees).
    struct MarketplaceAdminCap {
        id: UID,
        market_id: ID
    }
}
// JSON example of Display<marketplacers::market::MarketplaceAdminCap>
{
    "name": "Marketplace Manager Capability",
    "description": "Grants permission to manage Market #{market_id}",
    "link": "https://markeplacers.xyz/admin/{market_id}",
    "image_url": "https://marketplacers.xyz/cap.png",
    "project_url": "https://marketplacers.xyz/"
}

Not only it improves the user experience by giving utility objects visuals, but also provides helpful links and tips for the owners.

Is there a list of Display properties I can use?

Short answer - there is no. The set of fields is meant to defined by the community and the actual usecases within the Sui ecosystem. The best way to think about it is as of NFT Metadata standards - they appeared, they evolved, they were changing from platform to platform; but unlike most of the metadata examples we know today, Display proposal does not set any restrictions to current nor further development. In other words - if some marketplace or a wallet X adds support for a feature “background_url”, creators are free to update the Display for their objects to follow the trend; if ecosystem supports this addition and more actors enable it in their applications, this field de-facto becomes a part of the available set.

Nonetheless, we believe that it would be helpful to suggest a set of base properties which already exist (in that way or another) in the ecosystem:

  • name - displayable name of an Object
  • link - link to an Object in an application / marketplace / explorer
  • image_url - the url to a displayable image of an Object
  • project_url - the link to the application (eg https://capy.art/)
  • creator - any string or URL that shows the creator

We believe this set would make the adaptation and the switching process simpler as well as help bootstrap most of the applications we see today.

Adoption and DevX

As platform creators we’re in the perfect position to integrate significant features like this one into the Full Node and its JSON RPC. Having a single place for consumers to get a Display for an object X is crucial for adoption and application performance. Tracking of the new display objects should also be performed by an indexer to make it accessible as fast as possible.

For the first step we consider adding an RPC method of kind (*actual name and arguments might change):

name: "sui_getDisplay"
params: [ objectId ]
returns: {} # JSON of the Display contents with pre-inserted values

It would free the wallet/explorer/ecosystem developers from the need to implement and support the Display syntax and lookup by giving them a single tool.

Feedback

It is important to mention that this is only the beginning of the conversation and we’re open to any feedback. Please, submit your questions and suggestions to this thread.

22 Likes

Great to see this initiative!

I wanted to ask about a solution that might be a bit tangential to this one but would help solve this problem in an alternative way.

What Display does is in essence provide a “view” into a struct. An alternative approach to providing such views would be to allow return arguments from entry functions (or allow defining a custom view function).

This would be extremely useful for providing display types for objects with dynamic (object) fields or fields that need to be somehow transformed.

A module creator could then pull data from the object into a custom view struct or return the object outright for serialization. Such view functions would be immutable, limited in complexity, and run directly on the process that would otherwise be handling the display RPC call.

As an example, this standard would not be compatible with OriginByte domains for a couple of reasons:

  1. How NFTs are displayed is dependent on the domains registered on it
  2. Domains are indexed by type making it technically difficult to index into their fields if only string keys are indexable ({DisplayDomain.name}).

Is this a viable alternative to consider?

7 Likes

In damir’s solution it is possible to add new display features to Display object thus changing how the object is displayed. But if we were to use view functions for returning display props then we would have to upgrade the module each time we want to have a new display feature.

3 Likes

Thanks for the feedback!

What Display does is in essence provide a “view” into a struct. An alternative approach to providing such views would be to allow return arguments from entry functions (or allow defining a custom view function).

I don’t think that entry functions arguments would be a fit here; and it is important to actually draw the line between what happens on-chain and off-chain. The logic of this proposal is intentionally minimizes amount of actions / coding performed on chain for a number of reasons:

  1. We shouldn’t mix module code and offchain logic - Display is an off-chain property, and we shouldn’t pay for it if we can cut down costs
  2. If we were to define a struct / display function, as it was already noticed by @hikmo, it would require package upgrades; and this is rather not desirable
  3. Not just that, but also we’ll have to settle on the fields returned in this struct and that would make sudden changes and display extension almost unbearably hard; while the presented solution can be patched, updated with ease by anyone when they need it

A module creator could then pull data from the object into a custom view struct or return the object outright for serialization. Such view functions would be immutable, limited in complexity, and run directly on the process that would otherwise be handling the display RPC call.

Another thing to consider here is that Move does not work with strings well just yet; and it’s not the highest priority in the compiler as of right now; and for something like that to work we probably need a set of handy functions like string concatenation, splitting, slicing etc - which are not there.

As an example, this standard would not be compatible with OriginByte domains for a couple of reasons…

We kept in mind their solution while shaping out the proposal and we feel that it still will work, if we add support for dynamic fields, nested fields and table access to the template syntax. More updates on the syntax to come next week. But short summary is that there will be a function for every special case of kind (syntax might slightly change):

{
  "name": "Capy: { df(<dynamic_field_key>).name }",
  "image_url": "https://api.capy.art/capy/{ id }/svg"
}

Thanks again for the question! It’s a very good one and we explored this implementation path as well.

3 Likes

I really like this idea. i feel like a simple solution might be to have string interpolation also be function calls ? Like instead of {{ name }} it could be {{ getDisplayName() }}

Hello, is there any update regarding template syntax?