-
Notifications
You must be signed in to change notification settings - Fork 105
/
Copy pathTopShot.cdc
1762 lines (1521 loc) · 74.8 KB
/
TopShot.cdc
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
/*
Description: Central Smart Contract for NBA TopShot
This smart contract contains the core functionality for
NBA Top Shot, created by Dapper Labs
The contract manages the data associated with all the plays and sets
that are used as templates for the Moment NFTs
When a new Play wants to be added to the records, an Admin creates
a new Play struct that is stored in the smart contract.
Then an Admin can create new Sets. Sets consist of a public struct that
contains public information about a set, and a private resource used
to mint new moments based off of plays that have been linked to the Set.
The admin resource has the power to do all of the important actions
in the smart contract. When admins want to call functions in a set,
they call their borrowSet function to get a reference
to a set in the contract. Then, they can call functions on the set using that reference.
In this way, the smart contract and its defined resources interact
with great teamwork, just like the Indiana Pacers, the greatest NBA team
of all time.
When moments are minted, they are initialized with a MomentData struct and
are returned by the minter.
The contract also defines a Collection resource. This is an object that
every TopShot NFT owner will store in their account
to manage their NFT collection.
The main Top Shot account will also have its own Moment collections
it can use to hold its own moments that have not yet been sent to a user.
Note: All state changing functions will panic if an invalid argument is
provided or one of its pre-conditions or post conditions aren't met.
Functions that don't modify state will simply return 0 or nil
and those cases need to be handled by the caller.
It is also important to remember that
The Golden State Warriors blew a 3-1 lead in the 2016 NBA finals.
*/
import FungibleToken from 0xFUNGIBLETOKENADDRESS
import NonFungibleToken from 0xNFTADDRESS
import MetadataViews from 0xMETADATAVIEWSADDRESS
import TopShotLocking from 0xTOPSHOTLOCKINGADDRESS
import ViewResolver from 0xVIEWRESOLVERADDRESS
access(all) contract TopShot: NonFungibleToken {
// -----------------------------------------------------------------------
// TopShot deployment variables
// -----------------------------------------------------------------------
// The network the contract is deployed on
access(all) view fun Network(): String { return ${NETWORK} }
// The address to which royalties should be deposited
access(all) view fun RoyaltyAddress(): Address { return 0xTOPSHOTROYALTYADDRESS }
// The path to the Subedition Admin resource belonging to the Account
// which the contract is deployed on
access(all) view fun SubeditionAdminStoragePath(): StoragePath { return /storage/TopShotSubeditionAdmin}
// -----------------------------------------------------------------------
// TopShot contract Events
// -----------------------------------------------------------------------
// Emitted when a new Play struct is created
access(all) event PlayCreated(id: UInt32, metadata: {String: String})
// Emitted when a new series has been triggered by an admin
access(all) event NewSeriesStarted(newCurrentSeries: UInt32)
// Events for Set-Related actions
//
// Emitted when a new Set is created
access(all) event SetCreated(setID: UInt32, series: UInt32)
// Emitted when a new Play is added to a Set
access(all) event PlayAddedToSet(setID: UInt32, playID: UInt32)
// Emitted when a Play is retired from a Set and cannot be used to mint
access(all) event PlayRetiredFromSet(setID: UInt32, playID: UInt32, numMoments: UInt32)
// Emitted when a Set is locked, meaning Plays cannot be added
access(all) event SetLocked(setID: UInt32)
// Emitted when a Moment is minted from a Set
access(all) event MomentMinted(momentID: UInt64, playID: UInt32, setID: UInt32, serialNumber: UInt32, subeditionID: UInt32)
// Events for Collection-related actions
//
// Emitted when a moment is withdrawn from a Collection
access(all) event Withdraw(id: UInt64, from: Address?)
// Emitted when a moment is deposited into a Collection
access(all) event Deposit(id: UInt64, to: Address?)
// Emitted when a Moment is destroyed
access(all) event MomentDestroyed(id: UInt64)
// Emitted when a Subedition is created
access(all) event SubeditionCreated(subeditionID: UInt32, name: String, metadata: {String: String})
// Emitted when a Subedition is linked to the specific Moment
access(all) event SubeditionAddedToMoment(momentID: UInt64, subeditionID: UInt32, setID: UInt32, playID: UInt32)
// -----------------------------------------------------------------------
// TopShot contract-level fields.
// These contain actual values that are stored in the smart contract.
// -----------------------------------------------------------------------
// Series that this Set belongs to.
// Series is a concept that indicates a group of Sets through time.
// Many Sets can exist at a time, but only one series.
access(all) var currentSeries: UInt32
// Variable size dictionary of Play structs
access(self) var playDatas: {UInt32: Play}
// Variable size dictionary of SetData structs
access(self) var setDatas: {UInt32: SetData}
// Variable size dictionary of Set resources
access(self) var sets: @{UInt32: Set}
// The ID that is used to create Plays.
// Every time a Play is created, playID is assigned
// to the new Play's ID and then is incremented by 1.
access(all) var nextPlayID: UInt32
// The ID that is used to create Sets. Every time a Set is created
// setID is assigned to the new set's ID and then is incremented by 1.
access(all) var nextSetID: UInt32
// The total number of Top shot Moment NFTs that have been created
// Because NFTs can be destroyed, it doesn't necessarily mean that this
// reflects the total number of NFTs in existence, just the number that
// have been minted to date. Also used as global moment IDs for minting.
access(all) var totalSupply: UInt64
// -----------------------------------------------------------------------
// TopShot contract-level Composite Type definitions
// -----------------------------------------------------------------------
// These are just *definitions* for Types that this contract
// and other accounts can use. These definitions do not contain
// actual stored values, but an instance (or object) of one of these Types
// can be created by this contract that contains stored values.
// -----------------------------------------------------------------------
// Play is a Struct that holds metadata associated
// with a specific NBA play, like the legendary moment when
// Ray Allen hit the 3 to tie the Heat and Spurs in the 2013 finals game 6
// or when Lance Stephenson blew in the ear of Lebron James.
//
// Moment NFTs will all reference a single play as the owner of
// its metadata. The plays are publicly accessible, so anyone can
// read the metadata associated with a specific play ID
//
access(all) struct Play {
// The unique ID for the Play
access(all) let playID: UInt32
// Stores all the metadata about the play as a string mapping
// This is not the long term way NFT metadata will be stored. It's a temporary
// construct while we figure out a better way to do metadata.
//
access(all) let metadata: {String: String}
init(metadata: {String: String}) {
pre {
metadata.length != 0: "New Play metadata cannot be empty"
}
self.playID = TopShot.nextPlayID
self.metadata = metadata
}
/// This function is intended to backfill the Play on blockchain with a more detailed
/// description of the Play. The benefit of having the description is that anyone would
/// be able to know the story of the Play directly from Flow
access(contract) fun updateTagline(tagline: String): UInt32 {
self.metadata["Tagline"] = tagline
TopShot.playDatas[self.playID] = self
return self.playID
}
}
// A Set is a grouping of Plays that have occured in the real world
// that make up a related group of collectibles, like sets of baseball
// or Magic cards. A Play can exist in multiple different sets.
//
// SetData is a struct that is stored in a field of the contract.
// Anyone can query the constant information
// about a set by calling various getters located
// at the end of the contract. Only the admin has the ability
// to modify any data in the private Set resource.
//
access(all) struct SetData {
// Unique ID for the Set
access(all) let setID: UInt32
// Name of the Set
// ex. "Times when the Toronto Raptors choked in the playoffs"
access(all) let name: String
// Series that this Set belongs to.
// Series is a concept that indicates a group of Sets through time.
// Many Sets can exist at a time, but only one series.
access(all) let series: UInt32
init(name: String) {
pre {
name.length > 0: "New Set name cannot be empty"
}
self.setID = TopShot.nextSetID
self.name = name
self.series = TopShot.currentSeries
}
}
// Set is a resource type that contains the functions to add and remove
// Plays from a set and mint Moments.
//
// It is stored in a private field in the contract so that
// the admin resource can call its methods.
//
// The admin can add Plays to a Set so that the set can mint Moments
// that reference that playdata.
// The Moments that are minted by a Set will be listed as belonging to
// the Set that minted it, as well as the Play it references.
//
// Admin can also retire Plays from the Set, meaning that the retired
// Play can no longer have Moments minted from it.
//
// If the admin locks the Set, no more Plays can be added to it, but
// Moments can still be minted.
//
// If retireAll() and lock() are called back-to-back,
// the Set is closed off forever and nothing more can be done with it.
access(all) resource Set {
// Unique ID for the set
access(all) let setID: UInt32
// Array of plays that are a part of this set.
// When a play is added to the set, its ID gets appended here.
// The ID does not get removed from this array when a Play is retired.
access(contract) var plays: [UInt32]
// Map of Play IDs that Indicates if a Play in this Set can be minted.
// When a Play is added to a Set, it is mapped to false (not retired).
// When a Play is retired, this is set to true and cannot be changed.
access(contract) var retired: {UInt32: Bool}
// Indicates if the Set is currently locked.
// When a Set is created, it is unlocked
// and Plays are allowed to be added to it.
// When a set is locked, Plays cannot be added.
// A Set can never be changed from locked to unlocked,
// the decision to lock a Set it is final.
// If a Set is locked, Plays cannot be added, but
// Moments can still be minted from Plays
// that exist in the Set.
access(all) var locked: Bool
// Mapping of Play IDs that indicates the number of Moments
// that have been minted for specific Plays in this Set.
// When a Moment is minted, this value is stored in the Moment to
// show its place in the Set, eg. 13 of 60.
access(contract) var numberMintedPerPlay: {UInt32: UInt32}
init(name: String) {
self.setID = TopShot.nextSetID
self.plays = []
self.retired = {}
self.locked = false
self.numberMintedPerPlay = {}
// Create a new SetData for this Set and store it in contract storage
TopShot.setDatas[self.setID] = SetData(name: name)
}
// addPlay adds a play to the set
//
// Parameters: playID: The ID of the Play that is being added
//
// Pre-Conditions:
// The Play needs to be an existing play
// The Set needs to be not locked
// The Play can't have already been added to the Set
//
access(all) fun addPlay(playID: UInt32) {
pre {
TopShot.playDatas[playID] != nil: "Cannot add the Play to Set: Play doesn't exist."
!self.locked: "Cannot add the play to the Set after the set has been locked."
self.numberMintedPerPlay[playID] == nil: "The play has already beed added to the set."
}
// Add the Play to the array of Plays
self.plays.append(playID)
// Open the Play up for minting
self.retired[playID] = false
// Initialize the Moment count to zero
self.numberMintedPerPlay[playID] = 0
emit PlayAddedToSet(setID: self.setID, playID: playID)
}
// addPlays adds multiple Plays to the Set
//
// Parameters: playIDs: The IDs of the Plays that are being added
// as an array
//
access(all) fun addPlays(playIDs: [UInt32]) {
for play in playIDs {
self.addPlay(playID: play)
}
}
// retirePlay retires a Play from the Set so that it can't mint new Moments
//
// Parameters: playID: The ID of the Play that is being retired
//
// Pre-Conditions:
// The Play is part of the Set and not retired (available for minting).
//
access(all) fun retirePlay(playID: UInt32) {
pre {
self.retired[playID] != nil: "Cannot retire the Play: Play doesn't exist in this set!"
}
if !self.retired[playID]! {
self.retired[playID] = true
emit PlayRetiredFromSet(setID: self.setID, playID: playID, numMoments: self.numberMintedPerPlay[playID]!)
}
}
// retireAll retires all the plays in the Set
// Afterwards, none of the retired Plays will be able to mint new Moments
//
access(all) fun retireAll() {
for play in self.plays {
self.retirePlay(playID: play)
}
}
// lock() locks the Set so that no more Plays can be added to it
//
// Pre-Conditions:
// The Set should not be locked
access(all) fun lock() {
if !self.locked {
self.locked = true
emit SetLocked(setID: self.setID)
}
}
// mintMoment mints a new Moment and returns the newly minted Moment
//
// Parameters: playID: The ID of the Play that the Moment references
//
// Pre-Conditions:
// The Play must exist in the Set and be allowed to mint new Moments
//
// Returns: The NFT that was minted
//
access(all) fun mintMoment(playID: UInt32): @NFT {
pre {
self.retired[playID] != nil: "Cannot mint the moment: This play doesn't exist."
!self.retired[playID]!: "Cannot mint the moment from this play: This play has been retired."
}
// Gets the number of Moments that have been minted for this Play
// to use as this Moment's serial number
let numInPlay = self.numberMintedPerPlay[playID]!
// Mint the new moment
let newMoment: @NFT <- create NFT(
serialNumber: numInPlay + UInt32(1),
playID: playID,
setID: self.setID,
subeditionID: 0
)
// Increment the count of Moments minted for this Play
self.numberMintedPerPlay[playID] = numInPlay + UInt32(1)
return <-newMoment
}
// batchMintMoment mints an arbitrary quantity of Moments
// and returns them as a Collection
//
// Parameters: playID: the ID of the Play that the Moments are minted for
// quantity: The quantity of Moments to be minted
//
// Returns: Collection object that contains all the Moments that were minted
//
access(all) fun batchMintMoment(playID: UInt32, quantity: UInt64): @Collection {
let newCollection <- create Collection()
var i: UInt64 = 0
while i < quantity {
newCollection.deposit(token: <-self.mintMoment(playID: playID))
i = i + UInt64(1)
}
return <- newCollection
}
// mintMomentWithSubedition mints a new Moment with subedition and returns the newly minted Moment
//
// Parameters: playID: The ID of the Play that the Moment references
// subeditionID: The ID of the subedition within Edition that the Moment references
//
// Pre-Conditions:
// The Play must exist in the Set and be allowed to mint new Moments
//
// Returns: The NFT that was minted
//
access(all) fun mintMomentWithSubedition(playID: UInt32, subeditionID: UInt32): @NFT {
pre {
self.retired[playID] != nil: "Cannot mint the moment: This play doesn't exist."
!self.retired[playID]!: "Cannot mint the moment from this play: This play has been retired."
}
// Gets the number of Moments that have been minted for this subedition
// to use as this Moment's serial number
let subeditionRef = TopShot.account.storage.borrow<&SubeditionAdmin>(from: TopShot.SubeditionAdminStoragePath())
?? panic("No subedition admin resource in storage")
let numInSubedition = subeditionRef.getNumberMintedPerSubedition(
setID: self.setID,
playID: playID,
subeditionID: subeditionID
)
// Mint the new moment
let newMoment: @NFT <- create NFT(
serialNumber: numInSubedition + UInt32(1),
playID: playID,
setID: self.setID,
subeditionID: subeditionID
)
// Increment the count of Moments minted for this subedition
subeditionRef.addToNumberMintedPerSubedition(
setID: self.setID,
playID: playID,
subeditionID: subeditionID
)
subeditionRef.setMomentsSubedition(nftID: newMoment.id, subeditionID: subeditionID, setID: self.setID, playID: playID)
self.numberMintedPerPlay[playID] = self.numberMintedPerPlay[playID]! + UInt32(1)
return <- newMoment
}
// batchMintMomentWithSubedition mints an arbitrary quantity of Moments with subedition
// and returns them as a Collection
//
// Parameters: playID: the ID of the Play that the Moments are minted for
// quantity: The quantity of Moments to be minted
// subeditionID: The ID of the subedition within Edition that the Moments references
//
// Returns: Collection object that contains all the Moments that were minted
//
access(all) fun batchMintMomentWithSubedition(playID: UInt32, quantity: UInt64, subeditionID: UInt32): @Collection {
let newCollection <- create Collection()
var i: UInt64 = 0
while i < quantity {
newCollection.deposit(token: <-self.mintMomentWithSubedition(playID: playID, subeditionID: subeditionID))
i = i + UInt64(1)
}
return <-newCollection
}
access(all) view fun getPlays(): [UInt32] {
return self.plays
}
access(all) view fun getRetired(): {UInt32: Bool} {
return self.retired
}
access(all) view fun getNumMintedPerPlay(): {UInt32: UInt32} {
return self.numberMintedPerPlay
}
}
// Struct that contains all of the important data about a set
// Can be easily queried by instantiating the `QuerySetData` object
// with the desired set ID
// let setData = TopShot.QuerySetData(setID: 12)
//
access(all) struct QuerySetData {
access(all) let setID: UInt32
access(all) let name: String
access(all) let series: UInt32
access(self) var plays: [UInt32]
access(self) var retired: {UInt32: Bool}
access(all) var locked: Bool
access(self) var numberMintedPerPlay: {UInt32: UInt32}
init(setID: UInt32) {
pre {
TopShot.sets[setID] != nil: "The set with the provided ID does not exist"
}
let set = (&TopShot.sets[setID] as &Set?)!
let setData = TopShot.setDatas[setID]!
self.setID = setID
self.name = setData.name
self.series = setData.series
self.plays = set.getPlays()
self.retired = set.getRetired()
self.locked = set.locked
self.numberMintedPerPlay = set.getNumMintedPerPlay()
}
access(all) view fun getPlays(): [UInt32] {
return self.plays
}
access(all) view fun getRetired(): {UInt32: Bool} {
return self.retired
}
access(all) view fun getNumberMintedPerPlay(): {UInt32: UInt32} {
return self.numberMintedPerPlay
}
}
access(all) struct MomentData {
// The ID of the Set that the Moment comes from
access(all) let setID: UInt32
// The ID of the Play that the Moment references
access(all) let playID: UInt32
// The place in the edition that this Moment was minted
// Otherwise know as the serial number
access(all) let serialNumber: UInt32
init(setID: UInt32, playID: UInt32, serialNumber: UInt32) {
self.setID = setID
self.playID = playID
self.serialNumber = serialNumber
}
}
// This is an implementation of a custom metadata view for Top Shot.
// This view contains the play metadata.
//
access(all) struct TopShotMomentMetadataView {
access(all) let fullName: String?
access(all) let firstName: String?
access(all) let lastName: String?
access(all) let birthdate: String?
access(all) let birthplace: String?
access(all) let jerseyNumber: String?
access(all) let draftTeam: String?
access(all) let draftYear: String?
access(all) let draftSelection: String?
access(all) let draftRound: String?
access(all) let teamAtMomentNBAID: String?
access(all) let teamAtMoment: String?
access(all) let primaryPosition: String?
access(all) let height: String?
access(all) let weight: String?
access(all) let totalYearsExperience: String?
access(all) let nbaSeason: String?
access(all) let dateOfMoment: String?
access(all) let playCategory: String?
access(all) let playType: String?
access(all) let homeTeamName: String?
access(all) let awayTeamName: String?
access(all) let homeTeamScore: String?
access(all) let awayTeamScore: String?
access(all) let seriesNumber: UInt32?
access(all) let setName: String?
access(all) let serialNumber: UInt32
access(all) let playID: UInt32
access(all) let setID: UInt32
access(all) let numMomentsInEdition: UInt32?
init(
fullName: String?,
firstName: String?,
lastName: String?,
birthdate: String?,
birthplace: String?,
jerseyNumber: String?,
draftTeam: String?,
draftYear: String?,
draftSelection: String?,
draftRound: String?,
teamAtMomentNBAID: String?,
teamAtMoment: String?,
primaryPosition: String?,
height: String?,
weight: String?,
totalYearsExperience: String?,
nbaSeason: String?,
dateOfMoment: String?,
playCategory: String?,
playType: String?,
homeTeamName: String?,
awayTeamName: String?,
homeTeamScore: String?,
awayTeamScore: String?,
seriesNumber: UInt32?,
setName: String?,
serialNumber: UInt32,
playID: UInt32,
setID: UInt32,
numMomentsInEdition: UInt32?
) {
self.fullName = fullName
self.firstName = firstName
self.lastName = lastName
self.birthdate = birthdate
self.birthplace = birthplace
self.jerseyNumber = jerseyNumber
self.draftTeam = draftTeam
self.draftYear = draftYear
self.draftSelection = draftSelection
self.draftRound = draftRound
self.teamAtMomentNBAID = teamAtMomentNBAID
self.teamAtMoment = teamAtMoment
self.primaryPosition = primaryPosition
self.height = height
self.weight = weight
self.totalYearsExperience = totalYearsExperience
self.nbaSeason = nbaSeason
self.dateOfMoment= dateOfMoment
self.playCategory = playCategory
self.playType = playType
self.homeTeamName = homeTeamName
self.awayTeamName = awayTeamName
self.homeTeamScore = homeTeamScore
self.awayTeamScore = awayTeamScore
self.seriesNumber = seriesNumber
self.setName = setName
self.serialNumber = serialNumber
self.playID = playID
self.setID = setID
self.numMomentsInEdition = numMomentsInEdition
}
}
// The resource that represents the Moment NFTs
//
access(all) resource NFT: NonFungibleToken.NFT {
// Global unique moment ID
access(all) let id: UInt64
// Struct of Moment metadata
access(all) let data: MomentData
init(serialNumber: UInt32, playID: UInt32, setID: UInt32, subeditionID: UInt32) {
// Increment the global Moment IDs
TopShot.totalSupply = TopShot.totalSupply + UInt64(1)
self.id = TopShot.totalSupply
// Set the metadata struct
self.data = MomentData(setID: setID, playID: playID, serialNumber: serialNumber)
emit MomentMinted(
momentID: self.id,
playID: playID,
setID: self.data.setID,
serialNumber: self.data.serialNumber,
subeditionID: subeditionID
)
}
// If the Moment is destroyed, emit an event to indicate
// to outside observers that it has been destroyed
access(all) event ResourceDestroyed(
id: UInt64 = self.id,
serialNumber: UInt32 = self.data.serialNumber,
playID: UInt32 = self.data.playID,
setID: UInt32 = self.data.setID
)
access(all) view fun name(): String {
let fullName: String = TopShot.getPlayMetaDataByField(playID: self.data.playID, field: "FullName") ?? ""
let playType: String = TopShot.getPlayMetaDataByField(playID: self.data.playID, field: "PlayType") ?? ""
return fullName
.concat(" ")
.concat(playType)
}
// The description of the Moment.
// If the Tagline prop exists, use is as the description; else, build the description using set, series, and serial number.
access(all) view fun description(): String {
// Return early if the tagline is non-empty
if let tagline = TopShot.getPlayMetaDataByField(playID: self.data.playID, field: "Tagline") {
return tagline
}
// Build the description using set name, series number, and serial number
let setName: String = TopShot.getSetName(setID: self.data.setID) ?? ""
let serialNumber: String = self.data.serialNumber.toString()
let seriesNumber: String = TopShot.getSetSeries(setID: self.data.setID)?.toString() ?? ""
return "A series "
.concat(seriesNumber)
.concat(" ")
.concat(setName)
.concat(" moment with serial number ")
.concat(serialNumber)
}
// All supported metadata views for the Moment including the Core NFT Views
access(all) view fun getViews(): [Type] {
return [
Type<MetadataViews.Display>(),
Type<TopShotMomentMetadataView>(),
Type<MetadataViews.Royalties>(),
Type<MetadataViews.Editions>(),
Type<MetadataViews.ExternalURL>(),
Type<MetadataViews.NFTCollectionData>(),
Type<MetadataViews.NFTCollectionDisplay>(),
Type<MetadataViews.Serial>(),
Type<MetadataViews.Traits>(),
Type<MetadataViews.Medias>()
]
}
// resolves the view with the given type for the NFT
access(all) fun resolveView(_ view: Type): AnyStruct? {
switch view {
case Type<MetadataViews.Display>():
return MetadataViews.Display(
name: self.name(),
description: self.description(),
thumbnail: MetadataViews.HTTPFile(url: self.thumbnail())
)
// Custom metadata view unique to TopShot Moments
case Type<TopShotMomentMetadataView>():
return TopShotMomentMetadataView(
fullName: TopShot.getPlayMetaDataByField(playID: self.data.playID, field: "FullName"),
firstName: TopShot.getPlayMetaDataByField(playID: self.data.playID, field: "FirstName"),
lastName: TopShot.getPlayMetaDataByField(playID: self.data.playID, field: "LastName"),
birthdate: TopShot.getPlayMetaDataByField(playID: self.data.playID, field: "Birthdate"),
birthplace: TopShot.getPlayMetaDataByField(playID: self.data.playID, field: "Birthplace"),
jerseyNumber: TopShot.getPlayMetaDataByField(playID: self.data.playID, field: "JerseyNumber"),
draftTeam: TopShot.getPlayMetaDataByField(playID: self.data.playID, field: "DraftTeam"),
draftYear: TopShot.getPlayMetaDataByField(playID: self.data.playID, field: "DraftYear"),
draftSelection: TopShot.getPlayMetaDataByField(playID: self.data.playID, field: "DraftSelection"),
draftRound: TopShot.getPlayMetaDataByField(playID: self.data.playID, field: "DraftRound"),
teamAtMomentNBAID: TopShot.getPlayMetaDataByField(playID: self.data.playID, field: "TeamAtMomentNBAID"),
teamAtMoment: TopShot.getPlayMetaDataByField(playID: self.data.playID, field: "TeamAtMoment"),
primaryPosition: TopShot.getPlayMetaDataByField(playID: self.data.playID, field: "PrimaryPosition"),
height: TopShot.getPlayMetaDataByField(playID: self.data.playID, field: "Height"),
weight: TopShot.getPlayMetaDataByField(playID: self.data.playID, field: "Weight"),
totalYearsExperience: TopShot.getPlayMetaDataByField(playID: self.data.playID, field: "TotalYearsExperience"),
nbaSeason: TopShot.getPlayMetaDataByField(playID: self.data.playID, field: "NbaSeason"),
dateOfMoment: TopShot.getPlayMetaDataByField(playID: self.data.playID, field: "DateOfMoment"),
playCategory: TopShot.getPlayMetaDataByField(playID: self.data.playID, field: "PlayCategory"),
playType: TopShot.getPlayMetaDataByField(playID: self.data.playID, field: "PlayType"),
homeTeamName: TopShot.getPlayMetaDataByField(playID: self.data.playID, field: "HomeTeamName"),
awayTeamName: TopShot.getPlayMetaDataByField(playID: self.data.playID, field: "AwayTeamName"),
homeTeamScore: TopShot.getPlayMetaDataByField(playID: self.data.playID, field: "HomeTeamScore"),
awayTeamScore: TopShot.getPlayMetaDataByField(playID: self.data.playID, field: "AwayTeamScore"),
seriesNumber: TopShot.getSetSeries(setID: self.data.setID),
setName: TopShot.getSetName(setID: self.data.setID),
serialNumber: self.data.serialNumber,
playID: self.data.playID,
setID: self.data.setID,
numMomentsInEdition: TopShot.getNumMomentsInEdition(setID: self.data.setID, playID: self.data.playID)
)
case Type<MetadataViews.Editions>():
let name = self.getEditionName()
let max = TopShot.getNumMomentsInEdition(setID: self.data.setID, playID: self.data.playID) ?? 0
let editionInfo = MetadataViews.Edition(name: name, number: UInt64(self.data.serialNumber), max: max > 0 ? UInt64(max) : nil)
let editionList: [MetadataViews.Edition] = [editionInfo]
return MetadataViews.Editions(
editionList
)
case Type<MetadataViews.Serial>():
return MetadataViews.Serial(
UInt64(self.data.serialNumber)
)
case Type<MetadataViews.Royalties>():
return TopShot.resolveContractView(resourceType: nil, viewType: Type<MetadataViews.Royalties>())
case Type<MetadataViews.ExternalURL>():
return MetadataViews.ExternalURL(self.getMomentURL())
case Type<MetadataViews.NFTCollectionData>():
return TopShot.resolveContractView(resourceType: nil, viewType: Type<MetadataViews.NFTCollectionData>())
case Type<MetadataViews.NFTCollectionDisplay>():
return TopShot.resolveContractView(resourceType: nil, viewType: Type<MetadataViews.NFTCollectionDisplay>())
case Type<MetadataViews.Traits>():
return self.resolveTraitsView()
case Type<MetadataViews.Medias>():
return MetadataViews.Medias(
[
MetadataViews.Media(
file: MetadataViews.HTTPFile(
url: self.mediumimage()
),
mediaType: "image/jpeg"
),
MetadataViews.Media(
file: MetadataViews.HTTPFile(
url: self.video()
),
mediaType: "video/mp4"
)
]
)
}
return nil
}
// resolves this NFT's Traits view
access(all) fun resolveTraitsView(): MetadataViews.Traits {
// sports radar team id
let excludedNames: [String] = ["TeamAtMomentNBAID"]
// Get subedition
let subedition = TopShot.getSubeditionByNFTID(self.id)
// Create a dictionary of this NFT's traits with default metadata
var traits: {String: AnyStruct} = {
"SeriesNumber": TopShot.getSetSeries(setID: self.data.setID),
"SetName": TopShot.getSetName(setID: self.data.setID),
"SerialNumber": self.data.serialNumber,
"Locked": TopShotLocking.isLocked(nftRef: &self as &{NonFungibleToken.NFT}),
"Subedition": subedition?.name ?? "Standard",
"SubeditionID": subedition?.subeditionID ?? 0
}
// Add play specific data
traits = self.mapPlayData(dict: traits)
return MetadataViews.dictToTraits(dict: traits, excludedNames: excludedNames)
}
// Functions used for computing MetadataViews
// mapPlayData helps build our trait map from play metadata
// Returns: The trait map with all non-empty fields from play data added
access(all) fun mapPlayData(dict: {String: AnyStruct}) : {String: AnyStruct} {
let playMetadata = TopShot.getPlayMetaData(playID: self.data.playID) ?? {}
for name in playMetadata.keys {
let value = playMetadata[name] ?? ""
if value != "" {
dict.insert(key: name, value)
}
}
return dict
}
// getMomentURL
// Returns: The computed external url of the moment
access(all) view fun getMomentURL(): String {
return "https://nbatopshot.com/moment/".concat(self.id.toString())
}
// getEditionName Moment's edition name is a combination of the Moment's setName and playID
// `setName: #playID`
access(all) view fun getEditionName(): String {
let setName: String = TopShot.getSetName(setID: self.data.setID) ?? ""
let editionName = setName.concat(": #").concat(self.data.playID.toString())
return editionName
}
access(all) view fun assetPath(): String {
return "https://assets.nbatopshot.com/media/".concat(self.id.toString())
}
// returns a url to display an medium sized image
access(all) view fun mediumimage(): String {
return self.appendOptionalParams(url: self.assetPath().concat("?width=512"), firstDelim: "&")
}
// a url to display a thumbnail associated with the moment
access(all) view fun thumbnail(): String {
return self.appendOptionalParams(url: self.assetPath().concat("?width=256"), firstDelim: "&")
}
// a url to display a video associated with the moment
access(all) view fun video(): String {
return self.appendOptionalParams(url: self.assetPath().concat("/video"), firstDelim: "?")
}
// appends and optional network param needed to resolve the media
access(all) view fun appendOptionalParams(url: String, firstDelim: String): String {
if TopShot.Network() == "testnet" {
return url.concat(firstDelim).concat("testnet")
}
return url
}
// Create an empty Collection for TopShot NFTs and return it to the caller
access(all) fun createEmptyCollection(): @{NonFungibleToken.Collection} {
return <- TopShot.createEmptyCollection(nftType: Type<@NFT>())
}
}
// Admin is a special authorization resource that
// allows the owner to perform important functions to modify the
// various aspects of the Plays, Sets, and Moments
//
access(all) resource Admin {
// createPlay creates a new Play struct
// and stores it in the Plays dictionary in the TopShot smart contract
//
// Parameters: metadata: A dictionary mapping metadata titles to their data
// example: {"Player Name": "Kevin Durant", "Height": "7 feet"}
// (because we all know Kevin Durant is not 6'9")
//
// Returns: the ID of the new Play object
//
access(all) fun createPlay(metadata: {String: String}): UInt32 {
// Create the new Play
var newPlay = Play(metadata: metadata)
let newID = newPlay.playID
// Increment the ID so that it isn't used again
TopShot.nextPlayID = TopShot.nextPlayID + UInt32(1)
emit PlayCreated(id: newPlay.playID, metadata: metadata)
// Store it in the contract storage
TopShot.playDatas[newID] = newPlay
return newID
}
/// Temporarily enabled so the description of the play can be backfilled
/// Parameters: playID: The ID of the play to update
/// tagline: A string to be used as the tagline for the play
/// Returns: The ID of the play
access(all) fun updatePlayTagline(playID: UInt32, tagline: String): UInt32 {
let tmpPlay = TopShot.playDatas[playID]
?? panic("playID does not exist")
// Update the play's tagline
tmpPlay.updateTagline(tagline: tagline)
// Return the play's ID
return playID
}
// createSet creates a new Set resource and stores it
// in the sets mapping in the TopShot contract
//
// Parameters: name: The name of the Set
//
// Returns: The ID of the created set
access(all) fun createSet(name: String): UInt32 {
// Create the new Set
var newSet <- create Set(name: name)
// Increment the setID so that it isn't used again
TopShot.nextSetID = TopShot.nextSetID + UInt32(1)
let newID = newSet.setID
emit SetCreated(setID: newSet.setID, series: TopShot.currentSeries)
// Store it in the sets mapping field
TopShot.sets[newID] <-! newSet
return newID
}
// borrowSet returns a reference to a set in the TopShot
// contract so that the admin can call methods on it
//
// Parameters: setID: The ID of the Set that you want to
// get a reference to
//
// Returns: A reference to the Set with all of the fields
// and methods exposed
//
access(all) view fun borrowSet(setID: UInt32): &Set {
pre {
TopShot.sets[setID] != nil: "Cannot borrow Set: The Set doesn't exist"
}
// Get a reference to the Set and return it
// use `&` to indicate the reference to the object and type
return (&TopShot.sets[setID] as &Set?)!
}
// startNewSeries ends the current series by incrementing
// the series number, meaning that Moments minted after this
// will use the new series number
//
// Returns: The new series number
//