Smart Contract

DSSCollection

A.ec55c6f3dae179c8.DSSCollection

Deployed

2h ago
Mar 14, 2026, 08:04:22 AM UTC

Dependents

0 imports
1/*
2    DSSCollection contains collection group & completion functionality for DSS.
3    Author: Jeremy Ahrens jer.ahrens@dapperlabs.com
4*/
5import NonFungibleToken from 0x1d7e57aa55817448
6import MetadataViews from 0x1d7e57aa55817448
7
8// The DSSCollection contract
9//
10pub contract DSSCollection: NonFungibleToken {
11
12    // Contract Events
13    //
14    pub event ContractInitialized()
15
16    // NFT Collection Events
17    //
18    pub event Withdraw(id: UInt64, from: Address?)
19    pub event Deposit(id: UInt64, to: Address?)
20
21    // Events
22    //
23    pub event CollectionGroupCreated(
24        id: UInt64,
25        name: String,
26        description: String,
27        productName: String,
28        endTime: UFix64?,
29        metadata: {String: String}
30    )
31    pub event CollectionGroupClosed(id: UInt64)
32    pub event ItemCreatedInSlot(
33        itemID: String,
34        points: UInt64,
35        itemType: String,
36        comparator: String,
37        slotID: UInt64,
38        collectionGroupID: UInt64
39    )
40    pub event SlotCreated(
41        id: UInt64,
42        collectionGroupID: UInt64,
43        logicalOperator: String,
44        required: Bool,
45        typeName: Type,
46        metadata: {String: String}
47    )
48    pub event CollectionNFTMinted(
49        id: UInt64,
50        collectionGroupID: UInt64,
51        serialNumber: UInt64,
52        completionAddress: String,
53        completionDate: UFix64,
54        level: UInt8
55    )
56    pub event CollectionNFTCompletedWith(
57        collectionGroupID: UInt64,
58        completionAddress: String,
59        completionNftIds: [UInt64]
60    )
61
62    pub event CollectionNFTBurned(id: UInt64)
63
64    // Named Paths
65    //
66    pub let CollectionStoragePath:  StoragePath
67    pub let CollectionPublicPath:   PublicPath
68    pub let AdminStoragePath:       StoragePath
69    pub let MinterPrivatePath:      PrivatePath
70
71    // Entity Counts
72    //
73    pub var totalSupply:    UInt64
74    pub var collectionGroupNFTCount: {UInt64: UInt64}
75
76    // Placeholder for future updates
77    pub let extCollectionGroup: {String: AnyStruct}
78
79    // Lists in contract
80    //
81    access(self) let collectionGroupByID: @{UInt64: CollectionGroup}
82    access(self) let slotByID: @{UInt64: Slot}
83
84    // A public struct to stores the nftIDs used to complete a collection group
85    //
86    pub struct CollectionCompletedWith {
87        pub var collectionGroupID: UInt64
88        pub var nftIDs: [UInt64]
89
90        init(collectionGroupID: UInt64, nftIDs: [UInt64]) {
91            self.collectionGroupID = collectionGroupID
92            self.nftIDs = nftIDs
93        }
94    }
95
96    pub var completedCollections: {Address: [CollectionCompletedWith]}
97
98
99    // A public struct to access Item data
100    //
101    pub struct Item {
102        pub let itemID: String // the id of the edition, tier, play
103        pub let points: UInt64 // points for item
104        pub let itemType: String // (edition.id, edition.tier, play.id)
105        pub let comparator: String // (< | > | =)
106
107        init (
108            itemID: String,
109            points: UInt64,
110            itemType: String,
111            comparator: String
112        ) {
113            self.itemID = itemID
114            self.points = points
115            self.itemType = itemType
116            self.comparator = comparator
117        }
118    }
119
120    // A public struct to access Slot data
121    //
122    pub struct SlotData {
123        pub let id: UInt64
124        pub let collectionGroupID: UInt64
125        pub let logicalOperator: String // (AND / OR)
126        pub let required: Bool
127        pub let typeName: Type // (Type<A.f8d6e0586b0a20c7.ExampleNFT.NFT>()...)
128        pub var items: [Item]
129        pub let metadata: {String: String}
130
131        init (id: UInt64) {
132            if let slot = &DSSCollection.slotByID[id] as &DSSCollection.Slot? {
133                self.id = slot.id
134                self.collectionGroupID = slot.collectionGroupID
135                self.logicalOperator = slot.logicalOperator
136                self.required = slot.required
137                self.typeName = slot.typeName
138                self.items = slot.items
139                self.metadata = slot.metadata
140            } else {
141                panic("Slot does not exist")
142            }
143        }
144    }
145
146    // A top-level Slot with a unique ID
147    //
148    pub resource Slot {
149        pub let id: UInt64
150        pub let collectionGroupID: UInt64
151        pub let logicalOperator: String // (AND / OR)
152        pub let required: Bool
153        pub let typeName: Type // (Type<A.f8d6e0586b0a20c7.ExampleNFT.NFT>())
154        pub var items: [Item]
155        pub let metadata: {String: String}
156
157        // Create item in slot
158        //
159        access(contract) fun createItemInSlot(
160            itemID: String,
161            points: UInt64,
162            itemType: String,
163            comparator: String
164        ) {
165            pre {
166                DSSCollection.CollectionGroupData(
167                    id: self.collectionGroupID
168                ).active: "Collection group inactive"
169                DSSCollection.validateComparator(
170                    comparator: comparator
171                ) == true : "Slot submitted with unsupported comparator"
172            }
173
174            let item = DSSCollection.Item(
175                itemID: itemID,
176                points: points,
177                itemType: itemType,
178                comparator: comparator
179            )
180            self.items.append(item)
181
182            emit ItemCreatedInSlot(
183                itemID: itemID,
184                points: points,
185                itemType: itemType,
186                comparator: comparator,
187                slotID: self.id,
188                collectionGroupID: self.collectionGroupID
189            )
190        }
191
192        init (
193            collectionGroupID: UInt64,
194            logicalOperator: String,
195            required: Bool,
196            typeName: Type,
197            metadata: {String: String}
198        ) {
199            pre {
200                DSSCollection.CollectionGroupData(
201                    id: collectionGroupID
202                ).active: "Collection group inactive"
203                DSSCollection.validateLogicalOperator(
204                    logicalOperator: logicalOperator
205                ) == true : "Slot submitted with unsupported logical operator"
206            }
207
208            self.id = self.uuid
209            self.collectionGroupID = collectionGroupID
210            self.logicalOperator = logicalOperator
211            self.required = required
212            self.typeName = typeName
213            self.metadata = metadata
214            self.items = []
215
216            emit SlotCreated(
217                id: self.id,
218                collectionGroupID: self.collectionGroupID,
219                logicalOperator: self.logicalOperator,
220                required: self.required,
221                typeName: self.typeName,
222                metadata: self.metadata
223            )
224        }
225    }
226
227    // A public struct to access CollectionGroup data
228    //
229    pub struct CollectionGroupData {
230        pub let id: UInt64
231        pub let name: String
232        pub let description: String
233        pub let productName: String
234        pub let active: Bool
235        pub let endTime: UFix64?
236        pub let metadata: {String: String}
237
238        init (id: UInt64) {
239            if let collectionGroup = &DSSCollection.collectionGroupByID[id] as &DSSCollection.CollectionGroup? {
240                self.id = collectionGroup.id
241                self.name = collectionGroup.name
242                self.description = collectionGroup.description
243                self.productName = collectionGroup.productName
244                self.active = collectionGroup.active
245                self.endTime = collectionGroup.endTime
246                self.metadata = collectionGroup.metadata
247            } else {
248                panic("CollectionGroup does not exist")
249            }
250        }
251    }
252
253    // A top-level CollectionGroup with a unique ID and name
254    //
255    pub resource CollectionGroup {
256        pub let id: UInt64
257        pub let name: String
258        pub let description: String
259        pub let productName: String
260        pub var active: Bool
261        pub let endTime: UFix64?
262        pub var numMinted: UInt64
263        pub let metadata: {String: String}
264
265        // Close this collection group
266        //
267        access(contract) fun close() {
268            pre {
269                self.active :  "Already deactivated"
270            }
271
272            self.active = false
273
274            emit CollectionGroupClosed(id: self.id)
275        }
276
277        // Mint a DSSCollection NFT in this group
278        //
279        pub fun mint(completionAddress: String, level: UInt8): @DSSCollection.NFT {
280            pre {
281                !self.active : "Cannot mint an active collection group"
282                DSSCollection.validateTimeBound(
283                    endTime: self.endTime
284                ) == true : "Cannot mint a collection group outside of time bounds"
285                level <= 10: "Token level must be less than 10"
286            }
287
288            // Create the DSSCollection NFT, filled out with our information
289            //
290            let dssCollectionNFT <- create NFT(
291                collectionGroupID: self.id,
292                serialNumber: self.numMinted + 1,
293                completionAddress: completionAddress,
294                level: level,
295                extensionData: {},
296            )
297            DSSCollection.totalSupply = DSSCollection.totalSupply + 1
298            self.numMinted = self.numMinted + 1 as UInt64
299
300            return <- dssCollectionNFT
301        }
302
303        init (
304            name: String,
305            description: String,
306            productName: String,
307            endTime: UFix64?,
308            metadata: {String: String}
309        ) {
310            pre {
311                DSSCollection.validateTimeBound(
312                    endTime: endTime
313                ) == true : "Cannot create expired timebound collection group"
314            }
315            self.id = self.uuid
316            self.name = name
317            self.description = description
318            self.productName = productName
319            self.active = true
320            self.endTime = endTime
321            self.numMinted = 0 as UInt64
322            self.metadata = metadata
323
324            emit CollectionGroupCreated(
325                id: self.id,
326                name: self.name,
327                description: self.description,
328                productName: self.productName,
329                endTime: self.endTime,
330                metadata: self.metadata
331            )
332        }
333    }
334
335    // Get the publicly available data for a CollectionGroup by id
336    //
337    pub fun getCollectionGroupData(id: UInt64): DSSCollection.CollectionGroupData {
338        pre {
339            DSSCollection.collectionGroupByID[id] != nil: "Cannot borrow collection group, no such id"
340        }
341
342        return DSSCollection.CollectionGroupData(id: id)
343    }
344
345    // Get the publicly available data for a Slot by id
346    //
347    pub fun getSlotData(id: UInt64): DSSCollection.SlotData {
348        pre {
349            DSSCollection.slotByID[id] != nil: "Cannot borrow slot, no such id"
350        }
351
352        return DSSCollection.SlotData(id: id)
353    }
354
355    // Validate time range of collection group
356    //
357    pub fun validateTimeBound(endTime: UFix64?): Bool {
358        if endTime == nil {
359            return true
360        }
361        if endTime! >= getCurrentBlock().timestamp {
362            return true
363        }
364        return false
365    }
366
367    // Validate logical operator of slot
368    //
369    pub fun validateLogicalOperator(logicalOperator: String): Bool {
370        if logicalOperator == "OR" || logicalOperator == "AND" {
371            return true
372        }
373        return false
374    }
375
376    // Validate comparator of item
377    //
378    pub fun validateComparator(comparator: String): Bool {
379        if comparator == ">" || comparator == "<" || comparator == "=" {
380            return true
381        }
382        return false
383    }
384
385    // Get the nftIds for each completed collection for a given address
386    //
387    pub fun getCompletedCollectionIDs(address: Address): [CollectionCompletedWith]? {
388        return DSSCollection.completedCollections[address]
389    }
390
391    // A DSSCollection NFT
392    //
393    pub resource NFT: NonFungibleToken.INFT, MetadataViews.Resolver {
394        pub let id: UInt64
395        pub let collectionGroupID: UInt64
396        pub let serialNumber: UInt64
397        pub let completionDate: UFix64
398        pub let completionAddress: String
399        pub let level: UInt8
400
401        // Placeholder for future updates
402        pub let extensionData: {String: AnyStruct}
403
404        pub fun name(): String {
405            let collectionGroupData: DSSCollection.CollectionGroupData
406                = DSSCollection.getCollectionGroupData(id: self.collectionGroupID)
407            let level: String = self.level.toString()
408            return collectionGroupData.name
409                .concat(" Level ")
410                .concat(level)
411                .concat(" Completion Token")
412        }
413
414        pub fun description(): String {
415            let serialNumber: String = self.serialNumber.toString()
416            let completionDate: String = self.completionDate.toString()
417            return "Completed by "
418                .concat(self.completionAddress)
419                .concat(" on ")
420                .concat(completionDate)
421                .concat(" with serial number ")
422                .concat(serialNumber)
423        }
424
425        destroy() {
426            DSSCollection.totalSupply = DSSCollection.totalSupply - 1
427            emit CollectionNFTBurned(id: self.id)
428        }
429
430        pub fun getViews(): [Type] {
431            return [
432                Type<MetadataViews.Display>()
433            ]
434        }
435
436        pub fun resolveView(_ view: Type): AnyStruct? {
437            return MetadataViews.Display(
438                name: self.name(),
439                description: self.description(),
440                thumbnail: MetadataViews.HTTPFile(
441                    url:"https://storage.googleapis.com/dl-nfl-assets-prod/static/images/collection-group/token-placeholder.png"
442                )
443            )
444        }
445
446        init(
447            collectionGroupID: UInt64,
448            serialNumber: UInt64,
449            completionAddress: String,
450            level: UInt8,
451            extensionData: {String: AnyStruct},
452        ) {
453            pre {
454                DSSCollection.collectionGroupByID[collectionGroupID] != nil: "no such collectionGroupID"
455            }
456
457            self.id = self.uuid
458            self.collectionGroupID = collectionGroupID
459            self.serialNumber = serialNumber
460            self.completionDate = getCurrentBlock().timestamp
461            self.completionAddress = completionAddress
462            self.level = level
463            self.extensionData = extensionData
464
465            emit CollectionNFTMinted(
466                id: self.id,
467                collectionGroupID: self.collectionGroupID,
468                serialNumber: self.serialNumber,
469                completionAddress: self.completionAddress,
470                completionDate: self.completionDate,
471                level: self.level,
472            )
473        }
474    }
475
476    // A public collection interface that allows DSSCollection NFTs to be borrowed
477    //
478    pub resource interface DSSCollectionNFTCollectionPublic {
479        pub fun deposit(token: @NonFungibleToken.NFT)
480        pub fun batchDeposit(tokens: @NonFungibleToken.Collection)
481        pub fun getIDs(): [UInt64]
482        pub fun borrowNFT(id: UInt64): &NonFungibleToken.NFT
483        pub fun borrowDSSCollectionNFT(id: UInt64): &DSSCollection.NFT? {
484            // If the result isn't nil, the id of the returned reference
485            // should be the same as the argument to the function
486            post {
487                (result == nil) || (result?.id == id):
488                    "Cannot borrow Moment NFT reference: The ID of the returned reference is incorrect"
489            }
490        }
491    }
492
493    // An NFT Collection
494    //
495    pub resource Collection:
496        NonFungibleToken.Provider,
497        NonFungibleToken.Receiver,
498        NonFungibleToken.CollectionPublic,
499        DSSCollectionNFTCollectionPublic,
500        MetadataViews.ResolverCollection
501    {
502        // dictionary of NFT conforming tokens
503        // NFT is a resource type with an UInt64 ID field
504        //
505        pub var ownedNFTs: @{UInt64: NonFungibleToken.NFT}
506
507        // withdraw removes an NFT from the collection and moves it to the caller
508        //
509        pub fun withdraw(withdrawID: UInt64): @NonFungibleToken.NFT {
510            let token <- self.ownedNFTs.remove(key: withdrawID) ?? panic("Missing NFT")
511
512            emit Withdraw(id: token.id, from: self.owner?.address)
513
514            return <-token
515        }
516
517        // deposit takes a NFT and adds it to the collections dictionary
518        // and adds the ID to the id array
519        //
520        pub fun deposit(token: @NonFungibleToken.NFT) {
521            let token <- token as! @DSSCollection.NFT
522            let id: UInt64 = token.id
523
524            // add the new token to the dictionary which removes the old one
525            let oldToken <- self.ownedNFTs[id] <- token
526
527            emit Deposit(id: id, to: self.owner?.address)
528
529            destroy oldToken
530        }
531
532        // batchDeposit takes a Collection object as an argument
533        // and deposits each contained NFT into this Collection
534        //
535        pub fun batchDeposit(tokens: @NonFungibleToken.Collection) {
536            let keys = tokens.getIDs()
537
538            for key in keys {
539                self.deposit(token: <-tokens.withdraw(withdrawID: key))
540            }
541
542            destroy tokens
543        }
544
545        // getIDs returns an array of the IDs that are in the collection
546        //
547        pub fun getIDs(): [UInt64] {
548            return self.ownedNFTs.keys
549        }
550
551        // borrowNFT gets a reference to an NFT in the collection
552        //
553        pub fun borrowNFT(id: UInt64): &NonFungibleToken.NFT {
554            pre {
555                self.ownedNFTs[id] != nil: "Cannot borrow NFT, no such id"
556            }
557
558            return (&self.ownedNFTs[id] as &NonFungibleToken.NFT?)!
559        }
560
561        // borrowDSSCollectionNFT gets a reference to an NFT in the collection
562        //
563        pub fun borrowDSSCollectionNFT(id: UInt64): &DSSCollection.NFT? {
564            if self.ownedNFTs[id] != nil {
565                if let ref = &self.ownedNFTs[id] as auth &NonFungibleToken.NFT? {
566                    return ref! as! &DSSCollection.NFT
567                }
568                return nil
569            } else {
570                return nil
571            }
572        }
573
574        pub fun borrowViewResolver(id: UInt64): &AnyResource{MetadataViews.Resolver} {
575            let nft = (&self.ownedNFTs[id] as auth &NonFungibleToken.NFT?)!
576            let dssNFT = nft as! &DSSCollection.NFT
577            return dssNFT as &AnyResource{MetadataViews.Resolver}
578        }
579
580        destroy() {
581            destroy self.ownedNFTs
582        }
583
584        init() {
585            self.ownedNFTs <- {}
586        }
587    }
588
589    // public function that anyone can call to create a new empty collection
590    //
591    pub fun createEmptyCollection(): @NonFungibleToken.Collection {
592        return <- create Collection()
593    }
594
595    // An interface containing the Admin function that allows minting NFTs
596    //
597    pub resource interface NFTMinter {
598        pub fun mintNFT(collectionGroupID: UInt64, completionAddress: String, level: UInt8): @DSSCollection.NFT
599    }
600
601    // A resource that allows managing metadata and minting NFTs
602    //
603    pub resource Admin: NFTMinter {
604        // Record the nftIds that were used to complete a CollectionGroup
605        //
606        pub fun completedCollectionGroup(collectionGroupID: UInt64, userAddress: Address, nftIDs: [UInt64]) {
607            let collection = CollectionCompletedWith(collectionGroupID: collectionGroupID, nftIDs: nftIDs)
608            if DSSCollection.completedCollections[userAddress] == nil {
609                DSSCollection.completedCollections[userAddress] = [collection]
610            } else {
611                DSSCollection.completedCollections[userAddress]!.append(collection)
612            }
613
614            emit CollectionNFTCompletedWith(
615                collectionGroupID: collectionGroupID,
616                completionAddress: userAddress.toString(),
617                completionNftIds: nftIDs
618            )
619        }
620
621        // Borrow a Collection Group
622        //
623        pub fun borrowCollectionGroup(id: UInt64): &DSSCollection.CollectionGroup {
624            pre {
625                DSSCollection.collectionGroupByID[id] != nil: "Cannot borrow collection group, no such id"
626            }
627
628            return (&DSSCollection.collectionGroupByID[id] as &DSSCollection.CollectionGroup?)!
629        }
630
631        // Borrow a Slot
632        //
633        pub fun borrowSlot(id: UInt64): &DSSCollection.Slot {
634            pre {
635                DSSCollection.slotByID[id] != nil: "Cannot borrow slot, no such id"
636            }
637
638            return (&DSSCollection.slotByID[id] as &DSSCollection.Slot?)!
639        }
640
641        // Create a Collection Group
642        //
643        pub fun createCollectionGroup(
644            name: String,
645            description: String,
646            productName: String,
647            endTime: UFix64?,
648            metadata: {String: String}
649        ): UInt64 {
650            let collectionGroup <- create DSSCollection.CollectionGroup(
651                name: name,
652                description: description,
653                productName: productName,
654                endTime: endTime,
655                metadata: metadata
656            )
657            let collectionGroupID = collectionGroup.id
658            DSSCollection.collectionGroupByID[collectionGroup.id] <-! collectionGroup
659
660            return collectionGroupID
661        }
662
663        // Close a Collection Group
664        //
665        pub fun closeCollectionGroup(id: UInt64): UInt64 {
666            if let collectionGroup = &DSSCollection.collectionGroupByID[id] as &DSSCollection.CollectionGroup? {
667                collectionGroup.close()
668                return collectionGroup.id
669            }
670            panic("collection group does not exist")
671        }
672
673        // Create a Slot
674        //
675        pub fun createSlot(
676            collectionGroupID: UInt64,
677            logicalOperator: String,
678            required: Bool,
679            typeName: Type,
680            metadata: {String: String}
681        ): UInt64 {
682            let slot <- create DSSCollection.Slot(
683                collectionGroupID: collectionGroupID,
684                logicalOperator: logicalOperator,
685                required: required,
686                typeName: typeName,
687                metadata: metadata
688            )
689            let slotID = slot.id
690            DSSCollection.slotByID[slot.id] <-! slot
691            return slotID
692        }
693
694        // Create an Item in slot
695        //
696        pub fun createItemInSlot(
697            itemID: String,
698            points: UInt64,
699            itemType: String,
700            comparator: String,
701            slotID: UInt64
702        ) {
703            if let slot = &DSSCollection.slotByID[slotID] as &DSSCollection.Slot? {
704                slot.createItemInSlot(
705                     itemID: itemID,
706                     points: points,
707                     itemType: itemType,
708                     comparator: comparator
709                )
710                return
711            }
712            panic("Slot does not exist")
713        }
714
715        // Mint a single NFT
716        // The CollectionGroup for the given ID must already exist
717        //
718        pub fun mintNFT(collectionGroupID: UInt64, completionAddress: String, level: UInt8): @DSSCollection.NFT {
719            pre {
720                // Make sure the collection group exists
721                DSSCollection.collectionGroupByID.containsKey(collectionGroupID): "No such CollectionGroupID"
722            }
723
724            let nft <- self.borrowCollectionGroup(id: collectionGroupID).mint(completionAddress: completionAddress, level: level)
725
726            // Increment the count of minted NFTs for the Collection Group ID
727            let currentCount = DSSCollection.collectionGroupNFTCount[collectionGroupID] ?? 0
728            DSSCollection.collectionGroupNFTCount[collectionGroupID] = currentCount + 1
729
730            return <- nft
731        }
732    }
733
734    // DSSCollection contract initializer
735    //
736    init() {
737        // Set the named paths
738        self.CollectionStoragePath = /storage/DSSCollectionNFTCollection
739        self.CollectionPublicPath = /public/DSSCollectionNFTCollection
740        self.AdminStoragePath = /storage/CollectionGroupAdmin
741        self.MinterPrivatePath = /private/CollectionGroupMinter
742
743        // Initialize the entity counts
744        self.totalSupply = 0
745
746        // Initialize the metadata lookup dictionaries
747        self.collectionGroupNFTCount = {}
748        self.collectionGroupByID <- {}
749        self.slotByID <- {}
750        self.completedCollections = {}
751        self.extCollectionGroup = {}
752
753        // Create an Admin resource and save it to storage
754        let admin <- create Admin()
755        self.account.save(<-admin, to: self.AdminStoragePath)
756        // Link capabilites to the admin constrained to the Minter
757        // and Metadata interfaces
758        self.account.link<&DSSCollection.Admin{DSSCollection.NFTMinter}>(
759            self.MinterPrivatePath,
760            target: self.AdminStoragePath
761        )
762
763        emit ContractInitialized()
764    }
765}
766