Smart Contract
NFTStorefront
A.5eb12ad3d5a99945.NFTStorefront
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