Smart Contract

GroundNFTStorefrontV2

A.440776a8205e9f80.GroundNFTStorefrontV2

Deployed

1d ago
Feb 26, 2026, 09:44:39 PM UTC

Dependents

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