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