Smart Contract

Eternal

A.c38aea683c0c4d38.Eternal

Deployed

3h ago
Mar 01, 2026, 12:08:06 PM UTC

Dependents

0 imports
1/*
2    This contract is mostly copied from the "TopShot" contract except with
3    names changed.
4*/
5
6import NonFungibleToken from 0x1d7e57aa55817448
7import MetadataViews from 0x1d7e57aa55817448
8
9pub contract Eternal: NonFungibleToken {
10
11    // -----------------------------------------------------------------------
12    // Eternal contract Events
13    // -----------------------------------------------------------------------
14
15    // Emitted when the Eternal contract is created
16    pub event ContractInitialized()
17
18    // Emitted when a new Play struct is created
19    pub event PlayCreated(id: UInt32, metadata: {String:String})
20    // Emitted when a new series has been triggered by an admin
21    pub event NewSeriesStarted(newCurrentSeries: UInt32)
22
23    // Events for Set-Related actions
24    //
25    // Emitted when a new Set is created
26    pub event SetCreated(setID: UInt32, series: UInt32)
27    // Emitted when a new Play is added to a Set
28    pub event PlayAddedToSet(setID: UInt32, playID: UInt32)
29    // Emitted when a Play is retired from a Set and cannot be used to mint
30    pub event PlayRetiredFromSet(setID: UInt32, playID: UInt32, numMoments: UInt32)
31    // Emitted when a Set is locked, meaning Plays cannot be added
32    pub event SetLocked(setID: UInt32)
33    // Emitted when a Moment is minted from a Set
34    pub event MomentMinted(momentID: UInt64, playID: UInt32, setID: UInt32, serialNumber: UInt32)
35
36    // Events for Collection-related actions
37    //
38    // Emitted when a moment is withdrawn from a Collection
39    pub event Withdraw(id: UInt64, from: Address?)
40    // Emitted when a moment is deposited into a Collection
41    pub event Deposit(id: UInt64, to: Address?)
42
43    // Emitted when a Moment is destroyed
44    pub event MomentDestroyed(id: UInt64)
45
46    // -----------------------------------------------------------------------
47    // Eternal contract-level fields.
48    // These contain actual values that are stored in the smart contract.
49    // -----------------------------------------------------------------------
50
51    // Series that this Set belongs to.
52    // Series is a concept that indicates a group of Sets through time.
53    // Many Sets can exist at a time, but only one series.
54    pub var currentSeries: UInt32
55
56    // Variable size dictionary of Play structs
57    access(self) var playDatas: {UInt32: Play}
58
59    // Variable size dictionary of SetData structs
60    access(self) var setDatas: {UInt32: SetData}
61
62    // Variable size dictionary of Set resources
63    access(self) var sets: @{UInt32: Set}
64
65    // The ID that is used to create Plays. 
66    // Every time a Play is created, playID is assigned 
67    // to the new Play's ID and then is incremented by 1.
68    pub var nextPlayID: UInt32
69
70    // The ID that is used to create Sets. Every time a Set is created
71    // setID is assigned to the new set's ID and then is incremented by 1.
72    pub var nextSetID: UInt32
73
74    // The total number of Eternal Moment NFTs that have been created
75    // Because NFTs can be destroyed, it doesn't necessarily mean that this
76    // reflects the total number of NFTs in existence, just the number that
77    // have been minted to date. Also used as global moment IDs for minting.
78    pub var totalSupply: UInt64
79
80    // -----------------------------------------------------------------------
81    // Eternal contract-level Composite Type definitions
82    // -----------------------------------------------------------------------
83    // These are just *definitions* for Types that this contract
84    // and other accounts can use. These definitions do not contain
85    // actual stored values, but an instance (or object) of one of these Types
86    // can be created by this contract that contains stored values.
87    // -----------------------------------------------------------------------
88
89    // Play is a Struct that holds metadata associated 
90    // with a specific NBA play, like the legendary moment when 
91    // Ray Allen hit the 3 to tie the Heat and Spurs in the 2013 finals game 6
92    // or when Lance Stephenson blew in the ear of Lebron James.
93    //
94    // Moment NFTs will all reference a single play as the owner of
95    // its metadata. The plays are publicly accessible, so anyone can
96    // read the metadata associated with a specific play ID
97    //
98    pub struct Play {
99
100        // The unique ID for the Play
101        pub let playID: UInt32
102
103        // Stores all the metadata about the play as a string mapping
104        // This is not the long term way NFT metadata will be stored. It's a temporary
105        // construct while we figure out a better way to do metadata.
106        //
107        pub let metadata: {String: String}
108
109        init(metadata: {String: String}) {
110            pre {
111                metadata.length != 0: "New Play metadata cannot be empty"
112            }
113            self.playID = Eternal.nextPlayID
114            self.metadata = metadata
115
116            // Increment the ID so that it isn't used again
117            Eternal.nextPlayID = Eternal.nextPlayID + UInt32(1)
118
119            emit PlayCreated(id: self.playID, metadata: metadata)
120        }
121    }
122
123    // A Set is a grouping of Plays that have occured in the real world
124    // that make up a related group of collectibles, like sets of baseball
125    // or Magic cards. A Play can exist in multiple different sets.
126    // 
127    // SetData is a struct that is stored in a field of the contract.
128    // Anyone can query the constant information
129    // about a set by calling various getters located 
130    // at the end of the contract. Only the admin has the ability 
131    // to modify any data in the private Set resource.
132    //
133    pub struct SetData {
134
135        // Unique ID for the Set
136        pub let setID: UInt32
137
138        // Name of the Set
139        // ex. "Times when the Toronto Raptors choked in the playoffs"
140        pub let name: String
141
142        // Series that this Set belongs to.
143        // Series is a concept that indicates a group of Sets through time.
144        // Many Sets can exist at a time, but only one series.
145        pub let series: UInt32
146
147        init(name: String) {
148            pre {
149                name.length > 0: "New Set name cannot be empty"
150            }
151            self.setID = Eternal.nextSetID
152            self.name = name
153            self.series = Eternal.currentSeries
154
155            // Increment the setID so that it isn't used again
156            Eternal.nextSetID = Eternal.nextSetID + UInt32(1)
157
158            emit SetCreated(setID: self.setID, series: self.series)
159        }
160    }
161
162    // Set is a resource type that contains the functions to add and remove
163    // Plays from a set and mint Moments.
164    //
165    // It is stored in a private field in the contract so that
166    // the admin resource can call its methods.
167    //
168    // The admin can add Plays to a Set so that the set can mint Moments
169    // that reference that playdata.
170    // The Moments that are minted by a Set will be listed as belonging to
171    // the Set that minted it, as well as the Play it references.
172    // 
173    // Admin can also retire Plays from the Set, meaning that the retired
174    // Play can no longer have Moments minted from it.
175    //
176    // If the admin locks the Set, no more Plays can be added to it, but 
177    // Moments can still be minted.
178    //
179    // If retireAll() and lock() are called back-to-back, 
180    // the Set is closed off forever and nothing more can be done with it.
181    pub resource Set {
182
183        // Unique ID for the set
184        pub let setID: UInt32
185
186        // Array of plays that are a part of this set.
187        // When a play is added to the set, its ID gets appended here.
188        // The ID does not get removed from this array when a Play is retired.
189        pub var plays: [UInt32]
190
191        // Map of Play IDs that Indicates if a Play in this Set can be minted.
192        // When a Play is added to a Set, it is mapped to false (not retired).
193        // When a Play is retired, this is set to true and cannot be changed.
194        pub var retired: {UInt32: Bool}
195
196        // Indicates if the Set is currently locked.
197        // When a Set is created, it is unlocked 
198        // and Plays are allowed to be added to it.
199        // When a set is locked, Plays cannot be added.
200        // A Set can never be changed from locked to unlocked,
201        // the decision to lock a Set it is final.
202        // If a Set is locked, Plays cannot be added, but
203        // Moments can still be minted from Plays
204        // that exist in the Set.
205        pub var locked: Bool
206
207        // Mapping of Play IDs that indicates the number of Moments 
208        // that have been minted for specific Plays in this Set.
209        // When a Moment is minted, this value is stored in the Moment to
210        // show its place in the Set, eg. 13 of 60.
211        pub var numberMintedPerPlay: {UInt32: UInt32}
212
213        init(name: String) {
214            self.setID = Eternal.nextSetID
215            self.plays = []
216            self.retired = {}
217            self.locked = false
218            self.numberMintedPerPlay = {}
219
220            // Create a new SetData for this Set and store it in contract storage
221            Eternal.setDatas[self.setID] = SetData(name: name)
222        }
223
224        // addPlay adds a play to the set
225        //
226        // Parameters: playID: The ID of the Play that is being added
227        //
228        // Pre-Conditions:
229        // The Play needs to be an existing play
230        // The Set needs to be not locked
231        // The Play can't have already been added to the Set
232        //
233        pub fun addPlay(playID: UInt32) {
234            pre {
235                Eternal.playDatas[playID] != nil: "Cannot add the Play to Set: Play doesn't exist."
236                !self.locked: "Cannot add the play to the Set after the set has been locked."
237                self.numberMintedPerPlay[playID] == nil: "The play has already beed added to the set."
238            }
239
240            // Add the Play to the array of Plays
241            self.plays.append(playID)
242
243            // Open the Play up for minting
244            self.retired[playID] = false
245
246            // Initialize the Moment count to zero
247            self.numberMintedPerPlay[playID] = 0
248
249            emit PlayAddedToSet(setID: self.setID, playID: playID)
250        }
251
252        // addPlays adds multiple Plays to the Set
253        //
254        // Parameters: playIDs: The IDs of the Plays that are being added
255        //                      as an array
256        //
257        pub fun addPlays(playIDs: [UInt32]) {
258            for play in playIDs {
259                self.addPlay(playID: play)
260            }
261        }
262
263        // retirePlay retires a Play from the Set so that it can't mint new Moments
264        //
265        // Parameters: playID: The ID of the Play that is being retired
266        //
267        // Pre-Conditions:
268        // The Play is part of the Set and not retired (available for minting).
269        // 
270        pub fun retirePlay(playID: UInt32) {
271            pre {
272                self.retired[playID] != nil: "Cannot retire the Play: Play doesn't exist in this set!"
273            }
274
275            if !self.retired[playID]! {
276                self.retired[playID] = true
277
278                emit PlayRetiredFromSet(setID: self.setID, playID: playID, numMoments: self.numberMintedPerPlay[playID]!)
279            }
280        }
281
282        // retireAll retires all the plays in the Set
283        // Afterwards, none of the retired Plays will be able to mint new Moments
284        //
285        pub fun retireAll() {
286            for play in self.plays {
287                self.retirePlay(playID: play)
288            }
289        }
290
291        // lock() locks the Set so that no more Plays can be added to it
292        //
293        // Pre-Conditions:
294        // The Set should not be locked
295        pub fun lock() {
296            if !self.locked {
297                self.locked = true
298                emit SetLocked(setID: self.setID)
299            }
300        }
301
302        // mintMoment mints a new Moment and returns the newly minted Moment
303        // 
304        // Parameters: playID: The ID of the Play that the Moment references
305        //
306        // Pre-Conditions:
307        // The Play must exist in the Set and be allowed to mint new Moments
308        //
309        // Returns: The NFT that was minted
310        // 
311        pub fun mintMoment(playID: UInt32): @NFT {
312            pre {
313                self.retired[playID] != nil: "Cannot mint the moment: This play doesn't exist."
314                !self.retired[playID]!: "Cannot mint the moment from this play: This play has been retired."
315            }
316
317            // Gets the number of Moments that have been minted for this Play
318            // to use as this Moment's serial number
319            let numInPlay = self.numberMintedPerPlay[playID]!
320
321            // Mint the new moment
322            let newMoment: @NFT <- create NFT(serialNumber: numInPlay + UInt32(1),
323                                              playID: playID,
324                                              setID: self.setID)
325
326            // Increment the count of Moments minted for this Play
327            self.numberMintedPerPlay[playID] = numInPlay + UInt32(1)
328
329            return <-newMoment
330        }
331
332        // batchMintMoment mints an arbitrary quantity of Moments 
333        // and returns them as a Collection
334        //
335        // Parameters: playID: the ID of the Play that the Moments are minted for
336        //             quantity: The quantity of Moments to be minted
337        //
338        // Returns: Collection object that contains all the Moments that were minted
339        //
340        pub fun batchMintMoment(playID: UInt32, quantity: UInt64): @Collection {
341            let newCollection <- create Collection()
342
343            var i: UInt64 = 0
344            while i < quantity {
345                newCollection.deposit(token: <-self.mintMoment(playID: playID))
346                i = i + UInt64(1)
347            }
348
349            return <-newCollection
350        }
351    }
352
353    pub struct MomentData {
354
355        // The ID of the Set that the Moment comes from
356        pub let setID: UInt32
357
358        // The ID of the Play that the Moment references
359        pub let playID: UInt32
360
361        // The place in the edition that this Moment was minted
362        // Otherwise know as the serial number
363        pub let serialNumber: UInt32
364
365        init(setID: UInt32, playID: UInt32, serialNumber: UInt32) {
366            self.setID = setID
367            self.playID = playID
368            self.serialNumber = serialNumber
369        }
370
371    }
372
373    // The resource that represents the Moment NFTs
374    //
375    pub resource NFT: NonFungibleToken.INFT, MetadataViews.Resolver {
376
377        // Global unique moment ID
378        pub let id: UInt64
379        
380        // Struct of Moment metadata
381        pub let data: MomentData
382
383        init(serialNumber: UInt32, playID: UInt32, setID: UInt32) {
384            // Increment the global Moment IDs
385            Eternal.totalSupply = Eternal.totalSupply + UInt64(1)
386
387            self.id = Eternal.totalSupply
388
389            // Set the metadata struct
390            self.data = MomentData(setID: setID, playID: playID, serialNumber: serialNumber)
391
392            emit MomentMinted(momentID: self.id, playID: playID, setID: self.data.setID, serialNumber: self.data.serialNumber)
393        }
394
395        // If the Moment is destroyed, emit an event to indicate 
396        // to outside ovbservers that it has been destroyed
397        destroy() {
398            emit MomentDestroyed(id: self.id)
399        }
400
401        pub fun name(): String {
402            let influencer: String = Eternal.getPlayMetaDataByField(playID: self.data.playID, field: "Influencer") ?? ""
403            let title: String = Eternal.getPlayMetaDataByField(playID: self.data.playID, field: "Title") ?? ""
404            let game: String = Eternal.getPlayMetaDataByField(playID: self.data.playID, field: "Game") ?? ""
405            return influencer
406                .concat(" - ")
407                .concat(game)
408                .concat(" - ")
409                .concat(title)
410        }
411
412        pub fun description(): String {
413            let influencer: String = Eternal.getPlayMetaDataByField(playID: self.data.playID, field: "Influencer") ?? ""
414            let title: String = Eternal.getPlayMetaDataByField(playID: self.data.playID, field: "Title") ?? ""
415            let game: String = Eternal.getPlayMetaDataByField(playID: self.data.playID, field: "Game") ?? ""
416            let rarity: String = Eternal.getPlayMetaDataByField(playID: self.data.playID, field: "Rarity") ?? ""
417            let serialNumber: String = self.data.serialNumber.toString()
418            return influencer
419                .concat("'s ")
420                .concat(rarity)
421                .concat(" \"")
422                .concat(title)
423                .concat("\" moment in ")
424                .concat(game)
425                .concat(", with serial number ")
426                .concat(serialNumber)
427        }
428
429        pub fun getViews(): [Type] {
430            return [
431                Type<MetadataViews.Display>(),
432                Type<MetadataViews.Editions>(),
433                Type<MetadataViews.ExternalURL>(),
434                Type<MetadataViews.NFTCollectionData>(),
435                Type<MetadataViews.NFTCollectionDisplay>(),
436                Type<MetadataViews.Serial>(),
437                Type<MetadataViews.Royalties>(),
438                Type<MetadataViews.Traits>(),
439                Type<MetadataViews.Medias>()
440            ]
441        }
442
443        pub fun resolveView(_ view: Type): AnyStruct? {
444            switch view {
445                case Type<MetadataViews.Display>():
446                    return MetadataViews.Display(
447                        name: self.name(),
448                        description: self.description(),
449                        thumbnail: MetadataViews.HTTPFile(url: self.thumbnail())
450                    )
451                case Type<MetadataViews.Editions>():
452                    let name = self.name()
453                    let max = Eternal.getNumMomentsInEdition(setID: self.data.setID, playID: self.data.playID) ?? 0
454                    let editionInfo = MetadataViews.Edition(name: name, number: UInt64(self.data.serialNumber), max: max > 0 ? UInt64(max) : nil)
455                    let editionList: [MetadataViews.Edition] = [editionInfo]
456                    return MetadataViews.Editions(
457                        editionList
458                    )
459                case Type<MetadataViews.Serial>():
460                    return MetadataViews.Serial(
461                        UInt64(self.data.serialNumber)
462                    )
463                case Type<MetadataViews.Royalties>():
464                    return MetadataViews.Royalties(
465                        royalties: []
466                    )
467                case Type<MetadataViews.ExternalURL>():
468                    return MetadataViews.ExternalURL(self.getMomentURL())
469                case Type<MetadataViews.NFTCollectionData>():
470                    return MetadataViews.NFTCollectionData(
471                        storagePath: /storage/EternalMomentCollection,
472                        publicPath: /public/EternalMomentCollection,
473                        providerPath: /private/EternalMomentCollection,
474                        publicCollection: Type<&Eternal.Collection{Eternal.MomentCollectionPublic}>(),
475                        publicLinkedType: Type<&Eternal.Collection{Eternal.MomentCollectionPublic,NonFungibleToken.Receiver,NonFungibleToken.CollectionPublic,MetadataViews.ResolverCollection}>(),
476                        providerLinkedType: Type<&Eternal.Collection{NonFungibleToken.Provider,Eternal.MomentCollectionPublic,NonFungibleToken.Receiver,NonFungibleToken.CollectionPublic,MetadataViews.ResolverCollection}>(),
477                        createEmptyCollectionFunction: (fun (): @NonFungibleToken.Collection {
478                            return <-Eternal.createEmptyCollection()
479                        })
480                    )
481                case Type<MetadataViews.NFTCollectionDisplay>():
482                    let bannerImage = MetadataViews.Media(
483                        file: MetadataViews.HTTPFile(
484                            url: "https://eternal-zelos.s3.us-west-2.amazonaws.com/images/eternal-banner.jpeg"
485                        ),
486                        mediaType: "image/jpeg"
487                    )
488                    let squareImage = MetadataViews.Media(
489                        file: MetadataViews.HTTPFile(
490                            url: "https://eternal-zelos.s3.us-west-2.amazonaws.com/images/eternal-square.jpeg"
491                        ),
492                        mediaType: "image/jpeg"
493                    )
494                    return MetadataViews.NFTCollectionDisplay(
495                        name: "Eternal",
496                        description: "The best moments in streaming history.",
497                        externalURL: MetadataViews.ExternalURL("https://eternal.gg"),
498                        squareImage: squareImage,
499                        bannerImage: bannerImage,
500                        socials: {}
501                    )
502                case Type<MetadataViews.Traits>():
503                    // non play specific traits
504                    let traitDictionary: {String: AnyStruct} = {
505                        "SeriesNumber": Eternal.getSetSeries(setID: self.data.setID),
506                        "SetName": Eternal.getSetName(setID: self.data.setID),
507                        "SerialNumber": self.data.serialNumber
508                    }
509                    // add play specific data
510                    let fullDictionary = self.mapPlayData(dict: traitDictionary)
511                    return MetadataViews.dictToTraits(dict: fullDictionary, excludedNames: [])
512                case Type<MetadataViews.Medias>():
513                    return MetadataViews.Medias(
514                        items: [
515                            MetadataViews.Media(
516                                file: MetadataViews.HTTPFile(
517                                    url: self.thumbnail()
518                                ),
519                                mediaType: "image/png"
520                            ),
521                            MetadataViews.Media(
522                                file: MetadataViews.IPFSFile(
523                                    cid: Eternal.getPlayMetaDataByField(playID: self.data.playID, field: "Hash") ?? "",
524                                    path: ""
525                                ),
526                                mediaType: "video/mp4"
527                            )
528                        ]
529                    )
530            }
531
532            return nil
533        }   
534
535        pub fun mapPlayData(dict: {String: AnyStruct}) : {String: AnyStruct} {      
536            let playMetadata = Eternal.getPlayMetaData(playID: self.data.playID) ?? {}
537            for name in playMetadata.keys {
538                let value = playMetadata[name] ?? ""
539                if value != "" {
540                    dict.insert(key: name, value)
541                }
542            }
543            return dict
544        }
545
546        pub fun thumbnail(): String {
547            return "https://eternal-zelos.s3.us-west-2.amazonaws.com/moment-thumbnails/".concat(self.id.toString()).concat(".png")
548        }
549
550        pub fun getMomentURL(): String {
551            return "https://eternal.gg/moments/".concat(self.id.toString())
552        }
553
554    }
555
556    // Admin is a special authorization resource that 
557    // allows the owner to perform important functions to modify the 
558    // various aspects of the Plays, Sets, and Moments
559    //
560    pub resource Admin {
561
562        // createPlay creates a new Play struct 
563        // and stores it in the Plays dictionary in the Eternal smart contract
564        //
565        // Parameters: metadata: A dictionary mapping metadata titles to their data
566        //                       example: {"Player Name": "Kevin Durant", "Height": "7 feet"}
567        //                               (because we all know Kevin Durant is not 6'9")
568        //
569        // Returns: the ID of the new Play object
570        //
571        pub fun createPlay(metadata: {String: String}): UInt32 {
572            // Create the new Play
573            var newPlay = Play(metadata: metadata)
574            let newID = newPlay.playID
575
576            // Store it in the contract storage
577            Eternal.playDatas[newID] = newPlay
578
579            return newID
580        }
581
582        // createSet creates a new Set resource and stores it
583        // in the sets mapping in the Eternal contract
584        //
585        // Parameters: name: The name of the Set
586        //
587        pub fun createSet(name: String) {
588            // Create the new Set
589            var newSet <- create Set(name: name)
590
591            // Store it in the sets mapping field
592            Eternal.sets[newSet.setID] <-! newSet
593        }
594
595        // borrowSet returns a reference to a set in the Eternal
596        // contract so that the admin can call methods on it
597        //
598        // Parameters: setID: The ID of the Set that you want to
599        // get a reference to
600        //
601        // Returns: A reference to the Set with all of the fields
602        // and methods exposed
603        //
604        pub fun borrowSet(setID: UInt32): &Set {
605            pre {
606                Eternal.sets[setID] != nil: "Cannot borrow Set: The Set doesn't exist"
607            }
608            
609            // Get a reference to the Set and return it
610            // use `&` to indicate the reference to the object and type
611            return (&Eternal.sets[setID] as &Set?)!
612        }
613
614        // startNewSeries ends the current series by incrementing
615        // the series number, meaning that Moments minted after this
616        // will use the new series number
617        //
618        // Returns: The new series number
619        //
620        pub fun startNewSeries(): UInt32 {
621            // End the current series and start a new one
622            // by incrementing the Eternal series number
623            Eternal.currentSeries = Eternal.currentSeries + UInt32(1)
624
625            emit NewSeriesStarted(newCurrentSeries: Eternal.currentSeries)
626
627            return Eternal.currentSeries
628        }
629
630        // createNewAdmin creates a new Admin resource
631        //
632        pub fun createNewAdmin(): @Admin {
633            return <-create Admin()
634        }
635    }
636
637    // This is the interface that users can cast their Moment Collection as
638    // to allow others to deposit Moments into their Collection. It also allows for reading
639    // the IDs of Moments in the Collection.
640    pub resource interface MomentCollectionPublic {
641        pub fun deposit(token: @NonFungibleToken.NFT)
642        pub fun batchDeposit(tokens: @NonFungibleToken.Collection)
643        pub fun getIDs(): [UInt64]
644        pub fun borrowNFT(id: UInt64): &NonFungibleToken.NFT
645        pub fun borrowMoment(id: UInt64): &Eternal.NFT? {
646            // If the result isn't nil, the id of the returned reference
647            // should be the same as the argument to the function
648            post {
649                (result == nil) || (result?.id == id): 
650                    "Cannot borrow Moment reference: The ID of the returned reference is incorrect"
651            }
652        }
653    }
654
655    // Collection is a resource that every user who owns NFTs 
656    // will store in their account to manage their NFTS
657    //
658    pub resource Collection: MomentCollectionPublic, NonFungibleToken.Provider, NonFungibleToken.Receiver, NonFungibleToken.CollectionPublic, MetadataViews.ResolverCollection { 
659        // Dictionary of Moment conforming tokens
660        // NFT is a resource type with a UInt64 ID field
661        pub var ownedNFTs: @{UInt64: NonFungibleToken.NFT}
662
663        init() {
664            self.ownedNFTs <- {}
665        }
666
667        // withdraw removes an Moment from the Collection and moves it to the caller
668        //
669        // Parameters: withdrawID: The ID of the NFT 
670        // that is to be removed from the Collection
671        //
672        // returns: @NonFungibleToken.NFT the token that was withdrawn
673        pub fun withdraw(withdrawID: UInt64): @NonFungibleToken.NFT {
674
675            // Remove the nft from the Collection
676            let token <- self.ownedNFTs.remove(key: withdrawID) 
677                ?? panic("Cannot withdraw: Moment does not exist in the collection")
678
679            emit Withdraw(id: token.id, from: self.owner?.address)
680            
681            // Return the withdrawn token
682            return <-token
683        }
684
685        // batchWithdraw withdraws multiple tokens and returns them as a Collection
686        //
687        // Parameters: ids: An array of IDs to withdraw
688        //
689        // Returns: @NonFungibleToken.Collection: A collection that contains
690        //                                        the withdrawn moments
691        //
692        pub fun batchWithdraw(ids: [UInt64]): @NonFungibleToken.Collection {
693            // Create a new empty Collection
694            var batchCollection <- create Collection()
695            
696            // Iterate through the ids and withdraw them from the Collection
697            for id in ids {
698                batchCollection.deposit(token: <-self.withdraw(withdrawID: id))
699            }
700            
701            // Return the withdrawn tokens
702            return <-batchCollection
703        }
704
705        // deposit takes a Moment and adds it to the Collections dictionary
706        //
707        // Paramters: token: the NFT to be deposited in the collection
708        //
709        pub fun deposit(token: @NonFungibleToken.NFT) {
710            
711            // Cast the deposited token as a Eternal NFT to make sure
712            // it is the correct type
713            let token <- token as! @Eternal.NFT
714
715            // Get the token's ID
716            let id = token.id
717
718            // Add the new token to the dictionary
719            let oldToken <- self.ownedNFTs[id] <- token
720
721            // Only emit a deposit event if the Collection 
722            // is in an account's storage
723            if self.owner?.address != nil {
724                emit Deposit(id: id, to: self.owner?.address)
725            }
726
727            // Destroy the empty old token that was "removed"
728            destroy oldToken
729        }
730
731        // batchDeposit takes a Collection object as an argument
732        // and deposits each contained NFT into this Collection
733        pub fun batchDeposit(tokens: @NonFungibleToken.Collection) {
734
735            // Get an array of the IDs to be deposited
736            let keys = tokens.getIDs()
737
738            // Iterate through the keys in the collection and deposit each one
739            for key in keys {
740                self.deposit(token: <-tokens.withdraw(withdrawID: key))
741            }
742
743            // Destroy the empty Collection
744            destroy tokens
745        }
746
747        // getIDs returns an array of the IDs that are in the Collection
748        pub fun getIDs(): [UInt64] {
749            return self.ownedNFTs.keys
750        }
751
752        // borrowNFT Returns a borrowed reference to a Moment in the Collection
753        // so that the caller can read its ID
754        //
755        // Parameters: id: The ID of the NFT to get the reference for
756        //
757        // Returns: A reference to the NFT
758        //
759        // Note: This only allows the caller to read the ID of the NFT,
760        // not any Eternal specific data. Please use borrowMoment to 
761        // read Moment data.
762        //
763        pub fun borrowNFT(id: UInt64): &NonFungibleToken.NFT {
764            return (&self.ownedNFTs[id] as &NonFungibleToken.NFT?)!
765        }
766
767        // borrowMoment returns a borrowed reference to a Moment
768        // so that the caller can read data and call methods from it.
769        // They can use this to read its setID, playID, serialNumber,
770        // or any of the setData or Play data associated with it by
771        // getting the setID or playID and reading those fields from
772        // the smart contract.
773        //
774        // Parameters: id: The ID of the NFT to get the reference for
775        //
776        // Returns: A reference to the NFT
777        pub fun borrowMoment(id: UInt64): &Eternal.NFT? {
778            if self.ownedNFTs[id] != nil {
779                let ref = (&self.ownedNFTs[id] as auth &NonFungibleToken.NFT?)!
780                return ref as! &Eternal.NFT
781            } else {
782                return nil
783            }
784        }
785
786        pub fun borrowViewResolver(id: UInt64): &AnyResource{MetadataViews.Resolver} {
787            let nft = (&self.ownedNFTs[id] as auth &NonFungibleToken.NFT?)! 
788            let eternalNFT = nft as! &Eternal.NFT
789            return eternalNFT as &AnyResource{MetadataViews.Resolver}
790        }
791
792        // If a transaction destroys the Collection object,
793        // All the NFTs contained within are also destroyed!
794        // Much like when Damien Lillard destroys the hopes and
795        // dreams of the entire city of Houston.
796        //
797        destroy() {
798            destroy self.ownedNFTs
799        }
800    }
801
802    // -----------------------------------------------------------------------
803    // Eternal contract-level function definitions
804    // -----------------------------------------------------------------------
805
806    // createEmptyCollection creates a new, empty Collection object so that
807    // a user can store it in their account storage.
808    // Once they have a Collection in their storage, they are able to receive
809    // Moments in transactions.
810    //
811    pub fun createEmptyCollection(): @NonFungibleToken.Collection {
812        return <-create Eternal.Collection()
813    }
814
815    // getAllPlays returns all the plays in Eternal
816    //
817    // Returns: An array of all the plays that have been created
818    pub fun getAllPlays(): [Eternal.Play] {
819        return Eternal.playDatas.values
820    }
821
822    // getPlayMetaData returns all the metadata associated with a specific Play
823    // 
824    // Parameters: playID: The id of the Play that is being searched
825    //
826    // Returns: The metadata as a String to String mapping optional
827    pub fun getPlayMetaData(playID: UInt32): {String: String}? {
828        return self.playDatas[playID]?.metadata
829    }
830
831    // getPlayMetaDataByField returns the metadata associated with a 
832    //                        specific field of the metadata
833    //                        Ex: field: "Team" will return something
834    //                        like "Memphis Grizzlies"
835    // 
836    // Parameters: playID: The id of the Play that is being searched
837    //             field: The field to search for
838    //
839    // Returns: The metadata field as a String Optional
840    pub fun getPlayMetaDataByField(playID: UInt32, field: String): String? {
841        // Don't force a revert if the playID or field is invalid
842        if let play = Eternal.playDatas[playID] {
843            return play.metadata[field]
844        } else {
845            return nil
846        }
847    }
848
849    // getSetName returns the name that the specified Set
850    //            is associated with.
851    // 
852    // Parameters: setID: The id of the Set that is being searched
853    //
854    // Returns: The name of the Set
855    pub fun getSetName(setID: UInt32): String? {
856        // Don't force a revert if the setID is invalid
857        return Eternal.setDatas[setID]?.name
858    }
859
860    // getSetSeries returns the series that the specified Set
861    //              is associated with.
862    // 
863    // Parameters: setID: The id of the Set that is being searched
864    //
865    // Returns: The series that the Set belongs to
866    pub fun getSetSeries(setID: UInt32): UInt32? {
867        // Don't force a revert if the setID is invalid
868        return Eternal.setDatas[setID]?.series
869    }
870
871    // getSetIDsByName returns the IDs that the specified Set name
872    //                 is associated with.
873    // 
874    // Parameters: setName: The name of the Set that is being searched
875    //
876    // Returns: An array of the IDs of the Set if it exists, or nil if doesn't
877    pub fun getSetIDsByName(setName: String): [UInt32]? {
878        var setIDs: [UInt32] = []
879
880        // Iterate through all the setDatas and search for the name
881        for setData in Eternal.setDatas.values {
882            if setName == setData.name {
883                // If the name is found, return the ID
884                setIDs.append(setData.setID)
885            }
886        }
887
888        // If the name isn't found, return nil
889        // Don't force a revert if the setName is invalid
890        if setIDs.length == 0 {
891            return nil
892        } else {
893            return setIDs
894        }
895    }
896
897    // getPlaysInSet returns the list of Play IDs that are in the Set
898    // 
899    // Parameters: setID: The id of the Set that is being searched
900    //
901    // Returns: An array of Play IDs
902    pub fun getPlaysInSet(setID: UInt32): [UInt32]? {
903        // Don't force a revert if the setID is invalid
904        return Eternal.sets[setID]?.plays
905    }
906
907    // isEditionRetired returns a boolean that indicates if a Set/Play combo
908    //                  (otherwise known as an edition) is retired.
909    //                  If an edition is retired, it still remains in the Set,
910    //                  but Moments can no longer be minted from it.
911    // 
912    // Parameters: setID: The id of the Set that is being searched
913    //             playID: The id of the Play that is being searched
914    //
915    // Returns: Boolean indicating if the edition is retired or not
916    pub fun isEditionRetired(setID: UInt32, playID: UInt32): Bool? {
917        // Don't force a revert if the set or play ID is invalid
918        // Remove the set from the dictionary to get its field
919        if let setToRead <- Eternal.sets.remove(key: setID) {
920
921            // See if the Play is retired from this Set
922            let retired = setToRead.retired[playID]
923
924            // Put the Set back in the contract storage
925            Eternal.sets[setID] <-! setToRead
926
927            // Return the retired status
928            return retired
929        } else {
930
931            // If the Set wasn't found, return nil
932            return nil
933        }
934    }
935
936    // isSetLocked returns a boolean that indicates if a Set
937    //             is locked. If it's locked, 
938    //             new Plays can no longer be added to it,
939    //             but Moments can still be minted from Plays the set contains.
940    // 
941    // Parameters: setID: The id of the Set that is being searched
942    //
943    // Returns: Boolean indicating if the Set is locked or not
944    pub fun isSetLocked(setID: UInt32): Bool? {
945        // Don't force a revert if the setID is invalid
946        return Eternal.sets[setID]?.locked
947    }
948
949    // getNumMomentsInEdition return the number of Moments that have been 
950    //                        minted from a certain edition.
951    //
952    // Parameters: setID: The id of the Set that is being searched
953    //             playID: The id of the Play that is being searched
954    //
955    // Returns: The total number of Moments 
956    //          that have been minted from an edition
957    pub fun getNumMomentsInEdition(setID: UInt32, playID: UInt32): UInt32? {
958        // Don't force a revert if the Set or play ID is invalid
959        // Remove the Set from the dictionary to get its field
960        if let setToRead <- Eternal.sets.remove(key: setID) {
961
962            // Read the numMintedPerPlay
963            let amount = setToRead.numberMintedPerPlay[playID]
964
965            // Put the Set back into the Sets dictionary
966            Eternal.sets[setID] <-! setToRead
967
968            return amount
969        } else {
970            // If the set wasn't found return nil
971            return nil
972        }
973    }
974
975    // -----------------------------------------------------------------------
976    // Eternal initialization function
977    // -----------------------------------------------------------------------
978    //
979    init() {
980        // Initialize contract fields
981        self.currentSeries = 0
982        self.playDatas = {}
983        self.setDatas = {}
984        self.sets <- {}
985        self.nextPlayID = 1
986        self.nextSetID = 1
987        self.totalSupply = 0
988
989        // Put a new Collection in storage
990        self.account.save<@Collection>(<- create Collection(), to: /storage/EternalMomentCollection)
991
992        // Create a public capability for the Collection
993        self.account.link<&{MomentCollectionPublic}>(/public/EternalMomentCollection, target: /storage/EternalMomentCollection)
994
995        // Put the Minter in storage
996        self.account.save<@Admin>(<- create Admin(), to: /storage/EternalAdmin)
997
998        emit ContractInitialized()
999    }
1000}
1001
1002