feat: add batch operation for x/nft module (#12187)
This commit is contained in:
parent
4c3b7af936
commit
d705a8bc8f
@ -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
|
||||
|
||||
|
||||
@ -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
84
x/nft/keeper/nft_batch.go
Normal 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
|
||||
}
|
||||
363
x/nft/keeper/nft_batch_test.go
Normal file
363
x/nft/keeper/nft_batch_test.go
Normal 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)
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user