feat: add batch operation for x/nft module (#12187)

This commit is contained in:
Zhiqiang Zhang 2022-06-15 12:38:09 +08:00 committed by GitHub
parent 4c3b7af936
commit d705a8bc8f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 484 additions and 2 deletions

View File

@ -45,6 +45,7 @@ Ref: https://keepachangelog.com/en/1.0.0/
* [#12089](https://github.com/cosmos/cosmos-sdk/pull/12089) Mark the `TipDecorator` as beta, don't include it in simapp by default.
* [#12153](https://github.com/cosmos/cosmos-sdk/pull/12153) Add a new `NewSimulationManagerFromAppModules` constructor, to simplify simulation wiring.
* [#12187](https://github.com/cosmos/cosmos-sdk/pull/12187) Add batch operation for x/nft module.
### API Breaking Changes

View File

@ -17,6 +17,14 @@ func (k Keeper) Mint(ctx sdk.Context, token nft.NFT, receiver sdk.AccAddress) er
return sdkerrors.Wrap(nft.ErrNFTExists, token.Id)
}
k.mintWithNoCheck(ctx, token, receiver)
return nil
}
// mintWithNoCheck defines a method for minting a new nft
// Note: this method does not check whether the class already exists in nft.
// The upper-layer application needs to check it when it needs to use it.
func (k Keeper) mintWithNoCheck(ctx sdk.Context, token nft.NFT, receiver sdk.AccAddress) {
k.setNFT(ctx, token)
k.setOwner(ctx, token.ClassId, token.Id, receiver)
k.incrTotalSupply(ctx, token.ClassId)
@ -26,7 +34,6 @@ func (k Keeper) Mint(ctx sdk.Context, token nft.NFT, receiver sdk.AccAddress) er
Id: token.Id,
Owner: receiver.String(),
})
return nil
}
// Burn defines a method for burning a nft from a specific account.
@ -40,6 +47,14 @@ func (k Keeper) Burn(ctx sdk.Context, classID string, nftID string) error {
return sdkerrors.Wrap(nft.ErrNFTNotExists, nftID)
}
k.burnWithNoCheck(ctx, classID, nftID)
return nil
}
// burnWithNoCheck defines a method for burning a nft from a specific account.
// Note: this method does not check whether the class already exists in nft.
// The upper-layer application needs to check it when it needs to use it
func (k Keeper) burnWithNoCheck(ctx sdk.Context, classID string, nftID string) error {
owner := k.GetOwner(ctx, classID, nftID)
nftStore := k.getNFTStore(ctx, classID)
nftStore.Delete([]byte(nftID))
@ -64,10 +79,17 @@ func (k Keeper) Update(ctx sdk.Context, token nft.NFT) error {
if !k.HasNFT(ctx, token.ClassId, token.Id) {
return sdkerrors.Wrap(nft.ErrNFTNotExists, token.Id)
}
k.setNFT(ctx, token)
k.updateWithNoCheck(ctx, token)
return nil
}
// Update defines a method for updating an exist nft
// Note: this method does not check whether the class already exists in nft.
// The upper-layer application needs to check it when it needs to use it
func (k Keeper) updateWithNoCheck(ctx sdk.Context, token nft.NFT) {
k.setNFT(ctx, token)
}
// 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,
@ -83,6 +105,18 @@ func (k Keeper) Transfer(ctx sdk.Context,
return sdkerrors.Wrap(nft.ErrNFTNotExists, nftID)
}
k.transferWithNoCheck(ctx, classID, nftID, receiver)
return nil
}
// Transfer defines a method for sending a nft from one account to another account.
// Note: this method does not check whether the class already exists in nft.
// The upper-layer application needs to check it when it needs to use it
func (k Keeper) transferWithNoCheck(ctx sdk.Context,
classID string,
nftID string,
receiver sdk.AccAddress,
) error {
owner := k.GetOwner(ctx, classID, nftID)
k.deleteOwner(ctx, classID, nftID, owner)
k.setOwner(ctx, classID, nftID, receiver)

84
x/nft/keeper/nft_batch.go Normal file
View File

@ -0,0 +1,84 @@
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"
)
// BatchMint defines a method for minting a batch of nfts
func (k Keeper) BatchMint(ctx sdk.Context,
tokens []nft.NFT,
receiver sdk.AccAddress,
) error {
checked := make(map[string]bool, len(tokens))
for _, token := range tokens {
if !checked[token.ClassId] && !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)
}
checked[token.ClassId] = true
k.mintWithNoCheck(ctx, token, receiver)
}
return nil
}
// BatchBurn defines a method for burning a batch of nfts from a specific classID.
// Note: When the upper module uses this method, it needs to authenticate nft
func (k Keeper) BatchBurn(ctx sdk.Context, classID string, nftIDs []string) error {
if !k.HasClass(ctx, classID) {
return sdkerrors.Wrap(nft.ErrClassNotExists, classID)
}
for _, nftID := range nftIDs {
if !k.HasNFT(ctx, classID, nftID) {
return sdkerrors.Wrap(nft.ErrNFTNotExists, nftID)
}
if err := k.burnWithNoCheck(ctx, classID, nftID); err != nil {
return err
}
}
return nil
}
// BatchUpdate defines a method for updating a batch of exist nfts
// Note: When the upper module uses this method, it needs to authenticate nft
func (k Keeper) BatchUpdate(ctx sdk.Context, tokens []nft.NFT) error {
checked := make(map[string]bool, len(tokens))
for _, token := range tokens {
if !checked[token.ClassId] && !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)
}
checked[token.ClassId] = true
k.updateWithNoCheck(ctx, token)
}
return nil
}
// BatchTransfer defines a method for sending a batch of nfts from one account to another account from a specific classID.
// Note: When the upper module uses this method, it needs to authenticate nft
func (k Keeper) BatchTransfer(ctx sdk.Context,
classID string,
nftIDs []string,
receiver sdk.AccAddress,
) error {
if !k.HasClass(ctx, classID) {
return sdkerrors.Wrap(nft.ErrClassNotExists, classID)
}
for _, nftID := range nftIDs {
if !k.HasNFT(ctx, classID, nftID) {
return sdkerrors.Wrap(nft.ErrNFTNotExists, nftID)
}
if err := k.transferWithNoCheck(ctx, classID, nftID, receiver); err != nil {
return sdkerrors.Wrap(nft.ErrNFTNotExists, nftID)
}
}
return nil
}

