From 8701e97c6521356e2a506cd00b2693b5fbc0b85e Mon Sep 17 00:00:00 2001 From: HackMD Date: Sat, 8 May 2021 07:00:58 +0000 Subject: [PATCH 01/22] Add ADR-043 BaseNFT Module --- docs/architecture/adr-043-basenft-module.md | 126 ++++++++++++++++++++ 1 file changed, 126 insertions(+) create mode 100644 docs/architecture/adr-043-basenft-module.md diff --git a/docs/architecture/adr-043-basenft-module.md b/docs/architecture/adr-043-basenft-module.md new file mode 100644 index 000000000000..07b713b01b72 --- /dev/null +++ b/docs/architecture/adr-043-basenft-module.md @@ -0,0 +1,126 @@ +# ADR 43: BaseNFT Module + +## Changelog + +- 05.05.2021: Initial Draft + +## Status + +DRAFT + +## Abstract + +This ADR defines a generic NFT module of `x/nft` which supports NFTs as a `proto.Any` and contains `BaseNFT` as the default implementation. + +## Context + +As was discussed in [#9065](https://github.com/cosmos/cosmos-sdk/discussions/9065), several potential solutions can be considered: +- irismod/nft and modules/incubator/nft +- CW721 +- DID NFTs +- interNFT + +Considering generic usage and compatibility of interchain protocols including IBC and Gravity Bridge, it is preferred to have a very simple NFT module design which only stores NFTs by id and owner. + +Application-specific functions (minting, burning, transferring, etc.) should be managed by other modules on Cosmos Hub or on other Zones. + +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). + + +## Decision + +We will create a module `x/nft` which only stores NFTs by id and owner. + +The interface for the `x/nft` module: + +```go +// NFTI is an interface used to store NFTs at a given id and owner. +type NFTI interface { + GetId() string // can not return empty string. + GetOwner() sdk.AccAddress +} +``` + +We will also create `BaseNFT` as the default implementation of the `NFTI` interface: +```proto +message BaseNFT { + option (gogoproto.equal) = true; + + string id = 1; + string name = 2; + string uri = 3 [(gogoproto.customname) = "URI"]; + string data = 4; + string owner = 5; +} +``` + +Queries functions for `BaseNFT` is: +```proto +service Query { + + // NFT queries NFT details based on id. + rpc NFT(QueryNFTRequest) returns (QueryNFTResponse) { + option (google.api.http).get = "/cosmos/nft/v1beta1/nfts/{id}"; + } + + // NFTs queries all proposals based on the optional onwer. + rpc NFTs(QueryNFTsRequest) returns (QueryNFTsResponse) { + option (google.api.http).get = "/cosmos/nft/v1beta1/nfts"; + } +} + +// QueryNFTRequest is the request type for the Query/NFT RPC method +message QueryNFTRequest { + string id = 1; +} + +// QueryNFTResponse is the response type for the Query/NFT RPC method +message QueryNFTResponse { + google.protobuf.Any nft = 1 [(cosmos_proto.accepts_interface) = "NFTI", (gogoproto.customname) = "NFT"]; +} + +// QueryNFTsRequest is the request type for the Query/NFTs RPC method +message QueryNFTsRequest { + string owner = 1; + cosmos.base.query.v1beta1.PageResponse pagination = 2; +} + +// QueryNFTsResponse is the response type for the Query/NFTs RPC method +message QueryNFTsResponse { + repeated google.protobuf.Any nfts = 1 [(cosmos_proto.accepts_interface) = "NFTI", (gogoproto.customname) = "NFTs"]; + cosmos.base.query.v1beta1.PageResponse pagination = 2; +} +``` + + + +## Consequences + +### Backward Compatibility + +No backwards incompatibilities. + +### Positive + +- NFT functions available on Cosmos Hub +- NFT module which supports interoperability with IBC and other cross-chain infrastractures like Gravity Bridge + +### Negative + +### Neutral + +- 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 + +## Further Discussions + +For other kinds of applications on the Hub, more app-specific modules can be developed in the future: +- `x/collectibles`: grouping NFTs into collections and defining properties of NFTs such as minting, burning and transferring, etc. +- `x/nft_custody`: custody of NFTs to support trading functionality +- `x/nft_marketplace`: selling and buying NFTs using sdk.Coins + +Other networks in the Cosmos ecosystem could design and implement their own NFT modules for specific NFT applications and usecases. + +## References + +- Initial discussion: https://github.com/cosmos/cosmos-sdk/discussions/9065 +- x/nft: initialize module: https://github.com/cosmos/cosmos-sdk/pull/9174 \ No newline at end of file From 6c923d39be8855f929c4635b84421f61bde8e2cc Mon Sep 17 00:00:00 2001 From: Shahan Khatchadourian Date: Mon, 10 May 2021 15:35:30 -0400 Subject: [PATCH 02/22] Adding specifications to NFT fields and clarity on usage. --- docs/architecture/adr-043-basenft-module.md | 39 +++++++++++++++------ 1 file changed, 28 insertions(+), 11 deletions(-) diff --git a/docs/architecture/adr-043-basenft-module.md b/docs/architecture/adr-043-basenft-module.md index 07b713b01b72..0e718d92ed22 100644 --- a/docs/architecture/adr-043-basenft-module.md +++ b/docs/architecture/adr-043-basenft-module.md @@ -34,23 +34,32 @@ We will create a module `x/nft` which only stores NFTs by id and owner. The interface for the `x/nft` module: ```go -// NFTI is an interface used to store NFTs at a given id and owner. +// NFTI is an interface used to store NFTs at a given id. type NFTI interface { - GetId() string // can not return empty string. - GetOwner() sdk.AccAddress + GetId() string // define a simple identifier for the NFT + GetOwner() sdk.AccAddress // the current owner's address + GetData() string // specialized metadata to accompany the nft } ``` +The NFT conforms to the following specifications: + * The Id is an immutable field used as a unique identifier. NFT identifiers don't currently have a naming convention but + can be used in association with existing Hub attributes, e.g., defining an NFT's identifier as an immutable Hub address allows its integration into existing Hub account management modules. + We envision that identifiers can accommodate mint and transfer actions. + * Owner: This mutable field represents the current account owner (on the Hub) of the NFT, i.e., the account that will have authorization + to perform various activities in the future. This can be a user, a module account, or potentially a future NFT module that adds capabilities. + * Data: This mutable field allows for attaching pertinent metadata to the NFT, e.g., to record special parameter change upon transferring or criteria being met. + The data field is used to maintain special information, such as name and URI. Currently, there is no convention for the data placed into this field, + however, best practices recommend defining clear specifications that apply to each specific NFT type. + We will also create `BaseNFT` as the default implementation of the `NFTI` interface: ```proto message BaseNFT { option (gogoproto.equal) = true; string id = 1; - string name = 2; - string uri = 3 [(gogoproto.customname) = "URI"]; - string data = 4; - string owner = 5; + string owner = 2; + string data = 3; } ``` @@ -100,13 +109,21 @@ message QueryNFTsResponse { No backwards incompatibilities. +### Forwards Compatibility + +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. + ### Positive -- NFT functions available on Cosmos Hub +- NFT identifiers available on Cosmos Hub +- Ability to build different NFT modules for the Cosmos Hub, e.g., ERC-721. - NFT module which supports interoperability with IBC and other cross-chain infrastractures like Gravity Bridge ### Negative +- Currently, no methods are defined for this module except to store and retrieve data. + ### Neutral - 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 @@ -114,9 +131,9 @@ No backwards incompatibilities. ## Further Discussions For other kinds of applications on the Hub, more app-specific modules can be developed in the future: -- `x/collectibles`: grouping NFTs into collections and defining properties of NFTs such as minting, burning and transferring, etc. -- `x/nft_custody`: custody of NFTs to support trading functionality -- `x/nft_marketplace`: selling and buying NFTs using sdk.Coins +- `x/nft/collectibles`: grouping NFTs into collections and defining properties of NFTs such as minting, burning and transferring, etc. +- `x/nft/custody`: custody of NFTs to support trading functionality +- `x/nft/marketplace`: selling and buying NFTs using sdk.Coins Other networks in the Cosmos ecosystem could design and implement their own NFT modules for specific NFT applications and usecases. From af689350c778d48fca38c318f75636a943c9c656 Mon Sep 17 00:00:00 2001 From: chengwenxi Date: Mon, 17 May 2021 17:59:41 +0800 Subject: [PATCH 03/22] resolve #9284 comments --- ...asenft-module.md => adr-043-nft-module.md} | 43 +++++++++++-------- 1 file changed, 25 insertions(+), 18 deletions(-) rename docs/architecture/{adr-043-basenft-module.md => adr-043-nft-module.md} (74%) diff --git a/docs/architecture/adr-043-basenft-module.md b/docs/architecture/adr-043-nft-module.md similarity index 74% rename from docs/architecture/adr-043-basenft-module.md rename to docs/architecture/adr-043-nft-module.md index 0e718d92ed22..83b7c3853052 100644 --- a/docs/architecture/adr-043-basenft-module.md +++ b/docs/architecture/adr-043-nft-module.md @@ -1,4 +1,4 @@ -# ADR 43: BaseNFT Module +# ADR 43: NFT Module ## Changelog @@ -10,9 +10,11 @@ DRAFT ## Abstract -This ADR defines a generic NFT module of `x/nft` which supports NFTs as a `proto.Any` and contains `BaseNFT` as the default implementation. +This ADR defines a generic NFT module of `x/nft` which supports NFTs as a `proto.Any` and contains `NFT` as the default implementation. ## Context +`` +NFTs are more digital assets than only crypto arts, which is very helpful for accruing value to ATOM. As a result, Cosmos Hub should implement NFT functions and enable a unified mechanism for storing and sending the ownership representative of NFTs ss discussed in https://github.com/cosmos/cosmos-sdk/discussions/9065. As was discussed in [#9065](https://github.com/cosmos/cosmos-sdk/discussions/9065), several potential solutions can be considered: - irismod/nft and modules/incubator/nft @@ -20,9 +22,13 @@ As was discussed in [#9065](https://github.com/cosmos/cosmos-sdk/discussions/906 - DID NFTs - interNFT +Since NFTs functions/use cases 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 very simple NFT module design which only stores NFTs by id and owner. -Application-specific functions (minting, burning, transferring, etc.) should be managed by other modules on Cosmos Hub or on other Zones. +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. + +For example, NFTs can be grouped into collections in a collectibles module to define the behaviors such as minting, burning, transferring, etc. 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). @@ -34,11 +40,12 @@ We will create a module `x/nft` which only stores NFTs by id and owner. The interface for the `x/nft` module: ```go -// NFTI is an interface used to store NFTs at a given id. -type NFTI interface { - GetId() string // define a simple identifier for the NFT - GetOwner() sdk.AccAddress // the current owner's address - GetData() string // specialized metadata to accompany the nft +// NFT is an interface used to store NFTs at a given id. +type NFT interface { + GetId() string // define a simple identifier for the NFT + GetOwner() sdk.AccAddress // the current owner's address + GetData() string // specialized metadata to accompany the nft + Validate() error } ``` @@ -52,9 +59,9 @@ The NFT conforms to the following specifications: The data field is used to maintain special information, such as name and URI. Currently, there is no convention for the data placed into this field, however, best practices recommend defining clear specifications that apply to each specific NFT type. -We will also create `BaseNFT` as the default implementation of the `NFTI` interface: +We will also create `NFT` as the default implementation of the `NFT` interface: ```proto -message BaseNFT { +message NFT { option (gogoproto.equal) = true; string id = 1; @@ -63,7 +70,7 @@ message BaseNFT { } ``` -Queries functions for `BaseNFT` is: +Queries functions for `NFT` is: ```proto service Query { @@ -85,7 +92,7 @@ message QueryNFTRequest { // QueryNFTResponse is the response type for the Query/NFT RPC method message QueryNFTResponse { - google.protobuf.Any nft = 1 [(cosmos_proto.accepts_interface) = "NFTI", (gogoproto.customname) = "NFT"]; + google.protobuf.Any nft = 1 [(cosmos_proto.accepts_interface) = "NFT", (gogoproto.customname) = "NFT"]; } // QueryNFTsRequest is the request type for the Query/NFTs RPC method @@ -96,7 +103,7 @@ message QueryNFTsRequest { // QueryNFTsResponse is the response type for the Query/NFTs RPC method message QueryNFTsResponse { - repeated google.protobuf.Any nfts = 1 [(cosmos_proto.accepts_interface) = "NFTI", (gogoproto.customname) = "NFTs"]; + repeated google.protobuf.Any nfts = 1 [(cosmos_proto.accepts_interface) = "NFT", (gogoproto.customname) = "NFTs"]; cosmos.base.query.v1beta1.PageResponse pagination = 2; } ``` @@ -107,9 +114,9 @@ message QueryNFTsResponse { ### Backward Compatibility -No backwards incompatibilities. +No backward incompatibilities. -### Forwards Compatibility +### Forward Compatibility 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. @@ -118,7 +125,7 @@ because a single module is currently aimed to track NFT identifiers. Note: use o - NFT identifiers available on Cosmos Hub - Ability to build different NFT modules for the Cosmos Hub, e.g., ERC-721. -- NFT module which supports interoperability with IBC and other cross-chain infrastractures like Gravity Bridge +- NFT module which supports interoperability with IBC and other cross-chain infrastructures like Gravity Bridge ### Negative @@ -135,9 +142,9 @@ For other kinds of applications on the Hub, more app-specific modules can be dev - `x/nft/custody`: custody of NFTs to support trading functionality - `x/nft/marketplace`: selling and buying NFTs using sdk.Coins -Other networks in the Cosmos ecosystem could design and implement their own NFT modules for specific NFT applications and usecases. +Other networks in the Cosmos ecosystem could design and implement their own NFT modules for specific NFT applications and use cases. ## References - Initial discussion: https://github.com/cosmos/cosmos-sdk/discussions/9065 -- x/nft: initialize module: https://github.com/cosmos/cosmos-sdk/pull/9174 \ No newline at end of file +- x/nft: initialize module: https://github.com/cosmos/cosmos-sdk/pull/9174 From 96f9e114fa66e2bf1f68ee002c59f741e69399b8 Mon Sep 17 00:00:00 2001 From: vincent Date: Tue, 18 May 2021 09:38:46 +0800 Subject: [PATCH 04/22] fix typo Co-authored-by: Ryan Christoffersen <12519942+ryanchristo@users.noreply.github.com> --- docs/architecture/adr-043-nft-module.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/architecture/adr-043-nft-module.md b/docs/architecture/adr-043-nft-module.md index 83b7c3853052..39d3244a9d8f 100644 --- a/docs/architecture/adr-043-nft-module.md +++ b/docs/architecture/adr-043-nft-module.md @@ -14,7 +14,7 @@ This ADR defines a generic NFT module of `x/nft` which supports NFTs as a `proto ## Context `` -NFTs are more digital assets than only crypto arts, which is very helpful for accruing value to ATOM. As a result, Cosmos Hub should implement NFT functions and enable a unified mechanism for storing and sending the ownership representative of NFTs ss discussed in https://github.com/cosmos/cosmos-sdk/discussions/9065. +NFTs are more digital assets than only crypto arts, which is very helpful for accruing value to ATOM. 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. As was discussed in [#9065](https://github.com/cosmos/cosmos-sdk/discussions/9065), several potential solutions can be considered: - irismod/nft and modules/incubator/nft From 09c17a6f3142a176077c69d5573878b4e2c1710b Mon Sep 17 00:00:00 2001 From: vincent Date: Tue, 18 May 2021 09:39:55 +0800 Subject: [PATCH 05/22] fix description Co-authored-by: Ryan Christoffersen <12519942+ryanchristo@users.noreply.github.com> --- docs/architecture/adr-043-nft-module.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/architecture/adr-043-nft-module.md b/docs/architecture/adr-043-nft-module.md index 39d3244a9d8f..2a7147d66d6d 100644 --- a/docs/architecture/adr-043-nft-module.md +++ b/docs/architecture/adr-043-nft-module.md @@ -70,7 +70,7 @@ message NFT { } ``` -Queries functions for `NFT` is: +The query service methods for the `x/nft` module are: ```proto service Query { From 13cc5a283c48c68c07af53c4a9b3e642d599ebb3 Mon Sep 17 00:00:00 2001 From: vincent Date: Tue, 18 May 2021 17:42:15 +0800 Subject: [PATCH 06/22] rename NFT to BaseNFT Co-authored-by: Federico Kunze <31522760+fedekunze@users.noreply.github.com> --- docs/architecture/adr-043-nft-module.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/architecture/adr-043-nft-module.md b/docs/architecture/adr-043-nft-module.md index 2a7147d66d6d..7fe52eab6a77 100644 --- a/docs/architecture/adr-043-nft-module.md +++ b/docs/architecture/adr-043-nft-module.md @@ -61,7 +61,7 @@ The NFT conforms to the following specifications: We will also create `NFT` as the default implementation of the `NFT` interface: ```proto -message NFT { +message BaseNFT { option (gogoproto.equal) = true; string id = 1; From 435c2199998363d5953c874ed40ac9e216eea75b Mon Sep 17 00:00:00 2001 From: chengwenxi Date: Tue, 25 May 2021 14:34:34 +0800 Subject: [PATCH 07/22] more description --- docs/architecture/adr-043-nft-module.md | 21 ++++++++++++++++++--- 1 file changed, 18 insertions(+), 3 deletions(-) diff --git a/docs/architecture/adr-043-nft-module.md b/docs/architecture/adr-043-nft-module.md index 7fe52eab6a77..150f8e714d1e 100644 --- a/docs/architecture/adr-043-nft-module.md +++ b/docs/architecture/adr-043-nft-module.md @@ -28,7 +28,7 @@ Considering generic usage and compatibility of interchain protocols including IB 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. -For example, NFTs can be grouped into collections in a collectibles module to define the behaviors such as minting, burning, transferring, etc. +For example, NFTs can be grouped into collections in a collectibles module to define the behaviors such as minting, burning, etc. 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). @@ -52,14 +52,29 @@ type NFT interface { The NFT conforms to the following specifications: * The Id is an immutable field used as a unique identifier. NFT identifiers don't currently have a naming convention but can be used in association with existing Hub attributes, e.g., defining an NFT's identifier as an immutable Hub address allows its integration into existing Hub account management modules. - We envision that identifiers can accommodate mint and transfer actions. + We envision that identifiers can accommodate mint and transfer actions. + The Id is also the primary index for storing NFTs. + ``` + id (string) --> NFT (bytes) + ``` * Owner: This mutable field represents the current account owner (on the Hub) of the NFT, i.e., the account that will have authorization to perform various activities in the future. This can be a user, a module account, or potentially a future NFT module that adds capabilities. + Owner is also the secondary index for storing NFT IDs owned by an address + ``` + owner (address) --> []string + ``` * Data: This mutable field allows for attaching pertinent metadata to the NFT, e.g., to record special parameter change upon transferring or criteria being met. The data field is used to maintain special information, such as name and URI. Currently, there is no convention for the data placed into this field, however, best practices recommend defining clear specifications that apply to each specific NFT type. -We will also create `NFT` as the default implementation of the `NFT` interface: +The logic for transferring the ownership of an NFT to another address (no coins involved) should also be defined in this module +```go +func (k Keeper) TransferOwnership(nftID string, currentOwner, newOwner sdk.AccAddress) error +``` + +Other behaviors of NFT, including minting, burning and other business-logic implementation should be defined in other upper-level modules that import this NFT module. + +We will also create `BaseNFT` as the default implementation of the `NFT` interface: ```proto message BaseNFT { option (gogoproto.equal) = true; From 1571a5911bab653bb104d31d5f6c7550b7f117ad Mon Sep 17 00:00:00 2001 From: chengwenxi Date: Tue, 25 May 2021 14:42:14 +0800 Subject: [PATCH 08/22] fix typo --- docs/architecture/adr-043-nft-module.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/architecture/adr-043-nft-module.md b/docs/architecture/adr-043-nft-module.md index 150f8e714d1e..8f1176bc942d 100644 --- a/docs/architecture/adr-043-nft-module.md +++ b/docs/architecture/adr-043-nft-module.md @@ -94,7 +94,7 @@ service Query { option (google.api.http).get = "/cosmos/nft/v1beta1/nfts/{id}"; } - // NFTs queries all proposals based on the optional onwer. + // NFTs queries all NFTs based on the optional onwer. rpc NFTs(QueryNFTsRequest) returns (QueryNFTsResponse) { option (google.api.http).get = "/cosmos/nft/v1beta1/nfts"; } From 1f3ec2ae5fb0616625c9898612bd12a2a0da3c20 Mon Sep 17 00:00:00 2001 From: Shahan Khatchadourian Date: Tue, 25 May 2021 19:22:54 -0700 Subject: [PATCH 09/22] Adding clarity to ADR 043 nft data field (#9388) * Added clarity to data field description * Update docs/architecture/adr-043-nft-module.md Co-authored-by: Aleksandr Bezobchuk Co-authored-by: Aleksandr Bezobchuk --- docs/architecture/adr-043-nft-module.md | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/docs/architecture/adr-043-nft-module.md b/docs/architecture/adr-043-nft-module.md index 8f1176bc942d..108b255d514c 100644 --- a/docs/architecture/adr-043-nft-module.md +++ b/docs/architecture/adr-043-nft-module.md @@ -63,9 +63,14 @@ The NFT conforms to the following specifications: ``` owner (address) --> []string ``` - * Data: This mutable field allows for attaching pertinent metadata to the NFT, e.g., to record special parameter change upon transferring or criteria being met. - The data field is used to maintain special information, such as name and URI. Currently, there is no convention for the data placed into this field, - however, best practices recommend defining clear specifications that apply to each specific NFT type. + * Data: This mutable field allows attaching special information to the NFT, for example: + * metadata such as title of the work and URI + * immutable data and parameters (such actual NFT data, hash or seed for generators) + * mutable data and parameters that change when transferring or when certain criteria are met (such as provenance) + + This ADR doesn't specify values that this field can take; however, best practices recommend upper-level NFT modules clearly specify its contents. + Although the value of this field doesn't provide 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. The logic for transferring the ownership of an NFT to another address (no coins involved) should also be defined in this module ```go From b5a36b375262cc9ca464f13f6ddc343a1070c766 Mon Sep 17 00:00:00 2001 From: Federico Kunze <31522760+fedekunze@users.noreply.github.com> Date: Thu, 27 May 2021 09:53:24 -0400 Subject: [PATCH 10/22] Update docs/architecture/adr-043-nft-module.md --- docs/architecture/adr-043-nft-module.md | 1 - 1 file changed, 1 deletion(-) diff --git a/docs/architecture/adr-043-nft-module.md b/docs/architecture/adr-043-nft-module.md index 108b255d514c..5cfc37a7900e 100644 --- a/docs/architecture/adr-043-nft-module.md +++ b/docs/architecture/adr-043-nft-module.md @@ -13,7 +13,6 @@ DRAFT This ADR defines a generic NFT module of `x/nft` which supports NFTs as a `proto.Any` and contains `NFT` as the default implementation. ## Context -`` NFTs are more digital assets than only crypto arts, which is very helpful for accruing value to ATOM. 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. As was discussed in [#9065](https://github.com/cosmos/cosmos-sdk/discussions/9065), several potential solutions can be considered: From e39f05907d5251910fc626deb79e3e4c0aef69e0 Mon Sep 17 00:00:00 2001 From: chengwenxi Date: Mon, 31 May 2021 14:42:56 +0800 Subject: [PATCH 11/22] apply comments --- docs/architecture/adr-043-nft-module.md | 31 ++++++------------------- 1 file changed, 7 insertions(+), 24 deletions(-) diff --git a/docs/architecture/adr-043-nft-module.md b/docs/architecture/adr-043-nft-module.md index 5cfc37a7900e..f91679a5c99e 100644 --- a/docs/architecture/adr-043-nft-module.md +++ b/docs/architecture/adr-043-nft-module.md @@ -36,15 +36,11 @@ The current design is based on the work done by [IRISnet team](https://github.co We will create a module `x/nft` which only stores NFTs by id and owner. -The interface for the `x/nft` module: - -```go -// NFT is an interface used to store NFTs at a given id. -type NFT interface { - GetId() string // define a simple identifier for the NFT - GetOwner() sdk.AccAddress // the current owner's address - GetData() string // specialized metadata to accompany the nft - Validate() error +```proto +message NFT { + string id = 1; + string owner = 2; + google.protobuf.Any data = 3; } ``` @@ -78,17 +74,6 @@ func (k Keeper) TransferOwnership(nftID string, currentOwner, newOwner sdk.AccAd Other behaviors of NFT, including minting, burning and other business-logic implementation should be defined in other upper-level modules that import this NFT module. -We will also create `BaseNFT` as the default implementation of the `NFT` interface: -```proto -message BaseNFT { - option (gogoproto.equal) = true; - - string id = 1; - string owner = 2; - string data = 3; -} -``` - The query service methods for the `x/nft` module are: ```proto service Query { @@ -111,7 +96,7 @@ message QueryNFTRequest { // QueryNFTResponse is the response type for the Query/NFT RPC method message QueryNFTResponse { - google.protobuf.Any nft = 1 [(cosmos_proto.accepts_interface) = "NFT", (gogoproto.customname) = "NFT"]; + NFT nft = 1; } // QueryNFTsRequest is the request type for the Query/NFTs RPC method @@ -122,13 +107,11 @@ message QueryNFTsRequest { // QueryNFTsResponse is the response type for the Query/NFTs RPC method message QueryNFTsResponse { - repeated google.protobuf.Any nfts = 1 [(cosmos_proto.accepts_interface) = "NFT", (gogoproto.customname) = "NFTs"]; + repeated NFT nfts = 1; cosmos.base.query.v1beta1.PageResponse pagination = 2; } ``` - - ## Consequences ### Backward Compatibility From 51df2170f2b382da67a4629fc510a3564ce1bb66 Mon Sep 17 00:00:00 2001 From: chengwenxi Date: Thu, 10 Jun 2021 18:48:39 +0800 Subject: [PATCH 12/22] apply comments --- docs/architecture/adr-043-nft-module.md | 127 +++++++++++++++++++++--- 1 file changed, 114 insertions(+), 13 deletions(-) diff --git a/docs/architecture/adr-043-nft-module.md b/docs/architecture/adr-043-nft-module.md index f91679a5c99e..3367043cc9be 100644 --- a/docs/architecture/adr-043-nft-module.md +++ b/docs/architecture/adr-043-nft-module.md @@ -31,48 +31,148 @@ For example, NFTs can be grouped into collections in a collectibles module to de 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). - ## Decision -We will create a module `x/nft` which only stores NFTs by id and owner. +We will create a module `x/nft`, which provides the most basic storage, query functions for other upper-level modules to call. This module implements the OCAP security model in the [ADR 033](https://github.com/cosmos/cosmos-sdk/blob/master/docs/architecture/adr-033-protobuf-inter-module-comm.md) protocol. -```proto -message NFT { +### Types + +We define a general NFT model and `x/nft` module only stores NFTs by id and owner. + +```go +type NFT struct{ string id = 1; string owner = 2; - google.protobuf.Any data = 3; + Data data = 3; +} + +// Data defines a minimum funtion set consistent with `ERC721Metadata` interface. +type Data interface { + Name() string // A descriptive name for a collection of NFTs + Symbol() string // An abbreviated name for NFTs + URI() string // A distinct Uniform Resource Identifier (URI) for a given asset } ``` The NFT conforms to the following specifications: - * The Id is an immutable field used as a unique identifier. NFT identifiers don't currently have a naming convention but - can be used in association with existing Hub attributes, e.g., defining an NFT's identifier as an immutable Hub address allows its integration into existing Hub account management modules. - We envision that identifiers can accommodate mint and transfer actions. - The Id is also the primary index for storing NFTs. + + * The Id is an immutable field used as a unique identifier. NFT identifiers don't currently have a naming convention but can be used in association with existing Hub attributes, e.g., defining an NFT's identifier as an immutable Hub address allows its integration into existing Hub account management modules. + We envision that identifiers can accommodate mint and transfer actions. + The Id is also the primary index for storing NFTs. + ``` id (string) --> NFT (bytes) ``` + * Owner: This mutable field represents the current account owner (on the Hub) of the NFT, i.e., the account that will have authorization to perform various activities in the future. This can be a user, a module account, or potentially a future NFT module that adds capabilities. Owner is also the secondary index for storing NFT IDs owned by an address ``` owner (address) --> []string ``` + * Data: This mutable field allows attaching special information to the NFT, for example: * metadata such as title of the work and URI * immutable data and parameters (such actual NFT data, hash or seed for generators) * mutable data and parameters that change when transferring or when certain criteria are met (such as provenance) - + This ADR doesn't specify values that this field can take; however, best practices recommend upper-level NFT modules clearly specify its contents. Although the value of this field doesn't provide 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. -The logic for transferring the ownership of an NFT to another address (no coins involved) should also be defined in this module +### `Msg` Service + +```protobuf +service Msg { + rpc Mint(MsgMint) returns (MsgMintResponse); + rpc TransferOwnership(MsgTransferOwnership) returns (MsgTransferOwnershipResponse); + rpc Burn(MsgBurn) returns (MsgBurnResponse); +} + +message MsgMint { + string id = 1; + string owner = 2; + google.protobuf.Any data = 3; + string minter = 4; +} +message MsgMintResponse {} + +message MsgTransferOwnership { + string id = 1; + string sender = 2; + string reveiver = 3; + google.protobuf.Any data = 4; +} +message MsgTransferOwnershipResponse {} + +message MsgBurn { + string id = 1; + string owner = 2; +} +message MsgBurnResponse {} +``` + +`MsgMint` provides the ability to create a new nft. + +`MsgTransferOwnership` is responsible for transferring the ownership of an NFT to another address (no coins involved). + +`MsgBurn` provides the ability to destroy nft, thereby guaranteeing the uniqueness of cross-chain nft. + +Other business-logic implementation should be defined in other upper-level modules that import this NFT module. The implementation example of the server is as follows: + ```go -func (k Keeper) TransferOwnership(nftID string, currentOwner, newOwner sdk.AccAddress) error +type msgServer struct{ + k Keeper +} + +func (k msgServer) Mint(ctx context.Context, msg *types.MsgMint) (*types.MsgMintResponse, error){ + if k.has(msg.id) { + retutn sdkerrors.Wrapf(types.ErrExistedNFT, "%s", msg.id) + } + + nft := types.NFT{msg.Id,msg.Owner,msg.Data} + owner, _ := sdk.AccAddressFromBech32(nft.Owner) + store := ctx.KVStore(k.storeKey) + bz := k.cdc.MustMarshalBinaryBare(&nft) + store.Set(types.GetNFTKey(nft.Id), bz) + + ownerStore := k.getOwnerStore(ctx, owner) + ownerStore.Set(types.MarshalNFTID(nft.Id), types.MarshalNFTID(nft.Id)) +} + +func (k msgServer) TransferOwnership(ctx context.Context, msg *types.MsgTransferOwnership) (*types.MsgTransferOwnershipResponse, error){ + nft, has := k.GetNFT(ctx, msg.Id) + if !has { + return sdkerrors.Wrapf(types.ErrNotFoundNFT, "%s", msg.Id) + } + // remove nft from current owner store + currentOwnerStore := k.getOwnerStore(ctx, nft.Owner) + currentOwnerStore.Delete(types.MarshalNFTID(nft.Id)) + + nft.Data = msg.data + nft.Owner = msg.Receiver + k.SetNFT(ctx, nft) + return nil +} + +func (k Keeper) Burn(ctx sdk.Context, msg *types.MsgBurn) error { + nft, has := k.GetNFT(ctx, msg.Id) + if !has { + return sdkerrors.Wrapf(types.ErrNotFoundNFT, "%s", msg.Id) + } + // delete nft + store := ctx.KVStore(k.storeKey) + store.Delete(types.GetNFTKey(nft.Id)) + + owner, _ := sdk.AccAddressFromBech32(nft.Owner) + ownerStore := k.getOwnerStore(ctx, owner) + ownerStore.Delete(types.MarshalNFTID(nft.Id)) + return nil +} + ``` -Other behaviors of NFT, including minting, burning and other business-logic implementation should be defined in other upper-level modules that import this NFT module. +The upper application calls those methods by holding the MsgClient instance of the `x/nft` module. The execution authority of msg is guaranteed by the OCAPs mechanism. The query service methods for the `x/nft` module are: ```proto @@ -150,3 +250,4 @@ Other networks in the Cosmos ecosystem could design and implement their own NFT - Initial discussion: https://github.com/cosmos/cosmos-sdk/discussions/9065 - x/nft: initialize module: https://github.com/cosmos/cosmos-sdk/pull/9174 +- [ADR 033](https://github.com/cosmos/cosmos-sdk/blob/master/docs/architecture/adr-033-protobuf-inter-module-comm.md) \ No newline at end of file From 899e2a27bc1aa6e08e4f2c7711d89c15497f2b68 Mon Sep 17 00:00:00 2001 From: chengwenxi Date: Tue, 15 Jun 2021 19:39:52 +0800 Subject: [PATCH 13/22] store nft in bank module --- docs/architecture/adr-043-nft-module.md | 156 +++++++++++++----------- 1 file changed, 88 insertions(+), 68 deletions(-) diff --git a/docs/architecture/adr-043-nft-module.md b/docs/architecture/adr-043-nft-module.md index 3367043cc9be..34500922c72d 100644 --- a/docs/architecture/adr-043-nft-module.md +++ b/docs/architecture/adr-043-nft-module.md @@ -13,9 +13,11 @@ DRAFT This ADR defines a generic NFT module of `x/nft` which supports NFTs as a `proto.Any` and contains `NFT` as the default implementation. ## Context + NFTs are more digital assets than only crypto arts, which is very helpful for accruing value to ATOM. 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. As was discussed in [#9065](https://github.com/cosmos/cosmos-sdk/discussions/9065), several potential solutions can be considered: + - irismod/nft and modules/incubator/nft - CW721 - DID NFTs @@ -37,73 +39,79 @@ We will create a module `x/nft`, which provides the most basic storage, query fu ### Types -We define a general NFT model and `x/nft` module only stores NFTs by id and owner. +We define a general NFT model and `x/nft` module only stores NFTs by denom. -```go -type NFT struct{ - string id = 1; - string owner = 2; - Data data = 3; +```protobuf +message NFT { + string id = 1; + string denom = 2; + google.protobuf.Any data = 3; } +``` +```go // Data defines a minimum funtion set consistent with `ERC721Metadata` interface. -type Data interface { - Name() string // A descriptive name for a collection of NFTs - Symbol() string // An abbreviated name for NFTs - URI() string // A distinct Uniform Resource Identifier (URI) for a given asset +type Metadata struct { + string Name // A descriptive name for a collection of NFTs + string Symbol // An abbreviated name for NFTs + string URI // A distinct Uniform Resource Identifier (URI) for a given asset + sdktypes.Any data; // NFT specific data } ``` The NFT conforms to the following specifications: - * The Id is an immutable field used as a unique identifier. NFT identifiers don't currently have a naming convention but can be used in association with existing Hub attributes, e.g., defining an NFT's identifier as an immutable Hub address allows its integration into existing Hub account management modules. +- The Id is an immutable field used as a unique identifier. NFT identifiers don't currently have a naming convention but can be used in association with existing Hub attributes, e.g., defining an NFT's identifier as an immutable Hub address allows its integration into existing Hub account management modules. We envision that identifiers can accommodate mint and transfer actions. - The Id is also the primary index for storing NFTs. - - ``` - id (string) --> NFT (bytes) - ``` - - * Owner: This mutable field represents the current account owner (on the Hub) of the NFT, i.e., the account that will have authorization - to perform various activities in the future. This can be a user, a module account, or potentially a future NFT module that adds capabilities. - Owner is also the secondary index for storing NFT IDs owned by an address - ``` - owner (address) --> []string - ``` - - * Data: This mutable field allows attaching special information to the NFT, for example: - * metadata such as title of the work and URI - * immutable data and parameters (such actual NFT data, hash or seed for generators) - * mutable data and parameters that change when transferring or when certain criteria are met (such as provenance) - - This ADR doesn't specify values that this field can take; however, best practices recommend upper-level NFT modules clearly specify its contents. - Although the value of this field doesn't provide 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. + The Id is also the primary index for storing NFTs. + + + ``` + id (string) --> NFT (bytes) + ``` + +- Owner: This mutable field represents the current account owner (on the Hub) of the NFT, i.e., the account that will have the authorization to perform various activities in the future. This can be a user, a module account, or potentially a future NFT module that adds capabilities. + Owner is also the secondary index for storing NFT IDs owned by an address + + ``` + owner (address) --> []string + ``` + +- Data: This mutable field allows attaching special information to the NFT, for example: + + - metadata such as the title of the work and URI + - immutable data and parameters (such actual NFT data, hash or seed for generators) + - mutable data and parameters that change when transferring or when certain criteria are met (such as provenance) + + This ADR doesn't specify values that this field 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. ### `Msg` Service ```protobuf service Msg { rpc Mint(MsgMint) returns (MsgMintResponse); - rpc TransferOwnership(MsgTransferOwnership) returns (MsgTransferOwnershipResponse); + rpc Send(MsgSend) returns (MsgTransferOwnershipResponse); rpc Burn(MsgBurn) returns (MsgBurnResponse); } message MsgMint { - string id = 1; - string owner = 2; - google.protobuf.Any data = 3; - string minter = 4; + string denom = 1; + Metadata metadata = 2; + string owner = 3; +} +message MsgMintResponse { + string id = 1; } -message MsgMintResponse {} -message MsgTransferOwnership { +message MsgSend { string id = 1; string sender = 2; string reveiver = 3; - google.protobuf.Any data = 4; + google.protobuf.Any Data = 4; } -message MsgTransferOwnershipResponse {} +message MsgSendResponse {} message MsgBurn { string id = 1; @@ -118,7 +126,7 @@ message MsgBurnResponse {} `MsgBurn` provides the ability to destroy nft, thereby guaranteeing the uniqueness of cross-chain nft. -Other business-logic implementation should be defined in other upper-level modules that import this NFT module. The implementation example of the server is as follows: +Other business-logic implementations should be defined in other upper-level modules that import this NFT module. The implementation example of the server is as follows: ```go type msgServer struct{ @@ -126,47 +134,58 @@ type msgServer struct{ } func (k msgServer) Mint(ctx context.Context, msg *types.MsgMint) (*types.MsgMintResponse, error){ - if k.has(msg.id) { - retutn sdkerrors.Wrapf(types.ErrExistedNFT, "%s", msg.id) + nftID := fmt.Sprintf("nft/%s/%s",msg.Denom,nextID()) + metadata := bankTypes.Metadata{ + Symbol: msg.Metadata.Symbol, + Base: nftID, + Name: msg.Metadata.Name, + URI: msg.Metadata.URI, //TODO + } + _,has := k.bank.GetDenomMetaData(metadata.Base) + if has { + retutn sdkerrors.Wrapf(types.ErrExistedDenom, "%s", msg.Name) } + + k.bank.SetDenomMetaData(ctx,metadata) + k.bank.MintCoins(types.ModuleName, sdk.Coin{denom: msg.ID, amount: 1}) + k.bank.SendCoinsFromModuleToAccount(types.ModuleName, msg.Owner, coin) - nft := types.NFT{msg.Id,msg.Owner,msg.Data} - owner, _ := sdk.AccAddressFromBech32(nft.Owner) - store := ctx.KVStore(k.storeKey) + nft := NFT{nftID,msg.Metadata.Data} bz := k.cdc.MustMarshalBinaryBare(&nft) - store.Set(types.GetNFTKey(nft.Id), bz) - - ownerStore := k.getOwnerStore(ctx, owner) - ownerStore.Set(types.MarshalNFTID(nft.Id), types.MarshalNFTID(nft.Id)) + denomStore := k.getDenomStore(ctx, msg.Denom) + denomStore.Set(nft.Id,bz) } -func (k msgServer) TransferOwnership(ctx context.Context, msg *types.MsgTransferOwnership) (*types.MsgTransferOwnershipResponse, error){ - nft, has := k.GetNFT(ctx, msg.Id) +func (k msgServer) Send(ctx context.Context, msg *types.MsgSend) (*types.MsgSendResponse, error){ + denom := GetDenomFromID(msg.Id) + denomStore := k.getDenomStore(ctx, denom) + nft, has := denomStore.Get(ctx, msg.Id) if !has { return sdkerrors.Wrapf(types.ErrNotFoundNFT, "%s", msg.Id) } - // remove nft from current owner store - currentOwnerStore := k.getOwnerStore(ctx, nft.Owner) - currentOwnerStore.Delete(types.MarshalNFTID(nft.Id)) + + k.bank.SendCoins(ctx,msg.Sender,msg.Reveiver,sdk.Coin{denom: msg.ID, amount: 1}) - nft.Data = msg.data - nft.Owner = msg.Receiver - k.SetNFT(ctx, nft) + nft.Data := msg.Data + bz := k.cdc.MustMarshalBinaryBare(&nft) + denomStore.Set(nft.Id,bz) return nil } func (k Keeper) Burn(ctx sdk.Context, msg *types.MsgBurn) error { - nft, has := k.GetNFT(ctx, msg.Id) + denom := GetDenomFromID(msg.Id) + denomStore := k.getDenomStore(ctx, denom) + nft, has := denomStore.Get(ctx, msg.Id) if !has { return sdkerrors.Wrapf(types.ErrNotFoundNFT, "%s", msg.Id) } - // delete nft - store := ctx.KVStore(k.storeKey) - store.Delete(types.GetNFTKey(nft.Id)) - owner, _ := sdk.AccAddressFromBech32(nft.Owner) - ownerStore := k.getOwnerStore(ctx, owner) - ownerStore.Delete(types.MarshalNFTID(nft.Id)) + token := sdk.Coin{denom: msg.ID, amount: 1} + k.bank.SendCoinsFromAccountToModule(ctx,msg.Owner,types.ModuleName,token) + k.bank.BurnCoins(ctx,types.ModuleName,token) + + // delete nft + denomStore.Delete(types.GetNFTKey(nft.Id)) return nil } @@ -175,6 +194,7 @@ func (k Keeper) Burn(ctx sdk.Context, msg *types.MsgBurn) error { The upper application calls those methods by holding the MsgClient instance of the `x/nft` module. The execution authority of msg is guaranteed by the OCAPs mechanism. The query service methods for the `x/nft` module are: + ```proto service Query { @@ -220,8 +240,7 @@ No backward incompatibilities. ### Forward Compatibility -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. +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. ### Positive @@ -240,6 +259,7 @@ because a single module is currently aimed to track NFT identifiers. Note: use o ## Further Discussions For other kinds of applications on the Hub, more app-specific modules can be developed in the future: + - `x/nft/collectibles`: grouping NFTs into collections and defining properties of NFTs such as minting, burning and transferring, etc. - `x/nft/custody`: custody of NFTs to support trading functionality - `x/nft/marketplace`: selling and buying NFTs using sdk.Coins From b641f4caba8944a0b515d99306c9b04d38216a52 Mon Sep 17 00:00:00 2001 From: chengwenxi Date: Fri, 18 Jun 2021 09:59:56 +0800 Subject: [PATCH 14/22] refactor nft to base nft --- docs/architecture/adr-043-nft-module.md | 318 ++++++++++++++++-------- 1 file changed, 213 insertions(+), 105 deletions(-) diff --git a/docs/architecture/adr-043-nft-module.md b/docs/architecture/adr-043-nft-module.md index 34500922c72d..36d417b8c5b1 100644 --- a/docs/architecture/adr-043-nft-module.md +++ b/docs/architecture/adr-043-nft-module.md @@ -10,11 +10,11 @@ DRAFT ## Abstract -This ADR defines a generic NFT module of `x/nft` which supports NFTs as a `proto.Any` and contains `NFT` as the default implementation. +This ADR defines the `x/nft` module which as a generic implementation of the NFT standard API, with some enhancements. ## Context -NFTs are more digital assets than only crypto arts, which is very helpful for accruing value to ATOM. 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. +NFTs are more digital assets than only crypto arts, which is very helpful for accruing value to cosmos . 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. As was discussed in [#9065](https://github.com/cosmos/cosmos-sdk/discussions/9065), several potential solutions can be considered: @@ -25,106 +25,123 @@ As was discussed in [#9065](https://github.com/cosmos/cosmos-sdk/discussions/906 Since NFTs functions/use cases 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 very simple NFT module design which only stores NFTs by id and owner. +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. -For example, NFTs can be grouped into collections in a collectibles module to define the behaviors such as minting, burning, etc. - 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). ## Decision -We will create a module `x/nft`, which provides the most basic storage, query functions for other upper-level modules to call. This module implements the OCAP security model in the [ADR 033](https://github.com/cosmos/cosmos-sdk/blob/master/docs/architecture/adr-033-protobuf-inter-module-comm.md) protocol. +We will create a module `x/nft`, which contains the following functionality: + +- store and transfer the NFTs utilizing `x/bank`, if you want to modify the attribute value of nft, you can use the `send` function in `x/nft`. +- permission, we can utilize `x/authz`. +- mint and burn the NFTs. +- enumeration. ### Types -We define a general NFT model and `x/nft` module only stores NFTs by denom. +#### Metadata + +We define a general `Metadata` model for `denom`, it is same as an erc721 contract on Ethereum, which manages all nfts in the contract, and is more like a set of nfts. ```protobuf -message NFT { - string id = 1; - string denom = 2; - google.protobuf.Any data = 3; +message Metadata { + string denom = 1; // Required, unique key + string symbol = 2; // An abbreviated name for NFTs + string name = 3; // A descriptive name for a collection of NFTs + string description = 4; // Description of NFTs } ``` -```go -// Data defines a minimum funtion set consistent with `ERC721Metadata` interface. -type Metadata struct { - string Name // A descriptive name for a collection of NFTs - string Symbol // An abbreviated name for NFTs - string URI // A distinct Uniform Resource Identifier (URI) for a given asset - sdktypes.Any data; // NFT specific data +- The `denom` is the name of the same type of nft, just like the address of an erc721 contract on Ethereum. +- The `symbol` is an abbreviated name for NFTs in this denom. +- The `name` is a descriptive name for a collection of NFTs in this denom. +- The `description` is a detailed description of denom, which extends the function of erc721. + +#### NFT + +We define a general NFT model and `x/nft` module only stores NFTs by id. + +```protobuf +message NFT { + string denom = 1; + string id = 2; + string uri = 3; + google.protobuf.Any data = 4; } ``` The NFT conforms to the following specifications: -- The Id is an immutable field used as a unique identifier. NFT identifiers don't currently have a naming convention but can be used in association with existing Hub attributes, e.g., defining an NFT's identifier as an immutable Hub address allows its integration into existing Hub account management modules. +- The `id` is an immutable field used as a unique identifier in the same denom, It is generated by the system and may be expanded to DID in the future. NFT identifiers don't currently have a naming convention but can be used in association with existing Hub attributes, e.g., defining an NFT's identifier as an immutable Hub address allows its integration into existing Hub account management modules. We envision that identifiers can accommodate mint and transfer actions. - The Id is also the primary index for storing NFTs. - + The Id is also the primary index for storing NFTs. ``` - id (string) --> NFT (bytes) + {denom}/{id} --> NFT (bytes) ``` -- Owner: This mutable field represents the current account owner (on the Hub) of the NFT, i.e., the account that will have the authorization to perform various activities in the future. This can be a user, a module account, or potentially a future NFT module that adds capabilities. - Owner is also the secondary index for storing NFT IDs owned by an address +- The `uri` is the off-chain storage address of nft attribute `data`, such as ipfs, etc. - ``` - owner (address) --> []string - ``` +- The `data` is mutable field and allows attaching special information to the NFT, for example: -- Data: This mutable field allows attaching special information to the NFT, for example: - - - metadata such as the title of the work and URI - - immutable data and parameters (such actual NFT data, hash or seed for generators) - - mutable data and parameters that change when transferring or when certain criteria are met (such as provenance) + - metadata such as the title of the work and URI. + - immutable data and parameters (such actual NFT data, hash or seed for generators). + - mutable data and parameters that change when transferring or when certain criteria are met (such as provenance). This ADR doesn't specify values that this field 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, + 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. +- The ownership of nft is controlled by the `x/bank` module and the `metadata` part will be converted to `banktypes.Metadata` is stored in the `x/bank` module. + ### `Msg` Service ```protobuf service Msg { - rpc Mint(MsgMint) returns (MsgMintResponse); - rpc Send(MsgSend) returns (MsgTransferOwnershipResponse); - rpc Burn(MsgBurn) returns (MsgBurnResponse); + rpc Issue(MsgIssue) returns (MsgIssueResponse); + rpc Mint(MsgMint) returns (MsgMintResponse); + rpc Send(MsgSend) returns (MsgMsgSendResponse); + rpc Burn(MsgBurn) returns (MsgBurnResponse); } -message MsgMint { - string denom = 1; - Metadata metadata = 2; - string owner = 3; +message MsgIssue { + cosmos.nft.v1beta1.Metadata metadata = 1; + string issuer = 2; } -message MsgMintResponse { - string id = 1; +message MsgIssueResponse {} + +message MsgMint { + cosmos.nft.v1beta1.NFT nft = 1; + string owner = 2; } +message MsgMintResponse {} message MsgSend { - string id = 1; - string sender = 2; - string reveiver = 3; - google.protobuf.Any Data = 4; + string sender = 1; + string reveiver = 2; + repeated cosmos.nft.v1beta1.NFT nfts = 3; } + message MsgSendResponse {} -message MsgBurn { - string id = 1; - string owner = 2; +message MsgBurn { + string denom = 1; + string id = 2; + string owner = 3; } message MsgBurnResponse {} ``` -`MsgMint` provides the ability to create a new nft. +`MsgIssue` is responsible for issuing an nft classification, just like deploying an erc721 contract on Ethereum. + +`MsgMint` provides the ability to create a new nft. -`MsgTransferOwnership` is responsible for transferring the ownership of an NFT to another address (no coins involved). +`MsgSend` is responsible for transferring the ownership of an NFT to another address (no coins involved). -`MsgBurn` provides the ability to destroy nft, thereby guaranteeing the uniqueness of cross-chain nft. +`MsgBurn` provides the ability to destroy nft, thereby guaranteeing the uniqueness of cross-chain nft. Other business-logic implementations should be defined in other upper-level modules that import this NFT module. The implementation example of the server is as follows: @@ -133,62 +150,75 @@ type msgServer struct{ k Keeper } -func (k msgServer) Mint(ctx context.Context, msg *types.MsgMint) (*types.MsgMintResponse, error){ - nftID := fmt.Sprintf("nft/%s/%s",msg.Denom,nextID()) - metadata := bankTypes.Metadata{ - Symbol: msg.Metadata.Symbol, - Base: nftID, - Name: msg.Metadata.Name, - URI: msg.Metadata.URI, //TODO - } - _,has := k.bank.GetDenomMetaData(metadata.Base) - if has { - retutn sdkerrors.Wrapf(types.ErrExistedDenom, "%s", msg.Name) - } +func (m msgServer) Issue(ctx context.Context, msg *types.MsgIssue) (*types.MsgIssueResponse, error){ + m.keeper.AssertDenomNotExist(msg.Metadata.Denom) - k.bank.SetDenomMetaData(ctx,metadata) - k.bank.MintCoins(types.ModuleName, sdk.Coin{denom: msg.ID, amount: 1}) - k.bank.SendCoinsFromModuleToAccount(types.ModuleName, msg.Owner, coin) - - nft := NFT{nftID,msg.Metadata.Data} - bz := k.cdc.MustMarshalBinaryBare(&nft) - denomStore := k.getDenomStore(ctx, msg.Denom) - denomStore.Set(nft.Id,bz) + store := ctx.KVStore(m.keeper.storeKey) + bz := m.keeper.cdc.MustMarshalBinaryBare(msg.Metadata) + store.Set(msg.Denom, bz) + return &types.MsgIssueResponse{}, nil } -func (k msgServer) Send(ctx context.Context, msg *types.MsgSend) (*types.MsgSendResponse, error){ - denom := GetDenomFromID(msg.Id) - denomStore := k.getDenomStore(ctx, denom) - nft, has := denomStore.Get(ctx, msg.Id) - if !has { - return sdkerrors.Wrapf(types.ErrNotFoundNFT, "%s", msg.Id) +func (m msgServer) Mint(ctx context.Context, msg *types.MsgMint) (*types.MsgMintResponse, error){ + m.keeper.AssertDenomExist(msg.NFT.Denom) + + metadata := m.keeper.GetMetadata(ctx, msg.NFT.Denom) + //NOTICE: how to generate coin denom + baseDenom := fmt.Sprintf("nft/%s/%s", msg.NFT.Denom, msg.NFT.Id) + bkMetadata := bankTypes.Metadata{ + Symbol: metadata.Symbol, + Base: baseDenom, + Name: metadata.Name, + URI: msg.NFT.URI, + Description: metadata.Description, } + + m.keeper.bank.SetDenomMetaData(ctx, bkMetadata) + mintCoins := sdk.NewCoins(sdk.NewCoin(baseDenom,1)) + m.keeper.bank.MintCoins(types.ModuleName, mintCoins) + m.keeper.bank.SendCoinsFromModuleToAccount(types.ModuleName, msg.Owner, mintCoins) - k.bank.SendCoins(ctx,msg.Sender,msg.Reveiver,sdk.Coin{denom: msg.ID, amount: 1}) + bz := m.keeper.cdc.MustMarshalBinaryBare(&msg.NFT) + denomStore := m.keeper.getDenomStore(ctx, msg.NFT.Denom) + denomStore.Set(msg.NFT.Id, bz) + return nil, nil +} - nft.Data := msg.Data - bz := k.cdc.MustMarshalBinaryBare(&nft) - denomStore.Set(nft.Id,bz) - return nil +func (m msgServer) Send(ctx context.Context, msg *types.MsgSend) (*types.MsgSendResponse, error){ + sentCoins := sdk.NewCoins() + for _,nft := range msg.NFTs { + m.keeper.AssertNFTExist(nft.Id) + + denomStore := m.keeper.getDenomStore(ctx, nft.Denom) + nft := denomStore.Get(nft.Id) + bz := m.keeper.cdc.MustMarshalBinaryBare(&nft) + denomStore.Set(nft.Id, bz) + + // NOTICE: how to generate coin denom + baseDenom := fmt.Sprintf("nft/%s/%s", nft.Denom, nft.Id) + sentCoins = sentCoins.Add(sdk.NewCoin(baseDenom,1)) + } + m.keeper.bank.SendCoins(ctx, msg.Sender, msg.Reveiver, sentCoins) + return &types.MsgSendResponse{},nil } -func (k Keeper) Burn(ctx sdk.Context, msg *types.MsgBurn) error { - denom := GetDenomFromID(msg.Id) - denomStore := k.getDenomStore(ctx, denom) - nft, has := denomStore.Get(ctx, msg.Id) - if !has { - return sdkerrors.Wrapf(types.ErrNotFoundNFT, "%s", msg.Id) - } +func (m Keeper) Burn(ctx sdk.Context, msg *types.MsgBurn) (types.MsgBurnResponse,error) { + m.keeper.AssertNFTExist(msg.Id) - token := sdk.Coin{denom: msg.ID, amount: 1} - k.bank.SendCoinsFromAccountToModule(ctx,msg.Owner,types.ModuleName,token) - k.bank.BurnCoins(ctx,types.ModuleName,token) + denomStore := m.keeper.getDenomStore(ctx, msg.Denom) + nft := denomStore.Get(msg.Id) + // NOTICE: how to generate coin denom + baseDenom := fmt.Sprintf("nft/%s/%s", msg.Denom, msg.Id) + coins := sdk.NewCoins(sdk.NewCoin(baseDenom,1)) + m.keeper.bank.SendCoinsFromAccountToModule(ctx, msg.Owner, types.ModuleName, coins) + m.keeper.bank.BurnCoins(ctx, types.ModuleName, coins) + + // TODO: how to delete bank.metadata // delete nft - denomStore.Delete(types.GetNFTKey(nft.Id)) - return nil + denomStore.Delete(msg.Id) + return &types.MsgBurnResponse{}, nil } - ``` The upper application calls those methods by holding the MsgClient instance of the `x/nft` module. The execution authority of msg is guaranteed by the OCAPs mechanism. @@ -203,10 +233,35 @@ service Query { option (google.api.http).get = "/cosmos/nft/v1beta1/nfts/{id}"; } - // NFTs queries all NFTs based on the optional onwer. + // NFTs queries all NFTs based on the optional owner. rpc NFTs(QueryNFTsRequest) returns (QueryNFTsResponse) { option (google.api.http).get = "/cosmos/nft/v1beta1/nfts"; } + + // NFTsOf queries all NFTs based on the denom. + rpc NFTsOf(QueryNFTsOfRequest) returns (QueryNFTsOfResponse) { + option (google.api.http).get = "/cosmos/nft/v1beta1/nfts/denom/{denom}"; + } + + // Supply queries the number of nft based on the denom, same as totalSupply of erc721 + rpc Supply(QuerySupplyRequest) returns (QuerySupplyResponse) { + option (google.api.http).get = "/cosmos/nft/v1beta1/supply/{denom}"; + } + + // Balance queries the number of based on the owner, same as balanceOf of erc721 + rpc Balance(QueryBalanceRequest) returns (QueryBalanceResponse) { + option (google.api.http).get = "/cosmos/nft/v1beta1/balance/{owner}"; + } + + // Denom queries the definition of a given denom + rpc Denom(QueryDenomRequest) returns (QueryDenomResponse) { + option (google.api.http).get = "/cosmos/nft/v1beta1/denoms/{denom}"; + } + + // Denoms queries all the denoms + rpc Denoms(QueryDenomsRequest) returns (QueryDenomsResponse) { + option (google.api.http).get = "/cosmos/nft/v1beta1/denoms"; + } } // QueryNFTRequest is the request type for the Query/NFT RPC method @@ -216,7 +271,7 @@ message QueryNFTRequest { // QueryNFTResponse is the response type for the Query/NFT RPC method message QueryNFTResponse { - NFT nft = 1; + cosmos.nft.v1beta1.NFT nft = 1; } // QueryNFTsRequest is the request type for the Query/NFTs RPC method @@ -227,7 +282,61 @@ message QueryNFTsRequest { // QueryNFTsResponse is the response type for the Query/NFTs RPC method message QueryNFTsResponse { - repeated NFT nfts = 1; + repeated cosmos.nft.v1beta1.NFT nfts = 1; + cosmos.base.query.v1beta1.PageResponse pagination = 2; +} + +// QueryNFTsOfRequest is the request type for the Query/NFTsOf RPC method +message QueryNFTsOfRequest { + string denom = 1; + cosmos.base.query.v1beta1.PageResponse pagination = 2; +} + +// QueryNFTsOfResponse is the response type for the Query/NFTsOf RPC method +message QueryNFTsOfResponse { + repeated cosmos.nft.v1beta1.NFT nfts = 1; + cosmos.base.query.v1beta1.PageResponse pagination = 2; +} + +// QuerySupplyRequest is the request type for the Query/Supply RPC method +message QuerySupplyRequest{ + string denom = 1; +} + +// QuerySupplyResponse is the response type for the Query/Supply RPC method +message QuerySupplyResponse{ + uint64 amount = 1; +} + +// QueryBalanceRequest is the request type for the Query/Balance RPC method +message QueryBalanceRequest{ + string owner = 1; +} + +// QueryBalanceResponse is the response type for the Query/Balance RPC method +message QueryBalanceResponse{ + uint64 amount = 1; +} + +// QueryDenomRequest is the request type for the Query/Denom RPC method +message QueryDenomRequest { + string denom = 1; +} + +// QueryDenomResponse is the response type for the Query/Denom RPC method +message QueryDenomResponse { + cosmos.nft.v1beta1.Metadata metadata = 1; +} + +// QueryDenomsRequest is the request type for the Query/Denoms RPC method +message QueryDenomsRequest { + // pagination defines an optional pagination for the request. + cosmos.base.query.v1beta1.PageRequest pagination = 1; +} + +// QueryDenomsResponse is the response type for the Query/Denoms RPC method +message QueryDenomsResponse { + repeated cosmos.nft.v1beta1.Metadata metadatas = 1; cosmos.base.query.v1beta1.PageResponse pagination = 2; } ``` @@ -240,11 +349,11 @@ No backward incompatibilities. ### Forward Compatibility -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. +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 ### Positive -- NFT identifiers available on Cosmos Hub +- NFT identifiers available on Cosmos Hub. - Ability to build different NFT modules for the Cosmos Hub, e.g., ERC-721. - NFT module which supports interoperability with IBC and other cross-chain infrastructures like Gravity Bridge @@ -254,15 +363,14 @@ This specification conforms to the ERC-721 smart contract specification for NFT ### Neutral -- 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 +- 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. ## Further Discussions For other kinds of applications on the Hub, more app-specific modules can be developed in the future: -- `x/nft/collectibles`: grouping NFTs into collections and defining properties of NFTs such as minting, burning and transferring, etc. -- `x/nft/custody`: custody of NFTs to support trading functionality -- `x/nft/marketplace`: selling and buying NFTs using sdk.Coins +- `x/nft/custody`: custody of NFTs to support trading functionality. +- `x/nft/marketplace`: selling and buying NFTs using sdk.Coins. Other networks in the Cosmos ecosystem could design and implement their own NFT modules for specific NFT applications and use cases. From 9d60dfdfebe8b2adb96cb77fee4b371de686c031 Mon Sep 17 00:00:00 2001 From: chengwenxi Date: Fri, 18 Jun 2021 11:43:05 +0800 Subject: [PATCH 15/22] fix note --- docs/architecture/adr-043-nft-module.md | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/docs/architecture/adr-043-nft-module.md b/docs/architecture/adr-043-nft-module.md index 36d417b8c5b1..ce1d96951cbc 100644 --- a/docs/architecture/adr-043-nft-module.md +++ b/docs/architecture/adr-043-nft-module.md @@ -37,8 +37,8 @@ We will create a module `x/nft`, which contains the following functionality: - store and transfer the NFTs utilizing `x/bank`, if you want to modify the attribute value of nft, you can use the `send` function in `x/nft`. - permission, we can utilize `x/authz`. -- mint and burn the NFTs. -- enumeration. +- mint and burn the NFTs, in this module. +- enumeration, in this module. ### Types @@ -163,7 +163,7 @@ func (m msgServer) Mint(ctx context.Context, msg *types.MsgMint) (*types.MsgMint m.keeper.AssertDenomExist(msg.NFT.Denom) metadata := m.keeper.GetMetadata(ctx, msg.NFT.Denom) - //NOTICE: how to generate coin denom + // NOTE: we can use denom manager or DID baseDenom := fmt.Sprintf("nft/%s/%s", msg.NFT.Denom, msg.NFT.Id) bkMetadata := bankTypes.Metadata{ Symbol: metadata.Symbol, @@ -194,7 +194,6 @@ func (m msgServer) Send(ctx context.Context, msg *types.MsgSend) (*types.MsgSend bz := m.keeper.cdc.MustMarshalBinaryBare(&nft) denomStore.Set(nft.Id, bz) - // NOTICE: how to generate coin denom baseDenom := fmt.Sprintf("nft/%s/%s", nft.Denom, nft.Id) sentCoins = sentCoins.Add(sdk.NewCoin(baseDenom,1)) } @@ -208,13 +207,12 @@ func (m Keeper) Burn(ctx sdk.Context, msg *types.MsgBurn) (types.MsgBurnResponse denomStore := m.keeper.getDenomStore(ctx, msg.Denom) nft := denomStore.Get(msg.Id) - // NOTICE: how to generate coin denom baseDenom := fmt.Sprintf("nft/%s/%s", msg.Denom, msg.Id) - coins := sdk.NewCoins(sdk.NewCoin(baseDenom,1)) + coins := sdk.NewCoins(sdk.NewCoin(baseDenom, 1)) m.keeper.bank.SendCoinsFromAccountToModule(ctx, msg.Owner, types.ModuleName, coins) m.keeper.bank.BurnCoins(ctx, types.ModuleName, coins) - // TODO: how to delete bank.metadata + // TODO: Delete bank.metadata // delete nft denomStore.Delete(msg.Id) return &types.MsgBurnResponse{}, nil From 0dbfbdbda351b4280f21ba6d4290914a3787e241 Mon Sep 17 00:00:00 2001 From: Haifeng Xi Date: Fri, 18 Jun 2021 17:07:14 +0800 Subject: [PATCH 16/22] rename denom as type -- first batch To avoid confusion with coin denom in `x/bank` metadata --- docs/architecture/adr-043-nft-module.md | 38 ++++++++++++------------- 1 file changed, 19 insertions(+), 19 deletions(-) diff --git a/docs/architecture/adr-043-nft-module.md b/docs/architecture/adr-043-nft-module.md index ce1d96951cbc..8715d93ba559 100644 --- a/docs/architecture/adr-043-nft-module.md +++ b/docs/architecture/adr-043-nft-module.md @@ -10,7 +10,7 @@ DRAFT ## Abstract -This ADR defines the `x/nft` module which as a generic implementation of the NFT standard API, with some enhancements. +This ADR defines the `x/nft` module which as a generic implementation of the NFT API, roughly "compatible" with ERC721. ## Context @@ -44,46 +44,46 @@ We will create a module `x/nft`, which contains the following functionality: #### Metadata -We define a general `Metadata` model for `denom`, it is same as an erc721 contract on Ethereum, which manages all nfts in the contract, and is more like a set of nfts. +We define a `Metadata` model for `NFT Type`, which is comparable to an ERC721 contract on Ethereum, under which a collection of `NFT`s can be created and managed. ```protobuf message Metadata { - string denom = 1; // Required, unique key - string symbol = 2; // An abbreviated name for NFTs - string name = 3; // A descriptive name for a collection of NFTs - string description = 4; // Description of NFTs + string type = 1; // Required, unique key, alphanumeric + string name = 2; + string symbol = 3; + string description = 4; } ``` -- The `denom` is the name of the same type of nft, just like the address of an erc721 contract on Ethereum. -- The `symbol` is an abbreviated name for NFTs in this denom. -- The `name` is a descriptive name for a collection of NFTs in this denom. -- The `description` is a detailed description of denom, which extends the function of erc721. +- The `type` is the identifier of the NFT type/class. +- The `name` is a descriptive name of this NFT type. +- The `symbol` is the symbol usually shown on exchanges for this NFT type. +- The `description` is a detailed description of this NFT type. #### NFT -We define a general NFT model and `x/nft` module only stores NFTs by id. +We define a general model for `NFT` as follows. ```protobuf message NFT { - string denom = 1; - string id = 2; - string uri = 3; + string type = 1; // The type of this NFT + string id = 2; // The identifier of this NFT + string uri = 3; google.protobuf.Any data = 4; } ``` The NFT conforms to the following specifications: -- The `id` is an immutable field used as a unique identifier in the same denom, It is generated by the system and may be expanded to DID in the future. NFT identifiers don't currently have a naming convention but can be used in association with existing Hub attributes, e.g., defining an NFT's identifier as an immutable Hub address allows its integration into existing Hub account management modules. +- The `id` is an immutable field used as a unique identifier within the scope of an NFT type. It is specified by the creator of the NFT and may be expanded to use DID in the future. NFT identifiers don't currently have a naming convention but can be used in association with existing Hub attributes, e.g., defining an NFT's identifier as an immutable Hub address allows its integration into existing Hub account management modules. We envision that identifiers can accommodate mint and transfer actions. - The Id is also the primary index for storing NFTs. + The `id` is also the primary index for storing NFTs. ``` - {denom}/{id} --> NFT (bytes) + {type}/{id} --> NFT (bytes) ``` -- The `uri` is the off-chain storage address of nft attribute `data`, such as ipfs, etc. +- The `uri` points to an immutable off-chain resource containing more attributes about his NFT. - The `data` is mutable field and allows attaching special information to the NFT, for example: @@ -376,4 +376,4 @@ Other networks in the Cosmos ecosystem could design and implement their own NFT - Initial discussion: https://github.com/cosmos/cosmos-sdk/discussions/9065 - x/nft: initialize module: https://github.com/cosmos/cosmos-sdk/pull/9174 -- [ADR 033](https://github.com/cosmos/cosmos-sdk/blob/master/docs/architecture/adr-033-protobuf-inter-module-comm.md) \ No newline at end of file +- [ADR 033](https://github.com/cosmos/cosmos-sdk/blob/master/docs/architecture/adr-033-protobuf-inter-module-comm.md) From c8642f4c23a149b77bf9dcf9db6c3124aad3fa55 Mon Sep 17 00:00:00 2001 From: Haifeng Xi Date: Fri, 18 Jun 2021 17:34:51 +0800 Subject: [PATCH 17/22] rename denom as type -- second batch To avoid confusion with coin denom in `x/bank` metadata --- docs/architecture/adr-043-nft-module.md | 109 ++++++++++++------------ 1 file changed, 54 insertions(+), 55 deletions(-) diff --git a/docs/architecture/adr-043-nft-module.md b/docs/architecture/adr-043-nft-module.md index 8715d93ba559..16d5fb8d3c6b 100644 --- a/docs/architecture/adr-043-nft-module.md +++ b/docs/architecture/adr-043-nft-module.md @@ -128,7 +128,7 @@ message MsgSend { message MsgSendResponse {} message MsgBurn { - string denom = 1; + string type = 1; string id = 2; string owner = 3; } @@ -151,70 +151,68 @@ type msgServer struct{ } func (m msgServer) Issue(ctx context.Context, msg *types.MsgIssue) (*types.MsgIssueResponse, error){ - m.keeper.AssertDenomNotExist(msg.Metadata.Denom) + m.keeper.AssertTypeNotExist(msg.Metadata.Type) store := ctx.KVStore(m.keeper.storeKey) bz := m.keeper.cdc.MustMarshalBinaryBare(msg.Metadata) - store.Set(msg.Denom, bz) + store.Set(msg.Type, bz) + return &types.MsgIssueResponse{}, nil } func (m msgServer) Mint(ctx context.Context, msg *types.MsgMint) (*types.MsgMintResponse, error){ - m.keeper.AssertDenomExist(msg.NFT.Denom) + m.keeper.AssertTypeExist(msg.NFT.Type) - metadata := m.keeper.GetMetadata(ctx, msg.NFT.Denom) - // NOTE: we can use denom manager or DID - baseDenom := fmt.Sprintf("nft/%s/%s", msg.NFT.Denom, msg.NFT.Id) + metadata := m.keeper.GetMetadata(ctx, msg.NFT.Type) + + baseDenom := fmt.Sprintf("%s-%s", msg.NFT.Type, msg.NFT.Id) bkMetadata := bankTypes.Metadata{ Symbol: metadata.Symbol, Base: baseDenom, Name: metadata.Name, - URI: msg.NFT.URI, + URI: msg.NFT.Uri, Description: metadata.Description, } - m.keeper.bank.SetDenomMetaData(ctx, bkMetadata) - mintCoins := sdk.NewCoins(sdk.NewCoin(baseDenom,1)) - m.keeper.bank.MintCoins(types.ModuleName, mintCoins) - m.keeper.bank.SendCoinsFromModuleToAccount(types.ModuleName, msg.Owner, mintCoins) + mintedCoins := sdk.NewCoins(sdk.NewCoin(baseDenom, 1)) + m.keeper.bank.MintCoins(types.ModuleName, mintedCoins) + m.keeper.bank.SendCoinsFromModuleToAccount(types.ModuleName, msg.Owner, mintedCoins) bz := m.keeper.cdc.MustMarshalBinaryBare(&msg.NFT) - denomStore := m.keeper.getDenomStore(ctx, msg.NFT.Denom) - denomStore.Set(msg.NFT.Id, bz) + typeStore := m.keeper.getTypeStore(ctx, msg.NFT.Type) + typeStore.Set(msg.NFT.Id, bz) + return nil, nil } func (m msgServer) Send(ctx context.Context, msg *types.MsgSend) (*types.MsgSendResponse, error){ sentCoins := sdk.NewCoins() - for _,nft := range msg.NFTs { - m.keeper.AssertNFTExist(nft.Id) - - denomStore := m.keeper.getDenomStore(ctx, nft.Denom) - nft := denomStore.Get(nft.Id) - bz := m.keeper.cdc.MustMarshalBinaryBare(&nft) - denomStore.Set(nft.Id, bz) - - baseDenom := fmt.Sprintf("nft/%s/%s", nft.Denom, nft.Id) - sentCoins = sentCoins.Add(sdk.NewCoin(baseDenom,1)) + + for _, nft := range msg.NFTs { + m.keeper.AssertNFTExist(nft) + baseDenom := fmt.Sprintf("%s-%s", nft.Type, nft.Id) + sentCoins = sentCoins.Add(sdk.NewCoin(baseDenom, 1)) } m.keeper.bank.SendCoins(ctx, msg.Sender, msg.Reveiver, sentCoins) - return &types.MsgSendResponse{},nil + + return &types.MsgSendResponse{}, nil } func (m Keeper) Burn(ctx sdk.Context, msg *types.MsgBurn) (types.MsgBurnResponse,error) { - m.keeper.AssertNFTExist(msg.Id) + m.keeper.AssertNFTExist(msg.Type, msg.Id) - denomStore := m.keeper.getDenomStore(ctx, msg.Denom) - nft := denomStore.Get(msg.Id) + typeStore := m.keeper.getTypeStore(ctx, msg.Type) + nft := typeStore.Get(msg.Id) - baseDenom := fmt.Sprintf("nft/%s/%s", msg.Denom, msg.Id) + baseDenom := fmt.Sprintf("%s-%s", msg.Type, msg.Id) coins := sdk.NewCoins(sdk.NewCoin(baseDenom, 1)) m.keeper.bank.SendCoinsFromAccountToModule(ctx, msg.Owner, types.ModuleName, coins) m.keeper.bank.BurnCoins(ctx, types.ModuleName, coins) - // TODO: Delete bank.metadata - // delete nft - denomStore.Delete(msg.Id) + // Delete bank.Metadata (keeper method not available) + + typeStore.Delete(msg.Id) + return &types.MsgBurnResponse{}, nil } ``` @@ -236,29 +234,29 @@ service Query { option (google.api.http).get = "/cosmos/nft/v1beta1/nfts"; } - // NFTsOf queries all NFTs based on the denom. + // NFTsOf queries all NFTs based on the type. rpc NFTsOf(QueryNFTsOfRequest) returns (QueryNFTsOfResponse) { - option (google.api.http).get = "/cosmos/nft/v1beta1/nfts/denom/{denom}"; + option (google.api.http).get = "/cosmos/nft/v1beta1/nfts/{type}"; } - // Supply queries the number of nft based on the denom, same as totalSupply of erc721 + // Supply queries the number of nft based on the type, same as totalSupply of ERC721 rpc Supply(QuerySupplyRequest) returns (QuerySupplyResponse) { - option (google.api.http).get = "/cosmos/nft/v1beta1/supply/{denom}"; + option (google.api.http).get = "/cosmos/nft/v1beta1/supply/{type}"; } - // Balance queries the number of based on the owner, same as balanceOf of erc721 + // Balance queries the number of NFTs based on the owner and type, same as balanceOf of ERC721 rpc Balance(QueryBalanceRequest) returns (QueryBalanceResponse) { - option (google.api.http).get = "/cosmos/nft/v1beta1/balance/{owner}"; + option (google.api.http).get = "/cosmos/nft/v1beta1/balance/{owner}/type/{type}"; } - // Denom queries the definition of a given denom - rpc Denom(QueryDenomRequest) returns (QueryDenomResponse) { - option (google.api.http).get = "/cosmos/nft/v1beta1/denoms/{denom}"; + // Type queries the definition of a given type + rpc Type(QueryTypeRequest) returns (QueryTypeResponse) { + option (google.api.http).get = "/cosmos/nft/v1beta1/types/{type}"; } - // Denoms queries all the denoms - rpc Denoms(QueryDenomsRequest) returns (QueryDenomsResponse) { - option (google.api.http).get = "/cosmos/nft/v1beta1/denoms"; + // Types queries all the types + rpc Types(QueryTypesRequest) returns (QueryTypesResponse) { + option (google.api.http).get = "/cosmos/nft/v1beta1/types"; } } @@ -286,7 +284,7 @@ message QueryNFTsResponse { // QueryNFTsOfRequest is the request type for the Query/NFTsOf RPC method message QueryNFTsOfRequest { - string denom = 1; + string type = 1; cosmos.base.query.v1beta1.PageResponse pagination = 2; } @@ -298,7 +296,7 @@ message QueryNFTsOfResponse { // QuerySupplyRequest is the request type for the Query/Supply RPC method message QuerySupplyRequest{ - string denom = 1; + string type = 1; } // QuerySupplyResponse is the response type for the Query/Supply RPC method @@ -309,6 +307,7 @@ message QuerySupplyResponse{ // QueryBalanceRequest is the request type for the Query/Balance RPC method message QueryBalanceRequest{ string owner = 1; + string type = 2; } // QueryBalanceResponse is the response type for the Query/Balance RPC method @@ -316,24 +315,24 @@ message QueryBalanceResponse{ uint64 amount = 1; } -// QueryDenomRequest is the request type for the Query/Denom RPC method -message QueryDenomRequest { - string denom = 1; +// QueryTypeRequest is the request type for the Query/Type RPC method +message QueryTypeRequest { + string type = 1; } -// QueryDenomResponse is the response type for the Query/Denom RPC method -message QueryDenomResponse { +// QueryTypeResponse is the response type for the Query/Type RPC method +message QueryTypeResponse { cosmos.nft.v1beta1.Metadata metadata = 1; } -// QueryDenomsRequest is the request type for the Query/Denoms RPC method -message QueryDenomsRequest { +// QueryTypesRequest is the request type for the Query/Types RPC method +message QueryTypesRequest { // pagination defines an optional pagination for the request. cosmos.base.query.v1beta1.PageRequest pagination = 1; } -// QueryDenomsResponse is the response type for the Query/Denoms RPC method -message QueryDenomsResponse { +// QueryTypesResponse is the response type for the Query/Types RPC method +message QueryTypesResponse { repeated cosmos.nft.v1beta1.Metadata metadatas = 1; cosmos.base.query.v1beta1.PageResponse pagination = 2; } From 5152044d25d19b83751fcdf7fb139682bd1c9b15 Mon Sep 17 00:00:00 2001 From: chengwenxi Date: Fri, 18 Jun 2021 18:22:29 +0800 Subject: [PATCH 18/22] minor fix --- docs/architecture/adr-043-nft-module.md | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/docs/architecture/adr-043-nft-module.md b/docs/architecture/adr-043-nft-module.md index 16d5fb8d3c6b..eebbab9d523e 100644 --- a/docs/architecture/adr-043-nft-module.md +++ b/docs/architecture/adr-043-nft-module.md @@ -14,7 +14,7 @@ This ADR defines the `x/nft` module which as a generic implementation of the NFT ## Context -NFTs are more digital assets than only crypto arts, which is very helpful for accruing value to cosmos . 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. +NFTs are more digital assets than only crypto arts, 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. As was discussed in [#9065](https://github.com/cosmos/cosmos-sdk/discussions/9065), several potential solutions can be considered: @@ -135,13 +135,13 @@ message MsgBurn { message MsgBurnResponse {} ``` -`MsgIssue` is responsible for issuing an nft classification, just like deploying an erc721 contract on Ethereum. +`MsgIssue` is responsible for issuing an NFT classification, just like deploying an erc721 contract on Ethereum. -`MsgMint` provides the ability to create a new nft. +`MsgMint` provides the ability to create a new NFT. `MsgSend` is responsible for transferring the ownership of an NFT to another address (no coins involved). -`MsgBurn` provides the ability to destroy nft, thereby guaranteeing the uniqueness of cross-chain nft. +`MsgBurn` provides the ability to destroy NFT, thereby guaranteeing the uniqueness of cross-chain NFT. Other business-logic implementations should be defined in other upper-level modules that import this NFT module. The implementation example of the server is as follows: @@ -170,9 +170,10 @@ func (m msgServer) Mint(ctx context.Context, msg *types.MsgMint) (*types.MsgMint Symbol: metadata.Symbol, Base: baseDenom, Name: metadata.Name, - URI: msg.NFT.Uri, + URI: msg.NFT.URI, Description: metadata.Description, } + m.keeper.bank.SetDenomMetaData(ctx, bkMetadata) mintedCoins := sdk.NewCoins(sdk.NewCoin(baseDenom, 1)) m.keeper.bank.MintCoins(types.ModuleName, mintedCoins) @@ -210,7 +211,7 @@ func (m Keeper) Burn(ctx sdk.Context, msg *types.MsgBurn) (types.MsgBurnResponse m.keeper.bank.BurnCoins(ctx, types.ModuleName, coins) // Delete bank.Metadata (keeper method not available) - + typeStore := m.keeper.getTypeStore(ctx, msg.NFT.Type) typeStore.Delete(msg.Id) return &types.MsgBurnResponse{}, nil From 0092c538f90f8fe5e7fca82b688f255936ca3270 Mon Sep 17 00:00:00 2001 From: Haifeng Xi Date: Mon, 21 Jun 2021 17:35:31 +0800 Subject: [PATCH 19/22] incorporate feedbacks from 6/18 call 1. MsgSend is really not necessary 2. add two restricted flags in the metadata 3. add MsgEdit --- docs/architecture/adr-043-nft-module.md | 130 +++++++++++++++--------- 1 file changed, 82 insertions(+), 48 deletions(-) diff --git a/docs/architecture/adr-043-nft-module.md b/docs/architecture/adr-043-nft-module.md index eebbab9d523e..b71892a54e53 100644 --- a/docs/architecture/adr-043-nft-module.md +++ b/docs/architecture/adr-043-nft-module.md @@ -35,10 +35,10 @@ The current design is based on the work done by [IRISnet team](https://github.co We will create a module `x/nft`, which contains the following functionality: -- store and transfer the NFTs utilizing `x/bank`, if you want to modify the attribute value of nft, you can use the `send` function in `x/nft`. -- permission, we can utilize `x/authz`. -- mint and burn the NFTs, in this module. -- enumeration, in this module. +- Store and transfer NFTs utilizing `x/bank`. +- Mint and burn NFTs. +- Use `x/authz` to implement ERC721-style authorization. +- Query NFTs and their supply information. ### Types @@ -48,10 +48,12 @@ We define a `Metadata` model for `NFT Type`, which is comparable to an ERC721 co ```protobuf message Metadata { - string type = 1; // Required, unique key, alphanumeric - string name = 2; - string symbol = 3; - string description = 4; + string type = 1; // Required, unique key, alphanumeric + string name = 2; + string symbol = 3; + string description = 4; + bool mint_restricted = 5; + bool edit_restricted = 6; } ``` @@ -59,6 +61,8 @@ message Metadata { - The `name` is a descriptive name of this NFT type. - The `symbol` is the symbol usually shown on exchanges for this NFT type. - The `description` is a detailed description of this NFT type. +- The `mint_restricted` flag, if set to true, indicates that only the issuer of this type can mint NFTs for it. +- The `edit_restricted` flag, if set to true, indicates that NFTs of this type cannot be edited once minted. #### NFT @@ -66,10 +70,10 @@ We define a general model for `NFT` as follows. ```protobuf message NFT { - string type = 1; // The type of this NFT - string id = 2; // The identifier of this NFT - string uri = 3; - google.protobuf.Any data = 4; + string type = 1; // The type of this NFT + string id = 2; // The identifier of this NFT + string uri = 3; + google.protobuf.Any data = 4; } ``` @@ -103,6 +107,7 @@ The NFT conforms to the following specifications: service Msg { rpc Issue(MsgIssue) returns (MsgIssueResponse); rpc Mint(MsgMint) returns (MsgMintResponse); + rpc Edit(MsgEdit) returns (MsgEditResponse); rpc Send(MsgSend) returns (MsgMsgSendResponse); rpc Burn(MsgBurn) returns (MsgBurnResponse); } @@ -115,55 +120,67 @@ message MsgIssueResponse {} message MsgMint { cosmos.nft.v1beta1.NFT nft = 1; - string owner = 2; + string minter = 2; } message MsgMintResponse {} -message MsgSend { - string sender = 1; - string reveiver = 2; - repeated cosmos.nft.v1beta1.NFT nfts = 3; +message MsgEdit { + cosmos.nft.v1beta1.NFT nft = 1; + string editor = 2; } +message MsgEditResponse {} +message MsgSend { + string type = 1; + string id = 2; + string sender = 3; + string reveiver = 4; +} message MsgSendResponse {} message MsgBurn { - string type = 1; - string id = 2; - string owner = 3; + string type = 1; + string id = 2; + string destroyer = 3; } message MsgBurnResponse {} ``` -`MsgIssue` is responsible for issuing an NFT classification, just like deploying an erc721 contract on Ethereum. +`MsgIssue` can be used to issue an NFT type/class, just like deploying an ERC721 contract on Ethereum. + +`MsgMint` allows users to create new NFTs for a given type. -`MsgMint` provides the ability to create a new NFT. +`MsgEdit` allows users to edit/update their NFTs. -`MsgSend` is responsible for transferring the ownership of an NFT to another address (no coins involved). +`MsgSend` can be used to transfer the ownership of an NFT to another address. +**Note**: we could use `x/bank` to handle NFT transfer directly and do without this service. It's only for the sake of completeness of an ERC721 compatible API that we may choose to keep this service. -`MsgBurn` provides the ability to destroy NFT, thereby guaranteeing the uniqueness of cross-chain NFT. +`MsgBurn` allows users to destroy their NFTs. -Other business-logic implementations should be defined in other upper-level modules that import this NFT module. The implementation example of the server is as follows: +Other business logic implementations should be defined in other upper-level modules that import this NFT module. The implementation example of the server is as follows: ```go type msgServer struct{ k Keeper } -func (m msgServer) Issue(ctx context.Context, msg *types.MsgIssue) (*types.MsgIssueResponse, error){ +func (m msgServer) Issue(ctx context.Context, msg *types.MsgIssue) (*types.MsgIssueResponse, error) { m.keeper.AssertTypeNotExist(msg.Metadata.Type) - store := ctx.KVStore(m.keeper.storeKey) bz := m.keeper.cdc.MustMarshalBinaryBare(msg.Metadata) - store.Set(msg.Type, bz) + typeStore := m.keeper.getTypeStore(ctx) + typeStore.Set(msg.Type, bz) + + bz := m.keeper.cdc.MustMarshalBinaryBare(msg.Issuer) + typeOwnerStore := m.keeper.getTypeOwnerStore(ctx) + typeOwnerStore.Set(msg.Type, bz) return &types.MsgIssueResponse{}, nil } -func (m msgServer) Mint(ctx context.Context, msg *types.MsgMint) (*types.MsgMintResponse, error){ +func (m msgServer) Mint(ctx context.Context, msg *types.MsgMint) (*types.MsgMintResponse, error) { m.keeper.AssertTypeExist(msg.NFT.Type) - - metadata := m.keeper.GetMetadata(ctx, msg.NFT.Type) + m.keeper.AssertCanMint(msg.NFT.Type, msg.Minter) baseDenom := fmt.Sprintf("%s-%s", msg.NFT.Type, msg.NFT.Id) bkMetadata := bankTypes.Metadata{ @@ -180,20 +197,34 @@ func (m msgServer) Mint(ctx context.Context, msg *types.MsgMint) (*types.MsgMint m.keeper.bank.SendCoinsFromModuleToAccount(types.ModuleName, msg.Owner, mintedCoins) bz := m.keeper.cdc.MustMarshalBinaryBare(&msg.NFT) - typeStore := m.keeper.getTypeStore(ctx, msg.NFT.Type) - typeStore.Set(msg.NFT.Id, bz) - return nil, nil + nftStoreByType := m.keeper.getNFTStoreByType(ctx, msg.NFT.Type) + nftStoreByType.Set(msg.NFT.Id, bz) + + nftStore := m.keeper.getNFTStore(ctx) + nftStore.Set(baseDenom, bz) + + return &types.MsgMintResponse{}, nil } -func (m msgServer) Send(ctx context.Context, msg *types.MsgSend) (*types.MsgSendResponse, error){ - sentCoins := sdk.NewCoins() +func (m msgServer) Edit(ctx context.Context, msg *types.MsgEdit) (*types.MsgEditResponse, error) { + m.keeper.AssertNFTExist(msg.Type, msg.Id) + m.keeper.AssertCanEdit(msg.Type, msg.Id, msg.Editor) + + bz := m.keeper.cdc.MustMarshalBinaryBare(&msg.NFT) + + nftStoreByType := m.keeper.getNFTStoreByType(ctx, msg.NFT.Type) + nftStoreByType.Set(msg.NFT.Id, bz) - for _, nft := range msg.NFTs { - m.keeper.AssertNFTExist(nft) - baseDenom := fmt.Sprintf("%s-%s", nft.Type, nft.Id) - sentCoins = sentCoins.Add(sdk.NewCoin(baseDenom, 1)) - } + return &types.MsgEditResponse{}, nil +} + +func (m msgServer) Send(ctx context.Context, msg *types.MsgSend) (*types.MsgSendResponse, error) { + m.keeper.AssertNFTExist(msg.Type, msg.Id) + + sentCoins := sdk.NewCoins() + baseDenom := fmt.Sprintf("%s-%s", nft.Type, nft.Id) + sentCoins = sentCoins.Add(sdk.NewCoin(baseDenom, 1)) m.keeper.bank.SendCoins(ctx, msg.Sender, msg.Reveiver, sentCoins) return &types.MsgSendResponse{}, nil @@ -202,18 +233,22 @@ func (m msgServer) Send(ctx context.Context, msg *types.MsgSend) (*types.MsgSend func (m Keeper) Burn(ctx sdk.Context, msg *types.MsgBurn) (types.MsgBurnResponse,error) { m.keeper.AssertNFTExist(msg.Type, msg.Id) - typeStore := m.keeper.getTypeStore(ctx, msg.Type) - nft := typeStore.Get(msg.Id) + nftStoreByType := m.keeper.getNFTStoreByType(ctx, msg.Type) + nft := nftStoreByType.Get(msg.Id) baseDenom := fmt.Sprintf("%s-%s", msg.Type, msg.Id) coins := sdk.NewCoins(sdk.NewCoin(baseDenom, 1)) - m.keeper.bank.SendCoinsFromAccountToModule(ctx, msg.Owner, types.ModuleName, coins) + m.keeper.bank.SendCoinsFromAccountToModule(ctx, msg.Destroyer, types.ModuleName, coins) m.keeper.bank.BurnCoins(ctx, types.ModuleName, coins) // Delete bank.Metadata (keeper method not available) - typeStore := m.keeper.getTypeStore(ctx, msg.NFT.Type) - typeStore.Delete(msg.Id) + nftStoreByType := m.keeper.getNFTStoreByType(ctx, msg.NFT.Type) + nftStoreByType.Delete(msg.Id) + + nftStore := m.keeper.getNFTStore(ctx) + nftStore.Delete(baseDenom) + return &types.MsgBurnResponse{}, nil } ``` @@ -247,7 +282,7 @@ service Query { // Balance queries the number of NFTs based on the owner and type, same as balanceOf of ERC721 rpc Balance(QueryBalanceRequest) returns (QueryBalanceResponse) { - option (google.api.http).get = "/cosmos/nft/v1beta1/balance/{owner}/type/{type}"; + option (google.api.http).get = "/cosmos/nft/v1beta1/balance/{owner}/{type}"; } // Type queries the definition of a given type @@ -357,7 +392,6 @@ This specification conforms to the ERC-721 smart contract specification for NFT ### Negative -- Currently, no methods are defined for this module except to store and retrieve data. ### Neutral From 843f79c5ae60213d52d961500643473f4c9b6959 Mon Sep 17 00:00:00 2001 From: Haifeng Xi Date: Thu, 1 Jul 2021 16:13:29 +0800 Subject: [PATCH 20/22] incorporate Billy's feedback And make the spec better aligned with ERC721, ERC721Metadata and ERC721Enumerable --- docs/architecture/adr-043-nft-module.md | 358 ++++++++---------------- 1 file changed, 121 insertions(+), 237 deletions(-) diff --git a/docs/architecture/adr-043-nft-module.md b/docs/architecture/adr-043-nft-module.md index b71892a54e53..e8c73098a46a 100644 --- a/docs/architecture/adr-043-nft-module.md +++ b/docs/architecture/adr-043-nft-module.md @@ -3,6 +3,7 @@ ## Changelog - 05.05.2021: Initial Draft +- 07.01.2021: Incorporate Billy's feedback ## Status @@ -10,7 +11,7 @@ DRAFT ## Abstract -This ADR defines the `x/nft` module which as a generic implementation of the NFT API, roughly "compatible" with ERC721. +This ADR defines the `x/nft` module which is a generic storage of NFTs, roughly "compatible" with ERC721. ## Context @@ -23,7 +24,7 @@ As was discussed in [#9065](https://github.com/cosmos/cosmos-sdk/discussions/906 - DID NFTs - interNFT -Since NFTs functions/use cases 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. +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. @@ -35,34 +36,36 @@ The current design is based on the work done by [IRISnet team](https://github.co We will create a module `x/nft`, which contains the following functionality: -- Store and transfer NFTs utilizing `x/bank`. -- Mint and burn NFTs. -- Use `x/authz` to implement ERC721-style authorization. +- Store NFTs and track their ownership. +- Expose `Keeper` interface for composing modules to mint and burn NFTs. +- Expose external `Message` interface for users to transfer ownership of their NFTs. - Query NFTs and their supply information. ### Types -#### Metadata +#### Genre -We define a `Metadata` model for `NFT Type`, which is comparable to an ERC721 contract on Ethereum, under which a collection of `NFT`s can be created and managed. +We define a model for NFT **Genre**, which is comparable to an ERC721 Contract on Ethereum, under which a collection of NFTs can be created and managed. ```protobuf -message Metadata { - string type = 1; // Required, unique key, alphanumeric - string name = 2; - string symbol = 3; - string description = 4; - bool mint_restricted = 5; - bool edit_restricted = 6; +message Genre { + string id = 1; + string name = 2; + string symbol = 3; + string description = 4; + string uri = 5; + bool mint_restricted = 10; + bool update_restricted = 11; } ``` -- The `type` is the identifier of the NFT type/class. -- The `name` is a descriptive name of this NFT type. -- The `symbol` is the symbol usually shown on exchanges for this NFT type. -- The `description` is a detailed description of this NFT type. -- The `mint_restricted` flag, if set to true, indicates that only the issuer of this type can mint NFTs for it. -- The `edit_restricted` flag, if set to true, indicates that NFTs of this type cannot be edited once minted. +- `id` is an alphanumeric identifier of the NFT genre; it is used as the primary index for storing the genre; _required_ +- `name` is a descriptive name of the NFT genre; _optional_ +- `symbol` is the symbol usually shown on exchanges for the NFT genre; _optional_ +- `description` is a detailed description of the NFT genre; _optional_ +- `uri` is a URL pointing to an off-chain JSON file that contains metadata about this NFT genre ([OpenSea example](https://docs.opensea.io/docs/contract-level-metadata)); _optional_ +- `mint_restricted` flag, if set to true, indicates that only the genre owner can mint NFTs, otherwise anyone can do so; _required_ +- `udpate_restricted` flag, if set to true, indicates that no one can update NFTs, otherwise only NFT owners can do so; _required_ #### NFT @@ -70,306 +73,187 @@ We define a general model for `NFT` as follows. ```protobuf message NFT { - string type = 1; // The type of this NFT - string id = 2; // The identifier of this NFT - string uri = 3; - google.protobuf.Any data = 4; + string genre = 1; + string id = 2; + string uri = 3; + string data = 10; } ``` -The NFT conforms to the following specifications: - -- The `id` is an immutable field used as a unique identifier within the scope of an NFT type. It is specified by the creator of the NFT and may be expanded to use DID in the future. NFT identifiers don't currently have a naming convention but can be used in association with existing Hub attributes, e.g., defining an NFT's identifier as an immutable Hub address allows its integration into existing Hub account management modules. - We envision that identifiers can accommodate mint and transfer actions. - The `id` is also the primary index for storing NFTs. - +- `genre` is identifier of genre where the NFT belongs; _required_ +- `id` is an alphanumeric identifier of the NFT, unique within the scope of its genre. It is specified by the creator of the NFT and may be expanded to use DID in the future. `genre` combined with `id` uniquely identifies an NFT and is used as the primary index for storing the NFT; _required_ ``` - {type}/{id} --> NFT (bytes) + {genre}/{id} --> NFT (bytes) ``` +- `uri` is a URL pointing to an off-chain JSON file that contains metadata about this NFT (Ref: [ERC721 standard and OpenSea extension](https://docs.opensea.io/docs/metadata-standards)). +- `data` is a field that CAN be used by composing modules to specify additional properties for the NFT; _optional_ -- The `uri` points to an immutable off-chain resource containing more attributes about his NFT. - -- The `data` is mutable field and allows attaching special information to the NFT, for example: +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. - - metadata such as the title of the work and URI. - - immutable data and parameters (such actual NFT data, hash or seed for generators). - - mutable data and parameters that change when transferring or when certain criteria are met (such as provenance). - - This ADR doesn't specify values that this field 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. - -- The ownership of nft is controlled by the `x/bank` module and the `metadata` part will be converted to `banktypes.Metadata` is stored in the `x/bank` module. +### `Keeper` Interface (TODO) +Other business logic implementations should be defined in composing modules that import this NFT module and use its `Keeper`. ### `Msg` Service ```protobuf service Msg { - rpc Issue(MsgIssue) returns (MsgIssueResponse); - rpc Mint(MsgMint) returns (MsgMintResponse); - rpc Edit(MsgEdit) returns (MsgEditResponse); - rpc Send(MsgSend) returns (MsgMsgSendResponse); - rpc Burn(MsgBurn) returns (MsgBurnResponse); -} - -message MsgIssue { - cosmos.nft.v1beta1.Metadata metadata = 1; - string issuer = 2; -} -message MsgIssueResponse {} - -message MsgMint { - cosmos.nft.v1beta1.NFT nft = 1; - string minter = 2; -} -message MsgMintResponse {} - -message MsgEdit { - cosmos.nft.v1beta1.NFT nft = 1; - string editor = 2; + rpc Send(MsgSend) returns (MsgSendResponse); } -message MsgEditResponse {} message MsgSend { - string type = 1; + string genre = 1; string id = 2; string sender = 3; string reveiver = 4; } message MsgSendResponse {} -message MsgBurn { - string type = 1; - string id = 2; - string destroyer = 3; -} -message MsgBurnResponse {} -``` - -`MsgIssue` can be used to issue an NFT type/class, just like deploying an ERC721 contract on Ethereum. - -`MsgMint` allows users to create new NFTs for a given type. - -`MsgEdit` allows users to edit/update their NFTs. - `MsgSend` can be used to transfer the ownership of an NFT to another address. -**Note**: we could use `x/bank` to handle NFT transfer directly and do without this service. It's only for the sake of completeness of an ERC721 compatible API that we may choose to keep this service. - -`MsgBurn` allows users to destroy their NFTs. -Other business logic implementations should be defined in other upper-level modules that import this NFT module. The implementation example of the server is as follows: +The implementation outline of the server is as follows: ```go type msgServer struct{ k Keeper } -func (m msgServer) Issue(ctx context.Context, msg *types.MsgIssue) (*types.MsgIssueResponse, error) { - m.keeper.AssertTypeNotExist(msg.Metadata.Type) - - bz := m.keeper.cdc.MustMarshalBinaryBare(msg.Metadata) - typeStore := m.keeper.getTypeStore(ctx) - typeStore.Set(msg.Type, bz) - - bz := m.keeper.cdc.MustMarshalBinaryBare(msg.Issuer) - typeOwnerStore := m.keeper.getTypeOwnerStore(ctx) - typeOwnerStore.Set(msg.Type, bz) - - return &types.MsgIssueResponse{}, nil -} - -func (m msgServer) Mint(ctx context.Context, msg *types.MsgMint) (*types.MsgMintResponse, error) { - m.keeper.AssertTypeExist(msg.NFT.Type) - m.keeper.AssertCanMint(msg.NFT.Type, msg.Minter) - - baseDenom := fmt.Sprintf("%s-%s", msg.NFT.Type, msg.NFT.Id) - bkMetadata := bankTypes.Metadata{ - Symbol: metadata.Symbol, - Base: baseDenom, - Name: metadata.Name, - URI: msg.NFT.URI, - Description: metadata.Description, - } - - m.keeper.bank.SetDenomMetaData(ctx, bkMetadata) - mintedCoins := sdk.NewCoins(sdk.NewCoin(baseDenom, 1)) - m.keeper.bank.MintCoins(types.ModuleName, mintedCoins) - m.keeper.bank.SendCoinsFromModuleToAccount(types.ModuleName, msg.Owner, mintedCoins) - - bz := m.keeper.cdc.MustMarshalBinaryBare(&msg.NFT) - - nftStoreByType := m.keeper.getNFTStoreByType(ctx, msg.NFT.Type) - nftStoreByType.Set(msg.NFT.Id, bz) - - nftStore := m.keeper.getNFTStore(ctx) - nftStore.Set(baseDenom, bz) - - return &types.MsgMintResponse{}, nil -} - -func (m msgServer) Edit(ctx context.Context, msg *types.MsgEdit) (*types.MsgEditResponse, error) { - m.keeper.AssertNFTExist(msg.Type, msg.Id) - m.keeper.AssertCanEdit(msg.Type, msg.Id, msg.Editor) - - bz := m.keeper.cdc.MustMarshalBinaryBare(&msg.NFT) - - nftStoreByType := m.keeper.getNFTStoreByType(ctx, msg.NFT.Type) - nftStoreByType.Set(msg.NFT.Id, bz) - - return &types.MsgEditResponse{}, nil -} - func (m msgServer) Send(ctx context.Context, msg *types.MsgSend) (*types.MsgSendResponse, error) { - m.keeper.AssertNFTExist(msg.Type, msg.Id) - - sentCoins := sdk.NewCoins() - baseDenom := fmt.Sprintf("%s-%s", nft.Type, nft.Id) - sentCoins = sentCoins.Add(sdk.NewCoin(baseDenom, 1)) - m.keeper.bank.SendCoins(ctx, msg.Sender, msg.Reveiver, sentCoins) - - return &types.MsgSendResponse{}, nil -} - -func (m Keeper) Burn(ctx sdk.Context, msg *types.MsgBurn) (types.MsgBurnResponse,error) { - m.keeper.AssertNFTExist(msg.Type, msg.Id) - - nftStoreByType := m.keeper.getNFTStoreByType(ctx, msg.Type) - nft := nftStoreByType.Get(msg.Id) + // check current ownership + assertEqual(msg.Sender, m.k.GetNftOwner(msg.Genre, msg.Id)) - baseDenom := fmt.Sprintf("%s-%s", msg.Type, msg.Id) - coins := sdk.NewCoins(sdk.NewCoin(baseDenom, 1)) - m.keeper.bank.SendCoinsFromAccountToModule(ctx, msg.Destroyer, types.ModuleName, coins) - m.keeper.bank.BurnCoins(ctx, types.ModuleName, coins) + // change ownership mapping + m.k.SetNftOwner(msg.Genre, msg.Id, msg.Receiver) - // Delete bank.Metadata (keeper method not available) - - nftStoreByType := m.keeper.getNFTStoreByType(ctx, msg.NFT.Type) - nftStoreByType.Delete(msg.Id) - - nftStore := m.keeper.getNFTStore(ctx) - nftStore.Delete(baseDenom) - - return &types.MsgBurnResponse{}, nil + return &types.MsgSendResponse{}, nil } -``` - -The upper application calls those methods by holding the MsgClient instance of the `x/nft` module. The execution authority of msg is guaranteed by the OCAPs mechanism. The query service methods for the `x/nft` module are: ```proto service Query { - // NFT queries NFT details based on id. - rpc NFT(QueryNFTRequest) returns (QueryNFTResponse) { - option (google.api.http).get = "/cosmos/nft/v1beta1/nfts/{id}"; + // Balance queries the number of NFTs based on the genre and owner, same as balanceOf in ERC721 + rpc Balance(QueryBalanceRequest) returns (QueryBalanceResponse) { + option (google.api.http).get = "/cosmos/nft/v1beta1/balance/{genre}/{owner}"; } - // NFTs queries all NFTs based on the optional owner. - rpc NFTs(QueryNFTsRequest) returns (QueryNFTsResponse) { - option (google.api.http).get = "/cosmos/nft/v1beta1/nfts"; + // Owner queries the owner of the NFT based on the genre and id, same as ownerOf in ERC721 + rpc Owner(QueryOwnerRequest) returns (QueryOwnerResponse) { + option (google.api.http).get = "/cosmos/nft/v1beta1/owner/{genre}/{id}"; } - // NFTsOf queries all NFTs based on the type. - rpc NFTsOf(QueryNFTsOfRequest) returns (QueryNFTsOfResponse) { - option (google.api.http).get = "/cosmos/nft/v1beta1/nfts/{type}"; + // Supply queries the number of NFTs based on the genre, same as totalSupply in ERC721Enumerable + rpc Supply(QuerySupplyRequest) returns (QuerySupplyResponse) { + option (google.api.http).get = "/cosmos/nft/v1beta1/supply/{genre}"; } - // Supply queries the number of nft based on the type, same as totalSupply of ERC721 - rpc Supply(QuerySupplyRequest) returns (QuerySupplyResponse) { - option (google.api.http).get = "/cosmos/nft/v1beta1/supply/{type}"; + // NFTsOf queries all NFTs based on the genre, similar to tokenByIndex in ERC721Enumerable + rpc NFTs(QueryNFTsRequest) returns (QueryNFTsResponse) { + option (google.api.http).get = "/cosmos/nft/v1beta1/nfts/{genre}"; } - // Balance queries the number of NFTs based on the owner and type, same as balanceOf of ERC721 - rpc Balance(QueryBalanceRequest) returns (QueryBalanceResponse) { - option (google.api.http).get = "/cosmos/nft/v1beta1/balance/{owner}/{type}"; + // NFTsOfOwner queries the NFTs based on the genre and owner, similar to tokenOfOwnerByIndex in ERC721Enumerable + rpc NFTsOfOwner(QueryNFTsOfOwnerRequest) returns (QueryNFTsResponse) { + option (google.api.http).get = "/cosmos/nft/v1beta1/balance/{genre}/{owner}"; } - // Type queries the definition of a given type - rpc Type(QueryTypeRequest) returns (QueryTypeResponse) { - option (google.api.http).get = "/cosmos/nft/v1beta1/types/{type}"; + // NFT queries NFT details based on genre and id. + rpc NFT(QueryNFTRequest) returns (QueryNFTResponse) { + option (google.api.http).get = "/cosmos/nft/v1beta1/nfts/{genre}/{id}"; + } + + // Genre queries the definition of a given genre + rpc Genre(QueryGenreRequest) returns (QueryGenreResponse) { + option (google.api.http).get = "/cosmos/nft/v1beta1/genres/{genre}"; } - // Types queries all the types - rpc Types(QueryTypesRequest) returns (QueryTypesResponse) { - option (google.api.http).get = "/cosmos/nft/v1beta1/types"; + // Types queries all the genres + rpc Genres(QueryGenresRequest) returns (QueryGenresResponse) { + option (google.api.http).get = "/cosmos/nft/v1beta1/genres"; } } -// QueryNFTRequest is the request type for the Query/NFT RPC method -message QueryNFTRequest { - string id = 1; +// QueryBalanceRequest is the request type for the Query/Balance RPC method +message QueryBalanceRequest { + string genre = 1; + string owner = 2; } -// QueryNFTResponse is the response type for the Query/NFT RPC method -message QueryNFTResponse { - cosmos.nft.v1beta1.NFT nft = 1; +// QueryBalanceResponse is the response type for the Query/Balance RPC method +message QueryBalanceResponse{ + uint64 amount = 1; } -// QueryNFTsRequest is the request type for the Query/NFTs RPC method -message QueryNFTsRequest { - string owner = 1; - cosmos.base.query.v1beta1.PageResponse pagination = 2; +// QueryOwnerRequest is the request type for the Query/Owner RPC method +message QueryOwnerRequest { + string genre = 1; + string id = 2; } -// QueryNFTsResponse is the response type for the Query/NFTs RPC method -message QueryNFTsResponse { - repeated cosmos.nft.v1beta1.NFT nfts = 1; - cosmos.base.query.v1beta1.PageResponse pagination = 2; +// QueryOwnerResponse is the response type for the Query/Owner RPC method +message QueryOwnerResponse{ + string owner = 1; } -// QueryNFTsOfRequest is the request type for the Query/NFTsOf RPC method -message QueryNFTsOfRequest { - string type = 1; - cosmos.base.query.v1beta1.PageResponse pagination = 2; +// QuerySupplyRequest is the request type for the Query/Supply RPC method +message QuerySupplyRequest { + string genre = 1; } -// QueryNFTsOfResponse is the response type for the Query/NFTsOf RPC method -message QueryNFTsOfResponse { - repeated cosmos.nft.v1beta1.NFT nfts = 1; +// QuerySupplyResponse is the response type for the Query/Supply RPC method +message QuerySupplyResponse { + uint64 amount = 1; +} + +// QueryNFTsRequest is the request type for the Query/NFTs RPC method +message QueryNFTsRequest { + string genre = 1; cosmos.base.query.v1beta1.PageResponse pagination = 2; } -// QuerySupplyRequest is the request type for the Query/Supply RPC method -message QuerySupplyRequest{ - string type = 1; +// QueryNFTsOfOwnerRequest is the request type for the Query/NFTsOfOwner RPC method +message QueryNFTsOfOwnerRequest { + string genre = 1; + string owner = 2; + cosmos.base.query.v1beta1.PageResponse pagination = 3; } -// QuerySupplyResponse is the response type for the Query/Supply RPC method -message QuerySupplyResponse{ - uint64 amount = 1; +// QueryNFTsResponse is the response type for the Query/NFTs and Query/NFTsOfOwner RPC method +message QueryNFTsResponse { + repeated cosmos.nft.v1beta1.NFT nfts = 1; + cosmos.base.query.v1beta1.PageResponse pagination = 2; } -// QueryBalanceRequest is the request type for the Query/Balance RPC method -message QueryBalanceRequest{ - string owner = 1; - string type = 2; +// QueryNFTRequest is the request type for the Query/NFT RPC method +message QueryNFTRequest { + string genre = 1; + string id = 2; } -// QueryBalanceResponse is the response type for the Query/Balance RPC method -message QueryBalanceResponse{ - uint64 amount = 1; +// QueryNFTResponse is the response type for the Query/NFT RPC method +message QueryNFTResponse { + cosmos.nft.v1beta1.NFT nft = 1; } -// QueryTypeRequest is the request type for the Query/Type RPC method -message QueryTypeRequest { - string type = 1; +// QueryGenreRequest is the request type for the Query/Genre RPC method +message QueryGenreRequest { + string genre = 1; } -// QueryTypeResponse is the response type for the Query/Type RPC method -message QueryTypeResponse { - cosmos.nft.v1beta1.Metadata metadata = 1; +// QueryGenreResponse is the response type for the Query/Genre RPC method +message QueryGenreResponse { + cosmos.nft.v1beta1.Genre genre = 1; } -// QueryTypesRequest is the request type for the Query/Types RPC method -message QueryTypesRequest { +// QueryGenresRequest is the request type for the Query/Genres RPC method +message QueryGenresRequest { // pagination defines an optional pagination for the request. cosmos.base.query.v1beta1.PageRequest pagination = 1; } -// QueryTypesResponse is the response type for the Query/Types RPC method -message QueryTypesResponse { - repeated cosmos.nft.v1beta1.Metadata metadatas = 1; +// QueryGenresResponse is the response type for the Query/Genres RPC method +message QueryGenresResponse { + repeated cosmos.nft.v1beta1.Genre genres = 1; cosmos.base.query.v1beta1.PageResponse pagination = 2; } ``` From c98ce3abdb9d2793605547574314c468eabe2f6b Mon Sep 17 00:00:00 2001 From: Haifeng Xi Date: Thu, 1 Jul 2021 16:15:59 +0800 Subject: [PATCH 21/22] fix code formatting --- docs/architecture/adr-043-nft-module.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/architecture/adr-043-nft-module.md b/docs/architecture/adr-043-nft-module.md index e8c73098a46a..cce0a2e3ca8b 100644 --- a/docs/architecture/adr-043-nft-module.md +++ b/docs/architecture/adr-043-nft-module.md @@ -107,6 +107,7 @@ message MsgSend { string reveiver = 4; } message MsgSendResponse {} +``` `MsgSend` can be used to transfer the ownership of an NFT to another address. @@ -126,6 +127,7 @@ func (m msgServer) Send(ctx context.Context, msg *types.MsgSend) (*types.MsgSend return &types.MsgSendResponse{}, nil } +``` The query service methods for the `x/nft` module are: From 013e9caeb4064667be95e36e6961f90452942051 Mon Sep 17 00:00:00 2001 From: Haifeng Xi Date: Sat, 3 Jul 2021 00:40:44 +0800 Subject: [PATCH 22/22] incorporate feedbacks from aaron, shaun, billy... --- docs/architecture/adr-043-nft-module.md | 183 +++++++++++++----------- 1 file changed, 102 insertions(+), 81 deletions(-) diff --git a/docs/architecture/adr-043-nft-module.md b/docs/architecture/adr-043-nft-module.md index cce0a2e3ca8b..814be6cb1898 100644 --- a/docs/architecture/adr-043-nft-module.md +++ b/docs/architecture/adr-043-nft-module.md @@ -4,6 +4,7 @@ - 05.05.2021: Initial Draft - 07.01.2021: Incorporate Billy's feedback +- 07.02.2021: Incorporate feedbacks from Aaron, Shaun, Billy et al. ## Status @@ -11,11 +12,11 @@ DRAFT ## Abstract -This ADR defines the `x/nft` module which is a generic storage of NFTs, roughly "compatible" with ERC721. +This ADR defines the `x/nft` module which is a generic implementation of NFTs, roughly "compatible" with ERC721. ## Context -NFTs are more digital assets than only crypto arts, 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. +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. As was discussed in [#9065](https://github.com/cosmos/cosmos-sdk/discussions/9065), several potential solutions can be considered: @@ -43,29 +44,25 @@ We will create a module `x/nft`, which contains the following functionality: ### Types -#### Genre +#### Class -We define a model for NFT **Genre**, which is comparable to an ERC721 Contract on Ethereum, under which a collection of NFTs can be created and managed. +We define a model for NFT **Class**, which is comparable to an ERC721 Contract on Ethereum, under which a collection of NFTs can be created and managed. ```protobuf -message Genre { - string id = 1; - string name = 2; - string symbol = 3; - string description = 4; - string uri = 5; - bool mint_restricted = 10; - bool update_restricted = 11; +message Class { + string id = 1; + string name = 2; + string symbol = 3; + string description = 4; + string uri = 5; } ``` -- `id` is an alphanumeric identifier of the NFT genre; it is used as the primary index for storing the genre; _required_ -- `name` is a descriptive name of the NFT genre; _optional_ -- `symbol` is the symbol usually shown on exchanges for the NFT genre; _optional_ -- `description` is a detailed description of the NFT genre; _optional_ -- `uri` is a URL pointing to an off-chain JSON file that contains metadata about this NFT genre ([OpenSea example](https://docs.opensea.io/docs/contract-level-metadata)); _optional_ -- `mint_restricted` flag, if set to true, indicates that only the genre owner can mint NFTs, otherwise anyone can do so; _required_ -- `udpate_restricted` flag, if set to true, indicates that no one can update NFTs, otherwise only NFT owners can do so; _required_ +- `id` is an alphanumeric identifier of the NFT class; it 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 URL pointing to an off-chain JSON file that contains metadata about this NFT class ([OpenSea example](https://docs.opensea.io/docs/contract-level-metadata)); _optional_ #### NFT @@ -73,25 +70,49 @@ We define a general model for `NFT` as follows. ```protobuf message NFT { - string genre = 1; - string id = 2; - string uri = 3; - string data = 10; + string class_id = 1; + string id = 2; + string uri = 3; + google.protobuf.Any data = 10; } ``` -- `genre` is identifier of genre where the NFT belongs; _required_ -- `id` is an alphanumeric identifier of the NFT, unique within the scope of its genre. It is specified by the creator of the NFT and may be expanded to use DID in the future. `genre` combined with `id` uniquely identifies an NFT and is used as the primary index for storing the NFT; _required_ +- `class_id` is the identifier of the NFT class where the NFT belongs; _required_ +- `id` is an alphanumeric 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_ ``` - {genre}/{id} --> NFT (bytes) + {class_id}/{id} --> NFT (bytes) ``` -- `uri` is a URL pointing to an off-chain JSON file that contains metadata about this NFT (Ref: [ERC721 standard and OpenSea extension](https://docs.opensea.io/docs/metadata-standards)). +- `uri` is a URL pointing to an off-chain JSON file that contains metadata about this NFT (Ref: [ERC721 standard and OpenSea extension](https://docs.opensea.io/docs/metadata-standards)); _required_ - `data` is a field that CAN be used by composing modules to specify additional properties for 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. -### `Keeper` Interface (TODO) -Other business logic implementations should be defined in composing modules that import this NFT module and use its `Keeper`. +### `Keeper` Interface + +```go +type Keeper interface { + NewClass(class Class) + UpdateClass(class Class) + + Mint(nft NFT,receiver sdk.AccAddress) // updates totalSupply + Burn(classId string, nftId string) // updates totalSupply + Update(nft NFT) + Transfer(classId string, nftId string, receiver sdk.AccAddress) + + GetClass(classId string) Class + GetClasses() []Class + + GetNFT(classId string, nftId string) NFT + GetNFTsOfClassByOwner(classId string, owner sdk.AccAddress) []NFT + GetNFTsOfClass(classId string) []NFT + + GetOwner(classId string, nftId string) sdk.AccAddress + GetBalance(classId string, owner sdk.AccAddress) uint64 + GetTotalSupply(classId string) uint64 +} +``` + +Other business logic implementations should be defined in composing modules that import `x/nft` and use its `Keeper`. ### `Msg` Service @@ -101,7 +122,7 @@ service Msg { } message MsgSend { - string genre = 1; + string class_id = 1; string id = 2; string sender = 3; string reveiver = 4; @@ -120,10 +141,10 @@ type msgServer struct{ func (m msgServer) Send(ctx context.Context, msg *types.MsgSend) (*types.MsgSendResponse, error) { // check current ownership - assertEqual(msg.Sender, m.k.GetNftOwner(msg.Genre, msg.Id)) + assertEqual(msg.Sender, m.k.GetOwner(msg.ClassId, msg.Id)) - // change ownership mapping - m.k.SetNftOwner(msg.Genre, msg.Id, msg.Receiver) + // transfer ownership + m.k.Transfer(msg.ClassId, msg.Id, msg.Receiver) return &types.MsgSendResponse{}, nil } @@ -134,51 +155,51 @@ The query service methods for the `x/nft` module are: ```proto service Query { - // Balance queries the number of NFTs based on the genre and owner, same as balanceOf in ERC721 + // 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/{genre}/{owner}"; + option (google.api.http).get = "/cosmos/nft/v1beta1/balance/{class_id}/{owner}"; } - // Owner queries the owner of the NFT based on the genre and id, same as ownerOf in ERC721 + // 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/{genre}/{id}"; + option (google.api.http).get = "/cosmos/nft/v1beta1/owner/{class_id}/{id}"; } - // Supply queries the number of NFTs based on the genre, same as totalSupply in ERC721Enumerable + // Supply queries the number of NFTs of a given class, same as totalSupply in ERC721Enumerable rpc Supply(QuerySupplyRequest) returns (QuerySupplyResponse) { - option (google.api.http).get = "/cosmos/nft/v1beta1/supply/{genre}"; + option (google.api.http).get = "/cosmos/nft/v1beta1/supply/{class_id}"; } - // NFTsOf queries all NFTs based on the genre, similar to tokenByIndex in ERC721Enumerable - rpc NFTs(QueryNFTsRequest) returns (QueryNFTsResponse) { - option (google.api.http).get = "/cosmos/nft/v1beta1/nfts/{genre}"; + // NFTsOfClassByOwner queries the NFTs of a given class owned by the owner, similar to tokenOfOwnerByIndex in ERC721Enumerable + rpc NFTsOfClassByOwner(QueryNFTsOfClassByOwnerRequest) returns (QueryNFTsResponse) { + option (google.api.http).get = "/cosmos/nft/v1beta1/owned_nfts/{class_id}/{owner}"; } - // NFTsOfOwner queries the NFTs based on the genre and owner, similar to tokenOfOwnerByIndex in ERC721Enumerable - rpc NFTsOfOwner(QueryNFTsOfOwnerRequest) returns (QueryNFTsResponse) { - option (google.api.http).get = "/cosmos/nft/v1beta1/balance/{genre}/{owner}"; + // NFTsOfClass queries all NFTs of a given class, similar to tokenByIndex in ERC721Enumerable + rpc NFTsOfClass(QueryNFTsOfClassRequest) returns (QueryNFTsResponse) { + option (google.api.http).get = "/cosmos/nft/v1beta1/nfts/{class_id}"; } - // NFT queries NFT details based on genre and id. + // NFT queries an NFT based on its class and id. rpc NFT(QueryNFTRequest) returns (QueryNFTResponse) { - option (google.api.http).get = "/cosmos/nft/v1beta1/nfts/{genre}/{id}"; + option (google.api.http).get = "/cosmos/nft/v1beta1/nfts/{class_id}/{id}"; } - // Genre queries the definition of a given genre - rpc Genre(QueryGenreRequest) returns (QueryGenreResponse) { - option (google.api.http).get = "/cosmos/nft/v1beta1/genres/{genre}"; + // 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}"; } - // Types queries all the genres - rpc Genres(QueryGenresRequest) returns (QueryGenresResponse) { - option (google.api.http).get = "/cosmos/nft/v1beta1/genres"; + // 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 method message QueryBalanceRequest { - string genre = 1; - string owner = 2; + string class_id = 1; + string owner = 2; } // QueryBalanceResponse is the response type for the Query/Balance RPC method @@ -188,8 +209,8 @@ message QueryBalanceResponse{ // QueryOwnerRequest is the request type for the Query/Owner RPC method message QueryOwnerRequest { - string genre = 1; - string id = 2; + string class_id = 1; + string id = 2; } // QueryOwnerResponse is the response type for the Query/Owner RPC method @@ -199,7 +220,7 @@ message QueryOwnerResponse{ // QuerySupplyRequest is the request type for the Query/Supply RPC method message QuerySupplyRequest { - string genre = 1; + string class_id = 1; } // QuerySupplyResponse is the response type for the Query/Supply RPC method @@ -207,20 +228,20 @@ message QuerySupplyResponse { uint64 amount = 1; } -// QueryNFTsRequest is the request type for the Query/NFTs RPC method -message QueryNFTsRequest { - string genre = 1; - cosmos.base.query.v1beta1.PageResponse pagination = 2; -} - -// QueryNFTsOfOwnerRequest is the request type for the Query/NFTsOfOwner RPC method -message QueryNFTsOfOwnerRequest { - string genre = 1; +// QueryNFTsOfClassByOwnerRequest is the request type for the Query/NFTsOfClassByOwner RPC method +message QueryNFTsOfClassByOwnerRequest { + string class_id = 1; string owner = 2; cosmos.base.query.v1beta1.PageResponse pagination = 3; } -// QueryNFTsResponse is the response type for the Query/NFTs and Query/NFTsOfOwner RPC method +// QueryNFTsOfClassRequest is the request type for the Query/NFTsOfClass RPC method +message QueryNFTsOfClassRequest { + string class_id = 1; + cosmos.base.query.v1beta1.PageResponse pagination = 2; +} + +// QueryNFTsResponse is the response type for the Query/NFTsOfClass and Query/NFTsOfClassByOwner RPC methods message QueryNFTsResponse { repeated cosmos.nft.v1beta1.NFT nfts = 1; cosmos.base.query.v1beta1.PageResponse pagination = 2; @@ -228,8 +249,8 @@ message QueryNFTsResponse { // QueryNFTRequest is the request type for the Query/NFT RPC method message QueryNFTRequest { - string genre = 1; - string id = 2; + string class_id = 1; + string id = 2; } // QueryNFTResponse is the response type for the Query/NFT RPC method @@ -237,25 +258,25 @@ message QueryNFTResponse { cosmos.nft.v1beta1.NFT nft = 1; } -// QueryGenreRequest is the request type for the Query/Genre RPC method -message QueryGenreRequest { - string genre = 1; +// QueryClassRequest is the request type for the Query/Class RPC method +message QueryClassRequest { + string class_id = 1; } -// QueryGenreResponse is the response type for the Query/Genre RPC method -message QueryGenreResponse { - cosmos.nft.v1beta1.Genre genre = 1; +// QueryClassResponse is the response type for the Query/Class RPC method +message QueryClassResponse { + cosmos.nft.v1beta1.Class class = 1; } -// QueryGenresRequest is the request type for the Query/Genres RPC method -message QueryGenresRequest { +// QueryClassesRequest is the request type for the Query/Classes RPC method +message QueryClassesRequest { // pagination defines an optional pagination for the request. cosmos.base.query.v1beta1.PageRequest pagination = 1; } -// QueryGenresResponse is the response type for the Query/Genres RPC method -message QueryGenresResponse { - repeated cosmos.nft.v1beta1.Genre genres = 1; +// QueryClassesResponse is the response type for the Query/Classes RPC method +message QueryClassesResponse { + repeated cosmos.nft.v1beta1.Class classes = 1; cosmos.base.query.v1beta1.PageResponse pagination = 2; } ```