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