SNIP-721: Private, Non-Fungible Tokens (NFTs)
SNIP-721 is a specification for private non-fungible tokens based on CosmWasm on the Secret Network. The name and design is loosely based on ERC-721, and is a superset of CosmWasm's CW-721. While this specification is CW-721 compliant, because CW-721 is not capable of privacy, a number of the CW-721-compliant functions may not return all the information a CW-721 implementation would. For example, the OwnerOf query must not display the approvals for a token unless the token owner has supplied his address and viewing key. In order to strive for CW-721 compliance, a number of queries that require authentication use optional parameters that the CW-721 counterpart does not have. If the optional authentication parameters are not supplied, the responses must only display information that the token owner has made public.
The SNIP-721 reference implementation may be used to create SNIP-721-compliant token contracts either as-is or as a base upon which to build additional application-specific functionality.
This specification is split into multiple sections, a contract may only implement some of this functionality, but must implement the base functionality.
This document aims to set standard interfaces that SNIP-721 contract implementors will create, and that both wallet implementors & dependent contract creators will consume. For this reason, the focus of this document is to merely give SNIP-721 contract implementors the tools needed to create contracts that fully maintain privacy but not to specify implementation details. That said, this document may, at times, mention implementation details of the SNIP-721 reference implementation as non-base functionality that developers might choose to mirror.
- Message - This is an on-chain interface. It is triggered by sending a transaction, and receiving an on-chain response which is read by the client. Messages are authenticated both by the blockchain, and by the secret enclave.
- Query - This is an off-chain interface. Queries are done by returning data that a node has locally, and are not public. Query responses are returned immediately, and do not have to wait for blocks.
- Cosmos Message Sender - The account that is found under the
sender
field in a standard Cosmos SDK message. This is also the signer of the message.
Users may want to include private memos with transactions. While it is possible to include a memo with the Cosmos message, that message is publicly viewable. Therefore, to enable private memos, SNIP-721 token contracts must allow an optional
memo
field in any message that generates a mint, burn, or transfer transaction.Users may want to enforce constant length messages to avoid leaking data. To support this functionality, SNIP-721 token contracts must support the option to include a
padding
field in every message. This optional padding
field may be sent with any of the messages in this spec. Contracts must ignore this field if sent.Requests should be sent as base64 encoded JSON. Future versions of Secret Network may add support for other formats as well, but at this time we recommend usage of JSON only. For this reason the parameter descriptions specify the JSON type which must be used. In addition, request parameters will include in parentheses a CosmWasm (or other) underlying type that this value must conform to. E.g. a recipient address is sent as a string, but must also be parsed to a bech32 address.
Queries are off-chain requests that are not cryptographically validated. This means that contracts that wish to validate the caller of a query must implement some sort of authentication. SNIP-721 uses an "API key" scheme, which validates a
(viewing key, account)
pair.Authentication must happen on each query that reveals private account-specific information. Authentication must be a resource intensive operation that takes a significant amount of time to compute. This is because such queries are open to offline brute-force attacks, which can be parallelized to scale linearly with the resources of a motivated attacker. Authentication must perform the same computation even if the user does not have a viewing key set. The authentication response must be indistinguishable for both the case of a wrong viewing key and the case of a non-existent viewing key.
One should be aware that the current blockheight and time is not available to a query on Secret Network at this moment, but there are plans to make the BlockInfo available to queries in a future hardfork. To get around this limitation, the SNIP-721 contract may choose to store the BlockInfo every time a message is executed, in order to use the blockheight and time of the last message execution when checking the expiration of an approval during a query. Therefore it is possible that a whitelisted address may be able to view the owner or metadata of a token past its approval expiration if no one executed any contract message since before the expiration.
Unless otherwise specified, all message & query responses will be JSON encoded in the
data
field of the Cosmos response, rather than in the logs
. This is meant to reduce the potential for data-leakage through side-channel attacks. In addition, since all keys will be encrypted, it is not possible to use the log
events for event triggering.Some of the messages detailed in this document contain a
status
field. This field must hold one of two values: "success" or "failure".While errors during execution of contract functions should usually result in a proper and detailed error response, The "failure" status is reserved for cases where a contract might choose to obfuscate the exact cause of failure, or otherwise indicate that while nothing failed to happen, the operation itself could not be completed for some valid reason.
This handles ownership, transfers, approvals, and metadata. These messages and queries must be consistently supported by all SNIP-721 contracts; however, all metadata response fields are optional to allow for SNIP-721 contracts that choose not to implement metadata. Note that all tokens must have an owner as well as an ID. The ID is an arbitrary string, unique within the contract.
TransferNft is used to transfer ownership of the token to the
recipient
address. This requires a valid token_id
and the message sender must either be the owner or an address with valid transfer approval. If the token is transferred to a new owner, its single-token approvals must be cleared.Request
{
"transfer_nft": {
"recipient": "address_receiving_the_token",
"token_id": "ID_of_the_token_being_transferred",
"memo": "optional_memo_for_the_transfer_tx",
"padding": "optional_ignored_string_that_can_be_used_to_maintain_constant_message_length"
}
}
Name | Type | Description | Optional | Value If Omitted |
---|---|---|---|---|
recipient | string (HumanAddr) | Address receiving the token | no | |
token_id | string | Identifier of the token to be transferred | no | |
memo | string | memo for the transfer transaction that is only viewable by addresses involved in the transfer (recipient, sender, previous owner) | yes | nothing |
padding | string | An ignored string that can be used to maintain constant message length | yes | nothing |
Response
{
"transfer_nft": {
"status": "success"
}
}
SendNft is used to transfer ownership of the token to the
contract
address, and then call the recipient's BatchReceiveNft (or ReceiveNft, see below) if the recipient contract has registered its receiver interface with the NFT contract or if its ReceiverInfo is provided. While SendNft keeps the contract
field name in order to maintain CW-721 compliance, Secret Network does not have the same limitations as Cosmos, and it is possible to use SendNft to transfer token ownership to a personal address (not a contract) or to a contract that does not implement any Receiver Interface.SendNft requires a valid
token_id
and the message sender must either be the owner or an address with valid transfer approval. If the token is transferred to a new owner, its single-token approvals must be cleared. If the BatchReceiveNft (or ReceiveNft) callback fails, the entire transaction must be reverted (even the transfer must not take place).Request
{
"send_nft": {
"contract": "address_receiving_the_token",
"receiver_info": {
"recipient_code_hash": "code_hash_of_the_recipient_contract",
"also_implements_batch_receive_nft": true | false,
},
"token_id": "ID_of_the_token_being_transferred",
"msg": "optional_base64_encoded_Binary_message_sent_with_the_BatchReceiveNft_callback",
"memo": "optional_memo_for_the_transfer_tx",
"padding": "optional_ignored_string_that_can_be_used_to_maintain_constant_message_length"
}
}
Name | Type | Description | Optional | Value If Omitted |
---|---|---|---|---|
contract | string (HumanAddr) | Address receiving the token | no | |
receiver_info | Code hash and BatchReceiveNft implementation status of the recipient contract | yes | nothing | |
token_id | string | Identifier of the token to be transferred | no | |
msg | string (base64 encoded Binary) | msg included when calling the recipient contract's BatchReceiveNft (or ReceiveNft) | yes | nothing |
memo | string | memo for the tx that is only viewable by addresses involved (recipient, sender, previous owner) | yes | nothing |
padding | string | An ignored string that can be used to maintain constant message length | yes | nothing |
Response
{
"send_nft": {
"status": "success"
}
}
The optional ReceiverInfo object may be used to provide the code hash of the contract receiving tokens from either SendNft or BatchSendNft. It may also optionally indicate whether the recipient contract implements BatchReceiveNft in addition to ReceiveNft. If the
also_implements_batch_receive_nft
field is not provided, it defaults to false
.{
"recipient_code_hash": "code_hash_of_the_recipient_contract",
"also_implements_batch_receive_nft": true | false,
}
Name | Type | Description | Optional | Value If Omitted |
---|---|---|---|---|
recipient_code_hash | string | Code hash of the recipient contract | no | |
also_implements_batch_receive_nft | bool | yes | false |
Approve is used to grant an address permission to transfer a single token. This can only be performed by the token's owner or, in compliance with CW-721, an address that has inventory-wide approval to transfer the owner's tokens. Approve is provided to maintain compliance with CW-721, but the owner can use SetWhitelistedApproval to accomplish the same thing if specifying a
token_id
and approve_token
AccessLevel for transfer
.Request
{
"approve": {
"spender": "address_being_granted_approval_to_transfer_the_specified_token",
"token_id": "ID_of_the_token_that_can_now_be_transferred_by_the_spender",
"expires": "never" | {"at_height": 999999} | {"at_time":999999},
"padding": "optional_ignored_string_that_can_be_used_to_maintain_constant_message_length"
}
}
Name | Type | Description | Optional | Value If Omitted |
---|---|---|---|---|
spender | string (HumanAddr) | Address being granted approval to transfer the token | no | |
token_id | string | ID of the token that the spender can now transfer | no | |
expires | The expiration of this token transfer approval. Can be a blockheight, time, or never | yes | "never" | |
padding | string | An ignored string that can be used to maintain constant message length | yes | nothing |
Response
{
"approve": {
"status": "success"
}
}
Expiration
The Expiration object is used to set an expiration for any approvals granted in the message. Expiration can be set to a specified blockheight, a time in seconds since epoch 01/01/1970, or "never". Values for blockheight and time are specified as a u64. If no expiration is given, it must default to "never".
Also, because the current blockheight and time will not be available to queries until a future hardfork makes that possible, please see above regarding an imprecise, and possibly delayed way to enforce expirations on queries in the meantime. This imprecise method must only be applied when checking an expiration during a query. When checking an expiration during a message, the blockheight and time are available and exact expiration must be enforced.
"never"
- the approval will never expire{"at_time": 1700000000}
- the approval will expire 1700000000 seconds after 01/01/1970 (time value is u64){"at_height": 3000000}
- the approval will expire at blockheight 3000000 (height value is u64)
Revoke is used to revoke from an address the permission to transfer this single token. This can only be performed by the token's owner or, in compliance with CW-721, an address that has inventory-wide approval to transfer the owner's tokens (referred to as an operator later). However, one operator may not revoke transfer permission of even one single token away from another operator. Revoke is provided to maintain compliance with CW-721, but the owner can use SetWhitelistedApproval to accomplish the same thing if specifying a
token_id
and revoke_token
AccessLevel for transfer
.Request
{
"revoke": {
"spender": "address_being_revoked_approval_to_transfer_the_specified_token",
"token_id": "ID_of_the_token_that_can_no_longer_be_transferred_by_the_spender",
"padding": "optional_ignored_string_that_can_be_used_to_maintain_constant_message_length"
}
}
Name | Type | Description | Optional | Value If Omitted |
---|---|---|---|---|
spender | string (HumanAddr) | Address no longer permitted to transfer the token | no | |
token_id | string | ID of the token that the spender can no longer transfer | no | |
padding | string | An ignored string that can be used to maintain constant message length | yes | nothing |
Response
{
"revoke": {
"status": "success"
}
}
ApproveAll is used to grant an address permission to transfer all the tokens in the message sender's inventory. This must include the ability to transfer any tokens the sender acquires after granting this inventory-wide approval. This also gives the address the ability to grant another address the approval to transfer a single token. ApproveAll is provided to maintain compliance with CW-721, but the message sender can use SetWhitelistedApproval to accomplish the same thing by using
all
AccessLevel for transfer
.Request
{
"approve_all": {
"operator": "address_being_granted_inventory-wide_approval_to_transfer_tokens",
"expires": "never" | {"at_height": 999999} | {"at_time":999999},
"padding": "optional_ignored_string_that_can_be_used_to_maintain_constant_message_length"
}
}
Name | Type | Description | Optional | Value If Omitted |
---|---|---|---|---|
operator | string (HumanAddr) | Address being granted approval to transfer all of the message sender's tokens | no | |
expires | The expiration of this inventory-wide transfer approval. Can be a blockheight, time, or never | yes | "never" | |
padding | string | An ignored string that can be used to maintain constant message length | yes | nothing |
Response
{
"approve_all": {
"status": "success"
}
}
RevokeAll is used to revoke all transfer approvals granted to an address. RevokeAll is provided to maintain compliance with CW-721, but the message sender can use SetWhitelistedApproval to accomplish the same thing by using
none
AccessLevel for transfer
.Request
{
"revoke_all": {
"operator": "address_being_revoked_all_approvals_to_transfer_tokens",
"padding": "optional_ignored_string_that_can_be_used_to_maintain_constant_message_length"
}
}
Name | Type | Description | Optional | Value If Omitted |
---|---|---|---|---|
operator | string (HumanAddr) | Address being revoked all approvals to transfer the message sender's tokens | no | |
padding | string | An ignored string that can be used to maintain constant message length | yes | nothing |
Response
{
"revoke_all": {
"status": "success"
}
}
The owner of a token can use SetWhitelistedApproval to grant an address permission to view ownership, view private metadata, and/or to transfer a single token or every token in the owner's inventory. SetWhitelistedApproval can also be used to revoke any approval previously granted to the address.
Request
{
"set_whitelisted_approval": {
"address": "address_being_granted_or_revoked_approval",
"token_id": "optional_ID_of_the_token_to_grant_or_revoke_approval_on",
"view_owner": "approve_token" | "all" | "revoke_token" | "none",
"view_private_metadata": "approve_token" | "all" | "revoke_token" | "none",
"transfer": "approve_token" | "all" | "revoke_token" | "none",
"expires": "never" | {"at_height": 999999} | {"at_time":999999},
"padding": "optional_ignored_string_that_can_be_used_to_maintain_constant_message_length"
}
}
Name | Type | Description | Optional | Value If Omitted |
---|---|---|---|---|
address | string (HumanAddr) | Address to grant or revoke approval to/from | no | |
token_id | string | If supplying either approve_token or revoke_token access, the token whose privacy is being set | yes | nothing |
view_owner | Grant or revoke the address' permission to view the ownership of a token/inventory | yes | nothing | |
view_private_metadata | Grant or revoke the address' permission to view the private metadata of a token/inventory | yes | nothing | |
transfer | Grant or revoke the address' permission to transfer a token/inventory | yes | nothing | |
expires | The expiration of any approval granted in this message. Can be a blockheight, time, or never | yes | "never" | |
padding | string | An ignored string that can be used to maintain constant message length | yes | nothing |
Response
{
"set_whitelisted_approval": {
"status": "success"
}
}
AccessLevel
AccessLevel determines the type of access being granted or revoked to the specified address in a SetWhitelistedApproval message or to everyone in a SetGlobalApproval message. Inventory-wide approval and token-specific approval are mutually exclusive levels of access. The levels are:
"approve_token"
- grant approval only on the token specified in the message"revoke_token"
- revoke a previous approval on the specified token"all"
- grant approval for all tokens in the message signer's inventory. This approval must also apply to any tokens the signer acquires after grantingall
approval"none"
- revoke any approval (both token and inventory-wide) previously granted to the specified address (or for everyone if using SetGlobalApproval)
A contract will use RegisterReceiveNft to notify the SNIP-721 contract that it implements ReceiveNft and possibly also BatchReceiveNft (see below). This enables the SNIP-721 contract to call the registered contract whenever it is Sent a token (or tokens). In order to comply with CW-721, ReceiveNft only informs the recipient contract that it has been sent a single token, and it only informs the recipient contract who the token's previous owner was, not who sent the token (which may be different addresses) despite calling the previous owner
sender
(see below). BatchReceiveNft, on the other hand, can be used to inform a contract that it was sent multiple tokens, and notifies the recipient of both, the token's previous owner and the sender.Request
{
"register_receive_nft": {
"code_hash": "code_hash_of_the_contract_implementing_a_receiver_interface",
"also_implements_batch_receive_nft": true | false,
"padding": "optional_ignored_string_that_can_be_used_to_maintain_constant_message_length"
}
}
Name | Type | Description | Optional | Value If Omitted |
---|---|---|---|---|
code_hash | string | A 32-byte hex encoded string, with the code hash of the message sender, which is a contract that implements a receiver | no | |
also_implements_batch_receive_nft | bool | true if the message sender contract also implements BatchReceiveNft so it can be informed that it was sent a list of tokens | yes | false |
padding | string | An ignored string that can be used to maintain constant message length | yes | nothing |
Response
{
"register_receive_nft": {
"status": "success"
}
}
CreateViewingKey generates a new viewing key for the Cosmos message sender, which is used to authenticate account-specific queries, because queries in Cosmos have no way to cryptographically authenticate the querier's identity.
The Viewing Key must be implemented in such a way that validation takes a significant amount to time to perform, in order to be resistant to brute-force attacks. The viewing key must only be used to authenticate queries, as messages cryptographically authenticate the sender.
The
entropy
field of the request should be a client supplied string used for entropy for generation of the viewing key. Secure implementation is left to the contract developer, but it is recommended to use base-64 encoded random bytes and not predictable inputs.Request
{
"create_viewing_key": {
"entropy": "string_used_as_part_of_the_entropy_supplied_to_the_rng",
"padding": "optional_ignored_string_that_can_be_used_to_maintain_constant_message_length"
}
}
Name | Type | Description | Optional | Value If Omitted |
---|---|---|---|---|
entropy | string | String used as part of the entropy supplied to the rng that generates the random viewing key | no | |
padding | string | An ignored string that can be used to maintain constant message length | yes | nothing |
Response
{
"viewing_key": {
"key": "the_created_viewing_key"
}
}
SetViewingKey is used to set the viewing key to a predefined string. It must replace any key that currently exists. It would be best for users to call CreateViewingKey to ensure a strong key, but this function is provided so that contracts can also utilize viewing keys.
Request
{
"set_viewing_key": {
"key": "the_new_viewing_key",
"padding": "optional_ignored_string_that_can_be_used_to_maintain_constant_message_length"
}
}
Name | Type | Description | Optional | Value If Omitted |
---|---|---|---|---|
key | string | The new viewing key for the message sender | no | |
padding | string | An ignored string that can be used to maintain constant message length | yes | nothing |
Response
{
"viewing_key": {
"key": "the_message_sender's_viewing_key"
}
}
ContractInfo returns the contract's name and symbol. This query is not authenticated.
Request
{
"contract_info": {}
}
Response
{
"contract_info": {
"name": "contract_name",
"symbol": "contract_symbol"
}
}
NumTokens returns the number of tokens controlled by the contract. If the contract's token supply is private, the SNIP-721 contract may choose to only allow an authenticated minter's address to perform this query.
Request
{
"num_tokens": {
"viewer": {
"address": "address_of_the_querier_if_supplying_optional_ViewerInfo",
"viewing_key": "viewer's_key_if_supplying_optional_ViewerInfo"
}
}
}
Name | Type | Description | Optional | Value If Omitted |
---|---|---|---|---|
viewer | The address and viewing key performing this query | yes | nothing |
Response
{
"num_tokens": {
"count": 99999
}
}
Name | Type | Description | Optional |
---|---|---|---|
count | number (u32) | Number of tokens controlled by this contract | no |
ViewerInfo
The ViewerInfo object provides the address and viewing key of the querier. It is optionally provided in queries where public responses and address-specific responses will differ.
{
"address": "address_of_the_querier_if_supplying_optional_ViewerInfo",
"viewing_key": "viewer's_key_if_supplying_optional_ViewerInfo"
}
Name | Type | Description | Optional | Value If Omitted |
---|---|---|---|---|
address | string (HumanAddr) | Address performing the query | no | |
viewing_key | string | The querying address' viewing key | no | |
OwnerOf returns the owner of the specified token if the querier is the owner or has been granted permission to view the owner. If the querier is the owner, OwnerOf must also display all the addresses that have been given transfer permission. The transfer approval list is provided as part of CW-721 compliance; however, the token owner is advised to use NftDossier (see below) for a more complete list that includes view_owner and view_private_metadata approvals (which CW-721 is not capable of keeping private). If no viewer is provided, OwnerOf must only display the owner if ownership is public for this token.
Request
{
"owner_of": {
"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 | The address and viewing key performing this query | yes | nothing | |
include_expired | bool | True if expired transfer approvals should be included in the response | yes | false |
Response
{
"owner_of": {
"owner": "address_of_the_token_owner",
"approvals": [
{
"spender": "address_with_transfer_approval",
"expires": "never" | {"at_height": 999999} | {"at_time":999999}
},
{
"...": "..."
}
]
}
}
Name | Type | Description | Optional |
---|---|---|---|
owner | string (HumanAddr) | Address of the token's owner | no |
approvals | List of approvals to transfer this token | no |
Cw721Approval
The Cw721Approval object is used to display CW-721-style approvals which are limited to only permission to transfer, as CW-721 does not enable ownership or metadata privacy.
{
"spender": "address_with_transfer_approval",
"expires": "never" | {"at_height": 999999} | {"at_time":999999}
}
Name | Type | Description | Optional |
---|---|---|---|
spender | string (HumanAddr) | Address whitelisted to transfer a token | no |
expires | The expiration of this transfer approval. Can be a blockheight, time, or never | no |
NftInfo returns the public metadata of a token. All metadata fields are optional to allow for SNIP-721 contracts that choose not to implement metadata, but at most, one of the fields
token_uri
OR extension
should be defined. Metadata follows CW-721 specification, which is based on ERC-721 Metadata JSON Schema.Request
{
"nft_info": {
"token_id": "ID_of_the_token_being_queried"
}
}
Name | Type | Description | Optional | Value If Omitted |
---|---|---|---|---|
token_id | string | ID of the token being queried | no | |
Response
{
"nft_info": {
"token_uri": "optional_uri_pointing_to_off-chain_JSON_metadata",
"extension": {
"...": "..."
}
}
Name | Type | Description | Optional |
---|---|---|---|
token_uri | string | Uri pointing to off-chain JSON metadata | yes |
extension | Data structure defining on-chain metadata | yes | |
At most, one of the fields token_uri OR extension should be defined. | | | |
Metadata
This is the metadata for a token that follows CW-721 metadata specification, which is based on ERC721 Metadata JSON Schema. At most, one of the fields
token_uri
OR extension
should be defined.{
"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 | Data structure defining on-chain metadata | yes | nothing | |
At most, one of the fields token_uri OR extension should be defined. | | | | |
Extension
Extension can be any data structure representing token metadata that is stored on-chain. See here for the description of the Extension that the reference implementation uses.
AllNftInfo displays the result of both OwnerOf and NftInfo in a single query. This is provided for CW-721 compliance, but for more complete information about a token, use NftDossier, which will include private metadata and view_owner and view_private_metadata approvals if the querier is permitted to view this information.
Request
{
"all_nft_info": {
"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
}
}