Smart Contract
TopShot
A.0b2a3299cc857e29.TopShot
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