@flowtyio/flow-contracts 0.0.14 → 0.0.15

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