Smart Contract

KeeprNFTStorefront

A.5eb12ad3d5a99945.KeeprNFTStorefront

Valid From

86,970,343

Deployed

2d ago
Feb 24, 2026, 11:38:39 PM UTC

Dependents

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