Smart Contract
DSSCollection
A.ec55c6f3dae179c8.DSSCollection
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