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