This ADR defines the x/nft module which is a generic implementation of NFTs, roughly “compatible” with ERC721. Applications using the x/nft module must implement the following functions:
MsgNewClass - Receive the user’s request to create a class, and call the NewClass of the x/nft module.
MsgUpdateClass - Receive the user’s request to update a class, and call the UpdateClass of the x/nft module.
MsgMintNFT - Receive the user’s request to mint a nft, and call the MintNFT of the x/nft module.
BurnNFT - Receive the user’s request to burn a nft, and call the BurnNFT of the x/nft module.
UpdateNFT - Receive the user’s request to update a nft, and call the UpdateNFT of the x/nft module.
NFTs are more than just crypto art, which is very helpful for accruing value to the Cosmos ecosystem. As a result, Cosmos Hub should implement NFT functions and enable a unified mechanism for storing and sending the ownership representative of NFTs as discussed in Link.As discussed in #9065, several potential solutions can be considered:
irismod/nft and modules/incubator/nft
CW721
DID NFTs
interNFT
Since functions/use cases of NFTs are tightly connected with their logic, it is almost impossible to support all the NFTs’ use cases in one Cosmos SDK module by defining and implementing different transaction types.Considering generic usage and compatibility of interchain protocols including IBC and Gravity Bridge, it is preferred to have a generic NFT module design which handles the generic NFTs logic.
This design idea can enable composability that application-specific functions should be managed by other modules on Cosmos Hub or on other Zones by importing the NFT module.The current design is based on the work done by IRISnet team and an older implementation in the Cosmos repository.
We create a x/nft module, which contains the following functionality:
Store NFTs and track their ownership.
Expose Keeper interface for composing modules to transfer, mint and burn NFTs.
Expose external Message interface for users to transfer ownership of their NFTs.
Query NFTs and their supply information.
The proposed module is a base module for NFT app logic. It’s goal it to provide a common layer for storage, basic transfer functionality and IBC. The module should not be used as a standalone.
Instead an app should create a specialized module to handle app specific logic (eg: NFT ID construction, royalty), user level minting and burning. Moreover an app specialized module should handle auxiliary data to support the app logic (eg indexes, ORM, business data).All data carried over IBC must be part of the NFT or Class type described below. The app specific NFT data should be encoded in NFT.data for cross-chain integrity. Other objects related to NFT, which are not important for integrity can be part of the app specific module.
NFT Class is comparable to an ERC-721 smart contract (provides description of a smart contract), under which a collection of NFTs can be created and managed.
message Class { string id = 1; string name = 2; string symbol = 3; string description = 4; string uri = 5; string uri_hash = 6; google.protobuf.Any data = 7;}
id is used as the primary index for storing the class; required
name is a descriptive name of the NFT class; optional
symbol is the symbol usually shown on exchanges for the NFT class; optional
description is a detailed description of the NFT class; optional
uri is a URI for the class metadata stored off chain. It should be a JSON file that contains metadata about the NFT class and NFT data schema (OpenSea example); optional
uri_hash is a hash of the document pointed by uri; optional
data is app specific metadata of the class; optional
message NFT { string class_id = 1; string id = 2; string uri = 3; string uri_hash = 4; google.protobuf.Any data = 10;}
class_id is the identifier of the NFT class where the NFT belongs; required
id is an identifier of the NFT, unique within the scope of its class. It is specified by the creator of the NFT and may be expanded to use DID in the future. class_id combined with id uniquely identifies an NFT and is used as the primary index for storing the NFT; required
{class_id}/{id} --> NFT (bytes)
uri is a URI for the NFT metadata stored off chain. Should point to a JSON file that contains metadata about this NFT (Ref: ERC721 standard and OpenSea extension); required
uri_hash is a hash of the document pointed by uri; optional
data is an app specific data of the NFT. CAN be used by composing modules to specify additional properties of the NFT; optional
This ADR doesn’t specify values that data can take; however, best practices recommend upper-level NFT modules clearly specify their contents. Although the value of this field doesn’t provide the additional context required to manage NFT records, which means that the field can technically be removed from the specification, the field’s existence allows basic informational/UI functionality.
MsgSend can be used to transfer the ownership of an NFT to another address.The implementation outline of the server is as follows:
type msgServer struct{ k Keeper}func (m msgServer)Send(ctx context.Context, msg *types.MsgSend) (*types.MsgSendResponse, error) { // check current ownership assertEqual(msg.Sender, m.k.GetOwner(msg.ClassId, msg.Id)) // transfer ownership m.k.Transfer(msg.ClassId, msg.Id, msg.Receiver)return &types.MsgSendResponse{}, nil}
The query service methods for the x/nft module are:
service Query { // Balance queries the number of NFTs of a given class owned by the owner, same as balanceOf in ERC721 rpc Balance(QueryBalanceRequest) returns (QueryBalanceResponse) { option (google.api.http).get = "/cosmos/nft/v1beta1/balance/{owner}/{class_id}"; } // Owner queries the owner of the NFT based on its class and id, same as ownerOf in ERC721 rpc Owner(QueryOwnerRequest) returns (QueryOwnerResponse) { option (google.api.http).get = "/cosmos/nft/v1beta1/owner/{class_id}/{id}"; } // Supply queries the number of NFTs from the given class, same as totalSupply of ERC721. rpc Supply(QuerySupplyRequest) returns (QuerySupplyResponse) { option (google.api.http).get = "/cosmos/nft/v1beta1/supply/{class_id}"; } // NFTs queries all NFTs of a given class or owner,choose at least one of the two, similar to tokenByIndex in ERC721Enumerable rpc NFTs(QueryNFTsRequest) returns (QueryNFTsResponse) { option (google.api.http).get = "/cosmos/nft/v1beta1/nfts"; } // NFT queries an NFT based on its class and id. rpc NFT(QueryNFTRequest) returns (QueryNFTResponse) { option (google.api.http).get = "/cosmos/nft/v1beta1/nfts/{class_id}/{id}"; } // Class queries an NFT class based on its id rpc Class(QueryClassRequest) returns (QueryClassResponse) { option (google.api.http).get = "/cosmos/nft/v1beta1/classes/{class_id}"; } // Classes queries all NFT classes rpc Classes(QueryClassesRequest) returns (QueryClassesResponse) { option (google.api.http).get = "/cosmos/nft/v1beta1/classes"; }}// QueryBalanceRequest is the request type for the Query/Balance RPC methodmessage QueryBalanceRequest { string class_id = 1; string owner = 2;}// QueryBalanceResponse is the response type for the Query/Balance RPC methodmessage QueryBalanceResponse { uint64 amount = 1;}// QueryOwnerRequest is the request type for the Query/Owner RPC methodmessage QueryOwnerRequest { string class_id = 1; string id = 2;}// QueryOwnerResponse is the response type for the Query/Owner RPC methodmessage QueryOwnerResponse { string owner = 1;}// QuerySupplyRequest is the request type for the Query/Supply RPC methodmessage QuerySupplyRequest { string class_id = 1;}// QuerySupplyResponse is the response type for the Query/Supply RPC methodmessage QuerySupplyResponse { uint64 amount = 1;}// QueryNFTstRequest is the request type for the Query/NFTs RPC methodmessage QueryNFTsRequest { string class_id = 1; string owner = 2; cosmos.base.query.v1beta1.PageRequest pagination = 3;}// QueryNFTsResponse is the response type for the Query/NFTs RPC methodsmessage QueryNFTsResponse { repeated cosmos.nft.v1beta1.NFT nfts = 1; cosmos.base.query.v1beta1.PageResponse pagination = 2;}// QueryNFTRequest is the request type for the Query/NFT RPC methodmessage QueryNFTRequest { string class_id = 1; string id = 2;}// QueryNFTResponse is the response type for the Query/NFT RPC methodmessage QueryNFTResponse { cosmos.nft.v1beta1.NFT nft = 1;}// QueryClassRequest is the request type for the Query/Class RPC methodmessage QueryClassRequest { string class_id = 1;}// QueryClassResponse is the response type for the Query/Class RPC methodmessage QueryClassResponse { cosmos.nft.v1beta1.Class class = 1;}// QueryClassesRequest is the request type for the Query/Classes RPC methodmessage QueryClassesRequest { // pagination defines an optional pagination for the request. cosmos.base.query.v1beta1.PageRequest pagination = 1;}// QueryClassesResponse is the response type for the Query/Classes RPC methodmessage QueryClassesResponse { repeated cosmos.nft.v1beta1.Class classes = 1; cosmos.base.query.v1beta1.PageResponse pagination = 2;}
Interoperability is all about reusing assets between modules and chains. The former one is achieved by ADR-33: Protobuf client - server communication. At the time of writing ADR-33 is not finalized. The latter is achieved by IBC. Here we will focus on the IBC side.
IBC is implemented per module. Here, we aligned that NFTs will be recorded and managed in the x/nft. This requires creation of a new IBC standard and implementation of it.For IBC interoperability, NFT custom modules MUST use the NFT object type understood by the IBC client. So, for x/nft interoperability, custom NFT implementations (example: x/cryptokitty) should use the canonical x/nft module and proxy all NFT balance keeping functionality to x/nft or else re-implement all functionality using the NFT object type understood by the IBC client. In other words: x/nft becomes the standard NFT registry for all Cosmos NFTs (example: x/cryptokitty will register a kitty NFT in x/nft and use x/nft for book keeping). This was discussed in the context of using x/bank as a general asset balance book. Not using x/nft will require implementing another module for IBC.
This specification conforms to the ERC-721 smart contract specification for NFT identifiers. Note that ERC-721 defines uniqueness based on (contract address, uint256 tokenId), and we conform to this implicitly because a single module is currently aimed to track NFT identifiers. Note: use of the (mutable) data field to determine uniqueness is not safe.s
Other functions need more modules. For example, a custody module is needed for NFT trading function, a collectible module is needed for defining NFT properties.