Skip to main content

Metadata

NEP-177

Version 2.1.0

Summary

An interface for a non-fungible token's metadata. The goal is to keep the metadata future-proof as well as lightweight. This will be important to dApps needing additional information about an NFT's properties, and broadly compatible with other token standards such that the NEAR Rainbow Bridge can move tokens between chains.

Motivation

The primary value of non-fungible tokens comes from their metadata. While the core standard provides the minimum interface that can be considered a non-fungible token, most artists, developers, and dApps will want to associate more data with each NFT, and will want a predictable way to interact with any NFT's metadata.

NEAR's unique storage staking approach makes it feasible to store more data on-chain than other blockchains. This standard leverages this strength for common metadata attributes, and provides a standard way to link to additional offchain data to support rapid community experimentation.

This standard also provides a spec version. This makes it easy for consumers of NFTs, such as marketplaces, to know if they support all the features of a given token.

Prior art:

Interface

Metadata applies at both the contract level (NFTContractMetadata) and the token level (TokenMetadata). The relevant metadata for each:

type NFTContractMetadata = {
spec: string, // required, essentially a version like "nft-2.0.0", replacing "2.0.0" with the implemented version of NEP-177
name: string, // required, ex. "Mochi Rising — Digital Edition" or "Metaverse 3"
symbol: string, // required, ex. "MOCHI"
icon: string|null, // Data URL
base_uri: string|null, // Centralized gateway known to have reliable access to decentralized storage assets referenced by `reference` or `media` URLs
reference: string|null, // URL to a JSON file with more info
reference_hash: string|null, // Base64-encoded sha256 hash of JSON from reference field. Required if `reference` is included.
}

type TokenMetadata = {
title: string|null, // ex. "Arch Nemesis: Mail Carrier" or "Parcel #5055"
description: string|null, // free-form description
media: string|null, // URL to associated media, preferably to decentralized, content-addressed storage
media_hash: string|null, // Base64-encoded sha256 hash of content referenced by the `media` field. Required if `media` is included.
copies: number|null, // number of copies of this set of metadata in existence when token was minted.
issued_at: number|null, // When token was issued or minted, Unix epoch in milliseconds
expires_at: number|null, // When token expires, Unix epoch in milliseconds
starts_at: number|null, // When token starts being valid, Unix epoch in milliseconds
updated_at: number|null, // When token was last updated, Unix epoch in milliseconds
extra: string|null, // anything extra the NFT wants to store on-chain. Can be stringified JSON.
reference: string|null, // URL to an off-chain JSON file with more info.
reference_hash: string|null // Base64-encoded sha256 hash of JSON from reference field. Required if `reference` is included.
}

A new function MUST be supported on the NFT contract:

function nft_metadata(): NFTContractMetadata {}

A new attribute MUST be added to each Token struct:

 type Token = {
token_id: string,
owner_id: string,
+ metadata: TokenMetadata,
}

An implementing contract MUST include the following fields on-chain

  • spec: a string that MUST be formatted nft-n.n.n where "n.n.n" is replaced with the implemented version of this Metadata spec: for instance, "nft-2.0.0" to indicate NEP-177 version 2.0.0. This will allow consumers of the Non-Fungible Token to know which set of metadata features the contract supports.
  • name: the human-readable name of the contract.
  • symbol: the abbreviated symbol of the contract, like MOCHI or MV3
  • base_uri: Centralized gateway known to have reliable access to decentralized storage assets referenced by reference or media URLs. Can be used by other frontends for initial retrieval of assets, even if these frontends then replicate the data to their own decentralized nodes, which they are encouraged to do.

An implementing contract MAY include the following fields on-chain

For NFTContractMetadata:

  • icon: a small image associated with this contract. Encouraged to be a data URL, to help consumers display it quickly while protecting user data. Recommendation: use optimized SVG, which can result in high-resolution images with only 100s of bytes of storage cost. (Note that these storage costs are incurred to the contract deployer, but that querying these icons is a very cheap & cacheable read operation for all consumers of the contract and the RPC nodes that serve the data.) Recommendation: create icons that will work well with both light-mode and dark-mode websites by either using middle-tone color schemes, or by embedding media queries in the SVG.
  • reference: a link to a valid JSON file containing various keys offering supplementary details on the token. Example: /ipfs/QmdmQXB2mzChmMeKY47C43LxUdg1NDJ5MWcKMKxDu7RgQm, https://example.com/token.json, etc. If the information given in this document conflicts with the on-chain attributes, the values in reference shall be considered the source of truth.
  • reference_hash: the base64-encoded sha256 hash of the JSON file contained in the reference field. This is to guard against off-chain tampering.

For TokenMetadata:

  • title: The name of this specific token.
  • description: A longer description of the token.
  • media: URL to associated media. Preferably to decentralized, content-addressed storage.
  • media_hash: the base64-encoded sha256 hash of content referenced by the media field. This is to guard against off-chain tampering.
  • copies: The number of tokens with this set of metadata or media known to exist at time of minting.
  • issued_at: Unix epoch in milliseconds when token was issued or minted (an unsigned 32-bit integer would suffice until the year 2106)
  • expires_at: Unix epoch in milliseconds when token expires
  • starts_at: Unix epoch in milliseconds when token starts being valid
  • updated_at: Unix epoch in milliseconds when token was last updated
  • extra: anything extra the NFT wants to store on-chain. Can be stringified JSON.
  • reference: URL to an off-chain JSON file with more info.
  • reference_hash: Base64-encoded sha256 hash of JSON from reference field. Required if reference is included.

No incurred cost for core NFT behavior

Contracts should be implemented in a way to avoid extra gas fees for serialization & deserialization of metadata for calls to nft_* methods other than nft_metadata or nft_token. See near-contract-standards implementation using LazyOption as a reference example.

Drawbacks

  • When this NFT contract is created and initialized, the storage use per-token will be higher than an NFT Core version. Frontends can account for this by adding extra deposit when minting. This could be done by padding with a reasonable amount, or by the frontend using the RPC call detailed here that gets genesis configuration and actually determine precisely how much deposit is needed.
  • Convention of icon being a data URL rather than a link to an HTTP endpoint that could contain privacy-violating code cannot be done on deploy or update of contract metadata, and must be done on the consumer/app side when displaying token data.
  • If on-chain icon uses a data URL or is not set but the document given by reference contains a privacy-violating icon URL, consumers & apps of this data should not naïvely display the reference version, but should prefer the safe version. This is technically a violation of the "reference setting wins" policy described above.

Future possibilities

  • Detailed conventions that may be enforced for versions.
  • A fleshed out schema for what the reference object should contain.

Errata

  • 2022-02-03: updated Token struct field names. id was changed to token_id. This is to be consistent with current implementations of the standard and the rust SDK docs.

The first version (1.0.0) had confusing language regarding the fields:

  • issued_at
  • expires_at
  • starts_at
  • updated_at

It gave those fields the type string|null but it was unclear whether it should be a Unix epoch in milliseconds or ISO 8601. Upon having to revisit this, it was determined to be the most efficient to use epoch milliseconds as it would reduce the computation on the smart contract and can be derived trivially from the block timestamp.