github.com/DapperCollectives/CAST/backend@v0.0.0-20230921221157-1350c8be7c96/main/cadence/nba/TopShot.cdc (about) 1 /* 2 Description: Central Smart Contract for NBA TopShot 3 4 This smart contract contains the core functionality for 5 NBA Top Shot, created by Dapper Labs 6 7 The contract manages the data associated with all the plays and sets 8 that are used as templates for the Moment NFTs 9 10 When a new Play wants to be added to the records, an Admin creates 11 a new Play struct that is stored in the smart contract. 12 13 Then an Admin can create new Sets. Sets consist of a public struct that 14 contains public information about a set, and a private resource used 15 to mint new moments based off of plays that have been linked to the Set. 16 17 The admin resource has the power to do all of the important actions 18 in the smart contract. When admins want to call functions in a set, 19 they call their borrowSet function to get a reference 20 to a set in the contract. Then, they can call functions on the set using that reference. 21 22 In this way, the smart contract and its defined resources interact 23 with great teamwork, just like the Indiana Pacers, the greatest NBA team 24 of all time. 25 26 When moments are minted, they are initialized with a MomentData struct and 27 are returned by the minter. 28 29 The contract also defines a Collection resource. This is an object that 30 every TopShot NFT owner will store in their account 31 to manage their NFT collection. 32 33 The main Top Shot account will also have its own Moment collections 34 it can use to hold its own moments that have not yet been sent to a user. 35 36 Note: All state changing functions will panic if an invalid argument is 37 provided or one of its pre-conditions or post conditions aren't met. 38 Functions that don't modify state will simply return 0 or nil 39 and those cases need to be handled by the caller. 40 41 It is also important to remember that 42 The Golden State Warriors blew a 3-1 lead in the 2016 NBA finals. 43 44 */ 45 46 import NonFungibleToken from 0xf8d6e0586b0a20c7 47 import MetadataViews from 0xf8d6e0586b0a20c7 48 import TopShotLocking from 0xf8d6e0586b0a20c7 49 50 pub contract TopShot: NonFungibleToken { 51 52 // ----------------------------------------------------------------------- 53 // TopShot contract Events 54 // ----------------------------------------------------------------------- 55 56 // Emitted when the TopShot contract is created 57 pub event ContractInitialized() 58 59 // Emitted when a new Play struct is created 60 pub event PlayCreated(id: UInt32, metadata: {String:String}) 61 // Emitted when a new series has been triggered by an admin 62 pub event NewSeriesStarted(newCurrentSeries: UInt32) 63 64 // Events for Set-Related actions 65 // 66 // Emitted when a new Set is created 67 pub event SetCreated(setID: UInt32, series: UInt32) 68 // Emitted when a new Play is added to a Set 69 pub event PlayAddedToSet(setID: UInt32, playID: UInt32) 70 // Emitted when a Play is retired from a Set and cannot be used to mint 71 pub event PlayRetiredFromSet(setID: UInt32, playID: UInt32, numMoments: UInt32) 72 // Emitted when a Set is locked, meaning Plays cannot be added 73 pub event SetLocked(setID: UInt32) 74 // Emitted when a Moment is minted from a Set 75 pub event MomentMinted(momentID: UInt64, playID: UInt32, setID: UInt32, serialNumber: UInt32) 76 77 // Events for Collection-related actions 78 // 79 // Emitted when a moment is withdrawn from a Collection 80 pub event Withdraw(id: UInt64, from: Address?) 81 // Emitted when a moment is deposited into a Collection 82 pub event Deposit(id: UInt64, to: Address?) 83 84 // Emitted when a Moment is destroyed 85 pub event MomentDestroyed(id: UInt64) 86 87 // ----------------------------------------------------------------------- 88 // TopShot contract-level fields. 89 // These contain actual values that are stored in the smart contract. 90 // ----------------------------------------------------------------------- 91 92 // Series that this Set belongs to. 93 // Series is a concept that indicates a group of Sets through time. 94 // Many Sets can exist at a time, but only one series. 95 pub var currentSeries: UInt32 96 97 // Variable size dictionary of Play structs 98 access(self) var playDatas: {UInt32: Play} 99 100 // Variable size dictionary of SetData structs 101 access(self) var setDatas: {UInt32: SetData} 102 103 // Variable size dictionary of Set resources 104 access(self) var sets: @{UInt32: Set} 105 106 // The ID that is used to create Plays. 107 // Every time a Play is created, playID is assigned 108 // to the new Play's ID and then is incremented by 1. 109 pub var nextPlayID: UInt32 110 111 // The ID that is used to create Sets. Every time a Set is created 112 // setID is assigned to the new set's ID and then is incremented by 1. 113 pub var nextSetID: UInt32 114 115 // The total number of Top shot Moment NFTs that have been created 116 // Because NFTs can be destroyed, it doesn't necessarily mean that this 117 // reflects the total number of NFTs in existence, just the number that 118 // have been minted to date. Also used as global moment IDs for minting. 119 pub var totalSupply: UInt64 120 121 // ----------------------------------------------------------------------- 122 // TopShot contract-level Composite Type definitions 123 // ----------------------------------------------------------------------- 124 // These are just *definitions* for Types that this contract 125 // and other accounts can use. These definitions do not contain 126 // actual stored values, but an instance (or object) of one of these Types 127 // can be created by this contract that contains stored values. 128 // ----------------------------------------------------------------------- 129 130 // Play is a Struct that holds metadata associated 131 // with a specific NBA play, like the legendary moment when 132 // Ray Allen hit the 3 to tie the Heat and Spurs in the 2013 finals game 6 133 // or when Lance Stephenson blew in the ear of Lebron James. 134 // 135 // Moment NFTs will all reference a single play as the owner of 136 // its metadata. The plays are publicly accessible, so anyone can 137 // read the metadata associated with a specific play ID 138 // 139 pub struct Play { 140 141 // The unique ID for the Play 142 pub let playID: UInt32 143 144 // Stores all the metadata about the play as a string mapping 145 // This is not the long term way NFT metadata will be stored. It's a temporary 146 // construct while we figure out a better way to do metadata. 147 // 148 pub let metadata: {String: String} 149 150 init(metadata: {String: String}) { 151 pre { 152 metadata.length != 0: "New Play metadata cannot be empty" 153 } 154 self.playID = TopShot.nextPlayID 155 self.metadata = metadata 156 } 157 } 158 159 // A Set is a grouping of Plays that have occured in the real world 160 // that make up a related group of collectibles, like sets of baseball 161 // or Magic cards. A Play can exist in multiple different sets. 162 // 163 // SetData is a struct that is stored in a field of the contract. 164 // Anyone can query the constant information 165 // about a set by calling various getters located 166 // at the end of the contract. Only the admin has the ability 167 // to modify any data in the private Set resource. 168 // 169 pub struct SetData { 170 171 // Unique ID for the Set 172 pub let setID: UInt32 173 174 // Name of the Set 175 // ex. "Times when the Toronto Raptors choked in the playoffs" 176 pub let name: String 177 178 // Series that this Set belongs to. 179 // Series is a concept that indicates a group of Sets through time. 180 // Many Sets can exist at a time, but only one series. 181 pub let series: UInt32 182 183 init(name: String) { 184 pre { 185 name.length > 0: "New Set name cannot be empty" 186 } 187 self.setID = TopShot.nextSetID 188 self.name = name 189 self.series = TopShot.currentSeries 190 } 191 } 192 193 // Set is a resource type that contains the functions to add and remove 194 // Plays from a set and mint Moments. 195 // 196 // It is stored in a private field in the contract so that 197 // the admin resource can call its methods. 198 // 199 // The admin can add Plays to a Set so that the set can mint Moments 200 // that reference that playdata. 201 // The Moments that are minted by a Set will be listed as belonging to 202 // the Set that minted it, as well as the Play it references. 203 // 204 // Admin can also retire Plays from the Set, meaning that the retired 205 // Play can no longer have Moments minted from it. 206 // 207 // If the admin locks the Set, no more Plays can be added to it, but 208 // Moments can still be minted. 209 // 210 // If retireAll() and lock() are called back-to-back, 211 // the Set is closed off forever and nothing more can be done with it. 212 pub resource Set { 213 214 // Unique ID for the set 215 pub let setID: UInt32 216 217 // Array of plays that are a part of this set. 218 // When a play is added to the set, its ID gets appended here. 219 // The ID does not get removed from this array when a Play is retired. 220 access(contract) var plays: [UInt32] 221 222 // Map of Play IDs that Indicates if a Play in this Set can be minted. 223 // When a Play is added to a Set, it is mapped to false (not retired). 224 // When a Play is retired, this is set to true and cannot be changed. 225 access(contract) var retired: {UInt32: Bool} 226 227 // Indicates if the Set is currently locked. 228 // When a Set is created, it is unlocked 229 // and Plays are allowed to be added to it. 230 // When a set is locked, Plays cannot be added. 231 // A Set can never be changed from locked to unlocked, 232 // the decision to lock a Set it is final. 233 // If a Set is locked, Plays cannot be added, but 234 // Moments can still be minted from Plays 235 // that exist in the Set. 236 pub var locked: Bool 237 238 // Mapping of Play IDs that indicates the number of Moments 239 // that have been minted for specific Plays in this Set. 240 // When a Moment is minted, this value is stored in the Moment to 241 // show its place in the Set, eg. 13 of 60. 242 access(contract) var numberMintedPerPlay: {UInt32: UInt32} 243 244 init(name: String) { 245 self.setID = TopShot.nextSetID 246 self.plays = [] 247 self.retired = {} 248 self.locked = false 249 self.numberMintedPerPlay = {} 250 251 // Create a new SetData for this Set and store it in contract storage 252 TopShot.setDatas[self.setID] = SetData(name: name) 253 } 254 255 // addPlay adds a play to the set 256 // 257 // Parameters: playID: The ID of the Play that is being added 258 // 259 // Pre-Conditions: 260 // The Play needs to be an existing play 261 // The Set needs to be not locked 262 // The Play can't have already been added to the Set 263 // 264 pub fun addPlay(playID: UInt32) { 265 pre { 266 TopShot.playDatas[playID] != nil: "Cannot add the Play to Set: Play doesn't exist." 267 !self.locked: "Cannot add the play to the Set after the set has been locked." 268 self.numberMintedPerPlay[playID] == nil: "The play has already beed added to the set." 269 } 270 271 // Add the Play to the array of Plays 272 self.plays.append(playID) 273 274 // Open the Play up for minting 275 self.retired[playID] = false 276 277 // Initialize the Moment count to zero 278 self.numberMintedPerPlay[playID] = 0 279 280 emit PlayAddedToSet(setID: self.setID, playID: playID) 281 } 282 283 // addPlays adds multiple Plays to the Set 284 // 285 // Parameters: playIDs: The IDs of the Plays that are being added 286 // as an array 287 // 288 pub fun addPlays(playIDs: [UInt32]) { 289 for play in playIDs { 290 self.addPlay(playID: play) 291 } 292 } 293 294 // retirePlay retires a Play from the Set so that it can't mint new Moments 295 // 296 // Parameters: playID: The ID of the Play that is being retired 297 // 298 // Pre-Conditions: 299 // The Play is part of the Set and not retired (available for minting). 300 // 301 pub fun retirePlay(playID: UInt32) { 302 pre { 303 self.retired[playID] != nil: "Cannot retire the Play: Play doesn't exist in this set!" 304 } 305 306 if !self.retired[playID]! { 307 self.retired[playID] = true 308 309 emit PlayRetiredFromSet(setID: self.setID, playID: playID, numMoments: self.numberMintedPerPlay[playID]!) 310 } 311 } 312 313 // retireAll retires all the plays in the Set 314 // Afterwards, none of the retired Plays will be able to mint new Moments 315 // 316 pub fun retireAll() { 317 for play in self.plays { 318 self.retirePlay(playID: play) 319 } 320 } 321 322 // lock() locks the Set so that no more Plays can be added to it 323 // 324 // Pre-Conditions: 325 // The Set should not be locked 326 pub fun lock() { 327 if !self.locked { 328 self.locked = true 329 emit SetLocked(setID: self.setID) 330 } 331 } 332 333 // mintMoment mints a new Moment and returns the newly minted Moment 334 // 335 // Parameters: playID: The ID of the Play that the Moment references 336 // 337 // Pre-Conditions: 338 // The Play must exist in the Set and be allowed to mint new Moments 339 // 340 // Returns: The NFT that was minted 341 // 342 pub fun mintMoment(playID: UInt32): @NFT { 343 pre { 344 self.retired[playID] != nil: "Cannot mint the moment: This play doesn't exist." 345 !self.retired[playID]!: "Cannot mint the moment from this play: This play has been retired." 346 } 347 348 // Gets the number of Moments that have been minted for this Play 349 // to use as this Moment's serial number 350 let numInPlay = self.numberMintedPerPlay[playID]! 351 352 // Mint the new moment 353 let newMoment: @NFT <- create NFT(serialNumber: numInPlay + UInt32(1), 354 playID: playID, 355 setID: self.setID) 356 357 // Increment the count of Moments minted for this Play 358 self.numberMintedPerPlay[playID] = numInPlay + UInt32(1) 359 360 return <-newMoment 361 } 362 363 // batchMintMoment mints an arbitrary quantity of Moments 364 // and returns them as a Collection 365 // 366 // Parameters: playID: the ID of the Play that the Moments are minted for 367 // quantity: The quantity of Moments to be minted 368 // 369 // Returns: Collection object that contains all the Moments that were minted 370 // 371 pub fun batchMintMoment(playID: UInt32, quantity: UInt64): @Collection { 372 let newCollection <- create Collection() 373 374 var i: UInt64 = 0 375 while i < quantity { 376 newCollection.deposit(token: <-self.mintMoment(playID: playID)) 377 i = i + UInt64(1) 378 } 379 380 return <-newCollection 381 } 382 383 pub fun getPlays(): [UInt32] { 384 return self.plays 385 } 386 387 pub fun getRetired(): {UInt32: Bool} { 388 return self.retired 389 } 390 391 pub fun getNumMintedPerPlay(): {UInt32: UInt32} { 392 return self.numberMintedPerPlay 393 } 394 } 395 396 // Struct that contains all of the important data about a set 397 // Can be easily queried by instantiating the `QuerySetData` object 398 // with the desired set ID 399 // let setData = TopShot.QuerySetData(setID: 12) 400 // 401 pub struct QuerySetData { 402 pub let setID: UInt32 403 pub let name: String 404 pub let series: UInt32 405 access(self) var plays: [UInt32] 406 access(self) var retired: {UInt32: Bool} 407 pub var locked: Bool 408 access(self) var numberMintedPerPlay: {UInt32: UInt32} 409 410 init(setID: UInt32) { 411 pre { 412 TopShot.sets[setID] != nil: "The set with the provided ID does not exist" 413 } 414 415 let set = (&TopShot.sets[setID] as &Set?)! 416 let setData = TopShot.setDatas[setID]! 417 418 self.setID = setID 419 self.name = setData.name 420 self.series = setData.series 421 self.plays = set.plays 422 self.retired = set.retired 423 self.locked = set.locked 424 self.numberMintedPerPlay = set.numberMintedPerPlay 425 } 426 427 pub fun getPlays(): [UInt32] { 428 return self.plays 429 } 430 431 pub fun getRetired(): {UInt32: Bool} { 432 return self.retired 433 } 434 435 pub fun getNumberMintedPerPlay(): {UInt32: UInt32} { 436 return self.numberMintedPerPlay 437 } 438 } 439 440 pub struct MomentData { 441 442 // The ID of the Set that the Moment comes from 443 pub let setID: UInt32 444 445 // The ID of the Play that the Moment references 446 pub let playID: UInt32 447 448 // The place in the edition that this Moment was minted 449 // Otherwise know as the serial number 450 pub let serialNumber: UInt32 451 452 init(setID: UInt32, playID: UInt32, serialNumber: UInt32) { 453 self.setID = setID 454 self.playID = playID 455 self.serialNumber = serialNumber 456 } 457 458 } 459 460 // This is an implementation of a custom metadata view for Top Shot. 461 // This view contains the play metadata. 462 // 463 pub struct TopShotMomentMetadataView { 464 465 pub let fullName: String? 466 pub let firstName: String? 467 pub let lastName: String? 468 pub let birthdate: String? 469 pub let birthplace: String? 470 pub let jerseyNumber: String? 471 pub let draftTeam: String? 472 pub let draftYear: String? 473 pub let draftSelection: String? 474 pub let draftRound: String? 475 pub let teamAtMomentNBAID: String? 476 pub let teamAtMoment: String? 477 pub let primaryPosition: String? 478 pub let height: String? 479 pub let weight: String? 480 pub let totalYearsExperience: String? 481 pub let nbaSeason: String? 482 pub let dateOfMoment: String? 483 pub let playCategory: String? 484 pub let playType: String? 485 pub let homeTeamName: String? 486 pub let awayTeamName: String? 487 pub let homeTeamScore: String? 488 pub let awayTeamScore: String? 489 pub let seriesNumber: UInt32? 490 pub let setName: String? 491 pub let serialNumber: UInt32 492 pub let playID: UInt32 493 pub let setID: UInt32 494 pub let numMomentsInEdition: UInt32? 495 496 init( 497 fullName: String?, 498 firstName: String?, 499 lastName: String?, 500 birthdate: String?, 501 birthplace: String?, 502 jerseyNumber: String?, 503 draftTeam: String?, 504 draftYear: String?, 505 draftSelection: String?, 506 draftRound: String?, 507 teamAtMomentNBAID: String?, 508 teamAtMoment: String?, 509 primaryPosition: String?, 510 height: String?, 511 weight: String?, 512 totalYearsExperience: String?, 513 nbaSeason: String?, 514 dateOfMoment: String?, 515 playCategory: String?, 516 playType: String?, 517 homeTeamName: String?, 518 awayTeamName: String?, 519 homeTeamScore: String?, 520 awayTeamScore: String?, 521 seriesNumber: UInt32?, 522 setName: String?, 523 serialNumber: UInt32, 524 playID: UInt32, 525 setID: UInt32, 526 numMomentsInEdition: UInt32? 527 ) { 528 self.fullName = fullName 529 self.firstName = firstName 530 self.lastName = lastName 531 self.birthdate = birthdate 532 self.birthplace = birthplace 533 self.jerseyNumber = jerseyNumber 534 self.draftTeam = draftTeam 535 self.draftYear = draftYear 536 self.draftSelection = draftSelection 537 self.draftRound = draftRound 538 self.teamAtMomentNBAID = teamAtMomentNBAID 539 self.teamAtMoment = teamAtMoment 540 self.primaryPosition = primaryPosition 541 self.height = height 542 self.weight = weight 543 self.totalYearsExperience = totalYearsExperience 544 self.nbaSeason = nbaSeason 545 self.dateOfMoment= dateOfMoment 546 self.playCategory = playCategory 547 self.playType = playType 548 self.homeTeamName = homeTeamName 549 self.awayTeamName = awayTeamName 550 self.homeTeamScore = homeTeamScore 551 self.awayTeamScore = awayTeamScore 552 self.seriesNumber = seriesNumber 553 self.setName = setName 554 self.serialNumber = serialNumber 555 self.playID = playID 556 self.setID = setID 557 self.numMomentsInEdition = numMomentsInEdition 558 } 559 } 560 561 // The resource that represents the Moment NFTs 562 // 563 pub resource NFT: NonFungibleToken.INFT, MetadataViews.Resolver { 564 565 // Global unique moment ID 566 pub let id: UInt64 567 568 // Struct of Moment metadata 569 pub let data: MomentData 570 571 init(serialNumber: UInt32, playID: UInt32, setID: UInt32) { 572 // Increment the global Moment IDs 573 TopShot.totalSupply = TopShot.totalSupply + UInt64(1) 574 575 self.id = TopShot.totalSupply 576 577 // Set the metadata struct 578 self.data = MomentData(setID: setID, playID: playID, serialNumber: serialNumber) 579 580 emit MomentMinted(momentID: self.id, playID: playID, setID: self.data.setID, serialNumber: self.data.serialNumber) 581 } 582 583 // If the Moment is destroyed, emit an event to indicate 584 // to outside ovbservers that it has been destroyed 585 destroy() { 586 emit MomentDestroyed(id: self.id) 587 } 588 589 pub fun name(): String { 590 let fullName: String = TopShot.getPlayMetaDataByField(playID: self.data.playID, field: "FullName") ?? "" 591 let playType: String = TopShot.getPlayMetaDataByField(playID: self.data.playID, field: "PlayType") ?? "" 592 return fullName 593 .concat(" ") 594 .concat(playType) 595 } 596 597 pub fun description(): String { 598 let setName: String = TopShot.getSetName(setID: self.data.setID) ?? "" 599 let serialNumber: String = self.data.serialNumber.toString() 600 let seriesNumber: String = TopShot.getSetSeries(setID: self.data.setID)?.toString() ?? "" 601 return "A series " 602 .concat(seriesNumber) 603 .concat(" ") 604 .concat(setName) 605 .concat(" moment with serial number ") 606 .concat(serialNumber) 607 } 608 609 pub fun getViews(): [Type] { 610 return [ 611 Type<MetadataViews.Display>(), 612 Type<TopShotMomentMetadataView>() 613 ] 614 } 615 616 pub fun resolveView(_ view: Type): AnyStruct? { 617 switch view { 618 case Type<MetadataViews.Display>(): 619 return MetadataViews.Display( 620 name: self.name(), 621 description: self.description(), 622 thumbnail: MetadataViews.HTTPFile(url:"https://ipfs.dapperlabs.com/ipfs/Qmbdj1agtbzpPWZ81wCGaDiMKRFaRN3TU6cfztVCu6nh4o") 623 ) 624 case Type<TopShotMomentMetadataView>(): 625 return TopShotMomentMetadataView( 626 fullName: TopShot.getPlayMetaDataByField(playID: self.data.playID, field: "FullName"), 627 firstName: TopShot.getPlayMetaDataByField(playID: self.data.playID, field: "FirstName"), 628 lastName: TopShot.getPlayMetaDataByField(playID: self.data.playID, field: "LastName"), 629 birthdate: TopShot.getPlayMetaDataByField(playID: self.data.playID, field: "Birthdate"), 630 birthplace: TopShot.getPlayMetaDataByField(playID: self.data.playID, field: "Birthplace"), 631 jerseyNumber: TopShot.getPlayMetaDataByField(playID: self.data.playID, field: "JerseyNumber"), 632 draftTeam: TopShot.getPlayMetaDataByField(playID: self.data.playID, field: "DraftTeam"), 633 draftYear: TopShot.getPlayMetaDataByField(playID: self.data.playID, field: "DraftYear"), 634 draftSelection: TopShot.getPlayMetaDataByField(playID: self.data.playID, field: "DraftSelection"), 635 draftRound: TopShot.getPlayMetaDataByField(playID: self.data.playID, field: "DraftRound"), 636 teamAtMomentNBAID: TopShot.getPlayMetaDataByField(playID: self.data.playID, field: "TeamAtMomentNBAID"), 637 teamAtMoment: TopShot.getPlayMetaDataByField(playID: self.data.playID, field: "TeamAtMoment"), 638 primaryPosition: TopShot.getPlayMetaDataByField(playID: self.data.playID, field: "PrimaryPosition"), 639 height: TopShot.getPlayMetaDataByField(playID: self.data.playID, field: "Height"), 640 weight: TopShot.getPlayMetaDataByField(playID: self.data.playID, field: "Weight"), 641 totalYearsExperience: TopShot.getPlayMetaDataByField(playID: self.data.playID, field: "TotalYearsExperience"), 642 nbaSeason: TopShot.getPlayMetaDataByField(playID: self.data.playID, field: "NbaSeason"), 643 dateOfMoment: TopShot.getPlayMetaDataByField(playID: self.data.playID, field: "DateOfMoment"), 644 playCategory: TopShot.getPlayMetaDataByField(playID: self.data.playID, field: "PlayCategory"), 645 playType: TopShot.getPlayMetaDataByField(playID: self.data.playID, field: "PlayType"), 646 homeTeamName: TopShot.getPlayMetaDataByField(playID: self.data.playID, field: "HomeTeamName"), 647 awayTeamName: TopShot.getPlayMetaDataByField(playID: self.data.playID, field: "AwayTeamName"), 648 homeTeamScore: TopShot.getPlayMetaDataByField(playID: self.data.playID, field: "HomeTeamScore"), 649 awayTeamScore: TopShot.getPlayMetaDataByField(playID: self.data.playID, field: "AwayTeamScore"), 650 seriesNumber: TopShot.getSetSeries(setID: self.data.setID), 651 setName: TopShot.getSetName(setID: self.data.setID), 652 serialNumber: self.data.serialNumber, 653 playID: self.data.playID, 654 setID: self.data.setID, 655 numMomentsInEdition: TopShot.getNumMomentsInEdition(setID: self.data.setID, playID: self.data.playID) 656 ) 657 } 658 659 return nil 660 } 661 } 662 663 // Admin is a special authorization resource that 664 // allows the owner to perform important functions to modify the 665 // various aspects of the Plays, Sets, and Moments 666 // 667 pub resource Admin { 668 669 // createPlay creates a new Play struct 670 // and stores it in the Plays dictionary in the TopShot smart contract 671 // 672 // Parameters: metadata: A dictionary mapping metadata titles to their data 673 // example: {"Player Name": "Kevin Durant", "Height": "7 feet"} 674 // (because we all know Kevin Durant is not 6'9") 675 // 676 // Returns: the ID of the new Play object 677 // 678 pub fun createPlay(metadata: {String: String}): UInt32 { 679 // Create the new Play 680 var newPlay = Play(metadata: metadata) 681 let newID = newPlay.playID 682 683 // Increment the ID so that it isn't used again 684 TopShot.nextPlayID = TopShot.nextPlayID + UInt32(1) 685 686 emit PlayCreated(id: newPlay.playID, metadata: metadata) 687 688 // Store it in the contract storage 689 TopShot.playDatas[newID] = newPlay 690 691 return newID 692 } 693 694 // createSet creates a new Set resource and stores it 695 // in the sets mapping in the TopShot contract 696 // 697 // Parameters: name: The name of the Set 698 // 699 // Returns: The ID of the created set 700 pub fun createSet(name: String): UInt32 { 701 702 // Create the new Set 703 var newSet <- create Set(name: name) 704 705 // Increment the setID so that it isn't used again 706 TopShot.nextSetID = TopShot.nextSetID + UInt32(1) 707 708 let newID = newSet.setID 709 710 emit SetCreated(setID: newSet.setID, series: TopShot.currentSeries) 711 712 // Store it in the sets mapping field 713 TopShot.sets[newID] <-! newSet 714 715 return newID 716 } 717 718 // borrowSet returns a reference to a set in the TopShot 719 // contract so that the admin can call methods on it 720 // 721 // Parameters: setID: The ID of the Set that you want to 722 // get a reference to 723 // 724 // Returns: A reference to the Set with all of the fields 725 // and methods exposed 726 // 727 pub fun borrowSet(setID: UInt32): &Set { 728 pre { 729 TopShot.sets[setID] != nil: "Cannot borrow Set: The Set doesn't exist" 730 } 731 732 // Get a reference to the Set and return it 733 // use `&` to indicate the reference to the object and type 734 return (&TopShot.sets[setID] as &Set?)! 735 } 736 737 // startNewSeries ends the current series by incrementing 738 // the series number, meaning that Moments minted after this 739 // will use the new series number 740 // 741 // Returns: The new series number 742 // 743 pub fun startNewSeries(): UInt32 { 744 // End the current series and start a new one 745 // by incrementing the TopShot series number 746 TopShot.currentSeries = TopShot.currentSeries + UInt32(1) 747 748 emit NewSeriesStarted(newCurrentSeries: TopShot.currentSeries) 749 750 return TopShot.currentSeries 751 } 752 753 // createNewAdmin creates a new Admin resource 754 // 755 pub fun createNewAdmin(): @Admin { 756 return <-create Admin() 757 } 758 } 759 760 // This is the interface that users can cast their Moment Collection as 761 // to allow others to deposit Moments into their Collection. It also allows for reading 762 // the IDs of Moments in the Collection. 763 pub resource interface MomentCollectionPublic { 764 pub fun deposit(token: @NonFungibleToken.NFT) 765 pub fun batchDeposit(tokens: @NonFungibleToken.Collection) 766 pub fun getIDs(): [UInt64] 767 pub fun borrowNFT(id: UInt64): &NonFungibleToken.NFT 768 pub fun borrowMoment(id: UInt64): &TopShot.NFT? { 769 // If the result isn't nil, the id of the returned reference 770 // should be the same as the argument to the function 771 post { 772 (result == nil) || (result?.id == id): 773 "Cannot borrow Moment reference: The ID of the returned reference is incorrect" 774 } 775 } 776 } 777 778 // Collection is a resource that every user who owns NFTs 779 // will store in their account to manage their NFTS 780 // 781 pub resource Collection: MomentCollectionPublic, NonFungibleToken.Provider, NonFungibleToken.Receiver, NonFungibleToken.CollectionPublic, MetadataViews.ResolverCollection { 782 // Dictionary of Moment conforming tokens 783 // NFT is a resource type with a UInt64 ID field 784 pub var ownedNFTs: @{UInt64: NonFungibleToken.NFT} 785 786 init() { 787 self.ownedNFTs <- {} 788 } 789 790 // withdraw removes an Moment from the Collection and moves it to the caller 791 // 792 // Parameters: withdrawID: The ID of the NFT 793 // that is to be removed from the Collection 794 // 795 // returns: @NonFungibleToken.NFT the token that was withdrawn 796 pub fun withdraw(withdrawID: UInt64): @NonFungibleToken.NFT { 797 798 // Borrow nft and check if locked 799 let nft = self.borrowNFT(id: withdrawID) 800 if TopShotLocking.isLocked(nftRef: nft) { 801 panic("Cannot withdraw: Moment is locked") 802 } 803 804 // Remove the nft from the Collection 805 let token <- self.ownedNFTs.remove(key: withdrawID) 806 ?? panic("Cannot withdraw: Moment does not exist in the collection") 807 808 emit Withdraw(id: token.id, from: self.owner?.address) 809 810 // Return the withdrawn token 811 return <-token 812 } 813 814 // batchWithdraw withdraws multiple tokens and returns them as a Collection 815 // 816 // Parameters: ids: An array of IDs to withdraw 817 // 818 // Returns: @NonFungibleToken.Collection: A collection that contains 819 // the withdrawn moments 820 // 821 pub fun batchWithdraw(ids: [UInt64]): @NonFungibleToken.Collection { 822 // Create a new empty Collection 823 var batchCollection <- create Collection() 824 825 // Iterate through the ids and withdraw them from the Collection 826 for id in ids { 827 batchCollection.deposit(token: <-self.withdraw(withdrawID: id)) 828 } 829 830 // Return the withdrawn tokens 831 return <-batchCollection 832 } 833 834 // deposit takes a Moment and adds it to the Collections dictionary 835 // 836 // Paramters: token: the NFT to be deposited in the collection 837 // 838 pub fun deposit(token: @NonFungibleToken.NFT) { 839 840 // Cast the deposited token as a TopShot NFT to make sure 841 // it is the correct type 842 let token <- token as! @TopShot.NFT 843 844 // Get the token's ID 845 let id = token.id 846 847 // Add the new token to the dictionary 848 let oldToken <- self.ownedNFTs[id] <- token 849 850 // Only emit a deposit event if the Collection 851 // is in an account's storage 852 if self.owner?.address != nil { 853 emit Deposit(id: id, to: self.owner?.address) 854 } 855 856 // Destroy the empty old token that was "removed" 857 destroy oldToken 858 } 859 860 // batchDeposit takes a Collection object as an argument 861 // and deposits each contained NFT into this Collection 862 pub fun batchDeposit(tokens: @NonFungibleToken.Collection) { 863 864 // Get an array of the IDs to be deposited 865 let keys = tokens.getIDs() 866 867 // Iterate through the keys in the collection and deposit each one 868 for key in keys { 869 self.deposit(token: <-tokens.withdraw(withdrawID: key)) 870 } 871 872 // Destroy the empty Collection 873 destroy tokens 874 } 875 876 // lock takes a token id and a duration in seconds and locks 877 // the moment for that duration 878 pub fun lock(id: UInt64, duration: UFix64) { 879 // Remove the nft from the Collection 880 let token <- self.ownedNFTs.remove(key: id) 881 ?? panic("Cannot lock: Moment does not exist in the collection") 882 883 // pass the token to the locking contract 884 // store it again after it comes back 885 let oldToken <- self.ownedNFTs[id] <- TopShotLocking.lockNFT(nft: <- token, duration: duration) 886 887 destroy oldToken 888 } 889 890 // batchLock takes an array of token ids and a duration in seconds 891 // it iterates through the ids and locks each for the specified duration 892 pub fun batchLock(ids: [UInt64], duration: UFix64) { 893 // Iterate through the ids and lock them 894 for id in ids { 895 self.lock(id: id, duration: duration) 896 } 897 } 898 899 // unlock takes a token id and attempts to unlock it 900 // TopShotLocking.unlockNFT contains business logic around unlock eligibility 901 pub fun unlock(id: UInt64) { 902 // Remove the nft from the Collection 903 let token <- self.ownedNFTs.remove(key: id) 904 ?? panic("Cannot lock: Moment does not exist in the collection") 905 906 // Pass the token to the TopShotLocking contract then get it back 907 // Store it back to the ownedNFTs dictionary 908 let oldToken <- self.ownedNFTs[id] <- TopShotLocking.unlockNFT(nft: <- token) 909 910 destroy oldToken 911 } 912 913 // batchUnlock takes an array of token ids 914 // it iterates through the ids and unlocks each if they are eligible 915 pub fun batchUnlock(ids: [UInt64]) { 916 // Iterate through the ids and unlocks them 917 for id in ids { 918 self.unlock(id: id) 919 } 920 } 921 922 // getIDs returns an array of the IDs that are in the Collection 923 pub fun getIDs(): [UInt64] { 924 return self.ownedNFTs.keys 925 } 926 927 // borrowNFT Returns a borrowed reference to a Moment in the Collection 928 // so that the caller can read its ID 929 // 930 // Parameters: id: The ID of the NFT to get the reference for 931 // 932 // Returns: A reference to the NFT 933 // 934 // Note: This only allows the caller to read the ID of the NFT, 935 // not any topshot specific data. Please use borrowMoment to 936 // read Moment data. 937 // 938 pub fun borrowNFT(id: UInt64): &NonFungibleToken.NFT { 939 return (&self.ownedNFTs[id] as &NonFungibleToken.NFT?)! 940 } 941 942 // borrowMoment returns a borrowed reference to a Moment 943 // so that the caller can read data and call methods from it. 944 // They can use this to read its setID, playID, serialNumber, 945 // or any of the setData or Play data associated with it by 946 // getting the setID or playID and reading those fields from 947 // the smart contract. 948 // 949 // Parameters: id: The ID of the NFT to get the reference for 950 // 951 // Returns: A reference to the NFT 952 pub fun borrowMoment(id: UInt64): &TopShot.NFT? { 953 if self.ownedNFTs[id] != nil { 954 let ref = (&self.ownedNFTs[id] as auth &NonFungibleToken.NFT?)! 955 return ref as! &TopShot.NFT 956 } else { 957 return nil 958 } 959 } 960 961 pub fun borrowViewResolver(id: UInt64): &AnyResource{MetadataViews.Resolver} { 962 let nft = (&self.ownedNFTs[id] as auth &NonFungibleToken.NFT?)! 963 let topShotNFT = nft as! &TopShot.NFT 964 return topShotNFT as &AnyResource{MetadataViews.Resolver} 965 } 966 967 // If a transaction destroys the Collection object, 968 // All the NFTs contained within are also destroyed! 969 // Much like when Damian Lillard destroys the hopes and 970 // dreams of the entire city of Houston. 971 // 972 destroy() { 973 destroy self.ownedNFTs 974 } 975 } 976 977 // ----------------------------------------------------------------------- 978 // TopShot contract-level function definitions 979 // ----------------------------------------------------------------------- 980 981 // createEmptyCollection creates a new, empty Collection object so that 982 // a user can store it in their account storage. 983 // Once they have a Collection in their storage, they are able to receive 984 // Moments in transactions. 985 // 986 pub fun createEmptyCollection(): @NonFungibleToken.Collection { 987 return <-create TopShot.Collection() 988 } 989 990 // getAllPlays returns all the plays in topshot 991 // 992 // Returns: An array of all the plays that have been created 993 pub fun getAllPlays(): [TopShot.Play] { 994 return TopShot.playDatas.values 995 } 996 997 // getPlayMetaData returns all the metadata associated with a specific Play 998 // 999 // Parameters: playID: The id of the Play that is being searched 1000 // 1001 // Returns: The metadata as a String to String mapping optional 1002 pub fun getPlayMetaData(playID: UInt32): {String: String}? { 1003 return self.playDatas[playID]?.metadata 1004 } 1005 1006 // getPlayMetaDataByField returns the metadata associated with a 1007 // specific field of the metadata 1008 // Ex: field: "Team" will return something 1009 // like "Memphis Grizzlies" 1010 // 1011 // Parameters: playID: The id of the Play that is being searched 1012 // field: The field to search for 1013 // 1014 // Returns: The metadata field as a String Optional 1015 pub fun getPlayMetaDataByField(playID: UInt32, field: String): String? { 1016 // Don't force a revert if the playID or field is invalid 1017 if let play = TopShot.playDatas[playID] { 1018 return play.metadata[field] 1019 } else { 1020 return nil 1021 } 1022 } 1023 1024 // getSetData returns the data that the specified Set 1025 // is associated with. 1026 // 1027 // Parameters: setID: The id of the Set that is being searched 1028 // 1029 // Returns: The QuerySetData struct that has all the important information about the set 1030 pub fun getSetData(setID: UInt32): QuerySetData? { 1031 if TopShot.sets[setID] == nil { 1032 return nil 1033 } else { 1034 return QuerySetData(setID: setID) 1035 } 1036 } 1037 1038 // getSetName returns the name that the specified Set 1039 // is associated with. 1040 // 1041 // Parameters: setID: The id of the Set that is being searched 1042 // 1043 // Returns: The name of the Set 1044 pub fun getSetName(setID: UInt32): String? { 1045 // Don't force a revert if the setID is invalid 1046 return TopShot.setDatas[setID]?.name 1047 } 1048 1049 // getSetSeries returns the series that the specified Set 1050 // is associated with. 1051 // 1052 // Parameters: setID: The id of the Set that is being searched 1053 // 1054 // Returns: The series that the Set belongs to 1055 pub fun getSetSeries(setID: UInt32): UInt32? { 1056 // Don't force a revert if the setID is invalid 1057 return TopShot.setDatas[setID]?.series 1058 } 1059 1060 // getSetIDsByName returns the IDs that the specified Set name 1061 // is associated with. 1062 // 1063 // Parameters: setName: The name of the Set that is being searched 1064 // 1065 // Returns: An array of the IDs of the Set if it exists, or nil if doesn't 1066 pub fun getSetIDsByName(setName: String): [UInt32]? { 1067 var setIDs: [UInt32] = [] 1068 1069 // Iterate through all the setDatas and search for the name 1070 for setData in TopShot.setDatas.values { 1071 if setName == setData.name { 1072 // If the name is found, return the ID 1073 setIDs.append(setData.setID) 1074 } 1075 } 1076 1077 // If the name isn't found, return nil 1078 // Don't force a revert if the setName is invalid 1079 if setIDs.length == 0 { 1080 return nil 1081 } else { 1082 return setIDs 1083 } 1084 } 1085 1086 // getPlaysInSet returns the list of Play IDs that are in the Set 1087 // 1088 // Parameters: setID: The id of the Set that is being searched 1089 // 1090 // Returns: An array of Play IDs 1091 pub fun getPlaysInSet(setID: UInt32): [UInt32]? { 1092 // Don't force a revert if the setID is invalid 1093 return TopShot.sets[setID]?.plays 1094 } 1095 1096 // isEditionRetired returns a boolean that indicates if a Set/Play combo 1097 // (otherwise known as an edition) is retired. 1098 // If an edition is retired, it still remains in the Set, 1099 // but Moments can no longer be minted from it. 1100 // 1101 // Parameters: setID: The id of the Set that is being searched 1102 // playID: The id of the Play that is being searched 1103 // 1104 // Returns: Boolean indicating if the edition is retired or not 1105 pub fun isEditionRetired(setID: UInt32, playID: UInt32): Bool? { 1106 1107 if let setdata = self.getSetData(setID: setID) { 1108 1109 // See if the Play is retired from this Set 1110 let retired = setdata.getRetired()[playID] 1111 1112 // Return the retired status 1113 return retired 1114 } else { 1115 1116 // If the Set wasn't found, return nil 1117 return nil 1118 } 1119 } 1120 1121 // isSetLocked returns a boolean that indicates if a Set 1122 // is locked. If it's locked, 1123 // new Plays can no longer be added to it, 1124 // but Moments can still be minted from Plays the set contains. 1125 // 1126 // Parameters: setID: The id of the Set that is being searched 1127 // 1128 // Returns: Boolean indicating if the Set is locked or not 1129 pub fun isSetLocked(setID: UInt32): Bool? { 1130 // Don't force a revert if the setID is invalid 1131 return TopShot.sets[setID]?.locked 1132 } 1133 1134 // getNumMomentsInEdition return the number of Moments that have been 1135 // minted from a certain edition. 1136 // 1137 // Parameters: setID: The id of the Set that is being searched 1138 // playID: The id of the Play that is being searched 1139 // 1140 // Returns: The total number of Moments 1141 // that have been minted from an edition 1142 pub fun getNumMomentsInEdition(setID: UInt32, playID: UInt32): UInt32? { 1143 if let setdata = self.getSetData(setID: setID) { 1144 1145 // Read the numMintedPerPlay 1146 let amount = setdata.getNumberMintedPerPlay()[playID] 1147 1148 return amount 1149 } else { 1150 // If the set wasn't found return nil 1151 return nil 1152 } 1153 } 1154 1155 // ----------------------------------------------------------------------- 1156 // TopShot initialization function 1157 // ----------------------------------------------------------------------- 1158 // 1159 init() { 1160 // Initialize contract fields 1161 self.currentSeries = 0 1162 self.playDatas = {} 1163 self.setDatas = {} 1164 self.sets <- {} 1165 self.nextPlayID = 1 1166 self.nextSetID = 1 1167 self.totalSupply = 0 1168 1169 // Put a new Collection in storage 1170 self.account.save<@Collection>(<- create Collection(), to: /storage/MomentCollection) 1171 1172 // Create a public capability for the Collection 1173 self.account.link<&{MomentCollectionPublic}>(/public/MomentCollection, target: /storage/MomentCollection) 1174 1175 // Put the Minter in storage 1176 self.account.save<@Admin>(<- create Admin(), to: /storage/TopShotAdmin) 1177 1178 emit ContractInitialized() 1179 } 1180 }