Smart Contract
Eternal
A.c38aea683c0c4d38.Eternal
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