Smart Contract

TopShot

A.0b2a3299cc857e29.TopShot

Valid From

87,722,633

Deployed

2w ago
Feb 11, 2026, 05:25:39 PM UTC

Dependents

5968382 imports
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
46import FungibleToken from 0xf233dcee88fe0abe
47import NonFungibleToken from 0x1d7e57aa55817448
48import MetadataViews from 0x1d7e57aa55817448
49import TopShotLocking from 0x0b2a3299cc857e29
50import ViewResolver from 0x1d7e57aa55817448
51import CrossVMMetadataViews from 0x1d7e57aa55817448
52import EVM from 0xe467b9dd11fa00df
53
54access(all) contract TopShot: NonFungibleToken {
55    // -----------------------------------------------------------------------
56    // TopShot deployment variables
57    // -----------------------------------------------------------------------
58
59    // The network the contract is deployed on
60    access(all) view fun Network(): String { return "mainnet" }
61
62    // The address to which royalties should be deposited
63    access(all) view fun RoyaltyAddress(): Address { return 0xfaf0cc52c6e3acaf }
64
65    // The path to the Subedition Admin resource belonging to the Account
66    // which the contract is deployed on
67    access(all) view fun SubeditionAdminStoragePath(): StoragePath { return /storage/TopShotSubeditionAdmin}
68
69    // -----------------------------------------------------------------------
70    // TopShot contract Events
71    // -----------------------------------------------------------------------
72
73    // Emitted when a new Play struct is created
74    access(all) event PlayCreated(id: UInt32, metadata: {String: String})
75    // Emitted when a new series has been triggered by an admin
76    access(all) event NewSeriesStarted(newCurrentSeries: UInt32)
77
78    // Events for Set-Related actions
79    //
80    // Emitted when a new Set is created
81    access(all) event SetCreated(setID: UInt32, series: UInt32)
82    // Emitted when a new Play is added to a Set
83    access(all) event PlayAddedToSet(setID: UInt32, playID: UInt32)
84    // Emitted when a Play is retired from a Set and cannot be used to mint
85    access(all) event PlayRetiredFromSet(setID: UInt32, playID: UInt32, numMoments: UInt32)
86    // Emitted when a Set is locked, meaning Plays cannot be added
87    access(all) event SetLocked(setID: UInt32)
88    // Emitted when a Moment is minted from a Set
89    access(all) event MomentMinted(momentID: UInt64, playID: UInt32, setID: UInt32, serialNumber: UInt32, subeditionID: UInt32)
90
91    // Events for Collection-related actions
92    //
93    // Emitted when a moment is withdrawn from a Collection
94    access(all) event Withdraw(id: UInt64, from: Address?)
95    // Emitted when a moment is deposited into a Collection
96    access(all) event Deposit(id: UInt64, to: Address?)
97
98    // Emitted when a Moment is destroyed
99    access(all) event MomentDestroyed(id: UInt64)
100
101    // Emitted when a Subedition is created
102    access(all) event SubeditionCreated(subeditionID: UInt32, name: String, metadata: {String: String})
103
104    // Emitted when a Subedition is linked to the specific Moment
105    access(all) event SubeditionAddedToMoment(momentID: UInt64, subeditionID: UInt32, setID: UInt32, playID: UInt32)
106
107    // -----------------------------------------------------------------------
108    // TopShot contract-level fields.
109    // These contain actual values that are stored in the smart contract.
110    // -----------------------------------------------------------------------
111
112    // Series that this Set belongs to.
113    // Series is a concept that indicates a group of Sets through time.
114    // Many Sets can exist at a time, but only one series.
115    access(all) var currentSeries: UInt32
116
117    // Variable size dictionary of Play structs
118    access(self) var playDatas: {UInt32: Play}
119
120    // Variable size dictionary of SetData structs
121    access(self) var setDatas: {UInt32: SetData}
122
123    // Variable size dictionary of Set resources
124    access(self) var sets: @{UInt32: Set}
125
126    // The ID that is used to create Plays.
127    // Every time a Play is created, playID is assigned
128    // to the new Play's ID and then is incremented by 1.
129    access(all) var nextPlayID: UInt32
130
131    // The ID that is used to create Sets. Every time a Set is created
132    // setID is assigned to the new set's ID and then is incremented by 1.
133    access(all) var nextSetID: UInt32
134
135    // The total number of Top shot Moment NFTs that have been created
136    // Because NFTs can be destroyed, it doesn't necessarily mean that this
137    // reflects the total number of NFTs in existence, just the number that
138    // have been minted to date. Also used as global moment IDs for minting.
139    access(all) var totalSupply: UInt64
140
141    // -----------------------------------------------------------------------
142    // TopShot contract-level Composite Type definitions
143    // -----------------------------------------------------------------------
144    // These are just *definitions* for Types that this contract
145    // and other accounts can use. These definitions do not contain
146    // actual stored values, but an instance (or object) of one of these Types
147    // can be created by this contract that contains stored values.
148    // -----------------------------------------------------------------------
149
150    // Play is a Struct that holds metadata associated
151    // with a specific NBA play, like the legendary moment when
152    // Ray Allen hit the 3 to tie the Heat and Spurs in the 2013 finals game 6
153    // or when Lance Stephenson blew in the ear of Lebron James.
154    //
155    // Moment NFTs will all reference a single play as the owner of
156    // its metadata. The plays are publicly accessible, so anyone can
157    // read the metadata associated with a specific play ID
158    //
159    access(all) struct Play {
160        // The unique ID for the Play
161        access(all) let playID: UInt32
162
163        // Stores all the metadata about the play as a string mapping
164        // This is not the long term way NFT metadata will be stored. It's a temporary
165        // construct while we figure out a better way to do metadata.
166        //
167        access(all) let metadata: {String: String}
168
169        init(metadata: {String: String}) {
170            pre {
171                metadata.length != 0: "New Play metadata cannot be empty"
172            }
173            self.playID = TopShot.nextPlayID
174            self.metadata = metadata
175        }
176
177        /// This function is intended to backfill the Play on blockchain with a more detailed
178        /// description of the Play. The benefit of having the description is that anyone would
179        /// be able to know the story of the Play directly from Flow
180        access(contract) fun updateTagline(tagline: String): UInt32 {
181            self.metadata["Tagline"] = tagline
182
183            TopShot.playDatas[self.playID] = self
184            return self.playID
185        }
186    }
187
188    // A Set is a grouping of Plays that have occured in the real world
189    // that make up a related group of collectibles, like sets of baseball
190    // or Magic cards. A Play can exist in multiple different sets.
191    //
192    // SetData is a struct that is stored in a field of the contract.
193    // Anyone can query the constant information
194    // about a set by calling various getters located
195    // at the end of the contract. Only the admin has the ability
196    // to modify any data in the private Set resource.
197    //
198    access(all) struct SetData {
199        // Unique ID for the Set
200        access(all) let setID: UInt32
201
202        // Name of the Set
203        // ex. "Times when the Toronto Raptors choked in the playoffs"
204        access(all) let name: String
205
206        // Series that this Set belongs to.
207        // Series is a concept that indicates a group of Sets through time.
208        // Many Sets can exist at a time, but only one series.
209        access(all) let series: UInt32
210
211        init(name: String) {
212            pre {
213                name.length > 0: "New Set name cannot be empty"
214            }
215            self.setID = TopShot.nextSetID
216            self.name = name
217            self.series = TopShot.currentSeries
218        }
219    }
220
221    // Set is a resource type that contains the functions to add and remove
222    // Plays from a set and mint Moments.
223    //
224    // It is stored in a private field in the contract so that
225    // the admin resource can call its methods.
226    //
227    // The admin can add Plays to a Set so that the set can mint Moments
228    // that reference that playdata.
229    // The Moments that are minted by a Set will be listed as belonging to
230    // the Set that minted it, as well as the Play it references.
231    //
232    // Admin can also retire Plays from the Set, meaning that the retired
233    // Play can no longer have Moments minted from it.
234    //
235    // If the admin locks the Set, no more Plays can be added to it, but
236    // Moments can still be minted.
237    //
238    // If retireAll() and lock() are called back-to-back,
239    // the Set is closed off forever and nothing more can be done with it.
240    access(all) resource Set {
241        // Unique ID for the set
242        access(all) let setID: UInt32
243
244        // Array of plays that are a part of this set.
245        // When a play is added to the set, its ID gets appended here.
246        // The ID does not get removed from this array when a Play is retired.
247        access(contract) var plays: [UInt32]
248
249        // Map of Play IDs that Indicates if a Play in this Set can be minted.
250        // When a Play is added to a Set, it is mapped to false (not retired).
251        // When a Play is retired, this is set to true and cannot be changed.
252        access(contract) var retired: {UInt32: Bool}
253
254        // Indicates if the Set is currently locked.
255        // When a Set is created, it is unlocked
256        // and Plays are allowed to be added to it.
257        // When a set is locked, Plays cannot be added.
258        // A Set can never be changed from locked to unlocked,
259        // the decision to lock a Set it is final.
260        // If a Set is locked, Plays cannot be added, but
261        // Moments can still be minted from Plays
262        // that exist in the Set.
263        access(all) var locked: Bool
264
265        // Mapping of Play IDs that indicates the number of Moments
266        // that have been minted for specific Plays in this Set.
267        // When a Moment is minted, this value is stored in the Moment to
268        // show its place in the Set, eg. 13 of 60.
269        access(contract) var numberMintedPerPlay: {UInt32: UInt32}
270
271        init(name: String) {
272            self.setID = TopShot.nextSetID
273            self.plays = []
274            self.retired = {}
275            self.locked = false
276            self.numberMintedPerPlay = {}
277
278            // Create a new SetData for this Set and store it in contract storage
279            TopShot.setDatas[self.setID] = SetData(name: name)
280        }
281
282        // addPlay adds a play to the set
283        //
284        // Parameters: playID: The ID of the Play that is being added
285        //
286        // Pre-Conditions:
287        // The Play needs to be an existing play
288        // The Set needs to be not locked
289        // The Play can't have already been added to the Set
290        //
291        access(all) fun addPlay(playID: UInt32) {
292            pre {
293                TopShot.playDatas[playID] != nil: "Cannot add the Play to Set: Play doesn't exist."
294                !self.locked: "Cannot add the play to the Set after the set has been locked."
295                self.numberMintedPerPlay[playID] == nil: "The play has already beed added to the set."
296            }
297
298            // Add the Play to the array of Plays
299            self.plays.append(playID)
300
301            // Open the Play up for minting
302            self.retired[playID] = false
303
304            // Initialize the Moment count to zero
305            self.numberMintedPerPlay[playID] = 0
306
307            emit PlayAddedToSet(setID: self.setID, playID: playID)
308        }
309
310        // addPlays adds multiple Plays to the Set
311        //
312        // Parameters: playIDs: The IDs of the Plays that are being added
313        //                      as an array
314        //
315        access(all) fun addPlays(playIDs: [UInt32]) {
316            for play in playIDs {
317                self.addPlay(playID: play)
318            }
319        }
320
321        // retirePlay retires a Play from the Set so that it can't mint new Moments
322        //
323        // Parameters: playID: The ID of the Play that is being retired
324        //
325        // Pre-Conditions:
326        // The Play is part of the Set and not retired (available for minting).
327        //
328        access(all) fun retirePlay(playID: UInt32) {
329            pre {
330                self.retired[playID] != nil: "Cannot retire the Play: Play doesn't exist in this set!"
331            }
332
333            if !self.retired[playID]! {
334                self.retired[playID] = true
335
336                emit PlayRetiredFromSet(setID: self.setID, playID: playID, numMoments: self.numberMintedPerPlay[playID]!)
337            }
338        }
339
340        // retireAll retires all the plays in the Set
341        // Afterwards, none of the retired Plays will be able to mint new Moments
342        //
343        access(all) fun retireAll() {
344            for play in self.plays {
345                self.retirePlay(playID: play)
346            }
347        }
348
349        // lock() locks the Set so that no more Plays can be added to it
350        //
351        // Pre-Conditions:
352        // The Set should not be locked
353        access(all) fun lock() {
354            if !self.locked {
355                self.locked = true
356                emit SetLocked(setID: self.setID)
357            }
358        }
359
360        // mintMoment mints a new Moment and returns the newly minted Moment
361        //
362        // Parameters: playID: The ID of the Play that the Moment references
363        //
364        // Pre-Conditions:
365        // The Play must exist in the Set and be allowed to mint new Moments
366        //
367        // Returns: The NFT that was minted
368        //
369        access(all) fun mintMoment(playID: UInt32): @NFT {
370            pre {
371                self.retired[playID] != nil: "Cannot mint the moment: This play doesn't exist."
372                !self.retired[playID]!: "Cannot mint the moment from this play: This play has been retired."
373            }
374
375            // Gets the number of Moments that have been minted for this Play
376            // to use as this Moment's serial number
377            let numInPlay = self.numberMintedPerPlay[playID]!
378
379            // Mint the new moment
380            let newMoment: @NFT <- create NFT(
381                serialNumber: numInPlay + UInt32(1),
382                playID: playID,
383                setID: self.setID,
384                subeditionID: 0
385            )
386
387            // Increment the count of Moments minted for this Play
388            self.numberMintedPerPlay[playID] = numInPlay + UInt32(1)
389
390            return <-newMoment
391        }
392
393        // batchMintMoment mints an arbitrary quantity of Moments
394        // and returns them as a Collection
395        //
396        // Parameters: playID: the ID of the Play that the Moments are minted for
397        //             quantity: The quantity of Moments to be minted
398        //
399        // Returns: Collection object that contains all the Moments that were minted
400        //
401        access(all) fun batchMintMoment(playID: UInt32, quantity: UInt64): @Collection {
402            let newCollection <- create Collection()
403
404            var i: UInt64 = 0
405            while i < quantity {
406                newCollection.deposit(token: <-self.mintMoment(playID: playID))
407                i = i + UInt64(1)
408            }
409
410            return <- newCollection
411        }
412
413        // mintMomentWithSubedition mints a new Moment with subedition and returns the newly minted Moment
414        //
415        // Parameters: playID: The ID of the Play that the Moment references
416        //             subeditionID: The ID of the subedition within Edition that the Moment references
417        //
418        // Pre-Conditions:
419        // The Play must exist in the Set and be allowed to mint new Moments
420        //
421        // Returns: The NFT that was minted
422        //
423        access(all) fun mintMomentWithSubedition(playID: UInt32, subeditionID: UInt32): @NFT {
424            pre {
425                self.retired[playID] != nil: "Cannot mint the moment: This play doesn't exist."
426                !self.retired[playID]!: "Cannot mint the moment from this play: This play has been retired."
427            }
428
429            // Gets the number of Moments that have been minted for this subedition
430            // to use as this Moment's serial number
431            let subeditionRef = TopShot.account.storage.borrow<&SubeditionAdmin>(from: TopShot.SubeditionAdminStoragePath())
432                ?? panic("No subedition admin resource in storage")
433
434            let numInSubedition = subeditionRef.getNumberMintedPerSubedition(
435                setID: self.setID,
436                playID: playID,
437                subeditionID: subeditionID
438            )
439
440            // Mint the new moment
441            let newMoment: @NFT <- create NFT(
442                serialNumber: numInSubedition + UInt32(1),
443                playID: playID,
444                setID: self.setID,
445                subeditionID: subeditionID
446            )
447
448            // Increment the count of Moments minted for this subedition
449            subeditionRef.addToNumberMintedPerSubedition(
450                setID: self.setID,
451                playID: playID,
452                subeditionID: subeditionID
453            )
454
455            subeditionRef.setMomentsSubedition(nftID: newMoment.id, subeditionID: subeditionID, setID: self.setID, playID: playID)
456
457            self.numberMintedPerPlay[playID] = self.numberMintedPerPlay[playID]! + UInt32(1)
458
459            return <- newMoment
460        }
461
462        // batchMintMomentWithSubedition mints an arbitrary quantity of Moments with subedition
463        // and returns them as a Collection
464        //
465        // Parameters: playID: the ID of the Play that the Moments are minted for
466        //             quantity: The quantity of Moments to be minted
467        //             subeditionID: The ID of the subedition within Edition that the Moments references
468        //
469        // Returns: Collection object that contains all the Moments that were minted
470        //
471        access(all) fun batchMintMomentWithSubedition(playID: UInt32, quantity: UInt64, subeditionID: UInt32): @Collection {
472            let newCollection <- create Collection()
473
474            var i: UInt64 = 0
475            while i < quantity {
476                newCollection.deposit(token: <-self.mintMomentWithSubedition(playID: playID, subeditionID: subeditionID))
477                i = i + UInt64(1)
478            }
479
480            return <-newCollection
481        }
482
483        access(all) view fun getPlays(): [UInt32] {
484            return self.plays
485        }
486
487        access(all) view fun getRetired(): {UInt32: Bool} {
488            return self.retired
489        }
490
491        access(all) view fun getNumMintedPerPlay(): {UInt32: UInt32} {
492            return self.numberMintedPerPlay
493        }
494    }
495
496    // Struct that contains all of the important data about a set
497    // Can be easily queried by instantiating the `QuerySetData` object
498    // with the desired set ID
499    // let setData = TopShot.QuerySetData(setID: 12)
500    //
501    access(all) struct QuerySetData {
502        access(all) let setID: UInt32
503        access(all) let name: String
504        access(all) let series: UInt32
505        access(self) var plays: [UInt32]
506        access(self) var retired: {UInt32: Bool}
507        access(all) var locked: Bool
508        access(self) var numberMintedPerPlay: {UInt32: UInt32}
509
510        init(setID: UInt32) {
511            pre {
512                TopShot.sets[setID] != nil: "The set with the provided ID does not exist"
513            }
514
515            let set = (&TopShot.sets[setID] as &Set?)!
516            let setData = TopShot.setDatas[setID]!
517
518            self.setID = setID
519            self.name = setData.name
520            self.series = setData.series
521            self.plays = set.getPlays()
522            self.retired = set.getRetired()
523            self.locked = set.locked
524            self.numberMintedPerPlay = set.getNumMintedPerPlay()
525        }
526
527        access(all) view fun getPlays(): [UInt32] {
528            return self.plays
529        }
530
531        access(all) view fun getRetired(): {UInt32: Bool} {
532            return self.retired
533        }
534
535        access(all) view fun getNumberMintedPerPlay(): {UInt32: UInt32} {
536            return self.numberMintedPerPlay
537        }
538    }
539
540    access(all) struct MomentData {
541        // The ID of the Set that the Moment comes from
542        access(all) let setID: UInt32
543
544        // The ID of the Play that the Moment references
545        access(all) let playID: UInt32
546
547        // The place in the edition that this Moment was minted
548        // Otherwise know as the serial number
549        access(all) let serialNumber: UInt32
550
551        init(setID: UInt32, playID: UInt32, serialNumber: UInt32) {
552            self.setID = setID
553            self.playID = playID
554            self.serialNumber = serialNumber
555        }
556    }
557
558    // This is an implementation of a custom metadata view for Top Shot.
559    // This view contains the play metadata.
560    //
561    access(all) struct TopShotMomentMetadataView {
562        access(all) let fullName: String?
563        access(all) let firstName: String?
564        access(all) let lastName: String?
565        access(all) let birthdate: String?
566        access(all) let birthplace: String?
567        access(all) let jerseyNumber: String?
568        access(all) let draftTeam: String?
569        access(all) let draftYear: String?
570        access(all) let draftSelection: String?
571        access(all) let draftRound: String?
572        access(all) let teamAtMomentNBAID: String?
573        access(all) let teamAtMoment: String?
574        access(all) let primaryPosition: String?
575        access(all) let height: String?
576        access(all) let weight: String?
577        access(all) let totalYearsExperience: String?
578        access(all) let nbaSeason: String?
579        access(all) let dateOfMoment: String?
580        access(all) let playCategory: String?
581        access(all) let playType: String?
582        access(all) let homeTeamName: String?
583        access(all) let awayTeamName: String?
584        access(all) let homeTeamScore: String?
585        access(all) let awayTeamScore: String?
586        access(all) let seriesNumber: UInt32?
587        access(all) let setName: String?
588        access(all) let serialNumber: UInt32
589        access(all) let playID: UInt32
590        access(all) let setID: UInt32
591        access(all) let numMomentsInEdition: UInt32?
592
593        init(
594            fullName: String?,
595            firstName: String?,
596            lastName: String?,
597            birthdate: String?,
598            birthplace: String?,
599            jerseyNumber: String?,
600            draftTeam: String?,
601            draftYear: String?,
602            draftSelection: String?,
603            draftRound: String?,
604            teamAtMomentNBAID: String?,
605            teamAtMoment: String?,
606            primaryPosition: String?,
607            height: String?,
608            weight: String?,
609            totalYearsExperience: String?,
610            nbaSeason: String?,
611            dateOfMoment: String?,
612            playCategory: String?,
613            playType: String?,
614            homeTeamName: String?,
615            awayTeamName: String?,
616            homeTeamScore: String?,
617            awayTeamScore: String?,
618            seriesNumber: UInt32?,
619            setName: String?,
620            serialNumber: UInt32,
621            playID: UInt32,
622            setID: UInt32,
623            numMomentsInEdition: UInt32?
624        ) {
625            self.fullName = fullName
626            self.firstName = firstName
627            self.lastName = lastName
628            self.birthdate = birthdate
629            self.birthplace = birthplace
630            self.jerseyNumber = jerseyNumber
631            self.draftTeam = draftTeam
632            self.draftYear = draftYear
633            self.draftSelection = draftSelection
634            self.draftRound = draftRound
635            self.teamAtMomentNBAID = teamAtMomentNBAID
636            self.teamAtMoment = teamAtMoment
637            self.primaryPosition = primaryPosition
638            self.height = height
639            self.weight = weight
640            self.totalYearsExperience = totalYearsExperience
641            self.nbaSeason = nbaSeason
642            self.dateOfMoment= dateOfMoment
643            self.playCategory = playCategory
644            self.playType = playType
645            self.homeTeamName = homeTeamName
646            self.awayTeamName = awayTeamName
647            self.homeTeamScore = homeTeamScore
648            self.awayTeamScore = awayTeamScore
649            self.seriesNumber = seriesNumber
650            self.setName = setName
651            self.serialNumber = serialNumber
652            self.playID = playID
653            self.setID = setID
654            self.numMomentsInEdition = numMomentsInEdition
655        }
656    }
657
658    // The resource that represents the Moment NFTs
659    //
660    access(all) resource NFT: NonFungibleToken.NFT {
661        // Global unique moment ID
662        access(all) let id: UInt64
663
664        // Struct of Moment metadata
665        access(all) let data: MomentData
666
667        init(serialNumber: UInt32, playID: UInt32, setID: UInt32, subeditionID: UInt32) {
668            // Increment the global Moment IDs
669            TopShot.totalSupply = TopShot.totalSupply + UInt64(1)
670
671            self.id = TopShot.totalSupply
672
673            // Set the metadata struct
674            self.data = MomentData(setID: setID, playID: playID, serialNumber: serialNumber)
675
676            emit MomentMinted(
677                momentID: self.id,
678                playID: playID,
679                setID: self.data.setID,
680                serialNumber: self.data.serialNumber,
681                subeditionID: subeditionID
682            )
683        }
684
685        // If the Moment is destroyed, emit an event to indicate
686        // to outside observers that it has been destroyed
687        access(all) event ResourceDestroyed(
688            id: UInt64 = self.id,
689            serialNumber: UInt32 =  self.data.serialNumber,
690            playID: UInt32 =  self.data.playID,
691            setID: UInt32 = self.data.setID
692        )
693
694        access(all) view fun name(): String {
695            let fullName: String = TopShot.getPlayMetaDataByField(playID: self.data.playID, field: "FullName") ?? ""
696            let playType: String = TopShot.getPlayMetaDataByField(playID: self.data.playID, field: "PlayType") ?? ""
697            return fullName
698                .concat(" ")
699                .concat(playType)
700        }
701
702        // The description of the Moment.
703        // If the Tagline prop exists, use is as the description; else, build the description using set, series, and serial number.
704        access(all) view fun description(): String {
705            // Return early if the tagline is non-empty
706            if let tagline = TopShot.getPlayMetaDataByField(playID: self.data.playID, field: "Tagline") {
707                return tagline
708            }
709
710            // Build the description using set name, series number, and serial number
711            let setName: String = TopShot.getSetName(setID: self.data.setID) ?? ""
712            let serialNumber: String = self.data.serialNumber.toString()
713            let seriesNumber: String = TopShot.getSetSeries(setID: self.data.setID)?.toString() ?? ""
714            return "A series "
715                .concat(seriesNumber)
716                .concat(" ")
717                .concat(setName)
718                .concat(" moment with serial number ")
719                .concat(serialNumber)
720        }
721
722        // All supported metadata views for the Moment including the Core NFT Views
723        access(all) view fun getViews(): [Type] {
724            return [
725                Type<MetadataViews.Display>(),
726                Type<TopShotMomentMetadataView>(),
727                Type<MetadataViews.Royalties>(),
728                Type<MetadataViews.Editions>(),
729                Type<MetadataViews.ExternalURL>(),
730                Type<MetadataViews.NFTCollectionData>(),
731                Type<MetadataViews.NFTCollectionDisplay>(),
732                Type<CrossVMMetadataViews.EVMPointer>(),
733                Type<MetadataViews.EVMBridgedMetadata>(),
734                Type<MetadataViews.Serial>(),
735                Type<MetadataViews.Traits>(),
736                Type<MetadataViews.Medias>()
737            ]
738        }
739
740        // resolves the view with the given type for the NFT
741        access(all) fun resolveView(_ view: Type): AnyStruct? {
742            switch view {
743                case Type<MetadataViews.Display>():
744                    return MetadataViews.Display(
745                        name: self.name(),
746                        description: self.description(),
747                        thumbnail: MetadataViews.HTTPFile(url: self.thumbnail())
748                    )
749                // Custom metadata view unique to TopShot Moments
750                case Type<TopShotMomentMetadataView>():
751                    return TopShotMomentMetadataView(
752                        fullName: TopShot.getPlayMetaDataByField(playID: self.data.playID, field: "FullName"),
753                        firstName: TopShot.getPlayMetaDataByField(playID: self.data.playID, field: "FirstName"),
754                        lastName: TopShot.getPlayMetaDataByField(playID: self.data.playID, field: "LastName"),
755                        birthdate: TopShot.getPlayMetaDataByField(playID: self.data.playID, field: "Birthdate"),
756                        birthplace: TopShot.getPlayMetaDataByField(playID: self.data.playID, field: "Birthplace"),
757                        jerseyNumber: TopShot.getPlayMetaDataByField(playID: self.data.playID, field: "JerseyNumber"),
758                        draftTeam: TopShot.getPlayMetaDataByField(playID: self.data.playID, field: "DraftTeam"),
759                        draftYear: TopShot.getPlayMetaDataByField(playID: self.data.playID, field: "DraftYear"),
760                        draftSelection: TopShot.getPlayMetaDataByField(playID: self.data.playID, field: "DraftSelection"),
761                        draftRound: TopShot.getPlayMetaDataByField(playID: self.data.playID, field: "DraftRound"),
762                        teamAtMomentNBAID: TopShot.getPlayMetaDataByField(playID: self.data.playID, field: "TeamAtMomentNBAID"),
763                        teamAtMoment: TopShot.getPlayMetaDataByField(playID: self.data.playID, field: "TeamAtMoment"),
764                        primaryPosition: TopShot.getPlayMetaDataByField(playID: self.data.playID, field: "PrimaryPosition"),
765                        height: TopShot.getPlayMetaDataByField(playID: self.data.playID, field: "Height"),
766                        weight: TopShot.getPlayMetaDataByField(playID: self.data.playID, field: "Weight"),
767                        totalYearsExperience: TopShot.getPlayMetaDataByField(playID: self.data.playID, field: "TotalYearsExperience"),
768                        nbaSeason: TopShot.getPlayMetaDataByField(playID: self.data.playID, field: "NbaSeason"),
769                        dateOfMoment: TopShot.getPlayMetaDataByField(playID: self.data.playID, field: "DateOfMoment"),
770                        playCategory: TopShot.getPlayMetaDataByField(playID: self.data.playID, field: "PlayCategory"),
771                        playType: TopShot.getPlayMetaDataByField(playID: self.data.playID, field: "PlayType"),
772                        homeTeamName: TopShot.getPlayMetaDataByField(playID: self.data.playID, field: "HomeTeamName"),
773                        awayTeamName: TopShot.getPlayMetaDataByField(playID: self.data.playID, field: "AwayTeamName"),
774                        homeTeamScore: TopShot.getPlayMetaDataByField(playID: self.data.playID, field: "HomeTeamScore"),
775                        awayTeamScore: TopShot.getPlayMetaDataByField(playID: self.data.playID, field: "AwayTeamScore"),
776                        seriesNumber: TopShot.getSetSeries(setID: self.data.setID),
777                        setName: TopShot.getSetName(setID: self.data.setID),
778                        serialNumber: self.data.serialNumber,
779                        playID: self.data.playID,
780                        setID: self.data.setID,
781                        numMomentsInEdition: TopShot.getNumMomentsInEdition(setID: self.data.setID, playID: self.data.playID)
782                    )
783                case Type<MetadataViews.Editions>():
784                    let name = self.getEditionName()
785                    let max = TopShot.getNumMomentsInEdition(setID: self.data.setID, playID: self.data.playID) ?? 0
786                    let editionInfo = MetadataViews.Edition(name: name, number: UInt64(self.data.serialNumber), max: max > 0 ? UInt64(max) : nil)
787                    let editionList: [MetadataViews.Edition] = [editionInfo]
788                    return MetadataViews.Editions(
789                        editionList
790                    )
791                case Type<MetadataViews.Serial>():
792                    return MetadataViews.Serial(
793                        UInt64(self.data.serialNumber)
794                    )
795                case Type<MetadataViews.Royalties>():
796                    return TopShot.resolveContractView(resourceType: nil, viewType: Type<MetadataViews.Royalties>())
797                case Type<MetadataViews.ExternalURL>():
798                    return MetadataViews.ExternalURL(self.getMomentURL())
799                case Type<MetadataViews.NFTCollectionData>():
800                    return TopShot.resolveContractView(resourceType: nil, viewType: Type<MetadataViews.NFTCollectionData>())
801                case Type<MetadataViews.NFTCollectionDisplay>():
802                    return TopShot.resolveContractView(resourceType: nil, viewType: Type<MetadataViews.NFTCollectionDisplay>())
803                case Type<CrossVMMetadataViews.EVMPointer>():
804                    return TopShot.resolveContractView(resourceType: nil, viewType: Type<CrossVMMetadataViews.EVMPointer>())
805                case Type<MetadataViews.EVMBridgedMetadata>():
806                    // Project-defined ERC721 EVM contract stores baseURI, name, and symbol in its own contract storage
807                    // Name, symbol, and baseURI below are only used for legacy bridge-deployed ERC721 contract
808                    return MetadataViews.EVMBridgedMetadata(
809                        name: "NBA Top Shot",
810                        symbol: "NBAT",
811                        uri: MetadataViews.URI(
812                            baseURI: "https://metadata-api.production.studio-platform.dapperlabs.com/v1/topshot/moment/",
813                            value: self.id.toString()
814                        )
815                    )
816                case Type<MetadataViews.Traits>():
817                    return self.resolveTraitsView()
818                case Type<MetadataViews.Medias>():
819                    return MetadataViews.Medias(
820                        [
821                            MetadataViews.Media(
822                                file: MetadataViews.HTTPFile(
823                                    url: self.mediumimage()
824                                ),
825                                mediaType: "image/jpeg"
826                            ),
827                            MetadataViews.Media(
828                                file: MetadataViews.HTTPFile(
829                                    url: self.video()
830                                ),
831                                mediaType: "video/mp4"
832                            )
833                        ]
834                    )
835            }
836            return nil
837        }
838
839        // resolves this NFT's Traits view
840        access(all) fun resolveTraitsView(): MetadataViews.Traits {
841            // sports radar team id
842            let excludedNames: [String] = ["TeamAtMomentNBAID"]
843
844            // Get subedition
845            let subedition = TopShot.getSubeditionByNFTID(self.id)
846
847            // Create a dictionary of this NFT's traits with default metadata
848            var traits: {String: AnyStruct} = {
849                "SeriesNumber": TopShot.getSetSeries(setID: self.data.setID),
850                "SetName": TopShot.getSetName(setID: self.data.setID),
851                "SerialNumber": self.data.serialNumber,
852                "Locked": TopShotLocking.isLocked(nftRef: &self as &{NonFungibleToken.NFT}),
853                "Subedition": subedition?.name ?? "Standard",
854                "SubeditionID": subedition?.subeditionID ?? 0
855            }
856
857            // Add play specific data
858            traits = self.mapPlayData(dict: traits)
859
860            return MetadataViews.dictToTraits(dict: traits, excludedNames: excludedNames)
861        }
862
863        // Functions used for computing MetadataViews
864
865        // mapPlayData helps build our trait map from play metadata
866        // Returns: The trait map with all non-empty fields from play data added
867        access(all) fun mapPlayData(dict: {String: AnyStruct}) : {String: AnyStruct} {
868            let playMetadata = TopShot.getPlayMetaData(playID: self.data.playID) ?? {}
869            for name in playMetadata.keys {
870                let value = playMetadata[name] ?? ""
871                if value != "" {
872                    dict.insert(key: name, value)
873                }
874            }
875            return dict
876        }
877
878        // getMomentURL
879        // Returns: The computed external url of the moment
880        access(all) view fun getMomentURL(): String {
881            return "https://nbatopshot.com/moment/".concat(self.id.toString())
882        }
883
884        // getEditionName Moment's edition name is a combination of the Moment's setName and playID
885        // `setName: #playID`
886        access(all) view fun getEditionName(): String {
887            let setName: String = TopShot.getSetName(setID: self.data.setID) ?? ""
888            let editionName = setName.concat(": #").concat(self.data.playID.toString())
889            return editionName
890        }
891
892        access(all) view fun assetPath(): String {
893            return "https://assets.nbatopshot.com/media/".concat(self.id.toString())
894        }
895
896        // returns a url to display an medium sized image
897        access(all) view fun mediumimage(): String {
898            return self.appendOptionalParams(url: self.assetPath().concat("?width=512"), firstDelim: "&")
899        }
900
901        // a url to display a thumbnail associated with the moment
902        access(all) view fun thumbnail(): String {
903            return self.appendOptionalParams(url: self.assetPath().concat("?width=256"), firstDelim: "&")
904        }
905
906        // a url to display a video associated with the moment
907        access(all) view fun video(): String {
908            return self.appendOptionalParams(url: self.assetPath().concat("/video"), firstDelim: "?")
909        }
910
911        // appends and optional network param needed to resolve the media
912        access(all) view fun appendOptionalParams(url: String, firstDelim: String): String {
913            if TopShot.Network() == "testnet" {
914                return url.concat(firstDelim).concat("testnet")
915            }
916            return url
917        }
918
919        // Create an empty Collection for TopShot NFTs and return it to the caller
920        access(all) fun createEmptyCollection(): @{NonFungibleToken.Collection} {
921            return <- TopShot.createEmptyCollection(nftType: Type<@NFT>())
922        }
923    }
924
925    // Admin is a special authorization resource that
926    // allows the owner to perform important functions to modify the
927    // various aspects of the Plays, Sets, and Moments
928    //
929    access(all) resource Admin {
930        // createPlay creates a new Play struct
931        // and stores it in the Plays dictionary in the TopShot smart contract
932        //
933        // Parameters: metadata: A dictionary mapping metadata titles to their data
934        //                       example: {"Player Name": "Kevin Durant", "Height": "7 feet"}
935        //                               (because we all know Kevin Durant is not 6'9")
936        //
937        // Returns: the ID of the new Play object
938        //
939        access(all) fun createPlay(metadata: {String: String}): UInt32 {
940            // Create the new Play
941            var newPlay = Play(metadata: metadata)
942            let newID = newPlay.playID
943
944            // Increment the ID so that it isn't used again
945            TopShot.nextPlayID = TopShot.nextPlayID + UInt32(1)
946
947            emit PlayCreated(id: newPlay.playID, metadata: metadata)
948
949            // Store it in the contract storage
950            TopShot.playDatas[newID] = newPlay
951
952            return newID
953        }
954
955        /// Temporarily enabled so the description of the play can be backfilled
956        /// Parameters: playID: The ID of the play to update
957        ///             tagline: A string to be used as the tagline for the play
958        /// Returns: The ID of the play
959        access(all) fun updatePlayTagline(playID: UInt32, tagline: String): UInt32 {
960            let tmpPlay = TopShot.playDatas[playID]
961                ?? panic("playID does not exist")
962
963            // Update the play's tagline
964            tmpPlay.updateTagline(tagline: tagline)
965
966            // Return the play's ID
967            return playID
968        }
969
970        // createSet creates a new Set resource and stores it
971        // in the sets mapping in the TopShot contract
972        //
973        // Parameters: name: The name of the Set
974        //
975        // Returns: The ID of the created set
976        access(all) fun createSet(name: String): UInt32 {
977            // Create the new Set
978            var newSet <- create Set(name: name)
979
980            // Increment the setID so that it isn't used again
981            TopShot.nextSetID = TopShot.nextSetID + UInt32(1)
982
983            let newID = newSet.setID
984
985            emit SetCreated(setID: newSet.setID, series: TopShot.currentSeries)
986
987            // Store it in the sets mapping field
988            TopShot.sets[newID] <-! newSet
989
990            return newID
991        }
992
993        // borrowSet returns a reference to a set in the TopShot
994        // contract so that the admin can call methods on it
995        //
996        // Parameters: setID: The ID of the Set that you want to
997        // get a reference to
998        //
999        // Returns: A reference to the Set with all of the fields
1000        // and methods exposed
1001        //
1002        access(all) view fun borrowSet(setID: UInt32): &Set {
1003            pre {
1004                TopShot.sets[setID] != nil: "Cannot borrow Set: The Set doesn't exist"
1005            }
1006
1007            // Get a reference to the Set and return it
1008            // use `&` to indicate the reference to the object and type
1009            return (&TopShot.sets[setID] as &Set?)!
1010        }
1011
1012        // startNewSeries ends the current series by incrementing
1013        // the series number, meaning that Moments minted after this
1014        // will use the new series number
1015        //
1016        // Returns: The new series number
1017        //
1018        access(all) fun startNewSeries(): UInt32 {
1019            // End the current series and start a new one
1020            // by incrementing the TopShot series number
1021            TopShot.currentSeries = TopShot.currentSeries + UInt32(1)
1022
1023            emit NewSeriesStarted(newCurrentSeries: TopShot.currentSeries)
1024
1025            return TopShot.currentSeries
1026        }
1027
1028        // createSubeditionResource creates new SubeditionMap resource that
1029        // will be used to mint Moments with Subeditions
1030        access(all) fun createSubeditionAdminResource() {
1031            TopShot.account.storage.save<@SubeditionAdmin>(<- create SubeditionAdmin(), to: TopShot.SubeditionAdminStoragePath())
1032        }
1033
1034        // setMomentsSubedition saves which Subedition the Moment belongs to
1035        //
1036        // Parameters: nftID: The ID of the NFT
1037        //             subeditionID: The ID of the Subedition the Moment belongs to
1038        //             setID: The ID of the Set that the Moment references
1039        //             playID: The ID of the Play that the Moment references
1040        //
1041        access(all) fun setMomentsSubedition(nftID: UInt64, subeditionID: UInt32, setID: UInt32, playID: UInt32) {
1042            let subeditionAdmin = TopShot.account.storage.borrow<&SubeditionAdmin>(from: TopShot.SubeditionAdminStoragePath())
1043                ?? panic("No subedition admin resource in storage")
1044
1045            subeditionAdmin.setMomentsSubedition(nftID: nftID, subeditionID: subeditionID, setID: setID, playID: playID)
1046        }
1047
1048        // createSubedition creates a new Subedition struct
1049        // and stores it in the Subeditions dictionary in the SubeditionAdmin resource
1050        //
1051        // Parameters: name: The name of the Subedition
1052        //             metadata: A dictionary mapping metadata titles to their data
1053        //
1054        // Returns: the ID of the new Subedition object
1055        //
1056        access(all) fun createSubedition(name: String, metadata: {String: String}): UInt32 {
1057            let subeditionAdmin = TopShot.account.storage.borrow<&SubeditionAdmin>(from: TopShot.SubeditionAdminStoragePath())
1058                ?? panic("No subedition admin resource in storage")
1059
1060            return subeditionAdmin.createSubedition(name:name, metadata:metadata)
1061        }
1062
1063        // createNewAdmin creates a new Admin resource
1064        //
1065        access(all) fun createNewAdmin(): @Admin {
1066            return <- create Admin()
1067        }
1068    }
1069
1070    // This is the interface that users can cast their Moment Collection as
1071    // to allow others to deposit Moments into their Collection. It also allows for reading
1072    // the IDs of Moments in the Collection.
1073    /// Deprecated: This is no longer used for defining access control anymore.
1074    access(all) resource interface MomentCollectionPublic : NonFungibleToken.CollectionPublic {
1075        access(all) fun batchDeposit(tokens: @{NonFungibleToken.Collection})
1076        access(all) fun borrowMoment(id: UInt64): &NFT? {
1077            // If the result isn't nil, the id of the returned reference
1078            // should be the same as the argument to the function
1079            post {
1080                (result == nil) || (result?.id == id):
1081                    "Cannot borrow Moment reference: The ID of the returned reference is incorrect"
1082            }
1083        }
1084    }
1085
1086    // Collection is a resource that every user who owns NFTs
1087    // will store in their account to manage their NFTS
1088    //
1089    access(all) resource Collection: MomentCollectionPublic, NonFungibleToken.Collection {
1090        // Dictionary of Moment conforming tokens
1091        // NFT is a resource type with a UInt64 ID field
1092        access(all) var ownedNFTs: @{UInt64: {NonFungibleToken.NFT}}
1093
1094        init() {
1095            self.ownedNFTs <- {}
1096        }
1097
1098        // Return a list of NFT types that this receiver accepts
1099        access(all) view fun getSupportedNFTTypes(): {Type: Bool} {
1100            let supportedTypes: {Type: Bool} = {}
1101            supportedTypes[Type<@NFT>()] = true
1102            return supportedTypes
1103        }
1104
1105        // Return whether or not the given type is accepted by the collection
1106        // A collection that can accept any type should just return true by default
1107        access(all) view fun isSupportedNFTType(type: Type): Bool {
1108            if type == Type<@NFT>() {
1109                return true
1110            }
1111            return false
1112        }
1113
1114        // Return the amount of NFTs stored in the collection
1115        access(all) view fun getLength(): Int {
1116            return self.ownedNFTs.length
1117        }
1118
1119        // Create an empty Collection for TopShot NFTs and return it to the caller
1120        access(all) fun createEmptyCollection(): @{NonFungibleToken.Collection} {
1121            return <- TopShot.createEmptyCollection(nftType: Type<@NFT>())
1122        }
1123
1124        // withdraw removes an Moment from the Collection and moves it to the caller
1125        //
1126        // Parameters: withdrawID: The ID of the NFT
1127        // that is to be removed from the Collection
1128        //
1129        // returns: @NonFungibleToken.NFT the token that was withdrawn
1130        access(NonFungibleToken.Withdraw) fun withdraw(withdrawID: UInt64): @{NonFungibleToken.NFT} {
1131            // Borrow nft and check if locked
1132            let nft = self.borrowNFT(withdrawID)
1133                ?? panic("Cannot borrow: empty reference")
1134
1135            if TopShotLocking.isLocked(nftRef: nft) {
1136                panic("Cannot withdraw: Moment is locked")
1137            }
1138
1139            // Remove the nft from the Collection
1140            let token <- self.ownedNFTs.remove(key: withdrawID)
1141                ?? panic("Cannot withdraw: Moment does not exist in the collection")
1142
1143            emit Withdraw(id: token.id, from: self.owner?.address)
1144
1145            // Return the withdrawn token
1146            return <- token
1147        }
1148
1149        // batchWithdraw withdraws multiple tokens and returns them as a Collection
1150        //
1151        // Parameters: ids: An array of IDs to withdraw
1152        //
1153        // Returns: @NonFungibleToken.Collection: A collection that contains
1154        //                                        the withdrawn moments
1155        //
1156        access(NonFungibleToken.Withdraw) fun batchWithdraw(ids: [UInt64]): @{NonFungibleToken.Collection} {
1157            // Create a new empty Collection
1158            var batchCollection <- create Collection()
1159
1160            // Iterate through the ids and withdraw them from the Collection
1161            for id in ids {
1162                batchCollection.deposit(token: <- self.withdraw(withdrawID: id))
1163            }
1164
1165            // Return the withdrawn tokens
1166            return <- batchCollection
1167        }
1168
1169        // deposit takes a Moment and adds it to the Collections dictionary
1170        //
1171        // Paramters: token: the NFT to be deposited in the collection
1172        //
1173        access(all) fun deposit(token: @{NonFungibleToken.NFT}) {
1174            // Cast the deposited token as a TopShot NFT to make sure
1175            // it is the correct type
1176            let token <- token as! @NFT
1177
1178            // Get the token's ID
1179            let id = token.id
1180
1181            // Add the new token to the dictionary
1182            let oldToken <- self.ownedNFTs[id] <- token
1183
1184            // Only emit a deposit event if the Collection
1185            // is in an account's storage
1186            if self.owner?.address != nil {
1187                emit Deposit(id: id, to: self.owner?.address)
1188            }
1189
1190            // Destroy the empty old token that was "removed"
1191            destroy oldToken
1192        }
1193
1194        // batchDeposit takes a Collection object as an argument
1195        // and deposits each contained NFT into this Collection
1196        access(all) fun batchDeposit(tokens: @{NonFungibleToken.Collection}) {
1197            // Get an array of the IDs to be deposited
1198            let keys = tokens.getIDs()
1199
1200            // Iterate through the keys in the collection and deposit each one
1201            for key in keys {
1202                self.deposit(token: <- tokens.withdraw(withdrawID: key))
1203            }
1204
1205            // Destroy the empty Collection
1206            destroy tokens
1207        }
1208
1209        // lock takes a token id and a duration in seconds and locks
1210        // the moment for that duration
1211        access(NonFungibleToken.Update) fun lock(id: UInt64, duration: UFix64) {
1212            // Remove the nft from the Collection
1213            let token <- self.ownedNFTs.remove(key: id)
1214                ?? panic("Cannot lock: Moment does not exist in the collection")
1215
1216            TopShot.emitNFTUpdated(&token as auth(NonFungibleToken.Update) &{NonFungibleToken.NFT})
1217
1218            // pass the token to the locking contract
1219            // store it again after it comes back
1220            let oldToken <- self.ownedNFTs[id] <- TopShotLocking.lockNFT(nft: <- token, duration: duration)
1221
1222            destroy oldToken
1223        }
1224
1225        // batchLock takes an array of token ids and a duration in seconds
1226        // it iterates through the ids and locks each for the specified duration
1227        access(NonFungibleToken.Update) fun batchLock(ids: [UInt64], duration: UFix64) {
1228            // Iterate through the ids and lock them
1229            for id in ids {
1230                self.lock(id: id, duration: duration)
1231            }
1232        }
1233
1234        // unlock takes a token id and attempts to unlock it
1235        // TopShotLocking.unlockNFT contains business logic around unlock eligibility
1236        access(NonFungibleToken.Update) fun unlock(id: UInt64) {
1237            // Remove the nft from the Collection
1238            let token <- self.ownedNFTs.remove(key: id)
1239                ?? panic("Cannot lock: Moment does not exist in the collection")
1240
1241            TopShot.emitNFTUpdated(&token as auth(NonFungibleToken.Update) &{NonFungibleToken.NFT})
1242
1243            // Pass the token to the TopShotLocking contract then get it back
1244            // Store it back to the ownedNFTs dictionary
1245            let oldToken <- self.ownedNFTs[id] <- TopShotLocking.unlockNFT(nft: <- token)
1246
1247            destroy oldToken
1248        }
1249
1250        // batchUnlock takes an array of token ids
1251        // it iterates through the ids and unlocks each if they are eligible
1252        access(NonFungibleToken.Update) fun batchUnlock(ids: [UInt64]) {
1253            // Iterate through the ids and unlocks them
1254            for id in ids {
1255                self.unlock(id: id)
1256            }
1257        }
1258
1259        // destroyMoments destroys moments in this collection
1260        // unlocks the moments if they are locked
1261        //
1262        // Parameters: ids: An array of NFT IDs
1263        // to be destroyed from the Collection
1264        access(NonFungibleToken.Update) fun destroyMoments(ids: [UInt64]) {
1265            let topShotLockingAdmin = TopShot.account.storage.borrow<&TopShotLocking.Admin>(from: TopShotLocking.AdminStoragePath())
1266                ?? panic("No TopShotLocking admin resource in storage")
1267
1268            for id in ids {
1269                // Remove the nft from the Collection
1270                let token <- self.ownedNFTs.remove(key: id)
1271                    ?? panic("Cannot destroy: Moment does not exist in collection: ".concat(id.toString()))
1272
1273                // Emit a withdraw event here so that platforms do not have to understand TopShot-specific events to see ownership change
1274                // A withdraw without a corresponding deposit means the NFT in question has no owner address
1275                emit Withdraw(id: id, from: self.owner?.address)
1276
1277                // does nothing if the moment is not locked
1278                topShotLockingAdmin.unlockByID(id: id)
1279
1280                destroy token
1281            }
1282        }
1283
1284        // getIDs returns an array of the IDs that are in the Collection
1285        access(all) view fun getIDs(): [UInt64] {
1286            return self.ownedNFTs.keys
1287        }
1288
1289        // borrowNFT Returns a borrowed reference to a Moment in the Collection
1290        // so that the caller can read its ID
1291        //
1292        // Parameters: id: The ID of the NFT to get the reference for
1293        //
1294        // Returns: A reference to the NFT
1295        //
1296        // Note: This only allows the caller to read the ID of the NFT,
1297        // not any topshot specific data. Please use borrowMoment to
1298        // read Moment data.
1299        //
1300        access(all) view fun borrowNFT(_ id: UInt64): &{NonFungibleToken.NFT}? {
1301            return &self.ownedNFTs[id]
1302        }
1303
1304        // borrowMoment returns a borrowed reference to a Moment
1305        // so that the caller can read data and call methods from it.
1306        // They can use this to read its setID, playID, serialNumber,
1307        // or any of the setData or Play data associated with it by
1308        // getting the setID or playID and reading those fields from
1309        // the smart contract.
1310        //
1311        // Parameters: id: The ID of the NFT to get the reference for
1312        //
1313        // Returns: A reference to the NFT
1314        access(all) view fun borrowMoment(id: UInt64): &NFT? {
1315            return self.borrowNFT(id) as! &NFT?
1316        }
1317
1318        access(all) view fun borrowViewResolver(id: UInt64): &{ViewResolver.Resolver}? {
1319            if let nft = &self.ownedNFTs[id] as &{NonFungibleToken.NFT}? {
1320                return nft as &{ViewResolver.Resolver}
1321            }
1322            return nil
1323        }
1324    }
1325
1326    // -----------------------------------------------------------------------
1327    // TopShot contract-level function definitions
1328    // -----------------------------------------------------------------------
1329
1330    // createEmptyCollection creates a new, empty Collection object so that
1331    // a user can store it in their account storage.
1332    // Once they have a Collection in their storage, they are able to receive
1333    // Moments in transactions.
1334    //
1335    access(all) fun createEmptyCollection(nftType: Type): @{NonFungibleToken.Collection} {
1336        if nftType != Type<@NFT>() {
1337            panic("NFT type is not supported")
1338        }
1339        return <- create TopShot.Collection()
1340    }
1341
1342    // getAllPlays returns all the plays in topshot
1343    //
1344    // Returns: An array of all the plays that have been created
1345    access(all) view fun getAllPlays(): [Play] {
1346        return TopShot.playDatas.values
1347    }
1348
1349    // getPlayMetaData returns all the metadata associated with a specific Play
1350    //
1351    // Parameters: playID: The id of the Play that is being searched
1352    //
1353    // Returns: The metadata as a String to String mapping optional
1354    access(all) view fun getPlayMetaData(playID: UInt32): {String: String}? {
1355        return self.playDatas[playID]?.metadata
1356    }
1357
1358    // getPlayMetaDataByField returns the metadata associated with a
1359    //                        specific field of the metadata
1360    //                        Ex: field: "Team" will return something
1361    //                        like "Memphis Grizzlies"
1362    //
1363    // Parameters: playID: The id of the Play that is being searched
1364    //             field: The field to search for
1365    //
1366    // Returns: The metadata field as a String Optional
1367    access(all) view fun getPlayMetaDataByField(playID: UInt32, field: String): String? {
1368        // Don't force a revert if the playID or field is invalid
1369        if let play = TopShot.playDatas[playID] {
1370            return play.metadata[field]
1371        }
1372        return nil
1373    }
1374
1375    // getSetData returns the data that the specified Set
1376    //            is associated with.
1377    //
1378    // Parameters: setID: The id of the Set that is being searched
1379    //
1380    // Returns: The QuerySetData struct that has all the important information about the set
1381    access(all) fun getSetData(setID: UInt32): QuerySetData? {
1382        if TopShot.sets[setID] == nil {
1383            return nil
1384        }
1385        return QuerySetData(setID: setID)
1386    }
1387
1388    // getSetName returns the name that the specified Set
1389    //            is associated with.
1390    //
1391    // Parameters: setID: The id of the Set that is being searched
1392    //
1393    // Returns: The name of the Set
1394    access(all) view fun getSetName(setID: UInt32): String? {
1395        // Don't force a revert if the setID is invalid
1396        return TopShot.setDatas[setID]?.name
1397    }
1398
1399    // getSetSeries returns the series that the specified Set
1400    //              is associated with.
1401    //
1402    // Parameters: setID: The id of the Set that is being searched
1403    //
1404    // Returns: The series that the Set belongs to
1405    access(all) view fun getSetSeries(setID: UInt32): UInt32? {
1406        // Don't force a revert if the setID is invalid
1407        return TopShot.setDatas[setID]?.series
1408    }
1409
1410    // getSetIDsByName returns the IDs that the specified Set name
1411    //                 is associated with.
1412    //
1413    // Parameters: setName: The name of the Set that is being searched
1414    //
1415    // Returns: An array of the IDs of the Set if it exists, or nil if doesn't
1416    access(all) fun getSetIDsByName(setName: String): [UInt32]? {
1417        var setIDs: [UInt32] = []
1418
1419        // Iterate through all the setDatas and search for the name
1420        for setData in TopShot.setDatas.values {
1421            if setName == setData.name {
1422                // If the name is found, return the ID
1423                setIDs.append(setData.setID)
1424            }
1425        }
1426
1427        // If the name isn't found, return nil
1428        // Don't force a revert if the setName is invalid
1429        if setIDs.length == 0 {
1430            return nil
1431        }
1432        return setIDs
1433    }
1434
1435    // getPlaysInSet returns the list of Play IDs that are in the Set
1436    //
1437    // Parameters: setID: The id of the Set that is being searched
1438    //
1439    // Returns: An array of Play IDs
1440    access(all) view fun getPlaysInSet(setID: UInt32): [UInt32]? {
1441        // Don't force a revert if the setID is invalid
1442        return TopShot.sets[setID]?.plays
1443    }
1444
1445    // isEditionRetired returns a boolean that indicates if a Set/Play combo
1446    //                  (otherwise known as an edition) is retired.
1447    //                  If an edition is retired, it still remains in the Set,
1448    //                  but Moments can no longer be minted from it.
1449    //
1450    // Parameters: setID: The id of the Set that is being searched
1451    //             playID: The id of the Play that is being searched
1452    //
1453    // Returns: Boolean indicating if the edition is retired or not
1454    access(all) fun isEditionRetired(setID: UInt32, playID: UInt32): Bool? {
1455        // Return the retired status for the play in the set if it exists
1456        if let setdata = self.getSetData(setID: setID) {
1457            return setdata.getRetired()[playID]
1458        }
1459        return nil
1460    }
1461
1462    // isSetLocked returns a boolean that indicates if a Set
1463    //             is locked. If it's locked,
1464    //             new Plays can no longer be added to it,
1465    //             but Moments can still be minted from Plays the set contains.
1466    //
1467    // Parameters: setID: The id of the Set that is being searched
1468    //
1469    // Returns: Boolean indicating if the Set is locked or not
1470    access(all) view fun isSetLocked(setID: UInt32): Bool? {
1471        // Don't force a revert if the setID is invalid
1472        return TopShot.sets[setID]?.locked
1473    }
1474
1475    // getNumMomentsInEdition return the number of Moments that have been
1476    //                        minted from a certain edition.
1477    //
1478    // Parameters: setID: The id of the Set that is being searched
1479    //             playID: The id of the Play that is being searched
1480    //
1481    // Returns: The total number of Moments
1482    //          that have been minted from an edition
1483    access(all) fun getNumMomentsInEdition(setID: UInt32, playID: UInt32): UInt32? {
1484        // Return the number of moments minted for the play in the set if it exists
1485        if let setdata = self.getSetData(setID: setID) {
1486            return setdata.getNumberMintedPerPlay()[playID]
1487        }
1488        return nil
1489    }
1490
1491    // getMomentsSubedition returns the Subedition the Moment belongs to
1492    //
1493    // Parameters: nftID: The ID of the NFT
1494    //
1495    // returns: UInt32? Subedition's ID if exists
1496    //
1497    access(all) view fun getMomentsSubedition(nftID: UInt64): UInt32? {
1498        let subeditionAdmin = self.account.storage.borrow<&SubeditionAdmin>(from: TopShot.SubeditionAdminStoragePath())
1499            ?? panic("No subedition admin resource in storage")
1500        return subeditionAdmin.getMomentsSubedition(nftID: nftID)
1501    }
1502
1503    // getAllSubeditions returns all the subeditions in topshot subeditionAdmin resource
1504    //
1505    // Returns: An array of all the subeditions that have been created
1506    access(all) view fun getAllSubeditions(): &[Subedition] {
1507        let subeditionAdmin = self.account.storage.borrow<&SubeditionAdmin>(from: TopShot.SubeditionAdminStoragePath())
1508            ?? panic("No subedition admin resource in storage")
1509        return subeditionAdmin.subeditionDatas.values
1510    }
1511
1512    // getSubeditionByID returns the subedition struct entity
1513    //
1514    // Parameters: subeditionID: The id of the Subedition that is being searched
1515    //
1516    // Returns: The Subedition struct
1517    access(all) view fun getSubeditionByID(subeditionID: UInt32): &Subedition {
1518        let subeditionAdmin = self.account.storage.borrow<&SubeditionAdmin>(from: TopShot.SubeditionAdminStoragePath())
1519            ?? panic("No subedition admin resource in storage")
1520        return subeditionAdmin.subeditionDatas[subeditionID]!
1521    }
1522
1523    // getSubeditionByNFTID returns the subedition struct that the NFT belongs to
1524    //
1525    // Parameters: nftID: The id of the NFT that is being searched
1526    //
1527    // Returns: The subedition struct that the NFT belongs to
1528    access(all) view fun getSubeditionByNFTID(_ nftID: UInt64): &Subedition? {
1529        if let subeditionAdmin = self.account.storage.borrow<&SubeditionAdmin>(from: TopShot.SubeditionAdminStoragePath()) {
1530            if let subeditionID = subeditionAdmin.getMomentsSubedition(nftID: nftID) {
1531                return subeditionAdmin.subeditionDatas[subeditionID]
1532            }
1533        }
1534        return nil
1535    }
1536
1537    // This script reads the public nextSubeditionID from the SubeditionAdmin resource and
1538    // returns that number to the caller
1539    //
1540    // Returns: UInt32
1541    // the next number in nextSubeditionID from the SubeditionAdmin resource
1542    access(all) view fun getNextSubeditionID(): UInt32 {
1543        let subeditionAdmin = self.account.storage.borrow<&SubeditionAdmin>(from: TopShot.SubeditionAdminStoragePath())
1544            ?? panic("No subedition admin resource in storage")
1545        return subeditionAdmin.nextSubeditionID
1546    }
1547
1548    // SubeditionAdmin is a resource that allows Set to mint Moments with Subeditions
1549    //
1550    access(all) struct Subedition {
1551        access(all) let subeditionID: UInt32
1552
1553        access(all) let name: String
1554
1555        access(all) let metadata: {String: String}
1556
1557        init(subeditionID: UInt32, name: String, metadata: {String: String}) {
1558            pre {
1559                name.length != 0: "New Subedition name cannot be empty"
1560            }
1561            self.subeditionID = subeditionID
1562            self.name = name
1563            self.metadata = metadata
1564        }
1565    }
1566
1567    access(all) resource SubeditionAdmin {
1568        // Map of number of already minted Moments using Subedition.
1569        // When a new Moment with Subedition is minted, 1 is added to the
1570        // number in this map by the key, formed by concatinating of
1571        // SetID, PlayID and SubeditionID
1572        access(contract) let numberMintedPerSubedition: {String: UInt32}
1573
1574        // Map of Subedition which the Moment belongs to.
1575        // This map updates after each minting.
1576        access(contract) let momentsSubedition: {UInt64: UInt32}
1577
1578        // The ID that is used to create Subeditions.
1579        // Every time a Subeditions is created, subeditionID is assigned
1580        // to the new Subedition's ID and then is incremented by 1.
1581        access(contract) var nextSubeditionID: UInt32
1582
1583        // Variable size dictionary of Subedition structs
1584        access(contract) let subeditionDatas: {UInt32: Subedition}
1585
1586        // createSubedition creates a new Subedition struct
1587        // and stores it in the Subeditions dictionary in the SubeditionAdmin resource
1588        //
1589        // Parameters: name: The name of the Subedition
1590        //             metadata: A dictionary mapping metadata titles to their data
1591        //
1592        // Returns: the ID of the new Subedition object
1593        //
1594        access(all) fun createSubedition(name: String, metadata: {String: String}): UInt32 {
1595            let newID = self.nextSubeditionID
1596
1597            var newSubedition = Subedition(subeditionID: newID, name: name, metadata: metadata)
1598
1599            self.nextSubeditionID = self.nextSubeditionID + UInt32(1)
1600
1601            self.subeditionDatas[newID] = newSubedition
1602
1603            emit SubeditionCreated(subeditionID: newID, name: name, metadata: metadata)
1604
1605            return newID
1606        }
1607
1608        // getMomentsSubedition function that return's wich Subedition the Moment belongs to
1609        //
1610        // Parameters: nftID: The ID of the NFT
1611        //
1612        // returns: UInt32? Subedition's ID if exists
1613        //
1614        access(all) view fun getMomentsSubedition(nftID: UInt64): UInt32? {
1615            return self.momentsSubedition[nftID]
1616        }
1617
1618        // getNumberMintedPerSubedition function that return's
1619        // the number of Moments that have been minted for this subedition
1620        // to use as this Moment's serial number
1621        //
1622        // Parameters: setID: The ID of the Set Moment will be minted from
1623        //             playID: The ID of the Play Moment will be minted from
1624        //             subeditionID: The ID of the Subedition using which moment will be minted
1625        //
1626        // returns: UInt32 Number of Moments, already minted for this Subedition
1627        //
1628        access(all) fun getNumberMintedPerSubedition(setID: UInt32, playID: UInt32, subeditionID: UInt32): UInt32 {
1629            let setPlaySubedition = self.getSetPlaySubeditionString(setID, playID, subeditionID)
1630            if !self.numberMintedPerSubedition.containsKey(setPlaySubedition) {
1631                self.numberMintedPerSubedition.insert(key: setPlaySubedition, UInt32(0))
1632                return UInt32(0)
1633            }
1634            return self.numberMintedPerSubedition[setPlaySubedition]!
1635        }
1636
1637        // addToNumberMintedPerSubedition function that increments 1 to the
1638        // number of Moments that have been minted for this subedition
1639        //
1640        // Parameters: setID: The ID of the Set Moment will be minted from
1641        //             playID: The ID of the Play Moment will be minted from
1642        //             subeditionID: The ID of the Subedition using which moment will be minted
1643        //
1644        //
1645        access(contract) fun addToNumberMintedPerSubedition(setID: UInt32, playID: UInt32, subeditionID: UInt32) {
1646            let setPlaySubedition = self.getSetPlaySubeditionString(setID, playID, subeditionID)
1647
1648            // Get number of moments minted for this subedition
1649            let numberMinted = self.numberMintedPerSubedition[setPlaySubedition]
1650                ?? panic("Could not find number of moments minted for specified Subedition!")
1651
1652            // Increment the number of moments minted for this subedition
1653            self.numberMintedPerSubedition[setPlaySubedition] = numberMinted + UInt32(1)
1654        }
1655
1656        // getSetPlaySubeditionString builds a string that is used as a key in the numberMintedPerSubedition map
1657        access(self) view fun getSetPlaySubeditionString(_ setID: UInt32, _ playID: UInt32, _ subeditionID: UInt32): String {
1658            return setID.toString().concat(playID.toString()).concat(subeditionID.toString())
1659        }
1660
1661
1662        // setMomentsSubedition saves which Subedition the Moment belongs to
1663        //
1664        // Parameters: nftID: The ID of the NFT
1665        //             subeditionID: The ID of the Subedition the Moment belongs to
1666        //             setID: The ID of the Set that the Moment references
1667        //             playID: The ID of the Play that the Moment references
1668        //
1669        access(all) fun setMomentsSubedition(nftID: UInt64, subeditionID: UInt32, setID: UInt32, playID: UInt32) {
1670            pre {
1671                !self.momentsSubedition.containsKey(nftID) : "Subedition for this moment already exists!"
1672            }
1673
1674            self.momentsSubedition.insert(key: nftID, subeditionID)
1675
1676            emit SubeditionAddedToMoment(momentID: nftID, subeditionID: subeditionID, setID: setID, playID: playID)
1677        }
1678
1679        init() {
1680            self.momentsSubedition = {}
1681            self.numberMintedPerSubedition = {}
1682            self.subeditionDatas = {}
1683            self.nextSubeditionID = 1
1684        }
1685    }
1686
1687    //------------------------------------------------------------
1688    // Contract MetadataViews
1689    //------------------------------------------------------------
1690
1691    // getContractViews returns the metadata view types available for this contract
1692    access(all) view fun getContractViews(resourceType: Type?): [Type] {
1693        return [
1694            Type<MetadataViews.NFTCollectionData>(),
1695            Type<MetadataViews.NFTCollectionDisplay>(),
1696            Type<CrossVMMetadataViews.EVMPointer>(),
1697            Type<MetadataViews.Royalties>()
1698        ]
1699    }
1700
1701    // resolveContractView resolves this contract's metadata views
1702    access(all) fun resolveContractView(resourceType: Type?, viewType: Type): AnyStruct? {
1703        post {
1704            result == nil || result!.getType() == viewType: "The returned view must be of the given type or nil"
1705        }
1706        switch viewType {
1707            case Type<MetadataViews.NFTCollectionData>():
1708                return MetadataViews.NFTCollectionData(
1709                    storagePath: /storage/MomentCollection,
1710                    publicPath: /public/MomentCollection,
1711                    publicCollection: Type<&Collection>(),
1712                    publicLinkedType: Type<&Collection>(),
1713                    createEmptyCollectionFunction: (fun (): @{NonFungibleToken.Collection} {
1714                        return <- TopShot.createEmptyCollection(nftType: Type<@NFT>())
1715                    })
1716                )
1717            case Type<MetadataViews.NFTCollectionDisplay>():
1718                let bannerImage = MetadataViews.Media(
1719                    file: MetadataViews.HTTPFile(
1720                        url: "https://nbatopshot.com/static/img/top-shot-logo-horizontal-white.svg"
1721                    ),
1722                    mediaType: "image/svg+xml"
1723                )
1724                let squareImage = MetadataViews.Media(
1725                    file: MetadataViews.HTTPFile(
1726                        url: "https://nbatopshot.com/static/favicon/favicon.svg"
1727                    ),
1728                    mediaType: "image/svg+xml"
1729                )
1730                return MetadataViews.NFTCollectionDisplay(
1731                    name: "NBA Top Shot",
1732                    description: "NBA Top Shot is your chance to own, sell, and trade official digital collectibles of the NBA and WNBA's greatest plays and players",
1733                    externalURL: MetadataViews.ExternalURL("https://nbatopshot.com"),
1734                    squareImage: squareImage,
1735                    bannerImage: bannerImage,
1736                    socials: {
1737                        "twitter": MetadataViews.ExternalURL("https://twitter.com/nbatopshot"),
1738                        "discord": MetadataViews.ExternalURL("https://discord.com/invite/nbatopshot"),
1739                        "instagram": MetadataViews.ExternalURL("https://www.instagram.com/nbatopshot")
1740                    }
1741                )
1742            case Type<MetadataViews.Royalties>():
1743                let royaltyReceiver: Capability<&{FungibleToken.Receiver}> =
1744                    getAccount(TopShot.RoyaltyAddress()).capabilities.get<&{FungibleToken.Receiver}>(MetadataViews.getRoyaltyReceiverPublicPath())!
1745                return MetadataViews.Royalties(
1746                    [
1747                        MetadataViews.Royalty(
1748                            receiver: royaltyReceiver,
1749                            cut: 0.05,
1750                            description: "NBATopShot marketplace royalty"
1751                        )
1752                    ]
1753                )
1754            case Type<CrossVMMetadataViews.EVMPointer>():
1755                return CrossVMMetadataViews.EVMPointer(
1756                    cadenceType: Type<@TopShot.NFT>(),
1757                    cadenceContractAddress: self.account.address,
1758                    evmContractAddress: EVM.addressFromString("0x84c6a2e6765e88427c41bb38c82a78b570e24709"),
1759                    nativeVM: CrossVMMetadataViews.VM.Cadence
1760                )
1761        }
1762        return nil
1763    }
1764
1765    // -----------------------------------------------------------------------
1766    // TopShot initialization function
1767    // -----------------------------------------------------------------------
1768    //
1769    init() {
1770        // Initialize contract fields
1771        self.currentSeries = 0
1772        self.playDatas = {}
1773        self.setDatas = {}
1774        self.sets <- {}
1775        self.nextPlayID = 1
1776        self.nextSetID = 1
1777        self.totalSupply = 0
1778
1779        // Put a new Collection in storage
1780        self.account.storage.save<@Collection>(<- create Collection(), to: /storage/MomentCollection)
1781
1782        // Create and publish a capability for the collection
1783        self.account.capabilities.publish(
1784            self.account.capabilities.storage.issue<&Collection>(/storage/MomentCollection),
1785            at: /public/MomentCollection
1786        )
1787
1788        // Put the Minter in storage
1789        self.account.storage.save<@Admin>(<- create Admin(), to: /storage/TopShotAdmin)
1790    }
1791}
1792