Smart Contract
NFTList
A.15a918087ab12d86.NFTList
1/**
2> Author: Fixes Lab <https://github.com/fixes-world/>
3
4# NFT List - An on-chain list of Flow Standard Non-Fungible Tokens (NFTs).
5
6This is the basic contract of the NFT List.
7It will be used to store the list of all the Flow Standard Non-Fungible Tokens (NFTs) that are available on the Flow blockchain.
8
9*/
10import NonFungibleToken from 0x1d7e57aa55817448
11import MetadataViews from 0x1d7e57aa55817448
12import ViewResolver from 0x1d7e57aa55817448
13// TokenList Imports
14import ViewResolvers from 0x15a918087ab12d86
15import FTViewUtils from 0x15a918087ab12d86
16import NFTViewUtils from 0x15a918087ab12d86
17
18/// NFT List registry contract
19///
20access(all) contract NFTList {
21
22 /* --- Entitlement --- */
23
24 access(all) entitlement Maintainer
25 access(all) entitlement SuperAdmin
26
27
28 /* --- Events --- */
29
30 /// Event emitted when the contract is initialized
31 access(all) event ContractInitialized()
32
33 /// Event emitted when a new Non-Fungible Token is registered
34 access(all) event NFTCollectionRegistered(
35 _ address: Address,
36 _ contractName: String,
37 _ type: Type,
38 )
39 /// Event emitted when a new Non-Fungible Token is removed
40 access(all) event NFTCollectionRemoved(
41 _ address: Address,
42 _ contractName: String,
43 _ type: Type,
44 )
45 /// Event emitted when Reviewer Metadata is updated
46 access(all) event NFTListReviewerMetadataUpdated(
47 _ reviewer: Address,
48 name: String?,
49 url: String?,
50 )
51 /// Event emitted when reviewer rank is updated
52 access(all) event NFTListReviewerVerifiedUpdated(
53 _ reviewer: Address,
54 _ isVerified: Bool
55 )
56 /// Event emitted when reviewer rank is updated
57 access(all) event NFTListReviewerRankUpdated(
58 _ reviewer: Address,
59 _ rank: UInt8
60 )
61 /// Evenit emitted when an Editable FTDisplay is created
62 access(all) event NFTListReviewerEditableNFTCollectionDisplayCreated(
63 _ address: Address,
64 _ contractName: String,
65 reviewer: Address
66 )
67 /// Event emitted when a new Non-Fungible Token is reviewed
68 access(all) event NFTCollectionViewResolverUpdated(
69 _ address: Address,
70 _ contractName: String,
71 _ newResolverIdentifier: String
72 )
73 /// Event emitted when a Non-Fungible Token is evaluated
74 access(all) event NFTCollectionReviewEvaluated(
75 _ address: Address,
76 _ contractName: String,
77 _ rank: UInt8,
78 _ by: Address
79 )
80 /// Event emitted when a Non-Fungible Token is tagged
81 access(all) event NFTCollectionTagsAdded(
82 _ address: Address,
83 _ contractName: String,
84 _ tags: [String],
85 _ by: Address
86 )
87
88 /* --- Variable, Enums and Structs --- */
89
90 access(all) let registryStoragePath: StoragePath
91 access(all) let registryPublicPath: PublicPath
92 access(all) let reviewerStoragePath: StoragePath
93 access(all) let reviewerPublicPath: PublicPath
94 access(all) let maintainerStoragePath: StoragePath
95
96 /* --- Interfaces & Resources --- */
97
98 /// Interface for the FT Entry
99 ///
100 access(all) resource interface NFTEntryInterface {
101 /// Create an empty collection for the NFT
102 access(all)
103 fun createEmptyCollection(): @{NonFungibleToken.Collection}
104 // ----- View Methods -----
105 access(all)
106 view fun getIdentity(): NFTViewUtils.NFTIdentity
107 /// Get the NFT Type
108 access(all)
109 view fun getNFTType(): Type
110 /// Get the Collection Type
111 access(all)
112 view fun getCollectionType(): Type
113 /// Check if the Non-Fungible Token is reviewed by some one
114 access(all)
115 view fun isReviewedBy(_ reviewer: Address): Bool
116 /// Get the Non-Fungible Token Review
117 access(all)
118 view fun getReview(_ reviewer: Address): FTViewUtils.FTReview?
119 /// Get the display metadata of the FT, fallback to the highest rank reviewer
120 access(all)
121 fun getDisplay(_ reviewer: Address?): NFTViewUtils.NFTCollectionViewWithSource?
122 // ----- Quick Access For FTViews -----
123 /// Get the evaluation rank of the Non-Fungible Token, no fallback
124 access(all)
125 view fun getEvaluatedRank(_ reviewer: Address?): FTViewUtils.Evaluation? {
126 if let reviewerAddr = reviewer {
127 if let reviewRef = self.borrowReviewRef(reviewerAddr) {
128 return reviewRef.getEvaluationRank()
129 }
130 }
131 return nil
132 }
133 /// Get the tags of the Fungiuble Token, fallback to the highest rank reviewer
134 access(all)
135 fun getTags(_ reviewer: Address?): [String] {
136 if let fallbackedReviewerAddr = self.tryGetReviewer(reviewer) {
137 if let reviewRef = self.borrowReviewRef(fallbackedReviewerAddr) {
138 let returnTags = reviewRef.getTags()
139 // Add extra tags based on the evaluation rank if the reviewer is the fallbackedReviewer
140 if reviewer == fallbackedReviewerAddr {
141 if reviewRef.evalRank.rawValue == FTViewUtils.Evaluation.BLOCKED.rawValue {
142 returnTags.insert(at: 0, "Blocked")
143 } else if reviewRef.evalRank.rawValue == FTViewUtils.Evaluation.FEATURED.rawValue {
144 returnTags.insert(at: 0, "Featured")
145 returnTags.insert(at: 0, "Verified")
146 } else if reviewRef.evalRank.rawValue == FTViewUtils.Evaluation.VERIFIED.rawValue {
147 returnTags.insert(at: 0, "Verified")
148 } else if reviewRef.evalRank.rawValue == FTViewUtils.Evaluation.PENDING.rawValue {
149 returnTags.insert(at: 0, "Pending")
150 }
151 }
152 return returnTags
153 }
154 }
155 return []
156 }
157 // ----- Internal Methods: Used by Reviewer -----
158 /// Try to get a reviewer address
159 access(contract)
160 view fun tryGetReviewer(_ reviewer: Address?): Address? {
161 var reviewerAddr = reviewer
162 if reviewerAddr == nil {
163 let registry = NFTList.borrowRegistry()
164 reviewerAddr = registry.getHighestRankVerifiedCustomizedReviewer(self.getNFTType())
165 }
166 return reviewerAddr
167 }
168 access(contract)
169 view fun borrowReviewRef(_ reviewer: Address): &FTViewUtils.FTReview?
170 /// Add a new review to the FT
171 access(contract)
172 fun addReview(_ reviewer: Address, _ review: FTViewUtils.FTReview)
173 }
174
175 /// Resource for the Non-Fungible Token Entry
176 ///
177 access(all) resource NFTCollectionEntry: NFTEntryInterface {
178 access(all)
179 let identity: NFTViewUtils.NFTIdentity
180 // Reviewer => FTReview
181 access(self)
182 let reviewers: {Address: FTViewUtils.FTReview}
183 // view resolver
184 access(all)
185 var viewResolver: @{ViewResolver.Resolver}
186
187 init(
188 _ address: Address,
189 _ contractName: String,
190 ) {
191 self.reviewers = {}
192 self.identity = NFTViewUtils.NFTIdentity(address, contractName)
193 self.viewResolver <- ViewResolvers.createContractViewResolver(address: address, contractName: contractName)
194 // ensure display and vault exists
195 self.getDisplay(nil)
196 self.getCollectionData()
197 }
198
199 // ----- Implementing the FTMetadataInterface -----
200
201 /// Create an empty vault for the FT
202 ///
203 access(all)
204 fun createEmptyCollection(): @{NonFungibleToken.Collection} {
205 let contractRef = self.borrowNFTContract()
206 let nftType = self.identity.buildNFTType()
207 return <- contractRef.createEmptyCollection(nftType: nftType)
208 }
209
210 /// Get the Non-Fungible Token Identity
211 ///
212 access(all)
213 view fun getIdentity(): NFTViewUtils.NFTIdentity {
214 return self.identity
215 }
216
217 /// Check if the Non-Fungible Token is reviewed by some one
218 ///
219 access(all)
220 view fun isReviewedBy(_ reviewer: Address): Bool {
221 return self.reviewers[reviewer] != nil
222 }
223
224 /// Get the Non-Fungible Token Review
225 ///
226 access(all)
227 view fun getReview(_ reviewer: Address): FTViewUtils.FTReview? {
228 return self.reviewers[reviewer]
229 }
230
231 /// Get the display metadata of the FT
232 ///
233 access(all)
234 fun getDisplay(_ reviewer: Address?): NFTViewUtils.NFTCollectionViewWithSource? {
235 var source: Address? = nil
236
237 let viewResolver = self.borrowViewResolver()
238 var retNFTDisplay = MetadataViews.getNFTCollectionDisplay(viewResolver)
239
240 var reviewerAddr = self.tryGetReviewer(reviewer)
241 if let addr = reviewerAddr {
242 if let reviewerRef = NFTList.borrowReviewerPublic(addr) {
243 if let displayRef = reviewerRef.borrowNFTCollectionDisplayReader(self.identity.address, self.identity.contractName) {
244 let socials = retNFTDisplay?.socials ?? {}
245 let extraSocials = displayRef.getSocials()
246 for key in extraSocials.keys {
247 socials[key] = extraSocials[key]
248 }
249 source = addr
250 retNFTDisplay = MetadataViews.NFTCollectionDisplay(
251 name: displayRef.getName() ?? retNFTDisplay?.name ?? "Unkonwn",
252 description: displayRef.getDescription() ?? retNFTDisplay?.description ?? "No Description",
253 externalURL: displayRef.getExternalURL() ?? retNFTDisplay?.externalURL ?? MetadataViews.ExternalURL("https://fixes.world"),
254 squareImage: displayRef.getSquareImage() ?? retNFTDisplay?.squareImage ?? displayRef.getDefaultSquareImage(),
255 bannerImage: displayRef.getBannerImage() ?? retNFTDisplay?.bannerImage ?? displayRef.getDefaultBannerImage(),
256 socials: socials
257 )
258 }
259 }
260 }
261 return retNFTDisplay != nil
262 ? NFTViewUtils.NFTCollectionViewWithSource(source, retNFTDisplay!)
263 : nil
264 }
265
266 /// Get the vault data of the FT
267 ///
268 access(all)
269 fun getCollectionData(): MetadataViews.NFTCollectionData {
270 return MetadataViews.getNFTCollectionData(self.borrowViewResolver())
271 ?? panic("Failed to load the NFT Collection Data for ".concat(self.identity.toString()))
272 }
273
274 /// Get the FT Type
275 access(all)
276 view fun getNFTType(): Type {
277 return self.identity.buildNFTType()
278 }
279 /// Get the Collection Type
280 access(all)
281 view fun getCollectionType(): Type {
282 return self.identity.buildCollectionType()
283 }
284
285 // ----- Internal Methods -----
286
287 /// Add a new review to the FT
288 ///
289 access(contract)
290 fun addReview(_ reviewer: Address, _ review: FTViewUtils.FTReview) {
291 pre {
292 self.reviewers[reviewer] == nil:
293 "Reviewer already exists"
294 }
295 self.reviewers[reviewer] = review
296 }
297
298 /// Borrow the review reference
299 ///
300 access(contract)
301 view fun borrowReviewRef(_ reviewer: Address): &FTViewUtils.FTReview? {
302 return &self.reviewers[reviewer]
303 }
304
305 /// Borrow the View Resolver
306 ///
307 access(contract)
308 view fun borrowViewResolver(): &{ViewResolver.Resolver} {
309 return &self.viewResolver
310 }
311
312 /// Borrow the Non-Fungible Token Contract
313 ///
314 access(contract)
315 fun borrowNFTContract(): &{NonFungibleToken} {
316 return self.identity.borrowNFTContract()
317 }
318 }
319
320 /// Interface for the Non-Fungible Token Reviewer
321 ///
322 access(all) resource interface NFTListReviewerInterface {
323 access(all)
324 view fun getAddress(): Address {
325 return self.owner?.address ?? panic("Owner not found")
326 }
327 access(all)
328 view fun getName(): String? {
329 let metadata = self.getMetadata()
330 return metadata["name"]
331 }
332 access(all)
333 view fun getUrl(): String? {
334 let metadata = self.getMetadata()
335 return metadata["url"]
336 }
337 access(all)
338 view fun getMetadata(): {String: String}
339 access(all)
340 view fun getReviewedNFTAmount(): Int
341 access(all)
342 view fun getCustomizedNFTAmount(): Int
343 access(all)
344 view fun getReviewedNFTTypes(): [Type]
345 access(all)
346 view fun getFeaturedNFTTypes(): [Type]
347 access(all)
348 view fun getVerifiedNFTTypes(): [Type]
349 access(all)
350 view fun getBlockedNFTTypes(): [Type]
351 access(all)
352 view fun borrowNFTCollectionDisplayReaderByNftType(_ nftType: Type): &NFTViewUtils.EditableNFTCollectionDisplay?
353 access(all)
354 view fun borrowNFTCollectionDisplayReader(
355 _ address: Address,
356 _ contractName: String,
357 ): &NFTViewUtils.EditableNFTCollectionDisplay?
358 }
359
360 /// Maintainer interface for the Non-Fungible Token Reviewer
361 ///
362 access(all) resource interface NFTListReviewMaintainer {
363 /// Update Reviewer Metadata
364 access(Maintainer)
365 fun updateMetadata(name: String?, url: String?)
366 /// Review the Non-Fungible Token with Evaluation
367 access(Maintainer)
368 fun reviewNFTEvalute(
369 _ address: Address,
370 _ contractName: String,
371 rank: FTViewUtils.Evaluation
372 )
373 /// Review the Non-Fungible Token, add tags
374 access(Maintainer)
375 fun reviewNFTAddTags(
376 _ address: Address,
377 _ contractName: String,
378 tags: [String]
379 )
380 /// Register the Non-Fungible Token Display Patch
381 access(Maintainer)
382 fun registerNFTCollectionDisplayPatch(
383 _ address: Address,
384 _ contractName: String,
385 )
386 /// Borrow or create the FTDisplay editor
387 access(Maintainer)
388 view fun borrowNFTCollectionDisplayEditor(
389 _ address: Address,
390 _ contractName: String,
391 ): auth(NFTViewUtils.Editable) &NFTViewUtils.EditableNFTCollectionDisplay?
392 }
393
394 /// The resource for the FT Reviewer
395 ///
396 access(all) resource NFTListReviewer: NFTListReviewMaintainer, NFTListReviewerInterface {
397 access(self)
398 let metadata: {String: String}
399 access(self)
400 let storedDisplayPatches: @{Type: NFTViewUtils.EditableNFTCollectionDisplay}
401 access(self)
402 let reviewed: {Type: FTViewUtils.Evaluation}
403
404 init() {
405 self.metadata = {}
406 self.reviewed = {}
407 self.storedDisplayPatches <- {}
408 }
409
410 // --- Implement the FungibleTokenReviewMaintainer ---
411
412 /// Update Reviewer Metadata
413 ///
414 access(Maintainer)
415 fun updateMetadata(name: String?, url: String?) {
416 if name != nil {
417 self.metadata["name"] = name!
418 }
419
420 if url != nil {
421 self.metadata["url"] = url!
422 }
423
424 // emit the event
425 emit NFTListReviewerMetadataUpdated(
426 self.getAddress(),
427 name: name,
428 url: url,
429 )
430 }
431
432 /// Review the Non-Fungible Token with Evaluation
433 ///
434 access(Maintainer)
435 fun reviewNFTEvalute(
436 _ address: Address,
437 _ contractName: String,
438 rank: FTViewUtils.Evaluation
439 ) {
440 let registery = NFTList.borrowRegistry()
441 let entryRef = registery.borrowNFTEntryByContract(address, contractName)
442 ?? panic("Failed to load the Non-Fungible Token Entry")
443
444 let reviewerAddr = self.getAddress()
445 var isUpdated = false
446 if let reviewRef = entryRef.borrowReviewRef(reviewerAddr) {
447 if reviewRef.evalRank.rawValue != rank.rawValue {
448 reviewRef.updateEvaluationRank(rank)
449 isUpdated = true
450 }
451 } else {
452 entryRef.addReview(reviewerAddr, FTViewUtils.FTReview(rank))
453 isUpdated = true
454 }
455
456 // If not updated, then return
457 if !isUpdated {
458 return
459 }
460
461 let identity = entryRef.getIdentity()
462 let type = identity.buildNFTType()
463
464 // update reviewed status locally
465 self.reviewed[type] = rank
466
467 // emit the event
468 emit NFTCollectionReviewEvaluated(
469 identity.address,
470 identity.contractName,
471 rank.rawValue,
472 reviewerAddr
473 )
474 }
475
476 /// Review the Non-Fungible Token, add tags
477 ///
478 access(Maintainer)
479 fun reviewNFTAddTags(
480 _ address: Address,
481 _ contractName: String,
482 tags: [String]
483 ) {
484 pre {
485 tags.length > 0: "Tags should not be empty"
486 }
487
488 let registery = NFTList.borrowRegistry()
489 let entryRef = registery.borrowNFTEntryByContract(address, contractName)
490 ?? panic("Failed to load the Non-Fungible Token Entry")
491
492 let reviewerAddr = self.getAddress()
493 if entryRef.borrowReviewRef(reviewerAddr) == nil {
494 // add the review with UNVERIFIED evaluation
495 self.reviewNFTEvalute(address, contractName, rank: FTViewUtils.Evaluation.UNVERIFIED)
496 }
497 let ref = entryRef.borrowReviewRef(reviewerAddr)
498 ?? panic("Failed to load the Non-Fungible Token Review")
499 var isUpdated = false
500 for tag in tags {
501 // ingore the eval tags
502 if tag == "Verified" || tag == "Featured" || tag == "Pending" || tag == "Blocked" {
503 continue
504 }
505 if ref.addTag(tag) {
506 isUpdated = true
507 }
508 }
509
510 // If not updated, then return
511 if !isUpdated {
512 return
513 }
514
515 let identity = entryRef.getIdentity()
516 // emit the event
517 emit NFTCollectionTagsAdded(
518 identity.address,
519 identity.contractName,
520 tags,
521 reviewerAddr
522 )
523 }
524
525 /// Register the Non-Fungible Token Display Patch
526 ///
527 access(Maintainer)
528 fun registerNFTCollectionDisplayPatch(
529 _ address: Address,
530 _ contractName: String
531 ) {
532 let registery = NFTList.borrowRegistry()
533
534 let entry = registery.borrowNFTEntryByContract(address, contractName)
535 ?? panic("Token not registered")
536 let nftType = entry.getNFTType()
537 assert(
538 self.storedDisplayPatches[nftType] == nil,
539 message: "Editable FTDisplay already exists"
540 )
541
542 // create a Editable FTDisplay resource it the reviewer storage
543 let editableFTDisplay <- NFTViewUtils.createEditableCollectionDisplay(address, contractName)
544 let ftDisplayId = editableFTDisplay.uuid
545 // save in the resource
546 self.storedDisplayPatches[nftType] <-! editableFTDisplay
547
548 registery.onReviewerNFTCollectionCustomized(self.getAddress(), nftType)
549
550 // emit the event for editable FTDisplay
551 emit NFTListReviewerEditableNFTCollectionDisplayCreated(
552 address,
553 contractName,
554 reviewer: self.getAddress()
555 )
556 }
557
558 /// Borrow or create the editor
559 ///
560 access(Maintainer)
561 view fun borrowNFTCollectionDisplayEditor(
562 _ address: Address,
563 _ contractName: String,
564 ): auth(NFTViewUtils.Editable) &NFTViewUtils.EditableNFTCollectionDisplay? {
565 let nftType = NFTViewUtils.buildNFTType(address, contractName)
566 ?? panic("Failed to build the NFT Type")
567 return &self.storedDisplayPatches[nftType]
568 }
569
570 // --- Implement the FungibleTokenReviewerInterface ---
571
572 access(all)
573 view fun getMetadata(): {String: String} {
574 return self.metadata
575 }
576
577 /// Return the amount of Non-Fungible Token which reviewed by the reviewer
578 ///
579 access(all)
580 view fun getReviewedNFTAmount(): Int {
581 return self.reviewed.keys.length
582 }
583
584 /// Return the amount of Non-Fungible Token which display customized by the reviewer
585 ///
586 access(all)
587 view fun getCustomizedNFTAmount(): Int {
588 return self.storedDisplayPatches.keys.length
589 }
590
591 /// Return all Non-Fungible Token Types reviewed by the reviewer
592 ///
593 access(all)
594 view fun getReviewedNFTTypes(): [Type] {
595 return self.reviewed.keys
596 }
597
598 /// Return all Non-Fungible Token Types with FEATURED evaluation
599 ///
600 access(all)
601 view fun getFeaturedNFTTypes(): [Type] {
602 let reviewedRef = &self.reviewed as &{Type: FTViewUtils.Evaluation}
603 return self.reviewed.keys.filter(view fun(type: Type): Bool {
604 return reviewedRef[type]?.rawValue == FTViewUtils.Evaluation.FEATURED.rawValue
605 })
606 }
607
608 /// Return all Non-Fungible Token Types with VERIFIED or FEATURED evaluation
609 ///
610 access(all)
611 view fun getVerifiedNFTTypes(): [Type] {
612 let reviewedRef = &self.reviewed as &{Type: FTViewUtils.Evaluation}
613 return self.reviewed.keys.filter(view fun(type: Type): Bool {
614 return reviewedRef[type]?.rawValue == FTViewUtils.Evaluation.VERIFIED.rawValue
615 || reviewedRef[type]?.rawValue == FTViewUtils.Evaluation.FEATURED.rawValue
616 })
617 }
618
619 /// Return all Non-Fungible Token Types with BLOCKED evaluation
620 ///
621 access(all)
622 view fun getBlockedNFTTypes(): [Type] {
623 let reviewedRef = &self.reviewed as &{Type: FTViewUtils.Evaluation}
624 return self.reviewed.keys.filter(view fun(type: Type): Bool {
625 return reviewedRef[type]?.rawValue == FTViewUtils.Evaluation.BLOCKED.rawValue
626 })
627 }
628
629 /// Borrow the FTDisplay editor
630 ///
631 access(all)
632 view fun borrowNFTCollectionDisplayReader(
633 _ address: Address,
634 _ contractName: String,
635 ): &NFTViewUtils.EditableNFTCollectionDisplay? {
636 return self.borrowNFTCollectionDisplayEditor(address, contractName)
637 }
638
639 /// Borrow the FTDisplay editor
640 ///
641 access(all)
642 view fun borrowNFTCollectionDisplayReaderByNftType(_ nftType: Type): &NFTViewUtils.EditableNFTCollectionDisplay? {
643 return &self.storedDisplayPatches[nftType]
644 }
645
646 // --- Internal Methods ---
647 }
648
649 /// The Maintainer for the Non-Fungible Token Reviewer
650 ///
651 access(all) resource ReviewMaintainer: NFTListReviewMaintainer {
652 access(self)
653 let reviewerCap: Capability<auth(Maintainer) &NFTListReviewer>
654
655 init(
656 _ cap: Capability<auth(Maintainer) &NFTListReviewer>
657 ) {
658 pre {
659 cap.check(): "Invalid capability"
660 }
661 self.reviewerCap = cap
662 }
663
664 /// Get the reviewer address
665 ///
666 access(all)
667 view fun getReviewerAddress(): Address {
668 return self.reviewerCap.address
669 }
670
671 /// Update Reviewer Metadata
672 ///
673 access(Maintainer)
674 fun updateMetadata(name: String?, url: String?) {
675 self._borrowReviewer().updateMetadata(name: name, url: url)
676 }
677
678 /// Review the Non-Fungible Token with Evaluation
679 ///
680 access(Maintainer)
681 fun reviewNFTEvalute(_ address: Address, _ contractName: String, rank: FTViewUtils.Evaluation) {
682 self._borrowReviewer().reviewNFTEvalute(address, contractName, rank: rank)
683 }
684
685 /// Review the Non-Fungible Token, add tags
686 ///
687 access(Maintainer)
688 fun reviewNFTAddTags(_ address: Address, _ contractName: String, tags: [String]) {
689 self._borrowReviewer().reviewNFTAddTags(address, contractName, tags: tags)
690 }
691
692 /// Register the Non-Fungible Token Display Patch
693 access(Maintainer)
694 fun registerNFTCollectionDisplayPatch(
695 _ address: Address,
696 _ contractName: String
697 ) {
698 self._borrowReviewer().registerNFTCollectionDisplayPatch(address, contractName)
699 }
700
701 /// Borrow or create the FTDisplay editor
702 access(Maintainer)
703 view fun borrowNFTCollectionDisplayEditor(
704 _ address: Address,
705 _ contractName: String,
706 ): auth(NFTViewUtils.Editable) &NFTViewUtils.EditableNFTCollectionDisplay? {
707 return self._borrowReviewer().borrowNFTCollectionDisplayEditor(address, contractName)
708 }
709
710 /* ---- Internal Methods ---- */
711
712 access(self)
713 view fun _borrowReviewer(): auth(Maintainer) &NFTListReviewer {
714 return self.reviewerCap.borrow() ?? panic("Failed to borrow the reviewer")
715 }
716 }
717
718 /// Interface for the Token List Viewer
719 ///
720 access(all) resource interface NFTListViewer {
721 // --- Read Methods ---
722 /// Return all available reviewers
723 access(all)
724 view fun getReviewers(): [Address]
725 /// Get the reviewer rank
726 access(all)
727 view fun getReviewerRank(_ reviewer: Address): ReviewerRank?
728 /// Return all verified reviewers
729 access(all)
730 view fun getVerifiedReviewers(): [Address]
731 /// Return if the reviewer is verified
732 access(all)
733 view fun isReviewerVerified(_ reviewer: Address): Bool
734 /// Get the amount of Non-Fungible Token Entries
735 access(all)
736 view fun getNFTEntriesAmount(): Int
737 /// Get all the Non-Fungible Token Entries
738 access(all)
739 view fun getAllNFTEntries(): [Type]
740 /// Get the Non-Fungible Token Entry by the page and size
741 access(all)
742 view fun getNFTEntries(_ page: Int, _ size: Int): [Type]
743 /// Get the Non-Fungible Token Entry by the address
744 access(all)
745 view fun getNFTEntriesByAddress(_ address: Address): [Type]
746 /// Get the customized reviewers
747 access(all)
748 view fun getHighestRankVerifiedCustomizedReviewer(_ nftType: Type): Address?
749 /// Borrow the NFT Entry by the type
750 access(all)
751 view fun borrowNFTEntry(_ nftType: Type): &NFTCollectionEntry?
752 /// Borrow the NFT Entry by the contract address
753 access(all)
754 view fun borrowNFTEntryByContract(_ address: Address, _ contractName: String): &NFTCollectionEntry?
755 // --- Write Methods ---
756 /// Register a new standard Non-Fungible Token Entry to the registry
757 access(contract)
758 fun registerStandardNonFungibleToken(_ address: Address, _ contractName: String)
759 /// Invoked when a new Non-Fungible Token is customized by the reviewer
760 access(contract)
761 fun onReviewerNFTCollectionCustomized(_ reviewer: Address, _ nftType: Type)
762 }
763
764 access(all) enum ReviewerRank: UInt8 {
765 access(all) case NORMAL
766 access(all) case ADVANCED
767 access(all) case EXPERT
768 }
769
770 /// The Token List Registry
771 ///
772 access(all) resource Registry: NFTListViewer {
773 // Address => isVerified
774 access(self)
775 let verifiedReviewers: {Address: Bool}
776 access(self)
777 let reviewerRanks: {Address: ReviewerRank}
778 // NFT Type => NFT Collection Entry
779 access(self)
780 let entries: @{Type: NFTCollectionEntry}
781 // Entry ID => NFT Type
782 access(self)
783 let entriesIdMapping: {UInt64: Type}
784 // Address => [contractName]
785 access(self)
786 let addressMapping: {Address: [String]}
787 // Customized NFT Views
788 access(self)
789 let customizedNFTViews: {Type: [Address]}
790
791 init() {
792 self.verifiedReviewers = {}
793 self.reviewerRanks = {}
794 self.entriesIdMapping = {}
795 self.entries <- {}
796 self.addressMapping = {}
797 self.customizedNFTViews = {}
798 }
799
800 // ----- Read Methods -----
801
802 /// Return all available reviewers
803 ///
804 access(all)
805 view fun getReviewers(): [Address] {
806 return self.reviewerRanks.keys
807 }
808
809 /// Get the reviewer rank
810 ///
811 access(all)
812 view fun getReviewerRank(_ reviewer: Address): ReviewerRank? {
813 return self.reviewerRanks[reviewer]
814 }
815
816 /// Return all verified reviewers
817 ///
818 access(all)
819 view fun getVerifiedReviewers(): [Address] {
820 return self.verifiedReviewers.keys
821 }
822
823 /// Return if the reviewer is verified
824 ///
825 access(all)
826 view fun isReviewerVerified(_ reviewer: Address): Bool {
827 return self.verifiedReviewers[reviewer] ?? false
828 }
829
830 /// Get the amount of Non-Fungible Token Entries
831 access(all)
832 view fun getNFTEntriesAmount(): Int {
833 return self.entries.keys.length
834 }
835
836 /// Get all the Non-Fungible Token Entries
837 ///
838 access(all)
839 view fun getAllNFTEntries(): [Type] {
840 return self.entries.keys
841 }
842
843 /// Get the Non-Fungible Token Entry by the page and size
844 ///
845 access(all)
846 view fun getNFTEntries(_ page: Int, _ size: Int): [Type] {
847 pre {
848 page >= 0: "Invalid page"
849 size > 0: "Invalid size"
850 }
851 let max = self.getNFTEntriesAmount()
852 let start = page * size
853 if start > max {
854 return []
855 }
856 var end = start + size
857 if end > max {
858 end = max
859 }
860 return self.entries.keys.slice(from: start, upTo: end)
861 }
862
863 /// Get the Non-Fungible Token Entry by the address
864 ///
865 access(all)
866 view fun getNFTEntriesByAddress(_ address: Address): [Type] {
867 if let contracts = self.borrowAddressContractsRef(address) {
868 var types: [Type] = []
869 for contractName in contracts {
870 if let type = NFTViewUtils.buildNFTType(address, contractName) {
871 types = types.concat([type])
872 }
873 }
874 return types
875 }
876 return []
877 }
878
879 /// Get the highest rank customized reviewers
880 ///
881 access(all)
882 view fun getHighestRankVerifiedCustomizedReviewer(_ nftType: Type): Address? {
883 if let reviewers = self.customizedNFTViews[nftType] {
884 var highestRank: ReviewerRank? = nil
885 var highestReviewer: Address? = nil
886 for reviewer in reviewers {
887 if self.verifiedReviewers[reviewer] != true {
888 continue
889 }
890 if let rank = self.reviewerRanks[reviewer] {
891 if highestRank == nil || rank.rawValue > highestRank!.rawValue {
892 highestRank = rank
893 highestReviewer = reviewer
894 // break if the reviewer is verified
895 if self.verifiedReviewers[reviewer] == true {
896 break
897 }
898 }
899 }
900 }
901 return highestReviewer
902 }
903 return nil
904 }
905
906 /// Borrow the NFT Entry by the type
907 access(all)
908 view fun borrowNFTEntry(_ nftType: Type): &NFTCollectionEntry? {
909 return &self.entries[nftType]
910 }
911
912 /// Borrow the NFT Entry by the contract address
913 access(all)
914 view fun borrowNFTEntryByContract(_ address: Address, _ contractName: String): &NFTCollectionEntry? {
915 var entry: &NFTCollectionEntry? = nil
916 if let collectionType = NFTViewUtils.buildCollectionType(address, contractName) {
917 entry = self.borrowNFTEntry(collectionType)
918 }
919 if entry == nil {
920 if let nftType = NFTViewUtils.buildNFTType(address, contractName) {
921 entry = self.borrowNFTEntry(nftType)
922 }
923 }
924 return entry
925 }
926
927 // ----- Write Methods -----
928
929 /// Update the reviewer verified status
930 ///
931 access(SuperAdmin)
932 fun updateReviewerVerified(_ reviewer: Address, _ verified: Bool) {
933 pre {
934 NFTList.borrowReviewerPublic(reviewer) != nil: "FT Reviewer not found"
935 }
936 self.verifiedReviewers[reviewer] = verified
937
938 // emit the event
939 emit NFTListReviewerVerifiedUpdated(
940 reviewer,
941 verified
942 )
943 }
944
945 /// Remove a Non-Fungible Token Entry from the registry
946 ///
947 access(SuperAdmin)
948 fun removeNFTCollection(_ type: Type): Bool {
949 return self._removeNFTCollection(type)
950 }
951
952 /// Register a new standard Non-Fungible Token Entry to the registry
953 ///
954 access(contract)
955 fun registerStandardNonFungibleToken(_ address: Address, _ contractName: String) {
956 pre {
957 NFTList.isNFTCollectionRegistered(address, contractName) == false: "Fungible Token already registered"
958 }
959 self._register(<- create NFTCollectionEntry(address, contractName))
960 }
961
962 /// Invoked when a new Non-Fungible Token is customized by the reviewer
963 ///
964 access(contract)
965 fun onReviewerNFTCollectionCustomized(_ reviewer: Address, _ nftType: Type) {
966 pre {
967 NFTList.borrowReviewerPublic(reviewer) != nil: "FT Reviewer not found"
968 }
969 // ensure the tokenType exists
970 if self.customizedNFTViews[nftType] == nil {
971 self.customizedNFTViews[nftType] = []
972 }
973 // add the reviewer to the list
974 if let arrRef = &self.customizedNFTViews[nftType] as &[Address]? {
975 if arrRef.firstIndex(of: reviewer) == nil {
976 self.customizedNFTViews[nftType]?.append(reviewer)
977 }
978 }
979 // update the rank
980 self._updateReviewerRank(reviewer)
981 }
982
983 // ----- Internal Methods -----
984
985 access(self)
986 fun _updateReviewerRank(_ reviewer: Address) {
987 let reviewerRef = NFTList.borrowReviewerPublic(reviewer)
988 ?? panic("Could not find the FT Reviewer")
989 let oldRank = self.reviewerRanks[reviewer]
990 var newRank: NFTList.ReviewerRank? = nil
991 // ensure the reviewer rank exists
992 if self.reviewerRanks[reviewer] == nil {
993 newRank = ReviewerRank.NORMAL
994 } else {
995 // update the rank by the amount of customized FT
996 let reviewedAmt = reviewerRef.getReviewedNFTAmount()
997 let customizedAmt = reviewerRef.getCustomizedNFTAmount()
998 let weight = customizedAmt * 5 + reviewedAmt
999 if weight >= 1000 {
1000 newRank = ReviewerRank.EXPERT
1001 } else if weight >= 200 {
1002 newRank = ReviewerRank.ADVANCED
1003 }
1004 }
1005
1006 if newRank != nil && oldRank != newRank {
1007 self.reviewerRanks[reviewer] = newRank!
1008 // emit the event
1009 emit NFTListReviewerRankUpdated(
1010 reviewer,
1011 newRank!.rawValue
1012 )
1013 }
1014 }
1015
1016 /// Remove a Non-Fungible Token Entry from the registry
1017 ///
1018 access(self)
1019 fun _removeNFTCollection(_ nftType: Type): Bool {
1020 if self.entries[nftType] == nil {
1021 return false
1022 }
1023 let entry <- self.entries.remove(key: nftType)
1024 ?? panic("Could not remove the Non-Fungible Token Entry")
1025 self.entriesIdMapping.remove(key: entry.uuid)
1026
1027 let identity = entry.getIdentity()
1028 if let addrRef = self.borrowAddressContractsRef(identity.address) {
1029 if let idx = addrRef.firstIndex(of: identity.contractName) {
1030 addrRef.remove(at: idx)
1031 }
1032 }
1033 destroy entry
1034
1035 // emit the event
1036 emit NFTCollectionRemoved(
1037 identity.address,
1038 identity.contractName,
1039 nftType
1040 )
1041 return true
1042 }
1043
1044 /// Add a new Non-Fungible Token Entry to the registry
1045 ///
1046 access(self)
1047 fun _register(_ entry: @NFTCollectionEntry) {
1048 pre {
1049 self.entries[entry.getNFTType()] == nil:
1050 "FungibleToken Entry already exists in the registry"
1051 self.entriesIdMapping[entry.uuid] == nil:
1052 "FungibleToken Entry ID already exists in the registry"
1053 }
1054 let nftType = entry.getNFTType()
1055 self.entries[nftType] <-! entry
1056
1057 let ref = &self.entries[nftType] as &NFTCollectionEntry?
1058 ?? panic("Could not borrow the FT Entry")
1059 // Add the ID mapping
1060 self.entriesIdMapping[ref.uuid] = nftType
1061 // Add the address mapping
1062 if let addrRef = self.borrowAddressContractsRef(ref.identity.address) {
1063 addrRef.append(ref.identity.contractName)
1064 } else {
1065 self.addressMapping[ref.identity.address] = [ref.identity.contractName]
1066 }
1067
1068 // emit the event
1069 emit NFTCollectionRegistered(
1070 ref.identity.address,
1071 ref.identity.contractName,
1072 nftType
1073 )
1074 }
1075
1076 access(self)
1077 view fun borrowAddressContractsRef(_ addr: Address): auth(Mutate) &[String]? {
1078 return &self.addressMapping[addr]
1079 }
1080 }
1081
1082 /* --- Public Methods --- */
1083
1084 /// Create a new NFT List Reviewer
1085 ///
1086 access(all)
1087 fun createNFTListReviewer(): @NFTListReviewer {
1088 return <- create NFTListReviewer()
1089 }
1090
1091 /// Create a new Non-Fungible Token Review Maintainer
1092 ///
1093 access(all)
1094 fun createNFTListReviewMaintainer(
1095 _ cap: Capability<auth(Maintainer) &NFTListReviewer>
1096 ): @ReviewMaintainer {
1097 return <- create ReviewMaintainer(cap)
1098 }
1099
1100 /// Get the Non-Fungible Token Reviewer capability
1101 ///
1102 access(all)
1103 view fun getReviewerCapability(_ addr: Address): Capability<&NFTListReviewer> {
1104 return getAccount(addr).capabilities
1105 .get<&NFTListReviewer>(self.reviewerPublicPath)
1106 }
1107
1108 /// Borrow the public capability of the Non-Fungible Token Reviewer
1109 ///
1110 access(all)
1111 view fun borrowReviewerPublic(_ addr: Address): &NFTListReviewer? {
1112 return self.getReviewerCapability(addr).borrow()
1113 }
1114
1115 /// Borrow the public capability of Token List Registry
1116 ///
1117 access(all)
1118 view fun borrowRegistry(): &{NFTListViewer} {
1119 return getAccount(self.account.address)
1120 .capabilities.get<&{NFTListViewer}>(self.registryPublicPath)
1121 .borrow()
1122 ?? panic("Could not borrow the Registry reference")
1123 }
1124
1125 /// Check if the NFT is registered
1126 ///
1127 access(all)
1128 view fun isNFTCollectionRegistered(_ address: Address, _ contractName: String): Bool {
1129 let registry: &{NFTList.NFTListViewer} = self.borrowRegistry()
1130 let entry = registry.borrowNFTEntryByContract(address, contractName)
1131 return entry != nil
1132 }
1133
1134 /// Check if the NFT is valid to register
1135 access(all)
1136 fun isValidToRegister(_ address: Address, _ contractName: String): Bool {
1137 if let contractRef = ViewResolvers.borrowContractViewResolver(address, contractName) {
1138 let colDisplayType = Type<MetadataViews.NFTCollectionDisplay>()
1139 let colDataType = Type<MetadataViews.NFTCollectionData>()
1140 let contractViews = contractRef.getContractViews(resourceType: nil)
1141 if contractViews.contains(colDataType) == false || contractViews.contains(colDisplayType) == false {
1142 return false
1143 }
1144 var convertedDisplayView: MetadataViews.NFTCollectionDisplay? = nil
1145 var convertedDataView: MetadataViews.NFTCollectionData? = nil
1146 if let displayView = contractRef.resolveContractView(resourceType: nil, viewType: colDisplayType) {
1147 convertedDisplayView = displayView as? MetadataViews.NFTCollectionDisplay
1148 } else {
1149 return false
1150 }
1151 if let dataView = contractRef.resolveContractView(resourceType: nil, viewType: colDataType) {
1152 convertedDataView = dataView as? MetadataViews.NFTCollectionData
1153 } else {
1154 return false
1155 }
1156 if convertedDisplayView != nil && convertedDataView != nil {
1157 return true
1158 }
1159 }
1160 return false
1161 }
1162
1163 /// Try to register a new NFT, if already registered, then do nothing
1164 ///
1165 access(all)
1166 fun ensureNFTCollectionRegistered(_ address: Address, _ contractName: String) {
1167 if !self.isNFTCollectionRegistered(address, contractName) && self.isValidToRegister(address, contractName) {
1168 let registry = self.borrowRegistry()
1169 registry.registerStandardNonFungibleToken(address, contractName)
1170 }
1171 }
1172
1173 /// The prefix for the paths
1174 ///
1175 access(all)
1176 view fun getPathPrefix(): String {
1177 return "NFTList_".concat(self.account.address.toString()).concat("_")
1178 }
1179
1180 /// Generate the review maintainer capability ID
1181 ///
1182 access(all)
1183 view fun generateReviewMaintainerCapabilityId(_ addr: Address): String {
1184 return NFTList.getPathPrefix().concat("PrivateIdentity_ReviewMaintainer_".concat(addr.toString()))
1185 }
1186
1187 init() {
1188 // Identifiers
1189 let identifier = NFTList.getPathPrefix()
1190 self.registryStoragePath = StoragePath(identifier: identifier.concat("_Registry"))!
1191 self.registryPublicPath = PublicPath(identifier: identifier.concat("_Registry"))!
1192
1193 self.reviewerStoragePath = StoragePath(identifier: identifier.concat("_Reviewer"))!
1194 self.reviewerPublicPath = PublicPath(identifier: identifier.concat("_Reviewer"))!
1195
1196 self.maintainerStoragePath = StoragePath(identifier: identifier.concat("_Maintainer"))!
1197
1198 // Create the Token List Registry
1199 let registry <- create Registry()
1200 self.account.storage.save(<- registry, to: self.registryStoragePath)
1201 // link the public capability
1202 let cap = self.account.capabilities
1203 .storage.issue<&{NFTListViewer}>(self.registryStoragePath)
1204 self.account.capabilities.publish(cap, at: self.registryPublicPath)
1205
1206 // Emit the initialized event
1207 emit ContractInitialized()
1208 }
1209}
1210