Smart Contract

NFTStorefront

A.4eb8a10cb9f87357.NFTStorefront

Valid From

85,984,645

Deployed

1d ago
Feb 24, 2026, 11:58:38 PM UTC

Dependents

461653 imports
1import FungibleToken from 0xf233dcee88fe0abe
2import NonFungibleToken from 0x1d7e57aa55817448
3import Burner from 0xf233dcee88fe0abe
4
5/// NB: This contract is no longer supported. NFT Storefront V2 is recommended
6///
7/// NFTStorefront. 
8///
9/// A general purpose sale support contract for Flow NonFungibleTokens.
10/// 
11/// Each account that wants to list NFTs for sale installs a Storefront,
12/// and lists individual sales within that Storefront as Listings.
13/// There is one Storefront per account, it handles sales of all NFT types
14/// for that account.
15///
16/// Each Listing can have one or more "cut"s of the sale price that
17/// goes to one or more addresses. Cuts can be used to pay listing fees
18/// or other considerations.
19/// Each NFT may be listed in one or more Listings, the validity of each
20/// Listing can easily be checked.
21/// 
22/// Purchasers can watch for Listing events and check the NFT type and
23/// ID to see if they wish to buy the listed item.
24/// Marketplaces and other aggregators can watch for Listing events
25/// and list items of interest.
26///
27access(all) contract NFTStorefront {
28
29    access(all) entitlement CreateListing
30    access(all) entitlement RemoveListing
31
32    /// StorefrontInitialized
33    /// A Storefront resource has been created.
34    /// Event consumers can now expect events from this Storefront.
35    /// Note that we do not specify an address: we cannot and should not.
36    /// Created resources do not have an owner address, and may be moved
37    /// after creation in ways we cannot check.
38    /// ListingAvailable events can be used to determine the address
39    /// of the owner of the Storefront (...its location) at the time of
40    /// the listing but only at that precise moment in that precise transaction.
41    /// If the seller moves the Storefront while the listing is valid, 
42    /// that is on them.
43    ///
44    access(all) event StorefrontInitialized(storefrontResourceID: UInt64)
45
46    /// StorefrontDestroyed
47    /// A Storefront has been destroyed.
48    /// Event consumers can now stop processing events from this Storefront.
49    /// Note that we do not specify an address.
50    ///
51    access(all) event StorefrontDestroyed(storefrontResourceID: UInt64)
52
53    /// ListingAvailable
54    /// A listing has been created and added to a Storefront resource.
55    /// The Address values here are valid when the event is emitted, but
56    /// the state of the accounts they refer to may be changed outside of the
57    /// NFTStorefront workflow, so be careful to check when using them.
58    ///
59    access(all) event ListingAvailable(
60        storefrontAddress: Address,
61        listingResourceID: UInt64,
62        nftType: Type,
63        nftID: UInt64,
64        ftVaultType: Type,
65        price: UFix64
66    )
67
68    /// ListingCompleted
69    /// The listing has been resolved. It has either been purchased, or removed and destroyed.
70    ///
71    access(all) event ListingCompleted(
72        listingResourceID: UInt64, 
73        storefrontResourceID: UInt64, 
74        purchased: Bool,
75        nftType: Type,
76        nftID: UInt64
77    )
78
79    /// StorefrontStoragePath
80    /// The location in storage that a Storefront resource should be located.
81    access(all) let StorefrontStoragePath: StoragePath
82
83    /// StorefrontPublicPath
84    /// The public location for a Storefront link.
85    access(all) let StorefrontPublicPath: PublicPath
86
87
88    /// SaleCut
89    /// A struct representing a recipient that must be sent a certain amount
90    /// of the payment when a token is sold.
91    ///
92    access(all) struct SaleCut {
93        /// The receiver for the payment.
94        /// Note that we do not store an address to find the Vault that this represents,
95        /// as the link or resource that we fetch in this way may be manipulated,
96        /// so to find the address that a cut goes to you must get this struct and then
97        /// call receiver.borrow()!.owner.address on it.
98        /// This can be done efficiently in a script.
99        access(all) let receiver: Capability<&{FungibleToken.Receiver}>
100
101        /// The amount of the payment FungibleToken that will be paid to the receiver.
102        access(all) let amount: UFix64
103
104        /// initializer
105        ///
106        init(receiver: Capability<&{FungibleToken.Receiver}>, amount: UFix64) {
107            self.receiver = receiver
108            self.amount = amount
109        }
110    }
111
112
113    /// ListingDetails
114    /// A struct containing a Listing's data.
115    ///
116    access(all) struct ListingDetails {
117        /// The Storefront that the Listing is stored in.
118        /// Note that this resource cannot be moved to a different Storefront,
119        /// so this is OK. If we ever make it so that it *can* be moved,
120        /// this should be revisited.
121        access(all) var storefrontID: UInt64
122        /// Whether this listing has been purchased or not.
123        access(all) var purchased: Bool
124        /// The Type of the NonFungibleToken.NFT that is being listed.
125        access(all) let nftType: Type
126        /// The ID of the NFT within that type.
127        access(all) let nftID: UInt64
128        /// The Type of the FungibleToken that payments must be made in.
129        access(all) let salePaymentVaultType: Type
130        /// The amount that must be paid in the specified FungibleToken.
131        access(all) let salePrice: UFix64
132        /// This specifies the division of payment between recipients.
133        access(all) let saleCuts: [SaleCut]
134
135        /// setToPurchased
136        /// Irreversibly set this listing as purchased.
137        ///
138        access(contract) fun setToPurchased() {
139            self.purchased = true
140        }
141
142        /// initializer
143        ///
144        init (
145            nftType: Type,
146            nftID: UInt64,
147            salePaymentVaultType: Type,
148            saleCuts: [SaleCut],
149            storefrontID: UInt64
150        ) {
151            self.storefrontID = storefrontID
152            self.purchased = false
153            self.nftType = nftType
154            self.nftID = nftID
155            self.salePaymentVaultType = salePaymentVaultType
156            // Store the cuts
157            assert(saleCuts.length > 0, message: "Listing must have at least one payment cut recipient")
158            self.saleCuts = saleCuts
159
160            // Calculate the total price from the cuts
161            var salePrice = 0.0
162            // Perform initial check on capabilities, and calculate sale price from cut amounts.
163            for cut in self.saleCuts {
164                // Make sure we can borrow the receiver.
165                // We will check this again when the token is sold.
166                cut.receiver.borrow()
167                    ?? panic("Cannot borrow receiver")
168                // Add the cut amount to the total price
169                salePrice = salePrice + cut.amount
170            }
171            assert(salePrice > 0.0, message: "Listing must have non-zero price")
172
173            // Store the calculated sale price
174            self.salePrice = salePrice
175        }
176    }
177
178
179    /// ListingPublic
180    /// An interface providing a useful public interface to a Listing.
181    ///
182    access(all) resource interface ListingPublic {
183        /// borrowNFT
184        /// This will assert in the same way as the NFT standard borrowNFT()
185        /// if the NFT is absent, for example if it has been sold via another listing.
186        ///
187        access(all) fun borrowNFT(): &{NonFungibleToken.NFT}?
188
189        /// purchase
190        /// Purchase the listing, buying the token.
191        /// This pays the beneficiaries and returns the token to the buyer.
192        ///
193        access(all) fun purchase(payment: @{FungibleToken.Vault}): @{NonFungibleToken.NFT}
194
195        /// getDetails
196        ///
197        access(all) fun getDetails(): ListingDetails
198
199    }
200
201
202    /// Listing
203    /// A resource that allows an NFT to be sold for an amount of a given FungibleToken,
204    /// and for the proceeds of that sale to be split between several recipients.
205    /// 
206    access(all) resource Listing: ListingPublic, Burner.Burnable {
207        // Event to be emitted when this listing is destroyed.
208        // If the listing has not been purchased, we regard it as completed here.
209        // There is a separate event in purchase for purchased listings
210        access(all) event ResourceDestroyed(
211            listingResourceID: UInt64 = self.uuid,
212            storefrontResourceID: UInt64 = self.details.storefrontID,
213            purchased: Bool = self.details.purchased,
214            nftType: String = self.details.nftType.identifier,
215            nftID: UInt64 = self.details.nftID
216        )
217
218        access(contract) fun burnCallback() {
219            // If the listing has not been purchased, we regard it as completed here.
220            // Otherwise we regard it as completed in purchase().
221            // This is because we destroy the listing in Storefront.removeListing()
222            // or Storefront.cleanup() .
223            // If we change this destructor, revisit those functions.
224            if !self.details.purchased {
225                emit ListingCompleted(
226                    listingResourceID: self.uuid,
227                    storefrontResourceID: self.details.storefrontID,
228                    purchased: self.details.purchased,
229                    nftType: self.details.nftType,
230                    nftID: self.details.nftID
231                )
232            }
233        }
234
235        /// The simple (non-Capability, non-complex) details of the sale
236        access(self) let details: ListingDetails
237
238        /// A capability allowing this resource to withdraw the NFT with the given ID from its collection.
239        /// This capability allows the resource to withdraw *any* NFT, so you should be careful when giving
240        /// such a capability to a resource and always check its code to make sure it will use it in the
241        /// way that it claims.
242        access(contract) let nftProviderCapability: Capability<auth(NonFungibleToken.Withdraw) &{NonFungibleToken.Provider, NonFungibleToken.CollectionPublic}>
243
244        /// borrowNFT
245        /// This will assert in the same way as the NFT standard borrowNFT()
246        /// if the NFT is absent, for example if it has been sold via another listing.
247        ///
248        access(all) fun borrowNFT(): &{NonFungibleToken.NFT}? {
249            let ref = self.nftProviderCapability.borrow()!.borrowNFT(self.getDetails().nftID)
250            assert(ref != nil, message: "Could not borrow a reference to the NFT")
251            assert(ref!.isInstance(self.getDetails().nftType), message: "token has wrong type")
252            assert(ref?.id == self.getDetails().nftID, message: "token has wrong ID")
253            return (ref as &{NonFungibleToken.NFT}?)
254        }
255
256        /// getDetails
257        /// Get the details of the current state of the Listing as a struct.
258        /// This avoids having more public variables and getter methods for them, and plays
259        /// nicely with scripts (which cannot return resources). 
260        ///
261        access(all) fun getDetails(): ListingDetails {
262            return self.details
263        }
264        
265        /// purchase
266        /// Purchase the listing, buying the token.
267        /// This pays the beneficiaries and returns the token to the buyer.
268        ///
269        access(all) fun purchase(payment: @{FungibleToken.Vault}): @{NonFungibleToken.NFT} {
270            pre {
271                self.details.purchased == false: "listing has already been purchased"
272                payment.isInstance(self.details.salePaymentVaultType): "payment vault is not requested fungible token"
273                payment.balance == self.details.salePrice: "payment vault does not contain requested price"
274            }
275
276            // Make sure the listing cannot be purchased again.
277            self.details.setToPurchased()
278
279
280            // Fetch the token to return to the purchaser.
281            let nft <-self.nftProviderCapability.borrow()!.withdraw(withdrawID: self.details.nftID)
282            // Neither receivers nor providers are trustworthy, they must implement the correct
283            // interface but beyond complying with its pre/post conditions they are not gauranteed
284            // to implement the functionality behind the interface in any given way.
285            // Therefore we cannot trust the Collection resource behind the interface,
286            // and we must check the NFT resource it gives us to make sure that it is the correct one.
287            assert(nft.isInstance(self.details.nftType), message: "withdrawn NFT is not of specified type")
288            assert(nft.id == self.details.nftID, message: "withdrawn NFT does not have specified ID")
289
290            // Rather than aborting the transaction if any receiver is absent when we try to pay it,
291            // we send the cut to the first valid receiver.
292            // The first receiver should therefore either be the seller, or an agreed recipient for
293            // any unpaid cuts.
294            var residualReceiver: &{FungibleToken.Receiver}? = nil
295
296            // Pay each beneficiary their amount of the payment.
297            for cut in self.details.saleCuts {
298                if let receiver = cut.receiver.borrow() {
299                   let paymentCut <- payment.withdraw(amount: cut.amount)
300                    receiver.deposit(from: <-paymentCut)
301                    if (residualReceiver == nil) {
302                        residualReceiver = receiver
303                    }
304                }
305            }
306
307            assert(residualReceiver != nil, message: "No valid payment receivers")
308
309            // At this point, if all recievers were active and availabile, then the payment Vault will have
310            // zero tokens left, and this will functionally be a no-op that consumes the empty vault
311            residualReceiver!.deposit(from: <-payment)
312
313            // If the listing is purchased, we regard it as completed here.
314            // Otherwise we regard it as completed in the destructor.        
315
316            emit ListingCompleted(
317                listingResourceID: self.uuid,
318                storefrontResourceID: self.details.storefrontID,
319                purchased: self.details.purchased,
320                nftType: self.details.nftType,
321                nftID: self.details.nftID
322            )
323
324            return <-nft
325        }
326
327        /// initializer
328        ///
329        init (
330            nftProviderCapability: Capability<auth(NonFungibleToken.Withdraw) &{NonFungibleToken.Collection}>,
331            nftType: Type,
332            nftID: UInt64,
333            salePaymentVaultType: Type,
334            saleCuts: [SaleCut],
335            storefrontID: UInt64
336        ) {
337            // Store the sale information
338            self.details = ListingDetails(
339                nftType: nftType,
340                nftID: nftID,
341                salePaymentVaultType: salePaymentVaultType,
342                saleCuts: saleCuts,
343                storefrontID: storefrontID
344            )
345
346            // Store the NFT provider
347            self.nftProviderCapability = nftProviderCapability
348
349            // Check that the provider contains the NFT.
350            // We will check it again when the token is sold.
351            // We cannot move this into a function because initializers cannot call member functions.
352            let provider = self.nftProviderCapability.borrow()
353            assert(provider != nil, message: "cannot borrow nftProviderCapability")
354
355            let nft = provider!.borrowNFT(self.details.nftID)
356            // This will precondition assert if the token is not available.
357            assert(nft != nil, message: "Could not borrow a reference to the NFT")
358            assert(nft!.isInstance(self.details.nftType), message: "token is not of specified type")
359            assert(nft?.id == self.details.nftID, message: "token does not have specified ID")
360        }
361    }
362
363    /// StorefrontManager
364    /// An interface for adding and removing Listings within a Storefront,
365    /// intended for use by the Storefront's own
366    ///
367    access(all) resource interface StorefrontManager {
368        /// createListing
369        /// Allows the Storefront owner to create and insert Listings.
370        ///
371        access(CreateListing) fun createListing(
372            nftProviderCapability: Capability<auth(NonFungibleToken.Withdraw) &{NonFungibleToken.Collection}>,
373            nftType: Type,
374            nftID: UInt64,
375            salePaymentVaultType: Type,
376            saleCuts: [SaleCut]
377        ): UInt64
378        /// removeListing
379        /// Allows the Storefront owner to remove any sale listing, acepted or not.
380        ///
381        access(RemoveListing) fun removeListing(listingResourceID: UInt64)
382    }
383
384    /// StorefrontPublic
385    /// An interface to allow listing and borrowing Listings, and purchasing items via Listings
386    /// in a Storefront.
387    ///
388    access(all) resource interface StorefrontPublic {
389        access(all) view fun getListingIDs(): [UInt64]
390        access(all) view fun borrowListing(listingResourceID: UInt64): &{ListingPublic}? {
391            post {
392                result == nil || result!.getType() == Type<@Listing>():
393                    "Cannot borrow a non-NFTStorefront.Listing!"
394            }
395        }
396        access(all) fun cleanup(listingResourceID: UInt64)
397   }
398
399    /// Storefront
400    /// A resource that allows its owner to manage a list of Listings, and anyone to interact with them
401    /// in order to query their details and purchase the NFTs that they represent.
402    ///
403    access(all) resource Storefront: StorefrontManager, StorefrontPublic {
404        // Event to be emitted when this storefront is destroyed.
405        access(all) event ResourceDestroyed(
406            storefrontResourceID: UInt64 = self.uuid
407        )
408
409        /// The dictionary of Listing uuids to Listing resources.
410        access(self) var listings: @{UInt64: Listing}
411
412        /// insert
413        /// Create and publish a Listing for an NFT.
414        ///
415         access(CreateListing) fun createListing(
416            nftProviderCapability: Capability<auth(NonFungibleToken.Withdraw) &{NonFungibleToken.Collection}>,
417            nftType: Type,
418            nftID: UInt64,
419            salePaymentVaultType: Type,
420            saleCuts: [SaleCut]
421         ): UInt64 {
422            let listing <- create Listing(
423                nftProviderCapability: nftProviderCapability,
424                nftType: nftType,
425                nftID: nftID,
426                salePaymentVaultType: salePaymentVaultType,
427                saleCuts: saleCuts,
428                storefrontID: self.uuid
429            )
430
431            let listingResourceID = listing.uuid
432            let listingPrice = listing.getDetails().salePrice
433
434            // Add the new listing to the dictionary.
435            let oldListing <- self.listings[listingResourceID] <- listing
436            // Note that oldListing will always be nil, but we have to handle it.
437
438            Burner.burn(<-oldListing)
439
440            emit ListingAvailable(
441                storefrontAddress: self.owner?.address!,
442                listingResourceID: listingResourceID,
443                nftType: nftType,
444                nftID: nftID,
445                ftVaultType: salePaymentVaultType,
446                price: listingPrice
447            )
448
449            return listingResourceID
450        }
451        
452
453        /// removeListing
454        /// Remove a Listing that has not yet been purchased from the collection and destroy it.
455        ///
456        access(RemoveListing) fun removeListing(listingResourceID: UInt64) {
457            let listing <- self.listings.remove(key: listingResourceID)
458                ?? panic("missing Listing")
459    
460            // This will emit a ListingCompleted event.
461            Burner.burn(<-listing)
462        }
463
464        /// getListingIDs
465        /// Returns an array of the Listing resource IDs that are in the collection
466        ///
467        access(all) view fun getListingIDs(): [UInt64] {
468            return self.listings.keys
469        }
470
471        /// borrowSaleItem
472        /// Returns a read-only view of the SaleItem for the given listingID if it is contained by this collection.
473        ///
474        access(all) view fun borrowListing(listingResourceID: UInt64): &{ListingPublic}? {
475            if self.listings[listingResourceID] != nil {
476                return &self.listings[listingResourceID] as &{ListingPublic}?
477            } else {
478                return nil
479            }
480        }
481
482        /// cleanup
483        /// Remove an listing *if* it has been purchased.
484        /// Anyone can call, but at present it only benefits the account owner to do so.
485        /// Kind purchasers can however call it if they like.
486        ///
487        access(all) fun cleanup(listingResourceID: UInt64) {
488            pre {
489                self.listings[listingResourceID] != nil: "could not find listing with given id"
490            }
491
492            let listing <- self.listings.remove(key: listingResourceID)!
493            assert(listing.getDetails().purchased == true, message: "listing is not purchased, only admin can remove")
494            Burner.burn(<-listing)
495        }
496
497        /// constructor
498        ///
499        init () {
500            self.listings <- {}
501
502            // Let event consumers know that this storefront exists
503            emit StorefrontInitialized(storefrontResourceID: self.uuid)
504        }
505    }
506
507    /// createStorefront
508    /// Make creating a Storefront publicly accessible.
509    ///
510    access(all) fun createStorefront(): @Storefront {
511        return <-create Storefront()
512    }
513
514    init () {
515        self.StorefrontStoragePath = /storage/NFTStorefront
516        self.StorefrontPublicPath = /public/NFTStorefront
517    }
518}
519