Smart Contract
Genies
A.12450e4bb3b7666e.Genies
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}