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 }