SNIP-722 - Extension of SNIP-721 for Badges, POAPs, and Non-Transferable NFTs
This document describes several optional enhancements to the SNIP-721 specification. Contracts that support the existing SNIP-721 standard are still considered SNIP-721 compliant. Clients should be written so as to benefit from these SNIP-722 features when available, but provide fallbacks for when these features are not available.
The features specified in this document enable a SNIP-722 compliant contract to be used for badges and POAPs as well as non-transferable tokens.
-
Queries
-
Messages
Queries
The primary update for Badges/POAPs is the addition of a token_subtype
field in the Metadata Extension struct. This field is intended to be used by applications in order to differentiate NFTs that are used as Badges/POAPs so that they can be displayed as such because they will be used for things like trophies, achievements, proof of attendence, etc... An example of the definition of Metadata and Extension as used by Stashh is included below.
This is the metadata for a token that follows CW-721 metadata specification, which is based on ERC721 Metadata JSON Schema. The reference implementation will throw an error if both token_uri
and extension
are provided.
{
"token_uri": "optional_uri_pointing_to_off-chain_JSON_metadata",
"extension": {
"...": "..."
}
}
Name | Type | Description | Optional | Value If Omitted |
---|---|---|---|---|
token_uri | string | Uri pointing to off-chain JSON metadata | yes | nothing |
extension | Extension (see below) | Data structure defining on-chain metadata | yes | nothing |
The reference implementation will throw an error if both token_uri
and extension
are provided.
This is an on-chain metadata extension struct that conforms to the Stashh metadata standard (which in turn implements https://docs.opensea.io/docs/metadata-standards). Urls should be prefixed with http://
, https://
, ipfs://
, or ar://
.
{
"image": "optional_image_url",
"image_data": "optional_raw_svg_image_data",
"external_url": "optional_url_to_view_token_on_your_site",
"description": "optional_token_description",
"name": "optional_token_name",
"attributes": [
{
"display_type": "optional_display_format_for_numerical_traits",
"trait_type": "optional_name_of_the_trait",
"value": "trait value",
"max_value": "optional_max_value_for_numerical_traits"
},
{
"...": "...",
},
],
"background_color": "optional_six-character_hexadecimal_background_color_(without_pre-pended_`#`)",
"animation_url": "optional_url_to_multimedia_file",
"youtube_url": "optional_url_to_a_YouTube_video",
"media": [
{
"file_type": "optional_file_type",
"extension": "optional_file_extension",
"authentication": {
"key": "optional_decryption_key_or_password",
"user": "optional_username_for_authentication"
},
"url": "url_pointing_to_the_multimedia_file"
},
{
"...": "...",
},
],
"protected_attributes": [ "list", "of_attributes", "whose_types", "are_public", "but_values", "are_private" ],
"token_subtype": "badge"
}
Name | Type | Description | Optional | Value If Omitted |
---|---|---|---|---|
image | string | Url to the token's image | yes | nothing |
image_data | string | Raw SVG image data that should only be used if there is no image field |
yes | nothing |
external_url | string | Url to view the token on your site | yes | nothing |
description | string | Text description of the token | yes | nothing |
name | string | Name of the token | yes | nothing |
attributes | array of Trait (see below) | Token's attributes | yes | nothing |
background_color | string | Background color represented as a six-character hexadecimal without a pre-pended # | yes | nothing |
animation_url | string | Url to a multimedia file | yes | nothing |
youtube_url | string | Url to a YouTube video | yes | nothing |
media | array of MediaFile (see below) | List of multimedia files using Stashh specifications | yes | nothing |
protected_attributes | array of string | List of attributes whose types are public but whose values are private | yes | nothing |
token_subtype | string | token subtype used to signify what the NFT is used for, such as "badge" | yes | nothing |
Trait describes a token attribute as defined in https://docs.opensea.io/docs/metadata-standards.
{
"display_type": "optional_display_format_for_numerical_traits",
"trait_type": "optional_name_of_the_trait",
"value": "trait value",
"max_value": "optional_max_value_for_numerical_traits"
}
Name | Type | Description | Optional | Value If Omitted |
---|---|---|---|---|
display_type | string | Display format for numerical traits | yes | nothing |
trait_type | string | Name of the trait | yes | nothing |
value | string | Trait value | no | |
max_value | string | Maximum value for this numerical trait | yes | nothing |
MediaFile is the data structure used by Stashh to reference off-chain multimedia files. It allows for hosted files to be encrypted or authenticated with basic authentication, and for the decryption key or username/password to also be included in the on-chain private metadata. Urls should be prefixed with http://
, https://
, ipfs://
, or ar://
.
{
"file_type": "optional_file_type",
"extension": "optional_file_extension",
"authentication": {
"key": "optional_decryption_key_or_password",
"user": "optional_username_for_authentication"
},
"url": "url_pointing_to_the_multimedia_file"
}
Name | Type | Description | Optional | Value If Omitted |
---|---|---|---|---|
file_type | string | File type. Stashh currently uses: "image", "video", "audio", "text", "font", "application" | yes | nothing |
extension | string | File extension | yes | nothing |
authentication | Authentication (see below) | Credentials or decryption key for a protected file | yes | nothing |
url | string | Url to the multimedia file | no |
Authentication is used to provide the decryption key or username/password for protected files.
{
"key": "optional_decryption_key_or_password",
"user": "optional_username_for_authentication"
}
Name | Type | Description | Optional | Value If Omitted |
---|---|---|---|---|
key | string | Decryption key or password | yes | nothing |
user | string | Username for basic authentication | yes | nothing |
ImplementsTokenSubtype indicates whether the contract implements the token_subtype
Extension field. Because legacy SNIP-721 contracts do not implement this query and do not implement token subtypes, any use of this query should always check for an error response, and if the response is an error, it can be considered that the contract does not implement subtypes. Because message parsing ignores input fields that a contract does not expect, this query should be used before attempting a message that uses the token_subtype
Extension field. If the message is sent to a SNIP-721 contract that does not implement token_subtype
, that field will just be ignored and the resulting NFT will still be created/updated, but without a token_subtype
.
{
"implements_token_subtype": {}
}
{
"implements_token_subtype": {
"is_enabled": true | false
}
}
Name | Type | Description | Optional |
---|---|---|---|
is_enabled | bool | True if the contract implements token subtypes | no |
Non-transferable tokens are NFTs that can never have an owner other than the address it was minted to. In order to provide the owner of a non-transferable token a way to get rid of an unwanted NFT, non-transferable tokens must always have the ability to be burned (see here). Also, it should be noted that setting royalties for a non-transferable token has no purpose, because it can never be transferred as part of a sale. Because the intent of VerifyTransferApproval is to provide contracts a way to know before-hand whether an attempt to transfer tokens will fail, the reference implementation has modified VerifyTransferApproval to consider any non-transferable token as unapproved for transfer.
The following are examples of how the reference implementation of non-transferable tokens provides the ability to specify whether an NFT should be transferable at the time it is minted.
MintNft mints a single token.
{
"mint_nft": {
"token_id": "optional_ID_of_new_token",
"owner": "optional_address_the_new_token_will_be_minted_to",
"public_metadata": {
"token_uri": "optional_uri_pointing_to_off-chain_JSON_metadata",
"extension": {
"...": "..."
}
},
"private_metadata": {
"token_uri": "optional_uri_pointing_to_off-chain_JSON_metadata",
"extension": {
"...": "..."
}
},
"serial_number": {
"mint_run": 3,
"serial_number": 67,
"quantity_minted_this_run": 1000,
},
"royalty_info": {
"decimal_places_in_rates": 4,
"royalties": [
{
"recipient": "address_that_should_be_paid_this_royalty",
"rate": 100,
},
{
"...": "..."
}
],
},
"transferable": true | false,
"memo": "optional_memo_for_the_mint_tx",
"padding": "optional_ignored_string_that_can_be_used_to_maintain_constant_message_length"
}
}
Name | Type | Description | Optional | Value If Omitted |
---|---|---|---|---|
token_id | string | Identifier for the token to be minted | yes | minting order number |
owner | string (HumanAddr) | Address of the owner of the minted token | yes | env.message.sender |
public_metadata | Metadata (see above) | The metadata that is publicly viewable | yes | nothing |
private_metadata | Metadata (see above) | The metadata that is viewable only by the token owner and addresses the owner has whitelisted | yes | nothing |
serial_number | SerialNumber | The SerialNumber for this token | yes | nothing |
royalty_info | RoyaltyInfo | RoyaltyInfo for this token | yes | default RoyaltyInfo |
transferable | bool | True if the minted token should be transferable | yes | true |
memo | string | memo for the mint tx that is only viewable by addresses involved in the mint (minter, owner) |
yes | nothing |
padding | string | An ignored string that can be used to maintain constant message length | yes | nothing |
Setting royalties for a non-transferable token has no purpose, because it can never be transferred as part of a sale.
{
"mint_nft": {
"token_id": "ID_of_minted_token",
}
}
The ID of the minted token should also be returned in a LogAttribute with the key minted
.
BatchMintNft mints a list of tokens.
{
"batch_mint_nft": {
"mints": [
{
"token_id": "optional_ID_of_new_token",
"owner": "optional_address_the_new_token_will_be_minted_to",
"public_metadata": {
"token_uri": "optional_uri_pointing_to_off-chain_JSON_metadata",
"extension": {
"...": "..."
}
},
"private_metadata": {
"token_uri": "optional_uri_pointing_to_off-chain_JSON_metadata",
"extension": {
"...": "..."
}
},
"serial_number": {
"mint_run": 3,
"serial_number": 67,
"quantity_minted_this_run": 1000,
},
"royalty_info": {
"decimal_places_in_rates": 4,
"royalties": [
{
"recipient": "address_that_should_be_paid_this_royalty",
"rate": 100,
},
{
"...": "..."
}
],
},
"transferable": true | false,
"memo": "optional_memo_for_the_mint_tx"
},
{
"...": "..."
}
],
"padding": "optional_ignored_string_that_can_be_used_to_maintain_constant_message_length"
}
}
Name | Type | Description | Optional | Value If Omitted |
---|---|---|---|---|
mints | array of Mint (see below) | A list of all the mint operations to perform | no | |
padding | string | An ignored string that can be used to maintain constant message length | yes | nothing |
{
"batch_mint_nft": {
"token_ids": [
"IDs", "of", "tokens", "that", "were", "minted", "..."
]
}
}
The IDs of the minted tokens should also be returned in a LogAttribute with the key minted
.
The Mint object defines the data necessary to mint one token.
{
"token_id": "optional_ID_of_new_token",
"owner": "optional_address_the_new_token_will_be_minted_to",
"public_metadata": {
"token_uri": "optional_uri_pointing_to_off-chain_JSON_metadata",
"extension": {
"...": "..."
}
},
"private_metadata": {
"token_uri": "optional_uri_pointing_to_off-chain_JSON_metadata",
"extension": {
"...": "..."
}
},
"serial_number": {
"mint_run": 3,
"serial_number": 67,
"quantity_minted_this_run": 1000,
},
"royalty_info": {
"decimal_places_in_rates": 4,
"royalties": [
{
"recipient": "address_that_should_be_paid_this_royalty",
"rate": 100,
},
{
"...": "..."
}
],
},
"transferable": true | false,
"memo": "optional_memo_for_the_mint_tx"
}
Name | Type | Description | Optional | Value If Omitted |
---|---|---|---|---|
token_id | string | Identifier for the token to be minted | yes | minting order number |
owner | string (HumanAddr) | Address of the owner of the minted token | yes | env.message.sender |
public_metadata | Metadata (see above) | The metadata that is publicly viewable | yes | nothing |
private_metadata | Metadata (see above) | The metadata that is viewable only by the token owner and addresses the owner has whitelisted | yes | nothing |
serial_number | SerialNumber | The SerialNumber for this token | yes | nothing |
royalty_info | RoyaltyInfo | RoyaltyInfo for this token | yes | default RoyaltyInfo |
transferable | bool | True if the minted token should be transferable | yes | true |
memo | string | memo for the mint tx that is only viewable by addresses involved in the mint (minter, owner) |
yes | nothing |
Setting royalties for a non-transferable token has no purpose, because it can never be transferred as part of a sale.
SNIP-722 adds a transferable
field to the NftDossier response of SNIP-721. NftDossier returns all the information about a token that the viewer is permitted to view. If no viewer is provided, NftDossier will only display the information that has been made public. The response may include the owner, the public metadata, the private metadata, the reason the private metadata is not viewable, the royalty information, the mint run information, whether the token is transferable, whether ownership is public, whether the private metadata is public, and (if the querier is the owner,) the approvals for this token as well as the inventory-wide approvals for the owner. The implementation may choose to hide royalty recipient addresses. See here for a description of how the reference implementation determines who is permitted to view royalty recipient addresses.
{
"nft_dossier": {
"token_id": "ID_of_the_token_being_queried",
"viewer": {
"address": "address_of_the_querier_if_supplying_optional_ViewerInfo",
"viewing_key": "viewer's_key_if_supplying_optional_ViewerInfo"
},
"include_expired": true | false
}
}
Name | Type | Description | Optional | Value If Omitted |
---|---|---|---|---|
token_id | string | ID of the token being queried | no | |
viewer | ViewerInfo | The address and viewing key performing this query | yes | nothing |
include_expired | bool | True if expired approvals should be included in the response | yes | false |
{
"nft_dossier": {
"owner": "address_of_the_token_owner",
"public_metadata": {
"token_uri": "optional_uri_pointing_to_off-chain_JSON_metadata",
"extension": {
"...": "..."
}
},
"private_metadata": {
"token_uri": "optional_uri_pointing_to_off-chain_JSON_metadata",
"extension": {
"...": "..."
}
},
"display_private_metadata_error": "optional_error_describing_why_private_metadata_is_not_viewable_if_applicable",
"royalty_info": {
"decimal_places_in_rates": 4,
"royalties": [
{
"recipient": "optional_address_that_should_be_paid_this_royalty",
"rate": 100,
},
{
"...": "..."
}
],
},
"mint_run_info": {
"collection_creator": "optional_address_that_instantiated_this_contract",
"token_creator": "optional_address_that_minted_this_token",
"time_of_minting": 999999,
"mint_run": 3,
"serial_number": 67,
"quantity_minted_this_run": 1000,
},
"transferable": true | false,
"owner_is_public": true | false,
"public_ownership_expiration": "never" | {"at_height": 999999} | {"at_time":999999},
"private_metadata_is_public": true | false,
"private_metadata_is_public_expiration": "never" | {"at_height": 999999} | {"at_time":999999},
"token_approvals": [
{
"address": "whitelisted_address",
"view_owner_expiration": "never" | {"at_height": 999999} | {"at_time":999999},
"view_private_metadata_expiration": "never" | {"at_height": 999999} | {"at_time":999999},
"transfer_expiration": "never" | {"at_height": 999999} | {"at_time":999999},
},
{
"...": "..."
}
],
"inventory_approvals": [
{
"address": "whitelisted_address",
"view_owner_expiration": "never" | {"at_height": 999999} | {"at_time":999999},
"view_private_metadata_expiration": "never" | {"at_height": 999999} | {"at_time":999999},
"transfer_expiration": "never" | {"at_height": 999999} | {"at_time":999999},
},
{
"...": "..."
}
]
}
}
Name | Type | Description | Optional |
---|---|---|---|
owner | string (HumanAddr) | Address of the token's owner | yes |
public_metadata | Metadata (see above) | The token's public metadata | yes |
private_metadata | Metadata (see above) | The token's private metadata | yes |
display_private_metadata_error | string | If the private metadata is not displayed, the corresponding error message | yes |
royalty_info | RoyaltyInfo | The token's RoyaltyInfo | yes |
mint_run_info | MintRunInfo | The token's MintRunInfo | yes |
transferable | bool | True if this token is transferable | no* |
owner_is_public | bool | True if ownership is public for this token | no |
public_ownership_expiration | Expiration | When public ownership expires for this token. Can be a blockheight, time, or never | yes |
private_metadata_is_public | bool | True if private metadata is public for this token | no |
private_metadata_is_public_expiration | Expiration | When public display of private metadata expires. Can be a blockheight, time, or never | yes |
token_approvals | array of Snip721Approval | List of approvals for this token | yes |
inventory_approvals | array of Snip721Approval | List of inventory-wide approvals for the token's owner | yes |
The transferable
field is mandatory for SNIP-722 compliant contracts, but because SNIP-722 is an optional extension to SNIP-721, any NftDossier response that does not include the field can be considered to come from a contract that only implements transferable tokens (considered equivalent to transferable
= true)
IsTransferable indicates whether the token is transferable. This query is not authenticated.
{
"is_transferable": {
"token_id": "ID_of_the_token_whose_transferability_is_being_queried"
}
}
Name | Type | Description | Optional | Value If Omitted |
---|---|---|---|---|
token_id | string | The ID of the token whose transferability is being queried | no |
{
"is_transferable": {
"token_is_transferable": true | false
}
}
Name | Type | Description | Optional |
---|---|---|---|
token_is_transferable | bool | True if the token is transferable | no |
ImplementsNonTransferableTokens indicates whether the contract implements non-transferable tokens. Because legacy SNIP-721 contracts do not implement this query and do not implement non-transferable tokens, any use of this query should always check for an error response, and if the response is an error, it can be considered that the contract does not implement non-transferable tokens. Because message parsing ignores input fields that a contract does not expect, this query should be used before attempting to mint a non-transferable token. If the message is sent to a SNIP-721 contract that does not implement non-transferable tokens, the transferable
field will just be ignored and the resulting NFT will still be created, but will always be transferable.
{
"implements_non_transferable_tokens": {}
}
{
"implements_non_transferable_tokens": {
"is_enabled": true | false
}
}
Name | Type | Description | Optional |
---|---|---|---|
is_enabled | bool | True if the contract implements non-transferable tokens | no |