This is a description of the implementation of the Cosmos-SDK NFT Module and some thoughts behind the design.
The Cosmos-SDK is a software development kit for building application specific blockchains. It includes common functionality between blockchains and provides standards that can be expected across these blockchains including common patterns. An example of this is the MsgSend
message type which is used for transfering fungible tokens of designated denominations between two parties using the bank module. Another example is the account
querier which is part of the Auth Module and gives information about an account regardless of what chain it is part of.
The NFT Module described here is meant to be used as a module across chains for managing non-fungible token that represent individual assets with unique features. This standard was first developed on Ethereum within the ERC-721 and the subsequent EIP of the same name. This standard utilized the features of the Ethereum blockchain as well as the restrictions. The subsequent ERC-1155 standard addressed some of the restrictions of Ethereum regarding storage costs and semi-fungible assets.
NFTs on application specific blockchains share some but not all features as their Ethereum brethren. Since application specific blockchains are more flexible in how their resources are utilized it makes sense that should have the option of exploiting those resources. This includes the aility to use strings as IDs and to optionally store metadata on chain. The user-flow of composability with smart contracts should also be rethought on application specific blockchains with regard to Inter-Blockchain Communication as it is a different design experience from communicatoin between smart contracts.
The BaseNFT type is a struct that is used by the NFT interface to allow flexibility within the standard. It includes the following data:
The BaseNFT includes what was considered part of the off-chain metadata for the original ERC-721. These were the fields expected in the JSON object that was expected to be returned by resolvingn the URI found in the on-chain field tokenURI
. You can see that tokenURI
is also included here. This is to represent that it is possible to store this data on chain while allowing more data to be stored off chain. While this was the format chosen for the first version of the Cosmos NFT, it is under discussion to move all metadata to a separate module that can handle arbitrary amounts of data on chain and can be used to describe assets beyond Non-Fungible Tokens.
A stand-alone metadata Module would allow for independent standards to evolve regarding arbitrary asset types with expanding precision. The standards supported by http://schema.org and the process of adding nested information is being considered as a starting point for that standard.
With regard to Inter-Blockchain Communication the responsibility of the integrity of the metadata should be left to the origin chain. If a secondary chain was resopnsible for storing the source of truth of the metadata for an asset tracking that source of truth would become difficult if not impossible. Since origin chains are where the design and use of the NFT is determined, it should be up to that origin chain to decide who can update metadata and under what circumstances. Secondary chains can use IBC queriers to check needed metadata or keep redundant copies of the metadata locally. In that case it should be up to te secondary chain to keep the metadata in sync, similar to how layer 2 solutions keep metadata in sync with a source of truth using events.
The NFT Interface inherits the BaseNFT struct and includes getter functions for the asset data. It also lincludes a Stringer function in order to print the struct. The interface may change if metadata is moved to it's own module as it might no longer be necessary for the flexibility of an interface.
NFTs is an array of type NFT
A Collection is used to organized sets of NFTs. It contains the denomination of the NFT instead of storing it within each NFT. This saves storage space by removing redundancy.
Collections is a top level storage of all collections present on an applicationn specific blockchain.
An Owner is a struct that includes information about all NFTs owned by a single account. It would be possible to retrieve this information by looping through all Collections but that process could become computationaly prohibitive so a more efficient retrieval system is to store redundant information limited to the token ID by owner.
IDCollections is an array of type IDCollection.
An IDCollection is similar to a Collection
except instead of containing NFTs
it only contains an array of NFT IDs. This saves storage by avoiding redundancy.
This is the most commonly expected MsgType to be supported across chains. While each application specific blockchain will have vey different adoption of the MsgMintNFT
, MsgBurnNFT
and MsgEditNFTMetadata
it should be expected that each chain supports the ability to transfer ownership of the NFT asset. Even if that transfer is heavily restricted it should be mostly supported. The exception to this would be non-transferrable NFTs that might be attached to reputation or some asset which should not be transferrable. It still makes sense for this to be represented as an NFT because there are common queriers which will remain relevant to the NFT type even if non-transferrable.
Field | Type | Description |
---|---|---|
Sender | sdk.AccAddress |
The account address of the user sending the NFT. It is required that the sender is also the owner of the NFT. |
Recipient | sdk.AccAddress |
The account address who will receive the NFT as a result of the transfer transaction. |
Denom | string |
The denomination of the NFT, necessary as multiple denominations are able to be represented on each chain. |
ID | string |
The unique ID of the NFT being transferred |
In the V1 of the NFT Module you've seen that some metadata is stored on chain along with the TokenURI
that points to further metadata. This message type allows that specific metadata to be edited by the owner.
Field | Type | Description |
---|---|---|
Owner | sdk.AccAddress |
The owner of the NFT, which should also be the creator of the message |
ID | string |
The unique ID of the NFT being edited |
Denom | string |
The denomination of the NFT, necessary as multiple denominations are able to be represented on each chain. |
Name | string |
The name of the Token |
Description | string |
The description of the Token |
Image | string |
The Image of the Token |
TokenURI | string |
The URI pointing to a JSON object that contains subsequet metadata information off-chain |
This message type is used for minting new tokens. Without a restriction from a custom handler anyone can mint a new NFT
. If a new NFT
is minted under a new Denom
, a new Collection
will also be created, otherwise the NFT
is added to the existing Collection
. If a new NFT
is minted by a new account, a new Owner
is created, otherwise the NFT
ID
is added to the existing Owner
.
Field | Type | Description |
---|---|---|
Sender | sdk.AccAddress |
The sender of the Message |
Recipient | sdk.AccAddress |
The recipiet of the new NFT |
ID | string |
The unique ID of the NFT being minted |
Denom | string |
The denomination of the NFT. |
Name | string |
The name of the Token |
Description | string |
The description of the Token |
Image | string |
The Image of the Token |
TokenURI | string |
The URI pointing to a JSON object that contains subsequet metadata information off-chain |
This message type is used for burning tokens which destroys and deletes them. Without a restriction from a custom handler only the owner can burn an NFT
.
Field | Type | Description |
---|---|---|
Sender | sdk.AccAddress |
The account address of the user burning the token. |
ID | string |
The ID of the Token. |
Denom | string |
The Denom of the Token. |
Each Message type comes with a default handler that can be used by default but will most likely be too limited for each use case. We recommend that custom handlers are created to add in custom logic and restrictions over when the Message types can be executed. Below is a recomended method for initializing the module within the module manager so that a custom handler can be added. This can be seen in the example NFT app https://github.com/okwme/cosmos-nft.
You can see here that OverrideNFTModule
is the same as nft.AppModule
except for the NewHandler()
method. This method now returns a new Handler called CustomNFTHandler
. This custom handler can be seen below:
The default handlers are imported here with the NFT module and used for MsgTransferNFT
, MsgEditNFTMetadata
and MsgBurnNFT
. The MsgMintNFT
however is handled with a custom function called HandleMsgMintNFTCustom
. This custom function also utilizes the imported NFT module handler HandleMsgBurnNFT
, but only after certain conditions are checked. In this case it checks a function called checkTwilight
which returns a boolean. Only if isTwilight
is true will the Message succeed.
This pattern of inheriting and utlizing the module handlers wrapped in custom logic should allow each application specific blockchain to use the NFT while customizing it to their specific requirements.
The default handler for the MsgTransferNFT
type allows anyone to transfer tokens from one user to another. If there should be restrictions in place about who can transfer and when, then it should be handled by a custom wrapper that if successful utilizes the default HandleMsgTransferNFT
.
The default handler for the MsgEditNFTMetadata
type allows anyone to edit Metadata on a token that already exxx. If there should be restrictions in place about who can transfer and when, then it should be handled by a custom wrapper that if successful utilizes the default HandleMsgEditNFTMetadata
.
The default handler for the MsgMintNFT
type allows anyone to mint new tokens. If there should be restrictions in place about who can transfer and when, then it should be handled by a custom wrapper that if successful utilizes the default HandleMsgMintNFT
.
The default handler for the MsgBurnNFT
type allows anyone to burn a token that already exists. If there should be a restriction in place about who can transfer and when, then it should be handled by a custom wrapper that if successful utilizes the default HandleMsgBurnNFT
.
Param | Type | Description |
---|---|---|
Denom | string |
The denomination of the NFT being queried |
Return | Type | Description |
Supply | uint64 |
The amount of tokens that exist of that denomination |
Param | Type | Description |
---|---|---|
owner | sdk.AccAddress |
The account address of the owner in question |
Return | Type | Description |
owner | Owner |
The Owner type that includes IDCollections that contain Denom and IDCollection of all NFT ids. |
Param | Type | Description |
---|---|---|
owner | sdk.AccAddress |
The account address of the owner in question |
denom | string |
The denomination to be used as a filter on the owner. Only NFTs with this denom will be included with the returned owner. |
Return | Type | Description |
owner | Owner |
The Owner type that includes IDCollections of only the requested Denom and IDCollection of all NFT ids. |
Param | Type | Description |
---|---|---|
Denom | string |
The denomination of the Collectionn of NFTs to be retrieved |
Return | Type | Description |
collection | Collection |
The Collection type that includes all NFTs and their relevant metadata. |
Param | Type | Description |
---|---|---|
- | - | - |
Return | Type | Description |
denoms | []string |
An array of all denominations of NFTs on this application specific blockchain |
Param | Type | Description |
---|---|---|
Denom | string |
The denomination of the NFT in question |
ID | string |
The ID of the NFT in question |
Return | Type | Description |
nft | NFT |
Returnes the NFT type interface of the relevant denomination and ID |