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  }