Smart Contract

Genies

A.12450e4bb3b7666e.Genies

Deployed

3d ago
Feb 25, 2026, 03:36:54 PM UTC

Dependents

2 imports
1
2import NonFungibleToken from 0x1d7e57aa55817448
3import MetadataViews from 0x1d7e57aa55817448
4
5/*
6    Genies is structured similarly to TopShot.
7    Unlike TopShot, we use resources for all entities and manage access to their data
8    by copying it to structs (this simplifies access control, in particular write access).
9    We also encapsulate resource creation for the admin in member functions on the parent type.
10    
11    There are 4 levels of entity:
12    1. Series.
13    2. Genies Collection (not to be confused with an NFT Collection).
14    3. Edition.
15    4. Genies NFT (an NFT).
16    Each exists conceptually within the thing above it.
17    And each must be created or closed by the thing above it.
18
19    Note that we cache some information (Series names/ids, counts of deactivated entities) rather
20    than calculate it each time.
21    This is enabled by encapsulation and saves gas for entity lifecycle operations.
22
23    Note that the behaviours of Series.closeAllCollections(), Series.deactivate(), and Series.init()
24    are kept separate to allow ending one series in various ways without starting another.
25    They are called in the correct order in Admin.advanceSeries().
26 */
27
28// The Genies NFTs and metadata contract
29//
30pub contract Genies: NonFungibleToken {
31    //------------------------------------------------------------
32    // Events
33    //------------------------------------------------------------
34
35    // Contract Events
36    //
37    pub event ContractInitialized()
38
39    // NFT Collection (not Genies Collection!) Events
40    //
41    pub event Withdraw(id: UInt64, from: Address?)
42    pub event Deposit(id: UInt64, to: Address?)
43
44    // Series Events
45    //
46    // Emitted when a new series has been triggered by an admin
47    pub event NewSeriesStarted(newCurrentSeries: UInt32, name: String, metadata: {String: String})
48    pub event SeriesDeactivated(id: UInt32)
49
50    // Collection Events
51    //
52    pub event CollectionCreated(id: UInt32, seriesID: UInt32, name: String, metadata: {String: String})
53    pub event CollectionClosed(id: UInt32)
54
55    // Edition Events
56    //
57    pub event EditionCreated(id: UInt32, collectionID: UInt32, name: String, metadata: {String: String})
58    pub event EditionRetired(id: UInt32)
59
60    // NFT Events
61    //
62    pub event NFTMinted(id: UInt64, editionID: UInt32, serialNumber: UInt32)
63    pub event NFTBurned(id: UInt64)
64
65    //------------------------------------------------------------
66    // Named values
67    //------------------------------------------------------------
68
69    // Named Paths
70    //
71    pub let CollectionStoragePath:  StoragePath
72    pub let CollectionPublicPath:   PublicPath
73    pub let AdminStoragePath:       StoragePath
74    pub let MinterPrivatePath:      PrivatePath
75
76    //------------------------------------------------------------
77    // Publcly readable contract state
78    //------------------------------------------------------------
79
80    // Entity Counts
81    //
82    pub var totalSupply:        UInt64
83    pub var currentSeriesID:    UInt32
84    pub var nextCollectionID:   UInt32
85    pub var nextEditionID:    UInt32
86
87    //------------------------------------------------------------
88    // Internal contract state
89    //------------------------------------------------------------
90
91    // Metadata Dictionaries
92    //
93    // This is so we can find Series by their names (via seriesByID)
94    access(self) let seriesIDByName:    {String: UInt32}
95    // This avoids storing Series in an array where the index is off by one
96    access(self) let seriesByID:        @{UInt32: Series}
97    access(self) let collectionByID:    @{UInt32: GeniesCollection}
98    access(self) let editionByID:     @{UInt32: Edition}
99
100    //------------------------------------------------------------
101    // Series
102    //------------------------------------------------------------
103
104    // A public struct to access Series data
105    //
106    pub struct SeriesData {
107        pub let id: UInt32
108        pub let name: String
109        pub let metadata: {String: String}
110        pub let active: Bool
111        pub let collectionIDs: [UInt32]
112        pub let collectionsOpen: UInt32
113
114        // initializer
115        //
116        init (id: UInt32) {    
117            let series = (&Genies.seriesByID[id] as &Genies.Series?)!
118            self.id = series.id
119            self.name = series.name
120            self.metadata = series.metadata
121            self.active = series.active
122            self.collectionIDs = series.collectionIDs
123            self.collectionsOpen = series.collectionsOpen
124        }
125    }
126
127    // A top-level Series with a unique ID and name
128    //
129    pub resource Series {
130        pub let id: UInt32
131        pub let name: String
132        // Contents writable if borrowed!
133        // This is deliberate, as it allows admins to update the data.
134        pub let metadata: {String: String}
135        // We manage this list, but need to access it to fill out the struct,
136        // so it is access(contract)
137        access(contract) let collectionIDs: [UInt32]
138        pub var collectionsOpen: UInt32
139        pub var active: Bool
140
141        // Deactivate this series
142        //
143        pub fun deactivate() {
144            pre {
145                self.active == true: "not active"
146                self.collectionsOpen == 0: "must closeAllCollections before deactivating"
147            }
148
149            self.active = false
150
151            emit SeriesDeactivated(id: self.id)
152        }
153
154        // Create and add a collection to the series.
155        // You can only do so via this function, which updates the relevant fields.
156        //
157        pub fun addCollection(
158            collectionName: String,
159            collectionMetadata: {String: String}
160        ): UInt32 {
161            pre {
162                self.active == true: "Cannot add collection to previous series"
163            }
164
165            let collection <- create Genies.GeniesCollection(
166                seriesID: self.id,
167                name: collectionName,
168                metadata: collectionMetadata
169            )
170            let collectionID = collection.id
171            Genies.collectionByID[collectionID] <-! collection
172            self.collectionIDs.append(collectionID)
173            self.collectionsOpen = self.collectionsOpen + 1 as UInt32
174
175            return collectionID
176        }
177
178        // Close a collection, and update the relevant fields
179        //
180        pub fun closeGeniesCollection(collectionID: UInt32) {
181            pre {
182                Genies.collectionByID[collectionID] != nil: "no such collectionID"
183            }
184
185            let collection = (&Genies.collectionByID[collectionID] as &Genies.GeniesCollection?)!
186            collection.close()
187            // Add this check to fix the underflow issue caused by the mismatch of collectionsOpen and actual Open counts.
188            // Remove the if check in the next release 
189            if self.collectionsOpen > 0 {
190                self.collectionsOpen = self.collectionsOpen - 1 as UInt32
191            }   
192        }
193
194        // Recursively ensure that all of the collections are closed,
195        // and all the editions in each are retired,
196        // allowing advanceSeries to proceed
197        //
198        pub fun closeAllGeniesCollections() {
199            for collectionID in self.collectionIDs {
200                let collection = (&Genies.collectionByID[collectionID] as &Genies.GeniesCollection?)!
201                if collection.open {
202                    collection.retireAllEditions()
203                    self.closeGeniesCollection(collectionID: collectionID)
204                }
205            }
206        }
207
208        // initializer
209        // We pass in ID as the logic for it is more complex than it should be,
210        // and we don't want to spread it out.
211        //
212        init (id: UInt32, name: String, metadata: {String: String}) {
213            pre {
214                !Genies.seriesIDByName.containsKey(name): "A Series with that name already exists"
215            }
216
217            self.id = id
218            self.name = name
219            self.metadata = metadata
220            self.collectionIDs = []
221            self.collectionsOpen = 0 as UInt32
222            self.active = true
223
224            emit NewSeriesStarted(
225                newCurrentSeries: self.id,
226                name: self.name,
227                metadata: self.metadata
228            )
229        }
230    }
231
232    // Get the publicly available data for a Series by id
233    //
234    pub fun getSeriesData(id: UInt32): Genies.SeriesData {
235        pre {
236            Genies.seriesByID[id] != nil: "Cannot borrow series, no such id"
237        }
238
239        return Genies.SeriesData(id: id)
240    }
241
242    // Get the publicly available data for a Series by name
243    //
244    pub fun getSeriesDataByName(name: String): Genies.SeriesData {
245        pre {
246            Genies.seriesIDByName[name] != nil: "Cannot borrow series, no such name"
247        }
248
249        let id = Genies.seriesIDByName[name]!
250
251        return Genies.SeriesData(id: id)
252    }
253
254    // Get all series names (this will be *long*)
255    //
256    pub fun getAllSeriesNames(): [String] {
257        return Genies.seriesIDByName.keys
258    }
259
260    // Get series id for name
261    //
262    pub fun getSeriesIDByName(name: String): UInt32? {
263        return Genies.seriesIDByName[name]
264    }
265
266    //------------------------------------------------------------
267    // GeniesCollection
268    //------------------------------------------------------------
269
270    // A public struct to access GeniesCollection data
271    //
272    pub struct GeniesCollectionData {
273        pub let id: UInt32
274        pub let seriesID: UInt32
275        pub let name: String
276        pub let metadata: {String: String}
277        pub let open: Bool
278        pub let editionIDs: [UInt32]
279        pub let editionsActive: UInt32
280
281        // initializer
282        //
283        init (id: UInt32) {
284            let collection = (&Genies.collectionByID[id] as &Genies.GeniesCollection?)!
285            self.id = id
286            self.seriesID = collection.seriesID
287            self.name = collection.name
288            self.metadata = collection.metadata
289            self.open = collection.open
290            self.editionIDs = collection.editionIDs
291            self.editionsActive = collection.editionsActive
292        }
293    }
294
295    // A Genies collection (not to be confused with a NonFungibleToken.Collection) within a series
296    //
297    pub resource GeniesCollection {
298        pub let id: UInt32
299        pub let seriesID: UInt32
300        pub let name: String
301        // Contents writable if borrowed!
302        // This is deliberate, as it allows admins to update the data.
303        pub let metadata: {String: String}
304        pub var open: Bool
305        // We manage this list, but need to access it to fill out the struct,
306        // so it is access(contract)
307        access(contract) let editionIDs: [UInt32]
308        pub var editionsActive: UInt32
309
310        // Create and add an Edition to the collection.
311        // You can only do so via this function, which updates the relevant fields.
312        //
313        pub fun addEdition(
314            editionName: String,
315            editionMetadata: {String: String}
316        ): UInt32 {
317            pre {
318                self.open == true: "Cannot add edition to closed collection"
319            }
320            let edition <- create Genies.Edition(
321                collectionID: self.id,
322                name: editionName,
323                metadata: editionMetadata
324            )
325
326            let editionID = edition.id
327            Genies.editionByID[editionID] <-! edition
328            self.editionIDs.append(editionID)
329            self.editionsActive = self.editionsActive + 1 as UInt32
330
331            return editionID
332        }
333
334        // Update metadata field of an Edition to the collection
335        //
336        pub fun updateEdition(
337            editionID: UInt32,
338            editionMetadata: {String: String}
339        ) {
340            pre {
341                Genies.editionByID[editionID] != nil: "editionID doesn't exist"
342                self.editionIDs.contains(editionID) : "editionID doesn't belong to this collection"
343            }
344
345            let edition = (&Genies.editionByID[editionID] as &Edition?)!
346            for key in editionMetadata.keys {
347                let value = editionMetadata[key]
348                if value != nil {
349                    edition.setMetadata(key: key, value: value!)
350                }
351            }
352        }
353
354
355        // Close an Edition, and update the relevant fields
356        //
357        pub fun retireEdition(editionID: UInt32) {
358            pre {
359                Genies.editionByID[editionID] != nil: "editionID doesn't exist"
360            }
361
362            let edition = (&Genies.editionByID[editionID] as &Edition?)!
363            edition.retire()
364            self.editionsActive = self.editionsActive - 1 as UInt32
365        }
366
367        // Retire all of the Editions, allowing this collection to be closed
368        //
369        pub fun retireAllEditions() {
370            for editionID in self.editionIDs {
371                self.retireEdition(editionID: editionID)
372            }
373        }
374
375        // Close the collection
376        // access(contract) to enforce calling through its parent series
377        //
378        access(contract) fun close() {
379            pre{
380                self.open: "Already closed"
381                self.editionsActive == 0:
382                    "All editions in this collection must be closed before closing it"
383            }
384
385            self.open = false
386
387            emit CollectionClosed(id: self.id)
388        }
389
390        // initializer
391        //
392        init (seriesID: UInt32, name: String, metadata: {String: String}) {
393            pre {
394                Genies.seriesByID.containsKey(seriesID) != nil: "seriesID does not exist"
395            }
396
397            self.id = Genies.nextCollectionID
398            self.seriesID = seriesID
399            self.name = name
400            self.metadata = metadata
401            self.editionIDs = []
402            self.editionsActive = 0 as UInt32
403            self.open = true
404
405            Genies.nextCollectionID = Genies.nextCollectionID + 1 as UInt32
406
407            emit CollectionCreated(id: self.id, seriesID: self.seriesID, name: self.name, metadata: self.metadata)
408        }
409    }
410
411    // Get the publicly available data for a GeniesCollection
412    // Not an NFT Collection!
413    //
414    pub fun getGeniesCollectionData(id: UInt32): Genies.GeniesCollectionData {
415        pre {
416            Genies.collectionByID[id] != nil: "Cannot borrow Genies collection, no such id"
417        }
418
419        return GeniesCollectionData(id: id)
420    }
421
422    //------------------------------------------------------------
423    // Edition
424    //------------------------------------------------------------
425
426    // A public struct to access Edition data
427    //
428    pub struct EditionData {
429        pub let id: UInt32
430        pub let collectionID: UInt32
431        pub let name: String
432        pub let metadata: {String: String}
433        pub let open: Bool
434        pub let numMinted: UInt32
435
436        // initializer
437        //
438        init (id: UInt32) {
439            let edition = (&Genies.editionByID[id] as &Genies.Edition?)!
440            self.id = id
441            self.collectionID = edition.collectionID
442            self.name = edition.name
443            self.metadata = edition.metadata
444            self.open = edition.open
445            self.numMinted = edition.numMinted
446        }
447    }
448
449    // An Edition (NFT type) within a Genies collection
450    //
451    pub resource Edition {
452        pub let id: UInt32
453        pub let collectionID: UInt32
454        pub let name: String
455        // Contents writable if borrowed!
456        // This is deliberate, as it allows admins to update the data.
457        pub let metadata: {String: String}
458        pub var numMinted: UInt32
459        pub var open: Bool
460
461        // Retire this edition so that no more Genies NFTs can be minted in it
462        // access(contract) to enforce calling through its parent GeniesCollection
463        //
464        access(contract) fun retire() {
465            pre {
466                self.open == true: "already retired"
467            }
468
469            self.open = false
470
471            emit EditionRetired(id: self.id)
472        }
473
474        pub fun setMetadata(key: String, value: String) {
475            self.metadata[key] = value
476        }
477
478        // Mint a Genies NFT in this edition, with the given minting mintingDate.
479        // Note that this will panic if this edition is retired.
480        //
481        pub fun mint(): @Genies.NFT {
482            pre {
483                self.open: "edition closed, cannot mint"
484            }
485
486            // Keep a running total (you'll notice we used this as the serial number
487            // and pre-increment it so that serial numbers start at 1 ).
488            self.numMinted = self.numMinted + 1 as UInt32
489
490            // Create the Genies NFT, filled out with our information
491            let geniesNFT <- create NFT(
492                id: Genies.totalSupply,
493                editionID: self.id,
494                serialNumber: self.numMinted
495            )
496            Genies.totalSupply = Genies.totalSupply + 1
497
498            return <- geniesNFT
499        }
500
501        // initializer
502        //
503        init (
504            collectionID: UInt32,
505            name: String,
506            metadata: {String: String}
507        ) {
508            pre {
509                Genies.collectionByID.containsKey(collectionID): "collectionID does not exist"
510            }
511
512            self.id = Genies.nextEditionID
513            self.collectionID = collectionID
514            self.name = name
515            self.metadata = metadata
516            self.numMinted = 0 as UInt32
517            self.open = true
518
519            Genies.nextEditionID = Genies.nextEditionID + 1 as UInt32
520
521            emit EditionCreated(
522                id: self.id,
523                collectionID: self.collectionID,
524                name: self.name,
525                metadata: self.metadata
526            )
527        }
528    }
529
530    // Get the publicly available data for an Edition
531    //
532    pub fun getEditionData(id: UInt32): EditionData {
533        pre {
534            Genies.editionByID[id] != nil: "Cannot borrow edition, no such id"
535        }
536
537        let edition = (&Genies.editionByID[id] as &Genies.Edition?)!
538
539        return EditionData(id: id)
540    }
541
542    //------------------------------------------------------------
543    // NFT
544    //------------------------------------------------------------
545
546    // A Genies NFT
547    //
548    pub resource NFT: NonFungibleToken.INFT, MetadataViews.Resolver {
549        pub let id: UInt64
550        pub let editionID: UInt32
551        pub let serialNumber: UInt32
552        pub let mintingDate: UFix64
553
554        // Destructor
555        //
556        destroy() {
557            emit NFTBurned(id: self.id)
558        }
559
560        // NFT initializer
561        //
562        init(
563            id: UInt64,
564            editionID: UInt32,
565            serialNumber: UInt32
566        ) {
567            pre {
568                Genies.editionByID[editionID] != nil: "no such editionID"
569                (&Genies.editionByID[editionID] as &Edition?)!.open:
570                    "editionID is retired"
571            }
572
573            self.id = id
574            self.editionID = editionID
575            self.serialNumber = serialNumber
576            self.mintingDate = getCurrentBlock().timestamp
577
578            emit NFTMinted(id: self.id, editionID: self.editionID, serialNumber: self.serialNumber)
579        }
580
581        pub fun getWearableSKU(): String {
582            let edition = Genies.getEditionData(id: self.editionID)
583            if edition.metadata["avatarWearableSKU"] != nil {
584                return edition.metadata["avatarWearableSKU"]!
585            } else {
586                return ""
587            }
588        }
589
590        pub fun getViews(): [Type] {
591            return [
592                Type<MetadataViews.Display>(),
593                Type<MetadataViews.Editions>(),
594                Type<MetadataViews.Serial>(),
595                Type<MetadataViews.Royalties>(),
596                Type<MetadataViews.ExternalURL>(),
597                Type<MetadataViews.NFTCollectionData>(),
598                Type<MetadataViews.NFTCollectionDisplay>(),
599                Type<MetadataViews.Traits>()
600            ]
601        }
602
603        pub fun resolveView(_ view: Type): AnyStruct? {
604            let edition = Genies.getEditionData(id: self.editionID)
605            let collection = Genies.getGeniesCollectionData(id: edition.collectionID)
606            switch view {
607                case Type<MetadataViews.Display>():
608                    return MetadataViews.Display(
609                        name: edition.name,
610                        description: "Serial #"
611                                    .concat(self.serialNumber.toString())
612                                    .concat(" of ")
613                                    .concat(edition.name)
614                                    .concat(" from ")
615                                    .concat(collection.name)
616                                    .concat(" collection"),
617                        thumbnail: MetadataViews.HTTPFile(
618                                    url:"https://warehouse-assets.genies.com/"
619                                        .concat(self.getWearableSKU())
620                                        .concat("/wearable-container.png")
621                        )
622                    )
623                case Type<MetadataViews.Editions>():
624                    return MetadataViews.Editions([
625                        MetadataViews.Edition(
626                            name: edition.name,
627                            number: UInt64(self.serialNumber),
628                            max: UInt64(edition.numMinted),
629                        )
630                    ])
631                case Type<MetadataViews.Serial>():
632                    return MetadataViews.Serial(
633                        self.id
634                    )
635                case Type<MetadataViews.Royalties>():
636                    var royalties: [MetadataViews.Royalty] = []
637                    return MetadataViews.Royalties(royalties) 
638                case Type<MetadataViews.ExternalURL>():
639                    return MetadataViews.ExternalURL("https://warehouse.genies.com/nft/".concat(self.id.toString()))
640                case Type<MetadataViews.NFTCollectionData>():
641                    return MetadataViews.NFTCollectionData(
642                        storagePath: Genies.CollectionStoragePath,
643                        publicPath: Genies.CollectionPublicPath,
644                        providerPath: /private/GeniesNFTCollection,
645                        publicCollection: Type<&Genies.Collection{Genies.GeniesNFTCollectionPublic}>(),
646                        publicLinkedType: Type<&Genies.Collection{Genies.GeniesNFTCollectionPublic, NonFungibleToken.CollectionPublic,NonFungibleToken.Receiver,MetadataViews.ResolverCollection}>(),
647                        providerLinkedType: Type<&Genies.Collection{Genies.GeniesNFTCollectionPublic, NonFungibleToken.CollectionPublic,NonFungibleToken.Provider,MetadataViews.ResolverCollection}>(),
648                        createEmptyCollectionFunction: (fun (): @NonFungibleToken.Collection {
649                            return <- Genies.createEmptyCollection()
650                        })
651                    )
652                case Type<MetadataViews.NFTCollectionDisplay>():
653                    return MetadataViews.NFTCollectionDisplay(
654                        name: "Genies",
655                        description: "Empowering humans to build avatar ecosystems.",
656                        externalURL: MetadataViews.ExternalURL("https://warehouse.genies.com"),
657                        squareImage: MetadataViews.Media(
658                                        file: MetadataViews.HTTPFile(
659                                            url: "https://warehouse.genies.com/static/images/logo.png"
660                                        ),
661                                        mediaType: "image/png"
662                                    ),
663                        bannerImage: MetadataViews.Media(
664                                        file: MetadataViews.HTTPFile(
665                                            url: "https://warehouse.genies.com/static/images/banner.png"
666                                        ),
667                                        mediaType: "image/png"
668                                    ),
669                        socials: {
670                            "instagram": MetadataViews.ExternalURL("https://www.instagram.com/genies"),
671                            "twitter": MetadataViews.ExternalURL("https://twitter.com/genies"),
672                            "tiktok": MetadataViews.ExternalURL("https://www.tiktok.com/@genies"),
673                            "discord": MetadataViews.ExternalURL("https://discord.com/invite/genies")
674                        }
675                    )
676                case Type<MetadataViews.Traits>():
677                    let excludedTraits = ["rarity", "type", "designSlot", "publisher"];
678                    let traitsView = MetadataViews.dictToTraits(dict: edition.metadata, excludedNames: excludedTraits);
679                    
680                    if edition.metadata["designSlot"] != nil {
681                        let designSlot = edition.metadata["designSlot"]!
682                        let designSlotTrait = MetadataViews.Trait(
683                            name: "designSlot", 
684                            value: designSlot.slice(from: 20, upTo: designSlot.length), 
685                            displayType: "String", 
686                            rarity: nil
687                        ) 
688                        traitsView.addTrait(designSlotTrait)
689                    }
690                    return traitsView
691            }
692            return nil
693        }
694    }
695
696    //------------------------------------------------------------
697    // Collection
698    //------------------------------------------------------------
699
700    // A public collection interface that allows Genies NFTs to be borrowed
701    //
702    pub resource interface GeniesNFTCollectionPublic {
703        pub fun deposit(token: @NonFungibleToken.NFT)
704        pub fun batchDeposit(tokens: @NonFungibleToken.Collection)
705        pub fun getIDs(): [UInt64]
706        pub fun borrowNFT(id: UInt64): &NonFungibleToken.NFT
707        pub fun borrowGeniesNFT(id: UInt64): &Genies.NFT? {
708            // If the result isn't nil, the id of the returned reference
709            // should be the same as the argument to the function
710            post {
711                (result == nil) || (result?.id == id):
712                    "Cannot borrow Genies NFT reference: The ID of the returned reference is incorrect"
713            }
714        }
715    }
716
717    // An NFT Collection (not to be confused with a GeniesCollection)
718    //
719    pub resource Collection:
720        NonFungibleToken.Provider,
721        NonFungibleToken.Receiver,
722        NonFungibleToken.CollectionPublic,
723        GeniesNFTCollectionPublic,
724        MetadataViews.ResolverCollection
725    {
726        // dictionary of NFT conforming tokens
727        // NFT is a resource type with an UInt64 ID field
728        //
729        pub var ownedNFTs: @{UInt64: NonFungibleToken.NFT}
730
731        // withdraw removes an NFT from the collection and moves it to the caller
732        //
733        pub fun withdraw(withdrawID: UInt64): @NonFungibleToken.NFT {
734            let token <- self.ownedNFTs.remove(key: withdrawID) ?? panic("missing NFT")
735
736            emit Withdraw(id: token.id, from: self.owner?.address)
737
738            return <-token
739        }
740
741        // deposit takes a NFT and adds it to the collections dictionary
742        // and adds the ID to the id array
743        //
744        pub fun deposit(token: @NonFungibleToken.NFT) {
745            let token <- token as! @Genies.NFT
746            let id: UInt64 = token.id
747
748            // add the new token to the dictionary which removes the old one
749            let oldToken <- self.ownedNFTs[id] <- token
750
751            emit Deposit(id: id, to: self.owner?.address)
752
753            destroy oldToken
754        }
755
756        // batchDeposit takes a Collection object as an argument
757        // and deposits each contained NFT into this Collection
758        //
759        pub fun batchDeposit(tokens: @NonFungibleToken.Collection) {
760            // Get an array of the IDs to be deposited
761            let keys = tokens.getIDs()
762
763            // Iterate through the keys in the collection and deposit each one
764            for key in keys {
765                self.deposit(token: <-tokens.withdraw(withdrawID: key))
766            }
767
768            // Destroy the empty Collection
769            destroy tokens
770        }
771
772        // getIDs returns an array of the IDs that are in the collection
773        //
774        pub fun getIDs(): [UInt64] {
775            return self.ownedNFTs.keys
776        }
777
778        // borrowNFT gets a reference to an NFT in the collection
779        //
780        pub fun borrowNFT(id: UInt64): &NonFungibleToken.NFT {
781            return (&self.ownedNFTs[id] as &NonFungibleToken.NFT?)!
782        }
783
784        // borrowGeniesNFT gets a reference to an NFT in the collection
785        //
786        pub fun borrowGeniesNFT(id: UInt64): &Genies.NFT? {
787            if self.ownedNFTs[id] != nil {
788                return (&self.ownedNFTs[id] as auth &NonFungibleToken.NFT?)! as! &Genies.NFT
789            } else {
790                return nil
791            }
792        }
793
794        // borrowViewResolver
795        // Gets a reference to the MetadataViews resolver in the collection,
796        // giving access to all metadata information made available.
797        //
798        pub fun borrowViewResolver(id: UInt64): &AnyResource{MetadataViews.Resolver} {
799            let nft = (&self.ownedNFTs[id] as auth &NonFungibleToken.NFT?)!
800            let GeniesNft = nft as! &Genies.NFT
801            return GeniesNft as &AnyResource{MetadataViews.Resolver}
802        }
803
804        // Collection destructor
805        //
806        destroy() {
807            destroy self.ownedNFTs
808        }
809
810        // Collection initializer
811        //
812        init() {
813            self.ownedNFTs <- {}
814        }
815    }
816
817    // public function that anyone can call to create a new empty collection
818    //
819    pub fun createEmptyCollection(): @NonFungibleToken.Collection {
820        return <- create Collection()
821    }
822
823    //------------------------------------------------------------
824    // Admin
825    //------------------------------------------------------------
826
827    // An interface containing the Admin function that allows minting NFTs
828    //
829    pub resource interface NFTMinter {
830        // Mint a single NFT
831        // The Edition for the given ID must already exist
832        //
833        pub fun mintNFT(editionID: UInt32): @Genies.NFT
834    }
835
836    // A resource that allows managing metadata and minting NFTs
837    //
838    pub resource Admin: NFTMinter {
839        // Create a new series and set it to be the current one, deactivating the previous one if needed.
840        // You probably want to call closeAllCollections() on the current series before this.
841        //
842        pub fun advanceSeries(
843            nextSeriesName: String,
844            nextSeriesMetadata: {String: String}
845        ): UInt32 {
846            pre {
847                Genies.seriesByID[Genies.currentSeriesID] == nil
848                    || (&Genies.seriesByID[Genies.currentSeriesID] as &Genies.Series?)!.collectionsOpen == 0:
849                    "All collections must be closed before advancing the series"
850            }
851
852            // The contract starts with currentSeriesID 0 but no entry for series zero.
853            // We have to call advanceSeries to create series 0, so we have to handle that special case.
854            // This test handles that case.
855            // Its body will be called every time after the initial advance, which is what we want.
856            if Genies.seriesByID[Genies.currentSeriesID] != nil {
857                let currentSeries = (&Genies.seriesByID[Genies.currentSeriesID] as &Genies.Series?)!
858                if currentSeries.active {
859                    // Make sure everything in the series is closed
860                    currentSeries.closeAllGeniesCollections()
861                    // Deactivate the current series
862                    currentSeries.deactivate()
863                    // Advance the currentSeriesID
864                    Genies.currentSeriesID = Genies.currentSeriesID + 1 as UInt32
865                }
866            }
867
868            // Create and store the new series
869            let series <- create Genies.Series(
870                id: Genies.currentSeriesID,
871                name: nextSeriesName,
872                metadata: nextSeriesMetadata
873            )
874            Genies.seriesByID[Genies.currentSeriesID] <-! series
875
876            // Cache the new series's name => ID
877            Genies.seriesIDByName[nextSeriesName] = Genies.currentSeriesID
878
879            // Return the new ID for convenience
880            return Genies.currentSeriesID
881        }
882
883        // Borrow a Series
884        //
885        pub fun borrowSeries(id: UInt32): &Genies.Series {
886            pre {
887                Genies.seriesByID[id] != nil: "Cannot borrow series, no such id"
888            }
889
890            return (&Genies.seriesByID[id] as &Genies.Series?)!
891        }
892
893        // Borrow a Genies Collection. Not an NFT Collection!
894        //
895        pub fun borrowGeniesCollection(id: UInt32): &Genies.GeniesCollection {
896            pre {
897                Genies.collectionByID[id] != nil: "Cannot borrow Genies collection, no such id"
898            }
899
900            return (&Genies.collectionByID[id] as &Genies.GeniesCollection?)!
901        }
902
903        // Borrow an Edition
904        //
905        pub fun borrowEdition(id: UInt32): &Genies.Edition {
906            pre {
907                Genies.editionByID[id] != nil: "Cannot borrow edition, no such id"
908            }
909
910            return (&Genies.editionByID[id] as &Genies.Edition?)!
911        }
912
913        // Mint a single NFT
914        // The Edition for the given ID must already exist
915        //
916        pub fun mintNFT(editionID: UInt32): @Genies.NFT {
917            pre {
918                // Make sure the edition we are creating this NFT in exists
919                Genies.editionByID.containsKey(editionID): "No such EditionID"
920            }
921
922            return <- self.borrowEdition(id: editionID).mint()
923        }
924    }
925
926    //------------------------------------------------------------
927    // Contract lifecycle
928    //------------------------------------------------------------
929
930    // Genies contract initializer
931    //
932    init() {
933        // Set the named paths
934        self.CollectionStoragePath = /storage/GeniesNFTCollection
935        self.CollectionPublicPath = /public/GeniesNFTCollection
936        self.AdminStoragePath = /storage/GeniesAdmin
937        self.MinterPrivatePath = /private/GeniesMinter
938
939        // Initialize the entity counts
940        self.totalSupply = 0
941        self.currentSeriesID = 0
942        self.nextCollectionID = 0
943        self.nextEditionID = 0
944
945        // Initialize the metadata lookup dictionaries
946        self.seriesByID <- {}
947        self.seriesIDByName = {}
948        self.collectionByID <- {}
949        self.editionByID <- {}
950
951        // Create an Admin resource and save it to storage
952        let admin <- create Admin()
953        self.account.save(<-admin, to: self.AdminStoragePath)
954        // Link capabilites to the admin constrained to the Minter
955        // and Metadata interfaces
956        self.account.link<&Genies.Admin{Genies.NFTMinter}>(
957            self.MinterPrivatePath,
958            target: self.AdminStoragePath
959        )
960
961        // Let the world know we are here
962        emit ContractInitialized()
963    }
964}