View File

@ -0,0 +1,363 @@
package keeper_test
import (
"fmt"
"math/rand"
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/cosmos/cosmos-sdk/x/nft"
)
func (s *TestSuite) TestBatchMint() {
receiver := s.addrs[0]
testCases := []struct {
msg string
malleate func([]nft.NFT)
tokens []nft.NFT
expPass bool
}{
{
"success with empty nft",
func(tokens []nft.NFT) {
s.saveClass(tokens)
},
[]nft.NFT{},
true,
},
{
"success with single nft",
func(tokens []nft.NFT) {
s.saveClass(tokens)
},
[]nft.NFT{
{ClassId: "classID1", Id: "nftID1"},
},
true,
},
{
"success with multiple nft",
func(tokens []nft.NFT) {
s.saveClass(tokens)
},
[]nft.NFT{
{ClassId: "classID1", Id: "nftID1"},
{ClassId: "classID1", Id: "nftID2"},
},
true,
},
{
"success with multiple class and multiple nft",
func(tokens []nft.NFT) {
s.saveClass(tokens)
},
[]nft.NFT{
{ClassId: "classID1", Id: "nftID1"},
{ClassId: "classID1", Id: "nftID2"},
{ClassId: "classID2", Id: "nftID1"},
{ClassId: "classID2", Id: "nftID2"},
},
true,
},
{
"faild with repeated nft",
func(tokens []nft.NFT) {
s.saveClass(tokens)
},
[]nft.NFT{
{ClassId: "classID1", Id: "nftID1"},
{ClassId: "classID1", Id: "nftID1"},
{ClassId: "classID2", Id: "nftID2"},
},
false,
},
{
"faild with not exist class",
func(tokens []nft.NFT) {
//do nothing
},
[]nft.NFT{
{ClassId: "classID1", Id: "nftID1"},
{ClassId: "classID1", Id: "nftID1"},
{ClassId: "classID2", Id: "nftID2"},
},
false,
},
{
"faild with exist nft",
func(tokens []nft.NFT) {
s.saveClass(tokens)
idx := rand.Intn(len(tokens))
s.nftKeeper.Mint(s.ctx, tokens[idx], receiver)
},
[]nft.NFT{
{ClassId: "classID1", Id: "nftID1"},
{ClassId: "classID1", Id: "nftID2"},
{ClassId: "classID2", Id: "nftID2"},
},
false,
},
}
for _, tc := range testCases {
s.Run(fmt.Sprintf("Case %s", tc.msg), func() {
s.SetupTest() // reset
tc.malleate(tc.tokens)
err := s.nftKeeper.BatchMint(s.ctx, tc.tokens, receiver)
if tc.expPass {
s.Require().NoError(err)
classMap := groupByClassID(tc.tokens)
for classID, tokens := range classMap {
for _, token := range tokens {
actNFT, has := s.nftKeeper.GetNFT(s.ctx, token.ClassId, token.Id)
s.Require().True(has)
s.Require().EqualValues(token, actNFT)
owner := s.nftKeeper.GetOwner(s.ctx, token.ClassId, token.Id)
s.Require().True(receiver.Equals(owner))
}
actNFTs := s.nftKeeper.GetNFTsOfClass(s.ctx, classID)
s.Require().EqualValues(tokens, actNFTs)
actNFTs = s.nftKeeper.GetNFTsOfClassByOwner(s.ctx, classID, receiver)
s.Require().EqualValues(tokens, actNFTs)
balance := s.nftKeeper.GetBalance(s.ctx, classID, receiver)
s.Require().EqualValues(len(tokens), balance)
supply := s.nftKeeper.GetTotalSupply(s.ctx, classID)
s.Require().EqualValues(len(tokens), supply)
}
return
}
s.Require().Error(err)
})
}
}
func (s *TestSuite) TestBatchBurn() {
receiver := s.addrs[0]
tokens := []nft.NFT{
{ClassId: "classID1", Id: "nftID1"},
{ClassId: "classID1", Id: "nftID2"},
{ClassId: "classID2", Id: "nftID1"},
{ClassId: "classID2", Id: "nftID2"},
}
testCases := []struct {
msg string
malleate func()
classID string
nftIDs []string
expPass bool
}{
{
"success",
func() {
s.saveClass(tokens)
s.nftKeeper.BatchMint(s.ctx, tokens, receiver)
},
"classID1",
[]string{"nftID1", "nftID2"},
true,
},
{
"failed with not exist classID",
func() {},
"classID1",
[]string{"nftID1", "nftID2"},
false,
},
{
"failed with not exist nftID",
func() {
s.saveClass(tokens)
},
"classID1",
[]string{"nftID1", "nftID2"},
false,
},
}
for _, tc := range testCases {
s.Run(fmt.Sprintf("Case %s", tc.msg), func() {
s.SetupTest() // reset
tc.malleate()
err := s.nftKeeper.BatchBurn(s.ctx, tc.classID, tc.nftIDs)
if tc.expPass {
s.Require().NoError(err)
for _, nftID := range tc.nftIDs {
s.Require().False(s.nftKeeper.HasNFT(s.ctx, tc.classID, nftID))
}
return
}
s.Require().Error(err)
})
}
}
func (s *TestSuite) TestBatchUpdate() {
receiver := s.addrs[0]
tokens := []nft.NFT{
{ClassId: "classID1", Id: "nftID1"},
{ClassId: "classID1", Id: "nftID2"},
{ClassId: "classID2", Id: "nftID1"},
{ClassId: "classID2", Id: "nftID2"},
}
testCases := []struct {
msg string
malleate func()
tokens []nft.NFT
expPass bool
}{
{
"success",
func() {
s.saveClass(tokens)
s.nftKeeper.BatchMint(s.ctx, tokens, receiver)
},
[]nft.NFT{
{ClassId: "classID1", Id: "nftID1", Uri: "nftID1_URI"},
{ClassId: "classID2", Id: "nftID2", Uri: "nftID2_URI"},
},
true,
},
{
"failed with not exist classID",
func() {},
[]nft.NFT{
{ClassId: "classID1", Id: "nftID1", Uri: "nftID1_URI"},
{ClassId: "classID2", Id: "nftID2", Uri: "nftID2_URI"},
},
false,
},
{
"failed with not exist nftID",
func() {
s.saveClass(tokens)
},
[]nft.NFT{
{ClassId: "classID1", Id: "nftID1", Uri: "nftID1_URI"},
{ClassId: "classID2", Id: "nftID2", Uri: "nftID2_URI"},
},
false,
},
}
for _, tc := range testCases {
s.Run(fmt.Sprintf("Case %s", tc.msg), func() {
s.SetupTest() // reset
tc.malleate()
err := s.nftKeeper.BatchUpdate(s.ctx, tc.tokens)
if tc.expPass {
s.Require().NoError(err)
for _, token := range tc.tokens {
actToken, found := s.nftKeeper.GetNFT(s.ctx, token.ClassId, token.Id)
s.Require().True(found)
s.Require().EqualValues(token, actToken)
}
return
}
s.Require().Error(err)
})
}
}
func (s *TestSuite) TestBatchTransfer() {
owner := s.addrs[0]
receiver := s.addrs[1]
tokens := []nft.NFT{
{ClassId: "classID1", Id: "nftID1"},
{ClassId: "classID1", Id: "nftID2"},
{ClassId: "classID2", Id: "nftID1"},
{ClassId: "classID2", Id: "nftID2"},
}
testCases := []struct {
msg string
malleate func()
classID string
nftIDs []string
expPass bool
}{
{
"success",
func() {
s.saveClass(tokens)
s.nftKeeper.BatchMint(s.ctx, tokens, owner)
},
"classID1",
[]string{"nftID1", "nftID2"},
true,
},
{
"failed with not exist classID",
func() {
s.saveClass(tokens)
s.nftKeeper.BatchMint(s.ctx, tokens, receiver)
},
"classID3",
[]string{"nftID1", "nftID2"},
false,
},
{
"failed with not exist nftID",
func() {
s.saveClass(tokens)
},
"classID1",
[]string{"nftID1", "nftID2"},
false,
},
}
for _, tc := range testCases {
s.Run(fmt.Sprintf("Case %s", tc.msg), func() {
s.SetupTest() // reset
tc.malleate()
err := s.nftKeeper.BatchTransfer(s.ctx, tc.classID, tc.nftIDs, receiver)
if tc.expPass {
s.Require().NoError(err)
for _, nftID := range tc.nftIDs {
actOwner := s.nftKeeper.GetOwner(s.ctx, tc.classID, nftID)
s.Require().EqualValues(receiver, actOwner)
}
return
}
s.Require().Error(err)
})
}
}
func groupByClassID(tokens []nft.NFT) map[string][]nft.NFT {
classMap := make(map[string][]nft.NFT, len(tokens))
for _, token := range tokens {
if _, ok := classMap[token.ClassId]; !ok {
classMap[token.ClassId] = make([]nft.NFT, 0)
}
classMap[token.ClassId] = append(classMap[token.ClassId], token)
}
return classMap
}
func (s *TestSuite) saveClass(tokens []nft.NFT) {
classMap := groupByClassID(tokens)
for classID := range classMap {
err := s.nftKeeper.SaveClass(s.ctx, nft.Class{Id: classID})
s.Require().NoError(err)
}
}
func (s *TestSuite) mintNFT(tokens []nft.NFT, receiver sdk.AccAddress) {
for _, token := range tokens {
err := s.nftKeeper.Mint(s.ctx, token, receiver)
s.Require().NoError(err)
}
}