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