Smart Contract
NFTStorefrontV2
A.4eb8a10cb9f87357.NFTStorefrontV2
1import FungibleToken from 0xf233dcee88fe0abe
2import NonFungibleToken from 0x1d7e57aa55817448
3import Burner from 0xf233dcee88fe0abe
4
5/// NFTStorefrontV2
6///
7/// A general purpose sale support contract for NFTs that implement the Flow NonFungibleToken standard.
8///
9/// Each account that wants to list NFTs for sale installs a Storefront,
10/// and lists individual sales within that Storefront as Listings.
11/// There is one Storefront per account, it handles sales of all NFT types
12/// for that account.
13///
14/// Each Listing can have one or more "cuts" of the sale price that
15/// goes to one or more addresses. Cuts can be used to pay listing fees
16/// or other considerations.
17/// Each Listing can include a commission amount that is paid to whoever facilitates
18/// the purchase. The seller can also choose to provide an optional list of marketplace
19/// receiver capabilities. In this case, the commission amount must be transferred to
20/// one of the capabilities in the list.
21///
22/// Each NFT may be listed in one or more Listings, the validity of each
23/// Listing can easily be checked.
24///
25/// Purchasers can watch for Listing events and check the NFT type and
26/// ID to see if they wish to buy the listed item.
27/// Marketplaces and other aggregators can watch for Listing events
28/// and list items of interest.
29///
30access(all) contract NFTStorefrontV2 {
31
32 access(all) entitlement CreateListing
33 access(all) entitlement RemoveListing
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 /// ListingAvailable
50 /// A listing has been created and added to a Storefront resource.
51 /// The Address values here are valid when the event is emitted, but
52 /// the state of the accounts they refer to may change outside of the
53 /// NFTStorefrontV2 workflow, so be careful to check when using them.
54 ///
55 access(all) event ListingAvailable(
56 storefrontAddress: Address,
57 listingResourceID: UInt64,
58 nftType: Type,
59 nftUUID: UInt64,
60 nftID: UInt64,
61 salePaymentVaultType: Type,
62 salePrice: UFix64,
63 customID: String?,
64 commissionAmount: UFix64,
65 commissionReceivers: [Address]?,
66 expiry: UInt64
67 )
68
69 /// ListingCompleted
70 /// The listing has been resolved. It has either been purchased, removed or destroyed.
71 ///
72 access(all) event ListingCompleted(
73 listingResourceID: UInt64,
74 storefrontResourceID: UInt64,
75 purchased: Bool,
76 nftType: Type,
77 nftUUID: UInt64,
78 nftID: UInt64,
79 salePaymentVaultType: Type,
80 salePrice: UFix64,
81 customID: String?,
82 commissionAmount: UFix64,
83 commissionReceiver: Address?,
84 expiry: UInt64
85 )
86
87 /// UnpaidReceiver
88 /// A entitled receiver has not been paid during the sale of the NFT.
89 ///
90 access(all) event UnpaidReceiver(receiver: Address, entitledSaleCut: UFix64)
91
92 /// StorefrontStoragePath
93 /// The location in storage that a Storefront resource should be located.
94 access(all) let StorefrontStoragePath: StoragePath
95
96 /// StorefrontPublicPath
97 /// The public location for a Storefront link.
98 access(all) let StorefrontPublicPath: PublicPath
99
100
101 /// SaleCut
102 /// A struct representing a recipient that must be sent a certain amount
103 /// of the payment when a token is sold.
104 ///
105 access(all) struct SaleCut {
106 /// The receiver for the payment.
107 /// Note that we do not store an address to find the Vault that this represents,
108 /// as the link or resource that we fetch in this way may be manipulated,
109 /// so to find the address that a cut goes to you must get this struct and then
110 /// call receiver.borrow()!.owner.address on it.
111 /// This can be done efficiently in a script.
112 access(all) let receiver: Capability<&{FungibleToken.Receiver}>
113
114 /// The amount of the payment FungibleToken that will be paid to the receiver.
115 access(all) let amount: UFix64
116
117 /// initializer
118 ///
119 init(receiver: Capability<&{FungibleToken.Receiver}>, amount: UFix64) {
120 self.receiver = receiver
121 self.amount = amount
122 }
123 }
124
125
126 /// ListingDetails
127 /// A struct containing a Listing's data.
128 ///
129 access(all) struct ListingDetails {
130 /// The Storefront that the Listing is stored in.
131 /// Note that this resource cannot be moved to a different Storefront,
132 /// so this is OK. If we ever make it so that it *can* be moved,
133 /// this should be revisited.
134 access(all) var storefrontID: UInt64
135 /// Whether this listing has been purchased or not.
136 access(all) var purchased: Bool
137 /// The Type of the NonFungibleToken.NFT that is being listed.
138 access(all) let nftType: Type
139 /// The Resource ID of the NFT which can only be set in the contract
140 access(all) let nftUUID: UInt64
141 /// The unique identifier of the NFT that will get sell.
142 access(all) let nftID: UInt64
143 /// The Type of the FungibleToken that payments must be made in.
144 access(all) let salePaymentVaultType: Type
145 /// The amount that must be paid in the specified FungibleToken.
146 access(all) let salePrice: UFix64
147 /// This specifies the division of payment between recipients.
148 access(all) let saleCuts: [SaleCut]
149 /// Allow different dapp teams to provide custom strings as the distinguished string
150 /// that would help them to filter events related to their customID.
151 access(all) var customID: String?
152 /// Commission available to be claimed by whoever facilitates the sale.
153 access(all) let commissionAmount: UFix64
154 /// Expiry of listing
155 access(all) let expiry: UInt64
156
157 /// Irreversibly set this listing as purchased.
158 ///
159 access(contract) fun setToPurchased() {
160 self.purchased = true
161 }
162
163 access(contract) fun setCustomID(customID: String?){
164 self.customID = customID
165 }
166
167 /// Initializer
168 ///
169 init (
170 nftType: Type,
171 nftUUID: UInt64,
172 nftID: UInt64,
173 salePaymentVaultType: Type,
174 saleCuts: [SaleCut],
175 storefrontID: UInt64,
176 customID: String?,
177 commissionAmount: UFix64,
178 expiry: UInt64
179 ) {
180
181 pre {
182 // Validate the expiry
183 expiry > UInt64(getCurrentBlock().timestamp): "Expiry should be in the future"
184 // Validate the length of the sale cut
185 saleCuts.length > 0: "Listing must have at least one payment cut recipient"
186 }
187
188 self.storefrontID = storefrontID
189 self.purchased = false
190 self.nftType = nftType
191 self.nftUUID = nftUUID
192 self.nftID = nftID
193 self.salePaymentVaultType = salePaymentVaultType
194 self.customID = customID
195 self.commissionAmount = commissionAmount
196 self.expiry = expiry
197 self.saleCuts = saleCuts
198
199 // Calculate the total price from the cuts
200 var salePrice = commissionAmount
201 // Perform initial check on capabilities, and calculate sale price from cut amounts.
202 for cut in self.saleCuts {
203 // Make sure we can borrow the receiver.
204 // We will check this again when the token is sold.
205 cut.receiver.borrow()
206 ?? panic("Cannot borrow receiver")
207 // Add the cut amount to the total price
208 salePrice = salePrice + cut.amount
209 }
210 assert(salePrice > 0.0, message: "Listing must have non-zero price")
211
212 // Store the calculated sale price
213 self.salePrice = salePrice
214 }
215 }
216
217
218 /// ListingPublic
219 /// An interface providing a useful public interface to a Listing.
220 ///
221 access(all) resource interface ListingPublic {
222 /// borrowNFT
223 /// This will assert in the same way as the NFT standard borrowNFT()
224 /// if the NFT is absent, for example if it has been sold via another listing.
225 ///
226 access(all) fun borrowNFT(): &{NonFungibleToken.NFT}?
227
228 /// purchase
229 /// Purchase the listing, buying the token.
230 /// This pays the beneficiaries and returns the token to the buyer.
231 ///
232 access(all) fun purchase(
233 payment: @{FungibleToken.Vault},
234 commissionRecipient: Capability<&{FungibleToken.Receiver}>?,
235 ): @{NonFungibleToken.NFT}
236
237 /// getDetails
238 /// Fetches the details of the listing.
239 access(all) view fun getDetails(): ListingDetails
240
241 /// getAllowedCommissionReceivers
242 /// Fetches the allowed marketplaces capabilities or commission receivers.
243 /// If it returns `nil` then commission is up to grab by anyone.
244 access(all) view fun getAllowedCommissionReceivers(): [Capability<&{FungibleToken.Receiver}>]?
245
246 /// hasListingBecomeGhosted
247 /// Tells whether listed NFT is present in provided capability.
248 /// If it returns `false` then it means listing becomes ghost or sold out.
249 access(all) view fun hasListingBecomeGhosted(): Bool
250
251 }
252
253
254 /// Listing
255 /// A resource that allows an NFT to be sold for an amount of a given FungibleToken,
256 /// and for the proceeds of that sale to be split between several recipients.
257 ///
258 access(all) resource Listing: ListingPublic, Burner.Burnable {
259 /// The simple (non-Capability, non-complex) details of the sale
260 access(self) let details: ListingDetails
261
262 /// A capability allowing this resource to withdraw the NFT with the given ID from its collection.
263 /// This capability allows the resource to withdraw *any* NFT, so you should be careful when giving
264 /// such a capability to a resource and always check its code to make sure it will use it in the
265 /// way that it claims.
266 access(contract) let nftProviderCapability: Capability<auth(NonFungibleToken.Withdraw) &{NonFungibleToken.Provider, NonFungibleToken.CollectionPublic}>
267
268 /// An optional list of marketplaces capabilities that are approved
269 /// to receive the marketplace commission.
270 access(contract) let marketplacesCapability: [Capability<&{FungibleToken.Receiver}>]?
271
272 access(contract) fun burnCallback() {
273 // If the listing has not been purchased, we regard it as completed here.
274 // Otherwise we regard it as completed in purchase().
275 // This is because we destroy the listing in Storefront.removeListing()
276 // or Storefront.cleanup() .
277 // If we change this destructor, revisit those functions.
278 if !self.details.purchased {
279 emit ListingCompleted(
280 listingResourceID: self.uuid,
281 storefrontResourceID: self.details.storefrontID,
282 purchased: self.details.purchased,
283 nftType: self.details.nftType,
284 nftUUID: self.details.nftUUID,
285 nftID: self.details.nftID,
286 salePaymentVaultType: self.details.salePaymentVaultType,
287 salePrice: self.details.salePrice,
288 customID: self.details.customID,
289 commissionAmount: self.details.commissionAmount,
290 commissionReceiver: nil,
291 expiry: self.details.expiry
292 )
293 }
294 }
295
296 /// borrowNFT
297 /// Return the reference of the NFT that is listed for sale.
298 /// if the NFT is absent, for example if it has been sold via another listing.
299 /// it will return nil.
300 ///
301 access(all) fun borrowNFT(): &{NonFungibleToken.NFT}? {
302 if let ref = self.nftProviderCapability.borrow()!.borrowNFT(self.details.nftID) {
303 if ref.isInstance(self.details.nftType) && ref.id == self.details.nftID {
304 return ref
305 }
306 }
307
308 return nil
309 }
310
311 /// getDetails
312 /// Get the details of listing.
313 ///
314 access(all) view fun getDetails(): ListingDetails {
315 return self.details
316 }
317
318 /// getAllowedCommissionReceivers
319 /// Fetches the allowed marketplaces capabilities or commission receivers.
320 /// If it returns `nil` then commission is up to grab by anyone.
321 access(all) view fun getAllowedCommissionReceivers(): [Capability<&{FungibleToken.Receiver}>]? {
322 return self.marketplacesCapability
323 }
324
325 /// hasListingBecomeGhosted
326 /// Tells whether listed NFT is present in provided capability.
327 /// If it returns `false` then it means listing becomes ghost or sold out.
328 access(all) view fun hasListingBecomeGhosted(): Bool {
329 if let providerRef = self.nftProviderCapability.borrow() {
330 return providerRef.borrowNFT(self.details.nftID) != nil
331 }
332 return false
333 }
334
335 /// purchase
336 /// Purchase the listing, buying the token.
337 /// This pays the beneficiaries and commission to the facilitator and returns extra token to the buyer.
338 /// This also cleans up duplicate listings for the item being purchased.
339 access(all) fun purchase(
340 payment: @{FungibleToken.Vault},
341 commissionRecipient: Capability<&{FungibleToken.Receiver}>?,
342 ): @{NonFungibleToken.NFT} {
343
344 pre {
345 self.details.purchased == false: "listing has already been purchased"
346 payment.isInstance(self.details.salePaymentVaultType): "payment vault is not requested fungible token"
347 payment.balance == self.details.salePrice: "payment vault does not contain requested price"
348 self.details.expiry > UInt64(getCurrentBlock().timestamp): "Listing is expired"
349 self.owner != nil : "Resource doesn't have the assigned owner"
350 }
351
352 // Make sure the listing cannot be purchased again.
353 self.details.setToPurchased()
354
355 if self.details.commissionAmount > 0.0 {
356 // If commission recipient is nil, Throw panic.
357 let commissionReceiver = commissionRecipient ?? panic("Commission recipient can't be nil")
358 if self.marketplacesCapability != nil {
359 var isCommissionRecipientHasValidType = false
360 var isCommissionRecipientAuthorised = false
361 for cap in self.marketplacesCapability! {
362 // Check 1: Should have the same type
363 if cap.getType() == commissionReceiver.getType() {
364 isCommissionRecipientHasValidType = true
365 // Check 2: Should have the valid market address that holds approved capability.
366 if cap.address == commissionReceiver.address && cap.check() {
367 isCommissionRecipientAuthorised = true
368 break
369 }
370 }
371 }
372 assert(isCommissionRecipientHasValidType, message: "Given recipient does not has valid type")
373 assert(isCommissionRecipientAuthorised, message: "Given recipient is not authorised to receive the commission")
374 }
375 let commissionPayment <- payment.withdraw(amount: self.details.commissionAmount)
376 let recipient = commissionReceiver.borrow() ?? panic("Unable to borrow the recipient capability")
377 recipient.deposit(from: <- commissionPayment)
378 }
379 // Fetch the token to return to the purchaser.
380 let nft <-self.nftProviderCapability.borrow()!.withdraw(withdrawID: self.details.nftID)
381 // Neither receivers nor providers are trustworthy, they must implement the correct
382 // interface but beyond complying with its pre/post conditions they are not guaranteed
383 // to implement the functionality behind the interface in any given way.
384 // Therefore we cannot trust the Collection resource behind the interface,
385 // and we must check the NFT resource it gives us to make sure that it is the correct one.
386 assert(nft.getType() == self.details.nftType, message: "withdrawn NFT is not of specified type")
387 assert(nft.id == self.details.nftID, message: "withdrawn NFT does not have specified ID")
388
389 // Fetch the duplicate listing for the given NFT
390 // Access the StoreFrontManager resource reference to remove the duplicate listings if purchase would happen successfully.
391 let storeFrontPublicRef = getAccount(self.owner!.address).capabilities.borrow<&{NFTStorefrontV2.StorefrontPublic}>(
392 NFTStorefrontV2.StorefrontPublicPath
393 ) ?? panic("Unable to borrow the storeFrontManager resource")
394 let duplicateListings = storeFrontPublicRef.getDuplicateListingIDs(
395 nftType: self.details.nftType,
396 nftID: self.details.nftID,
397 listingID: self.uuid
398 )
399
400 // Let's force removal of the listing in this storefront for the NFT that is being purchased.
401 for listingID in duplicateListings {
402 storeFrontPublicRef.cleanup(listingResourceID: listingID)
403 }
404
405 // Rather than aborting the transaction if any receiver is absent when we try to pay it,
406 // we send the cut to the first valid receiver.
407 // The first receiver should therefore either be the seller, or an agreed recipient for
408 // any unpaid cuts.
409 var residualReceiver: &{FungibleToken.Receiver}? = nil
410 // Pay the commission
411 // Pay each beneficiary their amount of the payment.
412
413 for cut in self.details.saleCuts {
414 if let receiver = cut.receiver.borrow() {
415 let paymentCut <- payment.withdraw(amount: cut.amount)
416 receiver.deposit(from: <-paymentCut)
417 if (residualReceiver == nil) {
418 residualReceiver = receiver
419 }
420 } else {
421 emit UnpaidReceiver(receiver: cut.receiver.address, entitledSaleCut: cut.amount)
422 }
423 }
424
425 assert(residualReceiver != nil, message: "No valid payment receivers")
426
427 // At this point, if all receivers were active and available, then the payment Vault will have
428 // zero tokens left, and this will functionally be a no-op that consumes the empty vault
429 residualReceiver!.deposit(from: <-payment)
430
431 // If the listing is purchased, we regard it as completed here.
432 // Otherwise we regard it as completed in the destructor.
433
434 var commissionReceiver: Address? = nil
435 if (self.details.commissionAmount != 0.0) {
436 commissionReceiver = commissionRecipient!.address
437 }
438
439 emit ListingCompleted(
440 listingResourceID: self.uuid,
441 storefrontResourceID: self.details.storefrontID,
442 purchased: self.details.purchased,
443 nftType: self.details.nftType,
444 nftUUID: self.details.nftUUID,
445 nftID: self.details.nftID,
446 salePaymentVaultType: self.details.salePaymentVaultType,
447 salePrice: self.details.salePrice,
448 customID: self.details.customID,
449 commissionAmount: self.details.commissionAmount,
450 commissionReceiver: commissionReceiver,
451 expiry: self.details.expiry
452 )
453
454 return <-nft
455 }
456
457 // destructor event
458 //
459 access(all) event ResourceDestroyed(
460 listingResourceID: UInt64 = self.uuid,
461 storefrontResourceID: UInt64 = self.details.storefrontID,
462 purchased: Bool = self.details.purchased,
463 nftType: String = self.details.nftType.identifier,
464 nftUUID: UInt64 = self.details.nftUUID,
465 nftID: UInt64 = self.details.nftID,
466 salePaymentVaultType: String = self.details.salePaymentVaultType.identifier,
467 salePrice: UFix64 = self.details.salePrice,
468 customID: String? = self.details.customID,
469 commissionAmount: UFix64 = self.details.commissionAmount,
470 commissionReceiver: Address? = nil,
471 expiry: UInt64 = self.details.expiry
472 )
473
474 /// initializer
475 ///
476 init (
477 nftProviderCapability: Capability<auth(NonFungibleToken.Withdraw) &{NonFungibleToken.Collection}>,
478 nftType: Type,
479 nftUUID: UInt64,
480 nftID: UInt64,
481 salePaymentVaultType: Type,
482 saleCuts: [SaleCut],
483 marketplacesCapability: [Capability<&{FungibleToken.Receiver}>]?,
484 storefrontID: UInt64,
485 customID: String?,
486 commissionAmount: UFix64,
487 expiry: UInt64
488 ) {
489 // Store the sale information
490 self.details = ListingDetails(
491 nftType: nftType,
492 nftUUID: nftUUID,
493 nftID: nftID,
494 salePaymentVaultType: salePaymentVaultType,
495 saleCuts: saleCuts,
496 storefrontID: storefrontID,
497 customID: customID,
498 commissionAmount: commissionAmount,
499 expiry: expiry
500 )
501
502 // Store the NFT provider
503 self.nftProviderCapability = nftProviderCapability
504 self.marketplacesCapability = marketplacesCapability
505
506 // Check that the provider contains the NFT.
507 // We will check it again when the token is sold.
508 // We cannot move this into a function because initializers cannot call member functions.
509 let provider = self.nftProviderCapability.borrow()
510 assert(provider != nil, message: "cannot borrow nftProviderCapability")
511
512 // This will precondition assert if the token is not available.
513 let nft = provider!.borrowNFT(self.details.nftID)
514 assert(nft!.getType() == self.details.nftType, message: "token is not of specified type")
515 assert(nft?.id == self.details.nftID, message: "token does not have specified ID")
516 }
517 }
518
519 /// StorefrontManager
520 /// An interface for adding and removing Listings within a Storefront,
521 /// intended for use by the Storefront's owner
522 ///
523 access(all) resource interface StorefrontManager {
524 /// createListing
525 /// Allows the Storefront owner to create and insert Listings.
526 ///
527 access(CreateListing) fun createListing(
528 nftProviderCapability: Capability<auth(NonFungibleToken.Withdraw) &{NonFungibleToken.Collection}>,
529 nftType: Type,
530 nftID: UInt64,
531 salePaymentVaultType: Type,
532 saleCuts: [SaleCut],
533 marketplacesCapability: [Capability<&{FungibleToken.Receiver}>]?,
534 customID: String?,
535 commissionAmount: UFix64,
536 expiry: UInt64
537 ): UInt64
538
539 /// removeListing
540 /// Allows the Storefront owner to remove any sale listing, accepted or not.
541 ///
542 access(RemoveListing) fun removeListing(listingResourceID: UInt64)
543 }
544
545 /// StorefrontPublic
546 /// An interface to allow listing and borrowing Listings, and purchasing items via Listings
547 /// in a Storefront.
548 ///
549 access(all) resource interface StorefrontPublic {
550 access(all) view fun getListingIDs(): [UInt64]
551 access(all) fun getDuplicateListingIDs(nftType: Type, nftID: UInt64, listingID: UInt64): [UInt64]
552 access(all) view fun borrowListing(listingResourceID: UInt64): &{ListingPublic}? {
553 post {
554 result == nil || result!.getType() == Type<@Listing>():
555 "Cannot borrow a non-NFTStorefrontV2.Listing!"
556 }
557 }
558 access(all) fun cleanupExpiredListings(fromIndex: UInt64, toIndex: UInt64)
559 access(contract) fun cleanup(listingResourceID: UInt64)
560 access(all) fun getExistingListingIDs(nftType: Type, nftID: UInt64): [UInt64]
561 access(all) fun cleanupPurchasedListings(listingResourceID: UInt64)
562 access(all) fun cleanupGhostListings(listingResourceID: UInt64)
563 }
564
565 /// Storefront
566 /// A resource that allows its owner to manage a list of Listings, and anyone to interact with them
567 /// in order to query their details and purchase the NFTs that they represent.
568 ///
569 access(all) resource Storefront : StorefrontManager, StorefrontPublic {
570 // Resource destroyed event
571 access(all) event ResourceDestroyed(
572 storefrontResourceID: UInt64 = self.uuid
573 )
574
575 /// The dictionary of Listing uuids to Listing resources.
576 access(contract) var listings: @{UInt64: Listing}
577 /// Dictionary to keep track of listing ids for same NFTs listing.
578 /// nftType.identifier -> nftID -> [listing resource ID]
579 access(contract) var listedNFTs: {String: {UInt64 : [UInt64]}}
580
581 /// insert
582 /// Create and publish a Listing for an NFT.
583 ///
584 access(CreateListing) fun createListing(
585 nftProviderCapability: Capability<auth(NonFungibleToken.Withdraw) &{NonFungibleToken.Collection}>,
586 nftType: Type,
587 nftID: UInt64,
588 salePaymentVaultType: Type,
589 saleCuts: [SaleCut],
590 marketplacesCapability: [Capability<&{FungibleToken.Receiver}>]?,
591 customID: String?,
592 commissionAmount: UFix64,
593 expiry: UInt64
594 ): UInt64 {
595
596 // let's ensure that the seller does indeed hold the NFT being listed
597 let collectionRef = nftProviderCapability.borrow()
598 ?? panic("Could not borrow reference to collection")
599 let nftRef = collectionRef.borrowNFT(nftID)
600 ?? panic("Could not borrow a reference to the desired NFT ID")
601
602 // Instead of letting an arbitrary value be set for the UUID of a given NFT, the contract
603 // should fetch it itself
604 let uuid = nftRef.uuid
605 let listing <- create Listing(
606 nftProviderCapability: nftProviderCapability,
607 nftType: nftType,
608 nftUUID: uuid,
609 nftID: nftID,
610 salePaymentVaultType: salePaymentVaultType,
611 saleCuts: saleCuts,
612 marketplacesCapability: marketplacesCapability,
613 storefrontID: self.uuid,
614 customID: customID,
615 commissionAmount: commissionAmount,
616 expiry: expiry
617 )
618
619 let listingResourceID = listing.uuid
620 let listingPrice = listing.getDetails().salePrice
621 // Add the new listing to the dictionary.
622 let oldListing <- self.listings[listingResourceID] <- listing
623 // Note that oldListing will always be nil, but we have to handle it.
624
625 Burner.burn(<-oldListing)
626
627 // Add the `listingResourceID` in the tracked listings.
628 self.addDuplicateListing(nftIdentifier: nftType.identifier, nftID: nftID, listingResourceID: listingResourceID)
629
630 // Scraping addresses from the capabilities to emit in the event.
631 var allowedCommissionReceivers : [Address]? = nil
632 if let allowedReceivers = marketplacesCapability {
633 // Small hack here to make `allowedCommissionReceivers` variable compatible to
634 // array properties.
635 allowedCommissionReceivers = []
636 for receiver in allowedReceivers {
637 allowedCommissionReceivers!.append(receiver.address)
638 }
639 }
640
641 emit ListingAvailable(
642 storefrontAddress: self.owner?.address!,
643 listingResourceID: listingResourceID,
644 nftType: nftType,
645 nftUUID: uuid,
646 nftID: nftID,
647 salePaymentVaultType: salePaymentVaultType,
648 salePrice: listingPrice,
649 customID: customID,
650 commissionAmount: commissionAmount,
651 commissionReceivers: allowedCommissionReceivers,
652 expiry: expiry
653 )
654
655 return listingResourceID
656 }
657
658 /// addDuplicateListing
659 /// Helper function that allows to add duplicate listing of given nft in a map.
660 ///
661 access(contract) fun addDuplicateListing(nftIdentifier: String, nftID: UInt64, listingResourceID: UInt64) {
662 if !self.listedNFTs.containsKey(nftIdentifier) {
663 self.listedNFTs.insert(key: nftIdentifier, {nftID: [listingResourceID]})
664 } else {
665 if !self.listedNFTs[nftIdentifier]!.containsKey(nftID) {
666 self.listedNFTs[nftIdentifier]!.insert(key: nftID, [listingResourceID])
667 } else {
668 self.listedNFTs[nftIdentifier]![nftID]!.append(listingResourceID)
669 }
670 }
671 }
672
673 /// removeDuplicateListing
674 /// Helper function that allows to remove duplicate listing of given nft from a map.
675 ///
676 access(contract) fun removeDuplicateListing(nftIdentifier: String, nftID: UInt64, listingResourceID: UInt64) {
677 // Remove the listing from the listedNFTs dictionary.
678 let listingIndex = self.listedNFTs[nftIdentifier]![nftID]!.firstIndex(of: listingResourceID) ?? panic("Should contain the index")
679 self.listedNFTs[nftIdentifier]![nftID]!.remove(at: listingIndex)
680 }
681
682 /// removeListing
683 /// Remove a Listing that has not yet been purchased from the collection and destroy it.
684 /// It can only be executed by the StorefrontManager resource owner.
685 ///
686 access(RemoveListing) fun removeListing(listingResourceID: UInt64) {
687 let listing <- self.listings.remove(key: listingResourceID)
688 ?? panic("missing Listing")
689 let listingDetails = listing.getDetails()
690 self.removeDuplicateListing(nftIdentifier: listingDetails.nftType.identifier, nftID: listingDetails.nftID, listingResourceID: listingResourceID)
691 // This will emit a ListingCompleted event.
692 Burner.burn(<-listing)
693 }
694
695 /// getListingIDs
696 /// Returns an array of the Listing resource IDs that are in the collection
697 ///
698 access(all) view fun getListingIDs(): [UInt64] {
699 return self.listings.keys
700 }
701
702 /// getExistingListingIDs
703 /// Returns an array of listing IDs of the given `nftType` and `nftID`.
704 ///
705 access(all) fun getExistingListingIDs(nftType: Type, nftID: UInt64): [UInt64] {
706 if self.listedNFTs[nftType.identifier] == nil || self.listedNFTs[nftType.identifier]![nftID] == nil {
707 return []
708 }
709 var listingIDs = self.listedNFTs[nftType.identifier]![nftID]!
710 return listingIDs
711 }
712
713 /// cleanupPurchasedListings
714 /// Allows anyone to remove already purchased listings.
715 ///
716 access(all) fun cleanupPurchasedListings(listingResourceID: UInt64) {
717 pre {
718 self.listings[listingResourceID] != nil: "could not find listing with given id"
719 self.borrowListing(listingResourceID: listingResourceID)!.getDetails().purchased == true: "listing not purchased yet"
720 }
721 let listing <- self.listings.remove(key: listingResourceID)!
722 let listingDetails = listing.getDetails()
723 self.removeDuplicateListing(nftIdentifier: listingDetails.nftType.identifier, nftID: listingDetails.nftID, listingResourceID: listingResourceID)
724
725 Burner.burn(<-listing)
726 }
727
728 /// getDuplicateListingIDs
729 /// Returns an array of listing IDs that are duplicates of the given `nftType` and `nftID`.
730 ///
731 access(all) fun getDuplicateListingIDs(nftType: Type, nftID: UInt64, listingID: UInt64): [UInt64] {
732 var listingIDs = self.getExistingListingIDs(nftType: nftType, nftID: nftID)
733
734 // Verify that given listing Id also a part of the `listingIds`
735 let doesListingExist = listingIDs.contains(listingID)
736 // Find out the index of the existing listing.
737 if doesListingExist {
738 var index: Int = 0
739 for id in listingIDs {
740 if id == listingID {
741 break
742 }
743 index = index + 1
744 }
745 listingIDs.remove(at:index)
746 return listingIDs
747 }
748 return []
749 }
750
751 /// cleanupExpiredListings
752 /// Cleanup the expired listing by iterating over the provided range of indexes.
753 ///
754 access(all) fun cleanupExpiredListings(fromIndex: UInt64, toIndex: UInt64) {
755 pre {
756 fromIndex <= toIndex : "Incorrect start index"
757 Int(toIndex - fromIndex) < self.getListingIDs().length : "Provided range is out of bound"
758 }
759 var index = fromIndex
760 let listingsIDs = self.getListingIDs()
761 while index <= toIndex {
762 // There is a possibility that some index may not have the listing.
763 // because of that instead of failing the transaction, Execution moved to next index or listing.
764
765 if let listing = self.borrowListing(listingResourceID: listingsIDs[index]) {
766 if listing.getDetails().expiry <= UInt64(getCurrentBlock().timestamp) {
767 self.cleanup(listingResourceID: listingsIDs[index])
768 }
769 }
770 index = index + UInt64(1)
771 }
772 }
773
774 /// borrowSaleItem
775 /// Returns a read-only view of the SaleItem for the given listingID if it is contained by this collection.
776 ///
777 access(all) view fun borrowListing(listingResourceID: UInt64): &{ListingPublic}? {
778 return &self.listings[listingResourceID]
779 }
780
781 /// cleanup
782 /// Remove an listing, When given listing is duplicate or expired
783 /// Only contract is allowed to execute it.
784 ///
785 access(contract) fun cleanup(listingResourceID: UInt64) {
786 pre {
787 self.listings[listingResourceID] != nil: "Could not find listing with given id"
788 }
789 let listing <- self.listings.remove(key: listingResourceID)!
790 let listingDetails = listing.getDetails()
791 self.removeDuplicateListing(nftIdentifier: listingDetails.nftType.identifier, nftID: listingDetails.nftID, listingResourceID: listingResourceID)
792
793 Burner.burn(<-listing)
794 }
795
796 /// cleanupGhostListings
797 /// Allow anyone to cleanup ghost listings
798 /// Listings will become ghost listings if stored provider capability doesn't hold
799 /// the NFT anymore.
800 ///
801 /// @param listingResourceID ID of the listing resource which would get removed if it become ghost listing.
802 access(all) fun cleanupGhostListings(listingResourceID: UInt64) {
803 pre {
804 self.listings[listingResourceID] != nil: "Could not find listing with given id"
805 }
806 let listingRef = self.borrowListing(listingResourceID: listingResourceID)!
807 let details = listingRef.getDetails()
808 assert(!details.purchased, message: "Given listing is already purchased")
809 assert(!listingRef.hasListingBecomeGhosted(), message: "Listing is not ghost listing")
810 let listing <- self.listings.remove(key: listingResourceID)!
811 let duplicateListings = self.getDuplicateListingIDs(nftType: details.nftType, nftID: details.nftID, listingID: listingResourceID)
812
813 // Let's force removal of the listing in this storefront for the NFT that is being ghosted.
814 for listingID in duplicateListings {
815 self.cleanup(listingResourceID: listingID)
816 }
817 Burner.burn(<-listing)
818 }
819
820 /// constructor
821 ///
822 init () {
823 self.listings <- {}
824 self.listedNFTs = {}
825
826 // Let event consumers know that this storefront exists
827 emit StorefrontInitialized(storefrontResourceID: self.uuid)
828 }
829 }
830
831 /// createStorefront
832 /// Make creating a Storefront publicly accessible.
833 ///
834 access(all) fun createStorefront(): @Storefront {
835 return <-create Storefront()
836 }
837
838 init () {
839 self.StorefrontStoragePath = /storage/NFTStorefrontV2
840 self.StorefrontPublicPath = /public/NFTStorefrontV2
841 }
842}
843