diff --git a/docs/architecture/adr-043-nft-module.md b/docs/architecture/adr-043-nft-module.md index 8c2f967b1694..d4db3d3f9c92 100644 --- a/docs/architecture/adr-043-nft-module.md +++ b/docs/architecture/adr-043-nft-module.md @@ -12,7 +12,13 @@ DRAFT ## Abstract -This ADR defines the `x/nft` module which is a generic implementation of NFTs, roughly "compatible" with ERC721. +This ADR defines the `x/nft` module which is a generic implementation of NFTs, roughly "compatible" with ERC721. **Applications using the `x/nft` module must implement the following functions**: + +- `MsgNewClass` - Receive the user's request to create a class, and call the `NewClass` of the `x/nft` module. +- `MsgUpdateClass` - Receive the user's request to update a class, and call the `UpdateClass` of the `x/nft` module. +- `MsgMintNFT` - Receive the user's request to mint a nft, and call the `MintNFT` of the `x/nft` module. +- `BurnNFT` - Receive the user's request to burn a nft, and call the `BurnNFT` of the `x/nft` module. +- `UpdateNFT` - Receive the user's request to update a nft, and call the `UpdateNFT` of the `x/nft` module. ## Context @@ -55,6 +61,7 @@ message Class { string symbol = 3; string description = 4; string uri = 5; + string uri_hash = 6; } ``` @@ -63,6 +70,7 @@ message Class { - `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_ +- `uri_hash` is a hash of the `uri`; _optional_ #### NFT @@ -73,6 +81,7 @@ message NFT { string class_id = 1; string id = 2; string uri = 3; + string uri_hash = 4; google.protobuf.Any data = 10; } ``` @@ -83,6 +92,7 @@ message NFT { {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)); _required_ +- `uri_hash` is a hash of the `uri`; - `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. diff --git a/docs/core/proto-docs.md b/docs/core/proto-docs.md index 855e9e838a0a..04cffc6b13e9 100644 --- a/docs/core/proto-docs.md +++ b/docs/core/proto-docs.md @@ -7290,11 +7290,11 @@ Class defines the class of the nft type. | Field | Type | Label | Description | | ----- | ---- | ----- | ----------- | | `id` | [string](#string) | | id defines the unique identifier of the NFT classification, similar to the contract address of ERC721 | -| `name` | [string](#string) | | name defines the human-readable name of the NFT classification | -| `symbol` | [string](#string) | | symbol is an abbreviated name for nft classification | -| `description` | [string](#string) | | description is a brief description of nft classification | -| `uri` | [string](#string) | | uri is a URI may point to a JSON file that conforms to the nft classification Metadata JSON Schema. | -| `uri_hash` | [string](#string) | | uri_hash is a hash of the document pointed to uri | +| `name` | [string](#string) | | name defines the human-readable name of the NFT classification,optional | +| `symbol` | [string](#string) | | symbol is an abbreviated name for nft classification,optional | +| `description` | [string](#string) | | description is a brief description of nft classification,optional | +| `uri` | [string](#string) | | uri is a URI may point to a JSON file that conforms to the nft classification Metadata JSON Schema.optional | +| `uri_hash` | [string](#string) | | uri_hash is a hash of the document pointed to uri,optional | @@ -7313,7 +7313,7 @@ NFT defines the NFT. | `id` | [string](#string) | | id defines the unique identification of NFT | | `uri` | [string](#string) | | uri defines NFT's metadata storage address outside the chain | | `uri_hash` | [string](#string) | | uri_hash is a hash of the document pointed to uri | -| `data` | [google.protobuf.Any](#google.protobuf.Any) | | data is the metadata of the NFT | +| `data` | [google.protobuf.Any](#google.protobuf.Any) | | data is the metadata of the NFT,optional | diff --git a/proto/cosmos/nft/v1beta1/nft.proto b/proto/cosmos/nft/v1beta1/nft.proto index aed091427daf..b11dd7c699a4 100644 --- a/proto/cosmos/nft/v1beta1/nft.proto +++ b/proto/cosmos/nft/v1beta1/nft.proto @@ -10,19 +10,19 @@ message Class { // id defines the unique identifier of the NFT classification, similar to the contract address of ERC721 string id = 1; - // name defines the human-readable name of the NFT classification + // name defines the human-readable name of the NFT classification,optional string name = 2; - // symbol is an abbreviated name for nft classification + // symbol is an abbreviated name for nft classification,optional string symbol = 3; - // description is a brief description of nft classification + // description is a brief description of nft classification,optional string description = 4; - // uri is a URI may point to a JSON file that conforms to the nft classification Metadata JSON Schema. + // uri is a URI may point to a JSON file that conforms to the nft classification Metadata JSON Schema.optional string uri = 5; - // uri_hash is a hash of the document pointed to uri + // uri_hash is a hash of the document pointed to uri,optional string uri_hash = 6; } @@ -40,6 +40,6 @@ message NFT { // uri_hash is a hash of the document pointed to uri string uri_hash = 4; - // data is the metadata of the NFT + // data is the metadata of the NFT,optional google.protobuf.Any data = 10; } \ No newline at end of file diff --git a/simapp/app.go b/simapp/app.go index 4673514d4731..33fcab3e5cbe 100644 --- a/simapp/app.go +++ b/simapp/app.go @@ -69,6 +69,9 @@ import ( "github.com/cosmos/cosmos-sdk/x/mint" mintkeeper "github.com/cosmos/cosmos-sdk/x/mint/keeper" minttypes "github.com/cosmos/cosmos-sdk/x/mint/types" + "github.com/cosmos/cosmos-sdk/x/nft" + nftkeeper "github.com/cosmos/cosmos-sdk/x/nft/keeper" + nftmodule "github.com/cosmos/cosmos-sdk/x/nft/module" "github.com/cosmos/cosmos-sdk/x/params" paramsclient "github.com/cosmos/cosmos-sdk/x/params/client" paramskeeper "github.com/cosmos/cosmos-sdk/x/params/keeper" @@ -117,6 +120,7 @@ var ( evidence.AppModuleBasic{}, authzmodule.AppModuleBasic{}, vesting.AppModuleBasic{}, + nftmodule.AppModuleBasic{}, ) // module account permissions @@ -127,6 +131,7 @@ var ( stakingtypes.BondedPoolName: {authtypes.Burner, authtypes.Staking}, stakingtypes.NotBondedPoolName: {authtypes.Burner, authtypes.Staking}, govtypes.ModuleName: {authtypes.Burner}, + nft.ModuleName: nil, } ) @@ -168,6 +173,7 @@ type SimApp struct { AuthzKeeper authzkeeper.Keeper EvidenceKeeper evidencekeeper.Keeper FeeGrantKeeper feegrantkeeper.Keeper + NFTKeeper nftkeeper.Keeper // the module manager mm *module.Manager @@ -209,7 +215,7 @@ func NewSimApp( minttypes.StoreKey, distrtypes.StoreKey, slashingtypes.StoreKey, govtypes.StoreKey, paramstypes.StoreKey, upgradetypes.StoreKey, feegrant.StoreKey, evidencetypes.StoreKey, capabilitytypes.StoreKey, - authzkeeper.StoreKey, + authzkeeper.StoreKey, nftkeeper.StoreKey, ) tkeys := sdk.NewTransientStoreKeys(paramstypes.TStoreKey) // NOTE: The testingkey is just mounted for testing purposes. Actual applications should @@ -297,6 +303,7 @@ func NewSimApp( // register the governance hooks ), ) + app.NFTKeeper = nftkeeper.NewKeeper(keys[nftkeeper.StoreKey], appCodec, app.AccountKeeper, app.BankKeeper) // create evidence keeper with router evidenceKeeper := evidencekeeper.NewKeeper( @@ -333,6 +340,7 @@ func NewSimApp( evidence.NewAppModule(app.EvidenceKeeper), params.NewAppModule(app.ParamsKeeper), authzmodule.NewAppModule(appCodec, app.AuthzKeeper, app.AccountKeeper, app.BankKeeper, app.interfaceRegistry), + nftmodule.NewAppModule(appCodec, app.NFTKeeper, app.AccountKeeper, app.BankKeeper, app.interfaceRegistry), ) // During begin block slashing happens after distr.BeginBlocker so that @@ -355,7 +363,7 @@ func NewSimApp( capabilitytypes.ModuleName, authtypes.ModuleName, banktypes.ModuleName, distrtypes.ModuleName, stakingtypes.ModuleName, slashingtypes.ModuleName, govtypes.ModuleName, minttypes.ModuleName, crisistypes.ModuleName, genutiltypes.ModuleName, evidencetypes.ModuleName, authz.ModuleName, - feegrant.ModuleName, + feegrant.ModuleName, nft.ModuleName, ) app.mm.RegisterInvariants(&app.CrisisKeeper) diff --git a/x/nft/codec.go b/x/nft/codec.go new file mode 100644 index 000000000000..d98e836c37ea --- /dev/null +++ b/x/nft/codec.go @@ -0,0 +1,14 @@ +package nft + +import ( + types "github.com/cosmos/cosmos-sdk/codec/types" + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/cosmos/cosmos-sdk/types/msgservice" +) + +func RegisterInterfaces(registry types.InterfaceRegistry) { + registry.RegisterImplementations((*sdk.Msg)(nil), + &MsgSend{}, + ) + msgservice.RegisterMsgServiceDesc(registry, &_Msg_serviceDesc) +} diff --git a/x/nft/errors.go b/x/nft/errors.go new file mode 100644 index 000000000000..1982862d1f8b --- /dev/null +++ b/x/nft/errors.go @@ -0,0 +1,15 @@ +package nft + +import ( + sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" +) + +// x/nft module sentinel errors +var ( + ErrInvalidNFT = sdkerrors.Register(ModuleName, 2, "invalid nft") + ErrClassExists = sdkerrors.Register(ModuleName, 3, "nft class already exist") + ErrClassNotExists = sdkerrors.Register(ModuleName, 4, "nft class does not exist") + ErrNFTExists = sdkerrors.Register(ModuleName, 5, "nft already exist") + ErrNFTNotExists = sdkerrors.Register(ModuleName, 6, "nft does not exist") + ErrInvalidID = sdkerrors.Register(ModuleName, 7, "invalid id") +) diff --git a/x/nft/expected_keepers.go b/x/nft/expected_keepers.go new file mode 100644 index 000000000000..0e0f1efd6e45 --- /dev/null +++ b/x/nft/expected_keepers.go @@ -0,0 +1,18 @@ +package nft + +import ( + sdk "github.com/cosmos/cosmos-sdk/types" + authtypes "github.com/cosmos/cosmos-sdk/x/auth/types" +) + +// BankKeeper defines the contract needed to be fulfilled for banking and supply +// dependencies. +type BankKeeper interface { + SpendableCoins(ctx sdk.Context, addr sdk.AccAddress) sdk.Coins +} + +// AccountKeeper defines the contract required for account APIs. +type AccountKeeper interface { + GetModuleAddress(name string) sdk.AccAddress + GetAccount(ctx sdk.Context, addr sdk.AccAddress) authtypes.AccountI +} diff --git a/x/nft/genesis.go b/x/nft/genesis.go new file mode 100644 index 000000000000..43be6ffbddd1 --- /dev/null +++ b/x/nft/genesis.go @@ -0,0 +1,30 @@ +package nft + +import ( + sdk "github.com/cosmos/cosmos-sdk/types" +) + +// ValidateGenesis check the given genesis state has no integrity issues +func ValidateGenesis(data GenesisState) error { + for _, class := range data.Classes { + if err := ValidateClassID(class.Id); err != nil { + return err + } + } + for _, entry := range data.Entries { + for _, nft := range entry.Nfts { + if err := ValidateNFTID(nft.Id); err != nil { + return err + } + if _, err := sdk.AccAddressFromBech32(entry.Owner); err != nil { + return err + } + } + } + return nil +} + +// DefaultGenesisState - Return a default genesis state +func DefaultGenesisState() *GenesisState { + return &GenesisState{} +} diff --git a/x/nft/keeper/class.go b/x/nft/keeper/class.go new file mode 100644 index 000000000000..d887ec6627b2 --- /dev/null +++ b/x/nft/keeper/class.go @@ -0,0 +1,67 @@ +package keeper + +import ( + sdk "github.com/cosmos/cosmos-sdk/types" + sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" + "github.com/cosmos/cosmos-sdk/x/nft" +) + +// SaveClass defines a method for creating a new nft class +func (k Keeper) SaveClass(ctx sdk.Context, class nft.Class) error { + if k.HasClass(ctx, class.Id) { + return sdkerrors.Wrap(nft.ErrClassExists, class.Id) + } + bz, err := k.cdc.Marshal(&class) + if err != nil { + return sdkerrors.Wrap(err, "Marshal nft.Class failed") + } + store := ctx.KVStore(k.storeKey) + store.Set(classStoreKey(class.Id), bz) + return nil +} + +// UpdateClass defines a method for updating a exist nft class +func (k Keeper) UpdateClass(ctx sdk.Context, class nft.Class) error { + if !k.HasClass(ctx, class.Id) { + return sdkerrors.Wrap(nft.ErrClassNotExists, class.Id) + } + bz, err := k.cdc.Marshal(&class) + if err != nil { + return sdkerrors.Wrap(err, "Marshal nft.Class failed") + } + store := ctx.KVStore(k.storeKey) + store.Set(classStoreKey(class.Id), bz) + return nil +} + +// GetClass defines a method for returning the class information of the specified id +func (k Keeper) GetClass(ctx sdk.Context, classID string) (nft.Class, bool) { + store := ctx.KVStore(k.storeKey) + bz := store.Get(classStoreKey(classID)) + + var class nft.Class + if len(bz) == 0 { + return class, false + } + k.cdc.MustUnmarshal(bz, &class) + return class, true +} + +// GetClasses defines a method for returning all classes information +func (k Keeper) GetClasses(ctx sdk.Context) (classes []*nft.Class) { + store := ctx.KVStore(k.storeKey) + iterator := sdk.KVStorePrefixIterator(store, ClassKey) + defer iterator.Close() + for ; iterator.Valid(); iterator.Next() { + var class nft.Class + k.cdc.MustUnmarshal(iterator.Value(), &class) + classes = append(classes, &class) + } + return +} + +// HasClass determines whether the specified classID exist +func (k Keeper) HasClass(ctx sdk.Context, classID string) bool { + store := ctx.KVStore(k.storeKey) + return store.Has(classStoreKey(classID)) +} diff --git a/x/nft/keeper/genesis.go b/x/nft/keeper/genesis.go new file mode 100644 index 000000000000..f8cf05184c0c --- /dev/null +++ b/x/nft/keeper/genesis.go @@ -0,0 +1,65 @@ +package keeper + +import ( + "sort" + + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/cosmos/cosmos-sdk/x/nft" +) + +// InitGenesis new nft genesis +func (k Keeper) InitGenesis(ctx sdk.Context, data *nft.GenesisState) { + for _, class := range data.Classes { + if err := k.SaveClass(ctx, *class); err != nil { + panic(err) + } + + } + for _, entry := range data.Entries { + for _, nft := range entry.Nfts { + owner, err := sdk.AccAddressFromBech32(entry.Owner) + if err != nil { + panic(err) + } + + if err := k.Mint(ctx, *nft, owner); err != nil { + panic(err) + } + } + } +} + +// ExportGenesis returns a GenesisState for a given context. +func (k Keeper) ExportGenesis(ctx sdk.Context) *nft.GenesisState { + classes := k.GetClasses(ctx) + nftMap := make(map[string][]*nft.NFT) + for _, class := range classes { + nfts := k.GetNFTsOfClass(ctx, class.Id) + for i, n := range nfts { + owner := k.GetOwner(ctx, n.ClassId, n.Id) + nftArr, ok := nftMap[owner.String()] + if !ok { + nftArr = make([]*nft.NFT, 0) + } + nftMap[owner.String()] = append(nftArr, &nfts[i]) + } + } + + owners := make([]string, 0, len(nftMap)) + for owner := range nftMap { + owners = append(owners, owner) + } + sort.Strings(owners) + + entries := make([]*nft.Entry, 0, len(nftMap)) + for _, owner := range owners { + entries = append(entries, &nft.Entry{ + Owner: owner, + Nfts: nftMap[owner], + }) + } + return &nft.GenesisState{ + Classes: classes, + Entries: entries, + } +} diff --git a/x/nft/keeper/keeper.go b/x/nft/keeper/keeper.go new file mode 100644 index 000000000000..8ae13ca98e27 --- /dev/null +++ b/x/nft/keeper/keeper.go @@ -0,0 +1,30 @@ +package keeper + +import ( + "github.com/cosmos/cosmos-sdk/codec" + storetypes "github.com/cosmos/cosmos-sdk/store/types" + "github.com/cosmos/cosmos-sdk/x/nft" +) + +// Keeper of the nft store +type Keeper struct { + cdc codec.BinaryCodec + storeKey storetypes.StoreKey + bk nft.BankKeeper +} + +// NewKeeper creates a new nft Keeper instance +func NewKeeper(key storetypes.StoreKey, + cdc codec.BinaryCodec, ak nft.AccountKeeper, bk nft.BankKeeper, +) Keeper { + // ensure nft module account is set + if addr := ak.GetModuleAddress(nft.ModuleName); addr == nil { + panic("the nft module account has not been set") + } + + return Keeper{ + cdc: cdc, + storeKey: key, + bk: bk, + } +} diff --git a/x/nft/keeper/keeper_test.go b/x/nft/keeper/keeper_test.go new file mode 100644 index 000000000000..dafe49a88e9a --- /dev/null +++ b/x/nft/keeper/keeper_test.go @@ -0,0 +1,342 @@ +package keeper_test + +import ( + "testing" + + "github.com/stretchr/testify/suite" + tmproto "github.com/tendermint/tendermint/proto/tendermint/types" + tmtime "github.com/tendermint/tendermint/types/time" + + "github.com/cosmos/cosmos-sdk/simapp" + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/cosmos/cosmos-sdk/x/nft" +) + +const ( + testClassID = "kitty" + testClassName = "Crypto Kitty" + testClassSymbol = "kitty" + testClassDescription = "Crypto Kitty" + testClassURI = "class uri" + testClassURIHash = "ae702cefd6b6a65fe2f991ad6d9969ed" + testID = "kitty1" + testURI = "kitty uri" + testURIHash = "229bfd3c1b431c14a526497873897108" +) + +type TestSuite struct { + suite.Suite + + app *simapp.SimApp + ctx sdk.Context + addrs []sdk.AccAddress + queryClient nft.QueryClient +} + +func (s *TestSuite) SetupTest() { + app := simapp.Setup(s.T(), false) + ctx := app.BaseApp.NewContext(false, tmproto.Header{}) + ctx = ctx.WithBlockHeader(tmproto.Header{Time: tmtime.Now()}) + + s.app = app + s.ctx = ctx + s.addrs = simapp.AddTestAddrsIncremental(app, ctx, 3, sdk.NewInt(30000000)) +} + +func TestTestSuite(t *testing.T) { + suite.Run(t, new(TestSuite)) +} + +func (s *TestSuite) TestSaveClass() { + except := nft.Class{ + Id: testClassID, + Name: testClassName, + Symbol: testClassSymbol, + Description: testClassDescription, + Uri: testClassURI, + UriHash: testClassURIHash, + } + err := s.app.NFTKeeper.SaveClass(s.ctx, except) + s.Require().NoError(err) + + actual, has := s.app.NFTKeeper.GetClass(s.ctx, testClassID) + s.Require().True(has) + s.Require().EqualValues(except, actual) + + classes := s.app.NFTKeeper.GetClasses(s.ctx) + s.Require().EqualValues([]*nft.Class{&except}, classes) +} + +func (s *TestSuite) TestUpdateClass() { + class := nft.Class{ + Id: testClassID, + Name: testClassName, + Symbol: testClassSymbol, + Description: testClassDescription, + Uri: testClassURI, + UriHash: testClassURIHash, + } + err := s.app.NFTKeeper.SaveClass(s.ctx, class) + s.Require().NoError(err) + + noExistClass := nft.Class{ + Id: "kitty1", + Name: testClassName, + Symbol: testClassSymbol, + Description: testClassDescription, + Uri: testClassURI, + UriHash: testClassURIHash, + } + + err = s.app.NFTKeeper.UpdateClass(s.ctx, noExistClass) + s.Require().Error(err) + s.Require().Contains(err.Error(), "nft class does not exist") + + except := nft.Class{ + Id: testClassID, + Name: "My crypto Kitty", + Symbol: testClassSymbol, + Description: testClassDescription, + Uri: testClassURI, + UriHash: testClassURIHash, + } + + err = s.app.NFTKeeper.UpdateClass(s.ctx, except) + s.Require().NoError(err) + + actual, has := s.app.NFTKeeper.GetClass(s.ctx, testClassID) + s.Require().True(has) + s.Require().EqualValues(except, actual) +} + +func (s *TestSuite) TestMint() { + class := nft.Class{ + Id: testClassID, + Name: testClassName, + Symbol: testClassSymbol, + Description: testClassDescription, + Uri: testClassURI, + UriHash: testClassURIHash, + } + err := s.app.NFTKeeper.SaveClass(s.ctx, class) + s.Require().NoError(err) + + expNFT := nft.NFT{ + ClassId: testClassID, + Id: testID, + Uri: testURI, + } + err = s.app.NFTKeeper.Mint(s.ctx, expNFT, s.addrs[0]) + s.Require().NoError(err) + + // test GetNFT + actNFT, has := s.app.NFTKeeper.GetNFT(s.ctx, testClassID, testID) + s.Require().True(has) + s.Require().EqualValues(expNFT, actNFT) + + // test GetOwner + owner := s.app.NFTKeeper.GetOwner(s.ctx, testClassID, testID) + s.Require().True(s.addrs[0].Equals(owner)) + + // test GetNFTsOfClass + actNFTs := s.app.NFTKeeper.GetNFTsOfClass(s.ctx, testClassID) + s.Require().EqualValues([]nft.NFT{expNFT}, actNFTs) + + // test GetNFTsOfClassByOwner + actNFTs = s.app.NFTKeeper.GetNFTsOfClassByOwner(s.ctx, testClassID, s.addrs[0]) + s.Require().EqualValues([]nft.NFT{expNFT}, actNFTs) + + // test GetBalance + balance := s.app.NFTKeeper.GetBalance(s.ctx, testClassID, s.addrs[0]) + s.Require().EqualValues(uint64(1), balance) + + // test GetTotalSupply + supply := s.app.NFTKeeper.GetTotalSupply(s.ctx, testClassID) + s.Require().EqualValues(uint64(1), supply) +} + +func (s *TestSuite) TestBurn() { + except := nft.Class{ + Id: testClassID, + Name: testClassName, + Symbol: testClassSymbol, + Description: testClassDescription, + Uri: testClassURI, + UriHash: testClassURIHash, + } + err := s.app.NFTKeeper.SaveClass(s.ctx, except) + s.Require().NoError(err) + + expNFT := nft.NFT{ + ClassId: testClassID, + Id: testID, + Uri: testURI, + } + err = s.app.NFTKeeper.Mint(s.ctx, expNFT, s.addrs[0]) + s.Require().NoError(err) + + err = s.app.NFTKeeper.Burn(s.ctx, testClassID, testID) + s.Require().NoError(err) + + // test GetNFT + _, has := s.app.NFTKeeper.GetNFT(s.ctx, testClassID, testID) + s.Require().False(has) + + // test GetOwner + owner := s.app.NFTKeeper.GetOwner(s.ctx, testClassID, testID) + s.Require().Nil(owner) + + // test GetNFTsOfClass + actNFTs := s.app.NFTKeeper.GetNFTsOfClass(s.ctx, testClassID) + s.Require().Empty(actNFTs) + + // test GetNFTsOfClassByOwner + actNFTs = s.app.NFTKeeper.GetNFTsOfClassByOwner(s.ctx, testClassID, s.addrs[0]) + s.Require().Empty(actNFTs) + + // test GetBalance + balance := s.app.NFTKeeper.GetBalance(s.ctx, testClassID, s.addrs[0]) + s.Require().EqualValues(uint64(0), balance) + + // test GetTotalSupply + supply := s.app.NFTKeeper.GetTotalSupply(s.ctx, testClassID) + s.Require().EqualValues(uint64(0), supply) +} + +func (s *TestSuite) TestUpdate() { + class := nft.Class{ + Id: testClassID, + Name: testClassName, + Symbol: testClassSymbol, + Description: testClassDescription, + Uri: testClassURI, + UriHash: testClassURIHash, + } + err := s.app.NFTKeeper.SaveClass(s.ctx, class) + s.Require().NoError(err) + + myNFT := nft.NFT{ + ClassId: testClassID, + Id: testID, + Uri: testURI, + } + err = s.app.NFTKeeper.Mint(s.ctx, myNFT, s.addrs[0]) + s.Require().NoError(err) + + expNFT := nft.NFT{ + ClassId: testClassID, + Id: testID, + Uri: "updated", + } + + err = s.app.NFTKeeper.Update(s.ctx, expNFT) + s.Require().NoError(err) + + // test GetNFT + actNFT, has := s.app.NFTKeeper.GetNFT(s.ctx, testClassID, testID) + s.Require().True(has) + s.Require().EqualValues(expNFT, actNFT) +} + +func (s *TestSuite) TestTransfer() { + class := nft.Class{ + Id: testClassID, + Name: testClassName, + Symbol: testClassSymbol, + Description: testClassDescription, + Uri: testClassURI, + UriHash: testClassURIHash, + } + err := s.app.NFTKeeper.SaveClass(s.ctx, class) + s.Require().NoError(err) + + expNFT := nft.NFT{ + ClassId: testClassID, + Id: testID, + Uri: testURI, + } + err = s.app.NFTKeeper.Mint(s.ctx, expNFT, s.addrs[0]) + s.Require().NoError(err) + + //valid owner + err = s.app.NFTKeeper.Transfer(s.ctx, testClassID, testID, s.addrs[1]) + s.Require().NoError(err) + + // test GetOwner + owner := s.app.NFTKeeper.GetOwner(s.ctx, testClassID, testID) + s.Require().Equal(s.addrs[1], owner) + + balanceAddr0 := s.app.NFTKeeper.GetBalance(s.ctx, testClassID, s.addrs[0]) + s.Require().EqualValues(uint64(0), balanceAddr0) + + balanceAddr1 := s.app.NFTKeeper.GetBalance(s.ctx, testClassID, s.addrs[1]) + s.Require().EqualValues(uint64(1), balanceAddr1) + + // test GetNFTsOfClassByOwner + actNFTs := s.app.NFTKeeper.GetNFTsOfClassByOwner(s.ctx, testClassID, s.addrs[1]) + s.Require().EqualValues([]nft.NFT{expNFT}, actNFTs) +} + +func (s *TestSuite) TestExportGenesis() { + class := nft.Class{ + Id: testClassID, + Name: testClassName, + Symbol: testClassSymbol, + Description: testClassDescription, + Uri: testClassURI, + UriHash: testClassURIHash, + } + err := s.app.NFTKeeper.SaveClass(s.ctx, class) + s.Require().NoError(err) + + expNFT := nft.NFT{ + ClassId: testClassID, + Id: testID, + Uri: testURI, + } + err = s.app.NFTKeeper.Mint(s.ctx, expNFT, s.addrs[0]) + s.Require().NoError(err) + + expGenesis := &nft.GenesisState{ + Classes: []*nft.Class{&class}, + Entries: []*nft.Entry{{ + Owner: s.addrs[0].String(), + Nfts: []*nft.NFT{&expNFT}, + }}, + } + genesis := s.app.NFTKeeper.ExportGenesis(s.ctx) + s.Require().Equal(expGenesis, genesis) +} + +func (s *TestSuite) TestInitGenesis() { + expClass := nft.Class{ + Id: testClassID, + Name: testClassName, + Symbol: testClassSymbol, + Description: testClassDescription, + Uri: testClassURI, + UriHash: testClassURIHash, + } + expNFT := nft.NFT{ + ClassId: testClassID, + Id: testID, + Uri: testURI, + } + expGenesis := &nft.GenesisState{ + Classes: []*nft.Class{&expClass}, + Entries: []*nft.Entry{{ + Owner: s.addrs[0].String(), + Nfts: []*nft.NFT{&expNFT}, + }}, + } + s.app.NFTKeeper.InitGenesis(s.ctx, expGenesis) + + actual, has := s.app.NFTKeeper.GetClass(s.ctx, testClassID) + s.Require().True(has) + s.Require().EqualValues(expClass, actual) + + // test GetNFT + actNFT, has := s.app.NFTKeeper.GetNFT(s.ctx, testClassID, testID) + s.Require().True(has) + s.Require().EqualValues(expNFT, actNFT) +} diff --git a/x/nft/keeper/keys.go b/x/nft/keeper/keys.go new file mode 100644 index 000000000000..bf8d1ceab829 --- /dev/null +++ b/x/nft/keeper/keys.go @@ -0,0 +1,76 @@ +package keeper + +import ( + "github.com/cosmos/cosmos-sdk/internal/conv" + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/cosmos/cosmos-sdk/types/address" + "github.com/cosmos/cosmos-sdk/x/nft" +) + +var ( + ClassKey = []byte{0x01} + NFTKey = []byte{0x02} + NFTOfClassByOwnerKey = []byte{0x03} + OwnerKey = []byte{0x04} + ClassTotalSupply = []byte{0x05} + + Delimiter = []byte{0x00} + Placeholder = []byte{0x01} +) + +// StoreKey is the store key string for nft +const StoreKey = nft.ModuleName + +// classStoreKey returns the byte representation of the nft class key +func classStoreKey(classID string) []byte { + key := make([]byte, len(ClassKey)+len(classID)) + copy(key, ClassKey) + copy(key[len(ClassKey):], classID) + return key +} + +// nftStoreKey returns the byte representation of the nft +func nftStoreKey(classID string) []byte { + key := make([]byte, len(NFTKey)+len(classID)+len(Delimiter)) + copy(key, NFTKey) + copy(key[len(NFTKey):], classID) + copy(key[len(NFTKey)+len(classID):], Delimiter) + return key +} + +// classTotalSupply returns the byte representation of the ClassTotalSupply +func classTotalSupply(classID string) []byte { + key := make([]byte, len(ClassTotalSupply)+len(classID)) + copy(key, ClassTotalSupply) + copy(key[len(ClassTotalSupply):], classID) + return key +} + +// nftOfClassByOwnerStoreKey returns the byte representation of the nft owner +// Items are stored with the following key: values +// 0x03 +func nftOfClassByOwnerStoreKey(owner sdk.AccAddress, classID string) []byte { + owner = address.MustLengthPrefix(owner) + classIDBz := conv.UnsafeStrToBytes(classID) + + var key = make([]byte, len(NFTOfClassByOwnerKey)+len(owner)+len(Delimiter)) + copy(key, NFTOfClassByOwnerKey) + copy(key[len(NFTOfClassByOwnerKey):], owner) + copy(key[len(NFTOfClassByOwnerKey)+len(owner):], classIDBz) + return append(key, Delimiter...) +} + +// ownerStoreKey returns the byte representation of the nft owner +// Items are stored with the following key: values +// 0x04 +func ownerStoreKey(classID, nftID string) []byte { + // key is of format: + classIDBz := conv.UnsafeStrToBytes(classID) + nftIDBz := conv.UnsafeStrToBytes(nftID) + + var key = make([]byte, len(OwnerKey)+len(classIDBz)+len(Delimiter)+len(nftIDBz)) + copy(key, OwnerKey) + copy(key[len(OwnerKey):], classIDBz) + copy(key[len(OwnerKey)+len(classIDBz):], Delimiter) + return append(key, nftIDBz...) +} diff --git a/x/nft/keeper/msg_server.go b/x/nft/keeper/msg_server.go new file mode 100644 index 000000000000..fc14a60559f5 --- /dev/null +++ b/x/nft/keeper/msg_server.go @@ -0,0 +1,42 @@ +package keeper + +import ( + "context" + + sdk "github.com/cosmos/cosmos-sdk/types" + sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" + "github.com/cosmos/cosmos-sdk/x/nft" +) + +var _ nft.MsgServer = Keeper{} + +// Send implement Send method of the types.MsgServer. +func (k Keeper) Send(goCtx context.Context, msg *nft.MsgSend) (*nft.MsgSendResponse, error) { + ctx := sdk.UnwrapSDKContext(goCtx) + sender, err := sdk.AccAddressFromBech32(msg.Sender) + if err != nil { + return nil, err + } + + owner := k.GetOwner(ctx, msg.ClassId, msg.Id) + if !owner.Equals(sender) { + return nil, sdkerrors.Wrapf(sdkerrors.ErrUnauthorized, "%s is not the owner of nft %s", sender, msg.Id) + } + + receiver, err := sdk.AccAddressFromBech32(msg.Receiver) + if err != nil { + return nil, err + } + + if err := k.Transfer(ctx, msg.ClassId, msg.Id, receiver); err != nil { + return nil, err + } + + ctx.EventManager().EmitTypedEvent(&nft.EventSend{ + ClassId: msg.ClassId, + Id: msg.Id, + Sender: msg.Sender, + Receiver: msg.Receiver, + }) + return &nft.MsgSendResponse{}, nil +} diff --git a/x/nft/keeper/nft.go b/x/nft/keeper/nft.go new file mode 100644 index 000000000000..1cb4e817116c --- /dev/null +++ b/x/nft/keeper/nft.go @@ -0,0 +1,192 @@ +package keeper + +import ( + "github.com/cosmos/cosmos-sdk/store/prefix" + sdk "github.com/cosmos/cosmos-sdk/types" + sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" + "github.com/cosmos/cosmos-sdk/x/nft" +) + +// Mint defines a method for minting a new nft +func (k Keeper) Mint(ctx sdk.Context, token nft.NFT, receiver sdk.AccAddress) error { + if !k.HasClass(ctx, token.ClassId) { + return sdkerrors.Wrap(nft.ErrClassNotExists, token.ClassId) + } + + if k.HasNFT(ctx, token.ClassId, token.Id) { + return sdkerrors.Wrap(nft.ErrNFTExists, token.Id) + } + + k.setNFT(ctx, token) + k.setOwner(ctx, token.ClassId, token.Id, receiver) + k.incrTotalSupply(ctx, token.ClassId) + return nil +} + +// Burn defines a method for burning a nft from a specific account. +// Note: When the upper module uses this method, it needs to authenticate nft +func (k Keeper) Burn(ctx sdk.Context, classID string, nftID string) error { + if !k.HasClass(ctx, classID) { + return sdkerrors.Wrap(nft.ErrClassNotExists, classID) + } + + if !k.HasNFT(ctx, classID, nftID) { + return sdkerrors.Wrap(nft.ErrNFTNotExists, nftID) + } + + owner := k.GetOwner(ctx, classID, nftID) + nftStore := k.getNFTStore(ctx, classID) + nftStore.Delete([]byte(nftID)) + + k.deleteOwner(ctx, classID, nftID, owner) + k.decrTotalSupply(ctx, classID) + return nil +} + +// Update defines a method for updating an exist nft +// Note: When the upper module uses this method, it needs to authenticate nft +func (k Keeper) Update(ctx sdk.Context, token nft.NFT) error { + if !k.HasClass(ctx, token.ClassId) { + return sdkerrors.Wrap(nft.ErrClassNotExists, token.ClassId) + } + + if !k.HasNFT(ctx, token.ClassId, token.Id) { + return sdkerrors.Wrap(nft.ErrNFTNotExists, token.Id) + } + k.setNFT(ctx, token) + return nil +} + +// Transfer defines a method for sending a nft from one account to another account. +// Note: When the upper module uses this method, it needs to authenticate nft +func (k Keeper) Transfer(ctx sdk.Context, + classID string, + nftID string, + receiver sdk.AccAddress) error { + if !k.HasClass(ctx, classID) { + return sdkerrors.Wrap(nft.ErrClassNotExists, classID) + } + + if !k.HasNFT(ctx, classID, nftID) { + return sdkerrors.Wrap(nft.ErrNFTNotExists, nftID) + } + + owner := k.GetOwner(ctx, classID, nftID) + k.deleteOwner(ctx, classID, nftID, owner) + k.setOwner(ctx, classID, nftID, receiver) + return nil +} + +// GetNFT returns the nft information of the specified classID and nftID +func (k Keeper) GetNFT(ctx sdk.Context, classID, nftID string) (nft.NFT, bool) { + store := k.getNFTStore(ctx, classID) + bz := store.Get([]byte(nftID)) + if len(bz) == 0 { + return nft.NFT{}, false + } + var nft nft.NFT + k.cdc.MustUnmarshal(bz, &nft) + return nft, true +} + +// GetNFTsOfClassByOwner returns all nft information of the specified classID under the specified owner +func (k Keeper) GetNFTsOfClassByOwner(ctx sdk.Context, classID string, owner sdk.AccAddress) (nfts []nft.NFT) { + ownerStore := k.getClassStoreByOwner(ctx, owner, classID) + iterator := ownerStore.Iterator(nil, nil) + defer iterator.Close() + for ; iterator.Valid(); iterator.Next() { + nft, has := k.GetNFT(ctx, classID, string(iterator.Key())) + if has { + nfts = append(nfts, nft) + } + } + return nfts +} + +// GetNFTsOfClass returns all nft information under the specified classID +func (k Keeper) GetNFTsOfClass(ctx sdk.Context, classID string) (nfts []nft.NFT) { + nftStore := k.getNFTStore(ctx, classID) + iterator := nftStore.Iterator(nil, nil) + defer iterator.Close() + for ; iterator.Valid(); iterator.Next() { + var nft nft.NFT + k.cdc.MustUnmarshal(iterator.Value(), &nft) + nfts = append(nfts, nft) + } + return nfts +} + +// GetOwner returns the owner information of the specified nft +func (k Keeper) GetOwner(ctx sdk.Context, classID string, nftID string) sdk.AccAddress { + store := ctx.KVStore(k.storeKey) + bz := store.Get(ownerStoreKey(classID, nftID)) + return sdk.AccAddress(bz) +} + +// GetBalance returns the specified account, the number of all nfts under the specified classID +func (k Keeper) GetBalance(ctx sdk.Context, classID string, owner sdk.AccAddress) uint64 { + nfts := k.GetNFTsOfClassByOwner(ctx, classID, owner) + return uint64(len(nfts)) +} + +// GetTotalSupply returns the number of all nfts under the specified classID +func (k Keeper) GetTotalSupply(ctx sdk.Context, classID string) uint64 { + store := ctx.KVStore(k.storeKey) + bz := store.Get(classTotalSupply(classID)) + return sdk.BigEndianToUint64(bz) +} + +// HasNFT determines whether the specified classID and nftID exist +func (k Keeper) HasNFT(ctx sdk.Context, classID, id string) bool { + store := k.getNFTStore(ctx, classID) + return store.Has([]byte(id)) +} + +func (k Keeper) setNFT(ctx sdk.Context, token nft.NFT) { + nftStore := k.getNFTStore(ctx, token.ClassId) + bz := k.cdc.MustMarshal(&token) + nftStore.Set([]byte(token.Id), bz) +} + +func (k Keeper) setOwner(ctx sdk.Context, classID, nftID string, owner sdk.AccAddress) { + store := ctx.KVStore(k.storeKey) + store.Set(ownerStoreKey(classID, nftID), owner.Bytes()) + + ownerStore := k.getClassStoreByOwner(ctx, owner, classID) + ownerStore.Set([]byte(nftID), Placeholder) +} + +func (k Keeper) deleteOwner(ctx sdk.Context, classID, nftID string, owner sdk.AccAddress) { + store := ctx.KVStore(k.storeKey) + store.Delete(ownerStoreKey(classID, nftID)) + + ownerStore := k.getClassStoreByOwner(ctx, owner, classID) + ownerStore.Delete([]byte(nftID)) +} + +func (k Keeper) getNFTStore(ctx sdk.Context, classID string) prefix.Store { + store := ctx.KVStore(k.storeKey) + return prefix.NewStore(store, nftStoreKey(classID)) +} + +func (k Keeper) getClassStoreByOwner(ctx sdk.Context, owner sdk.AccAddress, classID string) prefix.Store { + store := ctx.KVStore(k.storeKey) + key := nftOfClassByOwnerStoreKey(owner, classID) + return prefix.NewStore(store, key) +} + +func (k Keeper) incrTotalSupply(ctx sdk.Context, classID string) { + supply := k.GetTotalSupply(ctx, classID) + 1 + k.updateTotalSupply(ctx, classID, supply) +} + +func (k Keeper) decrTotalSupply(ctx sdk.Context, classID string) { + supply := k.GetTotalSupply(ctx, classID) - 1 + k.updateTotalSupply(ctx, classID, supply) +} + +func (k Keeper) updateTotalSupply(ctx sdk.Context, classID string, supply uint64) { + store := ctx.KVStore(k.storeKey) + supplyKey := classTotalSupply(classID) + store.Set(supplyKey, sdk.Uint64ToBigEndian(supply)) +} diff --git a/x/nft/keys.go b/x/nft/keys.go new file mode 100644 index 000000000000..da03362d7cd4 --- /dev/null +++ b/x/nft/keys.go @@ -0,0 +1,12 @@ +package nft + +const ( + // module name + ModuleName = "nft" + + // StoreKey is the default store key for nft + StoreKey = ModuleName + + // RouterKey is the message route for nft + RouterKey = ModuleName +) diff --git a/x/nft/module/module.go b/x/nft/module/module.go new file mode 100644 index 000000000000..c68c5d28b87c --- /dev/null +++ b/x/nft/module/module.go @@ -0,0 +1,157 @@ +package nft + +import ( + "context" + "encoding/json" + + "github.com/gorilla/mux" + "github.com/grpc-ecosystem/grpc-gateway/runtime" + "github.com/spf13/cobra" + abci "github.com/tendermint/tendermint/abci/types" + + sdkclient "github.com/cosmos/cosmos-sdk/client" + "github.com/cosmos/cosmos-sdk/codec" + cdctypes "github.com/cosmos/cosmos-sdk/codec/types" + sdk "github.com/cosmos/cosmos-sdk/types" + sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" + "github.com/cosmos/cosmos-sdk/types/module" + + "github.com/cosmos/cosmos-sdk/x/nft" + "github.com/cosmos/cosmos-sdk/x/nft/keeper" +) + +var ( + _ module.AppModule = AppModule{} + _ module.AppModuleBasic = AppModuleBasic{} +) + +// AppModuleBasic defines the basic application module used by the nft module. +type AppModuleBasic struct { + cdc codec.Codec +} + +// Name returns the nft module's name. +func (AppModuleBasic) Name() string { + return nft.ModuleName +} + +// RegisterServices registers a gRPC query service to respond to the +// module-specific gRPC queries. +func (am AppModule) RegisterServices(cfg module.Configurator) { + nft.RegisterMsgServer(cfg.MsgServer(), am.keeper) +} + +// RegisterLegacyAminoCodec registers the nft module's types for the given codec. +func (AppModuleBasic) RegisterLegacyAminoCodec(cdc *codec.LegacyAmino) {} + +// RegisterInterfaces registers the nft module's interface types +func (AppModuleBasic) RegisterInterfaces(registry cdctypes.InterfaceRegistry) { + nft.RegisterInterfaces(registry) +} + +// DefaultGenesis returns default genesis state as raw bytes for the nft +// module. +func (AppModuleBasic) DefaultGenesis(cdc codec.JSONCodec) json.RawMessage { + return cdc.MustMarshalJSON(nft.DefaultGenesisState()) +} + +// ValidateGenesis performs genesis state validation for the nft module. +func (AppModuleBasic) ValidateGenesis(cdc codec.JSONCodec, config sdkclient.TxEncodingConfig, bz json.RawMessage) error { + var data nft.GenesisState + if err := cdc.UnmarshalJSON(bz, &data); err != nil { + return sdkerrors.Wrapf(err, "failed to unmarshal %s genesis state", nft.ModuleName) + } + + return nft.ValidateGenesis(data) +} + +// RegisterRESTRoutes registers the REST routes for the nft module. +func (AppModuleBasic) RegisterRESTRoutes(clientCtx sdkclient.Context, r *mux.Router) { +} + +// RegisterGRPCGatewayRoutes registers the gRPC Gateway routes for the nft module. +func (a AppModuleBasic) RegisterGRPCGatewayRoutes(clientCtx sdkclient.Context, mux *runtime.ServeMux) { + nft.RegisterQueryHandlerClient(context.Background(), mux, nft.NewQueryClient(clientCtx)) +} + +// GetQueryCmd returns the cli query commands for the nft module +func (AppModuleBasic) GetQueryCmd() *cobra.Command { + return nil +} + +// GetTxCmd returns the transaction commands for the nft module +func (AppModuleBasic) GetTxCmd() *cobra.Command { + return nil +} + +// AppModule implements the sdk.AppModule interface +type AppModule struct { + AppModuleBasic + keeper keeper.Keeper + // TODO accountKeeper,bankKeeper will be replaced by query service + accountKeeper nft.AccountKeeper + bankKeeper nft.BankKeeper + registry cdctypes.InterfaceRegistry +} + +// NewAppModule creates a new AppModule object +func NewAppModule(cdc codec.Codec, keeper keeper.Keeper, ak nft.AccountKeeper, bk nft.BankKeeper, registry cdctypes.InterfaceRegistry) AppModule { + return AppModule{ + AppModuleBasic: AppModuleBasic{cdc: cdc}, + keeper: keeper, + accountKeeper: ak, + bankKeeper: bk, + registry: registry, + } +} + +// Name returns the nft module's name. +func (AppModule) Name() string { + return nft.ModuleName +} + +// RegisterInvariants does nothing, there are no invariants to enforce +func (AppModule) RegisterInvariants(_ sdk.InvariantRegistry) {} + +// Route returns the message routing key for the staking module. +func (am AppModule) Route() sdk.Route { + return sdk.NewRoute(nft.RouterKey, nil) +} + +func (am AppModule) NewHandler() sdk.Handler { + return nil +} + +// QuerierRoute returns the route we respond to for abci queries +func (AppModule) QuerierRoute() string { return "" } + +// LegacyQuerierHandler returns the nft module sdk.Querier. +func (am AppModule) LegacyQuerierHandler(legacyQuerierCdc *codec.LegacyAmino) sdk.Querier { + return nil +} + +// InitGenesis performs genesis initialization for the nft module. It returns +// no validator updates. +func (am AppModule) InitGenesis(ctx sdk.Context, cdc codec.JSONCodec, data json.RawMessage) []abci.ValidatorUpdate { + var genesisState nft.GenesisState + cdc.MustUnmarshalJSON(data, &genesisState) + am.keeper.InitGenesis(ctx, &genesisState) + return []abci.ValidatorUpdate{} +} + +// ExportGenesis returns the exported genesis state as raw bytes for the nft +// module. +func (am AppModule) ExportGenesis(ctx sdk.Context, cdc codec.JSONCodec) json.RawMessage { + gs := am.keeper.ExportGenesis(ctx) + return cdc.MustMarshalJSON(gs) +} + +// ConsensusVersion implements AppModule/ConsensusVersion. +func (AppModule) ConsensusVersion() uint64 { return 1 } + +func (am AppModule) BeginBlock(ctx sdk.Context, req abci.RequestBeginBlock) {} + +// EndBlock does nothing +func (am AppModule) EndBlock(ctx sdk.Context, _ abci.RequestEndBlock) []abci.ValidatorUpdate { + return []abci.ValidatorUpdate{} +} diff --git a/x/nft/msgs.go b/x/nft/msgs.go new file mode 100644 index 000000000000..85fbc1ba4956 --- /dev/null +++ b/x/nft/msgs.go @@ -0,0 +1,43 @@ +package nft + +import ( + sdk "github.com/cosmos/cosmos-sdk/types" + sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" +) + +const ( + // TypeMsgSend nft message types + TypeMsgSend = "send" +) + +var ( + _ sdk.Msg = &MsgSend{} +) + +// GetSigners implements the Msg.ValidateBasic method. +func (m MsgSend) ValidateBasic() error { + if err := ValidateClassID(m.ClassId); err != nil { + return sdkerrors.Wrapf(ErrInvalidID, "Invalid class id (%s)", m.ClassId) + } + + if err := ValidateNFTID(m.Id); err != nil { + return sdkerrors.Wrapf(ErrInvalidID, "Invalid nft id (%s)", m.Id) + } + + _, err := sdk.AccAddressFromBech32(m.Sender) + if err != nil { + return sdkerrors.Wrapf(sdkerrors.ErrInvalidAddress, "Invalid sender address (%s)", m.Sender) + } + + _, err = sdk.AccAddressFromBech32(m.Receiver) + if err != nil { + return sdkerrors.Wrapf(sdkerrors.ErrInvalidAddress, "Invalid receiver address (%s)", m.Receiver) + } + return nil +} + +// GetSigners implements Msg +func (m MsgSend) GetSigners() []sdk.AccAddress { + signer, _ := sdk.AccAddressFromBech32(m.Sender) + return []sdk.AccAddress{signer} +} diff --git a/x/nft/nft.pb.go b/x/nft/nft.pb.go index 468cb1e58c3a..6c349f4bdfb5 100644 --- a/x/nft/nft.pb.go +++ b/x/nft/nft.pb.go @@ -27,15 +27,15 @@ const _ = proto.GoGoProtoPackageIsVersion3 // please upgrade the proto package type Class struct { // id defines the unique identifier of the NFT classification, similar to the contract address of ERC721 Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"` - // name defines the human-readable name of the NFT classification + // name defines the human-readable name of the NFT classification,optional Name string `protobuf:"bytes,2,opt,name=name,proto3" json:"name,omitempty"` - // symbol is an abbreviated name for nft classification + // symbol is an abbreviated name for nft classification,optional Symbol string `protobuf:"bytes,3,opt,name=symbol,proto3" json:"symbol,omitempty"` - // description is a brief description of nft classification + // description is a brief description of nft classification,optional Description string `protobuf:"bytes,4,opt,name=description,proto3" json:"description,omitempty"` - // uri is a URI may point to a JSON file that conforms to the nft classification Metadata JSON Schema. + // uri is a URI may point to a JSON file that conforms to the nft classification Metadata JSON Schema.optional Uri string `protobuf:"bytes,5,opt,name=uri,proto3" json:"uri,omitempty"` - // uri_hash is a hash of the document pointed to uri + // uri_hash is a hash of the document pointed to uri,optional UriHash string `protobuf:"bytes,6,opt,name=uri_hash,json=uriHash,proto3" json:"uri_hash,omitempty"` } @@ -124,7 +124,7 @@ type NFT struct { Uri string `protobuf:"bytes,3,opt,name=uri,proto3" json:"uri,omitempty"` // uri_hash is a hash of the document pointed to uri UriHash string `protobuf:"bytes,4,opt,name=uri_hash,json=uriHash,proto3" json:"uri_hash,omitempty"` - // data is the metadata of the NFT + // data is the metadata of the NFT,optional Data *types.Any `protobuf:"bytes,10,opt,name=data,proto3" json:"data,omitempty"` } diff --git a/x/nft/validation.go b/x/nft/validation.go new file mode 100644 index 000000000000..76621190f764 --- /dev/null +++ b/x/nft/validation.go @@ -0,0 +1,33 @@ +package nft + +import ( + fmt "fmt" + "regexp" +) + +var ( + // reClassIDString can be 3 ~ 100 characters long and support letters, followed by either + // a letter, a number or a slash ('/') or a colon (':'). + reClassIDString = `[a-zA-Z][a-zA-Z0-9/-:]{2,100}` + reClassID = regexp.MustCompile(fmt.Sprintf(`^%s$`, reClassIDString)) + + // reNFTIDString can be 3 ~ 100 characters long and support letters, followed by either + // a letter, a number or a slash ('/') or a colon (':'). + reNFTID = reClassID +) + +// ValidateClassID returns whether the class id is valid +func ValidateClassID(id string) error { + if !reClassID.MatchString(id) { + return fmt.Errorf("invalid class id: %s", id) + } + return nil +} + +// ValidateNFTID returns whether the nft id is valid +func ValidateNFTID(id string) error { + if !reNFTID.MatchString(id) { + return fmt.Errorf("invalid nft id: %s", id) + } + return nil +}