github.com/Finschia/finschia-sdk@v0.48.1/x/collection/keeper/msg_server_test.go (about)

     1  package keeper_test
     2  
     3  import (
     4  	abci "github.com/tendermint/tendermint/abci/types"
     5  
     6  	"github.com/Finschia/finschia-sdk/testutil"
     7  	sdk "github.com/Finschia/finschia-sdk/types"
     8  	"github.com/Finschia/finschia-sdk/types/query"
     9  	"github.com/Finschia/finschia-sdk/x/collection"
    10  	"github.com/Finschia/finschia-sdk/x/token/class"
    11  )
    12  
    13  func (s *KeeperTestSuite) TestMsgSendFT() {
    14  	testCases := map[string]struct {
    15  		isNegativeCase bool
    16  		req            *collection.MsgSendFT
    17  		ftID           string
    18  		expectedEvents sdk.Events
    19  		expectedError  error
    20  	}{
    21  		"valid request": {
    22  			req: &collection.MsgSendFT{
    23  				ContractId: s.contractID,
    24  				From:       s.vendor.String(),
    25  				To:         s.customer.String(),
    26  				Amount:     collection.NewCoins(collection.NewFTCoin(s.ftClassID, s.balance)),
    27  			},
    28  			ftID: collection.NewFTID(s.ftClassID),
    29  			expectedEvents: sdk.Events{
    30  				sdk.Event{
    31  					Type: "lbm.collection.v1.EventSent",
    32  					Attributes: []abci.EventAttribute{
    33  						{Key: []byte("amount"), Value: testutil.MustJSONMarshal(collection.NewCoins(collection.NewFTCoin(s.ftClassID, s.balance))), Index: false},
    34  						{Key: []byte("contract_id"), Value: testutil.W(s.contractID), Index: false},
    35  						{Key: []byte("from"), Value: testutil.W(s.vendor.String()), Index: false},
    36  						{Key: []byte("operator"), Value: testutil.W(s.vendor.String()), Index: false},
    37  						{Key: []byte("to"), Value: testutil.W(s.customer.String()), Index: false},
    38  					},
    39  				}},
    40  		},
    41  		"contract not found": {
    42  			isNegativeCase: true,
    43  			req: &collection.MsgSendFT{
    44  				ContractId: "deadbeef",
    45  				From:       s.vendor.String(),
    46  				To:         s.customer.String(),
    47  				Amount:     collection.NewCoins(collection.NewFTCoin(s.ftClassID, s.balance)),
    48  			},
    49  			ftID:          collection.NewFTID(s.ftClassID),
    50  			expectedError: class.ErrContractNotExist,
    51  		},
    52  		"insufficient funds": {
    53  			isNegativeCase: true,
    54  			req: &collection.MsgSendFT{
    55  				ContractId: s.contractID,
    56  				From:       s.vendor.String(),
    57  				To:         s.customer.String(),
    58  				Amount:     collection.NewCoins(collection.NewFTCoin(s.ftClassID, s.balance.Add(sdk.OneInt()))),
    59  			},
    60  			ftID:          collection.NewFTID(s.ftClassID),
    61  			expectedError: collection.ErrInsufficientToken,
    62  		},
    63  	}
    64  
    65  	for name, tc := range testCases {
    66  		s.Run(name, func() {
    67  			// Arrange
    68  			s.Require().NoError(tc.req.ValidateBasic())
    69  			from, err := sdk.AccAddressFromBech32(tc.req.From)
    70  			s.Require().NoError(err)
    71  			to, err := sdk.AccAddressFromBech32(tc.req.To)
    72  			s.Require().NoError(err)
    73  			ctx, _ := s.ctx.CacheContext()
    74  			prevFromBalance := s.keeper.GetBalance(ctx, tc.req.ContractId, from, tc.ftID)
    75  			prevToBalance := s.keeper.GetBalance(ctx, tc.req.ContractId, to, tc.ftID)
    76  
    77  			// Act
    78  			res, err := s.msgServer.SendFT(sdk.WrapSDKContext(ctx), tc.req)
    79  			if tc.isNegativeCase {
    80  				s.Require().ErrorIs(err, tc.expectedError)
    81  				return
    82  			}
    83  			s.Require().NoError(err)
    84  			s.Require().NotNil(res)
    85  
    86  			// Assert
    87  			events := ctx.EventManager().Events()
    88  			s.Require().Equal(tc.expectedEvents, events)
    89  			curFromBalance := s.keeper.GetBalance(ctx, tc.req.ContractId, from, tc.ftID)
    90  			curToBalance := s.keeper.GetBalance(ctx, tc.req.ContractId, to, tc.ftID)
    91  			s.Require().Equal(prevFromBalance.Sub(tc.req.Amount[0].Amount).Abs(), curFromBalance.Abs())
    92  			s.Require().Equal(prevToBalance.Add(tc.req.Amount[0].Amount), curToBalance)
    93  		})
    94  	}
    95  }
    96  
    97  func (s *KeeperTestSuite) TestMsgOperatorSendFT() {
    98  	testCases := map[string]struct {
    99  		isNegativeCase bool
   100  		req            *collection.MsgOperatorSendFT
   101  		ftID           string
   102  		expectedEvents sdk.Events
   103  		expectedError  error
   104  	}{
   105  		"valid request": {
   106  			req: &collection.MsgOperatorSendFT{
   107  				ContractId: s.contractID,
   108  				Operator:   s.operator.String(),
   109  				From:       s.customer.String(),
   110  				To:         s.vendor.String(),
   111  				Amount:     collection.NewCoins(collection.NewFTCoin(s.ftClassID, s.balance)),
   112  			},
   113  			ftID: collection.NewFTID(s.ftClassID),
   114  			expectedEvents: sdk.Events{
   115  				sdk.Event{
   116  					Type: "lbm.collection.v1.EventSent",
   117  					Attributes: []abci.EventAttribute{
   118  						{Key: []byte("amount"), Value: testutil.MustJSONMarshal(collection.NewCoins(collection.NewFTCoin(s.ftClassID, s.balance))), Index: false},
   119  						{Key: []byte("contract_id"), Value: testutil.W(s.contractID), Index: false},
   120  						{Key: []byte("from"), Value: testutil.W(s.customer.String()), Index: false},
   121  						{Key: []byte("operator"), Value: testutil.W(s.operator.String()), Index: false},
   122  						{Key: []byte("to"), Value: testutil.W(s.vendor.String()), Index: false},
   123  					},
   124  				},
   125  			},
   126  		},
   127  		"contract not found": {
   128  			isNegativeCase: true,
   129  			req: &collection.MsgOperatorSendFT{
   130  				ContractId: "deadbeef",
   131  				Operator:   s.operator.String(),
   132  				From:       s.customer.String(),
   133  				To:         s.vendor.String(),
   134  				Amount:     collection.NewCoins(collection.NewFTCoin(s.ftClassID, s.balance)),
   135  			},
   136  			expectedError: class.ErrContractNotExist,
   137  		},
   138  		"not approved": {
   139  			isNegativeCase: true,
   140  			req: &collection.MsgOperatorSendFT{
   141  				ContractId: s.contractID,
   142  				Operator:   s.vendor.String(),
   143  				From:       s.customer.String(),
   144  				To:         s.vendor.String(),
   145  				Amount:     collection.NewCoins(collection.NewFTCoin(s.ftClassID, s.balance)),
   146  			},
   147  			expectedError: collection.ErrCollectionNotApproved,
   148  		},
   149  		"insufficient funds": {
   150  			isNegativeCase: true,
   151  			req: &collection.MsgOperatorSendFT{
   152  				ContractId: s.contractID,
   153  				Operator:   s.operator.String(),
   154  				From:       s.customer.String(),
   155  				To:         s.vendor.String(),
   156  				Amount:     collection.NewCoins(collection.NewFTCoin(s.ftClassID, s.balance.Add(sdk.OneInt()))),
   157  			},
   158  			expectedError: collection.ErrInsufficientToken,
   159  		},
   160  	}
   161  
   162  	for name, tc := range testCases {
   163  		s.Run(name, func() {
   164  			// Arrange
   165  			s.Require().NoError(tc.req.ValidateBasic())
   166  			from, err := sdk.AccAddressFromBech32(tc.req.From)
   167  			s.Require().NoError(err)
   168  			to, err := sdk.AccAddressFromBech32(tc.req.To)
   169  			s.Require().NoError(err)
   170  			operator, err := sdk.AccAddressFromBech32(tc.req.Operator)
   171  			s.Require().NoError(err)
   172  			ctx, _ := s.ctx.CacheContext()
   173  			prevFromBalance := s.keeper.GetBalance(ctx, tc.req.ContractId, from, tc.ftID)
   174  			prevToBalance := s.keeper.GetBalance(ctx, tc.req.ContractId, to, tc.ftID)
   175  			prevOperatorBalance := s.keeper.GetBalance(ctx, tc.req.ContractId, operator, tc.ftID)
   176  
   177  			// Act
   178  			res, err := s.msgServer.OperatorSendFT(sdk.WrapSDKContext(ctx), tc.req)
   179  			if tc.isNegativeCase {
   180  				s.Require().ErrorIs(err, tc.expectedError)
   181  				return
   182  			}
   183  			s.Require().NoError(err)
   184  			s.Require().NotNil(res)
   185  
   186  			// Assert
   187  			events := ctx.EventManager().Events()
   188  			s.Require().Equal(tc.expectedEvents, events)
   189  			curFromBalance := s.keeper.GetBalance(ctx, tc.req.ContractId, from, tc.ftID)
   190  			curToBalance := s.keeper.GetBalance(ctx, tc.req.ContractId, to, tc.ftID)
   191  			curOperatorBalance := s.keeper.GetBalance(ctx, tc.req.ContractId, operator, tc.ftID)
   192  			s.Require().Equal(prevFromBalance.Sub(tc.req.Amount[0].Amount).Abs(), curFromBalance.Abs())
   193  			s.Require().Equal(prevToBalance.Add(tc.req.Amount[0].Amount), curToBalance)
   194  			s.Require().Equal(prevOperatorBalance, curOperatorBalance)
   195  		})
   196  	}
   197  }
   198  
   199  func (s *KeeperTestSuite) TestMsgSendNFT() {
   200  	rootNFTID := collection.NewNFTID(s.nftClassID, 1)
   201  	issuedTokenIDs := s.extractChainedNFTIDs(rootNFTID)
   202  
   203  	testCases := map[string]struct {
   204  		contractID string
   205  		tokenID    string
   206  		err        error
   207  		events     sdk.Events
   208  	}{
   209  		"valid request": {
   210  			contractID: s.contractID,
   211  			tokenID:    rootNFTID,
   212  			events: sdk.Events{
   213  				sdk.Event{
   214  					Type: "lbm.collection.v1.EventOwnerChanged",
   215  					Attributes: []abci.EventAttribute{
   216  						{Key: []byte("contract_id"), Value: testutil.W(s.contractID), Index: false},
   217  						{Key: []byte("from"), Value: testutil.W(s.customer.String()), Index: false},
   218  						{Key: []byte("to"), Value: testutil.W(s.vendor.String()), Index: false},
   219  						{Key: []byte("token_id"), Value: testutil.W(issuedTokenIDs[1]), Index: false},
   220  					},
   221  				},
   222  				sdk.Event{
   223  					Type: "lbm.collection.v1.EventOwnerChanged",
   224  					Attributes: []abci.EventAttribute{
   225  						{Key: []byte("contract_id"), Value: testutil.W(s.contractID), Index: false},
   226  						{Key: []byte("from"), Value: testutil.W(s.customer.String()), Index: false},
   227  						{Key: []byte("to"), Value: testutil.W(s.vendor.String()), Index: false},
   228  						{Key: []byte("token_id"), Value: testutil.W(issuedTokenIDs[2]), Index: false},
   229  					},
   230  				},
   231  				sdk.Event{
   232  					Type: "lbm.collection.v1.EventOwnerChanged",
   233  					Attributes: []abci.EventAttribute{
   234  						{Key: []byte("contract_id"), Value: testutil.W(s.contractID), Index: false},
   235  						{Key: []byte("from"), Value: testutil.W(s.customer.String()), Index: false},
   236  						{Key: []byte("to"), Value: testutil.W(s.vendor.String()), Index: false},
   237  						{Key: []byte("token_id"), Value: testutil.W(issuedTokenIDs[3]), Index: false},
   238  					},
   239  				},
   240  				sdk.Event{
   241  					Type: "lbm.collection.v1.EventSent",
   242  					Attributes: []abci.EventAttribute{
   243  						{Key: []byte("amount"), Value: testutil.MustJSONMarshal(collection.NewCoins(collection.Coin{TokenId: issuedTokenIDs[0], Amount: sdk.OneInt()})), Index: false},
   244  						{Key: []byte("contract_id"), Value: testutil.W(s.contractID), Index: false},
   245  						{Key: []byte("from"), Value: testutil.W(s.customer.String()), Index: false},
   246  						{Key: []byte("operator"), Value: testutil.W(s.customer.String()), Index: false},
   247  						{Key: []byte("to"), Value: testutil.W(s.vendor.String()), Index: false},
   248  					},
   249  				},
   250  			},
   251  		},
   252  		"contract not found": {
   253  			contractID: "deadbeef",
   254  			tokenID:    collection.NewNFTID(s.nftClassID, 1),
   255  			err:        class.ErrContractNotExist,
   256  		},
   257  		"not found": {
   258  			contractID: s.contractID,
   259  			tokenID:    collection.NewNFTID("deadbeef", 1),
   260  			err:        collection.ErrTokenNotExist,
   261  		},
   262  		"child": {
   263  			contractID: s.contractID,
   264  			tokenID:    collection.NewNFTID(s.nftClassID, 2),
   265  			err:        collection.ErrTokenCannotTransferChildToken,
   266  		},
   267  		"not owned by": {
   268  			contractID: s.contractID,
   269  			tokenID:    collection.NewNFTID(s.nftClassID, s.numNFTs+1),
   270  			err:        collection.ErrTokenNotOwnedBy,
   271  		},
   272  	}
   273  
   274  	for name, tc := range testCases {
   275  		s.Run(name, func() {
   276  			ctx, _ := s.ctx.CacheContext()
   277  
   278  			req := &collection.MsgSendNFT{
   279  				ContractId: tc.contractID,
   280  				From:       s.customer.String(),
   281  				To:         s.vendor.String(),
   282  				TokenIds:   []string{tc.tokenID},
   283  			}
   284  			res, err := s.msgServer.SendNFT(sdk.WrapSDKContext(ctx), req)
   285  			s.Require().ErrorIs(err, tc.err)
   286  			if tc.err != nil {
   287  				return
   288  			}
   289  
   290  			s.Require().NotNil(res)
   291  			s.Require().Equal(tc.events, ctx.EventManager().Events())
   292  		})
   293  	}
   294  }
   295  
   296  func (s *KeeperTestSuite) TestMsgOperatorSendNFT() {
   297  	rootNFTID := collection.NewNFTID(s.nftClassID, 1)
   298  	issuedTokenIDs := s.extractChainedNFTIDs(rootNFTID)
   299  
   300  	testCases := map[string]struct {
   301  		contractID string
   302  		operator   sdk.AccAddress
   303  		from       sdk.AccAddress
   304  		tokenID    string
   305  		err        error
   306  		events     sdk.Events
   307  	}{
   308  		"valid request": {
   309  			contractID: s.contractID,
   310  			operator:   s.operator,
   311  			from:       s.customer,
   312  			tokenID:    rootNFTID,
   313  			events: sdk.Events{
   314  				sdk.Event{
   315  					Type: "lbm.collection.v1.EventOwnerChanged",
   316  					Attributes: []abci.EventAttribute{
   317  						{Key: []byte("contract_id"), Value: testutil.W(s.contractID), Index: false},
   318  						{Key: []byte("from"), Value: testutil.W(s.customer.String()), Index: false},
   319  						{Key: []byte("to"), Value: testutil.W(s.vendor.String()), Index: false},
   320  						{Key: []byte("token_id"), Value: testutil.W(issuedTokenIDs[1]), Index: false},
   321  					},
   322  				},
   323  				sdk.Event{
   324  					Type: "lbm.collection.v1.EventOwnerChanged",
   325  					Attributes: []abci.EventAttribute{
   326  						{Key: []byte("contract_id"), Value: testutil.W(s.contractID), Index: false},
   327  						{Key: []byte("from"), Value: testutil.W(s.customer.String()), Index: false},
   328  						{Key: []byte("to"), Value: testutil.W(s.vendor.String()), Index: false},
   329  						{Key: []byte("token_id"), Value: testutil.W(issuedTokenIDs[2]), Index: false},
   330  					},
   331  				},
   332  				sdk.Event{
   333  					Type: "lbm.collection.v1.EventOwnerChanged",
   334  					Attributes: []abci.EventAttribute{
   335  						{Key: []byte("contract_id"), Value: testutil.W(s.contractID), Index: false},
   336  						{Key: []byte("from"), Value: testutil.W(s.customer.String()), Index: false},
   337  						{Key: []byte("to"), Value: testutil.W(s.vendor.String()), Index: false},
   338  						{Key: []byte("token_id"), Value: testutil.W(issuedTokenIDs[3]), Index: false},
   339  					},
   340  				},
   341  				sdk.Event{
   342  					Type: "lbm.collection.v1.EventSent",
   343  					Attributes: []abci.EventAttribute{
   344  						{Key: []byte("amount"), Value: testutil.MustJSONMarshal(collection.NewCoins(collection.Coin{TokenId: issuedTokenIDs[0], Amount: sdk.OneInt()})), Index: false},
   345  						{Key: []byte("contract_id"), Value: testutil.W(s.contractID), Index: false},
   346  						{Key: []byte("from"), Value: testutil.W(s.customer.String()), Index: false},
   347  						{Key: []byte("operator"), Value: testutil.W(s.operator.String()), Index: false},
   348  						{Key: []byte("to"), Value: testutil.W(s.vendor.String()), Index: false},
   349  					},
   350  				},
   351  			}},
   352  		"contract not found": {
   353  			contractID: "deadbeef",
   354  			operator:   s.operator,
   355  			from:       s.customer,
   356  			tokenID:    rootNFTID,
   357  			err:        class.ErrContractNotExist,
   358  		},
   359  		"not approved": {
   360  			contractID: s.contractID,
   361  			operator:   s.vendor,
   362  			from:       s.customer,
   363  			tokenID:    rootNFTID,
   364  			err:        collection.ErrCollectionNotApproved,
   365  		},
   366  		"not found": {
   367  			contractID: s.contractID,
   368  			operator:   s.operator,
   369  			from:       s.customer,
   370  			tokenID:    collection.NewNFTID("deadbeef", 1),
   371  			err:        collection.ErrTokenNotExist,
   372  		},
   373  		"child": {
   374  			contractID: s.contractID,
   375  			operator:   s.operator,
   376  			from:       s.customer,
   377  			tokenID:    collection.NewNFTID(s.nftClassID, 2),
   378  			err:        collection.ErrTokenCannotTransferChildToken,
   379  		},
   380  		"not owned by": {
   381  			contractID: s.contractID,
   382  			operator:   s.operator,
   383  			from:       s.customer,
   384  			tokenID:    collection.NewNFTID(s.nftClassID, s.numNFTs+1),
   385  			err:        collection.ErrTokenNotOwnedBy,
   386  		},
   387  	}
   388  
   389  	for name, tc := range testCases {
   390  		s.Run(name, func() {
   391  			ctx, _ := s.ctx.CacheContext()
   392  
   393  			req := &collection.MsgOperatorSendNFT{
   394  				ContractId: tc.contractID,
   395  				Operator:   tc.operator.String(),
   396  				From:       tc.from.String(),
   397  				To:         s.vendor.String(),
   398  				TokenIds:   []string{tc.tokenID},
   399  			}
   400  			res, err := s.msgServer.OperatorSendNFT(sdk.WrapSDKContext(ctx), req)
   401  			s.Require().ErrorIs(err, tc.err)
   402  			if tc.err != nil {
   403  				return
   404  			}
   405  
   406  			s.Require().NotNil(res)
   407  			s.Require().Equal(tc.events, ctx.EventManager().Events())
   408  		})
   409  	}
   410  }
   411  
   412  func (s *KeeperTestSuite) TestMsgAuthorizeOperator() {
   413  	testCases := map[string]struct {
   414  		isNegativeCase bool
   415  		req            *collection.MsgAuthorizeOperator
   416  		events         sdk.Events
   417  		expectedError  error
   418  	}{
   419  		"valid request": {
   420  			req: &collection.MsgAuthorizeOperator{
   421  				ContractId: s.contractID,
   422  				Holder:     s.customer.String(),
   423  				Operator:   s.vendor.String(),
   424  			},
   425  			events: sdk.Events{sdk.Event{
   426  				Type: "lbm.collection.v1.EventAuthorizedOperator",
   427  				Attributes: []abci.EventAttribute{
   428  					{Key: []byte("contract_id"), Value: testutil.W(s.contractID), Index: false},
   429  					{Key: []byte("holder"), Value: testutil.W(s.customer.String()), Index: false},
   430  					{Key: []byte("operator"), Value: testutil.W(s.vendor.String()), Index: false},
   431  				},
   432  			}},
   433  		},
   434  		"contract not found": {
   435  			isNegativeCase: true,
   436  			req: &collection.MsgAuthorizeOperator{
   437  				ContractId: "deadbeef",
   438  				Holder:     s.customer.String(),
   439  				Operator:   s.vendor.String(),
   440  			},
   441  			expectedError: class.ErrContractNotExist,
   442  		},
   443  		"already approved": {
   444  			isNegativeCase: true,
   445  			req: &collection.MsgAuthorizeOperator{
   446  				ContractId: s.contractID,
   447  				Holder:     s.customer.String(),
   448  				Operator:   s.operator.String(),
   449  			},
   450  			expectedError: collection.ErrCollectionAlreadyApproved,
   451  		},
   452  	}
   453  
   454  	for name, tc := range testCases {
   455  		s.Run(name, func() {
   456  			// Arrange
   457  			s.Require().NoError(tc.req.ValidateBasic())
   458  			holder, err := sdk.AccAddressFromBech32(tc.req.Holder)
   459  			s.Require().NoError(err)
   460  			operator, err := sdk.AccAddressFromBech32(tc.req.Operator)
   461  			s.Require().NoError(err)
   462  			ctx, _ := s.ctx.CacheContext()
   463  			prevAuth, _ := s.keeper.GetAuthorization(ctx, tc.req.ContractId, holder, operator)
   464  
   465  			// Act
   466  			res, err := s.msgServer.AuthorizeOperator(sdk.WrapSDKContext(ctx), tc.req)
   467  			if tc.isNegativeCase {
   468  				s.Require().ErrorIs(err, tc.expectedError)
   469  				return
   470  			}
   471  			s.Require().NoError(err)
   472  			s.Require().NotNil(res)
   473  			s.Require().Equal(tc.events, ctx.EventManager().Events())
   474  			curAuth, err := s.keeper.GetAuthorization(ctx, tc.req.ContractId, holder, operator)
   475  			s.Require().NoError(err)
   476  			s.Require().Nil(prevAuth)
   477  			s.Require().Equal(tc.req.Holder, curAuth.Holder)
   478  			s.Require().Equal(tc.req.Operator, curAuth.Operator)
   479  		})
   480  	}
   481  }
   482  
   483  func (s *KeeperTestSuite) TestMsgRevokeOperator() {
   484  	testCases := map[string]struct {
   485  		isNegativeCase bool
   486  		req            *collection.MsgRevokeOperator
   487  		events         sdk.Events
   488  		expectedError  error
   489  	}{
   490  		"valid request": {
   491  			req: &collection.MsgRevokeOperator{
   492  				ContractId: s.contractID,
   493  				Holder:     s.customer.String(),
   494  				Operator:   s.operator.String(),
   495  			},
   496  			events: sdk.Events{sdk.Event{
   497  				Type: "lbm.collection.v1.EventRevokedOperator",
   498  				Attributes: []abci.EventAttribute{
   499  					{Key: []byte("contract_id"), Value: testutil.W(s.contractID), Index: false},
   500  					{Key: []byte("holder"), Value: testutil.W(s.customer.String()), Index: false},
   501  					{Key: []byte("operator"), Value: testutil.W(s.operator.String()), Index: false},
   502  				},
   503  			}},
   504  		},
   505  		"contract not found": {
   506  			isNegativeCase: true,
   507  			req: &collection.MsgRevokeOperator{
   508  				ContractId: "deadbeef",
   509  				Holder:     s.customer.String(),
   510  				Operator:   s.operator.String(),
   511  			},
   512  			expectedError: class.ErrContractNotExist,
   513  		},
   514  		"no authorization": {
   515  			isNegativeCase: true,
   516  			req: &collection.MsgRevokeOperator{
   517  				ContractId: s.contractID,
   518  				Holder:     s.customer.String(),
   519  				Operator:   s.vendor.String(),
   520  			},
   521  			expectedError: collection.ErrCollectionNotApproved,
   522  		},
   523  	}
   524  
   525  	for name, tc := range testCases {
   526  		s.Run(name, func() {
   527  			// Arrange
   528  			s.Require().NoError(tc.req.ValidateBasic())
   529  			holder, err := sdk.AccAddressFromBech32(tc.req.Holder)
   530  			s.Require().NoError(err)
   531  			operator, err := sdk.AccAddressFromBech32(tc.req.Operator)
   532  			s.Require().NoError(err)
   533  			ctx, _ := s.ctx.CacheContext()
   534  			prevAuth, _ := s.keeper.GetAuthorization(ctx, tc.req.ContractId, holder, operator)
   535  
   536  			// Act
   537  			res, err := s.msgServer.RevokeOperator(sdk.WrapSDKContext(ctx), tc.req)
   538  			if tc.isNegativeCase {
   539  				s.Require().ErrorIs(err, tc.expectedError)
   540  				return
   541  			}
   542  			s.Require().NoError(err)
   543  			s.Require().NotNil(res)
   544  
   545  			s.Require().Equal(tc.events, ctx.EventManager().Events())
   546  			s.Require().NotNil(prevAuth)
   547  			s.Require().Equal(tc.req.Holder, prevAuth.Holder)
   548  			s.Require().Equal(tc.req.Operator, prevAuth.Operator)
   549  			curAuth, err := s.keeper.GetAuthorization(ctx, tc.req.ContractId, holder, operator)
   550  			s.Require().ErrorIs(err, collection.ErrCollectionNotApproved)
   551  			s.Require().Nil(curAuth)
   552  		})
   553  	}
   554  }
   555  
   556  func (s *KeeperTestSuite) TestMsgCreateContract() {
   557  	expectedNewContractID := "3336b76f"
   558  	testCases := map[string]struct {
   559  		owner  sdk.AccAddress
   560  		err    error
   561  		events sdk.Events
   562  	}{
   563  		"valid request": {
   564  			owner: s.vendor,
   565  			events: sdk.Events{
   566  				sdk.Event{
   567  					Type: "lbm.collection.v1.EventCreatedContract",
   568  					Attributes: []abci.EventAttribute{
   569  						{Key: []byte("contract_id"), Value: testutil.W(expectedNewContractID), Index: false},
   570  						{Key: []byte("creator"), Value: testutil.W(s.vendor.String()), Index: false},
   571  						{Key: []byte("meta"), Value: testutil.W(""), Index: false},
   572  						{Key: []byte("name"), Value: testutil.W(""), Index: false},
   573  						{Key: []byte("uri"), Value: testutil.W(""), Index: false},
   574  					},
   575  				},
   576  				sdk.Event{
   577  					Type: "lbm.collection.v1.EventGranted",
   578  					Attributes: []abci.EventAttribute{
   579  						{Key: []byte("contract_id"), Value: testutil.W(expectedNewContractID), Index: false},
   580  						{Key: []byte("grantee"), Value: testutil.W(s.vendor.String()), Index: false},
   581  						{Key: []byte("granter"), Value: testutil.W(""), Index: false},
   582  						{Key: []byte("permission"), Value: testutil.W(collection.Permission(collection.LegacyPermissionIssue).String()), Index: false},
   583  					}},
   584  				sdk.Event{
   585  					Type: "lbm.collection.v1.EventGranted",
   586  					Attributes: []abci.EventAttribute{
   587  						{Key: []byte("contract_id"), Value: testutil.W(expectedNewContractID), Index: false},
   588  						{Key: []byte("grantee"), Value: testutil.W(s.vendor.String()), Index: false},
   589  						{Key: []byte("granter"), Value: testutil.W(""), Index: false},
   590  						{Key: []byte("permission"), Value: testutil.W(collection.Permission(collection.LegacyPermissionModify).String()), Index: false},
   591  					}},
   592  				sdk.Event{
   593  					Type: "lbm.collection.v1.EventGranted",
   594  					Attributes: []abci.EventAttribute{
   595  						{Key: []byte("contract_id"), Value: testutil.W(expectedNewContractID), Index: false},
   596  						{Key: []byte("grantee"), Value: testutil.W(s.vendor.String()), Index: false},
   597  						{Key: []byte("granter"), Value: testutil.W(""), Index: false},
   598  						{Key: []byte("permission"), Value: testutil.W(collection.Permission(collection.LegacyPermissionMint).String()), Index: false},
   599  					}},
   600  				sdk.Event{
   601  					Type: "lbm.collection.v1.EventGranted",
   602  					Attributes: []abci.EventAttribute{
   603  						{Key: []byte("contract_id"), Value: testutil.W(expectedNewContractID), Index: false},
   604  						{Key: []byte("grantee"), Value: testutil.W(s.vendor.String()), Index: false},
   605  						{Key: []byte("granter"), Value: testutil.W(""), Index: false},
   606  						{Key: []byte("permission"), Value: testutil.W(collection.Permission(collection.LegacyPermissionBurn).String()), Index: false},
   607  					}},
   608  			}},
   609  	}
   610  
   611  	for name, tc := range testCases {
   612  		s.Run(name, func() {
   613  			ctx, _ := s.ctx.CacheContext()
   614  
   615  			req := &collection.MsgCreateContract{
   616  				Owner: tc.owner.String(),
   617  			}
   618  			res, err := s.msgServer.CreateContract(sdk.WrapSDKContext(ctx), req)
   619  			s.Require().Equal(expectedNewContractID, res.ContractId)
   620  			s.Require().ErrorIs(err, tc.err)
   621  			if tc.err != nil {
   622  				return
   623  			}
   624  
   625  			s.Require().NotNil(res)
   626  			s.Require().Equal(tc.events, ctx.EventManager().Events())
   627  
   628  		})
   629  	}
   630  }
   631  
   632  func (s *KeeperTestSuite) TestMsgIssueFT() {
   633  	expectedClassID := "00000002"
   634  	expectedTokenID := collection.NewFTID(expectedClassID)
   635  
   636  	testCases := map[string]struct {
   637  		contractID string
   638  		owner      sdk.AccAddress
   639  		amount     sdk.Int
   640  		err        error
   641  		events     sdk.Events
   642  	}{
   643  		"valid request": {
   644  			contractID: s.contractID,
   645  			owner:      s.vendor,
   646  			amount:     sdk.ZeroInt(),
   647  			events: sdk.Events{
   648  				sdk.Event{
   649  					Type: "lbm.collection.v1.EventCreatedFTClass",
   650  					Attributes: []abci.EventAttribute{
   651  						{Key: []byte("contract_id"), Value: testutil.W(s.contractID), Index: false},
   652  						{Key: []byte("decimals"), Value: []byte("0"), Index: false},
   653  						{Key: []byte("meta"), Value: testutil.W(""), Index: false},
   654  						{Key: []byte("mintable"), Value: []byte("false"), Index: false},
   655  						{Key: []byte("name"), Value: testutil.W(""), Index: false},
   656  						{Key: []byte("operator"), Value: testutil.W(s.vendor.String()), Index: false},
   657  						{Key: []byte("token_id"), Value: testutil.W(expectedTokenID), Index: false},
   658  					},
   659  				},
   660  			},
   661  		},
   662  		"valid request with supply": {
   663  			contractID: s.contractID,
   664  			owner:      s.vendor,
   665  			amount:     sdk.OneInt(),
   666  			events: sdk.Events{
   667  				sdk.Event{
   668  					Type: "lbm.collection.v1.EventCreatedFTClass",
   669  					Attributes: []abci.EventAttribute{
   670  						{Key: []byte("contract_id"), Value: testutil.W(s.contractID), Index: false},
   671  						{Key: []byte("decimals"), Value: []byte("0"), Index: false},
   672  						{Key: []byte("meta"), Value: testutil.W(""), Index: false},
   673  						{Key: []byte("mintable"), Value: []byte("false"), Index: false},
   674  						{Key: []byte("name"), Value: testutil.W(""), Index: false},
   675  						{Key: []byte("operator"), Value: testutil.W(s.vendor.String()), Index: false},
   676  						{Key: []byte("token_id"), Value: testutil.W(expectedTokenID), Index: false},
   677  					},
   678  				},
   679  				sdk.Event{
   680  					Type: "lbm.collection.v1.EventMintedFT",
   681  					Attributes: []abci.EventAttribute{
   682  						{Key: []byte("amount"), Value: testutil.MustJSONMarshal(collection.NewCoins(collection.Coin{TokenId: expectedTokenID, Amount: sdk.OneInt()})), Index: false},
   683  						{Key: []byte("contract_id"), Value: testutil.W(s.contractID), Index: false},
   684  						{Key: []byte("operator"), Value: testutil.W(s.vendor.String()), Index: false},
   685  						{Key: []byte("to"), Value: testutil.W(s.customer.String()), Index: false},
   686  					},
   687  				},
   688  			},
   689  		},
   690  		"contract not found": {
   691  			contractID: "deadbeef",
   692  			owner:      s.vendor,
   693  			amount:     sdk.ZeroInt(),
   694  			err:        class.ErrContractNotExist,
   695  		},
   696  		"no permission": {
   697  			contractID: s.contractID,
   698  			owner:      s.customer,
   699  			amount:     sdk.ZeroInt(),
   700  			err:        collection.ErrTokenNoPermission,
   701  		},
   702  	}
   703  
   704  	for name, tc := range testCases {
   705  		s.Run(name, func() {
   706  			ctx, _ := s.ctx.CacheContext()
   707  
   708  			req := &collection.MsgIssueFT{
   709  				ContractId: tc.contractID,
   710  				Owner:      tc.owner.String(),
   711  				To:         s.customer.String(),
   712  				Amount:     tc.amount,
   713  			}
   714  			res, err := s.msgServer.IssueFT(sdk.WrapSDKContext(ctx), req)
   715  			s.Require().ErrorIs(err, tc.err)
   716  			if tc.err != nil {
   717  				return
   718  			}
   719  
   720  			s.Require().NotNil(res)
   721  			s.Require().Equal(tc.events, ctx.EventManager().Events())
   722  
   723  			// check balance and tokenId
   724  			tokenId := collection.NewFTID(res.TokenId)
   725  			bal, err := s.queryServer.Balance(sdk.WrapSDKContext(ctx), &collection.QueryBalanceRequest{
   726  				ContractId: s.contractID,
   727  				Address:    s.customer.String(),
   728  				TokenId:    tokenId,
   729  			})
   730  			s.Require().NoError(err)
   731  			expectedCoin := collection.Coin{
   732  				TokenId: tokenId,
   733  				Amount:  tc.amount,
   734  			}
   735  			s.Require().Equal(expectedCoin, bal.Balance)
   736  		})
   737  	}
   738  }
   739  
   740  func (s *KeeperTestSuite) TestMsgIssueNFT() {
   741  	expectedTokenType := "10000002"
   742  
   743  	testCases := map[string]struct {
   744  		contractID string
   745  		owner      sdk.AccAddress
   746  		err        error
   747  		events     sdk.Events
   748  	}{
   749  		"valid request": {
   750  			contractID: s.contractID,
   751  			owner:      s.vendor,
   752  			events: sdk.Events{
   753  				sdk.Event{
   754  					Type: "lbm.collection.v1.EventCreatedNFTClass",
   755  					Attributes: []abci.EventAttribute{
   756  						{Key: []byte("contract_id"), Value: testutil.W(s.contractID), Index: false},
   757  						{Key: []byte("meta"), Value: testutil.W(""), Index: false},
   758  						{Key: []byte("name"), Value: testutil.W(""), Index: false},
   759  						{Key: []byte("operator"), Value: testutil.W(s.vendor.String()), Index: false},
   760  						{Key: []byte("token_type"), Value: testutil.W(expectedTokenType), Index: false},
   761  					}},
   762  				sdk.Event{
   763  					Type: "lbm.collection.v1.EventGranted",
   764  					Attributes: []abci.EventAttribute{
   765  						{Key: []byte("contract_id"), Value: testutil.W(s.contractID), Index: false},
   766  						{Key: []byte("grantee"), Value: testutil.W(s.vendor.String()), Index: false},
   767  						{Key: []byte("granter"), Value: testutil.W(""), Index: false},
   768  						{Key: []byte("permission"), Value: testutil.W(collection.Permission(collection.LegacyPermissionMint).String()), Index: false},
   769  					}},
   770  				sdk.Event{
   771  					Type: "lbm.collection.v1.EventGranted",
   772  					Attributes: []abci.EventAttribute{
   773  						{Key: []byte("contract_id"), Value: testutil.W(s.contractID), Index: false},
   774  						{Key: []byte("grantee"), Value: testutil.W(s.vendor.String()), Index: false},
   775  						{Key: []byte("granter"), Value: testutil.W(""), Index: false},
   776  						{Key: []byte("permission"), Value: testutil.W(collection.Permission(collection.LegacyPermissionBurn).String()), Index: false},
   777  					}},
   778  			},
   779  		},
   780  		"contract not found": {
   781  			contractID: "deadbeef",
   782  			owner:      s.vendor,
   783  			err:        class.ErrContractNotExist,
   784  		},
   785  		"no permission": {
   786  			contractID: s.contractID,
   787  			owner:      s.customer,
   788  			err:        collection.ErrTokenNoPermission,
   789  		},
   790  	}
   791  
   792  	for name, tc := range testCases {
   793  		s.Run(name, func() {
   794  			ctx, _ := s.ctx.CacheContext()
   795  
   796  			req := &collection.MsgIssueNFT{
   797  				ContractId: tc.contractID,
   798  				Owner:      tc.owner.String(),
   799  			}
   800  			res, err := s.msgServer.IssueNFT(sdk.WrapSDKContext(ctx), req)
   801  			s.Require().ErrorIs(err, tc.err)
   802  			if tc.err != nil {
   803  				return
   804  			}
   805  
   806  			s.Require().NotNil(res)
   807  			s.Require().Equal(tc.events, ctx.EventManager().Events())
   808  
   809  		})
   810  	}
   811  }
   812  
   813  func (s *KeeperTestSuite) TestMsgMintFT() {
   814  	// prepare multi tokens for test
   815  	// create a fungible token class (mintable true)
   816  	mintableFTClassID, err := s.keeper.CreateTokenClass(s.ctx, s.contractID, &collection.FTClass{
   817  		Name:     "tibetian fox2",
   818  		Mintable: true,
   819  	})
   820  	s.Require().NoError(err)
   821  
   822  	// create a fungible token class (mintable false)
   823  	nonmintableFTClassID, err := s.keeper.CreateTokenClass(s.ctx, s.contractID, &collection.FTClass{
   824  		Name:     "tibetian fox3",
   825  		Mintable: false,
   826  	})
   827  	s.Require().NoError(err)
   828  
   829  	amount := collection.NewCoins(
   830  		collection.NewFTCoin(s.ftClassID, sdk.NewInt(100000)),
   831  	)
   832  	amounts := collection.NewCoins(
   833  		collection.NewFTCoin(s.ftClassID, sdk.NewInt(100000)),
   834  		collection.NewFTCoin(*mintableFTClassID, sdk.NewInt(200000)),
   835  	)
   836  
   837  	testCases := map[string]struct {
   838  		contractID string
   839  		from       sdk.AccAddress
   840  		amount     []collection.Coin
   841  		err        error
   842  		events     sdk.Events
   843  	}{
   844  		"valid request - single token": {
   845  			contractID: s.contractID,
   846  			from:       s.vendor,
   847  			amount:     amount,
   848  			events: sdk.Events{
   849  				sdk.Event{
   850  					Type: "lbm.collection.v1.EventMintedFT",
   851  					Attributes: []abci.EventAttribute{
   852  						{Key: []byte("amount"), Value: testutil.MustJSONMarshal(amount), Index: false},
   853  						{Key: []byte("contract_id"), Value: testutil.W(s.contractID), Index: false},
   854  						{Key: []byte("operator"), Value: testutil.W(s.vendor.String()), Index: false},
   855  						{Key: []byte("to"), Value: testutil.W(s.customer.String()), Index: false},
   856  					},
   857  				},
   858  			},
   859  		},
   860  		"valid request - multi tokens": {
   861  			contractID: s.contractID,
   862  			from:       s.vendor,
   863  			amount:     amounts,
   864  			events: sdk.Events{
   865  				sdk.Event{
   866  					Type: "lbm.collection.v1.EventMintedFT",
   867  					Attributes: []abci.EventAttribute{
   868  						{Key: []byte("amount"), Value: testutil.MustJSONMarshal(amounts), Index: false},
   869  						{Key: []byte("contract_id"), Value: testutil.W(s.contractID), Index: false},
   870  						{Key: []byte("operator"), Value: testutil.W(s.vendor.String()), Index: false},
   871  						{Key: []byte("to"), Value: testutil.W(s.customer.String()), Index: false},
   872  					},
   873  				},
   874  			},
   875  		},
   876  		"valid request - empty amount": {
   877  			contractID: s.contractID,
   878  			from:       s.vendor,
   879  			events: sdk.Events{
   880  				sdk.Event{
   881  					Type: "lbm.collection.v1.EventMintedFT",
   882  					Attributes: []abci.EventAttribute{
   883  						{Key: []byte("amount"), Value: []byte("[]"), Index: false},
   884  						{Key: []byte("contract_id"), Value: testutil.W(s.contractID), Index: false},
   885  						{Key: []byte("operator"), Value: testutil.W(s.vendor.String()), Index: false},
   886  						{Key: []byte("to"), Value: testutil.W(s.customer.String()), Index: false},
   887  					},
   888  				},
   889  			},
   890  		},
   891  		"contract not found": {
   892  			contractID: "deadbeef",
   893  			from:       s.vendor,
   894  			amount:     amount,
   895  			err:        class.ErrContractNotExist,
   896  		},
   897  		"no permission": {
   898  			contractID: s.contractID,
   899  			from:       s.customer,
   900  			amount:     amount,
   901  			err:        collection.ErrTokenNoPermission,
   902  		},
   903  		"no class of the token": {
   904  			contractID: s.contractID,
   905  			from:       s.vendor,
   906  			amount: collection.NewCoins(
   907  				collection.NewFTCoin("00bab10c", sdk.OneInt()),
   908  			),
   909  			err: collection.ErrTokenNotExist,
   910  		},
   911  		"include invalid tokenId among 2 tokens": {
   912  			contractID: s.contractID,
   913  			from:       s.vendor,
   914  			amount: collection.NewCoins(
   915  				collection.NewFTCoin(s.ftClassID, sdk.OneInt()),
   916  				collection.NewFTCoin("00bab10b", sdk.OneInt()), // no exist tokenId
   917  			),
   918  			err: collection.ErrTokenNotExist,
   919  		},
   920  		"mintable false tokenId": {
   921  			contractID: s.contractID,
   922  			from:       s.vendor,
   923  			amount:     collection.NewCoins(collection.NewFTCoin(*nonmintableFTClassID, sdk.OneInt())),
   924  			err:        collection.ErrTokenNotMintable,
   925  		},
   926  		"include mintable false among 2 tokens": {
   927  			contractID: s.contractID,
   928  			from:       s.vendor,
   929  			amount: collection.NewCoins(
   930  				collection.NewFTCoin(*mintableFTClassID, sdk.OneInt()),
   931  				collection.NewFTCoin(*nonmintableFTClassID, sdk.OneInt()),
   932  			),
   933  			err: collection.ErrTokenNotMintable,
   934  		},
   935  	}
   936  
   937  	// query the values to be effected by MintFT
   938  	queryValuesEffectedByMintFT := func(ctx sdk.Context, coins collection.Coins, contractID string) (balances collection.Coins, supply []sdk.Int, minted []sdk.Int) {
   939  		for _, am := range coins {
   940  			// save balance
   941  			bal, err := s.queryServer.Balance(sdk.WrapSDKContext(ctx), &collection.QueryBalanceRequest{
   942  				ContractId: contractID,
   943  				Address:    s.customer.String(),
   944  				TokenId:    am.TokenId,
   945  			})
   946  			s.Require().NoError(err)
   947  			balances = append(balances, bal.Balance)
   948  
   949  			// save supply
   950  			res, err := s.queryServer.FTSupply(sdk.WrapSDKContext(ctx), &collection.QueryFTSupplyRequest{
   951  				ContractId: contractID,
   952  				TokenId:    am.TokenId,
   953  			})
   954  			s.Require().NoError(err)
   955  			supply = append(supply, res.Supply)
   956  
   957  			// save minted
   958  			m, err := s.queryServer.FTMinted(sdk.WrapSDKContext(ctx), &collection.QueryFTMintedRequest{
   959  				ContractId: contractID,
   960  				TokenId:    am.TokenId,
   961  			})
   962  			s.Require().NoError(err)
   963  			minted = append(minted, m.Minted)
   964  		}
   965  		return
   966  	}
   967  
   968  	for name, tc := range testCases {
   969  		s.Run(name, func() {
   970  			// test multiple times
   971  			ctx := s.ctx
   972  			for t := 0; t < 3; t++ {
   973  				ctx, _ = ctx.CacheContext()
   974  
   975  				prevAmount, prevSupply, prevMinted := queryValuesEffectedByMintFT(ctx, tc.amount, tc.contractID)
   976  
   977  				req := &collection.MsgMintFT{
   978  					ContractId: tc.contractID,
   979  					From:       tc.from.String(),
   980  					To:         s.customer.String(),
   981  					Amount:     tc.amount,
   982  				}
   983  				res, err := s.msgServer.MintFT(sdk.WrapSDKContext(ctx), req)
   984  				s.Require().ErrorIs(err, tc.err)
   985  				if tc.err != nil {
   986  					return
   987  				}
   988  
   989  				s.Require().NotNil(res)
   990  				s.Require().Equal(tc.events, ctx.EventManager().Events())
   991  
   992  				// check results
   993  				afterAmount, afterSupply, afterMinted := queryValuesEffectedByMintFT(ctx, tc.amount, tc.contractID)
   994  				for i, am := range tc.amount {
   995  					expectedBalance := collection.Coin{
   996  						TokenId: am.TokenId,
   997  						Amount:  prevAmount[i].Amount.Add(am.Amount),
   998  					}
   999  					s.Require().Equal(expectedBalance, afterAmount[i])
  1000  
  1001  					expectedSupply := prevSupply[i].Add(am.Amount)
  1002  					s.Require().True(expectedSupply.Equal(afterSupply[i]))
  1003  
  1004  					expectedMinted := prevMinted[i].Add(am.Amount)
  1005  					s.Require().True(expectedMinted.Equal(afterMinted[i]))
  1006  				}
  1007  			}
  1008  		})
  1009  	}
  1010  }
  1011  
  1012  func (s *KeeperTestSuite) TestMsgMintNFT() {
  1013  	params := []collection.MintNFTParam{{
  1014  		TokenType: s.nftClassID,
  1015  		Name:      "tester",
  1016  		Meta:      "Mint NFT",
  1017  	}}
  1018  	expectedTokens := []collection.NFT{
  1019  		{
  1020  			TokenId: "1000000100000016",
  1021  			Name:    params[0].Name,
  1022  			Meta:    params[0].Meta,
  1023  		},
  1024  	}
  1025  
  1026  	testCases := map[string]struct {
  1027  		contractID string
  1028  		from       sdk.AccAddress
  1029  		params     []collection.MintNFTParam
  1030  		err        error
  1031  		events     sdk.Events
  1032  	}{
  1033  		"valid request": {
  1034  			contractID: s.contractID,
  1035  			from:       s.vendor,
  1036  			params:     params,
  1037  			events: sdk.Events{
  1038  				sdk.Event{
  1039  					Type: "lbm.collection.v1.EventMintedNFT",
  1040  					Attributes: []abci.EventAttribute{
  1041  						{Key: []byte("contract_id"), Value: testutil.W(s.contractID), Index: false},
  1042  						{Key: []byte("operator"), Value: testutil.W(s.vendor.String()), Index: false},
  1043  						{Key: []byte("to"), Value: testutil.W(s.customer.String()), Index: false},
  1044  						{Key: []byte("tokens"), Value: testutil.MustJSONMarshal(expectedTokens), Index: false},
  1045  					},
  1046  				},
  1047  			}},
  1048  		"contract not found": {
  1049  			contractID: "deadbeef",
  1050  			from:       s.vendor,
  1051  			params:     params,
  1052  			err:        class.ErrContractNotExist,
  1053  		},
  1054  		"no permission": {
  1055  			contractID: s.contractID,
  1056  			from:       s.customer,
  1057  			params:     params,
  1058  			err:        collection.ErrTokenNoPermission,
  1059  		},
  1060  		"no class of the token": {
  1061  			contractID: s.contractID,
  1062  			from:       s.vendor,
  1063  			params: []collection.MintNFTParam{{
  1064  				TokenType: "deadbeef",
  1065  			}},
  1066  			err: collection.ErrTokenTypeNotExist,
  1067  		},
  1068  	}
  1069  
  1070  	for name, tc := range testCases {
  1071  		s.Run(name, func() {
  1072  			ctx, _ := s.ctx.CacheContext()
  1073  
  1074  			req := &collection.MsgMintNFT{
  1075  				ContractId: tc.contractID,
  1076  				From:       tc.from.String(),
  1077  				To:         s.customer.String(),
  1078  				Params:     tc.params,
  1079  			}
  1080  			res, err := s.msgServer.MintNFT(sdk.WrapSDKContext(ctx), req)
  1081  			s.Require().ErrorIs(err, tc.err)
  1082  			if tc.err != nil {
  1083  				return
  1084  			}
  1085  
  1086  			s.Require().NotNil(res)
  1087  			s.Require().Equal(tc.events, ctx.EventManager().Events())
  1088  		})
  1089  	}
  1090  }
  1091  
  1092  func (s *KeeperTestSuite) TestMsgBurnFT() {
  1093  	// prepare mutli token burn test
  1094  	singleAmount := collection.NewCoins(
  1095  		collection.NewFTCoin(s.ftClassID, sdk.NewInt(50000)),
  1096  	)
  1097  
  1098  	// create a fungible token class
  1099  	mintableFTClassID, err := s.keeper.CreateTokenClass(s.ctx, s.contractID, &collection.FTClass{
  1100  		Name:     "tibetian fox2",
  1101  		Mintable: true,
  1102  	})
  1103  	s.Require().NoError(err)
  1104  	multiAmounts := collection.NewCoins(
  1105  		collection.NewFTCoin(s.ftClassID, sdk.NewInt(50000)),
  1106  		collection.NewFTCoin(*mintableFTClassID, sdk.NewInt(60000)),
  1107  	)
  1108  
  1109  	// mintft
  1110  	mintedCoin := collection.NewFTCoin(*mintableFTClassID, sdk.NewInt(1000000))
  1111  	err = s.keeper.MintFT(s.ctx, s.contractID, s.vendor, []collection.Coin{mintedCoin})
  1112  	s.Require().NoError(err)
  1113  
  1114  	testCases := map[string]struct {
  1115  		contractID string
  1116  		from       sdk.AccAddress
  1117  		amount     []collection.Coin
  1118  		err        error
  1119  		events     sdk.Events
  1120  	}{
  1121  		"valid request": {
  1122  			contractID: s.contractID,
  1123  			from:       s.vendor,
  1124  			amount:     singleAmount,
  1125  			events: sdk.Events{
  1126  				sdk.Event{
  1127  					Type: "lbm.collection.v1.EventBurned",
  1128  					Attributes: []abci.EventAttribute{
  1129  						{Key: []byte("amount"), Value: testutil.MustJSONMarshal(collection.NewCoins(
  1130  							collection.NewFTCoin(s.ftClassID, sdk.NewInt(50000)),
  1131  						)), Index: false},
  1132  						{Key: []byte("contract_id"), Value: testutil.W(s.contractID), Index: false},
  1133  						{Key: []byte("from"), Value: testutil.W(s.vendor.String()), Index: false},
  1134  						{Key: []byte("operator"), Value: testutil.W(s.vendor.String()), Index: false},
  1135  					},
  1136  				},
  1137  			},
  1138  		},
  1139  		"valid multi amount burn": {
  1140  			contractID: s.contractID,
  1141  			from:       s.vendor,
  1142  			amount:     multiAmounts,
  1143  			events: sdk.Events{
  1144  				sdk.Event{
  1145  					Type: "lbm.collection.v1.EventBurned",
  1146  					Attributes: []abci.EventAttribute{
  1147  						{Key: []byte("amount"), Value: testutil.MustJSONMarshal(multiAmounts), Index: false},
  1148  						{Key: []byte("contract_id"), Value: testutil.W(s.contractID), Index: false},
  1149  						{Key: []byte("from"), Value: testutil.W(s.vendor.String()), Index: false},
  1150  						{Key: []byte("operator"), Value: testutil.W(s.vendor.String()), Index: false},
  1151  					},
  1152  				},
  1153  			},
  1154  		},
  1155  		"no amount - valid": {
  1156  			contractID: s.contractID,
  1157  			from:       s.vendor,
  1158  			events: sdk.Events{
  1159  				sdk.Event{
  1160  					Type: "lbm.collection.v1.EventBurned",
  1161  					Attributes: []abci.EventAttribute{
  1162  						{Key: []byte("amount"), Value: []byte("[]"), Index: false},
  1163  						{Key: []byte("contract_id"), Value: testutil.W(s.contractID), Index: false},
  1164  						{Key: []byte("from"), Value: testutil.W(s.vendor.String()), Index: false},
  1165  						{Key: []byte("operator"), Value: testutil.W(s.vendor.String()), Index: false},
  1166  					},
  1167  				},
  1168  			},
  1169  		},
  1170  		"contract not found": {
  1171  			contractID: "deadbeef",
  1172  			from:       s.vendor,
  1173  			amount:     singleAmount,
  1174  			err:        class.ErrContractNotExist,
  1175  		},
  1176  		"no permission": {
  1177  			contractID: s.contractID,
  1178  			from:       s.customer,
  1179  			amount:     singleAmount,
  1180  			err:        collection.ErrTokenNoPermission,
  1181  		},
  1182  		"insufficient funds": {
  1183  			contractID: s.contractID,
  1184  			from:       s.vendor,
  1185  			amount: collection.NewCoins(
  1186  				collection.NewFTCoin("00bab10c", sdk.OneInt()),
  1187  			),
  1188  			err: collection.ErrInsufficientToken,
  1189  		},
  1190  		"include insufficient funds amount 2 amounts": {
  1191  			contractID: s.contractID,
  1192  			from:       s.vendor,
  1193  			amount: collection.NewCoins(
  1194  				collection.NewFTCoin(s.ftClassID, s.balance),
  1195  				collection.NewFTCoin("00bab10c", sdk.OneInt()),
  1196  			),
  1197  			err: collection.ErrInsufficientToken,
  1198  		},
  1199  	}
  1200  
  1201  	// query the values to be effected by BurnFT
  1202  	queryValuesAffectedByBurnFT := func(ctx sdk.Context, coins collection.Coins, contractID, from string) (balances collection.Coins, supply []sdk.Int, burnt []sdk.Int) {
  1203  		for _, am := range coins {
  1204  			// save balance
  1205  			bal, err := s.queryServer.Balance(sdk.WrapSDKContext(ctx), &collection.QueryBalanceRequest{
  1206  				ContractId: contractID,
  1207  				Address:    from,
  1208  				TokenId:    am.TokenId,
  1209  			})
  1210  			s.Require().NoError(err)
  1211  			balances = append(balances, bal.Balance)
  1212  
  1213  			// save supply
  1214  			res, err := s.queryServer.FTSupply(sdk.WrapSDKContext(ctx), &collection.QueryFTSupplyRequest{
  1215  				ContractId: contractID,
  1216  				TokenId:    am.TokenId,
  1217  			})
  1218  			s.Require().NoError(err)
  1219  			supply = append(supply, res.Supply)
  1220  
  1221  			// save minted
  1222  			b, err := s.queryServer.FTBurnt(sdk.WrapSDKContext(ctx), &collection.QueryFTBurntRequest{
  1223  				ContractId: contractID,
  1224  				TokenId:    am.TokenId,
  1225  			})
  1226  			s.Require().NoError(err)
  1227  			burnt = append(burnt, b.Burnt)
  1228  		}
  1229  		return
  1230  	}
  1231  
  1232  	for name, tc := range testCases {
  1233  		s.Run(name, func() {
  1234  			// test multiple times
  1235  			ctx := s.ctx
  1236  			for t := 0; t < 3; t++ {
  1237  				ctx, _ = ctx.CacheContext()
  1238  				prevAmount, prevSupply, prevBurnt := queryValuesAffectedByBurnFT(ctx, tc.amount, tc.contractID, tc.from.String())
  1239  
  1240  				req := &collection.MsgBurnFT{
  1241  					ContractId: tc.contractID,
  1242  					From:       tc.from.String(),
  1243  					Amount:     tc.amount,
  1244  				}
  1245  				res, err := s.msgServer.BurnFT(sdk.WrapSDKContext(ctx), req)
  1246  				s.Require().ErrorIs(err, tc.err)
  1247  				if tc.err != nil {
  1248  					return
  1249  				}
  1250  
  1251  				s.Require().NotNil(res)
  1252  				s.Require().Equal(tc.events, ctx.EventManager().Events())
  1253  
  1254  				// check changed amount
  1255  				afterAmount, afterSupply, afterBurnt := queryValuesAffectedByBurnFT(ctx, tc.amount, tc.contractID, tc.from.String())
  1256  				for i, am := range tc.amount {
  1257  					expectedBalance := prevAmount[i].Amount.Sub(am.Amount)
  1258  					s.Require().Equal(am.TokenId, afterAmount[i].TokenId)
  1259  					s.Require().True(expectedBalance.Equal(afterAmount[i].Amount))
  1260  
  1261  					expectedSupply := prevSupply[i].Sub(am.Amount)
  1262  					s.Require().True(expectedSupply.Equal(afterSupply[i]))
  1263  
  1264  					expectedBurnt := prevBurnt[i].Add(am.Amount)
  1265  					s.Require().True(expectedBurnt.Equal(afterBurnt[i]))
  1266  				}
  1267  			}
  1268  		})
  1269  	}
  1270  }
  1271  
  1272  func (s *KeeperTestSuite) TestMsgOperatorBurnFT() {
  1273  	singleAmount := collection.NewCoins(
  1274  		collection.NewFTCoin(s.ftClassID, s.balance),
  1275  	)
  1276  
  1277  	testCases := map[string]struct {
  1278  		contractID string
  1279  		operator   sdk.AccAddress
  1280  		from       sdk.AccAddress
  1281  		amount     []collection.Coin
  1282  		err        error
  1283  		events     sdk.Events
  1284  	}{
  1285  		"valid request": {
  1286  			contractID: s.contractID,
  1287  			operator:   s.operator,
  1288  			from:       s.customer,
  1289  			amount:     singleAmount,
  1290  			events: sdk.Events{
  1291  				sdk.Event{
  1292  					Type: "lbm.collection.v1.EventBurned",
  1293  					Attributes: []abci.EventAttribute{
  1294  						{Key: []byte("amount"), Value: testutil.MustJSONMarshal(singleAmount), Index: false},
  1295  						{Key: []byte("contract_id"), Value: testutil.W(s.contractID), Index: false},
  1296  						{Key: []byte("from"), Value: testutil.W(s.customer.String()), Index: false},
  1297  						{Key: []byte("operator"), Value: testutil.W(s.operator.String()), Index: false},
  1298  					}}},
  1299  		},
  1300  		"contract not found": {
  1301  			contractID: "deadbeef",
  1302  			operator:   s.operator,
  1303  			from:       s.customer,
  1304  			amount:     singleAmount,
  1305  			err:        class.ErrContractNotExist,
  1306  		},
  1307  		"no authorization": {
  1308  			contractID: s.contractID,
  1309  			operator:   s.vendor,
  1310  			from:       s.customer,
  1311  			amount:     singleAmount,
  1312  			err:        collection.ErrCollectionNotApproved,
  1313  		},
  1314  		"no permission": {
  1315  			contractID: s.contractID,
  1316  			operator:   s.stranger,
  1317  			from:       s.customer,
  1318  			amount:     singleAmount,
  1319  			err:        collection.ErrTokenNoPermission,
  1320  		},
  1321  		"insufficient funds - exist token": {
  1322  			contractID: s.contractID,
  1323  			operator:   s.operator,
  1324  			from:       s.customer,
  1325  			amount: collection.NewCoins(
  1326  				collection.NewFTCoin(s.ftClassID, s.balance.Add(sdk.OneInt())),
  1327  			),
  1328  			err: collection.ErrInsufficientToken,
  1329  		},
  1330  		"insufficient funds - non-exist token": {
  1331  			contractID: s.contractID,
  1332  			operator:   s.operator,
  1333  			from:       s.customer,
  1334  			amount: collection.NewCoins(
  1335  				collection.NewFTCoin("00bab10c", sdk.OneInt()),
  1336  			),
  1337  			err: collection.ErrInsufficientToken,
  1338  		},
  1339  	}
  1340  
  1341  	for name, tc := range testCases {
  1342  		s.Run(name, func() {
  1343  			ctx, _ := s.ctx.CacheContext()
  1344  
  1345  			req := &collection.MsgOperatorBurnFT{
  1346  				ContractId: tc.contractID,
  1347  				Operator:   tc.operator.String(),
  1348  				From:       tc.from.String(),
  1349  				Amount:     tc.amount,
  1350  			}
  1351  			res, err := s.msgServer.OperatorBurnFT(sdk.WrapSDKContext(ctx), req)
  1352  			s.Require().ErrorIs(err, tc.err)
  1353  			if tc.err != nil {
  1354  				return
  1355  			}
  1356  
  1357  			s.Require().NotNil(res)
  1358  			s.Require().Equal(tc.events, ctx.EventManager().Events())
  1359  		})
  1360  	}
  1361  }
  1362  
  1363  func (s *KeeperTestSuite) TestMsgBurnNFT() {
  1364  	rootNFTID := collection.NewNFTID(s.nftClassID, s.numNFTs*2+1)
  1365  	issuedTokenIDs := s.extractChainedNFTIDs(rootNFTID)
  1366  	coins := make([]collection.Coin, 0)
  1367  	for _, id := range issuedTokenIDs {
  1368  		coins = append(coins, collection.NewCoin(id, sdk.NewInt(1)))
  1369  	}
  1370  
  1371  	testCases := map[string]struct {
  1372  		contractID string
  1373  		from       sdk.AccAddress
  1374  		tokenIDs   []string
  1375  		err        error
  1376  		events     sdk.Events
  1377  	}{
  1378  		"valid request": {
  1379  			contractID: s.contractID,
  1380  			from:       s.vendor,
  1381  			tokenIDs:   []string{rootNFTID},
  1382  			events: sdk.Events{
  1383  				sdk.Event{
  1384  					Type: "lbm.collection.v1.EventBurned",
  1385  					Attributes: []abci.EventAttribute{
  1386  						{Key: []byte("amount"), Value: testutil.MustJSONMarshal(coins), Index: false},
  1387  						{Key: []byte("contract_id"), Value: testutil.W(s.contractID), Index: false},
  1388  						{Key: []byte("from"), Value: testutil.W(s.vendor.String()), Index: false},
  1389  						{Key: []byte("operator"), Value: testutil.W(s.vendor.String()), Index: false},
  1390  					}}},
  1391  		},
  1392  		"contract not found": {
  1393  			contractID: "deadbeef",
  1394  			from:       s.vendor,
  1395  			tokenIDs:   []string{rootNFTID},
  1396  			err:        class.ErrContractNotExist,
  1397  		},
  1398  		"no permission": {
  1399  			contractID: s.contractID,
  1400  			from:       s.customer,
  1401  			tokenIDs:   []string{rootNFTID},
  1402  			err:        collection.ErrTokenNoPermission,
  1403  		},
  1404  		"not found": {
  1405  			contractID: s.contractID,
  1406  			from:       s.vendor,
  1407  			tokenIDs: []string{
  1408  				collection.NewNFTID("deadbeef", 1),
  1409  			},
  1410  			err: collection.ErrTokenNotExist,
  1411  		},
  1412  		"child": {
  1413  			contractID: s.contractID,
  1414  			from:       s.vendor,
  1415  			tokenIDs: []string{
  1416  				collection.NewNFTID(s.nftClassID, 2),
  1417  			},
  1418  			err: collection.ErrBurnNonRootNFT,
  1419  		},
  1420  		"not owned by": {
  1421  			contractID: s.contractID,
  1422  			from:       s.vendor,
  1423  			tokenIDs: []string{
  1424  				collection.NewNFTID(s.nftClassID, s.numNFTs+1),
  1425  			},
  1426  			err: collection.ErrTokenNotOwnedBy,
  1427  		},
  1428  	}
  1429  
  1430  	for name, tc := range testCases {
  1431  		s.Run(name, func() {
  1432  			ctx, _ := s.ctx.CacheContext()
  1433  
  1434  			req := &collection.MsgBurnNFT{
  1435  				ContractId: tc.contractID,
  1436  				From:       tc.from.String(),
  1437  				TokenIds:   tc.tokenIDs,
  1438  			}
  1439  			res, err := s.msgServer.BurnNFT(sdk.WrapSDKContext(ctx), req)
  1440  			s.Require().ErrorIs(err, tc.err)
  1441  			if tc.err != nil {
  1442  				return
  1443  			}
  1444  
  1445  			s.Require().NotNil(res)
  1446  			s.Require().Equal(tc.events, ctx.EventManager().Events())
  1447  		})
  1448  	}
  1449  }
  1450  
  1451  func (s *KeeperTestSuite) TestMsgOperatorBurnNFT() {
  1452  	rootNFTID := collection.NewNFTID(s.nftClassID, 1)
  1453  	issuedTokenIDs := s.extractChainedNFTIDs(rootNFTID)
  1454  	coins := make([]collection.Coin, 0)
  1455  	for _, id := range issuedTokenIDs {
  1456  		coins = append(coins, collection.NewCoin(id, sdk.NewInt(1)))
  1457  	}
  1458  
  1459  	testCases := map[string]struct {
  1460  		contractID string
  1461  		operator   sdk.AccAddress
  1462  		from       sdk.AccAddress
  1463  		tokenIDs   []string
  1464  		err        error
  1465  		events     sdk.Events
  1466  	}{
  1467  		"valid request": {
  1468  			contractID: s.contractID,
  1469  			operator:   s.operator,
  1470  			from:       s.customer,
  1471  			tokenIDs:   []string{rootNFTID},
  1472  			events: sdk.Events{
  1473  				sdk.Event{
  1474  					Type: "lbm.collection.v1.EventBurned",
  1475  					Attributes: []abci.EventAttribute{
  1476  						{Key: []byte("amount"), Value: testutil.MustJSONMarshal(coins), Index: false},
  1477  						{Key: []byte("contract_id"), Value: testutil.W(s.contractID), Index: false},
  1478  						{Key: []byte("from"), Value: testutil.W(s.customer.String()), Index: false},
  1479  						{Key: []byte("operator"), Value: testutil.W(s.operator.String()), Index: false},
  1480  					}}},
  1481  		},
  1482  		"contract not found": {
  1483  			contractID: "deadbeef",
  1484  			operator:   s.operator,
  1485  			from:       s.customer,
  1486  			tokenIDs:   []string{rootNFTID},
  1487  			err:        class.ErrContractNotExist,
  1488  		},
  1489  		"no authorization": {
  1490  			contractID: s.contractID,
  1491  			operator:   s.vendor,
  1492  			from:       s.customer,
  1493  			tokenIDs:   []string{rootNFTID},
  1494  			err:        collection.ErrCollectionNotApproved,
  1495  		},
  1496  		"no permission": {
  1497  			contractID: s.contractID,
  1498  			operator:   s.stranger,
  1499  			from:       s.customer,
  1500  			tokenIDs:   []string{rootNFTID},
  1501  			err:        collection.ErrTokenNoPermission,
  1502  		},
  1503  		"not found": {
  1504  			contractID: s.contractID,
  1505  			operator:   s.operator,
  1506  			from:       s.customer,
  1507  			tokenIDs: []string{
  1508  				collection.NewNFTID("deadbeef", 1),
  1509  			},
  1510  			err: collection.ErrTokenNotExist,
  1511  		},
  1512  		"child": {
  1513  			contractID: s.contractID,
  1514  			operator:   s.operator,
  1515  			from:       s.customer,
  1516  			tokenIDs: []string{
  1517  				collection.NewNFTID(s.nftClassID, 2),
  1518  			},
  1519  			err: collection.ErrBurnNonRootNFT,
  1520  		},
  1521  		"not owned by": {
  1522  			contractID: s.contractID,
  1523  			operator:   s.operator,
  1524  			from:       s.customer,
  1525  			tokenIDs: []string{
  1526  				collection.NewNFTID(s.nftClassID, s.numNFTs+1),
  1527  			},
  1528  			err: collection.ErrTokenNotOwnedBy,
  1529  		},
  1530  	}
  1531  
  1532  	for name, tc := range testCases {
  1533  		s.Run(name, func() {
  1534  			ctx, _ := s.ctx.CacheContext()
  1535  
  1536  			req := &collection.MsgOperatorBurnNFT{
  1537  				ContractId: tc.contractID,
  1538  				Operator:   tc.operator.String(),
  1539  				From:       tc.from.String(),
  1540  				TokenIds:   tc.tokenIDs,
  1541  			}
  1542  			res, err := s.msgServer.OperatorBurnNFT(sdk.WrapSDKContext(ctx), req)
  1543  			s.Require().ErrorIs(err, tc.err)
  1544  			if tc.err != nil {
  1545  				return
  1546  			}
  1547  
  1548  			s.Require().NotNil(res)
  1549  			s.Require().Equal(tc.events, ctx.EventManager().Events())
  1550  
  1551  		})
  1552  	}
  1553  }
  1554  
  1555  func (s *KeeperTestSuite) TestMsgModify() {
  1556  	expectedTokenIndex := collection.NewNFTID(s.nftClassID, 1)[8:]
  1557  	changes := []collection.Attribute{{
  1558  		Key:   collection.AttributeKeyName.String(),
  1559  		Value: "test",
  1560  	}}
  1561  
  1562  	testCases := map[string]struct {
  1563  		contractID string
  1564  		operator   sdk.AccAddress
  1565  		tokenType  string
  1566  		tokenIndex string
  1567  		err        error
  1568  		events     sdk.Events
  1569  	}{
  1570  		"valid request": {
  1571  			contractID: s.contractID,
  1572  			operator:   s.vendor,
  1573  			events: sdk.Events{
  1574  				sdk.Event{
  1575  					Type: "lbm.collection.v1.EventModifiedContract",
  1576  					Attributes: []abci.EventAttribute{
  1577  						{Key: []byte("changes"), Value: testutil.MustJSONMarshal(changes), Index: false},
  1578  						{Key: []byte("contract_id"), Value: testutil.W(s.contractID), Index: false},
  1579  						{Key: []byte("operator"), Value: testutil.W(s.vendor.String()), Index: false},
  1580  					}}}},
  1581  		"contract not found": {
  1582  			contractID: "deadbeef",
  1583  			operator:   s.vendor,
  1584  			err:        class.ErrContractNotExist,
  1585  		},
  1586  		"no permission": {
  1587  			contractID: s.contractID,
  1588  			operator:   s.customer,
  1589  			tokenType:  s.nftClassID,
  1590  			tokenIndex: expectedTokenIndex,
  1591  			err:        collection.ErrTokenNoPermission,
  1592  		},
  1593  		"nft not found": {
  1594  			contractID: s.contractID,
  1595  			operator:   s.vendor,
  1596  			tokenType:  s.nftClassID,
  1597  			tokenIndex: collection.NewNFTID(s.nftClassID, s.numNFTs*3+1)[8:],
  1598  			err:        collection.ErrTokenNotExist,
  1599  		},
  1600  		"ft class not found": {
  1601  			contractID: s.contractID,
  1602  			operator:   s.vendor,
  1603  			tokenType:  "00bab10c",
  1604  			tokenIndex: collection.NewFTID("00bab10c")[8:],
  1605  			err:        collection.ErrTokenNotExist,
  1606  		},
  1607  		"nft class not found": {
  1608  			contractID: s.contractID,
  1609  			operator:   s.vendor,
  1610  			tokenType:  "deadbeef",
  1611  			err:        collection.ErrTokenTypeNotExist,
  1612  		},
  1613  	}
  1614  
  1615  	for name, tc := range testCases {
  1616  		s.Run(name, func() {
  1617  			ctx, _ := s.ctx.CacheContext()
  1618  			req := &collection.MsgModify{
  1619  				ContractId: tc.contractID,
  1620  				Owner:      tc.operator.String(),
  1621  				TokenType:  tc.tokenType,
  1622  				TokenIndex: tc.tokenIndex,
  1623  				Changes:    changes,
  1624  			}
  1625  			res, err := s.msgServer.Modify(sdk.WrapSDKContext(ctx), req)
  1626  			s.Require().ErrorIs(err, tc.err)
  1627  			if tc.err != nil {
  1628  				return
  1629  			}
  1630  
  1631  			s.Require().NotNil(res)
  1632  			s.Require().Equal(tc.events, ctx.EventManager().Events())
  1633  		})
  1634  	}
  1635  }
  1636  
  1637  func (s *KeeperTestSuite) TestMsgGrantPermission() {
  1638  	testCases := map[string]struct {
  1639  		contractID string
  1640  		granter    sdk.AccAddress
  1641  		grantee    sdk.AccAddress
  1642  		permission string
  1643  		err        error
  1644  		events     sdk.Events
  1645  	}{
  1646  		"valid request": {
  1647  			contractID: s.contractID,
  1648  			granter:    s.vendor,
  1649  			grantee:    s.operator,
  1650  			permission: collection.LegacyPermissionModify.String(),
  1651  			events: sdk.Events{
  1652  				sdk.Event{
  1653  					Type: "lbm.collection.v1.EventGranted",
  1654  					Attributes: []abci.EventAttribute{
  1655  						{Key: []byte("contract_id"), Value: testutil.W(s.contractID), Index: false},
  1656  						{Key: []byte("grantee"), Value: testutil.W(s.operator.String()), Index: false},
  1657  						{Key: []byte("granter"), Value: testutil.W(s.vendor.String()), Index: false},
  1658  						{Key: []byte("permission"), Value: testutil.W(collection.Permission(collection.LegacyPermissionModify).String()), Index: false},
  1659  					}},
  1660  			},
  1661  		},
  1662  		"contract not found": {
  1663  			contractID: "deadbeef",
  1664  			granter:    s.vendor,
  1665  			grantee:    s.operator,
  1666  			permission: collection.LegacyPermissionModify.String(),
  1667  			err:        class.ErrContractNotExist,
  1668  		},
  1669  		"granter has no permission": {
  1670  			contractID: s.contractID,
  1671  			granter:    s.customer,
  1672  			grantee:    s.operator,
  1673  			permission: collection.LegacyPermissionModify.String(),
  1674  			err:        collection.ErrTokenNoPermission,
  1675  		},
  1676  	}
  1677  
  1678  	for name, tc := range testCases {
  1679  		s.Run(name, func() {
  1680  			ctx, _ := s.ctx.CacheContext()
  1681  
  1682  			req := &collection.MsgGrantPermission{
  1683  				ContractId: tc.contractID,
  1684  				From:       tc.granter.String(),
  1685  				To:         tc.grantee.String(),
  1686  				Permission: tc.permission,
  1687  			}
  1688  			res, err := s.msgServer.GrantPermission(sdk.WrapSDKContext(ctx), req)
  1689  			s.Require().ErrorIs(err, tc.err)
  1690  			if tc.err != nil {
  1691  				return
  1692  			}
  1693  
  1694  			s.Require().NotNil(res)
  1695  			s.Require().Equal(tc.events, ctx.EventManager().Events())
  1696  		})
  1697  	}
  1698  }
  1699  
  1700  func (s *KeeperTestSuite) TestMsgRevokePermission() {
  1701  	testCases := map[string]struct {
  1702  		contractID string
  1703  		from       sdk.AccAddress
  1704  		permission string
  1705  		err        error
  1706  		events     sdk.Events
  1707  	}{
  1708  		"valid request": {
  1709  			contractID: s.contractID,
  1710  			from:       s.operator,
  1711  			permission: collection.LegacyPermissionMint.String(),
  1712  			events: sdk.Events{
  1713  				sdk.Event{
  1714  					Type: "lbm.collection.v1.EventRenounced",
  1715  					Attributes: []abci.EventAttribute{
  1716  						{Key: []byte("contract_id"), Value: testutil.W(s.contractID), Index: false},
  1717  						{Key: []byte("grantee"), Value: testutil.W(s.operator.String()), Index: false},
  1718  						{Key: []byte("permission"), Value: testutil.W(collection.Permission(collection.LegacyPermissionMint).String()), Index: false},
  1719  					}},
  1720  			},
  1721  		},
  1722  		"contract not found": {
  1723  			contractID: "deadbeef",
  1724  			from:       s.operator,
  1725  			permission: collection.LegacyPermissionMint.String(),
  1726  			err:        class.ErrContractNotExist,
  1727  		},
  1728  		"not granted yet": {
  1729  			contractID: s.contractID,
  1730  			from:       s.operator,
  1731  			permission: collection.LegacyPermissionModify.String(),
  1732  			err:        collection.ErrTokenNoPermission,
  1733  		},
  1734  	}
  1735  
  1736  	for name, tc := range testCases {
  1737  		s.Run(name, func() {
  1738  			ctx, _ := s.ctx.CacheContext()
  1739  
  1740  			req := &collection.MsgRevokePermission{
  1741  				ContractId: tc.contractID,
  1742  				From:       tc.from.String(),
  1743  				Permission: tc.permission,
  1744  			}
  1745  			res, err := s.msgServer.RevokePermission(sdk.WrapSDKContext(ctx), req)
  1746  			s.Require().ErrorIs(err, tc.err)
  1747  			if tc.err != nil {
  1748  				return
  1749  			}
  1750  
  1751  			s.Require().NotNil(res)
  1752  			s.Require().Equal(tc.events, ctx.EventManager().Events())
  1753  		})
  1754  	}
  1755  }
  1756  
  1757  func (s *KeeperTestSuite) TestMsgAttach() {
  1758  	testCases := map[string]struct {
  1759  		contractID string
  1760  		subjectID  string
  1761  		targetID   string
  1762  		err        error
  1763  		events     sdk.Events
  1764  	}{
  1765  		"valid request": {
  1766  			contractID: s.contractID,
  1767  			subjectID:  collection.NewNFTID(s.nftClassID, s.depthLimit+1),
  1768  			targetID:   collection.NewNFTID(s.nftClassID, 1),
  1769  			events: sdk.Events{
  1770  				sdk.Event{
  1771  					Type: "lbm.collection.v1.EventAttached",
  1772  					Attributes: []abci.EventAttribute{
  1773  						{Key: []byte("contract_id"), Value: testutil.W(s.contractID), Index: false},
  1774  						{Key: []byte("holder"), Value: testutil.W(s.customer.String()), Index: false},
  1775  						{Key: []byte("operator"), Value: testutil.W(s.customer.String()), Index: false},
  1776  						{Key: []byte("subject"), Value: testutil.W(collection.NewNFTID(s.nftClassID, s.depthLimit+1)), Index: false},
  1777  						{Key: []byte("target"), Value: testutil.W(collection.NewNFTID(s.nftClassID, 1)), Index: false},
  1778  					}},
  1779  			},
  1780  		},
  1781  		"contract not found": {
  1782  			contractID: "deadbeef",
  1783  			subjectID:  collection.NewNFTID(s.nftClassID, collection.DefaultDepthLimit+1),
  1784  			targetID:   collection.NewNFTID(s.nftClassID, 1),
  1785  			err:        class.ErrContractNotExist,
  1786  		},
  1787  		"not owner of the token": {
  1788  			contractID: s.contractID,
  1789  			subjectID:  collection.NewNFTID(s.nftClassID, s.numNFTs+1),
  1790  			targetID:   collection.NewNFTID(s.nftClassID, 1),
  1791  			err:        collection.ErrTokenNotOwnedBy,
  1792  		},
  1793  	}
  1794  
  1795  	for name, tc := range testCases {
  1796  		s.Run(name, func() {
  1797  			ctx, _ := s.ctx.CacheContext()
  1798  
  1799  			req := &collection.MsgAttach{
  1800  				ContractId: tc.contractID,
  1801  				From:       s.customer.String(),
  1802  				TokenId:    tc.subjectID,
  1803  				ToTokenId:  tc.targetID,
  1804  			}
  1805  			res, err := s.msgServer.Attach(sdk.WrapSDKContext(ctx), req)
  1806  			s.Require().ErrorIs(err, tc.err)
  1807  			if tc.err != nil {
  1808  				return
  1809  			}
  1810  
  1811  			s.Require().NotNil(res)
  1812  			s.Require().Equal(tc.events, ctx.EventManager().Events())
  1813  		})
  1814  	}
  1815  }
  1816  
  1817  func (s *KeeperTestSuite) TestMsgDetach() {
  1818  	issuedNfts := make([]string, s.depthLimit)
  1819  	for i := 1; i <= s.depthLimit; i++ {
  1820  		issuedNfts[i-1] = collection.NewNFTID(s.nftClassID, i)
  1821  	}
  1822  
  1823  	testCases := map[string]struct {
  1824  		contractID string
  1825  		subjectID  string
  1826  		err        error
  1827  		events     sdk.Events
  1828  	}{
  1829  		"valid request": {
  1830  			contractID: s.contractID,
  1831  			subjectID:  issuedNfts[1],
  1832  			events: sdk.Events{
  1833  				sdk.Event{
  1834  					Type: "lbm.collection.v1.EventDetached",
  1835  					Attributes: []abci.EventAttribute{
  1836  						{Key: []byte("contract_id"), Value: testutil.W(s.contractID), Index: false},
  1837  						{Key: []byte("holder"), Value: testutil.W(s.customer.String()), Index: false},
  1838  						{Key: []byte("operator"), Value: testutil.W(s.customer.String()), Index: false},
  1839  						{Key: []byte("previous_parent"), Value: testutil.W(issuedNfts[0]), Index: false},
  1840  						{Key: []byte("subject"), Value: testutil.W(issuedNfts[1]), Index: false},
  1841  					}},
  1842  				sdk.Event{
  1843  					Type: "lbm.collection.v1.EventRootChanged",
  1844  					Attributes: []abci.EventAttribute{
  1845  						{Key: []byte("contract_id"), Value: testutil.W(s.contractID), Index: false},
  1846  						{Key: []byte("from"), Value: testutil.W(issuedNfts[0]), Index: false},
  1847  						{Key: []byte("to"), Value: testutil.W(issuedNfts[1]), Index: false},
  1848  						{Key: []byte("token_id"), Value: testutil.W(issuedNfts[2]), Index: false},
  1849  					}},
  1850  				sdk.Event{
  1851  					Type: "lbm.collection.v1.EventRootChanged",
  1852  					Attributes: []abci.EventAttribute{
  1853  						{Key: []byte("contract_id"), Value: testutil.W(s.contractID), Index: false},
  1854  						{Key: []byte("from"), Value: testutil.W(issuedNfts[0]), Index: false},
  1855  						{Key: []byte("to"), Value: testutil.W(issuedNfts[1]), Index: false},
  1856  						{Key: []byte("token_id"), Value: testutil.W(issuedNfts[3]), Index: false},
  1857  					}},
  1858  			},
  1859  		},
  1860  		"contract not found": {
  1861  			contractID: "deadbeef",
  1862  			subjectID:  collection.NewNFTID(s.nftClassID, 2),
  1863  			err:        class.ErrContractNotExist,
  1864  		},
  1865  		"not owner of the token": {
  1866  			contractID: s.contractID,
  1867  			subjectID:  collection.NewNFTID(s.nftClassID, s.numNFTs+2),
  1868  			err:        collection.ErrTokenNotOwnedBy,
  1869  		},
  1870  		"not a child": {
  1871  			contractID: s.contractID,
  1872  			subjectID:  collection.NewNFTID(s.nftClassID, 1),
  1873  			err:        collection.ErrTokenNotAChild,
  1874  		},
  1875  	}
  1876  
  1877  	for name, tc := range testCases {
  1878  		s.Run(name, func() {
  1879  			ctx, _ := s.ctx.CacheContext()
  1880  
  1881  			req := &collection.MsgDetach{
  1882  				ContractId: tc.contractID,
  1883  				From:       s.customer.String(),
  1884  				TokenId:    tc.subjectID,
  1885  			}
  1886  			res, err := s.msgServer.Detach(sdk.WrapSDKContext(ctx), req)
  1887  			s.Require().ErrorIs(err, tc.err)
  1888  			if tc.err != nil {
  1889  				return
  1890  			}
  1891  
  1892  			s.Require().NotNil(res)
  1893  			s.Require().Equal(tc.events, ctx.EventManager().Events())
  1894  
  1895  		})
  1896  	}
  1897  }
  1898  
  1899  func (s *KeeperTestSuite) TestMsgOperatorAttach() {
  1900  	testCases := map[string]struct {
  1901  		contractID string
  1902  		operator   sdk.AccAddress
  1903  		subjectID  string
  1904  		targetID   string
  1905  		err        error
  1906  		events     sdk.Events
  1907  	}{
  1908  		"valid request": {
  1909  			contractID: s.contractID,
  1910  			operator:   s.operator,
  1911  			subjectID:  collection.NewNFTID(s.nftClassID, s.depthLimit+1),
  1912  			targetID:   collection.NewNFTID(s.nftClassID, 1),
  1913  			events: sdk.Events{
  1914  				sdk.Event{
  1915  					Type: "lbm.collection.v1.EventAttached",
  1916  					Attributes: []abci.EventAttribute{
  1917  						{Key: []byte("contract_id"), Value: testutil.W(s.contractID), Index: false},
  1918  						{Key: []byte("holder"), Value: testutil.W(s.customer.String()), Index: false},
  1919  						{Key: []byte("operator"), Value: testutil.W(s.operator.String()), Index: false},
  1920  						{Key: []byte("subject"), Value: testutil.W(collection.NewNFTID(s.nftClassID, s.depthLimit+1)), Index: false},
  1921  						{Key: []byte("target"), Value: testutil.W(collection.NewNFTID(s.nftClassID, 1)), Index: false},
  1922  					}},
  1923  			}},
  1924  		"contract not found": {
  1925  			contractID: "deadbeef",
  1926  			operator:   s.operator,
  1927  			subjectID:  collection.NewNFTID(s.nftClassID, collection.DefaultDepthLimit+1),
  1928  			targetID:   collection.NewNFTID(s.nftClassID, 1),
  1929  			err:        class.ErrContractNotExist,
  1930  		},
  1931  		"not authorized": {
  1932  			contractID: s.contractID,
  1933  			operator:   s.vendor,
  1934  			subjectID:  collection.NewNFTID(s.nftClassID, s.depthLimit+1),
  1935  			targetID:   collection.NewNFTID(s.nftClassID, 1),
  1936  			err:        collection.ErrCollectionNotApproved,
  1937  		},
  1938  		"not owner of the token": {
  1939  			contractID: s.contractID,
  1940  			operator:   s.operator,
  1941  			subjectID:  collection.NewNFTID(s.nftClassID, s.numNFTs+1),
  1942  			targetID:   collection.NewNFTID(s.nftClassID, 1),
  1943  			err:        collection.ErrTokenNotOwnedBy,
  1944  		},
  1945  	}
  1946  
  1947  	for name, tc := range testCases {
  1948  		s.Run(name, func() {
  1949  			ctx, _ := s.ctx.CacheContext()
  1950  
  1951  			req := &collection.MsgOperatorAttach{
  1952  				ContractId: tc.contractID,
  1953  				Operator:   tc.operator.String(),
  1954  				From:       s.customer.String(),
  1955  				TokenId:    tc.subjectID,
  1956  				ToTokenId:  tc.targetID,
  1957  			}
  1958  			res, err := s.msgServer.OperatorAttach(sdk.WrapSDKContext(ctx), req)
  1959  			s.Require().ErrorIs(err, tc.err)
  1960  			if tc.err != nil {
  1961  				return
  1962  			}
  1963  
  1964  			s.Require().NotNil(res)
  1965  			s.Require().Equal(tc.events, ctx.EventManager().Events())
  1966  		})
  1967  	}
  1968  }
  1969  
  1970  func (s *KeeperTestSuite) TestMsgOperatorDetach() {
  1971  	nfts := make([]string, s.depthLimit)
  1972  	for i := 1; i <= s.depthLimit; i++ {
  1973  		nfts[i-1] = collection.NewNFTID(s.nftClassID, i)
  1974  	}
  1975  
  1976  	testCases := map[string]struct {
  1977  		contractID string
  1978  		operator   sdk.AccAddress
  1979  		subjectID  string
  1980  		err        error
  1981  		events     sdk.Events
  1982  	}{
  1983  		"valid request": {
  1984  			contractID: s.contractID,
  1985  			operator:   s.operator,
  1986  			subjectID:  collection.NewNFTID(s.nftClassID, 2),
  1987  			events: sdk.Events{
  1988  				sdk.Event{
  1989  					Type: "lbm.collection.v1.EventDetached",
  1990  					Attributes: []abci.EventAttribute{
  1991  						{Key: []byte("contract_id"), Value: testutil.W(s.contractID), Index: false},
  1992  						{Key: []byte("holder"), Value: testutil.W(s.customer.String()), Index: false},
  1993  						{Key: []byte("operator"), Value: testutil.W(s.operator.String()), Index: false},
  1994  						{Key: []byte("previous_parent"), Value: testutil.W(nfts[0]), Index: false},
  1995  						{Key: []byte("subject"), Value: testutil.W(nfts[1]), Index: false},
  1996  					}},
  1997  				sdk.Event{
  1998  					Type: "lbm.collection.v1.EventRootChanged",
  1999  					Attributes: []abci.EventAttribute{
  2000  						{Key: []byte("contract_id"), Value: testutil.W(s.contractID), Index: false},
  2001  						{Key: []byte("from"), Value: testutil.W(nfts[0]), Index: false},
  2002  						{Key: []byte("to"), Value: testutil.W(nfts[1]), Index: false},
  2003  						{Key: []byte("token_id"), Value: testutil.W(nfts[2]), Index: false},
  2004  					}},
  2005  				sdk.Event{
  2006  					Type: "lbm.collection.v1.EventRootChanged",
  2007  					Attributes: []abci.EventAttribute{
  2008  						{Key: []byte("contract_id"), Value: testutil.W(s.contractID), Index: false},
  2009  						{Key: []byte("from"), Value: testutil.W(nfts[0]), Index: false},
  2010  						{Key: []byte("to"), Value: testutil.W(nfts[1]), Index: false},
  2011  						{Key: []byte("token_id"), Value: testutil.W(nfts[3]), Index: false},
  2012  					}},
  2013  			}},
  2014  		"contract not found": {
  2015  			contractID: "deadbeef",
  2016  			operator:   s.operator,
  2017  			subjectID:  collection.NewNFTID(s.nftClassID, 2),
  2018  			err:        class.ErrContractNotExist,
  2019  		},
  2020  		"not authorized": {
  2021  			contractID: s.contractID,
  2022  			operator:   s.vendor,
  2023  			subjectID:  collection.NewNFTID(s.nftClassID, 2),
  2024  			err:        collection.ErrCollectionNotApproved,
  2025  		},
  2026  		"not owner of the token": {
  2027  			contractID: s.contractID,
  2028  			operator:   s.operator,
  2029  			subjectID:  collection.NewNFTID(s.nftClassID, s.numNFTs+2),
  2030  			err:        collection.ErrTokenNotOwnedBy,
  2031  		},
  2032  	}
  2033  
  2034  	for name, tc := range testCases {
  2035  		s.Run(name, func() {
  2036  			ctx, _ := s.ctx.CacheContext()
  2037  
  2038  			req := &collection.MsgOperatorDetach{
  2039  				ContractId: tc.contractID,
  2040  				Operator:   tc.operator.String(),
  2041  				From:       s.customer.String(),
  2042  				TokenId:    tc.subjectID,
  2043  			}
  2044  			res, err := s.msgServer.OperatorDetach(sdk.WrapSDKContext(ctx), req)
  2045  			s.Require().ErrorIs(err, tc.err)
  2046  			if tc.err != nil {
  2047  				return
  2048  			}
  2049  
  2050  			s.Require().NotNil(res)
  2051  			s.Require().Equal(tc.events, ctx.EventManager().Events())
  2052  
  2053  		})
  2054  	}
  2055  }
  2056  
  2057  func (s *KeeperTestSuite) extractChainedNFTIDs(root string) []string {
  2058  	allTokenIDs := make([]string, 0)
  2059  	allTokenIDs = append(allTokenIDs, root)
  2060  	cursor := allTokenIDs[0]
  2061  	for {
  2062  		ctx, _ := s.ctx.CacheContext()
  2063  		res, err := s.queryServer.Children(sdk.WrapSDKContext(ctx), &collection.QueryChildrenRequest{
  2064  			ContractId: s.contractID,
  2065  			TokenId:    cursor,
  2066  			Pagination: &query.PageRequest{},
  2067  		})
  2068  		s.Require().NoError(err)
  2069  		if res.Children == nil {
  2070  			break
  2071  		}
  2072  		allTokenIDs = append(allTokenIDs, res.Children[0].TokenId)
  2073  		cursor = allTokenIDs[len(allTokenIDs)-1]
  2074  	}
  2075  	return allTokenIDs
  2076  }