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