An NFT is minted from a Collection. It's a unique digital asset. NFTs can be mutable after being created, but most often are not.
NFTs can have jsonlogic to affect
conditional rendering and multiple resources which can be
prioritized by the owner, and of which one or more can be based on a Base. An NFT can own
other NFTs through two computed properties: children
when parent, and owner
/ rootowner
when
child.
{
"collection": {
"type": "string",
"description": "Collection ID, e.g. 0aff6865bed3a66b-ZOMB"
},
"symbol": {
"type": "string",
"description": "e.g. ZOMBBLUE. Must be limited to alphanumeric characters, no dash (-). Underscore is allowed as word separator. E.g. LOVE-POTION is NOT allowed. LOVE_POTION is allowed."
},
"transferable": {
"type": "number",
"description": "If 1, item is transferable. If 0, item is not transferable (i.e. reputation token). If positive integer, item will be transferable from that BLOCK onward, e.g. 1400000 means item can be traded after block 1400000. If negative integer, item will be transferable for that many blocks and then become non-transferable."
},
"sn": {
"type": "string",
"description": "Serial number or issuance number of the NFT, padded so that its total length is 8, e.g. 00000123. Should be sequential but can be arbitrary - the `max` propery of an NFT's collection only cares about the total number of NFTs, not their serial numbers."
},
"metadata": {
"type": "string",
"description": "HTTP(s) or IPFS URI. If IPFS, MUST be in the format of ipfs://ipfs/HASH"
}
}
An NFT can define a map of properties. Properties (previously Attributes) defined this way override properties in the metadata.
Due to on-chain storage concerns and constraints, this will usually be reserved only for mutable properties.
Inspired by Enjin,
export type IProperties = Record<string, IAttribute>;
export interface IAttribute {
_mutation?: {
allowed: boolean;
with?: {
opType: OP_TYPES;
condition?: string;
};
};
type: "array" | "object" | "int" | "float" | "string";
value: any;
}
NFT-level properties like these override a Collection's on-chain properties.
Computed fields are fields that are used in interactions, but are not explicitly set on their entities. Computed fields are the result of applying a standardized calculation or merger formula to specific fields. The NFT entity has the following computed fields, to be provided by implementations:
{
"id": {
"type": "computed",
"description": "An NFT is uniquely identified by the combination of its minting block number, collection ID, its instance ID, and its serial number, e.g. 5193445-0aff6865bed3a66b-ZOMB-ZOMBBLUE-00000001."
},
"children": {
"type": Child[],
"description": "Array of Child objects"
},
"owner": {
"type": "string",
"description": "Either account which owns the NFT or NFT ID of NFT that owns this NFT. Computed from SEND interactions after the initial MINT."
},
"rootowner": {
"type": "string",
"description": "Account which owns the top-most NFT in this stack. Example, Bob owns nft-a which owns nft-b. Root owner of nft-b is Bob."
},
"resources": {
"type": Resource[],
"description": "An array of Resource objects, computed from RESADD interactions."
},
"priority": {
"type": "string[]",
"description": "An array of resource IDs. Changing this changes the order of default rendering. Example: ['skdlj','wyeiu']. Changes are made with SETPROPERTY, and any resource not explicitly mentioned in the priority list is implied to go to the end of the list."
},
"logic": {
"type": Logic[],
"description": "Set of Logic objects, computed from SETPROPERTY interactions."
}
}
Example id: 5105000-0aff6865bed3a66b-DLEP-DLEP15-00000001
.
When processing NFTs and their interactions, tools MUST explode the NFT by -
and if the number of
fragments is anything other than 5, the remark should be discarded as invalid:
- element 0 is the block in which the NFT was minted.
- elements 1 and 2 together make the collection ID.
- element 3 is the instance ID of the NFT (its symbol).
- element 4 is the serial number of the current NFT instance.
children
is an array of Child objects:
export interface NFTChild {
id: string;
equipped: string;
pending: boolean;
}
Example of fully consolidated children
object:
"children": [
{
"id": "5105000-0aff6865bed3a66b-DLEP-DL15-00000001",
"equipped": "",
"pending": false,
},
{
"id": "5105000-0aff6865bed3a66b-DLEP-DL15-00000002",
"equipped": "",
"pending": true,
},
{
"id": "5105000-0aff6865bed3a66b-DLEP-DL15-00000003",
"equipped": "base_1.slot_1",
"pending": false,
}
]
The equipped
property will change when computed from the EQUIP
interaction. It will will be composed of two dot-delimited values, like so:
"base-4477293-kanaria_superbird.machine_gun_scope"
. This means: the child is now equipped into
this slot. The value of baseslot
can change from ""
to
"base-4477293-kanaria_superbird.machine_gun_scope"
ONLY if one of this child NFT's resources has
this value as a slot
property (see resources
below) and additional explained in the
EQUIP interaction.
These values are originally computed from the MINT interaction, depending on recipient. After minting, it is computed from SEND interactions.
rootowner
will ALWAYS be an account, whereas owner
can also be an NFT ID in cases where an NFT
owns another NFT (see children
above). An implementing toolset MUST take into consideration the
bubbling-up of ownership checks, so that if Owner O owns NFT A which owns NFT B which owns NFT C and
O issues a BURN to NFT C, it should just work. This is where rootowner
check should be utilized.
Resources can only be added to an NFT with the RESADD interaction. They cannot be included during minting. This is to maintain a separation of concerns.
A resource object is defined as such:
{
"id": "nanoid-of-resource",
"base?": "base-uri",
"src?": "media-uri",
"metadata?": "metadata-uri",
"slot?": "baseslot",
"license?": "url-or-identifier",
"thumb?": "uri-of-thumbnail"
}
id
is a 5-character string of reasonable uniqueness. The combination of base ID and resource id
should be unique across the entire RMRK ecosystem which, if entropy of the Base ID is combined with
a pseudo-randomly generated nanoid of a resource, is easy to satisfy. For JS-based implementations,
we recommend using the nanoid package.
If the resource is a Base, the src
property is absent. Base should be a
Base ID computed field. In this case, the resource can also list the
parts of this base which the NFT implements. A Base lists all the parts an NFT can possibly be
composed of, and the NFT itself cherry-picks from that list of parts. If the list of parts is
omitted, then it is assumed the NFT is composed of all the parts of the base. Example:
"resources": [
{
"id": "V1StG",
"base": "some-base-id",
"parts": ["left_wing_front", "left_wing_back", "gem_slot_1"]
},
{
"id": "Z5i6B",
"src": "hash-of-guest-bird-art-file",
"metadata": "hash-of-metadata-with-credits"
}
]
The first resource references a base, and picks 3 parts from it - two appear to be fixed, and one
appears to be a slot. Now this NFT can equip other NFTs into the slot, and will also render
left_wing_front
and left_wing_back
.
The resource can also pick a Base's Theme, like so:
"resources": [
{
"id": "V1StG",
"base": "some-base-id",
"parts": ["left_wing_front", "left_wing_back", "gem_slot_1"],
"theme": "yellow"
}
]
If the theme
value is omitted, it is implied to be using the SVG's fallback values, included in
the SVG itself as data attributes. To learn more about themes, see the
THEMEADD interaction.
If the resource is Media, the base
property is absent. Media src
should be a URI like an IPFS
hash.
The license
field, if present, should contain a link to a license (IPFS or static HTTP url), or an
identifier, like RMRK_nocopy
or ipfs://ipfs/someHashOfLicense
. This is a license transfering
copyright to the current owner. If omitted, it is assumed to be Full rights with current owner,
meaning the owner can use the art for any commercial or non commercial activity. This use must cease
with a transfer of ownership, so using an NFT one intends to sell as a company logo is probably not
a good idea.
If the resource has the slot
property, it was designed to fit into a specific Base's slot. The
baseslot will be composed of two dot-delimited values, like so:
"base-4477293-kanaria_superbird.machine_gun_scope"
. This means: "This resource is compatible with
the machine_gun_scope
slot of base base-4477293-kanaria_superbird
. If the NFT with this resource
becomes a child of an NFT which has base-4477293-kanaria_superbird
as its base, it will be
equippable into that NFT (see children
above).
If the resource has the thumb
property, this will be a URI to a thumbnail of the given resource.
For example, if we have a composable NFT like a Kanaria bird, the
resource is complex and too detailed to show in a search-results page or a list. Also, if a bird
owns another bird, showing the full render of one bird inside the other's inventory might be a bit
of a strain on the browser. For this reason, the thumb
value can contain a URI to an image that is
lighter and faster to load but representative of this resource. In the case of the birds when the
resource is a base
, the thumb
could be an image of the whole Kanaria project, or the Kanaria
logo, or a plain bird. Then when this resource is inspected in full view (individual NFT view), the
full composable NFT is shown and the thumb
value is ignored. In short, thumb
is what's shown in
search result and inventory pages for then the given resource is at top
priority.
A resource can have a slot
property, which indicates which slot in a base
it's meant for. It can happen that a newly added resource targets the same slot on the same target
base, in which case the render needs to know which one to display as equipped into an NFT's base. In
cases like these, resource priority should be respected. Priority can be switched with the
SETPRIORITY
interaction.
As an example:
- a Kanaria bird has a
left_hand
slot on itsbase1
base - an NFT
excalibur
has a resource targetingbase1.left_hand
- if equipped, the bird shows the
excalibur
'sbase1.left_hand
resource in thebase1.left_hand
slot - the original artist notices a glitch, or there's a copyright issue on the original drawing of the
base1.left_hand
resource ofexcalibur
and the artist makes a new rendition - a new resource is added to
excalibur
targeting the samebase1.left_hand
slot - the armed bird still shows the old resource until the owner of the
excalibur
NFT callsSETPRIORITY
on thepriority
field, changing the priority of the rendering
The metadata of a Resource:
{
"description?": {
"type": "string",
"description": "Description of the resource. Markdown is supported."
},
"external_url?": {
"type": "string",
"description": "HTTP or IPFS URL for finding out more about this project or linking to its author. If IPFS, MUST be in the format of ipfs://ipfs/HASH"
},
"name?": {
"type": "string",
"description": "Name of resource."
}
}
Metadata is optional and should only be rendered and fetched by implementers on-demand. It is meant to serve more as "credits" than anything else, in cases when several people worked on different aspects of the same NFT project and different resources inside it.
Example of complete resources array:
"resources": [
{
"id": "V1StG",
"base": "hash-of-base-svg.json"
},
{
"id": "tGXR8",
"src": "hash-of-metadata-containing-guest-bird-art",
"slot": "base-4477293-kanaria_superbird.wing_1_slot"
},
{
"id": "Z5i6B",
"src": "hash-of-metadata-guest-bird-art-with-jetpack",
"metadata": "hash-of-metadata-with-credits"
}
]
Priority defines the order in which resources are loaded. This is very useful when an NFT has
several resources of the same type, but wants one to be the default. For example, a
Kanaria bird has a secondary and primary artwork. One is a resource with
ID furio
, the other wyqyc
. By changing priority to ['wyqyc'], the second resource is rendered
before the first in various visual applications like marketplaces and galleries.
"priority": ["wyqyc", "furio"]
Because omitted resources are implied to go to the end of the list, the following is also valid:
"priority": ["wyqyc"]
A user can change the priority order by using the SETPRIORITY
interaction. Example, to change priority of resource loading on NFT
438637-0aff6865bed3a66b-KANS-oiyhi24yr28i7g4f
from ["wyqyc", "furio"]
to ["furio", "wyqyc"]
:
rmrk::SETPRIORITY::2.0.0::438637-0aff6865bed3a66b-KANS-oiyhi24yr28i7g4f::%5B%22wyqyc%22%2C%20%22furio%22%5D
TBD
The same as Opensea's but with Enjin's inspired attribute format.
{
"external_url": {
"type": "string",
"description": "HTTP or IPFS URL for finding out more about this project. If IPFS, MUST be in the format of ipfs://ipfs/HASH"
},
"image": {
"type": "string",
"description": "HTTP or IPFS URL to project's main image, in the vein of og:image. If IPFS, MUST be in the format of ipfs://ipfs/HASH"
},
"image_data": {
"type": "string?",
"description": "[OPTIONAL] Use only if you don't have the image field (they are mutually exclusive and image takes precedence). Raw base64 or SVG data for the image. If SVG, MUST start with <svg, if base64, MUST start with base64:"
},
"description": {
"type": "string",
"description": "Description of the nft. Markdown is supported."
},
"name": {
"type": "string",
"description": "Name of the NFT instance."
},
"properties": {
"type": "array",
"description": "A Map of JSON objects, inspired by Enjin's: https://github.com/ethereum/EIPs/blob/master/EIPS/eip-1155.md#erc-1155-metadata-uri-json-schema"
},
"background_color": {
"type": "string",
"description": "Background color of the item. Must be a six-character hexadecimal without #."
},
"animation_url": {
"type": "string",
"description": "HTTP or IPFS URL (format MUST be ipfs://ipfs/HASH) for an animated image of the item. GLTF, GLB, WEBM, MP4, M4V, and OGG are supported, and when using IPFS type MUST be appended, separated by colon, e.g. ipfs://ipfs/SOMEHASH:webm."
},
"youtube_url": {
"type": "string",
"description": "URL to Youtube video about this NFT"
}
}
NFT:
{
"collection": "0aff6865bed3a66b-DLEP",
"transferable": 1,
"sn": "00000001",
"metadata": "ipfs://ipfs/QmavoTVbVHnGEUztnBT2p3rif3qBPeCfyyUE5v4Z7oFvs4"
}
Metadata:
{
"external_url": "https://rmrk.app/registry/0aff6865bed3a66b-DLEP",
"image": "ipfs://ipfs/QmSY3VzdNdAphEs51GW9QMAUotaX3Rf6WeGQkvPPVhEQ3B",
"description": "Everyone who promoted Dot Leap via the in-email link in edition 15",
"name": "DL15",
"properties": {},
"background_color": "ffffff"
}
NFT:
{
"collection": "0aff6865bed3a66b-DLEP",
"transferable": 1,
"sn": "00000001",
"metadata": "ipfs://ipfs/QmR3EB16GANjYbT82jueyMbv7ewrwVAogmB4fgbtUrRPLb"
}
Metadata:
{
"external_url": "https://rmrk.app/registry/0aff6865bed3a66b-DLEP",
"image": "ipfs://ipfs/QmaCxd3omNNvjeVvzgn5gSjARDR4442WBtBAcZN7xEddeL",
"description": "Everyone who promoted Dot Leap via the in-email link in edition 16",
"name": "DL16",
"properties": {
"nickname": {
"_mutation": {
"allowed": true,
"with": {
"condition": "d43593c715a56da27d-KANARIAGEMS",
"opType": "BURN"
}
},
"type": "string",
"value": "foo"
}
},
"background_color": "ffffff"
}
Properties:
export type IProperties = Record<string, IAttribute>;
export interface IAttribute {
_mutation?: {
allowed: boolean;
with?: {
opType: OP_TYPES;
condition?: string;
};
};
type: "array" | "object" | "int" | "float" | "string";
value: any;
}