IOTA Object Display
The IOTA Object Display standard is a template engine that enables on-chain management of off-chain representation (display) for a type. With it, you can substitute data for an object into a template string. The standard doesn’t limit the fields you can set. You can use the {property}
syntax to access all object properties, and then insert them as a part of the template string.
Use a Publisher
object that you own to set iota::display
for a type.
In IOTA Move, Display<T>
represents an object that specifies a set of named templates for the type T
. For example, for a type 0x2::hero::Hero
the display syntax is: Display<0x2::hero::Hero>
.
IOTA Full nodes process all objects of the type T
by matching the Display
definition, and return the processed result when you query an object with the { showDisplay: true }
setting in the query.
Display properties
The basic set of properties suggested includes:
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.
An example IOTA Hero module
The following code sample demonstrates how the Display
for an example Hero
module varies based on the name
, id
, and image_url
properties of the type Hero
.
The following represents the template the init
function defines:
{
"name": "{name}",
"link": "https://iota-heroes.io/hero/{id}",
"image_url": "ipfs://{image_url}",
"description": "A true Hero of the IOTA ecosystem!",
"project_url": "https://iota-heroes.io",
"creator": "Unknown IOTA Fan"
}
/// Example of an unlimited "IOTA Hero" collection - anyone can
/// mint their Hero. Shows how to initialize the `Publisher` and how
/// to use it to get the `Display<Hero>` object - a way to describe a
/// type for the ecosystem.
module examples::my_hero {
use iota::tx_context::{sender};
use std::string::{utf8, String};
// The creator bundle: these two packages often go together.
use iota::package;
use iota::display;
/// The Hero - an outstanding collection of digital art.
public struct Hero has key, store {
id: UID,
name: String,
image_url: String,
}
/// One-Time-Witness for the module.
public struct MY_HERO has drop {}
/// In the module initializer one claims the `Publisher` object
/// to then create a `Display`. The `Display` is initialized with
/// a set of fields (but can be modified later) and published via
/// the `update_version` call.
///
/// Keys and values are set in the initializer but could also be
/// set after publishing if a `Publisher` object was created.
fun init(otw: MY_HERO, ctx: &mut TxContext) {
let keys = vector[
utf8(b"name"),
utf8(b"link"),
utf8(b"image_url"),
utf8(b"description"),
utf8(b"project_url"),
utf8(b"creator"),
];
let values = vector[
// For `name` one can use the `Hero.name` property
utf8(b"{name}"),
// For `link` one can build a URL using an `id` property
utf8(b"https://iota-heroes.io/hero/{id}"),
// For `image_url` use an IPFS template + `image_url` property.
utf8(b"ipfs://{image_url}"),
// Description is static for all `Hero` objects.
utf8(b"A true Hero of the IOTA ecosystem!"),
// Project URL is usually static
utf8(b"https://iota-heroes.io"),
// Creator field can be any
utf8(b"Unknown IOTA Fan")
];
// Claim the `Publisher` for the package!
let publisher = package::claim(otw, ctx);
// Get a new `Display` object for the `Hero` type.
let mut display = display::new_with_fields<Hero>(
&publisher, keys, values, ctx
);
// Commit first version of `Display` to apply changes.
display::update_version(&mut display);
transfer::public_transfer(publisher, sender(ctx));
transfer::public_transfer(display, sender(ctx));
}
/// Anyone can mint their `Hero`!
public fun mint(name: String, image_url: String, ctx: &mut TxContext): Hero {
let id = object::new(ctx);
Hero { id, name, image_url }
}
}
Work with Object Display
The display::new<T>
call creates a Display
, either in a custom function or module initializer, or as part of a programmable transaction.
The following code sample demonstrates how to create a Display
:
module iota::display {
/// Get a new Display object for the `T`.
/// Publisher must be the publisher of the T, `from_package`
/// check is performed.
public fun new<T>(pub: &Publisher): Display<T> { /* ... */ }
}
After you create the Display
, you can modify it. The following code sample demonstrates how to modify a Display
:
module iota::display {
/// Sets multiple fields at once.
public fun add_multiple(
self: &mut Display,
keys: vector<String>,
values: vector<String>
) { /* ... */ }
/// Edit a single field.
public fun edit(self: &mut Display, key: String, value: String) { /* ... */ }
/// Remove a key from Display.
public fun remove(self: &mut Display, key: String ) { /* ... */ }
}
Next, the update_version
call applies the changes and sets the Display
for the T
by emitting an event. Full nodes receive the event and use the data in the event to retrieve a template for the type.
The following code sample demonstrates how to use the update_version
call:
module iota::display {
/// Update the version of Display and emit an event
public fun update_version(self: &mut Display) { /* ... */ }
}
IOTA utility objects
In IOTA, utility objects enable authorization for capabilities. Almost all modules have features that can be accessed only with the required capability. Generic modules allow one capability per application, such as a marketplace. Some capabilities mark ownership of a shared object on-chain, or access the shared data from another account. With capabilities, it is important to provide a meaningful description of objects to facilitate user interface implementation. This helps avoid accidentally transferring the wrong object when objects are similar. It also provides a user-friendly description of items that users see.
The following example demonstrates how to create a pet capability:
module pet::utility {
/// A capability which grants Pet Manager permission to add
/// new genes and manage the Pet Market.
public struct PetManagerCap has key, store {
id: UID
}
}
Typical objects with data duplication
A common case with in-game items is to have a large number of similar objects grouped by some criteria. It is important to optimize their size and the cost to mint and update them. Typically, a game uses a single source image or URL per group or item criteria. Storing the source image inside of every object is not optimal. In some cases, users mint in-game items when a game allows them or when they purchase an in-game item. To enable this, some IPFS/Arweave metadata must be created and stored in advance. This requires additional logic that is usually not related to the in-game properties of the item.
The following example demonstrates how to create a Pet:
module pet::pet_items {
/// A wearable Pet item. For some items there can be an
/// unlimited supply. And items with the same name are identical.
public struct PetItem has key, store {
id: UID,
name: String
}
}
Unique objects with dynamic representation
IOTA Pets use dynamic image generation. When a Pet is born, its attributes determine the Pet’s appearance, such as color or pattern. When a user puts an item on a Pet, the Pet’s appearance changes. When users put multiple items on a Pet, there’s a chance of a bonus for a combination of items.
To implement this, the Pet's game API service refreshes the image in response to a user-initiated change. The URL for a Pet is a template with the pet.id
. But storing the full URL - as well as other fields in the Pet object due to their diverse population - also leads to users paying for excess storage and increased gas fees.
The following example demonstrates how to implement dynamic image generation:
module pet::pet {
/// A Pet - very diverse object with different combination
/// of genes. Created dynamically. For images, a dynamic SVG
/// generation is used.
public struct Pet has key, store {
id: UID,
genes: vector<u8>
}
}
Objects with unique static content
This is the simplest scenario - an object represents everything itself. It is very easy to apply a metadata standard to an object of this kind, especially if the object stays immutable forever. However, if the metadata standard evolves and some ecosystem projects add new features for some properties, this object always stays in its original form and might require backward-compatible changes.
module iota::devnet_nft {
/// A Collectible with static data.
/// URL, name and description are set only once during minting.
public struct DevNetNFT has key, store {
id: UID,
name: String,
description: String,
url: Url,
}
}