Smart Contract

NFTStorefront

A.5eb12ad3d5a99945.NFTStorefront

Deployed

1d ago
Feb 27, 2026, 12:31:07 PM UTC

Dependents

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