Smart Contract

NFTStorefrontV2

A.eee6bdee2b2bdfc8.NFTStorefrontV2

Deployed

3h ago
Mar 01, 2026, 01:38:18 AM UTC

Dependents

0 imports
1import FungibleToken from 0xf233dcee88fe0abe
2import NonFungibleToken from 0x1d7e57aa55817448
3
4/// NFTStorefrontV2
5///
6/// A general purpose sale support contract for NFTs that implement the Flow NonFungibleToken standard.
7/// 
8/// Each account that wants to list NFTs for sale installs a Storefront,
9/// and lists individual sales within that Storefront as Listings.
10/// There is one Storefront per account, it handles sales of all NFT types
11/// for that account.
12///
13/// Each Listing can have one or more "cuts" of the sale price that
14/// goes to one or more addresses. Cuts can be used to pay listing fees
15/// or other considerations. 
16/// Each Listing can include a commission amount that is paid to whoever facilitates
17/// the purchase. The seller can also choose to provide an optional list of marketplace 
18/// receiver capabilities. In this case, the commission amount must be transferred to
19/// one of the capabilities in the list.
20///
21/// Each NFT may be listed in one or more Listings, the validity of each
22/// Listing can easily be checked.
23/// 
24/// Purchasers can watch for Listing events and check the NFT type and
25/// ID to see if they wish to buy the listed item.
26/// Marketplaces and other aggregators can watch for Listing events
27/// and list items of interest.
28///
29pub contract NFTStorefrontV2 {
30
31    /// StorefrontInitialized
32    /// A Storefront resource has been created.
33    /// Event consumers can now expect events from this Storefront.
34    /// Note that we do not specify an address: we cannot and should not.
35    /// Created resources do not have an owner address, and may be moved
36    /// after creation in ways we cannot check.
37    /// ListingAvailable events can be used to determine the address
38    /// of the owner of the Storefront (...its location) at the time of
39    /// the listing but only at that precise moment in that precise transaction.
40    /// If the seller moves the Storefront while the listing is valid, 
41    /// that is on them.
42    ///
43    pub event StorefrontInitialized(storefrontResourceID: UInt64)
44
45    /// StorefrontDestroyed
46    /// A Storefront has been destroyed.
47    /// Event consumers can now stop processing events from this Storefront.
48    /// Note that we do not specify an address.
49    ///
50    pub event StorefrontDestroyed(storefrontResourceID: UInt64)
51
52    /// ListingAvailable
53    /// A listing has been created and added to a Storefront resource.
54    /// The Address values here are valid when the event is emitted, but
55    /// the state of the accounts they refer to may change outside of the
56    /// NFTStorefrontV2 workflow, so be careful to check when using them.
57    ///
58    pub event ListingAvailable(
59        storefrontAddress: Address,
60        listingResourceID: UInt64,
61        nftType: Type,
62        nftUUID: UInt64, 
63        nftID: UInt64,
64        salePaymentVaultType: Type,
65        salePrice: UFix64,
66        customID: String?,
67        commissionAmount: UFix64,
68        commissionReceivers: [Address]?,
69        expiry: UInt64
70    )
71
72    /// ListingCompleted
73    /// The listing has been resolved. It has either been purchased, removed or destroyed.
74    ///
75    pub event ListingCompleted(
76        listingResourceID: UInt64, 
77        storefrontResourceID: UInt64, 
78        purchased: Bool,
79        nftType: Type,
80        nftUUID: UInt64,
81        nftID: UInt64,
82        salePaymentVaultType: Type,
83        salePrice: UFix64,
84        customID: String?,
85        commissionAmount: UFix64,
86        commissionReceiver: Address?,
87        expiry: UInt64
88    )
89
90    /// UnpaidReceiver
91    /// A entitled receiver has not been paid during the sale of the NFT.
92    ///
93    pub event UnpaidReceiver(receiver: Address, entitledSaleCut: UFix64)
94
95    /// StorefrontStoragePath
96    /// The location in storage that a Storefront resource should be located.
97    pub let StorefrontStoragePath: StoragePath
98
99    /// StorefrontPublicPath
100    /// The public location for a Storefront link.
101    pub let StorefrontPublicPath: PublicPath
102
103
104    /// SaleCut
105    /// A struct representing a recipient that must be sent a certain amount
106    /// of the payment when a token is sold.
107    ///
108    pub struct SaleCut {
109        /// The receiver for the payment.
110        /// Note that we do not store an address to find the Vault that this represents,
111        /// as the link or resource that we fetch in this way may be manipulated,
112        /// so to find the address that a cut goes to you must get this struct and then
113        /// call receiver.borrow()!.owner.address on it.
114        /// This can be done efficiently in a script.
115        pub let receiver: Capability<&{FungibleToken.Receiver}>
116
117        /// The amount of the payment FungibleToken that will be paid to the receiver.
118        pub let amount: UFix64
119
120        /// initializer
121        ///
122        init(receiver: Capability<&{FungibleToken.Receiver}>, amount: UFix64) {
123            self.receiver = receiver
124            self.amount = amount
125        }
126    }
127
128
129    /// ListingDetails
130    /// A struct containing a Listing's data.
131    ///
132    pub struct ListingDetails {
133        /// The Storefront that the Listing is stored in.
134        /// Note that this resource cannot be moved to a different Storefront,
135        /// so this is OK. If we ever make it so that it *can* be moved,
136        /// this should be revisited.
137        pub var storefrontID: UInt64
138        /// Whether this listing has been purchased or not.
139        pub var purchased: Bool
140        /// The Type of the NonFungibleToken.NFT that is being listed.
141        pub let nftType: Type
142        /// The Resource ID of the NFT which can only be set in the contract
143        pub let nftUUID: UInt64
144        /// The unique identifier of the NFT that will get sell.
145        pub let nftID: UInt64
146        /// The Type of the FungibleToken that payments must be made in.
147        pub let salePaymentVaultType: Type
148        /// The amount that must be paid in the specified FungibleToken.
149        pub let salePrice: UFix64
150        /// This specifies the division of payment between recipients.
151        pub let saleCuts: [SaleCut]
152        /// Allow different dapp teams to provide custom strings as the distinguisher string
153        /// that would help them to filter events related to their customID.
154        pub var customID: String?
155        /// Commission available to be claimed by whoever facilitates the sale.
156        pub let commissionAmount: UFix64
157        /// Expiry of listing
158        pub let expiry: UInt64
159
160        /// Irreversibly set this listing as purchased.
161        ///
162        access(contract) fun setToPurchased() {
163            self.purchased = true
164        }
165
166        access(contract) fun setCustomID(customID: String?){
167            self.customID = customID
168        }
169
170        /// Initializer
171        ///
172        init (
173            nftType: Type,
174            nftUUID: UInt64,
175            nftID: UInt64,
176            salePaymentVaultType: Type,
177            saleCuts: [SaleCut],
178            storefrontID: UInt64,
179            customID: String?,
180            commissionAmount: UFix64,
181            expiry: UInt64
182        ) {
183
184            pre {
185                // Validate the expiry
186                expiry > UInt64(getCurrentBlock().timestamp) : "Expiry should be in the future"
187                // Validate the length of the sale cut
188                saleCuts.length > 0: "Listing must have at least one payment cut recipient"
189            }
190            self.storefrontID = storefrontID
191            self.purchased = false
192            self.nftType = nftType
193            self.nftUUID = nftUUID
194            self.nftID = nftID
195            self.salePaymentVaultType = salePaymentVaultType
196            self.customID = customID
197            self.commissionAmount = commissionAmount
198            self.expiry = expiry
199            self.saleCuts = saleCuts
200
201            // Calculate the total price from the cuts
202            var salePrice = commissionAmount
203            // Perform initial check on capabilities, and calculate sale price from cut amounts.
204            for cut in self.saleCuts {
205                // Make sure we can borrow the receiver.
206                // We will check this again when the token is sold.
207                cut.receiver.borrow()
208                    ?? panic("Cannot borrow receiver")
209                // Add the cut amount to the total price
210                salePrice = salePrice + cut.amount
211            }
212            assert(salePrice > 0.0, message: "Listing must have non-zero price")
213
214            // Store the calculated sale price
215            self.salePrice = salePrice
216        }
217    }
218
219
220    /// ListingPublic
221    /// An interface providing a useful public interface to a Listing.
222    ///
223    pub resource interface ListingPublic {
224        /// borrowNFT
225        /// This will assert in the same way as the NFT standard borrowNFT()
226        /// if the NFT is absent, for example if it has been sold via another listing.
227        ///
228        pub fun borrowNFT(): &NonFungibleToken.NFT?
229
230        /// purchase
231        /// Purchase the listing, buying the token.
232        /// This pays the beneficiaries and returns the token to the buyer.
233        ///
234        pub fun purchase(
235            payment: @FungibleToken.Vault, 
236            commissionRecipient: Capability<&{FungibleToken.Receiver}>?,
237        ): @NonFungibleToken.NFT
238
239        /// getDetails
240        /// Fetches the details of the listing.
241        pub fun getDetails(): ListingDetails
242
243        /// getAllowedCommissionReceivers
244        /// Fetches the allowed marketplaces capabilities or commission receivers.
245        /// If it returns `nil` then commission is up to grab by anyone.
246        pub fun getAllowedCommissionReceivers(): [Capability<&{FungibleToken.Receiver}>]?
247
248    }
249
250
251    /// Listing
252    /// A resource that allows an NFT to be sold for an amount of a given FungibleToken,
253    /// and for the proceeds of that sale to be split between several recipients.
254    /// 
255    pub resource Listing: ListingPublic {
256        /// The simple (non-Capability, non-complex) details of the sale
257        access(self) let details: ListingDetails
258
259        /// A capability allowing this resource to withdraw the NFT with the given ID from its collection.
260        /// This capability allows the resource to withdraw *any* NFT, so you should be careful when giving
261        /// such a capability to a resource and always check its code to make sure it will use it in the
262        /// way that it claims.
263        access(contract) let nftProviderCapability: Capability<&{NonFungibleToken.Provider, NonFungibleToken.CollectionPublic}>
264
265        /// An optional list of marketplaces capabilities that are approved 
266        /// to receive the marketplace commission.
267        access(contract) let marketplacesCapability: [Capability<&{FungibleToken.Receiver}>]?
268
269        /// borrowNFT
270        /// Return the reference of the NFT that is listed for sale.
271        /// if the NFT is absent, for example if it has been sold via another listing.
272        /// it will return nil.
273        ///
274        pub fun borrowNFT(): &NonFungibleToken.NFT? {
275            let ref = self.nftProviderCapability.borrow()!.borrowNFT(id: self.details.nftID)
276            if ref.isInstance(self.details.nftType) && ref.id == self.details.nftID {
277                return ref as! &NonFungibleToken.NFT  
278            } 
279            return nil
280        }
281
282        /// getDetails
283        /// Get the details of listing.
284        ///
285        pub fun getDetails(): ListingDetails {
286            return self.details
287        }
288
289        /// getAllowedCommissionReceivers
290        /// Fetches the allowed marketplaces capabilities or commission receivers.
291        /// If it returns `nil` then commission is up to grab by anyone.
292        pub fun getAllowedCommissionReceivers(): [Capability<&{FungibleToken.Receiver}>]? {
293            return self.marketplacesCapability
294        }
295
296        /// purchase
297        /// Purchase the listing, buying the token.
298        /// This pays the beneficiaries and commission to the facilitator and returns extra token to the buyer.
299        /// This also cleans up duplicate listings for the item being purchased.
300        pub fun purchase(
301            payment: @FungibleToken.Vault, 
302            commissionRecipient: Capability<&{FungibleToken.Receiver}>?,
303        ): @NonFungibleToken.NFT {
304
305            pre {
306                self.details.purchased == false: "listing has already been purchased"
307                payment.isInstance(self.details.salePaymentVaultType): "payment vault is not requested fungible token"
308                payment.balance == self.details.salePrice: "payment vault does not contain requested price"
309                self.details.expiry > UInt64(getCurrentBlock().timestamp): "Listing is expired"
310                self.owner != nil : "Resource doesn't have the assigned owner"
311            }
312            // Make sure the listing cannot be purchased again.
313            self.details.setToPurchased() 
314            
315            if self.details.commissionAmount > 0.0 {
316                // If commission recipient is nil, Throw panic.
317                let commissionReceiver = commissionRecipient ?? panic("Commission recipient can't be nil")
318                if self.marketplacesCapability != nil {
319                    var isCommissionRecipientHasValidType = false
320                    var isCommissionRecipientAuthorised = false
321                    for cap in self.marketplacesCapability! {
322                        // Check 1: Should have the same type
323                        if cap.getType() == commissionReceiver.getType() {
324                            isCommissionRecipientHasValidType = true
325                            // Check 2: Should have the valid market address that holds approved capability.
326                            if cap.address == commissionReceiver.address && cap.check() {
327                                isCommissionRecipientAuthorised = true
328                                break
329                            }
330                        }
331                    }
332                    assert(isCommissionRecipientHasValidType, message: "Given recipient does not has valid type")
333                    assert(isCommissionRecipientAuthorised,   message: "Given recipient has not authorised to receive the commission")
334                }
335                let commissionPayment <- payment.withdraw(amount: self.details.commissionAmount)
336                let recipient = commissionReceiver.borrow() ?? panic("Unable to borrow the recipent capability")
337                recipient.deposit(from: <- commissionPayment)
338            }
339            // Fetch the token to return to the purchaser.
340            let nft <-self.nftProviderCapability.borrow()!.withdraw(withdrawID: self.details.nftID)
341            // Neither receivers nor providers are trustworthy, they must implement the correct
342            // interface but beyond complying with its pre/post conditions they are not gauranteed
343            // to implement the functionality behind the interface in any given way.
344            // Therefore we cannot trust the Collection resource behind the interface,
345            // and we must check the NFT resource it gives us to make sure that it is the correct one.
346            assert(nft.isInstance(self.details.nftType), message: "withdrawn NFT is not of specified type")
347            assert(nft.id == self.details.nftID, message: "withdrawn NFT does not have specified ID")
348
349            // Fetch the duplicate listing for the given NFT
350            // Access the StoreFrontManager resource reference to remove the duplicate listings if purchase would happen successfully.
351            let storeFrontPublicRef = self.owner!.getCapability<&NFTStorefrontV2.Storefront{NFTStorefrontV2.StorefrontPublic}>(NFTStorefrontV2.StorefrontPublicPath)
352                                        .borrow() ?? panic("Unable to borrow the storeFrontManager resource")
353            let duplicateListings = storeFrontPublicRef.getDuplicateListingIDs(nftType: self.details.nftType, nftID: self.details.nftID, listingID: self.uuid)
354
355            // Let's force removal of the listing in this storefront for the NFT that is being purchased. 
356            for listingID in duplicateListings {
357                storeFrontPublicRef.cleanup(listingResourceID: listingID)
358            }
359
360            // Rather than aborting the transaction if any receiver is absent when we try to pay it,
361            // we send the cut to the first valid receiver.
362            // The first receiver should therefore either be the seller, or an agreed recipient for
363            // any unpaid cuts.
364            var residualReceiver: &{FungibleToken.Receiver}? = nil
365            // Pay the comission 
366            // Pay each beneficiary their amount of the payment.
367
368            for cut in self.details.saleCuts {
369                if let receiver = cut.receiver.borrow() {
370                   let paymentCut <- payment.withdraw(amount: cut.amount)
371                    receiver.deposit(from: <-paymentCut)
372                    if (residualReceiver == nil) {
373                        residualReceiver = receiver
374                    }
375                } else {
376                    emit UnpaidReceiver(receiver: cut.receiver.address, entitledSaleCut: cut.amount)
377                }
378            }
379
380            assert(residualReceiver != nil, message: "No valid payment receivers")
381
382            // At this point, if all recievers were active and availabile, then the payment Vault will have
383            // zero tokens left, and this will functionally be a no-op that consumes the empty vault
384            residualReceiver!.deposit(from: <-payment)
385
386            // If the listing is purchased, we regard it as completed here.
387            // Otherwise we regard it as completed in the destructor.
388
389            emit ListingCompleted(
390                listingResourceID: self.uuid,
391                storefrontResourceID: self.details.storefrontID,
392                purchased: self.details.purchased,
393                nftType: self.details.nftType,
394                nftUUID: self.details.nftUUID,
395                nftID: self.details.nftID,
396                salePaymentVaultType: self.details.salePaymentVaultType,
397                salePrice: self.details.salePrice,
398                customID: self.details.customID,
399                commissionAmount: self.details.commissionAmount,
400                commissionReceiver: self.details.commissionAmount != 0.0 ? commissionRecipient!.address : nil,
401                expiry: self.details.expiry
402            )
403
404            return <-nft
405        }
406
407        /// destructor
408        ///
409        destroy () {
410            // If the listing has not been purchased, we regard it as completed here.
411            // Otherwise we regard it as completed in purchase().
412            // This is because we destroy the listing in Storefront.removeListing()
413            // or Storefront.cleanup() .
414            // If we change this destructor, revisit those functions.
415            if !self.details.purchased {
416                emit ListingCompleted(
417                    listingResourceID: self.uuid,
418                    storefrontResourceID: self.details.storefrontID,
419                    purchased: self.details.purchased,
420                    nftType: self.details.nftType,
421                    nftUUID: self.details.nftUUID,
422                    nftID: self.details.nftID,
423                    salePaymentVaultType: self.details.salePaymentVaultType,
424                    salePrice: self.details.salePrice,
425                    customID: self.details.customID,
426                    commissionAmount: self.details.commissionAmount,
427                    commissionReceiver: nil,
428                    expiry: self.details.expiry
429                )
430            }
431        }
432
433        /// initializer
434        ///
435        init (
436            nftProviderCapability: Capability<&{NonFungibleToken.Provider, NonFungibleToken.CollectionPublic}>,
437            nftType: Type,
438            nftUUID: UInt64,
439            nftID: UInt64,
440            salePaymentVaultType: Type,
441            saleCuts: [SaleCut],
442            marketplacesCapability: [Capability<&{FungibleToken.Receiver}>]?,
443            storefrontID: UInt64,
444            customID: String?,
445            commissionAmount: UFix64,
446            expiry: UInt64
447        ) {
448            // Store the sale information
449            self.details = ListingDetails(
450                nftType: nftType,
451                nftUUID: nftUUID,
452                nftID: nftID,
453                salePaymentVaultType: salePaymentVaultType,
454                saleCuts: saleCuts,
455                storefrontID: storefrontID,
456                customID: customID,
457                commissionAmount: commissionAmount,
458                expiry: expiry
459            )
460
461            // Store the NFT provider
462            self.nftProviderCapability = nftProviderCapability
463            self.marketplacesCapability = marketplacesCapability
464
465            // Check that the provider contains the NFT.
466            // We will check it again when the token is sold.
467            // We cannot move this into a function because initializers cannot call member functions.
468            let provider = self.nftProviderCapability.borrow()
469            assert(provider != nil, message: "cannot borrow nftProviderCapability")
470
471            // This will precondition assert if the token is not available.
472            let nft = provider!.borrowNFT(id: self.details.nftID)
473            assert(nft.isInstance(self.details.nftType), message: "token is not of specified type")
474            assert(nft.id == self.details.nftID, message: "token does not have specified ID")
475        }
476    }
477
478    /// StorefrontManager
479    /// An interface for adding and removing Listings within a Storefront,
480    /// intended for use by the Storefront's owner
481    ///
482    pub resource interface StorefrontManager {
483        /// createListing
484        /// Allows the Storefront owner to create and insert Listings.
485        ///
486        pub fun createListing(
487            nftProviderCapability: Capability<&{NonFungibleToken.Provider, NonFungibleToken.CollectionPublic}>,
488            nftType: Type,
489            nftID: UInt64,
490            salePaymentVaultType: Type,
491            saleCuts: [SaleCut],
492            marketplacesCapability: [Capability<&{FungibleToken.Receiver}>]?,
493            customID: String?,
494            commissionAmount: UFix64,
495            expiry: UInt64
496        ): UInt64
497
498        /// removeListing
499        /// Allows the Storefront owner to remove any sale listing, acepted or not.
500        ///
501        pub fun removeListing(listingResourceID: UInt64)
502    }
503
504    /// StorefrontPublic
505    /// An interface to allow listing and borrowing Listings, and purchasing items via Listings
506    /// in a Storefront.
507    ///
508    pub resource interface StorefrontPublic {
509        pub fun getListingIDs(): [UInt64]
510        pub fun getDuplicateListingIDs(nftType: Type, nftID: UInt64, listingID: UInt64): [UInt64]
511        pub fun borrowListing(listingResourceID: UInt64): &Listing{ListingPublic}?
512        pub fun cleanupExpiredListings(fromIndex: UInt64, toIndex: UInt64)
513        access(contract) fun cleanup(listingResourceID: UInt64)
514   }
515
516    /// Storefront
517    /// A resource that allows its owner to manage a list of Listings, and anyone to interact with them
518    /// in order to query their details and purchase the NFTs that they represent.
519    ///
520    pub resource Storefront : StorefrontManager, StorefrontPublic {
521        /// The dictionary of Listing uuids to Listing resources.
522        access(contract) var listings: @{UInt64: Listing}
523        /// Dictionary to keep track of listing ids for same NFTs listing.
524        /// nftType.identifier -> nftID -> [listing resource ID]
525        access(contract) var listedNFTs: {String: {UInt64 : [UInt64]}}
526
527        /// insert
528        /// Create and publish a Listing for an NFT.
529        ///
530         pub fun createListing(
531            nftProviderCapability: Capability<&{NonFungibleToken.Provider, NonFungibleToken.CollectionPublic}>,
532            nftType: Type,
533            nftID: UInt64,
534            salePaymentVaultType: Type,
535            saleCuts: [SaleCut],
536            marketplacesCapability: [Capability<&{FungibleToken.Receiver}>]?,
537            customID: String?,
538            commissionAmount: UFix64,
539            expiry: UInt64
540         ): UInt64 {
541            
542            // let's ensure that the seller does indeed hold the NFT being listed
543            let collectionRef = nftProviderCapability.borrow()
544                ?? panic("Could not borrow reference to collection")
545            let nftRef = collectionRef.borrowNFT(id: nftID)
546
547            // Instead of letting an arbitrary value be set for the UUID of a given NFT, the contract
548            // should fetch it itelf     
549            let uuid = nftRef.uuid
550            let listing <- create Listing(
551                nftProviderCapability: nftProviderCapability,
552                nftType: nftType,
553                nftUUID: uuid,
554                nftID: nftID,
555                salePaymentVaultType: salePaymentVaultType,
556                saleCuts: saleCuts,
557                marketplacesCapability: marketplacesCapability,
558                storefrontID: self.uuid,
559                customID: customID,
560                commissionAmount: commissionAmount,
561                expiry: expiry
562            )
563        
564            let listingResourceID = listing.uuid
565            let listingPrice = listing.getDetails().salePrice
566            // Add the new listing to the dictionary.
567            let oldListing <- self.listings[listingResourceID] <- listing
568            // Note that oldListing will always be nil, but we have to handle it.
569
570            destroy oldListing
571
572            // Add the `listingResourceID` in the tracked listings.
573            self.addDuplicateListing(nftIdentifier: nftType.identifier, nftID: nftID, listingResourceID: listingResourceID)
574
575            // Scraping addresses from the capabilities to emit in the event.
576            var allowedCommissionReceivers : [Address]? = nil
577            if let allowedReceivers = marketplacesCapability {
578                // Small hack here to make `allowedCommissionReceivers` variable compatible to
579                // array properties.
580                allowedCommissionReceivers = []
581                for receiver in allowedReceivers {
582                    allowedCommissionReceivers!.append(receiver.address)
583                }
584            }
585
586            emit ListingAvailable(
587                storefrontAddress: self.owner?.address!,
588                listingResourceID: listingResourceID,
589                nftType: nftType,
590                nftUUID: uuid,
591                nftID: nftID,
592                salePaymentVaultType: salePaymentVaultType,
593                salePrice: listingPrice,
594                customID: customID,
595                commissionAmount: commissionAmount,
596                commissionReceivers: allowedCommissionReceivers,
597                expiry: expiry
598            )
599
600            return listingResourceID
601        }
602
603        /// addDuplicateListing
604        /// Helper function that allows to add duplicate listing of given nft in a map.
605        /// 
606        access(contract) fun addDuplicateListing(nftIdentifier: String, nftID: UInt64, listingResourceID: UInt64) {
607             if !self.listedNFTs.containsKey(nftIdentifier) {
608                self.listedNFTs.insert(key: nftIdentifier, {nftID: [listingResourceID]})
609            } else {
610                if !self.listedNFTs[nftIdentifier]!.containsKey(nftID) {
611                    self.listedNFTs[nftIdentifier]!.insert(key: nftID, [listingResourceID])
612                } else {
613                    self.listedNFTs[nftIdentifier]![nftID]!.append(listingResourceID)
614                } 
615            }
616        }
617
618        /// removeDuplicateListing
619        /// Helper function that allows to remove duplicate listing of given nft from a map.
620        /// 
621        access(contract) fun removeDuplicateListing(nftIdentifier: String, nftID: UInt64, listingResourceID: UInt64) {
622            // Remove the listing from the listedNFTs dictionary.
623            let listingIndex = self.listedNFTs[nftIdentifier]![nftID]!.firstIndex(of: listingResourceID) ?? panic("Should contain the index")
624            self.listedNFTs[nftIdentifier]![nftID]!.remove(at: listingIndex)
625        }
626        
627        /// removeListing
628        /// Remove a Listing that has not yet been purchased from the collection and destroy it.
629        ///
630        pub fun removeListing(listingResourceID: UInt64) {
631            let listing <- self.listings.remove(key: listingResourceID)
632                ?? panic("missing Listing")
633            let listingDetails = listing.getDetails()
634            self.removeDuplicateListing(nftIdentifier: listingDetails.nftType.identifier, nftID: listingDetails.nftID, listingResourceID: listingResourceID)
635            // This will emit a ListingCompleted event.
636            destroy listing
637        }
638
639        /// getListingIDs
640        /// Returns an array of the Listing resource IDs that are in the collection
641        ///
642        pub fun getListingIDs(): [UInt64] {
643            return self.listings.keys
644        }
645
646        /// getExistingListingIDs
647        /// Returns an array of listing IDs of the given `nftType` and `nftID`.
648        ///
649        pub fun getExistingListingIDs(nftType: Type, nftID: UInt64): [UInt64] {
650            if self.listedNFTs[nftType.identifier] == nil || self.listedNFTs[nftType.identifier]![nftID] == nil {
651                return []
652            }
653            var listingIDs = self.listedNFTs[nftType.identifier]![nftID]!
654            return listingIDs
655        }
656
657        /// getDuplicateListingIDs
658        /// Returns an array of listing IDs that are duplicates of the given `nftType` and `nftID`.
659        ///
660        pub fun getDuplicateListingIDs(nftType: Type, nftID: UInt64, listingID: UInt64): [UInt64] {
661            var listingIDs = self.getExistingListingIDs(nftType: nftType, nftID: nftID)
662
663            // Verify that given listing Id also a part of the `listingIds`
664            let doesListingExist = listingIDs.contains(listingID)
665            // Find out the index of the existing listing.
666            if doesListingExist {
667                var index: Int = 0
668                for id in listingIDs {
669                    if id == listingID {
670                        break
671                    }
672                    index = index + 1
673                }
674                listingIDs.remove(at:index)
675                return listingIDs
676            } 
677           return []
678        }
679
680        /// cleanupExpiredListings
681        /// Cleanup the expired listing by iterating over the provided range of indexes.
682        ///
683        pub fun cleanupExpiredListings(fromIndex: UInt64, toIndex: UInt64) {
684            pre {
685                fromIndex <= toIndex : "Incorrect start index"
686                Int(toIndex - fromIndex) < self.getListingIDs().length : "Provided range is out of bound"
687            }
688            var index = fromIndex
689            let listingsIDs = self.getListingIDs()
690            while index <= toIndex {
691                // There is a possibility that some index may not have the listing.
692                // becuase of that instead of failing the transaction, Execution moved to next index or listing.
693                
694                if let listing = self.borrowListing(listingResourceID: listingsIDs[index]) {
695                    if listing.getDetails().expiry <= UInt64(getCurrentBlock().timestamp) {
696                        self.cleanup(listingResourceID: listingsIDs[index])
697                    }
698                }
699                index = index + UInt64(1) 
700            }
701        } 
702
703        /// borrowSaleItem
704        /// Returns a read-only view of the SaleItem for the given listingID if it is contained by this collection.
705        ///
706        pub fun borrowListing(listingResourceID: UInt64): &Listing{ListingPublic}? {
707             if self.listings[listingResourceID] != nil {
708                return &self.listings[listingResourceID] as &Listing{ListingPublic}?
709            } else {
710                return nil
711            }
712        }
713
714        /// cleanup
715        /// Remove an listing, When given listing is duplicate or expired
716        /// Only contract is allowed to execute it.
717        ///
718        access(contract) fun cleanup(listingResourceID: UInt64) {
719            pre {
720                self.listings[listingResourceID] != nil: "could not find listing with given id"
721            }
722            let listing <- self.listings.remove(key: listingResourceID)!
723            let listingDetails = listing.getDetails()
724            self.removeDuplicateListing(nftIdentifier: listingDetails.nftType.identifier, nftID: listingDetails.nftID, listingResourceID: listingResourceID)
725
726            destroy listing
727        }
728
729        /// destructor
730        ///
731        destroy () {
732            destroy self.listings
733
734            // Let event consumers know that this storefront will no longer exist
735            emit StorefrontDestroyed(storefrontResourceID: self.uuid)
736        }
737
738        /// constructor
739        ///
740        init () {
741            self.listings <- {}
742            self.listedNFTs = {}
743
744            // Let event consumers know that this storefront exists
745            emit StorefrontInitialized(storefrontResourceID: self.uuid)
746        }
747    }
748
749    /// createStorefront
750    /// Make creating a Storefront publicly accessible.
751    ///
752    pub fun createStorefront(): @Storefront {
753        return <-create Storefront()
754    }
755
756    init () {
757        self.StorefrontStoragePath = /storage/NFTStorefrontV2
758        self.StorefrontPublicPath = /public/NFTStorefrontV2
759    }
760}