Smart Contract

FINDNFTCatalog

A.097bafa4e0b48eef.FINDNFTCatalog

Deployed

2d ago
Feb 26, 2026, 03:12:51 AM UTC

Dependents

6 imports
1import NFTCatalog from 0x49a7cda3a1eecc29
2import MetadataViews from 0x1d7e57aa55817448
3
4// NFTCatalog
5//
6// A general purpose NFT registry for Flow NonFungibleTokens.
7//
8// Each catalog entry stores data about the NFT including
9// its collection identifier, nft type, storage and public paths, etc.
10//
11// To make an addition to the catalog you can propose an NFT and provide its metadata.
12// An Admin can approve a proposal which would add the NFT to the catalog
13
14access(all) contract FINDNFTCatalog {
15    // EntryAdded
16    // An NFT collection has been added to the catalog
17    access(all) event EntryAdded(
18        collectionIdentifier : String,
19        contractName : String,
20        contractAddress : Address,
21        nftType : Type,
22        storagePath: StoragePath,
23        publicPath: PublicPath,
24        publicLinkedType : Type,
25        displayName : String,
26        description: String,
27        externalURL : String
28    )
29
30    // EntryUpdated
31    // An NFT Collection has been updated in the catalog
32    access(all) event EntryUpdated(
33        collectionIdentifier : String,
34        contractName : String,
35        contractAddress : Address,
36        nftType : Type,
37        storagePath: StoragePath,
38        publicPath: PublicPath,
39        publicLinkedType : Type,
40        displayName : String,
41        description: String,
42        externalURL : String
43    )
44
45    // EntryRemoved
46    // An NFT Collection has been removed from the catalog
47    access(all) event EntryRemoved(collectionIdentifier : String)
48
49    // ProposalEntryAdded
50    // A new proposal to make an addtion to the catalog has been made
51    access(all) event ProposalEntryAdded(proposalID : UInt64, collectionIdentifier : String, message: String, status: String, proposer : Address)
52
53    // ProposalEntryUpdated
54    // A proposal has been updated
55    access(all) event ProposalEntryUpdated(proposalID : UInt64, collectionIdentifier : String, message: String, status: String, proposer : Address)
56
57    // ProposalEntryRemoved
58    // A proposal has been removed from storage
59    access(all) event ProposalEntryRemoved(proposalID : UInt64)
60
61    access(all) let ProposalManagerStoragePath: StoragePath
62    access(all) let ProposalManagerPublicPath: PublicPath
63
64    access(self) let catalog: {String : NFTCatalog.NFTCatalogMetadata} // { collectionIdentifier -> Metadata }
65    access(self) let catalogTypeData: {String : {String : Bool}} // Additional view to go from { NFT Type Identifier -> {Collection Identifier : Bool } }
66
67    access(self) let catalogProposals : {UInt64 : NFTCatalog.NFTCatalogProposal} // { ProposalID : Metadata }
68
69    access(self) var totalProposals : UInt64
70
71    // Get FIND and Dapper NFTCatalog 
72    access(all) fun getCatalog() : {String : NFTCatalog.NFTCatalogMetadata} {
73        let find = self.catalog 
74        let dapper = NFTCatalog.getCatalog()
75        for item in dapper.keys {
76            find[item] = dapper[item]
77        }
78        return find
79    }
80
81    access(all) fun getCatalogEntry(collectionIdentifier : String) : NFTCatalog.NFTCatalogMetadata? {
82        if let dapper = NFTCatalog.getCatalogEntry(collectionIdentifier : collectionIdentifier) {
83            return dapper
84        }
85        return self.catalog[collectionIdentifier]
86    }
87
88    access(all) fun getCollectionsForType(nftTypeIdentifier: String) : {String : Bool}? {
89        if let dapper = NFTCatalog.getCollectionsForType(nftTypeIdentifier: nftTypeIdentifier) {
90            // If there's dappers input and Find input, dappers will overwrite what's on find
91            if let find = self.catalogTypeData[nftTypeIdentifier] {
92                for item in dapper.keys {
93                    find[item] = dapper[item]
94                }
95                return find
96            }
97            // If there's only dapper input, return it
98            return dapper
99        }
100        // Else return what's on find
101        return self.catalogTypeData[nftTypeIdentifier]
102    }
103
104    access(all) fun getCatalogTypeData() : {String : {String : Bool}} {
105        let find = self.catalogTypeData 
106        let dapper = NFTCatalog.getCatalogTypeData()
107        for item in dapper.keys {
108            find[item] = dapper[item]
109        }
110        return find
111    }
112
113    // A helper function to get CollectionData directly from NFTIdentifier
114    access(all) fun getCollectionDataForType(nftTypeIdentifier: String) : NFTCatalog.NFTCollectionData? {
115        if let collections = FINDNFTCatalog.getCollectionsForType(nftTypeIdentifier: nftTypeIdentifier) {
116            if collections.length < 1 {
117                return nil
118            }
119            if let collection = FINDNFTCatalog.getCatalogEntry(collectionIdentifier : collections.keys[0]) {
120                return collection.collectionData
121            }
122        }
123        return nil
124    }
125
126    // helper function to get Paths 
127    access(all) fun getMetadataFromType(_ t: Type) : NFTCatalog.NFTCatalogMetadata? {
128        let collectionIdentifier = self.getCollectionsForType(nftTypeIdentifier: t.identifier) 
129        if collectionIdentifier == nil || collectionIdentifier!.length < 1 {
130            return nil
131        }
132        return self.getCatalogEntry(collectionIdentifier : collectionIdentifier!.keys[0])
133    }
134
135    // Get only FIND NFTCatalog
136    access(all) fun getFINDCatalog() : {String : NFTCatalog.NFTCatalogMetadata} {
137        return self.catalog
138    }
139
140    access(all) fun getFINDCatalogEntry(collectionIdentifier : String) : NFTCatalog.NFTCatalogMetadata? {
141        return self.catalog[collectionIdentifier]
142    }
143
144    access(all) fun getFINDCollectionsForType(nftTypeIdentifier: String) : {String : Bool}? {
145        return self.catalogTypeData[nftTypeIdentifier]
146    }
147
148    access(all) fun getFINDCatalogTypeData() : {String : {String : Bool}} {
149        return self.catalogTypeData
150    }
151
152    // Propose an NFT collection to the catalog
153    // @param collectionIdentifier: The unique name assinged to this nft collection
154    // @param metadata: The Metadata for the NFT collection that will be stored in the catalog
155    // @param message: A message to the catalog owners
156    // @param proposer: Who is making the proposition(the address needs to be verified)
157    access(all) fun proposeNFTMetadata(collectionIdentifier : String, metadata : NFTCatalog.NFTCatalogMetadata, message : String, proposer : Address) : UInt64 {
158        let proposerManagerCap = getAccount(proposer).capabilities.get<&NFTCatalog.NFTCatalogProposalManager>(NFTCatalog.ProposalManagerPublicPath)!
159
160        assert(proposerManagerCap.check(), message : "Proposer needs to set up a manager")
161
162        let proposerManagerRef = proposerManagerCap.borrow()!
163
164        assert(proposerManagerRef.getCurrentProposalEntry()! == collectionIdentifier, message: "Expected proposal entry does not match entry for the proposer")
165
166        let catalogProposal = NFTCatalog.NFTCatalogProposal(collectionIdentifier : collectionIdentifier, metadata : metadata, message : message, status: "IN_REVIEW", proposer: proposer)
167        self.totalProposals = self.totalProposals + 1
168        self.catalogProposals[self.totalProposals] = catalogProposal
169
170        emit ProposalEntryAdded(proposalID : self.totalProposals, collectionIdentifier : collectionIdentifier, message: catalogProposal.message, status: catalogProposal.status, proposer: catalogProposal.proposer)
171        return self.totalProposals
172    }
173
174    // Withdraw a proposal from the catalog
175    // @param proposalID: The ID of proposal you want to withdraw
176    access(all) fun withdrawNFTProposal(proposalID : UInt64) {
177        pre {
178            self.catalogProposals[proposalID] != nil : "Invalid Proposal ID"
179        }
180        let proposal = self.catalogProposals[proposalID]!
181        let proposer = proposal.proposer
182
183        let proposerManagerCap = getAccount(proposer).capabilities.get<&NFTCatalog.NFTCatalogProposalManager>(NFTCatalog.ProposalManagerPublicPath)!
184
185        assert(proposerManagerCap.check(), message : "Proposer needs to set up a manager")
186
187        let proposerManagerRef = proposerManagerCap.borrow()!
188
189        assert(proposerManagerRef.getCurrentProposalEntry()! == proposal.collectionIdentifier, message: "Expected proposal entry does not match entry for the proposer")
190
191        self.removeCatalogProposal(proposalID : proposalID)
192    }
193
194    access(all) fun getCatalogProposals() : {UInt64 : NFTCatalog.NFTCatalogProposal} {
195        return self.catalogProposals
196    }
197
198    access(all) fun getCatalogProposalEntry(proposalID : UInt64) : NFTCatalog.NFTCatalogProposal? {
199        return self.catalogProposals[proposalID]
200    }
201
202    access(all) fun createNFTCatalogProposalManager(): @NFTCatalog.NFTCatalogProposalManager {
203        return <- NFTCatalog.createNFTCatalogProposalManager()
204    }
205
206    access(account) fun addCatalogEntry(collectionIdentifier : String, metadata: NFTCatalog.NFTCatalogMetadata) {
207        pre {
208            self.catalog[collectionIdentifier] == nil : "The nft name has already been added to the catalog"
209        }
210
211        self.addCatalogTypeEntry(collectionIdentifier : collectionIdentifier , metadata: metadata)
212
213        self.catalog[collectionIdentifier] = metadata
214
215        emit EntryAdded(
216            collectionIdentifier : collectionIdentifier,
217            contractName : metadata.contractName,
218            contractAddress : metadata.contractAddress,
219            nftType: metadata.nftType,
220            storagePath: metadata.collectionData.storagePath,
221            publicPath: metadata.collectionData.publicPath,
222            publicLinkedType : metadata.collectionData.publicLinkedType,
223            displayName : metadata.collectionDisplay.name,
224            description: metadata.collectionDisplay.description,
225            externalURL : metadata.collectionDisplay.externalURL.url
226        )
227    }
228
229    access(account) fun updateCatalogEntry(collectionIdentifier : String , metadata: NFTCatalog.NFTCatalogMetadata) {
230        pre {
231            self.catalog[collectionIdentifier] != nil : "Invalid collection identifier"
232        }
233        // remove previous nft type entry
234        self.removeCatalogTypeEntry(collectionIdentifier : collectionIdentifier , metadata: metadata)
235        // add updated nft type entry
236        self.addCatalogTypeEntry(collectionIdentifier : collectionIdentifier , metadata: metadata)
237
238        self.catalog[collectionIdentifier] = metadata
239
240        let nftType = metadata.nftType
241
242        emit EntryUpdated(
243            collectionIdentifier : collectionIdentifier,
244            contractName : metadata.contractName,
245            contractAddress : metadata.contractAddress,
246            nftType: metadata.nftType,
247            storagePath: metadata.collectionData.storagePath,
248            publicPath: metadata.collectionData.publicPath,
249            publicLinkedType : metadata.collectionData.publicLinkedType,
250            displayName : metadata.collectionDisplay.name,
251            description: metadata.collectionDisplay.description,
252            externalURL : metadata.collectionDisplay.externalURL.url
253        )
254    }
255
256    access(account) fun removeCatalogEntry(collectionIdentifier : String) {
257        pre {
258            self.catalog[collectionIdentifier] != nil : "Invalid collection identifier"
259        }
260
261        self.removeCatalogTypeEntry(collectionIdentifier : collectionIdentifier , metadata: self.catalog[collectionIdentifier]!)
262        self.catalog.remove(key: collectionIdentifier)
263
264        emit EntryRemoved(collectionIdentifier : collectionIdentifier)
265    }
266
267    access(account) fun updateCatalogProposal(proposalID: UInt64, proposalMetadata : NFTCatalog.NFTCatalogProposal) {
268        self.catalogProposals[proposalID] = proposalMetadata
269
270        emit ProposalEntryUpdated(proposalID : proposalID, collectionIdentifier : proposalMetadata.collectionIdentifier, message: proposalMetadata.message, status: proposalMetadata.status, proposer: proposalMetadata.proposer)
271    }
272
273    access(account) fun removeCatalogProposal(proposalID : UInt64) {
274        self.catalogProposals.remove(key : proposalID)
275
276        emit ProposalEntryRemoved(proposalID : proposalID)
277    }
278
279    access(contract) fun addCatalogTypeEntry(collectionIdentifier : String , metadata: NFTCatalog.NFTCatalogMetadata) {
280        if self.catalogTypeData[metadata.nftType.identifier] != nil {
281            let typeData : {String : Bool} = self.catalogTypeData[metadata.nftType.identifier]!
282            assert(self.catalogTypeData[metadata.nftType.identifier]![collectionIdentifier] == nil, message : "The nft name has already been added to the catalog")
283            typeData[collectionIdentifier] = true
284            self.catalogTypeData[metadata.nftType.identifier] = typeData
285        } else {
286            let typeData : {String : Bool} = {}
287            typeData[collectionIdentifier] = true
288            self.catalogTypeData[metadata.nftType.identifier] = typeData
289        }
290    }
291
292    access(contract) fun removeCatalogTypeEntry(collectionIdentifier : String , metadata: NFTCatalog.NFTCatalogMetadata) {
293        let prevMetadata = self.catalog[collectionIdentifier]!
294        let prevCollectionsForType = self.catalogTypeData[prevMetadata.nftType.identifier]!
295        prevCollectionsForType.remove(key : collectionIdentifier)
296        if prevCollectionsForType.length == 0 {
297            self.catalogTypeData.remove(key: prevMetadata.nftType.identifier)
298        } else {
299            self.catalogTypeData[prevMetadata.nftType.identifier] = prevCollectionsForType
300        }
301    }
302
303    init() {
304        self.ProposalManagerStoragePath = /storage/FINDnftCatalogProposalManager
305        self.ProposalManagerPublicPath = /public/FINDnftCatalogProposalManager
306
307        self.totalProposals = 0
308        self.catalog = {}
309        self.catalogTypeData = {}
310
311        self.catalogProposals = {}
312    }
313}
314