github.com/cosmos/cosmos-sdk@v0.50.10/docs/architecture/adr-043-nft-module.md (about) 1 # ADR 43: NFT Module 2 3 ## Changelog 4 5 * 2021-05-01: Initial Draft 6 * 2021-07-02: Review updates 7 * 2022-06-15: Add batch operation 8 * 2022-11-11: Remove strict validation of classID and tokenID 9 10 ## Status 11 12 PROPOSED 13 14 ## Abstract 15 16 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**: 17 18 * `MsgNewClass` - Receive the user's request to create a class, and call the `NewClass` of the `x/nft` module. 19 * `MsgUpdateClass` - Receive the user's request to update a class, and call the `UpdateClass` of the `x/nft` module. 20 * `MsgMintNFT` - Receive the user's request to mint a nft, and call the `MintNFT` of the `x/nft` module. 21 * `BurnNFT` - Receive the user's request to burn a nft, and call the `BurnNFT` of the `x/nft` module. 22 * `UpdateNFT` - Receive the user's request to update a nft, and call the `UpdateNFT` of the `x/nft` module. 23 24 ## Context 25 26 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 https://github.com/cosmos/cosmos-sdk/discussions/9065. 27 28 As discussed in [#9065](https://github.com/cosmos/cosmos-sdk/discussions/9065), several potential solutions can be considered: 29 30 * irismod/nft and modules/incubator/nft 31 * CW721 32 * DID NFTs 33 * interNFT 34 35 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. 36 37 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. 38 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. 39 40 The current design is based on the work done by [IRISnet team](https://github.com/irisnet/irismod/tree/master/modules/nft) and an older implementation in the [Cosmos repository](https://github.com/cosmos/modules/tree/master/incubator/nft). 41 42 ## Decision 43 44 We create a `x/nft` module, which contains the following functionality: 45 46 * Store NFTs and track their ownership. 47 * Expose `Keeper` interface for composing modules to transfer, mint and burn NFTs. 48 * Expose external `Message` interface for users to transfer ownership of their NFTs. 49 * Query NFTs and their supply information. 50 51 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. 52 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). 53 54 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. 55 56 ### Types 57 58 We propose two main types: 59 60 * `Class` -- describes NFT class. We can think about it as a smart contract address. 61 * `NFT` -- object representing unique, non fungible asset. Each NFT is associated with a Class. 62 63 #### Class 64 65 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. 66 67 ```protobuf 68 message Class { 69 string id = 1; 70 string name = 2; 71 string symbol = 3; 72 string description = 4; 73 string uri = 5; 74 string uri_hash = 6; 75 google.protobuf.Any data = 7; 76 } 77 ``` 78 79 * `id` is used as the primary index for storing the class; _required_ 80 * `name` is a descriptive name of the NFT class; _optional_ 81 * `symbol` is the symbol usually shown on exchanges for the NFT class; _optional_ 82 * `description` is a detailed description of the NFT class; _optional_ 83 * `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](https://docs.opensea.io/docs/contract-level-metadata)); _optional_ 84 * `uri_hash` is a hash of the document pointed by uri; _optional_ 85 * `data` is app specific metadata of the class; _optional_ 86 87 #### NFT 88 89 We define a general model for `NFT` as follows. 90 91 ```protobuf 92 message NFT { 93 string class_id = 1; 94 string id = 2; 95 string uri = 3; 96 string uri_hash = 4; 97 google.protobuf.Any data = 10; 98 } 99 ``` 100 101 * `class_id` is the identifier of the NFT class where the NFT belongs; _required_ 102 * `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_ 103 104 ```text 105 {class_id}/{id} --> NFT (bytes) 106 ``` 107 108 * `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](https://docs.opensea.io/docs/metadata-standards)); _required_ 109 * `uri_hash` is a hash of the document pointed by uri; _optional_ 110 * `data` is an app specific data of the NFT. CAN be used by composing modules to specify additional properties of the NFT; _optional_ 111 112 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. 113 114 ### `Keeper` Interface 115 116 ```go 117 type Keeper interface { 118 NewClass(ctx sdk.Context,class Class) 119 UpdateClass(ctx sdk.Context,class Class) 120 121 Mint(ctx sdk.Context,nft NFT,receiver sdk.AccAddress) // updates totalSupply 122 BatchMint(ctx sdk.Context, tokens []NFT,receiver sdk.AccAddress) error 123 124 Burn(ctx sdk.Context, classId string, nftId string) // updates totalSupply 125 BatchBurn(ctx sdk.Context, classID string, nftIDs []string) error 126 127 Update(ctx sdk.Context, nft NFT) 128 BatchUpdate(ctx sdk.Context, tokens []NFT) error 129 130 Transfer(ctx sdk.Context, classId string, nftId string, receiver sdk.AccAddress) 131 BatchTransfer(ctx sdk.Context, classID string, nftIDs []string, receiver sdk.AccAddress) error 132 133 GetClass(ctx sdk.Context, classId string) Class 134 GetClasses(ctx sdk.Context) []Class 135 136 GetNFT(ctx sdk.Context, classId string, nftId string) NFT 137 GetNFTsOfClassByOwner(ctx sdk.Context, classId string, owner sdk.AccAddress) []NFT 138 GetNFTsOfClass(ctx sdk.Context, classId string) []NFT 139 140 GetOwner(ctx sdk.Context, classId string, nftId string) sdk.AccAddress 141 GetBalance(ctx sdk.Context, classId string, owner sdk.AccAddress) uint64 142 GetTotalSupply(ctx sdk.Context, classId string) uint64 143 } 144 ``` 145 146 Other business logic implementations should be defined in composing modules that import `x/nft` and use its `Keeper`. 147 148 ### `Msg` Service 149 150 ```protobuf 151 service Msg { 152 rpc Send(MsgSend) returns (MsgSendResponse); 153 } 154 155 message MsgSend { 156 string class_id = 1; 157 string id = 2; 158 string sender = 3; 159 string reveiver = 4; 160 } 161 message MsgSendResponse {} 162 ``` 163 164 `MsgSend` can be used to transfer the ownership of an NFT to another address. 165 166 The implementation outline of the server is as follows: 167 168 ```go 169 type msgServer struct{ 170 k Keeper 171 } 172 173 func (m msgServer) Send(ctx context.Context, msg *types.MsgSend) (*types.MsgSendResponse, error) { 174 // check current ownership 175 assertEqual(msg.Sender, m.k.GetOwner(msg.ClassId, msg.Id)) 176 177 // transfer ownership 178 m.k.Transfer(msg.ClassId, msg.Id, msg.Receiver) 179 180 return &types.MsgSendResponse{}, nil 181 } 182 ``` 183 184 The query service methods for the `x/nft` module are: 185 186 ```protobuf 187 service Query { 188 // Balance queries the number of NFTs of a given class owned by the owner, same as balanceOf in ERC721 189 rpc Balance(QueryBalanceRequest) returns (QueryBalanceResponse) { 190 option (google.api.http).get = "/cosmos/nft/v1beta1/balance/{owner}/{class_id}"; 191 } 192 193 // Owner queries the owner of the NFT based on its class and id, same as ownerOf in ERC721 194 rpc Owner(QueryOwnerRequest) returns (QueryOwnerResponse) { 195 option (google.api.http).get = "/cosmos/nft/v1beta1/owner/{class_id}/{id}"; 196 } 197 198 // Supply queries the number of NFTs from the given class, same as totalSupply of ERC721. 199 rpc Supply(QuerySupplyRequest) returns (QuerySupplyResponse) { 200 option (google.api.http).get = "/cosmos/nft/v1beta1/supply/{class_id}"; 201 } 202 203 // NFTs queries all NFTs of a given class or owner,choose at least one of the two, similar to tokenByIndex in ERC721Enumerable 204 rpc NFTs(QueryNFTsRequest) returns (QueryNFTsResponse) { 205 option (google.api.http).get = "/cosmos/nft/v1beta1/nfts"; 206 } 207 208 // NFT queries an NFT based on its class and id. 209 rpc NFT(QueryNFTRequest) returns (QueryNFTResponse) { 210 option (google.api.http).get = "/cosmos/nft/v1beta1/nfts/{class_id}/{id}"; 211 } 212 213 // Class queries an NFT class based on its id 214 rpc Class(QueryClassRequest) returns (QueryClassResponse) { 215 option (google.api.http).get = "/cosmos/nft/v1beta1/classes/{class_id}"; 216 } 217 218 // Classes queries all NFT classes 219 rpc Classes(QueryClassesRequest) returns (QueryClassesResponse) { 220 option (google.api.http).get = "/cosmos/nft/v1beta1/classes"; 221 } 222 } 223 224 // QueryBalanceRequest is the request type for the Query/Balance RPC method 225 message QueryBalanceRequest { 226 string class_id = 1; 227 string owner = 2; 228 } 229 230 // QueryBalanceResponse is the response type for the Query/Balance RPC method 231 message QueryBalanceResponse { 232 uint64 amount = 1; 233 } 234 235 // QueryOwnerRequest is the request type for the Query/Owner RPC method 236 message QueryOwnerRequest { 237 string class_id = 1; 238 string id = 2; 239 } 240 241 // QueryOwnerResponse is the response type for the Query/Owner RPC method 242 message QueryOwnerResponse { 243 string owner = 1; 244 } 245 246 // QuerySupplyRequest is the request type for the Query/Supply RPC method 247 message QuerySupplyRequest { 248 string class_id = 1; 249 } 250 251 // QuerySupplyResponse is the response type for the Query/Supply RPC method 252 message QuerySupplyResponse { 253 uint64 amount = 1; 254 } 255 256 // QueryNFTstRequest is the request type for the Query/NFTs RPC method 257 message QueryNFTsRequest { 258 string class_id = 1; 259 string owner = 2; 260 cosmos.base.query.v1beta1.PageRequest pagination = 3; 261 } 262 263 // QueryNFTsResponse is the response type for the Query/NFTs RPC methods 264 message QueryNFTsResponse { 265 repeated cosmos.nft.v1beta1.NFT nfts = 1; 266 cosmos.base.query.v1beta1.PageResponse pagination = 2; 267 } 268 269 // QueryNFTRequest is the request type for the Query/NFT RPC method 270 message QueryNFTRequest { 271 string class_id = 1; 272 string id = 2; 273 } 274 275 // QueryNFTResponse is the response type for the Query/NFT RPC method 276 message QueryNFTResponse { 277 cosmos.nft.v1beta1.NFT nft = 1; 278 } 279 280 // QueryClassRequest is the request type for the Query/Class RPC method 281 message QueryClassRequest { 282 string class_id = 1; 283 } 284 285 // QueryClassResponse is the response type for the Query/Class RPC method 286 message QueryClassResponse { 287 cosmos.nft.v1beta1.Class class = 1; 288 } 289 290 // QueryClassesRequest is the request type for the Query/Classes RPC method 291 message QueryClassesRequest { 292 // pagination defines an optional pagination for the request. 293 cosmos.base.query.v1beta1.PageRequest pagination = 1; 294 } 295 296 // QueryClassesResponse is the response type for the Query/Classes RPC method 297 message QueryClassesResponse { 298 repeated cosmos.nft.v1beta1.Class classes = 1; 299 cosmos.base.query.v1beta1.PageResponse pagination = 2; 300 } 301 ``` 302 303 ### Interoperability 304 305 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. 306 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. 307 308 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](https://github.com/cosmos/cosmos-sdk/discussions/9065#discussioncomment-873206) in the context of using x/bank as a general asset balance book. Not using x/nft will require implementing another module for IBC. 309 310 ## Consequences 311 312 ### Backward Compatibility 313 314 No backward incompatibilities. 315 316 ### Forward Compatibility 317 318 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 319 320 ### Positive 321 322 * NFT identifiers available on Cosmos Hub. 323 * Ability to build different NFT modules for the Cosmos Hub, e.g., ERC-721. 324 * NFT module which supports interoperability with IBC and other cross-chain infrastructures like Gravity Bridge 325 326 ### Negative 327 328 * New IBC app is required for x/nft 329 * CW721 adapter is required 330 331 ### Neutral 332 333 * 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. 334 335 ## Further Discussions 336 337 For other kinds of applications on the Hub, more app-specific modules can be developed in the future: 338 339 * `x/nft/custody`: custody of NFTs to support trading functionality. 340 * `x/nft/marketplace`: selling and buying NFTs using sdk.Coins. 341 * `x/fractional`: a module to split an ownership of an asset (NFT or other assets) for multiple stakeholder. `x/group` should work for most of the cases. 342 343 Other networks in the Cosmos ecosystem could design and implement their own NFT modules for specific NFT applications and use cases. 344 345 ## References 346 347 * Initial discussion: https://github.com/cosmos/cosmos-sdk/discussions/9065 348 * x/nft: initialize module: https://github.com/cosmos/cosmos-sdk/pull/9174 349 * [ADR 033](https://github.com/cosmos/cosmos-sdk/blob/main/docs/architecture/adr-033-protobuf-inter-module-comm.md)