Smart Contract
NFTStorefront
A.4eb8a10cb9f87357.NFTStorefront
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