Cosmos NFT Module

Abstract

This is a description of the implementation of the Cosmos-SDK NFT Module and some thoughts behind the design.

Summary

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.

Types

BaseNFT & Metadata

The BaseNFT type is a struct that is used by the NFT interface to allow flexibility within the standard. It includes the following data:

// BaseNFT non fungible token definition
type BaseNFT struct {
	ID          string         `json:"id,omitempty"` // id of the token; not exported to clients
	Owner       sdk.AccAddress `json:"owner"`        // account address that owns the NFT
	Name        string         `json:"name"`         // name of the token
	Description string         `json:"description"`  // unique description of the NFT
	Image       string         `json:"image"`        // image path
	TokenURI    string         `json:"token_uri"`    // optional extra properties available for querying
}

*A Note on Metadata & IBC

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.

NFT Interface

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.

// NFT non fungible token interface
type NFT interface {
	GetID() string
	GetOwner() sdk.AccAddress
	SetOwner(address sdk.AccAddress) BaseNFT
	GetName() string
	GetDescription() string
	GetImage() string
	GetTokenURI() string
	EditMetadata(name, description, image, tokenURI string) BaseNFT
	String() string
}

var _ NFT = (*BaseNFT)(nil)

NFTs

NFTs is an array of type NFT

// NFTs define a list of NFT
type NFTs []NFT

Collection

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.

// Collection of non fungible tokens
type Collection struct {
	Denom string `json:"denom,omitempty"` // name of the collection; not exported to clients
	NFTs  NFTs   `json:"nfts"`            // NFTs that belong to a collection
}

Collections

Collections is a top level storage of all collections present on an applicationn specific blockchain.

// Collections define an array of Collection
type Collections []Collection

Owner

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.

// Owner of non fungible tokens
type Owner struct {
	Address       sdk.AccAddress `json:"address"`
	IDCollections IDCollections  `json:"IDCollections"`
}

IDCollections

IDCollections is an array of type IDCollection.

// IDCollections is an array of ID Collections whose sole purpose is for find
type IDCollections []IDCollection

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.

// IDCollection of non fungible tokens
type IDCollection struct {
	Denom string   `json:"denom"`
	IDs   []string `json:"IDs"`
}

Msg Types

MsgTransferNFT

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
// MsgTransferNFT defines a TransferNFT message
type MsgTransferNFT struct {
	Sender    sdk.AccAddress
	Recipient sdk.AccAddress
	Denom     string
	ID        string
}

MsgEditNFTMetadata

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
// MsgEditNFTMetadata edits an NFT's metadata
type MsgEditNFTMetadata struct {
	Owner       sdk.AccAddress
	ID          string
	Denom       string
	Name        string
	Description string
	Image       string
	TokenURI    string
}

MsgMintNFT

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
// MsgMintNFT defines a MintNFT message
type MsgMintNFT struct {
	Sender      sdk.AccAddress
	Recipient   sdk.AccAddress
	ID          string
	Denom       string
	Name        string
	Description string
	Image       string
	TokenURI    string
}

MsgBurnNFT

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.
// MsgBurnNFT defines a BurnNFT message
type MsgBurnNFT struct {
	Sender sdk.AccAddress
	ID     string
	Denom  string
}

Handlers

Custom App-Specific Logic

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.

// custom-handler.go

// OverrideNFTModule overrides the NFT module for custom handlers
type OverrideNFTModule struct {
	nft.AppModule
	k nft.Keeper
}

// NewHandler module handler for the OerrideNFTModule
func (am OverrideNFTModule) NewHandler() sdk.Handler {
	return CustomNFTHandler(am.k)
}

// NewOverrideNFTModule generates a new NFT Module
func NewOverrideNFTModule(appModule nft.AppModule, keeper nft.Keeper) OverrideNFTModule {
	return OverrideNFTModule{
		AppModule: appModule,
		k:         keeper,
	}
}

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:


// CustomNFTHandler routes the messages to the handlers
func CustomNFTHandler(k keeper.Keeper) sdk.Handler {
	return func(ctx sdk.Context, msg sdk.Msg) sdk.Result {
		switch msg := msg.(type) {
		case types.MsgTransferNFT:
			return nft.HandleMsgTransferNFT(ctx, msg, k)
		case types.MsgEditNFTMetadata:
			return nft.HandleMsgEditNFTMetadata(ctx, msg, k)
		case types.MsgMintNFT:
			return HandleMsgMintNFTCustom(ctx, msg, k)
		case types.MsgBurnNFT:
			return nft.HandleMsgBurnNFT(ctx, msg, k)
		default:
			errMsg := fmt.Sprintf("unrecognized nft message type: %T", msg)
			return sdk.ErrUnknownRequest(errMsg).Result()
		}
	}
}

// HandleMsgMintNFTCustom handles MsgMintNFT
func HandleMsgMintNFTCustom(ctx sdk.Context, msg types.MsgMintNFT, k keeper.Keeper,
) sdk.Result {

	isTwilight := checkTwilight(ctx)

	if isTwilight {
		return nft.HandleMsgMintNFT(ctx, msg, k)
	}

	errMsg := fmt.Sprintf("Can't mint astral bodies outside of twilight!")
	return sdk.ErrUnknownRequest(errMsg).Result()
}

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.

HandleMsgTransferNFT

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.

HandleMsgEditNFTMetadata

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.

HandleMsgMintNFT

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.

HandleMsgBurnNFT

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.

Queriers

QuerySupply

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

QueryOwner

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.

QueryOwnerByDenom

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.

QueryCollection

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.

QueryDenoms

Param Type Description
- - -
Return Type Description
denoms []string An array of all denominations of NFTs on this application specific blockchain

QueryNFT

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

Further Reading